@supersuit/session-player 0.4.0 → 0.5.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.
package/CHANGELOG.md CHANGED
@@ -1,5 +1,10 @@
1
1
  # Changelog
2
2
 
3
+ ## 0.5.0 (2026-10-11)
4
+
5
+ - **The player is the 0.2.3 experience again** (Gary, 2026-10-11: "really not loving the post-Wilson-merge case study widget, revert the UX"). Wilson's 0.3.0 changes and the 0.3.1 moments fix are reverted with `git revert`, so his commits stay in history: the Start card is back as it was (title, measured line, Start demo button, Autoplay choice; nothing plays on load; `start: 'auto'` opts in), Autoplay is on by default and playing the person when off, and device framing (Auto / iPhone / MacBook, the narrow-MacBook fallback, "Simulating a MacBook" on a phone, toasts) is unchanged from 0.2.3. Gone: `speed`, `highlights`, `moments`, `steps`, the highlights close card and the "Handled for" panel. An embed, tag or prop that still passes them is ignored rather than refused, so existing embeds keep working.
6
+ - Kept: the `session-player` name and CLI (and the `simulator` alias), and `recast`'s handling of a spec's `highlight` and `close.handled`, so scenarios recast for 0.3.x still build and their handled lines are still covered by `replace` and `forbid`. The player ignores both fields.
7
+
3
8
  ## 0.4.0 (2026-10-11)
4
9
 
5
10
  - **Renamed `@supersuit/session-player`** (Gary, 2026-10-11: it plays recorded sessions; case studies and the coming Helm demo are content it plays). Same code as 0.3.2. Swap the package name in your imports; the exports, the scenario format, `SimulatorPlayer` and `<supersuit-simulator>` are unchanged. The CLI is `session-player`, and `simulator` still works. The repository is SupersuitUp/session-player (GitHub redirects the old address).
package/README.md CHANGED
@@ -70,10 +70,6 @@ A page that runs modules can use the tag instead (it brings its own React throug
70
70
  <script type="module">import { defineSimulatorElement } from '@supersuit/session-player/embed'; defineSimulatorElement();</script>
71
71
  ```
72
72
 
73
- ## A highlights cut
74
-
75
- For a short page demo (about 40 seconds), pass `speed` (work between beats runs that many times faster), `highlights` (each turn shows only where it landed, its answer's first paragraph, with real reading time, and the panel ticks off what got handled), `moments` (one screen per moment instead of a scrolling log) and `steps` (how many steps a turn keeps). Same names as props, embed query switches (`?speed=3&highlights&moments&steps=1`) and tag attributes. A recast spec may name a turn's landing answer with `highlight: { "t6": 1 }`, and a scenario's `close.handled` lists what Freeda handled, in measured facts. The Start card still waits for the viewer.
76
-
77
73
  ## Device framing
78
74
 
79
75
  `device` is `auto` (the default), `iphone` or `macbook`: the `device` prop, `?device=` on the embed URL, or the `device` attribute on the tag. The viewer can also switch it on the demo (Auto / iPhone / MacBook).
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@supersuit/session-player",
3
- "version": "0.4.0",
3
+ "version": "0.5.0",
4
4
  "description": "Plays recorded agent sessions (case studies) on a simulated phone or MacBook: a scenario format, a converter from Claude Code transcripts, and a React player anyone can step into.",
5
5
  "type": "module",
6
6
  "bin": {
package/src/embed.mjs CHANGED
@@ -2,8 +2,7 @@
2
2
  // rather than in an iframe. The page supplies React through an import map (or a bundler).
3
3
  //
4
4
  // <supersuit-simulator src="/simulator/scenarios" scenario="low-tide-tapes"
5
- // artifacts="off" timeline="off" autoplay="off" height="620px" device="auto"
6
- // speed="3" highlights moments steps="1"></supersuit-simulator>
5
+ // artifacts="off" timeline="off" autoplay="off" height="620px" device="auto"></supersuit-simulator>
7
6
  // <script type="module">import { defineSimulatorElement } from '@supersuit/session-player/embed'; defineSimulatorElement();</script>
8
7
  //
9
8
  // `src` returns a JSON array of scenarios (or one scenario). Image paths in it resolve against
@@ -20,11 +19,6 @@ export function propsFromAttributes(a) {
20
19
  height: a.height || undefined,
21
20
  device: a.device || undefined,
22
21
  start: a.start || undefined,
23
- // The highlights cut (Wilson, 0.3.0): speed="3" highlights moments steps="1".
24
- speed: Number(a.speed) || undefined,
25
- highlights: a.highlights != null && a.highlights !== 'off',
26
- moments: a.moments != null && a.moments !== 'off',
27
- maxSteps: Number(a.steps) || undefined,
28
22
  };
29
23
  }
30
24
 
package/src/playback.mjs CHANGED
@@ -64,24 +64,13 @@ export function summarize(sc) {
64
64
  // One entry per turn: the command the viewer sends, then the events that follow it in the
65
65
  // order they really happened, each with a playback offset `t`. `length` is the turn's
66
66
  // playback time.
67
- // A reply's first paragraph is its answer (the house style leads with it), so a highlights
68
- // cut can show only that and give the viewer time to read it rather than a fixed beat.
69
- export function lead(text) {
70
- return String(text).split(/\n\s*\n/)[0].trim();
71
- }
72
- export function readMs(text) {
73
- const words = String(text).trim().split(/\s+/).filter(Boolean).length;
74
- return Math.max(900, Math.min(2800, 600 + 50 * words));
75
- }
76
-
77
- export function script(sc, { maxSteps = 5, speed = 1, leadOnly = false } = {}) {
67
+ export function script(sc, { maxSteps = 5 } = {}) {
78
68
  const byId = new Map((sc.artifacts ?? []).map((a) => [a.id, a]));
79
69
  return sc.turns.map((turn) => {
80
70
  const steps = condense(turn.steps, maxSteps);
81
71
  const items = [
82
72
  ...steps.map((s) => ({ kind: 'step', real: s.at ?? turn.at, step: s })),
83
- // A highlight shows where the turn landed: its last answer, not the progress notes before it.
84
- ...(leadOnly ? [turn.replies[turn.highlight ?? turn.replies.length - 1]].filter(Boolean) : turn.replies).map((r) => ({ kind: 'reply', real: r.at, reply: leadOnly ? { ...r, text: lead(r.text) } : r })),
73
+ ...turn.replies.map((r) => ({ kind: 'reply', real: r.at, reply: r })),
85
74
  ...turn.artifacts.map((id) => byId.get(id)).filter(Boolean).map((a) => ({ kind: 'artifact', real: a.at ?? turn.at, artifact: a })),
86
75
  ].map((e, i) => ({ ...e, i }));
87
76
  // Stable by real time; within one instant, steps before replies before artifacts.
@@ -90,8 +79,7 @@ export function script(sc, { maxSteps = 5, speed = 1, leadOnly = false } = {}) {
90
79
  let t = 0;
91
80
  const events = items.map(({ i, ...e }) => {
92
81
  const at = t;
93
- // speed shortens the work; reading time in a highlights cut is never sped up.
94
- t += e.kind === 'step' ? Math.round(pace(e.step.ms) / speed) : e.kind === 'reply' ? (leadOnly ? readMs(e.reply.text) : Math.round(900 / speed)) : Math.round(500 / speed);
82
+ t += e.kind === 'step' ? pace(e.step.ms) : e.kind === 'reply' ? 900 : 500;
95
83
  return { ...e, t: at };
96
84
  });
97
85
  return { id: turn.id, command: { text: turn.human.text, intervention: turn.intervention, images: turn.human.images }, events, length: t };
@@ -14,23 +14,6 @@ export const PLAYER_CSS = `
14
14
  .ss-titles b{font-family:Fraunces,Georgia,serif;font-size:16px;white-space:nowrap;overflow:hidden;text-overflow:ellipsis}
15
15
  .ss-titles small{color:#7a6d5a;white-space:nowrap;overflow:hidden;text-overflow:ellipsis}
16
16
  .ss-auto{display:flex;align-items:center;gap:4px;font-size:12px;color:#7a6d5a}
17
- .ss-moment{flex:1;min-height:0;display:flex;flex-direction:column;gap:10px;padding:16px 18px;overflow-y:auto;animation:ss-in .35s ease-out}
18
- .ss-moment>*{flex:none}
19
- .ss-mcount{font-size:11px;font-weight:700;letter-spacing:.06em;text-transform:uppercase;opacity:.6}
20
- .ss-mcmd{margin:0;font-size:14px;opacity:.75;overflow:hidden;display:-webkit-box;-webkit-line-clamp:2;-webkit-box-orient:vertical}
21
- .ss-mbig{margin:auto 0;font-size:clamp(16px,2.6cqi,22px);line-height:1.3;font-weight:600;overflow:hidden;display:-webkit-box;-webkit-line-clamp:5;-webkit-box-orient:vertical;animation:ss-in .4s ease-out}
22
- .ss-mwork{margin:auto 0;opacity:.6;font-size:15px}
23
- .ss-mticks{list-style:none;margin:0;padding:8px 0 0;border-top:1px dashed rgba(127,127,127,.35);display:flex;flex-wrap:wrap;gap:4px 14px;font-size:12.5px}
24
- .ss-mticks li{position:relative;padding-left:20px}
25
- .ss-mticks li::before{content:"\\2713";position:absolute;left:0;color:#2f8f5b;font-weight:800}
26
- .ss-mticks li.ss-new{font-weight:700;animation:ss-in .4s ease-out}
27
- .ss-tick{display:flex;gap:10px;align-items:flex-start;animation:ss-in .35s ease-out}
28
- .ss-check{flex:none;width:22px;height:22px;border-radius:50%;background:#2f8f5b;color:#fff;display:grid;place-items:center;font-size:13px;font-weight:700}
29
- .ss-handled{text-align:left;margin:10px 0 4px}
30
- .ss-handled small{display:block;font-weight:700;letter-spacing:.04em;text-transform:uppercase;font-size:11px;color:#7a6d5a;margin-bottom:4px}
31
- .ss-handled ul{list-style:none;margin:0;padding:0;display:flex;flex-direction:column;gap:4px}
32
- .ss-handled li{position:relative;padding-left:24px;opacity:0;animation:ss-in .3s ease-out forwards;animation-delay:calc(var(--i) * 180ms)}
33
- .ss-handled li::before{content:"\\2713";position:absolute;left:0;top:0;color:#2f8f5b;font-weight:800}
34
17
  .ss-counter{display:flex;justify-content:space-between;gap:8px;padding:8px 14px;background:var(--ss-ink);color:var(--ss-cream);font-size:13px;font-variant-numeric:tabular-nums}
35
18
  .ss-counter b{color:var(--ss-mustard)}
36
19
  .ss-log-wrap{position:relative;flex:1;min-height:0;display:flex;flex-direction:column}
@@ -153,10 +136,6 @@ export const PLAYER_CSS = `
153
136
  .ss-burst i{position:absolute;width:7px;height:7px;border-radius:2px;background:var(--ss-mustard);transform:rotate(calc(var(--i) * 25.7deg)) translateY(0);animation:ss-pop 1.1s ease-out forwards;opacity:0}
154
137
  .ss-burst i:nth-child(3n){background:var(--ss-tomato)}.ss-burst i:nth-child(3n+1){background:var(--ss-aqua)}
155
138
  .ss-kicker{display:block;font-size:11px;font-weight:700;letter-spacing:.08em;text-transform:uppercase;color:var(--ss-tomato);text-align:center}
156
- .ss-close-tight{padding:14px 16px 12px}
157
- .ss-close-tight h3{font-size:clamp(16px,2.6cqi,20px)!important;margin:2px 0 8px!important}
158
- .ss-close-tight .ss-handled li{font-size:12.5px;line-height:1.35}
159
- .ss-close-tight .ss-split{display:none}
160
139
  .ss-close h3{font-family:Fraunces,Georgia,serif;font-size:24px;margin:4px 0 14px;text-align:center}
161
140
  .ss-split{display:grid;grid-template-columns:1fr 1fr;gap:10px;margin-bottom:12px}
162
141
  .ss-split>div{background:#fff;border:1px solid #eadcbc;border-radius:14px;padding:10px;display:flex;flex-direction:column;gap:1px}
@@ -282,13 +261,12 @@ export const PLAYER_CSS = `
282
261
  .ss-starttitle{font-family:Fraunces,Georgia,serif;font-size:20px;line-height:1.25}
283
262
  .ss-startcard p{margin:0;color:#4a4238;font-size:14px}
284
263
  .ss-startcard small{color:#7a6d5a;font-size:12px}
285
- .ss-sim .ss-startbtn{margin-top:6px;display:flex;flex-direction:column;align-items:center;gap:8px;border:0;background:none;padding:0;color:inherit;font-weight:700;font-size:16px}
286
- .ss-startplay{width:76px;height:76px;border-radius:50%;background:var(--ss-tomato);color:#fff;display:grid;place-items:center;font-size:30px;padding-left:6px;box-sizing:border-box;box-shadow:0 8px 24px rgba(219,55,31,.35);animation:ss-pulse 1.6s infinite}
264
+ .ss-sim .ss-startbtn{margin-top:6px;border:0;border-radius:999px;background:var(--ss-tomato);color:#fff;font-weight:700;font-size:16px;padding:12px 26px;box-shadow:0 6px 18px rgba(219,55,31,.3)}
287
265
  .ss-sim .ss-startbtn:focus-visible{outline:3px solid var(--ss-petrol);outline-offset:2px}
288
266
  .ss-startauto{display:flex;align-items:center;gap:6px;font-size:14px;font-weight:600}
289
267
  .ss-term .ss-start{background:rgba(26,25,24,.86)}
290
268
  .ss-term .ss-startcard p{color:#cfc8bd}.ss-term .ss-startcard small{color:#8f887d}.ss-term .ss-starttitle,.ss-term .ss-startauto{color:#e6e1d9}
291
- .ss-term .ss-startplay{background:#d97757}.ss-term .ss-startbtn{color:#e6e1d9}
269
+ .ss-term .ss-startbtn{background:#d97757}
292
270
  .ss-devhint{font-size:inherit;opacity:.85}
293
271
  /* Toasts float over the top of the demo: no layout shift, gone after a few seconds. */
294
272
  .ss-toast{position:absolute;z-index:60;left:50%;top:40px;transform:translateX(-50%);width:max-content;max-width:min(92%,420px);pointer-events:none}
package/src/player.mjs CHANGED
@@ -41,7 +41,7 @@ function Reply({ text }) {
41
41
  // the composer, one question at a time, a tab per question. The viewer picks; the pick is
42
42
  // answered with what the person really chose, then the next question, then the replay goes
43
43
  // on along the real path (a recording has one path, so no branch is ever invented).
44
- export function Board({ ask, person, auto = false, onPick, images, onToast, speed = 1 }) {
44
+ export function Board({ ask, person, auto = false, onPick, images, onToast }) {
45
45
  const [idx, setIdx] = useState(0);
46
46
  const [picks, setPicks] = useState({});
47
47
  const q = ask.questions[idx];
@@ -53,12 +53,12 @@ export function Board({ ask, person, auto = false, onPick, images, onToast, spee
53
53
  if (!mine) return undefined;
54
54
  const id = setTimeout(() => {
55
55
  if (idx + 1 < ask.questions.length) setIdx(idx + 1); else onPick?.(picks);
56
- }, Math.round(1300 / speed));
56
+ }, 1300);
57
57
  return () => clearTimeout(id);
58
58
  }, [mine, idx]);
59
59
  useEffect(() => {
60
60
  if (!auto || mine) return undefined;
61
- const id = setTimeout(() => setPicks((p) => ({ ...p, [idx]: real })), Math.round(1200 / speed));
61
+ const id = setTimeout(() => setPicks((p) => ({ ...p, [idx]: real })), 1200);
62
62
  return () => clearTimeout(id);
63
63
  }, [auto, idx, mine]);
64
64
 
@@ -124,23 +124,7 @@ function StepLine({ step }) {
124
124
  h('span', { className: 'ss-dur' }, step.stalled ? 'timed out' : duration(step.ms)));
125
125
  }
126
126
 
127
- // One moment per screen (Wilson, 2026-10-11: "the scroll down ... doesn't give me the TL;DR"):
128
- // the person's line small, where Freeda landed large, and what is handled so far, with no log
129
- // to scroll. Everything shown is the recording's own text; nothing is summarised for it.
130
- export function Moment({ plan, turn, log, made, person, phase }) {
131
- const reply = [...log].reverse().find((l) => l.kind === 'reply' && l.key.startsWith(`${turn}-`));
132
- const done = made.filter((a) => a.kind === 'other');
133
- const here = done.find((a) => a.turnIndex === turn) ?? null;
134
- return h('div', { className: 'ss-moment', key: turn },
135
- h('div', { className: 'ss-mcount' }, `${turn + 1} of ${plan.length}`),
136
- h('p', { className: 'ss-mcmd' }, h('b', null, `${person}: `), plain(plan[turn].command.text).slice(0, 160)),
137
- reply ? h('p', { className: 'ss-mbig', key: reply.key }, plain(reply.reply.text))
138
- : h('p', { className: 'ss-mwork' }, phase === 'working' ? 'Freeda is on it\u2026' : ''),
139
- done.length > 0 && h('ul', { className: 'ss-mticks' }, done.map((a) => h('li', { key: a.id, className: a.id === here?.id ? 'ss-new' : '' }, a.title))));
140
- }
141
-
142
- function ArtifactCard({ a, tick = false }) {
143
- if (tick) return h('div', { className: 'ss-artifact ss-tick' }, h('span', { className: 'ss-check', 'aria-hidden': true }, '\u2713'), h('span', { className: 'ss-atitle' }, a.title));
127
+ function ArtifactCard({ a }) {
144
128
  return h('div', { className: 'ss-artifact' },
145
129
  h('span', { className: `ss-kind ss-${a.kind}` }, a.kind === 'link' ? 'page' : a.kind === 'other' ? 'done' : 'file'),
146
130
  h('span', { className: 'ss-atitle' }, a.title));
@@ -191,21 +175,16 @@ export function autoSendReady({ auto, phase, started }) {
191
175
  // measured. Freeda's work against the person's own attention, the things made, the edge
192
176
  // points the counter printed, and any chore the workflow catalog says is now off their hands.
193
177
  // Never "time saved": nobody measured the version where they did it by hand.
194
- export function CloseCard({ sc, person, onReplay, onBack, highlights = false }) {
178
+ export function CloseCard({ sc, person, onReplay, onBack }) {
195
179
  const t = summarize(sc);
196
180
  const close = sc.close ?? {};
197
181
  const edge = close.edge;
198
182
  const layers = edge ? ['fueled', 'encoded', 'built', 'retired'].filter((k) => edge.layers?.[k]) : [];
199
183
  return h('div', { className: 'ss-close-wrap' },
200
- h('div', { className: `ss-close${highlights ? ' ss-close-tight' : ''}`, role: 'dialog', 'aria-label': 'Session closed' },
184
+ h('div', { className: 'ss-close', role: 'dialog', 'aria-label': 'Session closed' },
201
185
  h('div', { className: 'ss-burst', 'aria-hidden': true }, Array.from({ length: 14 }, (_, i) => h('i', { key: i, style: { '--i': i } }))),
202
186
  h('span', { className: 'ss-kicker' }, 'Session closed'),
203
187
  h('h3', null, close.outcome ?? sc.title),
204
- // What the agent carried, in the session's own facts (close.handled): the weight that came
205
- // off the person, never a time-saved figure nobody measured.
206
- close.handled?.length > 0 && h('div', { className: 'ss-handled' },
207
- h('small', null, `What Freeda handled for ${person === 'You' ? 'you' : person}`),
208
- h('ul', null, close.handled.map((x, i) => h('li', { key: i, style: { '--i': i } }, x)))),
209
188
  h('div', { className: 'ss-split' },
210
189
  h('div', null, h('b', null, duration(t.agentMs)), h('small', null, 'of work by Freeda'), h('small', null, plural(t.steps, 'step'))),
211
190
  h('div', null, h('b', null, plural(t.commands, 'command')), h('small', null, `from ${person === 'You' ? 'you' : person}`),
@@ -214,7 +193,7 @@ export function CloseCard({ sc, person, onReplay, onBack, highlights = false })
214
193
  edge && edge.total > 0 && h('div', { className: 'ss-edge' },
215
194
  h('b', null, plural(edge.total, 'edge point')), ' earned',
216
195
  h('span', { className: 'ss-layers' }, layers.map((k) => h('span', { key: k }, `${edge.layers[k]} ${k}`)))),
217
- !highlights && sc.artifacts?.length > 0 && h('div', { className: 'ss-close-made' },
196
+ sc.artifacts?.length > 0 && h('div', { className: 'ss-close-made' },
218
197
  h('small', null, `${plural(sc.artifacts.length, 'thing')} made`),
219
198
  h('ul', null, sc.artifacts.slice(0, 6).map((a) => h('li', { key: a.id }, a.title)))),
220
199
  t.idleMs > 60000 && h('p', { className: 'ss-idle' }, `${duration(t.idleMs)} of idle time (hangs, timeouts, nobody at the keyboard) is not counted.`),
@@ -223,10 +202,7 @@ export function CloseCard({ sc, person, onReplay, onBack, highlights = false })
223
202
  onBack && h('button', { className: 'ss-chip ss-ghost', onClick: onBack }, 'Step into someone else'))));
224
203
  }
225
204
 
226
- // speed shortens the work between beats; highlights shows each turn's landing answer (first
227
- // paragraph) with reading time, and ticks off what got handled (Wilson, 2026-10-11: "make it 40
228
- // seconds", "I want the user to realize how much work, and how relieving it is").
229
- export function SimulatorPlayer({ scenario, scenarios, scenarioId, height, autoplay = true, artifacts = true, timeline = true, maxSteps = 5, device, viewer, speed = 1, highlights = false, moments = false, start: startMode }) {
205
+ export function SimulatorPlayer({ scenario, scenarios, scenarioId, height, autoplay = true, artifacts = true, timeline = true, maxSteps = 5, device, viewer, start: startMode }) {
230
206
  const list = scenarios ?? (scenario ? [scenario] : []);
231
207
  // An embed may name one scenario from a list and skip the picker.
232
208
  const start = scenarioId != null ? Math.max(0, list.findIndex((s) => s.id === scenarioId)) : list.length > 1 ? null : 0;
@@ -241,7 +217,7 @@ export function SimulatorPlayer({ scenario, scenarios, scenarioId, height, autop
241
217
  const sc = pick == null ? null : list[pick];
242
218
  return h('div', { className: 'ss-sim', style: height ? { '--ss-height': height } : undefined },
243
219
  h('style', null, PLAYER_CSS),
244
- sc ? h(Run, { key: sc.id, sc, autoplay, start: startMode, artifacts, timeline, maxSteps, speed, highlights, moments, mode, setMode, viewer: viewer ?? seen, onBack: list.length > 1 && scenarioId == null ? () => setPick(null) : null })
220
+ sc ? h(Run, { key: sc.id, sc, autoplay, start: startMode, artifacts, timeline, maxSteps, mode, setMode, viewer: viewer ?? seen, onBack: list.length > 1 && scenarioId == null ? () => setPick(null) : null })
245
221
  : h(Picker, { scenarios: list, onPick: setPick }));
246
222
  }
247
223
 
@@ -308,9 +284,9 @@ function TitleBar() {
308
284
  h('span', { className: 'ss-wtitle' }, 'Claude Code'));
309
285
  }
310
286
 
311
- function Run({ sc, autoplay, start, artifacts: showArtifacts, timeline, maxSteps, speed = 1, highlights = false, moments = false, onBack, mode = 'auto', setMode, viewer = 'desktop' }) {
287
+ function Run({ sc, autoplay, start, artifacts: showArtifacts, timeline, maxSteps, onBack, mode = 'auto', setMode, viewer = 'desktop' }) {
312
288
  const { frame, label } = resolveDevice({ mode, viewer, device: sc.device });
313
- const plan = useMemo(() => script(sc, { maxSteps, speed, leadOnly: highlights }), [sc, maxSteps, speed, highlights]);
289
+ const plan = useMemo(() => script(sc, { maxSteps }), [sc, maxSteps]);
314
290
  const totals = useMemo(() => summarize(sc), [sc]);
315
291
  const person = sc.persona?.name ?? 'You';
316
292
  const [turn, setTurn] = useState(0);
@@ -357,9 +333,9 @@ function Run({ sc, autoplay, start, artifacts: showArtifacts, timeline, maxSteps
357
333
  if (e.kind === 'step' && e.step.ask) resumeRef.current = (picks) => {
358
334
  setAnswers((a) => ({ ...a, [key]: picks }));
359
335
  setWaiting(null);
360
- playFrom(ti, i + 1, Math.round(500 / speed));
336
+ playFrom(ti, i + 1, 500);
361
337
  };
362
- if (e.kind === 'artifact') setMade((m) => [...m, { ...e.artifact, turnIndex: ti }]);
338
+ if (e.kind === 'artifact') setMade((m) => [...m, e.artifact]);
363
339
  if (e.kind === 'step' && !e.step.ask) setWork((w) => ({ ms: w.ms + e.step.ms, steps: w.steps + (e.step.count ?? 1) }));
364
340
  if (e.kind === 'step' && e.step.ask) { setWaiting(key); return; }
365
341
  const next = p.events[i + 1];
@@ -382,12 +358,12 @@ function Run({ sc, autoplay, start, artifacts: showArtifacts, timeline, maxSteps
382
358
  const sendNow = () => {
383
359
  setLog((l) => [...l, { kind: 'command', key: `c${turn}`, command: plan[turn].command }]);
384
360
  setPhase('working');
385
- playFrom(turn, 0, Math.round(450 / speed));
361
+ playFrom(turn, 0, 450);
386
362
  };
387
363
 
388
364
  useEffect(() => {
389
365
  if (!autoSendReady({ auto, phase, started })) return undefined;
390
- const id = setTimeout(send, Math.round(1400 / speed));
366
+ const id = setTimeout(send, 1400);
391
367
  return () => clearTimeout(id);
392
368
  });
393
369
 
@@ -398,10 +374,8 @@ function Run({ sc, autoplay, start, artifacts: showArtifacts, timeline, maxSteps
398
374
  };
399
375
 
400
376
  const next = plan[turn]?.command;
401
- // A highlights cut shows the load coming off: only what got handled, ticked as it lands.
402
- const shown = highlights ? made.filter((a) => a.kind === 'other') : made;
403
377
  const artifactList = h('div', { className: 'ss-artifact-list' },
404
- shown.length ? shown.map((a) => h(ArtifactCard, { key: a.id, a, tick: highlights })) : h('p', { className: 'ss-empty' }, highlights ? 'What gets handled will tick off here.' : 'What Freeda makes will collect here.'));
378
+ made.length ? made.map((a) => h(ArtifactCard, { key: a.id, a })) : h('p', { className: 'ss-empty' }, 'What Freeda makes will collect here.'));
405
379
 
406
380
  const phone = h('div', { className: 'ss-phone' },
407
381
  frame === 'iphone' && h(StatusBar),
@@ -414,7 +388,6 @@ function Run({ sc, autoplay, start, artifacts: showArtifacts, timeline, maxSteps
414
388
  h('span', null, work.ms ? [h('b', { key: 'd' }, duration(work.ms)), ` of work done for ${person}`] : 'No work yet'),
415
389
  h('span', null, `${work.steps} of ${totals.steps} steps`)),
416
390
  h('div', { className: 'ss-log-wrap' },
417
- moments && log.length > 0 ? h(Moment, { plan, turn, log, made, person, phase }) :
418
391
  h('div', { className: 'ss-log', ref: logRef },
419
392
  log.length === 0 && h('p', { className: 'ss-intro' }, `You are ${person}. Send the first command and watch what gets done.`),
420
393
  log.map((item) => {
@@ -426,8 +399,7 @@ function Run({ sc, autoplay, start, artifacts: showArtifacts, timeline, maxSteps
426
399
  }
427
400
  if (item.kind === 'step') return h(Step, { key: item.key, step: item.step, person, live: waiting === item.key, picks: answers[item.key] });
428
401
  if (item.kind === 'reply') return h(Reply, { key: item.key, text: item.reply.text });
429
- if (highlights && item.artifact.kind !== 'other') return null;
430
- return h('div', { key: item.key, className: 'ss-made' }, h('span', null, highlights ? '\u2713 handled' : 'made'), ' ', item.artifact.title);
402
+ return h('div', { key: item.key, className: 'ss-made' }, h('span', null, 'made'), ' ', item.artifact.title);
431
403
  }),
432
404
  phase === 'working' && !waiting && h('div', { className: 'ss-typing' }, h('i'), h('i'), h('i'))),
433
405
  showArtifacts && !waiting && h('button', { className: 'ss-tray-handle', onClick: () => setTray(!tray) }, `${made.length} made`, h('span', null, tray ? '⌄' : '⌃')),
@@ -436,12 +408,10 @@ function Run({ sc, autoplay, start, artifacts: showArtifacts, timeline, maxSteps
436
408
  h('div', { className: 'ss-startcard' },
437
409
  h('b', { className: 'ss-starttitle' }, sc.title),
438
410
  h('p', null, startLine(sc, person)),
439
- // One start (Wilson's big pulsing Play, Gary's Start demo card): the round Play is the
440
- // button, "Start demo" its words, Autoplay the choice beside it. Nothing else plays.
441
- h('button', { type: 'button', className: 'ss-startbtn', onClick: () => setStarted(true) }, h('span', { className: 'ss-startplay', 'aria-hidden': true }, '\u25B6'), 'Start demo'),
411
+ h('button', { type: 'button', className: 'ss-startbtn', onClick: () => setStarted(true) }, 'Start demo'),
442
412
  h('label', { className: 'ss-startauto' }, h('input', { type: 'checkbox', checked: auto, onChange: (e) => toggleAuto(e.target.checked) }), 'Autoplay'),
443
413
  h('small', null, auto ? 'It plays itself. Untick Autoplay to play the human.' : `You play ${person}: tap to send each message and make each call.`))),
444
- waiting ? h(Board, { speed, key: waiting, ask: log.find((l) => l.key === waiting).step.ask, images: log.find((l) => l.key === waiting).step.images, person, auto, onToast: auto ? undefined : showToast, onPick: (picks) => resumeRef.current?.(picks) }) :
414
+ waiting ? h(Board, { key: waiting, ask: log.find((l) => l.key === waiting).step.ask, images: log.find((l) => l.key === waiting).step.images, person, auto, onToast: auto ? undefined : showToast, onPick: (picks) => resumeRef.current?.(picks) }) :
445
415
  h('footer', { className: 'ss-composer' },
446
416
  phase === 'done'
447
417
  ? h('div', { className: 'ss-end' },
@@ -450,14 +420,12 @@ function Run({ sc, autoplay, start, artifacts: showArtifacts, timeline, maxSteps
450
420
  : h(Fragment, null,
451
421
  h('button', { className: 'ss-chip', disabled: phase !== 'ready', onClick: send },
452
422
  h('small', null, phase === 'working' ? 'Freeda is working…' : phase === 'saying' ? 'listening…' : auto ? 'sending on autoplay…' : `you are ${person}: tap to send, or tap the mic`),
453
- // In moments the line already sits on the moment's screen; repeating it here took the room
454
- // the answer needs in a short MacBook window.
455
- phase === 'saying' ? spoken : moments ? null : next && plain(next.text).slice(0, 140)),
423
+ phase === 'saying' ? spoken : next && plain(next.text).slice(0, 140)),
456
424
  h('button', { className: 'ss-send', disabled: phase !== 'ready', 'aria-label': 'Send', onClick: send }, '\u2191'),
457
425
  h('button', {
458
426
  className: `ss-mic${phase === 'saying' ? ' ss-live' : ''}`, disabled: phase !== 'ready', 'aria-label': 'Say it out loud', onClick: say,
459
427
  }, '\u{1F3A4}'))));
460
- const panel = showArtifacts && h('aside', { className: 'ss-panel' }, h('h3', null, `${highlights ? 'Handled' : 'Made'} for ${person}`, h('span', null, shown.length)), artifactList);
428
+ const panel = showArtifacts && h('aside', { className: 'ss-panel' }, h('h3', null, `Made for ${person}`, h('span', null, made.length)), artifactList);
461
429
  const bar = h('div', { className: 'ss-devicebar' },
462
430
  label ? h('span', { className: frame === 'macbook-compact' ? 'ss-simpill' : 'ss-simlabel' }, label) : h('span'),
463
431
  setMode && h(DeviceSwitch, { mode, setMode, hint: autoHint({ viewer, device: sc.device }), onToast: (m) => showToast(deviceToast({ mode: m, viewer, device: sc.device })) }));
@@ -489,5 +457,5 @@ function Run({ sc, autoplay, start, artifacts: showArtifacts, timeline, maxSteps
489
457
  device,
490
458
  inGrid && panel,
491
459
  timeline && h(Timeline, { sc, turnIndex: turn, person }),
492
- phase === 'done' && !closed && h(CloseCard, { sc, person, onReplay: restart, onBack, highlights })));
460
+ phase === 'done' && !closed && h(CloseCard, { sc, person, onReplay: restart, onBack })));
493
461
  }
package/src/serve.mjs CHANGED
@@ -31,7 +31,7 @@ import { SimulatorPlayer } from './src/player.mjs';
31
31
  const scenarios = await (await fetch('./scenarios.json')).json();
32
32
  const q = new URLSearchParams(location.search);
33
33
  createRoot(document.getElementById('root')).render(h(SimulatorPlayer, {
34
- scenarios, scenarioId: q.get('scenario') ?? undefined, height: q.get('height') ?? undefined, autoplay: q.get('autoplay') !== 'off', artifacts: q.get('artifacts') !== 'off', timeline: q.get('timeline') !== 'off', device: q.get('device') ?? undefined, start: q.get('start') ?? undefined, speed: Number(q.get('speed')) || undefined, highlights: q.has('highlights'), moments: q.has('moments'), maxSteps: Number(q.get('steps')) || undefined,
34
+ scenarios, scenarioId: q.get('scenario') ?? undefined, height: q.get('height') ?? undefined, autoplay: q.get('autoplay') !== 'off', artifacts: q.get('artifacts') !== 'off', timeline: q.get('timeline') !== 'off', device: q.get('device') ?? undefined, start: q.get('start') ?? undefined,
35
35
  }));
36
36
  </script></body></html>`;
37
37
  }