yaml-flow 4.0.0 → 5.0.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (95) hide show
  1. package/browser/board-livegraph-runtime.js +1453 -0
  2. package/browser/board-livegraph-runtime.js.map +1 -0
  3. package/browser/card-compute.js +36 -17
  4. package/browser/live-cards.js +848 -109
  5. package/browser/live-cards.schema.json +46 -21
  6. package/dist/board-livegraph-runtime/index.cjs +1448 -0
  7. package/dist/board-livegraph-runtime/index.cjs.map +1 -0
  8. package/dist/board-livegraph-runtime/index.d.cts +101 -0
  9. package/dist/board-livegraph-runtime/index.d.ts +101 -0
  10. package/dist/board-livegraph-runtime/index.js +1441 -0
  11. package/dist/board-livegraph-runtime/index.js.map +1 -0
  12. package/dist/card-compute/index.cjs +159 -44
  13. package/dist/card-compute/index.cjs.map +1 -1
  14. package/dist/card-compute/index.d.cts +36 -11
  15. package/dist/card-compute/index.d.ts +36 -11
  16. package/dist/card-compute/index.js +156 -44
  17. package/dist/card-compute/index.js.map +1 -1
  18. package/dist/cli/board-live-cards-cli.cjs +476 -105
  19. package/dist/cli/board-live-cards-cli.cjs.map +1 -1
  20. package/dist/cli/board-live-cards-cli.d.cts +8 -16
  21. package/dist/cli/board-live-cards-cli.d.ts +8 -16
  22. package/dist/cli/board-live-cards-cli.js +476 -106
  23. package/dist/cli/board-live-cards-cli.js.map +1 -1
  24. package/dist/continuous-event-graph/index.cjs +74 -33
  25. package/dist/continuous-event-graph/index.cjs.map +1 -1
  26. package/dist/continuous-event-graph/index.d.cts +7 -23
  27. package/dist/continuous-event-graph/index.d.ts +7 -23
  28. package/dist/continuous-event-graph/index.js +73 -32
  29. package/dist/continuous-event-graph/index.js.map +1 -1
  30. package/dist/index.cjs +1440 -56
  31. package/dist/index.cjs.map +1 -1
  32. package/dist/index.d.cts +21 -3
  33. package/dist/index.d.ts +21 -3
  34. package/dist/index.js +1434 -56
  35. package/dist/index.js.map +1 -1
  36. package/dist/journal-DRfJiheM.d.cts +28 -0
  37. package/dist/journal-NLYuqege.d.ts +28 -0
  38. package/dist/{journal-B_2JnBMF.d.ts → live-cards-bridge-Or7fdEJV.d.ts} +5 -32
  39. package/dist/{journal-BJDjWb5Q.d.cts → live-cards-bridge-vGJ6tMzN.d.cts} +5 -32
  40. package/dist/schedule-CMcZe5Ny.d.ts +21 -0
  41. package/dist/schedule-CiucyCan.d.cts +21 -0
  42. package/examples/browser/boards/portfolio-tracker/cards/holdings-table.json +1 -1
  43. package/examples/browser/boards/portfolio-tracker/cards/portfolio-form.json +3 -3
  44. package/examples/browser/boards/portfolio-tracker/cards/portfolio-value.json +1 -1
  45. package/examples/browser/boards/portfolio-tracker/cards/price-fetch.json +3 -3
  46. package/examples/browser/boards/portfolio-tracker/portfolio-tracker-task-executor.cjs +96 -0
  47. package/examples/browser/boards/portfolio-tracker/portfolio-tracker.js +33 -5
  48. package/examples/browser/livecards-browser/index.html +37 -684
  49. package/examples/cli/step-machine-cli/portfolio-tracker/cards/holdings-table.json +1 -1
  50. package/examples/cli/step-machine-cli/portfolio-tracker/cards/portfolio-form.json +3 -3
  51. package/examples/cli/step-machine-cli/portfolio-tracker/cards/portfolio-value.json +1 -1
  52. package/examples/cli/step-machine-cli/portfolio-tracker/cards/price-fetch.json +3 -3
  53. package/examples/cli/step-machine-cli/portfolio-tracker/handlers/update-holdings-cli.js +2 -2
  54. package/examples/example-board/board.yaml +23 -0
  55. package/examples/example-board/bootstrap_payload.json +1 -0
  56. package/examples/example-board/cards/card-chain-region-alert.json +39 -0
  57. package/examples/example-board/cards/card-chain-region-totals.json +26 -0
  58. package/examples/example-board/cards/card-chain-top-region.json +24 -0
  59. package/examples/example-board/cards/card-ex-actions.json +32 -0
  60. package/examples/example-board/cards/card-ex-chart.json +30 -0
  61. package/examples/example-board/cards/card-ex-filter.json +36 -0
  62. package/examples/example-board/cards/card-ex-filtered-by-preference.json +59 -0
  63. package/examples/example-board/cards/card-ex-form.json +91 -0
  64. package/examples/example-board/cards/card-ex-list.json +22 -0
  65. package/examples/example-board/cards/card-ex-markdown.json +17 -0
  66. package/examples/example-board/cards/card-ex-metric.json +19 -0
  67. package/examples/example-board/cards/card-ex-narrative.json +36 -0
  68. package/examples/example-board/cards/card-ex-source-http.json +28 -0
  69. package/examples/example-board/cards/card-ex-source.json +21 -0
  70. package/examples/example-board/cards/card-ex-status.json +35 -0
  71. package/examples/example-board/cards/card-ex-table.json +30 -0
  72. package/examples/example-board/cards/card-ex-todo.json +29 -0
  73. package/examples/example-board/demo-chat-handler.js +69 -0
  74. package/examples/example-board/demo-server.js +87 -0
  75. package/examples/example-board/demo-shell-browser.html +806 -0
  76. package/examples/example-board/demo-shell-with-server.html +280 -0
  77. package/examples/example-board/demo-shell.html +62 -0
  78. package/examples/example-board/demo-task-executor.js +255 -0
  79. package/examples/example-board/mock.db +15 -0
  80. package/examples/example-board/reusable-board-runtime-client.js +265 -0
  81. package/examples/example-board/reusable-runtime-artifacts-adapter.js +233 -0
  82. package/examples/example-board/reusable-server-runtime.js +1284 -0
  83. package/examples/index.html +16 -9
  84. package/examples/npm-libs/continuous-event-graph/live-cards-board.ts +17 -17
  85. package/examples/npm-libs/continuous-event-graph/live-portfolio-dashboard.ts +23 -23
  86. package/examples/step-machine-cli/portfolio-tracker/cards/holdings-table.json +1 -1
  87. package/examples/step-machine-cli/portfolio-tracker/cards/portfolio-form.json +3 -3
  88. package/examples/step-machine-cli/portfolio-tracker/cards/portfolio-value.json +1 -1
  89. package/examples/step-machine-cli/portfolio-tracker/cards/price-fetch.json +1 -1
  90. package/examples/step-machine-cli/portfolio-tracker/portfolio-tracker-task-executor.cjs +96 -0
  91. package/package.json +16 -2
  92. package/schema/card-runtime.schema.json +25 -0
  93. package/schema/live-cards.schema.json +46 -21
  94. package/browser/ingest-board.js +0 -296
  95. package/examples/ingest.js +0 -733
@@ -1,733 +0,0 @@
1
- // ingest.js — Ingest engine: composable batch UI
2
- //
3
- // Two composables (all require opts.apiBase — no defaults):
4
- //
5
- // 1. IngestUI.pane(containerEl, opts) — full ingest pane with all batches (new + existing)
6
- // opts.apiBase — REQUIRED. API prefix, e.g. '/api' or '/api/repo/finbook-data'
7
- // opts.compact — smaller dropzone (default: false)
8
- // opts.layout — 'grid' (card grid, default) or 'stack' (vertical stack)
9
- // opts.onBatchChange — callback(batches) when any batch state changes
10
- // Returns: { destroy(), refresh() }
11
- //
12
- // 2. IngestUI.mount(containerEl, opts) — single-batch UI (lower-level)
13
- // opts.apiBase — REQUIRED. API prefix.
14
- // (see mount() for remaining opts)
15
- //
16
- // Backward compat: loadBatches() delegates to IngestUI.pane('#ingestContainer', { apiBase: '/api/repo/' + ACTIVE_REPO_ID })
17
- //
18
- // All API calls route through apiBase — zero hardcoded paths.
19
- // Multi-repo = different apiBase per pane instance.
20
-
21
- // ---- Shared constants ----
22
- const ALLOWED_EXTENSIONS = new Set([
23
- '.txt', '.csv', '.md', '.json', '.html', '.xml',
24
- '.pdf', '.xlsx', '.docx', '.pptx',
25
- '.png', '.jpg', '.jpeg'
26
- ]);
27
-
28
- // ---- Composable ingest UI ----
29
- // eslint-disable-next-line no-unused-vars
30
- var IngestUI = {
31
- /**
32
- * Mount a complete ingest interface (chat + dropzone + send + confirm) into a container.
33
- * @param {HTMLElement} containerEl — target DOM element
34
- * @param {object} opts — see header comment
35
- * @returns {{ destroy: Function, getBatchId: Function }}
36
- */
37
- mount: function(containerEl, opts) {
38
- opts = opts || {};
39
- if (!opts.apiBase) throw new Error('IngestUI.mount: opts.apiBase is required');
40
- var apiBase = opts.apiBase.replace(/\/$/, '');
41
- var apiFn = function(path) { return apiBase + path; };
42
- var batchId = opts.batchId || (opts.batch && opts.batch.id) || null;
43
- var batch = opts.batch || null;
44
- var showConfirm = opts.showConfirm !== false;
45
- var compact = opts.compact || false;
46
- var uid = 'iu-' + Date.now().toString(36) + Math.random().toString(36).substr(2, 4);
47
-
48
- var isActive = !batch || batch.status === 'ready' || batch.status === 'open-items';
49
- var isConfirmed = batch && batch.status === 'confirmed';
50
-
51
- // Build HTML
52
- var dropZoneClass = compact ? 'drop-zone drop-zone-sm' : 'drop-zone';
53
- var dropZoneContent = compact
54
- ? '<div class="small text-muted">+ Drop files</div>'
55
- : '<svg width="32" height="32" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="1.5" class="text-muted mb-2"><path d="M21 15v4a2 2 0 01-2 2H5a2 2 0 01-2-2v-4"/><polyline points="17 8 12 3 7 8"/><line x1="12" y1="3" x2="12" y2="15"/></svg><div class="small text-muted">Drop files here or click to browse</div>';
56
-
57
- var html = '<div class="ingest-ui d-flex flex-column h-100">';
58
- html += '<div class="chat-messages flex-grow-1 mb-2" id="' + uid + '-chat"></div>';
59
-
60
- if (isActive) {
61
- html += '<div class="batch-input-bar">';
62
- html += '<div class="' + dropZoneClass + ' mb-2" id="' + uid + '-drop">' + dropZoneContent;
63
- html += '<input type="file" id="' + uid + '-file" multiple class="d-none" accept=".txt,.csv,.md,.json,.html,.xml,.pdf,.xlsx,.docx,.pptx,.png,.jpg,.jpeg">';
64
- html += '</div>';
65
- html += '<div id="' + uid + '-staged"></div>';
66
- html += '<div class="input-group input-group-sm">';
67
- html += '<input type="text" id="' + uid + '-input" class="form-control" placeholder="Add files or type a message...">';
68
- html += '<button id="' + uid + '-send" class="btn btn-outline-primary" type="button">';
69
- html += '<svg width="14" height="14" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2"><line x1="22" y1="2" x2="11" y2="13"/><polygon points="22 2 15 22 11 13 2 9 22 2"/></svg>';
70
- html += '</button></div></div>';
71
- if (showConfirm) {
72
- html += '<div class="batch-actions d-flex gap-2 mt-2">';
73
- html += '<button id="' + uid + '-confirm" class="btn btn-success btn-sm"' + ((!batch || batch.status !== 'ready' || batch.processing) ? ' disabled' : '') + '>';
74
- html += '<svg width="14" height="14" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2"><polyline points="20 6 9 17 4 12"/></svg> Confirm &amp; Merge</button>';
75
- html += '<button id="' + uid + '-discard" class="btn btn-outline-danger btn-sm">';
76
- html += '<svg width="14" height="14" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2"><line x1="18" y1="6" x2="6" y2="18"/><line x1="6" y1="6" x2="18" y2="18"/></svg> Discard</button>';
77
- html += '</div>';
78
- }
79
- } else if (isConfirmed && batch.chatCount > 0) {
80
- html += '<span class="badge bg-secondary" id="' + uid + '-chatpill" role="button">💬 ' + batch.chatCount + ' message(s)</span>';
81
- }
82
- html += '</div>';
83
-
84
- containerEl.innerHTML = html;
85
-
86
- var chatId = uid + '-chat';
87
-
88
- // Populate existing chat
89
- if (batch && batch.chat && batch.chat.length > 0) {
90
- for (var i = 0; i < batch.chat.length; i++) appendChat(chatId, batch.chat[i].role, batch.chat[i].text);
91
- }
92
-
93
- if (!isActive) {
94
- // Confirmed batch: wire chat pill modal
95
- var chatPill = document.getElementById(uid + '-chatpill');
96
- if (chatPill) {
97
- chatPill.addEventListener('click', async function() {
98
- chatPill.textContent = '💬 Loading...';
99
- try {
100
- var msgs = await fetch(apiFn('/batch/' + batchId + '/chat')).then(function(r) { return r.json(); });
101
- showChatModal(batchId, msgs);
102
- } catch (e) { alert('Failed to load chat'); }
103
- chatPill.textContent = '💬 ' + (batch.chatCount || 0) + ' message(s)';
104
- });
105
- }
106
- return { destroy: function() { containerEl.innerHTML = ''; }, getBatchId: function() { return batchId; } };
107
- }
108
-
109
- // Wire active batch UI
110
- var dropZone = document.getElementById(uid + '-drop');
111
- var fileInput = document.getElementById(uid + '-file');
112
- var stagedContainer = document.getElementById(uid + '-staged');
113
- var chatInput = document.getElementById(uid + '-input');
114
- var sendBtn = document.getElementById(uid + '-send');
115
- var confirmBtn = document.getElementById(uid + '-confirm');
116
- var discardBtn = document.getElementById(uid + '-discard');
117
-
118
- var updateSendState = function() {
119
- if (sendBtn) sendBtn.disabled = staging.getFiles().length === 0 && !chatInput.value.trim();
120
- };
121
-
122
- var staging = createFileStagingUI(dropZone, fileInput, stagedContainer, chatInput, updateSendState);
123
- if (sendBtn) sendBtn.disabled = true;
124
-
125
- var doSend = async function() {
126
- var resultId = await unifiedSend(batchId, staging, chatInput, sendBtn, dropZone, chatId, confirmBtn, apiFn);
127
- if (resultId && !batchId) {
128
- batchId = resultId;
129
- if (opts.onBatchCreated) opts.onBatchCreated(batchId);
130
- }
131
- };
132
-
133
- if (sendBtn) sendBtn.addEventListener('click', doSend);
134
- if (chatInput) {
135
- chatInput.addEventListener('keydown', function(e) { if (e.key === 'Enter' && !sendBtn.disabled) doSend(); });
136
- chatInput.addEventListener('input', updateSendState);
137
- }
138
-
139
- // Confirm
140
- if (confirmBtn) {
141
- confirmBtn.addEventListener('click', async function() {
142
- confirmBtn.disabled = true;
143
- try {
144
- var resp = await fetch(apiFn('/batch/' + batchId + '/confirm'), { method: 'POST' });
145
- if (!resp.ok) {
146
- var err = await resp.json();
147
- appendChat(chatId, 'system', err.error || 'Confirm failed');
148
- confirmBtn.disabled = false;
149
- return;
150
- }
151
- appendChat(chatId, 'system', 'Confirming and merging...');
152
- connectSSE(batchId, chatId, sendBtn, chatInput, dropZone, confirmBtn, apiFn);
153
- if (opts.onConfirmed) opts.onConfirmed(batchId);
154
- } catch (e) {
155
- appendChat(chatId, 'system', 'Confirm failed: ' + e.message);
156
- confirmBtn.disabled = false;
157
- }
158
- });
159
- }
160
-
161
- // Discard
162
- if (discardBtn) {
163
- discardBtn.addEventListener('click', async function() {
164
- if (!confirm('Discard batch ' + (batchId || '') + '? This will delete the branch and all changes.')) return;
165
- discardBtn.disabled = true;
166
- try {
167
- await fetch(apiFn('/batch/' + batchId + '/discard'), { method: 'POST' });
168
- if (opts.onDiscarded) opts.onDiscarded(batchId);
169
- } catch (e) { discardBtn.disabled = false; }
170
- });
171
- }
172
-
173
- // If batch is processing, show spinner and connect SSE
174
- if (batch && batch.processing) {
175
- showProcessingIndicator(chatId);
176
- if (confirmBtn) confirmBtn.disabled = true;
177
- connectSSE(batchId, chatId, sendBtn, chatInput, dropZone, confirmBtn, apiFn);
178
- }
179
-
180
- return {
181
- destroy: function() { containerEl.innerHTML = ''; },
182
- getBatchId: function() { return batchId; }
183
- };
184
- },
185
-
186
- /**
187
- * Mount a full ingest pane (all batches: new + active + confirmed) into a container.
188
- * Self-contained: manages its own state, fetch, render, SSE, polling.
189
- * @param {HTMLElement} containerEl — target DOM element
190
- * @param {object} opts — { apiBase, compact, layout, onBatchChange }
191
- * @returns {{ destroy: Function, refresh: Function }}
192
- */
193
- pane: function(containerEl, opts) {
194
- opts = opts || {};
195
- if (!opts.apiBase) throw new Error('IngestUI.pane: opts.apiBase is required');
196
- var apiBase = opts.apiBase.replace(/\/$/, '');
197
- var compact = opts.compact || false;
198
- var layout = opts.layout || 'grid';
199
- var onBatchChange = opts.onBatchChange || null;
200
- var batches = [];
201
- var pollId = null;
202
- var destroyed = false;
203
- var uid = 'ip-' + Date.now().toString(36) + Math.random().toString(36).substr(2, 4);
204
-
205
- function api(path) { return apiBase + path; }
206
-
207
- function refresh() {
208
- if (destroyed) return;
209
- return fetch(api('/batches'))
210
- .then(function(r) { return r.ok ? r.json() : []; })
211
- .then(function(data) {
212
- batches = data;
213
- render();
214
- if (onBatchChange) onBatchChange(batches);
215
- })
216
- .catch(function(e) { console.error('IngestUI.pane: load failed', e); });
217
- }
218
-
219
- function render() {
220
- if (destroyed || !containerEl) return;
221
- if (pollId) { clearInterval(pollId); pollId = null; }
222
-
223
- var hasActive = batches.some(function(b) { return b.status === 'ready' || b.status === 'open-items'; });
224
- var layoutClass = layout === 'stack' ? '' : 'row row-cols-1 row-cols-md-2 row-cols-xl-3 g-3';
225
- var html = '<div class="' + layoutClass + '">';
226
- if (!hasActive) html += renderPaneNewCard(uid, compact);
227
- for (var i = 0; i < batches.length; i++) {
228
- html += renderPaneBatchCard(batches[i], uid, compact);
229
- }
230
- html += '</div>';
231
- containerEl.innerHTML = html;
232
-
233
- if (!hasActive) wirePaneNewCard(uid, compact);
234
- for (var j = 0; j < batches.length; j++) {
235
- wirePaneBatchCard(batches[j], uid);
236
- }
237
-
238
- var hasProcessing = batches.some(function(b) { return b.processing; });
239
- if (hasProcessing) {
240
- pollId = setInterval(function() { refresh(); }, 3000);
241
- }
242
- }
243
-
244
- // ---- New Batch Card ----
245
- function renderPaneNewCard(puid, isCompact) {
246
- var dzClass = isCompact ? 'drop-zone drop-zone-sm' : 'drop-zone';
247
- var dzContent = isCompact
248
- ? '<div class="small text-muted">+ Drop files</div>'
249
- : '<svg width="32" height="32" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="1.5" class="text-muted mb-2"><path d="M21 15v4a2 2 0 01-2 2H5a2 2 0 01-2-2v-4"/><polyline points="17 8 12 3 7 8"/><line x1="12" y1="3" x2="12" y2="15"/></svg><div class="small text-muted">Drop files here or click to browse</div>';
250
- return '<div class="col"><div class="card h-100 border-primary" id="' + puid + '-newCard">' +
251
- '<div class="card-header bg-primary text-white d-flex align-items-center gap-2">' +
252
- '<svg width="16" height="16" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2"><line x1="12" y1="5" x2="12" y2="19"/><line x1="5" y1="12" x2="19" y2="12"/></svg> New Batch</div>' +
253
- '<div class="card-body d-flex flex-column">' +
254
- '<div id="' + puid + '-newChat" class="chat-messages flex-grow-1 mb-2"></div>' +
255
- '<div class="batch-input-bar">' +
256
- '<div class="' + dzClass + ' mb-2" id="' + puid + '-newDrop">' + dzContent +
257
- '<input type="file" id="' + puid + '-newFile" multiple class="d-none" accept=".txt,.csv,.md,.json,.html,.xml,.pdf,.xlsx,.docx,.pptx,.png,.jpg,.jpeg"></div>' +
258
- '<div id="' + puid + '-newStaged"></div>' +
259
- '<div class="input-group input-group-sm">' +
260
- '<input type="text" id="' + puid + '-newInput" class="form-control" placeholder="Add files or type a message...">' +
261
- '<button id="' + puid + '-newSend" class="btn btn-outline-primary" type="button">' +
262
- '<svg width="14" height="14" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2"><line x1="22" y1="2" x2="11" y2="13"/><polygon points="22 2 15 22 11 13 2 9 22 2"/></svg>' +
263
- '</button></div></div></div></div></div>';
264
- }
265
-
266
- function wirePaneNewCard(puid) {
267
- var drop = document.getElementById(puid + '-newDrop');
268
- var fileIn = document.getElementById(puid + '-newFile');
269
- var staged = document.getElementById(puid + '-newStaged');
270
- var input = document.getElementById(puid + '-newInput');
271
- var sendBtn = document.getElementById(puid + '-newSend');
272
- if (!drop || !fileIn) return;
273
-
274
- var updateSendState = function() {
275
- sendBtn.disabled = staging.getFiles().length === 0 && !input.value.trim();
276
- };
277
- var staging = createFileStagingUI(drop, fileIn, staged, input, updateSendState);
278
- sendBtn.disabled = true;
279
- var activeBatchId = null;
280
-
281
- var doSend = async function() {
282
- var resultId = await unifiedSend(activeBatchId, staging, input, sendBtn, drop, puid + '-newChat', null, api);
283
- if (resultId) activeBatchId = resultId;
284
- };
285
- sendBtn.addEventListener('click', doSend);
286
- input.addEventListener('keydown', function(e) { if (e.key === 'Enter' && !sendBtn.disabled) doSend(); });
287
- input.addEventListener('input', updateSendState);
288
- }
289
-
290
- // ---- Existing Batch Card ----
291
- function renderPaneBatchCard(batch, puid, isCompact) {
292
- var isConfirmed = batch.status === 'confirmed';
293
- var statusBadge = isConfirmed
294
- ? '<span class="badge bg-success">Confirmed</span>'
295
- : batch.status === 'open-items'
296
- ? '<span class="badge bg-warning text-dark">Open Items</span>'
297
- : '<span class="badge bg-info">Ready</span>';
298
-
299
- var filesHtml = (batch.files || []).map(function(f) {
300
- return '<a href="' + api('/batch/' + batch.id + '/files/' + encodeURIComponent(f)) + '" class="batch-file-link d-flex align-items-center gap-1 small" target="_blank" download>' +
301
- '<svg width="12" height="12" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2"><path d="M14 2H6a2 2 0 00-2 2v16a2 2 0 002 2h12a2 2 0 002-2V8z"/><polyline points="14 2 14 8 20 8"/></svg>' +
302
- escapeHtml(f) + '</a>';
303
- }).join('');
304
-
305
- var decisionsHtml = batch.decisions && batch.decisions.length > 0
306
- ? '<div class="mt-2"><span class="badge bg-secondary ' + puid + '-decisions-pill" data-batch="' + batch.id + '" role="button" tabindex="0">' +
307
- batch.decisions.length + ' decision(s) resolved</span>' +
308
- '<div class="decisions-popover d-none" id="' + puid + '-decisions-' + batch.id + '">' +
309
- '<div class="small">' + batch.decisions.map(function(d) { return escapeHtml(d); }).join('<hr>') + '</div></div></div>'
310
- : '';
311
-
312
- var isActive = batch.status === 'ready' || batch.status === 'open-items';
313
- var dzClass = isCompact ? 'drop-zone drop-zone-sm' : 'drop-zone drop-zone-sm';
314
-
315
- var activeHtml = '';
316
- if (isActive) {
317
- activeHtml = '<div class="batch-active-layout d-flex flex-column">' +
318
- '<div class="chat-messages flex-grow-1 mb-2" id="' + puid + '-batchChat-' + batch.id + '"></div>' +
319
- '<div class="batch-input-bar">' +
320
- '<div class="' + dzClass + ' mb-2" id="' + puid + '-batchDrop-' + batch.id + '">' +
321
- '<div class="small text-muted">+ Drop more files</div>' +
322
- '<input type="file" class="d-none" id="' + puid + '-batchFile-' + batch.id + '" multiple accept=".txt,.csv,.md,.json,.html,.xml,.pdf,.xlsx,.docx,.pptx,.png,.jpg,.jpeg"></div>' +
323
- '<div id="' + puid + '-batchStaged-' + batch.id + '"></div>' +
324
- '<div class="input-group input-group-sm">' +
325
- '<input type="text" class="form-control" id="' + puid + '-batchInput-' + batch.id + '" placeholder="Add files or type a message...">' +
326
- '<button class="btn btn-outline-primary" id="' + puid + '-batchSend-' + batch.id + '" type="button">' +
327
- '<svg width="14" height="14" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2"><line x1="22" y1="2" x2="11" y2="13"/><polygon points="22 2 15 22 11 13 2 9 22 2"/></svg>' +
328
- '</button></div></div>' +
329
- '<div class="batch-actions d-flex gap-2 mt-2">' +
330
- '<button class="btn btn-success btn-sm" id="' + puid + '-batchConfirm-' + batch.id + '"' + (batch.status !== 'ready' || batch.processing ? ' disabled' : '') + '>' +
331
- '<svg width="14" height="14" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2"><polyline points="20 6 9 17 4 12"/></svg> Confirm &amp; Merge</button>' +
332
- '<button class="btn btn-outline-danger btn-sm" id="' + puid + '-batchDiscard-' + batch.id + '">' +
333
- '<svg width="14" height="14" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2"><line x1="18" y1="6" x2="6" y2="18"/><line x1="6" y1="6" x2="18" y2="18"/></svg> Discard</button></div></div>';
334
- }
335
-
336
- var confirmedChatHtml = '';
337
- if (isConfirmed && batch.chatCount > 0) {
338
- confirmedChatHtml = '<div class="mt-2"><span class="badge bg-secondary ' + puid + '-chat-pill" data-batch="' + batch.id + '" role="button" tabindex="0">💬 ' + batch.chatCount + ' message(s)</span></div>';
339
- }
340
-
341
- var summaryHtml = '';
342
- if (isConfirmed && batch.summary) {
343
- summaryHtml = '<div class="batch-summary small text-muted mt-2">' + escapeHtml(batch.summary) + '</div>';
344
- }
345
-
346
- return '<div class="col"><div class="card h-100' + (isActive ? ' border-primary' : '') + '">' +
347
- '<div class="card-header d-flex align-items-center justify-content-between">' +
348
- '<span class="small fw-medium">' + batch.id + '</span>' + statusBadge + '</div>' +
349
- '<div class="card-body">' +
350
- '<div class="batch-files">' + filesHtml + '</div>' +
351
- summaryHtml + decisionsHtml + activeHtml + confirmedChatHtml +
352
- '</div></div></div>';
353
- }
354
-
355
- function wirePaneBatchCard(batch, puid) {
356
- // Decisions pill hover
357
- var pill = containerEl.querySelector('.' + puid + '-decisions-pill[data-batch="' + batch.id + '"]');
358
- if (pill) {
359
- var popover = document.getElementById(puid + '-decisions-' + batch.id);
360
- pill.addEventListener('mouseenter', function() { popover.classList.remove('d-none'); });
361
- pill.addEventListener('mouseleave', function() { popover.classList.add('d-none'); });
362
- }
363
-
364
- var isActive = batch.status === 'ready' || batch.status === 'open-items';
365
- var chatId = puid + '-batchChat-' + batch.id;
366
-
367
- // Confirmed: chat pill opens modal
368
- var chatPill = containerEl.querySelector('.' + puid + '-chat-pill[data-batch="' + batch.id + '"]');
369
- if (chatPill) {
370
- chatPill.addEventListener('click', async function() {
371
- chatPill.textContent = '💬 Loading...';
372
- try {
373
- var msgs = await fetch(api('/batch/' + batch.id + '/chat')).then(function(r) { return r.json(); });
374
- showChatModal(batch.id, msgs);
375
- } catch (e) { alert('Failed to load chat'); }
376
- chatPill.textContent = '💬 ' + batch.chatCount + ' message(s)';
377
- });
378
- }
379
-
380
- if (!isActive) return;
381
-
382
- // Populate existing chat
383
- if (batch.chat && batch.chat.length > 0) {
384
- for (var i = 0; i < batch.chat.length; i++) appendChat(chatId, batch.chat[i].role, batch.chat[i].text);
385
- }
386
-
387
- var confirmBtn = document.getElementById(puid + '-batchConfirm-' + batch.id);
388
- var sendBtn = document.getElementById(puid + '-batchSend-' + batch.id);
389
- var input = document.getElementById(puid + '-batchInput-' + batch.id);
390
- var drop = document.getElementById(puid + '-batchDrop-' + batch.id);
391
- var fileIn = document.getElementById(puid + '-batchFile-' + batch.id);
392
- var staged = document.getElementById(puid + '-batchStaged-' + batch.id);
393
-
394
- // Processing: spinner + SSE
395
- if (batch.processing) {
396
- showProcessingIndicator(chatId);
397
- if (confirmBtn) confirmBtn.disabled = true;
398
- connectSSE(batch.id, chatId, sendBtn, input, drop, confirmBtn, api, function() { refresh(); });
399
- }
400
-
401
- // File staging + send
402
- var updateSendState = function() {
403
- if (sendBtn) sendBtn.disabled = staging.getFiles().length === 0 && !input.value.trim();
404
- };
405
- var staging = createFileStagingUI(drop, fileIn, staged, input, updateSendState);
406
- if (sendBtn) sendBtn.disabled = true;
407
-
408
- if (sendBtn && input) {
409
- var doSend = function() { return unifiedSend(batch.id, staging, input, sendBtn, drop, chatId, confirmBtn, api); };
410
- sendBtn.addEventListener('click', doSend);
411
- input.addEventListener('keydown', function(e) { if (e.key === 'Enter' && !sendBtn.disabled) doSend(); });
412
- input.addEventListener('input', updateSendState);
413
- }
414
-
415
- // Confirm
416
- if (confirmBtn) {
417
- confirmBtn.addEventListener('click', async function() {
418
- confirmBtn.disabled = true;
419
- try {
420
- var resp = await fetch(api('/batch/' + batch.id + '/confirm'), { method: 'POST' });
421
- if (!resp.ok) {
422
- var err = await resp.json();
423
- appendChat(chatId, 'system', err.error || 'Confirm failed');
424
- confirmBtn.disabled = false;
425
- return;
426
- }
427
- appendChat(chatId, 'system', 'Confirming and merging...');
428
- connectSSE(batch.id, chatId, sendBtn, input, drop, confirmBtn, api, function() { refresh(); });
429
- } catch (e) {
430
- appendChat(chatId, 'system', 'Confirm failed: ' + e.message);
431
- confirmBtn.disabled = false;
432
- }
433
- });
434
- }
435
-
436
- // Discard
437
- var discardBtn = document.getElementById(puid + '-batchDiscard-' + batch.id);
438
- if (discardBtn) {
439
- discardBtn.addEventListener('click', async function() {
440
- if (!confirm('Discard batch ' + batch.id + '? This will delete the branch and all changes.')) return;
441
- discardBtn.disabled = true;
442
- try {
443
- await fetch(api('/batch/' + batch.id + '/discard'), { method: 'POST' });
444
- refresh();
445
- } catch (e) { discardBtn.disabled = false; }
446
- });
447
- }
448
- }
449
-
450
- // Initial load
451
- refresh();
452
-
453
- return {
454
- destroy: function() {
455
- destroyed = true;
456
- if (pollId) { clearInterval(pollId); pollId = null; }
457
- containerEl.innerHTML = '';
458
- },
459
- refresh: refresh
460
- };
461
- }
462
- };
463
-
464
- // ---- Section-mode backward compat ----
465
- // loadBatches() delegates to IngestUI.pane() targeting #ingestContainer.
466
- // Uses the active repo ID from the platform for namespaced routing.
467
- var _sectionPane = null;
468
- function loadBatches() {
469
- var container = document.getElementById('ingestContainer');
470
- if (!container) return;
471
- var repoId = window.ACTIVE_REPO_ID;
472
- if (!repoId) { console.warn('loadBatches: no ACTIVE_REPO_ID set'); return; }
473
- if (_sectionPane) { _sectionPane.refresh(); return; }
474
- _sectionPane = IngestUI.pane(container, { apiBase: '/api/repo/' + repoId });
475
- }
476
-
477
- // ---- Shared: file staging logic ----
478
- function createFileStagingUI(dropZoneEl, fileInputEl, stagedContainerEl, inputEl, onChange) {
479
- let stagedFiles = [];
480
-
481
- dropZoneEl.addEventListener('click', () => fileInputEl.click());
482
- dropZoneEl.addEventListener('dragover', e => { e.preventDefault(); dropZoneEl.classList.add('drag-over'); });
483
- dropZoneEl.addEventListener('dragleave', () => dropZoneEl.classList.remove('drag-over'));
484
- dropZoneEl.addEventListener('drop', e => {
485
- e.preventDefault();
486
- dropZoneEl.classList.remove('drag-over');
487
- addFiles(e.dataTransfer.files);
488
- });
489
- fileInputEl.addEventListener('change', e => {
490
- addFiles(e.target.files);
491
- e.target.value = '';
492
- });
493
-
494
- function addFiles(fileList) {
495
- const rejected = [];
496
- for (const f of fileList) {
497
- const ext = '.' + f.name.split('.').pop().toLowerCase();
498
- if (!ALLOWED_EXTENSIONS.has(ext)) { rejected.push(f.name); continue; }
499
- if (!stagedFiles.find(s => s.name === f.name)) stagedFiles.push(f);
500
- }
501
- if (rejected.length > 0) {
502
- alert('Unsupported file(s) skipped:\n' + rejected.join('\n') +
503
- '\n\nSupported: ' + [...ALLOWED_EXTENSIONS].join(', '));
504
- }
505
- renderStaged();
506
- if (onChange) onChange();
507
- }
508
-
509
- function renderStaged() {
510
- if (stagedFiles.length === 0) {
511
- stagedContainerEl.innerHTML = '';
512
- if (inputEl) inputEl.placeholder = 'Add files or type a message...';
513
- return;
514
- }
515
- if (inputEl) inputEl.placeholder = 'Add a note (optional) and hit Send to process...';
516
- stagedContainerEl.innerHTML = stagedFiles.map((f, i) => `
517
- <div class="staged-file d-flex align-items-center gap-2 mb-1">
518
- <svg width="14" height="14" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2"><path d="M14 2H6a2 2 0 00-2 2v16a2 2 0 002 2h12a2 2 0 002-2V8z"/><polyline points="14 2 14 8 20 8"/></svg>
519
- <span class="small flex-grow-1 text-truncate">${f.name}</span>
520
- <button class="btn btn-sm btn-link text-danger p-0 remove-staged-file" data-idx="${i}" title="Remove">&times;</button>
521
- </div>
522
- `).join('');
523
- stagedContainerEl.querySelectorAll('.remove-staged-file').forEach(btn => {
524
- btn.addEventListener('click', () => {
525
- stagedFiles.splice(parseInt(btn.dataset.idx), 1);
526
- renderStaged();
527
- if (onChange) onChange();
528
- });
529
- });
530
- }
531
-
532
- return {
533
- getFiles: () => stagedFiles,
534
- clear: () => { stagedFiles = []; renderStaged(); }
535
- };
536
- }
537
-
538
- // ---- Shared: unified send (files + optional message, or message only) ----
539
- // apiFn(path) → full URL. onDone() called after SSE completes.
540
- async function unifiedSend(batchId, staging, inputEl, sendBtnEl, dropZoneEl, chatContainerId, confirmBtnEl, apiFn, onDone) {
541
- const message = inputEl.value.trim();
542
- const files = staging.getFiles();
543
-
544
- if (files.length === 0 && !message) return null;
545
-
546
- // Get or create batch ID
547
- let targetBatchId = batchId;
548
- if (!targetBatchId) {
549
- try {
550
- const resp = await fetch(apiFn('/batch/new'), { method: 'POST' });
551
- const result = await resp.json();
552
- targetBatchId = result.batchId;
553
- } catch (e) {
554
- appendChat(chatContainerId, 'system', 'Failed to create batch: ' + e.message);
555
- return null;
556
- }
557
- }
558
-
559
- // Build request body — always FormData
560
- const body = new FormData();
561
- for (const f of files) body.append('files', f, f.name);
562
- if (message) body.append('message', message);
563
-
564
- // Show in chat
565
- if (message) appendChat(chatContainerId, 'user', message);
566
- for (const f of files) appendChat(chatContainerId, 'system', '📎 ' + f.name);
567
-
568
- // Disable UI
569
- sendBtnEl.disabled = true;
570
- inputEl.disabled = true;
571
- inputEl.value = '';
572
- if (dropZoneEl) dropZoneEl.classList.add('disabled');
573
- if (confirmBtnEl) confirmBtnEl.disabled = true;
574
- staging.clear();
575
- showProcessingIndicator(chatContainerId);
576
-
577
- // Single API call
578
- try {
579
- const resp = await fetch(apiFn('/batch/' + targetBatchId + '/send'), { method: 'POST', body });
580
- const result = await resp.json();
581
- if (!resp.ok) {
582
- removeProcessingIndicator(chatContainerId);
583
- appendChat(chatContainerId, 'system', result.error || 'Request failed');
584
- sendBtnEl.disabled = false;
585
- inputEl.disabled = false;
586
- if (dropZoneEl) dropZoneEl.classList.remove('disabled');
587
- return null;
588
- }
589
- connectSSE(targetBatchId, chatContainerId, sendBtnEl, inputEl, dropZoneEl, confirmBtnEl, apiFn, onDone);
590
- } catch (e) {
591
- removeProcessingIndicator(chatContainerId);
592
- appendChat(chatContainerId, 'system', 'Failed: ' + e.message);
593
- sendBtnEl.disabled = false;
594
- inputEl.disabled = false;
595
- if (dropZoneEl) dropZoneEl.classList.remove('disabled');
596
- return null;
597
- }
598
-
599
- return targetBatchId;
600
- }
601
-
602
- // ---- Shared: SSE connection ----
603
- // apiFn(path) → full URL. onDone() called when SSE signals completion.
604
- function connectSSE(batchId, chatContainerId, sendBtnEl, inputEl, dropZoneEl, confirmBtnEl, apiFn, onDone) {
605
- const sse = new EventSource(apiFn('/batch/' + batchId + '/stream'));
606
-
607
- sse.addEventListener('processing', e => {
608
- showProcessingIndicator(chatContainerId);
609
- if (confirmBtnEl) confirmBtnEl.disabled = true;
610
- });
611
-
612
- sse.addEventListener('message', e => {
613
- const data = JSON.parse(e.data);
614
- if (data.role) {
615
- if (data.role === 'system' && /^(Processing|Running|Ingesting|Starting|Loading)/i.test(data.text.trim())) {
616
- updateProcessingIndicator(chatContainerId, data.text);
617
- return;
618
- }
619
- removeProcessingIndicator(chatContainerId);
620
- appendChat(chatContainerId, data.role, data.text);
621
- }
622
- });
623
-
624
- sse.addEventListener('done', e => {
625
- const data = JSON.parse(e.data);
626
- removeProcessingIndicator(chatContainerId);
627
- appendChat(chatContainerId, 'system', data.summary || 'Done.');
628
- if (sendBtnEl) sendBtnEl.disabled = false;
629
- if (inputEl) { inputEl.disabled = false; inputEl.placeholder = 'Add files or type a message...'; }
630
- if (dropZoneEl) dropZoneEl.classList.remove('disabled');
631
- if (confirmBtnEl) confirmBtnEl.disabled = false;
632
- if (onDone) onDone();
633
- sse.close();
634
- });
635
-
636
- sse.onerror = () => {
637
- sse.close();
638
- if (sendBtnEl) sendBtnEl.disabled = false;
639
- if (inputEl) inputEl.disabled = false;
640
- if (dropZoneEl) dropZoneEl.classList.remove('disabled');
641
- removeProcessingIndicator(chatContainerId);
642
- };
643
- }
644
-
645
- // ---- Shared: chat helpers ----
646
- function appendChat(containerId, role, text) {
647
- const container = document.getElementById(containerId);
648
- if (!container) return;
649
- removeProcessingIndicator(containerId);
650
- const bubble = document.createElement('div');
651
- bubble.className = `chat-bubble chat-${role}`;
652
- const icon = role === 'user' ? '👤' : role === 'assistant' ? '🤖' : 'ℹ️';
653
- const rendered = role === 'assistant' && typeof marked !== 'undefined'
654
- ? marked.parse(text)
655
- : escapeHtml(text);
656
- bubble.innerHTML = `<span class="chat-icon">${icon}</span><span class="chat-text">${rendered}</span>`;
657
- container.appendChild(bubble);
658
- container.scrollTop = container.scrollHeight;
659
- }
660
-
661
- function showProcessingIndicator(containerId) {
662
- const container = document.getElementById(containerId);
663
- if (!container) return;
664
- removeProcessingIndicator(containerId);
665
- const indicator = document.createElement('div');
666
- indicator.className = 'chat-bubble chat-system chat-processing';
667
- indicator.innerHTML = `<span class="chat-icon"><span class="spinner-border spinner-border-sm" role="status"></span></span><span class="chat-text">Processing...</span>`;
668
- container.appendChild(indicator);
669
- container.scrollTop = container.scrollHeight;
670
- }
671
-
672
- function removeProcessingIndicator(containerId) {
673
- const container = document.getElementById(containerId);
674
- if (!container) return;
675
- const indicator = container.querySelector('.chat-processing');
676
- if (indicator) indicator.remove();
677
- }
678
-
679
- function updateProcessingIndicator(containerId, text) {
680
- const container = document.getElementById(containerId);
681
- if (!container) return;
682
- let indicator = container.querySelector('.chat-processing');
683
- if (!indicator) {
684
- showProcessingIndicator(containerId);
685
- indicator = container.querySelector('.chat-processing');
686
- }
687
- if (indicator) {
688
- const span = indicator.querySelector('.chat-text');
689
- if (span) span.textContent = text;
690
- }
691
- }
692
-
693
- function escapeHtml(text) {
694
- const div = document.createElement('div');
695
- div.textContent = text;
696
- return div.innerHTML;
697
- }
698
-
699
- function showChatModal(batchId, messages) {
700
- // Remove any existing modal
701
- let modal = document.getElementById('chatModal');
702
- if (modal) modal.remove();
703
- let backdrop = document.querySelector('.modal-backdrop');
704
- if (backdrop) backdrop.remove();
705
-
706
- const modalEl = document.createElement('div');
707
- modalEl.innerHTML = `
708
- <div class="modal fade" id="chatModal" tabindex="-1">
709
- <div class="modal-dialog modal-dialog-scrollable">
710
- <div class="modal-content">
711
- <div class="modal-header">
712
- <h6 class="modal-title">${escapeHtml(batchId)}</h6>
713
- <button type="button" class="btn-close" data-bs-dismiss="modal"></button>
714
- </div>
715
- <div class="modal-body">
716
- <div class="chat-messages" id="modalChat-${batchId}"></div>
717
- </div>
718
- </div>
719
- </div>
720
- </div>`;
721
- document.body.appendChild(modalEl.firstElementChild);
722
-
723
- // Reuse appendChat for consistent markdown rendering
724
- for (const msg of messages) appendChat(`modalChat-${batchId}`, msg.role, msg.text);
725
-
726
- const bsModal = new bootstrap.Modal(document.getElementById('chatModal'));
727
- bsModal.show();
728
-
729
- // Clean up on close
730
- document.getElementById('chatModal').addEventListener('hidden.bs.modal', () => {
731
- document.getElementById('chatModal').remove();
732
- });
733
- }