Skip to content

Commit 6b1efa5

Browse files
committed
feat: add download and copy-as-code for generated components
Add the ability to download any generated visualization as a standalone HTML file (with animations preserved) and copy the source code to clipboard. This addresses the artifact export story needed for the upcoming component gallery. - New export-utils.ts with assembleStandaloneHtml (widgets), chartToStandaloneHtml (bar/pie charts), and triggerDownload - Download and copy buttons added to SaveTemplateOverlay, visible on all generated components (widgets, bar charts, pie charts) - Export SVG_CLASSES_CSS and FORM_STYLES_CSS from widget-renderer - Update demo-gallery plan with Variant-style layout and #55 context Closes #14 Closes #42
1 parent 259e92c commit 6b1efa5

File tree

4 files changed

+471
-18
lines changed

4 files changed

+471
-18
lines changed

.chalk/plans/demo-gallery.md

Lines changed: 156 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,156 @@
1+
# Demo Gallery — Component Gallery with Download
2+
3+
## Context
4+
5+
The current template system (a drawer of raw HTML strings) doesn't communicate the value of agent-generated UI. We're replacing it with a **gallery of curated, saved conversation outputs** — each item is a snapshot: **1 user message → 1 generated component**. This lets visitors immediately see what the agent produces and interact with real outputs.
6+
7+
Relates to issues #49, #55. Depends on #14/#42 (download/copy-as-code) for the export utilities.
8+
9+
### Gallery items showcase two axes:
10+
11+
**1. Explainers & Visualizations** — Educational/explanatory generated components (e.g. "How a Plane Flies" interactive 3D explainer, binary search step-through, solar system model, neural network forward pass animation). Demonstrates the agent's ability to generate rich, interactive educational content from a simple prompt.
12+
13+
**2. Custom UI & Styled Components** — Generated components matching a specific design language or brand style (e.g. Clippy-style assistant UI, Spotify-inspired music dashboard, invoice card in a specific design system, themed dashboards). Demonstrates that generated UI isn't generic — the agent can produce components that feel intentionally designed.
14+
15+
### Each gallery item includes:
16+
- The original user prompt (1 message)
17+
- The generated component output (rendered, interactive)
18+
- Ability to **download** the generated component (via `export-utils.ts` from #14/#42)
19+
20+
## Layout — Variant-style (reference: variant.com)
21+
22+
**Current:** Chat is the main view. Template drawer slides over from right.
23+
24+
**New:** Inspired by variant.com — a fixed left panel with hero text + chat input, and a scrollable masonry grid of generated components filling the rest of the viewport.
25+
26+
```
27+
+--------------------+--------------------------------------------+
28+
| Logo | |
29+
| | [Card] [Card - tall] [Card] |
30+
| Endless generated | [Card - wide] [Card] |
31+
| UIs for your | [Card] [Card] [Card - tall] |
32+
| ideas, just scroll.| [Card] [Card - wide] |
33+
| | [Card - tall] [Card] [Card] |
34+
| Description text | ... infinite scroll ... |
35+
| | |
36+
| [Sign up] [Surprise| |
37+
| me] | |
38+
| | |
39+
| +----------------+ | |
40+
| | Chat input... | | |
41+
| +----------------+ | |
42+
+--------------------+--------------------------------------------+
43+
```
44+
45+
### Key layout properties:
46+
- **Left panel** (~340px, fixed): Logo, hero tagline, description, CTAs ("Sign up / Sign in", "Surprise me"), and a chat input pinned to the bottom. This panel does NOT scroll.
47+
- **Right area** (fills remaining width): Masonry grid of gallery cards that scrolls independently. Cards vary in size based on their content aspect ratio.
48+
- **Cards**: Dark themed, rounded corners, show live interactive iframe previews of generated components. Each card has a hover overlay with download button and "Try it" action.
49+
- **No category filter pills**: The masonry grid itself communicates variety. Items from both axes (explainers + styled UI) are interleaved.
50+
- **Chat input**: Lives at the bottom of the left panel (not a separate side panel). Typing a prompt generates a new component that appears in the grid.
51+
- **Dark theme**: The overall page uses a dark background to make the component previews pop.
52+
53+
## Files to Create
54+
55+
### 1. `apps/app/src/components/demo-gallery/gallery-data.ts`
56+
Gallery item definitions. Each item is a conversation snapshot: 1 user prompt → 1 generated component.
57+
58+
**Interface:**
59+
```ts
60+
export interface GalleryItem {
61+
id: string;
62+
title: string;
63+
description: string;
64+
axis: "explainer" | "styled"; // which showcase axis
65+
prompt: string; // the original user message
66+
html?: string; // generated HTML output (for live preview + download)
67+
component_type?: string;
68+
component_data?: Record<string, unknown>;
69+
size?: "normal" | "tall" | "wide"; // masonry size hint
70+
}
71+
```
72+
73+
**Curated items (~10), interleaved from both axes:**
74+
75+
*Explainers & Visualizations:*
76+
- How a Plane Flies — interactive 3D explainer
77+
- Binary Search — step-through visualization
78+
- Solar System — orbiting planets
79+
- Neural Network — animated forward pass
80+
- Sorting Comparison — bubble sort vs quicksort
81+
82+
*Custom UI & Styled Components:*
83+
- Weather Dashboard (reuse seed HTML)
84+
- KPI Dashboard (reuse seed HTML)
85+
- Invoice Card (reuse seed HTML)
86+
- Pomodoro Timer
87+
- Bike Battery Widget (like variant.com reference)
88+
89+
Items with pre-rendered `html` show live iframe previews. Items without show a styled placeholder with the prompt text.
90+
91+
### 2. `apps/app/src/components/demo-gallery/gallery-card.tsx`
92+
Masonry card component. Dark themed, rounded corners.
93+
- Shows live iframe preview (scaled down) of the generated component
94+
- Hover overlay: download button (uses `export-utils.ts`), "Try it" button
95+
- Card `size` prop controls CSS grid span (normal=1x1, tall=1x2, wide=2x1)
96+
97+
### 3. `apps/app/src/components/demo-gallery/index.tsx`
98+
Main gallery layout: fixed left panel + scrollable masonry grid.
99+
- Left panel: logo, hero text, CTAs, chat input at bottom
100+
- Right area: CSS grid masonry of `GalleryCard` components
101+
- No category filters — items from both axes interleaved for variety
102+
103+
## Files to Modify
104+
105+
### 4. `apps/app/src/app/page.tsx`
106+
Major layout restructure:
107+
- Replace current `ExampleLayout` + `CopilotChat` with the new gallery layout
108+
- The gallery component (`demo-gallery/index.tsx`) becomes the full-page view
109+
- Chat input is embedded in the left panel, not a separate component
110+
- "Try it" on a card sends the prompt to the agent and scrolls to / highlights the new output
111+
- "Surprise me" button picks a random prompt from gallery data and sends it
112+
113+
### 5. `apps/app/src/components/template-library/seed-templates.ts`
114+
Keep this file — the HTML strings are reused by `gallery-data.ts` for items that have live previews. Import from here rather than duplicating.
115+
116+
### 6. `apps/app/src/hooks/use-example-suggestions.tsx`
117+
May be replaced or simplified — the gallery itself serves as the suggestion surface now.
118+
119+
## Files to Remove
120+
121+
### 7. `apps/app/src/components/template-library/index.tsx` and `template-card.tsx`
122+
The template drawer is replaced by the gallery. `save-template-overlay.tsx` stays (it serves the in-chat widget interaction and now includes download/copy buttons from #14/#42).
123+
124+
## Dependencies
125+
126+
### Download/export (prerequisite — issues #14/#42)
127+
The gallery cards need download buttons. The `export-utils.ts` module (from the download PR) provides `assembleStandaloneHtml`, `triggerDownload`, and `slugify`. The gallery card hover overlay will import these directly.
128+
129+
### Sending messages programmatically
130+
Need to verify the CopilotKit v2 API for programmatic message sending. Options:
131+
1. `agent.sendMessage(text)` — if available in v2
132+
2. `agent.addMessage({ role: "user", content: text }) + agent.runAgent()`
133+
3. Fallback: programmatically set textarea value and dispatch submit
134+
135+
## Implementation Order
136+
137+
1. **(Prerequisite)** Land download/copy-as-code PR (#14/#42) — provides `export-utils.ts`
138+
2. Create `gallery-data.ts` with curated items from both axes
139+
3. Create `gallery-card.tsx` — dark card with iframe preview + hover overlay
140+
4. Create `demo-gallery/index.tsx` — left panel + masonry grid layout
141+
5. Restructure `page.tsx` — replace current layout with gallery
142+
6. Wire up "Try it" → send prompt to agent
143+
7. Wire up "Surprise me" → random prompt
144+
8. Wire up download button on cards (reuse `export-utils.ts`)
145+
9. Clean up old template drawer files
146+
147+
## Verification
148+
149+
1. `pnpm dev:app` — app builds and renders gallery as default view
150+
2. Gallery shows masonry grid of ~10 cards with live iframe previews
151+
3. Left panel has hero text, CTAs, and chat input
152+
4. Hovering a card shows download + "Try it" overlay
153+
5. Clicking "Try it" sends the prompt and generates a new component
154+
6. Download button produces a standalone `.html` file with working animations
155+
7. "Surprise me" picks a random prompt
156+
8. Dark theme looks clean, cards pop against background
Lines changed: 175 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,175 @@
1+
import { THEME_CSS } from "./widget-renderer";
2+
import { SVG_CLASSES_CSS, FORM_STYLES_CSS } from "./widget-renderer";
3+
4+
const CHART_COLORS = [
5+
"#3b82f6",
6+
"#8b5cf6",
7+
"#ec4899",
8+
"#f59e0b",
9+
"#10b981",
10+
"#06b6d4",
11+
"#f97316",
12+
];
13+
14+
// Import map matching widget-renderer's assembleShell — allows widgets that
15+
// use bare specifiers (e.g. `import * as THREE from "three"`) to work standalone.
16+
const IMPORT_MAP = `<script type="importmap">
17+
{
18+
"imports": {
19+
"three": "https://esm.sh/three",
20+
"three/": "https://esm.sh/three/",
21+
"gsap": "https://esm.sh/gsap",
22+
"gsap/": "https://esm.sh/gsap/",
23+
"d3": "https://esm.sh/d3",
24+
"d3/": "https://esm.sh/d3/",
25+
"chart.js": "https://esm.sh/chart.js",
26+
"chart.js/": "https://esm.sh/chart.js/"
27+
}
28+
}
29+
</script>`;
30+
31+
/**
32+
* Wrap a raw HTML fragment (the same string passed to WidgetRenderer)
33+
* in a standalone document that works when opened in a browser.
34+
*/
35+
export function assembleStandaloneHtml(html: string, title: string): string {
36+
return `<!DOCTYPE html>
37+
<html>
38+
<head>
39+
<meta charset="utf-8">
40+
<meta name="viewport" content="width=device-width, initial-scale=1">
41+
<title>${escapeHtml(title)}</title>
42+
${IMPORT_MAP}
43+
<style>
44+
${THEME_CSS}
45+
${SVG_CLASSES_CSS}
46+
${FORM_STYLES_CSS}
47+
</style>
48+
</head>
49+
<body>
50+
<div id="content">
51+
${html}
52+
</div>
53+
<script>
54+
// Stub bridge functions so onclick="sendPrompt(...)" doesn't throw
55+
window.sendPrompt = function() {};
56+
window.openLink = function(url) { if (url) window.open(url, '_blank'); };
57+
document.addEventListener('click', function(e) {
58+
var a = e.target.closest('a[href]');
59+
if (a && a.href.startsWith('http')) {
60+
e.preventDefault();
61+
window.open(a.href, '_blank');
62+
}
63+
});
64+
</script>
65+
</body>
66+
</html>`;
67+
}
68+
69+
/**
70+
* Generate a standalone HTML file that renders a chart using Chart.js from CDN.
71+
*/
72+
export function chartToStandaloneHtml(
73+
type: "bar" | "pie",
74+
data: { title: string; description: string; data: Array<{ label: string; value: number }> }
75+
): string {
76+
const labels = JSON.stringify(data.data.map((d) => d.label));
77+
const values = JSON.stringify(data.data.map((d) => d.value));
78+
const colors = JSON.stringify(
79+
data.data.map((_, i) => CHART_COLORS[i % CHART_COLORS.length])
80+
);
81+
82+
const chartConfig =
83+
type === "bar"
84+
? `{
85+
type: 'bar',
86+
data: {
87+
labels: ${labels},
88+
datasets: [{
89+
data: ${values},
90+
backgroundColor: ${colors},
91+
borderRadius: 4,
92+
}]
93+
},
94+
options: {
95+
responsive: true,
96+
plugins: {
97+
legend: { display: false },
98+
tooltip: { backgroundColor: '#1f2937', titleColor: '#fff', bodyColor: '#fff', cornerRadius: 8, padding: 10 }
99+
},
100+
scales: {
101+
x: { grid: { display: false } },
102+
y: { grid: { color: 'rgba(0,0,0,0.06)' } }
103+
}
104+
}
105+
}`
106+
: `{
107+
type: 'pie',
108+
data: {
109+
labels: ${labels},
110+
datasets: [{
111+
data: ${values},
112+
backgroundColor: ${colors},
113+
}]
114+
},
115+
options: {
116+
responsive: true,
117+
plugins: {
118+
legend: { position: 'bottom', labels: { padding: 16, usePointStyle: true } },
119+
tooltip: { backgroundColor: '#1f2937', titleColor: '#fff', bodyColor: '#fff', cornerRadius: 8, padding: 10 }
120+
}
121+
}
122+
}`;
123+
124+
return `<!DOCTYPE html>
125+
<html>
126+
<head>
127+
<meta charset="utf-8">
128+
<meta name="viewport" content="width=device-width, initial-scale=1">
129+
<title>${escapeHtml(data.title)}</title>
130+
<style>
131+
${THEME_CSS}
132+
body { font-family: system-ui, -apple-system, sans-serif; padding: 24px; max-width: 640px; margin: 0 auto; }
133+
h3 { font-size: 20px; font-weight: 700; margin: 0 0 4px; color: var(--color-text-primary); }
134+
p { font-size: 14px; color: var(--color-text-secondary); margin: 0 0 20px; }
135+
canvas { max-height: 360px; }
136+
</style>
137+
</head>
138+
<body>
139+
<h3>${escapeHtml(data.title)}</h3>
140+
<p>${escapeHtml(data.description)}</p>
141+
<canvas id="chart"></canvas>
142+
<script src="https://cdn.jsdelivr.net/npm/chart.js@4/dist/chart.umd.min.js"></script>
143+
<script>
144+
new Chart(document.getElementById('chart'), ${chartConfig});
145+
</script>
146+
</body>
147+
</html>`;
148+
}
149+
150+
export function slugify(text: string): string {
151+
return text
152+
.toLowerCase()
153+
.replace(/[^a-z0-9]+/g, "-")
154+
.replace(/^-+|-+$/g, "");
155+
}
156+
157+
export function triggerDownload(htmlString: string, filename: string): void {
158+
const blob = new Blob([htmlString], { type: "text/html" });
159+
const url = URL.createObjectURL(blob);
160+
const a = document.createElement("a");
161+
a.href = url;
162+
a.download = filename;
163+
document.body.appendChild(a);
164+
a.click();
165+
document.body.removeChild(a);
166+
URL.revokeObjectURL(url);
167+
}
168+
169+
function escapeHtml(text: string): string {
170+
return text
171+
.replace(/&/g, "&amp;")
172+
.replace(/</g, "&lt;")
173+
.replace(/>/g, "&gt;")
174+
.replace(/"/g, "&quot;");
175+
}

0 commit comments

Comments
 (0)