mirror of
https://github.com/EthanMarti/infio-copilot.git
synced 2026-01-16 08:21:55 +00:00
add new md content
This commit is contained in:
parent
a592f65828
commit
bd7eb2b57a
@ -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}
|
||||
|
||||
119
src/components/chat-view/MarkdownEditFileBlock.tsx
Normal file
119
src/components/chat-view/MarkdownEditFileBlock.tsx
Normal 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>
|
||||
)
|
||||
}
|
||||
52
src/components/chat-view/MarkdownListFilesBlock.tsx
Normal file
52
src/components/chat-view/MarkdownListFilesBlock.tsx
Normal 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>
|
||||
)
|
||||
}
|
||||
49
src/components/chat-view/MarkdownReadFileBlock.tsx
Normal file
49
src/components/chat-view/MarkdownReadFileBlock.tsx
Normal 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>
|
||||
)
|
||||
}
|
||||
@ -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
|
||||
|
||||
53
src/components/chat-view/MarkdownRegexSearchFilesBlock.tsx
Normal file
53
src/components/chat-view/MarkdownRegexSearchFilesBlock.tsx
Normal 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 "{regex}" in {path}</span>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
)
|
||||
}
|
||||
78
src/components/chat-view/MarkdownSearchAndReplace.tsx
Normal file
78
src/components/chat-view/MarkdownSearchAndReplace.tsx
Normal 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>
|
||||
)
|
||||
}
|
||||
@ -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 "{query}" in {path}</span>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
)
|
||||
}
|
||||
48
src/components/chat-view/MarkdownWithIcon.tsx
Normal file
48
src/components/chat-view/MarkdownWithIcon.tsx
Normal 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;
|
||||
Loading…
x
Reference in New Issue
Block a user