Skip to content
Open
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 @@ -10,14 +10,15 @@
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 skills import load_all_skills

# Load all visualization skills
_skills_text = load_all_skills()

agent = create_agent(
model=ChatOpenAI(model="gpt-5.4-2026-03-05"),
tools=[query_data, *todo_tools, generate_form],
tools=[query_data, *todo_tools, generate_form, *template_tools],
middleware=[CopilotKitMiddleware()],
state_schema=AgentState,
system_prompt=f"""
Expand Down Expand Up @@ -47,6 +48,23 @@
Follow the skills below for how to produce high-quality visuals:

{_skills_text}

## UI Templates

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 a user asks to apply/recreate a template with new data:**
The message includes the template name and ID in the format: "template name" (template-id)
1. Call `apply_template(template_id="...")` with the ID from the message
2. Take the returned HTML and COPY IT EXACTLY, only replacing the data values
(names, numbers, dates, labels, amounts) to match the user's new data
3. Render the modified HTML using `widgetRenderer`

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.
""",
)

Expand Down
162 changes: 162 additions & 0 deletions apps/agent/src/templates.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,162 @@
from langchain.tools import ToolRuntime, tool
from langchain.messages import ToolMessage
from langgraph.types import Command
from typing import Any, Optional, TypedDict
import uuid
from datetime import datetime


class UITemplate(TypedDict, total=False):
id: str
name: str
description: str
html: str
data_description: str
created_at: str
version: int
component_type: Optional[str]
component_data: Optional[dict[str, Any]]


@tool
def save_template(
name: str,
description: str,
html: str,
data_description: str,
runtime: ToolRuntime,
) -> Command:
"""
Save a generated UI as a reusable template.
Call this when the user wants to save a widget/visualization they liked for reuse later.

Args:
name: Short name for the template (e.g. "Invoice", "Dashboard")
description: What the template displays or does
html: The raw HTML string of the widget to save as a template
data_description: Description of the data shape this template expects
"""
templates = list(runtime.state.get("templates", []))

template: UITemplate = {
"id": str(uuid.uuid4()),
"name": name,
"description": description,
"html": html,
"data_description": data_description,
"created_at": datetime.now().isoformat(),
"version": 1,
}
templates.append(template)

return Command(update={
"templates": templates,
"messages": [
ToolMessage(
content=f"Template '{name}' saved successfully (id: {template['id']})",
tool_call_id=runtime.tool_call_id,
)
],
})


@tool
def list_templates(runtime: ToolRuntime):
"""
List all saved UI templates. Returns template summaries (id, name, description, data_description).
"""
templates = runtime.state.get("templates", [])
return [
{
"id": t["id"],
"name": t["name"],
"description": t["description"],
"data_description": t["data_description"],
"version": t["version"],
}
for t in templates
]


@tool
def apply_template(name: str = "", template_id: str = "", runtime: ToolRuntime = None):
"""
Retrieve a saved template's HTML so you can adapt it with new data.
After calling this, generate a NEW widget in the same style and render via widgetRenderer.

You can look up by name or ID. If both are provided, ID takes priority.
When multiple templates share the same name, returns the most recently created one.

Args:
name: The name of the template to apply (e.g. "Invoice")
template_id: The ID of the template to apply (optional)
"""
templates = runtime.state.get("templates", [])

# Look up by ID first
if template_id:
for t in templates:
if t["id"] == template_id:
return {
"name": t["name"],
"description": t["description"],
"html": t["html"],
"data_description": t.get("data_description", ""),
}
return {"error": f"Template with id '{template_id}' not found"}

# Look up by name (most recent match)
if name:
matches = [t for t in templates if t["name"].lower() == name.lower()]
if matches:
t = max(matches, key=lambda x: x.get("created_at", ""))
return {
"name": t["name"],
"description": t["description"],
"html": t["html"],
"data_description": t.get("data_description", ""),
}
return {"error": f"No template named '{name}' found"}

return {"error": "Provide either a name or template_id"}


@tool
def delete_template(template_id: str, runtime: ToolRuntime) -> Command:
"""
Delete a saved UI template.

Args:
template_id: The ID of the template to delete
"""
templates = list(runtime.state.get("templates", []))
original_len = len(templates)
templates = [t for t in templates if t["id"] != template_id]

if len(templates) == original_len:
return Command(update={
"messages": [
ToolMessage(
content=f"Template with id '{template_id}' not found",
tool_call_id=runtime.tool_call_id,
)
],
})

return Command(update={
"templates": templates,
"messages": [
ToolMessage(
content=f"Template deleted successfully",
tool_call_id=runtime.tool_call_id,
)
],
})


template_tools = [
save_template,
list_templates,
apply_template,
delete_template,
]
3 changes: 3 additions & 0 deletions apps/agent/src/todos.py
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,8 @@
from typing import TypedDict, Literal
import uuid

from src.templates import UITemplate

class Todo(TypedDict):
id: str
title: str
Expand All @@ -14,6 +16,7 @@ class Todo(TypedDict):

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

@tool
def manage_todos(todos: list[Todo], runtime: ToolRuntime) -> Command:
Expand Down
16 changes: 16 additions & 0 deletions apps/app/src/app/globals.css
Original file line number Diff line number Diff line change
Expand Up @@ -621,3 +621,19 @@ body, html {
@keyframes spin {
to { transform: rotate(360deg); }
}

/* Template save animations */
@keyframes tmpl-pop {
0% { transform: scale(0.8); opacity: 0; }
50% { transform: scale(1.05); }
100% { transform: scale(1); opacity: 1; }
}

@keyframes tmpl-check {
to { stroke-dashoffset: 0; }
}

@keyframes tmpl-slideIn {
from { transform: translateY(-4px) scale(0.97); opacity: 0; }
to { transform: translateY(0) scale(1); opacity: 1; }
}
59 changes: 44 additions & 15 deletions apps/app/src/app/page.tsx
Original file line number Diff line number Diff line change
@@ -1,17 +1,20 @@
"use client";

import { useEffect } from "react";
import { useEffect, useState } 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 { CopilotChat } from "@copilotkit/react-core/v2";

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

// Widget bridge: handle openLink from widget iframes
const [templateDrawerOpen, setTemplateDrawerOpen] = useState(false);

// Widget bridge: handle messages from widget iframes
useEffect(() => {
const handler = (e: MessageEvent) => {
if (e.data?.type === "open-link" && typeof e.data.url === "string") {
Expand All @@ -22,6 +25,7 @@ export default function HomePage() {
return () => window.removeEventListener("message", handler);
}, []);


return (
<>
{/* Animated background */}
Expand Down Expand Up @@ -58,19 +62,38 @@ export default function HomePage() {
<span className="font-normal" style={{ color: "var(--text-secondary)" }}> — powered by CopilotKit</span>
</p>
</div>
<a
href="https://github.com/CopilotKit/OpenGenerativeUI"
target="_blank"
rel="noopener noreferrer"
className="inline-flex items-center px-5 py-2 rounded-full text-sm font-semibold text-white no-underline whitespace-nowrap transition-all duration-150 hover:-translate-y-px"
style={{
background: "linear-gradient(135deg, var(--color-lilac-dark), var(--color-mint-dark))",
boxShadow: "0 1px 4px rgba(149,153,204,0.3)",
fontFamily: "var(--font-family)",
}}
>
Get started
</a>
<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"
rel="noopener noreferrer"
className="inline-flex items-center px-5 py-2 rounded-full text-sm font-semibold text-white no-underline whitespace-nowrap transition-all duration-150 hover:-translate-y-px"
style={{
background: "linear-gradient(135deg, var(--color-lilac-dark), var(--color-mint-dark))",
boxShadow: "0 1px 4px rgba(149,153,204,0.3)",
fontFamily: "var(--font-family)",
}}
>
Get started
</a>
</div>
</div>
</div>

Expand All @@ -84,6 +107,12 @@ export default function HomePage() {
<ExplainerCardsPortal />
</div>
</div>

{/* Template Library Drawer */}
<TemplateLibrary
open={templateDrawerOpen}
onClose={() => setTemplateDrawerOpen(false)}
/>
</>
);
}
Loading