@aparte/core 0.7.1 → 0.9.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 +20 -6
- package/dist/client/aparte-client.d.ts +113 -47
- package/dist/client/aparte-client.d.ts.map +1 -1
- package/dist/client/lifecycle-events.d.ts +55 -0
- package/dist/client/lifecycle-events.d.ts.map +1 -0
- package/dist/client/stream-adapter.d.ts +11 -5
- package/dist/client/stream-adapter.d.ts.map +1 -1
- package/dist/client/xml-artifact-feed.d.ts +76 -0
- package/dist/client/xml-artifact-feed.d.ts.map +1 -0
- package/dist/components/bubble/aparte-chat-bubble.d.ts +35 -8
- package/dist/components/bubble/aparte-chat-bubble.d.ts.map +1 -1
- package/dist/components/bubble/bubble-sync.d.ts.map +1 -1
- package/dist/components/chat/aparte-chat.d.ts +0 -6
- package/dist/components/chat/aparte-chat.d.ts.map +1 -1
- package/dist/components/composer/aparte-composer-action.d.ts +18 -3
- package/dist/components/composer/aparte-composer-action.d.ts.map +1 -1
- package/dist/components/composer/aparte-composer-add-attachment.d.ts.map +1 -1
- package/dist/components/composer/aparte-composer-attachments.d.ts +1 -1
- package/dist/components/composer/aparte-composer-attachments.d.ts.map +1 -1
- package/dist/components/composer/aparte-composer-cancel.d.ts.map +1 -1
- package/dist/components/composer/aparte-composer-input.d.ts +0 -2
- package/dist/components/composer/aparte-composer-input.d.ts.map +1 -1
- package/dist/components/composer/aparte-composer-send.d.ts +5 -0
- package/dist/components/composer/aparte-composer-send.d.ts.map +1 -1
- package/dist/components/composer/aparte-composer-toolbar.d.ts +4 -0
- package/dist/components/composer/aparte-composer-toolbar.d.ts.map +1 -1
- package/dist/components/composer/aparte-composer.d.ts +64 -6
- package/dist/components/composer/aparte-composer.d.ts.map +1 -1
- package/dist/components/composer/index.d.ts +1 -1
- package/dist/components/composer/index.d.ts.map +1 -1
- package/dist/components/conversation-list/aparte-conversation-list.d.ts +2 -5
- package/dist/components/conversation-list/aparte-conversation-list.d.ts.map +1 -1
- package/dist/components/conversation-list/index.d.ts +1 -1
- package/dist/components/conversation-list/index.d.ts.map +1 -1
- package/dist/components/elicitation/aparte-elicitation.d.ts +63 -3
- package/dist/components/elicitation/aparte-elicitation.d.ts.map +1 -1
- package/dist/components/index.d.ts +2 -2
- package/dist/components/index.d.ts.map +1 -1
- package/dist/components/status/aparte-chat-status.d.ts +8 -6
- package/dist/components/status/aparte-chat-status.d.ts.map +1 -1
- package/dist/components/viewport/aparte-chat-viewport.d.ts +45 -9
- package/dist/components/viewport/aparte-chat-viewport.d.ts.map +1 -1
- package/dist/config/action-provider.d.ts +4 -4
- package/dist/config/aparte-config.d.ts +124 -41
- package/dist/config/aparte-config.d.ts.map +1 -1
- package/dist/config/attachment-renderer.d.ts +1 -1
- package/dist/config/avatar-provider.d.ts +1 -1
- package/dist/config/config-context.d.ts +66 -11
- package/dist/config/config-context.d.ts.map +1 -1
- package/dist/config/elicitation-field-renderer.d.ts +73 -0
- package/dist/config/elicitation-field-renderer.d.ts.map +1 -0
- package/dist/config/error-renderer.d.ts +1 -1
- package/dist/config/icon-provider.d.ts +2 -2
- package/dist/config/icon-provider.d.ts.map +1 -1
- package/dist/config/index.d.ts +7 -5
- package/dist/config/index.d.ts.map +1 -1
- package/dist/config/locale.d.ts +43 -2
- package/dist/config/locale.d.ts.map +1 -1
- package/dist/config/sanitize.d.ts +1 -7
- package/dist/config/sanitize.d.ts.map +1 -1
- package/dist/config/sibling-nav-renderer.d.ts +1 -1
- package/dist/config/skeleton-provider.d.ts +2 -2
- package/dist/config/skeleton-provider.d.ts.map +1 -1
- package/dist/config/status-renderer.d.ts +1 -1
- package/dist/conversations/conversation-controller.d.ts +19 -5
- package/dist/conversations/conversation-controller.d.ts.map +1 -1
- package/dist/conversations/conversation-manager.d.ts +2 -2
- package/dist/conversations/conversation-manager.d.ts.map +1 -1
- package/dist/conversations/index.d.ts +1 -1
- package/dist/conversations/index.d.ts.map +1 -1
- package/dist/conversations/types.d.ts +1 -1
- package/dist/custom-elements.json +8418 -6295
- package/dist/elicitation/index.d.ts +1 -1
- package/dist/elicitation/panel.d.ts +32 -0
- package/dist/elicitation/panel.d.ts.map +1 -1
- package/dist/elicitation/types.d.ts +29 -2
- package/dist/elicitation/types.d.ts.map +1 -1
- package/dist/host/aparte-chat-host.d.ts +3 -3
- package/dist/host/aparte-chat-host.d.ts.map +1 -1
- package/dist/{index-Dn7TH14G.js → index-hQQljwfX.js} +1890 -1222
- package/dist/index-hQQljwfX.js.map +1 -0
- package/dist/index.css +299 -21
- package/dist/index.d.ts +22 -13
- package/dist/index.d.ts.map +1 -1
- package/dist/index.js +485 -210
- package/dist/index.js.map +1 -1
- package/dist/index.node.d.ts +23 -13
- package/dist/index.node.d.ts.map +1 -1
- package/dist/index.node.js +56 -46
- package/dist/index.node.js.map +1 -1
- package/dist/interop/element-props.d.ts +1 -1
- package/dist/interop/element-props.d.ts.map +1 -1
- package/dist/parsers/aparte-stream-parser.d.ts +25 -0
- package/dist/parsers/aparte-stream-parser.d.ts.map +1 -1
- package/dist/primitives/index.d.ts +1 -1
- package/dist/primitives/index.d.ts.map +1 -1
- package/dist/primitives/select/aparte-optgroup.d.ts +15 -4
- package/dist/primitives/select/aparte-optgroup.d.ts.map +1 -1
- package/dist/primitives/select/aparte-option.d.ts +0 -5
- package/dist/primitives/select/aparte-option.d.ts.map +1 -1
- package/dist/primitives/select/aparte-select.d.ts +17 -5
- package/dist/primitives/select/aparte-select.d.ts.map +1 -1
- package/dist/primitives/select/index.d.ts +1 -1
- package/dist/primitives/select/index.d.ts.map +1 -1
- package/dist/renderers/markdown-stream.d.ts +42 -0
- package/dist/renderers/markdown-stream.d.ts.map +1 -0
- package/dist/renderers/segment-renderers.d.ts +20 -14
- package/dist/renderers/segment-renderers.d.ts.map +1 -1
- package/dist/renderers/segments/artifact/binary-file.d.ts +45 -0
- package/dist/renderers/segments/artifact/binary-file.d.ts.map +1 -0
- package/dist/renderers/segments/artifact/card.d.ts +3 -0
- package/dist/renderers/segments/artifact/card.d.ts.map +1 -0
- package/dist/renderers/segments/artifact/preview-document.d.ts +27 -0
- package/dist/renderers/segments/artifact/preview-document.d.ts.map +1 -0
- package/dist/renderers/segments/artifact/shared.d.ts +12 -0
- package/dist/renderers/segments/artifact/shared.d.ts.map +1 -0
- package/dist/renderers/segments/code.d.ts +3 -0
- package/dist/renderers/segments/code.d.ts.map +1 -0
- package/dist/renderers/segments/error.d.ts +3 -0
- package/dist/renderers/segments/error.d.ts.map +1 -0
- package/dist/renderers/segments/file-tree.d.ts +3 -0
- package/dist/renderers/segments/file-tree.d.ts.map +1 -0
- package/dist/renderers/segments/pipeline-waiting.d.ts +3 -0
- package/dist/renderers/segments/pipeline-waiting.d.ts.map +1 -0
- package/dist/renderers/segments/progress.d.ts +3 -0
- package/dist/renderers/segments/progress.d.ts.map +1 -0
- package/dist/renderers/segments/terminal.d.ts +3 -0
- package/dist/renderers/segments/terminal.d.ts.map +1 -0
- package/dist/renderers/segments/text.d.ts +10 -0
- package/dist/renderers/segments/text.d.ts.map +1 -0
- package/dist/renderers/segments/thinking.d.ts +3 -0
- package/dist/renderers/segments/thinking.d.ts.map +1 -0
- package/dist/renderers/segments/tool-call.d.ts +3 -0
- package/dist/renderers/segments/tool-call.d.ts.map +1 -0
- package/dist/runtime/message-repository.d.ts +3 -3
- package/dist/runtime/message-repository.d.ts.map +1 -1
- package/dist/transport/backend-handler.d.ts +19 -7
- package/dist/transport/backend-handler.d.ts.map +1 -1
- package/dist/transport/backend-transport.d.ts +1 -1
- package/dist/transport/backend-transport.d.ts.map +1 -1
- package/dist/transport/direct-transport.d.ts +4 -4
- package/dist/transport/direct-transport.d.ts.map +1 -1
- package/dist/transport/index.d.ts +2 -2
- package/dist/transport/index.d.ts.map +1 -1
- package/dist/transport/types.d.ts +3 -3
- package/dist/types/element-map.d.ts +69 -0
- package/dist/types/element-map.d.ts.map +1 -0
- package/dist/types/event-map.d.ts +84 -23
- package/dist/types/event-map.d.ts.map +1 -1
- package/dist/types/events.d.ts +212 -64
- package/dist/types/events.d.ts.map +1 -1
- package/dist/types/index.d.ts +4 -5
- package/dist/types/index.d.ts.map +1 -1
- package/dist/types/model-provider.d.ts +100 -54
- package/dist/types/model-provider.d.ts.map +1 -1
- package/dist/types/models.d.ts +29 -7
- package/dist/types/models.d.ts.map +1 -1
- package/dist/types/segments.d.ts +33 -8
- package/dist/types/segments.d.ts.map +1 -1
- package/dist/types/theming.d.ts +25 -37
- package/dist/types/theming.d.ts.map +1 -1
- package/dist/types/tools.d.ts +60 -15
- package/dist/types/tools.d.ts.map +1 -1
- package/dist/utils/escape.d.ts +46 -0
- package/dist/utils/escape.d.ts.map +1 -0
- package/dist/utils/files-to-attachments.d.ts +18 -1
- package/dist/utils/files-to-attachments.d.ts.map +1 -1
- package/dist/utils/segments.d.ts +174 -0
- package/dist/utils/segments.d.ts.map +1 -0
- package/dist/utils/uuid.d.ts +26 -0
- package/dist/utils/uuid.d.ts.map +1 -0
- package/package.json +3 -5
- package/dist/index-Dn7TH14G.js.map +0 -1
- package/dist/types/providers.d.ts +0 -93
- package/dist/types/providers.d.ts.map +0 -1
|
@@ -35,7 +35,7 @@ class AparteError extends Error {
|
|
|
35
35
|
return new AparteError(message, defaultCode, void 0, error, status);
|
|
36
36
|
}
|
|
37
37
|
}
|
|
38
|
-
const
|
|
38
|
+
const APARTE_DEFAULT_ICON_FALLBACKS = {
|
|
39
39
|
copy: `<svg width="14" height="14" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><rect x="9" y="9" width="13" height="13" rx="2"/><path d="M5 15H4a2 2 0 0 1-2-2V4a2 2 0 0 1 2-2h9a2 2 0 0 1 2 2v1"/></svg>`,
|
|
40
40
|
check: `<svg width="14" height="14" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><polyline points="20 6 9 17 4 12"/></svg>`,
|
|
41
41
|
send: `<svg width="20" height="20" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><line x1="22" y1="2" x2="11" y2="13"/><polygon points="22 2 15 22 11 13 2 9 22 2"/></svg>`,
|
|
@@ -58,13 +58,22 @@ const DEFAULT_ICON_FALLBACKS = {
|
|
|
58
58
|
close: `<svg width="14" height="14" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><line x1="18" y1="6" x2="6" y2="18"/><line x1="6" y1="6" x2="18" y2="18"/></svg>`,
|
|
59
59
|
stop: `<svg width="16" height="16" viewBox="0 0 16 16" fill="currentColor"><rect x="2" y="2" width="12" height="12" rx="2"/></svg>`
|
|
60
60
|
};
|
|
61
|
-
const
|
|
61
|
+
const APARTE_DEFAULT_LOCALE = {
|
|
62
62
|
inputPlaceholder: "Type a message...",
|
|
63
63
|
sendButton: "Send",
|
|
64
64
|
copy: "Copy",
|
|
65
65
|
copied: "Copied!",
|
|
66
66
|
retry: "Retry",
|
|
67
67
|
thinking: "Thinking...",
|
|
68
|
+
elicitationOther: "Other…",
|
|
69
|
+
elicitationOtherPlaceholder: "Type your answer…",
|
|
70
|
+
elicitationOtherLabel: "Custom answer",
|
|
71
|
+
submitButton: "Submit",
|
|
72
|
+
elicitationNext: "Next",
|
|
73
|
+
elicitationSkip: "Skip",
|
|
74
|
+
elicitationYes: "Yes",
|
|
75
|
+
elicitationNo: "No",
|
|
76
|
+
elicitationAnswerLabel: "Your answer",
|
|
68
77
|
typing: "Typing...",
|
|
69
78
|
error: "Error",
|
|
70
79
|
running: "Running...",
|
|
@@ -114,7 +123,7 @@ async function parseNonStreamText(provider, response, providerId) {
|
|
|
114
123
|
}
|
|
115
124
|
return provider.parseText(json);
|
|
116
125
|
}
|
|
117
|
-
class
|
|
126
|
+
class AparteDirectTransport {
|
|
118
127
|
byok;
|
|
119
128
|
warnedBrowserKey = false;
|
|
120
129
|
constructor(options = {}) {
|
|
@@ -161,10 +170,16 @@ class DirectTransport {
|
|
|
161
170
|
if (this.warnedBrowserKey) return;
|
|
162
171
|
this.warnedBrowserKey = true;
|
|
163
172
|
console.warn(
|
|
164
|
-
`[Aparte]
|
|
173
|
+
`[Aparte] AparteDirectTransport is sending the "${providerId}" API key straight from the browser — it is visible to anyone who opens devtools. Fine for BYOK or local models: pass new AparteDirectTransport({ byok: true }) to silence this. For a server-held key, use AparteBackendTransport so the key never reaches the client.`
|
|
165
174
|
);
|
|
166
175
|
}
|
|
167
176
|
}
|
|
177
|
+
function uuid() {
|
|
178
|
+
if (typeof crypto !== "undefined" && typeof crypto.randomUUID === "function") {
|
|
179
|
+
return crypto.randomUUID();
|
|
180
|
+
}
|
|
181
|
+
return `${Date.now().toString(36)}-${Math.random().toString(36).slice(2, 10)}`;
|
|
182
|
+
}
|
|
168
183
|
const DEFAULT_THINKING_DELIMITERS = [
|
|
169
184
|
{ start: "<think>", end: "</think>" },
|
|
170
185
|
{ start: "<thinking>", end: "</thinking>" }
|
|
@@ -222,11 +237,11 @@ class AparteStreamParser {
|
|
|
222
237
|
if (seg.type === "text") content = stripTrailingFence(content);
|
|
223
238
|
seg.content = content;
|
|
224
239
|
}
|
|
225
|
-
finalSegments.push(this._state.activeSegment);
|
|
240
|
+
finalSegments.push(this._closed(this._state.activeSegment));
|
|
226
241
|
this._state.activeSegment = null;
|
|
227
242
|
this._state.buffer = "";
|
|
228
243
|
} else if (this._state.buffer.trim()) {
|
|
229
|
-
finalSegments.push(this._createTextSegment(stripTrailingFence(this._state.buffer)));
|
|
244
|
+
finalSegments.push(this._closed(this._createTextSegment(stripTrailingFence(this._state.buffer))));
|
|
230
245
|
this._state.buffer = "";
|
|
231
246
|
}
|
|
232
247
|
return finalSegments;
|
|
@@ -296,15 +311,23 @@ class AparteStreamParser {
|
|
|
296
311
|
}
|
|
297
312
|
let segmentToEmit = null;
|
|
298
313
|
if (this._state.activeSegment && this._state.activeSegment.type === "text") {
|
|
299
|
-
segmentToEmit = this._state.activeSegment;
|
|
314
|
+
segmentToEmit = this._closed(this._state.activeSegment);
|
|
300
315
|
this._state.activeSegment = null;
|
|
301
316
|
}
|
|
302
317
|
if (firstPattern.type === "code") {
|
|
303
318
|
const res = this._startCodeBlock(buffer);
|
|
304
|
-
|
|
319
|
+
if (!res) {
|
|
320
|
+
if (segmentToEmit) this._state.activeSegment = segmentToEmit;
|
|
321
|
+
return null;
|
|
322
|
+
}
|
|
323
|
+
return { segment: segmentToEmit, remaining: res.remaining };
|
|
305
324
|
} else if (firstPattern.type === "thinking") {
|
|
306
325
|
const res = this._startThinkingBlock(buffer);
|
|
307
|
-
|
|
326
|
+
if (!res) {
|
|
327
|
+
if (segmentToEmit) this._state.activeSegment = segmentToEmit;
|
|
328
|
+
return null;
|
|
329
|
+
}
|
|
330
|
+
return { segment: segmentToEmit, remaining: res.remaining };
|
|
308
331
|
} else {
|
|
309
332
|
const res = this._startArtifactBlock(buffer);
|
|
310
333
|
if (!res) {
|
|
@@ -337,7 +360,7 @@ class AparteStreamParser {
|
|
|
337
360
|
if (this._state.activeSegment && "content" in this._state.activeSegment) {
|
|
338
361
|
this._state.activeSegment.content += codeContent2;
|
|
339
362
|
}
|
|
340
|
-
const segment2 = this._state.activeSegment;
|
|
363
|
+
const segment2 = this._closed(this._state.activeSegment);
|
|
341
364
|
this._state.mode = "text";
|
|
342
365
|
this._state.activeSegment = null;
|
|
343
366
|
return { segment: segment2, remaining: "" };
|
|
@@ -355,7 +378,7 @@ class AparteStreamParser {
|
|
|
355
378
|
if (this._state.activeSegment && "content" in this._state.activeSegment) {
|
|
356
379
|
this._state.activeSegment.content += codeContent;
|
|
357
380
|
}
|
|
358
|
-
const segment = this._state.activeSegment;
|
|
381
|
+
const segment = this._closed(this._state.activeSegment);
|
|
359
382
|
this._state.mode = "text";
|
|
360
383
|
this._state.activeSegment = null;
|
|
361
384
|
const afterClose = buffer.slice(closeIndex + 4);
|
|
@@ -397,7 +420,7 @@ class AparteStreamParser {
|
|
|
397
420
|
if (this._state.activeSegment && "content" in this._state.activeSegment) {
|
|
398
421
|
this._state.activeSegment.content += thinkingContent;
|
|
399
422
|
}
|
|
400
|
-
const segment = this._state.activeSegment;
|
|
423
|
+
const segment = this._closed(this._state.activeSegment);
|
|
401
424
|
this._state.mode = "text";
|
|
402
425
|
this._state.activeSegment = null;
|
|
403
426
|
this._state.thinkingEnd = void 0;
|
|
@@ -420,9 +443,10 @@ class AparteStreamParser {
|
|
|
420
443
|
if (tagEnd === -1) return null;
|
|
421
444
|
const tag = buffer.slice(0, tagEnd + 1);
|
|
422
445
|
const inner = tag.slice("<artifact".length, -1);
|
|
446
|
+
const mimeMatch = inner.match(/\bmimeType\s*=\s*(?:"([^"]*)"|'([^']*)')/i);
|
|
423
447
|
const typeMatch = inner.match(/\btype\s*=\s*(?:"([^"]*)"|'([^']*)')/i);
|
|
424
448
|
const titleMatch = inner.match(/\btitle\s*=\s*(?:"([^"]*)"|'([^']*)')/i);
|
|
425
|
-
const mimeType = (typeMatch && (typeMatch[1] ?? typeMatch[2])) ?? "text/plain";
|
|
449
|
+
const mimeType = (mimeMatch && (mimeMatch[1] ?? mimeMatch[2])) ?? (typeMatch && (typeMatch[1] ?? typeMatch[2])) ?? "text/plain";
|
|
426
450
|
const title = titleMatch ? titleMatch[1] ?? titleMatch[2] : void 0;
|
|
427
451
|
this._state.mode = "artifact";
|
|
428
452
|
this._state.activeSegment = this._createArtifactSegment(mimeType, title);
|
|
@@ -458,7 +482,7 @@ class AparteStreamParser {
|
|
|
458
482
|
if (this._state.activeSegment && "content" in this._state.activeSegment) {
|
|
459
483
|
this._state.activeSegment.content += tail;
|
|
460
484
|
}
|
|
461
|
-
const segment = this._state.activeSegment;
|
|
485
|
+
const segment = this._closed(this._state.activeSegment);
|
|
462
486
|
this._state.mode = "text";
|
|
463
487
|
this._state.activeSegment = null;
|
|
464
488
|
return { segment, remaining: buffer.slice(closeIndex + endDelim.length) };
|
|
@@ -477,7 +501,35 @@ class AparteStreamParser {
|
|
|
477
501
|
}
|
|
478
502
|
_generateId() {
|
|
479
503
|
++this._state.segmentCounter;
|
|
480
|
-
return `${this._options.idPrefix}-${
|
|
504
|
+
return `${this._options.idPrefix}-${uuid()}`;
|
|
505
|
+
}
|
|
506
|
+
/**
|
|
507
|
+
* Mark a segment the parser has just CLOSED.
|
|
508
|
+
*
|
|
509
|
+
* The end of a delimited segment is not a guess: the closing token IS the end.
|
|
510
|
+
* `</think>`, a closing fence, `</artifact>` — and, for a text run, the opening
|
|
511
|
+
* of whatever comes next. The parser computes that moment at six sites and used
|
|
512
|
+
* to drop it, emitting the finished segment with no flag saying so.
|
|
513
|
+
*
|
|
514
|
+
* What that cost: nothing downstream could tell a finished reasoning block from
|
|
515
|
+
* one still streaming, so the only remaining signal was the END OF THE TURN. A
|
|
516
|
+
* reader watched "Thinking" for as long as the answer took to stream — twenty
|
|
517
|
+
* seconds after the model had stopped thinking — and the Markdown flush and the
|
|
518
|
+
* highlight-on-settle waited exactly as long. Two workarounds were written
|
|
519
|
+
* before this one: close at the end of the message (wrong value), and close when
|
|
520
|
+
* the next segment opens (right value, wrong moment, and no help for a reply
|
|
521
|
+
* that is reasoning and nothing else).
|
|
522
|
+
*
|
|
523
|
+
* Both agent loops share this parser, so saying it here says it once.
|
|
524
|
+
*
|
|
525
|
+
* Not every end is in band, and the exceptions are honest: a text run that ends
|
|
526
|
+
* only because the STREAM ended has no delimiter — `finalize()` is its truth —
|
|
527
|
+
* and a `tool_call` never passes through here at all, since its end is its
|
|
528
|
+
* `status`.
|
|
529
|
+
*/
|
|
530
|
+
_closed(segment) {
|
|
531
|
+
if (segment) segment.isStreaming = false;
|
|
532
|
+
return segment;
|
|
481
533
|
}
|
|
482
534
|
_createTextSegment(content) {
|
|
483
535
|
return {
|
|
@@ -597,7 +649,7 @@ function parseAparteEventStream(stream) {
|
|
|
597
649
|
}
|
|
598
650
|
});
|
|
599
651
|
}
|
|
600
|
-
class
|
|
652
|
+
class AparteBackendTransport {
|
|
601
653
|
constructor(options) {
|
|
602
654
|
this.options = options;
|
|
603
655
|
}
|
|
@@ -626,11 +678,9 @@ class BackendTransport {
|
|
|
626
678
|
function createAparteChatHandler(options) {
|
|
627
679
|
const doFetch = options.fetchImpl ?? fetch;
|
|
628
680
|
return async function handler(req) {
|
|
629
|
-
|
|
630
|
-
|
|
631
|
-
|
|
632
|
-
if (!verdict) return jsonError(401, "Unauthorized.");
|
|
633
|
-
}
|
|
681
|
+
const verdict = await options.authorize(req);
|
|
682
|
+
if (verdict instanceof Response) return verdict;
|
|
683
|
+
if (!verdict) return jsonError(401, "Unauthorized.");
|
|
634
684
|
let providerId;
|
|
635
685
|
let request;
|
|
636
686
|
try {
|
|
@@ -675,11 +725,25 @@ function createAparteChatHandler(options) {
|
|
|
675
725
|
return jsonError(502, `Vendor request failed: ${err?.message ?? "network error"}`);
|
|
676
726
|
}
|
|
677
727
|
if (!vendor.ok) {
|
|
678
|
-
const
|
|
679
|
-
|
|
680
|
-
|
|
681
|
-
|
|
682
|
-
|
|
728
|
+
const raw = await vendor.text().catch(() => "");
|
|
729
|
+
let code;
|
|
730
|
+
let type;
|
|
731
|
+
try {
|
|
732
|
+
const parsed = JSON.parse(raw);
|
|
733
|
+
code = parsed?.error?.code;
|
|
734
|
+
type = parsed?.error?.type;
|
|
735
|
+
} catch {
|
|
736
|
+
}
|
|
737
|
+
return new Response(
|
|
738
|
+
JSON.stringify({
|
|
739
|
+
error: {
|
|
740
|
+
message: `Vendor request failed (HTTP ${vendor.status}).`,
|
|
741
|
+
...typeof code === "string" ? { code } : {},
|
|
742
|
+
...typeof type === "string" ? { type } : {}
|
|
743
|
+
}
|
|
744
|
+
}),
|
|
745
|
+
{ status: vendor.status, headers: { "Content-Type": "application/json" } }
|
|
746
|
+
);
|
|
683
747
|
}
|
|
684
748
|
if (request.stream === false) {
|
|
685
749
|
const json = await vendor.json().catch(() => ({}));
|
|
@@ -843,8 +907,15 @@ const TAG_ATTRS = {
|
|
|
843
907
|
const URL_ATTRS = /* @__PURE__ */ new Set(["href", "src"]);
|
|
844
908
|
const SAFE_URL = /^(?:https?:|mailto:|tel:|ftp:|sms:)/i;
|
|
845
909
|
const RELATIVE_URL = /^(?:[#/.?]|[a-z0-9._~%+-]+(?:[/?#]|$))/i;
|
|
846
|
-
const SAFE_DATA_IMG = /^data:image\/(?:png|jpe?g|gif|webp|avif|bmp|x-icon|svg\+xml)
|
|
910
|
+
const SAFE_DATA_IMG = /^data:image\/(?:png|jpe?g|gif|webp|avif|bmp|x-icon|svg\+xml)[;,]/i;
|
|
847
911
|
const CONTROL_WS = /[\u0000-\u0020]+/g;
|
|
912
|
+
function isSafeSrcset(value, tag) {
|
|
913
|
+
const urlTag = tag === "source" ? "img" : tag;
|
|
914
|
+
for (const m of value.matchAll(/[a-z][a-z0-9+.-]*:[^\s,]*/gi)) {
|
|
915
|
+
if (!isSafeUrl(m[0], urlTag)) return false;
|
|
916
|
+
}
|
|
917
|
+
return true;
|
|
918
|
+
}
|
|
848
919
|
function isSafeUrl(value, tag) {
|
|
849
920
|
const v = value.replace(CONTROL_WS, "");
|
|
850
921
|
if (!v) return true;
|
|
@@ -877,6 +948,7 @@ function scrubStyle(value) {
|
|
|
877
948
|
const prop = trimmed.slice(0, idx).trim().toLowerCase();
|
|
878
949
|
const val = trimmed.slice(idx + 1);
|
|
879
950
|
if (!val.trim() || !SAFE_STYLE_PROPS.has(prop)) continue;
|
|
951
|
+
if (val.includes("\\")) continue;
|
|
880
952
|
if (/url\s*\(|expression\s*\(|javascript:|vbscript:|[<>]/i.test(val)) continue;
|
|
881
953
|
kept.push(trimmed);
|
|
882
954
|
}
|
|
@@ -890,7 +962,7 @@ function copyAttributes(src, dest, tag) {
|
|
|
890
962
|
if (name.startsWith("on") || name.startsWith("formaction")) continue;
|
|
891
963
|
if (!name.startsWith("data-") && !GLOBAL_ATTRS.has(name) && !(extra && extra.has(name))) continue;
|
|
892
964
|
if (URL_ATTRS.has(name) && !isSafeUrl(value, tag)) continue;
|
|
893
|
-
if (name === "srcset" &&
|
|
965
|
+
if (name === "srcset" && !isSafeSrcset(value, tag)) continue;
|
|
894
966
|
if (name === "style") {
|
|
895
967
|
const scrubbed = scrubStyle(value);
|
|
896
968
|
if (scrubbed === null) continue;
|
|
@@ -939,19 +1011,34 @@ const defaultSanitizer = (html) => {
|
|
|
939
1011
|
sanitizeChildren(doc.body, container, doc);
|
|
940
1012
|
return container.innerHTML;
|
|
941
1013
|
};
|
|
942
|
-
|
|
1014
|
+
function escapeHtml(value) {
|
|
1015
|
+
let out = "";
|
|
1016
|
+
for (let i = 0; i < value.length; i++) {
|
|
1017
|
+
const ch = value[i];
|
|
1018
|
+
if (ch === "&") out += "&";
|
|
1019
|
+
else if (ch === "<") out += "<";
|
|
1020
|
+
else if (ch === ">") out += ">";
|
|
1021
|
+
else if (ch === '"') out += """;
|
|
1022
|
+
else if (ch === "'") out += "'";
|
|
1023
|
+
else out += ch;
|
|
1024
|
+
}
|
|
1025
|
+
return out;
|
|
1026
|
+
}
|
|
1027
|
+
const escapeAttr = escapeHtml;
|
|
1028
|
+
const APARTE_DEFAULT_BUBBLE_ACTIONS = {
|
|
943
1029
|
copy: true,
|
|
944
1030
|
retry: false,
|
|
945
1031
|
edit: false,
|
|
946
1032
|
feedback: false,
|
|
947
1033
|
info: false
|
|
948
1034
|
};
|
|
949
|
-
const
|
|
1035
|
+
const APARTE_DEFAULT_HOST_HANDLERS = {
|
|
950
1036
|
attachmentPreview: false,
|
|
951
1037
|
terminalRun: false,
|
|
952
|
-
artifactRedownload: false
|
|
1038
|
+
artifactRedownload: false,
|
|
1039
|
+
artifactRehydrate: false
|
|
953
1040
|
};
|
|
954
|
-
class
|
|
1041
|
+
class AparteConfig {
|
|
955
1042
|
_markdownProvider;
|
|
956
1043
|
_streamingMarkdownProvider;
|
|
957
1044
|
_highlightProvider;
|
|
@@ -971,30 +1058,50 @@ class AparteConfigClass {
|
|
|
971
1058
|
_avatarProvider;
|
|
972
1059
|
_keyProvider;
|
|
973
1060
|
_artifactPreviewBuilder;
|
|
974
|
-
_locale =
|
|
1061
|
+
_locale = APARTE_DEFAULT_LOCALE;
|
|
975
1062
|
_actions = [];
|
|
976
1063
|
_listeners = /* @__PURE__ */ new Set();
|
|
977
1064
|
// AI Provider Management (BYORK)
|
|
978
1065
|
_aiProviders = /* @__PURE__ */ new Map();
|
|
1066
|
+
/**
|
|
1067
|
+
* What `refreshProviderModels()` last brought back, per provider.
|
|
1068
|
+
*
|
|
1069
|
+
* Without it, `getCurrentModel()` read `provider.getModels()` only — the
|
|
1070
|
+
* SYNCHRONOUS, hand-declared list, which every preset of
|
|
1071
|
+
* `@aparte/provider-openai-compat` leaves empty because a compat endpoint's
|
|
1072
|
+
* list is fetched at runtime. So the current model resolved to `undefined`
|
|
1073
|
+
* for the documented primary path, and everything that reads a capability off
|
|
1074
|
+
* it silently got nothing: `AparteClient._toolsForCurrentModel()` gates tools
|
|
1075
|
+
* on `capabilities.includes('function_calling')`, so NO tool was ever sent to
|
|
1076
|
+
* the model. A registered tool, an approval gate, `<aparte-elicitation>` — all
|
|
1077
|
+
* dead, with the model answering, correctly, that it had been given no tools.
|
|
1078
|
+
*/
|
|
1079
|
+
_fetchedModels = /* @__PURE__ */ new Map();
|
|
979
1080
|
_modelConfig = {};
|
|
980
1081
|
/** Opt-in: gate the composer (block send + grey out) until a model is selected. */
|
|
981
1082
|
_requireModelSelection = false;
|
|
982
|
-
|
|
983
|
-
|
|
1083
|
+
/** Host policy for the elicitation panel's free-text escape — see {@link setElicitationOptions}. */
|
|
1084
|
+
_elicitationAllowOther = true;
|
|
1085
|
+
_elicitationLayout = "stepped";
|
|
1086
|
+
_elicitationFieldRenderer;
|
|
1087
|
+
// Transport: where chat requests go + how auth is handled (AparteDirectTransport = browser-direct).
|
|
1088
|
+
_transport = new AparteDirectTransport();
|
|
984
1089
|
_modelPreferenceProvider;
|
|
985
1090
|
// Conversation persistence (optional, agnostic)
|
|
986
1091
|
_conversationManager;
|
|
987
|
-
// Human-in-the-loop: presents typed input requests (
|
|
1092
|
+
// Human-in-the-loop: presents typed input requests (ask_user,
|
|
988
1093
|
// tool approval, forms). Set by the <aparte-elicitation> Web Component.
|
|
989
1094
|
_elicitationPresenter;
|
|
1095
|
+
/** Warn once per config, not once per request — a tool loop can ask repeatedly. */
|
|
1096
|
+
_warnedNoPresenter = false;
|
|
990
1097
|
// Tool Registry
|
|
991
1098
|
_tools = /* @__PURE__ */ new Map();
|
|
992
1099
|
_toolRenderers = /* @__PURE__ */ new Map();
|
|
993
1100
|
// Host handlers — what the app declares it can actually complete.
|
|
994
|
-
_hostHandlers = { ...
|
|
995
|
-
// Bubble Actions —
|
|
1101
|
+
_hostHandlers = { ...APARTE_DEFAULT_HOST_HANDLERS };
|
|
1102
|
+
// Bubble Actions — APARTE_DEFAULT_BUBBLE_ACTIONS is the single source of truth
|
|
996
1103
|
// (init here, restored by reset(), and the fallback in getBubbleActions()).
|
|
997
|
-
_bubbleActionsConfig = { ...
|
|
1104
|
+
_bubbleActionsConfig = { ...APARTE_DEFAULT_BUBBLE_ACTIONS };
|
|
998
1105
|
// ─────────────────────────────────────────────────────────────────────────
|
|
999
1106
|
// Provider Setters (Dependency Injection)
|
|
1000
1107
|
// ─────────────────────────────────────────────────────────────────────────
|
|
@@ -1036,15 +1143,15 @@ class AparteConfigClass {
|
|
|
1036
1143
|
}
|
|
1037
1144
|
/**
|
|
1038
1145
|
* Configure which action buttons appear in message bubbles.
|
|
1039
|
-
* Unset keys keep their defaults — see {@link
|
|
1146
|
+
* Unset keys keep their defaults — see {@link APARTE_DEFAULT_BUBBLE_ACTIONS}: `copy`
|
|
1040
1147
|
* only, because every other button needs a host to honor it.
|
|
1041
1148
|
*
|
|
1042
1149
|
* @example
|
|
1043
|
-
*
|
|
1044
|
-
*
|
|
1045
|
-
*
|
|
1150
|
+
* aparteGlobalConfig.setBubbleActions({ retry: true, edit: true }) // you run AparteClient
|
|
1151
|
+
* aparteGlobalConfig.setBubbleActions({ feedback: true }) // you listen for aparte-feedback
|
|
1152
|
+
* aparteGlobalConfig.setBubbleActions({ copy: false }) // hide everything
|
|
1046
1153
|
* // Explicit per-role ordered sets (replace the flag defaults for that role):
|
|
1047
|
-
*
|
|
1154
|
+
* aparteGlobalConfig.setBubbleActions({ user: ['edit', 'copy'], assistant: ['copy', 'thumbUp', 'thumbDown', 'retry'] })
|
|
1048
1155
|
*/
|
|
1049
1156
|
setBubbleActions(config) {
|
|
1050
1157
|
this._bubbleActionsConfig = { ...this._bubbleActionsConfig, ...config };
|
|
@@ -1055,28 +1162,34 @@ class AparteConfigClass {
|
|
|
1055
1162
|
* trigger only for the ones you claim — see {@link AparteHostHandlersConfig}.
|
|
1056
1163
|
*
|
|
1057
1164
|
* @example
|
|
1058
|
-
*
|
|
1165
|
+
* aparteGlobalConfig.setHostHandlers({ attachmentPreview: true, terminalRun: true });
|
|
1059
1166
|
*/
|
|
1060
1167
|
setHostHandlers(config) {
|
|
1061
1168
|
this._hostHandlers = { ...this._hostHandlers, ...config };
|
|
1062
1169
|
this._notify();
|
|
1063
1170
|
}
|
|
1064
1171
|
/** Returns the resolved host-handler declarations (undeclared = false). */
|
|
1172
|
+
// `Required<...>` rather than a hand-written shape: the previous signature
|
|
1173
|
+
// listed the three keys literally, so adding a fourth to the interface left
|
|
1174
|
+
// this method silently returning a type without it — the caller's read did not
|
|
1175
|
+
// even typecheck. Derived from the interface, a new declaration cannot be
|
|
1176
|
+
// forgotten here.
|
|
1065
1177
|
getHostHandlers() {
|
|
1066
1178
|
return {
|
|
1067
|
-
attachmentPreview: this._hostHandlers.attachmentPreview ??
|
|
1068
|
-
terminalRun: this._hostHandlers.terminalRun ??
|
|
1069
|
-
artifactRedownload: this._hostHandlers.artifactRedownload ??
|
|
1179
|
+
attachmentPreview: this._hostHandlers.attachmentPreview ?? APARTE_DEFAULT_HOST_HANDLERS.attachmentPreview,
|
|
1180
|
+
terminalRun: this._hostHandlers.terminalRun ?? APARTE_DEFAULT_HOST_HANDLERS.terminalRun,
|
|
1181
|
+
artifactRedownload: this._hostHandlers.artifactRedownload ?? APARTE_DEFAULT_HOST_HANDLERS.artifactRedownload,
|
|
1182
|
+
artifactRehydrate: this._hostHandlers.artifactRehydrate ?? APARTE_DEFAULT_HOST_HANDLERS.artifactRehydrate
|
|
1070
1183
|
};
|
|
1071
1184
|
}
|
|
1072
1185
|
/** Returns the resolved bubble actions config (flag defaults applied; per-role sets passed through). */
|
|
1073
1186
|
getBubbleActions() {
|
|
1074
1187
|
return {
|
|
1075
|
-
copy: this._bubbleActionsConfig.copy ??
|
|
1076
|
-
retry: this._bubbleActionsConfig.retry ??
|
|
1077
|
-
edit: this._bubbleActionsConfig.edit ??
|
|
1078
|
-
feedback: this._bubbleActionsConfig.feedback ??
|
|
1079
|
-
info: this._bubbleActionsConfig.info ??
|
|
1188
|
+
copy: this._bubbleActionsConfig.copy ?? APARTE_DEFAULT_BUBBLE_ACTIONS.copy,
|
|
1189
|
+
retry: this._bubbleActionsConfig.retry ?? APARTE_DEFAULT_BUBBLE_ACTIONS.retry,
|
|
1190
|
+
edit: this._bubbleActionsConfig.edit ?? APARTE_DEFAULT_BUBBLE_ACTIONS.edit,
|
|
1191
|
+
feedback: this._bubbleActionsConfig.feedback ?? APARTE_DEFAULT_BUBBLE_ACTIONS.feedback,
|
|
1192
|
+
info: this._bubbleActionsConfig.info ?? APARTE_DEFAULT_BUBBLE_ACTIONS.info,
|
|
1080
1193
|
user: this._bubbleActionsConfig.user,
|
|
1081
1194
|
assistant: this._bubbleActionsConfig.assistant
|
|
1082
1195
|
};
|
|
@@ -1129,9 +1242,14 @@ class AparteConfigClass {
|
|
|
1129
1242
|
* hardened coverage, or `null` to DISABLE sanitization. Disabling exposes
|
|
1130
1243
|
* you to XSS from LLM-authored content — only do so for content you fully
|
|
1131
1244
|
* trust and have already sanitized upstream.
|
|
1132
|
-
* @example
|
|
1245
|
+
* @example aparteGlobalConfig.setHtmlSanitizer((html) => DOMPurify.sanitize(html));
|
|
1133
1246
|
*/
|
|
1134
1247
|
setHtmlSanitizer(sanitizer) {
|
|
1248
|
+
if (sanitizer === null) {
|
|
1249
|
+
console.warn(
|
|
1250
|
+
"[aparte] HTML sanitization is now DISABLED (setHtmlSanitizer(null)). Model-authored HTML will be inserted verbatim. Pass a sanitizer (e.g. DOMPurify.sanitize) unless you sanitize upstream yourself."
|
|
1251
|
+
);
|
|
1252
|
+
}
|
|
1135
1253
|
this._sanitizer = sanitizer;
|
|
1136
1254
|
}
|
|
1137
1255
|
/**
|
|
@@ -1202,7 +1320,7 @@ class AparteConfigClass {
|
|
|
1202
1320
|
}
|
|
1203
1321
|
/**
|
|
1204
1322
|
* The icon set as a **complete** provider: every name resolves, falling back
|
|
1205
|
-
* to `
|
|
1323
|
+
* to `APARTE_DEFAULT_ICON_FALLBACKS` for anything the registered provider doesn't
|
|
1206
1324
|
* implement. Callers (bubble action bar, composer controls) can therefore
|
|
1207
1325
|
* invoke `icons.copy()` unconditionally.
|
|
1208
1326
|
*
|
|
@@ -1214,9 +1332,9 @@ class AparteConfigClass {
|
|
|
1214
1332
|
getIconProvider() {
|
|
1215
1333
|
const registered = this._iconProvider;
|
|
1216
1334
|
const complete = {};
|
|
1217
|
-
for (const name of Object.keys(
|
|
1335
|
+
for (const name of Object.keys(APARTE_DEFAULT_ICON_FALLBACKS)) {
|
|
1218
1336
|
const fn = registered?.[name];
|
|
1219
|
-
complete[name] = fn ?? (() =>
|
|
1337
|
+
complete[name] = fn ?? (() => APARTE_DEFAULT_ICON_FALLBACKS[name]);
|
|
1220
1338
|
}
|
|
1221
1339
|
return complete;
|
|
1222
1340
|
}
|
|
@@ -1315,12 +1433,12 @@ class AparteConfigClass {
|
|
|
1315
1433
|
this._notify();
|
|
1316
1434
|
}
|
|
1317
1435
|
/**
|
|
1318
|
-
* Restore the built-in English locale (the same `
|
|
1436
|
+
* Restore the built-in English locale (the same `APARTE_DEFAULT_LOCALE` core ships
|
|
1319
1437
|
* with). Counterpart of {@link setLocale} for language toggles — avoids
|
|
1320
|
-
* having to import `
|
|
1438
|
+
* having to import `APARTE_DEFAULT_LOCALE` yourself.
|
|
1321
1439
|
*/
|
|
1322
1440
|
resetLocale() {
|
|
1323
|
-
this._locale =
|
|
1441
|
+
this._locale = APARTE_DEFAULT_LOCALE;
|
|
1324
1442
|
this._notify();
|
|
1325
1443
|
}
|
|
1326
1444
|
/**
|
|
@@ -1345,7 +1463,7 @@ class AparteConfigClass {
|
|
|
1345
1463
|
getIcon(name) {
|
|
1346
1464
|
const icon = this._iconProvider?.[name];
|
|
1347
1465
|
if (icon) return icon();
|
|
1348
|
-
return
|
|
1466
|
+
return APARTE_DEFAULT_ICON_FALLBACKS[name];
|
|
1349
1467
|
}
|
|
1350
1468
|
/**
|
|
1351
1469
|
* Set a custom Key provider (e.g., AparteVault override)
|
|
@@ -1372,7 +1490,9 @@ class AparteConfigClass {
|
|
|
1372
1490
|
if (!provider || !provider.fetchModels) return [];
|
|
1373
1491
|
try {
|
|
1374
1492
|
const apiKey = await this.getKey(providerId);
|
|
1375
|
-
|
|
1493
|
+
const models = await provider.fetchModels(apiKey);
|
|
1494
|
+
this._fetchedModels.set(providerId, models);
|
|
1495
|
+
return models;
|
|
1376
1496
|
} catch (error) {
|
|
1377
1497
|
console.warn(`[AparteConfig] Failed to refresh models for ${providerId}`, error);
|
|
1378
1498
|
return [];
|
|
@@ -1385,8 +1505,8 @@ class AparteConfigClass {
|
|
|
1385
1505
|
* Register one or more AI providers (e.g., OpenRouter, Gemini, Anthropic)
|
|
1386
1506
|
* @param providers AparteAIProvider implementations
|
|
1387
1507
|
* @example
|
|
1388
|
-
*
|
|
1389
|
-
*
|
|
1508
|
+
* aparteGlobalConfig.registerAIProvider(OpenRouterProvider);
|
|
1509
|
+
* aparteGlobalConfig.registerAIProvider(GeminiProvider, AnthropicProvider);
|
|
1390
1510
|
*/
|
|
1391
1511
|
registerAIProvider(...providers) {
|
|
1392
1512
|
for (const provider of providers) {
|
|
@@ -1426,14 +1546,14 @@ class AparteConfigClass {
|
|
|
1426
1546
|
}
|
|
1427
1547
|
/**
|
|
1428
1548
|
* Set the transport that decides where chat requests go and how auth is
|
|
1429
|
-
* handled. Defaults to {@link
|
|
1430
|
-
* Use a `
|
|
1549
|
+
* handled. Defaults to {@link AparteDirectTransport} (browser-direct — BYOK/local).
|
|
1550
|
+
* Use a `AparteBackendTransport` to keep API keys server-side (recommended for
|
|
1431
1551
|
* production).
|
|
1432
1552
|
*/
|
|
1433
1553
|
setTransport(transport) {
|
|
1434
1554
|
this._transport = transport;
|
|
1435
1555
|
}
|
|
1436
|
-
/** Get the active transport (
|
|
1556
|
+
/** Get the active transport (AparteDirectTransport by default). */
|
|
1437
1557
|
getTransport() {
|
|
1438
1558
|
return this._transport;
|
|
1439
1559
|
}
|
|
@@ -1441,7 +1561,7 @@ class AparteConfigClass {
|
|
|
1441
1561
|
* Register a model preference provider for agnostic persistence.
|
|
1442
1562
|
* The host app decides how/where to store the selected provider & model.
|
|
1443
1563
|
* @example
|
|
1444
|
-
*
|
|
1564
|
+
* aparteGlobalConfig.setModelPreferenceProvider({
|
|
1445
1565
|
* save: (p, m) => localStorage.setItem('model', JSON.stringify({p, m})),
|
|
1446
1566
|
* load: () => JSON.parse(localStorage.getItem('model') ?? 'null')
|
|
1447
1567
|
* });
|
|
@@ -1515,6 +1635,53 @@ class AparteConfigClass {
|
|
|
1515
1635
|
* still fetching its list). Off by default so single-model / backend setups
|
|
1516
1636
|
* that never select a model are unaffected.
|
|
1517
1637
|
*/
|
|
1638
|
+
/**
|
|
1639
|
+
* Elicitation policy: whether a choice offers a free-text "Other…" escape.
|
|
1640
|
+
*
|
|
1641
|
+
* This is the HOST's decision, not the model's. `ask_user` used to carry an
|
|
1642
|
+
* `allow_other` field in the schema it hands the model, which meant the model
|
|
1643
|
+
* decided your UX — and a small model fills a field it does not understand: one
|
|
1644
|
+
* sent two questions with `allow_other: true` and no options at all, so the
|
|
1645
|
+
* panel rendered a radio list whose only entry was "Other…", twice.
|
|
1646
|
+
*
|
|
1647
|
+
* Every serious implementation of this pattern makes the escape hatch a property
|
|
1648
|
+
* of the surface rather than of the request. Default `true`, which is what the
|
|
1649
|
+
* panel did before, so nothing a user sees changes — only who gets to say so.
|
|
1650
|
+
* A direct `requestUserInput` caller can still set `allowOther` per field; that
|
|
1651
|
+
* is the app talking, and it wins.
|
|
1652
|
+
*
|
|
1653
|
+
* `layout` decides how a form of SEVERAL questions is presented.
|
|
1654
|
+
* `'stepped'` (default) asks them one at a time with a chip per question;
|
|
1655
|
+
* `'stacked'` puts them all in the panel at once. Stacked was the only shape,
|
|
1656
|
+
* inherited unexamined from MCP elicitation — which describes a FORM for
|
|
1657
|
+
* collecting structured data, not two different questions asked mid-conversation.
|
|
1658
|
+
* No product asks a person two questions by stacking them in one box; it is kept
|
|
1659
|
+
* as an option because the form case is real, not because it was the right default.
|
|
1660
|
+
*/
|
|
1661
|
+
setElicitationOptions(options) {
|
|
1662
|
+
if (options.allowOther !== void 0) this._elicitationAllowOther = options.allowOther;
|
|
1663
|
+
if (options.layout !== void 0) this._elicitationLayout = options.layout;
|
|
1664
|
+
this._notify();
|
|
1665
|
+
}
|
|
1666
|
+
/** The elicitation policy (see {@link setElicitationOptions}). */
|
|
1667
|
+
getElicitationOptions() {
|
|
1668
|
+
return { allowOther: this._elicitationAllowOther, layout: this._elicitationLayout };
|
|
1669
|
+
}
|
|
1670
|
+
/**
|
|
1671
|
+
* Render one field of the question panel yourself.
|
|
1672
|
+
*
|
|
1673
|
+
* See {@link AparteElicitationFieldRenderer}. Return `null` for a field to let
|
|
1674
|
+
* the built-in render it, which is what makes overriding a single kind practical.
|
|
1675
|
+
* Pass `null` here to remove the renderer.
|
|
1676
|
+
*/
|
|
1677
|
+
setElicitationFieldRenderer(fn) {
|
|
1678
|
+
this._elicitationFieldRenderer = fn ?? void 0;
|
|
1679
|
+
this._notify();
|
|
1680
|
+
}
|
|
1681
|
+
/** The custom field renderer, if one is registered. */
|
|
1682
|
+
getElicitationFieldRenderer() {
|
|
1683
|
+
return this._elicitationFieldRenderer;
|
|
1684
|
+
}
|
|
1518
1685
|
setRequireModelSelection(required) {
|
|
1519
1686
|
if (this._requireModelSelection === required) return;
|
|
1520
1687
|
this._requireModelSelection = required;
|
|
@@ -1534,10 +1701,13 @@ class AparteConfigClass {
|
|
|
1534
1701
|
if (!defaultProvider || !defaultModel) return void 0;
|
|
1535
1702
|
const provider = this._aiProviders.get(defaultProvider);
|
|
1536
1703
|
if (!provider) return void 0;
|
|
1704
|
+
const fetched = this._fetchedModels.get(defaultProvider);
|
|
1705
|
+
const fromFetch = fetched?.find((m) => m.id === defaultModel);
|
|
1706
|
+
if (fromFetch) return fromFetch;
|
|
1537
1707
|
const models = provider.getModels();
|
|
1538
1708
|
if (models instanceof Promise) {
|
|
1539
1709
|
console.warn(
|
|
1540
|
-
`[AparteConfig] Provider "${defaultProvider}".getModels() returned a Promise — it is ignored here, so model
|
|
1710
|
+
`[AparteConfig] Provider "${defaultProvider}".getModels() returned a Promise — it is ignored here, so this model resolves to undefined and nothing can read its capabilities. getModels() must return the list synchronously; implement fetchModels() for async fetching.`
|
|
1541
1711
|
);
|
|
1542
1712
|
return void 0;
|
|
1543
1713
|
}
|
|
@@ -1550,7 +1720,7 @@ class AparteConfigClass {
|
|
|
1550
1720
|
* Get translated string by key
|
|
1551
1721
|
*/
|
|
1552
1722
|
t(key) {
|
|
1553
|
-
const val = this._locale[key] ||
|
|
1723
|
+
const val = this._locale[key] || APARTE_DEFAULT_LOCALE[key];
|
|
1554
1724
|
return val === void 0 ? "" : val;
|
|
1555
1725
|
}
|
|
1556
1726
|
/**
|
|
@@ -1601,7 +1771,7 @@ class AparteConfigClass {
|
|
|
1601
1771
|
* Register a tool and its handler together.
|
|
1602
1772
|
* The handler is called when the AI invokes the tool during streaming.
|
|
1603
1773
|
* @example
|
|
1604
|
-
*
|
|
1774
|
+
* aparteGlobalConfig.registerTool(askUserTool, askUserHandler);
|
|
1605
1775
|
*/
|
|
1606
1776
|
registerTool(tool, handler) {
|
|
1607
1777
|
this._tools.set(tool.name, { tool, handler });
|
|
@@ -1624,12 +1794,12 @@ class AparteConfigClass {
|
|
|
1624
1794
|
* Use this instead of the generic `tool_call` segment renderer for tool-specific UI.
|
|
1625
1795
|
*
|
|
1626
1796
|
* @example
|
|
1627
|
-
* // Hide the segment entirely (UI-only tool like
|
|
1628
|
-
*
|
|
1797
|
+
* // Hide the segment entirely (UI-only tool like ask_user)
|
|
1798
|
+
* aparteGlobalConfig.registerToolRenderer('ask_user', { render: () => '' });
|
|
1629
1799
|
*
|
|
1630
1800
|
* @example
|
|
1631
1801
|
* // Custom pill for a web-search tool
|
|
1632
|
-
*
|
|
1802
|
+
* aparteGlobalConfig.registerToolRenderer('web_search', { render: (seg) => `<div class="tool-pill">Searching...</div>` });
|
|
1633
1803
|
*/
|
|
1634
1804
|
registerToolRenderer(toolName, renderer) {
|
|
1635
1805
|
this._toolRenderers.set(toolName, renderer);
|
|
@@ -1646,13 +1816,13 @@ class AparteConfigClass {
|
|
|
1646
1816
|
// Conversation Manager (optional, agnostic persistence layer)
|
|
1647
1817
|
// ─────────────────────────────────────────────────────────────────────────
|
|
1648
1818
|
/**
|
|
1649
|
-
* Register a
|
|
1819
|
+
* Register a AparteConversationManager so any UI controller can persist & load
|
|
1650
1820
|
* conversations without coupling to a framework wrapper.
|
|
1651
1821
|
*/
|
|
1652
1822
|
setConversationManager(manager) {
|
|
1653
1823
|
this._conversationManager = manager;
|
|
1654
1824
|
}
|
|
1655
|
-
/** Returns the registered
|
|
1825
|
+
/** Returns the registered AparteConversationManager, or undefined if none. */
|
|
1656
1826
|
getConversationManager() {
|
|
1657
1827
|
return this._conversationManager;
|
|
1658
1828
|
}
|
|
@@ -1674,14 +1844,29 @@ class AparteConfigClass {
|
|
|
1674
1844
|
}
|
|
1675
1845
|
/**
|
|
1676
1846
|
* Ask the user for typed input mid-run and await their response. This is the
|
|
1677
|
-
* generic primitive behind `
|
|
1847
|
+
* generic primitive behind `ask_user` and tool approval — the KIND of
|
|
1678
1848
|
* question is the schema, not a bespoke tool. Resolves `accept` with the
|
|
1679
1849
|
* value, `decline` when the user declines, or `cancel` when the turn is
|
|
1680
|
-
* cancelled.
|
|
1681
|
-
*
|
|
1850
|
+
* cancelled.
|
|
1851
|
+
*
|
|
1852
|
+
* With NO presenter registered it resolves `cancel` rather than hanging — and
|
|
1853
|
+
* warns, once, because that `cancel` is otherwise a lie told quietly. The
|
|
1854
|
+
* model reads it as "the user refused"; the user was never asked, and the
|
|
1855
|
+
* developer sees nothing at all. The default presenter is
|
|
1856
|
+
* `<aparte-elicitation>`, which registers itself from its `connectedCallback`
|
|
1857
|
+
* — so it has to be IN THE DOM. A docs page of ours claimed it "installs
|
|
1858
|
+
* itself — nothing to register", which is how this failure mode was found.
|
|
1682
1859
|
*/
|
|
1683
1860
|
requestUserInput(request) {
|
|
1684
|
-
if (!this._elicitationPresenter)
|
|
1861
|
+
if (!this._elicitationPresenter) {
|
|
1862
|
+
if (!this._warnedNoPresenter) {
|
|
1863
|
+
this._warnedNoPresenter = true;
|
|
1864
|
+
console.warn(
|
|
1865
|
+
"[aparte] requestUserInput() was called with no elicitation presenter, so it resolved `cancel` — the model will read that as the user refusing, but nothing was ever shown. Add <aparte-elicitation></aparte-elicitation> inside your <aparte-chat>, or register your own by calling setElicitationPresenter() on the config this chat resolves — the scoped one if you passed a `config`, aparteGlobalConfig otherwise."
|
|
1866
|
+
);
|
|
1867
|
+
}
|
|
1868
|
+
return Promise.resolve({ action: "cancel" });
|
|
1869
|
+
}
|
|
1685
1870
|
return this._elicitationPresenter(request);
|
|
1686
1871
|
}
|
|
1687
1872
|
/**
|
|
@@ -1708,28 +1893,32 @@ class AparteConfigClass {
|
|
|
1708
1893
|
this._keyProvider = void 0;
|
|
1709
1894
|
this._conversationManager = void 0;
|
|
1710
1895
|
this._elicitationPresenter = void 0;
|
|
1711
|
-
this._locale =
|
|
1896
|
+
this._locale = APARTE_DEFAULT_LOCALE;
|
|
1712
1897
|
this._actions = [];
|
|
1713
1898
|
this._sanitizer = defaultSanitizer;
|
|
1714
1899
|
this._aiProviders.clear();
|
|
1900
|
+
this._fetchedModels.clear();
|
|
1715
1901
|
this._tools.clear();
|
|
1716
1902
|
this._toolRenderers.clear();
|
|
1717
1903
|
this._modelConfig = {};
|
|
1718
1904
|
this._requireModelSelection = false;
|
|
1905
|
+
this._elicitationAllowOther = true;
|
|
1906
|
+
this._elicitationLayout = "stepped";
|
|
1907
|
+
this._elicitationFieldRenderer = void 0;
|
|
1719
1908
|
this._modelPreferenceProvider = void 0;
|
|
1720
|
-
this._bubbleActionsConfig = { ...
|
|
1721
|
-
this._hostHandlers = { ...
|
|
1909
|
+
this._bubbleActionsConfig = { ...APARTE_DEFAULT_BUBBLE_ACTIONS };
|
|
1910
|
+
this._hostHandlers = { ...APARTE_DEFAULT_HOST_HANDLERS };
|
|
1722
1911
|
this._notify();
|
|
1723
1912
|
}
|
|
1724
1913
|
// ─────────────────────────────────────────────────────────────────────────
|
|
1725
1914
|
// Default Fallbacks (Zero-dependency)
|
|
1726
1915
|
// ─────────────────────────────────────────────────────────────────────────
|
|
1727
1916
|
_defaultMarkdownRenderer(raw) {
|
|
1728
|
-
const escaped =
|
|
1917
|
+
const escaped = escapeHtml(raw);
|
|
1729
1918
|
return escaped.replace(/\n/g, "<br>");
|
|
1730
1919
|
}
|
|
1731
1920
|
_defaultHighlightRenderer(code) {
|
|
1732
|
-
return `<pre><code>${
|
|
1921
|
+
return `<pre><code>${escapeHtml(code)}</code></pre>`;
|
|
1733
1922
|
}
|
|
1734
1923
|
_defaultSkeletonRenderer(type) {
|
|
1735
1924
|
const fallbacks = {
|
|
@@ -1742,34 +1931,44 @@ class AparteConfigClass {
|
|
|
1742
1931
|
};
|
|
1743
1932
|
return fallbacks[type] || fallbacks.text;
|
|
1744
1933
|
}
|
|
1745
|
-
_escapeHtml(text) {
|
|
1746
|
-
return text.replace(/&/g, "&").replace(/</g, "<").replace(/>/g, ">").replace(/"/g, """).replace(/'/g, "'");
|
|
1747
|
-
}
|
|
1748
1934
|
}
|
|
1749
|
-
const GLOBAL_CONFIG_KEY = "
|
|
1935
|
+
const GLOBAL_CONFIG_KEY = Symbol.for("aparte.globalConfig.1");
|
|
1750
1936
|
function getGlobalConfig() {
|
|
1751
1937
|
if (typeof window !== "undefined") {
|
|
1752
1938
|
if (!window[GLOBAL_CONFIG_KEY]) {
|
|
1753
|
-
window[GLOBAL_CONFIG_KEY] = new
|
|
1939
|
+
window[GLOBAL_CONFIG_KEY] = new AparteConfig();
|
|
1754
1940
|
}
|
|
1755
1941
|
return window[GLOBAL_CONFIG_KEY];
|
|
1756
1942
|
}
|
|
1757
|
-
return new
|
|
1943
|
+
return new AparteConfig();
|
|
1758
1944
|
}
|
|
1759
|
-
const
|
|
1945
|
+
const aparteGlobalConfig = getGlobalConfig();
|
|
1760
1946
|
const APARTE_HOST_ATTR = "data-aparte-host";
|
|
1761
1947
|
const CONFIG_SLOT = Symbol.for("aparte.instanceConfig");
|
|
1948
|
+
function notifyConfigChanged(root, next, previous) {
|
|
1949
|
+
if (next === previous) return;
|
|
1950
|
+
const call = (el2) => {
|
|
1951
|
+
const hook = el2.aparteConfigChanged;
|
|
1952
|
+
if (typeof hook === "function") hook.call(el2, next, previous);
|
|
1953
|
+
};
|
|
1954
|
+
call(root);
|
|
1955
|
+
for (const el2 of root.querySelectorAll("*")) call(el2);
|
|
1956
|
+
}
|
|
1762
1957
|
function attachConfig(el2, config) {
|
|
1958
|
+
const previous = el2[CONFIG_SLOT] ?? resolveConfig(el2.parentElement);
|
|
1763
1959
|
el2[CONFIG_SLOT] = config;
|
|
1764
1960
|
el2.setAttribute(APARTE_HOST_ATTR, "");
|
|
1961
|
+
notifyConfigChanged(el2, config, previous);
|
|
1765
1962
|
}
|
|
1766
1963
|
function detachConfig(el2) {
|
|
1964
|
+
const previous = el2[CONFIG_SLOT];
|
|
1767
1965
|
delete el2[CONFIG_SLOT];
|
|
1768
1966
|
el2.removeAttribute(APARTE_HOST_ATTR);
|
|
1967
|
+
if (previous) notifyConfigChanged(el2, resolveConfig(el2.parentElement), previous);
|
|
1769
1968
|
}
|
|
1770
1969
|
function resolveConfig(el2) {
|
|
1771
1970
|
const host = el2?.closest?.(`[${APARTE_HOST_ATTR}]`);
|
|
1772
|
-
return host?.[CONFIG_SLOT] ??
|
|
1971
|
+
return host?.[CONFIG_SLOT] ?? aparteGlobalConfig;
|
|
1773
1972
|
}
|
|
1774
1973
|
let _renderConfig = null;
|
|
1775
1974
|
function runWithConfig(config, fn) {
|
|
@@ -1784,7 +1983,7 @@ function runWithConfig(config, fn) {
|
|
|
1784
1983
|
function contextConfig(el2) {
|
|
1785
1984
|
return _renderConfig ?? resolveConfig(el2);
|
|
1786
1985
|
}
|
|
1787
|
-
const
|
|
1986
|
+
const APARTE_DEFAULT_SKELETON_FALLBACKS = {
|
|
1788
1987
|
message: '<div class="aparte-skeleton-fallback" style="padding:16px;color:#9ca3af;">Loading...</div>',
|
|
1789
1988
|
code: '<div class="aparte-skeleton-fallback" style="padding:16px;background:#1e293b;color:#64748b;border-radius:8px;">Loading code...</div>',
|
|
1790
1989
|
thinking: '<div class="aparte-skeleton-fallback" style="padding:8px;color:#9ca3af;">Thinking...</div>',
|
|
@@ -1792,103 +1991,45 @@ const DEFAULT_SKELETON_FALLBACKS = {
|
|
|
1792
1991
|
list: '<div class="aparte-skeleton-fallback" style="padding:16px;color:#9ca3af;">Loading items...</div>',
|
|
1793
1992
|
text: '<div class="aparte-skeleton-fallback" style="padding:8px;color:#9ca3af;">...</div>'
|
|
1794
1993
|
};
|
|
1795
|
-
function
|
|
1796
|
-
|
|
1797
|
-
|
|
1798
|
-
|
|
1799
|
-
|
|
1800
|
-
|
|
1801
|
-
|
|
1802
|
-
|
|
1803
|
-
|
|
1804
|
-
else out += ch;
|
|
1805
|
-
}
|
|
1806
|
-
return out;
|
|
1807
|
-
}
|
|
1808
|
-
function stripCodeFences(content) {
|
|
1809
|
-
let s = content;
|
|
1810
|
-
if (s.startsWith("```") || s.startsWith("~~~")) {
|
|
1811
|
-
const fenceChar = s[0];
|
|
1812
|
-
let i = 0;
|
|
1813
|
-
while (i < s.length && s[i] === fenceChar) i++;
|
|
1814
|
-
while (i < s.length && s[i] !== "\n") i++;
|
|
1815
|
-
if (i < s.length && s[i] === "\n") i++;
|
|
1816
|
-
s = s.slice(i);
|
|
1817
|
-
}
|
|
1818
|
-
const closeAt = findClosingFence(s);
|
|
1819
|
-
if (closeAt !== -1) s = s.slice(0, closeAt);
|
|
1820
|
-
return s.trim();
|
|
1821
|
-
}
|
|
1822
|
-
function findClosingFence(s) {
|
|
1823
|
-
let i = 0;
|
|
1824
|
-
while (i < s.length) {
|
|
1825
|
-
const lineStart = i;
|
|
1826
|
-
let k = lineStart;
|
|
1827
|
-
while (k < s.length && (s[k] === " " || s[k] === " ")) k++;
|
|
1828
|
-
if (k < s.length && (s[k] === "`" || s[k] === "~")) {
|
|
1829
|
-
const fenceChar = s[k];
|
|
1830
|
-
let runs = 0;
|
|
1831
|
-
while (k < s.length && s[k] === fenceChar) {
|
|
1832
|
-
runs++;
|
|
1833
|
-
k++;
|
|
1994
|
+
function writeStreamedMarkdown(host, contentEl, content, streaming) {
|
|
1995
|
+
if (streaming) {
|
|
1996
|
+
if (host._aparteSmd === void 0) {
|
|
1997
|
+
const renderer = contextConfig().createStreamingMarkdownRenderer(contentEl);
|
|
1998
|
+
if (renderer) {
|
|
1999
|
+
contentEl.textContent = "";
|
|
2000
|
+
host._aparteSmd = { renderer, written: 0 };
|
|
2001
|
+
} else {
|
|
2002
|
+
host._aparteSmd = null;
|
|
1834
2003
|
}
|
|
1835
|
-
|
|
1836
|
-
|
|
1837
|
-
|
|
1838
|
-
|
|
1839
|
-
|
|
1840
|
-
|
|
1841
|
-
|
|
1842
|
-
k++;
|
|
1843
|
-
}
|
|
1844
|
-
if (onlyWs) {
|
|
1845
|
-
let cut = lineStart;
|
|
1846
|
-
if (cut > 0 && s[cut - 1] === "\n") cut--;
|
|
1847
|
-
return cut;
|
|
1848
|
-
}
|
|
2004
|
+
}
|
|
2005
|
+
const smd = host._aparteSmd;
|
|
2006
|
+
if (smd) {
|
|
2007
|
+
const delta = content.slice(smd.written);
|
|
2008
|
+
if (delta) {
|
|
2009
|
+
smd.renderer.write(delta);
|
|
2010
|
+
smd.written = content.length;
|
|
1849
2011
|
}
|
|
2012
|
+
return;
|
|
1850
2013
|
}
|
|
1851
|
-
|
|
1852
|
-
|
|
2014
|
+
} else if (host._aparteSmd) {
|
|
2015
|
+
host._aparteSmd.renderer.end();
|
|
2016
|
+
host._aparteSmd = void 0;
|
|
1853
2017
|
}
|
|
1854
|
-
|
|
1855
|
-
}
|
|
1856
|
-
const renderers = /* @__PURE__ */ new Map();
|
|
1857
|
-
let styleElement = null;
|
|
1858
|
-
let defaultsInstalled = false;
|
|
1859
|
-
let defaultsDeclined = false;
|
|
1860
|
-
function registerSegmentRenderer(renderer) {
|
|
1861
|
-
renderers.set(renderer.type, renderer);
|
|
1862
|
-
injectRendererStyles();
|
|
1863
|
-
}
|
|
1864
|
-
function declineDefaultRenderers() {
|
|
1865
|
-
defaultsDeclined = true;
|
|
1866
|
-
}
|
|
1867
|
-
function installDefaultRenderersOnce() {
|
|
1868
|
-
if (defaultsInstalled || defaultsDeclined) return;
|
|
1869
|
-
defaultsInstalled = true;
|
|
1870
|
-
for (const renderer of DEFAULT_RENDERERS) {
|
|
1871
|
-
if (!renderers.has(renderer.type)) renderers.set(renderer.type, renderer);
|
|
1872
|
-
}
|
|
1873
|
-
injectRendererStyles();
|
|
1874
|
-
}
|
|
1875
|
-
function unregisterSegmentRenderer(type) {
|
|
1876
|
-
renderers.delete(type);
|
|
2018
|
+
contentEl.innerHTML = contextConfig().renderMarkdown(content);
|
|
1877
2019
|
}
|
|
1878
|
-
function
|
|
1879
|
-
|
|
1880
|
-
|
|
1881
|
-
|
|
1882
|
-
|
|
1883
|
-
|
|
1884
|
-
|
|
1885
|
-
|
|
1886
|
-
|
|
1887
|
-
|
|
1888
|
-
|
|
1889
|
-
|
|
2020
|
+
function highlightMarkdownFences(contentEl) {
|
|
2021
|
+
const blocks = contentEl.querySelectorAll("pre > code:not([data-aparte-highlighted])");
|
|
2022
|
+
for (const block of blocks) {
|
|
2023
|
+
const pre = block.parentElement;
|
|
2024
|
+
if (!pre) continue;
|
|
2025
|
+
const lang = Array.from(block.classList).find((c) => c.startsWith("language-"))?.slice("language-".length) ?? "";
|
|
2026
|
+
const source = block.textContent ?? "";
|
|
2027
|
+
block.setAttribute("data-aparte-highlighted", "");
|
|
2028
|
+
void contextConfig().highlightCode(source, lang).then((html) => {
|
|
2029
|
+
if (pre.isConnected || pre.parentElement) pre.outerHTML = html;
|
|
2030
|
+
}).catch(() => {
|
|
2031
|
+
});
|
|
1890
2032
|
}
|
|
1891
|
-
styleElement.textContent = collectRendererStyles();
|
|
1892
2033
|
}
|
|
1893
2034
|
const textRenderer = {
|
|
1894
2035
|
type: "text",
|
|
@@ -1896,41 +2037,41 @@ const textRenderer = {
|
|
|
1896
2037
|
update: (el2, segment) => {
|
|
1897
2038
|
const contentEl = el2.querySelector(".segment-content");
|
|
1898
2039
|
if (!contentEl) return;
|
|
1899
|
-
|
|
1900
|
-
|
|
1901
|
-
|
|
1902
|
-
|
|
1903
|
-
|
|
1904
|
-
|
|
1905
|
-
contentEl.textContent = "";
|
|
1906
|
-
host._aparteSmd = { renderer, written: 0 };
|
|
1907
|
-
} else {
|
|
1908
|
-
host._aparteSmd = null;
|
|
1909
|
-
}
|
|
1910
|
-
}
|
|
1911
|
-
const smd = host._aparteSmd;
|
|
1912
|
-
if (smd) {
|
|
1913
|
-
const delta = segment.content.slice(smd.written);
|
|
1914
|
-
if (delta) {
|
|
1915
|
-
smd.renderer.write(delta);
|
|
1916
|
-
smd.written = segment.content.length;
|
|
1917
|
-
}
|
|
1918
|
-
return;
|
|
1919
|
-
}
|
|
1920
|
-
} else if (host._aparteSmd) {
|
|
1921
|
-
host._aparteSmd.renderer.end();
|
|
1922
|
-
host._aparteSmd = void 0;
|
|
1923
|
-
}
|
|
1924
|
-
contentEl.innerHTML = contextConfig().renderMarkdown(segment.content);
|
|
2040
|
+
writeStreamedMarkdown(
|
|
2041
|
+
el2,
|
|
2042
|
+
contentEl,
|
|
2043
|
+
segment.content,
|
|
2044
|
+
segment.isStreaming !== false
|
|
2045
|
+
);
|
|
1925
2046
|
},
|
|
1926
2047
|
getStyles: () => ``
|
|
1927
2048
|
};
|
|
2049
|
+
const THINKING_ANCHOR_SLACK = 24;
|
|
1928
2050
|
const thinkingRenderer = {
|
|
1929
2051
|
type: "thinking",
|
|
1930
|
-
|
|
2052
|
+
/*
|
|
2053
|
+
* Markdown, like every other prose surface.
|
|
2054
|
+
*
|
|
2055
|
+
* The reasoning was rendered as escaped plain text in a `white-space: pre-wrap`
|
|
2056
|
+
* box, so a model that formats its thinking — headings, lists, `**bold**`, a
|
|
2057
|
+
* fenced snippet, which is most of them — showed its own syntax as literal
|
|
2058
|
+
* characters. Nothing said that was deliberate; it read as one, because the
|
|
2059
|
+
* `pre-wrap` made it look like a chosen preformatted block.
|
|
2060
|
+
*
|
|
2061
|
+
* `renderMarkdown` sanitizes its provider's output before it reaches innerHTML,
|
|
2062
|
+
* so this is exactly the text renderer's path, with the same guarantees and no
|
|
2063
|
+
* new class of risk. With no Markdown provider registered it degrades to the
|
|
2064
|
+
* zero-dependency default (escape + `<br>`), which is what it used to be.
|
|
2065
|
+
*/
|
|
2066
|
+
render: (segment) => `<details class="segment segment-thinking" data-segment-id="${escapeHtml(segment.id)}" ${segment.collapsed ? "" : "open"}><summary class="thinking-header"><span class="thinking-label">${escapeHtml(segment.label || contextConfig().t("thinking"))}</span><span class="thinking-toggle"></span></summary><div class="thinking-content">${contextConfig().renderMarkdown(segment.content)}</div></details>`,
|
|
1931
2067
|
update: (el2, segment) => {
|
|
1932
2068
|
const contentEl = el2.querySelector(".thinking-content");
|
|
1933
|
-
if (contentEl)
|
|
2069
|
+
if (!contentEl) return;
|
|
2070
|
+
const wasAtBottom = contentEl.scrollHeight - contentEl.scrollTop - contentEl.clientHeight <= THINKING_ANCHOR_SLACK;
|
|
2071
|
+
const streaming = segment.isStreaming !== false;
|
|
2072
|
+
writeStreamedMarkdown(el2, contentEl, segment.content, streaming);
|
|
2073
|
+
if (!streaming) highlightMarkdownFences(contentEl);
|
|
2074
|
+
if (wasAtBottom) contentEl.scrollTop = contentEl.scrollHeight;
|
|
1934
2075
|
},
|
|
1935
2076
|
getStyles: () => ``
|
|
1936
2077
|
};
|
|
@@ -1941,7 +2082,7 @@ const codeRenderer = {
|
|
|
1941
2082
|
<div class="code-header">
|
|
1942
2083
|
${segment.filename ? `<span class="code-filename">${escapeHtml(segment.filename)}</span>` : `<span class="code-header-filler"></span>`}
|
|
1943
2084
|
<span class="code-language">${escapeHtml(segment.language || "")}</span>
|
|
1944
|
-
<button class="code-copy" data-action="copy" title="${contextConfig().t("copy")}">
|
|
2085
|
+
<button class="code-copy" data-action="copy" title="${escapeAttr(contextConfig().t("copy"))}">
|
|
1945
2086
|
${contextConfig().getIcon("copy")}
|
|
1946
2087
|
</button>
|
|
1947
2088
|
</div>
|
|
@@ -1961,7 +2102,8 @@ const codeRenderer = {
|
|
|
1961
2102
|
const copyBtn = element.querySelector(".code-copy");
|
|
1962
2103
|
if (copyBtn) {
|
|
1963
2104
|
copyBtn.addEventListener("click", () => {
|
|
1964
|
-
|
|
2105
|
+
const source = element.querySelector(".code-content-wrapper")?.textContent ?? "";
|
|
2106
|
+
void navigator.clipboard.writeText(source).catch(() => {
|
|
1965
2107
|
});
|
|
1966
2108
|
copyBtn.innerHTML = contextConfig(copyBtn).getIcon("check");
|
|
1967
2109
|
copyBtn.setAttribute("title", contextConfig(copyBtn).t("copied"));
|
|
@@ -2003,8 +2145,8 @@ const terminalRenderer = {
|
|
|
2003
2145
|
</div>
|
|
2004
2146
|
<code class="terminal-command">${escapeHtml(segment.command || "")}</code>
|
|
2005
2147
|
<div class="terminal-actions">
|
|
2006
|
-
${segment.isRunning ? `<span class="terminal-running"><span class="spinner"></span>${contextConfig().t("running")}</span>` : contextConfig().getHostHandlers().terminalRun ? `<button class="terminal-run-btn" data-action="run" aria-label="${contextConfig().t("run")}" title="${contextConfig().t("run")}">${contextConfig().t("run")}</button>` : ""}
|
|
2007
|
-
<button class="terminal-copy-btn" data-action="copy" aria-label="${contextConfig().t("copy")}" title="${contextConfig().t("copy")}">
|
|
2148
|
+
${segment.isRunning ? `<span class="terminal-running"><span class="spinner"></span>${contextConfig().t("running")}</span>` : contextConfig().getHostHandlers().terminalRun ? `<button class="terminal-run-btn" data-action="run" aria-label="${escapeAttr(contextConfig().t("run"))}" title="${escapeAttr(contextConfig().t("run"))}">${contextConfig().t("run")}</button>` : ""}
|
|
2149
|
+
<button class="terminal-copy-btn" data-action="copy" aria-label="${escapeAttr(contextConfig().t("copy"))}" title="${escapeAttr(contextConfig().t("copy"))}">
|
|
2008
2150
|
${contextConfig().getIcon("copy")}
|
|
2009
2151
|
</button>
|
|
2010
2152
|
</div>
|
|
@@ -2013,7 +2155,7 @@ const terminalRenderer = {
|
|
|
2013
2155
|
${segment.exitCode !== void 0 && segment.exitCode !== 0 ? `<div class="terminal-error">Command failed with exit code ${segment.exitCode}</div>` : ""}
|
|
2014
2156
|
</div>
|
|
2015
2157
|
`,
|
|
2016
|
-
setup: (element) => {
|
|
2158
|
+
setup: (element, segment) => {
|
|
2017
2159
|
const copyBtn = element.querySelector(".terminal-copy-btn");
|
|
2018
2160
|
const command = element.querySelector(".terminal-command");
|
|
2019
2161
|
if (copyBtn && command) {
|
|
@@ -2031,12 +2173,12 @@ const terminalRenderer = {
|
|
|
2031
2173
|
const runBtn = element.querySelector(".terminal-run-btn");
|
|
2032
2174
|
if (runBtn) {
|
|
2033
2175
|
runBtn.addEventListener("click", () => {
|
|
2034
|
-
const segmentId = element.getAttribute("data-segment-id");
|
|
2035
2176
|
element.dispatchEvent(new CustomEvent("aparte-terminal-run", {
|
|
2036
2177
|
bubbles: true,
|
|
2037
2178
|
composed: true,
|
|
2038
2179
|
detail: {
|
|
2039
|
-
segmentId,
|
|
2180
|
+
segmentId: segment.id,
|
|
2181
|
+
messageId: segment.messageId,
|
|
2040
2182
|
command: command?.textContent || ""
|
|
2041
2183
|
}
|
|
2042
2184
|
}));
|
|
@@ -2131,13 +2273,13 @@ const toolCallRenderer = {
|
|
|
2131
2273
|
}
|
|
2132
2274
|
const customRenderer = contextConfig().getToolRenderer(segment.toolCall?.name);
|
|
2133
2275
|
if (customRenderer) {
|
|
2134
|
-
const
|
|
2135
|
-
if (
|
|
2276
|
+
const out = customRenderer.render(segment);
|
|
2277
|
+
if (out) return out;
|
|
2136
2278
|
}
|
|
2137
2279
|
const statusIcon = status === "resolved" ? contextConfig().getIcon("check") : status === "aborted" || status === "rejected" ? contextConfig().getIcon("close") : "";
|
|
2138
2280
|
const spinner = status === "pending" ? `<span class="tool-pill-spinner" aria-hidden="true"></span>` : "";
|
|
2139
2281
|
return `
|
|
2140
|
-
<div class="segment segment-tool-call" data-segment-id="${escapeHtml(segment.id)}" data-status="${status}">
|
|
2282
|
+
<div class="segment segment-tool-call" data-segment-id="${escapeHtml(segment.id)}" data-status="${escapeAttr(status)}">
|
|
2141
2283
|
<span class="tool-pill">
|
|
2142
2284
|
<span class="tool-pill-icon">${contextConfig().getIcon("tool")}</span>
|
|
2143
2285
|
<span class="tool-pill-name">${escapeHtml(name)}</span>
|
|
@@ -2151,10 +2293,11 @@ const toolCallRenderer = {
|
|
|
2151
2293
|
if (segment.status === "awaiting-approval") {
|
|
2152
2294
|
const toolCallId = segment.toolCall?.id;
|
|
2153
2295
|
if (!toolCallId) return;
|
|
2296
|
+
const host = element.closest("[data-aparte-host], aparte-chat, aparte-chat-viewport");
|
|
2154
2297
|
const decide = (approved) => element.dispatchEvent(new CustomEvent("aparte-tool-decision", {
|
|
2155
2298
|
bubbles: true,
|
|
2156
2299
|
composed: true,
|
|
2157
|
-
detail: { toolCallId, approved }
|
|
2300
|
+
detail: { toolCallId, approved, targetId: host?.id || void 0 }
|
|
2158
2301
|
}));
|
|
2159
2302
|
element.querySelector('[data-tool-decision="approve"]')?.addEventListener("click", () => decide(true));
|
|
2160
2303
|
element.querySelector('[data-tool-decision="reject"]')?.addEventListener("click", () => decide(false));
|
|
@@ -2260,442 +2403,182 @@ const pipelineWaitingRenderer = {
|
|
|
2260
2403
|
}
|
|
2261
2404
|
`
|
|
2262
2405
|
};
|
|
2263
|
-
function
|
|
2264
|
-
|
|
2265
|
-
|
|
2406
|
+
function stripCodeFences(content) {
|
|
2407
|
+
let s = content;
|
|
2408
|
+
if (s.startsWith("```") || s.startsWith("~~~")) {
|
|
2409
|
+
const fenceChar = s[0];
|
|
2410
|
+
let i = 0;
|
|
2411
|
+
while (i < s.length && s[i] === fenceChar) i++;
|
|
2412
|
+
while (i < s.length && s[i] !== "\n") i++;
|
|
2413
|
+
if (i < s.length && s[i] === "\n") i++;
|
|
2414
|
+
s = s.slice(i);
|
|
2415
|
+
}
|
|
2416
|
+
const closeAt = findClosingFence(s);
|
|
2417
|
+
if (closeAt !== -1) s = s.slice(0, closeAt);
|
|
2418
|
+
return s.trim();
|
|
2266
2419
|
}
|
|
2267
|
-
|
|
2268
|
-
|
|
2269
|
-
|
|
2270
|
-
|
|
2271
|
-
|
|
2272
|
-
|
|
2273
|
-
if (
|
|
2274
|
-
|
|
2275
|
-
|
|
2276
|
-
|
|
2277
|
-
|
|
2278
|
-
|
|
2279
|
-
|
|
2280
|
-
|
|
2281
|
-
|
|
2282
|
-
|
|
2283
|
-
|
|
2284
|
-
|
|
2285
|
-
|
|
2286
|
-
|
|
2287
|
-
|
|
2288
|
-
|
|
2289
|
-
|
|
2290
|
-
|
|
2291
|
-
|
|
2292
|
-
|
|
2293
|
-
|
|
2294
|
-
|
|
2295
|
-
<span class="aparte-art-card__kind" data-kind="${escapeHtml(kind)}">${escapeHtml(displayLang)}</span>
|
|
2296
|
-
<span class="aparte-art-card__title">${escapeHtml(title)}</span>
|
|
2297
|
-
${isStreaming ? '<span class="aparte-art-card__pulse" aria-label="Streaming"></span>' : ""}
|
|
2298
|
-
</div>
|
|
2299
|
-
<div class="aparte-art-card__actions">
|
|
2300
|
-
<button type="button" class="aparte-art-card__btn" data-action="copy" title="${contextConfig().t("copy")}" aria-label="Copy">
|
|
2301
|
-
${contextConfig().getIcon("copy")}
|
|
2302
|
-
</button>
|
|
2303
|
-
<button type="button" class="aparte-art-card__btn" data-action="download" title="Download" aria-label="Download" ${isStreaming ? "disabled" : ""}>
|
|
2304
|
-
<svg width="14" height="14" viewBox="0 0 16 16" fill="none" stroke="currentColor" stroke-width="1.5" stroke-linecap="round" stroke-linejoin="round"><path d="M8 2v9m0 0l-3-3m3 3l3-3M2 13h12"/></svg>
|
|
2305
|
-
</button>
|
|
2306
|
-
</div>
|
|
2307
|
-
</header>
|
|
2308
|
-
<nav class="aparte-art-card__tabs" role="tablist">
|
|
2309
|
-
${previewable ? `<button type="button" role="tab" data-tab-target="preview" ${initialTab === "preview" ? 'aria-selected="true"' : ""} ${isStreaming ? "disabled" : ""}>Preview</button>` : ""}
|
|
2310
|
-
<button type="button" role="tab" data-tab-target="code" ${initialTab === "code" ? 'aria-selected="true"' : ""}>Code</button>
|
|
2311
|
-
</nav>
|
|
2312
|
-
<div class="aparte-art-card__body">
|
|
2313
|
-
<div class="aparte-art-card__pane" data-pane="code">
|
|
2314
|
-
<div class="code-content-wrapper">
|
|
2315
|
-
<pre><code class="language-${escapeHtml(displayLang)}">${escapeHtml(cleanContent)}</code></pre>
|
|
2316
|
-
</div>
|
|
2317
|
-
</div>
|
|
2318
|
-
${previewable ? `
|
|
2319
|
-
<div class="aparte-art-card__pane" data-pane="preview">
|
|
2320
|
-
${previewSrcdoc ? `<iframe class="aparte-art-card__frame" sandbox="allow-scripts" referrerpolicy="no-referrer" loading="lazy" title="${escapeHtml(title)}" srcdoc="${escapeAttr(previewSrcdoc)}"></iframe>` : '<div class="aparte-art-card__pending">Generating preview…</div>'}
|
|
2321
|
-
</div>
|
|
2322
|
-
` : ""}
|
|
2323
|
-
</div>
|
|
2324
|
-
</div>
|
|
2325
|
-
`;
|
|
2326
|
-
},
|
|
2327
|
-
setup: (element, segment) => {
|
|
2328
|
-
const kind = (segment.artifactType || "").toLowerCase();
|
|
2329
|
-
if (BINARY_FILE_KINDS.has(kind)) {
|
|
2330
|
-
setupBinaryFileArtifact(element, segment, kind);
|
|
2331
|
-
return;
|
|
2420
|
+
function findClosingFence(s) {
|
|
2421
|
+
let i = 0;
|
|
2422
|
+
while (i < s.length) {
|
|
2423
|
+
const lineStart = i;
|
|
2424
|
+
let k = lineStart;
|
|
2425
|
+
while (k < s.length && (s[k] === " " || s[k] === " ")) k++;
|
|
2426
|
+
if (k < s.length && (s[k] === "`" || s[k] === "~")) {
|
|
2427
|
+
const fenceChar = s[k];
|
|
2428
|
+
let runs = 0;
|
|
2429
|
+
while (k < s.length && s[k] === fenceChar) {
|
|
2430
|
+
runs++;
|
|
2431
|
+
k++;
|
|
2432
|
+
}
|
|
2433
|
+
if (runs >= 3) {
|
|
2434
|
+
let onlyWs = true;
|
|
2435
|
+
while (k < s.length && s[k] !== "\n") {
|
|
2436
|
+
if (s[k] !== " " && s[k] !== " " && s[k] !== "\r") {
|
|
2437
|
+
onlyWs = false;
|
|
2438
|
+
break;
|
|
2439
|
+
}
|
|
2440
|
+
k++;
|
|
2441
|
+
}
|
|
2442
|
+
if (onlyWs) {
|
|
2443
|
+
let cut = lineStart;
|
|
2444
|
+
if (cut > 0 && s[cut - 1] === "\n") cut--;
|
|
2445
|
+
return cut;
|
|
2446
|
+
}
|
|
2447
|
+
}
|
|
2332
2448
|
}
|
|
2333
|
-
|
|
2334
|
-
if (
|
|
2335
|
-
|
|
2336
|
-
|
|
2337
|
-
|
|
2338
|
-
|
|
2339
|
-
|
|
2340
|
-
|
|
2341
|
-
|
|
2342
|
-
|
|
2343
|
-
|
|
2344
|
-
|
|
2345
|
-
|
|
2346
|
-
|
|
2347
|
-
|
|
2348
|
-
|
|
2349
|
-
|
|
2350
|
-
|
|
2351
|
-
|
|
2352
|
-
|
|
2353
|
-
|
|
2354
|
-
|
|
2355
|
-
|
|
2356
|
-
|
|
2357
|
-
|
|
2358
|
-
|
|
2359
|
-
|
|
2360
|
-
|
|
2361
|
-
|
|
2362
|
-
|
|
2363
|
-
|
|
2364
|
-
|
|
2365
|
-
|
|
2366
|
-
|
|
2367
|
-
|
|
2368
|
-
|
|
2369
|
-
|
|
2370
|
-
|
|
2371
|
-
|
|
2372
|
-
|
|
2373
|
-
|
|
2374
|
-
|
|
2375
|
-
|
|
2376
|
-
|
|
2377
|
-
|
|
2378
|
-
|
|
2379
|
-
|
|
2380
|
-
|
|
2381
|
-
|
|
2382
|
-
|
|
2383
|
-
|
|
2384
|
-
|
|
2385
|
-
|
|
2386
|
-
|
|
2387
|
-
|
|
2388
|
-
|
|
2389
|
-
|
|
2390
|
-
|
|
2391
|
-
|
|
2392
|
-
|
|
2393
|
-
|
|
2394
|
-
|
|
2395
|
-
|
|
2396
|
-
|
|
2397
|
-
|
|
2398
|
-
|
|
2399
|
-
|
|
2400
|
-
|
|
2401
|
-
|
|
2402
|
-
|
|
2403
|
-
|
|
2404
|
-
|
|
2405
|
-
|
|
2406
|
-
|
|
2407
|
-
|
|
2408
|
-
|
|
2409
|
-
|
|
2449
|
+
while (i < s.length && s[i] !== "\n") i++;
|
|
2450
|
+
if (i < s.length) i++;
|
|
2451
|
+
}
|
|
2452
|
+
return -1;
|
|
2453
|
+
}
|
|
2454
|
+
const _lastHighlightAt = /* @__PURE__ */ new Map();
|
|
2455
|
+
function markThrottle(map, id, at) {
|
|
2456
|
+
map.delete(id);
|
|
2457
|
+
if (map.size >= MAX_THROTTLE_ENTRIES) {
|
|
2458
|
+
const oldest = map.keys().next().value;
|
|
2459
|
+
if (oldest !== void 0) map.delete(oldest);
|
|
2460
|
+
}
|
|
2461
|
+
map.set(id, at);
|
|
2462
|
+
}
|
|
2463
|
+
function debounceHighlight(element, paneSelector, content, lang, segId) {
|
|
2464
|
+
const now = Date.now();
|
|
2465
|
+
const last = _lastHighlightAt.get(segId) ?? 0;
|
|
2466
|
+
if (now - last < HIGHLIGHT_DEBOUNCE_MS) return;
|
|
2467
|
+
markThrottle(_lastHighlightAt, segId, now);
|
|
2468
|
+
void contextConfig(element).highlightCode(content, lang).then((html) => {
|
|
2469
|
+
const wrapper = element.querySelector(paneSelector);
|
|
2470
|
+
if (wrapper) wrapper.innerHTML = html;
|
|
2471
|
+
}).catch(() => {
|
|
2472
|
+
});
|
|
2473
|
+
}
|
|
2474
|
+
function labelForKind(kind) {
|
|
2475
|
+
switch (kind) {
|
|
2476
|
+
case "react":
|
|
2477
|
+
return "React component";
|
|
2478
|
+
case "html":
|
|
2479
|
+
return "HTML document";
|
|
2480
|
+
case "svg":
|
|
2481
|
+
return "SVG image";
|
|
2482
|
+
case "js":
|
|
2483
|
+
return "JavaScript snippet";
|
|
2484
|
+
case "css":
|
|
2485
|
+
return "CSS stylesheet";
|
|
2486
|
+
case "json":
|
|
2487
|
+
return "JSON document";
|
|
2488
|
+
case "markdown":
|
|
2489
|
+
return "Markdown document";
|
|
2490
|
+
case "csv":
|
|
2491
|
+
return "CSV table";
|
|
2492
|
+
case "text":
|
|
2493
|
+
return "Text file";
|
|
2494
|
+
case "python":
|
|
2495
|
+
return "Python script";
|
|
2496
|
+
case "typescript":
|
|
2497
|
+
return "TypeScript file";
|
|
2498
|
+
case "bash":
|
|
2499
|
+
return "Bash script";
|
|
2500
|
+
case "sql":
|
|
2501
|
+
return "SQL query";
|
|
2502
|
+
case "pdf":
|
|
2503
|
+
return "PDF generator";
|
|
2504
|
+
case "xlsx":
|
|
2505
|
+
return "Excel generator";
|
|
2506
|
+
case "docx":
|
|
2507
|
+
return "Word generator";
|
|
2508
|
+
default:
|
|
2509
|
+
return "Artifact";
|
|
2510
|
+
}
|
|
2511
|
+
}
|
|
2512
|
+
const MAX_THROTTLE_ENTRIES = 256;
|
|
2513
|
+
const HIGHLIGHT_DEBOUNCE_MS = 400;
|
|
2514
|
+
const PREVIEW_CSP = "default-src 'none'; script-src 'unsafe-inline'; style-src 'unsafe-inline'; img-src data:; font-src data:";
|
|
2515
|
+
function withMetaCsp(doc) {
|
|
2516
|
+
const meta = `<meta http-equiv="Content-Security-Policy" content="${escapeAttr(PREVIEW_CSP)}">`;
|
|
2517
|
+
const doctype = doc.match(/^\s*<!doctype[^>]*>/i);
|
|
2518
|
+
const at = doctype ? doctype[0].length : 0;
|
|
2519
|
+
return `${doc.slice(0, at)}<head>${meta}</head>${doc.slice(at)}`;
|
|
2520
|
+
}
|
|
2521
|
+
function buildSafePreviewDocument(kind, body, title) {
|
|
2522
|
+
return withMetaCsp(buildPreviewBody(kind, body, title));
|
|
2523
|
+
}
|
|
2524
|
+
function buildPreviewBody(kind, body, title) {
|
|
2525
|
+
switch (kind) {
|
|
2526
|
+
case "html": {
|
|
2527
|
+
if (startsWithDoctype(body)) return body;
|
|
2528
|
+
return `<!doctype html><html><head><meta charset="utf-8"/><meta name="viewport" content="width=device-width,initial-scale=1"/><title>${escapeAttr(title)}</title></head><body>${body}</body></html>`;
|
|
2410
2529
|
}
|
|
2411
|
-
|
|
2412
|
-
|
|
2413
|
-
|
|
2530
|
+
case "svg":
|
|
2531
|
+
return `<!doctype html><html><head><meta charset="utf-8"/><title>${escapeAttr(title)}</title>
|
|
2532
|
+
<style>html,body{margin:0;height:100%;display:flex;align-items:center;justify-content:center;background:#fff}svg{max-width:90%;max-height:90%}</style>
|
|
2533
|
+
</head><body>${body}</body></html>`;
|
|
2534
|
+
case "js": {
|
|
2535
|
+
const safeBody = escapeClosingScriptTag(body);
|
|
2536
|
+
return `<!doctype html><html><head><meta charset="utf-8"/><title>${escapeAttr(title)}</title>
|
|
2537
|
+
<style>body{margin:0;font-family:ui-sans-serif,system-ui,sans-serif;padding:1rem;background:#fff;color:#0f172a}</style>
|
|
2538
|
+
</head><body><div id="root"></div><script>
|
|
2539
|
+
try { ${safeBody}
|
|
2540
|
+
} catch (e) { document.getElementById('root').innerHTML = '<pre style="color:#b91c1c">' + (e && e.stack || e) + '</pre>'; }
|
|
2541
|
+
<\/script></body></html>`;
|
|
2414
2542
|
}
|
|
2415
|
-
|
|
2416
|
-
|
|
2417
|
-
|
|
2418
|
-
|
|
2419
|
-
|
|
2420
|
-
|
|
2421
|
-
|
|
2422
|
-
|
|
2423
|
-
|
|
2424
|
-
|
|
2425
|
-
|
|
2426
|
-
|
|
2427
|
-
|
|
2428
|
-
|
|
2429
|
-
|
|
2430
|
-
|
|
2431
|
-
|
|
2432
|
-
|
|
2433
|
-
|
|
2434
|
-
|
|
2435
|
-
|
|
2436
|
-
|
|
2437
|
-
|
|
2438
|
-
|
|
2439
|
-
|
|
2440
|
-
|
|
2441
|
-
|
|
2442
|
-
|
|
2543
|
+
case "css":
|
|
2544
|
+
return `<!doctype html><html><head><meta charset="utf-8"/><title>${escapeAttr(title)}</title>
|
|
2545
|
+
<style>${body}</style></head><body>
|
|
2546
|
+
<div class="demo">
|
|
2547
|
+
<h1>Heading</h1>
|
|
2548
|
+
<p>Paragraph with a <a href="#">link</a> and <strong>strong</strong> text.</p>
|
|
2549
|
+
<button>Button</button>
|
|
2550
|
+
<input placeholder="Input"/>
|
|
2551
|
+
<ul><li>One</li><li>Two</li><li>Three</li></ul>
|
|
2552
|
+
</div></body></html>`;
|
|
2553
|
+
default:
|
|
2554
|
+
return `<!doctype html><html><head><meta charset="utf-8"/><title>${escapeAttr(title)}</title>
|
|
2555
|
+
<style>body{margin:0;font-family:ui-monospace,SFMono-Regular,Menlo,monospace;padding:1rem;background:#fff;color:#0f172a}pre{white-space:pre-wrap;word-break:break-word;margin:0}</style>
|
|
2556
|
+
</head><body><pre>${escapeHtml(body)}</pre></body></html>`;
|
|
2557
|
+
}
|
|
2558
|
+
}
|
|
2559
|
+
function startsWithDoctype(s) {
|
|
2560
|
+
let i = 0;
|
|
2561
|
+
while (i < s.length && (s[i] === " " || s[i] === " " || s[i] === "\n" || s[i] === "\r")) i++;
|
|
2562
|
+
const probe = s.slice(i, i + 9).toLowerCase();
|
|
2563
|
+
return probe === "<!doctype";
|
|
2564
|
+
}
|
|
2565
|
+
function escapeClosingScriptTag(body) {
|
|
2566
|
+
let out = "";
|
|
2567
|
+
let i = 0;
|
|
2568
|
+
while (i < body.length) {
|
|
2569
|
+
if (body[i] === "<" && body.slice(i, i + 8).toLowerCase() === "<\/script") {
|
|
2570
|
+
const next = body[i + 8];
|
|
2571
|
+
if (next === void 0 || next === "/" || next === ">" || /\s/.test(next)) {
|
|
2572
|
+
out += "<\\/script";
|
|
2573
|
+
i += 8;
|
|
2574
|
+
continue;
|
|
2443
2575
|
}
|
|
2444
2576
|
}
|
|
2445
|
-
|
|
2446
|
-
|
|
2447
|
-
|
|
2448
|
-
|
|
2449
|
-
|
|
2450
|
-
border: 1px solid var(--aparte-border, rgba(0,0,0,0.12));
|
|
2451
|
-
border-radius: 12px;
|
|
2452
|
-
background: var(--aparte-surface, #fff);
|
|
2453
|
-
overflow: hidden;
|
|
2454
|
-
font: inherit;
|
|
2455
|
-
color: var(--aparte-text, inherit);
|
|
2456
|
-
}
|
|
2457
|
-
.aparte-art-card__header {
|
|
2458
|
-
display: flex; align-items: center; justify-content: space-between;
|
|
2459
|
-
padding: 8px 10px;
|
|
2460
|
-
border-bottom: 1px solid var(--aparte-border, rgba(0,0,0,0.08));
|
|
2461
|
-
background: var(--aparte-surface-2, rgba(0,0,0,0.02));
|
|
2462
|
-
min-height: 36px;
|
|
2463
|
-
}
|
|
2464
|
-
.aparte-art-card__title-block { display: flex; align-items: center; gap: 8px; min-width: 0; }
|
|
2465
|
-
.aparte-art-card__kind {
|
|
2466
|
-
font-size: 0.7rem; font-weight: 600;
|
|
2467
|
-
text-transform: uppercase; letter-spacing: 0.04em;
|
|
2468
|
-
padding: 2px 6px; border-radius: 4px;
|
|
2469
|
-
background: var(--aparte-surface, #fff);
|
|
2470
|
-
border: 1px solid var(--aparte-border, rgba(0,0,0,0.1));
|
|
2471
|
-
color: var(--aparte-text-muted, rgba(0,0,0,0.6));
|
|
2472
|
-
}
|
|
2473
|
-
.aparte-art-card__title {
|
|
2474
|
-
font-size: 0.85rem; font-weight: 500;
|
|
2475
|
-
overflow: hidden; text-overflow: ellipsis; white-space: nowrap;
|
|
2476
|
-
}
|
|
2477
|
-
.aparte-art-card__pulse {
|
|
2478
|
-
width: 8px; height: 8px; border-radius: 50%;
|
|
2479
|
-
background: var(--aparte-accent, #3b82f6);
|
|
2480
|
-
animation: aparte-art-card-pulse 1.2s ease-in-out infinite;
|
|
2481
|
-
}
|
|
2482
|
-
@keyframes aparte-art-card-pulse {
|
|
2483
|
-
0%, 100% { opacity: 0.35; transform: scale(0.85); }
|
|
2484
|
-
50% { opacity: 1; transform: scale(1.1); }
|
|
2485
|
-
}
|
|
2486
|
-
.aparte-art-card__actions { display: flex; gap: 4px; }
|
|
2487
|
-
.aparte-art-card__btn {
|
|
2488
|
-
display: inline-flex; align-items: center; justify-content: center;
|
|
2489
|
-
width: 28px; height: 28px;
|
|
2490
|
-
background: transparent;
|
|
2491
|
-
border: 1px solid transparent;
|
|
2492
|
-
border-radius: 6px;
|
|
2493
|
-
color: var(--aparte-text-muted, rgba(0,0,0,0.55));
|
|
2494
|
-
cursor: pointer;
|
|
2495
|
-
transition: background 0.12s, border-color 0.12s, color 0.12s;
|
|
2496
|
-
}
|
|
2497
|
-
.aparte-art-card__btn:hover:not(:disabled) {
|
|
2498
|
-
background: var(--aparte-surface-hover, rgba(0,0,0,0.04));
|
|
2499
|
-
border-color: var(--aparte-border, rgba(0,0,0,0.1));
|
|
2500
|
-
color: var(--aparte-text, inherit);
|
|
2501
|
-
}
|
|
2502
|
-
.aparte-art-card__btn:disabled { opacity: 0.4; cursor: not-allowed; }
|
|
2503
|
-
.aparte-art-card__tabs {
|
|
2504
|
-
display: flex; gap: 2px;
|
|
2505
|
-
padding: 4px 8px 0;
|
|
2506
|
-
border-bottom: 1px solid var(--aparte-border, rgba(0,0,0,0.08));
|
|
2507
|
-
background: var(--aparte-surface-2, rgba(0,0,0,0.02));
|
|
2508
|
-
}
|
|
2509
|
-
.aparte-art-card__tabs button {
|
|
2510
|
-
padding: 6px 12px;
|
|
2511
|
-
font-size: 0.78rem;
|
|
2512
|
-
background: transparent;
|
|
2513
|
-
border: 1px solid transparent;
|
|
2514
|
-
border-bottom: none;
|
|
2515
|
-
border-radius: 6px 6px 0 0;
|
|
2516
|
-
color: var(--aparte-text-muted, rgba(0,0,0,0.6));
|
|
2517
|
-
cursor: pointer;
|
|
2518
|
-
transition: background 0.12s, color 0.12s;
|
|
2519
|
-
}
|
|
2520
|
-
.aparte-art-card__tabs button[aria-selected="true"] {
|
|
2521
|
-
background: var(--aparte-surface, #fff);
|
|
2522
|
-
border-color: var(--aparte-border, rgba(0,0,0,0.08));
|
|
2523
|
-
color: var(--aparte-text, inherit);
|
|
2524
|
-
font-weight: 500;
|
|
2525
|
-
}
|
|
2526
|
-
.aparte-art-card__tabs button:disabled { opacity: 0.4; cursor: not-allowed; }
|
|
2527
|
-
.aparte-art-card__body {
|
|
2528
|
-
position: relative;
|
|
2529
|
-
min-height: 80px;
|
|
2530
|
-
max-height: 600px;
|
|
2531
|
-
overflow: hidden;
|
|
2532
|
-
}
|
|
2533
|
-
.aparte-art-card__pane { display: none; height: 100%; }
|
|
2534
|
-
.segment-artifact-card[data-tab="code"] .aparte-art-card__pane[data-pane="code"] { display: block; }
|
|
2535
|
-
.segment-artifact-card[data-tab="preview"] .aparte-art-card__pane[data-pane="preview"] { display: block; }
|
|
2536
|
-
.aparte-art-card__pane[data-pane="code"] {
|
|
2537
|
-
max-height: 600px; overflow: auto;
|
|
2538
|
-
}
|
|
2539
|
-
.aparte-art-card__pane[data-pane="code"] pre {
|
|
2540
|
-
margin: 0; padding: 12px;
|
|
2541
|
-
font-size: 0.82rem;
|
|
2542
|
-
background: var(--aparte-code-bg, #fafafa);
|
|
2543
|
-
}
|
|
2544
|
-
.aparte-art-card__frame {
|
|
2545
|
-
display: block;
|
|
2546
|
-
width: 100%; height: 480px;
|
|
2547
|
-
border: 0;
|
|
2548
|
-
background: #fff;
|
|
2549
|
-
}
|
|
2550
|
-
.aparte-art-card__pending {
|
|
2551
|
-
display: flex; align-items: center; justify-content: center;
|
|
2552
|
-
height: 120px;
|
|
2553
|
-
color: var(--aparte-text-muted, rgba(0,0,0,0.5));
|
|
2554
|
-
font-size: 0.85rem;
|
|
2555
|
-
font-style: italic;
|
|
2556
|
-
}
|
|
2557
|
-
/* ── Binary file artifact (xlsx/pdf/docx) ──────────────────── */
|
|
2558
|
-
.segment-artifact-file {
|
|
2559
|
-
display: flex; flex-direction: column;
|
|
2560
|
-
margin: 8px 0;
|
|
2561
|
-
border: 1px solid var(--aparte-border, rgba(0,0,0,0.12));
|
|
2562
|
-
border-radius: 12px;
|
|
2563
|
-
background: var(--aparte-surface, #fff);
|
|
2564
|
-
overflow: hidden;
|
|
2565
|
-
color: var(--aparte-text, inherit);
|
|
2566
|
-
font: inherit;
|
|
2567
|
-
}
|
|
2568
|
-
.aparte-art-file__card {
|
|
2569
|
-
display: flex; align-items: center; gap: 12px;
|
|
2570
|
-
padding: 12px 14px;
|
|
2571
|
-
background: var(--aparte-surface-2, rgba(0,0,0,0.02));
|
|
2572
|
-
border-bottom: 1px solid var(--aparte-border, rgba(0,0,0,0.08));
|
|
2573
|
-
}
|
|
2574
|
-
.aparte-art-file__body {
|
|
2575
|
-
position: relative;
|
|
2576
|
-
}
|
|
2577
|
-
.aparte-art-file__code-pane {
|
|
2578
|
-
max-height: 360px; overflow: auto;
|
|
2579
|
-
background: var(--aparte-code-bg, #fafafa);
|
|
2580
|
-
}
|
|
2581
|
-
.aparte-art-file__code-pane pre {
|
|
2582
|
-
margin: 0; padding: 12px;
|
|
2583
|
-
font-size: 0.82rem;
|
|
2584
|
-
font-family: 'JetBrains Mono', 'Fira Code', Consolas, monospace;
|
|
2585
|
-
}
|
|
2586
|
-
.aparte-art-file__preview-pane {
|
|
2587
|
-
max-height: 460px;
|
|
2588
|
-
overflow: auto;
|
|
2589
|
-
background: #fff;
|
|
2590
|
-
/* Preview is a document view — force light scheme regardless of
|
|
2591
|
-
the app theme, with a dark text colour so cells stay readable. */
|
|
2592
|
-
color: #1f2937;
|
|
2593
|
-
}
|
|
2594
|
-
.aparte-art-file__icon {
|
|
2595
|
-
width: 40px; height: 40px;
|
|
2596
|
-
border-radius: 8px;
|
|
2597
|
-
display: flex; align-items: center; justify-content: center;
|
|
2598
|
-
background: linear-gradient(135deg, #1d6f42, #0f5132);
|
|
2599
|
-
color: #fff;
|
|
2600
|
-
font-weight: 700;
|
|
2601
|
-
font-size: 0.78rem;
|
|
2602
|
-
letter-spacing: 0.04em;
|
|
2603
|
-
flex-shrink: 0;
|
|
2604
|
-
}
|
|
2605
|
-
.aparte-art-file__icon[data-kind="pdf"] { background: linear-gradient(135deg, #c0392b, #7d1f17); }
|
|
2606
|
-
.aparte-art-file__icon[data-kind="docx"] { background: linear-gradient(135deg, #1e5288, #0f3060); }
|
|
2607
|
-
.aparte-art-file__meta { flex: 1 1 auto; min-width: 0; }
|
|
2608
|
-
.aparte-art-file__meta-name {
|
|
2609
|
-
font-weight: 600; font-size: 0.92rem;
|
|
2610
|
-
white-space: nowrap; overflow: hidden; text-overflow: ellipsis;
|
|
2611
|
-
}
|
|
2612
|
-
.aparte-art-file__meta-sub {
|
|
2613
|
-
font-size: 0.78rem;
|
|
2614
|
-
color: var(--aparte-text-muted, rgba(0,0,0,0.55));
|
|
2615
|
-
}
|
|
2616
|
-
.aparte-art-file__actions { display: flex; gap: 6px; flex-shrink: 0; }
|
|
2617
|
-
.aparte-art-file__btn {
|
|
2618
|
-
border: 1px solid var(--aparte-border, rgba(0,0,0,0.12));
|
|
2619
|
-
background: var(--aparte-surface, #fff);
|
|
2620
|
-
color: var(--aparte-text, inherit);
|
|
2621
|
-
padding: 6px 10px;
|
|
2622
|
-
border-radius: 6px;
|
|
2623
|
-
font-size: 0.8rem;
|
|
2624
|
-
cursor: pointer;
|
|
2625
|
-
transition: background 0.12s ease;
|
|
2626
|
-
}
|
|
2627
|
-
.aparte-art-file__btn:hover { background: var(--aparte-surface-2, rgba(0,0,0,0.05)); }
|
|
2628
|
-
.aparte-art-file__btn--primary {
|
|
2629
|
-
background: var(--aparte-primary, #6366f1);
|
|
2630
|
-
color: #fff;
|
|
2631
|
-
border-color: var(--aparte-primary, #6366f1);
|
|
2632
|
-
}
|
|
2633
|
-
.aparte-art-file__btn--primary:hover { filter: brightness(1.07); }
|
|
2634
|
-
/* Hide the download button until the sandbox has produced the buffer.
|
|
2635
|
-
Avoids confusing the user with a disabled-but-styled-primary button
|
|
2636
|
-
during streaming / compiling. */
|
|
2637
|
-
.segment-artifact-file:not([data-state="ready"]) .aparte-art-file__btn[data-action="download"] {
|
|
2638
|
-
display: none;
|
|
2639
|
-
}
|
|
2640
|
-
.aparte-art-file__preview-pane table {
|
|
2641
|
-
border-collapse: collapse;
|
|
2642
|
-
width: 100%;
|
|
2643
|
-
font-size: 0.82rem;
|
|
2644
|
-
font-family: inherit;
|
|
2645
|
-
}
|
|
2646
|
-
.aparte-art-file__preview-pane th,
|
|
2647
|
-
.aparte-art-file__preview-pane td {
|
|
2648
|
-
border: 1px solid rgba(0,0,0,0.10);
|
|
2649
|
-
padding: 6px 10px;
|
|
2650
|
-
text-align: left;
|
|
2651
|
-
white-space: nowrap;
|
|
2652
|
-
color: #1f2937;
|
|
2653
|
-
background: #fff;
|
|
2654
|
-
}
|
|
2655
|
-
.aparte-art-file__preview-pane tr:nth-child(odd) td { background: #f9fafb; }
|
|
2656
|
-
.aparte-art-file__preview-pane tr:first-child td {
|
|
2657
|
-
background: #f3f4f6;
|
|
2658
|
-
font-weight: 600;
|
|
2659
|
-
position: sticky; top: 0;
|
|
2660
|
-
color: #111827;
|
|
2661
|
-
}
|
|
2662
|
-
.aparte-art-file__preview-empty {
|
|
2663
|
-
padding: 20px; text-align: center;
|
|
2664
|
-
color: var(--aparte-text-muted, rgba(0,0,0,0.5));
|
|
2665
|
-
font-size: 0.85rem; font-style: italic;
|
|
2666
|
-
}
|
|
2667
|
-
.segment-artifact-file[data-state="error"] .aparte-art-file__icon {
|
|
2668
|
-
background: linear-gradient(135deg, #c0392b, #7d1f17);
|
|
2669
|
-
}
|
|
2670
|
-
.aparte-art-file__error {
|
|
2671
|
-
padding: 16px 18px;
|
|
2672
|
-
background: rgba(239, 68, 68, 0.05);
|
|
2673
|
-
border-top: 1px solid rgba(239, 68, 68, 0.2);
|
|
2674
|
-
color: var(--aparte-text, inherit);
|
|
2675
|
-
}
|
|
2676
|
-
.aparte-art-file__error-title {
|
|
2677
|
-
font-weight: 600;
|
|
2678
|
-
font-size: 0.9rem;
|
|
2679
|
-
margin-bottom: 6px;
|
|
2680
|
-
color: #b91c1c;
|
|
2681
|
-
}
|
|
2682
|
-
.aparte-art-file__error-msg {
|
|
2683
|
-
font-family: 'JetBrains Mono', 'Fira Code', Consolas, monospace;
|
|
2684
|
-
font-size: 0.78rem;
|
|
2685
|
-
padding: 6px 10px;
|
|
2686
|
-
background: rgba(0,0,0,0.04);
|
|
2687
|
-
border-radius: 4px;
|
|
2688
|
-
margin-bottom: 8px;
|
|
2689
|
-
color: #7f1d1d;
|
|
2690
|
-
word-break: break-word;
|
|
2691
|
-
}
|
|
2692
|
-
.aparte-art-file__error-hint {
|
|
2693
|
-
font-size: 0.78rem;
|
|
2694
|
-
color: var(--aparte-text-muted, rgba(0,0,0,0.55));
|
|
2695
|
-
font-style: italic;
|
|
2696
|
-
}
|
|
2697
|
-
`
|
|
2698
|
-
};
|
|
2577
|
+
out += body[i];
|
|
2578
|
+
i++;
|
|
2579
|
+
}
|
|
2580
|
+
return out;
|
|
2581
|
+
}
|
|
2699
2582
|
const FILE_ICON_LABEL = {
|
|
2700
2583
|
xlsx: "XLS",
|
|
2701
2584
|
pdf: "PDF",
|
|
@@ -2715,29 +2598,10 @@ const _fileGenHandlers = /* @__PURE__ */ new Map();
|
|
|
2715
2598
|
const MAX_FILE_GEN_HANDLERS = 32;
|
|
2716
2599
|
const _lastDispatchAt = /* @__PURE__ */ new Map();
|
|
2717
2600
|
const RE_DISPATCH_DEBOUNCE_MS = 3e4;
|
|
2718
|
-
|
|
2719
|
-
|
|
2720
|
-
|
|
2721
|
-
|
|
2722
|
-
map.delete(id);
|
|
2723
|
-
if (map.size >= MAX_THROTTLE_ENTRIES) {
|
|
2724
|
-
const oldest = map.keys().next().value;
|
|
2725
|
-
if (oldest !== void 0) map.delete(oldest);
|
|
2726
|
-
}
|
|
2727
|
-
map.set(id, at);
|
|
2728
|
-
}
|
|
2729
|
-
function debounceHighlight(element, paneSelector, content, lang, segId) {
|
|
2730
|
-
const now = Date.now();
|
|
2731
|
-
const last = _lastHighlightAt.get(segId) ?? 0;
|
|
2732
|
-
if (now - last < HIGHLIGHT_DEBOUNCE_MS) return;
|
|
2733
|
-
markThrottle(_lastHighlightAt, segId, now);
|
|
2734
|
-
void contextConfig(element).highlightCode(content, lang).then((html) => {
|
|
2735
|
-
const wrapper = element.querySelector(paneSelector);
|
|
2736
|
-
if (wrapper) wrapper.innerHTML = html;
|
|
2737
|
-
}).catch(() => {
|
|
2738
|
-
});
|
|
2739
|
-
}
|
|
2740
|
-
if (typeof window !== "undefined") {
|
|
2601
|
+
let _artifactReadyHookInstalled = false;
|
|
2602
|
+
function installArtifactReadyHook() {
|
|
2603
|
+
if (_artifactReadyHookInstalled || typeof window === "undefined") return;
|
|
2604
|
+
_artifactReadyHookInstalled = true;
|
|
2741
2605
|
window.addEventListener("aparte-artifact-ready", (event) => {
|
|
2742
2606
|
const segId = event.detail?.segmentId;
|
|
2743
2607
|
if (segId) markThrottle(_lastDispatchAt, segId, Date.now());
|
|
@@ -2865,6 +2729,7 @@ function setupBinaryFileArtifact(element, segment, kind) {
|
|
|
2865
2729
|
);
|
|
2866
2730
|
return;
|
|
2867
2731
|
}
|
|
2732
|
+
if (!contextConfig().getHostHandlers().artifactRehydrate) return;
|
|
2868
2733
|
const lastAt = _lastDispatchAt.get(segment.id) ?? 0;
|
|
2869
2734
|
if (Date.now() - lastAt < RE_DISPATCH_DEBOUNCE_MS) return;
|
|
2870
2735
|
markThrottle(_lastDispatchAt, segment.id, Date.now());
|
|
@@ -2953,44 +2818,425 @@ function showSandboxError(element, phase, errorMsg) {
|
|
|
2953
2818
|
`;
|
|
2954
2819
|
}
|
|
2955
2820
|
}
|
|
2956
|
-
|
|
2957
|
-
|
|
2958
|
-
|
|
2959
|
-
|
|
2960
|
-
|
|
2961
|
-
|
|
2962
|
-
|
|
2963
|
-
return
|
|
2964
|
-
|
|
2965
|
-
|
|
2966
|
-
|
|
2967
|
-
|
|
2968
|
-
|
|
2969
|
-
|
|
2970
|
-
|
|
2971
|
-
|
|
2972
|
-
|
|
2973
|
-
|
|
2974
|
-
|
|
2975
|
-
|
|
2976
|
-
|
|
2977
|
-
|
|
2978
|
-
|
|
2979
|
-
|
|
2980
|
-
|
|
2981
|
-
|
|
2982
|
-
|
|
2983
|
-
|
|
2984
|
-
|
|
2985
|
-
|
|
2986
|
-
|
|
2987
|
-
|
|
2988
|
-
|
|
2989
|
-
|
|
2990
|
-
|
|
2991
|
-
|
|
2992
|
-
|
|
2993
|
-
|
|
2821
|
+
const PREVIEWABLE_KINDS = /* @__PURE__ */ new Set(["react", "html", "svg", "js", "css"]);
|
|
2822
|
+
const BINARY_FILE_KINDS = /* @__PURE__ */ new Set(["pdf", "xlsx", "docx"]);
|
|
2823
|
+
const artifactRenderer = {
|
|
2824
|
+
type: "artifact",
|
|
2825
|
+
render: (segment) => {
|
|
2826
|
+
const kind = (segment.artifactType || "unknown").toLowerCase();
|
|
2827
|
+
if (BINARY_FILE_KINDS.has(kind)) {
|
|
2828
|
+
return renderBinaryFileArtifact(segment, kind);
|
|
2829
|
+
}
|
|
2830
|
+
const title = segment.title?.trim() || labelForKind(kind);
|
|
2831
|
+
const displayLang = languageForKind(kind);
|
|
2832
|
+
const isStreaming = !!segment.isStreaming;
|
|
2833
|
+
const previewable = PREVIEWABLE_KINDS.has(kind);
|
|
2834
|
+
const isBinary = BINARY_FILE_KINDS.has(kind);
|
|
2835
|
+
const cleanContent = stripCodeFences(segment.content || "");
|
|
2836
|
+
return `
|
|
2837
|
+
<div class="segment segment-artifact-card"
|
|
2838
|
+
data-segment-id="${escapeHtml(segment.id)}"
|
|
2839
|
+
data-artifact-type="${escapeHtml(kind)}"
|
|
2840
|
+
data-streaming="${isStreaming ? "true" : "false"}"
|
|
2841
|
+
data-tab="code"
|
|
2842
|
+
data-previewable="${previewable ? "true" : "false"}"
|
|
2843
|
+
data-binary="${isBinary ? "true" : "false"}">
|
|
2844
|
+
<header class="aparte-art-card__header">
|
|
2845
|
+
<div class="aparte-art-card__title-block">
|
|
2846
|
+
<span class="aparte-art-card__kind" data-kind="${escapeHtml(kind)}">${escapeHtml(displayLang)}</span>
|
|
2847
|
+
<span class="aparte-art-card__title">${escapeHtml(title)}</span>
|
|
2848
|
+
${isStreaming ? '<span class="aparte-art-card__pulse" aria-label="Streaming"></span>' : ""}
|
|
2849
|
+
</div>
|
|
2850
|
+
<div class="aparte-art-card__actions">
|
|
2851
|
+
<button type="button" class="aparte-art-card__btn" data-action="copy" title="${escapeAttr(contextConfig().t("copy"))}" aria-label="Copy">
|
|
2852
|
+
${contextConfig().getIcon("copy")}
|
|
2853
|
+
</button>
|
|
2854
|
+
<button type="button" class="aparte-art-card__btn" data-action="download" title="Download" aria-label="Download" ${isStreaming ? "disabled" : ""}>
|
|
2855
|
+
<svg width="14" height="14" viewBox="0 0 16 16" fill="none" stroke="currentColor" stroke-width="1.5" stroke-linecap="round" stroke-linejoin="round"><path d="M8 2v9m0 0l-3-3m3 3l3-3M2 13h12"/></svg>
|
|
2856
|
+
</button>
|
|
2857
|
+
</div>
|
|
2858
|
+
</header>
|
|
2859
|
+
<nav class="aparte-art-card__tabs" role="tablist">
|
|
2860
|
+
${previewable ? `<button type="button" role="tab" data-tab-target="preview" aria-selected="false" ${isStreaming ? "disabled" : ""}>Preview</button>` : ""}
|
|
2861
|
+
<button type="button" role="tab" data-tab-target="code" aria-selected="true">Code</button>
|
|
2862
|
+
</nav>
|
|
2863
|
+
<div class="aparte-art-card__body">
|
|
2864
|
+
<div class="aparte-art-card__pane" data-pane="code">
|
|
2865
|
+
<div class="code-content-wrapper">
|
|
2866
|
+
<pre><code class="language-${escapeHtml(displayLang)}">${escapeHtml(cleanContent)}</code></pre>
|
|
2867
|
+
</div>
|
|
2868
|
+
</div>
|
|
2869
|
+
${previewable ? `
|
|
2870
|
+
<div class="aparte-art-card__pane" data-pane="preview">
|
|
2871
|
+
<div class="aparte-art-card__pending">Press Preview to run this artifact.</div>
|
|
2872
|
+
</div>
|
|
2873
|
+
` : ""}
|
|
2874
|
+
</div>
|
|
2875
|
+
</div>
|
|
2876
|
+
`;
|
|
2877
|
+
},
|
|
2878
|
+
setup: (element, segment) => {
|
|
2879
|
+
latestSegment.set(element, segment);
|
|
2880
|
+
const kind = (segment.artifactType || "").toLowerCase();
|
|
2881
|
+
if (BINARY_FILE_KINDS.has(kind)) {
|
|
2882
|
+
setupBinaryFileArtifact(element, segment, kind);
|
|
2883
|
+
return;
|
|
2884
|
+
}
|
|
2885
|
+
const wrapper = element.querySelector(".code-content-wrapper");
|
|
2886
|
+
if (wrapper) {
|
|
2887
|
+
const displayLang = languageForKind(kind);
|
|
2888
|
+
const cleanContent = stripCodeFences(segment.content || "");
|
|
2889
|
+
void contextConfig().highlightCode(cleanContent, displayLang).then((html) => {
|
|
2890
|
+
wrapper.innerHTML = html;
|
|
2891
|
+
}).catch(() => {
|
|
2892
|
+
});
|
|
2893
|
+
}
|
|
2894
|
+
element.querySelectorAll("[data-tab-target]").forEach((btn) => {
|
|
2895
|
+
btn.addEventListener("click", () => {
|
|
2896
|
+
const target = btn.getAttribute("data-tab-target");
|
|
2897
|
+
if (!target) return;
|
|
2898
|
+
if (target === "preview") mountPreviewFrame(element, segment);
|
|
2899
|
+
element.setAttribute("data-tab", target);
|
|
2900
|
+
element.querySelectorAll("[data-tab-target]").forEach((b) => {
|
|
2901
|
+
b.setAttribute("aria-selected", b === btn ? "true" : "false");
|
|
2902
|
+
});
|
|
2903
|
+
});
|
|
2904
|
+
});
|
|
2905
|
+
const copyBtn = element.querySelector('[data-action="copy"]');
|
|
2906
|
+
if (copyBtn) {
|
|
2907
|
+
copyBtn.addEventListener("click", () => {
|
|
2908
|
+
const code = stripCodeFences(segment.content || "");
|
|
2909
|
+
void navigator.clipboard.writeText(code).catch(() => {
|
|
2910
|
+
});
|
|
2911
|
+
const original = copyBtn.innerHTML;
|
|
2912
|
+
copyBtn.innerHTML = contextConfig(copyBtn).getIcon("check");
|
|
2913
|
+
copyBtn.setAttribute("title", contextConfig(copyBtn).t("copied"));
|
|
2914
|
+
setTimeout(() => {
|
|
2915
|
+
copyBtn.innerHTML = original;
|
|
2916
|
+
copyBtn.setAttribute("title", contextConfig(copyBtn).t("copy"));
|
|
2917
|
+
}, 1500);
|
|
2918
|
+
});
|
|
2919
|
+
}
|
|
2920
|
+
const dlBtn = element.querySelector('[data-action="download"]');
|
|
2921
|
+
if (dlBtn) {
|
|
2922
|
+
dlBtn.addEventListener("click", () => {
|
|
2923
|
+
if (dlBtn.disabled) return;
|
|
2924
|
+
const kind2 = (segment.artifactType || "").toLowerCase();
|
|
2925
|
+
const isBinary = BINARY_FILE_KINDS.has(kind2);
|
|
2926
|
+
if (isBinary) {
|
|
2927
|
+
element.dispatchEvent(new CustomEvent("aparte-artifact-redownload", {
|
|
2928
|
+
bubbles: true,
|
|
2929
|
+
composed: true,
|
|
2930
|
+
detail: {
|
|
2931
|
+
segmentId: segment.id,
|
|
2932
|
+
mimeType: segment.mimeType,
|
|
2933
|
+
artifactType: segment.artifactType,
|
|
2934
|
+
title: segment.title,
|
|
2935
|
+
content: stripCodeFences(segment.content || "")
|
|
2936
|
+
}
|
|
2937
|
+
}));
|
|
2938
|
+
return;
|
|
2939
|
+
}
|
|
2940
|
+
downloadTextArtifact(segment);
|
|
2941
|
+
});
|
|
2942
|
+
}
|
|
2943
|
+
},
|
|
2944
|
+
update: (element, segment) => {
|
|
2945
|
+
const previous = latestSegment.get(element);
|
|
2946
|
+
latestSegment.set(element, segment);
|
|
2947
|
+
if (previous && previous.content !== segment.content) {
|
|
2948
|
+
element.querySelector('.aparte-art-card__pane[data-pane="preview"] iframe')?.remove();
|
|
2949
|
+
}
|
|
2950
|
+
const isStreaming = !!segment.isStreaming;
|
|
2951
|
+
const kind = (segment.artifactType || "").toLowerCase();
|
|
2952
|
+
if (BINARY_FILE_KINDS.has(kind)) {
|
|
2953
|
+
updateBinaryFileArtifact(element, segment, isStreaming);
|
|
2954
|
+
return;
|
|
2955
|
+
}
|
|
2956
|
+
const wasStreaming = element.getAttribute("data-streaming") === "true";
|
|
2957
|
+
const cleanContent = stripCodeFences(segment.content || "");
|
|
2958
|
+
const codeEl = element.querySelector(".code-content-wrapper code");
|
|
2959
|
+
if (codeEl) {
|
|
2960
|
+
codeEl.textContent = cleanContent;
|
|
2961
|
+
} else {
|
|
2962
|
+
const wrapper = element.querySelector(".code-content-wrapper");
|
|
2963
|
+
if (wrapper) {
|
|
2964
|
+
const displayLang = languageForKind(kind);
|
|
2965
|
+
wrapper.innerHTML = `<pre><code class="language-${escapeHtml(displayLang)}">${escapeHtml(cleanContent)}</code></pre>`;
|
|
2966
|
+
}
|
|
2967
|
+
}
|
|
2968
|
+
if (isStreaming) {
|
|
2969
|
+
const segId = element.getAttribute("data-segment-id") ?? segment.id;
|
|
2970
|
+
debounceHighlight(element, ".code-content-wrapper", cleanContent, languageForKind(kind), segId);
|
|
2971
|
+
}
|
|
2972
|
+
if (wasStreaming && !isStreaming) {
|
|
2973
|
+
element.setAttribute("data-streaming", "false");
|
|
2974
|
+
const wrapper = element.querySelector(".code-content-wrapper");
|
|
2975
|
+
if (wrapper) {
|
|
2976
|
+
const displayLang = languageForKind(kind);
|
|
2977
|
+
void contextConfig().highlightCode(cleanContent, displayLang).then((html) => {
|
|
2978
|
+
wrapper.innerHTML = html;
|
|
2979
|
+
}).catch(() => {
|
|
2980
|
+
});
|
|
2981
|
+
}
|
|
2982
|
+
element.querySelectorAll("button[disabled]").forEach((b) => {
|
|
2983
|
+
b.disabled = false;
|
|
2984
|
+
});
|
|
2985
|
+
}
|
|
2986
|
+
},
|
|
2987
|
+
getStyles: () => `
|
|
2988
|
+
.segment-artifact-card {
|
|
2989
|
+
display: flex; flex-direction: column;
|
|
2990
|
+
margin: 8px 0;
|
|
2991
|
+
border: 1px solid var(--aparte-border, rgba(0,0,0,0.12));
|
|
2992
|
+
border-radius: 12px;
|
|
2993
|
+
background: var(--aparte-surface, #fff);
|
|
2994
|
+
overflow: hidden;
|
|
2995
|
+
font: inherit;
|
|
2996
|
+
color: var(--aparte-text, inherit);
|
|
2997
|
+
}
|
|
2998
|
+
.aparte-art-card__header {
|
|
2999
|
+
display: flex; align-items: center; justify-content: space-between;
|
|
3000
|
+
padding: 8px 10px;
|
|
3001
|
+
border-bottom: 1px solid var(--aparte-border, rgba(0,0,0,0.08));
|
|
3002
|
+
background: var(--aparte-surface-2, rgba(0,0,0,0.02));
|
|
3003
|
+
min-height: 36px;
|
|
3004
|
+
}
|
|
3005
|
+
.aparte-art-card__title-block { display: flex; align-items: center; gap: 8px; min-width: 0; }
|
|
3006
|
+
.aparte-art-card__kind {
|
|
3007
|
+
font-size: 0.7rem; font-weight: 600;
|
|
3008
|
+
text-transform: uppercase; letter-spacing: 0.04em;
|
|
3009
|
+
padding: 2px 6px; border-radius: 4px;
|
|
3010
|
+
background: var(--aparte-surface, #fff);
|
|
3011
|
+
border: 1px solid var(--aparte-border, rgba(0,0,0,0.1));
|
|
3012
|
+
color: var(--aparte-text-muted, rgba(0,0,0,0.6));
|
|
3013
|
+
}
|
|
3014
|
+
.aparte-art-card__title {
|
|
3015
|
+
font-size: 0.85rem; font-weight: 500;
|
|
3016
|
+
overflow: hidden; text-overflow: ellipsis; white-space: nowrap;
|
|
3017
|
+
}
|
|
3018
|
+
.aparte-art-card__pulse {
|
|
3019
|
+
width: 8px; height: 8px; border-radius: 50%;
|
|
3020
|
+
background: var(--aparte-accent, #3b82f6);
|
|
3021
|
+
animation: aparte-art-card-pulse 1.2s ease-in-out infinite;
|
|
3022
|
+
}
|
|
3023
|
+
@keyframes aparte-art-card-pulse {
|
|
3024
|
+
0%, 100% { opacity: 0.35; transform: scale(0.85); }
|
|
3025
|
+
50% { opacity: 1; transform: scale(1.1); }
|
|
3026
|
+
}
|
|
3027
|
+
.aparte-art-card__actions { display: flex; gap: 4px; }
|
|
3028
|
+
.aparte-art-card__btn {
|
|
3029
|
+
display: inline-flex; align-items: center; justify-content: center;
|
|
3030
|
+
width: 28px; height: 28px;
|
|
3031
|
+
background: transparent;
|
|
3032
|
+
border: 1px solid transparent;
|
|
3033
|
+
border-radius: 6px;
|
|
3034
|
+
color: var(--aparte-text-muted, rgba(0,0,0,0.55));
|
|
3035
|
+
cursor: pointer;
|
|
3036
|
+
transition: background 0.12s, border-color 0.12s, color 0.12s;
|
|
3037
|
+
}
|
|
3038
|
+
.aparte-art-card__btn:hover:not(:disabled) {
|
|
3039
|
+
background: var(--aparte-surface-hover, rgba(0,0,0,0.04));
|
|
3040
|
+
border-color: var(--aparte-border, rgba(0,0,0,0.1));
|
|
3041
|
+
color: var(--aparte-text, inherit);
|
|
3042
|
+
}
|
|
3043
|
+
.aparte-art-card__btn:disabled { opacity: 0.4; cursor: not-allowed; }
|
|
3044
|
+
.aparte-art-card__tabs {
|
|
3045
|
+
display: flex; gap: 2px;
|
|
3046
|
+
padding: 4px 8px 0;
|
|
3047
|
+
border-bottom: 1px solid var(--aparte-border, rgba(0,0,0,0.08));
|
|
3048
|
+
background: var(--aparte-surface-2, rgba(0,0,0,0.02));
|
|
3049
|
+
}
|
|
3050
|
+
.aparte-art-card__tabs button {
|
|
3051
|
+
padding: 6px 12px;
|
|
3052
|
+
font-size: 0.78rem;
|
|
3053
|
+
background: transparent;
|
|
3054
|
+
border: 1px solid transparent;
|
|
3055
|
+
border-bottom: none;
|
|
3056
|
+
border-radius: 6px 6px 0 0;
|
|
3057
|
+
color: var(--aparte-text-muted, rgba(0,0,0,0.6));
|
|
3058
|
+
cursor: pointer;
|
|
3059
|
+
transition: background 0.12s, color 0.12s;
|
|
3060
|
+
}
|
|
3061
|
+
.aparte-art-card__tabs button[aria-selected="true"] {
|
|
3062
|
+
background: var(--aparte-surface, #fff);
|
|
3063
|
+
border-color: var(--aparte-border, rgba(0,0,0,0.08));
|
|
3064
|
+
color: var(--aparte-text, inherit);
|
|
3065
|
+
font-weight: 500;
|
|
3066
|
+
}
|
|
3067
|
+
.aparte-art-card__tabs button:disabled { opacity: 0.4; cursor: not-allowed; }
|
|
3068
|
+
.aparte-art-card__body {
|
|
3069
|
+
position: relative;
|
|
3070
|
+
min-height: 80px;
|
|
3071
|
+
max-height: 600px;
|
|
3072
|
+
overflow: hidden;
|
|
3073
|
+
}
|
|
3074
|
+
.aparte-art-card__pane { display: none; height: 100%; }
|
|
3075
|
+
.segment-artifact-card[data-tab="code"] .aparte-art-card__pane[data-pane="code"] { display: block; }
|
|
3076
|
+
.segment-artifact-card[data-tab="preview"] .aparte-art-card__pane[data-pane="preview"] { display: block; }
|
|
3077
|
+
.aparte-art-card__pane[data-pane="code"] {
|
|
3078
|
+
max-height: 600px; overflow: auto;
|
|
3079
|
+
}
|
|
3080
|
+
.aparte-art-card__pane[data-pane="code"] pre {
|
|
3081
|
+
margin: 0; padding: 12px;
|
|
3082
|
+
font-size: 0.82rem;
|
|
3083
|
+
background: var(--aparte-code-bg, #fafafa);
|
|
3084
|
+
}
|
|
3085
|
+
.aparte-art-card__frame {
|
|
3086
|
+
display: block;
|
|
3087
|
+
width: 100%; height: 480px;
|
|
3088
|
+
border: 0;
|
|
3089
|
+
background: #fff;
|
|
3090
|
+
}
|
|
3091
|
+
.aparte-art-card__pending {
|
|
3092
|
+
display: flex; align-items: center; justify-content: center;
|
|
3093
|
+
height: 120px;
|
|
3094
|
+
color: var(--aparte-text-muted, rgba(0,0,0,0.5));
|
|
3095
|
+
font-size: 0.85rem;
|
|
3096
|
+
font-style: italic;
|
|
3097
|
+
}
|
|
3098
|
+
/* ── Binary file artifact (xlsx/pdf/docx) ──────────────────── */
|
|
3099
|
+
.segment-artifact-file {
|
|
3100
|
+
display: flex; flex-direction: column;
|
|
3101
|
+
margin: 8px 0;
|
|
3102
|
+
border: 1px solid var(--aparte-border, rgba(0,0,0,0.12));
|
|
3103
|
+
border-radius: 12px;
|
|
3104
|
+
background: var(--aparte-surface, #fff);
|
|
3105
|
+
overflow: hidden;
|
|
3106
|
+
color: var(--aparte-text, inherit);
|
|
3107
|
+
font: inherit;
|
|
3108
|
+
}
|
|
3109
|
+
.aparte-art-file__card {
|
|
3110
|
+
display: flex; align-items: center; gap: 12px;
|
|
3111
|
+
padding: 12px 14px;
|
|
3112
|
+
background: var(--aparte-surface-2, rgba(0,0,0,0.02));
|
|
3113
|
+
border-bottom: 1px solid var(--aparte-border, rgba(0,0,0,0.08));
|
|
3114
|
+
}
|
|
3115
|
+
.aparte-art-file__body {
|
|
3116
|
+
position: relative;
|
|
3117
|
+
}
|
|
3118
|
+
.aparte-art-file__code-pane {
|
|
3119
|
+
max-height: 360px; overflow: auto;
|
|
3120
|
+
background: var(--aparte-code-bg, #fafafa);
|
|
3121
|
+
}
|
|
3122
|
+
.aparte-art-file__code-pane pre {
|
|
3123
|
+
margin: 0; padding: 12px;
|
|
3124
|
+
font-size: 0.82rem;
|
|
3125
|
+
font-family: 'JetBrains Mono', 'Fira Code', Consolas, monospace;
|
|
3126
|
+
}
|
|
3127
|
+
.aparte-art-file__preview-pane {
|
|
3128
|
+
max-height: 460px;
|
|
3129
|
+
overflow: auto;
|
|
3130
|
+
background: #fff;
|
|
3131
|
+
/* Preview is a document view — force light scheme regardless of
|
|
3132
|
+
the app theme, with a dark text colour so cells stay readable. */
|
|
3133
|
+
color: #1f2937;
|
|
3134
|
+
}
|
|
3135
|
+
.aparte-art-file__icon {
|
|
3136
|
+
width: 40px; height: 40px;
|
|
3137
|
+
border-radius: 8px;
|
|
3138
|
+
display: flex; align-items: center; justify-content: center;
|
|
3139
|
+
background: linear-gradient(135deg, #1d6f42, #0f5132);
|
|
3140
|
+
color: #fff;
|
|
3141
|
+
font-weight: 700;
|
|
3142
|
+
font-size: 0.78rem;
|
|
3143
|
+
letter-spacing: 0.04em;
|
|
3144
|
+
flex-shrink: 0;
|
|
3145
|
+
}
|
|
3146
|
+
.aparte-art-file__icon[data-kind="pdf"] { background: linear-gradient(135deg, #c0392b, #7d1f17); }
|
|
3147
|
+
.aparte-art-file__icon[data-kind="docx"] { background: linear-gradient(135deg, #1e5288, #0f3060); }
|
|
3148
|
+
.aparte-art-file__meta { flex: 1 1 auto; min-width: 0; }
|
|
3149
|
+
.aparte-art-file__meta-name {
|
|
3150
|
+
font-weight: 600; font-size: 0.92rem;
|
|
3151
|
+
white-space: nowrap; overflow: hidden; text-overflow: ellipsis;
|
|
3152
|
+
}
|
|
3153
|
+
.aparte-art-file__meta-sub {
|
|
3154
|
+
font-size: 0.78rem;
|
|
3155
|
+
color: var(--aparte-text-muted, rgba(0,0,0,0.55));
|
|
3156
|
+
}
|
|
3157
|
+
.aparte-art-file__actions { display: flex; gap: 6px; flex-shrink: 0; }
|
|
3158
|
+
.aparte-art-file__btn {
|
|
3159
|
+
border: 1px solid var(--aparte-border, rgba(0,0,0,0.12));
|
|
3160
|
+
background: var(--aparte-surface, #fff);
|
|
3161
|
+
color: var(--aparte-text, inherit);
|
|
3162
|
+
padding: 6px 10px;
|
|
3163
|
+
border-radius: 6px;
|
|
3164
|
+
font-size: 0.8rem;
|
|
3165
|
+
cursor: pointer;
|
|
3166
|
+
transition: background 0.12s ease;
|
|
3167
|
+
}
|
|
3168
|
+
.aparte-art-file__btn:hover { background: var(--aparte-surface-2, rgba(0,0,0,0.05)); }
|
|
3169
|
+
.aparte-art-file__btn--primary {
|
|
3170
|
+
background: var(--aparte-primary, #6366f1);
|
|
3171
|
+
color: #fff;
|
|
3172
|
+
border-color: var(--aparte-primary, #6366f1);
|
|
3173
|
+
}
|
|
3174
|
+
.aparte-art-file__btn--primary:hover { filter: brightness(1.07); }
|
|
3175
|
+
/* Hide the download button until the sandbox has produced the buffer.
|
|
3176
|
+
Avoids confusing the user with a disabled-but-styled-primary button
|
|
3177
|
+
during streaming / compiling. */
|
|
3178
|
+
.segment-artifact-file:not([data-state="ready"]) .aparte-art-file__btn[data-action="download"] {
|
|
3179
|
+
display: none;
|
|
3180
|
+
}
|
|
3181
|
+
.aparte-art-file__preview-pane table {
|
|
3182
|
+
border-collapse: collapse;
|
|
3183
|
+
width: 100%;
|
|
3184
|
+
font-size: 0.82rem;
|
|
3185
|
+
font-family: inherit;
|
|
3186
|
+
}
|
|
3187
|
+
.aparte-art-file__preview-pane th,
|
|
3188
|
+
.aparte-art-file__preview-pane td {
|
|
3189
|
+
border: 1px solid rgba(0,0,0,0.10);
|
|
3190
|
+
padding: 6px 10px;
|
|
3191
|
+
text-align: left;
|
|
3192
|
+
white-space: nowrap;
|
|
3193
|
+
color: #1f2937;
|
|
3194
|
+
background: #fff;
|
|
3195
|
+
}
|
|
3196
|
+
.aparte-art-file__preview-pane tr:nth-child(odd) td { background: #f9fafb; }
|
|
3197
|
+
.aparte-art-file__preview-pane tr:first-child td {
|
|
3198
|
+
background: #f3f4f6;
|
|
3199
|
+
font-weight: 600;
|
|
3200
|
+
position: sticky; top: 0;
|
|
3201
|
+
color: #111827;
|
|
3202
|
+
}
|
|
3203
|
+
.aparte-art-file__preview-empty {
|
|
3204
|
+
padding: 20px; text-align: center;
|
|
3205
|
+
color: var(--aparte-text-muted, rgba(0,0,0,0.5));
|
|
3206
|
+
font-size: 0.85rem; font-style: italic;
|
|
3207
|
+
}
|
|
3208
|
+
.segment-artifact-file[data-state="error"] .aparte-art-file__icon {
|
|
3209
|
+
background: linear-gradient(135deg, #c0392b, #7d1f17);
|
|
3210
|
+
}
|
|
3211
|
+
.aparte-art-file__error {
|
|
3212
|
+
padding: 16px 18px;
|
|
3213
|
+
background: rgba(239, 68, 68, 0.05);
|
|
3214
|
+
border-top: 1px solid rgba(239, 68, 68, 0.2);
|
|
3215
|
+
color: var(--aparte-text, inherit);
|
|
3216
|
+
}
|
|
3217
|
+
.aparte-art-file__error-title {
|
|
3218
|
+
font-weight: 600;
|
|
3219
|
+
font-size: 0.9rem;
|
|
3220
|
+
margin-bottom: 6px;
|
|
3221
|
+
color: #b91c1c;
|
|
3222
|
+
}
|
|
3223
|
+
.aparte-art-file__error-msg {
|
|
3224
|
+
font-family: 'JetBrains Mono', 'Fira Code', Consolas, monospace;
|
|
3225
|
+
font-size: 0.78rem;
|
|
3226
|
+
padding: 6px 10px;
|
|
3227
|
+
background: rgba(0,0,0,0.04);
|
|
3228
|
+
border-radius: 4px;
|
|
3229
|
+
margin-bottom: 8px;
|
|
3230
|
+
color: #7f1d1d;
|
|
3231
|
+
word-break: break-word;
|
|
3232
|
+
}
|
|
3233
|
+
.aparte-art-file__error-hint {
|
|
3234
|
+
font-size: 0.78rem;
|
|
3235
|
+
color: var(--aparte-text-muted, rgba(0,0,0,0.55));
|
|
3236
|
+
font-style: italic;
|
|
3237
|
+
}
|
|
3238
|
+
`
|
|
3239
|
+
};
|
|
2994
3240
|
function languageForKind(kind) {
|
|
2995
3241
|
if (kind === "react") return "jsx";
|
|
2996
3242
|
if (kind === "markdown") return "md";
|
|
@@ -3030,19 +3276,6 @@ function downloadTextArtifact(segment) {
|
|
|
3030
3276
|
document.body.removeChild(a);
|
|
3031
3277
|
setTimeout(() => URL.revokeObjectURL(url), 1e3);
|
|
3032
3278
|
}
|
|
3033
|
-
function escapeAttr(s) {
|
|
3034
|
-
let out = "";
|
|
3035
|
-
for (let i = 0; i < s.length; i++) {
|
|
3036
|
-
const ch = s[i];
|
|
3037
|
-
if (ch === "&") out += "&";
|
|
3038
|
-
else if (ch === '"') out += """;
|
|
3039
|
-
else if (ch === "<") out += "<";
|
|
3040
|
-
else if (ch === ">") out += ">";
|
|
3041
|
-
else if (ch === "'") out += "'";
|
|
3042
|
-
else out += ch;
|
|
3043
|
-
}
|
|
3044
|
-
return out;
|
|
3045
|
-
}
|
|
3046
3279
|
function slugifyForFilename(text) {
|
|
3047
3280
|
const lower = text.trim().toLowerCase();
|
|
3048
3281
|
let out = "";
|
|
@@ -3063,63 +3296,90 @@ function slugifyForFilename(text) {
|
|
|
3063
3296
|
if (out.endsWith("-")) out = out.slice(0, -1);
|
|
3064
3297
|
return out;
|
|
3065
3298
|
}
|
|
3066
|
-
|
|
3067
|
-
|
|
3068
|
-
|
|
3069
|
-
|
|
3070
|
-
|
|
3071
|
-
|
|
3072
|
-
|
|
3073
|
-
|
|
3074
|
-
|
|
3075
|
-
|
|
3076
|
-
|
|
3077
|
-
|
|
3078
|
-
|
|
3079
|
-
|
|
3080
|
-
|
|
3081
|
-
|
|
3082
|
-
|
|
3083
|
-
|
|
3084
|
-
|
|
3085
|
-
|
|
3086
|
-
|
|
3087
|
-
|
|
3088
|
-
|
|
3089
|
-
|
|
3090
|
-
|
|
3091
|
-
|
|
3092
|
-
|
|
3093
|
-
|
|
3094
|
-
|
|
3095
|
-
|
|
3096
|
-
|
|
3097
|
-
|
|
3098
|
-
|
|
3299
|
+
const latestSegment = /* @__PURE__ */ new WeakMap();
|
|
3300
|
+
function mountPreviewFrame(element, fallback) {
|
|
3301
|
+
const pane = element.querySelector('.aparte-art-card__pane[data-pane="preview"]');
|
|
3302
|
+
if (!pane || pane.querySelector("iframe")) return;
|
|
3303
|
+
const segment = latestSegment.get(element) ?? fallback;
|
|
3304
|
+
const kind = segment.artifactType || deriveArtifactKind(segment.mimeType ?? "", "text");
|
|
3305
|
+
if (!PREVIEWABLE_KINDS.has(kind)) return;
|
|
3306
|
+
const title = segment.title?.trim() || labelForKind(kind);
|
|
3307
|
+
const build = contextConfig().getArtifactPreviewBuilder() ?? buildSafePreviewDocument;
|
|
3308
|
+
const srcdoc = build(kind, stripCodeFences(segment.content || ""), title);
|
|
3309
|
+
pane.innerHTML = `<iframe class="aparte-art-card__frame" sandbox="allow-scripts" csp="${escapeAttr(PREVIEW_CSP)}" referrerpolicy="no-referrer" loading="lazy" title="${escapeAttr(title)}" srcdoc="${escapeAttr(srcdoc)}"></iframe>`;
|
|
3310
|
+
}
|
|
3311
|
+
const registries = /* @__PURE__ */ new WeakMap();
|
|
3312
|
+
function registryFor(config) {
|
|
3313
|
+
let reg = registries.get(config);
|
|
3314
|
+
if (!reg) {
|
|
3315
|
+
reg = { renderers: /* @__PURE__ */ new Map(), defaultsInstalled: false, defaultsDeclined: false };
|
|
3316
|
+
registries.set(config, reg);
|
|
3317
|
+
}
|
|
3318
|
+
return reg;
|
|
3319
|
+
}
|
|
3320
|
+
function effectiveRenderers(config) {
|
|
3321
|
+
const own = registryFor(config);
|
|
3322
|
+
if (config === aparteGlobalConfig || own.defaultsDeclined) return own.renderers;
|
|
3323
|
+
const merged = new Map(registryFor(aparteGlobalConfig).renderers);
|
|
3324
|
+
for (const [type, renderer] of own.renderers) merged.set(type, renderer);
|
|
3325
|
+
return merged;
|
|
3326
|
+
}
|
|
3327
|
+
let styleElement = null;
|
|
3328
|
+
function registerSegmentRenderer(renderer, config = contextConfig()) {
|
|
3329
|
+
registryFor(config).renderers.set(renderer.type, renderer);
|
|
3330
|
+
injectRendererStyles(config);
|
|
3331
|
+
}
|
|
3332
|
+
function declineDefaultRenderers(config = contextConfig()) {
|
|
3333
|
+
registryFor(config).defaultsDeclined = true;
|
|
3334
|
+
}
|
|
3335
|
+
function installDefaultRenderersOnce(config = contextConfig()) {
|
|
3336
|
+
const reg = registryFor(config);
|
|
3337
|
+
if (reg.defaultsInstalled || reg.defaultsDeclined) return;
|
|
3338
|
+
reg.defaultsInstalled = true;
|
|
3339
|
+
for (const renderer of DEFAULT_RENDERERS) {
|
|
3340
|
+
if (!reg.renderers.has(renderer.type)) reg.renderers.set(renderer.type, renderer);
|
|
3099
3341
|
}
|
|
3342
|
+
injectRendererStyles(config);
|
|
3343
|
+
installArtifactReadyHook();
|
|
3100
3344
|
}
|
|
3101
|
-
function
|
|
3102
|
-
|
|
3103
|
-
while (i < s.length && (s[i] === " " || s[i] === " " || s[i] === "\n" || s[i] === "\r")) i++;
|
|
3104
|
-
const probe = s.slice(i, i + 9).toLowerCase();
|
|
3105
|
-
return probe === "<!doctype";
|
|
3345
|
+
function unregisterSegmentRenderer(type, config = contextConfig()) {
|
|
3346
|
+
registryFor(config).renderers.delete(type);
|
|
3106
3347
|
}
|
|
3107
|
-
function
|
|
3108
|
-
|
|
3109
|
-
|
|
3110
|
-
|
|
3111
|
-
|
|
3112
|
-
|
|
3113
|
-
|
|
3114
|
-
|
|
3115
|
-
|
|
3116
|
-
|
|
3117
|
-
|
|
3118
|
-
|
|
3119
|
-
|
|
3120
|
-
|
|
3348
|
+
function getSegmentRenderer(type, config = contextConfig()) {
|
|
3349
|
+
return effectiveRenderers(config).get(type);
|
|
3350
|
+
}
|
|
3351
|
+
function getAllRenderers(config = contextConfig()) {
|
|
3352
|
+
return Array.from(effectiveRenderers(config).values());
|
|
3353
|
+
}
|
|
3354
|
+
function collectRendererStyles(config = contextConfig()) {
|
|
3355
|
+
return Array.from(registryFor(config).renderers.values()).map((r) => r.getStyles?.() || "").filter(Boolean).join("\n");
|
|
3356
|
+
}
|
|
3357
|
+
const injectedRendererStyles = /* @__PURE__ */ new Set();
|
|
3358
|
+
function injectRendererStyles(config = contextConfig()) {
|
|
3359
|
+
if (typeof document === "undefined") return;
|
|
3360
|
+
if (!styleElement || !styleElement.isConnected) {
|
|
3361
|
+
styleElement = document.createElement("style");
|
|
3362
|
+
styleElement.id = "aparte-renderer-styles";
|
|
3363
|
+
document.head.appendChild(styleElement);
|
|
3364
|
+
styleElement.textContent = Array.from(injectedRendererStyles).join("\n");
|
|
3121
3365
|
}
|
|
3122
|
-
|
|
3366
|
+
let added = false;
|
|
3367
|
+
for (const renderer of registryFor(config).renderers.values()) {
|
|
3368
|
+
const css = renderer.getStyles?.();
|
|
3369
|
+
if (!css || injectedRendererStyles.has(css)) continue;
|
|
3370
|
+
injectedRendererStyles.add(css);
|
|
3371
|
+
added = true;
|
|
3372
|
+
}
|
|
3373
|
+
if (added) styleElement.textContent = Array.from(injectedRendererStyles).join("\n");
|
|
3374
|
+
}
|
|
3375
|
+
function registerDefaultRenderers(config = contextConfig()) {
|
|
3376
|
+
const reg = registryFor(config);
|
|
3377
|
+
reg.defaultsInstalled = true;
|
|
3378
|
+
for (const renderer of DEFAULT_RENDERERS) {
|
|
3379
|
+
if (!reg.renderers.has(renderer.type)) registerSegmentRenderer(renderer, config);
|
|
3380
|
+
}
|
|
3381
|
+
injectRendererStyles(config);
|
|
3382
|
+
installArtifactReadyHook();
|
|
3123
3383
|
}
|
|
3124
3384
|
const DEFAULT_RENDERERS = [
|
|
3125
3385
|
textRenderer,
|
|
@@ -3133,11 +3393,64 @@ const DEFAULT_RENDERERS = [
|
|
|
3133
3393
|
artifactRenderer,
|
|
3134
3394
|
pipelineWaitingRenderer
|
|
3135
3395
|
];
|
|
3396
|
+
const OPEN_TOOL_STATUSES = /* @__PURE__ */ new Set(["pending", "awaiting-approval"]);
|
|
3397
|
+
const ACTIVITY_KEYS = ["content", "output", "result"];
|
|
3398
|
+
function isActivity(updates) {
|
|
3399
|
+
return ACTIVITY_KEYS.some((key) => key in updates);
|
|
3400
|
+
}
|
|
3401
|
+
function stampSegmentOnInsert(segments, segment, messageId) {
|
|
3402
|
+
return {
|
|
3403
|
+
...segment,
|
|
3404
|
+
messageId: segment.messageId ?? messageId,
|
|
3405
|
+
index: segment.index ?? segments.length,
|
|
3406
|
+
startedAt: segment.startedAt ?? Date.now()
|
|
3407
|
+
};
|
|
3408
|
+
}
|
|
3409
|
+
function isSegmentSettled(segment) {
|
|
3410
|
+
if (segment.type === "tool_call") return !OPEN_TOOL_STATUSES.has(segment.status);
|
|
3411
|
+
return segment.isStreaming === false;
|
|
3412
|
+
}
|
|
3413
|
+
function stampSegmentOnUpdate(segment, updates) {
|
|
3414
|
+
if (isSegmentSettled(segment)) return updates;
|
|
3415
|
+
const merged = { ...segment, ...updates };
|
|
3416
|
+
if (isSegmentSettled(merged)) {
|
|
3417
|
+
return { ...updates, endedAt: segment.endedAt ?? Date.now() };
|
|
3418
|
+
}
|
|
3419
|
+
return isActivity(updates) ? { ...updates, endedAt: Date.now() } : updates;
|
|
3420
|
+
}
|
|
3421
|
+
function segmentDuration(segment) {
|
|
3422
|
+
const { startedAt, endedAt } = segment;
|
|
3423
|
+
if (startedAt == null || endedAt == null) return void 0;
|
|
3424
|
+
return endedAt - startedAt;
|
|
3425
|
+
}
|
|
3426
|
+
function stampSegmentActivity(segment) {
|
|
3427
|
+
return isSegmentSettled(segment) ? {} : { endedAt: Date.now() };
|
|
3428
|
+
}
|
|
3429
|
+
function segmentContentUpdate(segment) {
|
|
3430
|
+
const content = segment.content;
|
|
3431
|
+
return {
|
|
3432
|
+
content,
|
|
3433
|
+
...segment.isStreaming === false ? { isStreaming: false } : {}
|
|
3434
|
+
};
|
|
3435
|
+
}
|
|
3436
|
+
function isTerminalStatus(status) {
|
|
3437
|
+
return status != null && status !== "streaming" && status !== "pending";
|
|
3438
|
+
}
|
|
3439
|
+
function openSegmentIds(segments) {
|
|
3440
|
+
return segments.filter((segment) => !isSegmentSettled(segment)).map((segment) => segment.id);
|
|
3441
|
+
}
|
|
3442
|
+
function renumberSegments(segments) {
|
|
3443
|
+
for (let i = 0; i < segments.length; i++) {
|
|
3444
|
+
const segment = segments[i];
|
|
3445
|
+
if (segment.index === i) continue;
|
|
3446
|
+
segments[i] = { ...segment, index: i };
|
|
3447
|
+
}
|
|
3448
|
+
}
|
|
3136
3449
|
function populateBubbleFromMessage(bubble, message, siblingInfo) {
|
|
3137
3450
|
if (message.segments && message.segments.length > 0) {
|
|
3138
3451
|
const existing = bubble.getSegments?.() ?? [];
|
|
3139
3452
|
if (existing.length === message.segments.length) {
|
|
3140
|
-
const settled = message.status
|
|
3453
|
+
const settled = isTerminalStatus(message.status);
|
|
3141
3454
|
for (const seg of message.segments) {
|
|
3142
3455
|
if ("content" in seg) {
|
|
3143
3456
|
const patch = { content: seg.content };
|
|
@@ -3191,7 +3504,7 @@ function applyRetention(messages, tree, maxMessages) {
|
|
|
3191
3504
|
};
|
|
3192
3505
|
return { messages: trimmedFlat, tree: trimmedTree };
|
|
3193
3506
|
}
|
|
3194
|
-
class
|
|
3507
|
+
class AparteConversationManager {
|
|
3195
3508
|
_adapter;
|
|
3196
3509
|
_conversations = [];
|
|
3197
3510
|
_activeId = null;
|
|
@@ -3243,7 +3556,7 @@ class ConversationManager {
|
|
|
3243
3556
|
async createNew(title = "New Chat") {
|
|
3244
3557
|
const now = Date.now();
|
|
3245
3558
|
const conv = {
|
|
3246
|
-
id:
|
|
3559
|
+
id: uuid(),
|
|
3247
3560
|
title,
|
|
3248
3561
|
createdAt: now,
|
|
3249
3562
|
updatedAt: now,
|
|
@@ -3374,7 +3687,7 @@ class ConversationManager {
|
|
|
3374
3687
|
try {
|
|
3375
3688
|
l(snapshot);
|
|
3376
3689
|
} catch (err) {
|
|
3377
|
-
console.error("[
|
|
3690
|
+
console.error("[AparteConversationManager] listener threw:", err);
|
|
3378
3691
|
}
|
|
3379
3692
|
});
|
|
3380
3693
|
}
|
|
@@ -3384,12 +3697,9 @@ class ConversationManager {
|
|
|
3384
3697
|
return text.trim() || "New Chat";
|
|
3385
3698
|
}
|
|
3386
3699
|
}
|
|
3387
|
-
function attachmentId() {
|
|
3388
|
-
return typeof crypto !== "undefined" && crypto.randomUUID ? crypto.randomUUID() : `${Date.now()}-${Math.random().toString(36).slice(2)}`;
|
|
3389
|
-
}
|
|
3390
3700
|
function filesToAttachments(files) {
|
|
3391
3701
|
return files.map((file) => ({
|
|
3392
|
-
id:
|
|
3702
|
+
id: uuid(),
|
|
3393
3703
|
name: file.name,
|
|
3394
3704
|
type: file.type || "application/octet-stream",
|
|
3395
3705
|
url: URL.createObjectURL(file),
|
|
@@ -3397,6 +3707,13 @@ function filesToAttachments(files) {
|
|
|
3397
3707
|
blob: file
|
|
3398
3708
|
}));
|
|
3399
3709
|
}
|
|
3710
|
+
function revokeAttachmentUrls(attachments) {
|
|
3711
|
+
if (!attachments?.length) return;
|
|
3712
|
+
if (typeof URL === "undefined" || typeof URL.revokeObjectURL !== "function") return;
|
|
3713
|
+
for (const attachment of attachments) {
|
|
3714
|
+
if (attachment.url?.startsWith("blob:")) URL.revokeObjectURL(attachment.url);
|
|
3715
|
+
}
|
|
3716
|
+
}
|
|
3400
3717
|
class AparteConversationController {
|
|
3401
3718
|
_binding;
|
|
3402
3719
|
_options;
|
|
@@ -3428,7 +3745,30 @@ class AparteConversationController {
|
|
|
3428
3745
|
* conversation lifecycle yet.
|
|
3429
3746
|
*/
|
|
3430
3747
|
_manager() {
|
|
3431
|
-
|
|
3748
|
+
if (this._options.manager) return this._options.manager;
|
|
3749
|
+
const scoped = resolveConfig(this._binding.host);
|
|
3750
|
+
const manager = scoped.getConversationManager();
|
|
3751
|
+
if (!manager) this._warnManagerOnTheWrongConfig(scoped);
|
|
3752
|
+
return manager;
|
|
3753
|
+
}
|
|
3754
|
+
/**
|
|
3755
|
+
* Degraded mode is legitimate — a host that never opted into the conversation
|
|
3756
|
+
* lifecycle has no manager and should not be nagged. Registering one on the
|
|
3757
|
+
* WRONG config is not: the four wrapper hooks used to write it to the global
|
|
3758
|
+
* singleton unconditionally, so `config` + persistence looked like it worked
|
|
3759
|
+
* (the optimistic UI still appends) and saved nothing at all.
|
|
3760
|
+
*
|
|
3761
|
+
* Warned once per controller, and only in the case that is unambiguously a
|
|
3762
|
+
* mistake: no manager here, but one on the global.
|
|
3763
|
+
*/
|
|
3764
|
+
_warnedNoManager = false;
|
|
3765
|
+
_warnManagerOnTheWrongConfig(scoped) {
|
|
3766
|
+
if (this._warnedNoManager) return;
|
|
3767
|
+
if (scoped === aparteGlobalConfig || !aparteGlobalConfig.getConversationManager()) return;
|
|
3768
|
+
this._warnedNoManager = true;
|
|
3769
|
+
console.warn(
|
|
3770
|
+
"[aparte] This chat resolves its own config, but the conversation manager was registered on aparteGlobalConfig — so nothing is being persisted. Pass the same config to init(): `init(adapter, myConfig)`, using the config you gave the chat."
|
|
3771
|
+
);
|
|
3432
3772
|
}
|
|
3433
3773
|
/**
|
|
3434
3774
|
* Attach event listeners to the binding host. Returns an unbind function.
|
|
@@ -3443,7 +3783,7 @@ class AparteConversationController {
|
|
|
3443
3783
|
const files = evt.detail?.files;
|
|
3444
3784
|
const attachments = files?.length ? filesToAttachments(files) : void 0;
|
|
3445
3785
|
const userMsg = {
|
|
3446
|
-
id:
|
|
3786
|
+
id: uuid(),
|
|
3447
3787
|
role: "user",
|
|
3448
3788
|
content,
|
|
3449
3789
|
timestamp: evt.detail?.timestamp ?? Date.now(),
|
|
@@ -3510,8 +3850,12 @@ class AparteConversationController {
|
|
|
3510
3850
|
return () => this.unbind();
|
|
3511
3851
|
}
|
|
3512
3852
|
/** Detach event listeners. */
|
|
3853
|
+
/** Release handle for the hydration-retry subscription (see setConversationId). */
|
|
3854
|
+
_stopHydrationRetry = null;
|
|
3513
3855
|
unbind() {
|
|
3514
3856
|
this._persistActive();
|
|
3857
|
+
this._stopHydrationRetry?.();
|
|
3858
|
+
this._stopHydrationRetry = null;
|
|
3515
3859
|
const host = this._binding.host;
|
|
3516
3860
|
if (this._onSendCapture) {
|
|
3517
3861
|
host.removeEventListener("aparte-send", this._onSendCapture, { capture: true });
|
|
@@ -3589,8 +3933,10 @@ class AparteConversationController {
|
|
|
3589
3933
|
const conv = manager?.conversations.find((c) => c.id === id);
|
|
3590
3934
|
if (!manager || !conv) {
|
|
3591
3935
|
if (manager && !manager.initialized) {
|
|
3936
|
+
this._stopHydrationRetry?.();
|
|
3592
3937
|
const stop = manager.subscribe(() => {
|
|
3593
3938
|
stop();
|
|
3939
|
+
this._stopHydrationRetry = null;
|
|
3594
3940
|
if (manager.conversations.some((c) => c.id === id)) {
|
|
3595
3941
|
void this.setConversationId(id);
|
|
3596
3942
|
} else if (this._activeId === null) {
|
|
@@ -3599,6 +3945,7 @@ class AparteConversationController {
|
|
|
3599
3945
|
this._binding.clearMessages();
|
|
3600
3946
|
}
|
|
3601
3947
|
});
|
|
3948
|
+
this._stopHydrationRetry = stop;
|
|
3602
3949
|
return;
|
|
3603
3950
|
}
|
|
3604
3951
|
console.warn("[ConversationController] unknown id or no manager — clearing. manager:", !!manager, "conv:", !!conv);
|
|
@@ -3680,6 +4027,9 @@ class AparteConversationController {
|
|
|
3680
4027
|
await manager.addMessage(convId, userMsg);
|
|
3681
4028
|
}
|
|
3682
4029
|
}
|
|
4030
|
+
function cssEscape(value) {
|
|
4031
|
+
return value.replace(/["\\]/g, "\\$&");
|
|
4032
|
+
}
|
|
3683
4033
|
class AparteChatHost {
|
|
3684
4034
|
binding;
|
|
3685
4035
|
options;
|
|
@@ -3786,7 +4136,7 @@ class AparteChatHost {
|
|
|
3786
4136
|
const convBinding = {
|
|
3787
4137
|
hostId: this.binding.hostId,
|
|
3788
4138
|
host,
|
|
3789
|
-
// Controller-driven flushes mirror
|
|
4139
|
+
// Controller-driven flushes mirror AparteConversationManager state, not
|
|
3790
4140
|
// user intent — deliberately NOT emitted as `messagesChange` to
|
|
3791
4141
|
// avoid a feedback loop with hosts using a two-way `messages` input.
|
|
3792
4142
|
setMessages: (msgs) => {
|
|
@@ -3856,12 +4206,18 @@ class AparteChatHost {
|
|
|
3856
4206
|
const index = msgs.findIndex((m) => m.id === messageId);
|
|
3857
4207
|
if (index === -1) return;
|
|
3858
4208
|
const next = [...msgs];
|
|
3859
|
-
|
|
4209
|
+
const merged = { ...next[index], ...updates };
|
|
4210
|
+
next[index] = merged;
|
|
3860
4211
|
this.binding.setMessages(next);
|
|
3861
4212
|
this.binding.onMessagesChange?.(next);
|
|
3862
4213
|
if (updates.status) {
|
|
3863
4214
|
this._bubbleById(messageId)?.updateMessage?.({ status: updates.status });
|
|
3864
4215
|
}
|
|
4216
|
+
if (isTerminalStatus(merged.status) && merged.segments) {
|
|
4217
|
+
for (const id of openSegmentIds(merged.segments)) {
|
|
4218
|
+
this.updateSegment(id, { isStreaming: false });
|
|
4219
|
+
}
|
|
4220
|
+
}
|
|
3865
4221
|
}
|
|
3866
4222
|
/** Append/replace the last message content (streaming text). */
|
|
3867
4223
|
updateLastMessage(content, options) {
|
|
@@ -3878,13 +4234,16 @@ class AparteChatHost {
|
|
|
3878
4234
|
/** Add a segment to the last message (immediate bubble + state sync). */
|
|
3879
4235
|
addSegment(segment) {
|
|
3880
4236
|
this._flushStreamState();
|
|
3881
|
-
const
|
|
3882
|
-
const
|
|
3883
|
-
if (
|
|
4237
|
+
const before = this.binding.getMessages();
|
|
4238
|
+
const target = before[before.length - 1];
|
|
4239
|
+
if (target && this._isOrphan(target.id)) return;
|
|
3884
4240
|
this.binding.onTypingChange?.(false);
|
|
3885
|
-
|
|
4241
|
+
const msgs = before;
|
|
4242
|
+
const last = target;
|
|
4243
|
+
const stamped = last ? stampSegmentOnInsert(last.segments || [], segment, last.id) : segment;
|
|
4244
|
+
this._lastBubble()?.addSegment?.(stamped);
|
|
3886
4245
|
if (!last) return;
|
|
3887
|
-
const segments = [...last.segments || [],
|
|
4246
|
+
const segments = [...last.segments || [], stamped];
|
|
3888
4247
|
const next = [...msgs.slice(0, -1), { ...last, segments }];
|
|
3889
4248
|
this.binding.setMessages(next);
|
|
3890
4249
|
this.binding.onMessagesChange?.(next);
|
|
@@ -3895,10 +4254,12 @@ class AparteChatHost {
|
|
|
3895
4254
|
const msgs = this.binding.getMessages();
|
|
3896
4255
|
const last = msgs[msgs.length - 1];
|
|
3897
4256
|
if (last && this._isOrphan(last.id)) return;
|
|
3898
|
-
|
|
4257
|
+
const current = last?.segments?.find((s) => s.id === segmentId);
|
|
4258
|
+
const stamped = current ? stampSegmentOnUpdate(current, updates) : updates;
|
|
4259
|
+
this._lastBubble()?.updateSegment?.(segmentId, stamped);
|
|
3899
4260
|
if (!last || !last.segments) return;
|
|
3900
4261
|
const segments = last.segments.map(
|
|
3901
|
-
(s) => s.id === segmentId ? { ...s, ...
|
|
4262
|
+
(s) => s.id === segmentId ? { ...s, ...stamped } : s
|
|
3902
4263
|
);
|
|
3903
4264
|
const next = [...msgs.slice(0, -1), { ...last, segments }];
|
|
3904
4265
|
this.binding.setMessages(next);
|
|
@@ -3913,6 +4274,7 @@ class AparteChatHost {
|
|
|
3913
4274
|
if (!last?.segments) return;
|
|
3914
4275
|
const segments = last.segments.filter((s) => s.id !== segmentId);
|
|
3915
4276
|
if (segments.length === last.segments.length) return;
|
|
4277
|
+
renumberSegments(segments);
|
|
3916
4278
|
const next = [...msgs.slice(0, -1), { ...last, segments }];
|
|
3917
4279
|
this.binding.setMessages(next);
|
|
3918
4280
|
this.binding.onMessagesChange?.(next);
|
|
@@ -3994,7 +4356,12 @@ class AparteChatHost {
|
|
|
3994
4356
|
if (last?.segments) {
|
|
3995
4357
|
const segments = last.segments.map((s) => {
|
|
3996
4358
|
const buffered = pending.get(s.id);
|
|
3997
|
-
|
|
4359
|
+
if (buffered === void 0 || !("content" in s)) return s;
|
|
4360
|
+
return {
|
|
4361
|
+
...s,
|
|
4362
|
+
content: buffered.base + buffered.text,
|
|
4363
|
+
...stampSegmentActivity(s)
|
|
4364
|
+
};
|
|
3998
4365
|
});
|
|
3999
4366
|
msgs = [...msgs.slice(0, -1), { ...last, segments }];
|
|
4000
4367
|
changed = true;
|
|
@@ -4173,7 +4540,7 @@ class AparteChatHost {
|
|
|
4173
4540
|
const vp = this.binding.viewport;
|
|
4174
4541
|
if (!vp) return null;
|
|
4175
4542
|
return vp.querySelector(
|
|
4176
|
-
`aparte-chat-bubble[message-id="${id}"]`
|
|
4543
|
+
`aparte-chat-bubble[message-id="${cssEscape(id)}"]`
|
|
4177
4544
|
);
|
|
4178
4545
|
}
|
|
4179
4546
|
_lastBubble() {
|
|
@@ -4257,31 +4624,34 @@ class AparteChatHost {
|
|
|
4257
4624
|
});
|
|
4258
4625
|
}
|
|
4259
4626
|
}
|
|
4260
|
-
function assertNever(value) {
|
|
4261
|
-
throw new Error(`Unhandled union member: ${JSON.stringify(value)}`);
|
|
4262
|
-
}
|
|
4263
4627
|
function dispatchLifecycleEvent(target, name, detail) {
|
|
4264
|
-
|
|
4628
|
+
const id = target.id || void 0;
|
|
4629
|
+
const stamped = detail && typeof detail === "object" ? { targetId: id, ...detail } : detail;
|
|
4630
|
+
target.dispatchEvent(new CustomEvent(name, { bubbles: true, composed: true, detail: stamped }));
|
|
4265
4631
|
}
|
|
4266
4632
|
function dispatchArtifactLifecycle(target, messageId, segment, progress, isFinal) {
|
|
4267
4633
|
const id = segment.id;
|
|
4268
4634
|
const content = segment.content ?? "";
|
|
4269
|
-
|
|
4270
|
-
if (seen === void 0) {
|
|
4635
|
+
if (progress.get(id) === void 0) {
|
|
4271
4636
|
target.dispatchEvent(new CustomEvent("aparte-artifact-start", {
|
|
4272
4637
|
bubbles: true,
|
|
4273
4638
|
composed: true,
|
|
4274
|
-
detail: {
|
|
4639
|
+
detail: {
|
|
4640
|
+
messageId,
|
|
4641
|
+
segmentId: id,
|
|
4642
|
+
mimeType: segment.mimeType,
|
|
4643
|
+
artifactType: segment.artifactType,
|
|
4644
|
+
title: segment.title
|
|
4645
|
+
}
|
|
4275
4646
|
}));
|
|
4276
4647
|
progress.set(id, 0);
|
|
4277
4648
|
}
|
|
4278
4649
|
const lastLen = progress.get(id) ?? 0;
|
|
4279
4650
|
if (content.length > lastLen) {
|
|
4280
|
-
const chunk = content.slice(lastLen);
|
|
4281
4651
|
target.dispatchEvent(new CustomEvent("aparte-artifact-delta", {
|
|
4282
4652
|
bubbles: true,
|
|
4283
4653
|
composed: true,
|
|
4284
|
-
detail: { segmentId: id, chunk }
|
|
4654
|
+
detail: { segmentId: id, chunk: content.slice(lastLen) }
|
|
4285
4655
|
}));
|
|
4286
4656
|
progress.set(id, content.length);
|
|
4287
4657
|
}
|
|
@@ -4289,10 +4659,187 @@ function dispatchArtifactLifecycle(target, messageId, segment, progress, isFinal
|
|
|
4289
4659
|
target.dispatchEvent(new CustomEvent("aparte-artifact-ready", {
|
|
4290
4660
|
bubbles: true,
|
|
4291
4661
|
composed: true,
|
|
4292
|
-
detail: {
|
|
4662
|
+
detail: {
|
|
4663
|
+
messageId,
|
|
4664
|
+
segmentId: id,
|
|
4665
|
+
mimeType: segment.mimeType,
|
|
4666
|
+
artifactType: segment.artifactType,
|
|
4667
|
+
title: segment.title,
|
|
4668
|
+
content
|
|
4669
|
+
}
|
|
4293
4670
|
}));
|
|
4294
4671
|
}
|
|
4295
4672
|
}
|
|
4673
|
+
const INLINE_MAX_LINES = 15;
|
|
4674
|
+
const XML_OPEN_TAG = "<artifact";
|
|
4675
|
+
function partialXmlOpenTagLength(text) {
|
|
4676
|
+
const max = Math.min(text.length, XML_OPEN_TAG.length - 1);
|
|
4677
|
+
for (let k = max; k > 0; k--) {
|
|
4678
|
+
if (text.endsWith(XML_OPEN_TAG.slice(0, k))) return k;
|
|
4679
|
+
}
|
|
4680
|
+
return 0;
|
|
4681
|
+
}
|
|
4682
|
+
function feedXmlArtifactDelta(delta, xml, ctx) {
|
|
4683
|
+
const { targetElement, messageId, textParser, streamingSegmentIds, artifactProgress, artifactXmlHint } = ctx;
|
|
4684
|
+
let remaining = delta;
|
|
4685
|
+
const emitChatText = (text, syncActive = true) => {
|
|
4686
|
+
if (!text) return;
|
|
4687
|
+
const r = textParser.parse(text);
|
|
4688
|
+
for (const seg of r.segments) {
|
|
4689
|
+
if (!streamingSegmentIds.has(seg.id)) {
|
|
4690
|
+
targetElement.addSegment?.(seg);
|
|
4691
|
+
streamingSegmentIds.add(seg.id);
|
|
4692
|
+
} else if ("content" in seg) {
|
|
4693
|
+
targetElement.updateSegment?.(seg.id, segmentContentUpdate(seg));
|
|
4694
|
+
}
|
|
4695
|
+
}
|
|
4696
|
+
const active = textParser.getState().activeSegment;
|
|
4697
|
+
if (syncActive && active) {
|
|
4698
|
+
if (!streamingSegmentIds.has(active.id)) {
|
|
4699
|
+
targetElement.addSegment?.(active);
|
|
4700
|
+
streamingSegmentIds.add(active.id);
|
|
4701
|
+
} else {
|
|
4702
|
+
targetElement.updateSegment?.(active.id, segmentContentUpdate(active));
|
|
4703
|
+
}
|
|
4704
|
+
} else if (!r.segments.length && !active) ;
|
|
4705
|
+
};
|
|
4706
|
+
while (remaining.length > 0) {
|
|
4707
|
+
if (xml.state === "normal") {
|
|
4708
|
+
const tagStart = remaining.indexOf(XML_OPEN_TAG);
|
|
4709
|
+
if (tagStart === -1) {
|
|
4710
|
+
const held = partialXmlOpenTagLength(remaining);
|
|
4711
|
+
if (held > 0) {
|
|
4712
|
+
emitChatText(remaining.slice(0, remaining.length - held), false);
|
|
4713
|
+
xml.scanBuf = remaining.slice(remaining.length - held);
|
|
4714
|
+
xml.state = "scanning";
|
|
4715
|
+
} else {
|
|
4716
|
+
emitChatText(remaining);
|
|
4717
|
+
}
|
|
4718
|
+
remaining = "";
|
|
4719
|
+
} else {
|
|
4720
|
+
emitChatText(remaining.slice(0, tagStart), false);
|
|
4721
|
+
xml.scanBuf = "";
|
|
4722
|
+
remaining = remaining.slice(tagStart);
|
|
4723
|
+
xml.state = "scanning";
|
|
4724
|
+
}
|
|
4725
|
+
} else if (xml.state === "scanning") {
|
|
4726
|
+
xml.scanBuf += remaining;
|
|
4727
|
+
remaining = "";
|
|
4728
|
+
const cmp = Math.min(xml.scanBuf.length, XML_OPEN_TAG.length);
|
|
4729
|
+
if (xml.scanBuf.slice(0, cmp) !== XML_OPEN_TAG.slice(0, cmp)) {
|
|
4730
|
+
emitChatText(xml.scanBuf);
|
|
4731
|
+
xml.scanBuf = "";
|
|
4732
|
+
xml.state = "normal";
|
|
4733
|
+
continue;
|
|
4734
|
+
}
|
|
4735
|
+
const gtIdx = xml.scanBuf.indexOf(">");
|
|
4736
|
+
if (gtIdx !== -1) {
|
|
4737
|
+
const tag = xml.scanBuf.slice(0, gtIdx + 1);
|
|
4738
|
+
const mimeMatch = /mimeType=['"]([^'"]+)['"]/.exec(tag);
|
|
4739
|
+
const titleMatch = /title=['"]([^'"]+)['"]/.exec(tag);
|
|
4740
|
+
xml.mime = mimeMatch?.[1] ?? artifactXmlHint.mimeType;
|
|
4741
|
+
xml.title = titleMatch?.[1] ?? artifactXmlHint.kind;
|
|
4742
|
+
xml.kind = deriveArtifactKind(xml.mime, artifactXmlHint.kind);
|
|
4743
|
+
xml.segId = `artifact-xml-${uuid()}`;
|
|
4744
|
+
xml.content = "";
|
|
4745
|
+
const openSeg = {
|
|
4746
|
+
id: xml.segId,
|
|
4747
|
+
type: "artifact",
|
|
4748
|
+
mimeType: xml.mime,
|
|
4749
|
+
artifactType: xml.kind,
|
|
4750
|
+
title: xml.title,
|
|
4751
|
+
content: ""
|
|
4752
|
+
};
|
|
4753
|
+
targetElement.addSegment?.(openSeg);
|
|
4754
|
+
streamingSegmentIds.add(xml.segId);
|
|
4755
|
+
dispatchArtifactLifecycle(targetElement, messageId, openSeg, artifactProgress, false);
|
|
4756
|
+
xml.state = "in-artifact";
|
|
4757
|
+
remaining = xml.scanBuf.slice(gtIdx + 1);
|
|
4758
|
+
xml.scanBuf = "";
|
|
4759
|
+
}
|
|
4760
|
+
} else {
|
|
4761
|
+
const CLOSE = "</artifact>";
|
|
4762
|
+
const combined = xml.closeBuf + remaining;
|
|
4763
|
+
const closeIdx = combined.indexOf(CLOSE);
|
|
4764
|
+
if (closeIdx !== -1) {
|
|
4765
|
+
xml.content += combined.slice(0, closeIdx);
|
|
4766
|
+
const lineCount = xml.content.split("\n").length;
|
|
4767
|
+
const isInline = lineCount < INLINE_MAX_LINES;
|
|
4768
|
+
const finalSeg = {
|
|
4769
|
+
id: xml.segId,
|
|
4770
|
+
mimeType: xml.mime,
|
|
4771
|
+
artifactType: xml.kind,
|
|
4772
|
+
title: xml.title,
|
|
4773
|
+
content: xml.content
|
|
4774
|
+
};
|
|
4775
|
+
targetElement.updateSegment?.(xml.segId, { content: xml.content, inline: isInline });
|
|
4776
|
+
dispatchArtifactLifecycle(targetElement, messageId, finalSeg, artifactProgress, true);
|
|
4777
|
+
xml.state = "normal";
|
|
4778
|
+
xml.closeBuf = "";
|
|
4779
|
+
remaining = combined.slice(closeIdx + CLOSE.length);
|
|
4780
|
+
} else {
|
|
4781
|
+
const safeLen = Math.max(0, combined.length - CLOSE.length + 1);
|
|
4782
|
+
const safe = combined.slice(0, safeLen);
|
|
4783
|
+
xml.content += safe;
|
|
4784
|
+
xml.closeBuf = combined.slice(safeLen);
|
|
4785
|
+
remaining = "";
|
|
4786
|
+
if (xml.segId) {
|
|
4787
|
+
targetElement.updateSegment?.(xml.segId, { content: xml.content });
|
|
4788
|
+
dispatchArtifactLifecycle(targetElement, messageId, {
|
|
4789
|
+
id: xml.segId,
|
|
4790
|
+
mimeType: xml.mime,
|
|
4791
|
+
artifactType: xml.kind,
|
|
4792
|
+
title: xml.title,
|
|
4793
|
+
content: xml.content
|
|
4794
|
+
}, artifactProgress, false);
|
|
4795
|
+
}
|
|
4796
|
+
}
|
|
4797
|
+
}
|
|
4798
|
+
}
|
|
4799
|
+
}
|
|
4800
|
+
function finalizeXmlArtifact(xml, ctx) {
|
|
4801
|
+
const { targetElement, messageId, artifactProgress, artifactXmlHint } = ctx;
|
|
4802
|
+
if (xml.state === "scanning") {
|
|
4803
|
+
const held = xml.scanBuf;
|
|
4804
|
+
xml.scanBuf = "";
|
|
4805
|
+
xml.state = "normal";
|
|
4806
|
+
if (held) {
|
|
4807
|
+
const r = ctx.textParser.parse(held);
|
|
4808
|
+
for (const seg of r.segments) {
|
|
4809
|
+
if (!ctx.streamingSegmentIds.has(seg.id)) {
|
|
4810
|
+
targetElement.addSegment?.(seg);
|
|
4811
|
+
ctx.streamingSegmentIds.add(seg.id);
|
|
4812
|
+
} else if ("content" in seg) {
|
|
4813
|
+
targetElement.updateSegment?.(seg.id, segmentContentUpdate(seg));
|
|
4814
|
+
}
|
|
4815
|
+
}
|
|
4816
|
+
const active = ctx.textParser.getState().activeSegment;
|
|
4817
|
+
if (active) {
|
|
4818
|
+
if (!ctx.streamingSegmentIds.has(active.id)) {
|
|
4819
|
+
targetElement.addSegment?.(active);
|
|
4820
|
+
ctx.streamingSegmentIds.add(active.id);
|
|
4821
|
+
} else {
|
|
4822
|
+
targetElement.updateSegment?.(active.id, segmentContentUpdate(active));
|
|
4823
|
+
}
|
|
4824
|
+
}
|
|
4825
|
+
}
|
|
4826
|
+
return;
|
|
4827
|
+
}
|
|
4828
|
+
if (artifactXmlHint && xml.state === "in-artifact" && xml.segId) {
|
|
4829
|
+
xml.content += xml.closeBuf;
|
|
4830
|
+
const lineCount = xml.content.split("\n").length;
|
|
4831
|
+
const isInline = lineCount < INLINE_MAX_LINES;
|
|
4832
|
+
targetElement.updateSegment?.(xml.segId, { content: xml.content, inline: isInline });
|
|
4833
|
+
dispatchArtifactLifecycle(targetElement, messageId, {
|
|
4834
|
+
id: xml.segId,
|
|
4835
|
+
mimeType: xml.mime,
|
|
4836
|
+
artifactType: xml.kind,
|
|
4837
|
+
title: xml.title,
|
|
4838
|
+
content: xml.content
|
|
4839
|
+
}, artifactProgress, true);
|
|
4840
|
+
console.warn("[AparteClient] XML artifact finalized without closing tag — content may be partial.");
|
|
4841
|
+
}
|
|
4842
|
+
}
|
|
4296
4843
|
function createStreamAdapter(opts) {
|
|
4297
4844
|
const { target, config, messageId, artifactHint } = opts;
|
|
4298
4845
|
let parser = new AparteStreamParser();
|
|
@@ -4322,7 +4869,7 @@ function createStreamAdapter(opts) {
|
|
|
4322
4869
|
thinkingContent += e.delta;
|
|
4323
4870
|
if (!thinkingId) {
|
|
4324
4871
|
const seg = {
|
|
4325
|
-
id: `think-${
|
|
4872
|
+
id: `think-${uuid()}`,
|
|
4326
4873
|
type: "thinking",
|
|
4327
4874
|
content: thinkingContent,
|
|
4328
4875
|
collapsed: true,
|
|
@@ -4338,7 +4885,7 @@ function createStreamAdapter(opts) {
|
|
|
4338
4885
|
}
|
|
4339
4886
|
case "text-delta": {
|
|
4340
4887
|
if (thinkingId && !thinkingCollapsed) {
|
|
4341
|
-
target.updateSegment?.(thinkingId, { collapsed: true });
|
|
4888
|
+
target.updateSegment?.(thinkingId, { collapsed: true, isStreaming: false });
|
|
4342
4889
|
thinkingCollapsed = true;
|
|
4343
4890
|
}
|
|
4344
4891
|
if (e.reduced) {
|
|
@@ -4347,12 +4894,10 @@ function createStreamAdapter(opts) {
|
|
|
4347
4894
|
if (!streaming.has(segment.id)) {
|
|
4348
4895
|
target.addSegment?.(segment);
|
|
4349
4896
|
streaming.add(segment.id);
|
|
4897
|
+
} else if ("content" in segment) {
|
|
4898
|
+
target.updateSegment?.(segment.id, segmentContentUpdate(segment));
|
|
4350
4899
|
}
|
|
4351
4900
|
}
|
|
4352
|
-
if (r.segments.length === 0 && !parser.getState().activeSegment) {
|
|
4353
|
-
if (target.typeName) target.typeName(e.delta);
|
|
4354
|
-
else target.updateLastMessage?.(e.delta, { append: true });
|
|
4355
|
-
}
|
|
4356
4901
|
break;
|
|
4357
4902
|
}
|
|
4358
4903
|
const result = parser.parse(e.delta);
|
|
@@ -4374,7 +4919,7 @@ function createStreamAdapter(opts) {
|
|
|
4374
4919
|
target.addSegment?.(segment);
|
|
4375
4920
|
streaming.add(segment.id);
|
|
4376
4921
|
} else if ("content" in segment) {
|
|
4377
|
-
target.updateSegment?.(segment.id,
|
|
4922
|
+
target.updateSegment?.(segment.id, segmentContentUpdate(segment));
|
|
4378
4923
|
}
|
|
4379
4924
|
if (segment.type === "artifact") {
|
|
4380
4925
|
dispatchArtifactLifecycle(target, messageId, segment, artifactProgress, true);
|
|
@@ -4389,15 +4934,12 @@ function createStreamAdapter(opts) {
|
|
|
4389
4934
|
dispatchArtifactLifecycle(target, messageId, active, artifactProgress, false);
|
|
4390
4935
|
}
|
|
4391
4936
|
} else {
|
|
4392
|
-
target.updateSegment?.(active.id,
|
|
4937
|
+
target.updateSegment?.(active.id, segmentContentUpdate(active));
|
|
4393
4938
|
if (active.type === "artifact") {
|
|
4394
4939
|
dispatchArtifactLifecycle(target, messageId, active, artifactProgress, false);
|
|
4395
4940
|
}
|
|
4396
4941
|
}
|
|
4397
|
-
} else if (result.segments.length === 0)
|
|
4398
|
-
if (target.typeName) target.typeName(e.delta);
|
|
4399
|
-
else target.updateLastMessage?.(e.delta, { append: true });
|
|
4400
|
-
}
|
|
4942
|
+
} else if (result.segments.length === 0) ;
|
|
4401
4943
|
break;
|
|
4402
4944
|
}
|
|
4403
4945
|
case "text-flush": {
|
|
@@ -4423,7 +4965,7 @@ function createStreamAdapter(opts) {
|
|
|
4423
4965
|
}
|
|
4424
4966
|
for (const s of finals) {
|
|
4425
4967
|
if (!streaming.has(s.id)) target.addSegment?.(s);
|
|
4426
|
-
else if ("content" in s) target.updateSegment?.(s.id,
|
|
4968
|
+
else if ("content" in s) target.updateSegment?.(s.id, segmentContentUpdate(s));
|
|
4427
4969
|
if (s.type === "artifact") {
|
|
4428
4970
|
dispatchArtifactLifecycle(target, messageId, s, artifactProgress, true);
|
|
4429
4971
|
}
|
|
@@ -4517,7 +5059,7 @@ function createStreamAdapter(opts) {
|
|
|
4517
5059
|
case "turn-limit-exceeded":
|
|
4518
5060
|
if (e.scope === "global") {
|
|
4519
5061
|
target.addSegment?.({
|
|
4520
|
-
id: `max-turns-${
|
|
5062
|
+
id: `max-turns-${uuid()}`,
|
|
4521
5063
|
type: "error",
|
|
4522
5064
|
content: `Stopped after ${e.limit} tool calls to prevent an infinite loop.`,
|
|
4523
5065
|
details: "MAX_TURNS_EXCEEDED"
|
|
@@ -4527,7 +5069,7 @@ function createStreamAdapter(opts) {
|
|
|
4527
5069
|
}
|
|
4528
5070
|
break;
|
|
4529
5071
|
case "phase-advance":
|
|
4530
|
-
target.addSegment?.({ id: `pw-${
|
|
5072
|
+
target.addSegment?.({ id: `pw-${uuid()}`, type: "pipeline-waiting" });
|
|
4531
5073
|
break;
|
|
4532
5074
|
case "run-aborted":
|
|
4533
5075
|
dispatchLifecycleEvent(target, "aparte-message-aborted", { messageId });
|
|
@@ -4564,6 +5106,10 @@ function readableToAsyncIterable(stream, signal) {
|
|
|
4564
5106
|
},
|
|
4565
5107
|
async return() {
|
|
4566
5108
|
signal.removeEventListener("abort", onAbort);
|
|
5109
|
+
try {
|
|
5110
|
+
await reader.cancel();
|
|
5111
|
+
} catch {
|
|
5112
|
+
}
|
|
4567
5113
|
try {
|
|
4568
5114
|
reader.releaseLock();
|
|
4569
5115
|
} catch {
|
|
@@ -4574,7 +5120,20 @@ function readableToAsyncIterable(stream, signal) {
|
|
|
4574
5120
|
}
|
|
4575
5121
|
};
|
|
4576
5122
|
}
|
|
4577
|
-
const
|
|
5123
|
+
const DEFAULT_TOOL_HANDLER_TIMEOUT_MS = 5 * 60 * 1e3;
|
|
5124
|
+
function withToolTimeout(work, controller, timeoutMs) {
|
|
5125
|
+
let timer;
|
|
5126
|
+
const timedOut = new Promise((_resolve, reject) => {
|
|
5127
|
+
timer = setTimeout(() => {
|
|
5128
|
+
controller.abort();
|
|
5129
|
+
reject(Object.assign(new Error(`tool handler exceeded ${timeoutMs}ms`), { name: "AbortError" }));
|
|
5130
|
+
}, timeoutMs);
|
|
5131
|
+
});
|
|
5132
|
+
return {
|
|
5133
|
+
raced: Promise.race([work(controller.signal), timedOut]),
|
|
5134
|
+
done: () => clearTimeout(timer)
|
|
5135
|
+
};
|
|
5136
|
+
}
|
|
4578
5137
|
class AparteClient {
|
|
4579
5138
|
_boundHandler = null;
|
|
4580
5139
|
_boundAbortHandler = null;
|
|
@@ -4583,6 +5142,30 @@ class AparteClient {
|
|
|
4583
5142
|
_boundEditHandler = null;
|
|
4584
5143
|
_activeToolControllers = /* @__PURE__ */ new Set();
|
|
4585
5144
|
_isAborted = false;
|
|
5145
|
+
/**
|
|
5146
|
+
* Does this window event belong to this client?
|
|
5147
|
+
*
|
|
5148
|
+
* One rule for all five handlers, because it used to be four near-copies and
|
|
5149
|
+
* one omission: `aparte-compact` had no guard at all, so in the two-client
|
|
5150
|
+
* layout the JSDoc documents, a single compact event made BOTH clients run —
|
|
5151
|
+
* two paid summarisation calls against whichever chat the DOM scan found
|
|
5152
|
+
* first, and a global reset that wiped the other conversation.
|
|
5153
|
+
*
|
|
5154
|
+
* A scoped client also answers only events ADDRESSED to it. The old guard let
|
|
5155
|
+
* an untargeted event through to every scoped client, which turned one
|
|
5156
|
+
* broadcast into an action on every chat on the page.
|
|
5157
|
+
*/
|
|
5158
|
+
_isForThisInstance(e) {
|
|
5159
|
+
if (!e) return true;
|
|
5160
|
+
const detail = e.detail;
|
|
5161
|
+
const scope = this.options.scopeToTargetId;
|
|
5162
|
+
if (scope) return detail?.targetId === scope;
|
|
5163
|
+
if (this._config === aparteGlobalConfig) return true;
|
|
5164
|
+
const target = this._resolveTarget(detail?.targetId);
|
|
5165
|
+
if (!target) return true;
|
|
5166
|
+
const owner = resolveConfig(target);
|
|
5167
|
+
return owner === this._config || owner === aparteGlobalConfig;
|
|
5168
|
+
}
|
|
4586
5169
|
/** Aborts the in-flight vendor/transport fetch when the user stops a stream. */
|
|
4587
5170
|
_streamController = null;
|
|
4588
5171
|
options;
|
|
@@ -4593,11 +5176,11 @@ class AparteClient {
|
|
|
4593
5176
|
autoRegister: true,
|
|
4594
5177
|
...options
|
|
4595
5178
|
};
|
|
4596
|
-
this._config = options.config ??
|
|
5179
|
+
this._config = options.config ?? aparteGlobalConfig;
|
|
4597
5180
|
if (this.options.autoRegister) {
|
|
4598
|
-
registerDefaultRenderers();
|
|
5181
|
+
registerDefaultRenderers(this._config);
|
|
4599
5182
|
} else {
|
|
4600
|
-
declineDefaultRenderers();
|
|
5183
|
+
declineDefaultRenderers(this._config);
|
|
4601
5184
|
}
|
|
4602
5185
|
this._setupListeners();
|
|
4603
5186
|
}
|
|
@@ -4610,10 +5193,7 @@ class AparteClient {
|
|
|
4610
5193
|
this._boundHandler = (e) => {
|
|
4611
5194
|
const event = e;
|
|
4612
5195
|
if (event.type !== "aparte-send") return;
|
|
4613
|
-
if (this.
|
|
4614
|
-
const evtTargetId = event.detail?.targetId;
|
|
4615
|
-
if (evtTargetId && evtTargetId !== this.options.scopeToTargetId) return;
|
|
4616
|
-
}
|
|
5196
|
+
if (!this._isForThisInstance(event)) return;
|
|
4617
5197
|
this._isAborted = false;
|
|
4618
5198
|
for (const controller of this._activeToolControllers) {
|
|
4619
5199
|
controller.abort();
|
|
@@ -4634,27 +5214,23 @@ class AparteClient {
|
|
|
4634
5214
|
}
|
|
4635
5215
|
if (!this._boundAbortHandler) {
|
|
4636
5216
|
this._boundAbortHandler = (e) => {
|
|
4637
|
-
if (this.
|
|
4638
|
-
const evtTargetId = e?.detail?.targetId;
|
|
4639
|
-
if (evtTargetId && evtTargetId !== this.options.scopeToTargetId) return;
|
|
4640
|
-
}
|
|
5217
|
+
if (!this._isForThisInstance(e)) return;
|
|
4641
5218
|
this.abort();
|
|
4642
5219
|
};
|
|
4643
5220
|
}
|
|
4644
5221
|
window.addEventListener("aparte-abort", this._boundAbortHandler);
|
|
4645
5222
|
if (!this._boundCompactHandler) {
|
|
4646
|
-
this._boundCompactHandler = () => {
|
|
4647
|
-
|
|
5223
|
+
this._boundCompactHandler = (e) => {
|
|
5224
|
+
if (!this._isForThisInstance(e)) return;
|
|
5225
|
+
const detail = e.detail;
|
|
5226
|
+
void this.compact(detail?.targetId);
|
|
4648
5227
|
};
|
|
4649
5228
|
}
|
|
4650
5229
|
window.addEventListener("aparte-compact", this._boundCompactHandler);
|
|
4651
5230
|
if (!this._boundRetryHandler) {
|
|
4652
5231
|
this._boundRetryHandler = (e) => {
|
|
4653
5232
|
const evt = e;
|
|
4654
|
-
if (this.
|
|
4655
|
-
const evtTargetId = evt.detail?.targetId;
|
|
4656
|
-
if (evtTargetId && evtTargetId !== this.options.scopeToTargetId) return;
|
|
4657
|
-
}
|
|
5233
|
+
if (!this._isForThisInstance(evt)) return;
|
|
4658
5234
|
void this._handleRetry(evt);
|
|
4659
5235
|
};
|
|
4660
5236
|
}
|
|
@@ -4662,10 +5238,7 @@ class AparteClient {
|
|
|
4662
5238
|
if (!this._boundEditHandler) {
|
|
4663
5239
|
this._boundEditHandler = (e) => {
|
|
4664
5240
|
const evt = e;
|
|
4665
|
-
if (this.
|
|
4666
|
-
const evtTargetId = evt.detail?.targetId;
|
|
4667
|
-
if (evtTargetId && evtTargetId !== this.options.scopeToTargetId) return;
|
|
4668
|
-
}
|
|
5241
|
+
if (!this._isForThisInstance(evt)) return;
|
|
4669
5242
|
void this._handleEdit(evt);
|
|
4670
5243
|
};
|
|
4671
5244
|
}
|
|
@@ -4675,6 +5248,7 @@ class AparteClient {
|
|
|
4675
5248
|
* Stop listening.
|
|
4676
5249
|
*/
|
|
4677
5250
|
stop() {
|
|
5251
|
+
this.abort();
|
|
4678
5252
|
if (!this._boundHandler) return;
|
|
4679
5253
|
window.removeEventListener("aparte-send", this._boundHandler);
|
|
4680
5254
|
this._boundHandler = null;
|
|
@@ -4718,7 +5292,27 @@ class AparteClient {
|
|
|
4718
5292
|
* approval to `{ approved: false }` — there is no timeout, since a human may
|
|
4719
5293
|
* take any amount of time to decide.
|
|
4720
5294
|
*/
|
|
4721
|
-
|
|
5295
|
+
/**
|
|
5296
|
+
* Await the human's Approve/Reject for one tool call.
|
|
5297
|
+
*
|
|
5298
|
+
* `target` is not decoration: it is the scope of the consent. The listener sits
|
|
5299
|
+
* on `document` and used to accept any `aparte-tool-decision` whose
|
|
5300
|
+
* `detail.toolCallId` matched — and that id is the tool-call id the MODEL chose.
|
|
5301
|
+
* The built-in buttons dispatch with `bubbles: true, composed: true`, so on a
|
|
5302
|
+
* page with two chats a click aimed at one tool could satisfy the gate awaiting
|
|
5303
|
+
* a different tool in a different conversation. The consented action and the
|
|
5304
|
+
* executed action came apart, which is the entire failure mode an approval gate
|
|
5305
|
+
* exists to prevent — and the handler behind it is arbitrary consumer code.
|
|
5306
|
+
*
|
|
5307
|
+
* The check is DOM CONTAINMENT rather than a `targetId` string comparison. A
|
|
5308
|
+
* model can choose an id; it cannot choose where in the tree the click happened.
|
|
5309
|
+
* It also needs no change on the dispatch side, so a consumer's own Approve
|
|
5310
|
+
* button keeps working as long as it fires from inside its own chat.
|
|
5311
|
+
*
|
|
5312
|
+
* The request half of this handshake was hardened with `targetId` for exactly
|
|
5313
|
+
* this hazard. This is its sibling, and it was missed.
|
|
5314
|
+
*/
|
|
5315
|
+
_awaitToolDecision(toolCallId, signal, target) {
|
|
4722
5316
|
return new Promise((resolve) => {
|
|
4723
5317
|
if (signal.aborted) {
|
|
4724
5318
|
resolve({ approved: false });
|
|
@@ -4731,6 +5325,7 @@ class AparteClient {
|
|
|
4731
5325
|
const onDecision = (e) => {
|
|
4732
5326
|
const detail = e.detail;
|
|
4733
5327
|
if (detail?.toolCallId !== toolCallId) return;
|
|
5328
|
+
if (target && e.target instanceof Node && e.target !== document && !target.contains(e.target)) return;
|
|
4734
5329
|
cleanup();
|
|
4735
5330
|
resolve({ approved: detail?.approved === true, payload: detail?.payload });
|
|
4736
5331
|
};
|
|
@@ -4744,7 +5339,7 @@ class AparteClient {
|
|
|
4744
5339
|
}
|
|
4745
5340
|
/**
|
|
4746
5341
|
* Resolve the auth for a provider: `options.keyResolver` takes precedence,
|
|
4747
|
-
* then the
|
|
5342
|
+
* then the aparteGlobalConfig key channel (`setKeyProvider`) so a key registered
|
|
4748
5343
|
* there reaches the request. One key source on the happy path.
|
|
4749
5344
|
*/
|
|
4750
5345
|
async _resolveAuth(providerId) {
|
|
@@ -4781,12 +5376,24 @@ class AparteClient {
|
|
|
4781
5376
|
if (baseRequest.toolChoice === "none") {
|
|
4782
5377
|
baseRequest = { ...baseRequest, tools: void 0 };
|
|
4783
5378
|
}
|
|
4784
|
-
this._isAborted
|
|
4785
|
-
|
|
5379
|
+
if (this._isAborted) {
|
|
5380
|
+
dispatchLifecycleEvent(targetElement, "aparte-message-aborted", { messageId });
|
|
5381
|
+
return;
|
|
5382
|
+
}
|
|
5383
|
+
dispatchLifecycleEvent(targetElement, "aparte-message-start", { messageId, role: "assistant" });
|
|
4786
5384
|
try {
|
|
4787
5385
|
const usage = await this._streamLoop(targetElement, messageId, provider, baseRequest, authConfig);
|
|
4788
|
-
this.
|
|
5386
|
+
if (this._isAborted) {
|
|
5387
|
+
this._updateMessage(targetElement, messageId, { status: "completed" });
|
|
5388
|
+
} else {
|
|
5389
|
+
dispatchLifecycleEvent(targetElement, "aparte-message-done", { messageId, role: "assistant", usage });
|
|
5390
|
+
}
|
|
4789
5391
|
} catch (error) {
|
|
5392
|
+
if (this._isAborted) {
|
|
5393
|
+
dispatchLifecycleEvent(targetElement, "aparte-message-aborted", { messageId });
|
|
5394
|
+
this._updateMessage(targetElement, messageId, { status: "completed" });
|
|
5395
|
+
return;
|
|
5396
|
+
}
|
|
4790
5397
|
const aparteError = AparteError.from(error, AparteErrorCode.UNKNOWN_ERROR);
|
|
4791
5398
|
this._handleLifecycleError(targetElement, messageId, aparteError);
|
|
4792
5399
|
}
|
|
@@ -4798,20 +5405,9 @@ class AparteClient {
|
|
|
4798
5405
|
* Triggered programmatically or by dispatching `window.dispatchEvent(new CustomEvent('aparte-compact'))`.
|
|
4799
5406
|
* Dispatches `aparte-compact-done` on window when complete, or `aparte-compact-error` on failure.
|
|
4800
5407
|
*/
|
|
4801
|
-
async compact() {
|
|
4802
|
-
let target =
|
|
4803
|
-
if (
|
|
4804
|
-
target = this.options.targetResolver();
|
|
4805
|
-
}
|
|
4806
|
-
if (!target) {
|
|
4807
|
-
const candidates = document.querySelectorAll("aparte-chat, [data-aparte-chat]");
|
|
4808
|
-
for (const el2 of Array.from(candidates)) {
|
|
4809
|
-
if (typeof el2.getMessages === "function") {
|
|
4810
|
-
target = el2;
|
|
4811
|
-
break;
|
|
4812
|
-
}
|
|
4813
|
-
}
|
|
4814
|
-
}
|
|
5408
|
+
async compact(targetId) {
|
|
5409
|
+
let target = this._resolveTarget(targetId);
|
|
5410
|
+
if (target && typeof target.getMessages !== "function") target = null;
|
|
4815
5411
|
if (!target) {
|
|
4816
5412
|
window.dispatchEvent(new CustomEvent("aparte-compact-error", { detail: { error: "No aparte-chat target found" } }));
|
|
4817
5413
|
return;
|
|
@@ -4860,7 +5456,14 @@ class AparteClient {
|
|
|
4860
5456
|
modelId: config.defaultModel || "",
|
|
4861
5457
|
stream: false
|
|
4862
5458
|
};
|
|
4863
|
-
const
|
|
5459
|
+
const compactController = new AbortController();
|
|
5460
|
+
this._streamController = compactController;
|
|
5461
|
+
const response = await this._config.getTransport().chat(
|
|
5462
|
+
provider,
|
|
5463
|
+
summarizeRequest,
|
|
5464
|
+
authConfig,
|
|
5465
|
+
{ providerId: provider.id, signal: compactController.signal }
|
|
5466
|
+
);
|
|
4864
5467
|
let summary;
|
|
4865
5468
|
if (typeof response === "string") {
|
|
4866
5469
|
summary = response;
|
|
@@ -4878,10 +5481,12 @@ class AparteClient {
|
|
|
4878
5481
|
if (!summary.trim()) {
|
|
4879
5482
|
throw new Error("Empty summary returned by model");
|
|
4880
5483
|
}
|
|
4881
|
-
|
|
5484
|
+
const clearAll = target.clearAll;
|
|
5485
|
+
if (typeof clearAll === "function") clearAll.call(target);
|
|
5486
|
+
else window.dispatchEvent(new CustomEvent("aparte-reset"));
|
|
4882
5487
|
await new Promise((resolve) => setTimeout(resolve, 50));
|
|
4883
5488
|
target.appendMessage?.({
|
|
4884
|
-
id:
|
|
5489
|
+
id: uuid(),
|
|
4885
5490
|
role: "assistant",
|
|
4886
5491
|
content: `📝 **Conversation summary**
|
|
4887
5492
|
|
|
@@ -4904,17 +5509,41 @@ ${summary}`,
|
|
|
4904
5509
|
* using the same conversation history minus the retried reply.
|
|
4905
5510
|
*/
|
|
4906
5511
|
/**
|
|
4907
|
-
*
|
|
4908
|
-
*
|
|
4909
|
-
* send / retry / edit
|
|
4910
|
-
* the initial send while retry
|
|
5512
|
+
* The registered tools, unless the current model says it cannot call one.
|
|
5513
|
+
*
|
|
5514
|
+
* Single source for the gate, so send / retry / edit cannot drift — that drift
|
|
5515
|
+
* is what once shipped `tools` on the initial send while retry and edit omitted
|
|
5516
|
+
* them.
|
|
5517
|
+
*
|
|
5518
|
+
* The gate used to be "the model DECLARES function_calling", defaulting to
|
|
5519
|
+
* stripping — and that made the whole tool surface unreachable on the primary
|
|
5520
|
+
* documented path. Three things had to line up for a tool to be sent, and one
|
|
5521
|
+
* never did: the app sets a model id by hand (`setModelConfig`) or a selector
|
|
5522
|
+
* sets it from a fetched list, and in both cases the model's declared
|
|
5523
|
+
* capabilities are commonly unknown. `undefined` capabilities then read as "no
|
|
5524
|
+
* function calling", so `tools: []` went on the wire while `getTools()` held
|
|
5525
|
+
* the tool the app had explicitly registered. The model answered, correctly,
|
|
5526
|
+
* that it had no such tool, and nothing anywhere said why.
|
|
5527
|
+
*
|
|
5528
|
+
* So the question is now "did this model say it cannot", not "did it say it
|
|
5529
|
+
* can". Registering a tool is an explicit act by the app; silently dropping it
|
|
5530
|
+
* because a `/models` listing is terse is the library second-guessing the
|
|
5531
|
+
* developer. A model that declares its capabilities and omits function calling
|
|
5532
|
+
* is still honoured — that is a statement, and it is respected.
|
|
5533
|
+
*
|
|
5534
|
+
* Failure modes, both ways round: over-sending means a model that cannot use
|
|
5535
|
+
* tools does not call one, or its own endpoint rejects the array with an error
|
|
5536
|
+
* the developer can read. Under-sending was silent, total, and looked like a
|
|
5537
|
+
* lying model.
|
|
4911
5538
|
*/
|
|
4912
5539
|
_toolsForCurrentModel() {
|
|
4913
|
-
const
|
|
4914
|
-
|
|
5540
|
+
const capabilities = this._config.getCurrentModel()?.capabilities;
|
|
5541
|
+
const declinesTools = capabilities ? !capabilities.includes("function_calling") : false;
|
|
5542
|
+
return declinesTools ? [] : this._config.getTools();
|
|
4915
5543
|
}
|
|
4916
5544
|
async _handleRetry(event) {
|
|
4917
5545
|
const { messageId, targetId } = event.detail ?? {};
|
|
5546
|
+
this._isAborted = false;
|
|
4918
5547
|
if (!messageId) return;
|
|
4919
5548
|
const targetElement = this._resolveTarget(targetId);
|
|
4920
5549
|
if (!targetElement) {
|
|
@@ -4925,9 +5554,9 @@ ${summary}`,
|
|
|
4925
5554
|
const retryIdx = allMessages.findIndex((m) => m.id === messageId);
|
|
4926
5555
|
const retryMsg = retryIdx >= 0 ? allMessages[retryIdx] : void 0;
|
|
4927
5556
|
const sliceEnd = retryMsg?.role === "user" ? retryIdx + 1 : retryIdx;
|
|
4928
|
-
const historyMessages = retryIdx
|
|
5557
|
+
const historyMessages = retryIdx >= 0 ? allMessages.slice(0, sliceEnd) : allMessages;
|
|
4929
5558
|
const newMsg = {
|
|
4930
|
-
id:
|
|
5559
|
+
id: uuid(),
|
|
4931
5560
|
role: "assistant",
|
|
4932
5561
|
content: "",
|
|
4933
5562
|
status: "pending",
|
|
@@ -4960,6 +5589,7 @@ ${summary}`,
|
|
|
4960
5589
|
*/
|
|
4961
5590
|
async _handleEdit(event) {
|
|
4962
5591
|
const { messageId, content: newContent, targetId } = event.detail ?? {};
|
|
5592
|
+
this._isAborted = false;
|
|
4963
5593
|
if (!messageId || newContent === void 0) return;
|
|
4964
5594
|
const targetElement = this._resolveTarget(targetId);
|
|
4965
5595
|
if (!targetElement) {
|
|
@@ -4988,7 +5618,7 @@ ${summary}`,
|
|
|
4988
5618
|
const provider = this._config.getAIProvider(providerId);
|
|
4989
5619
|
if (!provider) return;
|
|
4990
5620
|
const authConfig = await this._resolveAuth(providerId);
|
|
4991
|
-
const newMessageId =
|
|
5621
|
+
const newMessageId = uuid();
|
|
4992
5622
|
targetElement.appendMessage?.({
|
|
4993
5623
|
id: newMessageId,
|
|
4994
5624
|
role: "assistant",
|
|
@@ -5051,6 +5681,12 @@ ${summary}`,
|
|
|
5051
5681
|
}
|
|
5052
5682
|
async _handleSend(event) {
|
|
5053
5683
|
const { content, modelId, providerId: explicitProviderId } = event.detail;
|
|
5684
|
+
if (this._config.getRequireModelSelection() && !this._config.hasSelectedModel() && !modelId) {
|
|
5685
|
+
console.warn(
|
|
5686
|
+
"[AparteClient] Send refused: requireModelSelection is on and no model is selected yet. The composer blocks this, but this send did not come from it — disable your own send affordances while `hasSelectedModel()` is false."
|
|
5687
|
+
);
|
|
5688
|
+
return;
|
|
5689
|
+
}
|
|
5054
5690
|
let targetElement = null;
|
|
5055
5691
|
const targetId = event.detail?.targetId;
|
|
5056
5692
|
if (targetId) {
|
|
@@ -5093,7 +5729,7 @@ ${summary}`,
|
|
|
5093
5729
|
console.warn("[AparteClient] ⚠️ No target element found with appendMessage support. Provide a targetResolver in AparteClientOptions or ensure the composer has a `target` attribute.");
|
|
5094
5730
|
return;
|
|
5095
5731
|
}
|
|
5096
|
-
const messageId =
|
|
5732
|
+
const messageId = uuid();
|
|
5097
5733
|
const config = this._config.getModelConfig();
|
|
5098
5734
|
const providerId = explicitProviderId || config.defaultProvider;
|
|
5099
5735
|
if (!providerId) {
|
|
@@ -5177,10 +5813,69 @@ ${summary}`,
|
|
|
5177
5813
|
content: this._extractText(m)
|
|
5178
5814
|
})).filter((m) => m.content.length > 0);
|
|
5179
5815
|
}
|
|
5816
|
+
/**
|
|
5817
|
+
* What the model is told it said last turn — reconstructed from what was
|
|
5818
|
+
* RENDERED, not from the raw-text field.
|
|
5819
|
+
*
|
|
5820
|
+
* The order used to be the other way round, and it silently corrupted the
|
|
5821
|
+
* context on the most common opening in a coding chat: a reply starting with a
|
|
5822
|
+
* code fence was sent back as three backticks and nothing else. Three
|
|
5823
|
+
* correct-in-isolation decisions composed into it:
|
|
5824
|
+
*
|
|
5825
|
+
* 1. the parser withholds an ambiguous prefix (``` ``` ```, `` ` ``, `<`)
|
|
5826
|
+
* waiting for the next delta, and creates no active segment while it waits;
|
|
5827
|
+
* 2. so `_streamLoop` sees zero segments, concludes the parser produced
|
|
5828
|
+
* nothing, and appends the raw delta straight to `message.content`;
|
|
5829
|
+
* 3. and this method preferred `content`.
|
|
5830
|
+
*
|
|
5831
|
+
* The bubble hides its content element as soon as segments exist, so the UI was
|
|
5832
|
+
* perfect and only the NEXT request showed it. No unit test, no example and
|
|
5833
|
+
* no browser test could see it.
|
|
5834
|
+
*
|
|
5835
|
+
* `content` stays as the fallback, and that is not vestigial: a non-streaming
|
|
5836
|
+
* transport writes the whole reply there and creates no segments at all.
|
|
5837
|
+
*/
|
|
5180
5838
|
_extractText(message) {
|
|
5839
|
+
const rendered = this._segmentsToText(message.segments);
|
|
5840
|
+
if (rendered) return rendered;
|
|
5181
5841
|
if (typeof message.content === "string" && message.content) return message.content;
|
|
5182
|
-
|
|
5183
|
-
|
|
5842
|
+
return "";
|
|
5843
|
+
}
|
|
5844
|
+
/**
|
|
5845
|
+
* Segments → the text a model can read back.
|
|
5846
|
+
*
|
|
5847
|
+
* Fences and the language tag are kept. Without them the model re-reads its own
|
|
5848
|
+
* code as prose — which is how it starts explaining a snippet it believes it
|
|
5849
|
+
* wrote in English. Artifacts are included for the same reason: dropping them
|
|
5850
|
+
* made an artifact the model had just produced invisible on the very next turn,
|
|
5851
|
+
* so it could not be asked to change the thing it had built.
|
|
5852
|
+
*
|
|
5853
|
+
* `thinking` stays out on purpose — it is the model's own scratchpad, and most
|
|
5854
|
+
* APIs neither want it back nor bill for it kindly.
|
|
5855
|
+
*/
|
|
5856
|
+
_segmentsToText(segments) {
|
|
5857
|
+
if (!segments?.length) return "";
|
|
5858
|
+
const fence = "```";
|
|
5859
|
+
const parts = [];
|
|
5860
|
+
for (const segment of segments) {
|
|
5861
|
+
const content = segment.content ?? "";
|
|
5862
|
+
if (segment.type === "text") {
|
|
5863
|
+
if (content) parts.push(content);
|
|
5864
|
+
} else if (segment.type === "code") {
|
|
5865
|
+
const lang = segment.language ?? "";
|
|
5866
|
+
parts.push(`${fence}${lang}
|
|
5867
|
+
${content}
|
|
5868
|
+
${fence}`);
|
|
5869
|
+
} else if (segment.type === "artifact") {
|
|
5870
|
+
const title = segment.title ?? "artifact";
|
|
5871
|
+
const kind = segment.artifactType ?? "";
|
|
5872
|
+
parts.push(`${fence}${kind}
|
|
5873
|
+
<!-- artifact: ${title} -->
|
|
5874
|
+
${content}
|
|
5875
|
+
${fence}`);
|
|
5876
|
+
}
|
|
5877
|
+
}
|
|
5878
|
+
return parts.join("\n").trim();
|
|
5184
5879
|
}
|
|
5185
5880
|
/**
|
|
5186
5881
|
* Which of the send's pending files get inlined into the request: the
|
|
@@ -5238,135 +5933,6 @@ ${content}
|
|
|
5238
5933
|
);
|
|
5239
5934
|
return parts.filter((p) => p !== null);
|
|
5240
5935
|
}
|
|
5241
|
-
/**
|
|
5242
|
-
* Stream loop: runs one provider.chat() call and repeats if a tool was called.
|
|
5243
|
-
* Maintains a running messages array to inject tool_call / tool_result turns.
|
|
5244
|
-
*/
|
|
5245
|
-
/**
|
|
5246
|
-
* Feed one text delta to the Claude-style `<artifact>` XML streamer. Scans for
|
|
5247
|
-
* `<artifact …>` / `</artifact>`, routing chat text through the text parser and
|
|
5248
|
-
* artifact content into a dedicated artifact segment (handling tags split across
|
|
5249
|
-
* deltas). Mutates `xml` in place. Extracted from _streamLoop.
|
|
5250
|
-
*/
|
|
5251
|
-
_feedXmlArtifactDelta(delta, xml, ctx) {
|
|
5252
|
-
const { targetElement, messageId, textParser, streamingSegmentIds, artifactProgress, artifactXmlHint } = ctx;
|
|
5253
|
-
let remaining = delta;
|
|
5254
|
-
while (remaining.length > 0) {
|
|
5255
|
-
if (xml.state === "normal") {
|
|
5256
|
-
const tagStart = remaining.indexOf("<artifact");
|
|
5257
|
-
if (tagStart === -1) {
|
|
5258
|
-
const r = textParser.parse(remaining);
|
|
5259
|
-
for (const seg of r.segments) {
|
|
5260
|
-
if (!streamingSegmentIds.has(seg.id)) {
|
|
5261
|
-
targetElement.addSegment?.(seg);
|
|
5262
|
-
streamingSegmentIds.add(seg.id);
|
|
5263
|
-
} else if ("content" in seg) {
|
|
5264
|
-
targetElement.updateSegment?.(seg.id, { content: seg.content });
|
|
5265
|
-
}
|
|
5266
|
-
}
|
|
5267
|
-
const active = textParser.getState().activeSegment;
|
|
5268
|
-
if (active) {
|
|
5269
|
-
if (!streamingSegmentIds.has(active.id)) {
|
|
5270
|
-
targetElement.addSegment?.(active);
|
|
5271
|
-
streamingSegmentIds.add(active.id);
|
|
5272
|
-
} else {
|
|
5273
|
-
targetElement.updateSegment?.(active.id, { content: active.content });
|
|
5274
|
-
}
|
|
5275
|
-
} else if (!r.segments.length) {
|
|
5276
|
-
if (targetElement.typeName) targetElement.typeName(remaining);
|
|
5277
|
-
else targetElement.updateLastMessage?.(remaining, { append: true });
|
|
5278
|
-
}
|
|
5279
|
-
remaining = "";
|
|
5280
|
-
} else {
|
|
5281
|
-
const before = remaining.slice(0, tagStart);
|
|
5282
|
-
if (before) {
|
|
5283
|
-
const r = textParser.parse(before);
|
|
5284
|
-
for (const seg of r.segments) {
|
|
5285
|
-
if (!streamingSegmentIds.has(seg.id)) {
|
|
5286
|
-
targetElement.addSegment?.(seg);
|
|
5287
|
-
streamingSegmentIds.add(seg.id);
|
|
5288
|
-
}
|
|
5289
|
-
}
|
|
5290
|
-
if (!r.segments.length && !textParser.getState().activeSegment) {
|
|
5291
|
-
if (targetElement.typeName) targetElement.typeName(before);
|
|
5292
|
-
else targetElement.updateLastMessage?.(before, { append: true });
|
|
5293
|
-
}
|
|
5294
|
-
}
|
|
5295
|
-
xml.scanBuf = remaining.slice(tagStart);
|
|
5296
|
-
remaining = "";
|
|
5297
|
-
xml.state = "scanning";
|
|
5298
|
-
}
|
|
5299
|
-
} else if (xml.state === "scanning") {
|
|
5300
|
-
xml.scanBuf += remaining;
|
|
5301
|
-
remaining = "";
|
|
5302
|
-
const gtIdx = xml.scanBuf.indexOf(">");
|
|
5303
|
-
if (gtIdx !== -1) {
|
|
5304
|
-
const tag = xml.scanBuf.slice(0, gtIdx + 1);
|
|
5305
|
-
const mimeMatch = /mimeType=['"]([^'"]+)['"]/.exec(tag);
|
|
5306
|
-
const titleMatch = /title=['"]([^'"]+)['"]/.exec(tag);
|
|
5307
|
-
xml.mime = mimeMatch?.[1] ?? artifactXmlHint.mimeType;
|
|
5308
|
-
xml.title = titleMatch?.[1] ?? artifactXmlHint.kind;
|
|
5309
|
-
xml.kind = deriveArtifactKind(xml.mime, artifactXmlHint.kind);
|
|
5310
|
-
xml.segId = `artifact-xml-${crypto.randomUUID()}`;
|
|
5311
|
-
xml.content = "";
|
|
5312
|
-
const openSeg = {
|
|
5313
|
-
id: xml.segId,
|
|
5314
|
-
type: "artifact",
|
|
5315
|
-
mimeType: xml.mime,
|
|
5316
|
-
artifactType: xml.kind,
|
|
5317
|
-
title: xml.title,
|
|
5318
|
-
content: ""
|
|
5319
|
-
};
|
|
5320
|
-
targetElement.addSegment?.(openSeg);
|
|
5321
|
-
streamingSegmentIds.add(xml.segId);
|
|
5322
|
-
this._dispatchArtifactLifecycle(targetElement, messageId, openSeg, artifactProgress, false);
|
|
5323
|
-
xml.state = "in-artifact";
|
|
5324
|
-
remaining = xml.scanBuf.slice(gtIdx + 1);
|
|
5325
|
-
xml.scanBuf = "";
|
|
5326
|
-
}
|
|
5327
|
-
} else {
|
|
5328
|
-
const CLOSE = "</artifact>";
|
|
5329
|
-
const combined = xml.closeBuf + remaining;
|
|
5330
|
-
const closeIdx = combined.indexOf(CLOSE);
|
|
5331
|
-
if (closeIdx !== -1) {
|
|
5332
|
-
xml.content += combined.slice(0, closeIdx);
|
|
5333
|
-
const lineCount = xml.content.split("\n").length;
|
|
5334
|
-
const isInline = lineCount < 15;
|
|
5335
|
-
const finalSeg = {
|
|
5336
|
-
id: xml.segId,
|
|
5337
|
-
type: "artifact",
|
|
5338
|
-
mimeType: xml.mime,
|
|
5339
|
-
artifactType: xml.kind,
|
|
5340
|
-
title: xml.title,
|
|
5341
|
-
content: xml.content,
|
|
5342
|
-
inline: isInline
|
|
5343
|
-
};
|
|
5344
|
-
targetElement.updateSegment?.(xml.segId, { content: xml.content, inline: isInline });
|
|
5345
|
-
this._dispatchArtifactLifecycle(targetElement, messageId, finalSeg, artifactProgress, true);
|
|
5346
|
-
xml.state = "normal";
|
|
5347
|
-
xml.closeBuf = "";
|
|
5348
|
-
remaining = combined.slice(closeIdx + CLOSE.length);
|
|
5349
|
-
} else {
|
|
5350
|
-
const safeLen = Math.max(0, combined.length - CLOSE.length + 1);
|
|
5351
|
-
const safe = combined.slice(0, safeLen);
|
|
5352
|
-
xml.content += safe;
|
|
5353
|
-
xml.closeBuf = combined.slice(safeLen);
|
|
5354
|
-
remaining = "";
|
|
5355
|
-
if (xml.segId) {
|
|
5356
|
-
targetElement.updateSegment?.(xml.segId, { content: xml.content });
|
|
5357
|
-
this._dispatchArtifactLifecycle(targetElement, messageId, {
|
|
5358
|
-
id: xml.segId,
|
|
5359
|
-
type: "artifact",
|
|
5360
|
-
mimeType: xml.mime,
|
|
5361
|
-
artifactType: xml.kind,
|
|
5362
|
-
title: xml.title,
|
|
5363
|
-
content: xml.content
|
|
5364
|
-
}, artifactProgress, false);
|
|
5365
|
-
}
|
|
5366
|
-
}
|
|
5367
|
-
}
|
|
5368
|
-
}
|
|
5369
|
-
}
|
|
5370
5936
|
/**
|
|
5371
5937
|
* Turn-1 forced tool call. When `toolChoice = { name, input }`
|
|
5372
5938
|
* (orchestrator-driven), execute the handler directly instead of consulting
|
|
@@ -5380,7 +5946,7 @@ ${content}
|
|
|
5380
5946
|
if (!(turns === 1 && toolChoice && typeof toolChoice === "object" && toolChoice.input !== void 0)) {
|
|
5381
5947
|
return { baseRequest, skip: false };
|
|
5382
5948
|
}
|
|
5383
|
-
const syntheticId =
|
|
5949
|
+
const syntheticId = uuid();
|
|
5384
5950
|
const syntheticCall = { id: syntheticId, name: toolChoice.name, input: toolChoice.input };
|
|
5385
5951
|
const toolSeg = {
|
|
5386
5952
|
id: `tool-${syntheticId}`,
|
|
@@ -5403,9 +5969,16 @@ ${content}
|
|
|
5403
5969
|
}
|
|
5404
5970
|
const controller = new AbortController();
|
|
5405
5971
|
this._activeToolControllers.add(controller);
|
|
5406
|
-
const
|
|
5972
|
+
const { raced, done: clearToolTimeout } = withToolTimeout(
|
|
5973
|
+
(sig) => handler(syntheticCall, sig, {
|
|
5974
|
+
target: targetElement,
|
|
5975
|
+
config: this._config
|
|
5976
|
+
}),
|
|
5977
|
+
controller,
|
|
5978
|
+
this.options.toolTimeoutMs ?? DEFAULT_TOOL_HANDLER_TIMEOUT_MS
|
|
5979
|
+
);
|
|
5407
5980
|
try {
|
|
5408
|
-
const result = await
|
|
5981
|
+
const result = await raced;
|
|
5409
5982
|
targetElement.updateSegment?.(toolSeg.id, { status: "resolved", result: result.content });
|
|
5410
5983
|
messages.push({ role: "tool_call", content: "", toolCalls: [syntheticCall] });
|
|
5411
5984
|
messages.push({ role: "tool_result", content: result.content, toolCallId: syntheticId });
|
|
@@ -5417,7 +5990,7 @@ ${content}
|
|
|
5417
5990
|
}
|
|
5418
5991
|
throw err;
|
|
5419
5992
|
} finally {
|
|
5420
|
-
|
|
5993
|
+
clearToolTimeout();
|
|
5421
5994
|
this._activeToolControllers.delete(controller);
|
|
5422
5995
|
}
|
|
5423
5996
|
}
|
|
@@ -5435,7 +6008,7 @@ ${content}
|
|
|
5435
6008
|
if (event.name === "create_artifact") {
|
|
5436
6009
|
const input = event.input;
|
|
5437
6010
|
const mimeType = input.mimeType ?? "text/plain";
|
|
5438
|
-
const kind =
|
|
6011
|
+
const kind = deriveArtifactKind(mimeType, "text");
|
|
5439
6012
|
const artifactSeg = {
|
|
5440
6013
|
id: `artifact-${event.id}`,
|
|
5441
6014
|
type: "artifact",
|
|
@@ -5445,7 +6018,7 @@ ${content}
|
|
|
5445
6018
|
content: input.content ?? ""
|
|
5446
6019
|
};
|
|
5447
6020
|
targetElement.addSegment?.(artifactSeg);
|
|
5448
|
-
|
|
6021
|
+
dispatchArtifactLifecycle(targetElement, messageId, artifactSeg, artifactProgress, true);
|
|
5449
6022
|
messages.push({
|
|
5450
6023
|
role: "tool_call",
|
|
5451
6024
|
content: "",
|
|
@@ -5499,13 +6072,13 @@ ${content}
|
|
|
5499
6072
|
const approvalController = new AbortController();
|
|
5500
6073
|
this._activeToolControllers.add(approvalController);
|
|
5501
6074
|
targetElement.updateSegment?.(toolSeg.id, { status: "awaiting-approval" });
|
|
5502
|
-
targetElement
|
|
5503
|
-
|
|
5504
|
-
|
|
5505
|
-
|
|
5506
|
-
})
|
|
6075
|
+
dispatchLifecycleEvent(targetElement, "aparte-tool-approval-request", {
|
|
6076
|
+
toolCallId: event.id,
|
|
6077
|
+
toolName: event.name,
|
|
6078
|
+
input: event.input
|
|
6079
|
+
});
|
|
5507
6080
|
let decision;
|
|
5508
|
-
const resolveApproval = this.options.approvalResolver ?? ((id, sig) => this._awaitToolDecision(id, sig));
|
|
6081
|
+
const resolveApproval = this.options.approvalResolver ?? ((id, sig) => this._awaitToolDecision(id, sig, targetElement));
|
|
5509
6082
|
try {
|
|
5510
6083
|
decision = await resolveApproval(event.id, approvalController.signal);
|
|
5511
6084
|
} finally {
|
|
@@ -5535,12 +6108,17 @@ ${content}
|
|
|
5535
6108
|
}
|
|
5536
6109
|
const controller = new AbortController();
|
|
5537
6110
|
this._activeToolControllers.add(controller);
|
|
5538
|
-
const
|
|
5539
|
-
|
|
5540
|
-
const result = await handler(
|
|
6111
|
+
const { raced, done: clearToolTimeout } = withToolTimeout(
|
|
6112
|
+
(sig) => handler(
|
|
5541
6113
|
{ id: event.id, name: event.name, input: effectiveInput },
|
|
5542
|
-
|
|
5543
|
-
|
|
6114
|
+
sig,
|
|
6115
|
+
{ target: targetElement, config: this._config }
|
|
6116
|
+
),
|
|
6117
|
+
controller,
|
|
6118
|
+
this.options.toolTimeoutMs ?? DEFAULT_TOOL_HANDLER_TIMEOUT_MS
|
|
6119
|
+
);
|
|
6120
|
+
try {
|
|
6121
|
+
const result = await raced;
|
|
5544
6122
|
targetElement.updateSegment?.(toolSeg.id, { status: "resolved", result: result.content });
|
|
5545
6123
|
const existingToolCallMsg = messages.find(
|
|
5546
6124
|
(m) => m.role === "tool_call" && m.toolCalls?.some((tc) => tc.id === event.id)
|
|
@@ -5566,7 +6144,7 @@ ${content}
|
|
|
5566
6144
|
throw err;
|
|
5567
6145
|
}
|
|
5568
6146
|
} finally {
|
|
5569
|
-
|
|
6147
|
+
clearToolTimeout();
|
|
5570
6148
|
this._activeToolControllers.delete(controller);
|
|
5571
6149
|
}
|
|
5572
6150
|
} else {
|
|
@@ -5576,7 +6154,12 @@ ${content}
|
|
|
5576
6154
|
}
|
|
5577
6155
|
return { continueLoop };
|
|
5578
6156
|
}
|
|
6157
|
+
/**
|
|
6158
|
+
* Stream loop: runs one provider.chat() call and repeats if a tool was called.
|
|
6159
|
+
* Maintains a running messages array to inject tool_call / tool_result turns.
|
|
6160
|
+
*/
|
|
5579
6161
|
async _streamLoop(targetElement, messageId, provider, baseRequest, authConfig) {
|
|
6162
|
+
this._streamController?.abort();
|
|
5580
6163
|
const streamController = new AbortController();
|
|
5581
6164
|
this._streamController = streamController;
|
|
5582
6165
|
if (this.options.streamRunner) {
|
|
@@ -5595,14 +6178,14 @@ ${content}
|
|
|
5595
6178
|
}
|
|
5596
6179
|
while (continueLoop) {
|
|
5597
6180
|
if (this._isAborted) {
|
|
5598
|
-
|
|
6181
|
+
dispatchLifecycleEvent(targetElement, "aparte-message-aborted", { messageId });
|
|
5599
6182
|
break;
|
|
5600
6183
|
}
|
|
5601
6184
|
turns++;
|
|
5602
6185
|
if (turns > globalMaxTurns) {
|
|
5603
6186
|
console.warn(`[AparteClient] maxTurns (${globalMaxTurns}) exceeded — stopping loop.`);
|
|
5604
6187
|
targetElement.addSegment?.({
|
|
5605
|
-
id: `max-turns-${
|
|
6188
|
+
id: `max-turns-${uuid()}`,
|
|
5606
6189
|
type: "error",
|
|
5607
6190
|
content: `Stopped after ${globalMaxTurns} tool calls to prevent an infinite loop.`,
|
|
5608
6191
|
details: "MAX_TURNS_EXCEEDED"
|
|
@@ -5630,7 +6213,17 @@ ${content}
|
|
|
5630
6213
|
const request = { ...baseRequest, messages: phaseMessages, _meta: phaseMeta };
|
|
5631
6214
|
const response = await this._config.getTransport().chat(provider, request, authConfig, { providerId: provider.id, signal: streamController.signal });
|
|
5632
6215
|
if (typeof response === "string") {
|
|
5633
|
-
|
|
6216
|
+
const wholeParser = new AparteStreamParser();
|
|
6217
|
+
const parsed = [
|
|
6218
|
+
...wholeParser.parse(response).segments,
|
|
6219
|
+
...wholeParser.finalize()
|
|
6220
|
+
];
|
|
6221
|
+
if (parsed.length > 0) {
|
|
6222
|
+
for (const segment of parsed) targetElement.addSegment?.(segment);
|
|
6223
|
+
this._updateMessage(targetElement, messageId, { status: "completed" });
|
|
6224
|
+
} else {
|
|
6225
|
+
this._updateMessage(targetElement, messageId, { content: response, status: "completed" });
|
|
6226
|
+
}
|
|
5634
6227
|
return void 0;
|
|
5635
6228
|
}
|
|
5636
6229
|
const reader = response.getReader();
|
|
@@ -5646,7 +6239,7 @@ ${content}
|
|
|
5646
6239
|
let rawSegId = null;
|
|
5647
6240
|
let rawContent = "";
|
|
5648
6241
|
if (artifactRawHint) {
|
|
5649
|
-
rawSegId = `artifact-raw-${
|
|
6242
|
+
rawSegId = `artifact-raw-${uuid()}`;
|
|
5650
6243
|
const rawSeg = {
|
|
5651
6244
|
id: rawSegId,
|
|
5652
6245
|
type: "artifact",
|
|
@@ -5657,7 +6250,7 @@ ${content}
|
|
|
5657
6250
|
};
|
|
5658
6251
|
targetElement.addSegment?.(rawSeg);
|
|
5659
6252
|
streamingSegmentIds.add(rawSegId);
|
|
5660
|
-
|
|
6253
|
+
dispatchArtifactLifecycle(targetElement, messageId, rawSeg, artifactProgress, false);
|
|
5661
6254
|
}
|
|
5662
6255
|
const artifactXmlHint = baseRequest._meta?.artifactXml;
|
|
5663
6256
|
const xmlCtx = {
|
|
@@ -5672,25 +6265,32 @@ ${content}
|
|
|
5672
6265
|
};
|
|
5673
6266
|
let precedingText = "";
|
|
5674
6267
|
const toolCallsThisTurn = [];
|
|
6268
|
+
const bailOnAbort = () => {
|
|
6269
|
+
if (!this._isAborted) return false;
|
|
6270
|
+
try {
|
|
6271
|
+
void reader.cancel();
|
|
6272
|
+
} catch {
|
|
6273
|
+
}
|
|
6274
|
+
dispatchLifecycleEvent(targetElement, "aparte-message-aborted", { messageId });
|
|
6275
|
+
continueLoop = false;
|
|
6276
|
+
return true;
|
|
6277
|
+
};
|
|
6278
|
+
let streamDrained = false;
|
|
5675
6279
|
try {
|
|
5676
6280
|
while (true) {
|
|
5677
|
-
if (
|
|
5678
|
-
|
|
5679
|
-
|
|
5680
|
-
|
|
5681
|
-
|
|
5682
|
-
this._dispatchLifecycleEvent(targetElement, "aparte-message-aborted", { messageId });
|
|
5683
|
-
continueLoop = false;
|
|
6281
|
+
if (bailOnAbort()) break;
|
|
6282
|
+
const { done, value: event } = await reader.read();
|
|
6283
|
+
if (bailOnAbort()) break;
|
|
6284
|
+
if (done) {
|
|
6285
|
+
streamDrained = true;
|
|
5684
6286
|
break;
|
|
5685
6287
|
}
|
|
5686
|
-
const { done, value: event } = await reader.read();
|
|
5687
|
-
if (done) break;
|
|
5688
6288
|
switch (event.type) {
|
|
5689
6289
|
case "thinking": {
|
|
5690
6290
|
thinkingContent += event.delta;
|
|
5691
6291
|
if (!thinkingSegmentId) {
|
|
5692
6292
|
const seg = {
|
|
5693
|
-
id: `think-${
|
|
6293
|
+
id: `think-${uuid()}`,
|
|
5694
6294
|
type: "thinking",
|
|
5695
6295
|
content: thinkingContent,
|
|
5696
6296
|
collapsed: true,
|
|
@@ -5706,14 +6306,14 @@ ${content}
|
|
|
5706
6306
|
}
|
|
5707
6307
|
case "text": {
|
|
5708
6308
|
if (thinkingSegmentId && !thinkingCollapsed) {
|
|
5709
|
-
targetElement.updateSegment?.(thinkingSegmentId, { collapsed: true });
|
|
6309
|
+
targetElement.updateSegment?.(thinkingSegmentId, { collapsed: true, isStreaming: false });
|
|
5710
6310
|
thinkingCollapsed = true;
|
|
5711
6311
|
}
|
|
5712
6312
|
precedingText += event.delta;
|
|
5713
6313
|
if (artifactRawHint && rawSegId) {
|
|
5714
6314
|
rawContent += event.delta;
|
|
5715
6315
|
targetElement.updateSegment?.(rawSegId, { content: rawContent });
|
|
5716
|
-
|
|
6316
|
+
dispatchArtifactLifecycle(targetElement, messageId, {
|
|
5717
6317
|
id: rawSegId,
|
|
5718
6318
|
type: "artifact",
|
|
5719
6319
|
mimeType: artifactRawHint.mimeType,
|
|
@@ -5724,7 +6324,7 @@ ${content}
|
|
|
5724
6324
|
break;
|
|
5725
6325
|
}
|
|
5726
6326
|
if (artifactXmlHint) {
|
|
5727
|
-
|
|
6327
|
+
feedXmlArtifactDelta(event.delta, xmlCtx, {
|
|
5728
6328
|
targetElement,
|
|
5729
6329
|
messageId,
|
|
5730
6330
|
textParser,
|
|
@@ -5753,10 +6353,10 @@ ${content}
|
|
|
5753
6353
|
targetElement.addSegment?.(segment);
|
|
5754
6354
|
streamingSegmentIds.add(segment.id);
|
|
5755
6355
|
} else if ("content" in segment) {
|
|
5756
|
-
targetElement.updateSegment?.(segment.id,
|
|
6356
|
+
targetElement.updateSegment?.(segment.id, segmentContentUpdate(segment));
|
|
5757
6357
|
}
|
|
5758
6358
|
if (segment.type === "artifact") {
|
|
5759
|
-
|
|
6359
|
+
dispatchArtifactLifecycle(targetElement, messageId, segment, artifactProgress, true);
|
|
5760
6360
|
}
|
|
5761
6361
|
}
|
|
5762
6362
|
const active = textParser.getState().activeSegment;
|
|
@@ -5765,17 +6365,15 @@ ${content}
|
|
|
5765
6365
|
targetElement.addSegment?.(active);
|
|
5766
6366
|
streamingSegmentIds.add(active.id);
|
|
5767
6367
|
if (active.type === "artifact") {
|
|
5768
|
-
|
|
6368
|
+
dispatchArtifactLifecycle(targetElement, messageId, active, artifactProgress, false);
|
|
5769
6369
|
}
|
|
5770
6370
|
} else {
|
|
5771
|
-
targetElement.updateSegment?.(active.id,
|
|
6371
|
+
targetElement.updateSegment?.(active.id, segmentContentUpdate(active));
|
|
5772
6372
|
if (active.type === "artifact") {
|
|
5773
|
-
|
|
6373
|
+
dispatchArtifactLifecycle(targetElement, messageId, active, artifactProgress, false);
|
|
5774
6374
|
}
|
|
5775
6375
|
}
|
|
5776
6376
|
} else if (result.segments.length === 0) {
|
|
5777
|
-
if (targetElement.typeName) targetElement.typeName(event.delta);
|
|
5778
|
-
else targetElement.updateLastMessage?.(event.delta, { append: true });
|
|
5779
6377
|
}
|
|
5780
6378
|
break;
|
|
5781
6379
|
}
|
|
@@ -5792,16 +6390,22 @@ ${content}
|
|
|
5792
6390
|
case "done":
|
|
5793
6391
|
if (event.usage) lastUsage = event.usage;
|
|
5794
6392
|
break;
|
|
5795
|
-
default:
|
|
5796
|
-
|
|
6393
|
+
default: {
|
|
6394
|
+
const exhaustive = event;
|
|
6395
|
+
void exhaustive;
|
|
6396
|
+
this._warnUnknownStreamEvent(event);
|
|
6397
|
+
break;
|
|
6398
|
+
}
|
|
5797
6399
|
}
|
|
6400
|
+
if (!continueLoop) break;
|
|
5798
6401
|
}
|
|
6402
|
+
finalizeXmlArtifact(xmlCtx, { targetElement, messageId, artifactProgress, artifactXmlHint, textParser, streamingSegmentIds });
|
|
5799
6403
|
const finals = textParser.finalize();
|
|
5800
6404
|
if (artifactRawHint && rawSegId) {
|
|
5801
6405
|
const lineCount = rawContent.split("\n").length;
|
|
5802
6406
|
const isInline = lineCount < 15;
|
|
5803
6407
|
targetElement.updateSegment?.(rawSegId, { content: rawContent, inline: isInline });
|
|
5804
|
-
|
|
6408
|
+
dispatchArtifactLifecycle(targetElement, messageId, {
|
|
5805
6409
|
id: rawSegId,
|
|
5806
6410
|
type: "artifact",
|
|
5807
6411
|
mimeType: artifactRawHint.mimeType,
|
|
@@ -5811,22 +6415,6 @@ ${content}
|
|
|
5811
6415
|
inline: isInline
|
|
5812
6416
|
}, artifactProgress, true);
|
|
5813
6417
|
}
|
|
5814
|
-
if (artifactXmlHint && xmlCtx.state === "in-artifact" && xmlCtx.segId) {
|
|
5815
|
-
xmlCtx.content += xmlCtx.closeBuf;
|
|
5816
|
-
const lineCount = xmlCtx.content.split("\n").length;
|
|
5817
|
-
const isInline = lineCount < 15;
|
|
5818
|
-
targetElement.updateSegment?.(xmlCtx.segId, { content: xmlCtx.content, inline: isInline });
|
|
5819
|
-
this._dispatchArtifactLifecycle(targetElement, messageId, {
|
|
5820
|
-
id: xmlCtx.segId,
|
|
5821
|
-
type: "artifact",
|
|
5822
|
-
mimeType: xmlCtx.mime,
|
|
5823
|
-
artifactType: xmlCtx.kind,
|
|
5824
|
-
title: xmlCtx.title,
|
|
5825
|
-
content: xmlCtx.content,
|
|
5826
|
-
inline: isInline
|
|
5827
|
-
}, artifactProgress, true);
|
|
5828
|
-
console.warn("[AparteClient] XML artifact finalized without closing tag — content may be partial.");
|
|
5829
|
-
}
|
|
5830
6418
|
if (artifactHint && !artifactPromoted) {
|
|
5831
6419
|
const codeIdx = finals.findIndex((s) => s.type === "code");
|
|
5832
6420
|
if (codeIdx !== -1) {
|
|
@@ -5850,10 +6438,10 @@ ${content}
|
|
|
5850
6438
|
if (!streamingSegmentIds.has(s.id)) {
|
|
5851
6439
|
targetElement.addSegment?.(s);
|
|
5852
6440
|
} else if ("content" in s) {
|
|
5853
|
-
targetElement.updateSegment?.(s.id,
|
|
6441
|
+
targetElement.updateSegment?.(s.id, segmentContentUpdate(s));
|
|
5854
6442
|
}
|
|
5855
6443
|
if (s.type === "artifact") {
|
|
5856
|
-
|
|
6444
|
+
dispatchArtifactLifecycle(targetElement, messageId, s, artifactProgress, true);
|
|
5857
6445
|
}
|
|
5858
6446
|
}
|
|
5859
6447
|
if (toolCallsThisTurn.length === 0) {
|
|
@@ -5862,13 +6450,19 @@ ${content}
|
|
|
5862
6450
|
messages.push({ role: "assistant", content: precedingText.trim() });
|
|
5863
6451
|
}
|
|
5864
6452
|
pipelineIndex++;
|
|
5865
|
-
const pwId = `pw-${
|
|
6453
|
+
const pwId = `pw-${uuid()}`;
|
|
5866
6454
|
targetElement.addSegment?.({ id: pwId, type: "pipeline-waiting" });
|
|
5867
6455
|
} else {
|
|
5868
6456
|
continueLoop = false;
|
|
5869
6457
|
}
|
|
5870
6458
|
}
|
|
5871
6459
|
} finally {
|
|
6460
|
+
if (!streamDrained) {
|
|
6461
|
+
try {
|
|
6462
|
+
await reader.cancel();
|
|
6463
|
+
} catch {
|
|
6464
|
+
}
|
|
6465
|
+
}
|
|
5872
6466
|
reader.releaseLock();
|
|
5873
6467
|
}
|
|
5874
6468
|
}
|
|
@@ -5908,12 +6502,17 @@ ${content}
|
|
|
5908
6502
|
const response = await this._config.getTransport().chat(provider, request, authConfig, { providerId: provider.id, signal });
|
|
5909
6503
|
return typeof response === "string" ? response : readableToAsyncIterable(response, signal);
|
|
5910
6504
|
};
|
|
5911
|
-
const toolLookup = (name) =>
|
|
6505
|
+
const toolLookup = (name) => {
|
|
6506
|
+
const handler = this._config.getToolHandler(name);
|
|
6507
|
+
if (!handler) return void 0;
|
|
6508
|
+
const context = { target: targetElement, config: this._config };
|
|
6509
|
+
return (call, sig) => handler(call, sig, context);
|
|
6510
|
+
};
|
|
5912
6511
|
const toolConfigLookup = (name) => {
|
|
5913
6512
|
const tool = this._config.getTools().find((t) => t.name === name);
|
|
5914
6513
|
return tool ? { maxTurns: tool.maxTurns, needsApproval: tool.needsApproval } : void 0;
|
|
5915
6514
|
};
|
|
5916
|
-
const approvalResolver = this.options.approvalResolver ?? ((id, sig) => this._awaitToolDecision(id, sig));
|
|
6515
|
+
const approvalResolver = this.options.approvalResolver ?? ((id, sig) => this._awaitToolDecision(id, sig, targetElement));
|
|
5917
6516
|
const usage = await streamRunner({
|
|
5918
6517
|
messageId,
|
|
5919
6518
|
baseRequest,
|
|
@@ -5924,9 +6523,14 @@ ${content}
|
|
|
5924
6523
|
emitter,
|
|
5925
6524
|
signal,
|
|
5926
6525
|
maxTurns: this.options.maxTurns,
|
|
6526
|
+
// Forwarded, because the option's own JSDoc promises it means one thing
|
|
6527
|
+
// whichever loop runs — and the fix that introduced it only wired the
|
|
6528
|
+
// inline path, so setting it was silently ignored here. Exactly the
|
|
6529
|
+
// asymmetry the option was added to remove.
|
|
6530
|
+
toolTimeoutMs: this.options.toolTimeoutMs,
|
|
5927
6531
|
// Match the inline loop's id conventions: prefixed artifact ids, but a
|
|
5928
6532
|
// BARE uuid for the synthetic tool (the adapter renders `tool-<id>`).
|
|
5929
|
-
idGen: (prefix) => prefix === "synthetic-tool" ?
|
|
6533
|
+
idGen: (prefix) => prefix === "synthetic-tool" ? uuid() : `${prefix}-${uuid()}`
|
|
5930
6534
|
});
|
|
5931
6535
|
return usage ?? void 0;
|
|
5932
6536
|
}
|
|
@@ -5943,15 +6547,16 @@ ${content}
|
|
|
5943
6547
|
*/
|
|
5944
6548
|
_handleLifecycleError(target, messageId, error) {
|
|
5945
6549
|
const errorSegment = {
|
|
5946
|
-
id: `error-${
|
|
6550
|
+
id: `error-${uuid()}`,
|
|
5947
6551
|
type: "error",
|
|
5948
6552
|
content: error.message,
|
|
5949
6553
|
details: error.code
|
|
5950
6554
|
};
|
|
5951
6555
|
if (target.updateMessage) {
|
|
6556
|
+
const rendered = target.getMessages?.()?.find((m) => m.id === messageId)?.segments ?? [];
|
|
5952
6557
|
target.updateMessage(messageId, {
|
|
5953
6558
|
status: "error",
|
|
5954
|
-
segments: [errorSegment]
|
|
6559
|
+
segments: [...rendered, errorSegment]
|
|
5955
6560
|
});
|
|
5956
6561
|
} else if (target.appendMessage) {
|
|
5957
6562
|
target.appendMessage({
|
|
@@ -5962,65 +6567,17 @@ ${content}
|
|
|
5962
6567
|
segments: [errorSegment]
|
|
5963
6568
|
});
|
|
5964
6569
|
}
|
|
5965
|
-
|
|
5966
|
-
}
|
|
5967
|
-
_dispatchLifecycleEvent(target, name, detail) {
|
|
5968
|
-
target.dispatchEvent(new CustomEvent(name, {
|
|
5969
|
-
bubbles: true,
|
|
5970
|
-
composed: true,
|
|
5971
|
-
// Tag every lifecycle event with the target's id so several chats on
|
|
5972
|
-
// one page stay isolated — a composer reacts only to its own host's
|
|
5973
|
-
// turn (id-less single-instance pages still broadcast).
|
|
5974
|
-
detail: { targetId: target.id || void 0, ...detail }
|
|
5975
|
-
}));
|
|
6570
|
+
dispatchLifecycleEvent(target, "aparte-message-error", { messageId, error });
|
|
5976
6571
|
}
|
|
5977
|
-
/**
|
|
5978
|
-
|
|
5979
|
-
|
|
5980
|
-
|
|
5981
|
-
|
|
5982
|
-
|
|
5983
|
-
|
|
5984
|
-
|
|
5985
|
-
|
|
5986
|
-
if (seen === void 0) {
|
|
5987
|
-
target.dispatchEvent(new CustomEvent("aparte-artifact-start", {
|
|
5988
|
-
bubbles: true,
|
|
5989
|
-
composed: true,
|
|
5990
|
-
detail: {
|
|
5991
|
-
messageId,
|
|
5992
|
-
segmentId: id,
|
|
5993
|
-
mimeType: segment.mimeType,
|
|
5994
|
-
artifactType: segment.artifactType,
|
|
5995
|
-
title: segment.title
|
|
5996
|
-
}
|
|
5997
|
-
}));
|
|
5998
|
-
progress.set(id, 0);
|
|
5999
|
-
}
|
|
6000
|
-
const lastLen = progress.get(id) ?? 0;
|
|
6001
|
-
if (content.length > lastLen) {
|
|
6002
|
-
const chunk = content.slice(lastLen);
|
|
6003
|
-
target.dispatchEvent(new CustomEvent("aparte-artifact-delta", {
|
|
6004
|
-
bubbles: true,
|
|
6005
|
-
composed: true,
|
|
6006
|
-
detail: { segmentId: id, chunk }
|
|
6007
|
-
}));
|
|
6008
|
-
progress.set(id, content.length);
|
|
6009
|
-
}
|
|
6010
|
-
if (isFinal) {
|
|
6011
|
-
target.dispatchEvent(new CustomEvent("aparte-artifact-ready", {
|
|
6012
|
-
bubbles: true,
|
|
6013
|
-
composed: true,
|
|
6014
|
-
detail: {
|
|
6015
|
-
messageId,
|
|
6016
|
-
segmentId: id,
|
|
6017
|
-
mimeType: segment.mimeType,
|
|
6018
|
-
artifactType: segment.artifactType,
|
|
6019
|
-
title: segment.title,
|
|
6020
|
-
content
|
|
6021
|
-
}
|
|
6022
|
-
}));
|
|
6023
|
-
}
|
|
6572
|
+
/** Warn once per client for an unrecognised stream event, then carry on. */
|
|
6573
|
+
_warnedUnknownEvents = /* @__PURE__ */ new Set();
|
|
6574
|
+
_warnUnknownStreamEvent(event) {
|
|
6575
|
+
const type = String(event?.type ?? "undefined");
|
|
6576
|
+
if (this._warnedUnknownEvents.has(type)) return;
|
|
6577
|
+
this._warnedUnknownEvents.add(type);
|
|
6578
|
+
console.warn(
|
|
6579
|
+
`[AparteClient] Ignoring unrecognised stream event "${type}". The reply is unaffected; this usually means the provider or its SDK emits a part this version of aparté does not map yet.`
|
|
6580
|
+
);
|
|
6024
6581
|
}
|
|
6025
6582
|
}
|
|
6026
6583
|
function findHead(node) {
|
|
@@ -6028,7 +6585,7 @@ function findHead(node) {
|
|
|
6028
6585
|
if ("current" in node) return node;
|
|
6029
6586
|
return null;
|
|
6030
6587
|
}
|
|
6031
|
-
class
|
|
6588
|
+
class AparteMessageRepository {
|
|
6032
6589
|
_nodes = /* @__PURE__ */ new Map();
|
|
6033
6590
|
_head = null;
|
|
6034
6591
|
_root = { next: null, children: [] };
|
|
@@ -6095,7 +6652,7 @@ class MessageRepository {
|
|
|
6095
6652
|
const existing = this._nodes.get(message.id);
|
|
6096
6653
|
const parentNode = parentId ? this._nodes.get(parentId) : void 0;
|
|
6097
6654
|
if (parentId !== null && parentNode === void 0) {
|
|
6098
|
-
throw new Error(`
|
|
6655
|
+
throw new Error(`AparteMessageRepository: parent "${parentId}" not found.`);
|
|
6099
6656
|
}
|
|
6100
6657
|
const parent = parentNode ?? null;
|
|
6101
6658
|
if (existing) {
|
|
@@ -6260,7 +6817,7 @@ class MessageRepository {
|
|
|
6260
6817
|
node.next = null;
|
|
6261
6818
|
}
|
|
6262
6819
|
}
|
|
6263
|
-
const
|
|
6820
|
+
const APARTE_DEFAULT_UI_EVENTS = [
|
|
6264
6821
|
"aparte-send",
|
|
6265
6822
|
"aparte-action",
|
|
6266
6823
|
"aparte-retry",
|
|
@@ -6273,8 +6830,9 @@ function applyElementProps(el2, props, transformValue = (value) => value) {
|
|
|
6273
6830
|
for (const [key, value] of Object.entries(props)) {
|
|
6274
6831
|
if (key.startsWith("--")) {
|
|
6275
6832
|
el2.style.setProperty(key, String(value));
|
|
6276
|
-
} else if (key.startsWith("on")
|
|
6277
|
-
|
|
6833
|
+
} else if (key.startsWith("on")) {
|
|
6834
|
+
el2.removeAttribute(key);
|
|
6835
|
+
} else if (value === null || value === void 0 || value === false) {
|
|
6278
6836
|
el2.removeAttribute(key);
|
|
6279
6837
|
} else if (value === true) {
|
|
6280
6838
|
el2.setAttribute(key, "");
|
|
@@ -6295,17 +6853,30 @@ function el(tag, className, text) {
|
|
|
6295
6853
|
if (text != null) node.textContent = text;
|
|
6296
6854
|
return node;
|
|
6297
6855
|
}
|
|
6298
|
-
function fieldHeader(parent, field) {
|
|
6299
|
-
|
|
6856
|
+
function fieldHeader(parent, field, skipTitle) {
|
|
6857
|
+
let titleId;
|
|
6858
|
+
if (field.title && field.title.trim() !== skipTitle?.trim()) {
|
|
6859
|
+
const title = el("p", "aparte-elic-title", field.title);
|
|
6860
|
+
titleId = `elic-title-${uuid()}`;
|
|
6861
|
+
title.id = titleId;
|
|
6862
|
+
parent.appendChild(title);
|
|
6863
|
+
}
|
|
6300
6864
|
if (field.description) parent.appendChild(el("p", "aparte-elic-desc", field.description));
|
|
6865
|
+
return titleId;
|
|
6301
6866
|
}
|
|
6302
|
-
function
|
|
6867
|
+
function labelGroup(list, opts) {
|
|
6868
|
+
list.setAttribute("role", opts.multiple ? "group" : "radiogroup");
|
|
6869
|
+
if (opts.titleId) list.setAttribute("aria-labelledby", opts.titleId);
|
|
6870
|
+
else if (opts.fallbackLabel) list.setAttribute("aria-label", opts.fallbackLabel);
|
|
6871
|
+
}
|
|
6872
|
+
function buildEnumField(field, onChange, fallbackLabel) {
|
|
6303
6873
|
const wrap = el("div", "aparte-elic-field aparte-elic-enum");
|
|
6304
|
-
fieldHeader(wrap, field);
|
|
6874
|
+
const titleId = fieldHeader(wrap, field, fallbackLabel);
|
|
6305
6875
|
const list = el("div", "aparte-elic-options");
|
|
6306
|
-
|
|
6876
|
+
labelGroup(list, { multiple: field.multiple, titleId, fallbackLabel });
|
|
6877
|
+
const name = `elic-${uuid()}`;
|
|
6307
6878
|
const type = field.multiple ? "checkbox" : "radio";
|
|
6308
|
-
const allowOther = field.allowOther ??
|
|
6879
|
+
const allowOther = field.allowOther ?? contextConfig().getElicitationOptions().allowOther;
|
|
6309
6880
|
const defaults = new Set(Array.isArray(field.default) ? field.default : field.default != null ? [field.default] : []);
|
|
6310
6881
|
const buildOption = (value, label, description, recommended) => {
|
|
6311
6882
|
const row = el("label", "aparte-elic-option" + (recommended ? " aparte-elic-option--recommended" : ""));
|
|
@@ -6331,12 +6902,13 @@ function buildEnumField(field, onChange) {
|
|
|
6331
6902
|
control.name = name;
|
|
6332
6903
|
control.value = "__other__";
|
|
6333
6904
|
const body = el("span", "aparte-elic-option-body");
|
|
6334
|
-
|
|
6905
|
+
const t = contextConfig();
|
|
6906
|
+
body.appendChild(el("span", "aparte-elic-option-title", t.t("elicitationOther")));
|
|
6335
6907
|
otherText = el("input", "aparte-elic-other-input");
|
|
6336
6908
|
otherText.type = "text";
|
|
6337
|
-
otherText.placeholder = "
|
|
6909
|
+
otherText.placeholder = t.t("elicitationOtherPlaceholder");
|
|
6338
6910
|
otherText.style.display = "none";
|
|
6339
|
-
otherText.setAttribute("aria-label", "
|
|
6911
|
+
otherText.setAttribute("aria-label", t.t("elicitationOtherLabel"));
|
|
6340
6912
|
body.appendChild(otherText);
|
|
6341
6913
|
row.append(control, body);
|
|
6342
6914
|
list.appendChild(row);
|
|
@@ -6374,11 +6946,12 @@ function buildEnumField(field, onChange) {
|
|
|
6374
6946
|
focus: () => controls()[0]?.focus()
|
|
6375
6947
|
};
|
|
6376
6948
|
}
|
|
6377
|
-
function buildBooleanField(field, onChange) {
|
|
6949
|
+
function buildBooleanField(field, onChange, fallbackLabel) {
|
|
6378
6950
|
const wrap = el("div", "aparte-elic-field aparte-elic-boolean");
|
|
6379
|
-
fieldHeader(wrap, field);
|
|
6951
|
+
const titleId = fieldHeader(wrap, field, fallbackLabel);
|
|
6380
6952
|
const list = el("div", "aparte-elic-options");
|
|
6381
|
-
|
|
6953
|
+
labelGroup(list, { titleId, fallbackLabel });
|
|
6954
|
+
const name = `elic-${uuid()}`;
|
|
6382
6955
|
const mk = (val, label) => {
|
|
6383
6956
|
const row = el("label", "aparte-elic-option");
|
|
6384
6957
|
const control = el("input", "aparte-elic-control");
|
|
@@ -6391,8 +6964,9 @@ function buildBooleanField(field, onChange) {
|
|
|
6391
6964
|
row.append(control, body);
|
|
6392
6965
|
list.appendChild(row);
|
|
6393
6966
|
};
|
|
6394
|
-
|
|
6395
|
-
mk("
|
|
6967
|
+
const t = contextConfig();
|
|
6968
|
+
mk("true", field.trueLabel ?? t.t("elicitationYes"));
|
|
6969
|
+
mk("false", field.falseLabel ?? t.t("elicitationNo"));
|
|
6396
6970
|
wrap.appendChild(list);
|
|
6397
6971
|
list.addEventListener("change", onChange);
|
|
6398
6972
|
const checked = () => list.querySelector("input:checked");
|
|
@@ -6403,15 +6977,15 @@ function buildBooleanField(field, onChange) {
|
|
|
6403
6977
|
focus: () => list.querySelector("input")?.focus()
|
|
6404
6978
|
};
|
|
6405
6979
|
}
|
|
6406
|
-
function buildStringField(field, onChange) {
|
|
6980
|
+
function buildStringField(field, onChange, fallbackLabel) {
|
|
6407
6981
|
const wrap = el("div", "aparte-elic-field aparte-elic-string");
|
|
6408
|
-
fieldHeader(wrap, field);
|
|
6982
|
+
fieldHeader(wrap, field, fallbackLabel);
|
|
6409
6983
|
const input = field.multiline ? el("textarea", "aparte-elic-text") : el("input", "aparte-elic-text");
|
|
6410
6984
|
if (!field.multiline) input.type = "text";
|
|
6411
6985
|
if (field.placeholder) input.setAttribute("placeholder", field.placeholder);
|
|
6412
6986
|
if (field.default) input.value = field.default;
|
|
6413
6987
|
if (field.maxLength != null) input.setAttribute("maxlength", String(field.maxLength));
|
|
6414
|
-
input.setAttribute("aria-label", field.title ?? field.description ??
|
|
6988
|
+
input.setAttribute("aria-label", field.title ?? field.description ?? fallbackLabel ?? contextConfig().t("elicitationAnswerLabel"));
|
|
6415
6989
|
wrap.appendChild(input);
|
|
6416
6990
|
input.addEventListener("input", onChange);
|
|
6417
6991
|
const required = field.required ?? true;
|
|
@@ -6427,96 +7001,190 @@ function buildStringField(field, onChange) {
|
|
|
6427
7001
|
focus: () => input.focus()
|
|
6428
7002
|
};
|
|
6429
7003
|
}
|
|
6430
|
-
function buildField(field, onChange) {
|
|
7004
|
+
function buildField(field, onChange, fallbackLabel, key) {
|
|
7005
|
+
const custom = contextConfig().getElicitationFieldRenderer()?.(field, { key, notifyChange: onChange });
|
|
7006
|
+
if (custom) {
|
|
7007
|
+
return {
|
|
7008
|
+
el: custom.el,
|
|
7009
|
+
getValue: () => custom.getValue(),
|
|
7010
|
+
isComplete: () => custom.isComplete(),
|
|
7011
|
+
focus: () => custom.focus?.()
|
|
7012
|
+
};
|
|
7013
|
+
}
|
|
6431
7014
|
switch (field.type) {
|
|
6432
7015
|
case "enum":
|
|
6433
|
-
return buildEnumField(field, onChange);
|
|
7016
|
+
return buildEnumField(field, onChange, fallbackLabel);
|
|
6434
7017
|
case "boolean":
|
|
6435
|
-
return buildBooleanField(field, onChange);
|
|
7018
|
+
return buildBooleanField(field, onChange, fallbackLabel);
|
|
6436
7019
|
case "string":
|
|
6437
|
-
return buildStringField(field, onChange);
|
|
7020
|
+
return buildStringField(field, onChange, fallbackLabel);
|
|
6438
7021
|
}
|
|
6439
7022
|
}
|
|
6440
7023
|
function buildElicitationPanel(message, schema, onChange) {
|
|
6441
7024
|
const panel = el("div", "aparte-elic-panel");
|
|
6442
7025
|
if (message) panel.appendChild(el("p", "aparte-elic-message", message));
|
|
7026
|
+
const body = el("div", "aparte-elic-body");
|
|
7027
|
+
panel.appendChild(body);
|
|
7028
|
+
const dismiss = el("div", "aparte-elic-dismiss");
|
|
7029
|
+
panel.appendChild(dismiss);
|
|
6443
7030
|
if (schema.type === "object") {
|
|
7031
|
+
let show = function(index) {
|
|
7032
|
+
current = index;
|
|
7033
|
+
fields.forEach((f, i) => {
|
|
7034
|
+
f.field.el.hidden = i !== index;
|
|
7035
|
+
});
|
|
7036
|
+
syncNav();
|
|
7037
|
+
};
|
|
6444
7038
|
const entries = Object.entries(schema.properties);
|
|
6445
7039
|
const requiredKeys = new Set(schema.required ?? entries.map(([k]) => k));
|
|
7040
|
+
const cfg = contextConfig();
|
|
7041
|
+
let syncNav = () => {
|
|
7042
|
+
};
|
|
7043
|
+
const notify = () => {
|
|
7044
|
+
syncNav();
|
|
7045
|
+
onChange();
|
|
7046
|
+
};
|
|
6446
7047
|
const fields = entries.map(([key, field2]) => {
|
|
6447
|
-
const built = buildField(field2,
|
|
7048
|
+
const built = buildField(field2, notify, void 0, key);
|
|
6448
7049
|
if (!field2.title && !built.el.querySelector(".aparte-elic-title")) {
|
|
6449
7050
|
built.el.insertBefore(el("p", "aparte-elic-title", key), built.el.firstChild);
|
|
6450
7051
|
}
|
|
6451
|
-
|
|
6452
|
-
return { key, field: built, required: requiredKeys.has(key) };
|
|
7052
|
+
body.appendChild(built.el);
|
|
7053
|
+
return { key, field: built, required: requiredKeys.has(key), header: field2.header };
|
|
6453
7054
|
});
|
|
6454
|
-
|
|
7055
|
+
const isComplete = () => fields.every((f) => !f.required || f.field.isComplete());
|
|
7056
|
+
const api = {
|
|
6455
7057
|
el: panel,
|
|
7058
|
+
dismiss,
|
|
6456
7059
|
getContent: () => Object.fromEntries(fields.map((f) => [f.key, f.field.getValue()])),
|
|
6457
|
-
isComplete
|
|
6458
|
-
focus: () => fields[0]?.field.focus()
|
|
7060
|
+
isComplete,
|
|
7061
|
+
focus: () => fields[0]?.field.focus(),
|
|
7062
|
+
// A form of one behaves like a single field; only a real form steps.
|
|
7063
|
+
mode: () => "submit",
|
|
7064
|
+
canProceed: isComplete,
|
|
7065
|
+
proceed: () => {
|
|
7066
|
+
}
|
|
7067
|
+
};
|
|
7068
|
+
if (fields.length < 2 || cfg.getElicitationOptions().layout !== "stepped") return api;
|
|
7069
|
+
panel.classList.add("aparte-elic-panel--stepped");
|
|
7070
|
+
let current = 0;
|
|
7071
|
+
const steps = el("div", "aparte-elic-steps");
|
|
7072
|
+
steps.setAttribute("role", "tablist");
|
|
7073
|
+
const chips = fields.map((f, i) => {
|
|
7074
|
+
const chip = el("button", "aparte-elic-step");
|
|
7075
|
+
chip.type = "button";
|
|
7076
|
+
chip.setAttribute("role", "tab");
|
|
7077
|
+
chip.textContent = f.header?.trim() || String(i + 1);
|
|
7078
|
+
chip.addEventListener("click", () => {
|
|
7079
|
+
show(i);
|
|
7080
|
+
f.field.focus();
|
|
7081
|
+
});
|
|
7082
|
+
steps.appendChild(chip);
|
|
7083
|
+
return chip;
|
|
7084
|
+
});
|
|
7085
|
+
panel.insertBefore(steps, body);
|
|
7086
|
+
syncNav = () => {
|
|
7087
|
+
chips.forEach((chip, i) => {
|
|
7088
|
+
chip.setAttribute("aria-selected", String(i === current));
|
|
7089
|
+
chip.toggleAttribute("data-answered", fields[i].field.isComplete());
|
|
7090
|
+
});
|
|
7091
|
+
};
|
|
7092
|
+
show(0);
|
|
7093
|
+
const last = () => current === fields.length - 1;
|
|
7094
|
+
return {
|
|
7095
|
+
...api,
|
|
7096
|
+
focus: () => fields[current]?.field.focus(),
|
|
7097
|
+
mode: () => last() ? "submit" : "advance",
|
|
7098
|
+
// Advancing needs only THIS question answered; submitting needs them all,
|
|
7099
|
+
// which is what stops the last question from accepting a form with a hole
|
|
7100
|
+
// in it.
|
|
7101
|
+
canProceed: () => last() ? isComplete() : !!fields[current]?.field.isComplete(),
|
|
7102
|
+
proceed: () => {
|
|
7103
|
+
if (last()) return;
|
|
7104
|
+
show(current + 1);
|
|
7105
|
+
fields[current]?.field.focus();
|
|
7106
|
+
}
|
|
6459
7107
|
};
|
|
6460
7108
|
}
|
|
6461
|
-
const field = buildField(schema, onChange);
|
|
6462
|
-
|
|
7109
|
+
const field = buildField(schema, onChange, message);
|
|
7110
|
+
body.appendChild(field.el);
|
|
6463
7111
|
return {
|
|
6464
7112
|
el: panel,
|
|
7113
|
+
dismiss,
|
|
6465
7114
|
getContent: () => field.getValue(),
|
|
6466
7115
|
isComplete: () => field.isComplete(),
|
|
6467
|
-
focus: () => field.focus()
|
|
7116
|
+
focus: () => field.focus(),
|
|
7117
|
+
mode: () => "submit",
|
|
7118
|
+
canProceed: () => field.isComplete(),
|
|
7119
|
+
proceed: () => {
|
|
7120
|
+
}
|
|
6468
7121
|
};
|
|
6469
7122
|
}
|
|
6470
7123
|
function requestUserInput(request) {
|
|
6471
7124
|
return resolveConfig(request.target ?? null).requestUserInput(request);
|
|
6472
7125
|
}
|
|
6473
7126
|
export {
|
|
7127
|
+
segmentDuration as $,
|
|
6474
7128
|
APARTE_CONVERSATION_SCHEMA_VERSION as A,
|
|
6475
|
-
|
|
6476
|
-
|
|
6477
|
-
|
|
6478
|
-
|
|
6479
|
-
|
|
6480
|
-
|
|
6481
|
-
|
|
6482
|
-
|
|
6483
|
-
|
|
6484
|
-
|
|
6485
|
-
|
|
6486
|
-
|
|
6487
|
-
|
|
6488
|
-
|
|
6489
|
-
|
|
6490
|
-
|
|
6491
|
-
|
|
6492
|
-
|
|
6493
|
-
|
|
6494
|
-
|
|
6495
|
-
|
|
6496
|
-
|
|
6497
|
-
|
|
6498
|
-
|
|
6499
|
-
|
|
6500
|
-
|
|
6501
|
-
|
|
6502
|
-
|
|
6503
|
-
|
|
6504
|
-
|
|
6505
|
-
|
|
6506
|
-
|
|
6507
|
-
|
|
6508
|
-
|
|
6509
|
-
|
|
6510
|
-
|
|
6511
|
-
|
|
6512
|
-
|
|
6513
|
-
|
|
6514
|
-
|
|
6515
|
-
|
|
6516
|
-
|
|
6517
|
-
|
|
6518
|
-
|
|
6519
|
-
|
|
6520
|
-
|
|
7129
|
+
createStreamAdapter as B,
|
|
7130
|
+
cssEscape as C,
|
|
7131
|
+
declineDefaultRenderers as D,
|
|
7132
|
+
defaultSanitizer as E,
|
|
7133
|
+
deriveArtifactKind as F,
|
|
7134
|
+
detachConfig as G,
|
|
7135
|
+
escapeAttr as H,
|
|
7136
|
+
escapeHtml as I,
|
|
7137
|
+
filesToAttachments as J,
|
|
7138
|
+
getAllRenderers as K,
|
|
7139
|
+
getSegmentRenderer as L,
|
|
7140
|
+
installDefaultRenderersOnce as M,
|
|
7141
|
+
isAwaitingReply as N,
|
|
7142
|
+
isFormatAdapter as O,
|
|
7143
|
+
isSafeUrl as P,
|
|
7144
|
+
isSegmentSettled as Q,
|
|
7145
|
+
parseAparteEventStream as R,
|
|
7146
|
+
parseMarkdownToSegments as S,
|
|
7147
|
+
populateBubbleFromMessage as T,
|
|
7148
|
+
readableToAsyncIterable as U,
|
|
7149
|
+
registerDefaultRenderers as V,
|
|
7150
|
+
registerSegmentRenderer as W,
|
|
7151
|
+
requestUserInput as X,
|
|
7152
|
+
resolveConfig as Y,
|
|
7153
|
+
revokeAttachmentUrls as Z,
|
|
7154
|
+
runWithConfig as _,
|
|
7155
|
+
APARTE_DEFAULT_BUBBLE_ACTIONS as a,
|
|
7156
|
+
unregisterSegmentRenderer as a0,
|
|
7157
|
+
uuid as a1,
|
|
7158
|
+
stampSegmentActivity as a2,
|
|
7159
|
+
stampSegmentOnInsert as a3,
|
|
7160
|
+
stampSegmentOnUpdate as a4,
|
|
7161
|
+
renumberSegments as a5,
|
|
7162
|
+
openSegmentIds as a6,
|
|
7163
|
+
isTerminalStatus as a7,
|
|
7164
|
+
APARTE_DEFAULT_HOST_HANDLERS as b,
|
|
7165
|
+
APARTE_DEFAULT_ICON_FALLBACKS as c,
|
|
7166
|
+
APARTE_DEFAULT_LOCALE as d,
|
|
7167
|
+
APARTE_DEFAULT_SKELETON_FALLBACKS as e,
|
|
7168
|
+
APARTE_DEFAULT_UI_EVENTS as f,
|
|
7169
|
+
APARTE_HOST_ATTR as g,
|
|
7170
|
+
AparteBackendTransport as h,
|
|
7171
|
+
AparteChatHost as i,
|
|
7172
|
+
AparteClient as j,
|
|
7173
|
+
AparteConfig as k,
|
|
7174
|
+
AparteConversationController as l,
|
|
7175
|
+
AparteConversationManager as m,
|
|
7176
|
+
AparteDirectTransport as n,
|
|
7177
|
+
AparteError as o,
|
|
7178
|
+
AparteErrorCode as p,
|
|
7179
|
+
AparteMessageRepository as q,
|
|
7180
|
+
AparteStreamParser as r,
|
|
7181
|
+
aparteGlobalConfig as s,
|
|
7182
|
+
applyElementProps as t,
|
|
7183
|
+
attachConfig as u,
|
|
7184
|
+
buildElicitationPanel as v,
|
|
7185
|
+
collectRendererStyles as w,
|
|
7186
|
+
contentToText as x,
|
|
7187
|
+
contextConfig as y,
|
|
7188
|
+
createAparteChatHandler as z
|
|
6521
7189
|
};
|
|
6522
|
-
//# sourceMappingURL=index-
|
|
7190
|
+
//# sourceMappingURL=index-hQQljwfX.js.map
|