retake-demos 0.2.2 → 0.4.0

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.
@@ -1,724 +0,0 @@
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" />
6
- <title>Retake</title>
7
- <link rel="icon" href="/favicon.svg" type="image/svg+xml" />
8
- <link rel="apple-touch-icon" href="/icon-192.png" />
9
- <link rel="manifest" href="/manifest.webmanifest" />
10
- <meta name="theme-color" content="#edefe8" />
11
- <style>
12
- :root { --paper:#edefe8; --card:#f8f9f5; --ink:#191d19; --muted:#767e74; --rule:#dbdfd3; --rule2:#c9cec0; --accent:#1a1c1e; --amber:#e8b04b; --ok:#2f5d3f; --bad:#a23b2c; }
13
- * { box-sizing:border-box; }
14
- html,body { margin:0; background:var(--paper); color:var(--ink); font:15px/1.5 -apple-system, BlinkMacSystemFont, "SF Pro Text", "Helvetica Neue", Inter, Arial, sans-serif; -webkit-font-smoothing:antialiased; }
15
- button, input, select, textarea { font:inherit; color:inherit; }
16
- .top { display:flex; align-items:center; gap:12px; padding:14px 22px; border-bottom:1px solid var(--rule); position:sticky; top:0; z-index:5; background:var(--paper); }
17
- .mark { width:26px; height:26px; border-radius:7px; background:var(--accent); position:relative; flex:none; }
18
- .mark i { position:absolute; left:7px; height:2.5px; border-radius:2px; background:#eceee9; }
19
- .mark i:nth-child(1){top:6px;width:5px;opacity:.55} .mark i:nth-child(2){top:10px;width:9px;opacity:.8} .mark i:nth-child(3){top:14px;width:13px;background:var(--amber)} .mark i:nth-child(4){top:18px;width:9px;opacity:.8}
20
- .top h1 { font-size:17px; font-weight:600; margin:0; letter-spacing:-.01em; }
21
- .top .tag { color:var(--muted); font-size:13px; }
22
- .top .sp { flex:1; }
23
- .top .model { font-size:12px; color:var(--muted); }
24
- .iconbtn { width:34px; height:34px; border-radius:9px; border:1px solid var(--rule); background:var(--card); cursor:pointer; display:grid; place-items:center; color:var(--muted); }
25
- .iconbtn:hover { border-color:var(--rule2); color:var(--ink); }
26
- .wrap { max-width:1180px; margin:0 auto; padding:22px 22px 80px; display:grid; grid-template-columns:220px minmax(0,1fr); gap:20px; }
27
- @media (max-width: 860px){ .wrap { grid-template-columns:1fr; } }
28
- .side { position:sticky; top:78px; align-self:start; }
29
- .card { background:var(--card); border:1px solid var(--rule); border-radius:14px; padding:16px 18px; }
30
- .label { font:11px/1 ui-monospace, "SF Mono", Menlo, monospace; letter-spacing:.06em; text-transform:uppercase; color:var(--muted); margin-bottom:12px; }
31
- .demo { display:flex; align-items:center; gap:9px; padding:8px 10px; border-radius:10px; cursor:pointer; }
32
- .demo:hover { background:#eef1ea; }
33
- .demo.on { background:var(--accent); color:#eceee9; }
34
- .demo .name { flex:1; min-width:0; overflow:hidden; text-overflow:ellipsis; white-space:nowrap; font-size:14px; }
35
- .dot { width:7px; height:7px; border-radius:50%; background:var(--rule2); flex:none; }
36
- .dot.ok { background:var(--ok);} .dot.bad { background:var(--bad);}
37
- .x { display:none; border:0; background:transparent; color:inherit; cursor:pointer; padding:0 4px; line-height:1; font-size:14px; opacity:.6; }
38
- .demo:hover .x, .draft:hover .x { display:inline; }
39
- .x:hover { color:var(--bad); opacity:1; }
40
- .drafts { margin-top:12px; border-top:1px solid var(--rule); padding-top:10px; }
41
- .undo { margin-top:10px; font-size:13px; color:var(--muted); display:flex; align-items:center; gap:8px; }
42
- .undo button { border:0; background:transparent; color:var(--ink); text-decoration:underline; cursor:pointer; padding:0; font-size:13px; }
43
- .draft { display:flex; align-items:center; gap:8px; padding:7px 10px; border-radius:9px; cursor:pointer; font-size:13px; color:var(--muted); }
44
- .draft:hover { background:#eef1ea; color:var(--ink); }
45
- .draft .name { flex:1; min-width:0; overflow:hidden; text-overflow:ellipsis; white-space:nowrap; }
46
- .btn { border:1px solid var(--rule); background:var(--card); border-radius:10px; padding:9px 16px; cursor:pointer; line-height:1.2; }
47
- .btn:hover { border-color:var(--rule2); }
48
- .btn.primary { background:var(--accent); color:#eceee9; border-color:var(--accent); }
49
- .btn.primary:hover { background:#000; }
50
- .btn.big { padding:12px 22px; font-size:15px; }
51
- .btn:disabled { opacity:.45; cursor:default; }
52
- .btn.ghost { border-color:transparent; color:var(--muted); }
53
- .btn.wide { width:100%; }
54
- input[type=text], input[type=url], input[type=password], textarea, select { border:1px solid var(--rule); background:#fff; border-radius:10px; padding:10px 12px; width:100%; outline:none; }
55
- input:focus, textarea:focus, select:focus { border-color:var(--ink); }
56
- select { -webkit-appearance:none; appearance:none; background:#fff url("data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' width='12' height='8'%3E%3Cpath d='M1 1l5 5 5-5' stroke='%23767e74' stroke-width='1.6' fill='none'/%3E%3C/svg%3E") no-repeat right 12px center; padding-right:32px; }
57
- .row { display:flex; gap:10px; align-items:center; flex-wrap:wrap; }
58
- .hint { color:var(--muted); font-size:13px; }
59
- .msg { font-size:13px; } .msg.bad { color:var(--bad);} .msg.ok { color:var(--ok);}
60
- /* --- the demo page: video first --- */
61
- .head { display:flex; align-items:baseline; gap:12px; margin:0 0 12px; flex-wrap:wrap; }
62
- .head h2 { font-size:20px; font-weight:600; letter-spacing:-.02em; margin:0; flex:1; min-width:0; }
63
- .head .meta { font:12px ui-monospace, Menlo, monospace; color:var(--muted); }
64
- .stage { position:relative; background:#111; border-radius:12px; overflow:hidden; aspect-ratio:16/10; display:grid; place-items:center; color:#aaa; font-size:14px; }
65
- .stage video { width:100%; height:100%; object-fit:contain; display:block; background:#000; }
66
- .stage .empty { text-align:center; padding:30px; color:#9a9a9a; }
67
- .scrub { position:relative; height:4px; margin:14px 0 0; border-radius:99px; background:#e3e6dd; cursor:pointer; }
68
- .scrub i { position:absolute; top:0; bottom:0; left:0; width:0; background:var(--accent); border-radius:99px; pointer-events:none; }
69
- .scrub b { position:absolute; top:-3px; width:2px; height:10px; background:#c9b27a; border-radius:1px; opacity:.7; }
70
- .scrub b:hover, .scrub b.on { background:var(--amber); opacity:1; }
71
- .scrub b:hover::after, .scrub b.on::after { content:attr(data-label); position:absolute; top:-24px; left:50%; transform:translateX(-50%); font:11px ui-monospace, Menlo, monospace; color:var(--ink); background:var(--card); border:1px solid var(--rule); border-radius:6px; padding:1px 6px; white-space:nowrap; }
72
- .scene-edit { display:none; margin-top:10px; padding:12px 14px; border:1px solid var(--rule); border-radius:12px; background:#fff; }
73
- .scene-edit.show { display:grid; grid-template-columns:minmax(0,1fr) 150px auto; gap:10px; align-items:center; }
74
- .scene-edit .who { font:11px ui-monospace, Menlo, monospace; color:var(--muted); grid-column:1 / -1; }
75
- .ask { margin-top:30px; display:flex; gap:10px; align-items:stretch; }
76
- .ask input { flex:1; padding:13px 16px; font-size:15px; border-radius:12px; }
77
- .ask .btn { border-radius:12px; }
78
- .chips { display:flex; gap:8px; flex-wrap:wrap; margin-top:10px; max-height:0; overflow:hidden; opacity:0; transition:max-height .2s ease, opacity .2s ease; }
79
- .chips.show { max-height:80px; opacity:1; }
80
- .chip { font-size:12px; color:var(--muted); border:1px solid var(--rule); border-radius:99px; padding:3px 10px; cursor:pointer; background:transparent; }
81
- .chip:hover { color:var(--ink); border-color:var(--rule2); }
82
- .actions { display:flex; gap:18px; align-items:center; margin-top:30px; flex-wrap:wrap; }
83
- /* a switch, not a segmented control: one question, yes or no */
84
- .switch { display:inline-flex; align-items:center; gap:10px; font-size:14px; color:var(--muted); cursor:pointer; user-select:none; }
85
- .switch input { position:absolute; opacity:0; width:0; height:0; }
86
- .switch .tr { width:38px; height:22px; border-radius:99px; background:#d6dad0; position:relative; transition:background .15s; }
87
- .switch .tr::after { content:""; position:absolute; top:3px; left:3px; width:16px; height:16px; border-radius:50%; background:#fff; transition:left .15s; box-shadow:0 1px 2px rgba(0,0,0,.2); }
88
- .switch input:checked + .tr { background:var(--accent); }
89
- .switch input:checked + .tr::after { left:19px; }
90
- .switch input:checked ~ span { color:var(--ink); }
91
- .progress { margin-top:22px; display:none; }
92
- .progress.show { display:block; }
93
- .bar { height:6px; border-radius:99px; background:#e0e4da; overflow:hidden; }
94
- .bar i { display:block; height:100%; width:0; background:var(--accent); border-radius:99px; transition:width .6s linear; }
95
- .bar i.done { background:var(--ok);} .bar i.bad { background:var(--bad);}
96
- .stagetext { display:flex; justify-content:space-between; font-size:13px; color:var(--muted); margin-top:8px; }
97
- .steps { margin-top:10px; font:12.5px/1.6 ui-monospace, Menlo, monospace; color:#3d453e; }
98
- .steps div.bad { color:var(--bad);} .steps div.ok { color:var(--ok);}
99
- .receipts { margin-top:26px; display:flex; gap:6px 14px; flex-wrap:wrap; font-size:13px; color:var(--muted); align-items:center; }
100
- .receipts .more { display:none; gap:6px 14px; flex-wrap:wrap; }
101
- .receipts.open .more { display:contents; }
102
- .receipts .toggle { border:0; background:transparent; color:var(--muted); font-size:12px; cursor:pointer; padding:0; text-decoration:underline dotted; }
103
- .saved { margin-top:18px; display:flex; gap:10px; align-items:center; flex-wrap:wrap; }
104
- .saved .where { font:12px ui-monospace, Menlo, monospace; color:var(--muted); flex:1; min-width:0; overflow:hidden; text-overflow:ellipsis; white-space:nowrap; }
105
- .saved a.btn { text-decoration:none; display:inline-block; }
106
- .receipts span::before { content:"✓ "; color:var(--ok); }
107
- .receipts span.bad::before { content:"✗ "; color:var(--bad); }
108
- .receipts span.neutral::before { content:"· "; color:var(--muted); }
109
- details.adv { margin-top:28px; border-top:1px solid var(--rule); padding-top:14px; }
110
- details.adv > summary { cursor:pointer; color:var(--muted); font-size:13px; list-style:none; }
111
- details.adv > summary::before { content:"▸ "; } details.adv[open] > summary::before { content:"▾ "; }
112
- .adv-body { margin-top:14px; display:grid; gap:16px; }
113
- .settings { display:grid; grid-template-columns:repeat(4, minmax(0,1fr)); gap:10px; }
114
- @media (max-width: 860px){ .settings { grid-template-columns:repeat(2,1fr); } }
115
- .settings label { display:block; font-size:12px; color:var(--muted); margin-bottom:5px; }
116
- textarea.yaml { font:12.5px/1.55 ui-monospace, "SF Mono", Menlo, monospace; min-height:380px; resize:vertical; white-space:pre; overflow:auto; }
117
- .files { display:flex; gap:8px 16px; flex-wrap:wrap; font-size:13px; }
118
- .files a { color:var(--ink); text-decoration:none; }
119
- .files a small { color:var(--muted); font:11px ui-monospace, Menlo, monospace; margin-left:4px; }
120
- .log { font:12px/1.55 ui-monospace, Menlo, monospace; color:#3d453e; background:#f2f4ee; border-radius:10px; padding:10px 12px; max-height:220px; overflow:auto; white-space:pre-wrap; }
121
- pre.proof { font:12px/1.5 ui-monospace, Menlo, monospace; white-space:pre-wrap; color:#3d453e; max-height:300px; overflow:auto; margin:0; }
122
- /* --- new demo: one card --- */
123
- .one { max-width:600px; margin:30px auto; }
124
- .one h2 { font-size:26px; font-weight:600; letter-spacing:-.02em; margin:0 0 6px; }
125
- .one .why { color:var(--muted); margin:0 0 22px; }
126
- .one label { display:block; font-size:12px; color:var(--muted); margin:16px 0 6px; }
127
- .one input, .one textarea { font-size:16px; padding:13px 15px; border-radius:12px; }
128
- .one .go { margin-top:22px; display:flex; align-items:center; gap:12px; }
129
- .one .note { font-size:13px; color:var(--muted); margin-top:8px; min-height:1.3em; }
130
- .ideas { margin-top:10px; display:grid; gap:8px; }
131
- .idea { text-align:left; border:1px solid var(--rule); background:#fff; border-radius:12px; padding:11px 14px; cursor:pointer; }
132
- .idea:hover { border-color:var(--ink); }
133
- .idea b { display:block; font-weight:600; margin-bottom:2px; }
134
- .idea span { color:var(--muted); font-size:13px; }
135
- /* --- settings --- */
136
- .provider { display:flex; align-items:center; gap:12px; padding:12px 14px; border:1px solid var(--rule); border-radius:12px; margin-bottom:8px; cursor:pointer; background:#fff; }
137
- .provider.on { border-color:var(--ink); } .provider.off { opacity:.55; cursor:default; }
138
- .provider b { font-weight:600; } .provider .d { color:var(--muted); font-size:13px; }
139
- .field { margin:14px 0; } .field label { display:block; font-size:12px; color:var(--muted); margin-bottom:6px; }
140
- kbd { font:11px ui-monospace, Menlo, monospace; border:1px solid var(--rule2); border-bottom-width:2px; border-radius:5px; padding:1px 5px; color:var(--muted); background:#fff; }
141
- .hidden { display:none !important; }
142
- </style>
143
- </head>
144
- <body>
145
- <div class="top">
146
- <a href="/" style="display:flex;align-items:center;gap:10px;text-decoration:none;color:inherit" title="Back to your demos">
147
- <div class="mark"><i></i><i></i><i></i><i></i></div>
148
- <h1>Retake</h1>
149
- </a>
150
- <span class="tag">The workshop — the manifest, the takes, the knobs.</span>
151
- <span class="sp"></span>
152
- <span class="model" id="modelTag"></span>
153
- <button class="iconbtn" id="gear" title="Settings" aria-label="Settings"><svg width="17" height="17" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="1.8" stroke-linecap="round" stroke-linejoin="round"><circle cx="12" cy="12" r="3"/><path d="M19.4 15a1.7 1.7 0 0 0 .3 1.8l.1.1a2 2 0 1 1-2.8 2.8l-.1-.1a1.7 1.7 0 0 0-1.8-.3 1.7 1.7 0 0 0-1 1.5V21a2 2 0 1 1-4 0v-.1a1.7 1.7 0 0 0-1.1-1.5 1.7 1.7 0 0 0-1.8.3l-.1.1a2 2 0 1 1-2.8-2.8l.1-.1a1.7 1.7 0 0 0 .3-1.8 1.7 1.7 0 0 0-1.5-1H3a2 2 0 1 1 0-4h.1a1.7 1.7 0 0 0 1.5-1.1 1.7 1.7 0 0 0-.3-1.8l-.1-.1a2 2 0 1 1 2.8-2.8l.1.1a1.7 1.7 0 0 0 1.8.3H9a1.7 1.7 0 0 0 1-1.5V3a2 2 0 1 1 4 0v.1a1.7 1.7 0 0 0 1 1.5 1.7 1.7 0 0 0 1.8-.3l.1-.1a2 2 0 1 1 2.8 2.8l-.1.1a1.7 1.7 0 0 0-.3 1.8V9a1.7 1.7 0 0 0 1.5 1H21a2 2 0 1 1 0 4h-.1a1.7 1.7 0 0 0-1.5 1z"/></svg></button>
154
- </div>
155
-
156
- <div class="wrap">
157
- <aside class="side">
158
- <div class="card">
159
- <div class="label">Demos</div>
160
- <div id="demos"></div>
161
- <button class="btn wide" id="newBtn" style="margin-top:12px">+ New demo</button>
162
- <div class="undo hidden" id="undo"><span id="undoText"></span><button id="undoBtn">Undo</button></div>
163
- <div class="drafts hidden" id="draftsWrap"><div class="label" style="margin-bottom:6px">Unfinished</div><div id="drafts"></div></div>
164
- </div>
165
- </aside>
166
-
167
- <main>
168
- <!-- ============ DEMO: video first ============ -->
169
- <section class="card" id="viewDemo">
170
- <div class="head">
171
- <h2 id="title"></h2>
172
- <span class="meta" id="meta"></span>
173
- <span class="msg" id="msg"></span>
174
- </div>
175
-
176
- <div class="stage" id="stage"><div class="empty">No take yet. Press Record.</div></div>
177
- <div class="scrub" id="scrub" title="Click a marker to edit that scene"><i id="scrubFill"></i></div>
178
- <div class="scene-edit" id="sceneEdit">
179
- <span class="who" id="sceneWho"></span>
180
- <input type="text" id="sceneCap" placeholder="Caption (leave blank for none)" />
181
- <select id="sceneCam"><option value="auto">Camera: auto</option><option value="static">Camera: none</option><option value="1.2">Zoom 1.2×</option><option value="1.3">Zoom 1.3×</option><option value="1.5">Zoom 1.5×</option></select>
182
- <button class="btn" id="sceneApply">Apply</button>
183
- </div>
184
-
185
- <div class="ask">
186
- <input type="text" id="askBox" placeholder="Tell Retake what to change…" />
187
- <button class="btn primary" id="askBtn">Change it</button>
188
- </div>
189
- <div class="chips" id="chips">
190
- <button class="chip" data-q="Hold the last scene two seconds longer">hold the ending</button>
191
- <button class="chip" data-q="Make every caption shorter and plainer">plainer captions</button>
192
- <button class="chip" data-q="Cut the first 2 seconds">trim the start</button>
193
- <button class="chip" data-q="Zoom in on the result scene">zoom on the result</button>
194
- <button class="chip" data-q="Make it square for a social post">make it square</button>
195
- <button class="chip" data-q="No camera moves at all">no camera moves</button>
196
- </div>
197
-
198
- <div class="actions">
199
- <button class="btn primary big" id="runBtn">Record</button>
200
- <label class="switch" title="Off: a fast preview to check the story. On: the full-quality render."><input type="checkbox" id="qFinalSw" /><span class="tr"></span><span>Final quality</span></label>
201
- <span class="hint" id="runHint"></span>
202
- </div>
203
- <div class="progress" id="progress">
204
- <div class="bar"><i id="barFill"></i></div>
205
- <div class="stagetext"><span id="stageText">Starting…</span><span id="etaText"></span></div>
206
- <div class="steps" id="steps"></div>
207
- <div class="row hidden" id="retryRow" style="margin-top:12px"><button class="btn" id="retryBtn">Try again</button><span class="hint" id="retryHint"></span></div>
208
- </div>
209
-
210
- <div class="receipts" id="receipts"></div>
211
- <div class="saved hidden" id="saved">
212
- <a class="btn" id="dlBtn" download>Download MP4</a>
213
- <button class="btn ghost" id="openBtn">Show in Finder</button>
214
- <span class="where" id="where" title=""></span>
215
- </div>
216
-
217
- <details class="adv" id="adv">
218
- <summary>Advanced</summary>
219
- <div class="adv-body">
220
- <div class="settings">
221
- <div><label>Format</label><select id="sPreset"></select></div>
222
- <div><label>Captions</label><select id="sLayout"><option value="default">Below the frame</option><option value="card">Framed card</option><option value="overlay-bottom">Over the video</option><option value="none">None</option></select></div>
223
- <div><label>Camera</label><select id="sCamera"><option value="auto">Push in on results</option><option value="static">Static</option></select></div>
224
- <div><label>Cursor</label><select id="sCursor"><option value="default">Pointer</option><option value="touch">Tap dot</option><option value="none">None</option></select></div>
225
- </div>
226
- <div class="row"><button class="btn ghost" id="forceBtn" title="Record again from scratch, even if nothing changed">Record again from scratch</button></div>
227
- <div class="files" id="files"></div>
228
- <div><div class="label">Manifest</div><textarea class="yaml" id="yaml" spellcheck="false"></textarea><div class="row" style="margin-top:8px"><button class="btn" id="saveBtn">Save</button><span class="hint"><kbd>⌘S</kbd> save · <kbd>⌘⏎</kbd> record</span></div></div>
229
- <details><summary class="hint" style="cursor:pointer">Log</summary><div class="log" id="log"></div></details>
230
- <details><summary class="hint" style="cursor:pointer">Proof log</summary><pre class="proof" id="proof"></pre></details>
231
- </div>
232
- </details>
233
- </section>
234
-
235
- <!-- ============ NEW DEMO: one card ============ -->
236
- <section class="card hidden" id="viewNew">
237
- <div class="one">
238
- <h2>What should the demo show?</h2>
239
- <p class="why">Point at your app, say it in a sentence, and Retake does the rest: reads the page, writes the steps, checks them, and records a first take.</p>
240
- <label>Your app</label>
241
- <input type="url" id="fUrl" placeholder="http://localhost:3000" />
242
- <label>What to show</label>
243
- <textarea id="fDesc" rows="3" placeholder="A messy note being captured, landing as an action and a thread, then opening the thread."></textarea>
244
- <div class="row" style="margin-top:8px"><button class="chip" id="fIdeas">Suggest ideas</button><span class="hint" id="fIdeasNote"></span></div>
245
- <div class="ideas" id="ideaList"></div>
246
- <label>Code folder <span style="font-weight:400">(optional — better selectors, routes, sign-in fields)</span></label>
247
- <input type="text" id="fProject" placeholder="~/Documents/my-app" />
248
- <div class="note" id="fProjectNote"></div>
249
- <div class="go">
250
- <button class="btn primary big" id="fGo">Take it from here</button>
251
- <button class="btn ghost" id="fBlank" title="Just create the manifest; write the steps yourself">Start blank</button>
252
- <span style="flex:1"></span>
253
- <button class="btn ghost" id="fCancel">Cancel</button>
254
- </div>
255
- <div class="note" id="fBusy"></div>
256
- <div class="row hidden" id="startRow" style="margin-top:10px"><button class="btn primary" id="startBtn">Start it</button><span class="hint" id="startHint"></span></div>
257
- <div class="progress" id="fProgress">
258
- <div class="bar"><i id="fBar"></i></div>
259
- <div class="stagetext"><span id="fStage">Starting…</span><span id="fEta"></span></div>
260
- <div class="steps" id="fSteps"></div>
261
- <div class="row hidden" id="fRetryRow" style="margin-top:12px"><button class="btn primary" id="fRetry">Try again</button><span class="hint">Your answers are still here. Start the app, then retry.</span></div>
262
- </div>
263
- </div>
264
- </section>
265
-
266
- <!-- ============ SETTINGS ============ -->
267
- <section class="card hidden" id="viewSettings">
268
- <div class="one" style="margin-top:10px">
269
- <h2>Settings</h2>
270
- <p class="why">Which model drafts and fixes your demos. Recording and rendering never use a model.</p>
271
- <div id="providers"></div>
272
- <div id="localFields" class="hidden">
273
- <div class="field"><label>Local endpoint (OpenAI-compatible — Ollama, LM Studio)</label><input type="url" id="setLocalUrl" placeholder="http://localhost:11434/v1" /></div>
274
- <div class="field"><label>Local model</label><input type="text" id="setLocalModel" placeholder="llama3.1" /></div>
275
- </div>
276
- <div class="field" style="margin-top:22px"><label>Save videos to</label><input type="text" id="setOut" placeholder="outputs (inside Retake)" /><div class="hint" style="margin-top:6px" id="setOutHint"></div></div>
277
- <details style="margin-top:12px"><summary class="hint" style="cursor:pointer">API keys (optional)</summary>
278
- <div class="field"><label>Groq API key</label><input type="password" id="setGroq" placeholder="gsk_…" /></div>
279
- <div class="field"><label>Mistral API key</label><input type="password" id="setMistral" placeholder="…" /></div>
280
- </details>
281
- <p class="hint" id="toolsInfo" style="margin-top:18px"></p>
282
- <div class="go"><span style="flex:1"></span><button class="btn primary big" id="setSave">Save</button></div>
283
- </div>
284
- </section>
285
- </main>
286
- </div>
287
-
288
- <script>
289
- const $ = (id) => document.getElementById(id);
290
- const esc = (s) => String(s ?? '').replace(/[&<>"]/g, (c) => ({ '&': '&amp;', '<': '&lt;', '>': '&gt;', '"': '&quot;' }[c]));
291
- let current = null, dirty = false, es = null, demoList = [], settings = null, tick = null, pendingRender = false, needsRecord = true, lastTake = null, sceneTimes = {}, quality = localStorage.getItem('retake.quality') || 'final';
292
- async function api(url, opts) {
293
- const r = await fetch(url, opts); const j = await r.json().catch(() => ({}));
294
- if (!r.ok) { const e = new Error(j.error || r.statusText); Object.assign(e, j); throw e; }
295
- return j;
296
- }
297
- function setMsg(t, cls) { const m = $('msg'); m.textContent = t; m.className = 'msg ' + (cls || ''); }
298
- function show(view) { for (const v of ['viewDemo', 'viewNew', 'viewSettings']) $(v).classList.toggle('hidden', v !== view); }
299
- function setQuality(q) { quality = q; localStorage.setItem('retake.quality', q); $('qFinalSw').checked = q === 'final'; paintAction(); }
300
- $('qFinalSw').onchange = (e) => setQuality(e.target.checked ? 'final' : 'fast');
301
- // the example chips only appear while the box has focus
302
- $('askBox').addEventListener('focus', () => $('chips').classList.add('show'));
303
- $('askBox').addEventListener('blur', () => setTimeout(() => { if (!$('chips').matches(':hover')) $('chips').classList.remove('show'); }, 150));
304
-
305
- // ---------- sidebar ----------
306
- async function loadDemos() {
307
- demoList = await api('/api/demos');
308
- const el = $('demos'); el.innerHTML = '';
309
- if (!demoList.length) el.innerHTML = '<div class="hint" style="padding:6px 0">No demos yet.</div>';
310
- for (const d of demoList) {
311
- const div = document.createElement('div');
312
- div.className = 'demo' + (d.name === current ? ' on' : '');
313
- const dot = d.lastTake ? (d.lastTake.ok && !d.lastTake.partial ? 'ok' : 'bad') : '';
314
- div.innerHTML = `<span class="dot ${dot}"></span><span class="name">${esc(d.name)}</span><button class="x" title="Delete" aria-label="Delete ${esc(d.name)}">×</button>`;
315
- div.onclick = () => select(d.name);
316
- div.querySelector('.x').onclick = async (e) => {
317
- e.stopPropagation();
318
- if (!confirm(`Delete "${d.name}"? Its videos go too. You get a few seconds to undo.`)) return;
319
- try { await api('/api/demos/' + d.name, { method: 'DELETE' }); } catch (err) { setMsg(err.message, 'bad'); return; }
320
- if (current === d.name) current = null;
321
- offerUndo(d.name);
322
- const list = await loadDemos();
323
- if (!current) { if (list.length) select(list[0].name); else openNew(); }
324
- };
325
- el.appendChild(div);
326
- }
327
- await loadDrafts();
328
- return demoList;
329
- }
330
- // What the folder told us about starting the app, if anything — and where a
331
- // "nothing is running" failure last happened, so typing a folder afterwards
332
- // brings the Start-it offer straight up.
333
- let startInfo = null, lastDownWhere = null;
334
-
335
- /** Offer to run the project's own dev script — never do it unasked. */
336
- function offerStart(where) {
337
- if (!startInfo) { $('startRow').classList.add('hidden'); return false; }
338
- $('startRow').classList.remove('hidden');
339
- $('startHint').textContent = `Found \`${startInfo.command}\` in ${startInfo.dir.replace(/^\/Users\/[^/]+/, '~')}`;
340
- $('startBtn').onclick = async () => {
341
- $('startBtn').disabled = true; $('startHint').textContent = 'Starting… (watching for the port it comes up on)';
342
- try {
343
- const r = await api('/api/start', { method: 'POST', body: JSON.stringify({ dir: startInfo.dir, command: startInfo.command, url: $('fUrl').value.trim() || undefined }) });
344
- $('startRow').classList.add('hidden'); lastDownWhere = null;
345
- if (r.url && r.url !== $('fUrl').value.trim()) { $('fUrl').value = r.url; saveDraftSoon(); }
346
- $('fBusy').textContent = `Running at ${r.url}. Picking up where we left off…`;
347
- (where === 'create' ? $('fGo') : $('fIdeas')).click();
348
- } catch (e) {
349
- $('startHint').textContent = e.message;
350
- if (e.log) $('fBusy').textContent = e.log;
351
- } finally { $('startBtn').disabled = false; }
352
- };
353
- return true;
354
- }
355
-
356
- let undoTimer = null;
357
- function offerUndo(name) {
358
- clearTimeout(undoTimer);
359
- $('undoText').textContent = `Deleted ${name}.`; $('undo').classList.remove('hidden');
360
- $('undoBtn').onclick = async () => { try { await api('/api/demos/' + name + '/restore', { method: 'POST' }); } catch (e) { setMsg(e.message, 'bad'); } $('undo').classList.add('hidden'); await loadDemos(); select(name); };
361
- undoTimer = setTimeout(() => $('undo').classList.add('hidden'), 12000);
362
- }
363
- let draftId = null, draftTimer = null;
364
- async function loadDrafts() {
365
- let ds = []; try { ds = await api('/api/drafts'); } catch {}
366
- $('draftsWrap').classList.toggle('hidden', !ds.length);
367
- const el = $('drafts'); el.innerHTML = '';
368
- for (const d of ds) {
369
- const row = document.createElement('div'); row.className = 'draft';
370
- row.innerHTML = `<span class="name">${esc(d.name)}</span><button class="x" title="Discard" aria-label="Discard ${esc(d.name)}">×</button>`;
371
- row.onclick = () => { draftId = d.id; $('fUrl').value = d.url || ''; $('fProject').value = d.project || ''; $('fDesc').value = d.describe || ''; $('fBusy').textContent = ''; $('fProjectNote').textContent = ''; $('ideaList').innerHTML = ''; show('viewNew'); };
372
- row.querySelector('.x').onclick = async (e) => { e.stopPropagation(); await api('/api/drafts/' + d.id, { method: 'DELETE' }); if (draftId === d.id) draftId = null; loadDrafts(); };
373
- el.appendChild(row);
374
- }
375
- }
376
- function saveDraftSoon() { clearTimeout(draftTimer); draftTimer = setTimeout(async () => {
377
- if (!draftId) return;
378
- const body = { url: $('fUrl').value.trim(), project: $('fProject').value.trim(), describe: $('fDesc').value, step: 1 };
379
- if (!body.url && !body.describe && !body.project) return;
380
- try { await api('/api/drafts/' + draftId, { method: 'PUT', body: JSON.stringify(body) }); loadDrafts(); } catch {}
381
- }, 500); }
382
-
383
- // ---------- demo view ----------
384
- // Shape, not a resolution: the video is the page plus the caption band, so
385
- // "1080p" was never true of what post-landscape actually writes (1920×1230).
386
- const PRESET_LABEL = { 'post-landscape': 'landscape', 'post-square': 'square', 'post-vertical': 'vertical', 'docs-gif': 'docs', 'proof-fast': 'small', 'master': 'master', 'preview-fast': 'preview', 'draft': 'draft' };
387
- async function select(name) {
388
- if (dirty && !confirm('Discard unsaved manifest edits?')) return;
389
- current = name; dirty = false; setMsg('');
390
- show('viewDemo');
391
- const { yaml } = await api('/api/demos/' + name);
392
- $('yaml').value = yaml;
393
- await loadDemos();
394
- const d = demoList.find((x) => x.name === name);
395
- $('title').textContent = d?.title || name;
396
- fillSettings(d);
397
- pendingRender = false; $('progress').classList.remove('show'); $('steps').innerHTML = ''; $('log').innerHTML = ''; $('sceneEdit').classList.remove('show');
398
- await loadTake();
399
- }
400
- function fillSettings(d) {
401
- if (!d) return;
402
- const st = d.settings || {};
403
- $('sPreset').value = st.preset || 'post-landscape';
404
- $('sLayout').value = (st.layout === 'band' || !st.layout) ? 'default' : st.layout;
405
- $('sCamera').value = st.camera || 'auto';
406
- $('sCursor').value = st.cursor || 'default';
407
- paintAction();
408
- }
409
- function paintAction() {
410
- const d = demoList.find((x) => x.name === current);
411
- const has = !!(d && d.lastTake);
412
- needsRecord = !d || d.needsRecord;
413
- $('runBtn').textContent = !has ? 'Record' : needsRecord ? 'Re-record' : pendingRender ? 'Re-render' : 'Render again';
414
- $('forceBtn').classList.toggle('hidden', !has || needsRecord);
415
- $('runHint').textContent = !has ? (quality === 'final' ? 'About two minutes.' : 'About a minute and a half.') : needsRecord ? 'The steps changed, so it records again.' : pendingRender ? 'Just the render — seconds.' : '';
416
- const meta = [];
417
- if (d?.url) meta.push(d.url.replace(/^https?:\/\//, ''));
418
- if (d?.settings?.preset) meta.push(PRESET_LABEL[d.settings.preset] || d.settings.preset);
419
- if (d?.lastTake) meta.push(d.lastTake.duration + 's');
420
- $('meta').textContent = meta.join(' · ');
421
- }
422
- async function patchSetting(key, value) {
423
- const body = {};
424
- if (key === 'cursor') body.cursor = value === 'none' ? false : value === 'touch' ? { style: 'touch' } : null;
425
- else if (key === 'layout') body.layout = value === 'default' ? null : value;
426
- else body[key] = value;
427
- try { const r = await api('/api/demos/' + current + '/settings', { method: 'PATCH', body: JSON.stringify(body) }); $('yaml').value = r.yaml; dirty = false; pendingRender = true; await loadDemos(); fillSettings(demoList.find((x) => x.name === current)); flash('saved — re-render to see it'); } catch (e) { setMsg(e.message.split('\n')[0], 'bad'); }
428
- }
429
- $('sPreset').onchange = (e) => patchSetting('preset', e.target.value);
430
- $('sLayout').onchange = (e) => patchSetting('layout', e.target.value);
431
- $('sCamera').onchange = (e) => patchSetting('camera', e.target.value);
432
- $('sCursor').onchange = (e) => patchSetting('cursor', e.target.value);
433
- function flash(t, cls) { setMsg(t, cls || 'ok'); setTimeout(() => setMsg(''), 2600); }
434
- async function save() {
435
- if (!current) return false;
436
- if (!dirty) return true;
437
- try { const r = await api('/api/demos/' + current, { method: 'PUT', body: JSON.stringify({ yaml: $('yaml').value }) }); dirty = false; flash(`saved · ${r.steps} steps`); await loadDemos(); fillSettings(demoList.find((x) => x.name === current)); return true; }
438
- catch (e) { setMsg(e.message.split('\n')[0], 'bad'); return false; }
439
- }
440
- $('yaml').addEventListener('input', () => { dirty = true; setMsg('unsaved'); });
441
- $('yaml').addEventListener('keydown', (e) => { if ((e.metaKey || e.ctrlKey) && e.key === 'Enter') run('main'); if ((e.metaKey || e.ctrlKey) && e.key === 's') { e.preventDefault(); save(); } });
442
- $('saveBtn').onclick = save;
443
-
444
- // ---------- the take ----------
445
- async function loadTake(fresh) {
446
- const stage = $('stage'), scrub = $('scrub');
447
- scrub.querySelectorAll('b').forEach((b) => b.remove());
448
- let data;
449
- try { data = await api('/api/take/' + current); } catch { stage.innerHTML = '<div class="empty">No take yet. Press Record.</div>'; $('receipts').innerHTML = ''; $('files').innerHTML = ''; $('saved').classList.add('hidden'); lastTake = null; return; }
450
- const { take, files, proof, facts, stamp, dir } = data;
451
- lastTake = take;
452
- const dur = take.duration - take.trimBefore;
453
- const q = '?v=' + stamp;
454
- const has = (f) => files.includes(f);
455
- stage.innerHTML = has('demo.mp4') ? `<video controls playsinline src="/out/${current}/demo.mp4${q}"${has('thumbnail.png') ? ` poster="/out/${current}/thumbnail.png${q}"` : ''}></video>` : '<div class="empty">Recorded, but no video was rendered.</div>';
456
- // scene markers on the scrubber
457
- sceneTimes = {};
458
- for (const s of take.timeline.filter((e) => e.action === 'scene')) {
459
- const t = Math.max(0, s.start - take.trimBefore); sceneTimes[s.label] = t;
460
- const b = document.createElement('b'); b.style.left = (t / dur * 100) + '%'; b.dataset.label = s.label; b.title = s.label;
461
- b.onclick = (e) => { e.stopPropagation(); openScene(s.label, t); };
462
- scrub.appendChild(b);
463
- }
464
- const v = stage.querySelector('video');
465
- if (v) {
466
- v.ontimeupdate = () => { $('scrubFill').style.width = (v.currentTime / (v.duration || dur) * 100) + '%'; };
467
- scrub.onclick = (e) => { const r = scrub.getBoundingClientRect(); v.currentTime = (e.clientX - r.left) / r.width * (v.duration || dur); };
468
- if (fresh) v.play().catch(() => {});
469
- }
470
- // receipts: plain English, from real state
471
- // One short line; the rest behind "details".
472
- const head = `<span class="${take.ok ? '' : 'bad'}">${take.ok ? `All ${take.timeline.length} steps passed` : `${take.timeline.filter((t) => !t.ok).length} of ${take.timeline.length} steps failed`}</span><span class="neutral">${dur.toFixed(0)}s${facts && facts.encoder === 'videotoolbox' ? ' preview' : ''}</span>`;
473
- const more = [];
474
- if (take.timeline.some((t) => t.action === 'waitFor')) more.push('<span>Waited for results, not timers</span>');
475
- if (facts) more.push(`<span>Rendered ${facts.width}×${facts.height} @ ${facts.fps}fps</span>`);
476
- if (take.partial) more.push(`<span class="bad">Partial — ${esc(take.partial)}</span>`);
477
- if (take.stubbed && take.stubbed.length) more.push(`<span class="neutral">${take.stubbed.length} endpoint${take.stubbed.length > 1 ? 's' : ''} answered with canned data</span>`);
478
- if (take.timeline.some((t) => /••••/.test(t.summary))) more.push('<span>Secrets kept out of logs</span>');
479
- more.push(`<span class="neutral">${new Date(take.finishedAt).toLocaleTimeString()}</span>`);
480
- $('receipts').innerHTML = head + `<button class="toggle" id="rcToggle">details</button><span class="more">${more.join('')}</span>`;
481
- $('rcToggle').onclick = () => { const r = $('receipts'); r.classList.toggle('open'); $('rcToggle').textContent = r.classList.contains('open') ? 'less' : 'details'; };
482
- $('files').innerHTML = ['demo.mp4', 'master.mp4', 'demo.gif', 'thumbnail.png', 'proof-log.md'].filter(has).map((f) => `<a href="/out/${current}/${f}${q}" target="_blank">${f}${facts && facts.sizes && facts.sizes[f] ? `<small>${(facts.sizes[f] / 1e6).toFixed(1)} MB</small>` : ''}</a>`).join('') + (has('demo.gif') ? '' : `<a href="#" id="mkGif">Make a GIF</a>`);
483
- const gb = $('mkGif'); if (gb) gb.onclick = async (e) => { e.preventDefault(); gb.textContent = 'Making…'; try { await api('/api/gif/' + current, { method: 'POST' }); await loadTake(); } catch (err) { gb.textContent = err.message; } };
484
- $('proof').textContent = proof || '';
485
- const sv = $('saved');
486
- if (has('demo.mp4')) {
487
- sv.classList.remove('hidden');
488
- $('dlBtn').href = `/out/${current}/demo.mp4${q}`; $('dlBtn').setAttribute('download', `${current}.mp4`);
489
- $('where').textContent = dir ? dir.replace(/^\/Users\/[^/]+/, '~') : ''; $('where').title = dir || '';
490
- $('openBtn').onclick = async () => { try { await api('/api/open/' + current, { method: 'POST' }); } catch (e) { setMsg(e.message, 'bad'); } };
491
- } else sv.classList.add('hidden');
492
- paintAction();
493
- }
494
- // one scene at a time
495
- let sceneOpen = null;
496
- function openScene(label, t) {
497
- const d = demoList.find((x) => x.name === current);
498
- const sc = (d?.settings?.scenes || []).find((s) => s.label === label);
499
- sceneOpen = label;
500
- $('sceneWho').textContent = `Scene “${label}” · ${t.toFixed(1)}s`;
501
- $('sceneCap').value = sc?.caption || '';
502
- $('sceneCam').value = typeof sc?.camera === 'number' ? String(sc.camera) : (sc?.camera || 'auto');
503
- $('sceneEdit').classList.add('show');
504
- $('scrub').querySelectorAll('b').forEach((b) => b.classList.toggle('on', b.dataset.label === label));
505
- const v = $('stage').querySelector('video'); if (v) { v.currentTime = t; v.pause(); }
506
- }
507
- $('sceneApply').onclick = async () => {
508
- if (!sceneOpen) return;
509
- const cam = $('sceneCam').value;
510
- try {
511
- await api('/api/demos/' + current + '/scene', { method: 'PATCH', body: JSON.stringify({ label: sceneOpen, caption: $('sceneCap').value, camera: cam === 'auto' || cam === 'static' ? cam : { zoom: Number(cam) } }) });
512
- $('sceneEdit').classList.remove('show'); pendingRender = true; await loadDemos(); paintAction(); run('render');
513
- } catch (e) { setMsg(e.message, 'bad'); }
514
- };
515
-
516
- // ---------- the correction box ----------
517
- async function askChange(text) {
518
- const t = (text || $('askBox').value).trim(); if (!t || !current) return;
519
- $('askBtn').disabled = true; $('askBtn').textContent = 'Thinking…';
520
- try {
521
- const r = await api('/api/fix/' + current, { method: 'POST', body: JSON.stringify({ instruction: t }) });
522
- if (!r.applied.length) { flash(r.note || 'Nothing to change for that.', 'bad'); return; }
523
- $('yaml').value = r.yaml; $('askBox').value = '';
524
- await loadDemos(); fillSettings(demoList.find((x) => x.name === current));
525
- const what = r.applied.join(' · ');
526
- setMsg(what + (r.rerecord ? ' — recording again' : ' — re-rendering'), 'ok');
527
- run(r.rerecord ? 'main' : 'render');
528
- } catch (e) { setMsg(e.message, 'bad'); }
529
- finally { $('askBtn').disabled = false; $('askBtn').textContent = 'Change it'; }
530
- }
531
- $('askBtn').onclick = () => askChange();
532
- $('askBox').addEventListener('keydown', (e) => { if (e.key === 'Enter') askChange(); });
533
- for (const c of $('chips').querySelectorAll('.chip')) c.onclick = () => askChange(c.dataset.q);
534
-
535
- // ---------- run + progress ----------
536
- const STAGES = { starting: 'Starting…', scouting: 'Reading the page', drafting: 'Writing the steps', checking: 'Checking every step on the page', capture: 'Recording in the browser', 'capture-skip': 'Reusing last recording', render: 'Rendering', done: 'Done' };
537
- let pg = { wrap: 'progress', bar: 'barFill', stage: 'stageText', eta: 'etaText', steps: 'steps' };
538
- function setProgress(stage, at, est, startedAt) {
539
- $(pg.wrap).classList.add('show');
540
- const now = Date.now(), elapsed = (now - startedAt) / 1000;
541
- let frac;
542
- if (stage === 'done') frac = 1;
543
- else if (stage === 'render') frac = Math.min(0.98, (est.capture + Math.min((now - at) / 1000, est.render)) / (est.capture + est.render));
544
- else if (stage === 'capture-skip') frac = 0.05;
545
- else if (stage === 'scouting' || stage === 'drafting' || stage === 'checking') frac = Math.min(0.15, elapsed / 200);
546
- else frac = Math.min(elapsed / (est.capture + est.render), 0.6);
547
- $(pg.bar).style.width = Math.round(frac * 100) + '%'; $(pg.bar).className = stage === 'done' ? 'done' : '';
548
- $(pg.stage).textContent = STAGES[stage] || stage;
549
- const remaining = Math.max(0, Math.round(stage === 'render' ? est.render - (now - at) / 1000 : est.capture + est.render - elapsed));
550
- $(pg.eta).textContent = stage === 'done' ? `${Math.round(elapsed)}s` : (stage === 'scouting' || stage === 'drafting' || stage === 'checking') ? `${Math.round(elapsed)}s` : `about ${remaining}s left`;
551
- }
552
- function attachStream(name, startedAt, est, onDone) {
553
- if (es) es.close();
554
- let stage = 'starting', at = startedAt;
555
- setProgress(stage, at, est, startedAt);
556
- clearInterval(tick); tick = setInterval(() => setProgress(stage, at, est, startedAt), 1000);
557
- const steps = $(pg.steps); steps.innerHTML = ''; const log = $('log'); log.innerHTML = '';
558
- $('retryRow').classList.add('hidden'); $('fRetryRow').classList.add('hidden');
559
- es = new EventSource('/api/run/' + name + '/stream');
560
- es.addEventListener('stage', (ev) => { const d = JSON.parse(ev.data); stage = d.stage; at = d.at; if (d.estimate) est = d.estimate; if (d.startedAt) startedAt = d.startedAt; setProgress(stage, at, est, startedAt); });
561
- es.onmessage = (ev) => {
562
- const line = JSON.parse(ev.data);
563
- const bad = /✗|failed|Error/i.test(line), ok = /^✓|all steps ok|check: pass|Checked:|Found the page|Drafted|Fixed/.test(line);
564
- log.innerHTML += `<div>${esc(line)}</div>`;
565
- // the human-readable checkpoints only
566
- if (line === '__DOWN__') return;
567
- if (/^(Found the page|Read |Drafting|Drafted|Fixed|Checked|Recording|✗|⚠|No repair|\d+ step)/.test(line)) { steps.innerHTML += `<div class="${bad ? 'bad' : ok ? 'ok' : ''}">${esc(line)}</div>`; }
568
- };
569
- es.addEventListener('done', async (ev) => {
570
- es.close(); es = null; clearInterval(tick);
571
- const { code } = JSON.parse(ev.data);
572
- setProgress('done', Date.now(), est, startedAt);
573
- if (code !== 0) { $(pg.bar).className = 'bad'; $(pg.stage).textContent = code === 3 ? 'Stopped before recording — see the steps above' : 'Finished with problems'; }
574
- onDone(code);
575
- });
576
- }
577
- let lastRun = null;
578
- async function run(kind) {
579
- if (!current) return;
580
- if (!(await save())) return;
581
- lastRun = kind;
582
- pg = { wrap: 'progress', bar: 'barFill', stage: 'stageText', eta: 'etaText', steps: 'steps' };
583
- const renderOnly = kind === 'render' || (kind === 'main' && !needsRecord && !$('forceBtn').dataset.force);
584
- const preview = kind === 'main' && quality === 'fast' && !renderOnly;
585
- $('runBtn').disabled = true; $('forceBtn').disabled = true;
586
- const qs = preview ? '?mode=preview' : renderOnly ? '?mode=render' : `?reuse=${$('forceBtn').dataset.force ? 0 : 1}`;
587
- delete $('forceBtn').dataset.force;
588
- let info;
589
- try { info = await api('/api/run/' + current + qs, { method: 'POST' }); }
590
- catch (e) { setMsg(e.message, 'bad'); $('runBtn').disabled = false; $('forceBtn').disabled = false; return; }
591
- attachStream(current, info.startedAt || Date.now(), info.estimate || { capture: 90, render: 45 }, async (code) => {
592
- $('runBtn').disabled = false; $('forceBtn').disabled = false;
593
- if (code === 0) pendingRender = false;
594
- else { $('retryRow').classList.remove('hidden'); $('retryHint').textContent = /Nothing is running/.test($('log').textContent) ? 'Start the app, then try again.' : 'See Advanced › Log for what went wrong.'; }
595
- await loadDemos(); await loadTake(true);
596
- });
597
- }
598
- $('retryBtn').onclick = () => run(lastRun || 'main');
599
- $('runBtn').onclick = () => run('main');
600
- $('forceBtn').onclick = () => { $('forceBtn').dataset.force = '1'; run('main'); };
601
-
602
- // ---------- new demo: one card ----------
603
- function openNew() { draftId = 'd' + Date.now().toString(36); startInfo = null; lastDownWhere = null; $('startRow').classList.add('hidden'); for (const id of ['fUrl', 'fDesc', 'fProject']) $(id).value = ''; $('fBusy').textContent = ''; $('fProjectNote').textContent = ''; $('ideaList').innerHTML = ''; show('viewNew'); setTimeout(() => $('fUrl').focus(), 50); }
604
- $('newBtn').onclick = openNew;
605
- $('fCancel').onclick = () => { if (current) show('viewDemo'); };
606
- for (const id of ['fUrl', 'fDesc', 'fProject']) $(id).addEventListener('input', saveDraftSoon);
607
- let projTimer = null;
608
- async function checkProject() {
609
- const dir = $('fProject').value.trim();
610
- if (!dir) { $('fProjectNote').textContent = ''; startInfo = null; return; }
611
- $('fProjectNote').textContent = 'Reading…';
612
- try {
613
- const r = await api('/api/project', { method: 'POST', body: JSON.stringify({ dir }) });
614
- startInfo = r.start || null;
615
- if (startInfo && lastDownWhere) offerStart(lastDownWhere);
616
- const bits = [`${r.files} files`, r.stack.length ? r.stack.join(', ') : null, r.routes.length ? `${r.routes.length} routes` : null, r.selectors ? `${r.selectors} stable selectors` : null, r.auth ? 'a sign-in' : null, r.flaky ? `${r.flaky} things to watch` : null, startInfo ? `starts with ${startInfo.command}` : null].filter(Boolean);
617
- $('fProjectNote').innerHTML = `<span style="color:var(--ok)">Found ${esc(r.name)}</span> — ${esc(bits.join(' · '))}`;
618
- } catch (e) { $('fProjectNote').textContent = e.message; startInfo = null; }
619
- }
620
- $('fProject').addEventListener('change', checkProject);
621
- $('fProject').addEventListener('input', () => { clearTimeout(projTimer); projTimer = setTimeout(checkProject, 700); });
622
- $('fIdeas').onclick = async () => {
623
- const u = $('fUrl').value.trim(); if (!/^https?:\/\//.test(u)) { $('fUrl').focus(); return; }
624
- $('fIdeas').disabled = true; $('fIdeasNote').textContent = 'Looking at the app…';
625
- try { const r = await api('/api/ideas', { method: 'POST', body: JSON.stringify({ url: u, project: $('fProject').value.trim() || undefined }) }); $('ideaList').innerHTML = r.ideas.map((i, n) => `<button class="idea" data-i="${n}"><b>${esc(i.title)}</b><span>${esc(i.story || '')}${i.length ? ' · ' + esc(i.length) : ''}</span></button>`).join(''); $('fIdeasNote').textContent = `Saved to ${r.file}`; for (const b of $('ideaList').querySelectorAll('button[data-i]')) b.onclick = () => { const i = r.ideas[Number(b.dataset.i)]; $('fDesc').value = i.story || i.title; $('ideaList').innerHTML = ''; $('fDesc').focus(); }; }
626
- catch (e) {
627
- $('fIdeasNote').textContent = e.message;
628
- if (e.down) { lastDownWhere = 'ideas'; if (!offerStart('ideas')) { if ($('fProject').value.trim()) checkProject(); else { $('fIdeasNote').textContent += ' Point me at the code folder below and I can start it for you.'; $('fProject').focus(); } } }
629
- } finally { $('fIdeas').disabled = false; }
630
- };
631
- $('fGo').onclick = async () => {
632
- const url = $('fUrl').value.trim(), describe = $('fDesc').value.trim();
633
- if (!/^https?:\/\//.test(url)) { $('fUrl').style.borderColor = 'var(--bad)'; $('fUrl').focus(); return; } $('fUrl').style.borderColor = '';
634
- if (!describe) { $('fDesc').focus(); return; }
635
- if (!(settings && settings.active)) { $('fBusy').textContent = 'No model set up yet — choose one in Settings (gear, top right).'; return; }
636
- $('fGo').disabled = true; $('fBusy').textContent = ''; $('fRetryRow').classList.add('hidden');
637
- try {
638
- const r = await api('/api/create', { method: 'POST', body: JSON.stringify({ url, describe, project: $('fProject').value.trim() || undefined }) });
639
- // Stay on this card while Retake works, so a failure (app not running,
640
- // model missing) leaves the answers in place with a Try again button.
641
- pg = { wrap: 'fProgress', bar: 'fBar', stage: 'fStage', eta: 'fEta', steps: 'fSteps' };
642
- attachStream(r.name, r.startedAt, r.estimate, async (code) => {
643
- $('fGo').disabled = false;
644
- const exists = await api('/api/demos/' + r.name).then(() => true).catch(() => false);
645
- if (!exists) {
646
- // Nothing was drafted. If the app was simply down and we know how to
647
- // start it, offer that instead of a bare Try again.
648
- const wasDown = $('fSteps').textContent.includes('Nothing is running');
649
- if (wasDown) {
650
- lastDownWhere = 'create';
651
- if (!offerStart('create')) {
652
- if ($('fProject').value.trim()) { $('fBusy').textContent = 'Checking that folder for a way to start it…'; checkProject(); }
653
- else { $('fBusy').textContent = 'Point me at the code folder (below) and I can start it for you — or start the app yourself and press Try again.'; $('fProject').focus(); }
654
- $('fRetryRow').classList.remove('hidden');
655
- }
656
- } else $('fRetryRow').classList.remove('hidden');
657
- return;
658
- }
659
- if (draftId) { api('/api/drafts/' + draftId, { method: 'DELETE' }).catch(() => {}); draftId = null; }
660
- await loadDemos();
661
- current = r.name; show('viewDemo');
662
- const d = demoList.find((x) => x.name === r.name); $('title').textContent = d?.title || r.name;
663
- const { yaml } = await api('/api/demos/' + r.name).catch(() => ({ yaml: '' })); $('yaml').value = yaml || '';
664
- fillSettings(d); await loadTake(true);
665
- pg = { wrap: 'progress', bar: 'barFill', stage: 'stageText', eta: 'etaText', steps: 'steps' };
666
- if (code === 0) flash('First take ready. Tell it what to change, or make it Final.');
667
- else { $('progress').classList.add('show'); $('stageText').textContent = code === 3 ? 'Drafted, but some steps would fail — open Advanced to fix them' : 'Drafted, but the recording had problems'; $('retryRow').classList.remove('hidden'); $('retryHint').textContent = 'Or tell it what to change.'; lastRun = 'main'; }
668
- });
669
- } catch (e) { $('fBusy').textContent = e.message; $('fRetryRow').classList.remove('hidden'); $('fGo').disabled = false; }
670
- };
671
- $('fRetry').onclick = () => $('fGo').click();
672
- $('fBlank').onclick = async () => {
673
- const url = $('fUrl').value.trim(); if (!/^https?:\/\//.test(url)) { $('fUrl').focus(); return; }
674
- try { const r = await api('/api/demos', { method: 'POST', body: JSON.stringify({ url, describe: $('fDesc').value }) }); if (draftId) { api('/api/drafts/' + draftId, { method: 'DELETE' }).catch(() => {}); draftId = null; } await loadDemos(); await select(r.name); $('adv').open = true; }
675
- catch (e) { $('fBusy').textContent = e.message; }
676
- };
677
-
678
- // ---------- settings ----------
679
- const PROVIDERS = [['claude-code', 'Claude Code', 'The claude CLI you are signed into. No key.'], ['codex', 'Codex', 'The codex CLI you are signed into. No key.'], ['groq', 'Groq', 'Fast hosted models. Needs an API key.'], ['mistral', 'Mistral', 'Hosted. Needs an API key.'], ['local', 'Local model', 'Ollama or LM Studio on this machine. Fully offline.']];
680
- let chosen = '';
681
- function renderProviders() {
682
- const el = $('providers'); el.innerHTML = '';
683
- for (const [id, name, desc] of PROVIDERS) {
684
- const avail = settings.available.includes(id) || id === 'local';
685
- const div = document.createElement('div'); div.className = 'provider' + (chosen === id ? ' on' : '') + (avail ? '' : ' off');
686
- div.innerHTML = `<span class="dot ${avail ? 'ok' : ''}"></span><div><b>${name}</b><div class="d">${desc}${avail ? '' : ' Not detected.'}</div></div>`;
687
- if (avail) div.onclick = () => { chosen = id; renderProviders(); };
688
- el.appendChild(div);
689
- }
690
- $('localFields').classList.toggle('hidden', chosen !== 'local');
691
- }
692
- $('gear').onclick = async () => { settings = await api('/api/settings'); chosen = settings.model || settings.available[0] || ''; $('setLocalUrl').value = settings.env.localUrl || ''; $('setLocalModel').value = settings.env.localModel || ''; $('setOut').value = settings.outDir === settings.outDefault ? '' : settings.outDir; $('setOutHint').textContent = `Currently: ${settings.outDir}`; $('toolsInfo').textContent = `GIF encoder: ${settings.gifski ? 'gifski' : 'ffmpeg (install gifski for better GIFs)'} · Active model: ${settings.active || 'none'}`; renderProviders(); show('viewSettings'); };
693
- $('setSave').onclick = async () => {
694
- const body = { model: chosen, localUrl: $('setLocalUrl').value.trim(), localModel: $('setLocalModel').value.trim(), outDir: $('setOut').value.trim() };
695
- if ($('setGroq').value) body.groqKey = $('setGroq').value.trim(); if ($('setMistral').value) body.mistralKey = $('setMistral').value.trim();
696
- try { settings = await api('/api/settings', { method: 'PUT', body: JSON.stringify(body) }); } catch (e) { $('setOutHint').textContent = e.message; return; }
697
- $('setGroq').value = ''; $('setMistral').value = '';
698
- $('modelTag').textContent = settings.active ? 'model: ' + settings.active : 'no model'; show(current ? 'viewDemo' : 'viewNew');
699
- };
700
-
701
- // ---------- keyboard on the video ----------
702
- document.addEventListener('keydown', (e) => {
703
- const tag = (e.target.tagName || '').toLowerCase(); if (tag === 'input' || tag === 'textarea' || tag === 'select') return;
704
- const v = $('stage').querySelector('video'); if (!v) return;
705
- if (e.code === 'Space') { e.preventDefault(); v.paused ? v.play() : v.pause(); }
706
- else if (e.key === 'ArrowRight') { v.currentTime = Math.min(v.duration || 0, v.currentTime + (e.shiftKey ? 5 : 1)); }
707
- else if (e.key === 'ArrowLeft') { v.currentTime = Math.max(0, v.currentTime - (e.shiftKey ? 5 : 1)); }
708
- });
709
-
710
- // ---------- boot ----------
711
- (async () => {
712
- setQuality(quality);
713
- try { const ps = await api('/api/presets'); $('sPreset').innerHTML = ps.filter((p) => p.name !== 'preview-fast').map((p) => { const h = p.layout === 'card' ? p.height : p.height + (p.layout === 'band' ? p.bandHeight : 0); return `<option value="${p.name}">${p.name} — ${p.width}×${h}</option>`; }).join(''); } catch {}
714
- try { settings = await api('/api/settings'); $('modelTag').textContent = settings.active ? 'model: ' + settings.active : 'no model — set one in Settings'; } catch {}
715
- const list = await loadDemos();
716
- // Arriving from the review page's Edit link: /classic#<demo> opens that demo.
717
- const wanted = decodeURIComponent(location.hash.slice(1));
718
- const first = (wanted && list.find((d) => d.name === wanted)) || list.find((d) => d.valid);
719
- if (first) select(first.name); else openNew();
720
- if ('serviceWorker' in navigator) navigator.serviceWorker.register('/sw.js').catch(() => {});
721
- })();
722
- </script>
723
- </body>
724
- </html>