|
3 | 3 | import { useEffect, useMemo, useRef, useState } from 'react' |
4 | 4 | import clsx from 'clsx' |
5 | 5 | import { ChevronUp, LayoutList } from 'lucide-react' |
6 | | -import { Button, Code } from '@/components/emcn' |
| 6 | +import Editor from 'react-simple-code-editor' |
| 7 | +import { Button, Code, getCodeEditorProps, highlight, languages } from '@/components/emcn' |
7 | 8 | import { ClientToolCallState } from '@/lib/copilot/tools/client/base-tool' |
8 | 9 | import { getClientTool } from '@/lib/copilot/tools/client/manager' |
9 | 10 | import { getRegisteredTools } from '@/lib/copilot/tools/client/registry' |
@@ -753,36 +754,70 @@ function SubAgentToolCall({ toolCall: toolCallProp }: { toolCall: CopilotToolCal |
753 | 754 | const safeInputs = inputs && typeof inputs === 'object' ? inputs : {} |
754 | 755 | const inputEntries = Object.entries(safeInputs) |
755 | 756 | if (inputEntries.length === 0) return null |
| 757 | + |
| 758 | + /** |
| 759 | + * Format a value for display - handles objects, arrays, and primitives |
| 760 | + */ |
| 761 | + const formatValue = (value: unknown): string => { |
| 762 | + if (value === null || value === undefined) return '-' |
| 763 | + if (typeof value === 'string') return value || '-' |
| 764 | + if (typeof value === 'number' || typeof value === 'boolean') return String(value) |
| 765 | + try { |
| 766 | + return JSON.stringify(value, null, 2) |
| 767 | + } catch { |
| 768 | + return String(value) |
| 769 | + } |
| 770 | + } |
| 771 | + |
| 772 | + /** |
| 773 | + * Check if a value is a complex type (object or array) |
| 774 | + */ |
| 775 | + const isComplex = (value: unknown): boolean => { |
| 776 | + return typeof value === 'object' && value !== null |
| 777 | + } |
| 778 | + |
756 | 779 | return ( |
757 | | - <div className='mt-1.5 w-full overflow-hidden rounded-[4px] border border-[var(--border-1)] bg-[var(--surface-1)]'> |
758 | | - <table className='w-full table-fixed bg-transparent'> |
759 | | - <thead className='bg-transparent'> |
760 | | - <tr className='border-[var(--border-1)] border-b bg-transparent'> |
761 | | - <th className='w-[36%] border-[var(--border-1)] border-r bg-transparent px-[10px] py-[5px] text-left font-medium text-[12px] text-[var(--text-tertiary)]'> |
762 | | - Input |
763 | | - </th> |
764 | | - <th className='w-[64%] bg-transparent px-[10px] py-[5px] text-left font-medium text-[12px] text-[var(--text-tertiary)]'> |
765 | | - Value |
766 | | - </th> |
767 | | - </tr> |
768 | | - </thead> |
769 | | - <tbody className='bg-transparent'> |
770 | | - {inputEntries.map(([key, value]) => ( |
771 | | - <tr key={key} className='border-[var(--border-1)] border-t bg-transparent'> |
772 | | - <td className='w-[36%] border-[var(--border-1)] border-r bg-transparent px-[10px] py-[6px]'> |
773 | | - <span className='truncate font-medium text-[var(--text-primary)] text-xs'> |
774 | | - {key} |
775 | | - </span> |
776 | | - </td> |
777 | | - <td className='w-[64%] bg-transparent px-[10px] py-[6px]'> |
778 | | - <span className='font-mono text-[var(--text-muted)] text-xs'> |
779 | | - {String(value)} |
| 780 | + <div className='mt-1.5 w-full overflow-hidden rounded-md border border-[var(--border-1)] bg-[var(--surface-1)]'> |
| 781 | + {/* Header */} |
| 782 | + <div className='flex items-center gap-[8px] border-[var(--border-1)] border-b bg-[var(--surface-2)] p-[8px]'> |
| 783 | + <span className='font-medium text-[12px] text-[var(--text-primary)]'>Input</span> |
| 784 | + <span className='flex-shrink-0 font-medium text-[12px] text-[var(--text-tertiary)]'> |
| 785 | + {inputEntries.length} |
| 786 | + </span> |
| 787 | + </div> |
| 788 | + {/* Input entries */} |
| 789 | + <div className='flex flex-col'> |
| 790 | + {inputEntries.map(([key, value], index) => { |
| 791 | + const formattedValue = formatValue(value) |
| 792 | + const needsCodeViewer = isComplex(value) |
| 793 | + |
| 794 | + return ( |
| 795 | + <div |
| 796 | + key={key} |
| 797 | + className={clsx( |
| 798 | + 'flex flex-col gap-1 px-[10px] py-[6px]', |
| 799 | + index > 0 && 'border-[var(--border-1)] border-t' |
| 800 | + )} |
| 801 | + > |
| 802 | + {/* Input key */} |
| 803 | + <span className='font-medium text-[11px] text-[var(--text-primary)]'>{key}</span> |
| 804 | + {/* Value display */} |
| 805 | + {needsCodeViewer ? ( |
| 806 | + <Code.Viewer |
| 807 | + code={formattedValue} |
| 808 | + language='json' |
| 809 | + showGutter={false} |
| 810 | + className='max-h-[80px] min-h-0' |
| 811 | + /> |
| 812 | + ) : ( |
| 813 | + <span className='font-mono text-[11px] text-[var(--text-muted)] leading-[1.3]'> |
| 814 | + {formattedValue} |
780 | 815 | </span> |
781 | | - </td> |
782 | | - </tr> |
783 | | - ))} |
784 | | - </tbody> |
785 | | - </table> |
| 816 | + )} |
| 817 | + </div> |
| 818 | + ) |
| 819 | + })} |
| 820 | + </div> |
786 | 821 | </div> |
787 | 822 | ) |
788 | 823 | } |
@@ -2292,74 +2327,136 @@ export function ToolCall({ toolCall: toolCallProp, toolCallId, onStateChange }: |
2292 | 2327 | const safeInputs = inputs && typeof inputs === 'object' ? inputs : {} |
2293 | 2328 | const inputEntries = Object.entries(safeInputs) |
2294 | 2329 |
|
2295 | | - // Don't show the table if there are no inputs |
| 2330 | + // Don't show the section if there are no inputs |
2296 | 2331 | if (inputEntries.length === 0) { |
2297 | 2332 | return null |
2298 | 2333 | } |
2299 | 2334 |
|
| 2335 | + /** |
| 2336 | + * Format a value for display - handles objects, arrays, and primitives |
| 2337 | + */ |
| 2338 | + const formatValueForDisplay = (value: unknown): string => { |
| 2339 | + if (value === null || value === undefined) return '' |
| 2340 | + if (typeof value === 'string') return value |
| 2341 | + if (typeof value === 'number' || typeof value === 'boolean') return String(value) |
| 2342 | + // For objects and arrays, use JSON.stringify with formatting |
| 2343 | + try { |
| 2344 | + return JSON.stringify(value, null, 2) |
| 2345 | + } catch { |
| 2346 | + return String(value) |
| 2347 | + } |
| 2348 | + } |
| 2349 | + |
| 2350 | + /** |
| 2351 | + * Parse a string value back to its original type if possible |
| 2352 | + */ |
| 2353 | + const parseInputValue = (value: string, originalValue: unknown): unknown => { |
| 2354 | + // If original was a primitive, keep as string |
| 2355 | + if (typeof originalValue !== 'object' || originalValue === null) { |
| 2356 | + return value |
| 2357 | + } |
| 2358 | + // Try to parse as JSON for objects/arrays |
| 2359 | + try { |
| 2360 | + return JSON.parse(value) |
| 2361 | + } catch { |
| 2362 | + return value |
| 2363 | + } |
| 2364 | + } |
| 2365 | + |
| 2366 | + /** |
| 2367 | + * Check if a value is a complex type (object or array) |
| 2368 | + */ |
| 2369 | + const isComplexValue = (value: unknown): boolean => { |
| 2370 | + return typeof value === 'object' && value !== null |
| 2371 | + } |
| 2372 | + |
2300 | 2373 | return ( |
2301 | | - <div className='w-full overflow-hidden rounded-[4px] border border-[var(--border-1)] bg-[var(--surface-1)]'> |
2302 | | - <table className='w-full table-fixed bg-transparent'> |
2303 | | - <thead className='bg-transparent'> |
2304 | | - <tr className='border-[var(--border-1)] border-b bg-transparent'> |
2305 | | - <th className='w-[36%] border-[var(--border-1)] border-r bg-transparent px-[10px] py-[5px] text-left font-medium text-[14px] text-[var(--text-tertiary)]'> |
2306 | | - Input |
2307 | | - </th> |
2308 | | - <th className='w-[64%] bg-transparent px-[10px] py-[5px] text-left font-medium text-[14px] text-[var(--text-tertiary)]'> |
2309 | | - Value |
2310 | | - </th> |
2311 | | - </tr> |
2312 | | - </thead> |
2313 | | - <tbody className='bg-transparent'> |
2314 | | - {inputEntries.map(([key, value]) => ( |
2315 | | - <tr |
| 2374 | + <div className='w-full overflow-hidden rounded-md border border-[var(--border-1)] bg-[var(--surface-1)]'> |
| 2375 | + {/* Header */} |
| 2376 | + <div className='flex items-center gap-[8px] border-[var(--border-1)] border-b bg-[var(--surface-2)] p-[8px]'> |
| 2377 | + <span className='font-medium text-[12px] text-[var(--text-primary)]'>Edit Input</span> |
| 2378 | + <span className='flex-shrink-0 font-medium text-[12px] text-[var(--text-tertiary)]'> |
| 2379 | + {inputEntries.length} |
| 2380 | + </span> |
| 2381 | + </div> |
| 2382 | + {/* Input entries */} |
| 2383 | + <div className='flex flex-col'> |
| 2384 | + {inputEntries.map(([key, value], index) => { |
| 2385 | + const isComplex = isComplexValue(value) |
| 2386 | + const displayValue = formatValueForDisplay(value) |
| 2387 | + |
| 2388 | + return ( |
| 2389 | + <div |
2316 | 2390 | key={key} |
2317 | | - className='group relative border-[var(--border-1)] border-t bg-transparent' |
| 2391 | + className={clsx( |
| 2392 | + 'flex flex-col gap-1.5 px-[10px] py-[8px]', |
| 2393 | + index > 0 && 'border-[var(--border-1)] border-t' |
| 2394 | + )} |
2318 | 2395 | > |
2319 | | - <td className='relative w-[36%] border-[var(--border-1)] border-r bg-transparent p-0'> |
2320 | | - <div className='px-[10px] py-[8px]'> |
2321 | | - <span className='truncate font-medium text-[var(--text-primary)] text-xs'> |
2322 | | - {key} |
2323 | | - </span> |
2324 | | - </div> |
2325 | | - </td> |
2326 | | - <td className='relative w-[64%] bg-transparent p-0'> |
2327 | | - <div className='min-w-0 px-[10px] py-[8px]'> |
2328 | | - <input |
2329 | | - type='text' |
2330 | | - value={String(value)} |
2331 | | - onChange={(e) => { |
2332 | | - const newInputs = { ...safeInputs, [key]: e.target.value } |
2333 | | - |
2334 | | - // Determine how to update based on original structure |
2335 | | - if (isNestedInWorkflowInput) { |
2336 | | - // Update workflow_input |
2337 | | - setEditedParams({ ...editedParams, workflow_input: newInputs }) |
2338 | | - } else if (typeof editedParams.input === 'string') { |
2339 | | - // Input was a JSON string, serialize back |
2340 | | - setEditedParams({ ...editedParams, input: JSON.stringify(newInputs) }) |
2341 | | - } else if (editedParams.input && typeof editedParams.input === 'object') { |
2342 | | - // Input is an object |
2343 | | - setEditedParams({ ...editedParams, input: newInputs }) |
2344 | | - } else if ( |
2345 | | - editedParams.inputs && |
2346 | | - typeof editedParams.inputs === 'object' |
2347 | | - ) { |
2348 | | - // Inputs is an object |
2349 | | - setEditedParams({ ...editedParams, inputs: newInputs }) |
2350 | | - } else { |
2351 | | - // Flat structure - update at base level |
2352 | | - setEditedParams({ ...editedParams, [key]: e.target.value }) |
2353 | | - } |
2354 | | - }} |
2355 | | - className='w-full bg-transparent font-mono text-[var(--text-muted)] text-xs outline-none focus:text-[var(--text-primary)]' |
2356 | | - /> |
2357 | | - </div> |
2358 | | - </td> |
2359 | | - </tr> |
2360 | | - ))} |
2361 | | - </tbody> |
2362 | | - </table> |
| 2396 | + {/* Input key */} |
| 2397 | + <span className='font-medium text-[11px] text-[var(--text-primary)]'>{key}</span> |
| 2398 | + {/* Value editor */} |
| 2399 | + {isComplex ? ( |
| 2400 | + <Code.Container className='max-h-[168px] min-h-[60px]'> |
| 2401 | + <Code.Content> |
| 2402 | + <Editor |
| 2403 | + value={displayValue} |
| 2404 | + onValueChange={(newCode) => { |
| 2405 | + const parsedValue = parseInputValue(newCode, value) |
| 2406 | + const newInputs = { ...safeInputs, [key]: parsedValue } |
| 2407 | + |
| 2408 | + if (isNestedInWorkflowInput) { |
| 2409 | + setEditedParams({ ...editedParams, workflow_input: newInputs }) |
| 2410 | + } else if (typeof editedParams.input === 'string') { |
| 2411 | + setEditedParams({ ...editedParams, input: JSON.stringify(newInputs) }) |
| 2412 | + } else if ( |
| 2413 | + editedParams.input && |
| 2414 | + typeof editedParams.input === 'object' |
| 2415 | + ) { |
| 2416 | + setEditedParams({ ...editedParams, input: newInputs }) |
| 2417 | + } else if ( |
| 2418 | + editedParams.inputs && |
| 2419 | + typeof editedParams.inputs === 'object' |
| 2420 | + ) { |
| 2421 | + setEditedParams({ ...editedParams, inputs: newInputs }) |
| 2422 | + } else { |
| 2423 | + setEditedParams({ ...editedParams, [key]: parsedValue }) |
| 2424 | + } |
| 2425 | + }} |
| 2426 | + highlight={(code) => highlight(code, languages.json, 'json')} |
| 2427 | + {...getCodeEditorProps()} |
| 2428 | + className={clsx(getCodeEditorProps().className, 'min-h-[40px]')} |
| 2429 | + style={{ minHeight: '40px' }} |
| 2430 | + /> |
| 2431 | + </Code.Content> |
| 2432 | + </Code.Container> |
| 2433 | + ) : ( |
| 2434 | + <input |
| 2435 | + type='text' |
| 2436 | + value={displayValue} |
| 2437 | + onChange={(e) => { |
| 2438 | + const parsedValue = parseInputValue(e.target.value, value) |
| 2439 | + const newInputs = { ...safeInputs, [key]: parsedValue } |
| 2440 | + |
| 2441 | + if (isNestedInWorkflowInput) { |
| 2442 | + setEditedParams({ ...editedParams, workflow_input: newInputs }) |
| 2443 | + } else if (typeof editedParams.input === 'string') { |
| 2444 | + setEditedParams({ ...editedParams, input: JSON.stringify(newInputs) }) |
| 2445 | + } else if (editedParams.input && typeof editedParams.input === 'object') { |
| 2446 | + setEditedParams({ ...editedParams, input: newInputs }) |
| 2447 | + } else if (editedParams.inputs && typeof editedParams.inputs === 'object') { |
| 2448 | + setEditedParams({ ...editedParams, inputs: newInputs }) |
| 2449 | + } else { |
| 2450 | + setEditedParams({ ...editedParams, [key]: parsedValue }) |
| 2451 | + } |
| 2452 | + }} |
| 2453 | + className='w-full rounded-[4px] border border-[var(--border-1)] bg-[var(--surface-1)] px-[8px] py-[6px] font-medium font-mono text-[13px] text-[var(--text-primary)] outline-none transition-colors placeholder:text-[var(--text-muted)] focus:outline-none' |
| 2454 | + /> |
| 2455 | + )} |
| 2456 | + </div> |
| 2457 | + ) |
| 2458 | + })} |
| 2459 | + </div> |
2363 | 2460 | </div> |
2364 | 2461 | ) |
2365 | 2462 | } |
|
0 commit comments