Skip to content

Commit 90adfb6

Browse files
authored
Merge pull request #67 from CopilotKit/staging
feat: planning step, widget height fix, hide templates
2 parents df96336 + 991f9ea commit 90adfb6

File tree

12 files changed

+165
-88
lines changed

12 files changed

+165
-88
lines changed

apps/agent/main.py

Lines changed: 28 additions & 19 deletions
Original file line numberDiff line numberDiff line change
@@ -18,13 +18,13 @@
1818
from src.query import query_data
1919
from src.todos import AgentState, todo_tools
2020
from src.form import generate_form
21-
from src.templates import template_tools
21+
from src.plan import plan_visualization
2222

2323
load_dotenv()
2424

2525
agent = create_deep_agent(
2626
model=ChatOpenAI(model=os.environ.get("LLM_MODEL", "gpt-5.4-2026-03-05")),
27-
tools=[query_data, *todo_tools, generate_form, *template_tools],
27+
tools=[query_data, plan_visualization, *todo_tools, generate_form],
2828
middleware=[CopilotKitMiddleware()],
2929
context_schema=AgentState,
3030
skills=[str(Path(__file__).parent / "skills")],
@@ -53,27 +53,36 @@
5353
- Pre-styled form elements (buttons, inputs, sliders look native automatically)
5454
- Pre-built SVG CSS classes for color ramps (.c-purple, .c-teal, .c-blue, etc.)
5555
56-
## UI Templates
56+
## Visualization Workflow (MANDATORY)
5757
58-
Users can save generated UIs as reusable templates and apply them later.
59-
You have backend tools: `save_template`, `list_templates`, `apply_template`, `delete_template`.
58+
When producing ANY visual response (widgetRenderer, pieChart, barChart), you MUST
59+
follow this exact sequence:
6060
61-
**When a user asks to apply/recreate a template with new data:**
62-
Check `pending_template` in state — the frontend sets this when the user picks a template.
63-
If `pending_template` is present (has `id` and `name`):
64-
1. Call `apply_template(template_id=pending_template["id"])` to retrieve the HTML
65-
2. Take the returned HTML and COPY IT EXACTLY, only replacing the data values
66-
(names, numbers, dates, labels, amounts) to match the user's message
67-
3. Render the modified HTML using `widgetRenderer`
68-
4. Call `clear_pending_template` to reset the pending state
61+
1. **Acknowledge** — Reply with 1-2 sentences of plain text acknowledging the
62+
request and setting context for what the visualization will show.
63+
2. **Plan** — Call `plan_visualization` with your approach, technology choice,
64+
and 2-4 key elements. Keep it concise.
65+
3. **Build** — Call the appropriate visualization tool (widgetRenderer, pieChart,
66+
or barChart).
67+
4. **Narrate** — After the visualization, add 2-3 sentences walking through
68+
what was built and offering to go deeper.
6969
70-
If no `pending_template` is set but the user mentions a template by name, use
71-
`apply_template(name="...")` instead.
70+
NEVER skip the plan_visualization step. NEVER call widgetRenderer, pieChart, or
71+
barChart without calling plan_visualization first.
7272
73-
CRITICAL: Do NOT rewrite or generate HTML from scratch. Take the original HTML string,
74-
find-and-replace ONLY the data values, and pass the result to widgetRenderer.
75-
This preserves the exact layout and styling of the original template.
76-
For bar/pie chart templates, use `barChart` or `pieChart` component instead.
73+
## Visualization Quality Standards
74+
75+
The iframe has an import map with these ES module libraries — use `<script type="module">` and bare import specifiers:
76+
- `three` — 3D graphics. `import * as THREE from "three"`. Also `three/examples/jsm/controls/OrbitControls.js` for camera controls.
77+
- `gsap` — animation. `import gsap from "gsap"`.
78+
- `d3` — data visualization and force layouts. `import * as d3 from "d3"`.
79+
- `chart.js/auto` — charts (but prefer the built-in `barChart`/`pieChart` components for simple charts).
80+
81+
**3D content**: ALWAYS use Three.js with proper WebGL rendering. Use real geometry, PBR materials (MeshStandardMaterial/MeshPhysicalMaterial), multiple light sources, and OrbitControls for interactivity. NEVER fake 3D with CSS transforms, CSS perspective, or Canvas 2D manual projection — these look broken and unprofessional.
82+
83+
**Quality bar**: Every visualization should look polished and portfolio-ready. Use smooth animations, proper lighting (ambient + directional at minimum), responsive canvas sizing (`window.addEventListener('resize', ...)`), and antialiasing (`antialias: true`). No proof-of-concept quality.
84+
85+
**Critical**: `<script type="module">` is REQUIRED when using import map libraries. Regular `<script>` tags cannot use `import` statements.
7786
""",
7887
)
7988

apps/agent/src/plan.py

Lines changed: 21 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,21 @@
1+
"""Planning tool for visualization generation."""
2+
3+
from langchain.tools import tool
4+
5+
6+
@tool
7+
def plan_visualization(
8+
approach: str, technology: str, key_elements: list[str]
9+
) -> str:
10+
"""Plan a visualization before building it. MUST be called before
11+
widgetRenderer, pieChart, or barChart. Outlines the approach, technology
12+
choice, and key elements.
13+
14+
Args:
15+
approach: One sentence describing the visualization strategy.
16+
technology: The primary technology (e.g. "inline SVG", "Chart.js",
17+
"HTML + Canvas", "Three.js", "Mermaid", "D3.js").
18+
key_elements: 2-4 concise bullet points describing what will be built.
19+
"""
20+
elements = "\n".join(f" - {e}" for e in key_elements)
21+
return f"Plan: {approach}\nTech: {technology}\n{elements}"

apps/agent/src/todos.py

Lines changed: 1 addition & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -2,26 +2,18 @@
22
from langchain.tools import ToolRuntime, tool
33
from langchain.messages import ToolMessage
44
from langgraph.types import Command
5-
from typing import Optional, TypedDict, Literal
5+
from typing import TypedDict, Literal
66
import uuid
77

8-
from src.templates import UITemplate
9-
108
class Todo(TypedDict):
119
id: str
1210
title: str
1311
description: str
1412
emoji: str
1513
status: Literal["pending", "completed"]
1614

17-
class PendingTemplate(TypedDict, total=False):
18-
id: str
19-
name: str
20-
2115
class AgentState(BaseAgentState):
2216
todos: list[Todo]
23-
templates: list[UITemplate]
24-
pending_template: Optional[PendingTemplate]
2517

2618
@tool
2719
def manage_todos(todos: list[Todo], runtime: ToolRuntime) -> Command:

apps/app/src/app/page.tsx

Lines changed: 2 additions & 31 deletions
Original file line numberDiff line numberDiff line change
@@ -1,20 +1,15 @@
11
"use client";
22

3-
import { useEffect, useState } from "react";
3+
import { useEffect } from "react";
44
import { ExampleLayout } from "@/components/example-layout";
55
import { useGenerativeUIExamples, useExampleSuggestions } from "@/hooks";
66
import { ExplainerCardsPortal } from "@/components/explainer-cards";
7-
import { TemplateLibrary } from "@/components/template-library";
8-
import { TemplateChip } from "@/components/template-library/template-chip";
9-
107
import { CopilotChat } from "@copilotkit/react-core/v2";
118

129
export default function HomePage() {
1310
useGenerativeUIExamples();
1411
useExampleSuggestions();
1512

16-
const [templateDrawerOpen, setTemplateDrawerOpen] = useState(false);
17-
1813
// Widget bridge: handle messages from widget iframes
1914
useEffect(() => {
2015
const handler = (e: MessageEvent) => {
@@ -60,23 +55,6 @@ export default function HomePage() {
6055
</p>
6156
</div>
6257
<div className="flex items-center gap-2">
63-
{/* Template Library toggle */}
64-
<button
65-
onClick={() => setTemplateDrawerOpen(true)}
66-
className="inline-flex items-center gap-1.5 px-3 py-2 rounded-full text-sm font-medium no-underline whitespace-nowrap transition-all duration-150 hover:-translate-y-px"
67-
style={{
68-
color: "var(--text-secondary)",
69-
border: "1px solid var(--color-border-glass, rgba(0,0,0,0.1))",
70-
background: "var(--surface-primary, rgba(255,255,255,0.6))",
71-
fontFamily: "var(--font-family)",
72-
}}
73-
title="Open Template Library"
74-
>
75-
<svg width="15" height="15" viewBox="0 0 24 24" fill="none" stroke="currentColor" strokeWidth="2" strokeLinecap="round" strokeLinejoin="round">
76-
<path d="m19 21-7-4-7 4V5a2 2 0 0 1 2-2h10a2 2 0 0 1 2 2v16z" />
77-
</svg>
78-
Templates
79-
</button>
8058
<a
8159
href="https://github.com/CopilotKit/OpenGenerativeUI"
8260
target="_blank"
@@ -98,21 +76,14 @@ export default function HomePage() {
9876
<CopilotChat
9977
labels={{
10078
welcomeMessageText: "What do you want to visualize today?",
79+
chatDisclaimerText: "Visualizations are AI-generated. You can retry the same prompt or ask the AI to refine the result.",
10180
}}
10281
/>
10382
} />
10483
<ExplainerCardsPortal />
10584
</div>
10685
</div>
10786

108-
{/* Template chip — portal renders above chat input */}
109-
<TemplateChip />
110-
111-
{/* Template Library Drawer */}
112-
<TemplateLibrary
113-
open={templateDrawerOpen}
114-
onClose={() => setTemplateDrawerOpen(false)}
115-
/>
11687
</>
11788
);
11889
}

apps/app/src/components/generative-ui/charts/bar-chart.tsx

Lines changed: 3 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -1,7 +1,7 @@
11
import { BarChart as RechartsBarChart, Bar, XAxis, YAxis, Tooltip, ResponsiveContainer } from 'recharts';
22
import { z } from 'zod';
33
import { CHART_COLORS, CHART_CONFIG } from './config';
4-
import { SaveTemplateOverlay } from '../save-template-overlay';
4+
import { ExportOverlay } from '../export-overlay';
55

66
export const BarChartProps = z.object({
77
title: z.string().describe("Chart title"),
@@ -38,9 +38,8 @@ export function BarChart({ title, description, data }: BarChartProps) {
3838
}));
3939

4040
return (
41-
<SaveTemplateOverlay
41+
<ExportOverlay
4242
title={title}
43-
description={description}
4443
componentType="barChart"
4544
componentData={{ title, description, data }}
4645
>
@@ -66,6 +65,6 @@ export function BarChart({ title, description, data }: BarChartProps) {
6665
</RechartsBarChart>
6766
</ResponsiveContainer>
6867
</div>
69-
</SaveTemplateOverlay>
68+
</ExportOverlay>
7069
);
7170
}

apps/app/src/components/generative-ui/charts/pie-chart.tsx

Lines changed: 3 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -6,7 +6,7 @@ import {
66
} from "recharts";
77
import { z } from "zod";
88
import { CHART_COLORS, CHART_CONFIG } from "./config";
9-
import { SaveTemplateOverlay } from "../save-template-overlay";
9+
import { ExportOverlay } from "../export-overlay";
1010

1111
export const PieChartProps = z.object({
1212
title: z.string().describe("Chart title"),
@@ -47,9 +47,8 @@ export function PieChart({ title, description, data }: PieChartProps) {
4747
}));
4848

4949
return (
50-
<SaveTemplateOverlay
50+
<ExportOverlay
5151
title={title}
52-
description={description}
5352
componentType="pieChart"
5453
componentData={{ title, description, data }}
5554
>
@@ -94,6 +93,6 @@ export function PieChart({ title, description, data }: PieChartProps) {
9493
))}
9594
</div>
9695
</div>
97-
</SaveTemplateOverlay>
96+
</ExportOverlay>
9897
);
9998
}

apps/app/src/components/generative-ui/save-template-overlay.tsx renamed to apps/app/src/components/generative-ui/export-overlay.tsx

Lines changed: 14 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -8,24 +8,23 @@ import {
88
slugify,
99
} from "./export-utils";
1010

11-
interface SaveTemplateOverlayProps {
11+
interface ExportOverlayProps {
1212
title: string;
13-
description: string;
1413
html?: string;
1514
componentData?: Record<string, unknown>;
1615
componentType: string;
1716
ready?: boolean;
1817
children: ReactNode;
1918
}
2019

21-
export function SaveTemplateOverlay({
20+
export function ExportOverlay({
2221
title,
2322
html,
2423
componentData,
2524
componentType,
2625
ready = true,
2726
children,
28-
}: SaveTemplateOverlayProps) {
27+
}: ExportOverlayProps) {
2928
const [copyState, setCopyState] = useState<"idle" | "copied">("idle");
3029
const [menuOpen, setMenuOpen] = useState(false);
3130
const [hovered, setHovered] = useState(false);
@@ -67,11 +66,17 @@ export function SaveTemplateOverlay({
6766
const handleCopy = useCallback(() => {
6867
const textToCopy = componentType === "widgetRenderer" ? html : exportHtml;
6968
if (!textToCopy) return;
70-
navigator.clipboard.writeText(textToCopy).then(() => {
71-
setCopyState("copied");
72-
setMenuOpen(false);
73-
setTimeout(() => setCopyState("idle"), 1800);
74-
});
69+
navigator.clipboard.writeText(textToCopy).then(
70+
() => {
71+
setCopyState("copied");
72+
setMenuOpen(false);
73+
setTimeout(() => setCopyState("idle"), 1800);
74+
},
75+
() => {
76+
// Clipboard write failed (e.g. permission denied, iframe context)
77+
setMenuOpen(false);
78+
}
79+
);
7580
}, [componentType, html, exportHtml]);
7681

7782
const showTrigger = ready && exportHtml && (hovered || menuOpen);

apps/app/src/components/generative-ui/export-utils.ts

Lines changed: 1 addition & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,4 @@
1-
import { THEME_CSS } from "./widget-renderer";
2-
import { SVG_CLASSES_CSS, FORM_STYLES_CSS } from "./widget-renderer";
1+
import { THEME_CSS, SVG_CLASSES_CSS, FORM_STYLES_CSS } from "./widget-renderer";
32

43
const CHART_COLORS = [
54
"#3b82f6",
Lines changed: 56 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,56 @@
1+
"use client";
2+
3+
import { useEffect, useRef } from "react";
4+
5+
interface PlanCardProps {
6+
status: "executing" | "inProgress" | "complete";
7+
approach?: string;
8+
technology?: string;
9+
keyElements?: string[];
10+
}
11+
12+
export function PlanCard({ status, approach, technology, keyElements }: PlanCardProps) {
13+
const detailsRef = useRef<HTMLDetailsElement>(null);
14+
const isRunning = status === "executing" || status === "inProgress";
15+
16+
useEffect(() => {
17+
if (!detailsRef.current) return;
18+
detailsRef.current.open = isRunning;
19+
}, [isRunning]);
20+
21+
const spinner = (
22+
<span className="inline-block h-3 w-3 rounded-full border-2 border-gray-400 border-t-transparent animate-spin" />
23+
);
24+
const checkmark = <span className="text-green-500 text-xs"></span>;
25+
26+
return (
27+
<div className="my-2 text-sm">
28+
<details ref={detailsRef} open>
29+
<summary className="flex items-center gap-2 text-gray-600 dark:text-gray-400 cursor-pointer list-none">
30+
{isRunning ? spinner : checkmark}
31+
<span className="font-medium">
32+
{isRunning ? "Planning visualization…" : `Plan: ${technology || "visualization"}`}
33+
</span>
34+
<span className="text-[10px]"></span>
35+
</summary>
36+
{approach && (
37+
<div className="pl-5 mt-1.5 space-y-1.5 text-xs text-gray-500 dark:text-zinc-400">
38+
{technology && (
39+
<span className="inline-block px-1.5 py-0.5 rounded bg-gray-100 dark:bg-zinc-700 text-gray-600 dark:text-zinc-300 font-medium text-[11px]">
40+
{technology}
41+
</span>
42+
)}
43+
<p className="text-gray-600 dark:text-gray-400">{approach}</p>
44+
{keyElements && keyElements.length > 0 && (
45+
<ul className="list-disc pl-4 space-y-0.5">
46+
{keyElements.map((el, i) => (
47+
<li key={i}>{el}</li>
48+
))}
49+
</ul>
50+
)}
51+
</div>
52+
)}
53+
</details>
54+
</div>
55+
);
56+
}

0 commit comments

Comments
 (0)