From cd5eeef60c36e9564e9e4c2a037ec9e265544d32 Mon Sep 17 00:00:00 2001 From: jerelvelarde Date: Thu, 26 Mar 2026 08:17:05 -0700 Subject: [PATCH 1/3] feat: add planning step before visualization generation (#62) Add a plan_visualization tool that the agent must call before any visualization tool (widgetRenderer, pieChart, barChart). This gives users transparency into the agent's approach and improves output quality by forcing structured thinking before code generation. --- apps/agent/main.py | 20 ++++++- apps/agent/src/plan.py | 21 +++++++ .../components/generative-ui/plan-card.tsx | 56 +++++++++++++++++++ .../src/hooks/use-generative-ui-examples.tsx | 18 ++++++ 4 files changed, 114 insertions(+), 1 deletion(-) create mode 100644 apps/agent/src/plan.py create mode 100644 apps/app/src/components/generative-ui/plan-card.tsx diff --git a/apps/agent/main.py b/apps/agent/main.py index 3e43367..6bd3837 100644 --- a/apps/agent/main.py +++ b/apps/agent/main.py @@ -18,13 +18,14 @@ from src.query import query_data from src.todos import AgentState, todo_tools from src.form import generate_form +from src.plan import plan_visualization from src.templates import template_tools load_dotenv() agent = create_deep_agent( model=ChatOpenAI(model=os.environ.get("LLM_MODEL", "gpt-5.4-2026-03-05")), - tools=[query_data, *todo_tools, generate_form, *template_tools], + tools=[query_data, plan_visualization, *todo_tools, generate_form, *template_tools], middleware=[CopilotKitMiddleware()], context_schema=AgentState, skills=[str(Path(__file__).parent / "skills")], @@ -53,6 +54,23 @@ - Pre-styled form elements (buttons, inputs, sliders look native automatically) - Pre-built SVG CSS classes for color ramps (.c-purple, .c-teal, .c-blue, etc.) + ## Visualization Workflow (MANDATORY) + + When producing ANY visual response (widgetRenderer, pieChart, barChart), you MUST + follow this exact sequence: + + 1. **Acknowledge** — Reply with 1-2 sentences of plain text acknowledging the + request and setting context for what the visualization will show. + 2. **Plan** — Call `plan_visualization` with your approach, technology choice, + and 2-4 key elements. Keep it concise. + 3. **Build** — Call the appropriate visualization tool (widgetRenderer, pieChart, + or barChart). + 4. **Narrate** — After the visualization, add 2-3 sentences walking through + what was built and offering to go deeper. + + NEVER skip the plan_visualization step. NEVER call widgetRenderer, pieChart, or + barChart without calling plan_visualization first. + ## UI Templates Users can save generated UIs as reusable templates and apply them later. diff --git a/apps/agent/src/plan.py b/apps/agent/src/plan.py new file mode 100644 index 0000000..cdfd141 --- /dev/null +++ b/apps/agent/src/plan.py @@ -0,0 +1,21 @@ +"""Planning tool for visualization generation.""" + +from langchain.tools import tool + + +@tool +def plan_visualization( + approach: str, technology: str, key_elements: list[str] +) -> str: + """Plan a visualization before building it. MUST be called before + widgetRenderer, pieChart, or barChart. Outlines the approach, technology + choice, and key elements. + + Args: + approach: One sentence describing the visualization strategy. + technology: The primary technology (e.g. "inline SVG", "Chart.js", + "HTML + Canvas", "Three.js", "Mermaid", "D3.js"). + key_elements: 2-4 concise bullet points describing what will be built. + """ + elements = "\n".join(f" - {e}" for e in key_elements) + return f"Plan: {approach}\nTech: {technology}\n{elements}" diff --git a/apps/app/src/components/generative-ui/plan-card.tsx b/apps/app/src/components/generative-ui/plan-card.tsx new file mode 100644 index 0000000..9601e10 --- /dev/null +++ b/apps/app/src/components/generative-ui/plan-card.tsx @@ -0,0 +1,56 @@ +"use client"; + +import { useEffect, useRef } from "react"; + +interface PlanCardProps { + status: "executing" | "inProgress" | "complete"; + approach?: string; + technology?: string; + key_elements?: string[]; +} + +export function PlanCard({ status, approach, technology, key_elements }: PlanCardProps) { + const detailsRef = useRef(null); + const isRunning = status === "executing" || status === "inProgress"; + + useEffect(() => { + if (!detailsRef.current) return; + detailsRef.current.open = isRunning; + }, [isRunning]); + + const spinner = ( + + ); + const checkmark = ; + + return ( +
+
+ + {isRunning ? spinner : checkmark} + + {isRunning ? "Planning visualization…" : `Plan: ${technology || "visualization"}`} + + + + {approach && ( +
+ {technology && ( + + {technology} + + )} +

{approach}

+ {key_elements && key_elements.length > 0 && ( +
    + {key_elements.map((el, i) => ( +
  • {el}
  • + ))} +
+ )} +
+ )} +
+
+ ); +} diff --git a/apps/app/src/hooks/use-generative-ui-examples.tsx b/apps/app/src/hooks/use-generative-ui-examples.tsx index a81608e..6138453 100644 --- a/apps/app/src/hooks/use-generative-ui-examples.tsx +++ b/apps/app/src/hooks/use-generative-ui-examples.tsx @@ -7,6 +7,7 @@ import { useFrontendTool, useHumanInTheLoop, useDefaultRenderTool, + useRenderTool, } from "@copilotkit/react-core/v2"; // Generative UI imports @@ -15,6 +16,7 @@ import { BarChart, BarChartProps } from "@/components/generative-ui/charts/bar-c import { WidgetRenderer, WidgetRendererProps } from "@/components/generative-ui/widget-renderer"; import { MeetingTimePicker } from "@/components/generative-ui/meeting-time-picker"; import { ToolReasoning } from "@/components/tool-rendering"; +import { PlanCard } from "@/components/generative-ui/plan-card"; export const useGenerativeUIExamples = () => { const { theme, setTheme } = useTheme(); @@ -61,6 +63,22 @@ export const useGenerativeUIExamples = () => { render: WidgetRenderer, }); + // -------------------------- + // 🪁 Plan Visualization: Custom rendering for the planning step + // -------------------------- + const PlanVisualizationParams = z.object({ + approach: z.string(), + technology: z.string(), + key_elements: z.array(z.string()), + }); + useRenderTool({ + name: "plan_visualization", + parameters: PlanVisualizationParams, + render: ({ status, parameters }) => ( + + ), + }); + // -------------------------- // 🪁 Default Tool Rendering: https://docs.copilotkit.ai/langgraph/generative-ui/backend-tools // -------------------------- From 92fc981f393604c1ecd2a4f892e68baac8c8098c Mon Sep 17 00:00:00 2001 From: jerelvelarde Date: Thu, 26 Mar 2026 08:35:57 -0700 Subject: [PATCH 2/3] fix: widget renderer infinite height growth (#64), update example prompts - Break feedback loop in iframe height measurement by collapsing the content container before reading scrollHeight, so viewport-relative children don't inflate the measurement - Remove +8 padding from height setter that compounded each cycle - Replace "binary search" suggestion with "car axle" visualization --- .../components/generative-ui/widget-renderer.tsx | 13 ++++++++++--- apps/app/src/hooks/use-example-suggestions.tsx | 2 +- 2 files changed, 11 insertions(+), 4 deletions(-) diff --git a/apps/app/src/components/generative-ui/widget-renderer.tsx b/apps/app/src/components/generative-ui/widget-renderer.tsx index 43ccb19..1ffe7d5 100644 --- a/apps/app/src/components/generative-ui/widget-renderer.tsx +++ b/apps/app/src/components/generative-ui/widget-renderer.tsx @@ -455,10 +455,17 @@ window.addEventListener('message', function(e) { } }); -// Auto-resize: report content height to host +// Auto-resize: report content height to host. +// Temporarily collapse the container so viewport-relative children (100vh, 100%) +// don't inflate the measurement — this gives us intrinsic content height only. function reportHeight() { var content = document.getElementById('content'); - var h = content ? content.offsetHeight : document.documentElement.scrollHeight; + if (!content) return; + content.style.height = '0'; + content.style.overflow = 'hidden'; + var h = content.scrollHeight; + content.style.height = ''; + content.style.overflow = ''; window.parent.postMessage({ type: 'widget-resize', height: h }, '*'); } var ro = new ResizeObserver(reportHeight); @@ -573,7 +580,7 @@ export function WidgetRenderer({ title, description, html }: WidgetRendererProps e.data?.type === "widget-resize" && typeof e.data.height === "number" ) { - setHeight(Math.max(50, Math.min(e.data.height + 8, 4000))); + setHeight(Math.max(50, Math.min(e.data.height, 4000))); } }, []); diff --git a/apps/app/src/hooks/use-example-suggestions.tsx b/apps/app/src/hooks/use-example-suggestions.tsx index 528388d..38cb44c 100644 --- a/apps/app/src/hooks/use-example-suggestions.tsx +++ b/apps/app/src/hooks/use-example-suggestions.tsx @@ -3,7 +3,7 @@ import { useConfigureSuggestions } from "@copilotkit/react-core/v2"; export const useExampleSuggestions = () => { useConfigureSuggestions({ suggestions: [ - { title: "Visualize a binary search", message: "Visualize how binary search works on a sorted list. Step by step." }, + { title: "Visualize a car axle", message: "Visualize how a car axle works" }, { title: "Compare BFS vs DFS", message: "I want to understand the difference between BFS and DFS. Create an interactive comparison on a node graph." }, { 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." }, ], From 76976a072a31459685b96ce6854637d38423493c Mon Sep 17 00:00:00 2001 From: jerelvelarde Date: Thu, 26 Mar 2026 09:12:06 -0700 Subject: [PATCH 3/3] chore: hide demo gallery panel from UI Demo gallery preserved on feat/demo-gallery branch for future work. --- apps/app/src/app/page.tsx | 38 +------------------------------------- 1 file changed, 1 insertion(+), 37 deletions(-) diff --git a/apps/app/src/app/page.tsx b/apps/app/src/app/page.tsx index 640a137..c7fe372 100644 --- a/apps/app/src/app/page.tsx +++ b/apps/app/src/app/page.tsx @@ -1,27 +1,15 @@ "use client"; -import { useEffect, useState } from "react"; +import { useEffect } from "react"; import { ExampleLayout } from "@/components/example-layout"; import { useGenerativeUIExamples, useExampleSuggestions } from "@/hooks"; import { ExplainerCardsPortal } from "@/components/explainer-cards"; -import { DemoGallery, type DemoItem } from "@/components/demo-gallery"; - import { CopilotChat } from "@copilotkit/react-core/v2"; -import { useCopilotChat } from "@copilotkit/react-core"; export default function HomePage() { useGenerativeUIExamples(); useExampleSuggestions(); - const [demoDrawerOpen, setDemoDrawerOpen] = useState(false); - const { appendMessage } = useCopilotChat(); - - const handleTryDemo = (demo: DemoItem) => { - setDemoDrawerOpen(false); - // eslint-disable-next-line @typescript-eslint/no-explicit-any - appendMessage({ content: demo.prompt, role: "user" } as any); - }; - // Widget bridge: handle messages from widget iframes useEffect(() => { const handler = (e: MessageEvent) => { @@ -67,25 +55,6 @@ export default function HomePage() {

-
- setDemoDrawerOpen(false)} - onTryDemo={handleTryDemo} - /> ); }