Skip to content

Commit 070dcd7

Browse files
tylerslatonclaude
andcommitted
feat(showcase/google-adk/gen-ui-tool-based): port frontend from langgraph-python
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
1 parent 4353521 commit 070dcd7

8 files changed

Lines changed: 342 additions & 281 deletions

File tree

showcase/integrations/google-adk/src/agents/gen_ui_tool_based_agent.py

Lines changed: 9 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -1,8 +1,8 @@
11
"""Agent backing the Tool-Based Generative UI demo.
22
3-
The agent has no backend tools; the frontend registers `generate_haiku` (or
4-
similar) via `useFrontendTool({ render })` so the agent calls it and the
5-
React-side renderer paints the result.
3+
The frontend registers `render_bar_chart` and `render_pie_chart` tools via
4+
`useComponent`. The ADKAgent middleware injects those tools into the model
5+
request at runtime so the agent can call them.
66
"""
77

88
from __future__ import annotations
@@ -12,12 +12,12 @@
1212
from agents.shared_chat import get_model
1313

1414
_INSTRUCTION = (
15-
"You are a helpful assistant that generates richly-formatted UI via "
16-
"frontend tools. When the user asks for a haiku, call the generate_haiku "
17-
"tool with three lines in Japanese, three lines translated to English, "
18-
"a fitting image_name, and a CSS gradient string for the background. "
19-
"Always invoke the tool — never answer with plain text when a tool is "
20-
"available."
15+
"You are a data visualization assistant.\n\n"
16+
"When the user asks for a chart, call `render_bar_chart` or "
17+
"`render_pie_chart` with a concise title, short description, and a "
18+
"`data` array of `{label, value}` items. Pick bar for comparisons over "
19+
"a small set of categories; pick pie for composition / share-of-whole.\n\n"
20+
"Keep chat responses brief -- let the chart do the talking."
2121
)
2222

2323
gen_ui_tool_based_agent = LlmAgent(
Lines changed: 6 additions & 72 deletions
Original file line numberDiff line numberDiff line change
@@ -1,76 +1,10 @@
11
# Tool-Based Generative UI
22

3-
## What This Demo Shows
3+
The agent calls a backend tool that returns structured data; the frontend
4+
renders that tool result as a custom React component instead of plain text.
45

5-
Agent uses tools to trigger UI generation
6+
`useRenderTool` maps each tool name to a renderer that receives `args`,
7+
`result`, and `status`, so the UI can show loading and complete states.
68

7-
## How to Interact
8-
9-
Try asking your Copilot to:
10-
11-
- "What's the weather forecast for this week in San Francisco?"
12-
- "Show me the weather in Paris"
13-
- "Compare the weather in Tokyo and London"
14-
15-
The agent generates structured data via tools, and the frontend renders it as rich UI components.
16-
17-
## Technical Details
18-
19-
What's happening technically:
20-
21-
- **Generative UI** means the agent's tool calls produce structured data that the frontend renders as custom React components
22-
- Unlike plain text responses, the agent returns tool results with typed parameters (city, temperature, conditions)
23-
- `useRenderTool` maps each tool name to a React component, so `get_weather` renders a weather card with icons, temperature displays, and forecast details
24-
- The agent decides when to call the tool based on context — it can mix tool-based UI generation with regular text responses
25-
- This pattern enables agents to create dynamic, data-driven interfaces on demand
26-
27-
## Building With This
28-
29-
If you're extending this demo or building something similar, here are key things to know:
30-
31-
### Styling Inside the Chat
32-
33-
Content rendered inside CopilotKit's chat area (via `useRenderTool`, `useHumanInTheLoop`, `useFrontendTool`) runs inside CopilotKit's component tree. Standard Tailwind classes may not work here because Tailwind v4 can't statically detect them.
34-
35-
**Use inline styles** for any UI rendered inside the chat:
36-
37-
```tsx
38-
// Do this
39-
<div style={{ padding: "24px", borderRadius: "12px", background: "#fff" }}>
40-
41-
// Not this — Tailwind may purge these classes
42-
<div className="p-6 rounded-xl bg-white">
43-
```
44-
45-
### Chat Layout
46-
47-
Wrap `CopilotChat` in a constraining div for proper spacing:
48-
49-
```tsx
50-
<div className="flex justify-center items-center h-screen w-full">
51-
<div className="h-full w-full md:w-4/5 md:h-4/5 rounded-lg">
52-
<CopilotChat className="h-full rounded-2xl max-w-6xl mx-auto" />
53-
</div>
54-
</div>
55-
```
56-
57-
### Overriding CopilotKit Styles
58-
59-
CopilotKit uses `cpk:` prefixed classes internally. To override them, create a **separate CSS file** (not in globals.css — Tailwind purges it):
60-
61-
```css
62-
/* copilotkit-overrides.css */
63-
.copilotKitInput {
64-
border-radius: 0.75rem;
65-
border: 1px solid var(--copilot-kit-separator-color) !important;
66-
}
67-
```
68-
69-
Import it in `layout.tsx` after `globals.css`.
70-
71-
### Images and Icons
72-
73-
- Don't reference local image files from agent-generated content (they won't exist). Add `onError` fallbacks.
74-
- Use emoji instead of SVG icons inside chat messages (`fill="currentColor"` renders unpredictably in the chat context).
75-
76-
See the full [Styling Guide](https://github.com/CopilotKit/CopilotKit/blob/main/showcase/STYLING-GUIDE.md) for more details.
9+
The canonical description lives in the showcase manifest; this README is just
10+
a developer note alongside the demo source.

showcase/integrations/google-adk/src/app/demos/gen-ui-tool-based/agent.py

Lines changed: 0 additions & 6 deletions
This file was deleted.

showcase/integrations/google-adk/src/app/demos/gen-ui-tool-based/bar-chart-renderer.snippet.tsx

Lines changed: 0 additions & 32 deletions
This file was deleted.
Lines changed: 142 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,142 @@
1+
import React, { useRef } from "react";
2+
import {
3+
BarChart as RechartsBarChart,
4+
Bar,
5+
XAxis,
6+
YAxis,
7+
Tooltip,
8+
CartesianGrid,
9+
Cell,
10+
ResponsiveContainer,
11+
Rectangle,
12+
} from "recharts";
13+
import { z } from "zod";
14+
15+
export const barChartPropsSchema = z.object({
16+
title: z.string().describe("Chart title"),
17+
description: z.string().describe("Brief description or subtitle"),
18+
data: z.array(
19+
z.object({
20+
label: z.string(),
21+
value: z.number(),
22+
}),
23+
),
24+
});
25+
26+
export type BarChartProps = z.infer<typeof barChartPropsSchema>;
27+
28+
export function BarChart({ title, description, data }: BarChartProps) {
29+
const CHART_COLORS = [
30+
"#BEC2FF",
31+
"#85ECCE",
32+
"#FFAC4D",
33+
"#FFF388",
34+
"#189370",
35+
"#EEE6FE",
36+
"#FA5F67",
37+
];
38+
39+
const TOOLTIP_STYLE = {
40+
backgroundColor: "#ffffff",
41+
border: "1px solid #DBDBE5",
42+
borderRadius: "12px",
43+
padding: "10px 14px",
44+
color: "#010507",
45+
fontSize: "13px",
46+
boxShadow: "0 4px 12px rgba(1,5,7,0.06)",
47+
};
48+
49+
// Track which bars have already been rendered so only newly-arriving ones animate.
50+
const seen = useRef(new Set<number>());
51+
const isNew = (i: number) => {
52+
if (seen.current.has(i)) return false;
53+
seen.current.add(i);
54+
return true;
55+
};
56+
57+
if (!data || !Array.isArray(data) || data.length === 0) {
58+
return (
59+
<div className="max-w-2xl mx-auto my-4 rounded-2xl border border-[#DBDBE5] bg-white p-6 shadow-sm">
60+
<div className="font-semibold text-[#010507]">{title}</div>
61+
<div className="text-sm text-[#57575B]">{description}</div>
62+
<p className="text-[#AFAFB7] text-center py-8 text-sm">
63+
No data available
64+
</p>
65+
</div>
66+
);
67+
}
68+
69+
return (
70+
<div className="max-w-2xl mx-auto my-4 overflow-hidden rounded-2xl border border-[#DBDBE5] bg-white shadow-sm">
71+
<style>{`
72+
@keyframes barSlideIn {
73+
from { transform: translateY(40px); opacity: 0; }
74+
20% { opacity: 1; }
75+
to { transform: translateY(0); opacity: 1; }
76+
}
77+
`}</style>
78+
<div className="p-4 pb-2">
79+
<div className="font-semibold text-[#010507]">{title}</div>
80+
<div className="text-sm text-[#57575B]">{description}</div>
81+
</div>
82+
<div className="px-4 pb-4">
83+
<ResponsiveContainer width="100%" height={280}>
84+
<RechartsBarChart
85+
data={data}
86+
margin={{ top: 12, right: 12, bottom: 4, left: -8 }}
87+
>
88+
<CartesianGrid
89+
strokeDasharray="3 3"
90+
stroke="#E9E9EF"
91+
vertical={false}
92+
/>
93+
<XAxis
94+
dataKey="label"
95+
tick={{ fontSize: 12, fill: "#838389" }}
96+
stroke="#E9E9EF"
97+
tickLine={false}
98+
axisLine={false}
99+
/>
100+
<YAxis
101+
tick={{ fontSize: 12, fill: "#838389" }}
102+
stroke="#E9E9EF"
103+
tickLine={false}
104+
axisLine={false}
105+
/>
106+
<Tooltip
107+
contentStyle={TOOLTIP_STYLE}
108+
cursor={{ fill: "#F0F0F4", opacity: 0.5 }}
109+
/>
110+
<Bar
111+
isAnimationActive={false}
112+
dataKey="value"
113+
radius={[6, 6, 0, 0]}
114+
maxBarSize={48}
115+
shape={(props: any) => (
116+
<g
117+
style={
118+
isNew(props.index as number)
119+
? {
120+
animation:
121+
"barSlideIn 0.5s cubic-bezier(0.16, 1, 0.3, 1) both",
122+
}
123+
: undefined
124+
}
125+
>
126+
<Rectangle {...props} />
127+
</g>
128+
)}
129+
>
130+
{data.map((_, index) => (
131+
<Cell
132+
key={index}
133+
fill={CHART_COLORS[index % CHART_COLORS.length]}
134+
/>
135+
))}
136+
</Bar>
137+
</RechartsBarChart>
138+
</ResponsiveContainer>
139+
</div>
140+
</div>
141+
);
142+
}

0 commit comments

Comments
 (0)