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
25 changes: 1 addition & 24 deletions apps/agent/main.py
Original file line number Diff line number Diff line change
Expand Up @@ -18,13 +18,12 @@
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

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

## 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:**
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

If no `pending_template` is set but the user mentions a template by name, use
`apply_template(name="...")` instead.

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
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
38 changes: 22 additions & 16 deletions apps/app/src/app/page.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -4,16 +4,23 @@ 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 { TemplateChip } from "@/components/template-library/template-chip";
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 [templateDrawerOpen, setTemplateDrawerOpen] = useState(false);
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(() => {
Expand Down Expand Up @@ -60,22 +67,24 @@ 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"
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 Template Library"
title="Open Demo Gallery"
>
<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" />
<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>
Templates
Demos
</button>
<a
href="https://github.com/CopilotKit/OpenGenerativeUI"
Expand Down Expand Up @@ -105,13 +114,10 @@ export default function HomePage() {
</div>
</div>

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

{/* Template Library Drawer */}
<TemplateLibrary
open={templateDrawerOpen}
onClose={() => setTemplateDrawerOpen(false)}
<DemoGallery
open={demoDrawerOpen}
onClose={() => setDemoDrawerOpen(false)}
onTryDemo={handleTryDemo}
/>
</>
);
Expand Down
39 changes: 39 additions & 0 deletions apps/app/src/components/demo-gallery/category-filter.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,39 @@
"use client";

import type { DemoCategory } from "./demo-data";
import { DEMO_CATEGORIES } from "./demo-data";

interface CategoryFilterProps {
selected: DemoCategory | null;
onSelect: (category: DemoCategory | null) => void;
}

export function CategoryFilter({ selected, onSelect }: CategoryFilterProps) {
const categories: (DemoCategory | null)[] = [null, ...DEMO_CATEGORIES];

return (
<div className="flex gap-2 overflow-x-auto pb-1 scrollbar-none">
{categories.map((cat) => {
const isActive = cat === selected;
return (
<button
key={cat ?? "all"}
onClick={() => onSelect(cat)}
className="shrink-0 px-3 py-1.5 rounded-full text-xs font-medium transition-all duration-150 cursor-pointer"
style={{
background: isActive
? "linear-gradient(135deg, var(--color-lilac-dark, #6366f1), var(--color-mint-dark, #10b981))"
: "var(--surface-primary, rgba(255,255,255,0.6))",
color: isActive ? "#fff" : "var(--text-secondary, #666)",
border: isActive
? "none"
: "1px solid var(--color-border-glass, rgba(0,0,0,0.1))",
}}
>
{cat ?? "All"}
</button>
);
})}
</div>
);
}
109 changes: 109 additions & 0 deletions apps/app/src/components/demo-gallery/demo-card.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,109 @@
"use client";

import type { DemoItem } from "./demo-data";

// Category badge colors
const CATEGORY_COLORS: Record<string, { bg: string; text: string }> = {
"3D / Animation": { bg: "rgba(139,92,246,0.12)", text: "rgba(139,92,246,1)" },
"Data Visualization": { bg: "rgba(59,130,246,0.12)", text: "rgba(59,130,246,1)" },
Diagrams: { bg: "rgba(16,185,129,0.12)", text: "rgba(16,185,129,1)" },
Interactive: { bg: "rgba(245,158,11,0.12)", text: "rgba(245,158,11,1)" },
"UI Components": { bg: "rgba(236,72,153,0.12)", text: "rgba(236,72,153,1)" },
};

// Emoji background gradients
const EMOJI_GRADIENTS: Record<string, string> = {
"3D / Animation":
"linear-gradient(135deg, rgba(139,92,246,0.08) 0%, rgba(59,130,246,0.06) 100%)",
"Data Visualization":
"linear-gradient(135deg, rgba(59,130,246,0.08) 0%, rgba(16,185,129,0.06) 100%)",
Diagrams:
"linear-gradient(135deg, rgba(16,185,129,0.08) 0%, rgba(59,130,246,0.06) 100%)",
Interactive:
"linear-gradient(135deg, rgba(245,158,11,0.08) 0%, rgba(236,72,153,0.06) 100%)",
"UI Components":
"linear-gradient(135deg, rgba(236,72,153,0.08) 0%, rgba(139,92,246,0.06) 100%)",
};

interface DemoCardProps {
demo: DemoItem;
onTry: (demo: DemoItem) => void;
}

export function DemoCard({ demo, onTry }: DemoCardProps) {
const categoryColor = CATEGORY_COLORS[demo.category] ?? {
bg: "rgba(100,100,100,0.12)",
text: "rgba(100,100,100,1)",
};

return (
<div
className="rounded-xl overflow-hidden flex flex-col transition-all duration-200 hover:shadow-lg hover:-translate-y-0.5"
style={{
border: "1px solid var(--color-border-glass, rgba(0,0,0,0.1))",
background: "var(--surface-primary, #fff)",
}}
>
{/* Preview area */}
<div
className="relative overflow-hidden"
style={{
height: 160,
background:
EMOJI_GRADIENTS[demo.category] ??
"var(--color-background-secondary)",
}}
>
<div className="flex items-center justify-center h-full">
<span className="text-5xl" role="img" aria-label={demo.title}>
{demo.emoji}
</span>
</div>

{/* Category badge */}
<span
className="absolute top-2 right-2 text-[10px] font-semibold px-2 py-0.5 rounded-full"
style={{
background: categoryColor.bg,
color: categoryColor.text,
}}
>
{demo.category}
</span>
</div>

{/* Info */}
<div className="flex flex-col gap-1 p-3 flex-1">
<div className="flex items-center gap-2">
<span className="text-base">{demo.emoji}</span>
<h3
className="text-sm font-semibold truncate"
style={{ color: "var(--text-primary, #1a1a1a)" }}
>
{demo.title}
</h3>
</div>
<p
className="text-xs line-clamp-2"
style={{ color: "var(--text-secondary, #666)" }}
>
{demo.description}
</p>
</div>

{/* Action */}
<div className="p-3 pt-0">
<button
onClick={() => onTry(demo)}
className="w-full text-xs font-medium py-2 rounded-lg transition-all duration-150 hover:scale-[1.02] text-white cursor-pointer"
style={{
background:
"linear-gradient(135deg, var(--color-lilac-dark, #6366f1), var(--color-mint-dark, #10b981))",
}}
>
Try it
</button>
</div>
</div>
);
}
Loading
Loading