Skip to content

Commit 4353521

Browse files
tylerslatonclaude
andcommitted
feat(showcase/google-adk/tool-rendering-custom-catchall): port frontend from langgraph-python
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
1 parent 184507f commit 4353521

5 files changed

Lines changed: 280 additions & 75 deletions

File tree

Lines changed: 31 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,31 @@
1+
import * as React from "react";
2+
3+
// Inline-cloned shadcn/ui <Badge />. Plain Tailwind classes — no `cn()`,
4+
// no `cva`. Local to this demo only.
5+
6+
type Variant = "default" | "secondary" | "outline" | "success" | "warning";
7+
8+
const variantClasses: Record<Variant, string> = {
9+
default: "border-transparent bg-neutral-900 text-neutral-50",
10+
secondary: "border-transparent bg-neutral-100 text-neutral-700",
11+
outline: "border-neutral-200 text-neutral-900",
12+
success: "border-transparent bg-emerald-100 text-emerald-700",
13+
warning: "border-transparent bg-amber-100 text-amber-700",
14+
};
15+
16+
export interface BadgeProps extends React.HTMLAttributes<HTMLDivElement> {
17+
variant?: Variant;
18+
}
19+
20+
export function Badge({
21+
className = "",
22+
variant = "default",
23+
...props
24+
}: BadgeProps) {
25+
return (
26+
<div
27+
className={`inline-flex items-center gap-1 rounded-full border px-2 py-0.5 text-[10px] font-medium uppercase tracking-wider transition-colors ${variantClasses[variant]} ${className}`}
28+
{...props}
29+
/>
30+
);
31+
}
Lines changed: 57 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,57 @@
1+
import * as React from "react";
2+
3+
// Inline-cloned shadcn/ui <Card /> primitives. Hand-written with plain
4+
// Tailwind classes — no `cn()` helper, no `cva`, no Radix primitives.
5+
// Local to this demo only.
6+
7+
export const Card = React.forwardRef<
8+
HTMLDivElement,
9+
React.HTMLAttributes<HTMLDivElement>
10+
>(({ className = "", ...props }, ref) => (
11+
<div
12+
ref={ref}
13+
className={`rounded-xl border border-neutral-200 bg-white text-neutral-950 shadow-sm ${className}`}
14+
{...props}
15+
/>
16+
));
17+
Card.displayName = "Card";
18+
19+
export const CardHeader = React.forwardRef<
20+
HTMLDivElement,
21+
React.HTMLAttributes<HTMLDivElement>
22+
>(({ className = "", ...props }, ref) => (
23+
<div
24+
ref={ref}
25+
className={`flex flex-col space-y-1.5 p-4 ${className}`}
26+
{...props}
27+
/>
28+
));
29+
CardHeader.displayName = "CardHeader";
30+
31+
export const CardTitle = React.forwardRef<
32+
HTMLHeadingElement,
33+
React.HTMLAttributes<HTMLHeadingElement>
34+
>(({ className = "", ...props }, ref) => (
35+
<h3
36+
ref={ref}
37+
className={`text-sm font-semibold leading-none tracking-tight text-neutral-900 ${className}`}
38+
{...props}
39+
/>
40+
));
41+
CardTitle.displayName = "CardTitle";
42+
43+
export const CardDescription = React.forwardRef<
44+
HTMLParagraphElement,
45+
React.HTMLAttributes<HTMLParagraphElement>
46+
>(({ className = "", ...props }, ref) => (
47+
<p ref={ref} className={`text-xs text-neutral-500 ${className}`} {...props} />
48+
));
49+
CardDescription.displayName = "CardDescription";
50+
51+
export const CardContent = React.forwardRef<
52+
HTMLDivElement,
53+
React.HTMLAttributes<HTMLDivElement>
54+
>(({ className = "", ...props }, ref) => (
55+
<div ref={ref} className={`p-4 pt-0 ${className}`} {...props} />
56+
));
57+
CardContent.displayName = "CardContent";
Lines changed: 143 additions & 49 deletions
Original file line numberDiff line numberDiff line change
@@ -1,67 +1,161 @@
11
"use client";
22

33
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";
412

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 {
1320
name: string;
14-
args: Record<string, unknown>;
15-
result: unknown;
16-
status: "executing" | "complete";
21+
status: CatchallToolStatus;
22+
parameters: unknown;
23+
result: string | undefined;
1724
}
1825

19-
const FALLBACK_RESULT_LABEL = "tool returned no payload";
20-
2126
export function CustomCatchallRenderer({
2227
name,
23-
args,
24-
result,
2528
status,
29+
parameters,
30+
result,
2631
}: 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";
3334

3435
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"
3841
>
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)}
6264
</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}
65101
</div>
66102
);
67103
}
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+
}

showcase/integrations/google-adk/src/app/demos/tool-rendering-custom-catchall/page.tsx

Lines changed: 21 additions & 26 deletions
Original file line numberDiff line numberDiff line change
@@ -1,20 +1,30 @@
11
"use client";
22

3+
// Tool Rendering — CUSTOM CATCH-ALL variant (middle of the progression).
4+
//
5+
// Same backend tools as `tool-rendering-default-catchall`, but this
6+
// cell opts out of CopilotKit's built-in default tool-call UI by
7+
// registering a SINGLE custom wildcard renderer via
8+
// `useDefaultRenderTool`. The same branded card now paints every tool
9+
// call — no per-tool renderers yet.
10+
311
import React from "react";
412
import {
513
CopilotKit,
614
CopilotChat,
715
useDefaultRenderTool,
8-
useConfigureSuggestions,
916
} from "@copilotkit/react-core/v2";
10-
11-
import { CustomCatchallRenderer } from "./custom-catchall-renderer";
17+
import {
18+
CustomCatchallRenderer,
19+
type CatchallToolStatus,
20+
} from "./custom-catchall-renderer";
21+
import { useSuggestions } from "./suggestions";
1222

1323
export default function ToolRenderingCustomCatchallDemo() {
1424
return (
1525
<CopilotKit
1626
runtimeUrl="/api/copilotkit"
17-
agent="tool_rendering_custom_catchall"
27+
agent="tool-rendering-custom-catchall"
1828
>
1929
<div className="flex justify-center items-center h-screen w-full">
2030
<div className="h-full w-full max-w-4xl">
@@ -26,19 +36,17 @@ export default function ToolRenderingCustomCatchallDemo() {
2636
}
2737

2838
function Chat() {
29-
// The render prop is typed by useDefaultRenderTool's `DefaultRenderProps`
30-
// — destructure without casting and let inference flow through. The
31-
// runtime emits `"inProgress" | "executing" | "complete"`; both
32-
// pre-completion states render identically, so collapse them to
33-
// "executing" before handing off to the renderer.
3439
// @region[use-default-render-tool-wildcard]
40+
// `useDefaultRenderTool` is a convenience wrapper around
41+
// `useRenderTool({ name: "*", ... })` — a single wildcard renderer
42+
// that handles every tool call not claimed by a named renderer.
3543
useDefaultRenderTool(
3644
{
3745
render: ({ name, parameters, status, result }) => (
3846
<CustomCatchallRenderer
3947
name={name}
40-
args={(parameters ?? {}) as Record<string, unknown>}
41-
status={status === "complete" ? "complete" : "executing"}
48+
parameters={parameters}
49+
status={status as CatchallToolStatus}
4250
result={result}
4351
/>
4452
),
@@ -47,24 +55,11 @@ function Chat() {
4755
);
4856
// @endregion[use-default-render-tool-wildcard]
4957

50-
useConfigureSuggestions({
51-
suggestions: [
52-
{
53-
title: "Weather in SF",
54-
message: "What's the weather in San Francisco?",
55-
},
56-
{ title: "Find flights", message: "Find flights from SFO to JFK." },
57-
{
58-
title: "Sales chart",
59-
message: "Show me a quarterly revenue pie chart.",
60-
},
61-
],
62-
available: "always",
63-
});
58+
useSuggestions();
6459

6560
return (
6661
<CopilotChat
67-
agentId="tool_rendering_custom_catchall"
62+
agentId="tool-rendering-custom-catchall"
6863
className="h-full rounded-2xl"
6964
/>
7065
);

0 commit comments

Comments
 (0)