@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 +5 -0
- package/README.md +0 -4
- package/package.json +1 -1
- package/src/embed.mjs +1 -7
- package/src/playback.mjs +3 -15
- package/src/player-css.mjs +2 -24
- package/src/player.mjs +22 -54
- package/src/serve.mjs +1 -1
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.
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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 };
|
package/src/player-css.mjs
CHANGED
|
@@ -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;
|
|
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-
|
|
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
|
|
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
|
-
},
|
|
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 })),
|
|
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
|
-
|
|
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
|
|
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:
|
|
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
|
-
|
|
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
|
-
|
|
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,
|
|
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,
|
|
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
|
|
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,
|
|
336
|
+
playFrom(ti, i + 1, 500);
|
|
361
337
|
};
|
|
362
|
-
if (e.kind === 'artifact') setMade((m) => [...m,
|
|
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,
|
|
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,
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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, {
|
|
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
|
-
|
|
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,
|
|
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
|
|
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,
|
|
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
|
}
|