diff --git a/gadget-code/frontend/src/components/ChatTurn.tsx b/gadget-code/frontend/src/components/ChatTurn.tsx new file mode 100644 index 0000000..f4022d0 --- /dev/null +++ b/gadget-code/frontend/src/components/ChatTurn.tsx @@ -0,0 +1,183 @@ +import { useState, memo, useCallback } from "react"; +import type { ChatTurn as ChatTurnType } from "../lib/api"; + +interface ChatTurnProps { + turn: ChatTurnType; + onUpdate: (turnId: string, updates: Partial) => void; +} + +const ChatTurn = memo(function ChatTurn({ turn, onUpdate }: ChatTurnProps) { + const [thinkingExpanded, setThinkingExpanded] = useState(false); + const [toolCallsExpanded, setToolCallsExpanded] = useState(true); + + const handleThinkingToggle = useCallback(() => { + setThinkingExpanded((prev) => !prev); + }, []); + + const handleToolCallsToggle = useCallback(() => { + setToolCallsExpanded((prev) => !prev); + }, []); + + const formatDuration = (ms: number): string => { + if (ms < 1000) return `${ms}ms`; + const seconds = (ms / 1000).toFixed(1); + return `${seconds}s`; + }; + + const formatTokenCount = (count: number): string => { + if (count >= 1000) { + return `${(count / 1000).toFixed(1)}k`; + } + return count.toString(); + }; + + const modelId = turn.llm || "Unknown"; + const modeLabel = + turn.mode?.charAt(0).toUpperCase() + turn.mode?.slice(1) || "Unknown"; + const statusLabel = + turn.status?.charAt(0).toUpperCase() + turn.status?.slice(1) || "Unknown"; + const startedAt = new Date(turn.createdAt); + + return ( +
+ {/* Turn Header */} +
+
{startedAt.toLocaleTimeString()}
+
+ {modelId} +
+
+
{modeLabel}
+
+
+ {statusLabel} +
+
+ {turn.stats && ( + <> +
+
{formatDuration(turn.stats.durationMs)}
+
+
+ {turn.stats.inputTokens + ? formatTokenCount(turn.stats.inputTokens) + : "0"}{" "} + in /{" "} + {turn.stats.responseTokens + ? formatTokenCount(turn.stats.responseTokens) + : "0"}{" "} + out +
+ + )} +
+ + {/* User Prompt */} +
+
+
You
+
{turn.prompts?.user || ""}
+
+
+ + {/* Agent Response */} + {(turn.thinking || turn.response || (turn.toolCalls && turn.toolCalls.length > 0)) && ( +
+
+
+ Gadget +
+ + {/* Thinking Section */} + {turn.thinking && ( +
+ + {thinkingExpanded && ( +
+ {turn.thinking} +
+ )} +
+ )} + + {/* Response Section */} + {turn.response && ( +
+ {turn.response} +
+ )} + + {/* Tool Calls Section */} + {turn.toolCalls && turn.toolCalls.length > 0 && ( +
+ + {toolCallsExpanded && ( +
+ {turn.toolCalls.map((toolCall, idx) => ( +
+
+ + {toolCall.name} + {toolCall.response && ( + + )} +
+ {toolCall.parameters && ( +
+ {toolCall.parameters} +
+ )} + {toolCall.response && ( +
+ {toolCall.response} +
+ )} +
+ ))} +
+ )} +
+ )} + +
+ {startedAt.toLocaleTimeString()} +
+
+
+ )} +
+ ); +}); + +export default ChatTurn;