yaml-flow 2.7.0 → 3.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 +168 -3
- package/browser/ingest-board.js +296 -0
- package/browser/live-cards.js +303 -0
- package/browser/live-cards.schema.json +22 -2
- package/dist/card-compute/index.cjs +6751 -0
- package/dist/card-compute/index.cjs.map +1 -1
- package/dist/card-compute/index.d.cts +24 -1
- package/dist/card-compute/index.d.ts +24 -1
- package/dist/card-compute/index.js +6747 -1
- package/dist/card-compute/index.js.map +1 -1
- package/dist/{constants-BEbO2_OK.d.ts → constants-B_ftYTTE.d.ts} +36 -6
- package/dist/{constants-BNjeIlZ8.d.cts → constants-CiyHX8L-.d.cts} +36 -6
- package/dist/continuous-event-graph/index.cjs +399 -42
- package/dist/continuous-event-graph/index.cjs.map +1 -1
- package/dist/continuous-event-graph/index.d.cts +124 -5
- package/dist/continuous-event-graph/index.d.ts +124 -5
- package/dist/continuous-event-graph/index.js +396 -43
- package/dist/continuous-event-graph/index.js.map +1 -1
- package/dist/event-graph/index.cjs +6784 -44
- 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 +6777 -43
- package/dist/event-graph/index.js.map +1 -1
- package/dist/index.cjs +7678 -73
- package/dist/index.cjs.map +1 -1
- package/dist/index.d.cts +6 -6
- package/dist/index.d.ts +6 -6
- package/dist/index.js +7665 -73
- package/dist/index.js.map +1 -1
- package/dist/inference/index.cjs +17 -8
- 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 +17 -8
- package/dist/inference/index.js.map +1 -1
- package/dist/step-machine/index.cjs +6600 -0
- package/dist/step-machine/index.cjs.map +1 -1
- package/dist/step-machine/index.d.cts +26 -1
- package/dist/step-machine/index.d.ts +26 -1
- package/dist/step-machine/index.js +6596 -1
- package/dist/step-machine/index.js.map +1 -1
- package/dist/{types-DAI_a2as.d.ts → types-BpWrH1sf.d.cts} +16 -7
- package/dist/{types-DAI_a2as.d.cts → types-BpWrH1sf.d.ts} +16 -7
- package/dist/{types-mS_pPftm.d.ts → types-BuEo3wVG.d.ts} +1 -1
- package/dist/{types-C2lOwquM.d.cts → types-CxJg9Jrt.d.cts} +1 -1
- package/package.json +3 -2
- package/schema/event-graph.schema.json +254 -0
- package/schema/live-cards.schema.json +22 -2
package/README.md
CHANGED
|
@@ -280,7 +280,9 @@ That's the entire integration. ~30 lines. The engine is pure; your loop owns the
|
|
|
280
280
|
| Conditional routing | `on: { positive: [pos-result], negative: [neg-result] }` | Different outputs based on task result |
|
|
281
281
|
| Failure tokens | `on_failure: [data-unavailable]` | Inject tokens on failure so downstream alternatives can activate |
|
|
282
282
|
| Retry | `retry: { max_attempts: 3 }` | Auto-retry on failure (task resets to not-started) |
|
|
283
|
-
|
|
|
283
|
+
| Refresh strategy | `refreshStrategy: 'data-changed'` (default) | When a completed task should re-run: `data-changed`, `epoch-changed`, `time-based`, `manual`, `once` |
|
|
284
|
+
| Max executions | `maxExecutions: 5` | Cap how many times a task can execute |
|
|
285
|
+
| Refresh interval | `refreshInterval: 300` | Seconds between re-runs (for `time-based` strategy) |
|
|
284
286
|
| Circuit breaker | `circuit_breaker: { max_executions: 10, on_break: [stop-token] }` | Inject tokens after N executions |
|
|
285
287
|
| External events | `apply(state, { type: 'inject-tokens', tokens: ['user-approved'] })` | Unblock tasks waiting on external input |
|
|
286
288
|
| Dynamic tasks | `apply(state, { type: 'task-creation', taskName: 'new', taskConfig: {...} })` | Add tasks at runtime |
|
|
@@ -352,11 +354,55 @@ tasks:
|
|
|
352
354
|
revise:
|
|
353
355
|
requires: [needs-revision]
|
|
354
356
|
provides: [draft-answer]
|
|
355
|
-
|
|
357
|
+
refreshStrategy: epoch-changed
|
|
358
|
+
maxExecutions: 3
|
|
356
359
|
```
|
|
357
360
|
|
|
358
361
|
The three searches run in parallel. `synthesize` waits for all three. `verify` can produce different token sets depending on its result. If rejected, `revise` picks up and feeds back into `verify` (up to 3 times). If verify itself fails, `verification-skipped` unblocks any downstream task waiting on it.
|
|
359
362
|
|
|
363
|
+
### Refresh Strategies
|
|
364
|
+
|
|
365
|
+
| Strategy | Behavior |
|
|
366
|
+
|---|---|
|
|
367
|
+
| `data-changed` (default) | Re-run when upstream output content changes (tracked via `dataHash`) |
|
|
368
|
+
| `epoch-changed` | Re-run when upstream task execution count increases (classic "inputs refreshed") |
|
|
369
|
+
| `time-based` | Re-run after `refreshInterval` seconds since last completion |
|
|
370
|
+
| `manual` | Never auto-eligible; only via external `inject-tokens` or explicit push |
|
|
371
|
+
| `once` | Run once, never re-run (classic one-shot task) |
|
|
372
|
+
|
|
373
|
+
Set a board-level default in `settings.refreshStrategy`, then override per-task:
|
|
374
|
+
|
|
375
|
+
```yaml
|
|
376
|
+
settings:
|
|
377
|
+
completion: manual
|
|
378
|
+
refreshStrategy: epoch-changed # board default
|
|
379
|
+
tasks:
|
|
380
|
+
fetch_prices:
|
|
381
|
+
provides: [price-data]
|
|
382
|
+
refreshStrategy: time-based # override: poll every 60s
|
|
383
|
+
refreshInterval: 60
|
|
384
|
+
compute:
|
|
385
|
+
requires: [price-data]
|
|
386
|
+
provides: [indicators]
|
|
387
|
+
# inherits epoch-changed from settings
|
|
388
|
+
alert:
|
|
389
|
+
requires: [indicators]
|
|
390
|
+
provides: [alert-sent]
|
|
391
|
+
refreshStrategy: data-changed # override: only if indicators actually changed
|
|
392
|
+
maxExecutions: 10 # safety cap
|
|
393
|
+
```
|
|
394
|
+
|
|
395
|
+
Handlers can return a `dataHash` with completion events to enable content-aware freshness:
|
|
396
|
+
|
|
397
|
+
```typescript
|
|
398
|
+
apply(state, {
|
|
399
|
+
type: 'task-completed',
|
|
400
|
+
taskName: 'fetch_prices',
|
|
401
|
+
dataHash: crypto.createHash('md5').update(JSON.stringify(data)).digest('hex'),
|
|
402
|
+
timestamp: new Date().toISOString(),
|
|
403
|
+
}, graph);
|
|
404
|
+
```
|
|
405
|
+
|
|
360
406
|
### Pattern: Order Processing Pipeline (Step Machine)
|
|
361
407
|
|
|
362
408
|
```yaml
|
|
@@ -839,6 +885,37 @@ Use `validateGraphConfig()` for structural checks (JSON shape) and `validateGrap
|
|
|
839
885
|
|
|
840
886
|
---
|
|
841
887
|
|
|
888
|
+
## JSON Schema Validation
|
|
889
|
+
|
|
890
|
+
Full structural validation using AJV against the JSON Schema definitions. Catches malformed configs before they reach the engine.
|
|
891
|
+
|
|
892
|
+
```typescript
|
|
893
|
+
import { validateGraphSchema } from 'yaml-flow/event-graph';
|
|
894
|
+
import { validateFlowSchema } from 'yaml-flow/step-machine';
|
|
895
|
+
import { validateLiveCardSchema } from 'yaml-flow/card-compute';
|
|
896
|
+
|
|
897
|
+
// Event graph
|
|
898
|
+
const r1 = validateGraphSchema(config);
|
|
899
|
+
r1.ok; // true | false
|
|
900
|
+
r1.errors; // AJV error objects (when invalid)
|
|
901
|
+
|
|
902
|
+
// Step machine
|
|
903
|
+
const r2 = validateFlowSchema(config);
|
|
904
|
+
|
|
905
|
+
// Live cards
|
|
906
|
+
const r3 = validateLiveCardSchema(config);
|
|
907
|
+
```
|
|
908
|
+
|
|
909
|
+
| Validator | Schema file | What it checks |
|
|
910
|
+
|---|---|---|
|
|
911
|
+
| `validateGraphSchema` | `schema/event-graph.schema.json` | Tasks, settings, refreshStrategy, retry, circuit_breaker, inference hints |
|
|
912
|
+
| `validateFlowSchema` | `schema/flow.schema.json` | Steps, transitions, retry, terminal states |
|
|
913
|
+
| `validateLiveCardSchema` | `schema/live-cards.schema.json` | Cards, sources, elements, compute, data bindings |
|
|
914
|
+
|
|
915
|
+
All validators are synchronous, pure functions. They return `{ ok: boolean, errors?: ErrorObject[] }`.
|
|
916
|
+
|
|
917
|
+
---
|
|
918
|
+
|
|
842
919
|
## Continuous Event Graph
|
|
843
920
|
|
|
844
921
|
A **long-lived, evolving** event-graph where both the graph config and execution state mutate over time. Ideal for dashboards, monitoring systems, and any scenario where the workflow has no fixed endpoint.
|
|
@@ -980,6 +1057,80 @@ const restored = restore(data); // → LiveGraph (validates shape)
|
|
|
980
1057
|
| `getUnreachableNodes(live)` | Nodes that can never become eligible |
|
|
981
1058
|
| `snapshot(live)` | Serialize to a JSON-safe snapshot |
|
|
982
1059
|
| `restore(data)` | Restore a LiveGraph from a snapshot |
|
|
1060
|
+
| `applyEvents(live, events)` | Apply multiple events atomically (batch reduce) |
|
|
1061
|
+
|
|
1062
|
+
### Reactive Graph (Push-based Execution)
|
|
1063
|
+
|
|
1064
|
+
The reactive layer adds **self-sustaining execution** on top of the pure LiveGraph. Register handlers, push one event, and the graph drives itself to completion. No daemon, no polling — each handler callback triggers the next wave.
|
|
1065
|
+
|
|
1066
|
+
```typescript
|
|
1067
|
+
import { createReactiveGraph, MemoryJournal } from 'yaml-flow/continuous-event-graph';
|
|
1068
|
+
|
|
1069
|
+
// 1. Create with handlers
|
|
1070
|
+
const rg = createReactiveGraph(config, {
|
|
1071
|
+
handlers: {
|
|
1072
|
+
fetch: async (ctx) => { const data = await fetchAPI(); return { dataHash: hash(data) }; },
|
|
1073
|
+
transform: async (ctx) => { return { result: 'success' }; },
|
|
1074
|
+
notify: async (ctx) => { await sendSlack('done'); return {}; },
|
|
1075
|
+
},
|
|
1076
|
+
defaultTimeoutMs: 30_000,
|
|
1077
|
+
onDrain: (events, live, schedule) => console.log(`${events.length} events, ${schedule.eligible.length} eligible`),
|
|
1078
|
+
});
|
|
1079
|
+
|
|
1080
|
+
// 2. Push one event — the chain sustains itself
|
|
1081
|
+
rg.push({ type: 'inject-tokens', tokens: [], timestamp: new Date().toISOString() });
|
|
1082
|
+
// fetch runs -> completes -> transform becomes eligible -> runs -> notify -> done
|
|
1083
|
+
|
|
1084
|
+
// 3. Add nodes at runtime (with handler)
|
|
1085
|
+
rg.addNode('alert', { requires: ['anomaly'], provides: ['alerted'] }, async (ctx) => {
|
|
1086
|
+
await pageOncall(ctx.taskName);
|
|
1087
|
+
return {};
|
|
1088
|
+
});
|
|
1089
|
+
|
|
1090
|
+
// 4. Read state
|
|
1091
|
+
rg.getState(); // LiveGraph snapshot
|
|
1092
|
+
rg.getSchedule(); // current ScheduleResult
|
|
1093
|
+
rg.getDispatchState(); // Map<taskName, DispatchEntry>
|
|
1094
|
+
|
|
1095
|
+
// 5. Cleanup
|
|
1096
|
+
rg.dispose();
|
|
1097
|
+
```
|
|
1098
|
+
|
|
1099
|
+
**How it works internally:**
|
|
1100
|
+
|
|
1101
|
+
```
|
|
1102
|
+
push(event)
|
|
1103
|
+
-> applyEvent (pure state change)
|
|
1104
|
+
-> schedule (what's eligible?)
|
|
1105
|
+
-> dispatch handlers (fire-and-forget)
|
|
1106
|
+
-> handler completes -> appends to journal
|
|
1107
|
+
-> drain journal -> applyEvents (batch) -> schedule -> dispatch
|
|
1108
|
+
-> repeat until nothing is eligible
|
|
1109
|
+
```
|
|
1110
|
+
|
|
1111
|
+
The journal serializes concurrent callbacks — multiple handlers complete simultaneously, their events batch into a single `applyEvents()` call. No race conditions.
|
|
1112
|
+
|
|
1113
|
+
**Dispatch lifecycle (reactive-layer internal, NOT in core types):**
|
|
1114
|
+
|
|
1115
|
+
| Status | Meaning |
|
|
1116
|
+
|---|---|
|
|
1117
|
+
| `initiated` | Handler callback fired, awaiting response |
|
|
1118
|
+
| `dispatch-failed` | Handler threw synchronously |
|
|
1119
|
+
| `timed-out` | No callback within deadline |
|
|
1120
|
+
| `retry-queued` | Will retry on next drain cycle |
|
|
1121
|
+
| `abandoned` | Max dispatch retries exceeded -> pushes `task-failed` to core |
|
|
1122
|
+
|
|
1123
|
+
**Options:**
|
|
1124
|
+
|
|
1125
|
+
| Option | Default | Description |
|
|
1126
|
+
|---|---|---|
|
|
1127
|
+
| `handlers` | (required) | `Record<string, TaskHandler>` |
|
|
1128
|
+
| `maxDispatchRetries` | `3` | Times to retry invoking a handler |
|
|
1129
|
+
| `defaultTimeoutMs` | `30000` | Handler callback deadline (0 = no timeout) |
|
|
1130
|
+
| `journal` | `MemoryJournal` | Event log adapter (`MemoryJournal` or `FileJournal`) |
|
|
1131
|
+
| `onDrain` | — | Called after each drain cycle (observability) |
|
|
1132
|
+
| `onDispatchFailed` | — | Called when handler invocation fails |
|
|
1133
|
+
| `onAbandoned` | — | Called when task dispatch is abandoned |
|
|
983
1134
|
|
|
984
1135
|
---
|
|
985
1136
|
|
|
@@ -1234,15 +1385,25 @@ import { resolveVariables, resolveConfigTemplates } from 'yaml-flow/config';
|
|
|
1234
1385
|
|
|
1235
1386
|
// Continuous Event Graph (long-lived evolving workflows)
|
|
1236
1387
|
import {
|
|
1237
|
-
createLiveGraph, applyEvent, addNode, removeNode,
|
|
1388
|
+
createLiveGraph, applyEvent, applyEvents, addNode, removeNode,
|
|
1238
1389
|
addRequires, removeRequires, addProvides, removeProvides,
|
|
1239
1390
|
injectTokens, drainTokens, schedule, inspect,
|
|
1240
1391
|
resetNode, disableNode, enableNode, getNode,
|
|
1241
1392
|
snapshot, restore,
|
|
1242
1393
|
getUnreachableTokens, getUnreachableNodes,
|
|
1243
1394
|
getUpstream, getDownstream,
|
|
1395
|
+
createReactiveGraph, MemoryJournal, FileJournal,
|
|
1396
|
+
} from 'yaml-flow/continuous-event-graph';
|
|
1397
|
+
import type {
|
|
1398
|
+
ReactiveGraph, TaskHandler, TaskHandlerContext, TaskHandlerResult,
|
|
1399
|
+
DispatchEntry, Journal,
|
|
1244
1400
|
} from 'yaml-flow/continuous-event-graph';
|
|
1245
1401
|
|
|
1402
|
+
// JSON Schema Validators
|
|
1403
|
+
import { validateGraphSchema } from 'yaml-flow/event-graph';
|
|
1404
|
+
import { validateFlowSchema } from 'yaml-flow/step-machine';
|
|
1405
|
+
import { validateLiveCardSchema } from 'yaml-flow/card-compute';
|
|
1406
|
+
|
|
1246
1407
|
// LLM Inference (AI-assisted completion detection)
|
|
1247
1408
|
import {
|
|
1248
1409
|
buildInferencePrompt, inferCompletions, applyInferences, inferAndApply,
|
|
@@ -1321,6 +1482,10 @@ See the [examples/](./examples) directory:
|
|
|
1321
1482
|
| [URL Pipeline](./examples/graph-of-graphs/url-processing-pipeline.ts) | Graph-of-Graphs | Outer event-graph → batch × inner event-graph per item |
|
|
1322
1483
|
| [Multi-Stage ETL](./examples/graph-of-graphs/multi-stage-etl.ts) | Graph-of-Graphs | Mixed modes: event-graph outer → step-machine + event-graph subs |
|
|
1323
1484
|
| [Stock Dashboard](./examples/continuous-event-graph/stock-dashboard.ts) | Continuous Event Graph | Runtime mutations, token drain, upstream/downstream, snapshot |
|
|
1485
|
+
| [Reactive Pipeline](./examples/continuous-event-graph/reactive-pipeline.ts) | Reactive Graph | Self-driving ETL — push once, 4 tasks complete automatically |
|
|
1486
|
+
| [Reactive Monitoring](./examples/continuous-event-graph/reactive-monitoring.ts) | Reactive Graph | Conditional routing, on_failure escalation, runtime addNode |
|
|
1487
|
+
| [Executor Pipeline](./examples/event-graph/executor-pipeline.ts) | Event Graph (library) | You-drive-the-loop ETL with random async delays |
|
|
1488
|
+
| [Executor Diamond](./examples/event-graph/executor-diamond.ts) | Event Graph (library) | Parallel fan-out/fan-in diamond DAG with async executors |
|
|
1324
1489
|
| [Azure Deployment](./examples/inference/azure-deployment.ts) | Inference | LLM analyzes deployment logs, auto-completes checkpoints |
|
|
1325
1490
|
| [Data Pipeline](./examples/inference/data-pipeline.ts) | Inference | Iterative inference — evidence arrives in waves |
|
|
1326
1491
|
| [Pluggable Adapters](./examples/inference/pluggable-adapters.ts) | Inference | OpenAI, Anthropic, Azure, CLI, HTTP adapter factories |
|
|
@@ -0,0 +1,296 @@
|
|
|
1
|
+
// ingest-board.js — Ingest Board: a LiveCard Board type for batch ingest UIs
|
|
2
|
+
//
|
|
3
|
+
// Pure component. Zero I/O (no fetch, no EventSource, no polling).
|
|
4
|
+
// All side-effects delegated to host via callbacks.
|
|
5
|
+
//
|
|
6
|
+
// API:
|
|
7
|
+
// const ib = IngestBoard.create(containerEl, opts)
|
|
8
|
+
//
|
|
9
|
+
// ib.setBatches(batches) — rebuild board from batch array
|
|
10
|
+
// ib.getChat(boardId) — get chat element API for a card
|
|
11
|
+
// ib.getFileUpload(boardId) — get file-upload element API for a card
|
|
12
|
+
// ib.getActions(boardId) — get actions element API for a card
|
|
13
|
+
// ib.showChatModal(boardId, messages) — open Bootstrap modal with chat history
|
|
14
|
+
// ib.destroy()
|
|
15
|
+
//
|
|
16
|
+
// Required opts:
|
|
17
|
+
// onSend(boardId, { text, files }) — host handles: upload files + message → call getChat().appendMessage() with results
|
|
18
|
+
// onConfirm(boardId) — host handles: POST confirm → SSE → update board
|
|
19
|
+
// onDiscard(boardId) — host handles: POST discard → call ib.setBatches()
|
|
20
|
+
//
|
|
21
|
+
// Optional opts:
|
|
22
|
+
// onViewChat(boardId) — host handles: fetch chat → call ib.showChatModal()
|
|
23
|
+
// onRefresh() — host handles: fetch batches → call ib.setBatches()
|
|
24
|
+
// compact — smaller card columns (default: false)
|
|
25
|
+
// engine — existing LiveCard engine (one is created if omitted)
|
|
26
|
+
// markdown — markdown renderer fn
|
|
27
|
+
// sanitize — HTML sanitizer fn
|
|
28
|
+
|
|
29
|
+
// eslint-disable-next-line no-unused-vars
|
|
30
|
+
var IngestBoard = (function () {
|
|
31
|
+
'use strict';
|
|
32
|
+
|
|
33
|
+
function create(containerEl, opts) {
|
|
34
|
+
opts = opts || {};
|
|
35
|
+
if (!opts.onSend) throw new Error('IngestBoard: opts.onSend is required');
|
|
36
|
+
if (!opts.onConfirm) throw new Error('IngestBoard: opts.onConfirm is required');
|
|
37
|
+
if (!opts.onDiscard) throw new Error('IngestBoard: opts.onDiscard is required');
|
|
38
|
+
|
|
39
|
+
const compact = opts.compact || false;
|
|
40
|
+
const onSend = opts.onSend;
|
|
41
|
+
const onConfirm = opts.onConfirm;
|
|
42
|
+
const onDiscard = opts.onDiscard;
|
|
43
|
+
const onViewChat = opts.onViewChat || null;
|
|
44
|
+
const onRefresh = opts.onRefresh || null;
|
|
45
|
+
const mdFn = opts.markdown || (typeof marked !== 'undefined' ? function (t) { return marked.parse(t); } : null);
|
|
46
|
+
const sanFn = opts.sanitize || (typeof DOMPurify !== 'undefined' ? function (h) { return DOMPurify.sanitize(h); } : null);
|
|
47
|
+
|
|
48
|
+
let board = null;
|
|
49
|
+
const nodes = {}; // id → node
|
|
50
|
+
|
|
51
|
+
// ---- Engine ----
|
|
52
|
+
|
|
53
|
+
const engine = opts.engine || LiveCard.init({
|
|
54
|
+
resolve: function (id) { return nodes[id]; },
|
|
55
|
+
onPatch: function () {},
|
|
56
|
+
onPatchState: function () {},
|
|
57
|
+
onRefresh: onRefresh || function () {},
|
|
58
|
+
onAction: handleAction,
|
|
59
|
+
markdown: mdFn,
|
|
60
|
+
sanitize: sanFn,
|
|
61
|
+
});
|
|
62
|
+
|
|
63
|
+
// ---- Action dispatcher (pure — delegates to host callbacks) ----
|
|
64
|
+
|
|
65
|
+
function handleAction(nodeId, actionType, payload) {
|
|
66
|
+
if (actionType === 'chat-send') {
|
|
67
|
+
// For the "new" card, boardId is null until host creates one
|
|
68
|
+
var boardId = nodeId === '__new__' ? null : nodeId;
|
|
69
|
+
onSend(boardId, { text: payload.text, files: payload.files });
|
|
70
|
+
} else if (actionType === 'action') {
|
|
71
|
+
if (payload.buttonId === 'confirm') onConfirm(nodeId);
|
|
72
|
+
else if (payload.buttonId === 'discard') onDiscard(nodeId);
|
|
73
|
+
else if (payload.buttonId === 'view-chat' && onViewChat) onViewChat(nodeId);
|
|
74
|
+
}
|
|
75
|
+
}
|
|
76
|
+
|
|
77
|
+
// ---- Node builders ----
|
|
78
|
+
|
|
79
|
+
function buildActiveNode(batch) {
|
|
80
|
+
return {
|
|
81
|
+
id: batch.id,
|
|
82
|
+
type: 'card',
|
|
83
|
+
meta: { title: batch.id, tags: [batch.status === 'open-items' ? 'open-items' : 'ready'] },
|
|
84
|
+
state: {
|
|
85
|
+
status: batch.processing ? 'loading' : 'fresh',
|
|
86
|
+
messages: batch.chat || [],
|
|
87
|
+
files: batch.files || [],
|
|
88
|
+
batchStatus: batch.status,
|
|
89
|
+
},
|
|
90
|
+
view: {
|
|
91
|
+
elements: [
|
|
92
|
+
{
|
|
93
|
+
id: 'chat',
|
|
94
|
+
kind: 'chat',
|
|
95
|
+
data: { bind: 'state.messages', fileAttach: true, placeholder: 'Add files or type a message...' }
|
|
96
|
+
},
|
|
97
|
+
{
|
|
98
|
+
id: 'actions',
|
|
99
|
+
kind: 'actions',
|
|
100
|
+
data: {
|
|
101
|
+
buttons: [
|
|
102
|
+
{ id: 'confirm', label: 'Confirm & Merge', style: 'success', disabled: batch.status !== 'ready' || !!batch.processing },
|
|
103
|
+
{ id: 'discard', label: 'Discard', style: 'outline-danger' }
|
|
104
|
+
]
|
|
105
|
+
}
|
|
106
|
+
}
|
|
107
|
+
],
|
|
108
|
+
layout: { board: { col: compact ? 6 : 8, order: 0 } },
|
|
109
|
+
features: { refresh: false }
|
|
110
|
+
}
|
|
111
|
+
};
|
|
112
|
+
}
|
|
113
|
+
|
|
114
|
+
function buildCompletedNode(batch, order) {
|
|
115
|
+
var elements = [];
|
|
116
|
+
|
|
117
|
+
if (batch.files && batch.files.length) {
|
|
118
|
+
elements.push({
|
|
119
|
+
id: 'files',
|
|
120
|
+
kind: 'file-upload',
|
|
121
|
+
data: { bind: 'state.files', upload: false }
|
|
122
|
+
});
|
|
123
|
+
}
|
|
124
|
+
|
|
125
|
+
if (batch.summary) {
|
|
126
|
+
elements.push({
|
|
127
|
+
id: 'summary',
|
|
128
|
+
kind: 'text',
|
|
129
|
+
data: { bind: 'state.summary', style: 'muted' }
|
|
130
|
+
});
|
|
131
|
+
}
|
|
132
|
+
|
|
133
|
+
if (batch.chatCount > 0 && onViewChat) {
|
|
134
|
+
elements.push({
|
|
135
|
+
id: 'card-actions',
|
|
136
|
+
kind: 'actions',
|
|
137
|
+
data: { buttons: [{ id: 'view-chat', label: '\uD83D\uDCAC ' + batch.chatCount + ' message(s)', style: 'outline-secondary' }] }
|
|
138
|
+
});
|
|
139
|
+
}
|
|
140
|
+
|
|
141
|
+
return {
|
|
142
|
+
id: batch.id,
|
|
143
|
+
type: 'card',
|
|
144
|
+
meta: { title: batch.id, tags: ['confirmed'] },
|
|
145
|
+
state: {
|
|
146
|
+
status: 'fresh',
|
|
147
|
+
files: batch.files || [],
|
|
148
|
+
summary: batch.summary || '',
|
|
149
|
+
},
|
|
150
|
+
view: {
|
|
151
|
+
elements: elements,
|
|
152
|
+
layout: { board: { col: compact ? 6 : 4, order: order } },
|
|
153
|
+
features: { refresh: false }
|
|
154
|
+
}
|
|
155
|
+
};
|
|
156
|
+
}
|
|
157
|
+
|
|
158
|
+
function buildNewNode() {
|
|
159
|
+
return {
|
|
160
|
+
id: '__new__',
|
|
161
|
+
type: 'card',
|
|
162
|
+
meta: { title: 'New Batch', tags: ['new'] },
|
|
163
|
+
state: { status: 'fresh', messages: [] },
|
|
164
|
+
view: {
|
|
165
|
+
elements: [
|
|
166
|
+
{
|
|
167
|
+
id: 'chat',
|
|
168
|
+
kind: 'chat',
|
|
169
|
+
data: { bind: 'state.messages', fileAttach: true, placeholder: 'Add files or type a message...' }
|
|
170
|
+
}
|
|
171
|
+
],
|
|
172
|
+
layout: { board: { col: compact ? 6 : 8, order: -1 } },
|
|
173
|
+
features: { refresh: false }
|
|
174
|
+
}
|
|
175
|
+
};
|
|
176
|
+
}
|
|
177
|
+
|
|
178
|
+
// ---- setBatches — rebuild the board from data ----
|
|
179
|
+
|
|
180
|
+
function setBatches(batches) {
|
|
181
|
+
Object.keys(nodes).forEach(function (k) { delete nodes[k]; });
|
|
182
|
+
|
|
183
|
+
var hasActive = batches.some(function (b) { return b.status === 'ready' || b.status === 'open-items'; });
|
|
184
|
+
var allNodes = [];
|
|
185
|
+
|
|
186
|
+
if (!hasActive) {
|
|
187
|
+
var nn = buildNewNode();
|
|
188
|
+
nodes.__new__ = nn;
|
|
189
|
+
allNodes.push(nn);
|
|
190
|
+
}
|
|
191
|
+
|
|
192
|
+
var order = 1;
|
|
193
|
+
batches.forEach(function (b) {
|
|
194
|
+
var isActive = b.status === 'ready' || b.status === 'open-items';
|
|
195
|
+
var n = isActive ? buildActiveNode(b) : buildCompletedNode(b, order++);
|
|
196
|
+
nodes[b.id] = n;
|
|
197
|
+
allNodes.push(n);
|
|
198
|
+
});
|
|
199
|
+
|
|
200
|
+
if (board) board.destroy();
|
|
201
|
+
board = LiveCard.Board(engine, containerEl, {
|
|
202
|
+
nodes: allNodes,
|
|
203
|
+
mode: 'board',
|
|
204
|
+
showNotes: false,
|
|
205
|
+
showChat: false,
|
|
206
|
+
});
|
|
207
|
+
}
|
|
208
|
+
|
|
209
|
+
// ---- Element accessors (host uses these to push data in) ----
|
|
210
|
+
|
|
211
|
+
function getChat(boardId) {
|
|
212
|
+
var el = engine.getElement(boardId, 'chat');
|
|
213
|
+
return el && el._chat || null;
|
|
214
|
+
}
|
|
215
|
+
|
|
216
|
+
function getFileUpload(boardId) {
|
|
217
|
+
var el = engine.getElement(boardId, 'files');
|
|
218
|
+
return el && el._fileUpload || null;
|
|
219
|
+
}
|
|
220
|
+
|
|
221
|
+
function getActions(boardId) {
|
|
222
|
+
var el = engine.getElement(boardId, 'actions') || engine.getElement(boardId, 'card-actions');
|
|
223
|
+
return el && el._actions || null;
|
|
224
|
+
}
|
|
225
|
+
|
|
226
|
+
// ---- Chat modal (pure — caller passes messages) ----
|
|
227
|
+
|
|
228
|
+
function showChatModal(boardId, messages) {
|
|
229
|
+
var existing = document.getElementById('lc-chat-modal');
|
|
230
|
+
if (existing) existing.remove();
|
|
231
|
+
var bd = document.querySelector('.modal-backdrop');
|
|
232
|
+
if (bd) bd.remove();
|
|
233
|
+
|
|
234
|
+
var _e = function (t) { var d = document.createElement('div'); d.textContent = t; return d.innerHTML; };
|
|
235
|
+
|
|
236
|
+
var wrap = document.createElement('div');
|
|
237
|
+
wrap.innerHTML =
|
|
238
|
+
'<div class="modal fade" id="lc-chat-modal" tabindex="-1">' +
|
|
239
|
+
'<div class="modal-dialog modal-dialog-scrollable">' +
|
|
240
|
+
'<div class="modal-content">' +
|
|
241
|
+
'<div class="modal-header">' +
|
|
242
|
+
'<h6 class="modal-title">' + _e(boardId) + '</h6>' +
|
|
243
|
+
'<button type="button" class="btn-close" data-bs-dismiss="modal"></button>' +
|
|
244
|
+
'</div>' +
|
|
245
|
+
'<div class="modal-body"><div class="lc-chat-body" id="lc-chat-modal-body" style="max-height:none"></div></div>' +
|
|
246
|
+
'</div>' +
|
|
247
|
+
'</div>' +
|
|
248
|
+
'</div>';
|
|
249
|
+
document.body.appendChild(wrap.firstElementChild);
|
|
250
|
+
|
|
251
|
+
var bodyEl = document.getElementById('lc-chat-modal-body');
|
|
252
|
+
messages.forEach(function (msg) {
|
|
253
|
+
var bub = document.createElement('div');
|
|
254
|
+
var rc = msg.role === 'user' ? 'lc-chat-bubble-user'
|
|
255
|
+
: msg.role === 'assistant' ? 'lc-chat-bubble-assistant'
|
|
256
|
+
: 'lc-chat-bubble-system';
|
|
257
|
+
bub.className = 'lc-chat-bubble ' + rc;
|
|
258
|
+
if (msg.role === 'assistant' && mdFn) {
|
|
259
|
+
var html = mdFn(msg.text);
|
|
260
|
+
if (sanFn) html = sanFn(html);
|
|
261
|
+
bub.innerHTML = html;
|
|
262
|
+
} else {
|
|
263
|
+
bub.textContent = msg.text;
|
|
264
|
+
}
|
|
265
|
+
bodyEl.appendChild(bub);
|
|
266
|
+
});
|
|
267
|
+
|
|
268
|
+
var modal = new bootstrap.Modal(document.getElementById('lc-chat-modal'));
|
|
269
|
+
modal.show();
|
|
270
|
+
document.getElementById('lc-chat-modal').addEventListener('hidden.bs.modal', function () {
|
|
271
|
+
document.getElementById('lc-chat-modal').remove();
|
|
272
|
+
});
|
|
273
|
+
}
|
|
274
|
+
|
|
275
|
+
// ---- Lifecycle ----
|
|
276
|
+
|
|
277
|
+
function destroy() {
|
|
278
|
+
if (board) board.destroy();
|
|
279
|
+
board = null;
|
|
280
|
+
Object.keys(nodes).forEach(function (k) { delete nodes[k]; });
|
|
281
|
+
}
|
|
282
|
+
|
|
283
|
+
return {
|
|
284
|
+
setBatches: setBatches,
|
|
285
|
+
getChat: getChat,
|
|
286
|
+
getFileUpload: getFileUpload,
|
|
287
|
+
getActions: getActions,
|
|
288
|
+
showChatModal: showChatModal,
|
|
289
|
+
destroy: destroy,
|
|
290
|
+
get engine() { return engine; },
|
|
291
|
+
get board() { return board; },
|
|
292
|
+
};
|
|
293
|
+
}
|
|
294
|
+
|
|
295
|
+
return { create: create };
|
|
296
|
+
})();
|