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
47 changes: 28 additions & 19 deletions apps/agent/main.py
Original file line number Diff line number Diff line change
Expand Up @@ -18,13 +18,13 @@
from src.query import query_data
from src.todos import AgentState, todo_tools
from src.form import generate_form
from src.templates import template_tools
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, *template_tools],
tools=[query_data, plan_visualization, *todo_tools, generate_form],
middleware=[CopilotKitMiddleware()],
context_schema=AgentState,
skills=[str(Path(__file__).parent / "skills")],
Expand Down Expand Up @@ -53,27 +53,36 @@
- 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.)

## UI Templates
## Visualization Workflow (MANDATORY)

Users can save generated UIs as reusable templates and apply them later.
You have backend tools: `save_template`, `list_templates`, `apply_template`, `delete_template`.
When producing ANY visual response (widgetRenderer, pieChart, barChart), you MUST
follow this exact sequence:

**When a user asks to apply/recreate a template with new data:**
Check `pending_template` in state — the frontend sets this when the user picks a template.
If `pending_template` is present (has `id` and `name`):
1. Call `apply_template(template_id=pending_template["id"])` to retrieve the HTML
2. Take the returned HTML and COPY IT EXACTLY, only replacing the data values
(names, numbers, dates, labels, amounts) to match the user's message
3. Render the modified HTML using `widgetRenderer`
4. Call `clear_pending_template` to reset the pending state
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.

If no `pending_template` is set but the user mentions a template by name, use
`apply_template(name="...")` instead.
NEVER skip the plan_visualization step. NEVER call widgetRenderer, pieChart, or
barChart without calling plan_visualization first.

CRITICAL: Do NOT rewrite or generate HTML from scratch. Take the original HTML string,
find-and-replace ONLY the data values, and pass the result to widgetRenderer.
This preserves the exact layout and styling of the original template.
For bar/pie chart templates, use `barChart` or `pieChart` component instead.
## Visualization Quality Standards

The iframe has an import map with these ES module libraries — use `<script type="module">` and bare import specifiers:
- `three` — 3D graphics. `import * as THREE from "three"`. Also `three/examples/jsm/controls/OrbitControls.js` for camera controls.
- `gsap` — animation. `import gsap from "gsap"`.
- `d3` — data visualization and force layouts. `import * as d3 from "d3"`.
- `chart.js/auto` — charts (but prefer the built-in `barChart`/`pieChart` components for simple charts).

**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.

**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.

**Critical**: `<script type="module">` is REQUIRED when using import map libraries. Regular `<script>` tags cannot use `import` statements.
""",
)

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}"
10 changes: 1 addition & 9 deletions apps/agent/src/todos.py
Original file line number Diff line number Diff line change
Expand Up @@ -2,26 +2,18 @@
from langchain.tools import ToolRuntime, tool
from langchain.messages import ToolMessage
from langgraph.types import Command
from typing import Optional, TypedDict, Literal
from typing import TypedDict, Literal
import uuid

from src.templates import UITemplate

class Todo(TypedDict):
id: str
title: str
description: str
emoji: str
status: Literal["pending", "completed"]

class PendingTemplate(TypedDict, total=False):
id: str
name: str

class AgentState(BaseAgentState):
todos: list[Todo]
templates: list[UITemplate]
pending_template: Optional[PendingTemplate]

@tool
def manage_todos(todos: list[Todo], runtime: ToolRuntime) -> Command:
Expand Down
33 changes: 2 additions & 31 deletions apps/app/src/app/page.tsx
Original file line number Diff line number Diff line change
@@ -1,20 +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 { TemplateLibrary } from "@/components/template-library";
import { TemplateChip } from "@/components/template-library/template-chip";

import { CopilotChat } from "@copilotkit/react-core/v2";

export default function HomePage() {
useGenerativeUIExamples();
useExampleSuggestions();

const [templateDrawerOpen, setTemplateDrawerOpen] = useState(false);

// Widget bridge: handle messages from widget iframes
useEffect(() => {
const handler = (e: MessageEvent) => {
Expand Down Expand Up @@ -60,23 +55,6 @@ export default function HomePage() {
</p>
</div>
<div className="flex items-center gap-2">
{/* Template Library toggle */}
<button
onClick={() => setTemplateDrawerOpen(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"
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 Template Library"
>
<svg width="15" height="15" viewBox="0 0 24 24" fill="none" stroke="currentColor" strokeWidth="2" strokeLinecap="round" strokeLinejoin="round">
<path d="m19 21-7-4-7 4V5a2 2 0 0 1 2-2h10a2 2 0 0 1 2 2v16z" />
</svg>
Templates
</button>
<a
href="https://github.com/CopilotKit/OpenGenerativeUI"
target="_blank"
Expand All @@ -98,21 +76,14 @@ export default function HomePage() {
<CopilotChat
labels={{
welcomeMessageText: "What do you want to visualize today?",
chatDisclaimerText: "Visualizations are AI-generated. You can retry the same prompt or ask the AI to refine the result.",
}}
/>
} />
<ExplainerCardsPortal />
</div>
</div>

{/* Template chip — portal renders above chat input */}
<TemplateChip />

{/* Template Library Drawer */}
<TemplateLibrary
open={templateDrawerOpen}
onClose={() => setTemplateDrawerOpen(false)}
/>
</>
);
}
7 changes: 3 additions & 4 deletions apps/app/src/components/generative-ui/charts/bar-chart.tsx
Original file line number Diff line number Diff line change
@@ -1,7 +1,7 @@
import { BarChart as RechartsBarChart, Bar, XAxis, YAxis, Tooltip, ResponsiveContainer } from 'recharts';
import { z } from 'zod';
import { CHART_COLORS, CHART_CONFIG } from './config';
import { SaveTemplateOverlay } from '../save-template-overlay';
import { ExportOverlay } from '../export-overlay';

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

return (
<SaveTemplateOverlay
<ExportOverlay
title={title}
description={description}
componentType="barChart"
componentData={{ title, description, data }}
>
Expand All @@ -66,6 +65,6 @@ export function BarChart({ title, description, data }: BarChartProps) {
</RechartsBarChart>
</ResponsiveContainer>
</div>
</SaveTemplateOverlay>
</ExportOverlay>
);
}
7 changes: 3 additions & 4 deletions apps/app/src/components/generative-ui/charts/pie-chart.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -6,7 +6,7 @@ import {
} from "recharts";
import { z } from "zod";
import { CHART_COLORS, CHART_CONFIG } from "./config";
import { SaveTemplateOverlay } from "../save-template-overlay";
import { ExportOverlay } from "../export-overlay";

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

return (
<SaveTemplateOverlay
<ExportOverlay
title={title}
description={description}
componentType="pieChart"
componentData={{ title, description, data }}
>
Expand Down Expand Up @@ -94,6 +93,6 @@ export function PieChart({ title, description, data }: PieChartProps) {
))}
</div>
</div>
</SaveTemplateOverlay>
</ExportOverlay>
);
}
Original file line number Diff line number Diff line change
Expand Up @@ -8,24 +8,23 @@ import {
slugify,
} from "./export-utils";

interface SaveTemplateOverlayProps {
interface ExportOverlayProps {
title: string;
description: string;
html?: string;
componentData?: Record<string, unknown>;
componentType: string;
ready?: boolean;
children: ReactNode;
}

export function SaveTemplateOverlay({
export function ExportOverlay({
title,
html,
componentData,
componentType,
ready = true,
children,
}: SaveTemplateOverlayProps) {
}: ExportOverlayProps) {
const [copyState, setCopyState] = useState<"idle" | "copied">("idle");
const [menuOpen, setMenuOpen] = useState(false);
const [hovered, setHovered] = useState(false);
Expand Down Expand Up @@ -67,11 +66,17 @@ export function SaveTemplateOverlay({
const handleCopy = useCallback(() => {
const textToCopy = componentType === "widgetRenderer" ? html : exportHtml;
if (!textToCopy) return;
navigator.clipboard.writeText(textToCopy).then(() => {
setCopyState("copied");
setMenuOpen(false);
setTimeout(() => setCopyState("idle"), 1800);
});
navigator.clipboard.writeText(textToCopy).then(
() => {
setCopyState("copied");
setMenuOpen(false);
setTimeout(() => setCopyState("idle"), 1800);
},
() => {
// Clipboard write failed (e.g. permission denied, iframe context)
setMenuOpen(false);
}
);
}, [componentType, html, exportHtml]);

const showTrigger = ready && exportHtml && (hovered || menuOpen);
Expand Down
3 changes: 1 addition & 2 deletions apps/app/src/components/generative-ui/export-utils.ts
Original file line number Diff line number Diff line change
@@ -1,5 +1,4 @@
import { THEME_CSS } from "./widget-renderer";
import { SVG_CLASSES_CSS, FORM_STYLES_CSS } from "./widget-renderer";
import { THEME_CSS, SVG_CLASSES_CSS, FORM_STYLES_CSS } from "./widget-renderer";

const CHART_COLORS = [
"#3b82f6",
Expand Down
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;
keyElements?: string[];
}

export function PlanCard({ status, approach, technology, keyElements }: 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>
{keyElements && keyElements.length > 0 && (
<ul className="list-disc pl-4 space-y-0.5">
{keyElements.map((el, i) => (
<li key={i}>{el}</li>
))}
</ul>
)}
</div>
)}
</details>
</div>
);
}
Loading
Loading