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
|
@@ -0,0 +1,207 @@
|
|
|
1
|
+
// question-cards.js — Interactive question cards for plan mode
|
|
2
|
+
// Renders in the #plan-overlay when the backend sends plan_questions events
|
|
3
|
+
|
|
4
|
+
const AppState = window.AppState;
|
|
5
|
+
|
|
6
|
+
let currentSessionId = null;
|
|
7
|
+
let currentSelections = {}; // { questionId: [selectedOptionLabels] }
|
|
8
|
+
let currentQuestions = [];
|
|
9
|
+
let sseConfig = { apiBase: '', apiKey: '' };
|
|
10
|
+
|
|
11
|
+
/**
|
|
12
|
+
* Configure API connection (called from sse-client.js)
|
|
13
|
+
*/
|
|
14
|
+
export function configureQuestionCards(apiBase, apiKey) {
|
|
15
|
+
sseConfig.apiBase = apiBase || '';
|
|
16
|
+
sseConfig.apiKey = apiKey || '';
|
|
17
|
+
}
|
|
18
|
+
|
|
19
|
+
/**
|
|
20
|
+
* Show question cards in the plan overlay.
|
|
21
|
+
* Called when a plan_questions SSE event arrives.
|
|
22
|
+
*/
|
|
23
|
+
export function showQuestions({ questions, round, confidence, sessionId }) {
|
|
24
|
+
const overlay = document.getElementById('plan-overlay');
|
|
25
|
+
if (!overlay) return;
|
|
26
|
+
|
|
27
|
+
currentSessionId = sessionId;
|
|
28
|
+
currentQuestions = questions;
|
|
29
|
+
currentSelections = {};
|
|
30
|
+
|
|
31
|
+
// Initialize selections for each question
|
|
32
|
+
for (const q of questions) {
|
|
33
|
+
currentSelections[q.id] = [];
|
|
34
|
+
}
|
|
35
|
+
|
|
36
|
+
const questionsHTML = questions.map(q => renderQuestion(q)).join('');
|
|
37
|
+
|
|
38
|
+
overlay.innerHTML = `
|
|
39
|
+
<div class="plan-card plan-questions-card">
|
|
40
|
+
<div class="plan-questions-header">
|
|
41
|
+
<div class="plan-header">Refine Requirements</div>
|
|
42
|
+
<div class="plan-round">Round ${round} of 3</div>
|
|
43
|
+
</div>
|
|
44
|
+
<div class="confidence-meter">
|
|
45
|
+
<div class="confidence-bar">
|
|
46
|
+
<div class="confidence-fill" style="width: ${Math.round(confidence * 100)}%"></div>
|
|
47
|
+
</div>
|
|
48
|
+
<div class="confidence-label">Requirements Clarity: ${Math.round(confidence * 100)}%</div>
|
|
49
|
+
</div>
|
|
50
|
+
<div class="plan-questions">${questionsHTML}</div>
|
|
51
|
+
<button class="confirm-selections-btn" id="confirm-selections-btn" disabled>Confirm Selections</button>
|
|
52
|
+
</div>
|
|
53
|
+
`;
|
|
54
|
+
|
|
55
|
+
overlay.classList.add('visible');
|
|
56
|
+
|
|
57
|
+
// Bind option click handlers
|
|
58
|
+
overlay.querySelectorAll('.question-option').forEach(el => {
|
|
59
|
+
el.addEventListener('click', () => handleOptionClick(el));
|
|
60
|
+
});
|
|
61
|
+
|
|
62
|
+
// Bind confirm button
|
|
63
|
+
document.getElementById('confirm-selections-btn')?.addEventListener('click', handleConfirm);
|
|
64
|
+
}
|
|
65
|
+
|
|
66
|
+
/**
|
|
67
|
+
* Hide question cards and clear the overlay.
|
|
68
|
+
*/
|
|
69
|
+
export function hideQuestions() {
|
|
70
|
+
const overlay = document.getElementById('plan-overlay');
|
|
71
|
+
if (overlay) {
|
|
72
|
+
overlay.classList.remove('visible');
|
|
73
|
+
setTimeout(() => { overlay.innerHTML = ''; }, 500);
|
|
74
|
+
}
|
|
75
|
+
currentSelections = {};
|
|
76
|
+
currentQuestions = [];
|
|
77
|
+
}
|
|
78
|
+
|
|
79
|
+
// ── Internal ──
|
|
80
|
+
|
|
81
|
+
function renderQuestion(q) {
|
|
82
|
+
const hint = q.multiSelect ? 'Select all that apply' : 'Select one';
|
|
83
|
+
const optionsHTML = q.options.map(opt => `
|
|
84
|
+
<div class="question-option"
|
|
85
|
+
data-question-id="${q.id}"
|
|
86
|
+
data-option-label="${escapeAttr(opt.label)}"
|
|
87
|
+
data-multi="${q.multiSelect}">
|
|
88
|
+
<div class="option-label">${escapeHTML(opt.label)}</div>
|
|
89
|
+
<div class="option-description">${escapeHTML(opt.description)}</div>
|
|
90
|
+
</div>
|
|
91
|
+
`).join('');
|
|
92
|
+
|
|
93
|
+
return `
|
|
94
|
+
<div class="question-card" data-question-id="${q.id}">
|
|
95
|
+
<div class="question-header">${escapeHTML(q.header)}</div>
|
|
96
|
+
<div class="question-text">${escapeHTML(q.question)}</div>
|
|
97
|
+
<div class="select-hint">${hint}</div>
|
|
98
|
+
<div class="question-options">${optionsHTML}</div>
|
|
99
|
+
</div>
|
|
100
|
+
`;
|
|
101
|
+
}
|
|
102
|
+
|
|
103
|
+
function handleOptionClick(el) {
|
|
104
|
+
const questionId = el.dataset.questionId;
|
|
105
|
+
const label = el.dataset.optionLabel;
|
|
106
|
+
const isMulti = el.dataset.multi === 'true';
|
|
107
|
+
|
|
108
|
+
if (isMulti) {
|
|
109
|
+
// Toggle this option
|
|
110
|
+
el.classList.toggle('selected');
|
|
111
|
+
const idx = currentSelections[questionId].indexOf(label);
|
|
112
|
+
if (idx >= 0) {
|
|
113
|
+
currentSelections[questionId].splice(idx, 1);
|
|
114
|
+
} else {
|
|
115
|
+
currentSelections[questionId].push(label);
|
|
116
|
+
}
|
|
117
|
+
} else {
|
|
118
|
+
// Single select — deselect siblings, select this one
|
|
119
|
+
const siblings = document.querySelectorAll(`.question-option[data-question-id="${questionId}"]`);
|
|
120
|
+
siblings.forEach(s => s.classList.remove('selected'));
|
|
121
|
+
el.classList.add('selected');
|
|
122
|
+
currentSelections[questionId] = [label];
|
|
123
|
+
}
|
|
124
|
+
|
|
125
|
+
updateConfirmButton();
|
|
126
|
+
}
|
|
127
|
+
|
|
128
|
+
function updateConfirmButton() {
|
|
129
|
+
const btn = document.getElementById('confirm-selections-btn');
|
|
130
|
+
if (!btn) return;
|
|
131
|
+
|
|
132
|
+
// All single-select questions must have exactly one answer
|
|
133
|
+
const allSingleAnswered = currentQuestions
|
|
134
|
+
.filter(q => !q.multiSelect)
|
|
135
|
+
.every(q => currentSelections[q.id]?.length > 0);
|
|
136
|
+
|
|
137
|
+
btn.disabled = !allSingleAnswered;
|
|
138
|
+
}
|
|
139
|
+
|
|
140
|
+
async function handleConfirm() {
|
|
141
|
+
const btn = document.getElementById('confirm-selections-btn');
|
|
142
|
+
if (btn) {
|
|
143
|
+
btn.disabled = true;
|
|
144
|
+
btn.textContent = 'Submitting...';
|
|
145
|
+
}
|
|
146
|
+
|
|
147
|
+
const answers = Object.entries(currentSelections).map(([questionId, selectedOptions]) => ({
|
|
148
|
+
questionId,
|
|
149
|
+
selectedOptions,
|
|
150
|
+
}));
|
|
151
|
+
|
|
152
|
+
const headers = { 'Content-Type': 'application/json' };
|
|
153
|
+
if (sseConfig.apiKey) headers['Authorization'] = `Bearer ${sseConfig.apiKey}`;
|
|
154
|
+
|
|
155
|
+
const sessionId = currentSessionId || window.__appId;
|
|
156
|
+
|
|
157
|
+
try {
|
|
158
|
+
const res = await fetch(`${sseConfig.apiBase}/api/v1/app/${sessionId}/answers`, {
|
|
159
|
+
method: 'POST',
|
|
160
|
+
headers,
|
|
161
|
+
body: JSON.stringify({ answers }),
|
|
162
|
+
});
|
|
163
|
+
|
|
164
|
+
if (!res.ok) {
|
|
165
|
+
console.error('[QuestionCards] Submit failed:', res.status);
|
|
166
|
+
AppState.addLog({ level: 'error', message: `Failed to submit answers: ${res.status}` });
|
|
167
|
+
if (btn) {
|
|
168
|
+
btn.disabled = false;
|
|
169
|
+
btn.textContent = 'Confirm Selections';
|
|
170
|
+
}
|
|
171
|
+
return;
|
|
172
|
+
}
|
|
173
|
+
} catch (err) {
|
|
174
|
+
console.error('[QuestionCards] Submit error:', err);
|
|
175
|
+
AppState.addLog({ level: 'error', message: `Submit error: ${err.message}` });
|
|
176
|
+
if (btn) {
|
|
177
|
+
btn.disabled = false;
|
|
178
|
+
btn.textContent = 'Confirm Selections';
|
|
179
|
+
}
|
|
180
|
+
return;
|
|
181
|
+
}
|
|
182
|
+
|
|
183
|
+
// Show refining state
|
|
184
|
+
const overlay = document.getElementById('plan-overlay');
|
|
185
|
+
if (overlay) {
|
|
186
|
+
overlay.innerHTML = `
|
|
187
|
+
<div class="plan-card">
|
|
188
|
+
<div class="refining-state">
|
|
189
|
+
<div class="refining-spinner"></div>
|
|
190
|
+
<div>Refining requirements...</div>
|
|
191
|
+
</div>
|
|
192
|
+
</div>
|
|
193
|
+
`;
|
|
194
|
+
}
|
|
195
|
+
|
|
196
|
+
AppState.addLog({ level: 'info', message: 'Answers submitted. Refining requirements...' });
|
|
197
|
+
}
|
|
198
|
+
|
|
199
|
+
function escapeHTML(str) {
|
|
200
|
+
const div = document.createElement('div');
|
|
201
|
+
div.textContent = str;
|
|
202
|
+
return div.innerHTML;
|
|
203
|
+
}
|
|
204
|
+
|
|
205
|
+
function escapeAttr(str) {
|
|
206
|
+
return str.replace(/"/g, '"').replace(/'/g, ''');
|
|
207
|
+
}
|
|
@@ -0,0 +1,473 @@
|
|
|
1
|
+
// sse-client.js — EventSource to AppState bridge
|
|
2
|
+
// Connects to the Assay API SSE stream and maps events to state updates
|
|
3
|
+
|
|
4
|
+
import { showQuestions, hideQuestions, configureQuestionCards } from './question-cards.js';
|
|
5
|
+
import { configureChat, addArchitectMessage, closeChat } from './chat.js';
|
|
6
|
+
|
|
7
|
+
const AppState = window.AppState;
|
|
8
|
+
|
|
9
|
+
let eventSource = null;
|
|
10
|
+
let apiBase = '';
|
|
11
|
+
let apiKey = '';
|
|
12
|
+
|
|
13
|
+
function autoApprovePlan(appId) {
|
|
14
|
+
const headers = { 'Content-Type': 'application/json' };
|
|
15
|
+
if (apiKey) headers['Authorization'] = `Bearer ${apiKey}`;
|
|
16
|
+
fetch(`${apiBase}/api/v1/app/${appId}/approve`, {
|
|
17
|
+
method: 'POST',
|
|
18
|
+
headers,
|
|
19
|
+
body: JSON.stringify({ decision: 'approved' }),
|
|
20
|
+
}).then(res => {
|
|
21
|
+
if (res.ok) {
|
|
22
|
+
AppState.addLog({ level: 'info', message: 'Plan approved — building...' });
|
|
23
|
+
hidePlanOverlay();
|
|
24
|
+
} else {
|
|
25
|
+
console.warn('[SSE] Auto-approve failed:', res.status);
|
|
26
|
+
}
|
|
27
|
+
}).catch(err => {
|
|
28
|
+
console.error('[SSE] Auto-approve error:', err);
|
|
29
|
+
});
|
|
30
|
+
}
|
|
31
|
+
|
|
32
|
+
function showPlanOverlay(summary) {
|
|
33
|
+
let overlay = document.getElementById('plan-overlay');
|
|
34
|
+
if (!overlay) return;
|
|
35
|
+
|
|
36
|
+
const features = (summary.features || []).map(f =>
|
|
37
|
+
`<div class="plan-feature">
|
|
38
|
+
<span class="plan-feature-name">${f.name || f.id}</span>
|
|
39
|
+
<span class="plan-feature-meta">${f.complexity} · ${f.routeCount || 0} routes · ${f.pageCount || 0} pages</span>
|
|
40
|
+
</div>`
|
|
41
|
+
).join('');
|
|
42
|
+
|
|
43
|
+
const warnings = (summary.warnings || []).map(w =>
|
|
44
|
+
`<div class="plan-warning">${w}</div>`
|
|
45
|
+
).join('');
|
|
46
|
+
|
|
47
|
+
overlay.innerHTML = `
|
|
48
|
+
<div class="plan-card">
|
|
49
|
+
<div class="plan-header">Architecture Plan</div>
|
|
50
|
+
<div class="plan-meta">
|
|
51
|
+
<span>${summary.techStack || ''}</span>
|
|
52
|
+
<span>${summary.estimatedComplexity || ''}</span>
|
|
53
|
+
<span>${summary.featureCount || 0} features</span>
|
|
54
|
+
<span>${summary.schemaEntities || 0} entities</span>
|
|
55
|
+
<span>${summary.apiRouteCount || 0} routes</span>
|
|
56
|
+
<span>${summary.pageCount || 0} pages</span>
|
|
57
|
+
</div>
|
|
58
|
+
<div class="plan-features">${features}</div>
|
|
59
|
+
${warnings ? `<div class="plan-warnings">${warnings}</div>` : ''}
|
|
60
|
+
<div class="plan-auto">Auto-approving...</div>
|
|
61
|
+
</div>
|
|
62
|
+
`;
|
|
63
|
+
overlay.classList.add('visible');
|
|
64
|
+
}
|
|
65
|
+
|
|
66
|
+
function hidePlanOverlay() {
|
|
67
|
+
const overlay = document.getElementById('plan-overlay');
|
|
68
|
+
if (overlay) {
|
|
69
|
+
overlay.classList.remove('visible');
|
|
70
|
+
setTimeout(() => { overlay.innerHTML = ''; }, 500);
|
|
71
|
+
}
|
|
72
|
+
}
|
|
73
|
+
|
|
74
|
+
function showCompletionOverlay(data) {
|
|
75
|
+
const overlay = document.getElementById('completion-overlay');
|
|
76
|
+
if (!overlay) return;
|
|
77
|
+
|
|
78
|
+
const m = AppState.get().metrics;
|
|
79
|
+
const appName = data.appName || 'Application';
|
|
80
|
+
const status = data.status === 'completed' ? 'Build Complete' : 'Build Finished (with issues)';
|
|
81
|
+
const statusClass = data.status === 'completed' ? 'success' : 'partial';
|
|
82
|
+
|
|
83
|
+
// Grab total elapsed time from the global clock
|
|
84
|
+
const clockVal = document.getElementById('clock-value');
|
|
85
|
+
const totalTime = clockVal ? clockVal.textContent : '';
|
|
86
|
+
|
|
87
|
+
overlay.innerHTML = `
|
|
88
|
+
<div class="completion-card">
|
|
89
|
+
<div class="completion-icon ${statusClass}">${data.status === 'completed' ? '\u2713' : '\u26a0'}</div>
|
|
90
|
+
<div class="completion-header">${status}</div>
|
|
91
|
+
<div class="completion-app-name">${appName}</div>
|
|
92
|
+
<div class="completion-metrics">
|
|
93
|
+
<span>${m.filesGenerated} files</span>
|
|
94
|
+
<span>${m.claimsVerified} claims verified</span>
|
|
95
|
+
<span>${m.claimsFailed} caught</span>
|
|
96
|
+
${totalTime ? `<span>${totalTime} total</span>` : ''}
|
|
97
|
+
</div>
|
|
98
|
+
<button class="completion-launch-btn" id="launch-btn">Launch Application</button>
|
|
99
|
+
<div class="completion-status" id="launch-status"></div>
|
|
100
|
+
</div>
|
|
101
|
+
`;
|
|
102
|
+
overlay.classList.add('visible');
|
|
103
|
+
|
|
104
|
+
document.getElementById('launch-btn')?.addEventListener('click', () => {
|
|
105
|
+
launchApp(window.__appId);
|
|
106
|
+
});
|
|
107
|
+
}
|
|
108
|
+
|
|
109
|
+
function launchApp(appId) {
|
|
110
|
+
const btn = document.getElementById('launch-btn');
|
|
111
|
+
const statusEl = document.getElementById('launch-status');
|
|
112
|
+
if (btn) {
|
|
113
|
+
btn.disabled = true;
|
|
114
|
+
btn.textContent = 'Launching...';
|
|
115
|
+
}
|
|
116
|
+
if (statusEl) statusEl.textContent = 'Installing dependencies...';
|
|
117
|
+
|
|
118
|
+
const headers = { 'Content-Type': 'application/json' };
|
|
119
|
+
if (apiKey) headers['Authorization'] = `Bearer ${apiKey}`;
|
|
120
|
+
|
|
121
|
+
fetch(`${apiBase}/api/v1/app/${appId}/launch`, {
|
|
122
|
+
method: 'POST',
|
|
123
|
+
headers,
|
|
124
|
+
}).then(res => {
|
|
125
|
+
if (!res.ok) {
|
|
126
|
+
if (statusEl) statusEl.textContent = 'Launch failed. Check server logs.';
|
|
127
|
+
return;
|
|
128
|
+
}
|
|
129
|
+
// Start polling for launch status
|
|
130
|
+
pollLaunchStatus(appId);
|
|
131
|
+
}).catch(err => {
|
|
132
|
+
console.error('[Launch] Error:', err);
|
|
133
|
+
if (statusEl) statusEl.textContent = 'Launch error: ' + err.message;
|
|
134
|
+
});
|
|
135
|
+
}
|
|
136
|
+
|
|
137
|
+
function pollLaunchStatus(appId) {
|
|
138
|
+
const statusEl = document.getElementById('launch-status');
|
|
139
|
+
const btn = document.getElementById('launch-btn');
|
|
140
|
+
|
|
141
|
+
const headers = {};
|
|
142
|
+
if (apiKey) headers['Authorization'] = `Bearer ${apiKey}`;
|
|
143
|
+
|
|
144
|
+
const poll = setInterval(() => {
|
|
145
|
+
fetch(`${apiBase}/api/v1/app/${appId}/launch`, { headers })
|
|
146
|
+
.then(res => res.json())
|
|
147
|
+
.then(data => {
|
|
148
|
+
if (data.status === 'installing') {
|
|
149
|
+
if (statusEl) statusEl.textContent = 'Installing dependencies...';
|
|
150
|
+
} else if (data.status === 'starting') {
|
|
151
|
+
if (statusEl) statusEl.textContent = 'Starting dev server...';
|
|
152
|
+
} else if (data.status === 'ready') {
|
|
153
|
+
clearInterval(poll);
|
|
154
|
+
if (btn) {
|
|
155
|
+
btn.textContent = 'Open Application';
|
|
156
|
+
btn.disabled = false;
|
|
157
|
+
btn.onclick = () => window.open(data.url, '_blank');
|
|
158
|
+
btn.classList.add('ready');
|
|
159
|
+
}
|
|
160
|
+
if (statusEl) {
|
|
161
|
+
statusEl.innerHTML = `<a href="${data.url}" target="_blank" class="launch-url">${data.url}</a>`;
|
|
162
|
+
}
|
|
163
|
+
AppState.addLog({ level: 'info', message: `Application ready at ${data.url}` });
|
|
164
|
+
} else if (data.status === 'failed') {
|
|
165
|
+
clearInterval(poll);
|
|
166
|
+
if (btn) {
|
|
167
|
+
btn.textContent = 'Launch Failed';
|
|
168
|
+
btn.disabled = true;
|
|
169
|
+
}
|
|
170
|
+
if (statusEl) statusEl.textContent = data.error || 'Launch failed';
|
|
171
|
+
}
|
|
172
|
+
})
|
|
173
|
+
.catch(() => {
|
|
174
|
+
// Ignore polling errors, keep trying
|
|
175
|
+
});
|
|
176
|
+
}, 2000);
|
|
177
|
+
}
|
|
178
|
+
|
|
179
|
+
// Track whether completion overlay has been shown
|
|
180
|
+
let completionShown = false;
|
|
181
|
+
|
|
182
|
+
function handleSSEEvent(type, data) {
|
|
183
|
+
switch (type) {
|
|
184
|
+
case 'connected': {
|
|
185
|
+
console.log('[SSE] Stream connected:', data);
|
|
186
|
+
AppState.addLog({ level: 'info', message: `Connected to session: ${data.sessionId || 'unknown'}` });
|
|
187
|
+
completionShown = false;
|
|
188
|
+
break;
|
|
189
|
+
}
|
|
190
|
+
case 'progress': {
|
|
191
|
+
// The orchestrator emits AppCreateProgress with a phase object
|
|
192
|
+
const phase = data.phase;
|
|
193
|
+
const phaseName = typeof phase === 'string' ? phase : phase?.phase;
|
|
194
|
+
if (phaseName) {
|
|
195
|
+
AppState.update({ phase: phaseName });
|
|
196
|
+
|
|
197
|
+
// Extract feature info for voice narration
|
|
198
|
+
if (phase.featureId || phase.featureName || data.currentFeature) {
|
|
199
|
+
AppState.update({
|
|
200
|
+
currentTask: {
|
|
201
|
+
name: phase.featureName || phase.featureId || data.currentFeature,
|
|
202
|
+
status: 'in_progress'
|
|
203
|
+
}
|
|
204
|
+
});
|
|
205
|
+
}
|
|
206
|
+
|
|
207
|
+
// If phase reports counts, update metrics
|
|
208
|
+
if (data.totalFeatures) {
|
|
209
|
+
AppState.update({
|
|
210
|
+
metrics: {
|
|
211
|
+
totalTasks: data.totalFeatures,
|
|
212
|
+
tasksComplete: data.completedFeatures?.length || 0,
|
|
213
|
+
}
|
|
214
|
+
});
|
|
215
|
+
}
|
|
216
|
+
|
|
217
|
+
// Fallback: if progress says completed/failed but we haven't shown the overlay yet,
|
|
218
|
+
// wait briefly for the 'complete' event, then show overlay from whatever data we have
|
|
219
|
+
if ((phaseName === 'completed' || phaseName === 'failed') && !completionShown) {
|
|
220
|
+
setTimeout(() => {
|
|
221
|
+
if (!completionShown) {
|
|
222
|
+
console.log('[SSE] Showing completion overlay from progress fallback');
|
|
223
|
+
const input = document.getElementById('prompt-input');
|
|
224
|
+
const btn = document.getElementById('submit-btn');
|
|
225
|
+
if (input) input.disabled = false;
|
|
226
|
+
if (btn) btn.disabled = false;
|
|
227
|
+
showCompletionOverlay({
|
|
228
|
+
status: phaseName,
|
|
229
|
+
appName: data.appName || data.currentFeature || 'Application',
|
|
230
|
+
});
|
|
231
|
+
completionShown = true;
|
|
232
|
+
disconnect();
|
|
233
|
+
}
|
|
234
|
+
}, 2000);
|
|
235
|
+
}
|
|
236
|
+
}
|
|
237
|
+
break;
|
|
238
|
+
}
|
|
239
|
+
case 'task_update': {
|
|
240
|
+
const existing = AppState.get().tasks.find(t => t.id === data.id);
|
|
241
|
+
if (existing) {
|
|
242
|
+
AppState.updateTask(data.id, data);
|
|
243
|
+
} else {
|
|
244
|
+
AppState.addTask(data);
|
|
245
|
+
}
|
|
246
|
+
// Extract per-feature claim counts into verification metrics
|
|
247
|
+
if (data.claims) {
|
|
248
|
+
const c = data.claims;
|
|
249
|
+
const passed = c.passed || c.totalClaims - (c.failed || 0) || 0;
|
|
250
|
+
const failed = c.failed || 0;
|
|
251
|
+
for (let i = 0; i < passed; i++) {
|
|
252
|
+
AppState.addVerification({ claim: `${data.name || data.id}: claim ${i + 1}`, result: 'pass', method: 'formal' });
|
|
253
|
+
}
|
|
254
|
+
for (let i = 0; i < failed; i++) {
|
|
255
|
+
AppState.addVerification({ claim: `${data.name || data.id}: failed claim ${i + 1}`, result: 'fail', method: 'formal' });
|
|
256
|
+
}
|
|
257
|
+
}
|
|
258
|
+
break;
|
|
259
|
+
}
|
|
260
|
+
case 'verification': {
|
|
261
|
+
// Add the phase-level verification result
|
|
262
|
+
AppState.addVerification({
|
|
263
|
+
claim: data.claim,
|
|
264
|
+
result: data.passed ? 'pass' : 'fail',
|
|
265
|
+
method: data.method || data.verification_method || 'llm'
|
|
266
|
+
});
|
|
267
|
+
// If it includes passed/failed counts, synthesize individual claim metrics
|
|
268
|
+
if (data.passed_count > 0 || data.failed_count > 0) {
|
|
269
|
+
for (let i = 0; i < (data.passed_count || 0); i++) {
|
|
270
|
+
AppState.addVerification({ claim: `${data.phase}: check ${i + 1}`, result: 'pass', method: 'formal' });
|
|
271
|
+
}
|
|
272
|
+
for (let i = 0; i < (data.failed_count || 0); i++) {
|
|
273
|
+
AppState.addVerification({ claim: `${data.phase}: failed check ${i + 1}`, result: 'fail', method: 'formal' });
|
|
274
|
+
}
|
|
275
|
+
}
|
|
276
|
+
break;
|
|
277
|
+
}
|
|
278
|
+
case 'code_generated': {
|
|
279
|
+
AppState.addCodeFile({
|
|
280
|
+
path: data.path,
|
|
281
|
+
language: data.language || 'typescript',
|
|
282
|
+
content: data.content
|
|
283
|
+
});
|
|
284
|
+
break;
|
|
285
|
+
}
|
|
286
|
+
case 'agent_status': {
|
|
287
|
+
const agents = Array.isArray(data.agents) ? data.agents : (data.name ? [data] : []);
|
|
288
|
+
for (const agent of agents) {
|
|
289
|
+
const existing = AppState.get().agents.find(a => a.name === agent.name);
|
|
290
|
+
if (existing) {
|
|
291
|
+
AppState.updateAgent(agent.name, agent);
|
|
292
|
+
} else {
|
|
293
|
+
AppState.addAgent(agent);
|
|
294
|
+
}
|
|
295
|
+
}
|
|
296
|
+
break;
|
|
297
|
+
}
|
|
298
|
+
case 'plan_questions': {
|
|
299
|
+
AppState.update({ phase: 'plan_questioning' });
|
|
300
|
+
AppState.addLog({ level: 'info', message: `Requirements round ${data.round}: ${data.questions.length} questions (${Math.round(data.confidence * 100)}% confidence)` });
|
|
301
|
+
showQuestions(data);
|
|
302
|
+
break;
|
|
303
|
+
}
|
|
304
|
+
case 'plan_readiness': {
|
|
305
|
+
if (data.ready) {
|
|
306
|
+
AppState.addLog({ level: 'success', message: `Requirements confirmed (${Math.round(data.confidence * 100)}% confidence): ${data.summary}` });
|
|
307
|
+
} else {
|
|
308
|
+
const gapList = data.gaps.length > 0 ? data.gaps.join(', ') : 'unclear';
|
|
309
|
+
AppState.addLog({ level: 'info', message: `Still gathering requirements (${Math.round(data.confidence * 100)}% confidence). Gaps: ${gapList}` });
|
|
310
|
+
}
|
|
311
|
+
break;
|
|
312
|
+
}
|
|
313
|
+
case 'chat_message': {
|
|
314
|
+
AppState.update({ phase: 'chat' });
|
|
315
|
+
addArchitectMessage(data);
|
|
316
|
+
if (data.readiness) {
|
|
317
|
+
if (data.readiness.ready) {
|
|
318
|
+
AppState.addLog({ level: 'success', message: `Ready to build (${Math.round(data.readiness.confidence * 100)}%): ${data.readiness.summary}` });
|
|
319
|
+
} else if (data.readiness.gaps && data.readiness.gaps.length > 0) {
|
|
320
|
+
AppState.addLog({ level: 'info', message: `Gathering requirements (${Math.round(data.readiness.confidence * 100)}%). Gaps: ${data.readiness.gaps.join(', ')}` });
|
|
321
|
+
}
|
|
322
|
+
}
|
|
323
|
+
break;
|
|
324
|
+
}
|
|
325
|
+
case 'plan_summary': {
|
|
326
|
+
// Architecture plan arrived — show it in the UI overlay (questions are done)
|
|
327
|
+
hideQuestions();
|
|
328
|
+
closeChat();
|
|
329
|
+
AppState.update({ planSummary: data });
|
|
330
|
+
AppState.addLog({ level: 'info', message: `Architecture: ${data.featureCount} features, ${data.apiRouteCount} routes, ${data.pageCount} pages` });
|
|
331
|
+
showPlanOverlay(data);
|
|
332
|
+
break;
|
|
333
|
+
}
|
|
334
|
+
case 'awaiting_approval': {
|
|
335
|
+
// Pipeline is blocked waiting for approval — auto-approve after showing plan
|
|
336
|
+
AppState.update({ phase: 'requirements_refining' });
|
|
337
|
+
AppState.addLog({ level: 'info', message: 'Plan ready — auto-approving...' });
|
|
338
|
+
// Auto-approve after 4 seconds so audience can see the plan
|
|
339
|
+
setTimeout(() => {
|
|
340
|
+
autoApprovePlan(data.sessionId || window.__appId);
|
|
341
|
+
}, 4000);
|
|
342
|
+
break;
|
|
343
|
+
}
|
|
344
|
+
case 'functional_test': {
|
|
345
|
+
// Functional test progress
|
|
346
|
+
const ftPhase = data.phase || 'functional_testing';
|
|
347
|
+
AppState.update({ phase: ftPhase });
|
|
348
|
+
if (data.passedCount !== undefined) {
|
|
349
|
+
AppState.addLog({ level: 'info', message: `Functional: ${data.passedCount} passed, ${data.failedCount} failed` });
|
|
350
|
+
}
|
|
351
|
+
if (data.failureCount) {
|
|
352
|
+
AppState.addLog({ level: 'warn', message: `Repairing ${data.failureCount} test failures (attempt ${data.attempt})` });
|
|
353
|
+
}
|
|
354
|
+
break;
|
|
355
|
+
}
|
|
356
|
+
case 'error': {
|
|
357
|
+
AppState.update({ phase: 'failed' });
|
|
358
|
+
AppState.addLog({ level: 'error', message: data.message || data.error || 'Unknown error' });
|
|
359
|
+
if (!completionShown) {
|
|
360
|
+
const input = document.getElementById('prompt-input');
|
|
361
|
+
const btn = document.getElementById('submit-btn');
|
|
362
|
+
if (input) input.disabled = false;
|
|
363
|
+
if (btn) btn.disabled = false;
|
|
364
|
+
showCompletionOverlay({ status: 'failed', appName: 'Application' });
|
|
365
|
+
completionShown = true;
|
|
366
|
+
}
|
|
367
|
+
disconnect();
|
|
368
|
+
break;
|
|
369
|
+
}
|
|
370
|
+
case 'complete': {
|
|
371
|
+
AppState.update({ phase: 'completed' });
|
|
372
|
+
if (data.metrics) {
|
|
373
|
+
AppState.update({ metrics: data.metrics });
|
|
374
|
+
}
|
|
375
|
+
// Re-enable input
|
|
376
|
+
const input = document.getElementById('prompt-input');
|
|
377
|
+
const btn = document.getElementById('submit-btn');
|
|
378
|
+
if (input) input.disabled = false;
|
|
379
|
+
if (btn) btn.disabled = false;
|
|
380
|
+
// Show completion overlay with launch button
|
|
381
|
+
if (!completionShown) {
|
|
382
|
+
showCompletionOverlay(data);
|
|
383
|
+
completionShown = true;
|
|
384
|
+
}
|
|
385
|
+
// Now safe to disconnect — overlay is rendered
|
|
386
|
+
disconnect();
|
|
387
|
+
break;
|
|
388
|
+
}
|
|
389
|
+
}
|
|
390
|
+
}
|
|
391
|
+
|
|
392
|
+
function connect(appId) {
|
|
393
|
+
if (eventSource) {
|
|
394
|
+
eventSource.close();
|
|
395
|
+
eventSource = null;
|
|
396
|
+
}
|
|
397
|
+
|
|
398
|
+
const url = apiKey
|
|
399
|
+
? `${apiBase}/api/v1/app/${appId}/stream?key=${encodeURIComponent(apiKey)}`
|
|
400
|
+
: `${apiBase}/api/v1/app/${appId}/stream`;
|
|
401
|
+
console.log('[SSE] Connecting to:', url);
|
|
402
|
+
eventSource = new EventSource(url);
|
|
403
|
+
|
|
404
|
+
// Listen for named event types
|
|
405
|
+
const eventTypes = [
|
|
406
|
+
'connected', 'progress', 'task_update', 'verification',
|
|
407
|
+
'code_generated', 'agent_status', 'error', 'complete',
|
|
408
|
+
'plan_summary', 'awaiting_approval', 'functional_test',
|
|
409
|
+
'plan_questions',
|
|
410
|
+
'plan_readiness',
|
|
411
|
+
'chat_message'
|
|
412
|
+
];
|
|
413
|
+
|
|
414
|
+
for (const type of eventTypes) {
|
|
415
|
+
eventSource.addEventListener(type, (event) => {
|
|
416
|
+
try {
|
|
417
|
+
const data = JSON.parse(event.data);
|
|
418
|
+
handleSSEEvent(type, data);
|
|
419
|
+
} catch (err) {
|
|
420
|
+
console.error(`[SSE] Failed to parse ${type} event:`, err);
|
|
421
|
+
}
|
|
422
|
+
});
|
|
423
|
+
}
|
|
424
|
+
|
|
425
|
+
// Also handle unnamed messages (default event type)
|
|
426
|
+
eventSource.onmessage = (event) => {
|
|
427
|
+
try {
|
|
428
|
+
const data = JSON.parse(event.data);
|
|
429
|
+
// Try to infer the type from the data shape
|
|
430
|
+
if (data.phase) handleSSEEvent('progress', data);
|
|
431
|
+
else if (data.claim) handleSSEEvent('verification', data);
|
|
432
|
+
else if (data.path && data.content) handleSSEEvent('code_generated', data);
|
|
433
|
+
else if (data.error) handleSSEEvent('error', data);
|
|
434
|
+
} catch (err) {
|
|
435
|
+
console.error('[SSE] Failed to parse message:', err);
|
|
436
|
+
}
|
|
437
|
+
};
|
|
438
|
+
|
|
439
|
+
eventSource.onopen = () => {
|
|
440
|
+
console.log('[SSE] Connected successfully');
|
|
441
|
+
};
|
|
442
|
+
|
|
443
|
+
eventSource.onerror = (err) => {
|
|
444
|
+
console.error('[SSE] Connection error:', err);
|
|
445
|
+
if (eventSource.readyState === EventSource.CLOSED) {
|
|
446
|
+
console.warn('[SSE] Connection closed. Check API server and auth key.');
|
|
447
|
+
}
|
|
448
|
+
};
|
|
449
|
+
}
|
|
450
|
+
|
|
451
|
+
function disconnect() {
|
|
452
|
+
if (eventSource) {
|
|
453
|
+
eventSource.close();
|
|
454
|
+
eventSource = null;
|
|
455
|
+
}
|
|
456
|
+
}
|
|
457
|
+
|
|
458
|
+
export function initSSEClient(base, key) {
|
|
459
|
+
apiBase = base || '';
|
|
460
|
+
apiKey = key || '';
|
|
461
|
+
|
|
462
|
+
// Share config with question-cards module
|
|
463
|
+
configureQuestionCards(apiBase, apiKey);
|
|
464
|
+
configureChat(apiBase, apiKey);
|
|
465
|
+
|
|
466
|
+
// Expose connect for manual invocation from bootstrap
|
|
467
|
+
window.__connectSSE = connect;
|
|
468
|
+
|
|
469
|
+
// Reset completionShown on new runs
|
|
470
|
+
AppState.on('reset', () => {
|
|
471
|
+
completionShown = false;
|
|
472
|
+
});
|
|
473
|
+
}
|