yaml-flow 5.0.0 → 5.2.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/{examples/example-board/reusable-server-runtime.js → board-livecards-server-runtime.js} +103 -24
- package/{examples/example-board/reusable-board-runtime-client.js → browser/board-livecards-runtime-client.js} +6 -2
- package/browser/{board-livegraph-runtime.js → board-livegraph-engine.js} +212 -16
- package/browser/board-livegraph-engine.js.map +1 -0
- package/browser/live-cards.js +362 -38
- package/browser/live-cards.schema.json +20 -4
- package/dist/board-livegraph-runtime/index.cjs +210 -14
- package/dist/board-livegraph-runtime/index.cjs.map +1 -1
- package/dist/board-livegraph-runtime/index.d.cts +49 -5
- package/dist/board-livegraph-runtime/index.d.ts +49 -5
- package/dist/board-livegraph-runtime/index.js +209 -15
- package/dist/board-livegraph-runtime/index.js.map +1 -1
- package/dist/card-compute/index.cjs +63 -7
- package/dist/card-compute/index.cjs.map +1 -1
- package/dist/card-compute/index.d.cts +2 -2
- package/dist/card-compute/index.d.ts +2 -2
- package/dist/card-compute/index.js +63 -7
- package/dist/card-compute/index.js.map +1 -1
- package/dist/cli/board-live-cards-cli.cjs +664 -75
- package/dist/cli/board-live-cards-cli.cjs.map +1 -1
- package/dist/cli/board-live-cards-cli.d.cts +33 -5
- package/dist/cli/board-live-cards-cli.d.ts +33 -5
- package/dist/cli/board-live-cards-cli.js +661 -76
- package/dist/cli/board-live-cards-cli.js.map +1 -1
- package/dist/{constants-ozjf1Ejw.d.cts → constants-BzZUyYlp.d.cts} +1 -1
- package/dist/{constants-DuzE5n03.d.ts → constants-oCEbNpul.d.ts} +1 -1
- package/dist/continuous-event-graph/index.cjs +47 -14
- package/dist/continuous-event-graph/index.cjs.map +1 -1
- package/dist/continuous-event-graph/index.d.cts +9 -9
- package/dist/continuous-event-graph/index.d.ts +9 -9
- package/dist/continuous-event-graph/index.js +47 -14
- package/dist/continuous-event-graph/index.js.map +1 -1
- package/dist/event-graph/index.cjs +29 -12
- 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 +29 -12
- package/dist/event-graph/index.js.map +1 -1
- package/dist/index.cjs +93 -20
- package/dist/index.cjs.map +1 -1
- package/dist/index.d.cts +7 -7
- package/dist/index.d.ts +7 -7
- package/dist/index.js +93 -20
- package/dist/index.js.map +1 -1
- package/dist/inference/index.cjs +29 -12
- 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 +29 -12
- package/dist/inference/index.js.map +1 -1
- package/dist/{journal-NLYuqege.d.ts → journal-9HEgs7dU.d.ts} +1 -1
- package/dist/{journal-DRfJiheM.d.cts → journal-B-JCfQnh.d.cts} +1 -1
- package/dist/{live-cards-bridge-Or7fdEJV.d.ts → live-cards-bridge-CeNxiVcm.d.ts} +6 -2
- package/dist/{live-cards-bridge-vGJ6tMzN.d.cts → live-cards-bridge-z_rJCSbi.d.cts} +6 -2
- package/dist/{schedule-CMcZe5Ny.d.ts → schedule-Cszq9LYY.d.ts} +1 -1
- package/dist/{schedule-CiucyCan.d.cts → schedule-qWNL0RQh.d.cts} +1 -1
- package/dist/{types-CMFSIjpc.d.cts → types-BBhqYGhE.d.cts} +4 -0
- package/dist/{types-CMFSIjpc.d.ts → types-BBhqYGhE.d.ts} +4 -0
- package/dist/{types-BzLD8bjb.d.cts → types-CHSdoAAA.d.cts} +1 -1
- package/dist/{types-C2eJ7DAV.d.ts → types-CoW0gQl3.d.ts} +1 -1
- package/dist/{validate-DJQTQ6bP.d.ts → validate-BAVzUJWa.d.ts} +1 -1
- package/dist/{validate-ke92Cleg.d.cts → validate-Dbu7ygys.d.cts} +1 -1
- package/examples/browser/boards/portfolio-tracker/cards/portfolio-risk-assessment.json +28 -0
- package/examples/browser/boards/portfolio-tracker/cards/rebalancing-strategy.json +28 -0
- package/examples/browser/boards/portfolio-tracker/portfolio-tracker-inference-adapter.js +187 -0
- package/examples/browser/boards/portfolio-tracker/portfolio-tracker.js +139 -5
- package/examples/example-board/agent-instructions-cardlayout.md +28 -0
- package/examples/example-board/agent-instructions.md +603 -0
- package/examples/example-board/cards/card-concentration.json +42 -0
- package/examples/example-board/cards/card-market-prices.json +51 -0
- package/examples/example-board/cards/card-portfolio-action.json +19 -0
- package/examples/example-board/cards/card-portfolio-risks.json +19 -0
- package/examples/example-board/cards/card-portfolio-value.json +62 -0
- package/examples/example-board/cards/card-portfolio.json +44 -0
- package/examples/example-board/demo-chat-handler.js +373 -33
- package/examples/example-board/demo-server-config.json +5 -0
- package/examples/example-board/demo-server.js +83 -7
- package/examples/example-board/demo-shell-browser.html +75 -207
- package/examples/example-board/demo-shell-with-server.html +14 -9
- package/examples/example-board/demo-shell.html +1 -1
- package/examples/example-board/demo-task-executor.js +259 -41
- package/package.json +6 -2
- package/schema/live-cards.schema.json +20 -4
- package/browser/board-livegraph-runtime.js.map +0 -1
- package/examples/example-board/board.yaml +0 -23
- package/examples/example-board/bootstrap_payload.json +0 -1
- package/examples/example-board/cards/card-chain-region-alert.json +0 -39
- package/examples/example-board/cards/card-chain-region-totals.json +0 -26
- package/examples/example-board/cards/card-chain-top-region.json +0 -24
- package/examples/example-board/cards/card-ex-actions.json +0 -32
- package/examples/example-board/cards/card-ex-chart.json +0 -30
- package/examples/example-board/cards/card-ex-filter.json +0 -36
- package/examples/example-board/cards/card-ex-filtered-by-preference.json +0 -59
- package/examples/example-board/cards/card-ex-form.json +0 -91
- package/examples/example-board/cards/card-ex-list.json +0 -22
- package/examples/example-board/cards/card-ex-markdown.json +0 -17
- package/examples/example-board/cards/card-ex-metric.json +0 -19
- package/examples/example-board/cards/card-ex-narrative.json +0 -36
- package/examples/example-board/cards/card-ex-source-http.json +0 -28
- package/examples/example-board/cards/card-ex-source.json +0 -21
- package/examples/example-board/cards/card-ex-status.json +0 -35
- package/examples/example-board/cards/card-ex-table.json +0 -30
- package/examples/example-board/cards/card-ex-todo.json +0 -29
- package/examples/example-board/mock.db +0 -15
- package/examples/example-board/reusable-runtime-artifacts-adapter.js +0 -233
package/browser/live-cards.js
CHANGED
|
@@ -197,8 +197,12 @@ var LiveCard = (function () {
|
|
|
197
197
|
getChatMessages: config.getChatMessages || null,
|
|
198
198
|
};
|
|
199
199
|
|
|
200
|
-
const _cleanup = {};
|
|
201
|
-
const _subs = {};
|
|
200
|
+
const _cleanup = {}; // nodeId → { ac, timers, charts, unsubs }
|
|
201
|
+
const _subs = {}; // nodeId → Set<callback>
|
|
202
|
+
const _etState = {}; // stateKey → { currentState, pending } for editable-table dirty tracking
|
|
203
|
+
const _formState = {}; // stateKey → { currentState, pending } for form dirty tracking
|
|
204
|
+
const _notesState = {}; // stateKey → { currentState, pending } for notes dirty tracking
|
|
205
|
+
const _todoState = {}; // stateKey → { currentState, pending } for todo dirty tracking
|
|
202
206
|
const _renderers = {}; // kind → fn
|
|
203
207
|
const _nodeEls = {}; // nodeId → { container, resultEl, uid }
|
|
204
208
|
const _chatModal = {
|
|
@@ -949,7 +953,48 @@ var LiveCard = (function () {
|
|
|
949
953
|
const schema = ed.fields || {};
|
|
950
954
|
const props = schema.properties || {};
|
|
951
955
|
const required = schema.required || [];
|
|
952
|
-
|
|
956
|
+
|
|
957
|
+
// --- Journal-style dirty tracking (mirrors editable-table) ---
|
|
958
|
+
// currentState = what the server last sent (updated each SSE re-render)
|
|
959
|
+
// pending = user's local field values accumulated on top of currentState
|
|
960
|
+
// dirty = JSON.stringify(currentState) !== JSON.stringify(pending)
|
|
961
|
+
//
|
|
962
|
+
// On SSE re-render:
|
|
963
|
+
// - currentState updated to new server values
|
|
964
|
+
// - if NOT dirty: pending follows currentState (no unsaved edits)
|
|
965
|
+
// - if dirty: pending untouched, form is rebuilt from pending (edits survive)
|
|
966
|
+
const stateKey = node.id + ':' + (writeTo || '');
|
|
967
|
+
const incomingValues = writeTo ? (_resolveBind(node, writeTo) || {}) : (data && typeof data === 'object' ? data : {});
|
|
968
|
+
const incomingCopy = Object.assign({}, incomingValues);
|
|
969
|
+
|
|
970
|
+
if (!_formState[stateKey]) {
|
|
971
|
+
_formState[stateKey] = { currentState: incomingCopy, pending: Object.assign({}, incomingCopy) };
|
|
972
|
+
} else {
|
|
973
|
+
const s = _formState[stateKey];
|
|
974
|
+
const wasDirty = JSON.stringify(s.currentState) !== JSON.stringify(s.pending);
|
|
975
|
+
s.currentState = incomingCopy;
|
|
976
|
+
if (!wasDirty) {
|
|
977
|
+
s.pending = Object.assign({}, incomingCopy);
|
|
978
|
+
}
|
|
979
|
+
// if dirty, pending stays so user's in-progress edits survive the SSE tick
|
|
980
|
+
}
|
|
981
|
+
|
|
982
|
+
const st = _formState[stateKey];
|
|
983
|
+
|
|
984
|
+
function isDirty() {
|
|
985
|
+
return JSON.stringify(st.currentState) !== JSON.stringify(st.pending);
|
|
986
|
+
}
|
|
987
|
+
|
|
988
|
+
// Snapshot current input values into pending (called on each input event)
|
|
989
|
+
function capturePending(form) {
|
|
990
|
+
form.querySelectorAll('[data-key]').forEach(inp => {
|
|
991
|
+
const k = inp.dataset.key, p = props[k];
|
|
992
|
+
if (!p) return;
|
|
993
|
+
if (p.type === 'boolean') st.pending[k] = inp.checked;
|
|
994
|
+
else if (p.type === 'number' || p.type === 'integer') st.pending[k] = inp.value !== '' ? parseFloat(inp.value) : 0;
|
|
995
|
+
else st.pending[k] = inp.value;
|
|
996
|
+
});
|
|
997
|
+
}
|
|
953
998
|
|
|
954
999
|
const form = document.createElement('form');
|
|
955
1000
|
form.className = 'row g-2';
|
|
@@ -999,10 +1044,12 @@ var LiveCard = (function () {
|
|
|
999
1044
|
|
|
1000
1045
|
input.dataset.key = key;
|
|
1001
1046
|
if (isReq) input.required = true;
|
|
1002
|
-
|
|
1003
|
-
|
|
1004
|
-
|
|
1005
|
-
|
|
1047
|
+
// Populate from pending (not from server values directly)
|
|
1048
|
+
const v = st.pending[key];
|
|
1049
|
+
if (v != null) {
|
|
1050
|
+
if (prop.type === 'boolean') input.checked = !!v;
|
|
1051
|
+
else if (prop.format === 'date') input.value = String(v).slice(0, 10);
|
|
1052
|
+
else input.value = v;
|
|
1006
1053
|
}
|
|
1007
1054
|
form.appendChild(col);
|
|
1008
1055
|
});
|
|
@@ -1010,26 +1057,31 @@ var LiveCard = (function () {
|
|
|
1010
1057
|
const btnCol = document.createElement('div');
|
|
1011
1058
|
btnCol.className = 'col-12 mt-1';
|
|
1012
1059
|
const btn = document.createElement('button');
|
|
1013
|
-
btn.type = 'submit';
|
|
1060
|
+
btn.type = 'submit';
|
|
1061
|
+
btn.className = 'btn btn-sm btn-primary' + (isDirty() ? '' : ' d-none');
|
|
1062
|
+
btn.textContent = 'Submit';
|
|
1014
1063
|
btnCol.appendChild(btn);
|
|
1015
1064
|
form.appendChild(btnCol);
|
|
1016
1065
|
|
|
1017
1066
|
el.innerHTML = '';
|
|
1018
1067
|
el.appendChild(form);
|
|
1019
1068
|
|
|
1069
|
+
// Real-time input → update pending + show Submit if now dirty
|
|
1070
|
+
form.addEventListener('input', () => {
|
|
1071
|
+
capturePending(form);
|
|
1072
|
+
if (isDirty()) btn.classList.remove('d-none');
|
|
1073
|
+
}, { signal });
|
|
1074
|
+
|
|
1020
1075
|
form.addEventListener('submit', e => {
|
|
1021
1076
|
e.preventDefault();
|
|
1022
1077
|
if (!form.checkValidity()) { form.classList.add('was-validated'); return; }
|
|
1023
|
-
|
|
1024
|
-
|
|
1025
|
-
|
|
1026
|
-
|
|
1027
|
-
|
|
1028
|
-
|
|
1029
|
-
|
|
1030
|
-
if (writeTo) _deepSet(node, writeTo, vals);
|
|
1031
|
-
cfg.onPatchState(node.id, { fieldValues: vals });
|
|
1032
|
-
notify(node.id, vals);
|
|
1078
|
+
capturePending(form);
|
|
1079
|
+
if (writeTo) _deepSet(node, writeTo, st.pending);
|
|
1080
|
+
cfg.onPatchState(node.id, { fieldValues: st.pending });
|
|
1081
|
+
notify(node.id, st.pending);
|
|
1082
|
+
// After save, currentState = pending (no longer dirty)
|
|
1083
|
+
st.currentState = Object.assign({}, st.pending);
|
|
1084
|
+
btn.classList.add('d-none');
|
|
1033
1085
|
btn.textContent = '✓ Saved';
|
|
1034
1086
|
setTimeout(() => { btn.textContent = 'Submit'; }, 1500);
|
|
1035
1087
|
}, { signal });
|
|
@@ -1042,14 +1094,29 @@ var LiveCard = (function () {
|
|
|
1042
1094
|
const signal = cleanup.ac.signal;
|
|
1043
1095
|
const ed = elemDef.data || {};
|
|
1044
1096
|
const writeTo = ed.writeTo;
|
|
1045
|
-
const
|
|
1097
|
+
const incomingContent = typeof data === 'string' ? data : '';
|
|
1098
|
+
|
|
1099
|
+
// --- Journal-style dirty tracking ---
|
|
1100
|
+
// currentState = last server value; pending = textarea content
|
|
1101
|
+
// On SSE re-render: if dirty, leave textarea alone; if clean, sync from server
|
|
1102
|
+
const stateKey = node.id + ':' + (writeTo || '');
|
|
1103
|
+
if (!_notesState[stateKey]) {
|
|
1104
|
+
_notesState[stateKey] = { currentState: incomingContent, pending: incomingContent };
|
|
1105
|
+
} else {
|
|
1106
|
+
const s = _notesState[stateKey];
|
|
1107
|
+
const wasDirty = s.currentState !== s.pending;
|
|
1108
|
+
s.currentState = incomingContent;
|
|
1109
|
+
if (!wasDirty) s.pending = incomingContent;
|
|
1110
|
+
// if dirty, pending stays so user's typing survives the SSE tick
|
|
1111
|
+
}
|
|
1112
|
+
const st = _notesState[stateKey];
|
|
1046
1113
|
|
|
1047
1114
|
el.innerHTML = `
|
|
1048
1115
|
<div class="btn-group btn-group-sm mb-2" role="group">
|
|
1049
1116
|
<button class="btn btn-outline-secondary active lc-n-edit" type="button">Edit</button>
|
|
1050
1117
|
<button class="btn btn-outline-secondary lc-n-preview" type="button">Preview</button>
|
|
1051
1118
|
</div>
|
|
1052
|
-
<textarea class="form-control form-control-sm lc-notes-textarea" rows="8" placeholder="Write markdown...">${_esc(
|
|
1119
|
+
<textarea class="form-control form-control-sm lc-notes-textarea" rows="8" placeholder="Write markdown...">${_esc(st.pending)}</textarea>
|
|
1053
1120
|
<div class="lc-notes-preview d-none border rounded p-2 small"></div>`;
|
|
1054
1121
|
|
|
1055
1122
|
const textarea = el.querySelector('.lc-notes-textarea');
|
|
@@ -1069,10 +1136,12 @@ var LiveCard = (function () {
|
|
|
1069
1136
|
|
|
1070
1137
|
let timer;
|
|
1071
1138
|
textarea.addEventListener('input', () => {
|
|
1139
|
+
st.pending = textarea.value; // track in journal
|
|
1072
1140
|
clearTimeout(timer);
|
|
1073
1141
|
timer = setTimeout(() => {
|
|
1074
1142
|
if (writeTo) _deepSet(node, writeTo, textarea.value);
|
|
1075
1143
|
cfg.onPatchState(node.id, { notes: textarea.value });
|
|
1144
|
+
st.currentState = textarea.value; // saved — no longer dirty
|
|
1076
1145
|
}, 800);
|
|
1077
1146
|
cleanup.timers.push(timer);
|
|
1078
1147
|
}, { signal });
|
|
@@ -1080,19 +1149,210 @@ var LiveCard = (function () {
|
|
|
1080
1149
|
|
|
1081
1150
|
// ---- todo ----
|
|
1082
1151
|
|
|
1152
|
+
// ---- editable-table ----
|
|
1153
|
+
// Renders an array bound via `data.bind` as an inline-editable table.
|
|
1154
|
+
// Each row is editable in-place; changes are saved on blur (change event).
|
|
1155
|
+
// `data.writeTo` persists changes back to card_data (same pattern as form).
|
|
1156
|
+
// `data.columns` restricts which columns appear (and in what order).
|
|
1157
|
+
// `data.schema.properties[col].type` ("number"/"integer") controls input type.
|
|
1158
|
+
// `data.addRow` (default true) shows "+ Add row" button.
|
|
1159
|
+
// `data.deleteRow` (default true) shows per-row delete button.
|
|
1160
|
+
function _renderEditableTable(data, el, elemDef, node) {
|
|
1161
|
+
const cleanup = _getCleanup(node.id);
|
|
1162
|
+
const signal = cleanup.ac.signal;
|
|
1163
|
+
const ed = elemDef.data || {};
|
|
1164
|
+
const writeTo = ed.writeTo;
|
|
1165
|
+
const schemaProps = (ed.schema && ed.schema.properties) || {};
|
|
1166
|
+
const canAdd = ed.addRow !== false;
|
|
1167
|
+
const canDelete = ed.deleteRow !== false;
|
|
1168
|
+
|
|
1169
|
+
// Derive columns from rows if not specified
|
|
1170
|
+
function getCols(rows) {
|
|
1171
|
+
if (ed.columns && ed.columns.length) return ed.columns;
|
|
1172
|
+
const s = new Set();
|
|
1173
|
+
rows.forEach(r => { if (r && typeof r === 'object') Object.keys(r).forEach(k => s.add(k)); });
|
|
1174
|
+
return [...s];
|
|
1175
|
+
}
|
|
1176
|
+
|
|
1177
|
+
// --- Journal-style dirty tracking ---
|
|
1178
|
+
// currentState = what the server last sent (updated each SSE re-render)
|
|
1179
|
+
// pending = user's local accumulation of edits on top of currentState
|
|
1180
|
+
// dirty = JSON.stringify(currentState) !== JSON.stringify(pending)
|
|
1181
|
+
//
|
|
1182
|
+
// When SSE fires and re-renders this element:
|
|
1183
|
+
// - currentState is updated to the new server value
|
|
1184
|
+
// - if NOT dirty: pending follows currentState (no unsaved edits)
|
|
1185
|
+
// - if dirty: pending is left untouched (user's edits survive the SSE tick)
|
|
1186
|
+
const stateKey = node.id + ':' + (ed.bind || ed.writeTo || '');
|
|
1187
|
+
const incomingRows = Array.isArray(writeTo ? _resolveBind(node, writeTo) : data)
|
|
1188
|
+
? (writeTo ? _resolveBind(node, writeTo) : data)
|
|
1189
|
+
: [];
|
|
1190
|
+
const incomingCopy = incomingRows.map(r => Object.assign({}, r));
|
|
1191
|
+
|
|
1192
|
+
if (!_etState[stateKey]) {
|
|
1193
|
+
// First render — initialise both sides from server data
|
|
1194
|
+
_etState[stateKey] = { currentState: incomingCopy, pending: incomingCopy.map(r => Object.assign({}, r)) };
|
|
1195
|
+
} else {
|
|
1196
|
+
const s = _etState[stateKey];
|
|
1197
|
+
const wasDirty = JSON.stringify(s.currentState) !== JSON.stringify(s.pending);
|
|
1198
|
+
s.currentState = incomingCopy;
|
|
1199
|
+
if (!wasDirty) {
|
|
1200
|
+
// No unsaved edits — sync pending to new server state
|
|
1201
|
+
s.pending = incomingCopy.map(r => Object.assign({}, r));
|
|
1202
|
+
}
|
|
1203
|
+
// If dirty, pending stays as-is so user's edits survive the SSE tick
|
|
1204
|
+
}
|
|
1205
|
+
|
|
1206
|
+
const st = _etState[stateKey];
|
|
1207
|
+
|
|
1208
|
+
function isDirty() {
|
|
1209
|
+
return JSON.stringify(st.currentState) !== JSON.stringify(st.pending);
|
|
1210
|
+
}
|
|
1211
|
+
|
|
1212
|
+
function markDirty() {
|
|
1213
|
+
const saveBtn = el.querySelector('.lc-et-save');
|
|
1214
|
+
if (saveBtn) saveBtn.classList.remove('d-none');
|
|
1215
|
+
}
|
|
1216
|
+
|
|
1217
|
+
function commitSave() {
|
|
1218
|
+
if (writeTo) _deepSet(node, writeTo, st.pending);
|
|
1219
|
+
cfg.onPatchState(node.id, { fieldValues: st.pending });
|
|
1220
|
+
notify(node.id, st.pending);
|
|
1221
|
+
// After save, currentState = pending (no longer dirty)
|
|
1222
|
+
st.currentState = st.pending.map(r => Object.assign({}, r));
|
|
1223
|
+
build();
|
|
1224
|
+
}
|
|
1225
|
+
|
|
1226
|
+
function build() {
|
|
1227
|
+
const rows = st.pending;
|
|
1228
|
+
const cols = getCols(rows);
|
|
1229
|
+
|
|
1230
|
+
if (!cols.length && !canAdd) {
|
|
1231
|
+
el.innerHTML = `<p class="text-muted small">${_esc(ed.placeholder || 'No data')}</p>`;
|
|
1232
|
+
return;
|
|
1233
|
+
}
|
|
1234
|
+
|
|
1235
|
+
let h = '<div class="table-responsive"><table class="table table-sm table-bordered mb-0 lc-editable-table"><thead><tr>';
|
|
1236
|
+
cols.forEach(c => { h += `<th class="small text-nowrap">${_esc(c)}</th>`; });
|
|
1237
|
+
if (canDelete) h += '<th style="width:2rem"></th>';
|
|
1238
|
+
h += '</tr></thead><tbody>';
|
|
1239
|
+
|
|
1240
|
+
rows.forEach((row, rowIdx) => {
|
|
1241
|
+
h += `<tr>`;
|
|
1242
|
+
cols.forEach(c => {
|
|
1243
|
+
const v = row[c];
|
|
1244
|
+
const prop = schemaProps[c] || {};
|
|
1245
|
+
const isNum = prop.type === 'number' || prop.type === 'integer' || (v != null && typeof v === 'number');
|
|
1246
|
+
const displayVal = v != null ? String(v) : '';
|
|
1247
|
+
h += `<td class="p-0">` +
|
|
1248
|
+
`<input type="${isNum ? 'number' : 'text'}" ` +
|
|
1249
|
+
`class="form-control form-control-sm border-0 rounded-0 lc-et-cell" ` +
|
|
1250
|
+
`data-row="${rowIdx}" data-col="${_esc(c)}" value="${_esc(displayVal)}"` +
|
|
1251
|
+
`${isNum ? ' step="any"' : ''}>` +
|
|
1252
|
+
`</td>`;
|
|
1253
|
+
});
|
|
1254
|
+
if (canDelete) {
|
|
1255
|
+
h += `<td class="text-center align-middle p-0">` +
|
|
1256
|
+
`<button class="btn btn-sm btn-link text-danger p-0 lc-et-del" data-row="${rowIdx}" title="Remove row">✕</button>` +
|
|
1257
|
+
`</td>`;
|
|
1258
|
+
}
|
|
1259
|
+
h += '</tr>';
|
|
1260
|
+
});
|
|
1261
|
+
|
|
1262
|
+
if (!rows.length) {
|
|
1263
|
+
const span = cols.length + (canDelete ? 1 : 0);
|
|
1264
|
+
h += `<tr><td colspan="${span}" class="text-muted small text-center">${_esc(ed.placeholder || 'No rows')}</td></tr>`;
|
|
1265
|
+
}
|
|
1266
|
+
|
|
1267
|
+
h += '</tbody></table></div>';
|
|
1268
|
+
let footer = '';
|
|
1269
|
+
if (canAdd) footer += '<button class="btn btn-sm btn-outline-secondary mt-1 me-1 lc-et-add">+ Add row</button>';
|
|
1270
|
+
footer += `<button class="btn btn-sm btn-primary mt-1 lc-et-save${isDirty() ? '' : ' d-none'}">Save</button>`;
|
|
1271
|
+
el.innerHTML = h + footer;
|
|
1272
|
+
|
|
1273
|
+
// Cell edit → update pending on blur, show Save if now dirty
|
|
1274
|
+
el.querySelectorAll('.lc-et-cell').forEach(inp => {
|
|
1275
|
+
inp.addEventListener('change', () => {
|
|
1276
|
+
const rowIdx = parseInt(inp.dataset.row);
|
|
1277
|
+
const colName = inp.dataset.col;
|
|
1278
|
+
const prop = schemaProps[colName] || {};
|
|
1279
|
+
const isNum = prop.type === 'number' || prop.type === 'integer' || inp.type === 'number';
|
|
1280
|
+
if (!st.pending[rowIdx]) return;
|
|
1281
|
+
st.pending[rowIdx] = Object.assign({}, st.pending[rowIdx]);
|
|
1282
|
+
st.pending[rowIdx][colName] = isNum ? (inp.value !== '' ? parseFloat(inp.value) : 0) : inp.value;
|
|
1283
|
+
if (isDirty()) markDirty();
|
|
1284
|
+
}, { signal });
|
|
1285
|
+
});
|
|
1286
|
+
|
|
1287
|
+
// Delete row — updates pending, rebuilds (Save button shown if dirty)
|
|
1288
|
+
el.querySelectorAll('.lc-et-del').forEach(btn => {
|
|
1289
|
+
btn.addEventListener('click', () => {
|
|
1290
|
+
const rowIdx = parseInt(btn.dataset.row);
|
|
1291
|
+
st.pending = st.pending.filter((_, i) => i !== rowIdx);
|
|
1292
|
+
build();
|
|
1293
|
+
}, { signal });
|
|
1294
|
+
});
|
|
1295
|
+
|
|
1296
|
+
// Add row — appends blank row to pending, rebuilds (Save button shown if dirty)
|
|
1297
|
+
const addBtn = el.querySelector('.lc-et-add');
|
|
1298
|
+
if (addBtn) {
|
|
1299
|
+
addBtn.addEventListener('click', () => {
|
|
1300
|
+
const newRow = {};
|
|
1301
|
+
getCols(st.pending).forEach(c => { newRow[c] = ''; });
|
|
1302
|
+
st.pending = [...st.pending, newRow];
|
|
1303
|
+
build();
|
|
1304
|
+
}, { signal });
|
|
1305
|
+
}
|
|
1306
|
+
|
|
1307
|
+
// Save button — persist pending to server via fieldValues (same as form submit)
|
|
1308
|
+
const saveBtn = el.querySelector('.lc-et-save');
|
|
1309
|
+
if (saveBtn) {
|
|
1310
|
+
saveBtn.addEventListener('click', () => {
|
|
1311
|
+
commitSave();
|
|
1312
|
+
saveBtn.textContent = '✓ Saved';
|
|
1313
|
+
setTimeout(() => { saveBtn.textContent = 'Save'; }, 1500);
|
|
1314
|
+
}, { signal });
|
|
1315
|
+
}
|
|
1316
|
+
}
|
|
1317
|
+
|
|
1318
|
+
build();
|
|
1319
|
+
}
|
|
1320
|
+
|
|
1321
|
+
// ---- todo ----
|
|
1322
|
+
|
|
1083
1323
|
function _renderTodo(data, el, elemDef, node) {
|
|
1084
1324
|
const cleanup = _getCleanup(node.id);
|
|
1085
1325
|
const signal = cleanup.ac.signal;
|
|
1086
1326
|
const ed = elemDef.data || {};
|
|
1087
1327
|
const writeTo = ed.writeTo;
|
|
1088
|
-
|
|
1328
|
+
|
|
1329
|
+
// --- Journal-style dirty tracking ---
|
|
1330
|
+
// currentState = last confirmed server state; pending = local working copy
|
|
1331
|
+
// On SSE re-render: if dirty (action in-flight), keep pending; if clean, sync from server
|
|
1332
|
+
const stateKey = node.id + ':' + (writeTo || '');
|
|
1333
|
+
const incomingItems = Array.isArray(data) ? data.map(r => Object.assign({}, r)) : [];
|
|
1334
|
+
|
|
1335
|
+
if (!_todoState[stateKey]) {
|
|
1336
|
+
_todoState[stateKey] = { currentState: incomingItems, pending: incomingItems.map(r => Object.assign({}, r)) };
|
|
1337
|
+
} else {
|
|
1338
|
+
const s = _todoState[stateKey];
|
|
1339
|
+
const wasDirty = JSON.stringify(s.currentState) !== JSON.stringify(s.pending);
|
|
1340
|
+
s.currentState = incomingItems;
|
|
1341
|
+
if (!wasDirty) s.pending = incomingItems.map(r => Object.assign({}, r));
|
|
1342
|
+
// if dirty, pending stays so in-flight changes survive the SSE tick
|
|
1343
|
+
}
|
|
1344
|
+
const st = _todoState[stateKey];
|
|
1089
1345
|
|
|
1090
1346
|
function save() {
|
|
1091
|
-
if (writeTo) _deepSet(node, writeTo,
|
|
1092
|
-
cfg.onPatchState(node.id, {
|
|
1347
|
+
if (writeTo) _deepSet(node, writeTo, st.pending);
|
|
1348
|
+
cfg.onPatchState(node.id, { fieldValues: st.pending });
|
|
1349
|
+
notify(node.id, st.pending);
|
|
1350
|
+
// mark clean after save so next SSE sync resumes normally
|
|
1351
|
+
st.currentState = st.pending.map(r => Object.assign({}, r));
|
|
1093
1352
|
}
|
|
1094
1353
|
|
|
1095
1354
|
function build() {
|
|
1355
|
+
const items = st.pending;
|
|
1096
1356
|
let h = '<div class="lc-todo-list">';
|
|
1097
1357
|
items.forEach((item, i) => {
|
|
1098
1358
|
const chk = item.done ? ' checked' : '';
|
|
@@ -1108,14 +1368,25 @@ var LiveCard = (function () {
|
|
|
1108
1368
|
el.innerHTML = h;
|
|
1109
1369
|
|
|
1110
1370
|
el.querySelectorAll('input[data-idx]').forEach(cb => {
|
|
1111
|
-
cb.addEventListener('change', () => {
|
|
1371
|
+
cb.addEventListener('change', () => {
|
|
1372
|
+
st.pending[parseInt(cb.dataset.idx)].done = cb.checked;
|
|
1373
|
+
save(); build();
|
|
1374
|
+
}, { signal });
|
|
1112
1375
|
});
|
|
1113
1376
|
el.querySelectorAll('[data-rm]').forEach(btn => {
|
|
1114
|
-
btn.addEventListener('click', () => {
|
|
1377
|
+
btn.addEventListener('click', () => {
|
|
1378
|
+
st.pending.splice(parseInt(btn.dataset.rm), 1);
|
|
1379
|
+
save(); build();
|
|
1380
|
+
}, { signal });
|
|
1115
1381
|
});
|
|
1116
1382
|
const addInput = el.querySelector('.input-group input');
|
|
1117
1383
|
const addBtn = el.querySelector('.lc-todo-add');
|
|
1118
|
-
const addItem = () => {
|
|
1384
|
+
const addItem = () => {
|
|
1385
|
+
const t = addInput.value.trim();
|
|
1386
|
+
if (!t) return;
|
|
1387
|
+
st.pending.push({ text: t, done: false });
|
|
1388
|
+
save(); build();
|
|
1389
|
+
};
|
|
1119
1390
|
addBtn.addEventListener('click', addItem, { signal });
|
|
1120
1391
|
addInput.addEventListener('keydown', e => { if (e.key === 'Enter') { e.preventDefault(); addItem(); } }, { signal });
|
|
1121
1392
|
}
|
|
@@ -1171,7 +1442,10 @@ var LiveCard = (function () {
|
|
|
1171
1442
|
function _renderText(data, el, elemDef) {
|
|
1172
1443
|
const ed = elemDef.data || {};
|
|
1173
1444
|
const format = ed.format || 'default';
|
|
1174
|
-
const style = ed.style || 'default';
|
|
1445
|
+
const style = elemDef.style || ed.style || 'default';
|
|
1446
|
+
const hideIfEmpty = ed.hideIfEmpty || elemDef.hideIfEmpty;
|
|
1447
|
+
|
|
1448
|
+
if (hideIfEmpty && (data == null || data === '')) { el.innerHTML = ''; return; }
|
|
1175
1449
|
|
|
1176
1450
|
// Handle file-links format
|
|
1177
1451
|
if (format === 'file-links') {
|
|
@@ -1195,7 +1469,10 @@ var LiveCard = (function () {
|
|
|
1195
1469
|
|
|
1196
1470
|
// Default text rendering
|
|
1197
1471
|
const tag = style === 'heading' ? 'h4' : 'div';
|
|
1198
|
-
const cls = style === 'muted' ? 'text-muted small'
|
|
1472
|
+
const cls = style === 'muted' ? 'text-muted small'
|
|
1473
|
+
: style === 'muted-italic' ? 'text-muted small fst-italic'
|
|
1474
|
+
: style === 'heading' ? 'fw-bold'
|
|
1475
|
+
: 'small';
|
|
1199
1476
|
el.innerHTML = `<${tag} class="${cls}">${_esc(data != null ? String(data) : '')}</${tag}>`;
|
|
1200
1477
|
}
|
|
1201
1478
|
|
|
@@ -1527,8 +1804,9 @@ var LiveCard = (function () {
|
|
|
1527
1804
|
|
|
1528
1805
|
// ---- Register built-in renderers ----
|
|
1529
1806
|
|
|
1530
|
-
_renderers.table
|
|
1531
|
-
_renderers
|
|
1807
|
+
_renderers.table = _renderTable;
|
|
1808
|
+
_renderers['editable-table'] = _renderEditableTable;
|
|
1809
|
+
_renderers.filter = _renderFilter;
|
|
1532
1810
|
_renderers.metric = _renderMetric;
|
|
1533
1811
|
_renderers.list = _renderList;
|
|
1534
1812
|
_renderers.chart = _renderChart;
|
|
@@ -1558,6 +1836,26 @@ var LiveCard = (function () {
|
|
|
1558
1836
|
const container = document.createElement('div');
|
|
1559
1837
|
container.className = 'row g-2';
|
|
1560
1838
|
|
|
1839
|
+
const _taskStatus = node.runtime_state && node.runtime_state.task_status;
|
|
1840
|
+
if (_taskStatus && _taskStatus !== 'completed') {
|
|
1841
|
+
const statusEl = document.createElement('div');
|
|
1842
|
+
statusEl.className = 'col-12 d-flex align-items-center gap-2 mb-1';
|
|
1843
|
+
var _statusIconHtml;
|
|
1844
|
+
if (_taskStatus === 'running') {
|
|
1845
|
+
_statusIconHtml = '<span class="spinner-border spinner-border-sm text-muted" style="width:.75rem;height:.75rem;flex-shrink:0"></span>';
|
|
1846
|
+
} else if (_taskStatus === 'failed') {
|
|
1847
|
+
_statusIconHtml = '<span style="font-size:.75rem;line-height:1;flex-shrink:0;color:#dc3545">⚠︎</span>'; // ⚠ (text variant)
|
|
1848
|
+
} else if (_taskStatus === 'not-started') {
|
|
1849
|
+
_statusIconHtml = '<span style="font-size:.75rem;line-height:1;flex-shrink:0" class="text-muted">○</span>'; // ○
|
|
1850
|
+
} else if (_taskStatus === 'inactivated') {
|
|
1851
|
+
_statusIconHtml = '<span style="font-size:.75rem;line-height:1;flex-shrink:0" class="text-muted">⊖</span>'; // ⊖
|
|
1852
|
+
} else {
|
|
1853
|
+
_statusIconHtml = '<span style="font-size:.75rem;line-height:1;flex-shrink:0" class="text-muted">–</span>'; // –
|
|
1854
|
+
}
|
|
1855
|
+
statusEl.innerHTML = _statusIconHtml + '<span class="text-muted" style="font-size:.75rem">' + _esc(_taskStatus) + '</span>';
|
|
1856
|
+
container.appendChild(statusEl);
|
|
1857
|
+
}
|
|
1858
|
+
|
|
1561
1859
|
view.elements.forEach(elemDef => {
|
|
1562
1860
|
// Visibility gate
|
|
1563
1861
|
if (elemDef.visible) {
|
|
@@ -1631,18 +1929,35 @@ var LiveCard = (function () {
|
|
|
1631
1929
|
if (filesCount > 0) h += `<span class="ms-1 small" aria-label="${filesCount} files">${filesCount}</span>`;
|
|
1632
1930
|
h += '</button>';
|
|
1633
1931
|
// Chat icon button (speech bubble)
|
|
1634
|
-
h += `<button class="btn btn-sm btn-outline-secondary" id="${uid}-chat-open" title="Chat">`;
|
|
1932
|
+
h += `<button class="btn btn-sm btn-outline-secondary d-inline-flex align-items-center" id="${uid}-chat-open" title="Chat">`;
|
|
1635
1933
|
h += '<svg width="14" height="14" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><path d="M21 15a2 2 0 01-2 2H7l-4 4V5a2 2 0 012-2h14a2 2 0 012 2z"/></svg>';
|
|
1636
1934
|
h += '</button>';
|
|
1637
1935
|
// Refresh icon button
|
|
1638
1936
|
if (showRefresh) {
|
|
1639
|
-
h += `<button class="btn btn-sm btn-outline-secondary" id="${uid}-refresh" title="Refresh">`;
|
|
1937
|
+
h += `<button class="btn btn-sm btn-outline-secondary d-inline-flex align-items-center" id="${uid}-refresh" title="Refresh">`;
|
|
1640
1938
|
h += '<svg width="14" height="14" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2"><polyline points="23 4 23 10 17 10"/><path d="M20.49 15a9 9 0 11-2.12-9.36L23 10"/></svg>';
|
|
1641
1939
|
h += '</button>';
|
|
1642
1940
|
}
|
|
1643
1941
|
h += '</div>';
|
|
1644
1942
|
h += '</div>';
|
|
1645
1943
|
|
|
1944
|
+
// Inference status bar: completion criteria + task-completed tick
|
|
1945
|
+
const inferenceData = node.card_data && node.card_data.llm_task_completion_inference;
|
|
1946
|
+
const isTaskCompleted = !!(inferenceData && inferenceData.isTaskCompleted);
|
|
1947
|
+
const whenIs = node.card && typeof node.card.when_is_task_completed === 'string' && node.card.when_is_task_completed.trim();
|
|
1948
|
+
if (whenIs || isTaskCompleted) {
|
|
1949
|
+
h += `<div class="d-flex align-items-start gap-2 mb-2 px-1 py-1 rounded lc-inference-bar" style="background:rgba(0,0,0,.03)">`;
|
|
1950
|
+
if (isTaskCompleted) {
|
|
1951
|
+
h += `<span class="lc-inference-icon" title="Task completed" style="color:#198754;font-size:.75rem;line-height:1.2;flex-shrink:0">●</span>`;
|
|
1952
|
+
} else {
|
|
1953
|
+
h += `<span class="lc-inference-icon" style="color:#aaa;font-size:.75rem;line-height:1.4;flex-shrink:0" title="Awaiting inference">○</span>`;
|
|
1954
|
+
}
|
|
1955
|
+
if (whenIs) {
|
|
1956
|
+
h += `<span class="text-muted" style="font-size:.72rem;line-height:1.4;font-style:italic"><span style="opacity:.55;font-style:normal">done when:</span> ${_esc(whenIs)}</span>`;
|
|
1957
|
+
}
|
|
1958
|
+
h += `</div>`;
|
|
1959
|
+
}
|
|
1960
|
+
|
|
1646
1961
|
// Elements area
|
|
1647
1962
|
h += `<div class="lc-result" id="${uid}-result"></div>`;
|
|
1648
1963
|
|
|
@@ -1659,9 +1974,7 @@ var LiveCard = (function () {
|
|
|
1659
1974
|
const resultEl = document.getElementById(uid + '-result');
|
|
1660
1975
|
_nodeEls[node.id] = { container: containerEl, resultEl, uid };
|
|
1661
1976
|
|
|
1662
|
-
if (node.card_data && node.card_data.status === '
|
|
1663
|
-
resultEl.innerHTML = '<div class="d-flex align-items-center gap-2"><span class="spinner-border spinner-border-sm text-muted"></span><span class="text-muted small">Loading…</span></div>';
|
|
1664
|
-
} else if (node.card_data && node.card_data.status === 'error' && node.card_data.error) {
|
|
1977
|
+
if (node.card_data && node.card_data.status === 'error' && node.card_data.error) {
|
|
1665
1978
|
resultEl.innerHTML = `<div class="text-danger small fw-semibold">Refresh failed</div><pre class="text-muted small mt-1" style="white-space:pre-wrap">${_esc(node.card_data.error)}</pre>`;
|
|
1666
1979
|
} else {
|
|
1667
1980
|
_runCompute(node).then(function () { _renderElements(node, resultEl); });
|
|
@@ -1758,9 +2071,20 @@ var LiveCard = (function () {
|
|
|
1758
2071
|
const filesCountEl = document.getElementById(info.uid + '-files-count');
|
|
1759
2072
|
if (filesCountEl && filesCountEl.parentNode) filesCountEl.parentNode.removeChild(filesCountEl);
|
|
1760
2073
|
|
|
1761
|
-
|
|
1762
|
-
|
|
1763
|
-
|
|
2074
|
+
// Update inference status bar (tick / hourglass) if card_data changed
|
|
2075
|
+
const infBar = info.container.querySelector('.lc-inference-bar');
|
|
2076
|
+
if (infBar) {
|
|
2077
|
+
const infData = node.card_data && node.card_data.llm_task_completion_inference;
|
|
2078
|
+
const done = !!(infData && infData.isTaskCompleted);
|
|
2079
|
+
const iconEl = infBar.querySelector('.lc-inference-icon');
|
|
2080
|
+
if (iconEl) {
|
|
2081
|
+
iconEl.title = done ? 'Task completed' : 'Awaiting inference';
|
|
2082
|
+
iconEl.style.color = done ? '#198754' : '#aaa';
|
|
2083
|
+
iconEl.innerHTML = done ? '●' : '○';
|
|
2084
|
+
}
|
|
2085
|
+
}
|
|
2086
|
+
|
|
2087
|
+
if (node.card_data.status === 'error' && node.card_data.error) {
|
|
1764
2088
|
info.resultEl.innerHTML = `<div class="text-danger small fw-semibold">Refresh failed</div><pre class="text-muted small mt-1" style="white-space:pre-wrap">${_esc(node.card_data.error)}</pre>`;
|
|
1765
2089
|
} else {
|
|
1766
2090
|
_runCompute(node).then(function () { _renderElements(node, info.resultEl); });
|
|
@@ -100,9 +100,9 @@
|
|
|
100
100
|
},
|
|
101
101
|
|
|
102
102
|
"source_def": {
|
|
103
|
-
"description": "One source entry. The engine
|
|
103
|
+
"description": "One source entry. The engine requires 'bindTo' (compute namespace key) and 'outputFile' (delivery signal path). bindTo and outputFile must be unique across all sources in a card. Every other property is yours — add whatever your task-executor needs: kind, url, headers, mailbox, channel, model, query, etc. The full object is passed verbatim as the --in JSON to the executor.",
|
|
104
104
|
"type": "object",
|
|
105
|
-
"required": ["bindTo"],
|
|
105
|
+
"required": ["bindTo", "outputFile"],
|
|
106
106
|
"additionalProperties": true,
|
|
107
107
|
"properties": {
|
|
108
108
|
"bindTo": { "type": "string", "description": "Key under fetched_sources.* available in compute expressions" },
|
|
@@ -119,7 +119,7 @@
|
|
|
119
119
|
"properties": {
|
|
120
120
|
"id": { "type": "string", "description": "Optional element ID for targeted updates" },
|
|
121
121
|
"kind": {
|
|
122
|
-
"enum": ["metric", "table", "chart", "form", "filter", "list",
|
|
122
|
+
"enum": ["metric", "table", "editable-table", "chart", "form", "filter", "list",
|
|
123
123
|
"notes", "todo", "alert", "narrative", "badge", "text",
|
|
124
124
|
"markdown", "custom", "actions"]
|
|
125
125
|
},
|
|
@@ -227,7 +227,7 @@
|
|
|
227
227
|
"view": { "$ref": "#/definitions/view" },
|
|
228
228
|
"card_data": {
|
|
229
229
|
"type": "object",
|
|
230
|
-
"description": "Authored card data
|
|
230
|
+
"description": "Authored card data and runtime metadata. Includes uploaded-file metadata maintained by host handlers and inference evaluation results.",
|
|
231
231
|
"properties": {
|
|
232
232
|
"files": {
|
|
233
233
|
"type": "array",
|
|
@@ -255,6 +255,18 @@
|
|
|
255
255
|
},
|
|
256
256
|
"additionalProperties": false
|
|
257
257
|
}
|
|
258
|
+
},
|
|
259
|
+
"llm_task_completion_inference": {
|
|
260
|
+
"type": "object",
|
|
261
|
+
"description": "Runtime state written by the inference adapter (advanced/undocumented). Prefer the standard sources → compute → provides pattern for LLM-based signals.",
|
|
262
|
+
"properties": {
|
|
263
|
+
"inferenceRequested": { "type": "string", "format": "date-time", "description": "Timestamp when the latest inference request was initiated" },
|
|
264
|
+
"inferenceCompletedAt": { "type": "string", "format": "date-time", "description": "Timestamp when the latest inference request completed" },
|
|
265
|
+
"isTaskCompleted": { "type": "boolean", "description": "Whether the task is considered complete by the adapter" },
|
|
266
|
+
"reasoning": { "type": "string", "description": "Explanation of completion decision" },
|
|
267
|
+
"evidence": { "type": "array", "description": "Supporting evidence from sources/compute" }
|
|
268
|
+
},
|
|
269
|
+
"additionalProperties": true
|
|
258
270
|
}
|
|
259
271
|
},
|
|
260
272
|
"additionalProperties": true
|
|
@@ -268,6 +280,10 @@
|
|
|
268
280
|
"type": "array",
|
|
269
281
|
"description": "Ordered array of compute steps. Each reads card_data.*/requires.*/fetched_sources.*/computed_values.* and writes to ephemeral computed_values[bindTo].",
|
|
270
282
|
"items": { "$ref": "#/definitions/compute_step" }
|
|
283
|
+
},
|
|
284
|
+
"when_is_task_completed": {
|
|
285
|
+
"type": "string",
|
|
286
|
+
"description": "Advanced/undocumented: invokes a registered inference adapter instead of the default source-delivery completion gate. Prefer the standard pattern: use an LLM-backed source, compute a verdict token, and publish it via provides."
|
|
271
287
|
}
|
|
272
288
|
}
|
|
273
289
|
}
|