diff --git a/web/oss/src/components/DrillInView/BeautifiedJsonView.tsx b/web/oss/src/components/DrillInView/BeautifiedJsonView.tsx index 4ada672cfb..b4dedb3d01 100644 --- a/web/oss/src/components/DrillInView/BeautifiedJsonView.tsx +++ b/web/oss/src/components/DrillInView/BeautifiedJsonView.tsx @@ -1,4 +1,4 @@ -import {memo, useEffect, useLayoutEffect, useMemo} from "react" +import {memo, useEffect, useLayoutEffect, useMemo, useState} from "react" import { Editor as EditorWrapper, @@ -12,6 +12,16 @@ import { ROLE_COLOR_CLASSES, DEFAULT_ROLE_COLOR_CLASS, } from "@agenta/ui/cell-renderers" +import {Button} from "antd" + +import LargeValuePreview from "./LargeValuePreview" +import { + getPreviewItems, + getRenderStats, + previewValueString, + shouldUsePreview, + type RenderBudgetMode, +} from "./renderBudget" /** * "Beautified JSON" view. @@ -233,6 +243,39 @@ const getMessageText = (content: unknown): string => { } } +const formatToolCall = (toolCall: unknown): string => { + const rec = + toolCall && typeof toolCall === "object" ? (toolCall as Record) : {} + const fn = + rec.function && typeof rec.function === "object" + ? (rec.function as Record) + : undefined + const name = + (typeof fn?.name === "string" && fn.name) || + (typeof rec.name === "string" && rec.name) || + "tool" + const args = fn?.arguments ?? rec.arguments ?? rec.args ?? rec.input + + if (args === undefined || args === null || args === "") { + return `${name}()` + } + + if (typeof args === "string") { + return `${name}(${previewValueString(args)})` + } + + const argStats = getRenderStats(args) + if (shouldUsePreview(argStats, "beautified-json")) { + return `${name}(${previewValueString(args)})` + } + + try { + return `${name}(${JSON.stringify(args, null, 2)})` + } catch { + return `${name}(...)` + } +} + const RenderedChatMessages = memo(function RenderedChatMessages({ messages, keyPrefix, @@ -248,6 +291,7 @@ const RenderedChatMessages = memo(function RenderedChatMessages({ const roleColor = ROLE_COLOR_CLASSES[msg.role.toLowerCase()] ?? DEFAULT_ROLE_COLOR_CLASS const text = getMessageText(msg.content) + const toolCalls = Array.isArray(msg.tool_calls) ? msg.tool_calls : [] const editorId = `${keyPrefix}-msg-${i}` return ( @@ -255,24 +299,38 @@ const RenderedChatMessages = memo(function RenderedChatMessages({ {msg.role} - - - - + className={EDITOR_RESET_CLASSES} + > + + + + ) : null} + {toolCalls.length > 0 ? ( +
+ {toolCalls.map((toolCall, toolCallIndex) => ( +
+                                        {formatToolCall(toolCall)}
+                                    
+ ))} +
+ ) : null} ) })} @@ -339,6 +397,10 @@ const RenderedValueBlock = memo(function RenderedValueBlock({ maxDepth?: number }) { const value = useMemo(() => simplifyValue(rawValue), [rawValue]) + const [renderFull, setRenderFull] = useState(false) + const stats = useMemo(() => getRenderStats(value), [value]) + const shouldPreviewValue = + !renderFull && shouldUsePreview(stats, "beautified-json" as RenderBudgetMode) const chatMessages = useMemo(() => extractChatMessages(value), [value]) @@ -351,6 +413,18 @@ const RenderedValueBlock = memo(function RenderedValueBlock({ } if (typeof value === "string") { + if (shouldPreviewValue) { + return ( + setRenderFull(true)} + /> + ) + } + return ( — } + const previewItems = shouldPreviewValue ? getPreviewItems(value) : null + if ( depth < maxDepth && Array.isArray(value) && value.length > 0 && value.some((item) => item && typeof item === "object") ) { + const items = previewItems?.kind === "array" ? (previewItems.items as unknown[]) : value + return (
- {value.map((item, i) => { + {items.map((item, i) => { const simplified = simplifyValue(item) if ( typeof simplified === "string" || @@ -424,12 +502,20 @@ const RenderedValueBlock = memo(function RenderedValueBlock({ /> ) })} + {previewItems?.kind === "array" && previewItems.hiddenCount > 0 ? ( + + ) : null}
) } if (value && typeof value === "object" && !Array.isArray(value)) { - const entries = Object.entries(value as Record) + const entries = + previewItems?.kind === "object" + ? (previewItems.items as [string, unknown][]) + : Object.entries(value as Record) return (
{entries.map(([k, v]) => { @@ -490,10 +576,27 @@ const RenderedValueBlock = memo(function RenderedValueBlock({ /> ) })} + {previewItems?.kind === "object" && previewItems.hiddenCount > 0 ? ( + + ) : null}
) } + if (shouldPreviewValue) { + return ( + setRenderFull(true)} + /> + ) + } + return ( void + compact?: boolean +} + +export default function LargeValuePreview({ + value, + mode, + stats: statsProp, + title = "Large value preview", + onRenderFull, + compact = false, +}: LargeValuePreviewProps) { + const [isCopying, setIsCopying] = useState(false) + const stats = statsProp ?? getRenderStats(value) + const preview = useMemo(() => previewValueString(value), [value]) + + const handleCopyFull = async () => { + setIsCopying(true) + try { + await copyToClipboard(stringifyFullValue(value)) + } finally { + setIsCopying(false) + } + } + + const detailParts = [ + stats.type, + formatRenderSize(stats.estimatedChars), + stats.arrayLength !== undefined ? `${stats.arrayLength} items` : null, + stats.objectKeyCount !== undefined ? `${stats.objectKeyCount} keys` : null, + `mode: ${mode}`, + ].filter(Boolean) + + return ( +
+
+
+ {title} + + {detailParts.join(" · ")} + +
+
+ + {onRenderFull ? ( + + ) : null} +
+
+
+                {preview}
+            
+
+ ) +} diff --git a/web/oss/src/components/DrillInView/TraceSpanDrillInView.tsx b/web/oss/src/components/DrillInView/TraceSpanDrillInView.tsx index 66f37edc6f..94e7cb38a0 100644 --- a/web/oss/src/components/DrillInView/TraceSpanDrillInView.tsx +++ b/web/oss/src/components/DrillInView/TraceSpanDrillInView.tsx @@ -47,6 +47,13 @@ import { } from "./decodedJsonHelpers" import type {DrillInContentProps} from "./DrillInContent" import {EntityDrillInView} from "./EntityDrillInView" +import LargeValuePreview from "./LargeValuePreview" +import { + getRenderStats, + previewValueString, + shouldUsePreview, + type RenderBudgetMode, +} from "./renderBudget" import {getDefaultJsonViewMode} from "./viewModes" const ImagePreview = dynamic(() => import("@agenta/ui").then((mod) => mod.ImagePreview), { ssr: false, @@ -309,6 +316,7 @@ export const TraceSpanDrillInView = memo( const [searchTerm, setSearchTerm] = useState("") const [currentResultIndex, setCurrentResultIndex] = useState(0) const [resultCount, setResultCount] = useState(0) + const [renderFullModes, setRenderFullModes] = useState>({}) const isStringValue = typeof sanitizedSpanData === "string" const isObjectOrArrayValue = @@ -318,39 +326,6 @@ export const TraceSpanDrillInView = memo( [isStringValue, sanitizedSpanData], ) - const jsonOutput = useMemo( - () => - isStringValue - ? parsedStructuredString !== null - ? sanitizedSpanData - : (JSON.stringify(sanitizedSpanData) ?? "") - : getStringOrJson(sanitizedSpanData), - [isStringValue, parsedStructuredString, sanitizedSpanData], - ) - const yamlOutput = useMemo(() => { - const yamlSource = isStringValue ? parsedStructuredString : sanitizedSpanData - if (yamlSource === null || yamlSource === undefined) return "" - try { - return yaml.dump(yamlSource, {lineWidth: 120}) - } catch { - return "" - } - }, [isStringValue, parsedStructuredString, sanitizedSpanData]) - - const textOutput = useMemo(() => { - if (typeof sanitizedSpanData === "string") { - return parsedStructuredString !== null - ? normalizeEscapedLineBreaks(sanitizedSpanData) - : sanitizedSpanData - } - return getStringOrJson(sanitizedSpanData) - }, [parsedStructuredString, sanitizedSpanData]) - - const decodedJsonOutput = useMemo( - () => buildDecodedJsonOutput(sanitizedSpanData, parsedStructuredString), - [sanitizedSpanData, parsedStructuredString], - ) - const beautifiedJsonSource = useMemo(() => { if (isStringValue) return parsedStructuredString ?? sanitizedSpanData return sanitizedSpanData @@ -391,17 +366,63 @@ export const TraceSpanDrillInView = memo( const isCodeMode = viewMode === "json" || viewMode === "yaml" || viewMode === "decoded-json" const isBeautifiedJson = viewMode === "beautified-json" + const activeStatsSource = + viewMode === "json" || viewMode === "text" || viewMode === "markdown" + ? sanitizedSpanData + : beautifiedJsonSource + const activeRenderStats = useMemo( + () => getRenderStats(activeStatsSource), + [activeStatsSource], + ) + const shouldPreviewActiveMode = + !renderFullModes[viewMode] && + shouldUsePreview(activeRenderStats, viewMode as RenderBudgetMode) + + const activeOutput = useMemo(() => { + if (shouldPreviewActiveMode) return previewValueString(activeStatsSource) + + if (viewMode === "yaml") { + const yamlSource = isStringValue ? parsedStructuredString : sanitizedSpanData + if (yamlSource === null || yamlSource === undefined) return "" + try { + return yaml.dump(yamlSource, {lineWidth: 120}) + } catch { + return "" + } + } + + if (viewMode === "json") { + return isStringValue + ? parsedStructuredString !== null + ? sanitizedSpanData + : (JSON.stringify(sanitizedSpanData) ?? "") + : getStringOrJson(sanitizedSpanData) + } - const activeOutput = - viewMode === "yaml" - ? yamlOutput - : viewMode === "json" - ? jsonOutput - : viewMode === "decoded-json" - ? decodedJsonOutput - : viewMode === "beautified-json" - ? JSON.stringify(beautifiedJsonSource, null, 2) - : textOutput + if (viewMode === "decoded-json") { + return buildDecodedJsonOutput(sanitizedSpanData, parsedStructuredString) + } + + if (viewMode === "beautified-json") { + return JSON.stringify(beautifiedJsonSource, null, 2) + } + + if (typeof sanitizedSpanData === "string") { + return parsedStructuredString !== null + ? normalizeEscapedLineBreaks(sanitizedSpanData) + : sanitizedSpanData + } + + return getStringOrJson(sanitizedSpanData) + }, [ + activeStatsSource, + beautifiedJsonSource, + isStringValue, + parsedStructuredString, + sanitizedSpanData, + shouldPreviewActiveMode, + viewMode, + ]) const closeSearch = useCallback(() => { setIsSearchOpen(false) @@ -440,10 +461,14 @@ export const TraceSpanDrillInView = memo( }, [activeOutput, closeSearch]) useEffect(() => { - if (!isCodeMode) { + if (!isCodeMode || shouldPreviewActiveMode) { closeSearch() } - }, [isCodeMode, closeSearch]) + }, [isCodeMode, shouldPreviewActiveMode, closeSearch]) + + useEffect(() => { + setRenderFullModes({}) + }, [sanitizedSpanData]) const downloadFile = useCallback((url: string) => { const link = document.createElement("a") @@ -478,7 +503,7 @@ export const TraceSpanDrillInView = memo( className={`${isSearchOpen ? "!bg-[#17324D] !border-[#17324D]" : "text-gray-500"} !px-1 !h-6 text-xs`} icon={} onClick={() => setIsSearchOpen((prev) => !prev)} - disabled={!isCodeMode} + disabled={!isCodeMode || shouldPreviewActiveMode} />