From f6e8b11e4a2fba8e6635716fac576b6a3eabfb18 Mon Sep 17 00:00:00 2001 From: Declan Brady Date: Thu, 30 Oct 2025 14:49:02 -0400 Subject: [PATCH 1/4] Use JSONViewer for all json viewing except the prompt input --- agentex-ui/components/agentex/json-viewer.tsx | 38 ++++--- .../agentex/task-message-data-content.tsx | 14 +-- .../agentex/task-message-tool-pair.tsx | 93 +++++++++------ .../components/agentex/task-messages.tsx | 19 +--- .../components/agentex/traces-sidebar.tsx | 2 +- agentex-ui/components/ai-elements/tool.tsx | 107 ------------------ 6 files changed, 91 insertions(+), 182 deletions(-) delete mode 100644 agentex-ui/components/ai-elements/tool.tsx diff --git a/agentex-ui/components/agentex/json-viewer.tsx b/agentex-ui/components/agentex/json-viewer.tsx index d48927f7..70021fd5 100644 --- a/agentex-ui/components/agentex/json-viewer.tsx +++ b/agentex-ui/components/agentex/json-viewer.tsx @@ -8,7 +8,7 @@ import { ChevronDown, ChevronRight } from 'lucide-react'; import { CopyButton } from '@/components/agentex/copy-button'; import { cn } from '@/lib/utils'; -type JsonValue = +export type JsonValue = | string | number | boolean @@ -43,7 +43,7 @@ interface JsonCollapsibleProps extends React.HTMLAttributes { copyContent: string; collapsedContent: React.ReactNode; expandedContent: React.ReactNode; - defaultExpanded?: boolean; + shouldBeExpanded?: boolean; keyName?: string | undefined; } @@ -51,15 +51,15 @@ function JsonCollapsible({ copyContent, collapsedContent, expandedContent, - defaultExpanded = false, + shouldBeExpanded = false, keyName, ...props }: JsonCollapsibleProps) { - const [isExpanded, setIsExpanded] = useState(defaultExpanded); + const [isExpanded, setIsExpanded] = useState(shouldBeExpanded); return (
-
+
); diff --git a/agentex-ui/components/ai-elements/tool.tsx b/agentex-ui/components/ai-elements/tool.tsx deleted file mode 100644 index c4a1ffa6..00000000 --- a/agentex-ui/components/ai-elements/tool.tsx +++ /dev/null @@ -1,107 +0,0 @@ -'use client'; - -import type { ComponentProps, ReactNode } from 'react'; -import { isValidElement } from 'react'; - -import { - CheckCircleIcon, - CircleIcon, - ClockIcon, - XCircleIcon, -} from 'lucide-react'; - -import { CodeBlock } from '@/components/ai-elements/code-block'; -import { Badge } from '@/components/ui/badge'; -import { cn } from '@/lib/utils'; - -import type { ToolUIPart } from 'ai'; - -export type ToolHeaderProps = { - title?: string; - type: ToolUIPart['type']; - state: ToolUIPart['state']; - className?: string; -}; - -export const getStatusBadge = (status: ToolUIPart['state']) => { - const labels = { - 'input-streaming': 'Pending', - 'input-available': 'Running', - 'output-available': 'Completed', - 'output-error': 'Error', - } as const; - - const icons = { - 'input-streaming': , - 'input-available': , - 'output-available': , - 'output-error': , - } as const; - - return ( - - {icons[status]} - {labels[status]} - - ); -}; - -export type ToolInputProps = ComponentProps<'div'> & { - input: ToolUIPart['input']; -}; - -export const ToolInput = ({ className, input, ...props }: ToolInputProps) => ( -
-

- Parameters -

-
- -
-
-); - -export type ToolOutputProps = ComponentProps<'div'> & { - output: ToolUIPart['output']; - errorText: ToolUIPart['errorText']; -}; - -export const ToolOutput = ({ - className, - output, - errorText, - ...props -}: ToolOutputProps) => { - if (!(output || errorText)) { - return null; - } - - let Output =
{output as ReactNode}
; - - if (typeof output === 'object' && !isValidElement(output)) { - Output = ( - - ); - } else if (typeof output === 'string') { - Output = ; - } - - return ( -
-

- {errorText ? 'Error' : 'Result'} -

-
- {errorText &&
{errorText}
} - {Output} -
-
- ); -}; From b5eed58c15914b2a3d2267b026f419961c6a37d6 Mon Sep 17 00:00:00 2001 From: Declan Brady Date: Thu, 30 Oct 2025 15:40:45 -0400 Subject: [PATCH 2/4] Add collapse and expand buttons to jsonviewer --- agentex-ui/components/agentex/json-viewer.tsx | 107 +++++++++++++++--- 1 file changed, 90 insertions(+), 17 deletions(-) diff --git a/agentex-ui/components/agentex/json-viewer.tsx b/agentex-ui/components/agentex/json-viewer.tsx index 70021fd5..a0397438 100644 --- a/agentex-ui/components/agentex/json-viewer.tsx +++ b/agentex-ui/components/agentex/json-viewer.tsx @@ -1,11 +1,17 @@ 'use client'; -import { useState } from 'react'; +import { useMemo, useEffect, useState, useCallback } from 'react'; import { cva } from 'class-variance-authority'; -import { ChevronDown, ChevronRight } from 'lucide-react'; +import { + ChevronDown, + ChevronRight, + ChevronsDownUp, + ChevronsUpDown, +} from 'lucide-react'; import { CopyButton } from '@/components/agentex/copy-button'; +import { Button } from '@/components/ui/button'; import { cn } from '@/lib/utils'; export type JsonValue = @@ -44,7 +50,10 @@ interface JsonCollapsibleProps extends React.HTMLAttributes { collapsedContent: React.ReactNode; expandedContent: React.ReactNode; shouldBeExpanded?: boolean; + forceExpandState?: boolean | null; keyName?: string | undefined; + extraButtons?: React.ReactNode; + showCopyButton?: boolean; } function JsonCollapsible({ @@ -52,10 +61,21 @@ function JsonCollapsible({ collapsedContent, expandedContent, shouldBeExpanded = false, + forceExpandState = null, keyName, + extraButtons, + showCopyButton = true, ...props }: JsonCollapsibleProps) { - const [isExpanded, setIsExpanded] = useState(shouldBeExpanded); + const [isExpanded, setIsExpanded] = useState( + forceExpandState !== null ? forceExpandState : shouldBeExpanded + ); + + useEffect(() => { + if (forceExpandState !== null) { + setIsExpanded(forceExpandState); + } + }, [forceExpandState]); return (
@@ -76,8 +96,13 @@ function JsonCollapsible({ )} {collapsedContent} -
- +
+ {extraButtons} + {showCopyButton && ( +
+ +
+ )}
{isExpanded &&
{expandedContent}
} @@ -91,6 +116,8 @@ interface JsonNodeProps { level?: number; currentDepth?: number; maxOpenDepth?: number; + forceExpandState?: boolean | null; + extraButtons?: React.ReactNode; } function JsonNode({ @@ -99,8 +126,9 @@ function JsonNode({ level = 0, currentDepth = 0, maxOpenDepth = 0, + forceExpandState = null, + extraButtons, }: JsonNodeProps) { - // Try to parse JSON strings let parsedData = data; if (typeof data === 'string') { try { @@ -108,9 +136,7 @@ function JsonNode({ if (typeof parsed === 'object' && parsed !== null) { parsedData = parsed; } - } catch { - // Not valid JSON, keep as string - } + } catch {} } const copyContent = keyName @@ -119,9 +145,6 @@ function JsonNode({ const indentClassName = level > 0 ? 'ml-4' : ''; const [isExpanded, setIsExpanded] = useState(false); - - // Calculate if this node should be expanded based on depth - // maxOpenDepth < 0 means expand all (treat as infinity) const shouldExpand = maxOpenDepth < 0 || currentDepth < maxOpenDepth; let content = null; @@ -134,7 +157,6 @@ function JsonNode({ | 'array' | null = null; - // Render arrays if (Array.isArray(parsedData) && parsedData.length > 0) { return ( ))} shouldBeExpanded={shouldExpand} + forceExpandState={forceExpandState} + extraButtons={extraButtons} + showCopyButton={!extraButtons} className={indentClassName} /> ); } - // Render objects if ( typeof parsedData === 'object' && parsedData !== null && @@ -183,15 +208,18 @@ function JsonNode({ level={level + 1} currentDepth={currentDepth + 1} maxOpenDepth={maxOpenDepth} + forceExpandState={forceExpandState} /> ))} shouldBeExpanded={shouldExpand} + forceExpandState={forceExpandState} + extraButtons={extraButtons} + showCopyButton={!extraButtons} className={indentClassName} /> ); } - // Check if string is long (more than 6 lines worth of characters, ~80 chars per line) const isLongString = typeof parsedData === 'string' && parsedData.length > 480; @@ -239,7 +267,6 @@ function JsonNode({ )} onClick={e => { if (isLongString) { - // Only toggle if not clicking the copy button const target = e.target as HTMLElement; if (!target.closest('button')) { setIsExpanded(!isExpanded); @@ -276,6 +303,46 @@ export function JsonViewer({ defaultOpenDepth = 0, className, }: JsonViewerProps) { + const [forceExpandState, setForceExpandState] = useState( + null + ); + + const shouldShowExpand = useMemo(() => { + return forceExpandState === null + ? defaultOpenDepth === 0 + : !forceExpandState; + }, [forceExpandState, defaultOpenDepth]); + + const toggleForceExpandState = useCallback(() => { + setForceExpandState(shouldShowExpand); + }, [shouldShowExpand]); + + const extraButtons = useMemo(() => { + return ( + <> + + + + ); + }, [data, shouldShowExpand, toggleForceExpandState]); + return (
- +
); } From 4cb292249cad1162b4f943f7b8da3b6246baa46e Mon Sep 17 00:00:00 2001 From: Declan Brady Date: Thu, 30 Oct 2025 16:06:30 -0400 Subject: [PATCH 3/4] Add links to jsonviewer --- agentex-ui/components/agentex/json-viewer.tsx | 53 ++++++++++++++++++- 1 file changed, 51 insertions(+), 2 deletions(-) diff --git a/agentex-ui/components/agentex/json-viewer.tsx b/agentex-ui/components/agentex/json-viewer.tsx index a0397438..5caf3321 100644 --- a/agentex-ui/components/agentex/json-viewer.tsx +++ b/agentex-ui/components/agentex/json-viewer.tsx @@ -22,6 +22,9 @@ export type JsonValue = | JsonValue[] | { [key: string]: JsonValue }; +const URL_REGEX = + /https?:\/\/(www\.)?[-a-zA-Z0-9@:%._+~#=]{1,256}\.[a-zA-Z0-9()]{1,6}\b([-a-zA-Z0-9()@:%_+.~#?&//=]*)/g; + const valueStyles = cva('', { variants: { type: { @@ -45,6 +48,46 @@ function serializeValue(data: JsonValue): string { return String(data); } +function LinkifiedString({ value }: { value: string }) { + const parts: (string | React.ReactElement)[] = []; + let lastIndex = 0; + let match: RegExpExecArray | null; + + const regex = new RegExp(URL_REGEX); + + while ((match = regex.exec(value)) !== null) { + if (match.index > lastIndex) { + parts.push(value.substring(lastIndex, match.index)); + } + + const url = match[0]; + parts.push( + e.stopPropagation()} + > + {url} + + ); + + lastIndex = regex.lastIndex; + } + + if (lastIndex < value.length) { + parts.push(value.substring(lastIndex)); + } + + if (parts.length === 0) { + return <>{value}; + } + + return <>{parts}; +} + interface JsonCollapsibleProps extends React.HTMLAttributes { copyContent: string; collapsedContent: React.ReactNode; @@ -147,7 +190,7 @@ function JsonNode({ const [isExpanded, setIsExpanded] = useState(false); const shouldExpand = maxOpenDepth < 0 || currentDepth < maxOpenDepth; - let content = null; + let content: React.ReactNode = null; let dataType: | 'string' | 'number' @@ -226,7 +269,13 @@ function JsonNode({ switch (typeof parsedData) { case 'string': dataType = 'string'; - content = `"${parsedData}"`; + content = ( + <> + " + + " + + ); break; case 'number': dataType = 'number'; From a20401eca2d921b3fa902686747fbdc2ddd678cc Mon Sep 17 00:00:00 2001 From: Declan Brady Date: Fri, 31 Oct 2025 13:34:53 -0400 Subject: [PATCH 4/4] Fix typose, render link with same color as text --- agentex-ui/components/agentex/json-viewer.tsx | 13 ++++++------- .../agentex/task-message-tool-pair.tsx | 16 ++++++++-------- 2 files changed, 14 insertions(+), 15 deletions(-) diff --git a/agentex-ui/components/agentex/json-viewer.tsx b/agentex-ui/components/agentex/json-viewer.tsx index 5caf3321..7cc65937 100644 --- a/agentex-ui/components/agentex/json-viewer.tsx +++ b/agentex-ui/components/agentex/json-viewer.tsx @@ -51,12 +51,11 @@ function serializeValue(data: JsonValue): string { function LinkifiedString({ value }: { value: string }) { const parts: (string | React.ReactElement)[] = []; let lastIndex = 0; - let match: RegExpExecArray | null; - const regex = new RegExp(URL_REGEX); + const matches = value.matchAll(URL_REGEX); - while ((match = regex.exec(value)) !== null) { - if (match.index > lastIndex) { + for (const match of matches) { + if (match.index !== undefined && match.index > lastIndex) { parts.push(value.substring(lastIndex, match.index)); } @@ -67,14 +66,14 @@ function LinkifiedString({ value }: { value: string }) { href={url} target="_blank" rel="noopener noreferrer" - className="text-blue-600 underline hover:text-blue-800 dark:text-blue-400 dark:hover:text-blue-300" + className="underline" onClick={e => e.stopPropagation()} > {url} ); - lastIndex = regex.lastIndex; + lastIndex = (match.index ?? 0) + url.length; } if (lastIndex < value.length) { @@ -110,7 +109,7 @@ function JsonCollapsible({ showCopyButton = true, ...props }: JsonCollapsibleProps) { - const [isExpanded, setIsExpanded] = useState( + const [isExpanded, setIsExpanded] = useState(() => forceExpandState !== null ? forceExpandState : shouldBeExpanded ); diff --git a/agentex-ui/components/agentex/task-message-tool-pair.tsx b/agentex-ui/components/agentex/task-message-tool-pair.tsx index 1557a18a..dc714ff6 100644 --- a/agentex-ui/components/agentex/task-message-tool-pair.tsx +++ b/agentex-ui/components/agentex/task-message-tool-pair.tsx @@ -52,7 +52,7 @@ function TaskMessageToolPairComponentImpl({ }: TaskMessageToolPairProps) { const [isCollapsed, setIsCollapsed] = useState(true); - const reponseObject = useMemo(() => { + const responseObject = useMemo(() => { const content = toolResponseMessage?.content.content; if (typeof content === 'string') { try { @@ -66,12 +66,12 @@ function TaskMessageToolPairComponentImpl({ const isError = useMemo( () => - typeof reponseObject === 'object' && - reponseObject !== null && - 'status' in reponseObject && - typeof reponseObject.status === 'string' && - reponseObject.status.toLowerCase() === 'error', - [reponseObject] + typeof responseObject === 'object' && + responseObject !== null && + 'status' in responseObject && + typeof responseObject.status === 'string' && + responseObject.status.toLowerCase() === 'error', + [responseObject] ); return ( @@ -114,7 +114,7 @@ function TaskMessageToolPairComponentImpl({ />