@kazzle/app 0.1.693

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (103) hide show
  1. package/README.md +63 -0
  2. package/dist/client.d.ts +25 -0
  3. package/dist/client.js +83 -0
  4. package/dist/index.d.ts +86 -0
  5. package/dist/index.js +15 -0
  6. package/dist/migrations.d.ts +19 -0
  7. package/dist/migrations.js +74 -0
  8. package/dist/templates.d.ts +33 -0
  9. package/dist/templates.js +89 -0
  10. package/dist/tools.d.ts +36 -0
  11. package/dist/tools.js +22 -0
  12. package/dist/vite.d.ts +29 -0
  13. package/dist/vite.js +77 -0
  14. package/package.json +47 -0
  15. package/templates/ai-app/KAZZLE.md +21 -0
  16. package/templates/ai-app/bun.lock +290 -0
  17. package/templates/ai-app/components/server/index.ts +238 -0
  18. package/templates/ai-app/components/server/package.json +20 -0
  19. package/templates/ai-app/components/server/tsconfig.json +11 -0
  20. package/templates/ai-app/components/ui/index.html +12 -0
  21. package/templates/ai-app/components/ui/package.json +28 -0
  22. package/templates/ai-app/components/ui/public/favicon.svg +4 -0
  23. package/templates/ai-app/components/ui/src/App.tsx +52 -0
  24. package/templates/ai-app/components/ui/src/api.ts +85 -0
  25. package/templates/ai-app/components/ui/src/main.tsx +11 -0
  26. package/templates/ai-app/components/ui/src/styles/app.css +210 -0
  27. package/templates/ai-app/components/ui/src/styles/theme.css +181 -0
  28. package/templates/ai-app/components/ui/src/tabs/chat-tab.tsx +75 -0
  29. package/templates/ai-app/components/ui/src/tabs/extract-tab.tsx +92 -0
  30. package/templates/ai-app/components/ui/src/tabs/image-tab.tsx +51 -0
  31. package/templates/ai-app/components/ui/src/tabs/speech-tab.tsx +47 -0
  32. package/templates/ai-app/components/ui/src/tabs/transcribe-tab.tsx +42 -0
  33. package/templates/ai-app/components/ui/src/tabs/video-tab.tsx +76 -0
  34. package/templates/ai-app/components/ui/src/types.ts +53 -0
  35. package/templates/ai-app/components/ui/src/vite-env.d.ts +1 -0
  36. package/templates/ai-app/components/ui/tsconfig.json +21 -0
  37. package/templates/ai-app/components/ui/vite.config.ts +22 -0
  38. package/templates/ai-app/kazzle.config.ts +9 -0
  39. package/templates/ai-app/package.json +17 -0
  40. package/templates/empty-app/KAZZLE.md +10 -0
  41. package/templates/empty-app/kazzle.config.ts +8 -0
  42. package/templates/empty-app/package.json +15 -0
  43. package/templates/manifest.json +10 -0
  44. package/templates/process-app/KAZZLE.md +10 -0
  45. package/templates/process-app/components/process/index.ts +155 -0
  46. package/templates/process-app/components/process/package.json +20 -0
  47. package/templates/process-app/components/process/tsconfig.json +12 -0
  48. package/templates/process-app/kazzle.config.ts +32 -0
  49. package/templates/process-app/package.json +18 -0
  50. package/templates/process-app/skills/tools/SKILL.md +37 -0
  51. package/templates/process-app/skills/tools/tools.ts +19 -0
  52. package/templates/realtime-app/KAZZLE.md +11 -0
  53. package/templates/realtime-app/bun.lock +887 -0
  54. package/templates/realtime-app/components/server/deploy.ts +105 -0
  55. package/templates/realtime-app/components/server/index.ts +40 -0
  56. package/templates/realtime-app/components/server/migrations/001_items.sql +6 -0
  57. package/templates/realtime-app/components/server/package.json +20 -0
  58. package/templates/realtime-app/components/server/routes.ts +137 -0
  59. package/templates/realtime-app/components/server/tsconfig.json +15 -0
  60. package/templates/realtime-app/components/ui/index.html +13 -0
  61. package/templates/realtime-app/components/ui/package.json +36 -0
  62. package/templates/realtime-app/components/ui/public/favicon.svg +4 -0
  63. package/templates/realtime-app/components/ui/src/App.tsx +77 -0
  64. package/templates/realtime-app/components/ui/src/lib/connector.ts +80 -0
  65. package/templates/realtime-app/components/ui/src/lib/schema.ts +35 -0
  66. package/templates/realtime-app/components/ui/src/lib/sync.ts +24 -0
  67. package/templates/realtime-app/components/ui/src/main.tsx +49 -0
  68. package/templates/realtime-app/components/ui/src/styles/theme.css +181 -0
  69. package/templates/realtime-app/components/ui/src/vite-env.d.ts +2 -0
  70. package/templates/realtime-app/components/ui/tsconfig.json +14 -0
  71. package/templates/realtime-app/components/ui/vite.config.ts +86 -0
  72. package/templates/realtime-app/kazzle.config.ts +11 -0
  73. package/templates/realtime-app/package.json +17 -0
  74. package/templates/shared/icon.svg +4 -0
  75. package/templates/ui-app/KAZZLE.md +16 -0
  76. package/templates/ui-app/components/ui/bun.lock +894 -0
  77. package/templates/ui-app/components/ui/index.html +13 -0
  78. package/templates/ui-app/components/ui/package.json +30 -0
  79. package/templates/ui-app/components/ui/public/favicon.svg +4 -0
  80. package/templates/ui-app/components/ui/src/App.tsx +41 -0
  81. package/templates/ui-app/components/ui/src/main.tsx +24 -0
  82. package/templates/ui-app/components/ui/src/styles/theme.css +181 -0
  83. package/templates/ui-app/components/ui/tsconfig.json +21 -0
  84. package/templates/ui-app/components/ui/vite.config.ts +97 -0
  85. package/templates/ui-app/kazzle.config.ts +10 -0
  86. package/templates/ui-app/package.json +16 -0
  87. package/templates/ui-db-app/KAZZLE.md +12 -0
  88. package/templates/ui-db-app/components/server/index.ts +218 -0
  89. package/templates/ui-db-app/components/server/package.json +21 -0
  90. package/templates/ui-db-app/components/server/tsconfig.json +11 -0
  91. package/templates/ui-db-app/components/ui/index.html +12 -0
  92. package/templates/ui-db-app/components/ui/package.json +28 -0
  93. package/templates/ui-db-app/components/ui/public/favicon.svg +4 -0
  94. package/templates/ui-db-app/components/ui/src/App.tsx +221 -0
  95. package/templates/ui-db-app/components/ui/src/api.ts +79 -0
  96. package/templates/ui-db-app/components/ui/src/main.tsx +10 -0
  97. package/templates/ui-db-app/components/ui/src/styles/theme.css +181 -0
  98. package/templates/ui-db-app/components/ui/src/types.ts +17 -0
  99. package/templates/ui-db-app/components/ui/src/vite-env.d.ts +1 -0
  100. package/templates/ui-db-app/components/ui/tsconfig.json +21 -0
  101. package/templates/ui-db-app/components/ui/vite.config.ts +19 -0
  102. package/templates/ui-db-app/kazzle.config.ts +9 -0
  103. package/templates/ui-db-app/package.json +17 -0
@@ -0,0 +1,238 @@
1
+ /**
2
+ * {{APP_NAME}} — server component for the AI template.
3
+ *
4
+ * Thin proxy over Kazzle's AI gateway. Demonstrates the full modality
5
+ * surface customers can build on top of one `kzl_…` API key:
6
+ *
7
+ * POST /chat → /ai/chat/completions (OpenAI-shape, including structured output)
8
+ * POST /image → /ai/images/generations (standardized)
9
+ * POST /speech → /ai/audio/speech (binary audio)
10
+ * POST /transcribe → /ai/audio/transcriptions (multipart in)
11
+ * POST /video → /ai/video/generations (async job)
12
+ * GET /video/:id → /ai/responses/:id (poll)
13
+ * GET /health → local server/proxy health (no upstream call)
14
+ *
15
+ * The browser only ever talks to this server, never to Kazzle directly —
16
+ * that's how the `kzl_…` key stays server-side and how customers add
17
+ * their own auth/rate-limits/business rules around AI usage.
18
+ *
19
+ * Env:
20
+ * - `KAZZLE_API_KEY` required.
21
+ * - `KAZZLE_API_URL` required API root, for example `https://api.kazzle.app`.
22
+ * - `KAZZLE_AI_MODEL` default `openai/gpt-5.5`.
23
+ * - `KAZZLE_IMAGE_MODEL` default `@cf/black-forest-labs/flux-1-schnell`.
24
+ * - `KAZZLE_SPEECH_MODEL` default `openai/tts-1`.
25
+ * - `KAZZLE_TRANSCRIPTION_MODEL` default `openai/whisper-1`.
26
+ * - `KAZZLE_VIDEO_MODEL` default `openai/sora-2`.
27
+ *
28
+ * Override any model via the request body (`{ model: '…' }`); the env
29
+ * default is just a sensible fallback.
30
+ */
31
+
32
+ import { serve } from '@hono/node-server';
33
+ import { Hono } from 'hono';
34
+ import type { Context } from 'hono';
35
+ import { cors } from 'hono/cors';
36
+
37
+ const apiKey = process.env.KAZZLE_API_KEY;
38
+ if (!apiKey) throw new Error('KAZZLE_API_KEY is required (mint with `api_key { action: "create" }`)');
39
+ const apiUrl = process.env.KAZZLE_API_URL;
40
+ if (!apiUrl) throw new Error('KAZZLE_API_URL is required and must point at the matching Kazzle API environment');
41
+
42
+ const baseUrl = `${apiUrl.replace(/\/$/, '')}/ai`;
43
+ const defaults = {
44
+ chat: process.env.KAZZLE_AI_MODEL || 'openai/gpt-5.5',
45
+ image: process.env.KAZZLE_IMAGE_MODEL || '@cf/black-forest-labs/flux-1-schnell',
46
+ speech: process.env.KAZZLE_SPEECH_MODEL || 'openai/tts-1',
47
+ transcription: process.env.KAZZLE_TRANSCRIPTION_MODEL || 'openai/whisper-1',
48
+ video: process.env.KAZZLE_VIDEO_MODEL || 'openai/sora-2',
49
+ };
50
+
51
+ type ChatMessage = { role: 'system' | 'user' | 'assistant'; content: string };
52
+ type ChatCompletionPayload = { choices?: Array<{ message?: ChatMessage }>; model?: string };
53
+ type JsonSchemaResponseFormat = { type: 'json_schema'; json_schema?: unknown };
54
+ type ChatResponseBody = { message: ChatMessage; model: string; structured?: unknown };
55
+ const authHeader = `Bearer ${apiKey}`;
56
+
57
+ const app = new Hono();
58
+ app.use('*', cors());
59
+
60
+ app.get('/health', c => c.json({ ok: true, defaults }));
61
+
62
+ // ── Chat ─────────────────────────────────────────────────────────────
63
+ app.post('/chat', async c => {
64
+ const body = await readJsonBody(c);
65
+ if (!body) return c.json({ error: 'Invalid JSON body' }, 400);
66
+ const messages = Array.isArray(body.messages) ? body.messages : null;
67
+ if (!messages || messages.length === 0) {
68
+ return c.json({ error: '`messages` must be a non-empty array' }, 400);
69
+ }
70
+ for (const m of messages as Partial<ChatMessage>[]) {
71
+ if (m.role !== 'system' && m.role !== 'user' && m.role !== 'assistant') {
72
+ return c.json({ error: 'Each message needs role of system|user|assistant' }, 400);
73
+ }
74
+ if (typeof m.content !== 'string') {
75
+ return c.json({ error: 'Each message needs string content' }, 400);
76
+ }
77
+ }
78
+ const { model, stream, messages: _m, ...rest } = body;
79
+ const upstream = await fetch(`${baseUrl}/chat/completions`, {
80
+ method: 'POST',
81
+ headers: { 'Content-Type': 'application/json', Authorization: authHeader },
82
+ body: JSON.stringify({ model: model || defaults.chat, messages, ...rest, stream: false }),
83
+ });
84
+ if (!upstream.ok) return c.json({ error: `Upstream ${upstream.status}: ${await upstream.text()}` }, 502);
85
+ const data = await upstream.json() as ChatCompletionPayload;
86
+ const assistant = data.choices?.[0]?.message;
87
+ if (!assistant?.content) return c.json({ error: 'Upstream returned no assistant message' }, 502);
88
+ const response: ChatResponseBody = {
89
+ message: assistant,
90
+ model: data.model ?? (typeof model === 'string' ? model : defaults.chat),
91
+ };
92
+ if (isJsonSchemaResponseFormat(body.response_format)) {
93
+ const parsed = parseStructuredContent(assistant.content);
94
+ if (!parsed.ok) return c.json({ error: parsed.error }, 502);
95
+ response.structured = parsed.value;
96
+ }
97
+ return c.json(response);
98
+ });
99
+
100
+ // ── Image ────────────────────────────────────────────────────────────
101
+ // Standardized output: `{ images: [{ url? b64? mimeType }] }`. The proxy
102
+ // hands us this shape regardless of provider so the UI can render either
103
+ // a data URL (from b64) or a remote URL without provider branching.
104
+ app.post('/image', async c => {
105
+ const body = await readJsonBody(c);
106
+ if (!body || typeof body.prompt !== 'string' || !body.prompt.trim()) {
107
+ return c.json({ error: '`prompt` is required' }, 400);
108
+ }
109
+ const { model, prompt, ...rest } = body;
110
+ const upstream = await fetch(`${baseUrl}/images/generations`, {
111
+ method: 'POST',
112
+ headers: { 'Content-Type': 'application/json', Authorization: authHeader },
113
+ body: JSON.stringify({ model: model || defaults.image, prompt, ...rest }),
114
+ });
115
+ return passthroughJson(upstream);
116
+ });
117
+
118
+ // ── Speech (TTS, binary) ─────────────────────────────────────────────
119
+ // Returns raw audio bytes. The Content-Type tells the browser/client
120
+ // which decoder to use; the UI just hands it to <audio>.
121
+ app.post('/speech', async c => {
122
+ const body = await readJsonBody(c);
123
+ if (!body || typeof body.text !== 'string' || !body.text.trim()) {
124
+ return c.json({ error: '`text` is required' }, 400);
125
+ }
126
+ const { model, voice, format, ...rest } = body;
127
+ const upstream = await fetch(`${baseUrl}/audio/speech`, {
128
+ method: 'POST',
129
+ headers: { 'Content-Type': 'application/json', Authorization: authHeader },
130
+ body: JSON.stringify({
131
+ model: model || defaults.speech,
132
+ voice: voice || 'alloy',
133
+ format: format || 'mp3',
134
+ ...rest,
135
+ text: body.text,
136
+ }),
137
+ });
138
+ if (!upstream.ok) return c.json({ error: `Upstream ${upstream.status}: ${await upstream.text()}` }, 502);
139
+ const bytes = new Uint8Array(await upstream.arrayBuffer());
140
+ return new Response(bytes, {
141
+ status: 200,
142
+ headers: { 'content-type': upstream.headers.get('content-type') || 'audio/mpeg' },
143
+ });
144
+ });
145
+
146
+ // ── Transcription (multipart in, JSON out) ──────────────────────────
147
+ // Accepts `multipart/form-data` with `file` (audio bytes) and optional
148
+ // `model`, `language`, `prompt`. Forwards as multipart to Kazzle.
149
+ app.post('/transcribe', async c => {
150
+ const form = await c.req.parseBody();
151
+ const file = form.file as Blob | undefined;
152
+ if (!file || typeof (file as Blob).arrayBuffer !== 'function') {
153
+ return c.json({ error: '`file` is required (multipart/form-data)' }, 400);
154
+ }
155
+ const upstreamForm = new FormData();
156
+ upstreamForm.append('model', String(form.model || defaults.transcription));
157
+ if (typeof form.language === 'string') upstreamForm.append('language', form.language);
158
+ if (typeof form.prompt === 'string') upstreamForm.append('prompt', form.prompt);
159
+ // Re-wrap as a Blob so undici/Bun's FormData sets a clean file part —
160
+ // forwarding the parsed Blob directly preserves whatever Hono parsed.
161
+ const audio = new Blob([new Uint8Array(await file.arrayBuffer())], { type: file.type || 'audio/mpeg' });
162
+ upstreamForm.append('file', audio, (file as Blob & { name?: string }).name || 'audio.bin');
163
+ const upstream = await fetch(`${baseUrl}/audio/transcriptions`, {
164
+ method: 'POST',
165
+ headers: { Authorization: authHeader },
166
+ body: upstreamForm,
167
+ });
168
+ return passthroughJson(upstream);
169
+ });
170
+
171
+ // ── Video (async) ────────────────────────────────────────────────────
172
+ // Returns `{ jobId, status, pollUrl }`. The UI submits a prompt, gets a
173
+ // job id, and polls `/video/:id` until status is `succeeded` (then
174
+ // reads `output.videoUrl`) or `failed`.
175
+ app.post('/video', async c => {
176
+ const body = await readJsonBody(c);
177
+ if (!body || typeof body.prompt !== 'string' || !body.prompt.trim()) {
178
+ return c.json({ error: '`prompt` is required' }, 400);
179
+ }
180
+ const { model, prompt, ...rest } = body;
181
+ const upstream = await fetch(`${baseUrl}/video/generations`, {
182
+ method: 'POST',
183
+ headers: { 'Content-Type': 'application/json', Authorization: authHeader },
184
+ body: JSON.stringify({ model: model || defaults.video, prompt, ...rest }),
185
+ });
186
+ return passthroughJson(upstream);
187
+ });
188
+
189
+ app.get('/video/:id', async c => {
190
+ const id = c.req.param('id');
191
+ const upstream = await fetch(`${baseUrl}/responses/${encodeURIComponent(id)}`, {
192
+ headers: { Authorization: authHeader },
193
+ });
194
+ return passthroughJson(upstream);
195
+ });
196
+
197
+ // ── helpers ──────────────────────────────────────────────────────────
198
+
199
+ async function readJsonBody(c: Context): Promise<Record<string, unknown> | null> {
200
+ try { return await c.req.json<Record<string, unknown>>(); } catch { return null; }
201
+ }
202
+
203
+ function isJsonSchemaResponseFormat(value: unknown): value is JsonSchemaResponseFormat {
204
+ return typeof value === 'object'
205
+ && value !== null
206
+ && 'type' in value
207
+ && value.type === 'json_schema';
208
+ }
209
+
210
+ function parseStructuredContent(content: string): { ok: true; value: unknown } | { ok: false; error: string } {
211
+ try {
212
+ return { ok: true, value: JSON.parse(content) as unknown };
213
+ } catch {
214
+ return { ok: false, error: 'Upstream returned invalid structured JSON' };
215
+ }
216
+ }
217
+
218
+ async function passthroughJson(upstream: Response): Promise<Response> {
219
+ // We don't trust the upstream's content-encoding/length when we
220
+ // re-emit. The Kazzle proxy may pass through Cloudflare's framing
221
+ // headers; reading the body as text strips that.
222
+ const text = await upstream.text();
223
+ return new Response(text, {
224
+ status: upstream.status,
225
+ headers: { 'content-type': 'application/json' },
226
+ });
227
+ }
228
+
229
+ // ─────────────────────────────────────────────────────────────────────────
230
+ // DO NOT CHANGE THE NEXT 4 LINES. DO NOT ADD FALLBACKS. DO NOT HARDCODE.
231
+ // PORT and HOST are injected by `kazzle run`. Missing values must throw.
232
+ // ─────────────────────────────────────────────────────────────────────────
233
+ if (!process.env.PORT || !process.env.HOST) {
234
+ throw new Error('PORT and HOST must be set by "kazzle run" — never hardcode them.');
235
+ }
236
+ const port = Number(process.env.PORT);
237
+ const hostname = process.env.HOST;
238
+ serve({ fetch: app.fetch, port, hostname });
@@ -0,0 +1,20 @@
1
+ {
2
+ "name": "{{APP_SLUG}}-server",
3
+ "private": true,
4
+ "version": "1.0.0",
5
+ "type": "module",
6
+ "scripts": {
7
+ "start": "bun run index.ts",
8
+ "dev": "kazzle run -- bun --watch index.ts",
9
+ "typecheck": "tsc --noEmit --allowImportingTsExtensions",
10
+ "check": "bun run typecheck"
11
+ },
12
+ "dependencies": {
13
+ "@hono/node-server": "^1.13.0",
14
+ "hono": "^4.12.16"
15
+ },
16
+ "devDependencies": {
17
+ "@types/bun": "^1.3.6",
18
+ "typescript": "^5.8.3"
19
+ }
20
+ }
@@ -0,0 +1,11 @@
1
+ {
2
+ "compilerOptions": {
3
+ "target": "ES2022",
4
+ "module": "ESNext",
5
+ "moduleResolution": "Bundler",
6
+ "strict": true,
7
+ "skipLibCheck": true,
8
+ "types": ["bun"]
9
+ },
10
+ "include": ["*.ts"]
11
+ }
@@ -0,0 +1,12 @@
1
+ <!doctype html>
2
+ <html lang="en">
3
+ <head>
4
+ <meta charset="UTF-8" />
5
+ <meta name="viewport" content="width=device-width, initial-scale=1.0" />
6
+ <title>{{APP_NAME}}</title>
7
+ </head>
8
+ <body>
9
+ <div id="root"></div>
10
+ <script type="module" src="/src/main.tsx"></script>
11
+ </body>
12
+ </html>
@@ -0,0 +1,28 @@
1
+ {
2
+ "name": "{{APP_SLUG}}",
3
+ "private": true,
4
+ "version": "1.0.0",
5
+ "type": "module",
6
+ "scripts": {
7
+ "dev": "kazzle run -- vite",
8
+ "typecheck": "tsc --noEmit",
9
+ "build": "vite build",
10
+ "preview": "kazzle run -- vite preview",
11
+ "check": "bun run typecheck && bun run build"
12
+ },
13
+ "dependencies": {
14
+ "@kazzle/app": "{{KAZZLE_APP_VERSION}}",
15
+ "@vitejs/plugin-react": "^4.5.2",
16
+ "react": "^19.1.0",
17
+ "react-dom": "^19.1.0",
18
+ "vite": "^6.3.5"
19
+ },
20
+ "devDependencies": {
21
+ "@tailwindcss/vite": "^4.1.0",
22
+ "@types/node": "^25.1.0",
23
+ "@types/react": "^19.1.0",
24
+ "@types/react-dom": "^19.1.0",
25
+ "tailwindcss": "^4.1.0",
26
+ "typescript": "^5.8.3"
27
+ }
28
+ }
@@ -0,0 +1,4 @@
1
+ <svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 64 64" width="64" height="64">
2
+ <rect width="64" height="64" rx="14" fill="hsl(230,65%,88%)"/>
3
+ <text x="32" y="46" text-anchor="middle" font-family="system-ui,sans-serif" font-size="40" font-weight="600" fill="hsl(230,50%,25%)">K</text>
4
+ </svg>
@@ -0,0 +1,52 @@
1
+ import { useState } from 'react';
2
+ import { ChatTab } from './tabs/chat-tab';
3
+ import { ExtractTab } from './tabs/extract-tab';
4
+ import { ImageTab } from './tabs/image-tab';
5
+ import { SpeechTab } from './tabs/speech-tab';
6
+ import { TranscribeTab } from './tabs/transcribe-tab';
7
+ import { VideoTab } from './tabs/video-tab';
8
+ import type { ModalityTab } from './types';
9
+
10
+ const TABS: Array<{ id: ModalityTab; label: string }> = [
11
+ { id: 'chat', label: 'Chat' },
12
+ { id: 'extract', label: 'Extract' },
13
+ { id: 'image', label: 'Image' },
14
+ { id: 'speech', label: 'Speech' },
15
+ { id: 'transcribe', label: 'Transcribe' },
16
+ { id: 'video', label: 'Video' },
17
+ ];
18
+
19
+ export function App() {
20
+ const [tab, setTab] = useState<ModalityTab>('chat');
21
+
22
+ return (
23
+ <main className="app-shell">
24
+ <header className="header">
25
+ <p className="eyebrow">{'{{APP_NAME}}'}</p>
26
+ <h1>Kazzle AI playground</h1>
27
+ </header>
28
+
29
+ <nav className="tabs">
30
+ {TABS.map(t => (
31
+ <button
32
+ key={t.id}
33
+ className={t.id === tab ? 'tab active' : 'tab'}
34
+ onClick={() => setTab(t.id)}
35
+ data-testid={`tab-${t.id}`}
36
+ >
37
+ {t.label}
38
+ </button>
39
+ ))}
40
+ </nav>
41
+
42
+ <section className="panel">
43
+ {tab === 'chat' ? <ChatTab /> : null}
44
+ {tab === 'extract' ? <ExtractTab /> : null}
45
+ {tab === 'image' ? <ImageTab /> : null}
46
+ {tab === 'speech' ? <SpeechTab /> : null}
47
+ {tab === 'transcribe' ? <TranscribeTab /> : null}
48
+ {tab === 'video' ? <VideoTab /> : null}
49
+ </section>
50
+ </main>
51
+ );
52
+ }
@@ -0,0 +1,85 @@
1
+ /**
2
+ * Browser → template-server client. Mirrors every endpoint in
3
+ * `components/server/index.ts`. Each function builds a request, throws on
4
+ * non-2xx with the server's error body, and parses the response — keeping
5
+ * the React components free of fetch boilerplate.
6
+ *
7
+ * IMPORTANT: keep these as relative requests. Kazzle injects the
8
+ * sibling server URL into Vite only so the dev-server proxy can route these
9
+ * calls inside the checkout computer. Browser code must not read or fetch
10
+ * KAZZLE_APP_COMPONENT_*_URL directly.
11
+ */
12
+
13
+ import type {
14
+ AsyncJob,
15
+ ChatMessage,
16
+ ChatOptions,
17
+ ChatResponse,
18
+ ImageResult,
19
+ StructuredOutputSchema,
20
+ TranscriptionResult,
21
+ } from './types';
22
+
23
+ async function jsonRequest<T>(path: string, body: unknown): Promise<T> {
24
+ const res = await fetch(path, {
25
+ method: 'POST',
26
+ headers: { 'Content-Type': 'application/json' },
27
+ body: JSON.stringify(body),
28
+ });
29
+ if (!res.ok) throw new Error(`${path} failed: ${res.status} ${await res.text()}`);
30
+ return res.json() as Promise<T>;
31
+ }
32
+
33
+ export async function sendChat(messages: ChatMessage[], options: ChatOptions = {}): Promise<ChatMessage> {
34
+ const data = await jsonRequest<ChatResponse>('/chat', { messages, ...options });
35
+ return data.message;
36
+ }
37
+
38
+ export async function sendStructuredChat<T>(
39
+ messages: ChatMessage[],
40
+ jsonSchema: StructuredOutputSchema,
41
+ options: Omit<ChatOptions, 'response_format'> = {},
42
+ ): Promise<T> {
43
+ const data = await jsonRequest<ChatResponse>('/chat', {
44
+ messages,
45
+ ...options,
46
+ response_format: {
47
+ type: 'json_schema',
48
+ json_schema: jsonSchema,
49
+ },
50
+ });
51
+ if (!('structured' in data)) throw new Error('/chat returned no structured output');
52
+ return data.structured as T;
53
+ }
54
+
55
+ export async function generateImage(prompt: string): Promise<ImageResult> {
56
+ return jsonRequest<ImageResult>('/image', { prompt });
57
+ }
58
+
59
+ export async function synthesizeSpeech(text: string, voice = 'alloy'): Promise<Blob> {
60
+ const res = await fetch('/speech', {
61
+ method: 'POST',
62
+ headers: { 'Content-Type': 'application/json' },
63
+ body: JSON.stringify({ text, voice }),
64
+ });
65
+ if (!res.ok) throw new Error(`/speech failed: ${res.status} ${await res.text()}`);
66
+ return res.blob();
67
+ }
68
+
69
+ export async function transcribeAudio(audio: Blob): Promise<TranscriptionResult> {
70
+ const form = new FormData();
71
+ form.append('file', audio, 'audio.mp3');
72
+ const res = await fetch('/transcribe', { method: 'POST', body: form });
73
+ if (!res.ok) throw new Error(`/transcribe failed: ${res.status} ${await res.text()}`);
74
+ return res.json() as Promise<TranscriptionResult>;
75
+ }
76
+
77
+ export async function startVideoJob(prompt: string): Promise<AsyncJob> {
78
+ return jsonRequest<AsyncJob>('/video', { prompt });
79
+ }
80
+
81
+ export async function pollVideoJob(jobId: string): Promise<AsyncJob> {
82
+ const res = await fetch(`/video/${encodeURIComponent(jobId)}`);
83
+ if (!res.ok) throw new Error(`/video/${jobId} failed: ${res.status} ${await res.text()}`);
84
+ return res.json() as Promise<AsyncJob>;
85
+ }
@@ -0,0 +1,11 @@
1
+ import React from 'react';
2
+ import { createRoot } from 'react-dom/client';
3
+ import { App } from './App';
4
+ import './styles/theme.css';
5
+ import './styles/app.css';
6
+
7
+ createRoot(document.getElementById('root')!).render(
8
+ <React.StrictMode>
9
+ <App />
10
+ </React.StrictMode>,
11
+ );
@@ -0,0 +1,210 @@
1
+ /* ─────────────────────────────────────────────────────────────────────────
2
+ * App-specific component classes for the AI playground chrome.
3
+ *
4
+ * The shared design system lives in `theme.css` (Tailwind v4 + design tokens
5
+ * + generic primitives like `.btn`, `.card`, `.input`). This file adds the
6
+ * few component classes that are specific to THIS app's layout — the tab
7
+ * bar, chat transcript, and message bubbles — which are reused across every
8
+ * tab in `src/tabs/`.
9
+ *
10
+ * For NEW UI, prefer Tailwind utilities + the `theme.css` primitives. Only
11
+ * add a class here when the same component markup repeats across tabs.
12
+ * ───────────────────────────────────────────────────────────────────────── */
13
+
14
+ @layer components {
15
+ .app-shell {
16
+ display: grid;
17
+ grid-template-rows: auto auto 1fr;
18
+ min-height: 100vh;
19
+ max-width: 48rem;
20
+ margin: 0 auto;
21
+ padding: 1.5rem;
22
+ gap: 1rem;
23
+ }
24
+
25
+ .header {
26
+ display: flex;
27
+ align-items: baseline;
28
+ gap: 1rem;
29
+ }
30
+ .header h1 {
31
+ font-size: 1.375rem;
32
+ flex: 1;
33
+ }
34
+ .eyebrow {
35
+ margin: 0;
36
+ letter-spacing: 0.12em;
37
+ font-size: 0.6875rem;
38
+ color: var(--color-muted);
39
+ }
40
+
41
+ .tabs {
42
+ display: flex;
43
+ gap: 0.25rem;
44
+ border-bottom: 1px solid var(--color-border);
45
+ }
46
+ .tab {
47
+ background: transparent;
48
+ border: none;
49
+ color: var(--color-muted);
50
+ padding: 0.5rem 0.875rem;
51
+ border-radius: 0.5rem 0.5rem 0 0;
52
+ cursor: pointer;
53
+ font: inherit;
54
+ }
55
+ .tab.active {
56
+ background: color-mix(in srgb, var(--color-accent) 18%, transparent);
57
+ color: var(--color-fg);
58
+ }
59
+
60
+ .panel {
61
+ display: flex;
62
+ flex-direction: column;
63
+ min-height: 0;
64
+ }
65
+ .tab-body {
66
+ display: flex;
67
+ flex-direction: column;
68
+ gap: 0.75rem;
69
+ min-height: 0;
70
+ }
71
+
72
+ .image-grid {
73
+ display: grid;
74
+ grid-template-columns: repeat(auto-fill, minmax(13.75rem, 1fr));
75
+ gap: 0.75rem;
76
+ }
77
+ .image-grid img {
78
+ width: 100%;
79
+ border-radius: 0.625rem;
80
+ background: color-mix(in srgb, var(--color-muted) 10%, transparent);
81
+ }
82
+ audio,
83
+ video {
84
+ width: 100%;
85
+ border-radius: 0.625rem;
86
+ }
87
+
88
+ .upload {
89
+ display: inline-flex;
90
+ align-items: center;
91
+ gap: 0.5rem;
92
+ padding: 0.625rem 0.875rem;
93
+ border: 1px dashed var(--color-border);
94
+ border-radius: 0.625rem;
95
+ cursor: pointer;
96
+ }
97
+
98
+ .transcript {
99
+ display: flex;
100
+ flex-direction: column;
101
+ gap: 0.75rem;
102
+ overflow-y: auto;
103
+ padding: 0.5rem;
104
+ background: var(--color-surface);
105
+ border: 1px solid var(--color-border);
106
+ border-radius: var(--radius-app);
107
+ min-height: 17.5rem;
108
+ }
109
+ .message {
110
+ display: flex;
111
+ flex-direction: column;
112
+ padding: 0.625rem 0.875rem;
113
+ border-radius: 0.625rem;
114
+ background: color-mix(in srgb, var(--color-muted) 8%, transparent);
115
+ }
116
+ .message-user {
117
+ align-self: flex-end;
118
+ background: color-mix(in srgb, var(--color-accent) 25%, transparent);
119
+ }
120
+ .message-assistant {
121
+ align-self: flex-start;
122
+ }
123
+ .message .who {
124
+ font-size: 0.6875rem;
125
+ color: var(--color-muted);
126
+ letter-spacing: 0.08em;
127
+ }
128
+ .message .body {
129
+ margin: 0.25rem 0 0;
130
+ white-space: pre-wrap;
131
+ line-height: 1.5;
132
+ }
133
+ .message.pending .body {
134
+ color: var(--color-muted);
135
+ }
136
+
137
+ .empty {
138
+ margin: auto;
139
+ color: var(--color-muted);
140
+ }
141
+
142
+ .error {
143
+ margin: 0;
144
+ padding: 0.625rem 0.875rem;
145
+ background: color-mix(in srgb, var(--color-danger) 15%, transparent);
146
+ border: 1px solid color-mix(in srgb, var(--color-danger) 40%, transparent);
147
+ color: var(--color-danger);
148
+ border-radius: 0.625rem;
149
+ }
150
+
151
+ .composer {
152
+ display: flex;
153
+ gap: 0.5rem;
154
+ }
155
+ .composer input,
156
+ .stack-form textarea {
157
+ flex: 1;
158
+ padding: 0.625rem 0.75rem;
159
+ background: var(--color-surface-raised);
160
+ border: 1px solid var(--color-border);
161
+ border-radius: 0.625rem;
162
+ color: inherit;
163
+ font: inherit;
164
+ }
165
+ .stack-form {
166
+ display: flex;
167
+ flex-direction: column;
168
+ gap: 0.5rem;
169
+ }
170
+ .stack-form textarea {
171
+ min-height: 8.75rem;
172
+ resize: vertical;
173
+ }
174
+ .composer button,
175
+ .stack-form button {
176
+ padding: 0.625rem 1rem;
177
+ background: var(--color-accent);
178
+ border: none;
179
+ border-radius: 0.625rem;
180
+ color: var(--color-accent-fg);
181
+ cursor: pointer;
182
+ font: inherit;
183
+ }
184
+ .composer button:disabled,
185
+ .stack-form button:disabled {
186
+ opacity: 0.4;
187
+ cursor: not-allowed;
188
+ }
189
+
190
+ .ghost {
191
+ background: transparent;
192
+ border: 1px solid var(--color-border);
193
+ color: var(--color-fg);
194
+ padding: 0.375rem 0.75rem;
195
+ border-radius: 0.5rem;
196
+ cursor: pointer;
197
+ font: inherit;
198
+ }
199
+ .ghost:disabled {
200
+ opacity: 0.4;
201
+ cursor: not-allowed;
202
+ }
203
+
204
+ .structured-result {
205
+ padding: 0.75rem 0.875rem;
206
+ background: color-mix(in srgb, #22c55e 12%, transparent);
207
+ border: 1px solid color-mix(in srgb, #22c55e 35%, transparent);
208
+ border-radius: 0.625rem;
209
+ }
210
+ }