Skip to content

Commit 490b4a0

Browse files
authored
Merge pull request #66 from CopilotKit/worktree-issue-62-planning-step
feat: add planning step before visualization generation
2 parents 3060930 + 76976a0 commit 490b4a0

File tree

7 files changed

+126
-42
lines changed

7 files changed

+126
-42
lines changed

apps/agent/main.py

Lines changed: 19 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -18,12 +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.plan import plan_visualization
2122

2223
load_dotenv()
2324

2425
agent = create_deep_agent(
2526
model=ChatOpenAI(model=os.environ.get("LLM_MODEL", "gpt-5.4-2026-03-05")),
26-
tools=[query_data, *todo_tools, generate_form],
27+
tools=[query_data, plan_visualization, *todo_tools, generate_form],
2728
middleware=[CopilotKitMiddleware()],
2829
context_schema=AgentState,
2930
skills=[str(Path(__file__).parent / "skills")],
@@ -52,6 +53,23 @@
5253
- Pre-styled form elements (buttons, inputs, sliders look native automatically)
5354
- Pre-built SVG CSS classes for color ramps (.c-purple, .c-teal, .c-blue, etc.)
5455
56+
## Visualization Workflow (MANDATORY)
57+
58+
When producing ANY visual response (widgetRenderer, pieChart, barChart), you MUST
59+
follow this exact sequence:
60+
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.
69+
70+
NEVER skip the plan_visualization step. NEVER call widgetRenderer, pieChart, or
71+
barChart without calling plan_visualization first.
72+
5573
## Visualization Quality Standards
5674
5775
The iframe has an import map with these ES module libraries — use `<script type="module">` and bare import specifiers:

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/app/src/app/page.tsx

Lines changed: 1 addition & 37 deletions
Original file line numberDiff line numberDiff line change
@@ -1,27 +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 { DemoGallery, type DemoItem } from "@/components/demo-gallery";
8-
97
import { CopilotChat } from "@copilotkit/react-core/v2";
10-
import { useCopilotChat } from "@copilotkit/react-core";
118

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

16-
const [demoDrawerOpen, setDemoDrawerOpen] = useState(false);
17-
const { appendMessage } = useCopilotChat();
18-
19-
const handleTryDemo = (demo: DemoItem) => {
20-
setDemoDrawerOpen(false);
21-
// eslint-disable-next-line @typescript-eslint/no-explicit-any
22-
appendMessage({ content: demo.prompt, role: "user" } as any);
23-
};
24-
2513
// Widget bridge: handle messages from widget iframes
2614
useEffect(() => {
2715
const handler = (e: MessageEvent) => {
@@ -67,25 +55,6 @@ export default function HomePage() {
6755
</p>
6856
</div>
6957
<div className="flex items-center gap-2">
70-
<button
71-
onClick={() => setDemoDrawerOpen(true)}
72-
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 cursor-pointer"
73-
style={{
74-
color: "var(--text-secondary)",
75-
border: "1px solid var(--color-border-glass, rgba(0,0,0,0.1))",
76-
background: "var(--surface-primary, rgba(255,255,255,0.6))",
77-
fontFamily: "var(--font-family)",
78-
}}
79-
title="Open Demo Gallery"
80-
>
81-
<svg width="15" height="15" viewBox="0 0 24 24" fill="none" stroke="currentColor" strokeWidth="2" strokeLinecap="round" strokeLinejoin="round">
82-
<rect width="7" height="7" x="3" y="3" rx="1" />
83-
<rect width="7" height="7" x="14" y="3" rx="1" />
84-
<rect width="7" height="7" x="14" y="14" rx="1" />
85-
<rect width="7" height="7" x="3" y="14" rx="1" />
86-
</svg>
87-
Demos
88-
</button>
8958
<a
9059
href="https://github.com/CopilotKit/OpenGenerativeUI"
9160
target="_blank"
@@ -114,11 +83,6 @@ export default function HomePage() {
11483
</div>
11584
</div>
11685

117-
<DemoGallery
118-
open={demoDrawerOpen}
119-
onClose={() => setDemoDrawerOpen(false)}
120-
onTryDemo={handleTryDemo}
121-
/>
12286
</>
12387
);
12488
}
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+
key_elements?: string[];
10+
}
11+
12+
export function PlanCard({ status, approach, technology, key_elements }: 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+
{key_elements && key_elements.length > 0 && (
45+
<ul className="list-disc pl-4 space-y-0.5">
46+
{key_elements.map((el, i) => (
47+
<li key={i}>{el}</li>
48+
))}
49+
</ul>
50+
)}
51+
</div>
52+
)}
53+
</details>
54+
</div>
55+
);
56+
}

apps/app/src/components/generative-ui/widget-renderer.tsx

Lines changed: 10 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -455,10 +455,17 @@ window.addEventListener('message', function(e) {
455455
}
456456
});
457457
458-
// Auto-resize: report content height to host
458+
// Auto-resize: report content height to host.
459+
// Temporarily collapse the container so viewport-relative children (100vh, 100%)
460+
// don't inflate the measurement — this gives us intrinsic content height only.
459461
function reportHeight() {
460462
var content = document.getElementById('content');
461-
var h = content ? content.offsetHeight : document.documentElement.scrollHeight;
463+
if (!content) return;
464+
content.style.height = '0';
465+
content.style.overflow = 'hidden';
466+
var h = content.scrollHeight;
467+
content.style.height = '';
468+
content.style.overflow = '';
462469
window.parent.postMessage({ type: 'widget-resize', height: h }, '*');
463470
}
464471
var ro = new ResizeObserver(reportHeight);
@@ -573,7 +580,7 @@ export function WidgetRenderer({ title, html }: WidgetRendererProps) {
573580
e.data?.type === "widget-resize" &&
574581
typeof e.data.height === "number"
575582
) {
576-
setHeight(Math.max(50, Math.min(e.data.height + 8, 4000)));
583+
setHeight(Math.max(50, Math.min(e.data.height, 4000)));
577584
}
578585
}, []);
579586

apps/app/src/hooks/use-example-suggestions.tsx

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -3,7 +3,7 @@ import { useConfigureSuggestions } from "@copilotkit/react-core/v2";
33
export const useExampleSuggestions = () => {
44
useConfigureSuggestions({
55
suggestions: [
6-
{ title: "Visualize a binary search", message: "Visualize how binary search works on a sorted list. Step by step." },
6+
{ title: "Visualize a car axle", message: "Visualize how a car axle works" },
77
{ title: "3D Plane Controls", message: "Create a 3D plane in Three.js to explain how pitch, roll, and yaw work with buttons that animate on hover." },
88
{ title: "Cool 3D sphere", message: "Create a 3D animation of a sphere turning into an icosahedron when the mouse is on it and back to a sphere when it's not on the icosahedron, make it cool." },
99
],

apps/app/src/hooks/use-generative-ui-examples.tsx

Lines changed: 18 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -7,6 +7,7 @@ import {
77
useFrontendTool,
88
useHumanInTheLoop,
99
useDefaultRenderTool,
10+
useRenderTool,
1011
} from "@copilotkit/react-core/v2";
1112

1213
// Generative UI imports
@@ -15,6 +16,7 @@ import { BarChart, BarChartProps } from "@/components/generative-ui/charts/bar-c
1516
import { WidgetRenderer, WidgetRendererProps } from "@/components/generative-ui/widget-renderer";
1617
import { MeetingTimePicker } from "@/components/generative-ui/meeting-time-picker";
1718
import { ToolReasoning } from "@/components/tool-rendering";
19+
import { PlanCard } from "@/components/generative-ui/plan-card";
1820

1921
export const useGenerativeUIExamples = () => {
2022
const { theme, setTheme } = useTheme();
@@ -61,6 +63,22 @@ export const useGenerativeUIExamples = () => {
6163
render: WidgetRenderer,
6264
});
6365

66+
// --------------------------
67+
// 🪁 Plan Visualization: Custom rendering for the planning step
68+
// --------------------------
69+
const PlanVisualizationParams = z.object({
70+
approach: z.string(),
71+
technology: z.string(),
72+
key_elements: z.array(z.string()),
73+
});
74+
useRenderTool({
75+
name: "plan_visualization",
76+
parameters: PlanVisualizationParams,
77+
render: ({ status, parameters }) => (
78+
<PlanCard status={status} {...parameters} />
79+
),
80+
});
81+
6482
// --------------------------
6583
// 🪁 Default Tool Rendering: https://docs.copilotkit.ai/langgraph/generative-ui/backend-tools
6684
// --------------------------

0 commit comments

Comments
 (0)