|
| 1 | +import { useMemo } from 'react' |
1 | 2 | import { PillsRing } from '@/components/emcn' |
| 3 | +import { FunctionExecute } from '@/lib/copilot/generated/tool-catalog-v1' |
2 | 4 | import type { ToolCallStatus } from '../../../../types' |
3 | 5 | import { getToolIcon } from '../../utils' |
| 6 | +import { ChatContent } from '../chat-content/chat-content' |
4 | 7 |
|
5 | 8 | function CircleCheck({ className }: { className?: string }) { |
6 | 9 | return ( |
@@ -54,19 +57,72 @@ function StatusIcon({ status, toolName }: { status: ToolCallStatus; toolName: st |
54 | 57 | return <CircleCheck className='h-[15px] w-[15px] text-[var(--text-tertiary)]' /> |
55 | 58 | } |
56 | 59 |
|
| 60 | +const LANG_ALIASES: Record<string, string> = { |
| 61 | + javascript: 'javascript', |
| 62 | + python: 'python', |
| 63 | + shell: 'bash', |
| 64 | + bash: 'bash', |
| 65 | +} |
| 66 | + |
| 67 | +function extractFunctionExecutePreview(raw: string): { code: string; lang: string } | null { |
| 68 | + if (!raw) return null |
| 69 | + const langMatch = raw.match(/"language"\s*:\s*"(\w+)"/) |
| 70 | + const lang = langMatch ? (LANG_ALIASES[langMatch[1]] ?? langMatch[1]) : 'javascript' |
| 71 | + |
| 72 | + const codeStart = raw.indexOf('"code"') |
| 73 | + if (codeStart === -1) return null |
| 74 | + const colonIdx = raw.indexOf(':', codeStart + 6) |
| 75 | + if (colonIdx === -1) return null |
| 76 | + const quoteIdx = raw.indexOf('"', colonIdx + 1) |
| 77 | + if (quoteIdx === -1) return null |
| 78 | + |
| 79 | + let value = raw.slice(quoteIdx + 1) |
| 80 | + if (value.endsWith('"}') || value.endsWith('"\n}')) { |
| 81 | + value = value.replace(/"\s*\}?\s*$/, '') |
| 82 | + } |
| 83 | + if (value.endsWith('"')) { |
| 84 | + value = value.slice(0, -1) |
| 85 | + } |
| 86 | + |
| 87 | + const code = value |
| 88 | + .replace(/\\n/g, '\n') |
| 89 | + .replace(/\\t/g, '\t') |
| 90 | + .replace(/\\"/g, '"') |
| 91 | + .replace(/\\\\/g, '\\') |
| 92 | + |
| 93 | + return code.length > 0 ? { code, lang } : null |
| 94 | +} |
| 95 | + |
57 | 96 | interface ToolCallItemProps { |
58 | 97 | toolName: string |
59 | 98 | displayTitle: string |
60 | 99 | status: ToolCallStatus |
| 100 | + streamingArgs?: string |
61 | 101 | } |
62 | 102 |
|
63 | | -export function ToolCallItem({ toolName, displayTitle, status }: ToolCallItemProps) { |
| 103 | +export function ToolCallItem({ toolName, displayTitle, status, streamingArgs }: ToolCallItemProps) { |
| 104 | + const extracted = useMemo(() => { |
| 105 | + if (toolName !== FunctionExecute.id || !streamingArgs) return null |
| 106 | + return extractFunctionExecutePreview(streamingArgs) |
| 107 | + }, [toolName, streamingArgs]) |
| 108 | + const markdown = useMemo( |
| 109 | + () => (extracted ? `\`\`\`${extracted.lang}\n${extracted.code}\n\`\`\`` : null), |
| 110 | + [extracted] |
| 111 | + ) |
| 112 | + |
64 | 113 | return ( |
65 | | - <div className='flex items-center gap-[8px] pl-[24px]'> |
66 | | - <div className='flex h-[16px] w-[16px] flex-shrink-0 items-center justify-center'> |
67 | | - <StatusIcon status={status} toolName={toolName} /> |
| 114 | + <div className='flex flex-col pl-[24px]'> |
| 115 | + <div className='flex items-center gap-[8px]'> |
| 116 | + <div className='flex h-[16px] w-[16px] flex-shrink-0 items-center justify-center'> |
| 117 | + <StatusIcon status={status} toolName={toolName} /> |
| 118 | + </div> |
| 119 | + <span className='font-base text-[13px] text-[var(--text-secondary)]'>{displayTitle}</span> |
68 | 120 | </div> |
69 | | - <span className='font-base text-[13px] text-[var(--text-secondary)]'>{displayTitle}</span> |
| 121 | + {markdown && ( |
| 122 | + <div className='ml-[24px] max-h-[300px] overflow-auto'> |
| 123 | + <ChatContent content={markdown} isStreaming={status === 'executing'} /> |
| 124 | + </div> |
| 125 | + )} |
70 | 126 | </div> |
71 | 127 | ) |
72 | 128 | } |
0 commit comments