|
1 | 1 | "use client"; |
2 | 2 |
|
3 | 3 | import React from "react"; |
| 4 | +import { |
| 5 | + Card, |
| 6 | + CardContent, |
| 7 | + CardDescription, |
| 8 | + CardHeader, |
| 9 | + CardTitle, |
| 10 | +} from "./_components/card"; |
| 11 | +import { Badge } from "./_components/badge"; |
4 | 12 |
|
5 | | -// Narrow union to match what the runtime + page actually emit. The v2 |
6 | | -// `DefaultRenderProps` runtime status enum is `"inProgress" | "executing" |
7 | | -// | "complete"` and the page collapses both pre-completion variants to |
8 | | -// `"executing"`. There is no `"incomplete"` state today; including it |
9 | | -// here would create a dead branch that hides the lack of UX for |
10 | | -// truly-failed tool calls (which would surface via a different |
11 | | -// mechanism, not this status enum). |
12 | | -interface CustomCatchallRendererProps { |
| 13 | +// ShadCN-styled catch-all renderer for the tool-rendering-custom-catchall |
| 14 | +// cell. A single wildcard renderer handles every tool call — name, |
| 15 | +// status, arguments, and result rendered inside a shadcn <Card />. |
| 16 | + |
| 17 | +export type CatchallToolStatus = "inProgress" | "executing" | "complete"; |
| 18 | + |
| 19 | +export interface CustomCatchallRendererProps { |
13 | 20 | name: string; |
14 | | - args: Record<string, unknown>; |
15 | | - result: unknown; |
16 | | - status: "executing" | "complete"; |
| 21 | + status: CatchallToolStatus; |
| 22 | + parameters: unknown; |
| 23 | + result: string | undefined; |
17 | 24 | } |
18 | 25 |
|
19 | | -const FALLBACK_RESULT_LABEL = "tool returned no payload"; |
20 | | - |
21 | 26 | export function CustomCatchallRenderer({ |
22 | 27 | name, |
23 | | - args, |
24 | | - result, |
25 | 28 | status, |
| 29 | + parameters, |
| 30 | + result, |
26 | 31 | }: CustomCatchallRendererProps) { |
27 | | - const formatted = |
28 | | - result === undefined || result === null |
29 | | - ? FALLBACK_RESULT_LABEL |
30 | | - : typeof result === "string" |
31 | | - ? result |
32 | | - : JSON.stringify(result, null, 2); |
| 32 | + const parsedResult = parseResult(result); |
| 33 | + const done = status === "complete"; |
33 | 34 |
|
34 | 35 | return ( |
35 | | - <div |
36 | | - data-testid="custom-catchall-card" |
37 | | - className="my-2 rounded-xl border border-[#1A73E8]/20 bg-gradient-to-br from-blue-50 via-white to-purple-50 p-4 shadow-sm" |
| 36 | + <Card |
| 37 | + data-testid="custom-wildcard-card" |
| 38 | + data-tool-name={name} |
| 39 | + data-status={status} |
| 40 | + className="my-3 overflow-hidden" |
38 | 41 | > |
39 | | - <header className="flex items-center justify-between mb-3"> |
40 | | - <span className="text-[11px] uppercase tracking-wider text-[#1A73E8] font-medium"> |
41 | | - tool · {name} |
42 | | - </span> |
43 | | - <span |
44 | | - className={`text-[11px] font-medium px-2 py-0.5 rounded-full ${ |
45 | | - status === "complete" |
46 | | - ? "bg-emerald-50 text-emerald-700 border border-emerald-200" |
47 | | - : "bg-amber-50 text-amber-700 border border-amber-200" |
48 | | - }`} |
49 | | - > |
50 | | - {status} |
51 | | - </span> |
52 | | - </header> |
53 | | - <div className="text-xs text-[#57575B] mb-1">arguments</div> |
54 | | - <pre className="text-xs text-[#010507] bg-white border border-[#E9E9EF] rounded-lg p-2.5 overflow-x-auto font-mono"> |
55 | | - {JSON.stringify(args ?? {}, null, 2)} |
56 | | - </pre> |
57 | | - {status === "complete" && ( |
58 | | - <> |
59 | | - <div className="text-xs text-[#57575B] mt-3 mb-1">result</div> |
60 | | - <pre className="text-xs text-[#010507] bg-white border border-[#E9E9EF] rounded-lg p-2.5 overflow-x-auto font-mono whitespace-pre-wrap"> |
61 | | - {formatted} |
| 42 | + <CardHeader className="flex flex-row items-center justify-between space-y-0 border-b border-neutral-200 bg-neutral-50/60 py-3"> |
| 43 | + <div className="flex items-center gap-2"> |
| 44 | + <CardTitle |
| 45 | + data-testid="custom-wildcard-tool-name" |
| 46 | + className="font-mono text-sm text-neutral-900" |
| 47 | + > |
| 48 | + {name} |
| 49 | + </CardTitle> |
| 50 | + <CardDescription className="text-[10px] uppercase tracking-wider text-neutral-500"> |
| 51 | + tool call |
| 52 | + </CardDescription> |
| 53 | + </div> |
| 54 | + <StatusBadge status={status} /> |
| 55 | + </CardHeader> |
| 56 | + |
| 57 | + <CardContent className="grid gap-3 p-4 text-sm"> |
| 58 | + <Section label="Arguments"> |
| 59 | + <pre |
| 60 | + data-testid="custom-wildcard-args" |
| 61 | + className="overflow-x-auto rounded-md border border-neutral-200 bg-neutral-50 p-2.5 font-mono text-xs text-neutral-900" |
| 62 | + > |
| 63 | + {safeStringify(parameters)} |
62 | 64 | </pre> |
63 | | - </> |
64 | | - )} |
| 65 | + </Section> |
| 66 | + |
| 67 | + <Section label="Result"> |
| 68 | + {done ? ( |
| 69 | + <pre |
| 70 | + data-testid="custom-wildcard-result" |
| 71 | + className="overflow-x-auto rounded-md border border-emerald-200 bg-emerald-50 p-2.5 font-mono text-xs text-neutral-900" |
| 72 | + > |
| 73 | + {parsedResult !== undefined |
| 74 | + ? safeStringify(parsedResult) |
| 75 | + : "(empty)"} |
| 76 | + </pre> |
| 77 | + ) : ( |
| 78 | + <p className="text-xs italic text-neutral-500"> |
| 79 | + waiting for tool to finish… |
| 80 | + </p> |
| 81 | + )} |
| 82 | + </Section> |
| 83 | + </CardContent> |
| 84 | + </Card> |
| 85 | + ); |
| 86 | +} |
| 87 | + |
| 88 | +function Section({ |
| 89 | + label, |
| 90 | + children, |
| 91 | +}: { |
| 92 | + label: string; |
| 93 | + children: React.ReactNode; |
| 94 | +}) { |
| 95 | + return ( |
| 96 | + <div> |
| 97 | + <div className="mb-1.5 text-[10px] font-medium uppercase tracking-wider text-neutral-500"> |
| 98 | + {label} |
| 99 | + </div> |
| 100 | + {children} |
65 | 101 | </div> |
66 | 102 | ); |
67 | 103 | } |
| 104 | + |
| 105 | +function StatusBadge({ status }: { status: CatchallToolStatus }) { |
| 106 | + const { label, variant, dot } = describeStatus(status); |
| 107 | + return ( |
| 108 | + <Badge data-testid="custom-wildcard-status" variant={variant}> |
| 109 | + <span |
| 110 | + className={`inline-block h-1.5 w-1.5 rounded-full ${dot}`} |
| 111 | + aria-hidden |
| 112 | + /> |
| 113 | + {label} |
| 114 | + </Badge> |
| 115 | + ); |
| 116 | +} |
| 117 | + |
| 118 | +function describeStatus(status: CatchallToolStatus): { |
| 119 | + label: string; |
| 120 | + variant: "warning" | "secondary" | "success"; |
| 121 | + dot: string; |
| 122 | +} { |
| 123 | + switch (status) { |
| 124 | + case "inProgress": |
| 125 | + return { |
| 126 | + label: "streaming", |
| 127 | + variant: "warning", |
| 128 | + dot: "bg-amber-500 animate-pulse", |
| 129 | + }; |
| 130 | + case "executing": |
| 131 | + return { |
| 132 | + label: "running", |
| 133 | + variant: "secondary", |
| 134 | + dot: "bg-neutral-500 animate-pulse", |
| 135 | + }; |
| 136 | + case "complete": |
| 137 | + return { |
| 138 | + label: "done", |
| 139 | + variant: "success", |
| 140 | + dot: "bg-emerald-500", |
| 141 | + }; |
| 142 | + } |
| 143 | +} |
| 144 | + |
| 145 | +function parseResult(result: string | undefined): unknown { |
| 146 | + if (result === undefined || result === null) return undefined; |
| 147 | + if (typeof result !== "string") return result; |
| 148 | + try { |
| 149 | + return JSON.parse(result); |
| 150 | + } catch { |
| 151 | + return result; |
| 152 | + } |
| 153 | +} |
| 154 | + |
| 155 | +function safeStringify(value: unknown): string { |
| 156 | + try { |
| 157 | + return JSON.stringify(value, null, 2); |
| 158 | + } catch { |
| 159 | + return String(value); |
| 160 | + } |
| 161 | +} |
0 commit comments