@luckydraw/cumulus 1.0.0 → 1.0.1
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/CHANGELOG.md +9 -0
- package/dist/gateway/adapters/webchat.d.ts +13 -0
- package/dist/gateway/adapters/webchat.d.ts.map +1 -1
- package/dist/gateway/adapters/webchat.js +56 -3
- package/dist/gateway/adapters/webchat.js.map +1 -1
- package/dist/gateway/static/blex-render.js +341 -0
- package/dist/gateway/static/chat.html +1 -0
- package/dist/gateway/static/widget.js +112 -127
- package/docs/web-app-agent-guide.md +40 -1
- package/examples/web-app-agent/README.md +161 -14
- package/examples/web-app-agent/gateway.config.example.json +22 -4
- package/examples/web-app-agent/public/agent/blex-mount.js +136 -0
- package/examples/web-app-agent/public/agent/bridge-mount.js +19 -3
- package/examples/web-app-agent/public/agent/commands.js +10 -4
- package/examples/web-app-agent/public/agent/device-thread.js +5 -5
- package/examples/web-app-agent/public/agent/panel.css +6 -0
- package/examples/web-app-agent/public/agent/panel.js +27 -4
- package/examples/web-app-agent/public/index.html +15 -0
- package/examples/web-app-agent/server.js +148 -11
- package/package.json +1 -1
|
@@ -0,0 +1,341 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Cumulus shared blex renderer (task 131, item 3).
|
|
3
|
+
*
|
|
4
|
+
* ONE renderer, many surfaces. The standalone chat widget and any embedding app
|
|
5
|
+
* (the web-app-agent kit, kalendeer, …) drive this same file through an injected
|
|
6
|
+
* adapter, rather than each re-deriving a port of it. Copying the port is what
|
|
7
|
+
* guarantees drift in every consumer at once.
|
|
8
|
+
*
|
|
9
|
+
* Served by the gateway at /blex-render.js. It is plain ES5-compatible script —
|
|
10
|
+
* no modules, no build step — so a browser can load it directly, cross-origin.
|
|
11
|
+
*
|
|
12
|
+
* ── Adapter contract ────────────────────────────────────────────────────────
|
|
13
|
+
* Required:
|
|
14
|
+
* allowType(type) -> boolean. Which block types this surface renders.
|
|
15
|
+
* getInput() -> the text input element, or null.
|
|
16
|
+
* getSendButton() -> the send button element, or null.
|
|
17
|
+
* Optional (default to no-ops, which is what makes a surface render-only):
|
|
18
|
+
* addChip(interaction, handle, sourceId)
|
|
19
|
+
* readPersisted(storeKey) -> previously stored interaction value
|
|
20
|
+
* persist(storeKey, value)
|
|
21
|
+
* onRendered(container, storeKey)
|
|
22
|
+
*
|
|
23
|
+
* A surface that supplies only the three required members gets render-only
|
|
24
|
+
* behaviour for free: interactions that complete locally still work, and
|
|
25
|
+
* deferred ones land in a no-op instead of a dead button.
|
|
26
|
+
*/
|
|
27
|
+
(function (global) {
|
|
28
|
+
'use strict';
|
|
29
|
+
|
|
30
|
+
// ── Fenced-code spans ──────────────────────────────────────────────────────
|
|
31
|
+
//
|
|
32
|
+
// Held as a source string, not a shared RegExp object — a /g regex carries
|
|
33
|
+
// lastIndex state, and these call sites must not be able to affect each other.
|
|
34
|
+
var CODE_FENCE_SRC = '```([^\\n`]*)\\n([\\s\\S]*?)```';
|
|
35
|
+
var BLEX_FENCE_SRC = '~~~blex:(\\w[\\w-]*)\\n([\\s\\S]*?)\\n~~~';
|
|
36
|
+
|
|
37
|
+
function codeFenceRe() {
|
|
38
|
+
return new RegExp(CODE_FENCE_SRC, 'g');
|
|
39
|
+
}
|
|
40
|
+
|
|
41
|
+
function findCodeFenceSpans(text) {
|
|
42
|
+
var spans = [];
|
|
43
|
+
var re = codeFenceRe();
|
|
44
|
+
var m;
|
|
45
|
+
while ((m = re.exec(text)) !== null) {
|
|
46
|
+
spans.push([m.index, m.index + m[0].length]);
|
|
47
|
+
}
|
|
48
|
+
return spans;
|
|
49
|
+
}
|
|
50
|
+
|
|
51
|
+
/**
|
|
52
|
+
* Replace ~~~blex fences with \x00BLEX_n\x00 tokens.
|
|
53
|
+
*
|
|
54
|
+
* A fence INSIDE a code block is an example being displayed, not a block to
|
|
55
|
+
* render — extracting it would delete it from the code block that exists to
|
|
56
|
+
* show it. String.replace reports offsets into the original string, so one
|
|
57
|
+
* pass over precomputed spans is enough.
|
|
58
|
+
*/
|
|
59
|
+
function extractBlocks(text) {
|
|
60
|
+
var blocks = [];
|
|
61
|
+
var protectedSpans = findCodeFenceSpans(text);
|
|
62
|
+
var replaced = text.replace(
|
|
63
|
+
new RegExp(BLEX_FENCE_SRC, 'g'),
|
|
64
|
+
function (match, type, json, offset) {
|
|
65
|
+
for (var i = 0; i < protectedSpans.length; i++) {
|
|
66
|
+
if (offset >= protectedSpans[i][0] && offset < protectedSpans[i][1]) return match;
|
|
67
|
+
}
|
|
68
|
+
var idx = blocks.length;
|
|
69
|
+
blocks.push({ type: type, json: json.trim(), idx: idx });
|
|
70
|
+
return '\x00BLEX_' + idx + '\x00';
|
|
71
|
+
}
|
|
72
|
+
);
|
|
73
|
+
return { text: replaced, blocks: blocks };
|
|
74
|
+
}
|
|
75
|
+
|
|
76
|
+
function escapeHtml(text) {
|
|
77
|
+
var div = document.createElement('div');
|
|
78
|
+
div.textContent = text;
|
|
79
|
+
return div.innerHTML;
|
|
80
|
+
}
|
|
81
|
+
|
|
82
|
+
/** The original fence text, reconstructed for display. */
|
|
83
|
+
function fenceSource(block) {
|
|
84
|
+
return '~~~blex:' + block.type + '\n' + block.json + '\n~~~';
|
|
85
|
+
}
|
|
86
|
+
|
|
87
|
+
/**
|
|
88
|
+
* HTML for one placeholder container.
|
|
89
|
+
*
|
|
90
|
+
* The container carries the raw fence as its body. `render()` clears it before
|
|
91
|
+
* rendering, so a CLAIMED container is unchanged — but an unclaimed one shows
|
|
92
|
+
* what the author actually wrote instead of a blank rectangle. Containers go
|
|
93
|
+
* unclaimed on several real paths: user messages and changelog entries (which
|
|
94
|
+
* render markdown but never call render()), any page where blex.min.js failed
|
|
95
|
+
* to fetch, and — deliberately — any block type the adapter denies.
|
|
96
|
+
*
|
|
97
|
+
* opts.streaming suppresses the fallback: a completed-but-not-yet-rendered
|
|
98
|
+
* fence would otherwise flash its JSON before flipping to the rendered block.
|
|
99
|
+
*/
|
|
100
|
+
function placeholderHtml(idx, block, opts) {
|
|
101
|
+
var streaming = !!(opts && opts.streaming);
|
|
102
|
+
var body =
|
|
103
|
+
streaming || !block
|
|
104
|
+
? ''
|
|
105
|
+
: '<pre class="blex-fallback">' + escapeHtml(fenceSource(block)) + '</pre>';
|
|
106
|
+
return '<div class="blex-block-container" data-blex-idx="' + idx + '">' + body + '</div>';
|
|
107
|
+
}
|
|
108
|
+
|
|
109
|
+
/** Replace every \x00BLEX_n\x00 token in rendered HTML with its placeholder. */
|
|
110
|
+
function insertPlaceholders(html, blocks, opts) {
|
|
111
|
+
// The NUL is a deliberate in-band sentinel: it cannot occur in model output,
|
|
112
|
+
// so a token can never be forged by message content.
|
|
113
|
+
// eslint-disable-next-line no-control-regex
|
|
114
|
+
return html.replace(/\x00BLEX_(\d+)\x00/g, function (_, idx) {
|
|
115
|
+
return placeholderHtml(idx, blocks[parseInt(idx, 10)], opts);
|
|
116
|
+
});
|
|
117
|
+
}
|
|
118
|
+
|
|
119
|
+
// ── The render-only allow set ──────────────────────────────────────────────
|
|
120
|
+
//
|
|
121
|
+
// Derived, not argued. A type may render on a non-interactive surface iff every
|
|
122
|
+
// affordance it draws either completes locally or is purely visual:
|
|
123
|
+
//
|
|
124
|
+
// table / code / file-tree / timeline / image — selection is a highlight.
|
|
125
|
+
// Nothing is labelled with a verb; an emit landing in a no-op is invisible.
|
|
126
|
+
// terminal / svg — labelled buttons, but clipboard write and the object-URL
|
|
127
|
+
// download both run BEFORE the emit. The emit is a receipt, not the mechanism.
|
|
128
|
+
// chart / status / metric / progress / gallery / kanban / calendar /
|
|
129
|
+
// branch / layout — no clickable affordances at all.
|
|
130
|
+
//
|
|
131
|
+
// Excluded, and the reason each is excluded rather than merely unwired:
|
|
132
|
+
// confirm / poll / form — interaction is the entire point.
|
|
133
|
+
// diff — Apply/Reject have no local half. The emit IS the mechanism, so on a
|
|
134
|
+
// render-only surface they are guaranteed dead buttons that read as
|
|
135
|
+
// "click to apply" — worse than not rendering, because a dead button looks
|
|
136
|
+
// alive in a way raw JSON does not.
|
|
137
|
+
// mermaid — excluded on a DIFFERENT axis from the rest, and the distinction
|
|
138
|
+
// matters: it draws no affordance at all, so the local-completion test
|
|
139
|
+
// above would admit it. It cannot LOAD. Its renderer is the one real dynamic
|
|
140
|
+
// import in the 340KB bundle — `await import("mermaid")`, a bare specifier
|
|
141
|
+
// with no resolution in a classic script and no import map on any cumulus
|
|
142
|
+
// page. Upstream catches the throw and paints the literal string
|
|
143
|
+
// "Mermaid render error", so allowing it ships a guaranteed error box.
|
|
144
|
+
// Upstream ships a companion global bundle for `chart` (whose loader
|
|
145
|
+
// resolves in-bundle) and none for mermaid, so this is not a wiring
|
|
146
|
+
// oversight on our side. Measured by @cdda, re-verified against our own
|
|
147
|
+
// dist/gateway/static/blex.min.js.
|
|
148
|
+
var RENDER_ONLY_TYPES = [
|
|
149
|
+
'table',
|
|
150
|
+
'status',
|
|
151
|
+
'metric',
|
|
152
|
+
'progress',
|
|
153
|
+
'code',
|
|
154
|
+
'timeline',
|
|
155
|
+
'svg',
|
|
156
|
+
'image',
|
|
157
|
+
'gallery',
|
|
158
|
+
'file-tree',
|
|
159
|
+
'terminal',
|
|
160
|
+
'chart',
|
|
161
|
+
'kanban',
|
|
162
|
+
'calendar',
|
|
163
|
+
'branch',
|
|
164
|
+
'layout',
|
|
165
|
+
];
|
|
166
|
+
|
|
167
|
+
var INTERACTIVE_TYPES = ['confirm', 'poll', 'form', 'diff'];
|
|
168
|
+
|
|
169
|
+
/**
|
|
170
|
+
* An adapter for a surface that has an input but no chip tray.
|
|
171
|
+
*
|
|
172
|
+
* Denied types are NOT routed to a library fallback renderer: the global blex
|
|
173
|
+
* bundle exports BaseRenderer, getPluginInfo, getRegisteredTypes, hasBlockType,
|
|
174
|
+
* registerBlockType, registerLazyBlockType, registerPlugin, renderBlock,
|
|
175
|
+
* renderPlaceholder, unregisterBlockType, unregisterPlugin — and no fallback
|
|
176
|
+
* renderer among them. Leaving the container unclaimed reuses the raw-fence
|
|
177
|
+
* body above, which is honest and costs nothing.
|
|
178
|
+
*
|
|
179
|
+
* Deny is a per-render gate on purpose. Do NOT implement it with
|
|
180
|
+
* unregisterBlockType: the registry is module-global, so unregistering
|
|
181
|
+
* `confirm` removes it for every surface sharing that module instance.
|
|
182
|
+
*/
|
|
183
|
+
function renderOnlyAdapter(overrides) {
|
|
184
|
+
var base = {
|
|
185
|
+
allowType: function (type) {
|
|
186
|
+
return RENDER_ONLY_TYPES.indexOf(type) !== -1;
|
|
187
|
+
},
|
|
188
|
+
getInput: function () {
|
|
189
|
+
return null;
|
|
190
|
+
},
|
|
191
|
+
getSendButton: function () {
|
|
192
|
+
return null;
|
|
193
|
+
},
|
|
194
|
+
};
|
|
195
|
+
if (overrides) {
|
|
196
|
+
for (var k in overrides) {
|
|
197
|
+
if (Object.prototype.hasOwnProperty.call(overrides, k)) base[k] = overrides[k];
|
|
198
|
+
}
|
|
199
|
+
}
|
|
200
|
+
return base;
|
|
201
|
+
}
|
|
202
|
+
|
|
203
|
+
// ── Handle lifecycle ───────────────────────────────────────────────────────
|
|
204
|
+
|
|
205
|
+
var handleMap = typeof WeakMap !== 'undefined' ? new WeakMap() : null;
|
|
206
|
+
|
|
207
|
+
function trackHandle(el, handle) {
|
|
208
|
+
if (!handleMap) return;
|
|
209
|
+
var existing = handleMap.get(el) || [];
|
|
210
|
+
existing.push(handle);
|
|
211
|
+
handleMap.set(el, existing);
|
|
212
|
+
}
|
|
213
|
+
|
|
214
|
+
function destroy(el) {
|
|
215
|
+
if (!handleMap) return;
|
|
216
|
+
var handles = handleMap.get(el);
|
|
217
|
+
if (!handles) return;
|
|
218
|
+
handles.forEach(function (h) {
|
|
219
|
+
try {
|
|
220
|
+
h.destroy();
|
|
221
|
+
} catch {
|
|
222
|
+
/* a renderer that cannot tear down must not block the rest */
|
|
223
|
+
}
|
|
224
|
+
});
|
|
225
|
+
handleMap.delete(el);
|
|
226
|
+
}
|
|
227
|
+
|
|
228
|
+
function errorBox(message) {
|
|
229
|
+
return (
|
|
230
|
+
'<pre style="color:#f66;font-size:0.85em;padding:0.5em;">' + escapeHtml(message) + '</pre>'
|
|
231
|
+
);
|
|
232
|
+
}
|
|
233
|
+
|
|
234
|
+
function callOptional(adapter, name, args) {
|
|
235
|
+
if (adapter && typeof adapter[name] === 'function') {
|
|
236
|
+
return adapter[name].apply(adapter, args);
|
|
237
|
+
}
|
|
238
|
+
return undefined;
|
|
239
|
+
}
|
|
240
|
+
|
|
241
|
+
/**
|
|
242
|
+
* Render extracted blocks into the placeholder containers inside `el`.
|
|
243
|
+
*
|
|
244
|
+
* opts: { msgKey, streaming, keyPrefix }
|
|
245
|
+
*/
|
|
246
|
+
function render(el, blocks, adapter, opts) {
|
|
247
|
+
if (!blocks || blocks.length === 0) return;
|
|
248
|
+
// Contract violation, checked before the library guard: an adapter without
|
|
249
|
+
// allowType is a programming error and must surface whether or not blex
|
|
250
|
+
// happened to load, otherwise it hides until the first page that has blex.
|
|
251
|
+
if (!adapter || typeof adapter.allowType !== 'function') {
|
|
252
|
+
throw new Error('[blex] render() requires an adapter with allowType()');
|
|
253
|
+
}
|
|
254
|
+
if (typeof global.Blex === 'undefined') {
|
|
255
|
+
// The raw-fence fallback is already in the DOM, so the reader sees the
|
|
256
|
+
// payload rather than a blank box. warn, not error: some embedders fail
|
|
257
|
+
// their build gate on any console error.
|
|
258
|
+
console.warn('[blex] library not loaded — leaving raw fences in place');
|
|
259
|
+
return;
|
|
260
|
+
}
|
|
261
|
+
var options = opts || {};
|
|
262
|
+
var streaming = !!options.streaming;
|
|
263
|
+
var msgKey = options.msgKey;
|
|
264
|
+
|
|
265
|
+
var containers = el.querySelectorAll('.blex-block-container');
|
|
266
|
+
Array.prototype.forEach.call(containers, function (container) {
|
|
267
|
+
var idx = parseInt(container.getAttribute('data-blex-idx'), 10);
|
|
268
|
+
if (isNaN(idx) || idx >= blocks.length) return;
|
|
269
|
+
var block = blocks[idx];
|
|
270
|
+
if (!adapter.allowType(block.type)) {
|
|
271
|
+
// Deliberately unclaimed — the raw fence stays visible.
|
|
272
|
+
console.warn('[blex] type not rendered on this surface: ' + block.type);
|
|
273
|
+
return;
|
|
274
|
+
}
|
|
275
|
+
var storeKey = msgKey ? msgKey + ':' + idx : null;
|
|
276
|
+
|
|
277
|
+
try {
|
|
278
|
+
var data = JSON.parse(block.json);
|
|
279
|
+
var blockObj = { type: block.type, id: block.type + '-' + idx, data: data };
|
|
280
|
+
|
|
281
|
+
// Claim the container: drop the raw-fence fallback. Bad JSON throws
|
|
282
|
+
// before this line and is reported by the catch below, which names the
|
|
283
|
+
// problem — better than either the fallback or a blank box.
|
|
284
|
+
container.innerHTML = '';
|
|
285
|
+
|
|
286
|
+
if (streaming) {
|
|
287
|
+
global.Blex.renderPlaceholder(block.type, container);
|
|
288
|
+
return;
|
|
289
|
+
}
|
|
290
|
+
|
|
291
|
+
var renderOpts = {};
|
|
292
|
+
var previous = callOptional(adapter, 'readPersisted', [storeKey]);
|
|
293
|
+
if (previous !== undefined) renderOpts.previousValue = previous;
|
|
294
|
+
|
|
295
|
+
global.Blex.renderBlock(blockObj, container, renderOpts)
|
|
296
|
+
.then(function (handle) {
|
|
297
|
+
trackHandle(el, handle);
|
|
298
|
+
callOptional(adapter, 'onRendered', [container, storeKey]);
|
|
299
|
+
if (!handle || !handle.onInteraction) return;
|
|
300
|
+
handle.onInteraction(function (interaction) {
|
|
301
|
+
if (storeKey && interaction.value !== undefined) {
|
|
302
|
+
callOptional(adapter, 'persist', [storeKey, interaction.value]);
|
|
303
|
+
}
|
|
304
|
+
if (!interaction.serialized) return;
|
|
305
|
+
var inputEl = adapter.getInput();
|
|
306
|
+
if (!inputEl) return;
|
|
307
|
+
if (interaction.immediate) {
|
|
308
|
+
var existingText = (inputEl.value || '').trim();
|
|
309
|
+
inputEl.value = existingText
|
|
310
|
+
? existingText + '\n\n' + interaction.serialized
|
|
311
|
+
: interaction.serialized;
|
|
312
|
+
inputEl.dispatchEvent(new Event('input', { bubbles: true }));
|
|
313
|
+
var sendBtn = adapter.getSendButton();
|
|
314
|
+
if (sendBtn) sendBtn.click();
|
|
315
|
+
} else {
|
|
316
|
+
callOptional(adapter, 'addChip', [interaction, handle, block.type + '-' + idx]);
|
|
317
|
+
}
|
|
318
|
+
});
|
|
319
|
+
})
|
|
320
|
+
.catch(function (err) {
|
|
321
|
+
container.innerHTML = errorBox('Blex error: ' + (err.message || err));
|
|
322
|
+
});
|
|
323
|
+
} catch (e) {
|
|
324
|
+
container.innerHTML = errorBox('Invalid blex JSON: ' + e.message);
|
|
325
|
+
}
|
|
326
|
+
});
|
|
327
|
+
}
|
|
328
|
+
|
|
329
|
+
global.CumulusBlexRender = {
|
|
330
|
+
extractBlocks: extractBlocks,
|
|
331
|
+
placeholderHtml: placeholderHtml,
|
|
332
|
+
insertPlaceholders: insertPlaceholders,
|
|
333
|
+
fenceSource: fenceSource,
|
|
334
|
+
render: render,
|
|
335
|
+
destroy: destroy,
|
|
336
|
+
renderOnlyAdapter: renderOnlyAdapter,
|
|
337
|
+
RENDER_ONLY_TYPES: RENDER_ONLY_TYPES,
|
|
338
|
+
INTERACTIVE_TYPES: INTERACTIVE_TYPES,
|
|
339
|
+
CODE_FENCE_SRC: CODE_FENCE_SRC,
|
|
340
|
+
};
|
|
341
|
+
})(typeof window !== 'undefined' ? window : globalThis);
|
|
@@ -24,6 +24,7 @@
|
|
|
24
24
|
</head>
|
|
25
25
|
<body>
|
|
26
26
|
<script src="/blex.min.js" onload="console.log('[blex] blex.min.js loaded, Blex:', typeof Blex)" onerror="console.error('[blex] FAILED to load blex.min.js')"></script>
|
|
27
|
+
<script src="/blex-render.js"></script>
|
|
27
28
|
<script src="/widget.js" data-standalone="true" data-api-key=""></script>
|
|
28
29
|
</body>
|
|
29
30
|
</html>
|
|
@@ -409,6 +409,19 @@
|
|
|
409
409
|
'.blex-block-container[data-blex-ready="true"] {',
|
|
410
410
|
' min-height: auto;',
|
|
411
411
|
'}',
|
|
412
|
+
/* Raw-fence fallback: shown when nothing claims the container (user message,
|
|
413
|
+
changelog, or blex.min.js failed to load). Better a visible fence than the
|
|
414
|
+
blank rectangle min-height used to produce. */
|
|
415
|
+
'.blex-fallback {',
|
|
416
|
+
' margin: 0; padding: 0.6em 0.8em;',
|
|
417
|
+
' background: rgba(255,255,255,0.04);',
|
|
418
|
+
' border: 1px solid rgba(255,255,255,0.08);',
|
|
419
|
+
' border-radius: 8px;',
|
|
420
|
+
' font-family: ui-monospace, SFMono-Regular, Menlo, Consolas, monospace;',
|
|
421
|
+
' font-size: 0.82em; line-height: 1.45;',
|
|
422
|
+
' white-space: pre-wrap; word-break: break-word;',
|
|
423
|
+
' opacity: 0.85;',
|
|
424
|
+
'}',
|
|
412
425
|
/* Blex CSS isolation — revert widget resets inside blex blocks */
|
|
413
426
|
'.blex-block-container * { margin: revert; padding: revert; line-height: revert; list-style: revert; border-spacing: revert; border: revert; }',
|
|
414
427
|
'.blex-block-container { isolation: isolate; }',
|
|
@@ -2037,8 +2050,8 @@
|
|
|
2037
2050
|
|
|
2038
2051
|
// ── Blex Integration ─────────────────────────────────────────────────────
|
|
2039
2052
|
|
|
2040
|
-
//
|
|
2041
|
-
|
|
2053
|
+
// Handle lifecycle (element -> BlockHandle[]) lives in blex-render.js — one
|
|
2054
|
+
// owner, so destroy() cannot miss handles the other side created.
|
|
2042
2055
|
|
|
2043
2056
|
// Per-thread store for blex interaction values (poll answers, confirm clicks, etc.)
|
|
2044
2057
|
// Key: "threadName" → Map<"msgTimestamp:blockIdx", interactionValue>
|
|
@@ -2189,136 +2202,99 @@
|
|
|
2189
2202
|
});
|
|
2190
2203
|
}
|
|
2191
2204
|
|
|
2192
|
-
//
|
|
2193
|
-
//
|
|
2205
|
+
// ── Shared renderer seam (task 131) ────────────────────────────────────────
|
|
2206
|
+
//
|
|
2207
|
+
// Extraction, placeholder markup, block rendering and handle lifecycle live in
|
|
2208
|
+
// /blex-render.js — ONE implementation shared with the web-app-agent kit and
|
|
2209
|
+
// any embedding app. chat.html loads it before this file.
|
|
2210
|
+
//
|
|
2211
|
+
// The EMBEDDED widget (a bare <script src="/widget.js"> on a third-party page)
|
|
2212
|
+
// loads neither blex.min.js nor blex-render.js, and never has — blex has only
|
|
2213
|
+
// ever worked on /chat. There, fences degrade to literal text, which is what
|
|
2214
|
+
// an author who typed one would expect to see.
|
|
2215
|
+
function blexCore() {
|
|
2216
|
+
return typeof CumulusBlexRender !== 'undefined' ? CumulusBlexRender : null;
|
|
2217
|
+
}
|
|
2218
|
+
|
|
2219
|
+
// Fenced-code regex, sourced from the shared module so the blex extractor and
|
|
2220
|
+
// Phase 1 of renderMarkdown cannot disagree about where a code block starts —
|
|
2221
|
+
// a fence falling through that gap is exactly the bug this seam closes.
|
|
2222
|
+
function codeFenceRe() {
|
|
2223
|
+
var core = blexCore();
|
|
2224
|
+
return new RegExp(core ? core.CODE_FENCE_SRC : '```([^\\n`]*)\\n([\\s\\S]*?)```', 'g');
|
|
2225
|
+
}
|
|
2226
|
+
|
|
2194
2227
|
function extractBlexBlocks(text) {
|
|
2195
|
-
var
|
|
2196
|
-
|
|
2197
|
-
var replaced = text.replace(/~~~blex:(\w[\w-]*)\n([\s\S]*?)\n~~~/g, function (_, type, json) {
|
|
2198
|
-
var idx = blocks.length;
|
|
2199
|
-
blocks.push({ type: type, json: json.trim(), idx: idx });
|
|
2200
|
-
return BLEX_PREFIX + idx + '\x00';
|
|
2201
|
-
});
|
|
2202
|
-
return { text: replaced, blocks: blocks };
|
|
2228
|
+
var core = blexCore();
|
|
2229
|
+
return core ? core.extractBlocks(text) : { text: text, blocks: [] };
|
|
2203
2230
|
}
|
|
2204
2231
|
|
|
2205
|
-
//
|
|
2206
|
-
|
|
2207
|
-
|
|
2208
|
-
|
|
2209
|
-
|
|
2210
|
-
|
|
2211
|
-
|
|
2212
|
-
|
|
2213
|
-
// Get the interaction store for this thread
|
|
2232
|
+
// The widget's adapter over the shared renderer. It supplies all five members;
|
|
2233
|
+
// a render-only surface (the web-app-agent kit's panel) supplies the three
|
|
2234
|
+
// required ones and lets addChip/persist default to no-ops.
|
|
2235
|
+
//
|
|
2236
|
+
// `el` is captured because it may be DETACHED by the time an interaction fires
|
|
2237
|
+
// — renderPanelMessages rebuilds the message list — which is why the panel
|
|
2238
|
+
// lookup falls back to a document query.
|
|
2239
|
+
function widgetBlexAdapter(el, threadName) {
|
|
2214
2240
|
var store = threadName
|
|
2215
2241
|
? blexInteractionStore[threadName] || (blexInteractionStore[threadName] = {})
|
|
2216
2242
|
: null;
|
|
2217
2243
|
|
|
2218
|
-
|
|
2219
|
-
|
|
2220
|
-
|
|
2221
|
-
|
|
2222
|
-
|
|
2223
|
-
|
|
2224
|
-
|
|
2225
|
-
|
|
2226
|
-
var blockObj = { type: block.type, id: block.type + '-' + idx, data: data };
|
|
2227
|
-
|
|
2228
|
-
if (isStreaming) {
|
|
2229
|
-
// During streaming, show placeholder skeleton
|
|
2230
|
-
Blex.renderPlaceholder(block.type, container);
|
|
2231
|
-
} else {
|
|
2232
|
-
// Check for previously stored interaction value
|
|
2233
|
-
var renderOpts = {};
|
|
2234
|
-
if (store && storeKey && store[storeKey]) {
|
|
2235
|
-
renderOpts.previousValue = store[storeKey];
|
|
2236
|
-
}
|
|
2244
|
+
function panelFor() {
|
|
2245
|
+
return (
|
|
2246
|
+
el.closest('.cumulus-thread-panel') ||
|
|
2247
|
+
el.closest('.cumulus-panel') ||
|
|
2248
|
+
document.querySelector('.cumulus-thread-panel') ||
|
|
2249
|
+
document.querySelector('.cumulus-panel')
|
|
2250
|
+
);
|
|
2251
|
+
}
|
|
2237
2252
|
|
|
2238
|
-
|
|
2239
|
-
|
|
2240
|
-
|
|
2241
|
-
|
|
2242
|
-
|
|
2243
|
-
|
|
2244
|
-
|
|
2245
|
-
|
|
2246
|
-
|
|
2247
|
-
|
|
2248
|
-
|
|
2249
|
-
|
|
2250
|
-
|
|
2251
|
-
|
|
2252
|
-
|
|
2253
|
-
|
|
2254
|
-
|
|
2255
|
-
|
|
2256
|
-
|
|
2253
|
+
return {
|
|
2254
|
+
// The widget is the interactive surface: every type is allowed here.
|
|
2255
|
+
allowType: function () {
|
|
2256
|
+
return true;
|
|
2257
|
+
},
|
|
2258
|
+
getInput: function () {
|
|
2259
|
+
var panel = panelFor();
|
|
2260
|
+
return panel
|
|
2261
|
+
? panel.querySelector('[data-testid="webchat-input"]')
|
|
2262
|
+
: document.querySelector('[data-testid="webchat-input"]');
|
|
2263
|
+
},
|
|
2264
|
+
getSendButton: function () {
|
|
2265
|
+
var panel = panelFor();
|
|
2266
|
+
return panel ? panel.querySelector('[data-testid*="send"]') : null;
|
|
2267
|
+
},
|
|
2268
|
+
addChip: function (interaction, handle, sourceId) {
|
|
2269
|
+
addInteractionChip(panelFor(), interaction, handle, sourceId);
|
|
2270
|
+
},
|
|
2271
|
+
readPersisted: function (storeKey) {
|
|
2272
|
+
if (!store || !storeKey) return undefined;
|
|
2273
|
+
return store[storeKey] || undefined;
|
|
2274
|
+
},
|
|
2275
|
+
persist: function (storeKey, value) {
|
|
2276
|
+
if (!store || !storeKey) return;
|
|
2277
|
+
store[storeKey] = value;
|
|
2278
|
+
_flushBlexStore();
|
|
2279
|
+
},
|
|
2280
|
+
onRendered: function (container, storeKey) {
|
|
2281
|
+
attachBlexResize(container, threadName, storeKey);
|
|
2282
|
+
},
|
|
2283
|
+
};
|
|
2284
|
+
}
|
|
2257
2285
|
|
|
2258
|
-
|
|
2259
|
-
|
|
2260
|
-
|
|
2261
|
-
|
|
2262
|
-
|
|
2263
|
-
|
|
2264
|
-
document.querySelector('.cumulus-panel');
|
|
2265
|
-
}
|
|
2266
|
-
var inputEl = panel
|
|
2267
|
-
? panel.querySelector('[data-testid="webchat-input"]')
|
|
2268
|
-
: document.querySelector('[data-testid="webchat-input"]');
|
|
2269
|
-
|
|
2270
|
-
if (inputEl && interaction.serialized) {
|
|
2271
|
-
if (interaction.immediate) {
|
|
2272
|
-
// Immediate: prepend any existing input text, then auto-send
|
|
2273
|
-
var existingText = inputEl.value.trim();
|
|
2274
|
-
inputEl.value = existingText
|
|
2275
|
-
? existingText + '\n\n' + interaction.serialized
|
|
2276
|
-
: interaction.serialized;
|
|
2277
|
-
inputEl.dispatchEvent(new Event('input', { bubbles: true }));
|
|
2278
|
-
// Find and click the send button
|
|
2279
|
-
var sendBtn = panel ? panel.querySelector('[data-testid*="send"]') : null;
|
|
2280
|
-
if (sendBtn) {
|
|
2281
|
-
sendBtn.click();
|
|
2282
|
-
}
|
|
2283
|
-
} else {
|
|
2284
|
-
// Deferred: add to chip tray (sourceId deduplicates rapid-fire from same block)
|
|
2285
|
-
addInteractionChip(panel, interaction, handle, block.type + '-' + idx);
|
|
2286
|
-
}
|
|
2287
|
-
}
|
|
2288
|
-
});
|
|
2289
|
-
}
|
|
2290
|
-
})
|
|
2291
|
-
.catch(function (err) {
|
|
2292
|
-
container.innerHTML =
|
|
2293
|
-
'<pre style="color:#f66;font-size:0.85em;padding:0.5em;">Blex error: ' +
|
|
2294
|
-
(err.message || err) +
|
|
2295
|
-
'</pre>';
|
|
2296
|
-
});
|
|
2297
|
-
}
|
|
2298
|
-
} catch (e) {
|
|
2299
|
-
container.innerHTML =
|
|
2300
|
-
'<pre style="color:#f66;font-size:0.85em;padding:0.5em;">Invalid blex JSON: ' +
|
|
2301
|
-
e.message +
|
|
2302
|
-
'</pre>';
|
|
2303
|
-
}
|
|
2286
|
+
function renderBlexBlocks(el, blexBlocks, isStreaming, msgKey, threadName) {
|
|
2287
|
+
var core = blexCore();
|
|
2288
|
+
if (!core) return;
|
|
2289
|
+
core.render(el, blexBlocks, widgetBlexAdapter(el, threadName), {
|
|
2290
|
+
msgKey: msgKey,
|
|
2291
|
+
streaming: isStreaming,
|
|
2304
2292
|
});
|
|
2305
|
-
|
|
2306
|
-
// Note: handles are stored inside the async .then() callback above,
|
|
2307
|
-
// not here — by this point the promises haven't resolved yet.
|
|
2308
2293
|
}
|
|
2309
2294
|
|
|
2310
|
-
// Destroy blex block handles for a message element
|
|
2311
2295
|
function destroyBlexBlocks(el) {
|
|
2312
|
-
|
|
2313
|
-
|
|
2314
|
-
if (handles) {
|
|
2315
|
-
handles.forEach(function (h) {
|
|
2316
|
-
try {
|
|
2317
|
-
h.destroy();
|
|
2318
|
-
} catch (e) {}
|
|
2319
|
-
});
|
|
2320
|
-
blexHandles.delete(el);
|
|
2321
|
-
}
|
|
2296
|
+
var core = blexCore();
|
|
2297
|
+
if (core) core.destroy(el);
|
|
2322
2298
|
}
|
|
2323
2299
|
|
|
2324
2300
|
// Add a deferred interaction chip to the chip tray
|
|
@@ -2681,7 +2657,12 @@
|
|
|
2681
2657
|
return out;
|
|
2682
2658
|
}
|
|
2683
2659
|
|
|
2684
|
-
|
|
2660
|
+
// opts.streaming — suppress the raw-fence fallback body while a turn is still
|
|
2661
|
+
// streaming. A completed-but-not-yet-rendered fence would otherwise flash its
|
|
2662
|
+
// JSON before flipping to the rendered block at `done`.
|
|
2663
|
+
function renderMarkdown(text, opts) {
|
|
2664
|
+
var streaming = !!(opts && opts.streaming);
|
|
2665
|
+
|
|
2685
2666
|
// Phase 0.5: Extract blex fences BEFORE code blocks
|
|
2686
2667
|
var blexResult = extractBlexBlocks(text);
|
|
2687
2668
|
text = blexResult.text;
|
|
@@ -2690,7 +2671,7 @@
|
|
|
2690
2671
|
// Phase 1: Extract fenced code blocks — replace with unique tokens
|
|
2691
2672
|
var codeBlocks = [];
|
|
2692
2673
|
var TOKEN_PREFIX = '\x00CODEBLOCK_';
|
|
2693
|
-
var processed = text.replace(
|
|
2674
|
+
var processed = text.replace(codeFenceRe(), function (_, lang, code) {
|
|
2694
2675
|
var idx = codeBlocks.length;
|
|
2695
2676
|
var langLabel = lang.trim() || 'text';
|
|
2696
2677
|
var escapedCode = escapeHtml(code.replace(/\n$/, '')); // trim trailing newline
|
|
@@ -2767,10 +2748,14 @@
|
|
|
2767
2748
|
return codeBlocks[parseInt(idx, 10)];
|
|
2768
2749
|
});
|
|
2769
2750
|
|
|
2770
|
-
// Phase 7.5: Reinsert blex block placeholders
|
|
2771
|
-
|
|
2772
|
-
|
|
2773
|
-
|
|
2751
|
+
// Phase 7.5: Reinsert blex block placeholders — shared markup, so the widget
|
|
2752
|
+
// and any embedding app produce byte-identical containers and one renderer
|
|
2753
|
+
// can claim either. See blex-render.js for why the placeholder carries the
|
|
2754
|
+
// raw fence as its body.
|
|
2755
|
+
var core = blexCore();
|
|
2756
|
+
if (core) {
|
|
2757
|
+
html = core.insertPlaceholders(html, blexBlocks, { streaming: streaming });
|
|
2758
|
+
}
|
|
2774
2759
|
|
|
2775
2760
|
return { html: html, blexBlocks: blexBlocks };
|
|
2776
2761
|
}
|
|
@@ -3760,7 +3745,7 @@
|
|
|
3760
3745
|
el.className = 'cumulus-msg assistant';
|
|
3761
3746
|
if (isStreaming) el.setAttribute('data-testid', 'webchat-streaming');
|
|
3762
3747
|
if (content) {
|
|
3763
|
-
var mdResult = renderMarkdown(content);
|
|
3748
|
+
var mdResult = renderMarkdown(content, { streaming: isStreaming });
|
|
3764
3749
|
el.innerHTML = mdResult.html;
|
|
3765
3750
|
if (isStreaming) {
|
|
3766
3751
|
el.innerHTML += '<span class="cumulus-cursor"></span>';
|
|
@@ -7461,7 +7446,7 @@
|
|
|
7461
7446
|
el.className = 'cumulus-msg assistant';
|
|
7462
7447
|
if (isStreaming) el.setAttribute('data-testid', 'webchat-streaming');
|
|
7463
7448
|
if (content) {
|
|
7464
|
-
var mdResult = renderMarkdown(content);
|
|
7449
|
+
var mdResult = renderMarkdown(content, { streaming: isStreaming });
|
|
7465
7450
|
el.innerHTML = mdResult.html;
|
|
7466
7451
|
if (isStreaming) {
|
|
7467
7452
|
el.innerHTML += '<span class="cumulus-cursor"></span>';
|