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.
- package/README.md +4 -4
- package/demo/.claude/.truth_last_prompt +1 -0
- package/demo/.claude/truth_status +1 -0
- package/demo/css/style.css +1181 -0
- package/demo/data/demo-events.json +103 -0
- package/demo/index.html +222 -0
- package/demo/js/chat.js +292 -0
- package/demo/js/code-panel.js +206 -0
- package/demo/js/demo-mode.js +107 -0
- package/demo/js/orb.js +634 -0
- package/demo/js/question-cards.js +207 -0
- package/demo/js/sse-client.js +473 -0
- package/demo/js/state.js +162 -0
- package/demo/js/timeline.js +394 -0
- package/demo/js/voice.js +154 -0
- package/dist/api/server.d.ts +1 -0
- package/dist/api/server.js +65 -2
- package/dist/api/server.js.map +1 -1
- package/dist/cli.js +13 -0
- package/dist/cli.js.map +1 -1
- package/dist/commands/demo.d.ts +5 -0
- package/dist/commands/demo.js +107 -0
- package/dist/commands/demo.js.map +1 -0
- package/dist/commands/runtime.d.ts +4 -0
- package/dist/commands/runtime.js +50 -3
- package/dist/commands/runtime.js.map +1 -1
- package/dist/runtime/agents/planner-agent.d.ts +5 -2
- package/dist/runtime/agents/planner-agent.js +232 -1
- package/dist/runtime/agents/planner-agent.js.map +1 -1
- package/dist/runtime/app-create-orchestrator.d.ts +4 -0
- package/dist/runtime/app-create-orchestrator.js +151 -48
- package/dist/runtime/app-create-orchestrator.js.map +1 -1
- package/dist/runtime/dashboard-sync.d.ts +25 -0
- package/dist/runtime/dashboard-sync.js +169 -0
- package/dist/runtime/dashboard-sync.js.map +1 -0
- package/dist/runtime/types.d.ts +28 -0
- package/package.json +3 -2
package/demo/js/state.js
ADDED
|
@@ -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
|
+
}
|
package/demo/js/voice.js
ADDED
|
@@ -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
|
+
}
|