@alexkroman1/aai-cli 0.9.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.
Files changed (58) hide show
  1. package/LICENSE +21 -0
  2. package/dist/_build-p1HHkdon.mjs +132 -0
  3. package/dist/_discover-BzlCDVZ6.mjs +161 -0
  4. package/dist/_init-l_uoyFCN.mjs +82 -0
  5. package/dist/_link-BGXGFYWa.mjs +47 -0
  6. package/dist/_server-common-qLA1QU2C.mjs +36 -0
  7. package/dist/_ui-kJIua5L9.mjs +44 -0
  8. package/dist/cli.mjs +318 -0
  9. package/dist/deploy-KyNJaoP5.mjs +86 -0
  10. package/dist/dev-DBFvKyzk.mjs +39 -0
  11. package/dist/init-BWG5OrQa.mjs +65 -0
  12. package/dist/rag-BnCMnccf.mjs +173 -0
  13. package/dist/secret-CzeHIGzE.mjs +50 -0
  14. package/dist/start-C1qkhU4O.mjs +23 -0
  15. package/package.json +39 -0
  16. package/templates/_shared/.env.example +5 -0
  17. package/templates/_shared/CLAUDE.md +1051 -0
  18. package/templates/_shared/biome.json +32 -0
  19. package/templates/_shared/global.d.ts +1 -0
  20. package/templates/_shared/index.html +16 -0
  21. package/templates/_shared/package.json +23 -0
  22. package/templates/_shared/tsconfig.json +15 -0
  23. package/templates/code-interpreter/agent.ts +27 -0
  24. package/templates/code-interpreter/client.tsx +3 -0
  25. package/templates/css.d.ts +1 -0
  26. package/templates/dispatch-center/agent.ts +1227 -0
  27. package/templates/dispatch-center/client.tsx +505 -0
  28. package/templates/embedded-assets/agent.ts +48 -0
  29. package/templates/embedded-assets/client.tsx +3 -0
  30. package/templates/embedded-assets/knowledge.json +20 -0
  31. package/templates/health-assistant/agent.ts +160 -0
  32. package/templates/health-assistant/client.tsx +3 -0
  33. package/templates/infocom-adventure/agent.ts +164 -0
  34. package/templates/infocom-adventure/client.tsx +300 -0
  35. package/templates/math-buddy/agent.ts +21 -0
  36. package/templates/math-buddy/client.tsx +3 -0
  37. package/templates/memory-agent/agent.ts +20 -0
  38. package/templates/memory-agent/client.tsx +3 -0
  39. package/templates/night-owl/agent.ts +98 -0
  40. package/templates/night-owl/client.tsx +12 -0
  41. package/templates/personal-finance/agent.ts +26 -0
  42. package/templates/personal-finance/client.tsx +3 -0
  43. package/templates/pizza-ordering/agent.ts +218 -0
  44. package/templates/pizza-ordering/client.tsx +264 -0
  45. package/templates/simple/agent.ts +6 -0
  46. package/templates/simple/client.tsx +3 -0
  47. package/templates/smart-research/agent.ts +164 -0
  48. package/templates/smart-research/client.tsx +3 -0
  49. package/templates/solo-rpg/agent.ts +1244 -0
  50. package/templates/solo-rpg/client.tsx +698 -0
  51. package/templates/support/README.md +62 -0
  52. package/templates/support/agent.ts +19 -0
  53. package/templates/support/client.tsx +3 -0
  54. package/templates/travel-concierge/agent.ts +29 -0
  55. package/templates/travel-concierge/client.tsx +3 -0
  56. package/templates/tsconfig.json +1 -0
  57. package/templates/web-researcher/agent.ts +17 -0
  58. package/templates/web-researcher/client.tsx +3 -0
@@ -0,0 +1,698 @@
1
+ import "@alexkroman1/aai-ui/styles.css";
2
+ import { useState } from "preact/hooks";
3
+ import {
4
+ ChatView,
5
+ SidebarLayout,
6
+ StartScreen,
7
+ mount,
8
+ useToolResult,
9
+ } from "@alexkroman1/aai-ui";
10
+
11
+ // ── Types ────────────────────────────────────────────────────────────────────
12
+
13
+ type Disposition = "hostile" | "distrustful" | "neutral" | "friendly" | "loyal";
14
+
15
+ interface NPC {
16
+ id: string;
17
+ name: string;
18
+ description: string;
19
+ disposition: Disposition;
20
+ bond: number;
21
+ agenda: string;
22
+ status: "active" | "background" | "deceased";
23
+ }
24
+
25
+ interface ClockData {
26
+ id: string;
27
+ name: string;
28
+ clockType: "threat" | "progress" | "scheme";
29
+ segments: number;
30
+ filled: number;
31
+ triggerDescription: string;
32
+ }
33
+
34
+ interface StoryInfo {
35
+ structureType: string;
36
+ currentAct: number;
37
+ totalActs: number;
38
+ centralConflict: string;
39
+ thematicThread: string;
40
+ storyComplete: boolean;
41
+ currentPhase: string;
42
+ }
43
+
44
+ interface SessionLogEntry {
45
+ scene: number;
46
+ summary: string;
47
+ location: string;
48
+ }
49
+
50
+ interface GameState {
51
+ initialized: boolean;
52
+ phase: string;
53
+ settingGenre: string;
54
+ settingTone: string;
55
+ settingArchetype: string;
56
+ settingDescription: string;
57
+ playerName: string;
58
+ characterConcept: string;
59
+ edge: number; heart: number; iron: number; shadow: number; wits: number;
60
+ health: number; spirit: number; supply: number;
61
+ momentum: number; maxMomentum: number;
62
+ currentLocation: string;
63
+ currentSceneContext: string;
64
+ timeOfDay: string;
65
+ chaosFactor: number;
66
+ crisisMode: boolean;
67
+ gameOver: boolean;
68
+ sceneCount: number;
69
+ npcs: NPC[];
70
+ clocks: ClockData[];
71
+ storyBlueprint: StoryInfo | null;
72
+ kidMode: boolean;
73
+ sessionLog: SessionLogEntry[];
74
+ }
75
+
76
+ const INITIAL: GameState = {
77
+ initialized: false,
78
+ phase: "genre",
79
+ settingGenre: "", settingTone: "", settingArchetype: "",
80
+ settingDescription: "",
81
+ playerName: "", characterConcept: "",
82
+ edge: 1, heart: 1, iron: 1, shadow: 1, wits: 1,
83
+ health: 5, spirit: 5, supply: 5,
84
+ momentum: 2, maxMomentum: 10,
85
+ currentLocation: "", currentSceneContext: "", timeOfDay: "",
86
+ chaosFactor: 5, crisisMode: false, gameOver: false,
87
+ sceneCount: 0,
88
+ npcs: [], clocks: [],
89
+ storyBlueprint: null,
90
+ kidMode: false,
91
+ sessionLog: [],
92
+ };
93
+
94
+ // ── Color Palette ────────────────────────────────────────────────────────────
95
+ const C = {
96
+ bg: "#0a0a0c",
97
+ surface: "#0f0f12",
98
+ surfaceLight: "#16161b",
99
+ border: "#1e1e26",
100
+ borderLight: "#2a2a36",
101
+ accent: "#c9a84c", // gold
102
+ accentDim: "#8a7232",
103
+ accentGlow: "rgba(201,168,76,0.15)",
104
+ text: "#e0dcd0",
105
+ textMuted: "rgba(224,220,208,0.5)",
106
+ textDim: "rgba(224,220,208,0.25)",
107
+ health: "#8b3030",
108
+ healthBright: "#c44040",
109
+ spirit: "#3a5a8a",
110
+ spiritBright: "#5a8acd",
111
+ supply: "#4a6a3a",
112
+ supplyBright: "#6a9a4a",
113
+ chaos: {
114
+ low: "#3a6a3a",
115
+ mid: "#8a7a3a",
116
+ high: "#8a4a2a",
117
+ critical: "#8b2020",
118
+ },
119
+ disposition: {
120
+ hostile: "#c44040",
121
+ distrustful: "#c47a30",
122
+ neutral: "#888888",
123
+ friendly: "#4a9a4a",
124
+ loyal: "#c9a84c",
125
+ },
126
+ threat: "#8b2020",
127
+ progress: "#3a7a9a",
128
+ scheme: "#7a4a8a",
129
+ };
130
+
131
+ // ── Disposition Icons ────────────────────────────────────────────────────────
132
+ const DISP_ICON: Record<Disposition, string> = {
133
+ hostile: "\u2620", distrustful: "\u26A0", neutral: "\u25CB",
134
+ friendly: "\u2665", loyal: "\u2726",
135
+ };
136
+
137
+ // ── Time Labels ──────────────────────────────────────────────────────────────
138
+ const TIME_LABELS: Record<string, string> = {
139
+ early_morning: "Dawn", morning: "Morning", midday: "Midday",
140
+ afternoon: "Afternoon", evening: "Dusk", late_evening: "Twilight",
141
+ night: "Night", deep_night: "Witching Hour",
142
+ };
143
+
144
+ // ── Genre Labels ─────────────────────────────────────────────────────────────
145
+ const GENRE_LABELS: Record<string, string> = {
146
+ dark_fantasy: "Dark Fantasy", high_fantasy: "High Fantasy",
147
+ science_fiction: "Sci-Fi", horror_mystery: "Horror / Mystery",
148
+ steampunk: "Steampunk", cyberpunk: "Cyberpunk",
149
+ urban_fantasy: "Urban Fantasy", victorian_crime: "Victorian Crime",
150
+ historical_roman: "Historical", fairy_tale: "Fairy Tale",
151
+ slice_of_life_90s: "Slice of Life", outdoor_survival: "Survival",
152
+ };
153
+
154
+ // ── Phase Labels ─────────────────────────────────────────────────────────────
155
+ const PHASE_LABELS: Record<string, string> = {
156
+ setup: "Act I", confrontation: "Act II", climax: "Act III",
157
+ ki_introduction: "Ki", sho_development: "Sho",
158
+ ten_twist: "Ten", ketsu_resolution: "Ketsu",
159
+ };
160
+
161
+ // ── Components ───────────────────────────────────────────────────────────────
162
+
163
+ function ResourceBar({ label, current, max, color, colorBright, icon }: {
164
+ label: string; current: number; max: number; color: string; colorBright: string; icon: string;
165
+ }) {
166
+ const pips = [];
167
+ for (let i = 0; i < max; i++) {
168
+ const filled = i < current;
169
+ pips.push(
170
+ <div
171
+ key={i}
172
+ style={{
173
+ width: "18px", height: "10px", borderRadius: "2px",
174
+ background: filled
175
+ ? `linear-gradient(135deg, ${color}, ${colorBright})`
176
+ : "rgba(255,255,255,0.03)",
177
+ border: `1px solid ${filled ? colorBright : "rgba(255,255,255,0.06)"}`,
178
+ boxShadow: filled ? `0 0 4px ${color}66` : "none",
179
+ transition: "all 0.4s ease",
180
+ }}
181
+ />,
182
+ );
183
+ }
184
+ return (
185
+ <div style={{ marginBottom: "8px" }}>
186
+ <div style={{ display: "flex", justifyContent: "space-between", alignItems: "center", marginBottom: "3px" }}>
187
+ <span style={{ fontSize: "10px", color: C.textDim, letterSpacing: "0.05em" }}>
188
+ {icon} {label}
189
+ </span>
190
+ <span style={{ fontSize: "11px", fontWeight: 700, color: current > 0 ? colorBright : C.threat }}>
191
+ {current}
192
+ </span>
193
+ </div>
194
+ <div style={{ display: "flex", gap: "2px" }}>{pips}</div>
195
+ </div>
196
+ );
197
+ }
198
+
199
+ function MomentumTrack({ momentum, max }: { momentum: number; max: number }) {
200
+ const range: number[] = [];
201
+ for (let i = -6; i <= 10; i++) range.push(i);
202
+ return (
203
+ <div style={{ marginBottom: "8px" }}>
204
+ <div style={{ display: "flex", justifyContent: "space-between", alignItems: "center", marginBottom: "3px" }}>
205
+ <span style={{ fontSize: "10px", color: C.textDim, letterSpacing: "0.05em" }}>Momentum</span>
206
+ <span style={{
207
+ fontSize: "11px", fontWeight: 700,
208
+ color: momentum > 0 ? C.spiritBright : momentum < 0 ? C.healthBright : C.textMuted,
209
+ }}>
210
+ {momentum > 0 ? "+" : ""}{momentum}
211
+ </span>
212
+ </div>
213
+ <div style={{ display: "flex", gap: "1px" }}>
214
+ {range.map((v) => (
215
+ <div
216
+ key={v}
217
+ style={{
218
+ flex: 1, height: "6px", borderRadius: "1px",
219
+ background:
220
+ v > max ? "rgba(255,255,255,0.01)"
221
+ : v <= momentum && v > 0 ? C.spiritBright
222
+ : v >= momentum && v < 0 ? C.healthBright
223
+ : v === 0 ? "rgba(255,255,255,0.12)"
224
+ : "rgba(255,255,255,0.03)",
225
+ boxShadow:
226
+ (v <= momentum && v > 0) ? `0 0 3px ${C.spirit}` :
227
+ (v >= momentum && v < 0) ? `0 0 3px ${C.health}` : "none",
228
+ transition: "all 0.3s ease",
229
+ }}
230
+ />
231
+ ))}
232
+ </div>
233
+ <div style={{ display: "flex", justifyContent: "space-between", marginTop: "1px" }}>
234
+ <span style={{ fontSize: "7px", color: C.textDim }}>-6</span>
235
+ <span style={{ fontSize: "7px", color: C.textDim }}>0</span>
236
+ <span style={{ fontSize: "7px", color: C.textDim }}>+10</span>
237
+ </div>
238
+ </div>
239
+ );
240
+ }
241
+
242
+ function ChaosGauge({ chaos }: { chaos: number }) {
243
+ const pct = ((chaos - 3) / 6) * 100;
244
+ const color = chaos <= 4 ? C.chaos.low : chaos <= 6 ? C.chaos.mid : chaos <= 8 ? C.chaos.high : C.chaos.critical;
245
+ const label = chaos <= 4 ? "Calm" : chaos <= 6 ? "Tense" : chaos <= 8 ? "Volatile" : "Critical";
246
+ return (
247
+ <div style={{ marginBottom: "8px" }}>
248
+ <div style={{ display: "flex", justifyContent: "space-between", alignItems: "center", marginBottom: "3px" }}>
249
+ <span style={{ fontSize: "10px", color: C.textDim, letterSpacing: "0.05em" }}>Chaos</span>
250
+ <span style={{ fontSize: "9px", fontWeight: 600, color, textTransform: "uppercase", letterSpacing: "0.08em" }}>
251
+ {label} ({chaos})
252
+ </span>
253
+ </div>
254
+ <div style={{ height: "4px", borderRadius: "2px", background: "rgba(255,255,255,0.04)", overflow: "hidden" }}>
255
+ <div style={{
256
+ height: "100%", width: `${pct}%`, borderRadius: "2px",
257
+ background: `linear-gradient(90deg, ${C.chaos.low}, ${color})`,
258
+ boxShadow: `0 0 6px ${color}66`,
259
+ transition: "width 0.5s ease, background 0.5s ease",
260
+ }} />
261
+ </div>
262
+ </div>
263
+ );
264
+ }
265
+
266
+ function StatPip({ label, value }: { label: string; value: number }) {
267
+ return (
268
+ <div style={{ textAlign: "center" }}>
269
+ <div style={{ fontSize: "8px", color: C.textDim, textTransform: "uppercase", letterSpacing: "0.1em" }}>
270
+ {label}
271
+ </div>
272
+ <div style={{
273
+ fontSize: "18px", fontWeight: 700, color: C.accent, lineHeight: 1,
274
+ textShadow: `0 0 8px ${C.accentGlow}`,
275
+ }}>
276
+ {value}
277
+ </div>
278
+ </div>
279
+ );
280
+ }
281
+
282
+ function ClockDisplay({ clock }: { clock: ClockData }) {
283
+ const typeColor = clock.clockType === "threat" ? C.threat :
284
+ clock.clockType === "progress" ? C.progress : C.scheme;
285
+ const segments = [];
286
+ for (let i = 0; i < clock.segments; i++) {
287
+ segments.push(
288
+ <div
289
+ key={i}
290
+ style={{
291
+ width: "10px", height: "10px", borderRadius: "50%",
292
+ background: i < clock.filled
293
+ ? typeColor
294
+ : "rgba(255,255,255,0.04)",
295
+ border: `1px solid ${i < clock.filled ? typeColor : "rgba(255,255,255,0.08)"}`,
296
+ boxShadow: i < clock.filled ? `0 0 4px ${typeColor}66` : "none",
297
+ transition: "all 0.3s ease",
298
+ }}
299
+ />,
300
+ );
301
+ }
302
+ const isFull = clock.filled >= clock.segments;
303
+ return (
304
+ <div style={{
305
+ marginBottom: "8px", padding: "6px 8px",
306
+ borderRadius: "4px", background: "rgba(255,255,255,0.015)",
307
+ border: `1px solid ${isFull ? typeColor : "rgba(255,255,255,0.04)"}`,
308
+ opacity: isFull ? 0.5 : 1,
309
+ }}>
310
+ <div style={{ display: "flex", justifyContent: "space-between", alignItems: "center", marginBottom: "4px" }}>
311
+ <span style={{ fontSize: "10px", fontWeight: 600, color: C.text }}>{clock.name}</span>
312
+ <span style={{
313
+ fontSize: "8px", padding: "1px 4px", borderRadius: "2px",
314
+ background: `${typeColor}22`, color: typeColor, textTransform: "uppercase",
315
+ letterSpacing: "0.06em", border: `1px solid ${typeColor}44`,
316
+ }}>
317
+ {clock.clockType}
318
+ </span>
319
+ </div>
320
+ <div style={{ display: "flex", gap: "3px", flexWrap: "wrap" }}>{segments}</div>
321
+ </div>
322
+ );
323
+ }
324
+
325
+ function NpcCard({ npc }: { npc: NPC }) {
326
+ const dispColor = C.disposition[npc.disposition] || C.textMuted;
327
+ const icon = DISP_ICON[npc.disposition] || "\u25CB";
328
+ return (
329
+ <div style={{
330
+ marginBottom: "6px", padding: "6px 8px",
331
+ borderRadius: "4px", background: "rgba(255,255,255,0.015)",
332
+ borderLeft: `2px solid ${dispColor}`,
333
+ }}>
334
+ <div style={{ display: "flex", justifyContent: "space-between", alignItems: "center" }}>
335
+ <span style={{ fontSize: "11px", fontWeight: 600, color: C.text }}>{npc.name}</span>
336
+ <span style={{ fontSize: "10px", color: dispColor }}>{icon}</span>
337
+ </div>
338
+ {npc.agenda && (
339
+ <div style={{ fontSize: "9px", color: C.textMuted, marginTop: "2px", fontStyle: "italic" }}>
340
+ {npc.agenda}
341
+ </div>
342
+ )}
343
+ <div style={{ display: "flex", gap: "8px", marginTop: "3px" }}>
344
+ <span style={{ fontSize: "8px", color: C.textDim }}>
345
+ {npc.disposition}
346
+ </span>
347
+ {npc.bond !== 0 && (
348
+ <span style={{ fontSize: "8px", color: npc.bond > 0 ? C.supplyBright : C.healthBright }}>
349
+ bond {npc.bond > 0 ? "+" : ""}{npc.bond}
350
+ </span>
351
+ )}
352
+ </div>
353
+ </div>
354
+ );
355
+ }
356
+
357
+ function StoryArc({ story }: { story: StoryInfo }) {
358
+ const pct = story.totalActs > 0 ? ((story.currentAct - 1) / story.totalActs) * 100 : 0;
359
+ const phaseLabel = PHASE_LABELS[story.currentPhase] || story.currentPhase;
360
+ return (
361
+ <div style={{ marginBottom: "8px" }}>
362
+ <div style={{ display: "flex", justifyContent: "space-between", alignItems: "center", marginBottom: "3px" }}>
363
+ <span style={{ fontSize: "10px", color: C.textDim, letterSpacing: "0.05em" }}>Story Arc</span>
364
+ <span style={{ fontSize: "9px", color: C.accent }}>
365
+ {phaseLabel} ({story.currentAct}/{story.totalActs})
366
+ </span>
367
+ </div>
368
+ <div style={{ height: "3px", borderRadius: "2px", background: "rgba(255,255,255,0.04)", overflow: "hidden" }}>
369
+ <div style={{
370
+ height: "100%", width: `${pct}%`, borderRadius: "2px",
371
+ background: `linear-gradient(90deg, ${C.accentDim}, ${C.accent})`,
372
+ transition: "width 0.5s ease",
373
+ }} />
374
+ </div>
375
+ {story.storyComplete && (
376
+ <div style={{
377
+ fontSize: "8px", color: C.accent, textTransform: "uppercase",
378
+ letterSpacing: "0.1em", marginTop: "3px", textAlign: "center",
379
+ }}>
380
+ Story Complete
381
+ </div>
382
+ )}
383
+ </div>
384
+ );
385
+ }
386
+
387
+ // ── Sidebar ──────────────────────────────────────────────────────────────────
388
+
389
+ function Sidebar({ game }: { game: GameState }) {
390
+ return (
391
+ <div style={{
392
+ height: "100%", overflowY: "auto", background: C.bg,
393
+ fontFamily: "'Crimson Text', 'Georgia', serif",
394
+ }}>
395
+ <style>
396
+ {`
397
+ @import url('https://fonts.googleapis.com/css2?family=Crimson+Text:ital,wght@0,400;0,600;0,700;1,400&display=swap');
398
+ .et-section { border-bottom: 1px solid ${C.border}; padding: 10px 12px; }
399
+ .et-section:last-child { border-bottom: none; }
400
+ .et-label {
401
+ font-size: 9px; text-transform: uppercase; letter-spacing: 0.14em;
402
+ color: ${C.textDim}; margin-bottom: 6px; font-family: sans-serif;
403
+ }
404
+ .et-gold { color: ${C.accent}; text-shadow: 0 0 8px ${C.accentGlow}; }
405
+ .et-crisis-pulse { animation: crisisPulse 2s ease-in-out infinite; }
406
+ @keyframes crisisPulse {
407
+ 0%,100% { opacity: 0.6; } 50% { opacity: 1; }
408
+ }
409
+ .et-scroll::-webkit-scrollbar { width: 3px; }
410
+ .et-scroll::-webkit-scrollbar-thumb { background: ${C.borderLight}; border-radius: 3px; }
411
+ .et-scroll::-webkit-scrollbar-track { background: transparent; }
412
+ `}
413
+ </style>
414
+
415
+ {/* Header */}
416
+ <div class="et-section" style={{ textAlign: "center", paddingTop: "16px", paddingBottom: "12px" }}>
417
+ <div style={{
418
+ fontSize: "8px", letterSpacing: "0.3em", color: C.textDim,
419
+ textTransform: "uppercase", fontFamily: "sans-serif",
420
+ }}>
421
+ Solo RPG
422
+ </div>
423
+ {game.initialized ? (
424
+ <>
425
+ <div class="et-gold" style={{ fontSize: "16px", fontWeight: 700, marginTop: "4px" }}>
426
+ {game.playerName}
427
+ </div>
428
+ {game.characterConcept && (
429
+ <div style={{ fontSize: "11px", color: C.textMuted, fontStyle: "italic", marginTop: "2px" }}>
430
+ {game.characterConcept}
431
+ </div>
432
+ )}
433
+ {game.settingGenre && (
434
+ <div style={{
435
+ fontSize: "8px", color: C.accentDim, marginTop: "4px",
436
+ textTransform: "uppercase", letterSpacing: "0.1em", fontFamily: "sans-serif",
437
+ }}>
438
+ {GENRE_LABELS[game.settingGenre] || game.settingGenre}
439
+ </div>
440
+ )}
441
+ </>
442
+ ) : (
443
+ <div style={{ fontSize: "11px", color: C.textDim, marginTop: "8px", fontStyle: "italic" }}>
444
+ Creating your story...
445
+ </div>
446
+ )}
447
+ </div>
448
+
449
+ {/* Pre-init placeholder */}
450
+ {!game.initialized && (
451
+ <div class="et-section" style={{ textAlign: "center", padding: "32px 12px" }}>
452
+ <div style={{ fontSize: "36px", opacity: 0.08 }}>{"\u2726"}</div>
453
+ <div style={{
454
+ fontSize: "10px", color: C.textDim, marginTop: "10px",
455
+ lineHeight: 1.7, fontStyle: "italic",
456
+ }}>
457
+ Choose your world.<br />Shape your character.<br />Begin your tale.
458
+ </div>
459
+ </div>
460
+ )}
461
+
462
+ {game.initialized && (
463
+ <>
464
+ {/* Crisis / Game Over Banner */}
465
+ {(game.crisisMode || game.gameOver) && (
466
+ <div class="et-section et-crisis-pulse" style={{
467
+ textAlign: "center", padding: "8px 12px",
468
+ background: `${C.threat}15`, borderBottom: `1px solid ${C.threat}33`,
469
+ }}>
470
+ <div style={{
471
+ fontSize: "10px", fontWeight: 700, color: C.healthBright,
472
+ textTransform: "uppercase", letterSpacing: "0.15em", fontFamily: "sans-serif",
473
+ }}>
474
+ {game.gameOver ? (game.kidMode ? "In Trouble" : "Finale") :
475
+ (game.kidMode ? "In Trouble" : "Crisis")}
476
+ </div>
477
+ </div>
478
+ )}
479
+
480
+ {/* Stats */}
481
+ <div class="et-section">
482
+ <div class="et-label">Attributes</div>
483
+ <div style={{ display: "flex", justifyContent: "space-between", padding: "0 4px" }}>
484
+ <StatPip label="Edge" value={game.edge} />
485
+ <StatPip label="Heart" value={game.heart} />
486
+ <StatPip label="Iron" value={game.iron} />
487
+ <StatPip label="Shadow" value={game.shadow} />
488
+ <StatPip label="Wits" value={game.wits} />
489
+ </div>
490
+ </div>
491
+
492
+ {/* Resources */}
493
+ <div class="et-section">
494
+ <ResourceBar label="Health" current={game.health} max={5} color={C.health} colorBright={C.healthBright} icon={"\u2665"} />
495
+ <ResourceBar label="Spirit" current={game.spirit} max={5} color={C.spirit} colorBright={C.spiritBright} icon={"\u25C6"} />
496
+ <ResourceBar label="Supply" current={game.supply} max={5} color={C.supply} colorBright={C.supplyBright} icon={"\u25A0"} />
497
+ <MomentumTrack momentum={game.momentum} max={game.maxMomentum} />
498
+ <ChaosGauge chaos={game.chaosFactor} />
499
+ </div>
500
+
501
+ {/* Location & Time */}
502
+ <div class="et-section">
503
+ <div class="et-label">Location</div>
504
+ <div class="et-gold" style={{ fontSize: "13px", fontWeight: 600 }}>
505
+ {game.currentLocation || "Unknown"}
506
+ </div>
507
+ {game.currentSceneContext && (
508
+ <div style={{ fontSize: "10px", color: C.textMuted, marginTop: "3px", fontStyle: "italic", lineHeight: 1.4 }}>
509
+ {game.currentSceneContext}
510
+ </div>
511
+ )}
512
+ {game.timeOfDay && (
513
+ <div style={{
514
+ fontSize: "9px", color: C.accentDim, marginTop: "4px",
515
+ textTransform: "uppercase", letterSpacing: "0.08em", fontFamily: "sans-serif",
516
+ }}>
517
+ {TIME_LABELS[game.timeOfDay] || game.timeOfDay}
518
+ </div>
519
+ )}
520
+ </div>
521
+
522
+ {/* Story Arc */}
523
+ {game.storyBlueprint && (
524
+ <div class="et-section">
525
+ <StoryArc story={game.storyBlueprint} />
526
+ </div>
527
+ )}
528
+
529
+ {/* Clocks */}
530
+ {game.clocks.length > 0 && (
531
+ <div class="et-section">
532
+ <div class="et-label">Clocks</div>
533
+ {game.clocks.map((clock) => (
534
+ <ClockDisplay key={clock.id || clock.name} clock={clock} />
535
+ ))}
536
+ </div>
537
+ )}
538
+
539
+ {/* NPCs */}
540
+ {game.npcs.filter(n => n.status !== "deceased").length > 0 && (
541
+ <div class="et-section">
542
+ <div class="et-label">Characters</div>
543
+ {game.npcs.filter(n => n.status === "active").map((npc) => (
544
+ <NpcCard key={npc.id} npc={npc} />
545
+ ))}
546
+ {game.npcs.filter(n => n.status === "background").length > 0 && (
547
+ <>
548
+ <div style={{
549
+ fontSize: "8px", color: C.textDim, textTransform: "uppercase",
550
+ letterSpacing: "0.1em", margin: "6px 0 4px", fontFamily: "sans-serif",
551
+ }}>
552
+ Known
553
+ </div>
554
+ {game.npcs.filter(n => n.status === "background").map((npc) => (
555
+ <div key={npc.id} style={{
556
+ fontSize: "10px", color: C.textMuted, marginBottom: "2px",
557
+ paddingLeft: "8px", borderLeft: `1px solid ${C.border}`,
558
+ }}>
559
+ {npc.name}
560
+ <span style={{ fontSize: "8px", color: C.textDim, marginLeft: "4px" }}>
561
+ {npc.disposition}
562
+ </span>
563
+ </div>
564
+ ))}
565
+ </>
566
+ )}
567
+ </div>
568
+ )}
569
+
570
+ {/* Session Log */}
571
+ {game.sessionLog.length > 0 && (
572
+ <div class="et-section">
573
+ <div class="et-label">Chronicle</div>
574
+ {game.sessionLog.slice(-5).map((entry, i) => (
575
+ <div
576
+ key={i}
577
+ style={{
578
+ fontSize: "10px", color: C.textDim,
579
+ fontStyle: "italic", lineHeight: 1.5, marginBottom: "4px",
580
+ paddingLeft: "8px", borderLeft: `1px solid ${C.border}`,
581
+ }}
582
+ >
583
+ <span style={{ color: C.textMuted, fontStyle: "normal", fontSize: "8px" }}>
584
+ {entry.scene}.{" "}
585
+ </span>
586
+ {entry.summary}
587
+ </div>
588
+ ))}
589
+ </div>
590
+ )}
591
+
592
+ {/* Scene Counter */}
593
+ <div class="et-section" style={{ textAlign: "center", padding: "8px 12px" }}>
594
+ <span style={{ fontSize: "8px", color: C.textDim, letterSpacing: "0.15em", fontFamily: "sans-serif" }}>
595
+ {game.kidMode ? "\u2726 " : ""}SCENE {game.sceneCount}{game.kidMode ? " \u2726" : ""}
596
+ </span>
597
+ </div>
598
+ </>
599
+ )}
600
+ </div>
601
+ );
602
+ }
603
+
604
+ // ── App ──────────────────────────────────────────────────────────────────────
605
+
606
+ function SoloRPGApp() {
607
+ const [game, setGame] = useState<GameState>(structuredClone(INITIAL));
608
+
609
+ // Merge a full-state result into the game
610
+ const mergeState = (result: any, prev: GameState): GameState => ({
611
+ initialized: result.initialized ?? prev.initialized,
612
+ phase: result.phase ?? prev.phase,
613
+ settingGenre: result.settingGenre ?? prev.settingGenre,
614
+ settingTone: result.settingTone ?? prev.settingTone,
615
+ settingArchetype: result.settingArchetype ?? prev.settingArchetype,
616
+ settingDescription: result.settingDescription ?? prev.settingDescription,
617
+ playerName: result.playerName ?? prev.playerName,
618
+ characterConcept: result.characterConcept ?? prev.characterConcept,
619
+ edge: result.edge ?? result.stats?.edge ?? prev.edge,
620
+ heart: result.heart ?? result.stats?.heart ?? prev.heart,
621
+ iron: result.iron ?? result.stats?.iron ?? prev.iron,
622
+ shadow: result.shadow ?? result.stats?.shadow ?? prev.shadow,
623
+ wits: result.wits ?? result.stats?.wits ?? prev.wits,
624
+ health: result.health ?? prev.health,
625
+ spirit: result.spirit ?? prev.spirit,
626
+ supply: result.supply ?? prev.supply,
627
+ momentum: result.momentum ?? prev.momentum,
628
+ maxMomentum: result.maxMomentum ?? prev.maxMomentum,
629
+ currentLocation: result.currentLocation ?? prev.currentLocation,
630
+ currentSceneContext: result.currentSceneContext ?? prev.currentSceneContext,
631
+ timeOfDay: result.timeOfDay ?? prev.timeOfDay,
632
+ chaosFactor: result.chaosFactor ?? prev.chaosFactor,
633
+ crisisMode: result.crisisMode ?? prev.crisisMode,
634
+ gameOver: result.gameOver ?? prev.gameOver,
635
+ sceneCount: result.sceneCount ?? prev.sceneCount,
636
+ npcs: result.npcs ?? prev.npcs,
637
+ clocks: result.clocks ?? prev.clocks,
638
+ storyBlueprint: result.storyBlueprint ?? prev.storyBlueprint,
639
+ kidMode: result.kidMode ?? prev.kidMode,
640
+ sessionLog: result.sessionLog ?? prev.sessionLog,
641
+ });
642
+
643
+ useToolResult((toolName, result: any) => {
644
+ // setup_character and update_state both return full state
645
+ if ((toolName === "setup_character" || toolName === "update_state") && result.success) {
646
+ setGame((prev) => mergeState(result, prev));
647
+ }
648
+
649
+ // action_roll auto-applies consequences — sync sidebar
650
+ if (toolName === "action_roll" && result.currentHealth !== undefined) {
651
+ setGame((prev) => ({
652
+ ...prev,
653
+ health: result.currentHealth,
654
+ spirit: result.currentSpirit ?? prev.spirit,
655
+ supply: result.currentSupply ?? prev.supply,
656
+ momentum: result.currentMomentum ?? prev.momentum,
657
+ chaosFactor: result.chaosFactor ?? prev.chaosFactor,
658
+ crisisMode: result.crisisMode ?? prev.crisisMode,
659
+ gameOver: result.gameOver ?? prev.gameOver,
660
+ sceneCount: result.sceneCount ?? prev.sceneCount,
661
+ }));
662
+ }
663
+
664
+ // burn_momentum resets momentum
665
+ if (toolName === "burn_momentum" && result.burned) {
666
+ setGame((prev) => ({ ...prev, momentum: result.newMomentum }));
667
+ }
668
+
669
+ // load_game restores full state
670
+ if (toolName === "load_game" && result.loaded) {
671
+ setGame((prev) => mergeState(result, prev));
672
+ }
673
+ });
674
+
675
+ return (
676
+ <StartScreen
677
+ icon={<span style={{ fontSize: "28px", color: C.accent }}>{"\u2726"}</span>}
678
+ title="Solo RPG"
679
+ subtitle="A Narrative Solo-RPG Engine"
680
+ buttonText="Begin Your Story"
681
+ >
682
+ <SidebarLayout sidebar={<Sidebar game={game} />} width="260px" side="right">
683
+ <ChatView />
684
+ </SidebarLayout>
685
+ </StartScreen>
686
+ );
687
+ }
688
+
689
+ mount(SoloRPGApp, {
690
+ title: "Solo RPG",
691
+ theme: {
692
+ bg: C.bg,
693
+ primary: C.accent,
694
+ text: C.text,
695
+ surface: C.surface,
696
+ border: C.border,
697
+ },
698
+ });