@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,181 @@
1
+ /* ─────────────────────────────────────────────────────────────────────────
2
+ * Kazzle app theme — the ONE styling entry point for this app.
3
+ *
4
+ * This file is the design system. It is imported once (in `main.tsx`) and
5
+ * gives you three things, in order:
6
+ *
7
+ * 1. Tailwind v4 — every utility class (`flex`, `gap-4`,
8
+ * `rounded-xl`, `text-zinc-500`, …) works in
9
+ * your JSX. No config file, no PostCSS setup.
10
+ * 2. Design tokens (@theme) — colors, radius, fonts as CSS variables. Edit
11
+ * these to re-skin the whole app at once.
12
+ * 3. Primitives (@layer) — a few ready-made classes (`.btn`, `.card`,
13
+ * `.input`, `.field`, `.page`, `.stack`,
14
+ * `.row`) so common UI looks good with zero
15
+ * effort.
16
+ *
17
+ * HOW TO STYLE — pick whichever is easier, mix freely:
18
+ * • Tailwind utilities in `className` for one-off layout/spacing.
19
+ * • A primitive class (`.btn`, `.card`) for the common building blocks.
20
+ *
21
+ * WHY IT'S BUILT THIS WAY: styles live in the markup (utilities) or in named
22
+ * primitives here — never in a separate per-screen stylesheet keyed to class
23
+ * names you have to keep in sync. Rewriting a component can't silently strip
24
+ * its styling, because the styling travels with the elements.
25
+ *
26
+ * TO EXTEND: add new design tokens under `@theme`, and add new reusable
27
+ * building blocks under `@layer components`. Keep app-specific one-offs as
28
+ * inline Tailwind utilities in the component instead of adding a class here.
29
+ * ───────────────────────────────────────────────────────────────────────── */
30
+
31
+ @import "tailwindcss";
32
+
33
+ /* ── Design tokens ──────────────────────────────────────────────────────────
34
+ * Exposed to Tailwind as `bg-surface`, `text-fg`, `text-muted`,
35
+ * `bg-accent`, `rounded-app`, etc. Change a value here and every utility and
36
+ * primitive that references it updates at once. */
37
+ @theme {
38
+ --color-bg: #0b0f1a;
39
+ --color-surface: #131a2a;
40
+ --color-surface-raised: #1b2438;
41
+ --color-fg: #e8edf7;
42
+ --color-muted: #94a3b8;
43
+ --color-border: rgba(148, 163, 184, 0.18);
44
+ --color-accent: #6366f1;
45
+ --color-accent-hover: #4f46e5;
46
+ --color-accent-fg: #ffffff;
47
+ --color-danger: #f87171;
48
+
49
+ --radius-app: 0.875rem;
50
+
51
+ --font-sans: "Inter", ui-sans-serif, system-ui, -apple-system,
52
+ BlinkMacSystemFont, "Segoe UI", Roboto, sans-serif;
53
+ }
54
+
55
+ /* ── Base ────────────────────────────────────────────────────────────────────
56
+ * Sensible document defaults so even unstyled markup reads well. */
57
+ @layer base {
58
+ html,
59
+ body,
60
+ #root {
61
+ min-height: 100%;
62
+ }
63
+
64
+ body {
65
+ margin: 0;
66
+ background: var(--color-bg);
67
+ color: var(--color-fg);
68
+ font-family: var(--font-sans);
69
+ -webkit-font-smoothing: antialiased;
70
+ }
71
+
72
+ h1 {
73
+ font-size: 1.875rem;
74
+ font-weight: 700;
75
+ letter-spacing: -0.02em;
76
+ margin: 0;
77
+ }
78
+
79
+ h2 {
80
+ font-size: 1.25rem;
81
+ font-weight: 600;
82
+ margin: 0;
83
+ }
84
+ }
85
+
86
+ /* ── Primitives ──────────────────────────────────────────────────────────────
87
+ * Reusable building blocks. Use them directly (`<button class="btn btn-primary">`)
88
+ * or layer Tailwind utilities on top. Add new ones here when a pattern repeats. */
89
+ @layer components {
90
+ /* Page wrapper — centers content with comfortable gutters. */
91
+ .page {
92
+ width: min(100% - 2rem, 48rem);
93
+ margin-inline: auto;
94
+ padding-block: 3rem;
95
+ display: flex;
96
+ flex-direction: column;
97
+ gap: 1.5rem;
98
+ }
99
+
100
+ /* Vertical / horizontal stacks. */
101
+ .stack {
102
+ display: flex;
103
+ flex-direction: column;
104
+ gap: 0.75rem;
105
+ }
106
+ .row {
107
+ display: flex;
108
+ align-items: center;
109
+ gap: 0.75rem;
110
+ }
111
+
112
+ /* Card surface. */
113
+ .card {
114
+ background: var(--color-surface);
115
+ border: 1px solid var(--color-border);
116
+ border-radius: var(--radius-app);
117
+ padding: 1.25rem;
118
+ }
119
+
120
+ /* Buttons. `.btn` is the neutral base; add `.btn-primary` for the accent. */
121
+ .btn {
122
+ display: inline-flex;
123
+ align-items: center;
124
+ justify-content: center;
125
+ gap: 0.5rem;
126
+ padding: 0.625rem 1rem;
127
+ border-radius: var(--radius-app);
128
+ border: 1px solid var(--color-border);
129
+ background: var(--color-surface-raised);
130
+ color: var(--color-fg);
131
+ font: inherit;
132
+ font-weight: 500;
133
+ cursor: pointer;
134
+ transition: background 0.15s, border-color 0.15s, opacity 0.15s;
135
+ }
136
+ .btn:hover {
137
+ background: color-mix(in srgb, var(--color-surface-raised) 80%, white);
138
+ }
139
+ .btn:disabled {
140
+ opacity: 0.5;
141
+ cursor: not-allowed;
142
+ }
143
+ .btn-primary {
144
+ background: var(--color-accent);
145
+ border-color: transparent;
146
+ color: var(--color-accent-fg);
147
+ }
148
+ .btn-primary:hover {
149
+ background: var(--color-accent-hover);
150
+ }
151
+
152
+ /* Text input / select. */
153
+ .input {
154
+ width: 100%;
155
+ min-width: 0;
156
+ padding: 0.625rem 0.875rem;
157
+ border-radius: var(--radius-app);
158
+ border: 1px solid var(--color-border);
159
+ background: var(--color-surface-raised);
160
+ color: var(--color-fg);
161
+ font: inherit;
162
+ outline: none;
163
+ }
164
+ .input:focus {
165
+ border-color: var(--color-accent);
166
+ }
167
+
168
+ /* Labelled field: <label class="field"><span>Label</span><input class="input"/></label> */
169
+ .field {
170
+ display: flex;
171
+ flex-direction: column;
172
+ gap: 0.375rem;
173
+ font-size: 0.875rem;
174
+ color: var(--color-muted);
175
+ }
176
+
177
+ /* Muted helper / secondary text. */
178
+ .muted {
179
+ color: var(--color-muted);
180
+ }
181
+ }
@@ -0,0 +1,75 @@
1
+ import { FormEvent, useEffect, useRef, useState } from 'react';
2
+ import { sendChat } from '../api';
3
+ import type { ChatMessage } from '../types';
4
+
5
+ const SYSTEM_PROMPT: ChatMessage = {
6
+ role: 'system',
7
+ content: 'You are a helpful assistant. Answer concisely.',
8
+ };
9
+
10
+ export function ChatTab() {
11
+ const [messages, setMessages] = useState<ChatMessage[]>([]);
12
+ const [draft, setDraft] = useState('');
13
+ const [sending, setSending] = useState(false);
14
+ const [error, setError] = useState<string | null>(null);
15
+ const listRef = useRef<HTMLDivElement | null>(null);
16
+
17
+ useEffect(() => {
18
+ if (listRef.current) listRef.current.scrollTop = listRef.current.scrollHeight;
19
+ }, [messages, sending]);
20
+
21
+ async function onSubmit(event: FormEvent) {
22
+ event.preventDefault();
23
+ const content = draft.trim();
24
+ if (!content || sending) return;
25
+
26
+ const next: ChatMessage[] = [...messages, { role: 'user', content }];
27
+ setMessages(next);
28
+ setDraft('');
29
+ setSending(true);
30
+ setError(null);
31
+ try {
32
+ const assistant = await sendChat([SYSTEM_PROMPT, ...next]);
33
+ setMessages([...next, assistant]);
34
+ } catch (err) {
35
+ setError(err instanceof Error ? err.message : String(err));
36
+ } finally {
37
+ setSending(false);
38
+ }
39
+ }
40
+
41
+ return (
42
+ <div className="tab-body">
43
+ <div className="transcript" ref={listRef} data-testid="transcript">
44
+ {messages.length === 0 ? <p className="empty">Ask the assistant anything to get started.</p> : null}
45
+ {messages.map((m, idx) => (
46
+ <article key={idx} className={`message message-${m.role}`} data-role={m.role}>
47
+ <span className="who">{m.role === 'user' ? 'You' : 'Assistant'}</span>
48
+ <p className="body">{m.content}</p>
49
+ </article>
50
+ ))}
51
+ {sending ? (
52
+ <article className="message message-assistant pending" data-testid="pending">
53
+ <span className="who">Assistant</span>
54
+ <p className="body">…</p>
55
+ </article>
56
+ ) : null}
57
+ </div>
58
+
59
+ {error ? <p className="error" data-testid="error">{error}</p> : null}
60
+
61
+ <form className="composer" onSubmit={onSubmit}>
62
+ <input
63
+ value={draft}
64
+ onChange={e => setDraft(e.target.value)}
65
+ placeholder="Ask anything…"
66
+ disabled={sending}
67
+ data-testid="composer-input"
68
+ />
69
+ <button type="submit" disabled={sending || !draft.trim()} data-testid="composer-send">
70
+ {sending ? 'Sending…' : 'Send'}
71
+ </button>
72
+ </form>
73
+ </div>
74
+ );
75
+ }
@@ -0,0 +1,92 @@
1
+ import { FormEvent, useState } from 'react';
2
+ import { sendStructuredChat } from '../api';
3
+ import type { ChatMessage, StructuredOutputSchema } from '../types';
4
+
5
+ interface ExtractionResult {
6
+ summary: string;
7
+ sentiment: 'positive' | 'neutral' | 'negative';
8
+ keywords: string[];
9
+ }
10
+
11
+ const EXTRACTION_SCHEMA: StructuredOutputSchema = {
12
+ name: 'text_extraction',
13
+ strict: true,
14
+ schema: {
15
+ type: 'object',
16
+ additionalProperties: false,
17
+ properties: {
18
+ summary: { type: 'string', description: 'One short sentence summarizing the input.' },
19
+ sentiment: { type: 'string', enum: ['positive', 'neutral', 'negative'] },
20
+ keywords: {
21
+ type: 'array',
22
+ items: { type: 'string' },
23
+ description: 'Three to five important keywords from the input.',
24
+ },
25
+ },
26
+ required: ['summary', 'sentiment', 'keywords'],
27
+ },
28
+ };
29
+
30
+ const SYSTEM_PROMPT: ChatMessage = {
31
+ role: 'system',
32
+ content: 'Extract structured facts from the user text. Return only data matching the JSON schema.',
33
+ };
34
+
35
+ export function ExtractTab() {
36
+ const [text, setText] = useState('Kazzle makes it much faster to turn rough ideas into working apps.');
37
+ const [extracting, setExtracting] = useState(false);
38
+ const [result, setResult] = useState<ExtractionResult | null>(null);
39
+ const [error, setError] = useState<string | null>(null);
40
+
41
+ async function onSubmit(event: FormEvent) {
42
+ event.preventDefault();
43
+ const content = text.trim();
44
+ if (!content || extracting) return;
45
+
46
+ setExtracting(true);
47
+ setError(null);
48
+ try {
49
+ const extracted = await sendStructuredChat<ExtractionResult>(
50
+ [SYSTEM_PROMPT, { role: 'user', content }],
51
+ EXTRACTION_SCHEMA,
52
+ { max_completion_tokens: 512 },
53
+ );
54
+ setResult(extracted);
55
+ } catch (err) {
56
+ setError(err instanceof Error ? err.message : String(err));
57
+ } finally {
58
+ setExtracting(false);
59
+ }
60
+ }
61
+
62
+ return (
63
+ <div className="tab-body">
64
+ <p className="empty">
65
+ Use structured output when the app needs data, not prose. The server forwards
66
+ OpenAI-style <code>response_format.json_schema</code> and returns parsed JSON.
67
+ </p>
68
+
69
+ <form className="stack-form" onSubmit={onSubmit}>
70
+ <textarea
71
+ value={text}
72
+ onChange={event => setText(event.target.value)}
73
+ disabled={extracting}
74
+ data-testid="extract-input"
75
+ />
76
+ <button type="submit" disabled={extracting || !text.trim()} data-testid="extract-submit">
77
+ {extracting ? 'Extracting…' : 'Extract structured data'}
78
+ </button>
79
+ </form>
80
+
81
+ {error ? <p className="error" data-testid="error">{error}</p> : null}
82
+
83
+ {result ? (
84
+ <section className="structured-result" data-testid="structured-result">
85
+ <p><strong>Summary:</strong> {result.summary}</p>
86
+ <p><strong>Sentiment:</strong> {result.sentiment}</p>
87
+ <p><strong>Keywords:</strong> {result.keywords.join(', ')}</p>
88
+ </section>
89
+ ) : null}
90
+ </div>
91
+ );
92
+ }
@@ -0,0 +1,51 @@
1
+ import { FormEvent, useState } from 'react';
2
+ import { generateImage } from '../api';
3
+ import type { ImageResult } from '../types';
4
+
5
+ export function ImageTab() {
6
+ const [prompt, setPrompt] = useState('a single red dot on white');
7
+ const [result, setResult] = useState<ImageResult | null>(null);
8
+ const [busy, setBusy] = useState(false);
9
+ const [error, setError] = useState<string | null>(null);
10
+
11
+ async function onSubmit(event: FormEvent) {
12
+ event.preventDefault();
13
+ if (!prompt.trim() || busy) return;
14
+ setBusy(true);
15
+ setError(null);
16
+ setResult(null);
17
+ try {
18
+ setResult(await generateImage(prompt));
19
+ } catch (err) {
20
+ setError(err instanceof Error ? err.message : String(err));
21
+ } finally {
22
+ setBusy(false);
23
+ }
24
+ }
25
+
26
+ return (
27
+ <div className="tab-body">
28
+ <form className="composer" onSubmit={onSubmit}>
29
+ <input
30
+ value={prompt}
31
+ onChange={e => setPrompt(e.target.value)}
32
+ placeholder="Describe an image…"
33
+ disabled={busy}
34
+ data-testid="image-prompt"
35
+ />
36
+ <button type="submit" disabled={busy || !prompt.trim()} data-testid="image-go">
37
+ {busy ? 'Generating…' : 'Generate'}
38
+ </button>
39
+ </form>
40
+
41
+ {error ? <p className="error">{error}</p> : null}
42
+
43
+ <div className="image-grid" data-testid="image-result">
44
+ {result?.images.map((img, idx) => {
45
+ const src = img.b64 ? `data:${img.mimeType};base64,${img.b64}` : img.url;
46
+ return src ? <img key={idx} src={src} alt="" /> : null;
47
+ })}
48
+ </div>
49
+ </div>
50
+ );
51
+ }
@@ -0,0 +1,47 @@
1
+ import { FormEvent, useState } from 'react';
2
+ import { synthesizeSpeech } from '../api';
3
+
4
+ export function SpeechTab() {
5
+ const [text, setText] = useState('Hello from Kazzle.');
6
+ const [audioUrl, setAudioUrl] = useState<string | null>(null);
7
+ const [busy, setBusy] = useState(false);
8
+ const [error, setError] = useState<string | null>(null);
9
+
10
+ async function onSubmit(event: FormEvent) {
11
+ event.preventDefault();
12
+ if (!text.trim() || busy) return;
13
+ setBusy(true);
14
+ setError(null);
15
+ try {
16
+ const blob = await synthesizeSpeech(text);
17
+ // Revoke the previous URL to avoid leaking blob handles across
18
+ // multiple syntheses in the same session.
19
+ if (audioUrl) URL.revokeObjectURL(audioUrl);
20
+ setAudioUrl(URL.createObjectURL(blob));
21
+ } catch (err) {
22
+ setError(err instanceof Error ? err.message : String(err));
23
+ } finally {
24
+ setBusy(false);
25
+ }
26
+ }
27
+
28
+ return (
29
+ <div className="tab-body">
30
+ <form className="composer" onSubmit={onSubmit}>
31
+ <input
32
+ value={text}
33
+ onChange={e => setText(e.target.value)}
34
+ placeholder="Text to speak…"
35
+ disabled={busy}
36
+ data-testid="speech-text"
37
+ />
38
+ <button type="submit" disabled={busy || !text.trim()} data-testid="speech-go">
39
+ {busy ? 'Synthesising…' : 'Speak'}
40
+ </button>
41
+ </form>
42
+
43
+ {error ? <p className="error">{error}</p> : null}
44
+ {audioUrl ? <audio src={audioUrl} controls data-testid="speech-audio" /> : null}
45
+ </div>
46
+ );
47
+ }
@@ -0,0 +1,42 @@
1
+ import { ChangeEvent, useState } from 'react';
2
+ import { transcribeAudio } from '../api';
3
+ import type { TranscriptionResult } from '../types';
4
+
5
+ export function TranscribeTab() {
6
+ const [result, setResult] = useState<TranscriptionResult | null>(null);
7
+ const [busy, setBusy] = useState(false);
8
+ const [error, setError] = useState<string | null>(null);
9
+
10
+ async function onFile(event: ChangeEvent<HTMLInputElement>) {
11
+ const file = event.target.files?.[0];
12
+ if (!file) return;
13
+ setBusy(true);
14
+ setError(null);
15
+ setResult(null);
16
+ try {
17
+ setResult(await transcribeAudio(file));
18
+ } catch (err) {
19
+ setError(err instanceof Error ? err.message : String(err));
20
+ } finally {
21
+ setBusy(false);
22
+ }
23
+ }
24
+
25
+ return (
26
+ <div className="tab-body">
27
+ <label className="upload">
28
+ <input type="file" accept="audio/*" onChange={onFile} disabled={busy} data-testid="transcribe-file" />
29
+ <span>{busy ? 'Transcribing…' : 'Choose an audio file'}</span>
30
+ </label>
31
+
32
+ {error ? <p className="error">{error}</p> : null}
33
+
34
+ {result ? (
35
+ <article className="message message-assistant" data-testid="transcribe-result">
36
+ <span className="who">Transcript</span>
37
+ <p className="body">{result.text}</p>
38
+ </article>
39
+ ) : null}
40
+ </div>
41
+ );
42
+ }
@@ -0,0 +1,76 @@
1
+ import { FormEvent, useEffect, useRef, useState } from 'react';
2
+ import { pollVideoJob, startVideoJob } from '../api';
3
+ import type { AsyncJob } from '../types';
4
+
5
+ const POLL_INTERVAL_MS = 5_000;
6
+
7
+ export function VideoTab() {
8
+ const [prompt, setPrompt] = useState('a fox running across a snowy field');
9
+ const [job, setJob] = useState<AsyncJob | null>(null);
10
+ const [busy, setBusy] = useState(false);
11
+ const [error, setError] = useState<string | null>(null);
12
+ const pollTimer = useRef<ReturnType<typeof setTimeout> | null>(null);
13
+
14
+ useEffect(() => () => { if (pollTimer.current) clearTimeout(pollTimer.current); }, []);
15
+
16
+ function schedulePoll(jobId: string) {
17
+ if (pollTimer.current) clearTimeout(pollTimer.current);
18
+ pollTimer.current = setTimeout(async () => {
19
+ try {
20
+ const next = await pollVideoJob(jobId);
21
+ setJob(next);
22
+ if (next.status === 'queued' || next.status === 'processing') schedulePoll(jobId);
23
+ } catch (err) {
24
+ setError(err instanceof Error ? err.message : String(err));
25
+ }
26
+ }, POLL_INTERVAL_MS);
27
+ }
28
+
29
+ async function onSubmit(event: FormEvent) {
30
+ event.preventDefault();
31
+ if (!prompt.trim() || busy) return;
32
+ setBusy(true);
33
+ setError(null);
34
+ setJob(null);
35
+ try {
36
+ const started = await startVideoJob(prompt);
37
+ setJob(started);
38
+ const id = started.jobId ?? started.id;
39
+ if (id && (started.status === 'queued' || started.status === 'processing')) schedulePoll(id);
40
+ } catch (err) {
41
+ setError(err instanceof Error ? err.message : String(err));
42
+ } finally {
43
+ setBusy(false);
44
+ }
45
+ }
46
+
47
+ const videoUrl = (job?.output as { videoUrl?: string } | undefined)?.videoUrl;
48
+
49
+ return (
50
+ <div className="tab-body">
51
+ <form className="composer" onSubmit={onSubmit}>
52
+ <input
53
+ value={prompt}
54
+ onChange={e => setPrompt(e.target.value)}
55
+ placeholder="Describe a short clip…"
56
+ disabled={busy}
57
+ data-testid="video-prompt"
58
+ />
59
+ <button type="submit" disabled={busy || !prompt.trim()} data-testid="video-go">
60
+ {busy ? 'Submitting…' : 'Generate'}
61
+ </button>
62
+ </form>
63
+
64
+ {error ? <p className="error">{error}</p> : null}
65
+
66
+ {job ? (
67
+ <article className="message message-assistant" data-testid="video-status">
68
+ <span className="who">Job {(job.jobId ?? job.id ?? '').slice(0, 12)}…</span>
69
+ <p className="body">Status: {job.status}</p>
70
+ </article>
71
+ ) : null}
72
+
73
+ {videoUrl ? <video src={videoUrl} controls data-testid="video-player" /> : null}
74
+ </div>
75
+ );
76
+ }
@@ -0,0 +1,53 @@
1
+ export type ChatRole = 'system' | 'user' | 'assistant';
2
+
3
+ export interface ChatMessage {
4
+ role: ChatRole;
5
+ content: string;
6
+ }
7
+
8
+ export interface StructuredOutputSchema {
9
+ name: string;
10
+ schema: Record<string, unknown>;
11
+ strict?: boolean;
12
+ }
13
+
14
+ export interface ChatOptions {
15
+ model?: string;
16
+ max_completion_tokens?: number;
17
+ temperature?: number;
18
+ response_format?: {
19
+ type: 'json_schema';
20
+ json_schema: StructuredOutputSchema;
21
+ } | {
22
+ type: 'json_object';
23
+ } | {
24
+ type: 'text';
25
+ };
26
+ [key: string]: unknown;
27
+ }
28
+
29
+ export interface ChatResponse {
30
+ message: ChatMessage;
31
+ model: string;
32
+ structured?: unknown;
33
+ }
34
+
35
+ export interface ImageResult {
36
+ images: Array<{ url?: string; b64?: string; mimeType: string }>;
37
+ }
38
+
39
+ export interface AsyncJob {
40
+ jobId?: string;
41
+ id?: string;
42
+ status: 'queued' | 'processing' | 'succeeded' | 'failed';
43
+ pollUrl?: string;
44
+ output?: { videoUrl?: string; mimeType?: string } | Record<string, unknown>;
45
+ error?: string;
46
+ }
47
+
48
+ export interface TranscriptionResult {
49
+ text: string;
50
+ segments?: Array<{ start: number; end: number; text: string }>;
51
+ }
52
+
53
+ export type ModalityTab = 'chat' | 'extract' | 'image' | 'speech' | 'transcribe' | 'video';
@@ -0,0 +1 @@
1
+ /// <reference types="vite/client" />
@@ -0,0 +1,21 @@
1
+ {
2
+ "compilerOptions": {
3
+ "target": "ES2020",
4
+ "useDefineForClassFields": true,
5
+ "lib": ["ES2020", "DOM", "DOM.Iterable"],
6
+ "allowJs": false,
7
+ "skipLibCheck": true,
8
+ "esModuleInterop": true,
9
+ "allowSyntheticDefaultImports": true,
10
+ "strict": true,
11
+ "forceConsistentCasingInFileNames": true,
12
+ "module": "ESNext",
13
+ "moduleResolution": "bundler",
14
+ "resolveJsonModule": true,
15
+ "isolatedModules": true,
16
+ "noEmit": true,
17
+ "jsx": "react-jsx"
18
+ },
19
+ "include": ["src", "vite.config.ts"],
20
+ "references": []
21
+ }
@@ -0,0 +1,22 @@
1
+ /**
2
+ * Vite config for the UI component.
3
+ *
4
+ * Kazzle preview/runtime wiring (HOST/PORT binding, strictPort, allowedHosts,
5
+ * backend proxy) lives in @kazzle/app/vite — the ONE shared place that must
6
+ * never be reinvented. This template declares a backend `server` component; the
7
+ * browser calls relative owned routes (/chat, /image, …) and the helper proxies
8
+ * them to the sibling component. Add new backend routes to `proxyPaths`. Do not
9
+ * expose Kazzle's internal component URLs to the browser.
10
+ */
11
+ import { defineConfig } from 'vite';
12
+ import react from '@vitejs/plugin-react';
13
+ import tailwindcss from '@tailwindcss/vite';
14
+ import { kazzleAppVite } from '@kazzle/app/vite';
15
+
16
+ export default defineConfig(({ command }) => ({
17
+ plugins: [react(), tailwindcss()],
18
+ ...kazzleAppVite(command, {
19
+ apiComponent: 'server',
20
+ proxyPaths: ['/chat', '/image', '/speech', '/transcribe', '/video', '/health'],
21
+ }),
22
+ }));
@@ -0,0 +1,9 @@
1
+ import { defineConfig } from '@kazzle/app';
2
+
3
+ export default defineConfig({
4
+ icon: 'components/ui/public/favicon.svg',
5
+ components: [
6
+ { name: 'ui', type: 'ui', path: './components/ui' },
7
+ { name: 'server', type: 'process', path: './components/server/index.ts', runtime: { dev: 'bun run dev', run: 'bun run start' } },
8
+ ],
9
+ });