-
-
+
+ {extraButtons}
+ {showCopyButton && (
+
+
+
+ )}
{isExpanded &&
{expandedContent}
}
@@ -89,16 +156,21 @@ interface JsonNodeProps {
data: JsonValue;
keyName?: string;
level?: number;
- defaultExpanded?: boolean;
+ currentDepth?: number;
+ maxOpenDepth?: number;
+ forceExpandState?: boolean | null;
+ extraButtons?: React.ReactNode;
}
function JsonNode({
data,
keyName,
level = 0,
- defaultExpanded = false,
+ currentDepth = 0,
+ maxOpenDepth = 0,
+ forceExpandState = null,
+ extraButtons,
}: JsonNodeProps) {
- // Try to parse JSON strings
let parsedData = data;
if (typeof data === 'string') {
try {
@@ -106,9 +178,7 @@ function JsonNode({
if (typeof parsed === 'object' && parsed !== null) {
parsedData = parsed;
}
- } catch {
- // Not valid JSON, keep as string
- }
+ } catch {}
}
const copyContent = keyName
@@ -117,8 +187,9 @@ function JsonNode({
const indentClassName = level > 0 ? 'ml-4' : '';
const [isExpanded, setIsExpanded] = useState(false);
+ const shouldExpand = maxOpenDepth < 0 || currentDepth < maxOpenDepth;
- let content = null;
+ let content: React.ReactNode = null;
let dataType:
| 'string'
| 'number'
@@ -128,7 +199,6 @@ function JsonNode({
| 'array'
| null = null;
- // Render arrays
if (Array.isArray(parsedData) && parsedData.length > 0) {
return (
))}
- defaultExpanded={defaultExpanded}
+ shouldBeExpanded={shouldExpand}
+ forceExpandState={forceExpandState}
+ extraButtons={extraButtons}
+ showCopyButton={!extraButtons}
className={indentClassName}
/>
);
}
- // Render objects
if (
typeof parsedData === 'object' &&
parsedData !== null &&
@@ -174,23 +248,33 @@ function JsonNode({
data={value}
keyName={key}
level={level + 1}
- defaultExpanded={defaultExpanded}
+ currentDepth={currentDepth + 1}
+ maxOpenDepth={maxOpenDepth}
+ forceExpandState={forceExpandState}
/>
))}
- defaultExpanded={defaultExpanded}
+ 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;
switch (typeof parsedData) {
case 'string':
dataType = 'string';
- content = `"${parsedData}"`;
+ content = (
+ <>
+ "
+
+ "
+ >
+ );
break;
case 'number':
dataType = 'number';
@@ -220,7 +304,7 @@ function JsonNode({
return (
@@ -231,7 +315,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);
@@ -259,15 +342,55 @@ function JsonNode({
interface JsonViewerProps {
data: JsonValue;
- defaultExpanded?: boolean;
+ defaultOpenDepth?: number;
className?: string;
}
export function JsonViewer({
data,
- defaultExpanded = false,
+ 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 (
-
+
);
}
diff --git a/agentex-ui/components/agentex/task-message-data-content.tsx b/agentex-ui/components/agentex/task-message-data-content.tsx
index 15de3d2a..fc684ac9 100644
--- a/agentex-ui/components/agentex/task-message-data-content.tsx
+++ b/agentex-ui/components/agentex/task-message-data-content.tsx
@@ -1,8 +1,6 @@
-import { useMemo } from 'react';
-
import { cva } from 'class-variance-authority';
-import { CodeBlock } from '@/components/ai-elements/code-block';
+import { JsonViewer, type JsonValue } from '@/components/agentex/json-viewer';
import { cn } from '@/lib/utils';
import type { DataContent } from 'agentex/resources';
@@ -25,13 +23,13 @@ function TaskMessageDataContentComponent({
content,
key,
}: TaskMessageDataContentComponentProps) {
- const dataString = useMemo(
- () => JSON.stringify(content.data, null, 2),
- [content.data]
- );
return (
-
+
);
}
diff --git a/agentex-ui/components/agentex/task-message-tool-pair.tsx b/agentex-ui/components/agentex/task-message-tool-pair.tsx
index c84ee9ff..dc714ff6 100644
--- a/agentex-ui/components/agentex/task-message-tool-pair.tsx
+++ b/agentex-ui/components/agentex/task-message-tool-pair.tsx
@@ -1,65 +1,79 @@
import { memo, useMemo, useState } from 'react';
import { motion } from 'framer-motion';
-import { ChevronDownIcon, Wrench } from 'lucide-react';
+import { ChevronDownIcon, Wrench, XCircleIcon } from 'lucide-react';
-import { ToolInput, ToolOutput } from '@/components/ai-elements/tool';
+import { JsonViewer, type JsonValue } from '@/components/agentex/json-viewer';
+import { Collapsible } from '@/components/ui/collapsible';
+import { ShimmeringText } from '@/components/ui/shimmering-text';
import { cn } from '@/lib/utils';
-import { Collapsible } from '../ui/collapsible';
-import { ShimmeringText } from '../ui/shimmering-text';
-
import type {
TaskMessage,
ToolRequestContent,
ToolResponseContent,
} from 'agentex/resources';
-export type TaskMessageToolPairProps = {
+type TaskMessageToolHeaderAndJsonProps = {
+ title: string;
+ data: JsonValue;
+};
+
+function TaskMessageToolHeaderAndJsonComponent({
+ title,
+ data,
+}: TaskMessageToolHeaderAndJsonProps) {
+ return (
+
+ );
+}
+
+type TaskMessageToolPairProps = {
toolRequestMessage: TaskMessage & { content: ToolRequestContent };
toolResponseMessage?:
| (TaskMessage & { content: ToolResponseContent })
| undefined;
};
-function TaskMessageToolPairComponent({
+function TaskMessageToolPairComponentImpl({
toolRequestMessage,
toolResponseMessage,
}: TaskMessageToolPairProps) {
const [isCollapsed, setIsCollapsed] = useState(true);
- // const state = useMemo(() => {
- // const streamingStatus = toolResponseMessage?.streaming_status;
- // if (streamingStatus === 'IN_PROGRESS') {
- // return 'input-streaming';
- // } else {
- // try {
- // const content = toolResponseMessage?.content.content;
- // if (typeof content === 'string') {
- // const parsed = JSON.parse(content);
- // if (parsed.status === 'error') {
- // return 'output-error';
- // }
- // }
- // } catch {
- // return 'output-available';
- // }
- // return 'output-available';
- // }
- // }, [toolResponseMessage]);
-
- const reponseObject = useMemo(() => {
+ const responseObject = useMemo(() => {
const content = toolResponseMessage?.content.content;
if (typeof content === 'string') {
try {
- return JSON.parse(content);
+ return JSON.parse(content) as JsonValue;
} catch {
- return content;
+ return content as JsonValue;
}
}
- return content;
+ return content as JsonValue;
}, [toolResponseMessage]);
+ const isError = useMemo(
+ () =>
+ typeof responseObject === 'object' &&
+ responseObject !== null &&
+ 'status' in responseObject &&
+ typeof responseObject.status === 'string' &&
+ responseObject.status.toLowerCase() === 'error',
+ [responseObject]
+ );
+
return (
+ {isError && }
-
-
+
+
);
}
-const MemoizedTaskMessageToolPairComponent = memo(TaskMessageToolPairComponent);
+const MemoizedTaskMessageToolPairComponent = memo(
+ TaskMessageToolPairComponentImpl
+);
-export { MemoizedTaskMessageToolPairComponent, TaskMessageToolPairComponent };
+export { MemoizedTaskMessageToolPairComponent };
diff --git a/agentex-ui/components/agentex/task-messages.tsx b/agentex-ui/components/agentex/task-messages.tsx
index f7d9621a..c377dbd4 100644
--- a/agentex-ui/components/agentex/task-messages.tsx
+++ b/agentex-ui/components/agentex/task-messages.tsx
@@ -9,7 +9,7 @@ import { TaskMessageTextContentComponent } from '@/components/agentex/task-messa
import { useAgentexClient } from '@/components/providers';
import { useTaskMessages } from '@/hooks/use-task-messages';
-import { TaskMessageToolPairComponent } from './task-message-tool-pair';
+import { MemoizedTaskMessageToolPairComponent } from './task-message-tool-pair';
import { ShimmeringText } from '../ui/shimmering-text';
import type {
@@ -36,10 +36,8 @@ function MemoizedTaskMessagesComponentImpl({
const { agentexClient } = useAgentexClient();
- // Use query hook to get messages from cache if taskId is provided
const { data: queryData } = useTaskMessages({ agentexClient, taskId });
- // Prefer query data if available, otherwise use prop messages
const messages = useMemo(() => queryData?.messages ?? [], [queryData]);
const previousMessageCountRef = useRef(messages.length);
@@ -58,7 +56,6 @@ function MemoizedTaskMessagesComponentImpl({
[messages]
);
- // Group messages into pairs (user message + subsequent agent messages)
const messagePairs = useMemo(() => {
const pairs: MessagePair[] = [];
let currentUserMessage: TaskMessage | null = null;
@@ -68,7 +65,6 @@ function MemoizedTaskMessagesComponentImpl({
const isUserMessage = message.content.author === 'user';
if (isUserMessage) {
- // Save previous pair if exists
if (currentUserMessage) {
pairs.push({
id: currentUserMessage.id || `pair-${pairs.length}`,
@@ -76,25 +72,21 @@ function MemoizedTaskMessagesComponentImpl({
agentMessages: currentAgentMessages,
});
}
- // Start new pair
currentUserMessage = message;
currentAgentMessages = [];
} else {
- // Add to current agent messages
if (currentUserMessage) {
currentAgentMessages.push(message);
} else {
- // Agent message without a user message - create a pair with just agent messages
pairs.push({
id: message.id || `pair-${pairs.length}`,
- userMessage: message, // Use the agent message as the "user" message
+ userMessage: message,
agentMessages: [],
});
}
}
}
- // Add the last pair
if (currentUserMessage) {
pairs.push({
id: currentUserMessage.id || `pair-${pairs.length}`,
@@ -118,11 +110,9 @@ function MemoizedTaskMessagesComponentImpl({
);
}, [messagePairs, queryData?.rpcStatus]);
- // Measure the scrollable container height
useEffect(() => {
const measureHeight = () => {
if (containerRef.current) {
- // Walk up the DOM tree to find the scrollable container (has overflow-y-auto)
let element = containerRef.current.parentElement;
while (element) {
const overflowY = window.getComputedStyle(element).overflowY;
@@ -135,10 +125,8 @@ function MemoizedTaskMessagesComponentImpl({
}
};
- // Measure on mount and when messages change
measureHeight();
- // Recalculate on window resize
window.addEventListener('resize', measureHeight);
return () => window.removeEventListener('resize', measureHeight);
}, [messages]);
@@ -159,7 +147,6 @@ function MemoizedTaskMessagesComponentImpl({
previousMessageCountRef.current = currentCount;
}, [messagePairs.length]);
- // Helper function to render a message
const renderMessage = (message: TaskMessage) => {
switch (message.content.type) {
case 'text':
@@ -170,7 +157,7 @@ function MemoizedTaskMessagesComponentImpl({
return ;
case 'tool_request':
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) => (
-
-);
-
-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}
-
-
- );
-};