Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
20 changes: 19 additions & 1 deletion apps/agent/main.py
Original file line number Diff line number Diff line change
Expand Up @@ -18,12 +18,13 @@
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

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],
tools=[query_data, plan_visualization, *todo_tools, generate_form],
middleware=[CopilotKitMiddleware()],
context_schema=AgentState,
skills=[str(Path(__file__).parent / "skills")],
Expand Down Expand Up @@ -52,6 +53,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.

## Visualization Quality Standards

The iframe has an import map with these ES module libraries — use `<script type="module">` and bare import specifiers:
Expand Down
21 changes: 21 additions & 0 deletions apps/agent/src/plan.py
Original file line number Diff line number Diff line change
@@ -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}"
38 changes: 1 addition & 37 deletions apps/app/src/app/page.tsx
Original file line number Diff line number Diff line change
@@ -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) => {
Expand Down Expand Up @@ -67,25 +55,6 @@ export default function HomePage() {
</p>
</div>
<div className="flex items-center gap-2">
<button
onClick={() => setDemoDrawerOpen(true)}
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"
style={{
color: "var(--text-secondary)",
border: "1px solid var(--color-border-glass, rgba(0,0,0,0.1))",
background: "var(--surface-primary, rgba(255,255,255,0.6))",
fontFamily: "var(--font-family)",
}}
title="Open Demo Gallery"
>
<svg width="15" height="15" viewBox="0 0 24 24" fill="none" stroke="currentColor" strokeWidth="2" strokeLinecap="round" strokeLinejoin="round">
<rect width="7" height="7" x="3" y="3" rx="1" />
<rect width="7" height="7" x="14" y="3" rx="1" />
<rect width="7" height="7" x="14" y="14" rx="1" />
<rect width="7" height="7" x="3" y="14" rx="1" />
</svg>
Demos
</button>
<a
href="https://github.com/CopilotKit/OpenGenerativeUI"
target="_blank"
Expand Down Expand Up @@ -114,11 +83,6 @@ export default function HomePage() {
</div>
</div>

<DemoGallery
open={demoDrawerOpen}
onClose={() => setDemoDrawerOpen(false)}
onTryDemo={handleTryDemo}
/>
</>
);
}
56 changes: 56 additions & 0 deletions apps/app/src/components/generative-ui/plan-card.tsx
Original file line number Diff line number Diff line change
@@ -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<HTMLDetailsElement>(null);
const isRunning = status === "executing" || status === "inProgress";

useEffect(() => {
if (!detailsRef.current) return;
detailsRef.current.open = isRunning;
}, [isRunning]);

const spinner = (
<span className="inline-block h-3 w-3 rounded-full border-2 border-gray-400 border-t-transparent animate-spin" />
);
const checkmark = <span className="text-green-500 text-xs">✓</span>;

return (
<div className="my-2 text-sm">
<details ref={detailsRef} open>
<summary className="flex items-center gap-2 text-gray-600 dark:text-gray-400 cursor-pointer list-none">
{isRunning ? spinner : checkmark}
<span className="font-medium">
{isRunning ? "Planning visualization…" : `Plan: ${technology || "visualization"}`}
</span>
<span className="text-[10px]">▼</span>
</summary>
{approach && (
<div className="pl-5 mt-1.5 space-y-1.5 text-xs text-gray-500 dark:text-zinc-400">
{technology && (
<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]">
{technology}
</span>
)}
<p className="text-gray-600 dark:text-gray-400">{approach}</p>
{key_elements && key_elements.length > 0 && (
<ul className="list-disc pl-4 space-y-0.5">
{key_elements.map((el, i) => (
<li key={i}>{el}</li>
))}
</ul>
)}
</div>
)}
</details>
</div>
);
}
13 changes: 10 additions & 3 deletions apps/app/src/components/generative-ui/widget-renderer.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -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);
Expand Down Expand Up @@ -573,7 +580,7 @@ export function WidgetRenderer({ title, 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)));
}
}, []);

Expand Down
2 changes: 1 addition & 1 deletion apps/app/src/hooks/use-example-suggestions.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -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: "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." },
{ 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." },
],
Expand Down
18 changes: 18 additions & 0 deletions apps/app/src/hooks/use-generative-ui-examples.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -7,6 +7,7 @@ import {
useFrontendTool,
useHumanInTheLoop,
useDefaultRenderTool,
useRenderTool,
} from "@copilotkit/react-core/v2";

// Generative UI imports
Expand All @@ -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();
Expand Down Expand Up @@ -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 }) => (
<PlanCard status={status} {...parameters} />
),
});

// --------------------------
// 🪁 Default Tool Rendering: https://docs.copilotkit.ai/langgraph/generative-ui/backend-tools
// --------------------------
Expand Down