add new md content

This commit is contained in:
duanfuxiang 2025-03-12 21:36:58 +08:00
parent a592f65828
commit bd7eb2b57a
9 changed files with 468 additions and 12 deletions

View File

@ -2,12 +2,14 @@ import { Check, CopyIcon, Loader2 } from 'lucide-react'
import { PropsWithChildren, useMemo, useState } from 'react'
import { useDarkModeContext } from '../../contexts/DarkModeContext'
import { ToolArgs } from "../../types/apply"
import { InfioBlockAction } from '../../utils/parse-infio-block'
import { MemoizedSyntaxHighlighterWrapper } from './SyntaxHighlighterWrapper'
export default function MarkdownActionBlock({
onApply,
msgId,
onApply,
isApplying,
language,
filename,
@ -16,12 +18,8 @@ export default function MarkdownActionBlock({
action,
children,
}: PropsWithChildren<{
onApply: (blockInfo: {
content: string
filename?: string
startLine?: number
endLine?: number
}) => void
msgId: string,
onApply: (args: ToolArgs) => void
isApplying: boolean
language?: string
filename?: string
@ -71,9 +69,11 @@ export default function MarkdownActionBlock({
{action === InfioBlockAction.Edit && (
<button
onClick={() => {
onApply({
onApply({
type: 'write_to_file',
msgId,
content: String(children),
filename,
filepath: filename,
startLine,
endLine
})
@ -92,9 +92,13 @@ export default function MarkdownActionBlock({
{action === InfioBlockAction.New && (
<button
onClick={() => {
onApply({
onApply({
type: 'write_to_file',
msgId,
content: String(children),
filename
filepath: filename,
startLine: 1,
endLine: undefined
})
}}
disabled={isApplying}

View File

@ -0,0 +1,119 @@
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({
type: mode,
filepath: path,
content: String(children),
startLine,
endLine
})
}
return (
<div className={`infio-chat-code-block ${path ? 'has-filename' : ''}`}>
<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>
<MemoizedSyntaxHighlighterWrapper
isDarkMode={isDarkMode}
language={language}
hasFilename={!!path}
wrapLines={wrapLines}
>
{String(children)}
</MemoizedSyntaxHighlighterWrapper>
</div>
)
}

View File

@ -0,0 +1,52 @@
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(() => {
console.log('finish', finish, applyStatus)
if (finish && applyStatus === ApplyStatus.Idle) {
console.log('finish auto list files', path)
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,49 @@
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(() => {
console.log('finish', finish, applyStatus)
if (finish && applyStatus === ApplyStatus.Idle) {
console.log('finish auto read file', path)
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

@ -1,4 +1,4 @@
import { ChevronDown, ChevronRight } from 'lucide-react'
import { ChevronDown, ChevronRight, Brain } from 'lucide-react'
import { PropsWithChildren, useEffect, useRef, useState } from 'react'
import { useDarkModeContext } from '../../contexts/DarkModeContext'
@ -27,6 +27,7 @@ export default function MarkdownReasoningBlock({
>
<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

View File

@ -0,0 +1,53 @@
import { FolderOpen } 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(() => {
console.log('finish', finish, applyStatus)
if (finish && applyStatus === ApplyStatus.Idle) {
console.log('finish auto regex search files', path)
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'}>
<FolderOpen 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,78 @@
import { Check, Loader2, Replace, X } from 'lucide-react'
import React from 'react'
import { useApp } from '../../contexts/AppContext'
import { ApplyStatus, SearchAndReplaceToolArgs } from '../../types/apply'
import { openMarkdownFile } from '../../utils/obsidian'
export default function MarkdownSearchAndReplace({
applyStatus,
onApply,
path,
operations,
finish
}: {
applyStatus: ApplyStatus
onApply: (args: SearchAndReplaceToolArgs) => void
path: string,
operations: SearchAndReplaceToolArgs['operations'],
finish: boolean
}) {
const app = useApp()
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' : ''}`}
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}
>
{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>
)
}

View File

@ -0,0 +1,52 @@
import { FolderOpen } 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(() => {
console.log('finish', finish, applyStatus)
if (finish && applyStatus === ApplyStatus.Idle) {
console.log('finish auto semantic search files', path)
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'}>
<FolderOpen 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,48 @@
import { CircleCheckBig, CircleHelp } from 'lucide-react';
import { ComponentPropsWithoutRef } from 'react';
import ReactMarkdown from 'react-markdown';
import rehypeRaw from 'rehype-raw';
const MarkdownWithIcons = ({ markdownContent, className }: { markdownContent: string, className?: string }) => {
// 预处理markdown内容将<icon>标签转换为ReactMarkdown可以处理的格式
const processedContent = markdownContent.replace(
/<icon\s+name=['"]([^'"]+)['"]\s+size=\{(\d+)\}(\s+className=['"]([^'"]+)['"])?[^>]*\/>/g,
(_, name, size, __, className) =>
`<span data-icon="${name}" data-size="${size}" ${className ? `class="${className}"` : ''}></span>`
);
const components = {
span: (props: ComponentPropsWithoutRef<'span'> & {
'data-icon'?: string;
'data-size'?: string;
}) => {
if (props['data-icon']) {
const name = props['data-icon'];
const size = props['data-size'] ? Number(props['data-size']) : 16;
const className = props.className || '';
switch (name) {
case 'ask_followup_question':
return <CircleHelp size={size} className={className} />;
case 'attempt_completion':
return <CircleCheckBig size={size} className={className} />;
default:
return null;
}
}
return <span {...props} />;
},
};
return (
<ReactMarkdown
className={`${className}`}
components={components}
rehypePlugins={[rehypeRaw]}
>
{processedContent}
</ReactMarkdown>
);
};
export default MarkdownWithIcons;