|
1 | 1 | 'use client' |
2 | 2 |
|
3 | 3 | import { useEffect, useMemo, useRef, useState } from 'react' |
4 | | -import { ChevronDown, ChevronUp, Code, X } from 'lucide-react' |
| 4 | +import { ChevronDown, ChevronUp, X } from 'lucide-react' |
5 | 5 | import { Button } from '@/components/ui/button' |
6 | 6 | import { CopyButton } from '@/components/ui/copy-button' |
7 | 7 | import { ScrollArea } from '@/components/ui/scroll-area' |
8 | 8 | import { Tooltip, TooltipContent, TooltipProvider, TooltipTrigger } from '@/components/ui/tooltip' |
| 9 | +import { redactApiKeys } from '@/lib/utils' |
9 | 10 | import { WorkflowLog } from '@/app/w/logs/stores/types' |
10 | 11 | import { formatDate } from '@/app/w/logs/utils/format-date' |
11 | 12 | import { formatCost } from '@/providers/utils' |
12 | 13 | import { ToolCallsDisplay } from '../tool-calls/tool-calls-display' |
13 | 14 | import { TraceSpansDisplay } from '../trace-spans/trace-spans-display' |
| 15 | +import LogMarkdownRenderer from './components/markdown-renderer' |
14 | 16 |
|
15 | 17 | interface LogSidebarProps { |
16 | 18 | log: WorkflowLog | null |
@@ -49,38 +51,127 @@ const tryPrettifyJson = (content: string): { isJson: boolean; formatted: string |
49 | 51 | /** |
50 | 52 | * Formats JSON content for display, handling multiple JSON objects separated by '--' |
51 | 53 | */ |
52 | | -const formatJsonContent = (content: string): React.ReactNode => { |
| 54 | +const formatJsonContent = (content: string, blockInput?: Record<string, any>): React.ReactNode => { |
53 | 55 | // Look for a pattern like "Block Agent 1 (agent):" to separate system comment from content |
54 | 56 | const blockPattern = /^(Block .+?\(.+?\):)\s*/ |
55 | 57 | const match = content.match(blockPattern) |
56 | 58 |
|
57 | 59 | if (match) { |
58 | 60 | const systemComment = match[1] |
59 | 61 | const actualContent = content.substring(match[0].length).trim() |
60 | | - const { formatted } = tryPrettifyJson(actualContent) |
| 62 | + const { isJson, formatted } = tryPrettifyJson(actualContent) |
61 | 63 |
|
62 | 64 | return ( |
63 | | - <div className="w-full"> |
64 | | - <div className="text-sm font-medium mb-2 text-muted-foreground">{systemComment}</div> |
65 | | - <div className="bg-secondary/30 p-3 rounded-md relative group"> |
66 | | - <CopyButton text={formatted} className="h-7 w-7 z-10" /> |
67 | | - <pre className="text-sm whitespace-pre-wrap break-all w-full overflow-y-auto max-h-[500px] overflow-x-hidden"> |
68 | | - {formatted} |
69 | | - </pre> |
70 | | - </div> |
71 | | - </div> |
| 65 | + <BlockContentDisplay |
| 66 | + systemComment={systemComment} |
| 67 | + formatted={formatted} |
| 68 | + isJson={isJson} |
| 69 | + blockInput={blockInput} |
| 70 | + /> |
72 | 71 | ) |
73 | 72 | } |
74 | 73 |
|
75 | 74 | // If no system comment pattern found, show the whole content |
76 | | - const { formatted } = tryPrettifyJson(content) |
| 75 | + const { isJson, formatted } = tryPrettifyJson(content) |
77 | 76 |
|
78 | 77 | return ( |
79 | 78 | <div className="bg-secondary/30 p-3 rounded-md relative group w-full"> |
80 | 79 | <CopyButton text={formatted} className="h-7 w-7 z-10" /> |
81 | | - <pre className="text-sm whitespace-pre-wrap break-all w-full overflow-y-auto max-h-[500px] overflow-x-hidden"> |
82 | | - {formatted} |
83 | | - </pre> |
| 80 | + {isJson ? ( |
| 81 | + <pre className="text-sm whitespace-pre-wrap break-all w-full overflow-y-auto max-h-[500px] overflow-x-hidden"> |
| 82 | + {formatted} |
| 83 | + </pre> |
| 84 | + ) : ( |
| 85 | + <LogMarkdownRenderer content={formatted} /> |
| 86 | + )} |
| 87 | + </div> |
| 88 | + ) |
| 89 | +} |
| 90 | + |
| 91 | +const BlockContentDisplay = ({ |
| 92 | + systemComment, |
| 93 | + formatted, |
| 94 | + isJson, |
| 95 | + blockInput, |
| 96 | +}: { |
| 97 | + systemComment: string |
| 98 | + formatted: string |
| 99 | + isJson: boolean |
| 100 | + blockInput?: Record<string, any> |
| 101 | +}) => { |
| 102 | + const [activeTab, setActiveTab] = useState<'output' | 'input'>(blockInput ? 'output' : 'output') |
| 103 | + |
| 104 | + const redactedBlockInput = useMemo(() => { |
| 105 | + return blockInput ? redactApiKeys(blockInput) : undefined |
| 106 | + }, [blockInput]) |
| 107 | + |
| 108 | + const redactedOutput = useMemo(() => { |
| 109 | + if (!isJson) return formatted |
| 110 | + |
| 111 | + try { |
| 112 | + const parsedOutput = JSON.parse(formatted) |
| 113 | + const redactedJson = redactApiKeys(parsedOutput) |
| 114 | + return JSON.stringify(redactedJson, null, 2) |
| 115 | + } catch (e) { |
| 116 | + return formatted |
| 117 | + } |
| 118 | + }, [formatted, isJson]) |
| 119 | + |
| 120 | + return ( |
| 121 | + <div className="w-full"> |
| 122 | + <div className="text-sm font-medium text-muted-foreground mb-2">{systemComment}</div> |
| 123 | + |
| 124 | + {/* Tabs for switching between output and input */} |
| 125 | + {redactedBlockInput && ( |
| 126 | + <div className="flex space-x-1 mb-2"> |
| 127 | + <button |
| 128 | + onClick={() => setActiveTab('output')} |
| 129 | + className={`px-3 py-1 text-xs rounded-md transition-colors ${ |
| 130 | + activeTab === 'output' |
| 131 | + ? 'bg-secondary text-foreground' |
| 132 | + : 'hover:bg-secondary/50 text-muted-foreground' |
| 133 | + }`} |
| 134 | + > |
| 135 | + Output |
| 136 | + </button> |
| 137 | + <button |
| 138 | + onClick={() => setActiveTab('input')} |
| 139 | + className={`px-3 py-1 text-xs rounded-md transition-colors ${ |
| 140 | + activeTab === 'input' |
| 141 | + ? 'bg-secondary text-foreground' |
| 142 | + : 'hover:bg-secondary/50 text-muted-foreground' |
| 143 | + }`} |
| 144 | + > |
| 145 | + Input |
| 146 | + </button> |
| 147 | + </div> |
| 148 | + )} |
| 149 | + |
| 150 | + {/* Content based on active tab */} |
| 151 | + <div className="bg-secondary/30 p-3 rounded-md relative group"> |
| 152 | + {activeTab === 'output' ? ( |
| 153 | + <> |
| 154 | + <CopyButton text={redactedOutput} className="h-7 w-7 z-10" /> |
| 155 | + {isJson ? ( |
| 156 | + <pre className="text-sm whitespace-pre-wrap break-all w-full overflow-visible"> |
| 157 | + {redactedOutput} |
| 158 | + </pre> |
| 159 | + ) : ( |
| 160 | + <LogMarkdownRenderer content={redactedOutput} /> |
| 161 | + )} |
| 162 | + </> |
| 163 | + ) : ( |
| 164 | + <> |
| 165 | + <CopyButton |
| 166 | + text={JSON.stringify(redactedBlockInput, null, 2)} |
| 167 | + className="h-7 w-7 z-10" |
| 168 | + /> |
| 169 | + <pre className="text-sm whitespace-pre-wrap break-all w-full overflow-visible"> |
| 170 | + {JSON.stringify(redactedBlockInput, null, 2)} |
| 171 | + </pre> |
| 172 | + </> |
| 173 | + )} |
| 174 | + </div> |
84 | 175 | </div> |
85 | 176 | ) |
86 | 177 | } |
@@ -115,10 +206,29 @@ export function Sidebar({ |
115 | 206 |
|
116 | 207 | const formattedContent = useMemo(() => { |
117 | 208 | if (!log) return null |
118 | | - return formatJsonContent(log.message) |
| 209 | + |
| 210 | + let blockInput: Record<string, any> | undefined = undefined |
| 211 | + |
| 212 | + if (log.metadata?.blockInput) { |
| 213 | + blockInput = log.metadata.blockInput |
| 214 | + } else if (log.metadata?.traceSpans) { |
| 215 | + const blockIdMatch = log.message.match(/Block .+?(\d+)/i) |
| 216 | + const blockId = blockIdMatch ? blockIdMatch[1] : null |
| 217 | + |
| 218 | + if (blockId) { |
| 219 | + const matchingSpan = log.metadata.traceSpans.find( |
| 220 | + (span) => span.blockId === blockId || span.name.includes(`Block ${blockId}`) |
| 221 | + ) |
| 222 | + |
| 223 | + if (matchingSpan && matchingSpan.input) { |
| 224 | + blockInput = matchingSpan.input |
| 225 | + } |
| 226 | + } |
| 227 | + } |
| 228 | + |
| 229 | + return formatJsonContent(log.message, blockInput) |
119 | 230 | }, [log]) |
120 | 231 |
|
121 | | - // Reset scroll position when log changes |
122 | 232 | useEffect(() => { |
123 | 233 | if (scrollAreaRef.current) { |
124 | 234 | scrollAreaRef.current.scrollTop = 0 |
@@ -297,8 +407,11 @@ export function Sidebar({ |
297 | 407 | </div> |
298 | 408 |
|
299 | 409 | {/* Content */} |
300 | | - <ScrollArea className="h-[calc(100vh-64px-49px)] w-full" ref={scrollAreaRef}> |
301 | | - <div className="p-4 space-y-4 w-full overflow-hidden pr-6"> |
| 410 | + <ScrollArea |
| 411 | + className="h-[calc(100vh-64px-49px)] w-full overflow-y-auto" |
| 412 | + ref={scrollAreaRef} |
| 413 | + > |
| 414 | + <div className="p-4 space-y-4 w-full pr-6"> |
302 | 415 | {/* Timestamp */} |
303 | 416 | <div> |
304 | 417 | <h3 className="text-xs font-medium text-muted-foreground mb-1">Timestamp</h3> |
@@ -374,7 +487,7 @@ export function Sidebar({ |
374 | 487 | </div> |
375 | 488 | )} |
376 | 489 |
|
377 | | - {/* Message Content - MOVED ABOVE the Trace Spans and Cost */} |
| 490 | + {/* Message Content */} |
378 | 491 | <div className="pb-2 w-full"> |
379 | 492 | <h3 className="text-xs font-medium text-muted-foreground mb-1">Message</h3> |
380 | 493 | <div className="w-full">{formattedContent}</div> |
|
0 commit comments