@runcontext/ui 0.5.1 → 0.5.3
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/dist/index.cjs +706 -151
- package/dist/index.cjs.map +1 -1
- package/dist/index.mjs +706 -151
- package/dist/index.mjs.map +1 -1
- package/package.json +11 -8
- package/static/setup.css +1069 -300
- package/static/setup.js +1371 -285
- package/static/uxd.css +672 -0
package/static/setup.js
CHANGED
|
@@ -1,31 +1,134 @@
|
|
|
1
1
|
(function () {
|
|
2
2
|
'use strict';
|
|
3
3
|
|
|
4
|
+
var STORAGE_KEY = 'runcontext_wizard_state';
|
|
5
|
+
var STEP_LABELS = ['Connect', 'Define', 'Scaffold', 'Checkpoint', 'Enrich', 'Serve'];
|
|
6
|
+
|
|
7
|
+
// ---- State persistence ----
|
|
8
|
+
|
|
9
|
+
function loadSavedState() {
|
|
10
|
+
try {
|
|
11
|
+
var saved = sessionStorage.getItem(STORAGE_KEY);
|
|
12
|
+
if (saved) return JSON.parse(saved);
|
|
13
|
+
} catch (e) { /* ignore */ }
|
|
14
|
+
return null;
|
|
15
|
+
}
|
|
16
|
+
|
|
17
|
+
function saveState() {
|
|
18
|
+
try {
|
|
19
|
+
var toSave = {
|
|
20
|
+
step: state.step,
|
|
21
|
+
brief: state.brief,
|
|
22
|
+
sources: state.sources,
|
|
23
|
+
pipelineId: state.pipelineId,
|
|
24
|
+
};
|
|
25
|
+
sessionStorage.setItem(STORAGE_KEY, JSON.stringify(toSave));
|
|
26
|
+
} catch (e) { /* ignore */ }
|
|
27
|
+
}
|
|
28
|
+
|
|
29
|
+
var saved = loadSavedState();
|
|
4
30
|
var state = {
|
|
5
|
-
step: 1,
|
|
6
|
-
brief: {
|
|
31
|
+
step: saved ? saved.step : 1,
|
|
32
|
+
brief: saved ? saved.brief : {
|
|
7
33
|
product_name: '',
|
|
8
34
|
description: '',
|
|
9
35
|
owner: { name: '', team: '', email: '' },
|
|
10
36
|
sensitivity: 'internal',
|
|
11
37
|
docs: [],
|
|
12
38
|
},
|
|
13
|
-
sources: [],
|
|
14
|
-
pipelineId: null,
|
|
39
|
+
sources: saved ? saved.sources : [],
|
|
40
|
+
pipelineId: saved ? saved.pipelineId : null,
|
|
15
41
|
pollTimer: null,
|
|
42
|
+
mcpPollTimer: null,
|
|
16
43
|
};
|
|
17
44
|
|
|
45
|
+
// ---- WebSocket Client ----
|
|
46
|
+
var ws = null;
|
|
47
|
+
var wsSessionId = null;
|
|
48
|
+
|
|
49
|
+
function connectWebSocket(sessionId) {
|
|
50
|
+
wsSessionId = sessionId;
|
|
51
|
+
var protocol = location.protocol === 'https:' ? 'wss:' : 'ws:';
|
|
52
|
+
ws = new WebSocket(protocol + '//' + location.host + '/ws?session=' + encodeURIComponent(sessionId) + '&role=wizard');
|
|
53
|
+
|
|
54
|
+
ws.onmessage = function (evt) {
|
|
55
|
+
try {
|
|
56
|
+
var event = JSON.parse(evt.data);
|
|
57
|
+
handleWsEvent(event);
|
|
58
|
+
} catch (e) { /* ignore */ }
|
|
59
|
+
};
|
|
60
|
+
|
|
61
|
+
ws.onclose = function () {
|
|
62
|
+
setTimeout(function () {
|
|
63
|
+
if (wsSessionId) connectWebSocket(wsSessionId);
|
|
64
|
+
}, 2000);
|
|
65
|
+
};
|
|
66
|
+
|
|
67
|
+
ws.onerror = function () { /* ignore, onclose handles reconnect */ };
|
|
68
|
+
}
|
|
69
|
+
|
|
70
|
+
function sendWsEvent(type, payload) {
|
|
71
|
+
if (ws && ws.readyState === WebSocket.OPEN) {
|
|
72
|
+
ws.send(JSON.stringify({ type: type, sessionId: wsSessionId, payload: payload || {} }));
|
|
73
|
+
}
|
|
74
|
+
}
|
|
75
|
+
|
|
76
|
+
function handleWsEvent(event) {
|
|
77
|
+
switch (event.type) {
|
|
78
|
+
case 'setup:step':
|
|
79
|
+
if (event.payload && event.payload.step) {
|
|
80
|
+
goToStep(event.payload.step);
|
|
81
|
+
}
|
|
82
|
+
break;
|
|
83
|
+
case 'setup:field':
|
|
84
|
+
if (event.payload && event.payload.fieldId) {
|
|
85
|
+
var input = document.getElementById(event.payload.fieldId);
|
|
86
|
+
if (input) {
|
|
87
|
+
input.value = event.payload.value || '';
|
|
88
|
+
input.dispatchEvent(new Event('input'));
|
|
89
|
+
}
|
|
90
|
+
}
|
|
91
|
+
break;
|
|
92
|
+
case 'pipeline:stage':
|
|
93
|
+
updateStageFromWs(event.payload || {});
|
|
94
|
+
break;
|
|
95
|
+
case 'enrich:progress':
|
|
96
|
+
updateEnrichProgress(event.payload || {});
|
|
97
|
+
break;
|
|
98
|
+
case 'enrich:log':
|
|
99
|
+
appendEnrichLog(event.payload || {});
|
|
100
|
+
break;
|
|
101
|
+
case 'tier:update':
|
|
102
|
+
if (event.payload && event.payload.tier) {
|
|
103
|
+
var badge = document.getElementById('tier-badge');
|
|
104
|
+
if (badge) badge.textContent = event.payload.tier;
|
|
105
|
+
}
|
|
106
|
+
break;
|
|
107
|
+
}
|
|
108
|
+
}
|
|
109
|
+
|
|
110
|
+
function updateStageFromWs(payload) {
|
|
111
|
+
// Update stage dots in the scaffold step
|
|
112
|
+
var stageEl = document.querySelector('[data-stage="' + payload.stage + '"]');
|
|
113
|
+
if (!stageEl) return;
|
|
114
|
+
var dot = stageEl.querySelector('.stage-dot');
|
|
115
|
+
if (dot) {
|
|
116
|
+
dot.className = 'stage-dot';
|
|
117
|
+
if (payload.status === 'running') dot.className += ' running';
|
|
118
|
+
else if (payload.status === 'done') dot.className += ' done';
|
|
119
|
+
else if (payload.status === 'error') dot.className += ' error';
|
|
120
|
+
}
|
|
121
|
+
var summary = stageEl.querySelector('.stage-summary');
|
|
122
|
+
if (summary && payload.summary) {
|
|
123
|
+
summary.textContent = payload.summary;
|
|
124
|
+
}
|
|
125
|
+
}
|
|
126
|
+
|
|
18
127
|
// ---- Helpers ----
|
|
19
128
|
|
|
20
129
|
function $(sel) { return document.querySelector(sel); }
|
|
21
130
|
function $$(sel) { return document.querySelectorAll(sel); }
|
|
22
131
|
|
|
23
|
-
function esc(s) {
|
|
24
|
-
var d = document.createElement('div');
|
|
25
|
-
d.textContent = s;
|
|
26
|
-
return d.innerHTML;
|
|
27
|
-
}
|
|
28
|
-
|
|
29
132
|
function showError(fieldId, msg) {
|
|
30
133
|
var field = document.getElementById(fieldId);
|
|
31
134
|
if (!field) return;
|
|
@@ -62,7 +165,7 @@
|
|
|
62
165
|
return ct.includes('json') ? res.json() : res.text();
|
|
63
166
|
}
|
|
64
167
|
|
|
65
|
-
// ---- DOM builder
|
|
168
|
+
// ---- DOM builder helper (avoid innerHTML for security) ----
|
|
66
169
|
|
|
67
170
|
function createElement(tag, attrs, children) {
|
|
68
171
|
var el = document.createElement(tag);
|
|
@@ -70,6 +173,8 @@
|
|
|
70
173
|
Object.keys(attrs).forEach(function (key) {
|
|
71
174
|
if (key === 'className') el.className = attrs[key];
|
|
72
175
|
else if (key === 'textContent') el.textContent = attrs[key];
|
|
176
|
+
else if (key === 'htmlFor') el.htmlFor = attrs[key];
|
|
177
|
+
else if (key === 'type') el.type = attrs[key];
|
|
73
178
|
else el.setAttribute(key, attrs[key]);
|
|
74
179
|
});
|
|
75
180
|
}
|
|
@@ -85,328 +190,1217 @@
|
|
|
85
190
|
return el;
|
|
86
191
|
}
|
|
87
192
|
|
|
193
|
+
// ---- Breadcrumb Stepper ----
|
|
194
|
+
|
|
195
|
+
function renderStepper() {
|
|
196
|
+
var container = document.getElementById('stepper');
|
|
197
|
+
if (!container) return;
|
|
198
|
+
container.textContent = '';
|
|
199
|
+
|
|
200
|
+
STEP_LABELS.forEach(function (label, i) {
|
|
201
|
+
var stepNum = i + 1;
|
|
202
|
+
var span = document.createElement('span');
|
|
203
|
+
span.textContent = label;
|
|
204
|
+
|
|
205
|
+
if (stepNum < state.step) {
|
|
206
|
+
span.className = 'step-completed';
|
|
207
|
+
span.addEventListener('click', function () {
|
|
208
|
+
goToStep(stepNum);
|
|
209
|
+
});
|
|
210
|
+
} else if (stepNum === state.step) {
|
|
211
|
+
span.className = 'step-active';
|
|
212
|
+
} else {
|
|
213
|
+
span.className = 'step-future';
|
|
214
|
+
}
|
|
215
|
+
|
|
216
|
+
container.appendChild(span);
|
|
217
|
+
|
|
218
|
+
if (stepNum < STEP_LABELS.length) {
|
|
219
|
+
var sep = document.createElement('span');
|
|
220
|
+
sep.className = 'step-separator';
|
|
221
|
+
sep.textContent = '>';
|
|
222
|
+
container.appendChild(sep);
|
|
223
|
+
}
|
|
224
|
+
});
|
|
225
|
+
}
|
|
226
|
+
|
|
88
227
|
// ---- Navigation ----
|
|
89
228
|
|
|
90
229
|
function goToStep(n) {
|
|
91
|
-
if (n < 1 || n >
|
|
92
|
-
|
|
93
|
-
|
|
94
|
-
|
|
95
|
-
|
|
96
|
-
var
|
|
97
|
-
if (
|
|
98
|
-
for (var j = n + 1; j <= 5; j++) {
|
|
99
|
-
var fut = $('.progress-step[data-step="' + j + '"]');
|
|
100
|
-
if (fut) { fut.classList.remove('active', 'completed'); }
|
|
101
|
-
}
|
|
102
|
-
$$('.step').forEach(function (el) { el.classList.remove('active'); });
|
|
103
|
-
var panel = $('#step-' + n);
|
|
104
|
-
if (panel) panel.classList.add('active');
|
|
230
|
+
if (n < 1 || n > 6) return;
|
|
231
|
+
|
|
232
|
+
// Clear any running pipeline poll timer when navigating away
|
|
233
|
+
if (state.pollTimer) { clearInterval(state.pollTimer); state.pollTimer = null; }
|
|
234
|
+
|
|
235
|
+
var content = document.getElementById('wizard-content');
|
|
236
|
+
if (content) content.textContent = '';
|
|
105
237
|
|
|
106
238
|
state.step = n;
|
|
239
|
+
saveState();
|
|
107
240
|
|
|
108
|
-
|
|
109
|
-
|
|
110
|
-
|
|
241
|
+
renderStepper();
|
|
242
|
+
|
|
243
|
+
switch (n) {
|
|
244
|
+
case 1: renderConnectStep(); break;
|
|
245
|
+
case 2: renderDefineStep(); break;
|
|
246
|
+
case 3: renderScaffoldStep(); break;
|
|
247
|
+
case 4: renderCheckpointStep(); break;
|
|
248
|
+
case 5: renderEnrichStep(); break;
|
|
249
|
+
case 6: renderServeStep(); break;
|
|
250
|
+
}
|
|
111
251
|
}
|
|
112
252
|
|
|
113
253
|
function validateStep(n) {
|
|
114
254
|
clearErrors();
|
|
115
|
-
|
|
116
|
-
|
|
117
|
-
|
|
118
|
-
|
|
119
|
-
|
|
120
|
-
|
|
121
|
-
|
|
255
|
+
// Step-specific validation will be added as each step is implemented
|
|
256
|
+
return true;
|
|
257
|
+
}
|
|
258
|
+
|
|
259
|
+
// ---- Step action buttons helper ----
|
|
260
|
+
|
|
261
|
+
function createStepActions(showBack, showNext, nextLabel) {
|
|
262
|
+
var actions = createElement('div', { className: 'step-actions' });
|
|
263
|
+
if (showBack) {
|
|
264
|
+
var backBtn = createElement('button', { className: 'btn btn-secondary', textContent: 'Back' });
|
|
265
|
+
backBtn.addEventListener('click', function () {
|
|
266
|
+
goToStep(state.step - 1);
|
|
267
|
+
});
|
|
268
|
+
actions.appendChild(backBtn);
|
|
269
|
+
} else {
|
|
270
|
+
actions.appendChild(createElement('span'));
|
|
122
271
|
}
|
|
123
|
-
if (
|
|
124
|
-
|
|
125
|
-
|
|
126
|
-
|
|
127
|
-
|
|
272
|
+
if (showNext) {
|
|
273
|
+
var nextBtn = createElement('button', { className: 'btn btn-primary', textContent: nextLabel || 'Next' });
|
|
274
|
+
nextBtn.addEventListener('click', function () {
|
|
275
|
+
if (validateStep(state.step)) goToStep(state.step + 1);
|
|
276
|
+
});
|
|
277
|
+
actions.appendChild(nextBtn);
|
|
128
278
|
}
|
|
129
|
-
return
|
|
279
|
+
return actions;
|
|
130
280
|
}
|
|
131
281
|
|
|
132
|
-
// ---- Step
|
|
282
|
+
// ---- Step 1: Connect ----
|
|
133
283
|
|
|
134
|
-
|
|
135
|
-
var
|
|
136
|
-
|
|
137
|
-
|
|
138
|
-
|
|
139
|
-
|
|
140
|
-
|
|
284
|
+
function renderConnectStep() {
|
|
285
|
+
var content = document.getElementById('wizard-content');
|
|
286
|
+
if (!content) return;
|
|
287
|
+
|
|
288
|
+
var card = createElement('div', { className: 'card' });
|
|
289
|
+
|
|
290
|
+
card.appendChild(createElement('h2', { className: 'connect-heading', textContent: 'Connect Your Database' }));
|
|
291
|
+
card.appendChild(createElement('p', { className: 'connect-subheading', textContent: 'RunContext connects directly to your database via OAuth \u2014 your credentials never pass through the AI agent.' }));
|
|
292
|
+
|
|
293
|
+
// Detected databases hint (from MCP/IDE configs)
|
|
294
|
+
var detectedHint = createElement('div', { id: 'connect-detected-hint' });
|
|
295
|
+
card.appendChild(detectedHint);
|
|
296
|
+
|
|
297
|
+
// OAuth result area — positioned right after detected hint so db cards appear here
|
|
298
|
+
var oauthResult = createElement('div', { id: 'connect-oauth-result' });
|
|
299
|
+
card.appendChild(oauthResult);
|
|
300
|
+
|
|
301
|
+
// Platform picker grid (populated after fetching providers)
|
|
302
|
+
var platformGrid = createElement('div', { className: 'platform-grid', id: 'connect-platforms' });
|
|
303
|
+
platformGrid.appendChild(createElement('p', { className: 'muted', textContent: 'Loading providers\u2026' }));
|
|
304
|
+
card.appendChild(platformGrid);
|
|
305
|
+
|
|
306
|
+
// Manual connection string
|
|
307
|
+
var manual = createElement('div', { className: 'manual-connect' });
|
|
308
|
+
manual.appendChild(createElement('label', { className: 'label-uppercase', textContent: 'Manual Connection' }));
|
|
309
|
+
var manualRow = createElement('div', { className: 'manual-connect-row' });
|
|
310
|
+
var connInput = createElement('input', {
|
|
311
|
+
className: 'input',
|
|
312
|
+
id: 'connect-url',
|
|
313
|
+
type: 'text',
|
|
314
|
+
placeholder: 'postgres://user:pass@host:5432/dbname',
|
|
315
|
+
});
|
|
316
|
+
manualRow.appendChild(connInput);
|
|
317
|
+
var connBtn = createElement('button', { className: 'btn btn-primary', textContent: 'Connect' });
|
|
318
|
+
manualRow.appendChild(connBtn);
|
|
319
|
+
manual.appendChild(manualRow);
|
|
320
|
+
card.appendChild(manual);
|
|
321
|
+
|
|
322
|
+
content.appendChild(card);
|
|
323
|
+
|
|
324
|
+
// --- Detect sources first, then render providers (detected ones get highlighted) ---
|
|
325
|
+
fetchDetectedSources(detectedHint, platformGrid, oauthResult).then(function () {
|
|
326
|
+
fetchAuthProviders(platformGrid, oauthResult);
|
|
327
|
+
});
|
|
328
|
+
|
|
329
|
+
// --- Manual connect handler ---
|
|
330
|
+
connBtn.addEventListener('click', async function () {
|
|
331
|
+
var url = connInput.value.trim();
|
|
332
|
+
if (!url) return;
|
|
333
|
+
connBtn.textContent = 'Connecting\u2026';
|
|
334
|
+
connBtn.disabled = true;
|
|
335
|
+
try {
|
|
336
|
+
var result = await api('POST', '/api/sources', { connection: url });
|
|
337
|
+
var src = result.source || result;
|
|
338
|
+
state.sources = state.sources || [];
|
|
339
|
+
state.sources.push(src);
|
|
340
|
+
saveState();
|
|
341
|
+
updateDbStatus(src);
|
|
342
|
+
goToStep(2);
|
|
343
|
+
} catch (e) {
|
|
344
|
+
connBtn.textContent = 'Connect';
|
|
345
|
+
connBtn.disabled = false;
|
|
346
|
+
var errP = manual.querySelector('.field-error');
|
|
347
|
+
if (errP) errP.remove();
|
|
348
|
+
manual.appendChild(createElement('p', { className: 'field-error', textContent: e.message || 'Connection failed' }));
|
|
349
|
+
}
|
|
350
|
+
});
|
|
351
|
+
}
|
|
352
|
+
|
|
353
|
+
/** Map adapter type to the provider ID for OAuth */
|
|
354
|
+
var ADAPTER_TO_PROVIDER = {
|
|
355
|
+
postgres: 'neon', // default; detected sources refine below
|
|
356
|
+
mysql: 'planetscale',
|
|
357
|
+
duckdb: null, // local file, no OAuth needed
|
|
358
|
+
sqlite: null,
|
|
359
|
+
snowflake: 'snowflake',
|
|
360
|
+
bigquery: 'gcp',
|
|
361
|
+
clickhouse: 'clickhouse',
|
|
362
|
+
databricks: 'databricks',
|
|
363
|
+
mssql: 'azure-sql',
|
|
364
|
+
mongodb: 'mongodb',
|
|
365
|
+
};
|
|
366
|
+
|
|
367
|
+
/** Refine provider from origin hint (e.g. "mcp:claude-code/neon" → neon) */
|
|
368
|
+
function providerFromOrigin(origin, adapter) {
|
|
369
|
+
if (!origin) return ADAPTER_TO_PROVIDER[adapter] || null;
|
|
370
|
+
var o = origin.toLowerCase();
|
|
371
|
+
if (o.includes('neon')) return 'neon';
|
|
372
|
+
if (o.includes('supabase')) return 'supabase';
|
|
373
|
+
if (o.includes('aws') || o.includes('rds')) return 'aws-rds';
|
|
374
|
+
if (o.includes('azure')) return 'azure-sql';
|
|
375
|
+
if (o.includes('gcp') || o.includes('bigquery')) return 'gcp';
|
|
376
|
+
if (o.includes('planetscale')) return 'planetscale';
|
|
377
|
+
if (o.includes('cockroach')) return 'cockroachdb';
|
|
378
|
+
if (o.includes('snowflake')) return 'snowflake';
|
|
379
|
+
if (o.includes('clickhouse')) return 'clickhouse';
|
|
380
|
+
if (o.includes('databricks')) return 'databricks';
|
|
381
|
+
if (o.includes('mongodb') || o.includes('atlas')) return 'mongodb';
|
|
382
|
+
if (o.includes('motherduck') || o.includes('duckdb')) return null;
|
|
383
|
+
return ADAPTER_TO_PROVIDER[adapter] || null;
|
|
384
|
+
}
|
|
385
|
+
|
|
386
|
+
function fetchDetectedSources(container, platformGrid, oauthResult) {
|
|
387
|
+
return api('GET', '/api/sources').then(function (data) {
|
|
388
|
+
var sources = data.sources || data || [];
|
|
141
389
|
container.textContent = '';
|
|
142
|
-
if (
|
|
143
|
-
|
|
390
|
+
if (sources.length === 0) {
|
|
391
|
+
updateDbStatus(null);
|
|
144
392
|
return;
|
|
145
393
|
}
|
|
146
|
-
|
|
147
|
-
|
|
148
|
-
|
|
149
|
-
|
|
150
|
-
|
|
394
|
+
|
|
395
|
+
// Group detected sources by provider
|
|
396
|
+
var byProvider = {};
|
|
397
|
+
var localFiles = [];
|
|
398
|
+
sources.forEach(function (src) {
|
|
399
|
+
var prov = providerFromOrigin(src.origin, src.adapter);
|
|
400
|
+
if (!prov) {
|
|
401
|
+
localFiles.push(src);
|
|
402
|
+
} else {
|
|
403
|
+
byProvider[prov] = byProvider[prov] || [];
|
|
404
|
+
byProvider[prov].push(src);
|
|
405
|
+
}
|
|
406
|
+
});
|
|
407
|
+
|
|
408
|
+
// Show detected hint banner
|
|
409
|
+
var hint = createElement('div', { className: 'detected-hint' });
|
|
410
|
+
var hintIcon = createElement('span', { className: 'detected-hint-icon', textContent: '\u{1F50D}' });
|
|
411
|
+
var provNames = Object.keys(byProvider);
|
|
412
|
+
var hintText;
|
|
413
|
+
if (provNames.length > 0) {
|
|
414
|
+
var names = provNames.map(function (p) { return p.charAt(0).toUpperCase() + p.slice(1); });
|
|
415
|
+
hintText = 'We detected ' + names.join(', ') + ' in your IDE configs. Click a provider below to connect securely via OAuth.';
|
|
416
|
+
} else {
|
|
417
|
+
hintText = 'We detected local database files. Use manual connection or select a provider below.';
|
|
418
|
+
}
|
|
419
|
+
hint.appendChild(hintIcon);
|
|
420
|
+
hint.appendChild(createElement('span', { textContent: hintText }));
|
|
421
|
+
container.appendChild(hint);
|
|
422
|
+
|
|
423
|
+
// For local files (duckdb, sqlite), show direct-use cards
|
|
424
|
+
localFiles.forEach(function (src) {
|
|
425
|
+
var card = createElement('div', { className: 'source-card source-card-local' }, [
|
|
426
|
+
createElement('span', { className: 'source-card-name', textContent: src.name || src.adapter }),
|
|
427
|
+
createElement('span', { className: 'source-card-meta', textContent: 'Local file' }),
|
|
428
|
+
createElement('button', { className: 'btn btn-primary', textContent: 'Use This' }),
|
|
151
429
|
]);
|
|
152
|
-
card.addEventListener('click', function () {
|
|
153
|
-
|
|
154
|
-
|
|
155
|
-
|
|
430
|
+
card.querySelector('.btn').addEventListener('click', function () {
|
|
431
|
+
state.sources = state.sources || [];
|
|
432
|
+
state.sources.push(src);
|
|
433
|
+
saveState();
|
|
434
|
+
updateDbStatus(src);
|
|
435
|
+
goToStep(2);
|
|
156
436
|
});
|
|
157
437
|
container.appendChild(card);
|
|
158
438
|
});
|
|
159
|
-
|
|
160
|
-
|
|
161
|
-
|
|
162
|
-
|
|
439
|
+
|
|
440
|
+
// Highlight matching providers in the platform grid
|
|
441
|
+
state._detectedProviders = provNames;
|
|
442
|
+
|
|
443
|
+
}).catch(function () {
|
|
444
|
+
container.textContent = '';
|
|
445
|
+
updateDbStatus(null);
|
|
446
|
+
});
|
|
447
|
+
}
|
|
448
|
+
|
|
449
|
+
function fetchAuthProviders(container, oauthResult) {
|
|
450
|
+
api('GET', '/api/auth/providers').then(function (data) {
|
|
451
|
+
var providers = data.providers || data || [];
|
|
452
|
+
// Replace the platform-grid with a plain wrapper for mixed content
|
|
453
|
+
var wrapper = createElement('div', { className: 'connect-providers' });
|
|
454
|
+
container.replaceWith(wrapper);
|
|
455
|
+
|
|
456
|
+
if (providers.length === 0) {
|
|
457
|
+
wrapper.appendChild(createElement('p', { className: 'muted', textContent: 'No OAuth providers available.' }));
|
|
458
|
+
return;
|
|
163
459
|
}
|
|
164
|
-
|
|
460
|
+
|
|
461
|
+
var detected = state._detectedProviders || [];
|
|
462
|
+
|
|
463
|
+
// Show detected providers first (highlighted)
|
|
464
|
+
var detectedProvs = [];
|
|
465
|
+
var otherProvs = [];
|
|
466
|
+
providers.forEach(function (prov) {
|
|
467
|
+
if (detected.indexOf(prov.id) !== -1) {
|
|
468
|
+
detectedProvs.push(prov);
|
|
469
|
+
} else {
|
|
470
|
+
otherProvs.push(prov);
|
|
471
|
+
}
|
|
472
|
+
});
|
|
473
|
+
|
|
474
|
+
// Detected providers get prominent cards
|
|
475
|
+
if (detectedProvs.length > 0) {
|
|
476
|
+
var detectedGrid = createElement('div', { className: 'source-cards' });
|
|
477
|
+
detectedProvs.forEach(function (prov) {
|
|
478
|
+
var card = createElement('div', { className: 'source-card source-card-detected' }, [
|
|
479
|
+
createElement('span', { className: 'source-card-badge', textContent: 'Detected' }),
|
|
480
|
+
createElement('span', { className: 'source-card-name', textContent: prov.displayName || prov.display_name || prov.id }),
|
|
481
|
+
createElement('span', { className: 'source-card-meta', textContent: (prov.cliAuthenticated ? 'CLI authenticated' : prov.cliInstalled ? 'CLI installed' : 'OAuth available') }),
|
|
482
|
+
createElement('button', { className: 'btn btn-primary', textContent: 'Connect via OAuth' }),
|
|
483
|
+
]);
|
|
484
|
+
card.querySelector('.btn').addEventListener('click', function () {
|
|
485
|
+
startOAuthFlow(prov, wrapper, oauthResult);
|
|
486
|
+
});
|
|
487
|
+
detectedGrid.appendChild(card);
|
|
488
|
+
});
|
|
489
|
+
wrapper.appendChild(detectedGrid);
|
|
490
|
+
|
|
491
|
+
if (otherProvs.length > 0) {
|
|
492
|
+
wrapper.appendChild(createElement('div', { className: 'section-divider' }, ['Other providers']));
|
|
493
|
+
}
|
|
494
|
+
}
|
|
495
|
+
|
|
496
|
+
// Other providers as button grid
|
|
497
|
+
if (otherProvs.length > 0) {
|
|
498
|
+
var grid = createElement('div', { className: 'platform-grid' });
|
|
499
|
+
otherProvs.forEach(function (prov) {
|
|
500
|
+
var btn = createElement('button', { className: 'platform-btn', textContent: prov.displayName || prov.display_name || prov.name || prov.id });
|
|
501
|
+
btn.addEventListener('click', function () {
|
|
502
|
+
startOAuthFlow(prov, wrapper, oauthResult);
|
|
503
|
+
});
|
|
504
|
+
grid.appendChild(btn);
|
|
505
|
+
});
|
|
506
|
+
wrapper.appendChild(grid);
|
|
507
|
+
}
|
|
508
|
+
}).catch(function () {
|
|
165
509
|
container.textContent = '';
|
|
166
|
-
container.appendChild(createElement('p', { className: 'muted', textContent: 'Could not
|
|
510
|
+
container.appendChild(createElement('p', { className: 'muted', textContent: 'Could not load providers.' }));
|
|
511
|
+
});
|
|
512
|
+
}
|
|
513
|
+
|
|
514
|
+
async function startOAuthFlow(provider, providerWrapper, oauthResult) {
|
|
515
|
+
// Disable all buttons
|
|
516
|
+
providerWrapper.querySelectorAll('.platform-btn').forEach(function (b) { b.disabled = true; });
|
|
517
|
+
providerWrapper.querySelectorAll('.source-card .btn').forEach(function (b) { b.disabled = true; });
|
|
518
|
+
|
|
519
|
+
// Show loading in the oauth result area (right after detected cards)
|
|
520
|
+
oauthResult.textContent = '';
|
|
521
|
+
oauthResult.appendChild(createElement('p', { className: 'muted', textContent: 'Connecting to ' + (provider.displayName || provider.display_name || provider.id) + '\u2026 A browser window may open for authentication.' }));
|
|
522
|
+
|
|
523
|
+
try {
|
|
524
|
+
var data = await api('POST', '/api/auth/start', { provider: provider.id });
|
|
525
|
+
var databases = data.databases || data || [];
|
|
526
|
+
oauthResult.textContent = '';
|
|
527
|
+
|
|
528
|
+
if (databases.length === 0) {
|
|
529
|
+
oauthResult.appendChild(createElement('p', { className: 'muted', textContent: 'No databases found for this provider.' }));
|
|
530
|
+
providerWrapper.querySelectorAll('.platform-btn').forEach(function (b) { b.disabled = false; });
|
|
531
|
+
providerWrapper.querySelectorAll('.source-card .btn').forEach(function (b) { b.disabled = false; });
|
|
532
|
+
return;
|
|
533
|
+
}
|
|
534
|
+
|
|
535
|
+
// Hide the other providers / platform grid — show only database results
|
|
536
|
+
providerWrapper.style.display = 'none';
|
|
537
|
+
|
|
538
|
+
var oauthHeader = createElement('div', { className: 'oauth-result-header' });
|
|
539
|
+
oauthHeader.appendChild(createElement('label', { className: 'label-uppercase', textContent: 'Select a database from ' + (provider.displayName || provider.display_name || provider.id) }));
|
|
540
|
+
var backLink = createElement('button', { className: 'btn btn-secondary btn-sm', textContent: '\u2190 Back to providers' });
|
|
541
|
+
backLink.addEventListener('click', function () {
|
|
542
|
+
oauthResult.textContent = '';
|
|
543
|
+
providerWrapper.style.display = '';
|
|
544
|
+
providerWrapper.querySelectorAll('.platform-btn').forEach(function (b) { b.disabled = false; });
|
|
545
|
+
providerWrapper.querySelectorAll('.source-card .btn').forEach(function (b) { b.disabled = false; });
|
|
546
|
+
});
|
|
547
|
+
oauthHeader.appendChild(backLink);
|
|
548
|
+
oauthResult.appendChild(oauthHeader);
|
|
549
|
+
|
|
550
|
+
var dbGrid = createElement('div', { className: 'source-cards' });
|
|
551
|
+
databases.forEach(function (db) {
|
|
552
|
+
var m = db.metadata || {};
|
|
553
|
+
// Title: "project / branch" if available, else just db name
|
|
554
|
+
var title = m.project ? m.project + ' / ' + (m.branch || 'main') : (db.name || db.database);
|
|
555
|
+
// Subtitle line 1: db name + adapter + region + org
|
|
556
|
+
var parts = [db.name || db.database];
|
|
557
|
+
if (db.adapter) parts.push(db.adapter);
|
|
558
|
+
if (m.region) parts.push(m.region);
|
|
559
|
+
if (m.org && m.org !== 'Personal') parts.push(m.org);
|
|
560
|
+
else if (m.org === 'Personal') parts.push('personal');
|
|
561
|
+
var line1 = parts.join(' \u2022 ');
|
|
562
|
+
// Subtitle line 2: host (truncated)
|
|
563
|
+
var line2 = db.host || '';
|
|
564
|
+
|
|
565
|
+
var dbCard = createElement('div', { className: 'source-card' }, [
|
|
566
|
+
createElement('span', { className: 'source-card-name', textContent: title }),
|
|
567
|
+
createElement('span', { className: 'source-card-meta', textContent: line1 }),
|
|
568
|
+
line2 ? createElement('span', { className: 'source-card-host', textContent: line2 }) : null,
|
|
569
|
+
createElement('button', { className: 'btn btn-primary', textContent: 'Use This' }),
|
|
570
|
+
].filter(Boolean));
|
|
571
|
+
dbCard.querySelector('.btn').addEventListener('click', async function () {
|
|
572
|
+
try {
|
|
573
|
+
await api('POST', '/api/auth/select-db', { provider: provider.id, database: db });
|
|
574
|
+
state.sources = state.sources || [];
|
|
575
|
+
state.sources.push(db);
|
|
576
|
+
saveState();
|
|
577
|
+
updateDbStatus(db);
|
|
578
|
+
goToStep(2);
|
|
579
|
+
} catch (e) {
|
|
580
|
+
oauthResult.appendChild(createElement('p', { className: 'field-error', textContent: e.message || 'Failed to select database' }));
|
|
581
|
+
}
|
|
582
|
+
});
|
|
583
|
+
dbGrid.appendChild(dbCard);
|
|
584
|
+
});
|
|
585
|
+
oauthResult.appendChild(dbGrid);
|
|
586
|
+
} catch (e) {
|
|
587
|
+
oauthResult.textContent = '';
|
|
588
|
+
oauthResult.appendChild(createElement('p', { className: 'field-error', textContent: e.message || 'OAuth flow failed' }));
|
|
589
|
+
providerWrapper.querySelectorAll('.platform-btn').forEach(function (b) { b.disabled = false; });
|
|
590
|
+
providerWrapper.querySelectorAll('.source-card .btn').forEach(function (b) { b.disabled = false; });
|
|
167
591
|
}
|
|
168
592
|
}
|
|
169
593
|
|
|
170
|
-
|
|
171
|
-
var area = $('#upload-area');
|
|
172
|
-
var input = $('#file-input');
|
|
173
|
-
if (!area || !input) return;
|
|
594
|
+
// ---- Step 2: Define ----
|
|
174
595
|
|
|
175
|
-
|
|
596
|
+
function renderDefineStep() {
|
|
597
|
+
var content = document.getElementById('wizard-content');
|
|
598
|
+
if (!content) return;
|
|
176
599
|
|
|
177
|
-
|
|
178
|
-
|
|
179
|
-
|
|
180
|
-
});
|
|
181
|
-
|
|
182
|
-
|
|
183
|
-
|
|
184
|
-
|
|
185
|
-
|
|
186
|
-
|
|
187
|
-
|
|
600
|
+
var card = createElement('div', { className: 'card' });
|
|
601
|
+
|
|
602
|
+
card.appendChild(createElement('h2', { textContent: 'Define Your Data Product' }));
|
|
603
|
+
card.appendChild(createElement('p', { className: 'muted', textContent: 'Tell us about your data product. This metadata helps AI agents understand what they\u2019re working with.' }));
|
|
604
|
+
|
|
605
|
+
var form = createElement('div', { className: 'define-form' });
|
|
606
|
+
|
|
607
|
+
// Product Name (full width, required)
|
|
608
|
+
var nameGroup = createElement('div', { className: 'field full-width' });
|
|
609
|
+
nameGroup.appendChild(createElement('label', { htmlFor: 'product_name', textContent: 'Product Name *' }));
|
|
610
|
+
nameGroup.appendChild(createElement('input', {
|
|
611
|
+
className: 'input',
|
|
612
|
+
id: 'product_name',
|
|
613
|
+
type: 'text',
|
|
614
|
+
placeholder: 'my-data-product',
|
|
615
|
+
}));
|
|
616
|
+
nameGroup.appendChild(createElement('p', { className: 'hint', textContent: 'Alphanumeric, hyphens, and underscores only.' }));
|
|
617
|
+
form.appendChild(nameGroup);
|
|
618
|
+
|
|
619
|
+
// Description (full width, required)
|
|
620
|
+
var descGroup = createElement('div', { className: 'field full-width' });
|
|
621
|
+
descGroup.appendChild(createElement('label', { htmlFor: 'description', textContent: 'Description *' }));
|
|
622
|
+
var descInput = createElement('textarea', {
|
|
623
|
+
className: 'textarea',
|
|
624
|
+
id: 'description',
|
|
625
|
+
placeholder: 'What does this data product provide?',
|
|
188
626
|
});
|
|
627
|
+
descGroup.appendChild(descInput);
|
|
628
|
+
form.appendChild(descGroup);
|
|
629
|
+
|
|
630
|
+
// Owner Name (left column)
|
|
631
|
+
var ownerNameGroup = createElement('div', { className: 'field' });
|
|
632
|
+
ownerNameGroup.appendChild(createElement('label', { htmlFor: 'owner_name', textContent: 'Owner Name' }));
|
|
633
|
+
ownerNameGroup.appendChild(createElement('input', {
|
|
634
|
+
className: 'input',
|
|
635
|
+
id: 'owner_name',
|
|
636
|
+
type: 'text',
|
|
637
|
+
placeholder: 'Jane Doe',
|
|
638
|
+
}));
|
|
639
|
+
form.appendChild(ownerNameGroup);
|
|
640
|
+
|
|
641
|
+
// Team (right column)
|
|
642
|
+
var teamGroup = createElement('div', { className: 'field' });
|
|
643
|
+
teamGroup.appendChild(createElement('label', { htmlFor: 'owner_team', textContent: 'Team' }));
|
|
644
|
+
teamGroup.appendChild(createElement('input', {
|
|
645
|
+
className: 'input',
|
|
646
|
+
id: 'owner_team',
|
|
647
|
+
type: 'text',
|
|
648
|
+
placeholder: 'Data Engineering',
|
|
649
|
+
}));
|
|
650
|
+
form.appendChild(teamGroup);
|
|
189
651
|
|
|
190
|
-
|
|
191
|
-
|
|
192
|
-
|
|
652
|
+
// Email (left column)
|
|
653
|
+
var emailGroup = createElement('div', { className: 'field' });
|
|
654
|
+
emailGroup.appendChild(createElement('label', { htmlFor: 'owner_email', textContent: 'Email' }));
|
|
655
|
+
emailGroup.appendChild(createElement('input', {
|
|
656
|
+
className: 'input',
|
|
657
|
+
id: 'owner_email',
|
|
658
|
+
type: 'text',
|
|
659
|
+
placeholder: 'jane@example.com',
|
|
660
|
+
}));
|
|
661
|
+
form.appendChild(emailGroup);
|
|
662
|
+
|
|
663
|
+
// Sensitivity (right column)
|
|
664
|
+
var sensGroup = createElement('div', { className: 'field' });
|
|
665
|
+
sensGroup.appendChild(createElement('label', { htmlFor: 'sensitivity', textContent: 'Sensitivity' }));
|
|
666
|
+
var sensSelect = createElement('select', { className: 'select', id: 'sensitivity' });
|
|
667
|
+
[
|
|
668
|
+
{ value: 'public', text: 'Public' },
|
|
669
|
+
{ value: 'internal', text: 'Internal' },
|
|
670
|
+
{ value: 'confidential', text: 'Confidential' },
|
|
671
|
+
{ value: 'restricted', text: 'Restricted' },
|
|
672
|
+
].forEach(function (opt) {
|
|
673
|
+
var option = createElement('option', { value: opt.value, textContent: opt.text });
|
|
674
|
+
sensSelect.appendChild(option);
|
|
193
675
|
});
|
|
194
|
-
|
|
676
|
+
sensSelect.value = 'internal';
|
|
677
|
+
sensGroup.appendChild(sensSelect);
|
|
678
|
+
form.appendChild(sensGroup);
|
|
679
|
+
|
|
680
|
+
// Action buttons (full width)
|
|
681
|
+
var actions = createElement('div', { className: 'define-actions' });
|
|
682
|
+
|
|
683
|
+
var backBtn = createElement('button', { className: 'btn btn-secondary', textContent: 'Back' });
|
|
684
|
+
backBtn.addEventListener('click', function () { goToStep(1); });
|
|
685
|
+
actions.appendChild(backBtn);
|
|
686
|
+
|
|
687
|
+
var continueBtn = createElement('button', { className: 'btn btn-primary', textContent: 'Continue' });
|
|
688
|
+
continueBtn.addEventListener('click', async function () {
|
|
689
|
+
clearErrors();
|
|
690
|
+
var valid = true;
|
|
691
|
+
|
|
692
|
+
var productName = document.getElementById('product_name').value.trim();
|
|
693
|
+
var description = document.getElementById('description').value.trim();
|
|
694
|
+
|
|
695
|
+
if (!productName) {
|
|
696
|
+
showError('product_name', 'Product name is required.');
|
|
697
|
+
valid = false;
|
|
698
|
+
} else if (!/^[a-zA-Z0-9_-]+$/.test(productName)) {
|
|
699
|
+
showError('product_name', 'Only letters, numbers, hyphens, and underscores allowed.');
|
|
700
|
+
valid = false;
|
|
701
|
+
}
|
|
702
|
+
|
|
703
|
+
if (!description) {
|
|
704
|
+
showError('description', 'Description is required.');
|
|
705
|
+
valid = false;
|
|
706
|
+
}
|
|
707
|
+
|
|
708
|
+
if (!valid) return;
|
|
709
|
+
|
|
710
|
+
state.brief.product_name = productName;
|
|
711
|
+
state.brief.description = description;
|
|
712
|
+
state.brief.owner.name = document.getElementById('owner_name').value.trim();
|
|
713
|
+
state.brief.owner.team = document.getElementById('owner_team').value.trim();
|
|
714
|
+
state.brief.owner.email = document.getElementById('owner_email').value.trim();
|
|
715
|
+
state.brief.sensitivity = document.getElementById('sensitivity').value;
|
|
716
|
+
saveState();
|
|
195
717
|
|
|
196
|
-
|
|
197
|
-
|
|
198
|
-
for (var i = 0; i < files.length; i++) {
|
|
199
|
-
var file = files[i];
|
|
200
|
-
var fd = new FormData();
|
|
201
|
-
fd.append('file', file);
|
|
202
|
-
addFileRow(file.name, 'uploading...');
|
|
718
|
+
continueBtn.textContent = 'Saving\u2026';
|
|
719
|
+
continueBtn.disabled = true;
|
|
203
720
|
try {
|
|
204
|
-
await api('POST', '/api/
|
|
205
|
-
|
|
206
|
-
state.brief.docs.push(file.name);
|
|
721
|
+
await api('POST', '/api/brief', state.brief);
|
|
722
|
+
goToStep(3);
|
|
207
723
|
} catch (e) {
|
|
208
|
-
|
|
724
|
+
continueBtn.textContent = 'Continue';
|
|
725
|
+
continueBtn.disabled = false;
|
|
726
|
+
var errP = createElement('p', { className: 'field-error', textContent: e.message || 'Failed to save. Please try again.' });
|
|
727
|
+
actions.appendChild(errP);
|
|
209
728
|
}
|
|
210
|
-
}
|
|
211
|
-
|
|
729
|
+
});
|
|
730
|
+
actions.appendChild(continueBtn);
|
|
731
|
+
form.appendChild(actions);
|
|
212
732
|
|
|
213
|
-
|
|
214
|
-
|
|
215
|
-
|
|
216
|
-
|
|
217
|
-
|
|
218
|
-
|
|
219
|
-
|
|
220
|
-
|
|
733
|
+
card.appendChild(form);
|
|
734
|
+
content.appendChild(card);
|
|
735
|
+
|
|
736
|
+
// Pre-fill from saved state first
|
|
737
|
+
if (state.brief.product_name) document.getElementById('product_name').value = state.brief.product_name;
|
|
738
|
+
if (state.brief.description) document.getElementById('description').value = state.brief.description;
|
|
739
|
+
if (state.brief.owner.name) document.getElementById('owner_name').value = state.brief.owner.name;
|
|
740
|
+
if (state.brief.owner.team) document.getElementById('owner_team').value = state.brief.owner.team;
|
|
741
|
+
if (state.brief.owner.email) document.getElementById('owner_email').value = state.brief.owner.email;
|
|
742
|
+
if (state.brief.sensitivity) document.getElementById('sensitivity').value = state.brief.sensitivity;
|
|
221
743
|
|
|
222
|
-
|
|
223
|
-
var
|
|
224
|
-
if (
|
|
225
|
-
var
|
|
226
|
-
|
|
744
|
+
// Auto-suggest from selected source (if fields are still empty)
|
|
745
|
+
var hasAnyField = state.brief.product_name || state.brief.description || state.brief.owner.name;
|
|
746
|
+
if (!hasAnyField && state.sources && state.sources.length > 0) {
|
|
747
|
+
var suggestNote = createElement('p', { className: 'muted suggest-loading', textContent: 'Auto-filling from your database\u2026' });
|
|
748
|
+
card.insertBefore(suggestNote, form);
|
|
749
|
+
|
|
750
|
+
api('POST', '/api/suggest-brief', { source: state.sources[0] }).then(function (data) {
|
|
751
|
+
suggestNote.remove();
|
|
752
|
+
// Only fill empty fields
|
|
753
|
+
var fields = [
|
|
754
|
+
{ id: 'product_name', val: data.product_name, stateKey: 'product_name' },
|
|
755
|
+
{ id: 'description', val: data.description, stateKey: 'description' },
|
|
756
|
+
{ id: 'owner_name', val: data.owner && data.owner.name, stateKey: null },
|
|
757
|
+
{ id: 'owner_team', val: data.owner && data.owner.team, stateKey: null },
|
|
758
|
+
{ id: 'owner_email', val: data.owner && data.owner.email, stateKey: null },
|
|
759
|
+
{ id: 'sensitivity', val: data.sensitivity, stateKey: 'sensitivity' },
|
|
760
|
+
];
|
|
761
|
+
fields.forEach(function (f) {
|
|
762
|
+
var el = document.getElementById(f.id);
|
|
763
|
+
if (el && !el.value && f.val) {
|
|
764
|
+
el.value = f.val;
|
|
765
|
+
}
|
|
766
|
+
});
|
|
767
|
+
}).catch(function () {
|
|
768
|
+
suggestNote.textContent = '';
|
|
769
|
+
});
|
|
227
770
|
}
|
|
228
771
|
}
|
|
229
772
|
|
|
230
|
-
// ----
|
|
773
|
+
// ---- Step 3: Scaffold ----
|
|
231
774
|
|
|
232
|
-
|
|
233
|
-
|
|
234
|
-
|
|
235
|
-
|
|
236
|
-
|
|
237
|
-
|
|
238
|
-
|
|
775
|
+
var SCAFFOLD_STAGES = [
|
|
776
|
+
{ key: 'introspect', label: 'Extracting schema from database...' },
|
|
777
|
+
{ key: 'scaffold', label: 'Building semantic plane files...' },
|
|
778
|
+
{ key: 'verify', label: 'Validating semantic plane...' },
|
|
779
|
+
{ key: 'autofix', label: 'Fixing any issues...' },
|
|
780
|
+
{ key: 'agent-instructions', label: 'Generating agent instructions...' },
|
|
781
|
+
];
|
|
782
|
+
|
|
783
|
+
function renderScaffoldStep() {
|
|
784
|
+
var content = document.getElementById('wizard-content');
|
|
785
|
+
if (!content) return;
|
|
786
|
+
|
|
787
|
+
var card = createElement('div', { className: 'card' });
|
|
788
|
+
|
|
789
|
+
card.appendChild(createElement('h2', { textContent: 'Building Your Semantic Plane' }));
|
|
790
|
+
card.appendChild(createElement('p', { className: 'muted', textContent: 'Connecting to your database and extracting schema metadata. This creates a Bronze-tier semantic plane.' }));
|
|
791
|
+
|
|
792
|
+
// Stage rows container
|
|
793
|
+
var stagesContainer = createElement('div', { className: 'scaffold-stages', id: 'scaffold-stages' });
|
|
794
|
+
SCAFFOLD_STAGES.forEach(function (stage) {
|
|
795
|
+
var row = createElement('div', { className: 'stage-row', id: 'stage-' + stage.key, 'data-stage': stage.key }, [
|
|
796
|
+
createElement('span', { className: 'stage-dot' }),
|
|
797
|
+
createElement('span', { className: 'stage-name', textContent: stage.label }),
|
|
798
|
+
]);
|
|
799
|
+
stagesContainer.appendChild(row);
|
|
239
800
|
});
|
|
801
|
+
card.appendChild(stagesContainer);
|
|
802
|
+
|
|
803
|
+
// Error area
|
|
804
|
+
var errorArea = createElement('div', { id: 'scaffold-error' });
|
|
805
|
+
card.appendChild(errorArea);
|
|
806
|
+
|
|
807
|
+
// Action area (Start / Retry button)
|
|
808
|
+
var actionArea = createElement('div', { className: 'step-actions', id: 'scaffold-actions' });
|
|
809
|
+
|
|
810
|
+
var backBtn = createElement('button', { className: 'btn btn-secondary', textContent: 'Back' });
|
|
811
|
+
backBtn.addEventListener('click', function () { goToStep(2); });
|
|
812
|
+
actionArea.appendChild(backBtn);
|
|
813
|
+
|
|
814
|
+
// If we already have a pipelineId, resume polling; otherwise show Start button
|
|
815
|
+
if (state.pipelineId) {
|
|
816
|
+
actionArea.appendChild(createElement('span', { className: 'muted', textContent: 'Build in progress...' }));
|
|
817
|
+
card.appendChild(actionArea);
|
|
818
|
+
content.appendChild(card);
|
|
819
|
+
startScaffoldPolling();
|
|
820
|
+
} else {
|
|
821
|
+
var startBtn = createElement('button', { className: 'btn btn-primary', textContent: 'Start Build', id: 'scaffold-start-btn' });
|
|
822
|
+
startBtn.addEventListener('click', function () { startScaffoldBuild(startBtn); });
|
|
823
|
+
actionArea.appendChild(startBtn);
|
|
824
|
+
card.appendChild(actionArea);
|
|
825
|
+
content.appendChild(card);
|
|
826
|
+
}
|
|
240
827
|
}
|
|
241
828
|
|
|
242
|
-
|
|
829
|
+
async function startScaffoldBuild(btn) {
|
|
830
|
+
btn.textContent = 'Starting...';
|
|
831
|
+
btn.disabled = true;
|
|
832
|
+
|
|
833
|
+
var errorArea = document.getElementById('scaffold-error');
|
|
834
|
+
if (errorArea) errorArea.textContent = '';
|
|
243
835
|
|
|
244
|
-
|
|
245
|
-
|
|
246
|
-
|
|
247
|
-
|
|
248
|
-
if (
|
|
249
|
-
|
|
250
|
-
return;
|
|
836
|
+
var body = {
|
|
837
|
+
productName: state.brief.product_name,
|
|
838
|
+
targetTier: 'bronze',
|
|
839
|
+
};
|
|
840
|
+
if (state.sources && state.sources[0]) {
|
|
841
|
+
body.dataSource = state.sources[0];
|
|
251
842
|
}
|
|
252
|
-
|
|
253
|
-
|
|
254
|
-
|
|
255
|
-
|
|
256
|
-
|
|
257
|
-
|
|
258
|
-
|
|
259
|
-
|
|
260
|
-
|
|
261
|
-
|
|
843
|
+
|
|
844
|
+
try {
|
|
845
|
+
var result = await api('POST', '/api/pipeline/start', body);
|
|
846
|
+
state.pipelineId = result.id;
|
|
847
|
+
saveState();
|
|
848
|
+
btn.textContent = 'Building...';
|
|
849
|
+
startScaffoldPolling();
|
|
850
|
+
} catch (e) {
|
|
851
|
+
btn.textContent = 'Start Build';
|
|
852
|
+
btn.disabled = false;
|
|
853
|
+
var errorArea = document.getElementById('scaffold-error');
|
|
854
|
+
if (errorArea) {
|
|
855
|
+
errorArea.textContent = '';
|
|
856
|
+
errorArea.appendChild(createElement('p', { className: 'field-error', textContent: e.message || 'Failed to start build.' }));
|
|
262
857
|
}
|
|
263
|
-
|
|
264
|
-
|
|
265
|
-
recognition.start();
|
|
266
|
-
});
|
|
858
|
+
}
|
|
859
|
+
}
|
|
267
860
|
|
|
268
|
-
|
|
269
|
-
|
|
270
|
-
var desc = $('#description');
|
|
271
|
-
if (desc) desc.value = (desc.value ? desc.value + ' ' : '') + transcript;
|
|
272
|
-
});
|
|
861
|
+
function startScaffoldPolling() {
|
|
862
|
+
if (state.pollTimer) { clearInterval(state.pollTimer); state.pollTimer = null; }
|
|
273
863
|
|
|
274
|
-
|
|
275
|
-
|
|
276
|
-
|
|
277
|
-
|
|
864
|
+
async function poll() {
|
|
865
|
+
if (!state.pipelineId) return;
|
|
866
|
+
try {
|
|
867
|
+
var status = await api('GET', '/api/pipeline/status/' + state.pipelineId);
|
|
868
|
+
var stages = status.stages || [];
|
|
869
|
+
var hasError = false;
|
|
870
|
+
var allDone = true;
|
|
278
871
|
|
|
279
|
-
|
|
280
|
-
|
|
281
|
-
|
|
282
|
-
|
|
872
|
+
SCAFFOLD_STAGES.forEach(function (def) {
|
|
873
|
+
var row = document.getElementById('stage-' + def.key);
|
|
874
|
+
if (!row) return;
|
|
875
|
+
|
|
876
|
+
var match = null;
|
|
877
|
+
for (var i = 0; i < stages.length; i++) {
|
|
878
|
+
if (stages[i].name === def.key) { match = stages[i]; break; }
|
|
879
|
+
}
|
|
880
|
+
|
|
881
|
+
var dot = row.querySelector('.stage-dot');
|
|
882
|
+
// Reset classes
|
|
883
|
+
dot.className = 'stage-dot';
|
|
884
|
+
|
|
885
|
+
// Remove any previous summary/error elements
|
|
886
|
+
var oldSummary = row.querySelector('.stage-summary');
|
|
887
|
+
if (oldSummary) oldSummary.remove();
|
|
888
|
+
var oldError = row.querySelector('.stage-error');
|
|
889
|
+
if (oldError) oldError.remove();
|
|
890
|
+
|
|
891
|
+
if (!match || match.status === 'pending') {
|
|
892
|
+
allDone = false;
|
|
893
|
+
} else if (match.status === 'running') {
|
|
894
|
+
dot.classList.add('running');
|
|
895
|
+
allDone = false;
|
|
896
|
+
} else if (match.status === 'done') {
|
|
897
|
+
dot.classList.add('done');
|
|
898
|
+
if (match.summary) {
|
|
899
|
+
row.appendChild(createElement('span', { className: 'stage-summary', textContent: match.summary }));
|
|
900
|
+
}
|
|
901
|
+
} else if (match.status === 'error') {
|
|
902
|
+
dot.classList.add('error');
|
|
903
|
+
hasError = true;
|
|
904
|
+
allDone = false;
|
|
905
|
+
if (match.error) {
|
|
906
|
+
row.appendChild(createElement('span', { className: 'stage-error', textContent: match.error }));
|
|
907
|
+
}
|
|
908
|
+
}
|
|
909
|
+
});
|
|
910
|
+
|
|
911
|
+
if (hasError) {
|
|
912
|
+
if (state.pollTimer) { clearInterval(state.pollTimer); state.pollTimer = null; }
|
|
913
|
+
showScaffoldError(status.error || 'A pipeline stage failed.');
|
|
914
|
+
} else if (allDone && stages.length > 0) {
|
|
915
|
+
if (state.pollTimer) { clearInterval(state.pollTimer); state.pollTimer = null; }
|
|
916
|
+
goToStep(4);
|
|
917
|
+
}
|
|
918
|
+
} catch (e) {
|
|
919
|
+
// Network error — keep polling, it may recover
|
|
920
|
+
}
|
|
921
|
+
}
|
|
922
|
+
|
|
923
|
+
poll();
|
|
924
|
+
state.pollTimer = setInterval(poll, 2000);
|
|
283
925
|
}
|
|
284
926
|
|
|
285
|
-
|
|
286
|
-
|
|
287
|
-
|
|
288
|
-
|
|
289
|
-
|
|
290
|
-
|
|
291
|
-
var
|
|
292
|
-
|
|
293
|
-
|
|
294
|
-
|
|
295
|
-
|
|
296
|
-
|
|
297
|
-
|
|
298
|
-
|
|
299
|
-
|
|
300
|
-
|
|
301
|
-
|
|
302
|
-
|
|
303
|
-
|
|
304
|
-
|
|
305
|
-
|
|
306
|
-
|
|
927
|
+
function showScaffoldError(msg) {
|
|
928
|
+
var errorArea = document.getElementById('scaffold-error');
|
|
929
|
+
if (!errorArea) return;
|
|
930
|
+
errorArea.textContent = '';
|
|
931
|
+
errorArea.appendChild(createElement('p', { className: 'field-error', textContent: msg }));
|
|
932
|
+
|
|
933
|
+
var actions = document.getElementById('scaffold-actions');
|
|
934
|
+
if (!actions) return;
|
|
935
|
+
// Remove old start/building button if any
|
|
936
|
+
var oldBtn = actions.querySelector('.btn-primary');
|
|
937
|
+
if (oldBtn) oldBtn.remove();
|
|
938
|
+
var oldMuted = actions.querySelector('.muted');
|
|
939
|
+
if (oldMuted) oldMuted.remove();
|
|
940
|
+
|
|
941
|
+
var retryBtn = createElement('button', { className: 'btn btn-primary', textContent: 'Retry' });
|
|
942
|
+
retryBtn.addEventListener('click', function () {
|
|
943
|
+
state.pipelineId = null;
|
|
944
|
+
saveState();
|
|
945
|
+
// Reset stage dots
|
|
946
|
+
SCAFFOLD_STAGES.forEach(function (def) {
|
|
947
|
+
var row = document.getElementById('stage-' + def.key);
|
|
948
|
+
if (!row) return;
|
|
949
|
+
var dot = row.querySelector('.stage-dot');
|
|
950
|
+
if (dot) dot.className = 'stage-dot';
|
|
951
|
+
var s = row.querySelector('.stage-summary');
|
|
952
|
+
if (s) s.remove();
|
|
953
|
+
var e = row.querySelector('.stage-error');
|
|
954
|
+
if (e) e.remove();
|
|
955
|
+
});
|
|
956
|
+
errorArea.textContent = '';
|
|
957
|
+
startScaffoldBuild(retryBtn);
|
|
307
958
|
});
|
|
959
|
+
actions.appendChild(retryBtn);
|
|
308
960
|
}
|
|
309
961
|
|
|
310
|
-
// ---- Step
|
|
962
|
+
// ---- Step 4: Checkpoint ----
|
|
311
963
|
|
|
312
|
-
|
|
313
|
-
|
|
314
|
-
|
|
315
|
-
'Extracting schema metadata',
|
|
316
|
-
'Generating semantic descriptions',
|
|
317
|
-
'Writing OSI-ready context',
|
|
318
|
-
];
|
|
964
|
+
function renderCheckpointStep() {
|
|
965
|
+
var content = document.getElementById('wizard-content');
|
|
966
|
+
if (!content) return;
|
|
319
967
|
|
|
320
|
-
|
|
321
|
-
|
|
322
|
-
|
|
323
|
-
|
|
324
|
-
|
|
325
|
-
|
|
326
|
-
|
|
327
|
-
|
|
328
|
-
|
|
329
|
-
|
|
330
|
-
|
|
331
|
-
|
|
332
|
-
|
|
333
|
-
|
|
334
|
-
|
|
335
|
-
|
|
336
|
-
|
|
337
|
-
|
|
338
|
-
|
|
339
|
-
|
|
340
|
-
|
|
968
|
+
var card = createElement('div', { className: 'card checkpoint-card' });
|
|
969
|
+
|
|
970
|
+
card.appendChild(createElement('h2', { textContent: 'Bronze Tier Achieved' }));
|
|
971
|
+
|
|
972
|
+
// Tier scorecard
|
|
973
|
+
var scorecard = createElement('div', { className: 'tier-scorecard' });
|
|
974
|
+
|
|
975
|
+
// Bronze (achieved)
|
|
976
|
+
var bronzeRow = createElement('div', { className: 'tier-row achieved' }, [
|
|
977
|
+
createElement('span', { className: 'tier-label', textContent: 'Bronze' }),
|
|
978
|
+
createElement('span', { className: 'tier-desc', textContent: 'Schema metadata, table/column names, types, row counts' }),
|
|
979
|
+
]);
|
|
980
|
+
scorecard.appendChild(bronzeRow);
|
|
981
|
+
|
|
982
|
+
// Silver
|
|
983
|
+
var silverRow = createElement('div', { className: 'tier-row' }, [
|
|
984
|
+
createElement('span', { className: 'tier-label', textContent: 'Silver' }),
|
|
985
|
+
createElement('span', { className: 'tier-desc', textContent: 'Column descriptions, sample values, trust tags' }),
|
|
986
|
+
]);
|
|
987
|
+
scorecard.appendChild(silverRow);
|
|
988
|
+
|
|
989
|
+
// Gold
|
|
990
|
+
var goldRow = createElement('div', { className: 'tier-row' }, [
|
|
991
|
+
createElement('span', { className: 'tier-label', textContent: 'Gold' }),
|
|
992
|
+
createElement('span', { className: 'tier-desc', textContent: 'Join rules, grain statements, semantic roles, golden queries, guardrail filters' }),
|
|
993
|
+
]);
|
|
994
|
+
scorecard.appendChild(goldRow);
|
|
995
|
+
|
|
996
|
+
card.appendChild(scorecard);
|
|
997
|
+
|
|
998
|
+
// Explanatory text
|
|
999
|
+
card.appendChild(createElement('p', { className: 'checkpoint-explain', textContent: 'Your semantic plane has basic schema metadata. AI tools can use this now, but with Gold tier they\'ll understand join relationships, business descriptions, and query patterns.' }));
|
|
1000
|
+
|
|
1001
|
+
// CTA buttons
|
|
1002
|
+
var ctas = createElement('div', { className: 'checkpoint-ctas' });
|
|
1003
|
+
|
|
1004
|
+
var serveBtn = createElement('button', { className: 'btn btn-secondary', textContent: 'Start MCP Server' });
|
|
1005
|
+
serveBtn.addEventListener('click', function () { goToStep(6); });
|
|
1006
|
+
ctas.appendChild(serveBtn);
|
|
1007
|
+
|
|
1008
|
+
var goldBtn = createElement('button', { className: 'btn btn-primary', textContent: 'Continue to Gold' });
|
|
1009
|
+
goldBtn.addEventListener('click', function () { goToStep(5); });
|
|
1010
|
+
ctas.appendChild(goldBtn);
|
|
1011
|
+
|
|
1012
|
+
card.appendChild(ctas);
|
|
1013
|
+
content.appendChild(card);
|
|
341
1014
|
}
|
|
342
1015
|
|
|
343
|
-
|
|
344
|
-
|
|
345
|
-
|
|
346
|
-
|
|
347
|
-
|
|
348
|
-
|
|
1016
|
+
// ---- Step 5: Enrich ----
|
|
1017
|
+
|
|
1018
|
+
var ENRICH_REQUIREMENTS = [
|
|
1019
|
+
{ key: 'column-descriptions', label: 'Column descriptions', initial: '0/45 columns' },
|
|
1020
|
+
{ key: 'sample-values', label: 'Sample values', initial: '0/45 columns' },
|
|
1021
|
+
{ key: 'join-rules', label: 'Join rules', initial: '0/0' },
|
|
1022
|
+
{ key: 'grain-statements', label: 'Grain statements', initial: '0/0' },
|
|
1023
|
+
{ key: 'semantic-roles', label: 'Semantic roles', initial: '0/0' },
|
|
1024
|
+
{ key: 'golden-queries', label: 'Golden queries', initial: '0/0' },
|
|
1025
|
+
{ key: 'guardrail-filters', label: 'Guardrail filters', initial: '0/0' },
|
|
1026
|
+
];
|
|
1027
|
+
|
|
1028
|
+
function renderEnrichStep() {
|
|
1029
|
+
var content = document.getElementById('wizard-content');
|
|
1030
|
+
if (!content) return;
|
|
1031
|
+
|
|
1032
|
+
var card = createElement('div', { className: 'card' });
|
|
1033
|
+
|
|
1034
|
+
card.appendChild(createElement('h2', { textContent: 'Enriching to Gold' }));
|
|
1035
|
+
card.appendChild(createElement('p', { className: 'muted', textContent: 'RunContext is analyzing your schema to add descriptions, join rules, and query patterns.' }));
|
|
1036
|
+
|
|
1037
|
+
// Start Enrichment button area
|
|
1038
|
+
var startArea = createElement('div', { className: 'step-actions', id: 'enrich-start-area' });
|
|
1039
|
+
var backBtn = createElement('button', { className: 'btn btn-secondary', textContent: 'Back' });
|
|
1040
|
+
backBtn.addEventListener('click', function () { goToStep(4); });
|
|
1041
|
+
startArea.appendChild(backBtn);
|
|
1042
|
+
|
|
1043
|
+
var startBtn = createElement('button', { className: 'btn btn-primary', textContent: 'Start Enrichment', id: 'enrich-start-btn' });
|
|
1044
|
+
startBtn.addEventListener('click', function () { startEnrichment(startBtn); });
|
|
1045
|
+
startArea.appendChild(startBtn);
|
|
1046
|
+
card.appendChild(startArea);
|
|
1047
|
+
|
|
1048
|
+
// Dashboard (always visible — shows checklist pre-enrichment)
|
|
1049
|
+
var dashboard = createElement('div', { className: 'enrich-dashboard', id: 'enrich-dashboard' });
|
|
1050
|
+
|
|
1051
|
+
// Top panel: Requirements checklist
|
|
1052
|
+
var checklist = createElement('div', { className: 'enrich-checklist' });
|
|
1053
|
+
ENRICH_REQUIREMENTS.forEach(function (req) {
|
|
1054
|
+
var row = createElement('div', { className: 'enrich-row', id: 'enrich-req-' + req.key });
|
|
1055
|
+
|
|
1056
|
+
var header = createElement('div', { className: 'enrich-row-header' });
|
|
1057
|
+
header.appendChild(createElement('span', { className: 'stage-dot' }));
|
|
1058
|
+
header.appendChild(createElement('span', { className: 'enrich-req-name', textContent: req.label }));
|
|
1059
|
+
header.appendChild(createElement('span', { className: 'enrich-progress', textContent: req.initial }));
|
|
1060
|
+
header.appendChild(createElement('span', { className: 'enrich-arrow', textContent: '\u25B6' }));
|
|
1061
|
+
|
|
1062
|
+
header.addEventListener('click', function () {
|
|
1063
|
+
row.classList.toggle('expanded');
|
|
349
1064
|
});
|
|
350
|
-
|
|
351
|
-
|
|
352
|
-
|
|
353
|
-
|
|
1065
|
+
|
|
1066
|
+
var detail = createElement('div', { className: 'enrich-row-detail' }, [
|
|
1067
|
+
'Details will appear as enrichment progresses.',
|
|
1068
|
+
]);
|
|
1069
|
+
|
|
1070
|
+
row.appendChild(header);
|
|
1071
|
+
row.appendChild(detail);
|
|
1072
|
+
checklist.appendChild(row);
|
|
354
1073
|
});
|
|
1074
|
+
dashboard.appendChild(checklist);
|
|
1075
|
+
|
|
1076
|
+
// Bottom panel: Activity log
|
|
1077
|
+
var logSection = createElement('div', { className: 'activity-log' });
|
|
1078
|
+
logSection.appendChild(createElement('div', { className: 'activity-log-title', textContent: 'Activity Log' }));
|
|
1079
|
+
var logContainer = createElement('div', { id: 'activity-log' });
|
|
1080
|
+
logContainer.appendChild(createElement('div', { className: 'log-entry' }, [
|
|
1081
|
+
createElement('span', { className: 'log-time', textContent: new Date().toLocaleTimeString() }),
|
|
1082
|
+
createElement('span', {}, [' Waiting for enrichment to start...']),
|
|
1083
|
+
]));
|
|
1084
|
+
logSection.appendChild(logContainer);
|
|
1085
|
+
dashboard.appendChild(logSection);
|
|
1086
|
+
|
|
1087
|
+
// Error area
|
|
1088
|
+
var errorArea = createElement('div', { id: 'enrich-error' });
|
|
1089
|
+
dashboard.appendChild(errorArea);
|
|
1090
|
+
|
|
1091
|
+
card.appendChild(dashboard);
|
|
1092
|
+
content.appendChild(card);
|
|
355
1093
|
}
|
|
356
1094
|
|
|
357
|
-
function
|
|
358
|
-
|
|
359
|
-
|
|
360
|
-
|
|
361
|
-
|
|
362
|
-
|
|
363
|
-
|
|
364
|
-
|
|
365
|
-
|
|
366
|
-
|
|
367
|
-
|
|
368
|
-
|
|
369
|
-
|
|
370
|
-
else if (i === data.currentStep) status = 'running';
|
|
371
|
-
return { stage: name, status: status, detail: '' };
|
|
372
|
-
});
|
|
1095
|
+
async function startEnrichment(btn) {
|
|
1096
|
+
btn.textContent = 'Starting...';
|
|
1097
|
+
btn.disabled = true;
|
|
1098
|
+
|
|
1099
|
+
var errorArea = document.getElementById('enrich-error');
|
|
1100
|
+
if (errorArea) errorArea.textContent = '';
|
|
1101
|
+
|
|
1102
|
+
var body = {
|
|
1103
|
+
productName: state.brief.product_name,
|
|
1104
|
+
targetTier: 'gold',
|
|
1105
|
+
};
|
|
1106
|
+
if (state.sources && state.sources[0]) {
|
|
1107
|
+
body.dataSource = state.sources[0];
|
|
373
1108
|
}
|
|
374
|
-
renderTimeline(items);
|
|
375
|
-
}
|
|
376
1109
|
|
|
377
|
-
async function startBuild() {
|
|
378
|
-
renderTimeline([]);
|
|
379
1110
|
try {
|
|
380
|
-
await api('POST', '/api/
|
|
381
|
-
|
|
382
|
-
|
|
383
|
-
|
|
384
|
-
|
|
385
|
-
|
|
386
|
-
|
|
1111
|
+
var result = await api('POST', '/api/pipeline/start', body);
|
|
1112
|
+
state.pipelineId = result.id;
|
|
1113
|
+
saveState();
|
|
1114
|
+
|
|
1115
|
+
// Hide start button area
|
|
1116
|
+
var startArea = document.getElementById('enrich-start-area');
|
|
1117
|
+
if (startArea) startArea.style.display = 'none';
|
|
1118
|
+
|
|
1119
|
+
appendEnrichLog({ message: 'Enrichment pipeline started.' });
|
|
1120
|
+
startEnrichPolling();
|
|
387
1121
|
} catch (e) {
|
|
388
|
-
|
|
1122
|
+
btn.textContent = 'Start Enrichment';
|
|
1123
|
+
btn.disabled = false;
|
|
1124
|
+
if (errorArea) {
|
|
1125
|
+
errorArea.textContent = '';
|
|
1126
|
+
errorArea.appendChild(createElement('p', { className: 'field-error', textContent: e.message || 'Failed to start enrichment.' }));
|
|
1127
|
+
}
|
|
389
1128
|
}
|
|
390
1129
|
}
|
|
391
1130
|
|
|
392
|
-
function
|
|
393
|
-
if (
|
|
394
|
-
|
|
1131
|
+
function startEnrichPolling() {
|
|
1132
|
+
if (state.pollTimer) { clearInterval(state.pollTimer); state.pollTimer = null; }
|
|
1133
|
+
|
|
1134
|
+
async function poll() {
|
|
1135
|
+
if (!state.pipelineId) return;
|
|
395
1136
|
try {
|
|
396
|
-
var
|
|
397
|
-
|
|
398
|
-
|
|
399
|
-
|
|
400
|
-
|
|
401
|
-
|
|
402
|
-
|
|
403
|
-
|
|
1137
|
+
var status = await api('GET', '/api/pipeline/status/' + state.pipelineId);
|
|
1138
|
+
var stages = status.stages || [];
|
|
1139
|
+
var hasError = false;
|
|
1140
|
+
var silverDone = false;
|
|
1141
|
+
var goldDone = false;
|
|
1142
|
+
|
|
1143
|
+
for (var i = 0; i < stages.length; i++) {
|
|
1144
|
+
var s = stages[i];
|
|
1145
|
+
if (s.name === 'enrich-silver' && s.status === 'done') silverDone = true;
|
|
1146
|
+
if (s.name === 'enrich-gold' && s.status === 'done') goldDone = true;
|
|
1147
|
+
if (s.status === 'error') hasError = true;
|
|
1148
|
+
}
|
|
1149
|
+
|
|
1150
|
+
if (hasError) {
|
|
1151
|
+
if (state.pollTimer) { clearInterval(state.pollTimer); state.pollTimer = null; }
|
|
1152
|
+
showEnrichError(status.error || 'An enrichment stage failed.');
|
|
1153
|
+
return;
|
|
1154
|
+
}
|
|
1155
|
+
|
|
1156
|
+
if (silverDone) {
|
|
1157
|
+
appendEnrichLog({ message: 'Silver enrichment complete.' });
|
|
1158
|
+
}
|
|
1159
|
+
if (silverDone && goldDone) {
|
|
1160
|
+
if (state.pollTimer) { clearInterval(state.pollTimer); state.pollTimer = null; }
|
|
1161
|
+
appendEnrichLog({ message: 'Gold enrichment complete! Advancing...' });
|
|
1162
|
+
goToStep(6);
|
|
404
1163
|
}
|
|
405
1164
|
} catch (e) {
|
|
406
|
-
|
|
407
|
-
renderTimeline([{ stage: 'Error', status: 'error', detail: e.message }]);
|
|
1165
|
+
// Network error — keep polling
|
|
408
1166
|
}
|
|
409
|
-
}
|
|
1167
|
+
}
|
|
1168
|
+
|
|
1169
|
+
poll();
|
|
1170
|
+
state.pollTimer = setInterval(poll, 3000);
|
|
1171
|
+
}
|
|
1172
|
+
|
|
1173
|
+
function showEnrichError(msg) {
|
|
1174
|
+
var errorArea = document.getElementById('enrich-error');
|
|
1175
|
+
if (!errorArea) return;
|
|
1176
|
+
errorArea.textContent = '';
|
|
1177
|
+
errorArea.appendChild(createElement('p', { className: 'field-error', textContent: msg }));
|
|
1178
|
+
|
|
1179
|
+
var retryBtn = createElement('button', { className: 'btn btn-primary', textContent: 'Retry' });
|
|
1180
|
+
retryBtn.addEventListener('click', function () {
|
|
1181
|
+
errorArea.textContent = '';
|
|
1182
|
+
state.pipelineId = null;
|
|
1183
|
+
saveState();
|
|
1184
|
+
// Reset requirement rows to pending
|
|
1185
|
+
ENRICH_REQUIREMENTS.forEach(function (req) {
|
|
1186
|
+
var row = document.getElementById('enrich-req-' + req.key);
|
|
1187
|
+
if (!row) return;
|
|
1188
|
+
var dot = row.querySelector('.stage-dot');
|
|
1189
|
+
if (dot) dot.className = 'stage-dot';
|
|
1190
|
+
var prog = row.querySelector('.enrich-progress');
|
|
1191
|
+
if (prog) prog.textContent = req.initial;
|
|
1192
|
+
});
|
|
1193
|
+
// Show start area again, hide dashboard
|
|
1194
|
+
var startArea = document.getElementById('enrich-start-area');
|
|
1195
|
+
if (startArea) startArea.style.display = '';
|
|
1196
|
+
var dashboard = document.getElementById('enrich-dashboard');
|
|
1197
|
+
if (dashboard) dashboard.style.display = 'none';
|
|
1198
|
+
var startBtn = document.getElementById('enrich-start-btn');
|
|
1199
|
+
if (startBtn) {
|
|
1200
|
+
startBtn.textContent = 'Start Enrichment';
|
|
1201
|
+
startBtn.disabled = false;
|
|
1202
|
+
}
|
|
1203
|
+
});
|
|
1204
|
+
errorArea.appendChild(retryBtn);
|
|
1205
|
+
}
|
|
1206
|
+
|
|
1207
|
+
function updateEnrichProgress(payload) {
|
|
1208
|
+
// payload: { requirement: string, status: string, progress: string }
|
|
1209
|
+
var row = document.getElementById('enrich-req-' + payload.requirement);
|
|
1210
|
+
if (!row) return;
|
|
1211
|
+
var dot = row.querySelector('.stage-dot');
|
|
1212
|
+
var prog = row.querySelector('.enrich-progress');
|
|
1213
|
+
if (dot) {
|
|
1214
|
+
dot.className = 'stage-dot' + (payload.status === 'working' ? ' running' : payload.status === 'done' ? ' done' : '');
|
|
1215
|
+
}
|
|
1216
|
+
if (prog) prog.textContent = payload.progress || '';
|
|
1217
|
+
}
|
|
1218
|
+
|
|
1219
|
+
function appendEnrichLog(payload) {
|
|
1220
|
+
// payload: { message: string, timestamp?: string }
|
|
1221
|
+
var log = document.getElementById('activity-log');
|
|
1222
|
+
if (!log) return;
|
|
1223
|
+
var ts = payload.timestamp ? new Date(payload.timestamp).toLocaleTimeString() : new Date().toLocaleTimeString();
|
|
1224
|
+
var entry = createElement('div', { className: 'log-entry' }, [
|
|
1225
|
+
createElement('span', { className: 'log-time' }, [ts]),
|
|
1226
|
+
createElement('span', {}, [' ' + payload.message]),
|
|
1227
|
+
]);
|
|
1228
|
+
log.appendChild(entry);
|
|
1229
|
+
log.scrollTop = log.scrollHeight;
|
|
1230
|
+
}
|
|
1231
|
+
|
|
1232
|
+
// ---- Step 6: Serve ----
|
|
1233
|
+
|
|
1234
|
+
function renderServeStep() {
|
|
1235
|
+
var content = document.getElementById('wizard-content');
|
|
1236
|
+
if (!content) return;
|
|
1237
|
+
|
|
1238
|
+
var card = createElement('div', { className: 'card serve-card' });
|
|
1239
|
+
|
|
1240
|
+
card.appendChild(createElement('h2', { textContent: 'Your Semantic Plane is Ready' }));
|
|
1241
|
+
|
|
1242
|
+
// Tier detection placeholder — will be filled async
|
|
1243
|
+
var tierBadge = createElement('span', { className: 'serve-tier-badge bronze', id: 'tier-badge', textContent: 'Bronze' });
|
|
1244
|
+
card.appendChild(tierBadge);
|
|
1245
|
+
|
|
1246
|
+
var messageEl = createElement('p', { className: 'muted' });
|
|
1247
|
+
messageEl.textContent = 'Your Bronze tier semantic plane is ready for AI agents.';
|
|
1248
|
+
card.appendChild(messageEl);
|
|
1249
|
+
|
|
1250
|
+
var upgradeHint = createElement('p', { className: 'muted' });
|
|
1251
|
+
upgradeHint.style.marginTop = '8px';
|
|
1252
|
+
upgradeHint.style.display = 'none';
|
|
1253
|
+
card.appendChild(upgradeHint);
|
|
1254
|
+
|
|
1255
|
+
// Fetch tier from pipeline status
|
|
1256
|
+
if (state.pipelineId) {
|
|
1257
|
+
fetch('/api/pipeline/status/' + state.pipelineId)
|
|
1258
|
+
.then(function (res) { return res.json(); })
|
|
1259
|
+
.then(function (data) {
|
|
1260
|
+
var stages = data.stages || [];
|
|
1261
|
+
var tier = 'Bronze';
|
|
1262
|
+
var tierClass = 'bronze';
|
|
1263
|
+
var hasSilver = false;
|
|
1264
|
+
var hasGold = false;
|
|
1265
|
+
for (var i = 0; i < stages.length; i++) {
|
|
1266
|
+
if (stages[i].name === 'enrich-gold' && stages[i].status === 'done') {
|
|
1267
|
+
hasGold = true;
|
|
1268
|
+
}
|
|
1269
|
+
if (stages[i].name === 'enrich-silver' && stages[i].status === 'done') {
|
|
1270
|
+
hasSilver = true;
|
|
1271
|
+
}
|
|
1272
|
+
}
|
|
1273
|
+
if (hasGold) {
|
|
1274
|
+
tier = 'Gold';
|
|
1275
|
+
tierClass = 'gold';
|
|
1276
|
+
} else if (hasSilver) {
|
|
1277
|
+
tier = 'Silver';
|
|
1278
|
+
tierClass = 'silver';
|
|
1279
|
+
}
|
|
1280
|
+
|
|
1281
|
+
tierBadge.className = 'serve-tier-badge ' + tierClass;
|
|
1282
|
+
tierBadge.textContent = tier;
|
|
1283
|
+
messageEl.textContent = 'Your ' + tier + ' tier semantic plane is ready for AI agents.';
|
|
1284
|
+
|
|
1285
|
+
if (!hasGold) {
|
|
1286
|
+
var missing = [];
|
|
1287
|
+
if (!hasSilver) missing.push('Silver enrichment');
|
|
1288
|
+
missing.push('Gold enrichment');
|
|
1289
|
+
upgradeHint.textContent = 'To reach Gold, you still need: ' + missing.join(', ') + '. You can run enrichment later with `context enrich --target gold`.';
|
|
1290
|
+
upgradeHint.style.display = 'block';
|
|
1291
|
+
}
|
|
1292
|
+
})
|
|
1293
|
+
.catch(function () { /* keep Bronze default */ });
|
|
1294
|
+
}
|
|
1295
|
+
|
|
1296
|
+
// CTAs
|
|
1297
|
+
var ctas = createElement('div', { className: 'serve-ctas' });
|
|
1298
|
+
|
|
1299
|
+
var startBtn = createElement('button', { className: 'btn btn-primary', textContent: 'Start MCP Server' });
|
|
1300
|
+
startBtn.addEventListener('click', function () {
|
|
1301
|
+
startBtn.disabled = true;
|
|
1302
|
+
startBtn.textContent = 'Loading...';
|
|
1303
|
+
fetch('/api/mcp-config')
|
|
1304
|
+
.then(function (res) { return res.json(); })
|
|
1305
|
+
.then(function (config) {
|
|
1306
|
+
startBtn.textContent = 'Start MCP Server';
|
|
1307
|
+
startBtn.disabled = false;
|
|
1308
|
+
// Remove existing config block if any
|
|
1309
|
+
var existing = card.querySelector('.mcp-config-block');
|
|
1310
|
+
if (existing) existing.remove();
|
|
1311
|
+
var configBlock = createElement('div', { className: 'mcp-config-block' });
|
|
1312
|
+
configBlock.textContent = JSON.stringify(config, null, 2);
|
|
1313
|
+
// Insert after CTAs
|
|
1314
|
+
var ctaParent = ctas.parentNode;
|
|
1315
|
+
if (ctaParent) ctaParent.insertBefore(configBlock, ctas.nextSibling);
|
|
1316
|
+
|
|
1317
|
+
var copyHint = card.querySelector('.mcp-copy-hint');
|
|
1318
|
+
if (!copyHint) {
|
|
1319
|
+
copyHint = createElement('p', { className: 'muted mcp-copy-hint' });
|
|
1320
|
+
copyHint.textContent = 'Copy the JSON above into your IDE\'s MCP settings, or run: context serve';
|
|
1321
|
+
copyHint.style.marginTop = '8px';
|
|
1322
|
+
copyHint.style.fontSize = '0.85rem';
|
|
1323
|
+
var configEl = card.querySelector('.mcp-config-block');
|
|
1324
|
+
if (configEl && configEl.nextSibling) {
|
|
1325
|
+
ctaParent.insertBefore(copyHint, configEl.nextSibling);
|
|
1326
|
+
} else if (configEl) {
|
|
1327
|
+
ctaParent.appendChild(copyHint);
|
|
1328
|
+
}
|
|
1329
|
+
}
|
|
1330
|
+
})
|
|
1331
|
+
.catch(function () {
|
|
1332
|
+
startBtn.textContent = 'Start MCP Server';
|
|
1333
|
+
startBtn.disabled = false;
|
|
1334
|
+
});
|
|
1335
|
+
});
|
|
1336
|
+
ctas.appendChild(startBtn);
|
|
1337
|
+
|
|
1338
|
+
var publishBtn = createElement('button', { className: 'btn btn-secondary', textContent: 'Publish to Cloud' });
|
|
1339
|
+
publishBtn.disabled = true;
|
|
1340
|
+
publishBtn.title = 'Coming soon';
|
|
1341
|
+
ctas.appendChild(publishBtn);
|
|
1342
|
+
|
|
1343
|
+
card.appendChild(ctas);
|
|
1344
|
+
|
|
1345
|
+
// CLI Commands section
|
|
1346
|
+
var cmds = createElement('div', { className: 'serve-commands' });
|
|
1347
|
+
cmds.appendChild(createElement('div', { className: 'serve-commands-title', textContent: 'CLI Commands' }));
|
|
1348
|
+
|
|
1349
|
+
var cmdData = [
|
|
1350
|
+
['context serve', 'Start the MCP server'],
|
|
1351
|
+
['context tier', 'Check your current tier'],
|
|
1352
|
+
['context enrich --target gold', 'Enrich to Gold tier'],
|
|
1353
|
+
['context verify', 'Validate your semantic plane'],
|
|
1354
|
+
];
|
|
1355
|
+
for (var i = 0; i < cmdData.length; i++) {
|
|
1356
|
+
var row = createElement('div', { className: 'serve-cmd-row' }, [
|
|
1357
|
+
createElement('span', { className: 'serve-cmd', textContent: cmdData[i][0] }),
|
|
1358
|
+
createElement('span', { className: 'serve-cmd-desc', textContent: cmdData[i][1] }),
|
|
1359
|
+
]);
|
|
1360
|
+
cmds.appendChild(row);
|
|
1361
|
+
}
|
|
1362
|
+
card.appendChild(cmds);
|
|
1363
|
+
|
|
1364
|
+
// Continue Enrichment link (shown if not Gold — updated async)
|
|
1365
|
+
var continueLink = createElement('p', {});
|
|
1366
|
+
continueLink.style.marginTop = '16px';
|
|
1367
|
+
var linkEl = createElement('a', { textContent: 'Continue Enrichment' });
|
|
1368
|
+
linkEl.style.color = 'var(--rc-color-accent, #c9a55a)';
|
|
1369
|
+
linkEl.style.cursor = 'pointer';
|
|
1370
|
+
linkEl.style.fontSize = '0.875rem';
|
|
1371
|
+
linkEl.addEventListener('click', function () {
|
|
1372
|
+
goToStep(5);
|
|
1373
|
+
});
|
|
1374
|
+
continueLink.appendChild(linkEl);
|
|
1375
|
+
card.appendChild(continueLink);
|
|
1376
|
+
|
|
1377
|
+
// Hide continue link if Gold
|
|
1378
|
+
if (state.pipelineId) {
|
|
1379
|
+
fetch('/api/pipeline/status/' + state.pipelineId)
|
|
1380
|
+
.then(function (res) { return res.json(); })
|
|
1381
|
+
.then(function (data) {
|
|
1382
|
+
var stages = data.stages || [];
|
|
1383
|
+
for (var j = 0; j < stages.length; j++) {
|
|
1384
|
+
if (stages[j].name === 'enrich-gold' && stages[j].status === 'done') {
|
|
1385
|
+
continueLink.style.display = 'none';
|
|
1386
|
+
break;
|
|
1387
|
+
}
|
|
1388
|
+
}
|
|
1389
|
+
})
|
|
1390
|
+
.catch(function () { /* keep visible */ });
|
|
1391
|
+
}
|
|
1392
|
+
|
|
1393
|
+
card.appendChild(createStepActions(true, false));
|
|
1394
|
+
content.appendChild(card);
|
|
1395
|
+
}
|
|
1396
|
+
|
|
1397
|
+
// ---- Review / Build helpers (will be re-implemented in later tasks) ----
|
|
1398
|
+
|
|
1399
|
+
function createReviewRow(label, value) {
|
|
1400
|
+
return createElement('div', { className: 'review-row' }, [
|
|
1401
|
+
createElement('span', { className: 'review-label', textContent: label }),
|
|
1402
|
+
createElement('span', { className: 'review-value', textContent: value }),
|
|
1403
|
+
]);
|
|
410
1404
|
}
|
|
411
1405
|
|
|
412
1406
|
// ---- Existing Products Banner ----
|
|
@@ -416,27 +1410,27 @@
|
|
|
416
1410
|
var res = await fetch('/api/products');
|
|
417
1411
|
var products = await res.json();
|
|
418
1412
|
if (products.length > 0) {
|
|
419
|
-
var
|
|
420
|
-
|
|
1413
|
+
var content = document.getElementById('wizard-content');
|
|
1414
|
+
if (!content) return;
|
|
1415
|
+
|
|
1416
|
+
var banner = createElement('div', { className: 'existing-products-banner' });
|
|
421
1417
|
|
|
422
|
-
var title =
|
|
423
|
-
title.className = 'banner-title';
|
|
1418
|
+
var title = createElement('p', { className: 'banner-title' });
|
|
424
1419
|
title.textContent = 'Your semantic plane has ' + products.length + ' data product' + (products.length === 1 ? '' : 's') + '. Adding another.';
|
|
425
1420
|
banner.appendChild(title);
|
|
426
1421
|
|
|
427
|
-
var list =
|
|
428
|
-
list.className = 'product-chips';
|
|
1422
|
+
var list = createElement('div', { className: 'product-chips' });
|
|
429
1423
|
for (var i = 0; i < products.length; i++) {
|
|
430
|
-
|
|
431
|
-
chip.className = 'product-chip';
|
|
432
|
-
chip.textContent = products[i].name;
|
|
433
|
-
list.appendChild(chip);
|
|
1424
|
+
list.appendChild(createElement('span', { className: 'product-chip', textContent: products[i].name }));
|
|
434
1425
|
}
|
|
435
1426
|
banner.appendChild(list);
|
|
436
1427
|
|
|
437
|
-
|
|
438
|
-
|
|
439
|
-
|
|
1428
|
+
// Insert banner before the first card
|
|
1429
|
+
var firstCard = content.querySelector('.card');
|
|
1430
|
+
if (firstCard) {
|
|
1431
|
+
content.insertBefore(banner, firstCard);
|
|
1432
|
+
} else {
|
|
1433
|
+
content.appendChild(banner);
|
|
440
1434
|
}
|
|
441
1435
|
}
|
|
442
1436
|
} catch (e) {
|
|
@@ -444,25 +1438,117 @@
|
|
|
444
1438
|
}
|
|
445
1439
|
}
|
|
446
1440
|
|
|
447
|
-
// ----
|
|
1441
|
+
// ---- Sidebar Interactions ----
|
|
448
1442
|
|
|
449
|
-
function
|
|
450
|
-
|
|
451
|
-
|
|
452
|
-
|
|
1443
|
+
function setupSidebarLocked() {
|
|
1444
|
+
var tooltip = document.getElementById('locked-tooltip');
|
|
1445
|
+
if (!tooltip) return;
|
|
1446
|
+
|
|
1447
|
+
$$('.nav-item.locked').forEach(function (item) {
|
|
1448
|
+
item.addEventListener('click', function (e) {
|
|
1449
|
+
e.preventDefault();
|
|
1450
|
+
e.stopPropagation();
|
|
1451
|
+
var rect = item.getBoundingClientRect();
|
|
1452
|
+
tooltip.style.display = 'block';
|
|
1453
|
+
tooltip.style.left = rect.left + 'px';
|
|
1454
|
+
tooltip.style.top = (rect.bottom + 6) + 'px';
|
|
453
1455
|
});
|
|
454
1456
|
});
|
|
455
|
-
|
|
456
|
-
|
|
457
|
-
|
|
458
|
-
|
|
1457
|
+
|
|
1458
|
+
document.addEventListener('click', function (e) {
|
|
1459
|
+
if (tooltip.style.display !== 'none') {
|
|
1460
|
+
var isLocked = false;
|
|
1461
|
+
$$('.nav-item.locked').forEach(function (item) {
|
|
1462
|
+
if (item.contains(e.target)) isLocked = true;
|
|
1463
|
+
});
|
|
1464
|
+
if (!isLocked) {
|
|
1465
|
+
tooltip.style.display = 'none';
|
|
1466
|
+
}
|
|
1467
|
+
}
|
|
459
1468
|
});
|
|
1469
|
+
}
|
|
1470
|
+
|
|
1471
|
+
// ---- Sidebar Status Polling ----
|
|
460
1472
|
|
|
461
|
-
|
|
462
|
-
|
|
463
|
-
|
|
464
|
-
|
|
465
|
-
|
|
1473
|
+
function pollMcpStatus() {
|
|
1474
|
+
async function check() {
|
|
1475
|
+
var dot = document.getElementById('mcp-status-dot');
|
|
1476
|
+
var text = document.getElementById('mcp-status-text');
|
|
1477
|
+
var serverDot = document.getElementById('mcp-server-dot');
|
|
1478
|
+
var serverText = document.getElementById('mcp-server-text');
|
|
1479
|
+
if (!dot || !text) return;
|
|
1480
|
+
try {
|
|
1481
|
+
var controller = new AbortController();
|
|
1482
|
+
var timer = setTimeout(function () { controller.abort(); }, 2000);
|
|
1483
|
+
var res = await fetch('http://localhost:3333/health', {
|
|
1484
|
+
method: 'GET',
|
|
1485
|
+
mode: 'no-cors',
|
|
1486
|
+
signal: controller.signal,
|
|
1487
|
+
});
|
|
1488
|
+
clearTimeout(timer);
|
|
1489
|
+
// mode: 'no-cors' returns opaque response (status 0) but means server is reachable
|
|
1490
|
+
dot.classList.remove('error');
|
|
1491
|
+
dot.classList.add('success');
|
|
1492
|
+
text.textContent = 'connected';
|
|
1493
|
+
if (serverDot) { serverDot.classList.remove('error'); serverDot.classList.add('success'); }
|
|
1494
|
+
if (serverText) serverText.textContent = 'MCP running';
|
|
1495
|
+
} catch (e) {
|
|
1496
|
+
dot.classList.remove('success');
|
|
1497
|
+
text.textContent = 'offline';
|
|
1498
|
+
if (serverDot) { serverDot.classList.remove('success'); }
|
|
1499
|
+
if (serverText) serverText.textContent = 'MCP stopped';
|
|
1500
|
+
}
|
|
1501
|
+
}
|
|
1502
|
+
check();
|
|
1503
|
+
state.mcpPollTimer = setInterval(check, 10000);
|
|
1504
|
+
}
|
|
1505
|
+
|
|
1506
|
+
function updateDbStatus(source) {
|
|
1507
|
+
var dot = document.getElementById('db-status-dot');
|
|
1508
|
+
var text = document.getElementById('db-status-text');
|
|
1509
|
+
if (!dot || !text) return;
|
|
1510
|
+
if (source) {
|
|
1511
|
+
dot.classList.remove('error');
|
|
1512
|
+
dot.classList.add('success');
|
|
1513
|
+
text.textContent = (source.name || source.adapter) + ' connected';
|
|
1514
|
+
} else {
|
|
1515
|
+
dot.classList.remove('success');
|
|
1516
|
+
dot.classList.add('error');
|
|
1517
|
+
text.textContent = 'No database';
|
|
1518
|
+
}
|
|
1519
|
+
}
|
|
1520
|
+
|
|
1521
|
+
// ---- Init ----
|
|
1522
|
+
|
|
1523
|
+
function init() {
|
|
1524
|
+
setupSidebarLocked();
|
|
1525
|
+
pollMcpStatus();
|
|
1526
|
+
|
|
1527
|
+
// Restore DB status from saved state
|
|
1528
|
+
if (state.sources && state.sources.length > 0) {
|
|
1529
|
+
updateDbStatus(state.sources[0]);
|
|
1530
|
+
}
|
|
1531
|
+
|
|
1532
|
+
goToStep(state.step);
|
|
1533
|
+
|
|
1534
|
+
// Create or resume a WebSocket session
|
|
1535
|
+
var urlParams = new URLSearchParams(window.location.search);
|
|
1536
|
+
var sessionParam = urlParams.get('session');
|
|
1537
|
+
if (sessionParam) {
|
|
1538
|
+
connectWebSocket(sessionParam);
|
|
1539
|
+
} else {
|
|
1540
|
+
fetch('/api/session', { method: 'POST' })
|
|
1541
|
+
.then(function (r) { return r.json(); })
|
|
1542
|
+
.then(function (data) {
|
|
1543
|
+
if (data.sessionId) {
|
|
1544
|
+
connectWebSocket(data.sessionId);
|
|
1545
|
+
// Update URL without reload so session persists on refresh
|
|
1546
|
+
var newUrl = window.location.pathname + '?session=' + encodeURIComponent(data.sessionId);
|
|
1547
|
+
window.history.replaceState({}, '', newUrl);
|
|
1548
|
+
}
|
|
1549
|
+
})
|
|
1550
|
+
.catch(function () { /* WebSocket is best-effort */ });
|
|
1551
|
+
}
|
|
466
1552
|
}
|
|
467
1553
|
|
|
468
1554
|
document.addEventListener('DOMContentLoaded', init);
|