caudate-cli 0.1.0__py3-none-any.whl

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 (153) hide show
  1. api/__init__.py +5 -0
  2. api/anthropic_compat.py +1518 -0
  3. api/artifact_viewer.py +366 -0
  4. api/caudate_middleware.py +618 -0
  5. api/forge_bootstrapper_routes.py +377 -0
  6. api/forge_routes.py +630 -0
  7. api/forge_system_routes.py +294 -0
  8. api/openai_compat.py +1993 -0
  9. api/server.py +667 -0
  10. api/storyboard_page.py +677 -0
  11. caudate_cli-0.1.0.dist-info/METADATA +354 -0
  12. caudate_cli-0.1.0.dist-info/RECORD +153 -0
  13. caudate_cli-0.1.0.dist-info/WHEEL +5 -0
  14. caudate_cli-0.1.0.dist-info/entry_points.txt +2 -0
  15. caudate_cli-0.1.0.dist-info/licenses/LICENSE +21 -0
  16. caudate_cli-0.1.0.dist-info/top_level.txt +14 -0
  17. cognos_mcp/__init__.py +4 -0
  18. cognos_mcp/bridge.py +41 -0
  19. cognos_mcp/client.py +70 -0
  20. cognos_mcp/config.py +49 -0
  21. cognos_mcp/server.py +66 -0
  22. config.py +82 -0
  23. core/__init__.py +0 -0
  24. core/agent.py +468 -0
  25. core/agentic_loop.py +731 -0
  26. core/anthropic_auth.py +91 -0
  27. core/background.py +113 -0
  28. core/banner.py +134 -0
  29. core/bootstrap.py +292 -0
  30. core/citations.py +131 -0
  31. core/compaction.py +109 -0
  32. core/constitution.py +198 -0
  33. core/diff_viewer.py +87 -0
  34. core/export.py +85 -0
  35. core/file_refs.py +119 -0
  36. core/files.py +199 -0
  37. core/hooks.py +209 -0
  38. core/image.py +599 -0
  39. core/input.py +91 -0
  40. core/loop.py +238 -0
  41. core/memory_md.py +147 -0
  42. core/notifications.py +99 -0
  43. core/ownership.py +181 -0
  44. core/paste.py +81 -0
  45. core/permissions.py +210 -0
  46. core/plan_mode.py +215 -0
  47. core/sandbox_prompt.py +185 -0
  48. core/scheduler.py +195 -0
  49. core/schemas.py +202 -0
  50. core/session.py +90 -0
  51. core/settings.py +132 -0
  52. core/skills.py +398 -0
  53. core/slash_commands.py +977 -0
  54. core/statusline.py +61 -0
  55. core/subagent.py +300 -0
  56. core/thinking.py +50 -0
  57. core/updater.py +122 -0
  58. core/usage.py +109 -0
  59. core/worktree.py +93 -0
  60. execution/__init__.py +0 -0
  61. execution/executor.py +329 -0
  62. execution/plugins.py +108 -0
  63. execution/tools/__init__.py +0 -0
  64. execution/tools/agent_tool.py +107 -0
  65. execution/tools/agentic_tool.py +297 -0
  66. execution/tools/artifact_tool.py +191 -0
  67. execution/tools/ask_user_question_tool.py +137 -0
  68. execution/tools/base.py +81 -0
  69. execution/tools/calculator_tool.py +137 -0
  70. execution/tools/cognos_card_tool.py +124 -0
  71. execution/tools/cron_tool.py +215 -0
  72. execution/tools/datetime_tool.py +215 -0
  73. execution/tools/describe_image_tool.py +161 -0
  74. execution/tools/draw_tool.py +164 -0
  75. execution/tools/edit_image_tool.py +262 -0
  76. execution/tools/edit_tool.py +245 -0
  77. execution/tools/file_tool.py +90 -0
  78. execution/tools/find_anywhere_tool.py +255 -0
  79. execution/tools/forge_feature_tools.py +377 -0
  80. execution/tools/glob_tool.py +59 -0
  81. execution/tools/grep_tool.py +89 -0
  82. execution/tools/http_request_tool.py +224 -0
  83. execution/tools/load_skill_tool.py +104 -0
  84. execution/tools/longcat_avatar_tool.py +384 -0
  85. execution/tools/mcp_tool.py +100 -0
  86. execution/tools/notebook_tool.py +279 -0
  87. execution/tools/openapi_tool.py +440 -0
  88. execution/tools/plan_mode_tool.py +95 -0
  89. execution/tools/push_notification_tool.py +157 -0
  90. execution/tools/python_tool.py +61 -0
  91. execution/tools/respond_tool.py +40 -0
  92. execution/tools/sandbox_tool.py +378 -0
  93. execution/tools/search_tool.py +153 -0
  94. execution/tools/semantic_search_tool.py +106 -0
  95. execution/tools/shell_tool.py +283 -0
  96. execution/tools/speak_tool.py +134 -0
  97. execution/tools/storyboard_tool.py +727 -0
  98. execution/tools/system_info_tool.py +212 -0
  99. execution/tools/task_tool.py +323 -0
  100. execution/tools/think_tool.py +49 -0
  101. execution/tools/transcribe_audio_tool.py +86 -0
  102. execution/tools/update_memory_tool.py +92 -0
  103. execution/tools/web_fetch_tool.py +82 -0
  104. execution/tools/worktree_tool.py +174 -0
  105. llm/__init__.py +0 -0
  106. llm/fallback.py +116 -0
  107. llm/models.py +320 -0
  108. llm/provider.py +1356 -0
  109. llm/router.py +373 -0
  110. main.py +1889 -0
  111. memory/__init__.py +0 -0
  112. memory/episodic.py +99 -0
  113. memory/procedural.py +145 -0
  114. memory/semantic.py +71 -0
  115. memory/working.py +64 -0
  116. nn/__init__.py +43 -0
  117. nn/auto_evolve.py +245 -0
  118. nn/caudate.py +136 -0
  119. nn/config.py +141 -0
  120. nn/consolidator.py +81 -0
  121. nn/data.py +1635 -0
  122. nn/encoder.py +258 -0
  123. nn/forge_advisor.py +303 -0
  124. nn/format.py +235 -0
  125. nn/heads.py +432 -0
  126. nn/observer.py +994 -0
  127. nn/policy.py +214 -0
  128. nn/runtime.py +343 -0
  129. nn/scorer.py +175 -0
  130. nn/trainer.py +515 -0
  131. nn/vision.py +352 -0
  132. personality/__init__.py +23 -0
  133. personality/engine.py +129 -0
  134. personality/identity.py +144 -0
  135. personality/inner_voice.py +100 -0
  136. personality/mood.py +205 -0
  137. planning/__init__.py +0 -0
  138. planning/dev_server.py +221 -0
  139. planning/forge_models.py +718 -0
  140. planning/orchestrator.py +1363 -0
  141. planning/planner.py +451 -0
  142. planning/task_graph.py +61 -0
  143. reflection/__init__.py +0 -0
  144. reflection/meta_learner.py +156 -0
  145. reflection/reflector.py +127 -0
  146. ui/__init__.py +5 -0
  147. ui/display.py +88 -0
  148. voice/__init__.py +0 -0
  149. voice/conversation.py +125 -0
  150. voice/listener.py +111 -0
  151. voice/speaker.py +59 -0
  152. voice/stt.py +126 -0
  153. voice/tts.py +214 -0
api/storyboard_page.py ADDED
@@ -0,0 +1,677 @@
1
+ """Storyboard — single-page HTML app.
2
+
3
+ Served at `GET /storyboard`. Talks to `POST /storyboard/generate` over
4
+ Server-Sent Events for live progress as each panel renders.
5
+
6
+ Self-contained: no build step, no npm, no React. All deps come from
7
+ CDN at runtime (JSZip for ZIP export). IndexedDB persists run history
8
+ client-side.
9
+
10
+ This file produces the HTML as a single string. Loaded once on app
11
+ boot and cached.
12
+ """
13
+
14
+ from __future__ import annotations
15
+
16
+
17
+ def render_storyboard_page() -> str:
18
+ return _PAGE
19
+
20
+
21
+ _PAGE = r"""<!DOCTYPE html>
22
+ <html lang="en" data-theme="dark">
23
+ <head>
24
+ <meta charset="UTF-8">
25
+ <meta name="viewport" content="width=device-width, initial-scale=1.0">
26
+ <title>Storyboard — Cognos</title>
27
+ <script src="https://cdnjs.cloudflare.com/ajax/libs/jszip/3.10.1/jszip.min.js"></script>
28
+ <style>
29
+ :root {
30
+ --bg: #0f1115;
31
+ --bg-elev: #181b21;
32
+ --bg-pre: #0c0e12;
33
+ --bg-hover: #232730;
34
+ --text: #e6e7eb;
35
+ --text-dim: #8a8e98;
36
+ --border: #262a32;
37
+ --accent: #7aa2f7;
38
+ --accent-strong:#5b87ff;
39
+ --green: #6cba6c;
40
+ --red: #e57373;
41
+ --yellow: #d8b65a;
42
+ }
43
+ [data-theme="light"] {
44
+ --bg: #f5f6f8;
45
+ --bg-elev: #ffffff;
46
+ --bg-pre: #ebedf1;
47
+ --bg-hover: #e6e9ee;
48
+ --text: #1c1f24;
49
+ --text-dim: #5b6068;
50
+ --border: #d0d5dc;
51
+ --accent: #2c63d6;
52
+ --accent-strong: #1a4cba;
53
+ }
54
+ * { box-sizing: border-box; margin: 0; padding: 0; }
55
+ html, body { height: 100%; }
56
+ body {
57
+ background: var(--bg);
58
+ color: var(--text);
59
+ font-family: -apple-system, BlinkMacSystemFont, "Segoe UI", Roboto, sans-serif;
60
+ font-size: 14px;
61
+ line-height: 1.5;
62
+ display: flex;
63
+ flex-direction: column;
64
+ min-height: 100vh;
65
+ }
66
+ header {
67
+ display: flex; align-items: center; gap: 16px;
68
+ padding: 12px 20px;
69
+ background: var(--bg-elev);
70
+ border-bottom: 1px solid var(--border);
71
+ }
72
+ header h1 { font-size: 16px; font-weight: 600; }
73
+ header .meta { color: var(--text-dim); font-size: 12px; }
74
+ header .controls { margin-left: auto; display: flex; gap: 8px; }
75
+ main {
76
+ flex: 1; display: flex; gap: 16px;
77
+ padding: 16px 20px; max-width: 1600px;
78
+ width: 100%; margin: 0 auto;
79
+ }
80
+ .panel {
81
+ background: var(--bg-elev);
82
+ border: 1px solid var(--border);
83
+ border-radius: 6px;
84
+ padding: 16px;
85
+ }
86
+ .left { flex: 0 0 380px; display: flex; flex-direction: column; gap: 12px; }
87
+ .right { flex: 1; min-width: 0; display: flex; flex-direction: column; gap: 12px; }
88
+
89
+ label { display: block; color: var(--text-dim); font-size: 12px; margin-bottom: 4px; }
90
+ textarea, input[type="text"], input[type="number"], select {
91
+ width: 100%;
92
+ background: var(--bg);
93
+ color: var(--text);
94
+ border: 1px solid var(--border);
95
+ border-radius: 4px;
96
+ padding: 8px 10px;
97
+ font-family: inherit; font-size: 13px;
98
+ resize: vertical;
99
+ }
100
+ textarea { min-height: 180px; line-height: 1.5; }
101
+ input:focus, textarea:focus, select:focus {
102
+ outline: none; border-color: var(--accent);
103
+ }
104
+ .row { display: flex; gap: 8px; }
105
+ .row > * { flex: 1; }
106
+ .form-grid {
107
+ display: grid; grid-template-columns: 1fr 1fr; gap: 8px;
108
+ }
109
+ .form-grid > .full { grid-column: 1 / -1; }
110
+
111
+ button, .btn {
112
+ background: var(--accent);
113
+ color: white;
114
+ border: 1px solid var(--accent-strong);
115
+ padding: 8px 16px;
116
+ font-size: 13px; font-weight: 500;
117
+ border-radius: 4px; cursor: pointer;
118
+ font-family: inherit;
119
+ }
120
+ button:hover { background: var(--accent-strong); }
121
+ button:disabled {
122
+ background: var(--bg-pre); color: var(--text-dim);
123
+ border-color: var(--border); cursor: not-allowed;
124
+ }
125
+ button.secondary {
126
+ background: var(--bg);
127
+ color: var(--text);
128
+ border: 1px solid var(--border);
129
+ }
130
+ button.secondary:hover { background: var(--bg-hover); }
131
+
132
+ .progress-strip {
133
+ padding: 8px 12px;
134
+ background: var(--bg-pre);
135
+ border-radius: 4px;
136
+ font-size: 12px; color: var(--text-dim);
137
+ font-family: ui-monospace, "SF Mono", Consolas, monospace;
138
+ max-height: 140px; overflow-y: auto;
139
+ }
140
+ .progress-strip .ev { padding: 2px 0; }
141
+ .progress-strip .ev.error { color: var(--red); }
142
+ .progress-strip .ev.done { color: var(--green); }
143
+ .progress-strip .ev.breakdown { color: var(--accent); }
144
+ .progress-strip .ev.panel { color: var(--text); }
145
+
146
+ .grid {
147
+ display: grid;
148
+ grid-template-columns: repeat(auto-fill, minmax(280px, 1fr));
149
+ gap: 12px;
150
+ }
151
+ .card {
152
+ background: var(--bg-elev);
153
+ border: 1px solid var(--border);
154
+ border-radius: 6px;
155
+ overflow: hidden;
156
+ display: flex; flex-direction: column;
157
+ transition: border-color 0.15s;
158
+ }
159
+ .card:hover { border-color: var(--accent); }
160
+ .card .img-wrap {
161
+ aspect-ratio: 1 / 1;
162
+ background: var(--bg-pre);
163
+ display: flex; align-items: center; justify-content: center;
164
+ cursor: pointer; overflow: hidden;
165
+ }
166
+ .card .img-wrap img {
167
+ width: 100%; height: 100%; object-fit: cover;
168
+ }
169
+ .card .img-wrap.placeholder {
170
+ color: var(--text-dim); font-size: 11px;
171
+ }
172
+ .card .body {
173
+ padding: 10px 12px; font-size: 12px;
174
+ }
175
+ .card .body .panel-num {
176
+ color: var(--accent); font-weight: 600; font-size: 11px;
177
+ text-transform: uppercase; letter-spacing: 0.5px;
178
+ }
179
+ .card .body .scene {
180
+ margin-top: 4px; line-height: 1.4;
181
+ display: -webkit-box; -webkit-line-clamp: 3;
182
+ -webkit-box-orient: vertical; overflow: hidden;
183
+ }
184
+
185
+ .modal-bg {
186
+ position: fixed; inset: 0;
187
+ background: rgba(0,0,0,0.85);
188
+ display: none; z-index: 100;
189
+ padding: 24px;
190
+ align-items: center; justify-content: center;
191
+ flex-direction: column; gap: 16px;
192
+ }
193
+ .modal-bg.show { display: flex; }
194
+ .modal-bg img { max-width: 95vw; max-height: 80vh; border-radius: 4px; }
195
+ .modal-bg .modal-caption {
196
+ max-width: 800px; text-align: center;
197
+ color: white; font-size: 13px; line-height: 1.5;
198
+ }
199
+ .modal-bg .close {
200
+ position: absolute; top: 16px; right: 20px;
201
+ background: transparent; border: none;
202
+ color: white; font-size: 24px; cursor: pointer;
203
+ }
204
+
205
+ .history h3 { font-size: 12px; color: var(--text-dim); margin-bottom: 8px; text-transform: uppercase; letter-spacing: 0.5px; }
206
+ .history-list { display: flex; flex-direction: column; gap: 4px; }
207
+ .history-item {
208
+ padding: 6px 10px; background: var(--bg-pre); border-radius: 4px;
209
+ cursor: pointer; font-size: 12px;
210
+ display: flex; justify-content: space-between;
211
+ align-items: center; gap: 8px;
212
+ }
213
+ .history-item:hover { background: var(--bg-hover); }
214
+ .history-item .meta { color: var(--text-dim); font-size: 10px; }
215
+ .history-item .delete {
216
+ background: transparent; border: none;
217
+ color: var(--text-dim); cursor: pointer;
218
+ padding: 2px 4px; font-size: 14px;
219
+ }
220
+ .history-item .delete:hover { color: var(--red); }
221
+ .empty-msg { color: var(--text-dim); font-size: 12px; font-style: italic; padding: 8px 0; }
222
+
223
+ .characters {
224
+ background: var(--bg-pre);
225
+ border: 1px solid var(--border);
226
+ border-radius: 4px; padding: 10px 12px;
227
+ font-size: 12px;
228
+ }
229
+ .characters .label { color: var(--accent); font-weight: 600; margin-bottom: 4px; }
230
+ .characters .char { margin: 4px 0; }
231
+ .characters .char .name { font-weight: 600; color: var(--text); }
232
+ .characters .char .desc { color: var(--text-dim); }
233
+
234
+ @media (max-width: 800px) {
235
+ main { flex-direction: column; }
236
+ .left { flex: 1 1 auto; }
237
+ }
238
+ </style>
239
+ </head>
240
+ <body>
241
+ <header>
242
+ <h1>Storyboard — Cognos</h1>
243
+ <span class="meta">FLUX-schnell + FLUX.1-Kontext-dev</span>
244
+ <div class="controls">
245
+ <button class="secondary" id="theme-toggle" title="Toggle theme">🌓</button>
246
+ <a class="btn secondary" href="/healthz" target="_blank">Server</a>
247
+ </div>
248
+ </header>
249
+
250
+ <main>
251
+ <aside class="left">
252
+ <div class="panel">
253
+ <label for="script">Script</label>
254
+ <textarea id="script" placeholder="Paste a story, scene description, or short script. Name your characters; describe their look once and reuse the names. Example: 'A small ginger cat named Rust, with a white chest and bright green eyes, wakes in a cozy attic, climbs out a moonlit window, leaps across rooftops chasing fireflies, then sleeps on a mossy chimney as the sun rises.'"></textarea>
255
+
256
+ <div class="form-grid" style="margin-top: 12px;">
257
+ <div>
258
+ <label for="panels">Panels (1–12)</label>
259
+ <input type="number" id="panels" min="1" max="12" value="6">
260
+ </div>
261
+ <div>
262
+ <label for="aspect">Aspect</label>
263
+ <select id="aspect">
264
+ <option value="1024x1024">1:1 (square)</option>
265
+ <option value="1280x720">16:9</option>
266
+ <option value="768x1024">3:4</option>
267
+ </select>
268
+ </div>
269
+ <div class="full">
270
+ <label for="style">Style hint</label>
271
+ <input type="text" id="style" value="warm storybook illustration, soft lighting" placeholder="e.g. comic book panels, watercolor, anime, noir film stills">
272
+ </div>
273
+ <div>
274
+ <label for="seed">Seed (optional)</label>
275
+ <input type="number" id="seed" placeholder="random">
276
+ </div>
277
+ <div>
278
+ <label>&nbsp;</label>
279
+ <button id="generate">Generate</button>
280
+ </div>
281
+ </div>
282
+ <div id="progress" class="progress-strip" style="margin-top: 12px; display: none;"></div>
283
+ </div>
284
+
285
+ <div class="panel history">
286
+ <h3>History</h3>
287
+ <div id="history-list" class="history-list"></div>
288
+ </div>
289
+ </aside>
290
+
291
+ <section class="right">
292
+ <div id="characters-card" class="characters" style="display: none;">
293
+ <div class="label">Cast</div>
294
+ <div id="characters-body"></div>
295
+ <div style="margin-top: 8px; color: var(--text-dim); font-size: 11px;">
296
+ Style: <span id="art-style-text"></span>
297
+ </div>
298
+ </div>
299
+
300
+ <div id="grid" class="grid"></div>
301
+
302
+ <div id="actions" class="panel" style="display: none;">
303
+ <div class="row">
304
+ <button id="download-zip" class="secondary">Download ZIP</button>
305
+ <button id="copy-links" class="secondary">Copy markdown gallery</button>
306
+ <button id="open-in-chat" class="secondary">Open in chat</button>
307
+ </div>
308
+ </div>
309
+ </section>
310
+ </main>
311
+
312
+ <div class="modal-bg" id="modal" tabindex="-1">
313
+ <button class="close" id="modal-close">×</button>
314
+ <img id="modal-img" src="" alt="">
315
+ <div class="modal-caption" id="modal-caption"></div>
316
+ </div>
317
+
318
+ <script>
319
+ // -------------------------------------------------------------------
320
+ // Theme
321
+ // -------------------------------------------------------------------
322
+ const themeBtn = document.getElementById('theme-toggle');
323
+ function applyTheme() {
324
+ const t = localStorage.getItem('cognos-storyboard-theme') || 'dark';
325
+ document.documentElement.setAttribute('data-theme', t);
326
+ }
327
+ applyTheme();
328
+ themeBtn.addEventListener('click', () => {
329
+ const cur = document.documentElement.getAttribute('data-theme');
330
+ const next = cur === 'dark' ? 'light' : 'dark';
331
+ document.documentElement.setAttribute('data-theme', next);
332
+ localStorage.setItem('cognos-storyboard-theme', next);
333
+ });
334
+
335
+ // -------------------------------------------------------------------
336
+ // IndexedDB history
337
+ // -------------------------------------------------------------------
338
+ const DB_NAME = 'cognos-storyboard';
339
+ const STORE = 'runs';
340
+ const dbReady = new Promise((resolve, reject) => {
341
+ const req = indexedDB.open(DB_NAME, 1);
342
+ req.onupgradeneeded = (ev) => {
343
+ const db = ev.target.result;
344
+ if (!db.objectStoreNames.contains(STORE)) {
345
+ const os = db.createObjectStore(STORE, { keyPath: 'id', autoIncrement: true });
346
+ os.createIndex('created_at', 'created_at');
347
+ }
348
+ };
349
+ req.onsuccess = () => resolve(req.result);
350
+ req.onerror = () => reject(req.error);
351
+ });
352
+ async function dbAdd(rec) {
353
+ const db = await dbReady;
354
+ return new Promise((resolve, reject) => {
355
+ const tx = db.transaction(STORE, 'readwrite');
356
+ const r = tx.objectStore(STORE).add(rec);
357
+ r.onsuccess = () => resolve(r.result);
358
+ r.onerror = () => reject(r.error);
359
+ });
360
+ }
361
+ async function dbAll() {
362
+ const db = await dbReady;
363
+ return new Promise((resolve, reject) => {
364
+ const tx = db.transaction(STORE, 'readonly');
365
+ const r = tx.objectStore(STORE).getAll();
366
+ r.onsuccess = () => resolve((r.result || []).sort((a, b) => b.created_at - a.created_at));
367
+ r.onerror = () => reject(r.error);
368
+ });
369
+ }
370
+ async function dbDelete(id) {
371
+ const db = await dbReady;
372
+ return new Promise((resolve, reject) => {
373
+ const tx = db.transaction(STORE, 'readwrite');
374
+ const r = tx.objectStore(STORE).delete(id);
375
+ r.onsuccess = () => resolve();
376
+ r.onerror = () => reject(r.error);
377
+ });
378
+ }
379
+
380
+ // -------------------------------------------------------------------
381
+ // State + DOM refs
382
+ // -------------------------------------------------------------------
383
+ const $ = (id) => document.getElementById(id);
384
+ const grid = $('grid');
385
+ const progress = $('progress');
386
+ const charactersCard = $('characters-card');
387
+ const charactersBody = $('characters-body');
388
+ const artStyleText = $('art-style-text');
389
+ const actions = $('actions');
390
+ const historyList = $('history-list');
391
+
392
+ let currentRun = null; // { plan, panels: [{file_id, scene_action, ...}] }
393
+ let isGenerating = false;
394
+
395
+ function logProgress(msg, kind) {
396
+ const ev = document.createElement('div');
397
+ ev.className = 'ev ' + (kind || '');
398
+ ev.textContent = `[${new Date().toLocaleTimeString()}] ${msg}`;
399
+ progress.appendChild(ev);
400
+ progress.scrollTop = progress.scrollHeight;
401
+ }
402
+
403
+ function renderPlaceholders(n) {
404
+ grid.innerHTML = '';
405
+ for (let i = 0; i < n; i++) {
406
+ const card = document.createElement('div');
407
+ card.className = 'card';
408
+ card.id = `card-${i + 1}`;
409
+ card.innerHTML = `
410
+ <div class="img-wrap placeholder">Panel ${i + 1} — pending</div>
411
+ <div class="body">
412
+ <div class="panel-num">Panel ${i + 1}</div>
413
+ <div class="scene"></div>
414
+ </div>
415
+ `;
416
+ grid.appendChild(card);
417
+ }
418
+ }
419
+ function fillCard(i, panel) {
420
+ const card = $(`card-${i}`);
421
+ if (!card) return;
422
+ const wrap = card.querySelector('.img-wrap');
423
+ const url = `/files/${panel.file_id}/content`;
424
+ wrap.classList.remove('placeholder');
425
+ wrap.innerHTML = `<img src="${url}" alt="Panel ${i}">`;
426
+ wrap.addEventListener('click', () => openModal(url, panel.scene_action || ''));
427
+ card.querySelector('.scene').textContent = panel.scene_action || panel.panel_text || '';
428
+ }
429
+ function fillBreakdown(plan) {
430
+ charactersCard.style.display = 'block';
431
+ artStyleText.textContent = plan.art_style || '';
432
+ charactersBody.innerHTML = '';
433
+ (plan.characters || []).forEach((c) => {
434
+ const row = document.createElement('div');
435
+ row.className = 'char';
436
+ row.innerHTML = `<span class="name">${escapeHtml(c.name)}:</span> <span class="desc">${escapeHtml(c.visual_description)}</span>`;
437
+ charactersBody.appendChild(row);
438
+ });
439
+ if (!(plan.characters || []).length) {
440
+ charactersBody.innerHTML = '<div class="empty-msg">No named cast — story used a generic narrator.</div>';
441
+ }
442
+ // Update placeholder scene text to match the plan's panels so users
443
+ // see where each panel is going before pixels arrive.
444
+ (plan.panels || []).forEach((p, idx) => {
445
+ const card = $(`card-${idx + 1}`);
446
+ if (card) card.querySelector('.scene').textContent = p.scene_action || '';
447
+ });
448
+ }
449
+ function escapeHtml(s) {
450
+ return String(s || '').replace(/[&<>"']/g, (c) =>
451
+ ({ '&': '&amp;', '<': '&lt;', '>': '&gt;', '"': '&quot;', "'": '&#39;' }[c]));
452
+ }
453
+
454
+ // -------------------------------------------------------------------
455
+ // Modal
456
+ // -------------------------------------------------------------------
457
+ const modal = $('modal');
458
+ $('modal-close').addEventListener('click', () => modal.classList.remove('show'));
459
+ modal.addEventListener('click', (e) => { if (e.target === modal) modal.classList.remove('show'); });
460
+ window.addEventListener('keydown', (e) => { if (e.key === 'Escape') modal.classList.remove('show'); });
461
+ function openModal(url, caption) {
462
+ $('modal-img').src = url;
463
+ $('modal-caption').textContent = caption || '';
464
+ modal.classList.add('show');
465
+ }
466
+
467
+ // -------------------------------------------------------------------
468
+ // Generate (SSE)
469
+ // -------------------------------------------------------------------
470
+ $('generate').addEventListener('click', async () => {
471
+ if (isGenerating) return;
472
+ const story = $('script').value.trim();
473
+ if (!story) return alert('Paste a story or script first.');
474
+
475
+ const panels = parseInt($('panels').value, 10) || 6;
476
+ const size = $('aspect').value;
477
+ const style = $('style').value.trim();
478
+ const seedRaw = $('seed').value.trim();
479
+ const seed = seedRaw ? parseInt(seedRaw, 10) : null;
480
+
481
+ isGenerating = true;
482
+ $('generate').disabled = true;
483
+ $('generate').textContent = 'Generating…';
484
+ actions.style.display = 'none';
485
+ charactersCard.style.display = 'none';
486
+ progress.style.display = 'block';
487
+ progress.innerHTML = '';
488
+ renderPlaceholders(panels);
489
+ currentRun = { plan: null, panels: [] };
490
+
491
+ logProgress(`Starting ${panels}-panel storyboard…`, 'breakdown');
492
+ let resp;
493
+ try {
494
+ resp = await fetch('/storyboard/generate', {
495
+ method: 'POST',
496
+ headers: { 'Content-Type': 'application/json' },
497
+ body: JSON.stringify({ story, panels, style, size, seed }),
498
+ });
499
+ } catch (e) {
500
+ logProgress('network error: ' + e.message, 'error');
501
+ finishGeneration(false);
502
+ return;
503
+ }
504
+ if (!resp.ok || !resp.body) {
505
+ logProgress(`HTTP ${resp.status}`, 'error');
506
+ finishGeneration(false);
507
+ return;
508
+ }
509
+ const reader = resp.body.getReader();
510
+ const decoder = new TextDecoder();
511
+ let buffer = '';
512
+ let aborted = false;
513
+ while (true) {
514
+ const { done, value } = await reader.read();
515
+ if (done) break;
516
+ buffer += decoder.decode(value, { stream: true });
517
+ let idx;
518
+ while ((idx = buffer.indexOf('\n\n')) >= 0) {
519
+ const chunk = buffer.slice(0, idx);
520
+ buffer = buffer.slice(idx + 2);
521
+ const lines = chunk.split('\n');
522
+ let dataLine = '';
523
+ for (const ln of lines) {
524
+ if (ln.startsWith('data:')) dataLine += ln.slice(5).trim();
525
+ }
526
+ if (!dataLine) continue;
527
+ let ev;
528
+ try { ev = JSON.parse(dataLine); } catch { continue; }
529
+ handleEvent(ev);
530
+ if (ev.type === 'error') aborted = true;
531
+ }
532
+ }
533
+ finishGeneration(!aborted && currentRun.panels.length > 0);
534
+ });
535
+
536
+ function handleEvent(ev) {
537
+ if (ev.type === 'breakdown') {
538
+ currentRun.plan = ev.plan;
539
+ fillBreakdown(ev.plan);
540
+ logProgress(`Got breakdown: ${(ev.plan.characters || []).length} character(s), ${(ev.plan.panels || []).length} panel(s)`, 'breakdown');
541
+ } else if (ev.type === 'panel') {
542
+ currentRun.panels.push(ev);
543
+ fillCard(ev.index, ev);
544
+ logProgress(`Panel ${ev.index} done — ${ev.scene_action.slice(0, 60)}`, 'panel');
545
+ } else if (ev.type === 'done') {
546
+ logProgress(`Done. ${ev.panel_count} panel(s) saved.`, 'done');
547
+ } else if (ev.type === 'error') {
548
+ logProgress('error: ' + ev.message, 'error');
549
+ }
550
+ }
551
+
552
+ async function finishGeneration(success) {
553
+ isGenerating = false;
554
+ $('generate').disabled = false;
555
+ $('generate').textContent = 'Generate';
556
+ if (success && currentRun && currentRun.panels.length > 0) {
557
+ actions.style.display = 'block';
558
+ // Persist
559
+ try {
560
+ await dbAdd({
561
+ created_at: Date.now(),
562
+ story: $('script').value.trim(),
563
+ plan: currentRun.plan,
564
+ panels: currentRun.panels,
565
+ size: $('aspect').value,
566
+ style_hint: $('style').value.trim(),
567
+ panel_count: $('panels').value,
568
+ });
569
+ refreshHistory();
570
+ } catch (e) {
571
+ console.warn('history save failed', e);
572
+ }
573
+ }
574
+ }
575
+
576
+ // -------------------------------------------------------------------
577
+ // Actions: ZIP, copy markdown, open in chat
578
+ // -------------------------------------------------------------------
579
+ $('download-zip').addEventListener('click', async () => {
580
+ if (!currentRun || !currentRun.panels.length) return;
581
+ const zip = new JSZip();
582
+ const folder = zip.folder('storyboard');
583
+ for (const p of currentRun.panels) {
584
+ const blob = await fetch(`/files/${p.file_id}/content`).then(r => r.blob());
585
+ folder.file(`panel_${String(p.index).padStart(2, '0')}.png`, blob);
586
+ }
587
+ if (currentRun.plan) {
588
+ folder.file('plan.json', JSON.stringify({
589
+ art_style: currentRun.plan.art_style,
590
+ characters: currentRun.plan.characters,
591
+ panels: currentRun.panels,
592
+ }, null, 2));
593
+ }
594
+ const out = await zip.generateAsync({ type: 'blob' });
595
+ const url = URL.createObjectURL(out);
596
+ const a = document.createElement('a');
597
+ a.href = url;
598
+ a.download = 'storyboard.zip';
599
+ a.click();
600
+ URL.revokeObjectURL(url);
601
+ });
602
+
603
+ $('copy-links').addEventListener('click', async () => {
604
+ if (!currentRun || !currentRun.panels.length) return;
605
+ const lines = [];
606
+ if (currentRun.plan) {
607
+ lines.push(`**Style:** ${currentRun.plan.art_style}`);
608
+ lines.push('');
609
+ }
610
+ currentRun.panels.forEach((p) => {
611
+ const url = `${window.location.origin}/files/${p.file_id}/content`;
612
+ lines.push(`**Panel ${p.index}.** ${p.scene_action || ''}`);
613
+ lines.push(`![${escapeHtml(p.scene_action || '')}](${url})`);
614
+ lines.push('');
615
+ });
616
+ await navigator.clipboard.writeText(lines.join('\n'));
617
+ alert('Copied markdown gallery to clipboard.');
618
+ });
619
+
620
+ $('open-in-chat').addEventListener('click', () => {
621
+ // Open WebUI in a new tab — assumes default port 8080.
622
+ window.open('http://127.0.0.1:8080', '_blank');
623
+ });
624
+
625
+ // -------------------------------------------------------------------
626
+ // History
627
+ // -------------------------------------------------------------------
628
+ async function refreshHistory() {
629
+ const runs = await dbAll();
630
+ historyList.innerHTML = '';
631
+ if (!runs.length) {
632
+ historyList.innerHTML = '<div class="empty-msg">No past runs yet.</div>';
633
+ return;
634
+ }
635
+ runs.slice(0, 30).forEach((r) => {
636
+ const item = document.createElement('div');
637
+ item.className = 'history-item';
638
+ const dt = new Date(r.created_at).toLocaleString();
639
+ const story = (r.story || '').slice(0, 60) + (r.story && r.story.length > 60 ? '…' : '');
640
+ item.innerHTML = `
641
+ <div style="flex:1; min-width:0;">
642
+ <div style="overflow: hidden; text-overflow: ellipsis; white-space: nowrap;">${escapeHtml(story)}</div>
643
+ <div class="meta">${r.panel_count} panel(s) · ${dt}</div>
644
+ </div>
645
+ <button class="delete" data-id="${r.id}" title="Delete">×</button>
646
+ `;
647
+ item.addEventListener('click', (e) => {
648
+ if (e.target.classList.contains('delete')) return;
649
+ restoreRun(r);
650
+ });
651
+ item.querySelector('.delete').addEventListener('click', async (e) => {
652
+ e.stopPropagation();
653
+ await dbDelete(r.id);
654
+ refreshHistory();
655
+ });
656
+ historyList.appendChild(item);
657
+ });
658
+ }
659
+ function restoreRun(r) {
660
+ currentRun = { plan: r.plan, panels: r.panels };
661
+ $('script').value = r.story || '';
662
+ $('panels').value = r.panel_count || 6;
663
+ if (r.size) $('aspect').value = r.size;
664
+ if (r.style_hint) $('style').value = r.style_hint;
665
+
666
+ if (r.plan) fillBreakdown(r.plan);
667
+ renderPlaceholders((r.panels || []).length);
668
+ (r.panels || []).forEach((p) => fillCard(p.index, p));
669
+ actions.style.display = 'block';
670
+ progress.style.display = 'none';
671
+ window.scrollTo(0, 0);
672
+ }
673
+ refreshHistory();
674
+ </script>
675
+ </body>
676
+ </html>
677
+ """