retake-demos 0.3.0 → 0.4.1
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.
- package/AGENTS.md +64 -0
- package/README.md +14 -11
- package/dist/captions.js +9 -10
- package/dist/cards.js +57 -14
- package/dist/cli.js +197 -20
- package/dist/describe.js +2 -1
- package/dist/dryrun.js +182 -2
- package/dist/manifest.js +67 -4
- package/dist/operator/tools.js +120 -24
- package/dist/presets.js +13 -13
- package/dist/record.js +458 -43
- package/dist/render.js +255 -23
- package/dist/tidy.js +103 -0
- package/dist/ui/chat.html +346 -56
- package/dist/ui/server.js +76 -11
- package/package.json +2 -3
- package/site/releases.json +55 -0
- package/skill/SKILL.md +148 -2
- package/demos/example.yaml +0 -65
- package/dist/ui/index.html +0 -724
- package/site/index.html +0 -260
- package/site/logos/avexjets.png +0 -0
package/dist/ui/chat.html
CHANGED
|
@@ -16,13 +16,13 @@
|
|
|
16
16
|
html,body { margin:0; height:100%; background:var(--paper); color:var(--ink); font:15px/1.6 -apple-system, BlinkMacSystemFont, "SF Pro Text", "Helvetica Neue", Inter, Arial, sans-serif; -webkit-font-smoothing:antialiased; }
|
|
17
17
|
button, input { font:inherit; color:inherit; }
|
|
18
18
|
.app { min-height:100%; display:flex; flex-direction:column; }
|
|
19
|
-
.top { display:flex; align-items:center; gap:10px; padding:
|
|
19
|
+
.top { display:flex; align-items:center; gap:10px; padding:0 24px; height:56px; border-bottom:1px solid var(--rule); background:var(--paper); }
|
|
20
20
|
.mark { width:22px; height:22px; border-radius:7px; background:var(--accent); position:relative; flex:none; }
|
|
21
21
|
.mark i { position:absolute; left:5.5px; height:2.5px; border-radius:2px; background:#eceee9; }
|
|
22
22
|
.mark i:nth-child(1){top:4px;width:4px;opacity:.55} .mark i:nth-child(2){top:8px;width:7px;opacity:.8} .mark i:nth-child(3){top:12px;width:11px;background:var(--amber)} .mark i:nth-child(4){top:16px;width:7px;opacity:.8}
|
|
23
23
|
.top h1 { font-size:15px; font-weight:600; margin:0; }
|
|
24
24
|
.controls { position:relative; display:flex; align-items:center; gap:8px; margin-left:8px; }
|
|
25
|
-
.chip { border:1px solid var(--rule2); background:#fff; border-radius:999px; padding:
|
|
25
|
+
.chip { border:1px solid var(--rule2); background:#fff; border-radius:999px; height:32px; padding:0 14px; display:inline-flex; align-items:center; font-size:13px; font-weight:500; color:var(--ink); cursor:pointer; transition:border-color .12s; }
|
|
26
26
|
.chip:hover { border-color:var(--ink); }
|
|
27
27
|
.pop { position:absolute; left:0; top:38px; z-index:40; background:#fff; border:1px solid var(--rule2); border-radius:14px; box-shadow:0 12px 34px rgba(24,28,24,.13); padding:8px; width:290px; display:none; }
|
|
28
28
|
.pop.show { display:block; }
|
|
@@ -36,13 +36,13 @@
|
|
|
36
36
|
@keyframes pulse { 0%,100%{opacity:.35} 50%{opacity:1} }
|
|
37
37
|
.quiet { padding:8px 10px; color:var(--muted); font-size:13px; }
|
|
38
38
|
.sp { flex:1; }
|
|
39
|
-
.wrap { width:100%; margin:0; padding:
|
|
40
|
-
.col-main { max-width:
|
|
39
|
+
.wrap { width:100%; margin:0; padding:24px 24px 56px; display:grid; grid-template-columns:minmax(0,1fr); gap:24px; align-items:start; }
|
|
40
|
+
.col-main { max-width:760px; width:100%; margin:0 auto; min-width:0; }
|
|
41
41
|
/* wide screens: the library floats as its own card at the left, detached
|
|
42
42
|
from the window edge; the video centers in the space beside it. */
|
|
43
43
|
@media (min-width: 1000px) {
|
|
44
|
-
.wrap.two { grid-template-columns:
|
|
45
|
-
.wrap.two .lib { position:sticky; top:
|
|
44
|
+
.wrap.two { grid-template-columns:272px minmax(0,1fr); gap:32px; }
|
|
45
|
+
.wrap.two .lib { position:sticky; top:24px; order:-1; }
|
|
46
46
|
}
|
|
47
47
|
|
|
48
48
|
/* live activity — what an agent is doing to this library right now */
|
|
@@ -58,46 +58,58 @@
|
|
|
58
58
|
.pin.show { display:block; }
|
|
59
59
|
/* The element takes the video's own shape: width follows the height cap, so
|
|
60
60
|
a 1.6:1 take gets no black bars at the sides. */
|
|
61
|
-
.pin video { width:auto; max-width:100%; height:auto; max-height:
|
|
62
|
-
.under { display:flex; align-items:
|
|
63
|
-
.
|
|
61
|
+
.pin video { width:auto; max-width:100%; height:auto; max-height:60vh; margin:0 auto; border-radius:14px; background:#000; display:block; border:1px solid var(--rule2); box-shadow:0 16px 40px rgba(24,28,24,.10); }
|
|
62
|
+
.under { display:flex; align-items:baseline; gap:8px; margin-top:16px; flex-wrap:wrap; }
|
|
63
|
+
.vtitle { font-size:15px; font-weight:600; color:var(--ink); }
|
|
64
|
+
.vmeta { font-size:13px; color:var(--muted); }
|
|
65
|
+
.under a, .under button { color:var(--ink); text-decoration:none; border:1px solid var(--rule2); background:#fff; border-radius:999px; padding:0 14px; cursor:pointer; font-size:13px; font-weight:500; display:inline-flex; align-items:center; transition:border-color .12s; }
|
|
64
66
|
.under a:hover, .under button:hover { border-color:var(--ink); }
|
|
65
67
|
/* A native select with its arrow removed leaves lopsided padding; make it
|
|
66
68
|
sit exactly like the pills beside it, text centred, same height. */
|
|
67
|
-
.under select { color:var(--ink); border:1px solid var(--rule2); background:#fff; border-radius:999px; padding:0 12px; height:
|
|
68
|
-
.under a, .under button { height:
|
|
69
|
+
.under select { color:var(--ink); border:1px solid var(--rule2); background:#fff; border-radius:999px; padding:0 12px; height:32px; font-size:13px; font-weight:500; font-family:inherit; text-align:center; text-align-last:center; cursor:pointer; -webkit-appearance:none; appearance:none; min-width:58px; transition:border-color .12s; }
|
|
70
|
+
.under a, .under button { height:32px; box-sizing:border-box; align-self:center; }
|
|
69
71
|
.under select:hover { border-color:var(--ink); }
|
|
70
|
-
.
|
|
71
|
-
.
|
|
72
|
+
.frames { margin-top:16px; border:1px solid var(--rule); background:var(--card); border-radius:16px; padding:14px 16px; }
|
|
73
|
+
.frames .fhead { display:flex; align-items:center; margin-bottom:10px; }
|
|
74
|
+
.frames .fhead .lab { font:11px ui-monospace, Menlo, monospace; letter-spacing:.08em; text-transform:uppercase; color:var(--muted); }
|
|
75
|
+
.frames .fhead .hint { margin-left:10px; font-size:12.5px; color:var(--muted); }
|
|
76
|
+
.stills { display:flex; gap:8px; overflow-x:auto; padding-bottom:2px; align-items:center; }
|
|
77
|
+
.stills label.up { font-size:12.5px; font-weight:500; color:var(--muted); border:1px dashed var(--rule2); border-radius:8px; height:58px; display:inline-flex; align-items:center; padding:0 14px; cursor:pointer; flex:none; transition:border-color .12s, color .12s; }
|
|
78
|
+
.stills label.up:hover { border-color:var(--ink); color:var(--ink); }
|
|
79
|
+
.stills img { height:58px; border-radius:8px; border:1px solid var(--rule2); cursor:pointer; flex:none; transition:border-color .12s; }
|
|
72
80
|
.stills img:hover { border-color:var(--ink); }
|
|
73
|
-
.stills img.poster, .coverrow img.poster { border-color:var(--
|
|
74
|
-
.coverrow { display:flex; gap:8px; align-items:center;
|
|
75
|
-
.coverrow
|
|
81
|
+
.stills img.poster, .coverrow img.poster { border-color:var(--ink); box-shadow:0 0 0 2px var(--ink); }
|
|
82
|
+
.coverrow { display:flex; gap:8px; align-items:center; flex-wrap:wrap; margin-bottom:8px; }
|
|
83
|
+
.coverrow:empty { display:none; margin:0; }
|
|
84
|
+
.coverrow img { height:58px; border-radius:8px; border:1px solid var(--rule2); cursor:pointer; transition:border-color .12s; }
|
|
76
85
|
.coverrow img:hover { border-color:var(--ink); }
|
|
77
|
-
.coverrow .lab {
|
|
78
|
-
.coverrow label.up { font-size:12.5px; color:var(--muted); border:1px dashed var(--rule2); border-radius:8px; padding:
|
|
86
|
+
.coverrow .lab { display:none; }
|
|
87
|
+
.coverrow label.up { font-size:12.5px; font-weight:500; color:var(--muted); border:1px dashed var(--rule2); border-radius:8px; height:58px; display:inline-flex; align-items:center; padding:0 14px; cursor:pointer; transition:border-color .12s, color .12s; }
|
|
79
88
|
.coverrow label.up:hover { border-color:var(--ink); color:var(--ink); }
|
|
80
89
|
/* the library: one quiet card at the video's side */
|
|
81
|
-
.lib { border:1px solid var(--rule); background:var(--card); border-radius:16px; padding:
|
|
82
|
-
.lib h2 { font:11px ui-monospace, Menlo, monospace; letter-spacing:.
|
|
90
|
+
.lib { border:1px solid var(--rule); background:var(--card); border-radius:16px; padding:12px 8px; position:sticky; top:24px; line-height:1.4; }
|
|
91
|
+
.lib h2 { font:11px ui-monospace, Menlo, monospace; letter-spacing:.08em; text-transform:uppercase; color:var(--muted); font-weight:500; margin:4px 4px 10px 14px; }
|
|
83
92
|
details.group { margin-bottom:0; }
|
|
84
|
-
details.group summary { list-style:none; cursor:pointer; display:flex; align-items:center; gap:
|
|
93
|
+
details.group summary { list-style:none; cursor:pointer; display:flex; align-items:center; gap:8px; padding:0 12px 0 6px; border-radius:10px; font-size:14px; color:var(--muted); height:38px; }
|
|
85
94
|
details.group summary::-webkit-details-marker { display:none; }
|
|
86
95
|
details.group summary:hover { background:#eef1ea; color:var(--ink); }
|
|
87
|
-
details.group summary .tri { transition:transform .15s; font-size:
|
|
88
|
-
details.group summary:hover .tri {
|
|
89
|
-
|
|
90
|
-
|
|
91
|
-
details.group summary .fold
|
|
96
|
+
details.group summary .tri { transition:transform .15s; font-size:9px; color:var(--rule2); width:18px; height:18px; display:flex; align-items:center; justify-content:center; flex:none; }
|
|
97
|
+
details.group summary:hover .tri { color:var(--muted); }
|
|
98
|
+
/* The folder mark is quiet: amber belongs to the brand and the live dot,
|
|
99
|
+
not to every row of a list. */
|
|
100
|
+
details.group summary .fold { width:15px; height:12px; flex:none; position:relative; opacity:.8; }
|
|
101
|
+
details.group summary .fold::before { content:""; position:absolute; inset:0; border-radius:2.5px; background:var(--rule2); }
|
|
102
|
+
details.group summary .fold::after { content:""; position:absolute; left:0; top:-2.5px; width:7px; height:4px; border-radius:2px 2px 0 0; background:var(--rule2); }
|
|
103
|
+
details.group[open] summary .fold::before, details.group[open] summary .fold::after { background:var(--amber); opacity:.85; }
|
|
92
104
|
details.group[open] summary .tri { transform:rotate(90deg); }
|
|
93
|
-
details.group summary b { color:var(--ink); font-weight:
|
|
94
|
-
details.group summary small { margin-left:auto; font-size:
|
|
95
|
-
details.group .demo { margin-left:
|
|
96
|
-
.demo { display:flex; align-items:center; gap:
|
|
105
|
+
details.group summary b { color:var(--ink); font-weight:500; font-size:13.5px; flex:1; min-width:0; overflow:hidden; text-overflow:ellipsis; white-space:nowrap; }
|
|
106
|
+
details.group summary small { margin-left:auto; font-size:11.5px; color:var(--muted); font-variant-numeric:tabular-nums; }
|
|
107
|
+
details.group .demo { margin-left:24px; }
|
|
108
|
+
.demo { display:flex; align-items:center; gap:9px; padding:0 10px; border-radius:9px; cursor:pointer; height:34px; }
|
|
97
109
|
.demo:hover { background:#eef1ea; }
|
|
98
|
-
.demo.on { background:#fff; border:1px solid var(--rule); }
|
|
99
|
-
.demo .nm { flex:1; min-width:0; overflow:hidden; text-overflow:ellipsis; white-space:nowrap; font-size:
|
|
100
|
-
.demo small { color:var(--muted); font-size:
|
|
110
|
+
.demo.on { background:#fff; border:1px solid var(--rule); box-shadow:0 1px 3px rgba(24,28,24,.05); }
|
|
111
|
+
.demo .nm { flex:1; min-width:0; overflow:hidden; text-overflow:ellipsis; white-space:nowrap; font-size:13.5px; }
|
|
112
|
+
.demo small { color:var(--muted); font-size:11.5px; flex:none; font-variant-numeric:tabular-nums; }
|
|
101
113
|
.demo .zap { flex:none; border:0; background:transparent; color:var(--muted); cursor:pointer; font-size:14px; width:26px; height:26px; border-radius:7px; visibility:hidden; }
|
|
102
114
|
.demo .zap:hover { background:#f3e9e6; }
|
|
103
115
|
.demo:hover .zap { visibility:visible; }
|
|
@@ -123,6 +135,55 @@
|
|
|
123
135
|
.empty h2 { color:var(--ink); font-size:17px; margin:0 0 6px; }
|
|
124
136
|
.empty p { margin:0; font-size:14px; }
|
|
125
137
|
.empty code { background:var(--card); border:1px solid var(--rule); border-radius:6px; padding:2px 7px; font:13px ui-monospace, Menlo, monospace; }
|
|
138
|
+
.modal.wide { width:min(94vw, 560px); max-height:82vh; display:flex; flex-direction:column; }
|
|
139
|
+
.modal.wide h3 { display:flex; align-items:baseline; gap:9px; }
|
|
140
|
+
.modal.wide .scroll { overflow-y:auto; margin:0 -4px; padding:0 4px; }
|
|
141
|
+
.rel { border-top:1px solid var(--rule); padding:15px 0; }
|
|
142
|
+
.rel:first-child { border-top:0; padding-top:2px; }
|
|
143
|
+
.relhead { display:flex; flex-wrap:wrap; align-items:baseline; gap:9px; margin-bottom:7px; }
|
|
144
|
+
.relhead b { font-size:14.5px; color:var(--ink); letter-spacing:-.01em; }
|
|
145
|
+
.relmeta { font-size:12px; color:var(--muted); font-variant-numeric:tabular-nums; }
|
|
146
|
+
/* --accent is near-black in this window; the badge needs the amber token or
|
|
147
|
+
it renders dark ink on a dark pill and reads as a redaction bar. */
|
|
148
|
+
.rel.latest .relmeta::after { content:"newest"; margin-left:7px; padding:1.5px 7px; border-radius:99px; background:var(--amber); color:#241d0c; font-size:10.5px; font-weight:600; letter-spacing:.01em; }
|
|
149
|
+
.rel ul { margin:0; padding-left:17px; }
|
|
150
|
+
.rel li { margin:4px 0; font-size:13.5px; line-height:1.55; color:var(--muted); }
|
|
151
|
+
.rel:not(.latest) li { opacity:.8; }
|
|
152
|
+
.chip .ndot { display:inline-block; width:6px; height:6px; border-radius:99px; background:var(--amber); margin-left:6px; vertical-align:middle; }
|
|
153
|
+
.lib h2 { display:flex; align-items:center; gap:8px; }
|
|
154
|
+
.lib h2 .selbtn { margin-left:auto; border:1px solid var(--rule2); background:#fff; color:var(--muted); border-radius:8px; font-size:11.5px; padding:3px 9px; cursor:pointer; line-height:1.4; }
|
|
155
|
+
.lib h2 .selbtn:hover { border-color:var(--ink); color:var(--ink); }
|
|
156
|
+
.lib h2 .selbtn.on { background:var(--ink); border-color:var(--ink); color:#fff; }
|
|
157
|
+
.demo .pick { display:none; width:15px; height:15px; margin-right:2px; accent-color:var(--ink); flex:none; cursor:pointer; }
|
|
158
|
+
body.selecting .demo .pick { display:inline-block; }
|
|
159
|
+
body.selecting .demo .zap { display:none; }
|
|
160
|
+
body.selecting .demo { cursor:pointer; }
|
|
161
|
+
.selbar { position:sticky; bottom:0; margin-top:10px; padding:10px 12px; background:var(--paper); border-top:1px solid var(--rule2); display:flex; align-items:center; gap:9px; }
|
|
162
|
+
.selbar small { color:var(--muted); font-size:12px; }
|
|
163
|
+
.selbar .sp { flex:1; }
|
|
164
|
+
.selbar button { border:1px solid var(--rule2); background:#fff; border-radius:8px; font-size:12px; padding:5px 11px; cursor:pointer; }
|
|
165
|
+
.selbar button:hover { border-color:var(--ink); }
|
|
166
|
+
.selbar button.go { background:var(--bad); border-color:var(--bad); color:#fff; }
|
|
167
|
+
.selbar button.go:disabled { opacity:.4; cursor:default; }
|
|
168
|
+
.selbar button.go:not(:disabled):hover { filter:brightness(.92); }
|
|
169
|
+
.group summary .gpick { display:none; width:15px; height:15px; margin-right:2px; accent-color:var(--ink); flex:none; cursor:pointer; vertical-align:middle; }
|
|
170
|
+
body.selecting .group summary .gpick { display:inline-block; }
|
|
171
|
+
.arch summary .emptybtn { margin-left:auto; border:1px solid var(--rule2); background:#fff; color:var(--muted); border-radius:8px; font-size:11px; padding:2px 8px; cursor:pointer; }
|
|
172
|
+
.arch summary .emptybtn:hover { background:var(--bad); border-color:var(--bad); color:#fff; }
|
|
173
|
+
.arch summary { display:flex; align-items:center; gap:6px; }
|
|
174
|
+
.live .prog { margin-top:12px; }
|
|
175
|
+
.live .bar { height:4px; border-radius:99px; background:var(--rule); overflow:hidden; }
|
|
176
|
+
.live .bar i { display:block; height:100%; background:var(--amber); border-radius:99px;
|
|
177
|
+
transition:width .45s cubic-bezier(.2,.7,.3,1); }
|
|
178
|
+
.live .under { display:flex; align-items:baseline; gap:8px; margin-top:7px; font-size:12.5px; color:var(--muted); }
|
|
179
|
+
.live .under .sp { flex:1; }
|
|
180
|
+
.live .count { font-variant-numeric:tabular-nums; }
|
|
181
|
+
.live .quip { color:var(--ink); font-weight:500; }
|
|
182
|
+
.live .elapsed { font-variant-numeric:tabular-nums; opacity:.7; }
|
|
183
|
+
.spin { width:9px; height:9px; border-radius:99px; background:var(--amber); flex:none;
|
|
184
|
+
animation:pulse 1.4s ease-in-out infinite; }
|
|
185
|
+
@keyframes pulse { 0%,100% { opacity:.35; transform:scale(.82) } 50% { opacity:1; transform:scale(1) } }
|
|
186
|
+
@media (prefers-reduced-motion:reduce) { .spin { animation:none; opacity:.9 } .live .bar i { transition:none } }
|
|
126
187
|
</style>
|
|
127
188
|
</head>
|
|
128
189
|
<body>
|
|
@@ -132,29 +193,41 @@
|
|
|
132
193
|
<h1>Retake</h1>
|
|
133
194
|
<span class="sp"></span>
|
|
134
195
|
<a class="chip" href="/guide" style="text-decoration:none">How it works</a>
|
|
196
|
+
<button class="chip" id="newsBtn" title="What changed in this version">What's new</button>
|
|
135
197
|
<button class="chip" id="gremlinBtn" title="Something's off? Tell me.">Bug?</button>
|
|
136
198
|
</div>
|
|
137
199
|
<div class="wrap two">
|
|
138
200
|
<div class="col-main">
|
|
139
201
|
<div class="live" id="live">
|
|
140
|
-
<div class="head"><span class="
|
|
202
|
+
<div class="head"><span class="spin" id="liveDot"></span><b id="liveWho"></b><span id="liveWhat"></span></div>
|
|
203
|
+
<div class="prog" id="liveProg" style="display:none">
|
|
204
|
+
<div class="bar"><i id="liveBar" style="width:0%"></i></div>
|
|
205
|
+
<div class="under"><span class="quip" id="liveQuip"></span><span class="sp"></span><span class="count" id="liveCount"></span><span class="elapsed" id="liveElapsed"></span></div>
|
|
206
|
+
</div>
|
|
141
207
|
<div class="lines" id="liveLines"></div>
|
|
142
208
|
</div>
|
|
143
209
|
<div class="pin" id="pin">
|
|
144
210
|
<video id="vid" controls playsinline></video>
|
|
145
211
|
<div class="under">
|
|
146
|
-
<span id="
|
|
212
|
+
<span class="vtitle" id="vidName"></span>
|
|
213
|
+
<span class="vmeta" id="vidMeta"></span>
|
|
147
214
|
<span class="sp"></span>
|
|
148
215
|
<select id="vidSpeed" title="Playback speed — preview now, and Download gets this speed">
|
|
149
216
|
<option value="0.75">0.75×</option><option value="0.85">0.85×</option><option value="1" selected>1×</option><option value="1.15">1.15×</option><option value="1.3">1.3×</option>
|
|
150
217
|
</select>
|
|
151
218
|
<a id="vidDl" download>Download</a>
|
|
152
219
|
<button id="vidFolder">Show in folder</button>
|
|
153
|
-
<button id="vidCover" title="Make the frame showing right now the poster">Use this frame</button>
|
|
154
|
-
<!-- The workshop (/classic) still exists but is not advertised until it has been tested as carefully as this page. -->
|
|
155
220
|
</div>
|
|
156
|
-
<div class="
|
|
157
|
-
|
|
221
|
+
<div class="frames">
|
|
222
|
+
<div class="fhead">
|
|
223
|
+
<span class="lab">Poster & frames</span>
|
|
224
|
+
<span class="hint">click any frame to make it the poster</span>
|
|
225
|
+
<span class="sp"></span>
|
|
226
|
+
<button class="chip" id="vidCover" title="Make the frame showing right now the poster">Use current frame</button>
|
|
227
|
+
</div>
|
|
228
|
+
<div class="coverrow" id="coverrow"></div>
|
|
229
|
+
<div class="stills" id="stills"></div>
|
|
230
|
+
</div>
|
|
158
231
|
</div>
|
|
159
232
|
<div class="empty" id="empty" style="display:none">
|
|
160
233
|
<div class="mark"><i></i><i></i><i></i><i></i></div>
|
|
@@ -163,7 +236,7 @@
|
|
|
163
236
|
</div>
|
|
164
237
|
</div>
|
|
165
238
|
<aside class="lib" id="lib" style="display:none">
|
|
166
|
-
<h2 id="libTtl">
|
|
239
|
+
<h2><span id="libTtl">Library</span><button class="selbtn" id="selBtn" title="Pick several to archive at once">Select</button></h2>
|
|
167
240
|
<div id="demos"></div>
|
|
168
241
|
</aside>
|
|
169
242
|
</div>
|
|
@@ -188,6 +261,45 @@ async function setCover(body, marked) {
|
|
|
188
261
|
} catch (e) { $('vidMeta').textContent = 'could not set the poster: ' + e.message; }
|
|
189
262
|
}
|
|
190
263
|
|
|
264
|
+
|
|
265
|
+
// What's new — reads the same list the landing page does. The dot appears when
|
|
266
|
+
// the newest entry is one this browser has not seen, and only until it is read;
|
|
267
|
+
// nothing interrupts a person who is in the middle of something.
|
|
268
|
+
const SEEN_KEY = 'retake:seen-release';
|
|
269
|
+
let releases = null;
|
|
270
|
+
async function loadReleases() {
|
|
271
|
+
if (releases) return releases;
|
|
272
|
+
try { releases = (await api('/api/releases')).releases || []; } catch { releases = []; }
|
|
273
|
+
return releases;
|
|
274
|
+
}
|
|
275
|
+
(async () => {
|
|
276
|
+
const rs = await loadReleases();
|
|
277
|
+
const btn = $('newsBtn');
|
|
278
|
+
if (!rs.length) { btn.style.display = 'none'; return; }
|
|
279
|
+
if (localStorage.getItem(SEEN_KEY) !== rs[0].version) btn.insertAdjacentHTML('beforeend', '<span class="ndot"></span>');
|
|
280
|
+
})();
|
|
281
|
+
$('newsBtn').onclick = async () => {
|
|
282
|
+
const rs = await loadReleases();
|
|
283
|
+
if (!rs.length) return;
|
|
284
|
+
const fmt = (d) => { const x = new Date(d + 'T00:00:00'); return isNaN(x) ? '' : x.toLocaleDateString(undefined, { month: 'long', day: 'numeric', year: 'numeric' }); };
|
|
285
|
+
const veil = document.createElement('div');
|
|
286
|
+
veil.className = 'veil';
|
|
287
|
+
veil.innerHTML = '<div class="modal wide"><h3>What\'s new</h3>' +
|
|
288
|
+
'<div class="scroll">' + rs.map((r, i) =>
|
|
289
|
+
'<div class="rel' + (i ? '' : ' latest') + '">' +
|
|
290
|
+
'<div class="relhead"><b>' + esc(r.headline) + '</b><span class="relmeta">' + esc(r.version) + (r.date ? ' · ' + fmt(r.date) : '') + '</span></div>' +
|
|
291
|
+
'<ul>' + (r.notes || []).map((n) => '<li>' + esc(n) + '</li>').join('') + '</ul>' +
|
|
292
|
+
'</div>').join('') + '</div>' +
|
|
293
|
+
'<div class="row" style="margin-top:18px"><button class="btn" id="newsClose">Close</button></div></div>';
|
|
294
|
+
document.body.appendChild(veil);
|
|
295
|
+
localStorage.setItem(SEEN_KEY, rs[0].version);
|
|
296
|
+
$('newsBtn').querySelector('.ndot')?.remove();
|
|
297
|
+
const shut = () => veil.remove();
|
|
298
|
+
veil.querySelector('#newsClose').onclick = shut;
|
|
299
|
+
veil.onclick = (e) => { if (e.target === veil) shut(); };
|
|
300
|
+
document.addEventListener('keydown', function esc2(e) { if (e.key === 'Escape') { shut(); document.removeEventListener('keydown', esc2); } });
|
|
301
|
+
};
|
|
302
|
+
|
|
191
303
|
document.getElementById('gremlinBtn').onclick = async () => {
|
|
192
304
|
const veil = document.createElement('div');
|
|
193
305
|
veil.className = 'veil';
|
|
@@ -266,7 +378,8 @@ async function pin(name) {
|
|
|
266
378
|
}
|
|
267
379
|
const t = d.take;
|
|
268
380
|
const failed = (t.timeline || []).find((e) => e.ok === false);
|
|
269
|
-
$('
|
|
381
|
+
$('vidName').textContent = name;
|
|
382
|
+
$('vidMeta').textContent = `${(t.duration - t.trimBefore).toFixed(0)}s · ${t.ok ? 'all steps passed' : failed ? `stopped at step ${failed.index}: ${failed.summary}` : 'some steps failed'}${d.facts ? ` · ${d.facts.width}×${d.facts.height}` : ''}`;
|
|
270
383
|
$('vidMeta').title = failed?.screen ? 'On screen at that moment: ' + failed.screen : '';
|
|
271
384
|
$('vidDl').href = `/out/${name}/demo.mp4${q}`; $('vidDl').setAttribute('download', name + '.mp4');
|
|
272
385
|
$('vidFolder').onclick = () => api('/api/open/' + name, { method: 'POST' }).catch(() => {});
|
|
@@ -278,15 +391,17 @@ $('vidCover').onclick = () => setCover({ at: $('vid').currentTime });
|
|
|
278
391
|
// here, not guessed: any scene still, any frame of the take, a generated
|
|
279
392
|
// cover, or the person's own image. All of it just rewrites thumbnail.png,
|
|
280
393
|
// so it costs no re-render.
|
|
394
|
+
// One tray: generated covers first, then every scene frame, then upload —
|
|
395
|
+
// a single row a person scans once, left to right.
|
|
281
396
|
const st = (d.files || []).filter((f) => f.startsWith('stills/')).sort();
|
|
282
|
-
$('stills').innerHTML = st.map((f) => `<img src="/out/${name}/${f}${q}" title="Make this the poster — ${esc(f.slice(7, -4))}" loading="lazy" data-still="${esc(f.slice(7))}">`).join('');
|
|
283
|
-
for (const img of $('stills').querySelectorAll('img')) img.onclick = () => setCover({ still: img.dataset.still }, img);
|
|
284
|
-
|
|
285
397
|
const covers = (d.files || []).filter((f) => f === 'cover.png' || f === 'cover-titled.png');
|
|
286
|
-
$('coverrow').innerHTML =
|
|
287
|
-
|
|
398
|
+
$('coverrow').innerHTML = '';
|
|
399
|
+
$('stills').innerHTML =
|
|
400
|
+
covers.map((f) => `<img src="/out/${name}/${f}${q}" title="${f === 'cover.png' ? 'The title card' : 'This frame, with the title on it'}" data-file="${f}">`).join('') +
|
|
401
|
+
st.map((f) => `<img src="/out/${name}/${f}${q}" title="Make this the poster — ${esc(f.slice(7, -4))}" loading="lazy" data-still="${esc(f.slice(7))}">`).join('') +
|
|
288
402
|
`<label class="up">Upload an image<input type="file" accept="image/png,image/jpeg,image/webp" hidden id="coverUp"></label>`;
|
|
289
|
-
for (const img of $('
|
|
403
|
+
for (const img of $('stills').querySelectorAll('img[data-still]')) img.onclick = () => setCover({ still: img.dataset.still }, img);
|
|
404
|
+
for (const img of $('stills').querySelectorAll('img[data-file]')) img.onclick = () => setCover({ file: img.dataset.file }, img);
|
|
290
405
|
$('coverUp').onchange = (e) => {
|
|
291
406
|
const f = e.target.files?.[0];
|
|
292
407
|
if (!f) return;
|
|
@@ -306,7 +421,7 @@ async function drawDemos() {
|
|
|
306
421
|
$('empty').style.display = has ? 'none' : '';
|
|
307
422
|
$('lib').style.display = has ? '' : 'none';
|
|
308
423
|
document.body.classList.toggle('rail', has);
|
|
309
|
-
$('libTtl').textContent = `
|
|
424
|
+
$('libTtl').textContent = `Library · ${demos.length}`;
|
|
310
425
|
$('demos').innerHTML = '';
|
|
311
426
|
const groups = new Map();
|
|
312
427
|
for (const p of demos) { const g = p.group || 'unsorted'; if (!groups.has(g)) groups.set(g, []); groups.get(g).push(p); }
|
|
@@ -316,7 +431,26 @@ async function drawDemos() {
|
|
|
316
431
|
det.className = 'group';
|
|
317
432
|
const holdsCurrent = items.some((x) => x.name === current);
|
|
318
433
|
if (open.has(g) || groups.size === 1 || holdsCurrent) det.open = true;
|
|
319
|
-
det.innerHTML = `<summary><span class="tri">▶</span><span class="fold" aria-hidden="true"></span><b>${esc(g)}</b><small>${items.length}</small></summary>`;
|
|
434
|
+
det.innerHTML = `<summary><input type="checkbox" class="gpick"><span class="tri">▶</span><span class="fold" aria-hidden="true"></span><b>${esc(g)}</b><small>${items.length}</small></summary>`;
|
|
435
|
+
// A whole folder in one click — with 35 demos in a handful of projects,
|
|
436
|
+
// ticking them one at a time is the thing this is meant to replace.
|
|
437
|
+
const gbox = det.querySelector('.gpick');
|
|
438
|
+
const names = items.map((x) => x.name);
|
|
439
|
+
gbox.checked = names.length > 0 && names.every((n) => picked.has(n));
|
|
440
|
+
// Clicking a checkbox inside <summary> also toggles the folder open/shut,
|
|
441
|
+
// and preventDefault would stop BOTH — including the tick, which is what
|
|
442
|
+
// makes "select the folder" look like it did nothing. So the fold is
|
|
443
|
+
// allowed to happen and then put straight back, and the tick is left
|
|
444
|
+
// entirely alone.
|
|
445
|
+
gbox.onclick = (ev) => {
|
|
446
|
+
ev.stopPropagation();
|
|
447
|
+
const wasOpen = det.open;
|
|
448
|
+
const all = names.every((n) => picked.has(n));
|
|
449
|
+
for (const n of names) all ? picked.delete(n) : picked.add(n);
|
|
450
|
+
for (const b of det.querySelectorAll('.demo .pick')) b.checked = !all;
|
|
451
|
+
drawSelBar();
|
|
452
|
+
setTimeout(() => { det.open = wasOpen; }, 0);
|
|
453
|
+
};
|
|
320
454
|
det.addEventListener('toggle', () => {
|
|
321
455
|
const now = [...document.querySelectorAll('details.group[open] summary b')].map((b) => b.textContent);
|
|
322
456
|
localStorage.setItem('retake.openGroups', JSON.stringify(now));
|
|
@@ -326,8 +460,16 @@ async function drawDemos() {
|
|
|
326
460
|
el.className = 'demo' + (p.name === current ? ' on' : '');
|
|
327
461
|
const dot = p.lastTake ? (p.lastTake.ok && !p.lastTake.partial ? 'ok' : 'bad') : '';
|
|
328
462
|
const when = p.lastTake ? new Date(p.lastTake.finishedAt).toLocaleDateString(undefined, { month: 'short', day: 'numeric' }) : 'no take yet';
|
|
329
|
-
el.innerHTML = `<span class="dot ${dot}"></span><span class="nm">${esc(p.name)}</span><small>${esc(when)}</small><button class="zap" title="Archive this demo">✕</button>`;
|
|
330
|
-
|
|
463
|
+
el.innerHTML = `<input type="checkbox" class="pick"${picked.has(p.name) ? ' checked' : ''}><span class="dot ${dot}"></span><span class="nm">${esc(p.name)}</span><small>${esc(when)}</small><button class="zap" title="Archive this demo">✕</button>`;
|
|
464
|
+
const box = el.querySelector('.pick');
|
|
465
|
+
const syncGroup = () => { const gb = det.querySelector('.gpick'); if (gb) gb.checked = items.every((x) => picked.has(x.name)); };
|
|
466
|
+
box.onclick = (ev) => { ev.stopPropagation(); box.checked ? picked.add(p.name) : picked.delete(p.name); syncGroup(); drawSelBar(); };
|
|
467
|
+
el.onclick = () => {
|
|
468
|
+
// In select mode the whole row is the checkbox — hunting for a 15px
|
|
469
|
+
// target is exactly the fiddliness this is meant to remove.
|
|
470
|
+
if (selecting) { box.checked = !box.checked; box.checked ? picked.add(p.name) : picked.delete(p.name); syncGroup(); drawSelBar(); return; }
|
|
471
|
+
if (p.lastTake) pin(p.name);
|
|
472
|
+
};
|
|
331
473
|
el.querySelector('.zap').onclick = async (ev) => {
|
|
332
474
|
ev.stopPropagation();
|
|
333
475
|
// Archive, not destroy — but moving someone's work still deserves a beat.
|
|
@@ -341,8 +483,59 @@ async function drawDemos() {
|
|
|
341
483
|
$('demos').appendChild(det);
|
|
342
484
|
}
|
|
343
485
|
await drawArchive();
|
|
486
|
+
drawSelBar();
|
|
344
487
|
}
|
|
345
488
|
|
|
489
|
+
|
|
490
|
+
// Bulk archive. Off by default: a list you can accidentally multi-select from
|
|
491
|
+
// is worse than one that needs a deliberate "Select" first.
|
|
492
|
+
let selecting = false;
|
|
493
|
+
const picked = new Set();
|
|
494
|
+
|
|
495
|
+
function drawSelBar() {
|
|
496
|
+
const old = document.querySelector('.selbar'); if (old) old.remove();
|
|
497
|
+
if (!selecting) return;
|
|
498
|
+
const bar = document.createElement('div');
|
|
499
|
+
bar.className = 'selbar';
|
|
500
|
+
const n = picked.size;
|
|
501
|
+
bar.innerHTML = `<small>${n ? n + ' selected' : 'none selected'}</small><span class="sp"></span>` +
|
|
502
|
+
`<button id="selAll">${n === demos.length ? 'None' : 'All'}</button>` +
|
|
503
|
+
`<button class="go" id="selGo"${n ? '' : ' disabled'}>Archive${n ? ' ' + n : ''}</button>`;
|
|
504
|
+
$('lib').appendChild(bar);
|
|
505
|
+
bar.querySelector('#selAll').onclick = () => {
|
|
506
|
+
if (picked.size === demos.length) picked.clear(); else for (const d of demos) picked.add(d.name);
|
|
507
|
+
for (const b of document.querySelectorAll('.demo .pick')) b.checked = picked.has(b.parentElement.querySelector('.nm').textContent);
|
|
508
|
+
drawSelBar();
|
|
509
|
+
};
|
|
510
|
+
bar.querySelector('#selGo').onclick = async () => {
|
|
511
|
+
const names = [...picked];
|
|
512
|
+
if (!names.length) return;
|
|
513
|
+
if (!(await ask(`Archive ${names.length} demo${names.length === 1 ? '' : 's'}?`,
|
|
514
|
+
names.slice(0, 8).join(', ') + (names.length > 8 ? ` and ${names.length - 8} more` : '') +
|
|
515
|
+
'. Their videos are removed. The demos move to the archive below and can be restored any time.', 'Archive'))) return;
|
|
516
|
+
const failed = [];
|
|
517
|
+
for (const nm of names) {
|
|
518
|
+
try { await api('/api/demos/' + nm, { method: 'DELETE' }); } catch (e) { failed.push(nm + ' (' + e.message + ')'); }
|
|
519
|
+
}
|
|
520
|
+
if (current && picked.has(current)) { current = null; $('pin').classList.remove('show'); }
|
|
521
|
+
picked.clear();
|
|
522
|
+
selecting = false;
|
|
523
|
+
document.body.classList.remove('selecting');
|
|
524
|
+
$('selBtn').classList.remove('on');
|
|
525
|
+
await drawDemos();
|
|
526
|
+
if (failed.length) alert('Could not archive:\n' + failed.join('\n'));
|
|
527
|
+
};
|
|
528
|
+
}
|
|
529
|
+
|
|
530
|
+
$('selBtn').onclick = () => {
|
|
531
|
+
selecting = !selecting;
|
|
532
|
+
picked.clear();
|
|
533
|
+
document.body.classList.toggle('selecting', selecting);
|
|
534
|
+
$('selBtn').classList.toggle('on', selecting);
|
|
535
|
+
$('selBtn').textContent = selecting ? 'Done' : 'Select';
|
|
536
|
+
drawDemos();
|
|
537
|
+
};
|
|
538
|
+
|
|
346
539
|
async function drawArchive() {
|
|
347
540
|
let items = [];
|
|
348
541
|
try { items = await api('/api/trash'); } catch { return; }
|
|
@@ -350,7 +543,7 @@ async function drawArchive() {
|
|
|
350
543
|
if (!items.length) return;
|
|
351
544
|
const det = document.createElement('details');
|
|
352
545
|
det.className = 'group arch';
|
|
353
|
-
det.innerHTML = `<summary><span class="tri">▶</span><b>Archive</b><small>${items.length}</small></summary>`;
|
|
546
|
+
det.innerHTML = `<summary><span class="tri">▶</span><b>Archive</b><small>${items.length}</small><button class="emptybtn" id="emptyArch" title="Delete every archived demo, permanently">Empty</button></summary>`;
|
|
354
547
|
for (const t of items) {
|
|
355
548
|
const el = document.createElement('div');
|
|
356
549
|
el.className = 'row2';
|
|
@@ -360,12 +553,15 @@ async function drawArchive() {
|
|
|
360
553
|
}
|
|
361
554
|
const clear = document.createElement('button');
|
|
362
555
|
clear.className = 'clear';
|
|
363
|
-
clear.textContent =
|
|
364
|
-
|
|
365
|
-
if (
|
|
556
|
+
clear.textContent = `Empty the archive — delete ${items.length} demo${items.length === 1 ? '' : 's'} for good`;
|
|
557
|
+
const empty = async (ev) => {
|
|
558
|
+
if (ev) { ev.stopPropagation(); ev.preventDefault(); } // the summary must not fold
|
|
559
|
+
if (!(await ask('Empty the archive?', `${items.length} archived demo${items.length === 1 ? '' : 's'} will be deleted permanently — the manifests and everything recorded from them. There is no way back from this one.`, 'Delete forever'))) return;
|
|
366
560
|
await api('/api/trash/clear', { method: 'POST' });
|
|
367
561
|
await drawDemos();
|
|
368
562
|
};
|
|
563
|
+
clear.onclick = empty;
|
|
564
|
+
det.querySelector('#emptyArch').onclick = empty;
|
|
369
565
|
det.appendChild(clear);
|
|
370
566
|
$('demos').appendChild(det);
|
|
371
567
|
}
|
|
@@ -434,14 +630,107 @@ function askSecrets(r) {
|
|
|
434
630
|
|
|
435
631
|
// ---- the live feed: an agent working shows up here as it works -------------
|
|
436
632
|
function watchActivity() {
|
|
633
|
+
|
|
634
|
+
// While a take runs there is nothing to look at and a lot of time to look at
|
|
635
|
+
// it. A bar for how far in, a clock for how long, and something to read that
|
|
636
|
+
// is not a spinner. Recording-flavoured, because that is what is happening,
|
|
637
|
+
// and none of them claim progress they do not have.
|
|
638
|
+
const QUIPS = [
|
|
639
|
+
'Rolling.', 'Lining up the shot.', 'Waiting on your app.',
|
|
640
|
+
'Not fumbling the take.', 'Holding the frame.', 'Letting the app think.',
|
|
641
|
+
'Clicking things, patiently.', 'Doing the boring part.',
|
|
642
|
+
'Take one. Also the only take.', 'Typing at a speed people can read.',
|
|
643
|
+
'Watching the spinner so you do not have to.', 'No sighing this time.',
|
|
644
|
+
'Getting it right the first time, on purpose.',
|
|
645
|
+
];
|
|
646
|
+
let quipAt = 0, quipTimer = null, elapsedTimer = null, runStart = 0, lastActivity = null;
|
|
647
|
+
|
|
648
|
+
function fmtElapsed(ms) {
|
|
649
|
+
const t = Math.max(0, Math.round(ms / 1000));
|
|
650
|
+
return t < 60 ? t + 's' : Math.floor(t / 60) + 'm ' + String(t % 60).padStart(2, '0') + 's';
|
|
651
|
+
}
|
|
652
|
+
|
|
653
|
+
function startLiveTickers(startedAt) {
|
|
654
|
+
const want = startedAt || Date.now();
|
|
655
|
+
if (runStart !== want) runStart = want; // a new run restarts the clock
|
|
656
|
+
if (!quipTimer) {
|
|
657
|
+
// Slow enough to read, and it only moves while something is happening.
|
|
658
|
+
quipTimer = setInterval(() => {
|
|
659
|
+
quipAt = (quipAt + 1) % QUIPS.length;
|
|
660
|
+
const q = $('liveQuip'); if (q) q.textContent = QUIPS[quipAt];
|
|
661
|
+
}, 4200);
|
|
662
|
+
}
|
|
663
|
+
if (!elapsedTimer) {
|
|
664
|
+
elapsedTimer = setInterval(() => {
|
|
665
|
+
const e = $('liveElapsed'); if (e) e.textContent = fmtElapsed(Date.now() - runStart);
|
|
666
|
+
// No events arrive while ffmpeg works, so the render bar advances on
|
|
667
|
+
// the clock or it would look frozen for the whole render.
|
|
668
|
+
if (lastActivity && lastActivity.progress && lastActivity.progress.phase === 'rendering') drawProgress(lastActivity);
|
|
669
|
+
}, 1000);
|
|
670
|
+
}
|
|
671
|
+
}
|
|
672
|
+
function stopLiveTickers() {
|
|
673
|
+
clearInterval(quipTimer); clearInterval(elapsedTimer);
|
|
674
|
+
quipTimer = elapsedTimer = null;
|
|
675
|
+
}
|
|
676
|
+
|
|
677
|
+
function drawProgress(a) {
|
|
678
|
+
const box = $('liveProg'); if (!box) return;
|
|
679
|
+
if (a) lastActivity = a;
|
|
680
|
+
const p = a && a.progress;
|
|
681
|
+
if (!a || !a.active) { box.style.display = 'none'; stopLiveTickers(); return; }
|
|
682
|
+
// Nothing has reported in five minutes: the run is over, or whatever was
|
|
683
|
+
// running is gone. Either way the bar is lying, so stop showing it.
|
|
684
|
+
if (p && p.since && Date.now() - p.since > 5 * 60_000) { box.style.display = 'none'; stopLiveTickers(); return; }
|
|
685
|
+
if (!p) { box.style.display = 'none'; stopLiveTickers(); return; }
|
|
686
|
+
// Measure THIS run, not the activity feed. The feed goes active on the
|
|
687
|
+
// first line and stays that way, so a window left open all day reported a
|
|
688
|
+
// four-minute take as 529 minutes.
|
|
689
|
+
startLiveTickers(p.runStart || a.startedAt);
|
|
690
|
+
if (!$('liveQuip').textContent) $('liveQuip').textContent = QUIPS[0];
|
|
691
|
+
$('liveElapsed').textContent = fmtElapsed(Date.now() - runStart);
|
|
692
|
+
// Rendering: there are no steps to count, but the last render of this demo
|
|
693
|
+
// measured itself, so the bar can run on that. Capped at 95% — it must not
|
|
694
|
+
// claim finished before it is, and an estimate is never a promise.
|
|
695
|
+
if (p && p.phase === 'rendering') {
|
|
696
|
+
box.style.display = '';
|
|
697
|
+
const eta = (p.etaSec || 0) * 1000;
|
|
698
|
+
const gone = Date.now() - (p.since || Date.now());
|
|
699
|
+
$('liveBar').style.width = (eta ? Math.min(95, Math.round((gone / eta) * 100)) : 0) + '%';
|
|
700
|
+
const left = Math.max(0, Math.round((eta - gone) / 1000));
|
|
701
|
+
$('liveCount').textContent = eta
|
|
702
|
+
? (left > 0 ? `rendering · about ${left}s left` : 'rendering · nearly there')
|
|
703
|
+
: 'rendering';
|
|
704
|
+
return;
|
|
705
|
+
}
|
|
706
|
+
if (!p || !p.total) {
|
|
707
|
+
// Working, but nothing countable yet — show the clock, not a fake bar.
|
|
708
|
+
box.style.display = '';
|
|
709
|
+
$('liveBar').style.width = '0%';
|
|
710
|
+
$('liveCount').textContent = '';
|
|
711
|
+
return;
|
|
712
|
+
}
|
|
713
|
+
box.style.display = '';
|
|
714
|
+
// The last STEP is not the end of the take: the final hold still has to
|
|
715
|
+
// play, the video still has to be finalised and its dead tail trimmed. A
|
|
716
|
+
// bar sitting at 100% while the thing is visibly still going is the kind
|
|
717
|
+
// of small lie that makes someone stop trusting the rest of the window.
|
|
718
|
+
const pct = Math.min(96, Math.round((p.step / p.total) * 96));
|
|
719
|
+
$('liveBar').style.width = pct + '%';
|
|
720
|
+
$('liveCount').textContent = `step ${p.step} of ${p.total}`;
|
|
721
|
+
}
|
|
722
|
+
|
|
437
723
|
const es = new EventSource('/api/activity/stream');
|
|
438
724
|
const apply = (a) => {
|
|
439
725
|
if (!a || (!a.active && !a.lines?.length)) return;
|
|
440
726
|
$('live').classList.add('show');
|
|
441
|
-
|
|
727
|
+
// 'spin' has to survive: this line used to replace it wholesale, so the
|
|
728
|
+
// spinner existed in the markup and never once appeared.
|
|
729
|
+
$('liveDot').className = a.active ? 'spin' : 'dot ok';
|
|
442
730
|
$('liveWho').textContent = a.active ? (a.who || 'your agent') : 'Done';
|
|
443
731
|
$('liveWhat').textContent = a.demo ? `· ${a.demo}` : '';
|
|
444
732
|
$('liveLines').innerHTML = (a.lines || []).slice(-4).map((l) => `<div>${esc(l)}</div>`).join('');
|
|
733
|
+
drawProgress(a);
|
|
445
734
|
};
|
|
446
735
|
es.addEventListener('state', (ev) => {
|
|
447
736
|
const d = JSON.parse(ev.data);
|
|
@@ -452,6 +741,7 @@ function watchActivity() {
|
|
|
452
741
|
for (const r of d.secrets || []) askSecrets(r);
|
|
453
742
|
});
|
|
454
743
|
es.addEventListener('line', (ev) => apply(JSON.parse(ev.data)));
|
|
744
|
+
es.addEventListener('progress', (ev) => apply(JSON.parse(ev.data)));
|
|
455
745
|
es.addEventListener('secrets', (ev) => askSecrets(JSON.parse(ev.data)));
|
|
456
746
|
es.addEventListener('finished', async (ev) => { apply(JSON.parse(ev.data)); await drawDemos(); await newest(); setTimeout(() => $('live').classList.remove('show'), 8000); });
|
|
457
747
|
es.onerror = () => { es.close(); setTimeout(watchActivity, 3000); };
|