tryassay 0.21.1 → 0.22.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 (37) hide show
  1. package/README.md +4 -4
  2. package/demo/.claude/.truth_last_prompt +1 -0
  3. package/demo/.claude/truth_status +1 -0
  4. package/demo/css/style.css +1181 -0
  5. package/demo/data/demo-events.json +103 -0
  6. package/demo/index.html +222 -0
  7. package/demo/js/chat.js +292 -0
  8. package/demo/js/code-panel.js +206 -0
  9. package/demo/js/demo-mode.js +107 -0
  10. package/demo/js/orb.js +634 -0
  11. package/demo/js/question-cards.js +207 -0
  12. package/demo/js/sse-client.js +473 -0
  13. package/demo/js/state.js +162 -0
  14. package/demo/js/timeline.js +394 -0
  15. package/demo/js/voice.js +154 -0
  16. package/dist/api/server.d.ts +1 -0
  17. package/dist/api/server.js +65 -2
  18. package/dist/api/server.js.map +1 -1
  19. package/dist/cli.js +13 -0
  20. package/dist/cli.js.map +1 -1
  21. package/dist/commands/demo.d.ts +5 -0
  22. package/dist/commands/demo.js +107 -0
  23. package/dist/commands/demo.js.map +1 -0
  24. package/dist/commands/runtime.d.ts +4 -0
  25. package/dist/commands/runtime.js +50 -3
  26. package/dist/commands/runtime.js.map +1 -1
  27. package/dist/runtime/agents/planner-agent.d.ts +5 -2
  28. package/dist/runtime/agents/planner-agent.js +232 -1
  29. package/dist/runtime/agents/planner-agent.js.map +1 -1
  30. package/dist/runtime/app-create-orchestrator.d.ts +4 -0
  31. package/dist/runtime/app-create-orchestrator.js +151 -48
  32. package/dist/runtime/app-create-orchestrator.js.map +1 -1
  33. package/dist/runtime/dashboard-sync.d.ts +25 -0
  34. package/dist/runtime/dashboard-sync.js +169 -0
  35. package/dist/runtime/dashboard-sync.js.map +1 -0
  36. package/dist/runtime/types.d.ts +28 -0
  37. package/package.json +3 -2
@@ -0,0 +1,162 @@
1
+ // state.js — Shared state + pub/sub event bus
2
+ // All modules read from and write to this central store
3
+
4
+ const AppState = (() => {
5
+ const listeners = {};
6
+
7
+ const state = {
8
+ phase: 'idle',
9
+ currentTask: null,
10
+ agents: [],
11
+ tasks: [],
12
+ verifications: [],
13
+ codeFiles: [],
14
+ metrics: {
15
+ tasksComplete: 0,
16
+ totalTasks: 0,
17
+ claimsVerified: 0,
18
+ claimsFailed: 0,
19
+ filesGenerated: 0
20
+ },
21
+ log: []
22
+ };
23
+
24
+ function on(event, fn) {
25
+ if (!listeners[event]) listeners[event] = [];
26
+ listeners[event].push(fn);
27
+ return () => {
28
+ listeners[event] = listeners[event].filter(f => f !== fn);
29
+ };
30
+ }
31
+
32
+ function emit(event, data) {
33
+ if (listeners[event]) {
34
+ listeners[event].forEach(fn => fn(data));
35
+ }
36
+ // Always emit wildcard
37
+ if (listeners['*']) {
38
+ listeners['*'].forEach(fn => fn(event, data));
39
+ }
40
+ }
41
+
42
+ function get() {
43
+ return state;
44
+ }
45
+
46
+ function update(partial) {
47
+ const oldPhase = state.phase;
48
+
49
+ // Merge top-level primitives and objects
50
+ for (const key of Object.keys(partial)) {
51
+ if (Array.isArray(partial[key]) && Array.isArray(state[key])) {
52
+ state[key] = partial[key];
53
+ } else if (typeof partial[key] === 'object' && partial[key] !== null && !Array.isArray(partial[key])) {
54
+ state[key] = { ...state[key], ...partial[key] };
55
+ } else {
56
+ state[key] = partial[key];
57
+ }
58
+ }
59
+
60
+ // Emit specific events based on what changed
61
+ if (partial.phase !== undefined && partial.phase !== oldPhase) {
62
+ emit('phase_change', { from: oldPhase, to: state.phase });
63
+ }
64
+ if (partial.currentTask !== undefined) {
65
+ emit('task_update', state.currentTask);
66
+ }
67
+ if (partial.tasks !== undefined) {
68
+ emit('task_update', state.tasks);
69
+ }
70
+ if (partial.verifications !== undefined) {
71
+ emit('verification', state.verifications[state.verifications.length - 1]);
72
+ }
73
+ if (partial.codeFiles !== undefined) {
74
+ emit('code_generated', state.codeFiles[state.codeFiles.length - 1]);
75
+ }
76
+ if (partial.agents !== undefined) {
77
+ emit('agent_status', state.agents);
78
+ }
79
+ if (partial.metrics !== undefined) {
80
+ emit('metric_update', state.metrics);
81
+ }
82
+ }
83
+
84
+ function addTask(task) {
85
+ state.tasks.push(task);
86
+ state.metrics.totalTasks = state.tasks.length;
87
+ if (task.status === 'completed') {
88
+ state.metrics.tasksComplete = state.tasks.filter(t => t.status === 'completed').length;
89
+ }
90
+ emit('task_update', task);
91
+ emit('metric_update', state.metrics);
92
+ }
93
+
94
+ function updateTask(id, updates) {
95
+ const task = state.tasks.find(t => t.id === id);
96
+ if (task) {
97
+ Object.assign(task, updates);
98
+ if (updates.status === 'completed') {
99
+ state.metrics.tasksComplete = state.tasks.filter(t => t.status === 'completed').length;
100
+ }
101
+ state.currentTask = task;
102
+ emit('task_update', task);
103
+ emit('metric_update', state.metrics);
104
+ }
105
+ }
106
+
107
+ function addVerification(v) {
108
+ state.verifications.push(v);
109
+ if (v.result === 'pass') state.metrics.claimsVerified++;
110
+ else state.metrics.claimsFailed++;
111
+ emit('verification', v);
112
+ emit('metric_update', state.metrics);
113
+ }
114
+
115
+ function addCodeFile(file) {
116
+ const existing = state.codeFiles.findIndex(f => f.path === file.path);
117
+ if (existing >= 0) {
118
+ state.codeFiles[existing] = file;
119
+ } else {
120
+ state.codeFiles.push(file);
121
+ state.metrics.filesGenerated = state.codeFiles.length;
122
+ }
123
+ emit('code_generated', file);
124
+ emit('metric_update', state.metrics);
125
+ }
126
+
127
+ function addAgent(agent) {
128
+ state.agents.push(agent);
129
+ emit('agent_status', state.agents);
130
+ }
131
+
132
+ function updateAgent(name, updates) {
133
+ const agent = state.agents.find(a => a.name === name);
134
+ if (agent) {
135
+ Object.assign(agent, updates);
136
+ emit('agent_status', state.agents);
137
+ }
138
+ }
139
+
140
+ function addLog(entry) {
141
+ state.log.push({ timestamp: Date.now(), ...entry });
142
+ emit('log', entry);
143
+ }
144
+
145
+ function reset() {
146
+ state.phase = 'idle';
147
+ state.currentTask = null;
148
+ state.agents = [];
149
+ state.tasks = [];
150
+ state.verifications = [];
151
+ state.codeFiles = [];
152
+ state.metrics = { tasksComplete: 0, totalTasks: 0, claimsVerified: 0, claimsFailed: 0, filesGenerated: 0 };
153
+ state.log = [];
154
+ emit('reset', null);
155
+ }
156
+
157
+ return { on, emit, get, update, addTask, updateTask, addVerification, addCodeFile, addAgent, updateAgent, addLog, reset };
158
+ })();
159
+
160
+ // Make globally available for all modules
161
+ window.AppState = AppState;
162
+ export default AppState;
@@ -0,0 +1,394 @@
1
+ // timeline.js — Right panel phase steps + metrics + timing
2
+ // Subscribes to AppState events, renders phase progression, verification results,
3
+ // global elapsed clock, per-phase durations, and heartbeat feedback
4
+
5
+ const PHASES = [
6
+ { id: 'idle', name: 'Idle' },
7
+ { id: 'initializing', name: 'Initializing' },
8
+ { id: 'plan_questioning', name: 'Refining Requirements' },
9
+ { id: 'plan_refining', name: 'Updating Requirements' },
10
+ { id: 'planning', name: 'Planning Architecture' },
11
+ { id: 'verifying_plan', name: 'Verifying Plan' },
12
+ { id: 'requirements_refining', name: 'Reviewing Plan' },
13
+ { id: 'scaffolding', name: 'Scaffolding Project' },
14
+ { id: 'building_feature', name: 'Building Features' },
15
+ { id: 'build_verifying', name: 'Verifying Code' },
16
+ { id: 'build_repairing', name: 'Repairing Build' },
17
+ { id: 'integration_verifying', name: 'Integration Verification' },
18
+ { id: 'functional_testing', name: 'Functional Testing' },
19
+ { id: 'functional_repairing', name: 'Repairing Failures' },
20
+ { id: 'cross_verifying', name: 'Cross-Verifying' },
21
+ { id: 'finalizing', name: 'Finalizing' },
22
+ { id: 'completed', name: 'Completed' },
23
+ { id: 'failed', name: 'Failed' }
24
+ ];
25
+
26
+ // Phases that map to a canonical phase for display
27
+ const PHASE_ALIASES = {
28
+ 'building_wave': 'building_feature',
29
+ 'awaiting_approval': 'requirements_refining',
30
+ };
31
+
32
+ // Track per-phase data
33
+ const phaseData = {};
34
+
35
+ // ── Timing state ──
36
+ let globalStartTime = null;
37
+ let globalClockInterval = null;
38
+ let activePhaseClockInterval = null;
39
+ const phaseStartTimes = {}; // phaseId → timestamp
40
+ const phaseDurations = {}; // phaseId → seconds
41
+ let currentActivePhase = 'idle';
42
+
43
+ // ── Heartbeat state ──
44
+ let lastEventTime = null;
45
+ let heartbeatInterval = null;
46
+ const HEARTBEAT_THRESHOLD_MS = 4000; // show heartbeat after 4s of silence
47
+
48
+ function resetPhaseData() {
49
+ for (const p of PHASES) {
50
+ phaseData[p.id] = { tasks: [], verifications: [] };
51
+ }
52
+ }
53
+
54
+ function resetTimingState() {
55
+ globalStartTime = null;
56
+ if (globalClockInterval) clearInterval(globalClockInterval);
57
+ if (activePhaseClockInterval) clearInterval(activePhaseClockInterval);
58
+ if (heartbeatInterval) clearInterval(heartbeatInterval);
59
+ globalClockInterval = null;
60
+ activePhaseClockInterval = null;
61
+ heartbeatInterval = null;
62
+ lastEventTime = null;
63
+ currentActivePhase = 'idle';
64
+ for (const key of Object.keys(phaseStartTimes)) delete phaseStartTimes[key];
65
+ for (const key of Object.keys(phaseDurations)) delete phaseDurations[key];
66
+
67
+ // Reset clock display
68
+ const clockEl = document.getElementById('global-clock');
69
+ const clockVal = document.getElementById('clock-value');
70
+ if (clockEl) clockEl.classList.add('hidden');
71
+ if (clockVal) clockVal.textContent = '00:00';
72
+ }
73
+
74
+ function resolvePhase(phaseId) {
75
+ return PHASE_ALIASES[phaseId] || phaseId;
76
+ }
77
+
78
+ function getPhaseIndex(phaseId) {
79
+ const resolved = resolvePhase(phaseId);
80
+ return PHASES.findIndex(p => p.id === resolved);
81
+ }
82
+
83
+ // ── Time formatting ──
84
+
85
+ function formatElapsed(ms) {
86
+ const totalSec = Math.floor(ms / 1000);
87
+ const min = Math.floor(totalSec / 60);
88
+ const sec = totalSec % 60;
89
+ const tenths = Math.floor((ms % 1000) / 100);
90
+ if (min > 0) {
91
+ return `${min}:${sec.toString().padStart(2, '0')}.${tenths}`;
92
+ }
93
+ return `${sec}.${tenths}s`;
94
+ }
95
+
96
+ function formatClockDisplay(ms) {
97
+ const totalSec = Math.floor(ms / 1000);
98
+ const min = Math.floor(totalSec / 60);
99
+ const sec = totalSec % 60;
100
+ return `${min.toString().padStart(2, '0')}:${sec.toString().padStart(2, '0')}`;
101
+ }
102
+
103
+ // ── Global clock ──
104
+
105
+ function startGlobalClock() {
106
+ if (globalStartTime) return; // already running
107
+ globalStartTime = Date.now();
108
+ const clockEl = document.getElementById('global-clock');
109
+ const clockVal = document.getElementById('clock-value');
110
+ if (clockEl) clockEl.classList.remove('hidden');
111
+
112
+ globalClockInterval = setInterval(() => {
113
+ if (!globalStartTime) return;
114
+ const elapsed = Date.now() - globalStartTime;
115
+ if (clockVal) clockVal.textContent = formatClockDisplay(elapsed);
116
+ }, 100);
117
+ }
118
+
119
+ function stopGlobalClock() {
120
+ // Don't clear globalStartTime — keep final time displayed
121
+ if (globalClockInterval) {
122
+ clearInterval(globalClockInterval);
123
+ globalClockInterval = null;
124
+ }
125
+ }
126
+
127
+ // ── Phase timing ──
128
+
129
+ function onPhaseEnter(phaseId) {
130
+ const resolved = resolvePhase(phaseId);
131
+
132
+ // Start global clock on first non-idle phase
133
+ if (resolved !== 'idle') {
134
+ startGlobalClock();
135
+ }
136
+
137
+ // Record phase start
138
+ phaseStartTimes[resolved] = Date.now();
139
+ currentActivePhase = resolved;
140
+ lastEventTime = Date.now();
141
+
142
+ // Start active phase clock (updates the active step's elapsed badge)
143
+ if (activePhaseClockInterval) clearInterval(activePhaseClockInterval);
144
+ activePhaseClockInterval = setInterval(() => {
145
+ updateActivePhaseElapsed();
146
+ updateHeartbeat();
147
+ }, 100);
148
+
149
+ // Stop global clock on terminal phases
150
+ if (resolved === 'completed' || resolved === 'failed') {
151
+ stopGlobalClock();
152
+ // Record final duration for the terminal phase itself
153
+ phaseDurations[resolved] = 0;
154
+ if (activePhaseClockInterval) {
155
+ clearInterval(activePhaseClockInterval);
156
+ activePhaseClockInterval = null;
157
+ }
158
+ if (heartbeatInterval) {
159
+ clearInterval(heartbeatInterval);
160
+ heartbeatInterval = null;
161
+ }
162
+ }
163
+ }
164
+
165
+ function onPhaseExit(phaseId) {
166
+ const resolved = resolvePhase(phaseId);
167
+ const start = phaseStartTimes[resolved];
168
+ if (start) {
169
+ phaseDurations[resolved] = (Date.now() - start) / 1000;
170
+ }
171
+ }
172
+
173
+ function updateActivePhaseElapsed() {
174
+ const resolved = currentActivePhase;
175
+ const start = phaseStartTimes[resolved];
176
+ if (!start) return;
177
+
178
+ const elapsedEl = document.querySelector('.phase-elapsed-live');
179
+ if (elapsedEl) {
180
+ const elapsed = Date.now() - start;
181
+ elapsedEl.textContent = formatElapsed(elapsed);
182
+ }
183
+ }
184
+
185
+ // ── Heartbeat ──
186
+
187
+ function recordEvent() {
188
+ lastEventTime = Date.now();
189
+ // Hide heartbeat immediately when event arrives
190
+ const hb = document.querySelector('.heartbeat-indicator');
191
+ if (hb) hb.classList.remove('visible');
192
+ }
193
+
194
+ function updateHeartbeat() {
195
+ if (!lastEventTime) return;
196
+ const silence = Date.now() - lastEventTime;
197
+ const hb = document.querySelector('.heartbeat-indicator');
198
+ if (!hb) return;
199
+
200
+ if (silence > HEARTBEAT_THRESHOLD_MS) {
201
+ hb.classList.add('visible');
202
+ } else {
203
+ hb.classList.remove('visible');
204
+ }
205
+ }
206
+
207
+ // ── Render ──
208
+
209
+ function renderSteps(currentPhase) {
210
+ const container = document.getElementById('timeline-steps');
211
+ if (!container) return;
212
+
213
+ const currentIdx = getPhaseIndex(currentPhase);
214
+
215
+ container.innerHTML = '';
216
+
217
+ for (let i = 0; i < PHASES.length; i++) {
218
+ const phase = PHASES[i];
219
+ const step = document.createElement('div');
220
+ step.className = 'timeline-step';
221
+ step.dataset.phase = phase.id;
222
+
223
+ if (i < currentIdx) {
224
+ step.classList.add('completed');
225
+ } else if (i === currentIdx) {
226
+ step.classList.add('active');
227
+ } else {
228
+ step.classList.add('pending');
229
+ }
230
+
231
+ // Step header row: name + duration
232
+ const headerRow = document.createElement('div');
233
+ headerRow.className = 'step-header';
234
+
235
+ const nameEl = document.createElement('div');
236
+ nameEl.className = 'step-name';
237
+ nameEl.textContent = phase.name;
238
+ headerRow.appendChild(nameEl);
239
+
240
+ // Duration badge for completed phases
241
+ if (i < currentIdx && phaseDurations[phase.id] !== undefined) {
242
+ const dur = phaseDurations[phase.id];
243
+ const durEl = document.createElement('span');
244
+ durEl.className = 'phase-duration';
245
+ durEl.textContent = dur < 60 ? `${dur.toFixed(1)}s` : `${Math.floor(dur / 60)}m${Math.floor(dur % 60)}s`;
246
+ headerRow.appendChild(durEl);
247
+ }
248
+
249
+ // Live elapsed for active phase
250
+ if (i === currentIdx && phase.id !== 'idle' && phase.id !== 'completed' && phase.id !== 'failed') {
251
+ const elapsedEl = document.createElement('span');
252
+ elapsedEl.className = 'phase-elapsed-live';
253
+ const start = phaseStartTimes[phase.id];
254
+ if (start) {
255
+ elapsedEl.textContent = formatElapsed(Date.now() - start);
256
+ }
257
+ headerRow.appendChild(elapsedEl);
258
+ }
259
+
260
+ step.appendChild(headerRow);
261
+
262
+ // Heartbeat indicator (only on active step)
263
+ if (i === currentIdx && phase.id !== 'idle' && phase.id !== 'completed' && phase.id !== 'failed') {
264
+ const hbEl = document.createElement('div');
265
+ hbEl.className = 'heartbeat-indicator';
266
+ hbEl.innerHTML = '<span class="heartbeat-dot"></span> working';
267
+ step.appendChild(hbEl);
268
+ }
269
+
270
+ const detailsEl = document.createElement('div');
271
+ detailsEl.className = 'step-details';
272
+
273
+ const data = phaseData[phase.id];
274
+ if (data) {
275
+ for (const task of data.tasks) {
276
+ const taskEl = document.createElement('div');
277
+ taskEl.className = 'step-task';
278
+ const badge = document.createElement('span');
279
+ badge.className = 'agent-badge';
280
+ badge.textContent = task.agent || 'agent';
281
+ taskEl.appendChild(badge);
282
+ const label = document.createTextNode(` ${task.name || task.title || task.id}`);
283
+ taskEl.appendChild(label);
284
+ detailsEl.appendChild(taskEl);
285
+ }
286
+
287
+ for (const v of data.verifications) {
288
+ const vEl = document.createElement('div');
289
+ vEl.className = 'verification-item';
290
+ vEl.classList.add(v.result === 'pass' ? 'pass' : 'fail');
291
+
292
+ const icon = document.createTextNode(v.result === 'pass' ? '\u2713 ' : '\u2717 ');
293
+ vEl.appendChild(icon);
294
+
295
+ const claim = document.createTextNode(v.claim || v.message || 'claim');
296
+ vEl.appendChild(claim);
297
+
298
+ const methodBadge = document.createElement('span');
299
+ methodBadge.className = 'method-badge';
300
+ methodBadge.textContent = v.method || 'llm';
301
+ vEl.appendChild(methodBadge);
302
+
303
+ detailsEl.appendChild(vEl);
304
+ }
305
+ }
306
+
307
+ step.appendChild(detailsEl);
308
+ container.appendChild(step);
309
+ }
310
+
311
+ // Auto-scroll to active step
312
+ const activeStep = container.querySelector('.timeline-step.active');
313
+ if (activeStep) {
314
+ activeStep.scrollIntoView({ behavior: 'smooth', block: 'nearest' });
315
+ }
316
+ }
317
+
318
+ function updateMetrics(metrics) {
319
+ const tasks = document.getElementById('metric-tasks');
320
+ const claims = document.getElementById('metric-claims');
321
+ const failed = document.getElementById('metric-failed');
322
+ const files = document.getElementById('metric-files');
323
+
324
+ if (tasks) tasks.textContent = `${metrics.tasksComplete}/${metrics.totalTasks}`;
325
+ if (claims) claims.textContent = metrics.claimsVerified;
326
+ if (failed) failed.textContent = metrics.claimsFailed;
327
+ if (files) files.textContent = metrics.filesGenerated;
328
+ }
329
+
330
+ export function initTimeline() {
331
+ const state = window.AppState;
332
+ if (!state) return;
333
+
334
+ resetPhaseData();
335
+ resetTimingState();
336
+ renderSteps('idle');
337
+ updateMetrics(state.get().metrics);
338
+
339
+ state.on('phase_change', ({ from, to }) => {
340
+ const resolvedFrom = resolvePhase(from);
341
+ const resolvedTo = resolvePhase(to);
342
+
343
+ // Record duration for outgoing phase
344
+ onPhaseExit(resolvedFrom);
345
+
346
+ // Start timing for incoming phase
347
+ onPhaseEnter(resolvedTo);
348
+
349
+ renderSteps(resolvedTo);
350
+ recordEvent();
351
+ });
352
+
353
+ state.on('task_update', (task) => {
354
+ if (!task) return;
355
+ const currentPhase = resolvePhase(state.get().phase);
356
+
357
+ // Handle single task
358
+ if (task && task.id) {
359
+ const pd = phaseData[currentPhase];
360
+ if (pd && !pd.tasks.find(t => t.id === task.id)) {
361
+ pd.tasks.push(task);
362
+ }
363
+ renderSteps(currentPhase);
364
+ }
365
+ recordEvent();
366
+ });
367
+
368
+ state.on('verification', (v) => {
369
+ if (!v) return;
370
+ const currentPhase = resolvePhase(state.get().phase);
371
+ const pd = phaseData[currentPhase];
372
+ if (pd) {
373
+ pd.verifications.push(v);
374
+ renderSteps(currentPhase);
375
+ }
376
+ recordEvent();
377
+ });
378
+
379
+ state.on('code_generated', () => {
380
+ recordEvent();
381
+ });
382
+
383
+ state.on('metric_update', (metrics) => {
384
+ updateMetrics(metrics);
385
+ recordEvent();
386
+ });
387
+
388
+ state.on('reset', () => {
389
+ resetPhaseData();
390
+ resetTimingState();
391
+ renderSteps('idle');
392
+ updateMetrics({ tasksComplete: 0, totalTasks: 0, claimsVerified: 0, claimsFailed: 0, filesGenerated: 0 });
393
+ });
394
+ }
@@ -0,0 +1,154 @@
1
+ // voice.js — ElevenLabs TTS narration
2
+ // High-quality voice synthesis per phase transition
3
+
4
+ const API_URL = 'https://api.elevenlabs.io/v1/text-to-speech';
5
+ const API_KEY = 'sk_80c5021a889afdd474a7f9920cb0b73981ec38c912f9cecf';
6
+
7
+ // Josh — deep young male narrator, fits sci-fi/tech aesthetic
8
+ const VOICE_ID = 'TxGEqnHWrfWFTfGW9XjX';
9
+
10
+ let isMuted = false;
11
+ let currentAudio = null;
12
+ let audioQueue = [];
13
+ let isPlaying = false;
14
+
15
+ const NARRATION = {
16
+ initializing: 'Analyzing your request. Breaking it down into an architecture plan.',
17
+ planning: 'Designing the application structure. Identifying components, routes, and data models.',
18
+ verifying_plan: 'Verifying the architecture. Extracting claims and checking each one against formal rules.',
19
+ requirements_refining: 'Architecture plan generated. Reviewing features, routes, and data models before building.',
20
+ scaffolding: 'Plan approved. Scaffolding the project. Creating the file structure and base configuration.',
21
+ building_feature: null,
22
+ build_verifying: 'Verifying the generated code. Checking for security issues, type errors, and logic bugs.',
23
+ build_repairing: 'Build issues detected. Repairing automatically.',
24
+ integration_verifying: 'Running integration verification. Checking cross-feature consistency.',
25
+ functional_testing: 'Running functional tests. Starting the application and testing every route with real HTTP requests.',
26
+ functional_repairing: 'Functional test failures detected. Feeding errors back to the code agent for repair.',
27
+ cross_verifying: 'Cross-verifying all features together. Checking that routes connect, schemas match, and imports resolve.',
28
+ finalizing: 'Finalizing the application. All verifications passed.',
29
+ completed: null
30
+ };
31
+
32
+ async function synthesize(text) {
33
+ const res = await fetch(`${API_URL}/${VOICE_ID}`, {
34
+ method: 'POST',
35
+ headers: {
36
+ 'Content-Type': 'application/json',
37
+ 'xi-api-key': API_KEY
38
+ },
39
+ body: JSON.stringify({
40
+ text,
41
+ model_id: 'eleven_multilingual_v2',
42
+ voice_settings: {
43
+ stability: 0.6,
44
+ similarity_boost: 0.8,
45
+ style: 0.3,
46
+ use_speaker_boost: true
47
+ }
48
+ })
49
+ });
50
+
51
+ if (!res.ok) {
52
+ console.error('[Voice] ElevenLabs API error:', res.status, await res.text());
53
+ return null;
54
+ }
55
+
56
+ const blob = await res.blob();
57
+ return URL.createObjectURL(blob);
58
+ }
59
+
60
+ function playNext() {
61
+ if (isPlaying || audioQueue.length === 0 || isMuted) return;
62
+
63
+ isPlaying = true;
64
+ const audioUrl = audioQueue.shift();
65
+
66
+ currentAudio = new Audio(audioUrl);
67
+ currentAudio.addEventListener('ended', () => {
68
+ URL.revokeObjectURL(audioUrl);
69
+ currentAudio = null;
70
+ isPlaying = false;
71
+ playNext();
72
+ });
73
+ currentAudio.addEventListener('error', () => {
74
+ URL.revokeObjectURL(audioUrl);
75
+ currentAudio = null;
76
+ isPlaying = false;
77
+ playNext();
78
+ });
79
+ currentAudio.play().catch(() => {
80
+ isPlaying = false;
81
+ playNext();
82
+ });
83
+ }
84
+
85
+ async function speak(text) {
86
+ if (isMuted || !text) return;
87
+
88
+ // Synthesize in background, queue when ready
89
+ const audioUrl = await synthesize(text);
90
+ if (audioUrl && !isMuted) {
91
+ audioQueue.push(audioUrl);
92
+ playNext();
93
+ }
94
+ }
95
+
96
+ function stopAll() {
97
+ if (currentAudio) {
98
+ currentAudio.pause();
99
+ currentAudio = null;
100
+ }
101
+ // Revoke any queued URLs
102
+ audioQueue.forEach(url => URL.revokeObjectURL(url));
103
+ audioQueue = [];
104
+ isPlaying = false;
105
+ }
106
+
107
+ function getNarration(phase) {
108
+ const state = window.AppState;
109
+
110
+ if (phase === 'building_feature') {
111
+ const taskName = state.get().currentTask?.name || 'current feature';
112
+ return `Building feature: ${taskName}. Generating the implementation.`;
113
+ }
114
+
115
+ if (phase === 'completed') {
116
+ const m = state.get().metrics;
117
+ return `Application complete. ${m.filesGenerated} files generated. ${m.claimsVerified} claims verified. ${m.claimsFailed} issues caught and fixed.`;
118
+ }
119
+
120
+ return NARRATION[phase] || null;
121
+ }
122
+
123
+ function initMuteButton() {
124
+ const btn = document.getElementById('mute-toggle');
125
+ if (!btn) return;
126
+
127
+ btn.addEventListener('click', () => {
128
+ isMuted = !isMuted;
129
+ if (isMuted) {
130
+ btn.textContent = '\uD83D\uDD07';
131
+ btn.classList.add('muted');
132
+ stopAll();
133
+ } else {
134
+ btn.textContent = '\uD83D\uDD0A';
135
+ btn.classList.remove('muted');
136
+ }
137
+ });
138
+ }
139
+
140
+ export function initVoice() {
141
+ const state = window.AppState;
142
+ if (!state) return;
143
+
144
+ initMuteButton();
145
+
146
+ state.on('phase_change', ({ to }) => {
147
+ const text = getNarration(to);
148
+ if (text) speak(text);
149
+ });
150
+
151
+ state.on('reset', () => {
152
+ stopAll();
153
+ });
154
+ }