mirror of
https://github.com/EthanMarti/infio-copilot.git
synced 2026-05-06 15:00:09 +00:00
udpate markdown tsx file path
This commit is contained in:
92
src/components/chat-view/Markdown/MarkdownApplyDiffBlock.tsx
Normal file
92
src/components/chat-view/Markdown/MarkdownApplyDiffBlock.tsx
Normal 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>
|
||||
)
|
||||
}
|
||||
122
src/components/chat-view/Markdown/MarkdownEditFileBlock.tsx
Normal file
122
src/components/chat-view/Markdown/MarkdownEditFileBlock.tsx
Normal 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>
|
||||
)
|
||||
}
|
||||
@@ -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>
|
||||
)
|
||||
)
|
||||
}
|
||||
50
src/components/chat-view/Markdown/MarkdownListFilesBlock.tsx
Normal file
50
src/components/chat-view/Markdown/MarkdownListFilesBlock.tsx
Normal 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>
|
||||
)
|
||||
}
|
||||
47
src/components/chat-view/Markdown/MarkdownReadFileBlock.tsx
Normal file
47
src/components/chat-view/Markdown/MarkdownReadFileBlock.tsx
Normal 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>
|
||||
)
|
||||
}
|
||||
57
src/components/chat-view/Markdown/MarkdownReasoningBlock.tsx
Normal file
57
src/components/chat-view/Markdown/MarkdownReasoningBlock.tsx
Normal 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>
|
||||
)
|
||||
)
|
||||
}
|
||||
75
src/components/chat-view/Markdown/MarkdownReferenceBlock.tsx
Normal file
75
src/components/chat-view/Markdown/MarkdownReferenceBlock.tsx
Normal 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>
|
||||
)
|
||||
)
|
||||
}
|
||||
@@ -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 "{regex}" in {path}</span>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
)
|
||||
}
|
||||
@@ -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>
|
||||
)
|
||||
}
|
||||
75
src/components/chat-view/Markdown/MarkdownSearchWebBlock.tsx
Normal file
75
src/components/chat-view/Markdown/MarkdownSearchWebBlock.tsx
Normal 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>
|
||||
)
|
||||
}
|
||||
@@ -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 "{query}" in {path}</span>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
)
|
||||
}
|
||||
@@ -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 "{mode.charAt(0).toUpperCase() + mode.slice(1)}" 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>
|
||||
)
|
||||
}
|
||||
155
src/components/chat-view/Markdown/MarkdownWithIcon.tsx
Normal file
155
src/components/chat-view/Markdown/MarkdownWithIcon.tsx
Normal 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;
|
||||
@@ -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)
|
||||
Reference in New Issue
Block a user