yaml-flow 3.1.0 → 4.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.
- package/README.md +81 -20
- package/board-live-cards-cli.js +37 -0
- package/browser/card-compute.js +132 -431
- package/browser/live-cards.js +41 -27
- package/browser/live-cards.schema.json +59 -77
- package/dist/card-compute/index.cjs +135 -415
- package/dist/card-compute/index.cjs.map +1 -1
- package/dist/card-compute/index.d.cts +52 -49
- package/dist/card-compute/index.d.ts +52 -49
- package/dist/card-compute/index.js +134 -415
- package/dist/card-compute/index.js.map +1 -1
- package/dist/cli/board-live-cards-cli.cjs +2379 -0
- package/dist/cli/board-live-cards-cli.cjs.map +1 -0
- package/dist/cli/board-live-cards-cli.d.cts +213 -0
- package/dist/cli/board-live-cards-cli.d.ts +213 -0
- package/dist/cli/board-live-cards-cli.js +2332 -0
- package/dist/cli/board-live-cards-cli.js.map +1 -0
- package/dist/{constants-B2zqu10b.d.ts → constants-DuzE5n03.d.ts} +2 -2
- package/dist/{constants-DJZU1pwJ.d.cts → constants-ozjf1Ejw.d.cts} +2 -2
- package/dist/continuous-event-graph/index.cjs +201 -448
- package/dist/continuous-event-graph/index.cjs.map +1 -1
- package/dist/continuous-event-graph/index.d.cts +16 -340
- package/dist/continuous-event-graph/index.d.ts +16 -340
- package/dist/continuous-event-graph/index.js +198 -448
- package/dist/continuous-event-graph/index.js.map +1 -1
- package/dist/event-graph/index.cjs +4 -4
- package/dist/event-graph/index.cjs.map +1 -1
- package/dist/event-graph/index.d.cts +5 -5
- package/dist/event-graph/index.d.ts +5 -5
- package/dist/event-graph/index.js +4 -4
- package/dist/event-graph/index.js.map +1 -1
- package/dist/index.cjs +278 -533
- package/dist/index.cjs.map +1 -1
- package/dist/index.d.cts +8 -7
- package/dist/index.d.ts +8 -7
- package/dist/index.js +278 -533
- package/dist/index.js.map +1 -1
- package/dist/inference/index.cjs +138 -19
- package/dist/inference/index.cjs.map +1 -1
- package/dist/inference/index.d.cts +2 -2
- package/dist/inference/index.d.ts +2 -2
- package/dist/inference/index.js +138 -19
- package/dist/inference/index.js.map +1 -1
- package/dist/journal-BJDjWb5Q.d.cts +343 -0
- package/dist/journal-B_2JnBMF.d.ts +343 -0
- package/dist/step-machine/index.cjs +18 -1
- package/dist/step-machine/index.cjs.map +1 -1
- package/dist/step-machine/index.d.cts +2 -2
- package/dist/step-machine/index.d.ts +2 -2
- package/dist/step-machine/index.js +18 -1
- package/dist/step-machine/index.js.map +1 -1
- package/dist/stores/file.d.cts +1 -1
- package/dist/stores/file.d.ts +1 -1
- package/dist/stores/index.d.cts +1 -1
- package/dist/stores/index.d.ts +1 -1
- package/dist/stores/localStorage.d.cts +1 -1
- package/dist/stores/localStorage.d.ts +1 -1
- package/dist/stores/memory.d.cts +1 -1
- package/dist/stores/memory.d.ts +1 -1
- package/dist/{types-BwvgvlOO.d.cts → types-BzLD8bjb.d.cts} +1 -1
- package/dist/{types-ClRA8hzC.d.ts → types-C2eJ7DAV.d.ts} +1 -1
- package/dist/{types-DEj7OakX.d.cts → types-CMFSIjpc.d.cts} +39 -4
- package/dist/{types-DEj7OakX.d.ts → types-CMFSIjpc.d.ts} +39 -4
- package/dist/{types-FZ_eyErS.d.cts → types-ycun84cq.d.cts} +1 -0
- package/dist/{types-FZ_eyErS.d.ts → types-ycun84cq.d.ts} +1 -0
- package/dist/{validate-DEZ2Ymdb.d.ts → validate-DJQTQ6bP.d.ts} +1 -1
- package/dist/{validate-DqKTZg_o.d.cts → validate-ke92Cleg.d.cts} +1 -1
- package/examples/browser/boards/portfolio-tracker/cards/holdings-table.json +22 -0
- package/examples/browser/boards/portfolio-tracker/cards/portfolio-form.json +16 -0
- package/examples/browser/boards/portfolio-tracker/cards/portfolio-value.json +15 -0
- package/examples/browser/boards/portfolio-tracker/cards/price-fetch.json +15 -0
- package/examples/browser/boards/portfolio-tracker/fetch-prices.js +43 -0
- package/examples/browser/boards/portfolio-tracker/portfolio-tracker.bat +7 -0
- package/examples/browser/boards/portfolio-tracker/portfolio-tracker.js +189 -0
- package/examples/browser/livecards-browser/index.html +688 -0
- package/examples/browser/step-machine-browser/index.html +367 -0
- package/examples/cli/step-machine-cli/portfolio-tracker/cards/holdings-table.json +22 -0
- package/examples/cli/step-machine-cli/portfolio-tracker/cards/portfolio-form.json +43 -0
- package/examples/cli/step-machine-cli/portfolio-tracker/cards/portfolio-value.json +15 -0
- package/examples/cli/step-machine-cli/portfolio-tracker/cards/price-fetch.json +15 -0
- package/examples/cli/step-machine-cli/portfolio-tracker/fetch-prices.js +48 -0
- package/examples/cli/step-machine-cli/portfolio-tracker/handlers/_board-cli.js +58 -0
- package/examples/cli/step-machine-cli/portfolio-tracker/handlers/add-cards-cli.js +27 -0
- package/examples/cli/step-machine-cli/portfolio-tracker/handlers/init-board-cli.js +25 -0
- package/examples/cli/step-machine-cli/portfolio-tracker/handlers/reset-board-dir-cli.js +29 -0
- package/examples/cli/step-machine-cli/portfolio-tracker/handlers/retrigger-cli.js +27 -0
- package/examples/cli/step-machine-cli/portfolio-tracker/handlers/status-cli.js +25 -0
- package/examples/cli/step-machine-cli/portfolio-tracker/handlers/update-holdings-cli.js +37 -0
- package/examples/cli/step-machine-cli/portfolio-tracker/handlers/wait-completed-cli.js +53 -0
- package/examples/cli/step-machine-cli/portfolio-tracker/handlers/write-prices-cli.js +35 -0
- package/examples/cli/step-machine-cli/portfolio-tracker/portfolio-tracker.flow.yaml +227 -0
- package/examples/cli/step-machine-cli/portfolio-tracker/portfolio-tracker.input.json +38 -0
- package/examples/cli/step-machine-cli/portfolio-tracker/run-portfolio-tracker.bat +29 -0
- package/examples/cli/step-machine-demo/jsonata-init-board-cli.js +36 -0
- package/examples/cli/step-machine-demo/jsonata-init-board.flow.yaml +30 -0
- package/examples/cli/step-machine-demo/one-step-cli-only.flow.yaml +19 -0
- package/examples/cli/step-machine-demo/step-cli-echo-y.js +15 -0
- package/examples/cli/step-machine-demo/step2-double-cli.js +39 -0
- package/examples/cli/step-machine-demo/two-step-math-handlers.js +32 -0
- package/examples/cli/step-machine-demo/two-step-math.flow.yaml +31 -0
- package/examples/cli/step-machine-demo/two-step-mixed-handlers.js +24 -0
- package/examples/cli/step-machine-demo/two-step-mixed.flow.yaml +35 -0
- package/examples/index.html +792 -0
- package/examples/ingest.js +733 -0
- package/examples/npm-libs/batch/batch-step-machine.ts +121 -0
- package/examples/npm-libs/continuous-event-graph/live-cards-board.ts +215 -0
- package/examples/npm-libs/continuous-event-graph/live-portfolio-dashboard.ts +555 -0
- package/examples/npm-libs/continuous-event-graph/portfolio-tracker.ts +287 -0
- package/examples/npm-libs/continuous-event-graph/reactive-monitoring.ts +265 -0
- package/examples/npm-libs/continuous-event-graph/reactive-pipeline.ts +168 -0
- package/examples/npm-libs/continuous-event-graph/soc-incident-board.ts +287 -0
- package/examples/npm-libs/continuous-event-graph/stock-dashboard.ts +229 -0
- package/examples/npm-libs/event-graph/ci-cd-pipeline.ts +243 -0
- package/examples/npm-libs/event-graph/executor-diamond.ts +165 -0
- package/examples/npm-libs/event-graph/executor-pipeline.ts +161 -0
- package/examples/npm-libs/event-graph/research-pipeline.ts +137 -0
- package/examples/npm-libs/flows/ai-conversation.yaml +116 -0
- package/examples/npm-libs/flows/order-processing.yaml +143 -0
- package/examples/npm-libs/flows/simple-greeting.yaml +54 -0
- package/examples/npm-libs/graph-of-graphs/multi-stage-etl.ts +307 -0
- package/examples/npm-libs/graph-of-graphs/url-processing-pipeline.ts +254 -0
- package/examples/npm-libs/inference/azure-deployment.ts +149 -0
- package/examples/npm-libs/inference/copilot-cli.ts +138 -0
- package/examples/npm-libs/inference/data-pipeline.ts +145 -0
- package/examples/npm-libs/inference/pluggable-adapters.ts +254 -0
- package/examples/npm-libs/node/ai-conversation.ts +195 -0
- package/examples/npm-libs/node/simple-greeting.ts +101 -0
- package/examples/step-machine-cli/portfolio-tracker/cards/holdings-table.json +22 -0
- package/examples/step-machine-cli/portfolio-tracker/cards/portfolio-form.json +43 -0
- package/examples/step-machine-cli/portfolio-tracker/cards/portfolio-value.json +15 -0
- package/examples/step-machine-cli/portfolio-tracker/cards/price-fetch.json +15 -0
- package/examples/step-machine-cli/portfolio-tracker/fetch-prices.js +48 -0
- package/examples/step-machine-cli/portfolio-tracker/handlers/_board-cli.js +58 -0
- package/examples/step-machine-cli/portfolio-tracker/handlers/add-cards-cli.js +27 -0
- package/examples/step-machine-cli/portfolio-tracker/handlers/init-board-cli.js +25 -0
- package/examples/step-machine-cli/portfolio-tracker/handlers/reset-board-dir-cli.js +29 -0
- package/examples/step-machine-cli/portfolio-tracker/handlers/retrigger-cli.js +27 -0
- package/examples/step-machine-cli/portfolio-tracker/handlers/status-cli.js +25 -0
- package/examples/step-machine-cli/portfolio-tracker/handlers/update-holdings-cli.js +37 -0
- package/examples/step-machine-cli/portfolio-tracker/handlers/wait-completed-cli.js +53 -0
- package/examples/step-machine-cli/portfolio-tracker/handlers/write-prices-cli.js +35 -0
- package/examples/step-machine-cli/portfolio-tracker/portfolio-tracker.flow.yaml +227 -0
- package/examples/step-machine-cli/portfolio-tracker/portfolio-tracker.input.json +38 -0
- package/examples/step-machine-cli/portfolio-tracker/run-portfolio-tracker.bat +29 -0
- package/package.json +14 -2
- package/schema/board-status.schema.json +118 -0
- package/schema/flow.schema.json +5 -0
- package/schema/live-cards.schema.json +59 -77
- package/step-machine-cli.js +674 -0
|
@@ -0,0 +1,733 @@
|
|
|
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 & 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 & 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">×</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
|
+
}
|