udpate markdown tsx file path

This commit is contained in:
duanfuxiang
2025-04-13 17:34:11 +08:00
parent 1d88715666
commit 4a5823721e
17 changed files with 49 additions and 49 deletions

View File

@@ -0,0 +1,92 @@
import { Check, Diff, Loader2, X } from 'lucide-react'
import { PropsWithChildren, useState } from 'react'
import { useDarkModeContext } from "../../../contexts/DarkModeContext"
import { ApplyStatus, ToolArgs } from "../../../types/apply"
import { MemoizedSyntaxHighlighterWrapper } from "./SyntaxHighlighterWrapper"
export default function MarkdownApplyDiffBlock({
mode,
applyStatus,
onApply,
path,
diff,
finish,
}: PropsWithChildren<{
mode: string
applyStatus: ApplyStatus
onApply: (args: ToolArgs) => void
path: string
diff: string
finish: boolean
}>) {
const [applying, setApplying] = useState(false)
const { isDarkMode } = useDarkModeContext()
const handleApply = async () => {
if (applyStatus !== ApplyStatus.Idle) {
return
}
setApplying(true)
onApply({
type: "apply_diff",
filepath: path,
diff,
finish,
})
}
return (
<div className={`infio-chat-code-block ${path ? 'has-filename' : ''} infio-reasoning-block`}>
<div className={'infio-chat-code-block-header'}>
{path && (
<div className={'infio-chat-code-block-header-filename'}>
<Diff size={10} className="infio-chat-code-block-header-icon" />
{mode}: {path}
</div>
)}
<div className={'infio-chat-code-block-header-button'}>
<button
onClick={handleApply}
style={{ color: '#008000' }}
disabled={applyStatus !== ApplyStatus.Idle || applying || !finish}
>
{
!finish ? (
<>
<Loader2 className="spinner" size={14} /> Loading...
</>
) : applyStatus === ApplyStatus.Idle ? (
applying ? (
<>
<Loader2 className="spinner" size={14} /> Applying...
</>
) : (
'Apply'
)
) : applyStatus === ApplyStatus.Applied ? (
<>
<Check size={14} /> Success
</>
) : (
<>
<X size={14} /> Failed
</>
)}
</button>
</div>
</div>
<div className="infio-reasoning-content-wrapper">
<MemoizedSyntaxHighlighterWrapper
isDarkMode={isDarkMode}
language="diff"
hasFilename={!!path}
wrapLines={true}
>
{diff}
</MemoizedSyntaxHighlighterWrapper>
</div>
</div>
)
}

View File

@@ -0,0 +1,122 @@
import { Check, CopyIcon, Edit, Loader2, X } from 'lucide-react'
import { PropsWithChildren, useMemo, useState } from 'react'
import { useDarkModeContext } from "../../../contexts/DarkModeContext"
import { ApplyStatus, ToolArgs } from "../../../types/apply"
import { MemoizedSyntaxHighlighterWrapper } from "./SyntaxHighlighterWrapper"
export default function MarkdownEditFileBlock({
mode,
applyStatus,
onApply,
language,
path,
startLine,
endLine,
children,
}: PropsWithChildren<{
mode: string
applyStatus: ApplyStatus
onApply: (args: ToolArgs) => void
language?: string
path?: string
startLine?: number
endLine?: number
}>) {
const [copied, setCopied] = useState(false)
const [applying, setApplying] = useState(false)
const { isDarkMode } = useDarkModeContext()
const wrapLines = useMemo(() => {
return !language || ['markdown'].includes(language)
}, [language])
const handleCopy = async () => {
try {
await navigator.clipboard.writeText(String(children))
setCopied(true)
setTimeout(() => setCopied(false), 2000)
} catch (err) {
console.error('Failed to copy text: ', err)
}
}
const handleApply = async () => {
if (applyStatus !== ApplyStatus.Idle) {
return
}
setApplying(true)
onApply({
// @ts-ignore
type: mode,
filepath: path,
content: String(children),
startLine,
endLine
})
}
return (
<div className={`infio-chat-code-block ${path ? 'has-filename' : ''} infio-reasoning-block`}>
<div className={'infio-chat-code-block-header'}>
{path && (
<div className={'infio-chat-code-block-header-filename'}>
<Edit size={10} className="infio-chat-code-block-header-icon" />
{mode}: {path}
</div>
)}
<div className={'infio-chat-code-block-header-button'}>
<button
onClick={() => {
handleCopy()
}}
>
{copied ? (
<>
<Check size={10} /> Copied
</>
) : (
<>
<CopyIcon size={10} /> Copy
</>
)}
</button>
<button
onClick={handleApply}
style={{ color: '#008000' }}
disabled={applyStatus !== ApplyStatus.Idle || applying}
>
{applyStatus === ApplyStatus.Idle ? (
applying ? (
<>
<Loader2 className="spinner" size={14} /> Applying...
</>
) : (
'Apply'
)
) : applyStatus === ApplyStatus.Applied ? (
<>
<Check size={14} /> Success
</>
) : (
<>
<X size={14} /> Failed
</>
)}
</button>
</div>
</div>
<div className="infio-reasoning-content-wrapper">
<MemoizedSyntaxHighlighterWrapper
isDarkMode={isDarkMode}
language={language}
hasFilename={!!path}
wrapLines={wrapLines}
>
{String(children)}
</MemoizedSyntaxHighlighterWrapper>
</div>
</div>
)
}

View File

@@ -0,0 +1,94 @@
import { Check, ChevronDown, ChevronRight, Globe, Loader2, X } from 'lucide-react'
import React, { useEffect, useRef, useState } from 'react'
import { ApplyStatus, FetchUrlsContentToolArgs } from "../../../types/apply"
export default function MarkdownFetchUrlsContentBlock({
applyStatus,
onApply,
urls,
finish
}: {
applyStatus: ApplyStatus
onApply: (args: FetchUrlsContentToolArgs) => void
urls: string[],
finish: boolean
}) {
const containerRef = useRef<HTMLDivElement>(null)
const [isOpen, setIsOpen] = useState(true)
React.useEffect(() => {
if (finish && applyStatus === ApplyStatus.Idle) {
onApply({
type: 'fetch_urls_content',
urls: urls
})
}
}, [finish])
useEffect(() => {
if (containerRef.current) {
containerRef.current.scrollTop = containerRef.current.scrollHeight
}
}, [urls])
return (
urls.length > 0 && (
<div className="infio-chat-code-block has-filename infio-reasoning-block">
<div className="infio-chat-code-block-header">
<div className="infio-chat-code-block-header-filename">
<Globe size={10} className="infio-chat-code-block-header-icon" />
Fetch URLs Content
</div>
<div className="infio-chat-code-block-header-button">
<button
className="infio-chat-code-block-status-button"
disabled={true}
>
{
!finish || applyStatus === ApplyStatus.Idle ? (
<>
<Loader2 className="spinner" size={14} /> Fetching...
</>
) : applyStatus === ApplyStatus.Applied ? (
<>
<Check size={14} /> Done
</>
) : (
<>
<X size={14} /> Failed
</>
)}
</button>
<button
className="clickable-icon infio-chat-list-dropdown"
onClick={() => setIsOpen(!isOpen)}
>
{isOpen ? <ChevronDown size={16} /> : <ChevronRight size={16} />}
</button>
</div>
</div>
<div
ref={containerRef}
className="infio-reasoning-content-wrapper"
style={{ display: isOpen ? 'block' : 'none' }}
>
<ul className="infio-chat-code-block-url-list">
{urls.map((url, index) => (
<li key={index}>
<a
href={url}
target="_blank"
rel="noopener noreferrer"
className="infio-chat-code-block-url-link"
>
{url}
</a>
</li>
))}
</ul>
</div>
</div>
)
)
}

View File

@@ -0,0 +1,50 @@
import { FolderOpen } from 'lucide-react'
import React from 'react'
import { useApp } from "../../../contexts/AppContext"
import { ApplyStatus, ListFilesToolArgs } from "../../../types/apply"
import { openMarkdownFile } from "../../../utils/obsidian"
export default function MarkdownListFilesBlock({
applyStatus,
onApply,
path,
recursive,
finish
}: {
applyStatus: ApplyStatus
onApply: (args: ListFilesToolArgs) => void
path: string,
recursive: boolean,
finish: boolean
}) {
const app = useApp()
const handleClick = () => {
openMarkdownFile(app, path)
}
React.useEffect(() => {
if (finish && applyStatus === ApplyStatus.Idle) {
onApply({
type: 'list_files',
filepath: path,
recursive
})
}
}, [finish])
return (
<div
className={`infio-chat-code-block ${path ? 'has-filename' : ''}`}
onClick={handleClick}
>
<div className={'infio-chat-code-block-header'}>
<div className={'infio-chat-code-block-header-filename'}>
<FolderOpen size={14} className="infio-chat-code-block-header-icon" />
List files: {path}
</div>
</div>
</div>
)
}

View File

@@ -0,0 +1,47 @@
import { ExternalLink } from 'lucide-react'
import React from 'react'
import { useApp } from "../../../contexts/AppContext"
import { ApplyStatus, ReadFileToolArgs } from "../../../types/apply"
import { openMarkdownFile } from "../../../utils/obsidian"
export default function MarkdownReadFileBlock({
applyStatus,
onApply,
path,
finish
}: {
applyStatus: ApplyStatus
onApply: (args: ReadFileToolArgs) => void
path: string,
finish: boolean
}) {
const app = useApp()
const handleClick = () => {
openMarkdownFile(app, path)
}
React.useEffect(() => {
if (finish && applyStatus === ApplyStatus.Idle) {
onApply({
type: 'read_file',
filepath: path
})
}
}, [finish])
return (
<div
className={`infio-chat-code-block ${path ? 'has-filename' : ''}`}
onClick={handleClick}
>
<div className={'infio-chat-code-block-header'}>
<div className={'infio-chat-code-block-header-filename'}>
<ExternalLink size={10} className="infio-chat-code-block-header-icon" />
Read file: {path}
</div>
</div>
</div>
)
}

View File

@@ -0,0 +1,57 @@
import { ChevronDown, ChevronRight, Brain } from 'lucide-react'
import { PropsWithChildren, useEffect, useRef, useState } from 'react'
import { useDarkModeContext } from "../../../contexts/DarkModeContext"
import { MemoizedSyntaxHighlighterWrapper } from "./SyntaxHighlighterWrapper"
export default function MarkdownReasoningBlock({
reasoningContent,
}: PropsWithChildren<{
reasoningContent: string
}>) {
const { isDarkMode } = useDarkModeContext()
const containerRef = useRef<HTMLDivElement>(null)
const [isOpen, setIsOpen] = useState(true)
useEffect(() => {
if (containerRef.current) {
containerRef.current.scrollTop = containerRef.current.scrollHeight
}
}, [reasoningContent])
return (
reasoningContent && (
<div
className={`infio-chat-code-block has-filename infio-reasoning-block`}
>
<div className={'infio-chat-code-block-header'}>
<div className={'infio-chat-code-block-header-filename'}>
<Brain size={10} className="infio-chat-code-block-header-icon" />
Reasoning
</div>
<button
className="clickable-icon infio-chat-list-dropdown"
onClick={() => setIsOpen(!isOpen)}
>
{isOpen ? <ChevronDown size={16} /> : <ChevronRight size={16} />}
</button>
</div>
<div
ref={containerRef}
className="infio-reasoning-content-wrapper"
>
<MemoizedSyntaxHighlighterWrapper
isDarkMode={isDarkMode}
language="markdown"
hasFilename={true}
wrapLines={true}
isOpen={isOpen}
>
{reasoningContent}
</MemoizedSyntaxHighlighterWrapper>
</div>
</div>
)
)
}

View File

@@ -0,0 +1,75 @@
import { PropsWithChildren, useEffect, useMemo, useState } from 'react'
import { useApp } from "../../../contexts/AppContext"
import { useDarkModeContext } from "../../../contexts/DarkModeContext"
import { openMarkdownFile, readTFileContent } from "../../../utils/obsidian"
import { MemoizedSyntaxHighlighterWrapper } from "./SyntaxHighlighterWrapper"
export default function MarkdownReferenceBlock({
filename,
startLine,
endLine,
language,
}: PropsWithChildren<{
filename: string
startLine: number
endLine: number
language?: string
}>) {
const app = useApp()
const { isDarkMode } = useDarkModeContext()
const [blockContent, setBlockContent] = useState<string | null>(null)
const wrapLines = useMemo(() => {
return !language || ['markdown'].includes(language)
}, [language])
useEffect(() => {
async function fetchBlockContent() {
const file = app.vault.getFileByPath(filename)
if (!file) {
setBlockContent(null)
return
}
const fileContent = await readTFileContent(file, app.vault)
const content = fileContent
.split('\n')
.slice(startLine - 1, endLine)
.join('\n')
setBlockContent(content)
}
fetchBlockContent()
}, [filename, startLine, endLine, app.vault])
const handleClick = () => {
openMarkdownFile(app, filename, startLine)
}
// TODO: Update styles
return (
blockContent && (
<div
className={`infio-chat-code-block ${filename ? 'has-filename' : ''}`}
onClick={handleClick}
>
<div className={'infio-chat-code-block-header'}>
{filename && (
<div className={'infio-chat-code-block-header-filename'}>
{filename}
</div>
)}
</div>
<MemoizedSyntaxHighlighterWrapper
isDarkMode={isDarkMode}
language={language}
hasFilename={!!filename}
wrapLines={wrapLines}
>
{blockContent}
</MemoizedSyntaxHighlighterWrapper>
</div>
)
)
}

View File

@@ -0,0 +1,51 @@
import { FileSearch } from 'lucide-react'
import React from 'react'
import { useApp } from "../../../contexts/AppContext"
import { ApplyStatus, RegexSearchFilesToolArgs } from "../../../types/apply"
import { openMarkdownFile } from "../../../utils/obsidian"
export default function MarkdownRegexSearchFilesBlock({
applyStatus,
onApply,
path,
regex,
finish
}: {
applyStatus: ApplyStatus
onApply: (args: RegexSearchFilesToolArgs) => void
path: string,
regex: string,
finish: boolean
}) {
const app = useApp()
const handleClick = () => {
openMarkdownFile(app, path)
}
React.useEffect(() => {
if (finish && applyStatus === ApplyStatus.Idle) {
onApply({
type: 'regex_search_files',
filepath: path,
regex: regex,
file_pattern: ".md",
})
}
}, [finish])
return (
<div
className={`infio-chat-code-block ${path ? 'has-filename' : ''}`}
onClick={handleClick}
>
<div className={'infio-chat-code-block-header'}>
<div className={'infio-chat-code-block-header-filename'}>
<FileSearch size={14} className="infio-chat-code-block-header-icon" />
<span>regex search files &quot;{regex}&quot; in {path}</span>
</div>
</div>
</div>
)
}

View File

@@ -0,0 +1,98 @@
import { Check, Loader2, Replace, X } from 'lucide-react'
import React from 'react'
import { useApp } from '../../../contexts/AppContext'
import { useDarkModeContext } from '../../../contexts/DarkModeContext'
import { ApplyStatus, SearchAndReplaceToolArgs } from '../../../types/apply'
import { openMarkdownFile } from '../../../utils/obsidian'
import { MemoizedSyntaxHighlighterWrapper } from "./SyntaxHighlighterWrapper"
export default function MarkdownSearchAndReplace({
applyStatus,
onApply,
path,
content,
operations,
finish
}: {
applyStatus: ApplyStatus
onApply: (args: SearchAndReplaceToolArgs) => void
path: string,
content: string,
operations: SearchAndReplaceToolArgs['operations'],
finish: boolean
}) {
const app = useApp()
const { isDarkMode } = useDarkModeContext()
const [applying, setApplying] = React.useState(false)
const handleClick = () => {
openMarkdownFile(app, path)
}
const handleApply = async () => {
if (applyStatus !== ApplyStatus.Idle) {
return
}
setApplying(true)
onApply({
type: 'search_and_replace',
filepath: path,
operations
})
}
return (
<div
className={`infio-chat-code-block ${path ? 'has-filename' : ''} infio-reasoning-block`}
onClick={handleClick}
>
<div className={'infio-chat-code-block-header'}>
<div className={'infio-chat-code-block-header-filename'}>
<Replace size={10} className="infio-chat-code-block-header-icon" />
Search and replace in {path}
</div>
<div className={'infio-chat-code-block-header-button'}>
<button
onClick={handleApply}
disabled={applyStatus !== ApplyStatus.Idle || applying || !finish}
>
{!finish ? (
<>
<Loader2 className="spinner" size={14} />
</>
) : applyStatus === ApplyStatus.Idle ? (
applying ? (
<>
<Loader2 className="spinner" size={14} /> Applying...
</>
) : (
'Apply'
)
) : applyStatus === ApplyStatus.Applied ? (
<>
<Check size={14} /> Success
</>
) : (
<>
<X size={14} /> Failed
</>
)}
</button>
</div>
</div>
<div className="infio-reasoning-content-wrapper">
<MemoizedSyntaxHighlighterWrapper
isDarkMode={isDarkMode}
language="markdown"
hasFilename={!!path}
wrapLines={true}
>
{content}
</MemoizedSyntaxHighlighterWrapper>
</div>
</div>
)
}

View File

@@ -0,0 +1,75 @@
import { Check, Loader2, Search, X } from 'lucide-react'
import React from 'react'
import { useSettings } from "../../../contexts/SettingsContext"
import { ApplyStatus, SearchWebToolArgs } from "../../../types/apply"
export default function MarkdownWebSearchBlock({
applyStatus,
onApply,
query,
finish
}: {
applyStatus: ApplyStatus
onApply: (args: SearchWebToolArgs) => void
query: string,
finish: boolean
}) {
const { settings } = useSettings()
const handleClick = () => {
if (settings.serperSearchEngine === 'google') {
window.open(`https://www.google.com/search?q=${query}`, '_blank')
} else if (settings.serperSearchEngine === 'bing') {
window.open(`https://www.bing.com/search?q=${query}`, '_blank')
} else {
window.open(`https://duckduckgo.com/?q=${query}`, '_blank')
}
}
React.useEffect(() => {
if (finish && applyStatus === ApplyStatus.Idle) {
onApply({
type: 'search_web',
query: query,
})
}
}, [finish])
return (
<div
className={`infio-chat-code-block has-filename`
}
onClick={handleClick}
>
<div className={'infio-chat-code-block-header'}>
<div className={'infio-chat-code-block-header-filename'}>
<Search size={14} className="infio-chat-code-block-header-icon" />
Web search: {query}
</div>
<div className={'infio-chat-code-block-header-button'}>
<button
style={{ color: '#008000' }}
disabled={true}
>
{
!finish || applyStatus === ApplyStatus.Idle ? (
<>
<Loader2 className="spinner" size={14} /> Searching...
</>
) : applyStatus === ApplyStatus.Applied ? (
<>
<Check size={14} /> Done
</>
) : (
<>
<X size={14} /> Failed
</>
)}
</button>
</div>
</div>
</div>
)
}

View File

@@ -0,0 +1,50 @@
import { FileSearch } from 'lucide-react'
import React from 'react'
import { useApp } from "../../../contexts/AppContext"
import { ApplyStatus, SemanticSearchFilesToolArgs } from "../../../types/apply"
import { openMarkdownFile } from "../../../utils/obsidian"
export default function MarkdownSemanticSearchFilesBlock({
applyStatus,
onApply,
path,
query,
finish
}: {
applyStatus: ApplyStatus
onApply: (args: SemanticSearchFilesToolArgs) => void
path: string,
query: string,
finish: boolean
}) {
const app = useApp()
const handleClick = () => {
openMarkdownFile(app, path)
}
React.useEffect(() => {
if (finish && applyStatus === ApplyStatus.Idle) {
onApply({
type: 'semantic_search_files',
filepath: path,
query: query,
})
}
}, [finish])
return (
<div
className={`infio-chat-code-block ${path ? 'has-filename' : ''}`}
onClick={handleClick}
>
<div className={'infio-chat-code-block-header'}>
<div className={'infio-chat-code-block-header-filename'}>
<FileSearch size={14} className="infio-chat-code-block-header-icon" />
<span>semantic search files &quot;{query}&quot; in {path}</span>
</div>
</div>
</div>
)
}

View File

@@ -0,0 +1,82 @@
import { Check, Loader2, Settings2, X } from 'lucide-react'
import { PropsWithChildren, useState } from 'react'
import { useDarkModeContext } from "../../../contexts/DarkModeContext"
import { ApplyStatus, ToolArgs } from "../../../types/apply"
import { MemoizedSyntaxHighlighterWrapper } from "./SyntaxHighlighterWrapper"
export default function MarkdownSwitchModeBlock({
mode,
applyStatus,
onApply,
reason,
finish,
}: PropsWithChildren<{
mode: string
applyStatus: ApplyStatus
onApply: (args: ToolArgs) => void
reason: string
finish: boolean
}>) {
const [applying, setApplying] = useState(false)
const { isDarkMode } = useDarkModeContext()
const handleApply = async () => {
if (applyStatus !== ApplyStatus.Idle) {
return
}
setApplying(true)
onApply({
type: 'switch_mode',
mode: mode,
reason: reason,
finish: finish,
})
}
return (
<div className={`infio-chat-code-block has-filename`}>
<div className={'infio-chat-code-block-header'}>
<div className={'infio-chat-code-block-header-filename'}>
<Settings2 size={10} className="infio-chat-code-block-header-icon" />
Switch to &quot;{mode.charAt(0).toUpperCase() + mode.slice(1)}&quot; mode
</div>
<div className={'infio-chat-code-block-header-button'}>
<button
onClick={handleApply}
style={{ color: '#008000' }}
disabled={applyStatus !== ApplyStatus.Idle || applying}
>
{applyStatus === ApplyStatus.Idle ? (
applying ? (
<>
<Loader2 className="spinner" size={14} /> Allowing...
</>
) : (
'Allow'
)
) : applyStatus === ApplyStatus.Applied ? (
<>
<Check size={14} /> Success
</>
) : (
<>
<X size={14} /> Failed
</>
)}
</button>
</div>
</div>
<MemoizedSyntaxHighlighterWrapper
isDarkMode={isDarkMode}
language="markdown"
hasFilename={true}
wrapLines={true}
isOpen={true}
>
{reason}
</MemoizedSyntaxHighlighterWrapper>
</div>
)
}

View File

@@ -0,0 +1,155 @@
import * as Tooltip from '@radix-ui/react-tooltip';
import { Check, CircleCheckBig, CircleHelp, CopyIcon, FilePlus2 } from 'lucide-react';
import { ReactNode, useState } from 'react';
import ReactMarkdown from 'react-markdown';
import rehypeRaw from 'rehype-raw';
import { useApp } from 'src/contexts/AppContext';
function CopyButton({ message }: { message: string }) {
const [copied, setCopied] = useState(false)
const handleCopy = async () => {
await navigator.clipboard.writeText(message)
setCopied(true)
setTimeout(() => {
setCopied(false)
}, 1500)
}
return (
<Tooltip.Provider delayDuration={0}>
<Tooltip.Root>
<Tooltip.Trigger asChild>
<button>
{copied ? (
<Check
size={12}
className="infio-chat-message-actions-icon--copied"
/>
) : (
<CopyIcon onClick={handleCopy} size={12} />
)}
</button>
</Tooltip.Trigger>
<Tooltip.Portal>
<Tooltip.Content className="infio-tooltip-content">
Copy message
</Tooltip.Content>
</Tooltip.Portal>
</Tooltip.Root>
</Tooltip.Provider>
)
}
function CreateNewFileButton({ message }: { message: string }) {
const app = useApp()
const [created, setCreated] = useState(false)
const cleanMarkdownTitle = (text: string): string => {
// 移除所有 # 开头的标题标记
return text.replace(/^#+\s*/g, '');
}
const handleCreate = async () => {
const firstLine = cleanMarkdownTitle(message.trimStart().split('\n')[0].trim()).replace(/[\\/:]/g, '');
const filename = firstLine.slice(0, 200) + (firstLine.length > 200 ? '...' : '') || 'untitled';
await app.vault.create(`/${filename}.md`, message)
await app.workspace.openLinkText(filename, 'split', true)
setCreated(true)
setTimeout(() => {
setCreated(false)
}, 1500)
}
return (
<Tooltip.Provider delayDuration={0}>
<Tooltip.Root>
<Tooltip.Trigger asChild>
<button style={{ color: '#008000' }}>
{created ? (
<Check
size={12}
className="infio-chat-message-actions-icon--copied"
/>
) : (
<FilePlus2 onClick={handleCreate} size={12} />
)}
</button>
</Tooltip.Trigger>
<Tooltip.Portal>
<Tooltip.Content className="infio-tooltip-content">
Create new note
</Tooltip.Content>
</Tooltip.Portal>
</Tooltip.Root>
</Tooltip.Provider>
)
}
type IconType = 'ask_followup_question' | 'attempt_completion';
interface MarkdownWithIconsProps {
markdownContent: string;
finish: boolean
className?: string;
iconName?: IconType;
iconSize?: number;
iconClassName?: string;
}
const MarkdownWithIcons = ({
markdownContent,
finish,
className,
iconName,
iconSize = 14,
iconClassName = "infio-markdown-icon"
}: MarkdownWithIconsProps) => {
// Handle icon rendering directly without string manipulation
const renderIcon = (): ReactNode => {
if (!iconName) return null;
switch (iconName) {
case 'ask_followup_question':
return <CircleHelp size={iconSize} className={iconClassName} />;
case 'attempt_completion':
return <CircleCheckBig size={iconSize} className={iconClassName} />;
default:
return null;
}
};
const renderTitle = (): ReactNode => {
if (!iconName) return null;
switch (iconName) {
case 'ask_followup_question':
return 'Ask Followup Question:';
case 'attempt_completion':
return 'Task Completion';
default:
return null;
}
};
// Component for markdown content
return (
<>
<div className={`${className}`}>
<span>{iconName && renderIcon()} {renderTitle()}</span>
<ReactMarkdown
className={`${className}`}
rehypePlugins={[rehypeRaw]}
>
{markdownContent}
</ReactMarkdown>
</div>
{markdownContent && finish && iconName === "attempt_completion" &&
<div className="infio-chat-message-actions">
<CopyButton message={markdownContent} />
<CreateNewFileButton message={markdownContent} />
</div>}
</>
);
};
export default MarkdownWithIcons;

View File

@@ -0,0 +1,55 @@
import { memo } from 'react'
import { Prism as SyntaxHighlighter } from 'react-syntax-highlighter'
import {
oneDark,
oneLight,
} from 'react-syntax-highlighter/dist/esm/styles/prism'
function SyntaxHighlighterWrapper({
isDarkMode,
language,
hasFilename,
wrapLines,
children,
isOpen = true,
}: {
isDarkMode: boolean
language: string | undefined
hasFilename: boolean
wrapLines: boolean
children: string
isOpen?: boolean
}) {
if (!isOpen) return null;
return (
<SyntaxHighlighter
language={language}
style={isDarkMode ? oneDark : oneLight}
customStyle={{
borderRadius: hasFilename
? '0 0 var(--radius-s) var(--radius-s)'
: 'var(--radius-s)',
margin: 0,
padding: 'var(--size-4-2)',
fontSize: 'var(--font-ui-small)',
fontFamily:
language === 'markdown' ? 'var(--font-interface)' : 'inherit',
}}
wrapLines={wrapLines}
lineProps={
// Wrapping should work without lineProps, but Obsidian's default CSS seems to override SyntaxHighlighter's styles.
// We manually override the white-space property to ensure proper wrapping.
wrapLines
? {
style: { whiteSpace: 'pre-wrap' },
}
: undefined
}
>
{children}
</SyntaxHighlighter>
)
}
export const MemoizedSyntaxHighlighterWrapper = memo(SyntaxHighlighterWrapper)