@aparte/core 0.7.1 → 0.8.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/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 +35 -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 +6664 -4877
- 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-BieFbKeW.js} +1795 -1238
- package/dist/index-BieFbKeW.js.map +1 -0
- package/dist/index.css +299 -21
- package/dist/index.d.ts +21 -13
- package/dist/index.d.ts.map +1 -1
- package/dist/index.js +447 -200
- package/dist/index.js.map +1 -1
- package/dist/index.node.d.ts +22 -13
- package/dist/index.node.d.ts.map +1 -1
- package/dist/index.node.js +54 -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.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 +206 -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 +1 -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/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>" }
|
|
@@ -301,10 +316,18 @@ class AparteStreamParser {
|
|
|
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) {
|
|
@@ -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);
|
|
@@ -477,7 +501,7 @@ class AparteStreamParser {
|
|
|
477
501
|
}
|
|
478
502
|
_generateId() {
|
|
479
503
|
++this._state.segmentCounter;
|
|
480
|
-
return `${this._options.idPrefix}-${
|
|
504
|
+
return `${this._options.idPrefix}-${uuid()}`;
|
|
481
505
|
}
|
|
482
506
|
_createTextSegment(content) {
|
|
483
507
|
return {
|
|
@@ -597,7 +621,7 @@ function parseAparteEventStream(stream) {
|
|
|
597
621
|
}
|
|
598
622
|
});
|
|
599
623
|
}
|
|
600
|
-
class
|
|
624
|
+
class AparteBackendTransport {
|
|
601
625
|
constructor(options) {
|
|
602
626
|
this.options = options;
|
|
603
627
|
}
|
|
@@ -626,11 +650,9 @@ class BackendTransport {
|
|
|
626
650
|
function createAparteChatHandler(options) {
|
|
627
651
|
const doFetch = options.fetchImpl ?? fetch;
|
|
628
652
|
return async function handler(req) {
|
|
629
|
-
|
|
630
|
-
|
|
631
|
-
|
|
632
|
-
if (!verdict) return jsonError(401, "Unauthorized.");
|
|
633
|
-
}
|
|
653
|
+
const verdict = await options.authorize(req);
|
|
654
|
+
if (verdict instanceof Response) return verdict;
|
|
655
|
+
if (!verdict) return jsonError(401, "Unauthorized.");
|
|
634
656
|
let providerId;
|
|
635
657
|
let request;
|
|
636
658
|
try {
|
|
@@ -675,11 +697,25 @@ function createAparteChatHandler(options) {
|
|
|
675
697
|
return jsonError(502, `Vendor request failed: ${err?.message ?? "network error"}`);
|
|
676
698
|
}
|
|
677
699
|
if (!vendor.ok) {
|
|
678
|
-
const
|
|
679
|
-
|
|
680
|
-
|
|
681
|
-
|
|
682
|
-
|
|
700
|
+
const raw = await vendor.text().catch(() => "");
|
|
701
|
+
let code;
|
|
702
|
+
let type;
|
|
703
|
+
try {
|
|
704
|
+
const parsed = JSON.parse(raw);
|
|
705
|
+
code = parsed?.error?.code;
|
|
706
|
+
type = parsed?.error?.type;
|
|
707
|
+
} catch {
|
|
708
|
+
}
|
|
709
|
+
return new Response(
|
|
710
|
+
JSON.stringify({
|
|
711
|
+
error: {
|
|
712
|
+
message: `Vendor request failed (HTTP ${vendor.status}).`,
|
|
713
|
+
...typeof code === "string" ? { code } : {},
|
|
714
|
+
...typeof type === "string" ? { type } : {}
|
|
715
|
+
}
|
|
716
|
+
}),
|
|
717
|
+
{ status: vendor.status, headers: { "Content-Type": "application/json" } }
|
|
718
|
+
);
|
|
683
719
|
}
|
|
684
720
|
if (request.stream === false) {
|
|
685
721
|
const json = await vendor.json().catch(() => ({}));
|
|
@@ -843,8 +879,15 @@ const TAG_ATTRS = {
|
|
|
843
879
|
const URL_ATTRS = /* @__PURE__ */ new Set(["href", "src"]);
|
|
844
880
|
const SAFE_URL = /^(?:https?:|mailto:|tel:|ftp:|sms:)/i;
|
|
845
881
|
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)
|
|
882
|
+
const SAFE_DATA_IMG = /^data:image\/(?:png|jpe?g|gif|webp|avif|bmp|x-icon|svg\+xml)[;,]/i;
|
|
847
883
|
const CONTROL_WS = /[\u0000-\u0020]+/g;
|
|
884
|
+
function isSafeSrcset(value, tag) {
|
|
885
|
+
const urlTag = tag === "source" ? "img" : tag;
|
|
886
|
+
for (const m of value.matchAll(/[a-z][a-z0-9+.-]*:[^\s,]*/gi)) {
|
|
887
|
+
if (!isSafeUrl(m[0], urlTag)) return false;
|
|
888
|
+
}
|
|
889
|
+
return true;
|
|
890
|
+
}
|
|
848
891
|
function isSafeUrl(value, tag) {
|
|
849
892
|
const v = value.replace(CONTROL_WS, "");
|
|
850
893
|
if (!v) return true;
|
|
@@ -877,6 +920,7 @@ function scrubStyle(value) {
|
|
|
877
920
|
const prop = trimmed.slice(0, idx).trim().toLowerCase();
|
|
878
921
|
const val = trimmed.slice(idx + 1);
|
|
879
922
|
if (!val.trim() || !SAFE_STYLE_PROPS.has(prop)) continue;
|
|
923
|
+
if (val.includes("\\")) continue;
|
|
880
924
|
if (/url\s*\(|expression\s*\(|javascript:|vbscript:|[<>]/i.test(val)) continue;
|
|
881
925
|
kept.push(trimmed);
|
|
882
926
|
}
|
|
@@ -890,7 +934,7 @@ function copyAttributes(src, dest, tag) {
|
|
|
890
934
|
if (name.startsWith("on") || name.startsWith("formaction")) continue;
|
|
891
935
|
if (!name.startsWith("data-") && !GLOBAL_ATTRS.has(name) && !(extra && extra.has(name))) continue;
|
|
892
936
|
if (URL_ATTRS.has(name) && !isSafeUrl(value, tag)) continue;
|
|
893
|
-
if (name === "srcset" &&
|
|
937
|
+
if (name === "srcset" && !isSafeSrcset(value, tag)) continue;
|
|
894
938
|
if (name === "style") {
|
|
895
939
|
const scrubbed = scrubStyle(value);
|
|
896
940
|
if (scrubbed === null) continue;
|
|
@@ -939,19 +983,34 @@ const defaultSanitizer = (html) => {
|
|
|
939
983
|
sanitizeChildren(doc.body, container, doc);
|
|
940
984
|
return container.innerHTML;
|
|
941
985
|
};
|
|
942
|
-
|
|
986
|
+
function escapeHtml(value) {
|
|
987
|
+
let out = "";
|
|
988
|
+
for (let i = 0; i < value.length; i++) {
|
|
989
|
+
const ch = value[i];
|
|
990
|
+
if (ch === "&") out += "&";
|
|
991
|
+
else if (ch === "<") out += "<";
|
|
992
|
+
else if (ch === ">") out += ">";
|
|
993
|
+
else if (ch === '"') out += """;
|
|
994
|
+
else if (ch === "'") out += "'";
|
|
995
|
+
else out += ch;
|
|
996
|
+
}
|
|
997
|
+
return out;
|
|
998
|
+
}
|
|
999
|
+
const escapeAttr = escapeHtml;
|
|
1000
|
+
const APARTE_DEFAULT_BUBBLE_ACTIONS = {
|
|
943
1001
|
copy: true,
|
|
944
1002
|
retry: false,
|
|
945
1003
|
edit: false,
|
|
946
1004
|
feedback: false,
|
|
947
1005
|
info: false
|
|
948
1006
|
};
|
|
949
|
-
const
|
|
1007
|
+
const APARTE_DEFAULT_HOST_HANDLERS = {
|
|
950
1008
|
attachmentPreview: false,
|
|
951
1009
|
terminalRun: false,
|
|
952
|
-
artifactRedownload: false
|
|
1010
|
+
artifactRedownload: false,
|
|
1011
|
+
artifactRehydrate: false
|
|
953
1012
|
};
|
|
954
|
-
class
|
|
1013
|
+
class AparteConfig {
|
|
955
1014
|
_markdownProvider;
|
|
956
1015
|
_streamingMarkdownProvider;
|
|
957
1016
|
_highlightProvider;
|
|
@@ -971,30 +1030,50 @@ class AparteConfigClass {
|
|
|
971
1030
|
_avatarProvider;
|
|
972
1031
|
_keyProvider;
|
|
973
1032
|
_artifactPreviewBuilder;
|
|
974
|
-
_locale =
|
|
1033
|
+
_locale = APARTE_DEFAULT_LOCALE;
|
|
975
1034
|
_actions = [];
|
|
976
1035
|
_listeners = /* @__PURE__ */ new Set();
|
|
977
1036
|
// AI Provider Management (BYORK)
|
|
978
1037
|
_aiProviders = /* @__PURE__ */ new Map();
|
|
1038
|
+
/**
|
|
1039
|
+
* What `refreshProviderModels()` last brought back, per provider.
|
|
1040
|
+
*
|
|
1041
|
+
* Without it, `getCurrentModel()` read `provider.getModels()` only — the
|
|
1042
|
+
* SYNCHRONOUS, hand-declared list, which every preset of
|
|
1043
|
+
* `@aparte/provider-openai-compat` leaves empty because a compat endpoint's
|
|
1044
|
+
* list is fetched at runtime. So the current model resolved to `undefined`
|
|
1045
|
+
* for the documented primary path, and everything that reads a capability off
|
|
1046
|
+
* it silently got nothing: `AparteClient._toolsForCurrentModel()` gates tools
|
|
1047
|
+
* on `capabilities.includes('function_calling')`, so NO tool was ever sent to
|
|
1048
|
+
* the model. A registered tool, an approval gate, `<aparte-elicitation>` — all
|
|
1049
|
+
* dead, with the model answering, correctly, that it had been given no tools.
|
|
1050
|
+
*/
|
|
1051
|
+
_fetchedModels = /* @__PURE__ */ new Map();
|
|
979
1052
|
_modelConfig = {};
|
|
980
1053
|
/** Opt-in: gate the composer (block send + grey out) until a model is selected. */
|
|
981
1054
|
_requireModelSelection = false;
|
|
982
|
-
|
|
983
|
-
|
|
1055
|
+
/** Host policy for the elicitation panel's free-text escape — see {@link setElicitationOptions}. */
|
|
1056
|
+
_elicitationAllowOther = true;
|
|
1057
|
+
_elicitationLayout = "stepped";
|
|
1058
|
+
_elicitationFieldRenderer;
|
|
1059
|
+
// Transport: where chat requests go + how auth is handled (AparteDirectTransport = browser-direct).
|
|
1060
|
+
_transport = new AparteDirectTransport();
|
|
984
1061
|
_modelPreferenceProvider;
|
|
985
1062
|
// Conversation persistence (optional, agnostic)
|
|
986
1063
|
_conversationManager;
|
|
987
|
-
// Human-in-the-loop: presents typed input requests (
|
|
1064
|
+
// Human-in-the-loop: presents typed input requests (ask_user,
|
|
988
1065
|
// tool approval, forms). Set by the <aparte-elicitation> Web Component.
|
|
989
1066
|
_elicitationPresenter;
|
|
1067
|
+
/** Warn once per config, not once per request — a tool loop can ask repeatedly. */
|
|
1068
|
+
_warnedNoPresenter = false;
|
|
990
1069
|
// Tool Registry
|
|
991
1070
|
_tools = /* @__PURE__ */ new Map();
|
|
992
1071
|
_toolRenderers = /* @__PURE__ */ new Map();
|
|
993
1072
|
// Host handlers — what the app declares it can actually complete.
|
|
994
|
-
_hostHandlers = { ...
|
|
995
|
-
// Bubble Actions —
|
|
1073
|
+
_hostHandlers = { ...APARTE_DEFAULT_HOST_HANDLERS };
|
|
1074
|
+
// Bubble Actions — APARTE_DEFAULT_BUBBLE_ACTIONS is the single source of truth
|
|
996
1075
|
// (init here, restored by reset(), and the fallback in getBubbleActions()).
|
|
997
|
-
_bubbleActionsConfig = { ...
|
|
1076
|
+
_bubbleActionsConfig = { ...APARTE_DEFAULT_BUBBLE_ACTIONS };
|
|
998
1077
|
// ─────────────────────────────────────────────────────────────────────────
|
|
999
1078
|
// Provider Setters (Dependency Injection)
|
|
1000
1079
|
// ─────────────────────────────────────────────────────────────────────────
|
|
@@ -1036,15 +1115,15 @@ class AparteConfigClass {
|
|
|
1036
1115
|
}
|
|
1037
1116
|
/**
|
|
1038
1117
|
* Configure which action buttons appear in message bubbles.
|
|
1039
|
-
* Unset keys keep their defaults — see {@link
|
|
1118
|
+
* Unset keys keep their defaults — see {@link APARTE_DEFAULT_BUBBLE_ACTIONS}: `copy`
|
|
1040
1119
|
* only, because every other button needs a host to honor it.
|
|
1041
1120
|
*
|
|
1042
1121
|
* @example
|
|
1043
|
-
*
|
|
1044
|
-
*
|
|
1045
|
-
*
|
|
1122
|
+
* aparteGlobalConfig.setBubbleActions({ retry: true, edit: true }) // you run AparteClient
|
|
1123
|
+
* aparteGlobalConfig.setBubbleActions({ feedback: true }) // you listen for aparte-feedback
|
|
1124
|
+
* aparteGlobalConfig.setBubbleActions({ copy: false }) // hide everything
|
|
1046
1125
|
* // Explicit per-role ordered sets (replace the flag defaults for that role):
|
|
1047
|
-
*
|
|
1126
|
+
* aparteGlobalConfig.setBubbleActions({ user: ['edit', 'copy'], assistant: ['copy', 'thumbUp', 'thumbDown', 'retry'] })
|
|
1048
1127
|
*/
|
|
1049
1128
|
setBubbleActions(config) {
|
|
1050
1129
|
this._bubbleActionsConfig = { ...this._bubbleActionsConfig, ...config };
|
|
@@ -1055,28 +1134,34 @@ class AparteConfigClass {
|
|
|
1055
1134
|
* trigger only for the ones you claim — see {@link AparteHostHandlersConfig}.
|
|
1056
1135
|
*
|
|
1057
1136
|
* @example
|
|
1058
|
-
*
|
|
1137
|
+
* aparteGlobalConfig.setHostHandlers({ attachmentPreview: true, terminalRun: true });
|
|
1059
1138
|
*/
|
|
1060
1139
|
setHostHandlers(config) {
|
|
1061
1140
|
this._hostHandlers = { ...this._hostHandlers, ...config };
|
|
1062
1141
|
this._notify();
|
|
1063
1142
|
}
|
|
1064
1143
|
/** Returns the resolved host-handler declarations (undeclared = false). */
|
|
1144
|
+
// `Required<...>` rather than a hand-written shape: the previous signature
|
|
1145
|
+
// listed the three keys literally, so adding a fourth to the interface left
|
|
1146
|
+
// this method silently returning a type without it — the caller's read did not
|
|
1147
|
+
// even typecheck. Derived from the interface, a new declaration cannot be
|
|
1148
|
+
// forgotten here.
|
|
1065
1149
|
getHostHandlers() {
|
|
1066
1150
|
return {
|
|
1067
|
-
attachmentPreview: this._hostHandlers.attachmentPreview ??
|
|
1068
|
-
terminalRun: this._hostHandlers.terminalRun ??
|
|
1069
|
-
artifactRedownload: this._hostHandlers.artifactRedownload ??
|
|
1151
|
+
attachmentPreview: this._hostHandlers.attachmentPreview ?? APARTE_DEFAULT_HOST_HANDLERS.attachmentPreview,
|
|
1152
|
+
terminalRun: this._hostHandlers.terminalRun ?? APARTE_DEFAULT_HOST_HANDLERS.terminalRun,
|
|
1153
|
+
artifactRedownload: this._hostHandlers.artifactRedownload ?? APARTE_DEFAULT_HOST_HANDLERS.artifactRedownload,
|
|
1154
|
+
artifactRehydrate: this._hostHandlers.artifactRehydrate ?? APARTE_DEFAULT_HOST_HANDLERS.artifactRehydrate
|
|
1070
1155
|
};
|
|
1071
1156
|
}
|
|
1072
1157
|
/** Returns the resolved bubble actions config (flag defaults applied; per-role sets passed through). */
|
|
1073
1158
|
getBubbleActions() {
|
|
1074
1159
|
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 ??
|
|
1160
|
+
copy: this._bubbleActionsConfig.copy ?? APARTE_DEFAULT_BUBBLE_ACTIONS.copy,
|
|
1161
|
+
retry: this._bubbleActionsConfig.retry ?? APARTE_DEFAULT_BUBBLE_ACTIONS.retry,
|
|
1162
|
+
edit: this._bubbleActionsConfig.edit ?? APARTE_DEFAULT_BUBBLE_ACTIONS.edit,
|
|
1163
|
+
feedback: this._bubbleActionsConfig.feedback ?? APARTE_DEFAULT_BUBBLE_ACTIONS.feedback,
|
|
1164
|
+
info: this._bubbleActionsConfig.info ?? APARTE_DEFAULT_BUBBLE_ACTIONS.info,
|
|
1080
1165
|
user: this._bubbleActionsConfig.user,
|
|
1081
1166
|
assistant: this._bubbleActionsConfig.assistant
|
|
1082
1167
|
};
|
|
@@ -1129,9 +1214,14 @@ class AparteConfigClass {
|
|
|
1129
1214
|
* hardened coverage, or `null` to DISABLE sanitization. Disabling exposes
|
|
1130
1215
|
* you to XSS from LLM-authored content — only do so for content you fully
|
|
1131
1216
|
* trust and have already sanitized upstream.
|
|
1132
|
-
* @example
|
|
1217
|
+
* @example aparteGlobalConfig.setHtmlSanitizer((html) => DOMPurify.sanitize(html));
|
|
1133
1218
|
*/
|
|
1134
1219
|
setHtmlSanitizer(sanitizer) {
|
|
1220
|
+
if (sanitizer === null) {
|
|
1221
|
+
console.warn(
|
|
1222
|
+
"[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."
|
|
1223
|
+
);
|
|
1224
|
+
}
|
|
1135
1225
|
this._sanitizer = sanitizer;
|
|
1136
1226
|
}
|
|
1137
1227
|
/**
|
|
@@ -1202,7 +1292,7 @@ class AparteConfigClass {
|
|
|
1202
1292
|
}
|
|
1203
1293
|
/**
|
|
1204
1294
|
* The icon set as a **complete** provider: every name resolves, falling back
|
|
1205
|
-
* to `
|
|
1295
|
+
* to `APARTE_DEFAULT_ICON_FALLBACKS` for anything the registered provider doesn't
|
|
1206
1296
|
* implement. Callers (bubble action bar, composer controls) can therefore
|
|
1207
1297
|
* invoke `icons.copy()` unconditionally.
|
|
1208
1298
|
*
|
|
@@ -1214,9 +1304,9 @@ class AparteConfigClass {
|
|
|
1214
1304
|
getIconProvider() {
|
|
1215
1305
|
const registered = this._iconProvider;
|
|
1216
1306
|
const complete = {};
|
|
1217
|
-
for (const name of Object.keys(
|
|
1307
|
+
for (const name of Object.keys(APARTE_DEFAULT_ICON_FALLBACKS)) {
|
|
1218
1308
|
const fn = registered?.[name];
|
|
1219
|
-
complete[name] = fn ?? (() =>
|
|
1309
|
+
complete[name] = fn ?? (() => APARTE_DEFAULT_ICON_FALLBACKS[name]);
|
|
1220
1310
|
}
|
|
1221
1311
|
return complete;
|
|
1222
1312
|
}
|
|
@@ -1315,12 +1405,12 @@ class AparteConfigClass {
|
|
|
1315
1405
|
this._notify();
|
|
1316
1406
|
}
|
|
1317
1407
|
/**
|
|
1318
|
-
* Restore the built-in English locale (the same `
|
|
1408
|
+
* Restore the built-in English locale (the same `APARTE_DEFAULT_LOCALE` core ships
|
|
1319
1409
|
* with). Counterpart of {@link setLocale} for language toggles — avoids
|
|
1320
|
-
* having to import `
|
|
1410
|
+
* having to import `APARTE_DEFAULT_LOCALE` yourself.
|
|
1321
1411
|
*/
|
|
1322
1412
|
resetLocale() {
|
|
1323
|
-
this._locale =
|
|
1413
|
+
this._locale = APARTE_DEFAULT_LOCALE;
|
|
1324
1414
|
this._notify();
|
|
1325
1415
|
}
|
|
1326
1416
|
/**
|
|
@@ -1345,7 +1435,7 @@ class AparteConfigClass {
|
|
|
1345
1435
|
getIcon(name) {
|
|
1346
1436
|
const icon = this._iconProvider?.[name];
|
|
1347
1437
|
if (icon) return icon();
|
|
1348
|
-
return
|
|
1438
|
+
return APARTE_DEFAULT_ICON_FALLBACKS[name];
|
|
1349
1439
|
}
|
|
1350
1440
|
/**
|
|
1351
1441
|
* Set a custom Key provider (e.g., AparteVault override)
|
|
@@ -1372,7 +1462,9 @@ class AparteConfigClass {
|
|
|
1372
1462
|
if (!provider || !provider.fetchModels) return [];
|
|
1373
1463
|
try {
|
|
1374
1464
|
const apiKey = await this.getKey(providerId);
|
|
1375
|
-
|
|
1465
|
+
const models = await provider.fetchModels(apiKey);
|
|
1466
|
+
this._fetchedModels.set(providerId, models);
|
|
1467
|
+
return models;
|
|
1376
1468
|
} catch (error) {
|
|
1377
1469
|
console.warn(`[AparteConfig] Failed to refresh models for ${providerId}`, error);
|
|
1378
1470
|
return [];
|
|
@@ -1385,8 +1477,8 @@ class AparteConfigClass {
|
|
|
1385
1477
|
* Register one or more AI providers (e.g., OpenRouter, Gemini, Anthropic)
|
|
1386
1478
|
* @param providers AparteAIProvider implementations
|
|
1387
1479
|
* @example
|
|
1388
|
-
*
|
|
1389
|
-
*
|
|
1480
|
+
* aparteGlobalConfig.registerAIProvider(OpenRouterProvider);
|
|
1481
|
+
* aparteGlobalConfig.registerAIProvider(GeminiProvider, AnthropicProvider);
|
|
1390
1482
|
*/
|
|
1391
1483
|
registerAIProvider(...providers) {
|
|
1392
1484
|
for (const provider of providers) {
|
|
@@ -1426,14 +1518,14 @@ class AparteConfigClass {
|
|
|
1426
1518
|
}
|
|
1427
1519
|
/**
|
|
1428
1520
|
* Set the transport that decides where chat requests go and how auth is
|
|
1429
|
-
* handled. Defaults to {@link
|
|
1430
|
-
* Use a `
|
|
1521
|
+
* handled. Defaults to {@link AparteDirectTransport} (browser-direct — BYOK/local).
|
|
1522
|
+
* Use a `AparteBackendTransport` to keep API keys server-side (recommended for
|
|
1431
1523
|
* production).
|
|
1432
1524
|
*/
|
|
1433
1525
|
setTransport(transport) {
|
|
1434
1526
|
this._transport = transport;
|
|
1435
1527
|
}
|
|
1436
|
-
/** Get the active transport (
|
|
1528
|
+
/** Get the active transport (AparteDirectTransport by default). */
|
|
1437
1529
|
getTransport() {
|
|
1438
1530
|
return this._transport;
|
|
1439
1531
|
}
|
|
@@ -1441,7 +1533,7 @@ class AparteConfigClass {
|
|
|
1441
1533
|
* Register a model preference provider for agnostic persistence.
|
|
1442
1534
|
* The host app decides how/where to store the selected provider & model.
|
|
1443
1535
|
* @example
|
|
1444
|
-
*
|
|
1536
|
+
* aparteGlobalConfig.setModelPreferenceProvider({
|
|
1445
1537
|
* save: (p, m) => localStorage.setItem('model', JSON.stringify({p, m})),
|
|
1446
1538
|
* load: () => JSON.parse(localStorage.getItem('model') ?? 'null')
|
|
1447
1539
|
* });
|
|
@@ -1515,6 +1607,53 @@ class AparteConfigClass {
|
|
|
1515
1607
|
* still fetching its list). Off by default so single-model / backend setups
|
|
1516
1608
|
* that never select a model are unaffected.
|
|
1517
1609
|
*/
|
|
1610
|
+
/**
|
|
1611
|
+
* Elicitation policy: whether a choice offers a free-text "Other…" escape.
|
|
1612
|
+
*
|
|
1613
|
+
* This is the HOST's decision, not the model's. `ask_user` used to carry an
|
|
1614
|
+
* `allow_other` field in the schema it hands the model, which meant the model
|
|
1615
|
+
* decided your UX — and a small model fills a field it does not understand: one
|
|
1616
|
+
* sent two questions with `allow_other: true` and no options at all, so the
|
|
1617
|
+
* panel rendered a radio list whose only entry was "Other…", twice.
|
|
1618
|
+
*
|
|
1619
|
+
* Every serious implementation of this pattern makes the escape hatch a property
|
|
1620
|
+
* of the surface rather than of the request. Default `true`, which is what the
|
|
1621
|
+
* panel did before, so nothing a user sees changes — only who gets to say so.
|
|
1622
|
+
* A direct `requestUserInput` caller can still set `allowOther` per field; that
|
|
1623
|
+
* is the app talking, and it wins.
|
|
1624
|
+
*
|
|
1625
|
+
* `layout` decides how a form of SEVERAL questions is presented.
|
|
1626
|
+
* `'stepped'` (default) asks them one at a time with a chip per question;
|
|
1627
|
+
* `'stacked'` puts them all in the panel at once. Stacked was the only shape,
|
|
1628
|
+
* inherited unexamined from MCP elicitation — which describes a FORM for
|
|
1629
|
+
* collecting structured data, not two different questions asked mid-conversation.
|
|
1630
|
+
* No product asks a person two questions by stacking them in one box; it is kept
|
|
1631
|
+
* as an option because the form case is real, not because it was the right default.
|
|
1632
|
+
*/
|
|
1633
|
+
setElicitationOptions(options) {
|
|
1634
|
+
if (options.allowOther !== void 0) this._elicitationAllowOther = options.allowOther;
|
|
1635
|
+
if (options.layout !== void 0) this._elicitationLayout = options.layout;
|
|
1636
|
+
this._notify();
|
|
1637
|
+
}
|
|
1638
|
+
/** The elicitation policy (see {@link setElicitationOptions}). */
|
|
1639
|
+
getElicitationOptions() {
|
|
1640
|
+
return { allowOther: this._elicitationAllowOther, layout: this._elicitationLayout };
|
|
1641
|
+
}
|
|
1642
|
+
/**
|
|
1643
|
+
* Render one field of the question panel yourself.
|
|
1644
|
+
*
|
|
1645
|
+
* See {@link AparteElicitationFieldRenderer}. Return `null` for a field to let
|
|
1646
|
+
* the built-in render it, which is what makes overriding a single kind practical.
|
|
1647
|
+
* Pass `null` here to remove the renderer.
|
|
1648
|
+
*/
|
|
1649
|
+
setElicitationFieldRenderer(fn) {
|
|
1650
|
+
this._elicitationFieldRenderer = fn ?? void 0;
|
|
1651
|
+
this._notify();
|
|
1652
|
+
}
|
|
1653
|
+
/** The custom field renderer, if one is registered. */
|
|
1654
|
+
getElicitationFieldRenderer() {
|
|
1655
|
+
return this._elicitationFieldRenderer;
|
|
1656
|
+
}
|
|
1518
1657
|
setRequireModelSelection(required) {
|
|
1519
1658
|
if (this._requireModelSelection === required) return;
|
|
1520
1659
|
this._requireModelSelection = required;
|
|
@@ -1534,10 +1673,13 @@ class AparteConfigClass {
|
|
|
1534
1673
|
if (!defaultProvider || !defaultModel) return void 0;
|
|
1535
1674
|
const provider = this._aiProviders.get(defaultProvider);
|
|
1536
1675
|
if (!provider) return void 0;
|
|
1676
|
+
const fetched = this._fetchedModels.get(defaultProvider);
|
|
1677
|
+
const fromFetch = fetched?.find((m) => m.id === defaultModel);
|
|
1678
|
+
if (fromFetch) return fromFetch;
|
|
1537
1679
|
const models = provider.getModels();
|
|
1538
1680
|
if (models instanceof Promise) {
|
|
1539
1681
|
console.warn(
|
|
1540
|
-
`[AparteConfig] Provider "${defaultProvider}".getModels() returned a Promise — it is ignored here, so model
|
|
1682
|
+
`[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
1683
|
);
|
|
1542
1684
|
return void 0;
|
|
1543
1685
|
}
|
|
@@ -1550,7 +1692,7 @@ class AparteConfigClass {
|
|
|
1550
1692
|
* Get translated string by key
|
|
1551
1693
|
*/
|
|
1552
1694
|
t(key) {
|
|
1553
|
-
const val = this._locale[key] ||
|
|
1695
|
+
const val = this._locale[key] || APARTE_DEFAULT_LOCALE[key];
|
|
1554
1696
|
return val === void 0 ? "" : val;
|
|
1555
1697
|
}
|
|
1556
1698
|
/**
|
|
@@ -1601,7 +1743,7 @@ class AparteConfigClass {
|
|
|
1601
1743
|
* Register a tool and its handler together.
|
|
1602
1744
|
* The handler is called when the AI invokes the tool during streaming.
|
|
1603
1745
|
* @example
|
|
1604
|
-
*
|
|
1746
|
+
* aparteGlobalConfig.registerTool(askUserTool, askUserHandler);
|
|
1605
1747
|
*/
|
|
1606
1748
|
registerTool(tool, handler) {
|
|
1607
1749
|
this._tools.set(tool.name, { tool, handler });
|
|
@@ -1624,12 +1766,12 @@ class AparteConfigClass {
|
|
|
1624
1766
|
* Use this instead of the generic `tool_call` segment renderer for tool-specific UI.
|
|
1625
1767
|
*
|
|
1626
1768
|
* @example
|
|
1627
|
-
* // Hide the segment entirely (UI-only tool like
|
|
1628
|
-
*
|
|
1769
|
+
* // Hide the segment entirely (UI-only tool like ask_user)
|
|
1770
|
+
* aparteGlobalConfig.registerToolRenderer('ask_user', { render: () => '' });
|
|
1629
1771
|
*
|
|
1630
1772
|
* @example
|
|
1631
1773
|
* // Custom pill for a web-search tool
|
|
1632
|
-
*
|
|
1774
|
+
* aparteGlobalConfig.registerToolRenderer('web_search', { render: (seg) => `<div class="tool-pill">Searching...</div>` });
|
|
1633
1775
|
*/
|
|
1634
1776
|
registerToolRenderer(toolName, renderer) {
|
|
1635
1777
|
this._toolRenderers.set(toolName, renderer);
|
|
@@ -1646,13 +1788,13 @@ class AparteConfigClass {
|
|
|
1646
1788
|
// Conversation Manager (optional, agnostic persistence layer)
|
|
1647
1789
|
// ─────────────────────────────────────────────────────────────────────────
|
|
1648
1790
|
/**
|
|
1649
|
-
* Register a
|
|
1791
|
+
* Register a AparteConversationManager so any UI controller can persist & load
|
|
1650
1792
|
* conversations without coupling to a framework wrapper.
|
|
1651
1793
|
*/
|
|
1652
1794
|
setConversationManager(manager) {
|
|
1653
1795
|
this._conversationManager = manager;
|
|
1654
1796
|
}
|
|
1655
|
-
/** Returns the registered
|
|
1797
|
+
/** Returns the registered AparteConversationManager, or undefined if none. */
|
|
1656
1798
|
getConversationManager() {
|
|
1657
1799
|
return this._conversationManager;
|
|
1658
1800
|
}
|
|
@@ -1674,14 +1816,29 @@ class AparteConfigClass {
|
|
|
1674
1816
|
}
|
|
1675
1817
|
/**
|
|
1676
1818
|
* Ask the user for typed input mid-run and await their response. This is the
|
|
1677
|
-
* generic primitive behind `
|
|
1819
|
+
* generic primitive behind `ask_user` and tool approval — the KIND of
|
|
1678
1820
|
* question is the schema, not a bespoke tool. Resolves `accept` with the
|
|
1679
1821
|
* value, `decline` when the user declines, or `cancel` when the turn is
|
|
1680
|
-
* cancelled.
|
|
1681
|
-
*
|
|
1822
|
+
* cancelled.
|
|
1823
|
+
*
|
|
1824
|
+
* With NO presenter registered it resolves `cancel` rather than hanging — and
|
|
1825
|
+
* warns, once, because that `cancel` is otherwise a lie told quietly. The
|
|
1826
|
+
* model reads it as "the user refused"; the user was never asked, and the
|
|
1827
|
+
* developer sees nothing at all. The default presenter is
|
|
1828
|
+
* `<aparte-elicitation>`, which registers itself from its `connectedCallback`
|
|
1829
|
+
* — so it has to be IN THE DOM. A docs page of ours claimed it "installs
|
|
1830
|
+
* itself — nothing to register", which is how this failure mode was found.
|
|
1682
1831
|
*/
|
|
1683
1832
|
requestUserInput(request) {
|
|
1684
|
-
if (!this._elicitationPresenter)
|
|
1833
|
+
if (!this._elicitationPresenter) {
|
|
1834
|
+
if (!this._warnedNoPresenter) {
|
|
1835
|
+
this._warnedNoPresenter = true;
|
|
1836
|
+
console.warn(
|
|
1837
|
+
"[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."
|
|
1838
|
+
);
|
|
1839
|
+
}
|
|
1840
|
+
return Promise.resolve({ action: "cancel" });
|
|
1841
|
+
}
|
|
1685
1842
|
return this._elicitationPresenter(request);
|
|
1686
1843
|
}
|
|
1687
1844
|
/**
|
|
@@ -1708,28 +1865,32 @@ class AparteConfigClass {
|
|
|
1708
1865
|
this._keyProvider = void 0;
|
|
1709
1866
|
this._conversationManager = void 0;
|
|
1710
1867
|
this._elicitationPresenter = void 0;
|
|
1711
|
-
this._locale =
|
|
1868
|
+
this._locale = APARTE_DEFAULT_LOCALE;
|
|
1712
1869
|
this._actions = [];
|
|
1713
1870
|
this._sanitizer = defaultSanitizer;
|
|
1714
1871
|
this._aiProviders.clear();
|
|
1872
|
+
this._fetchedModels.clear();
|
|
1715
1873
|
this._tools.clear();
|
|
1716
1874
|
this._toolRenderers.clear();
|
|
1717
1875
|
this._modelConfig = {};
|
|
1718
1876
|
this._requireModelSelection = false;
|
|
1877
|
+
this._elicitationAllowOther = true;
|
|
1878
|
+
this._elicitationLayout = "stepped";
|
|
1879
|
+
this._elicitationFieldRenderer = void 0;
|
|
1719
1880
|
this._modelPreferenceProvider = void 0;
|
|
1720
|
-
this._bubbleActionsConfig = { ...
|
|
1721
|
-
this._hostHandlers = { ...
|
|
1881
|
+
this._bubbleActionsConfig = { ...APARTE_DEFAULT_BUBBLE_ACTIONS };
|
|
1882
|
+
this._hostHandlers = { ...APARTE_DEFAULT_HOST_HANDLERS };
|
|
1722
1883
|
this._notify();
|
|
1723
1884
|
}
|
|
1724
1885
|
// ─────────────────────────────────────────────────────────────────────────
|
|
1725
1886
|
// Default Fallbacks (Zero-dependency)
|
|
1726
1887
|
// ─────────────────────────────────────────────────────────────────────────
|
|
1727
1888
|
_defaultMarkdownRenderer(raw) {
|
|
1728
|
-
const escaped =
|
|
1889
|
+
const escaped = escapeHtml(raw);
|
|
1729
1890
|
return escaped.replace(/\n/g, "<br>");
|
|
1730
1891
|
}
|
|
1731
1892
|
_defaultHighlightRenderer(code) {
|
|
1732
|
-
return `<pre><code>${
|
|
1893
|
+
return `<pre><code>${escapeHtml(code)}</code></pre>`;
|
|
1733
1894
|
}
|
|
1734
1895
|
_defaultSkeletonRenderer(type) {
|
|
1735
1896
|
const fallbacks = {
|
|
@@ -1742,34 +1903,44 @@ class AparteConfigClass {
|
|
|
1742
1903
|
};
|
|
1743
1904
|
return fallbacks[type] || fallbacks.text;
|
|
1744
1905
|
}
|
|
1745
|
-
_escapeHtml(text) {
|
|
1746
|
-
return text.replace(/&/g, "&").replace(/</g, "<").replace(/>/g, ">").replace(/"/g, """).replace(/'/g, "'");
|
|
1747
|
-
}
|
|
1748
1906
|
}
|
|
1749
|
-
const GLOBAL_CONFIG_KEY = "
|
|
1907
|
+
const GLOBAL_CONFIG_KEY = Symbol.for("aparte.globalConfig.1");
|
|
1750
1908
|
function getGlobalConfig() {
|
|
1751
1909
|
if (typeof window !== "undefined") {
|
|
1752
1910
|
if (!window[GLOBAL_CONFIG_KEY]) {
|
|
1753
|
-
window[GLOBAL_CONFIG_KEY] = new
|
|
1911
|
+
window[GLOBAL_CONFIG_KEY] = new AparteConfig();
|
|
1754
1912
|
}
|
|
1755
1913
|
return window[GLOBAL_CONFIG_KEY];
|
|
1756
1914
|
}
|
|
1757
|
-
return new
|
|
1915
|
+
return new AparteConfig();
|
|
1758
1916
|
}
|
|
1759
|
-
const
|
|
1917
|
+
const aparteGlobalConfig = getGlobalConfig();
|
|
1760
1918
|
const APARTE_HOST_ATTR = "data-aparte-host";
|
|
1761
1919
|
const CONFIG_SLOT = Symbol.for("aparte.instanceConfig");
|
|
1920
|
+
function notifyConfigChanged(root, next, previous) {
|
|
1921
|
+
if (next === previous) return;
|
|
1922
|
+
const call = (el2) => {
|
|
1923
|
+
const hook = el2.aparteConfigChanged;
|
|
1924
|
+
if (typeof hook === "function") hook.call(el2, next, previous);
|
|
1925
|
+
};
|
|
1926
|
+
call(root);
|
|
1927
|
+
for (const el2 of root.querySelectorAll("*")) call(el2);
|
|
1928
|
+
}
|
|
1762
1929
|
function attachConfig(el2, config) {
|
|
1930
|
+
const previous = el2[CONFIG_SLOT] ?? resolveConfig(el2.parentElement);
|
|
1763
1931
|
el2[CONFIG_SLOT] = config;
|
|
1764
1932
|
el2.setAttribute(APARTE_HOST_ATTR, "");
|
|
1933
|
+
notifyConfigChanged(el2, config, previous);
|
|
1765
1934
|
}
|
|
1766
1935
|
function detachConfig(el2) {
|
|
1936
|
+
const previous = el2[CONFIG_SLOT];
|
|
1767
1937
|
delete el2[CONFIG_SLOT];
|
|
1768
1938
|
el2.removeAttribute(APARTE_HOST_ATTR);
|
|
1939
|
+
if (previous) notifyConfigChanged(el2, resolveConfig(el2.parentElement), previous);
|
|
1769
1940
|
}
|
|
1770
1941
|
function resolveConfig(el2) {
|
|
1771
1942
|
const host = el2?.closest?.(`[${APARTE_HOST_ATTR}]`);
|
|
1772
|
-
return host?.[CONFIG_SLOT] ??
|
|
1943
|
+
return host?.[CONFIG_SLOT] ?? aparteGlobalConfig;
|
|
1773
1944
|
}
|
|
1774
1945
|
let _renderConfig = null;
|
|
1775
1946
|
function runWithConfig(config, fn) {
|
|
@@ -1784,7 +1955,7 @@ function runWithConfig(config, fn) {
|
|
|
1784
1955
|
function contextConfig(el2) {
|
|
1785
1956
|
return _renderConfig ?? resolveConfig(el2);
|
|
1786
1957
|
}
|
|
1787
|
-
const
|
|
1958
|
+
const APARTE_DEFAULT_SKELETON_FALLBACKS = {
|
|
1788
1959
|
message: '<div class="aparte-skeleton-fallback" style="padding:16px;color:#9ca3af;">Loading...</div>',
|
|
1789
1960
|
code: '<div class="aparte-skeleton-fallback" style="padding:16px;background:#1e293b;color:#64748b;border-radius:8px;">Loading code...</div>',
|
|
1790
1961
|
thinking: '<div class="aparte-skeleton-fallback" style="padding:8px;color:#9ca3af;">Thinking...</div>',
|
|
@@ -1792,103 +1963,45 @@ const DEFAULT_SKELETON_FALLBACKS = {
|
|
|
1792
1963
|
list: '<div class="aparte-skeleton-fallback" style="padding:16px;color:#9ca3af;">Loading items...</div>',
|
|
1793
1964
|
text: '<div class="aparte-skeleton-fallback" style="padding:8px;color:#9ca3af;">...</div>'
|
|
1794
1965
|
};
|
|
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++;
|
|
1966
|
+
function writeStreamedMarkdown(host, contentEl, content, streaming) {
|
|
1967
|
+
if (streaming) {
|
|
1968
|
+
if (host._aparteSmd === void 0) {
|
|
1969
|
+
const renderer = contextConfig().createStreamingMarkdownRenderer(contentEl);
|
|
1970
|
+
if (renderer) {
|
|
1971
|
+
contentEl.textContent = "";
|
|
1972
|
+
host._aparteSmd = { renderer, written: 0 };
|
|
1973
|
+
} else {
|
|
1974
|
+
host._aparteSmd = null;
|
|
1834
1975
|
}
|
|
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
|
-
}
|
|
1976
|
+
}
|
|
1977
|
+
const smd = host._aparteSmd;
|
|
1978
|
+
if (smd) {
|
|
1979
|
+
const delta = content.slice(smd.written);
|
|
1980
|
+
if (delta) {
|
|
1981
|
+
smd.renderer.write(delta);
|
|
1982
|
+
smd.written = content.length;
|
|
1849
1983
|
}
|
|
1984
|
+
return;
|
|
1850
1985
|
}
|
|
1851
|
-
|
|
1852
|
-
|
|
1853
|
-
|
|
1854
|
-
return -1;
|
|
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);
|
|
1986
|
+
} else if (host._aparteSmd) {
|
|
1987
|
+
host._aparteSmd.renderer.end();
|
|
1988
|
+
host._aparteSmd = void 0;
|
|
1872
1989
|
}
|
|
1873
|
-
|
|
1990
|
+
contentEl.innerHTML = contextConfig().renderMarkdown(content);
|
|
1874
1991
|
}
|
|
1875
|
-
function
|
|
1876
|
-
|
|
1877
|
-
|
|
1878
|
-
|
|
1879
|
-
|
|
1880
|
-
|
|
1881
|
-
|
|
1882
|
-
|
|
1883
|
-
|
|
1884
|
-
|
|
1885
|
-
|
|
1886
|
-
|
|
1887
|
-
styleElement = document.createElement("style");
|
|
1888
|
-
styleElement.id = "aparte-renderer-styles";
|
|
1889
|
-
document.head.appendChild(styleElement);
|
|
1992
|
+
function highlightMarkdownFences(contentEl) {
|
|
1993
|
+
const blocks = contentEl.querySelectorAll("pre > code:not([data-aparte-highlighted])");
|
|
1994
|
+
for (const block of blocks) {
|
|
1995
|
+
const pre = block.parentElement;
|
|
1996
|
+
if (!pre) continue;
|
|
1997
|
+
const lang = Array.from(block.classList).find((c) => c.startsWith("language-"))?.slice("language-".length) ?? "";
|
|
1998
|
+
const source = block.textContent ?? "";
|
|
1999
|
+
block.setAttribute("data-aparte-highlighted", "");
|
|
2000
|
+
void contextConfig().highlightCode(source, lang).then((html) => {
|
|
2001
|
+
if (pre.isConnected || pre.parentElement) pre.outerHTML = html;
|
|
2002
|
+
}).catch(() => {
|
|
2003
|
+
});
|
|
1890
2004
|
}
|
|
1891
|
-
styleElement.textContent = collectRendererStyles();
|
|
1892
2005
|
}
|
|
1893
2006
|
const textRenderer = {
|
|
1894
2007
|
type: "text",
|
|
@@ -1896,41 +2009,41 @@ const textRenderer = {
|
|
|
1896
2009
|
update: (el2, segment) => {
|
|
1897
2010
|
const contentEl = el2.querySelector(".segment-content");
|
|
1898
2011
|
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);
|
|
2012
|
+
writeStreamedMarkdown(
|
|
2013
|
+
el2,
|
|
2014
|
+
contentEl,
|
|
2015
|
+
segment.content,
|
|
2016
|
+
segment.isStreaming !== false
|
|
2017
|
+
);
|
|
1925
2018
|
},
|
|
1926
2019
|
getStyles: () => ``
|
|
1927
2020
|
};
|
|
2021
|
+
const THINKING_ANCHOR_SLACK = 24;
|
|
1928
2022
|
const thinkingRenderer = {
|
|
1929
2023
|
type: "thinking",
|
|
1930
|
-
|
|
2024
|
+
/*
|
|
2025
|
+
* Markdown, like every other prose surface.
|
|
2026
|
+
*
|
|
2027
|
+
* The reasoning was rendered as escaped plain text in a `white-space: pre-wrap`
|
|
2028
|
+
* box, so a model that formats its thinking — headings, lists, `**bold**`, a
|
|
2029
|
+
* fenced snippet, which is most of them — showed its own syntax as literal
|
|
2030
|
+
* characters. Nothing said that was deliberate; it read as one, because the
|
|
2031
|
+
* `pre-wrap` made it look like a chosen preformatted block.
|
|
2032
|
+
*
|
|
2033
|
+
* `renderMarkdown` sanitizes its provider's output before it reaches innerHTML,
|
|
2034
|
+
* so this is exactly the text renderer's path, with the same guarantees and no
|
|
2035
|
+
* new class of risk. With no Markdown provider registered it degrades to the
|
|
2036
|
+
* zero-dependency default (escape + `<br>`), which is what it used to be.
|
|
2037
|
+
*/
|
|
2038
|
+
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
2039
|
update: (el2, segment) => {
|
|
1932
2040
|
const contentEl = el2.querySelector(".thinking-content");
|
|
1933
|
-
if (contentEl)
|
|
2041
|
+
if (!contentEl) return;
|
|
2042
|
+
const wasAtBottom = contentEl.scrollHeight - contentEl.scrollTop - contentEl.clientHeight <= THINKING_ANCHOR_SLACK;
|
|
2043
|
+
const streaming = segment.isStreaming !== false;
|
|
2044
|
+
writeStreamedMarkdown(el2, contentEl, segment.content, streaming);
|
|
2045
|
+
if (!streaming) highlightMarkdownFences(contentEl);
|
|
2046
|
+
if (wasAtBottom) contentEl.scrollTop = contentEl.scrollHeight;
|
|
1934
2047
|
},
|
|
1935
2048
|
getStyles: () => ``
|
|
1936
2049
|
};
|
|
@@ -1941,7 +2054,7 @@ const codeRenderer = {
|
|
|
1941
2054
|
<div class="code-header">
|
|
1942
2055
|
${segment.filename ? `<span class="code-filename">${escapeHtml(segment.filename)}</span>` : `<span class="code-header-filler"></span>`}
|
|
1943
2056
|
<span class="code-language">${escapeHtml(segment.language || "")}</span>
|
|
1944
|
-
<button class="code-copy" data-action="copy" title="${contextConfig().t("copy")}">
|
|
2057
|
+
<button class="code-copy" data-action="copy" title="${escapeAttr(contextConfig().t("copy"))}">
|
|
1945
2058
|
${contextConfig().getIcon("copy")}
|
|
1946
2059
|
</button>
|
|
1947
2060
|
</div>
|
|
@@ -2003,8 +2116,8 @@ const terminalRenderer = {
|
|
|
2003
2116
|
</div>
|
|
2004
2117
|
<code class="terminal-command">${escapeHtml(segment.command || "")}</code>
|
|
2005
2118
|
<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")}">
|
|
2119
|
+
${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>` : ""}
|
|
2120
|
+
<button class="terminal-copy-btn" data-action="copy" aria-label="${escapeAttr(contextConfig().t("copy"))}" title="${escapeAttr(contextConfig().t("copy"))}">
|
|
2008
2121
|
${contextConfig().getIcon("copy")}
|
|
2009
2122
|
</button>
|
|
2010
2123
|
</div>
|
|
@@ -2131,13 +2244,13 @@ const toolCallRenderer = {
|
|
|
2131
2244
|
}
|
|
2132
2245
|
const customRenderer = contextConfig().getToolRenderer(segment.toolCall?.name);
|
|
2133
2246
|
if (customRenderer) {
|
|
2134
|
-
const
|
|
2135
|
-
if (
|
|
2247
|
+
const out = customRenderer.render(segment);
|
|
2248
|
+
if (out) return out;
|
|
2136
2249
|
}
|
|
2137
2250
|
const statusIcon = status === "resolved" ? contextConfig().getIcon("check") : status === "aborted" || status === "rejected" ? contextConfig().getIcon("close") : "";
|
|
2138
2251
|
const spinner = status === "pending" ? `<span class="tool-pill-spinner" aria-hidden="true"></span>` : "";
|
|
2139
2252
|
return `
|
|
2140
|
-
<div class="segment segment-tool-call" data-segment-id="${escapeHtml(segment.id)}" data-status="${status}">
|
|
2253
|
+
<div class="segment segment-tool-call" data-segment-id="${escapeHtml(segment.id)}" data-status="${escapeAttr(status)}">
|
|
2141
2254
|
<span class="tool-pill">
|
|
2142
2255
|
<span class="tool-pill-icon">${contextConfig().getIcon("tool")}</span>
|
|
2143
2256
|
<span class="tool-pill-name">${escapeHtml(name)}</span>
|
|
@@ -2151,10 +2264,11 @@ const toolCallRenderer = {
|
|
|
2151
2264
|
if (segment.status === "awaiting-approval") {
|
|
2152
2265
|
const toolCallId = segment.toolCall?.id;
|
|
2153
2266
|
if (!toolCallId) return;
|
|
2267
|
+
const host = element.closest("[data-aparte-host], aparte-chat, aparte-chat-viewport");
|
|
2154
2268
|
const decide = (approved) => element.dispatchEvent(new CustomEvent("aparte-tool-decision", {
|
|
2155
2269
|
bubbles: true,
|
|
2156
2270
|
composed: true,
|
|
2157
|
-
detail: { toolCallId, approved }
|
|
2271
|
+
detail: { toolCallId, approved, targetId: host?.id || void 0 }
|
|
2158
2272
|
}));
|
|
2159
2273
|
element.querySelector('[data-tool-decision="approve"]')?.addEventListener("click", () => decide(true));
|
|
2160
2274
|
element.querySelector('[data-tool-decision="reject"]')?.addEventListener("click", () => decide(false));
|
|
@@ -2260,464 +2374,55 @@ const pipelineWaitingRenderer = {
|
|
|
2260
2374
|
}
|
|
2261
2375
|
`
|
|
2262
2376
|
};
|
|
2263
|
-
function
|
|
2264
|
-
|
|
2265
|
-
|
|
2377
|
+
function stripCodeFences(content) {
|
|
2378
|
+
let s = content;
|
|
2379
|
+
if (s.startsWith("```") || s.startsWith("~~~")) {
|
|
2380
|
+
const fenceChar = s[0];
|
|
2381
|
+
let i = 0;
|
|
2382
|
+
while (i < s.length && s[i] === fenceChar) i++;
|
|
2383
|
+
while (i < s.length && s[i] !== "\n") i++;
|
|
2384
|
+
if (i < s.length && s[i] === "\n") i++;
|
|
2385
|
+
s = s.slice(i);
|
|
2386
|
+
}
|
|
2387
|
+
const closeAt = findClosingFence(s);
|
|
2388
|
+
if (closeAt !== -1) s = s.slice(0, closeAt);
|
|
2389
|
+
return s.trim();
|
|
2266
2390
|
}
|
|
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;
|
|
2332
|
-
}
|
|
2333
|
-
const wrapper = element.querySelector(".code-content-wrapper");
|
|
2334
|
-
if (wrapper) {
|
|
2335
|
-
const displayLang = languageForKind(kind);
|
|
2336
|
-
const cleanContent = stripCodeFences(segment.content || "");
|
|
2337
|
-
void contextConfig().highlightCode(cleanContent, displayLang).then((html) => {
|
|
2338
|
-
wrapper.innerHTML = html;
|
|
2339
|
-
}).catch(() => {
|
|
2340
|
-
});
|
|
2391
|
+
function findClosingFence(s) {
|
|
2392
|
+
let i = 0;
|
|
2393
|
+
while (i < s.length) {
|
|
2394
|
+
const lineStart = i;
|
|
2395
|
+
let k = lineStart;
|
|
2396
|
+
while (k < s.length && (s[k] === " " || s[k] === " ")) k++;
|
|
2397
|
+
if (k < s.length && (s[k] === "`" || s[k] === "~")) {
|
|
2398
|
+
const fenceChar = s[k];
|
|
2399
|
+
let runs = 0;
|
|
2400
|
+
while (k < s.length && s[k] === fenceChar) {
|
|
2401
|
+
runs++;
|
|
2402
|
+
k++;
|
|
2403
|
+
}
|
|
2404
|
+
if (runs >= 3) {
|
|
2405
|
+
let onlyWs = true;
|
|
2406
|
+
while (k < s.length && s[k] !== "\n") {
|
|
2407
|
+
if (s[k] !== " " && s[k] !== " " && s[k] !== "\r") {
|
|
2408
|
+
onlyWs = false;
|
|
2409
|
+
break;
|
|
2410
|
+
}
|
|
2411
|
+
k++;
|
|
2412
|
+
}
|
|
2413
|
+
if (onlyWs) {
|
|
2414
|
+
let cut = lineStart;
|
|
2415
|
+
if (cut > 0 && s[cut - 1] === "\n") cut--;
|
|
2416
|
+
return cut;
|
|
2417
|
+
}
|
|
2418
|
+
}
|
|
2341
2419
|
}
|
|
2342
|
-
|
|
2343
|
-
|
|
2344
|
-
const target = btn.getAttribute("data-tab-target");
|
|
2345
|
-
if (!target) return;
|
|
2346
|
-
element.setAttribute("data-tab", target);
|
|
2347
|
-
element.querySelectorAll("[data-tab-target]").forEach((b) => {
|
|
2348
|
-
b.setAttribute("aria-selected", b === btn ? "true" : "false");
|
|
2349
|
-
});
|
|
2350
|
-
});
|
|
2351
|
-
});
|
|
2352
|
-
const copyBtn = element.querySelector('[data-action="copy"]');
|
|
2353
|
-
if (copyBtn) {
|
|
2354
|
-
copyBtn.addEventListener("click", () => {
|
|
2355
|
-
const code = stripCodeFences(segment.content || "");
|
|
2356
|
-
void navigator.clipboard.writeText(code).catch(() => {
|
|
2357
|
-
});
|
|
2358
|
-
const original = copyBtn.innerHTML;
|
|
2359
|
-
copyBtn.innerHTML = contextConfig(copyBtn).getIcon("check");
|
|
2360
|
-
copyBtn.setAttribute("title", contextConfig(copyBtn).t("copied"));
|
|
2361
|
-
setTimeout(() => {
|
|
2362
|
-
copyBtn.innerHTML = original;
|
|
2363
|
-
copyBtn.setAttribute("title", contextConfig(copyBtn).t("copy"));
|
|
2364
|
-
}, 1500);
|
|
2365
|
-
});
|
|
2366
|
-
}
|
|
2367
|
-
const dlBtn = element.querySelector('[data-action="download"]');
|
|
2368
|
-
if (dlBtn) {
|
|
2369
|
-
dlBtn.addEventListener("click", () => {
|
|
2370
|
-
if (dlBtn.disabled) return;
|
|
2371
|
-
const kind2 = (segment.artifactType || "").toLowerCase();
|
|
2372
|
-
const isBinary = BINARY_FILE_KINDS.has(kind2);
|
|
2373
|
-
if (isBinary) {
|
|
2374
|
-
element.dispatchEvent(new CustomEvent("aparte-artifact-redownload", {
|
|
2375
|
-
bubbles: true,
|
|
2376
|
-
composed: true,
|
|
2377
|
-
detail: {
|
|
2378
|
-
segmentId: segment.id,
|
|
2379
|
-
mimeType: segment.mimeType,
|
|
2380
|
-
artifactType: segment.artifactType,
|
|
2381
|
-
title: segment.title,
|
|
2382
|
-
content: stripCodeFences(segment.content || "")
|
|
2383
|
-
}
|
|
2384
|
-
}));
|
|
2385
|
-
return;
|
|
2386
|
-
}
|
|
2387
|
-
downloadTextArtifact(segment);
|
|
2388
|
-
});
|
|
2389
|
-
}
|
|
2390
|
-
},
|
|
2391
|
-
update: (element, segment) => {
|
|
2392
|
-
const isStreaming = !!segment.isStreaming;
|
|
2393
|
-
const kind = (segment.artifactType || "").toLowerCase();
|
|
2394
|
-
if (BINARY_FILE_KINDS.has(kind)) {
|
|
2395
|
-
updateBinaryFileArtifact(element, segment, isStreaming);
|
|
2396
|
-
return;
|
|
2397
|
-
}
|
|
2398
|
-
const previewable = PREVIEWABLE_KINDS.has(kind);
|
|
2399
|
-
const wasStreaming = element.getAttribute("data-streaming") === "true";
|
|
2400
|
-
const cleanContent = stripCodeFences(segment.content || "");
|
|
2401
|
-
const codeEl = element.querySelector(".code-content-wrapper code");
|
|
2402
|
-
if (codeEl) {
|
|
2403
|
-
codeEl.textContent = cleanContent;
|
|
2404
|
-
} else {
|
|
2405
|
-
const wrapper = element.querySelector(".code-content-wrapper");
|
|
2406
|
-
if (wrapper) {
|
|
2407
|
-
const displayLang = languageForKind(kind);
|
|
2408
|
-
wrapper.innerHTML = `<pre><code class="language-${escapeHtml(displayLang)}">${escapeHtml(cleanContent)}</code></pre>`;
|
|
2409
|
-
}
|
|
2410
|
-
}
|
|
2411
|
-
if (isStreaming) {
|
|
2412
|
-
const segId = element.getAttribute("data-segment-id") ?? segment.id;
|
|
2413
|
-
debounceHighlight(element, ".code-content-wrapper", cleanContent, languageForKind(kind), segId);
|
|
2414
|
-
}
|
|
2415
|
-
if (wasStreaming && !isStreaming) {
|
|
2416
|
-
element.setAttribute("data-streaming", "false");
|
|
2417
|
-
const wrapper = element.querySelector(".code-content-wrapper");
|
|
2418
|
-
if (wrapper) {
|
|
2419
|
-
const displayLang = languageForKind(kind);
|
|
2420
|
-
void contextConfig().highlightCode(cleanContent, displayLang).then((html) => {
|
|
2421
|
-
wrapper.innerHTML = html;
|
|
2422
|
-
}).catch(() => {
|
|
2423
|
-
});
|
|
2424
|
-
}
|
|
2425
|
-
element.querySelectorAll("button[disabled]").forEach((b) => {
|
|
2426
|
-
b.disabled = false;
|
|
2427
|
-
});
|
|
2428
|
-
if (previewable) {
|
|
2429
|
-
const pane = element.querySelector('.aparte-art-card__pane[data-pane="preview"]');
|
|
2430
|
-
if (pane) {
|
|
2431
|
-
const title = segment.title?.trim() || labelForKind(kind);
|
|
2432
|
-
const buildPreview = contextConfig().getArtifactPreviewBuilder() ?? buildSafePreviewDocument;
|
|
2433
|
-
const srcdoc = buildPreview(kind, cleanContent, title);
|
|
2434
|
-
pane.innerHTML = `<iframe class="aparte-art-card__frame" sandbox="allow-scripts" referrerpolicy="no-referrer" loading="lazy" title="${escapeAttr(title)}" srcdoc="${escapeAttr(srcdoc)}"></iframe>`;
|
|
2435
|
-
}
|
|
2436
|
-
const currentTab = element.getAttribute("data-tab");
|
|
2437
|
-
if (currentTab !== "code") {
|
|
2438
|
-
element.setAttribute("data-tab", "preview");
|
|
2439
|
-
element.querySelectorAll("[data-tab-target]").forEach((b) => {
|
|
2440
|
-
b.setAttribute("aria-selected", b.getAttribute("data-tab-target") === "preview" ? "true" : "false");
|
|
2441
|
-
});
|
|
2442
|
-
}
|
|
2443
|
-
}
|
|
2444
|
-
}
|
|
2445
|
-
},
|
|
2446
|
-
getStyles: () => `
|
|
2447
|
-
.segment-artifact-card {
|
|
2448
|
-
display: flex; flex-direction: column;
|
|
2449
|
-
margin: 8px 0;
|
|
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
|
-
};
|
|
2699
|
-
const FILE_ICON_LABEL = {
|
|
2700
|
-
xlsx: "XLS",
|
|
2701
|
-
pdf: "PDF",
|
|
2702
|
-
docx: "DOC"
|
|
2703
|
-
};
|
|
2704
|
-
const _binaryArtifactCache = /* @__PURE__ */ new Map();
|
|
2705
|
-
const MAX_BINARY_ARTIFACTS = 24;
|
|
2706
|
-
function cacheBinaryArtifact(id, entry) {
|
|
2707
|
-
_binaryArtifactCache.delete(id);
|
|
2708
|
-
if (_binaryArtifactCache.size >= MAX_BINARY_ARTIFACTS) {
|
|
2709
|
-
const oldest = _binaryArtifactCache.keys().next().value;
|
|
2710
|
-
if (oldest !== void 0) _binaryArtifactCache.delete(oldest);
|
|
2420
|
+
while (i < s.length && s[i] !== "\n") i++;
|
|
2421
|
+
if (i < s.length) i++;
|
|
2711
2422
|
}
|
|
2712
|
-
|
|
2423
|
+
return -1;
|
|
2713
2424
|
}
|
|
2714
|
-
const _fileGenHandlers = /* @__PURE__ */ new Map();
|
|
2715
|
-
const MAX_FILE_GEN_HANDLERS = 32;
|
|
2716
|
-
const _lastDispatchAt = /* @__PURE__ */ new Map();
|
|
2717
|
-
const RE_DISPATCH_DEBOUNCE_MS = 3e4;
|
|
2718
2425
|
const _lastHighlightAt = /* @__PURE__ */ new Map();
|
|
2719
|
-
const HIGHLIGHT_DEBOUNCE_MS = 400;
|
|
2720
|
-
const MAX_THROTTLE_ENTRIES = 256;
|
|
2721
2426
|
function markThrottle(map, id, at) {
|
|
2722
2427
|
map.delete(id);
|
|
2723
2428
|
if (map.size >= MAX_THROTTLE_ENTRIES) {
|
|
@@ -2737,7 +2442,137 @@ function debounceHighlight(element, paneSelector, content, lang, segId) {
|
|
|
2737
2442
|
}).catch(() => {
|
|
2738
2443
|
});
|
|
2739
2444
|
}
|
|
2740
|
-
|
|
2445
|
+
function labelForKind(kind) {
|
|
2446
|
+
switch (kind) {
|
|
2447
|
+
case "react":
|
|
2448
|
+
return "React component";
|
|
2449
|
+
case "html":
|
|
2450
|
+
return "HTML document";
|
|
2451
|
+
case "svg":
|
|
2452
|
+
return "SVG image";
|
|
2453
|
+
case "js":
|
|
2454
|
+
return "JavaScript snippet";
|
|
2455
|
+
case "css":
|
|
2456
|
+
return "CSS stylesheet";
|
|
2457
|
+
case "json":
|
|
2458
|
+
return "JSON document";
|
|
2459
|
+
case "markdown":
|
|
2460
|
+
return "Markdown document";
|
|
2461
|
+
case "csv":
|
|
2462
|
+
return "CSV table";
|
|
2463
|
+
case "text":
|
|
2464
|
+
return "Text file";
|
|
2465
|
+
case "python":
|
|
2466
|
+
return "Python script";
|
|
2467
|
+
case "typescript":
|
|
2468
|
+
return "TypeScript file";
|
|
2469
|
+
case "bash":
|
|
2470
|
+
return "Bash script";
|
|
2471
|
+
case "sql":
|
|
2472
|
+
return "SQL query";
|
|
2473
|
+
case "pdf":
|
|
2474
|
+
return "PDF generator";
|
|
2475
|
+
case "xlsx":
|
|
2476
|
+
return "Excel generator";
|
|
2477
|
+
case "docx":
|
|
2478
|
+
return "Word generator";
|
|
2479
|
+
default:
|
|
2480
|
+
return "Artifact";
|
|
2481
|
+
}
|
|
2482
|
+
}
|
|
2483
|
+
const MAX_THROTTLE_ENTRIES = 256;
|
|
2484
|
+
const HIGHLIGHT_DEBOUNCE_MS = 400;
|
|
2485
|
+
const PREVIEW_CSP = "default-src 'none'; script-src 'unsafe-inline'; style-src 'unsafe-inline'; img-src data:; font-src data:";
|
|
2486
|
+
function withMetaCsp(doc) {
|
|
2487
|
+
const meta = `<meta http-equiv="Content-Security-Policy" content="${escapeAttr(PREVIEW_CSP)}">`;
|
|
2488
|
+
const doctype = doc.match(/^\s*<!doctype[^>]*>/i);
|
|
2489
|
+
const at = doctype ? doctype[0].length : 0;
|
|
2490
|
+
return `${doc.slice(0, at)}<head>${meta}</head>${doc.slice(at)}`;
|
|
2491
|
+
}
|
|
2492
|
+
function buildSafePreviewDocument(kind, body, title) {
|
|
2493
|
+
return withMetaCsp(buildPreviewBody(kind, body, title));
|
|
2494
|
+
}
|
|
2495
|
+
function buildPreviewBody(kind, body, title) {
|
|
2496
|
+
switch (kind) {
|
|
2497
|
+
case "html": {
|
|
2498
|
+
if (startsWithDoctype(body)) return body;
|
|
2499
|
+
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>`;
|
|
2500
|
+
}
|
|
2501
|
+
case "svg":
|
|
2502
|
+
return `<!doctype html><html><head><meta charset="utf-8"/><title>${escapeAttr(title)}</title>
|
|
2503
|
+
<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>
|
|
2504
|
+
</head><body>${body}</body></html>`;
|
|
2505
|
+
case "js": {
|
|
2506
|
+
const safeBody = escapeClosingScriptTag(body);
|
|
2507
|
+
return `<!doctype html><html><head><meta charset="utf-8"/><title>${escapeAttr(title)}</title>
|
|
2508
|
+
<style>body{margin:0;font-family:ui-sans-serif,system-ui,sans-serif;padding:1rem;background:#fff;color:#0f172a}</style>
|
|
2509
|
+
</head><body><div id="root"></div><script>
|
|
2510
|
+
try { ${safeBody}
|
|
2511
|
+
} catch (e) { document.getElementById('root').innerHTML = '<pre style="color:#b91c1c">' + (e && e.stack || e) + '</pre>'; }
|
|
2512
|
+
<\/script></body></html>`;
|
|
2513
|
+
}
|
|
2514
|
+
case "css":
|
|
2515
|
+
return `<!doctype html><html><head><meta charset="utf-8"/><title>${escapeAttr(title)}</title>
|
|
2516
|
+
<style>${body}</style></head><body>
|
|
2517
|
+
<div class="demo">
|
|
2518
|
+
<h1>Heading</h1>
|
|
2519
|
+
<p>Paragraph with a <a href="#">link</a> and <strong>strong</strong> text.</p>
|
|
2520
|
+
<button>Button</button>
|
|
2521
|
+
<input placeholder="Input"/>
|
|
2522
|
+
<ul><li>One</li><li>Two</li><li>Three</li></ul>
|
|
2523
|
+
</div></body></html>`;
|
|
2524
|
+
default:
|
|
2525
|
+
return `<!doctype html><html><head><meta charset="utf-8"/><title>${escapeAttr(title)}</title>
|
|
2526
|
+
<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>
|
|
2527
|
+
</head><body><pre>${escapeHtml(body)}</pre></body></html>`;
|
|
2528
|
+
}
|
|
2529
|
+
}
|
|
2530
|
+
function startsWithDoctype(s) {
|
|
2531
|
+
let i = 0;
|
|
2532
|
+
while (i < s.length && (s[i] === " " || s[i] === " " || s[i] === "\n" || s[i] === "\r")) i++;
|
|
2533
|
+
const probe = s.slice(i, i + 9).toLowerCase();
|
|
2534
|
+
return probe === "<!doctype";
|
|
2535
|
+
}
|
|
2536
|
+
function escapeClosingScriptTag(body) {
|
|
2537
|
+
let out = "";
|
|
2538
|
+
let i = 0;
|
|
2539
|
+
while (i < body.length) {
|
|
2540
|
+
if (body[i] === "<" && body.slice(i, i + 8).toLowerCase() === "<\/script") {
|
|
2541
|
+
const next = body[i + 8];
|
|
2542
|
+
if (next === void 0 || next === "/" || next === ">" || /\s/.test(next)) {
|
|
2543
|
+
out += "<\\/script";
|
|
2544
|
+
i += 8;
|
|
2545
|
+
continue;
|
|
2546
|
+
}
|
|
2547
|
+
}
|
|
2548
|
+
out += body[i];
|
|
2549
|
+
i++;
|
|
2550
|
+
}
|
|
2551
|
+
return out;
|
|
2552
|
+
}
|
|
2553
|
+
const FILE_ICON_LABEL = {
|
|
2554
|
+
xlsx: "XLS",
|
|
2555
|
+
pdf: "PDF",
|
|
2556
|
+
docx: "DOC"
|
|
2557
|
+
};
|
|
2558
|
+
const _binaryArtifactCache = /* @__PURE__ */ new Map();
|
|
2559
|
+
const MAX_BINARY_ARTIFACTS = 24;
|
|
2560
|
+
function cacheBinaryArtifact(id, entry) {
|
|
2561
|
+
_binaryArtifactCache.delete(id);
|
|
2562
|
+
if (_binaryArtifactCache.size >= MAX_BINARY_ARTIFACTS) {
|
|
2563
|
+
const oldest = _binaryArtifactCache.keys().next().value;
|
|
2564
|
+
if (oldest !== void 0) _binaryArtifactCache.delete(oldest);
|
|
2565
|
+
}
|
|
2566
|
+
_binaryArtifactCache.set(id, entry);
|
|
2567
|
+
}
|
|
2568
|
+
const _fileGenHandlers = /* @__PURE__ */ new Map();
|
|
2569
|
+
const MAX_FILE_GEN_HANDLERS = 32;
|
|
2570
|
+
const _lastDispatchAt = /* @__PURE__ */ new Map();
|
|
2571
|
+
const RE_DISPATCH_DEBOUNCE_MS = 3e4;
|
|
2572
|
+
let _artifactReadyHookInstalled = false;
|
|
2573
|
+
function installArtifactReadyHook() {
|
|
2574
|
+
if (_artifactReadyHookInstalled || typeof window === "undefined") return;
|
|
2575
|
+
_artifactReadyHookInstalled = true;
|
|
2741
2576
|
window.addEventListener("aparte-artifact-ready", (event) => {
|
|
2742
2577
|
const segId = event.detail?.segmentId;
|
|
2743
2578
|
if (segId) markThrottle(_lastDispatchAt, segId, Date.now());
|
|
@@ -2865,6 +2700,7 @@ function setupBinaryFileArtifact(element, segment, kind) {
|
|
|
2865
2700
|
);
|
|
2866
2701
|
return;
|
|
2867
2702
|
}
|
|
2703
|
+
if (!contextConfig().getHostHandlers().artifactRehydrate) return;
|
|
2868
2704
|
const lastAt = _lastDispatchAt.get(segment.id) ?? 0;
|
|
2869
2705
|
if (Date.now() - lastAt < RE_DISPATCH_DEBOUNCE_MS) return;
|
|
2870
2706
|
markThrottle(_lastDispatchAt, segment.id, Date.now());
|
|
@@ -2905,92 +2741,473 @@ function updateBinaryFileArtifact(element, segment, isStreaming) {
|
|
|
2905
2741
|
element.setAttribute("data-state", "compiling");
|
|
2906
2742
|
const wrapper = element.querySelector('[data-role="code-pane"]');
|
|
2907
2743
|
if (wrapper) {
|
|
2908
|
-
void contextConfig(element).highlightCode(cleanContent, "js").then((html) => {
|
|
2744
|
+
void contextConfig(element).highlightCode(cleanContent, "js").then((html) => {
|
|
2745
|
+
wrapper.innerHTML = html;
|
|
2746
|
+
}).catch(() => {
|
|
2747
|
+
});
|
|
2748
|
+
}
|
|
2749
|
+
const sub = element.querySelector('[data-role="file-sub"]');
|
|
2750
|
+
if (sub) sub.textContent = "Running sandbox…";
|
|
2751
|
+
}
|
|
2752
|
+
}
|
|
2753
|
+
function swapToPreview(element, detail, kind) {
|
|
2754
|
+
element.setAttribute("data-state", "ready");
|
|
2755
|
+
const codePane = element.querySelector('[data-role="code-pane"]');
|
|
2756
|
+
if (codePane) codePane.hidden = true;
|
|
2757
|
+
const preview = element.querySelector('[data-role="preview-pane"]');
|
|
2758
|
+
if (preview) {
|
|
2759
|
+
preview.innerHTML = detail.previewHtml ? contextConfig().sanitizeHtml(detail.previewHtml) : `<div class="aparte-art-file__preview-empty">Preview not available for ${escapeHtml(kind)} yet</div>`;
|
|
2760
|
+
preview.hidden = false;
|
|
2761
|
+
}
|
|
2762
|
+
const nameEl = element.querySelector('[data-role="file-name"]');
|
|
2763
|
+
if (nameEl) nameEl.textContent = detail.filename;
|
|
2764
|
+
const sub = element.querySelector('[data-role="file-sub"]');
|
|
2765
|
+
if (sub) sub.textContent = `${formatBytes(detail.bytes)} · ${kind.toUpperCase()}`;
|
|
2766
|
+
const dlBtn = element.querySelector('[data-action="download"]');
|
|
2767
|
+
if (dlBtn) dlBtn.disabled = false;
|
|
2768
|
+
}
|
|
2769
|
+
function formatBytes(n) {
|
|
2770
|
+
if (n < 1024) return `${n} B`;
|
|
2771
|
+
if (n < 1024 * 1024) return `${(n / 1024).toFixed(1)} KB`;
|
|
2772
|
+
return `${(n / (1024 * 1024)).toFixed(2)} MB`;
|
|
2773
|
+
}
|
|
2774
|
+
function showSandboxError(element, phase, errorMsg) {
|
|
2775
|
+
const state = element.getAttribute("data-state");
|
|
2776
|
+
if (state === "ready") return;
|
|
2777
|
+
element.setAttribute("data-state", "error");
|
|
2778
|
+
const sub = element.querySelector('[data-role="file-sub"]');
|
|
2779
|
+
if (sub) sub.textContent = `Error (${phase})`;
|
|
2780
|
+
const body = element.querySelector(".aparte-art-file__body");
|
|
2781
|
+
if (body) {
|
|
2782
|
+
const short = (errorMsg.split("\n")[0] ?? "").slice(0, 240);
|
|
2783
|
+
body.innerHTML = `
|
|
2784
|
+
<div class="aparte-art-file__error">
|
|
2785
|
+
<div class="aparte-art-file__error-title">The sandbox failed during generation.</div>
|
|
2786
|
+
<div class="aparte-art-file__error-msg">${escapeHtml(short)}</div>
|
|
2787
|
+
<div class="aparte-art-file__error-hint">Common cause: the model produced invalid code (undefined variable, wrong argument type). Retry the request — the model may produce different code.</div>
|
|
2788
|
+
</div>
|
|
2789
|
+
`;
|
|
2790
|
+
}
|
|
2791
|
+
}
|
|
2792
|
+
const PREVIEWABLE_KINDS = /* @__PURE__ */ new Set(["react", "html", "svg", "js", "css"]);
|
|
2793
|
+
const BINARY_FILE_KINDS = /* @__PURE__ */ new Set(["pdf", "xlsx", "docx"]);
|
|
2794
|
+
const artifactRenderer = {
|
|
2795
|
+
type: "artifact",
|
|
2796
|
+
render: (segment) => {
|
|
2797
|
+
const kind = (segment.artifactType || "unknown").toLowerCase();
|
|
2798
|
+
if (BINARY_FILE_KINDS.has(kind)) {
|
|
2799
|
+
return renderBinaryFileArtifact(segment, kind);
|
|
2800
|
+
}
|
|
2801
|
+
const title = segment.title?.trim() || labelForKind(kind);
|
|
2802
|
+
const displayLang = languageForKind(kind);
|
|
2803
|
+
const isStreaming = !!segment.isStreaming;
|
|
2804
|
+
const previewable = PREVIEWABLE_KINDS.has(kind);
|
|
2805
|
+
const isBinary = BINARY_FILE_KINDS.has(kind);
|
|
2806
|
+
const cleanContent = stripCodeFences(segment.content || "");
|
|
2807
|
+
return `
|
|
2808
|
+
<div class="segment segment-artifact-card"
|
|
2809
|
+
data-segment-id="${escapeHtml(segment.id)}"
|
|
2810
|
+
data-artifact-type="${escapeHtml(kind)}"
|
|
2811
|
+
data-streaming="${isStreaming ? "true" : "false"}"
|
|
2812
|
+
data-tab="code"
|
|
2813
|
+
data-previewable="${previewable ? "true" : "false"}"
|
|
2814
|
+
data-binary="${isBinary ? "true" : "false"}">
|
|
2815
|
+
<header class="aparte-art-card__header">
|
|
2816
|
+
<div class="aparte-art-card__title-block">
|
|
2817
|
+
<span class="aparte-art-card__kind" data-kind="${escapeHtml(kind)}">${escapeHtml(displayLang)}</span>
|
|
2818
|
+
<span class="aparte-art-card__title">${escapeHtml(title)}</span>
|
|
2819
|
+
${isStreaming ? '<span class="aparte-art-card__pulse" aria-label="Streaming"></span>' : ""}
|
|
2820
|
+
</div>
|
|
2821
|
+
<div class="aparte-art-card__actions">
|
|
2822
|
+
<button type="button" class="aparte-art-card__btn" data-action="copy" title="${escapeAttr(contextConfig().t("copy"))}" aria-label="Copy">
|
|
2823
|
+
${contextConfig().getIcon("copy")}
|
|
2824
|
+
</button>
|
|
2825
|
+
<button type="button" class="aparte-art-card__btn" data-action="download" title="Download" aria-label="Download" ${isStreaming ? "disabled" : ""}>
|
|
2826
|
+
<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>
|
|
2827
|
+
</button>
|
|
2828
|
+
</div>
|
|
2829
|
+
</header>
|
|
2830
|
+
<nav class="aparte-art-card__tabs" role="tablist">
|
|
2831
|
+
${previewable ? `<button type="button" role="tab" data-tab-target="preview" aria-selected="false" ${isStreaming ? "disabled" : ""}>Preview</button>` : ""}
|
|
2832
|
+
<button type="button" role="tab" data-tab-target="code" aria-selected="true">Code</button>
|
|
2833
|
+
</nav>
|
|
2834
|
+
<div class="aparte-art-card__body">
|
|
2835
|
+
<div class="aparte-art-card__pane" data-pane="code">
|
|
2836
|
+
<div class="code-content-wrapper">
|
|
2837
|
+
<pre><code class="language-${escapeHtml(displayLang)}">${escapeHtml(cleanContent)}</code></pre>
|
|
2838
|
+
</div>
|
|
2839
|
+
</div>
|
|
2840
|
+
${previewable ? `
|
|
2841
|
+
<div class="aparte-art-card__pane" data-pane="preview">
|
|
2842
|
+
<div class="aparte-art-card__pending">Press Preview to run this artifact.</div>
|
|
2843
|
+
</div>
|
|
2844
|
+
` : ""}
|
|
2845
|
+
</div>
|
|
2846
|
+
</div>
|
|
2847
|
+
`;
|
|
2848
|
+
},
|
|
2849
|
+
setup: (element, segment) => {
|
|
2850
|
+
latestSegment.set(element, segment);
|
|
2851
|
+
const kind = (segment.artifactType || "").toLowerCase();
|
|
2852
|
+
if (BINARY_FILE_KINDS.has(kind)) {
|
|
2853
|
+
setupBinaryFileArtifact(element, segment, kind);
|
|
2854
|
+
return;
|
|
2855
|
+
}
|
|
2856
|
+
const wrapper = element.querySelector(".code-content-wrapper");
|
|
2857
|
+
if (wrapper) {
|
|
2858
|
+
const displayLang = languageForKind(kind);
|
|
2859
|
+
const cleanContent = stripCodeFences(segment.content || "");
|
|
2860
|
+
void contextConfig().highlightCode(cleanContent, displayLang).then((html) => {
|
|
2909
2861
|
wrapper.innerHTML = html;
|
|
2910
2862
|
}).catch(() => {
|
|
2911
2863
|
});
|
|
2912
2864
|
}
|
|
2913
|
-
|
|
2914
|
-
|
|
2915
|
-
|
|
2916
|
-
|
|
2917
|
-
|
|
2918
|
-
|
|
2919
|
-
|
|
2920
|
-
|
|
2921
|
-
|
|
2922
|
-
|
|
2923
|
-
|
|
2924
|
-
|
|
2925
|
-
|
|
2926
|
-
|
|
2927
|
-
|
|
2928
|
-
|
|
2929
|
-
|
|
2930
|
-
|
|
2931
|
-
|
|
2932
|
-
|
|
2933
|
-
|
|
2934
|
-
|
|
2935
|
-
|
|
2936
|
-
|
|
2937
|
-
}
|
|
2938
|
-
|
|
2939
|
-
|
|
2940
|
-
|
|
2941
|
-
|
|
2942
|
-
|
|
2943
|
-
|
|
2944
|
-
|
|
2945
|
-
|
|
2946
|
-
|
|
2947
|
-
|
|
2948
|
-
|
|
2949
|
-
|
|
2950
|
-
|
|
2951
|
-
|
|
2952
|
-
|
|
2953
|
-
|
|
2954
|
-
|
|
2955
|
-
}
|
|
2956
|
-
|
|
2957
|
-
|
|
2958
|
-
|
|
2959
|
-
|
|
2960
|
-
|
|
2961
|
-
|
|
2962
|
-
|
|
2963
|
-
|
|
2964
|
-
|
|
2965
|
-
|
|
2966
|
-
|
|
2967
|
-
|
|
2968
|
-
|
|
2969
|
-
|
|
2970
|
-
|
|
2971
|
-
|
|
2972
|
-
|
|
2973
|
-
return
|
|
2974
|
-
|
|
2975
|
-
|
|
2976
|
-
|
|
2977
|
-
|
|
2978
|
-
|
|
2979
|
-
|
|
2980
|
-
|
|
2981
|
-
|
|
2982
|
-
|
|
2983
|
-
|
|
2984
|
-
|
|
2985
|
-
|
|
2986
|
-
|
|
2987
|
-
|
|
2988
|
-
|
|
2989
|
-
|
|
2990
|
-
|
|
2991
|
-
|
|
2992
|
-
|
|
2993
|
-
|
|
2865
|
+
element.querySelectorAll("[data-tab-target]").forEach((btn) => {
|
|
2866
|
+
btn.addEventListener("click", () => {
|
|
2867
|
+
const target = btn.getAttribute("data-tab-target");
|
|
2868
|
+
if (!target) return;
|
|
2869
|
+
if (target === "preview") mountPreviewFrame(element, segment);
|
|
2870
|
+
element.setAttribute("data-tab", target);
|
|
2871
|
+
element.querySelectorAll("[data-tab-target]").forEach((b) => {
|
|
2872
|
+
b.setAttribute("aria-selected", b === btn ? "true" : "false");
|
|
2873
|
+
});
|
|
2874
|
+
});
|
|
2875
|
+
});
|
|
2876
|
+
const copyBtn = element.querySelector('[data-action="copy"]');
|
|
2877
|
+
if (copyBtn) {
|
|
2878
|
+
copyBtn.addEventListener("click", () => {
|
|
2879
|
+
const code = stripCodeFences(segment.content || "");
|
|
2880
|
+
void navigator.clipboard.writeText(code).catch(() => {
|
|
2881
|
+
});
|
|
2882
|
+
const original = copyBtn.innerHTML;
|
|
2883
|
+
copyBtn.innerHTML = contextConfig(copyBtn).getIcon("check");
|
|
2884
|
+
copyBtn.setAttribute("title", contextConfig(copyBtn).t("copied"));
|
|
2885
|
+
setTimeout(() => {
|
|
2886
|
+
copyBtn.innerHTML = original;
|
|
2887
|
+
copyBtn.setAttribute("title", contextConfig(copyBtn).t("copy"));
|
|
2888
|
+
}, 1500);
|
|
2889
|
+
});
|
|
2890
|
+
}
|
|
2891
|
+
const dlBtn = element.querySelector('[data-action="download"]');
|
|
2892
|
+
if (dlBtn) {
|
|
2893
|
+
dlBtn.addEventListener("click", () => {
|
|
2894
|
+
if (dlBtn.disabled) return;
|
|
2895
|
+
const kind2 = (segment.artifactType || "").toLowerCase();
|
|
2896
|
+
const isBinary = BINARY_FILE_KINDS.has(kind2);
|
|
2897
|
+
if (isBinary) {
|
|
2898
|
+
element.dispatchEvent(new CustomEvent("aparte-artifact-redownload", {
|
|
2899
|
+
bubbles: true,
|
|
2900
|
+
composed: true,
|
|
2901
|
+
detail: {
|
|
2902
|
+
segmentId: segment.id,
|
|
2903
|
+
mimeType: segment.mimeType,
|
|
2904
|
+
artifactType: segment.artifactType,
|
|
2905
|
+
title: segment.title,
|
|
2906
|
+
content: stripCodeFences(segment.content || "")
|
|
2907
|
+
}
|
|
2908
|
+
}));
|
|
2909
|
+
return;
|
|
2910
|
+
}
|
|
2911
|
+
downloadTextArtifact(segment);
|
|
2912
|
+
});
|
|
2913
|
+
}
|
|
2914
|
+
},
|
|
2915
|
+
update: (element, segment) => {
|
|
2916
|
+
const previous = latestSegment.get(element);
|
|
2917
|
+
latestSegment.set(element, segment);
|
|
2918
|
+
if (previous && previous.content !== segment.content) {
|
|
2919
|
+
element.querySelector('.aparte-art-card__pane[data-pane="preview"] iframe')?.remove();
|
|
2920
|
+
}
|
|
2921
|
+
const isStreaming = !!segment.isStreaming;
|
|
2922
|
+
const kind = (segment.artifactType || "").toLowerCase();
|
|
2923
|
+
if (BINARY_FILE_KINDS.has(kind)) {
|
|
2924
|
+
updateBinaryFileArtifact(element, segment, isStreaming);
|
|
2925
|
+
return;
|
|
2926
|
+
}
|
|
2927
|
+
const wasStreaming = element.getAttribute("data-streaming") === "true";
|
|
2928
|
+
const cleanContent = stripCodeFences(segment.content || "");
|
|
2929
|
+
const codeEl = element.querySelector(".code-content-wrapper code");
|
|
2930
|
+
if (codeEl) {
|
|
2931
|
+
codeEl.textContent = cleanContent;
|
|
2932
|
+
} else {
|
|
2933
|
+
const wrapper = element.querySelector(".code-content-wrapper");
|
|
2934
|
+
if (wrapper) {
|
|
2935
|
+
const displayLang = languageForKind(kind);
|
|
2936
|
+
wrapper.innerHTML = `<pre><code class="language-${escapeHtml(displayLang)}">${escapeHtml(cleanContent)}</code></pre>`;
|
|
2937
|
+
}
|
|
2938
|
+
}
|
|
2939
|
+
if (isStreaming) {
|
|
2940
|
+
const segId = element.getAttribute("data-segment-id") ?? segment.id;
|
|
2941
|
+
debounceHighlight(element, ".code-content-wrapper", cleanContent, languageForKind(kind), segId);
|
|
2942
|
+
}
|
|
2943
|
+
if (wasStreaming && !isStreaming) {
|
|
2944
|
+
element.setAttribute("data-streaming", "false");
|
|
2945
|
+
const wrapper = element.querySelector(".code-content-wrapper");
|
|
2946
|
+
if (wrapper) {
|
|
2947
|
+
const displayLang = languageForKind(kind);
|
|
2948
|
+
void contextConfig().highlightCode(cleanContent, displayLang).then((html) => {
|
|
2949
|
+
wrapper.innerHTML = html;
|
|
2950
|
+
}).catch(() => {
|
|
2951
|
+
});
|
|
2952
|
+
}
|
|
2953
|
+
element.querySelectorAll("button[disabled]").forEach((b) => {
|
|
2954
|
+
b.disabled = false;
|
|
2955
|
+
});
|
|
2956
|
+
}
|
|
2957
|
+
},
|
|
2958
|
+
getStyles: () => `
|
|
2959
|
+
.segment-artifact-card {
|
|
2960
|
+
display: flex; flex-direction: column;
|
|
2961
|
+
margin: 8px 0;
|
|
2962
|
+
border: 1px solid var(--aparte-border, rgba(0,0,0,0.12));
|
|
2963
|
+
border-radius: 12px;
|
|
2964
|
+
background: var(--aparte-surface, #fff);
|
|
2965
|
+
overflow: hidden;
|
|
2966
|
+
font: inherit;
|
|
2967
|
+
color: var(--aparte-text, inherit);
|
|
2968
|
+
}
|
|
2969
|
+
.aparte-art-card__header {
|
|
2970
|
+
display: flex; align-items: center; justify-content: space-between;
|
|
2971
|
+
padding: 8px 10px;
|
|
2972
|
+
border-bottom: 1px solid var(--aparte-border, rgba(0,0,0,0.08));
|
|
2973
|
+
background: var(--aparte-surface-2, rgba(0,0,0,0.02));
|
|
2974
|
+
min-height: 36px;
|
|
2975
|
+
}
|
|
2976
|
+
.aparte-art-card__title-block { display: flex; align-items: center; gap: 8px; min-width: 0; }
|
|
2977
|
+
.aparte-art-card__kind {
|
|
2978
|
+
font-size: 0.7rem; font-weight: 600;
|
|
2979
|
+
text-transform: uppercase; letter-spacing: 0.04em;
|
|
2980
|
+
padding: 2px 6px; border-radius: 4px;
|
|
2981
|
+
background: var(--aparte-surface, #fff);
|
|
2982
|
+
border: 1px solid var(--aparte-border, rgba(0,0,0,0.1));
|
|
2983
|
+
color: var(--aparte-text-muted, rgba(0,0,0,0.6));
|
|
2984
|
+
}
|
|
2985
|
+
.aparte-art-card__title {
|
|
2986
|
+
font-size: 0.85rem; font-weight: 500;
|
|
2987
|
+
overflow: hidden; text-overflow: ellipsis; white-space: nowrap;
|
|
2988
|
+
}
|
|
2989
|
+
.aparte-art-card__pulse {
|
|
2990
|
+
width: 8px; height: 8px; border-radius: 50%;
|
|
2991
|
+
background: var(--aparte-accent, #3b82f6);
|
|
2992
|
+
animation: aparte-art-card-pulse 1.2s ease-in-out infinite;
|
|
2993
|
+
}
|
|
2994
|
+
@keyframes aparte-art-card-pulse {
|
|
2995
|
+
0%, 100% { opacity: 0.35; transform: scale(0.85); }
|
|
2996
|
+
50% { opacity: 1; transform: scale(1.1); }
|
|
2997
|
+
}
|
|
2998
|
+
.aparte-art-card__actions { display: flex; gap: 4px; }
|
|
2999
|
+
.aparte-art-card__btn {
|
|
3000
|
+
display: inline-flex; align-items: center; justify-content: center;
|
|
3001
|
+
width: 28px; height: 28px;
|
|
3002
|
+
background: transparent;
|
|
3003
|
+
border: 1px solid transparent;
|
|
3004
|
+
border-radius: 6px;
|
|
3005
|
+
color: var(--aparte-text-muted, rgba(0,0,0,0.55));
|
|
3006
|
+
cursor: pointer;
|
|
3007
|
+
transition: background 0.12s, border-color 0.12s, color 0.12s;
|
|
3008
|
+
}
|
|
3009
|
+
.aparte-art-card__btn:hover:not(:disabled) {
|
|
3010
|
+
background: var(--aparte-surface-hover, rgba(0,0,0,0.04));
|
|
3011
|
+
border-color: var(--aparte-border, rgba(0,0,0,0.1));
|
|
3012
|
+
color: var(--aparte-text, inherit);
|
|
3013
|
+
}
|
|
3014
|
+
.aparte-art-card__btn:disabled { opacity: 0.4; cursor: not-allowed; }
|
|
3015
|
+
.aparte-art-card__tabs {
|
|
3016
|
+
display: flex; gap: 2px;
|
|
3017
|
+
padding: 4px 8px 0;
|
|
3018
|
+
border-bottom: 1px solid var(--aparte-border, rgba(0,0,0,0.08));
|
|
3019
|
+
background: var(--aparte-surface-2, rgba(0,0,0,0.02));
|
|
3020
|
+
}
|
|
3021
|
+
.aparte-art-card__tabs button {
|
|
3022
|
+
padding: 6px 12px;
|
|
3023
|
+
font-size: 0.78rem;
|
|
3024
|
+
background: transparent;
|
|
3025
|
+
border: 1px solid transparent;
|
|
3026
|
+
border-bottom: none;
|
|
3027
|
+
border-radius: 6px 6px 0 0;
|
|
3028
|
+
color: var(--aparte-text-muted, rgba(0,0,0,0.6));
|
|
3029
|
+
cursor: pointer;
|
|
3030
|
+
transition: background 0.12s, color 0.12s;
|
|
3031
|
+
}
|
|
3032
|
+
.aparte-art-card__tabs button[aria-selected="true"] {
|
|
3033
|
+
background: var(--aparte-surface, #fff);
|
|
3034
|
+
border-color: var(--aparte-border, rgba(0,0,0,0.08));
|
|
3035
|
+
color: var(--aparte-text, inherit);
|
|
3036
|
+
font-weight: 500;
|
|
3037
|
+
}
|
|
3038
|
+
.aparte-art-card__tabs button:disabled { opacity: 0.4; cursor: not-allowed; }
|
|
3039
|
+
.aparte-art-card__body {
|
|
3040
|
+
position: relative;
|
|
3041
|
+
min-height: 80px;
|
|
3042
|
+
max-height: 600px;
|
|
3043
|
+
overflow: hidden;
|
|
3044
|
+
}
|
|
3045
|
+
.aparte-art-card__pane { display: none; height: 100%; }
|
|
3046
|
+
.segment-artifact-card[data-tab="code"] .aparte-art-card__pane[data-pane="code"] { display: block; }
|
|
3047
|
+
.segment-artifact-card[data-tab="preview"] .aparte-art-card__pane[data-pane="preview"] { display: block; }
|
|
3048
|
+
.aparte-art-card__pane[data-pane="code"] {
|
|
3049
|
+
max-height: 600px; overflow: auto;
|
|
3050
|
+
}
|
|
3051
|
+
.aparte-art-card__pane[data-pane="code"] pre {
|
|
3052
|
+
margin: 0; padding: 12px;
|
|
3053
|
+
font-size: 0.82rem;
|
|
3054
|
+
background: var(--aparte-code-bg, #fafafa);
|
|
3055
|
+
}
|
|
3056
|
+
.aparte-art-card__frame {
|
|
3057
|
+
display: block;
|
|
3058
|
+
width: 100%; height: 480px;
|
|
3059
|
+
border: 0;
|
|
3060
|
+
background: #fff;
|
|
3061
|
+
}
|
|
3062
|
+
.aparte-art-card__pending {
|
|
3063
|
+
display: flex; align-items: center; justify-content: center;
|
|
3064
|
+
height: 120px;
|
|
3065
|
+
color: var(--aparte-text-muted, rgba(0,0,0,0.5));
|
|
3066
|
+
font-size: 0.85rem;
|
|
3067
|
+
font-style: italic;
|
|
3068
|
+
}
|
|
3069
|
+
/* ── Binary file artifact (xlsx/pdf/docx) ──────────────────── */
|
|
3070
|
+
.segment-artifact-file {
|
|
3071
|
+
display: flex; flex-direction: column;
|
|
3072
|
+
margin: 8px 0;
|
|
3073
|
+
border: 1px solid var(--aparte-border, rgba(0,0,0,0.12));
|
|
3074
|
+
border-radius: 12px;
|
|
3075
|
+
background: var(--aparte-surface, #fff);
|
|
3076
|
+
overflow: hidden;
|
|
3077
|
+
color: var(--aparte-text, inherit);
|
|
3078
|
+
font: inherit;
|
|
3079
|
+
}
|
|
3080
|
+
.aparte-art-file__card {
|
|
3081
|
+
display: flex; align-items: center; gap: 12px;
|
|
3082
|
+
padding: 12px 14px;
|
|
3083
|
+
background: var(--aparte-surface-2, rgba(0,0,0,0.02));
|
|
3084
|
+
border-bottom: 1px solid var(--aparte-border, rgba(0,0,0,0.08));
|
|
3085
|
+
}
|
|
3086
|
+
.aparte-art-file__body {
|
|
3087
|
+
position: relative;
|
|
3088
|
+
}
|
|
3089
|
+
.aparte-art-file__code-pane {
|
|
3090
|
+
max-height: 360px; overflow: auto;
|
|
3091
|
+
background: var(--aparte-code-bg, #fafafa);
|
|
3092
|
+
}
|
|
3093
|
+
.aparte-art-file__code-pane pre {
|
|
3094
|
+
margin: 0; padding: 12px;
|
|
3095
|
+
font-size: 0.82rem;
|
|
3096
|
+
font-family: 'JetBrains Mono', 'Fira Code', Consolas, monospace;
|
|
3097
|
+
}
|
|
3098
|
+
.aparte-art-file__preview-pane {
|
|
3099
|
+
max-height: 460px;
|
|
3100
|
+
overflow: auto;
|
|
3101
|
+
background: #fff;
|
|
3102
|
+
/* Preview is a document view — force light scheme regardless of
|
|
3103
|
+
the app theme, with a dark text colour so cells stay readable. */
|
|
3104
|
+
color: #1f2937;
|
|
3105
|
+
}
|
|
3106
|
+
.aparte-art-file__icon {
|
|
3107
|
+
width: 40px; height: 40px;
|
|
3108
|
+
border-radius: 8px;
|
|
3109
|
+
display: flex; align-items: center; justify-content: center;
|
|
3110
|
+
background: linear-gradient(135deg, #1d6f42, #0f5132);
|
|
3111
|
+
color: #fff;
|
|
3112
|
+
font-weight: 700;
|
|
3113
|
+
font-size: 0.78rem;
|
|
3114
|
+
letter-spacing: 0.04em;
|
|
3115
|
+
flex-shrink: 0;
|
|
3116
|
+
}
|
|
3117
|
+
.aparte-art-file__icon[data-kind="pdf"] { background: linear-gradient(135deg, #c0392b, #7d1f17); }
|
|
3118
|
+
.aparte-art-file__icon[data-kind="docx"] { background: linear-gradient(135deg, #1e5288, #0f3060); }
|
|
3119
|
+
.aparte-art-file__meta { flex: 1 1 auto; min-width: 0; }
|
|
3120
|
+
.aparte-art-file__meta-name {
|
|
3121
|
+
font-weight: 600; font-size: 0.92rem;
|
|
3122
|
+
white-space: nowrap; overflow: hidden; text-overflow: ellipsis;
|
|
3123
|
+
}
|
|
3124
|
+
.aparte-art-file__meta-sub {
|
|
3125
|
+
font-size: 0.78rem;
|
|
3126
|
+
color: var(--aparte-text-muted, rgba(0,0,0,0.55));
|
|
3127
|
+
}
|
|
3128
|
+
.aparte-art-file__actions { display: flex; gap: 6px; flex-shrink: 0; }
|
|
3129
|
+
.aparte-art-file__btn {
|
|
3130
|
+
border: 1px solid var(--aparte-border, rgba(0,0,0,0.12));
|
|
3131
|
+
background: var(--aparte-surface, #fff);
|
|
3132
|
+
color: var(--aparte-text, inherit);
|
|
3133
|
+
padding: 6px 10px;
|
|
3134
|
+
border-radius: 6px;
|
|
3135
|
+
font-size: 0.8rem;
|
|
3136
|
+
cursor: pointer;
|
|
3137
|
+
transition: background 0.12s ease;
|
|
3138
|
+
}
|
|
3139
|
+
.aparte-art-file__btn:hover { background: var(--aparte-surface-2, rgba(0,0,0,0.05)); }
|
|
3140
|
+
.aparte-art-file__btn--primary {
|
|
3141
|
+
background: var(--aparte-primary, #6366f1);
|
|
3142
|
+
color: #fff;
|
|
3143
|
+
border-color: var(--aparte-primary, #6366f1);
|
|
3144
|
+
}
|
|
3145
|
+
.aparte-art-file__btn--primary:hover { filter: brightness(1.07); }
|
|
3146
|
+
/* Hide the download button until the sandbox has produced the buffer.
|
|
3147
|
+
Avoids confusing the user with a disabled-but-styled-primary button
|
|
3148
|
+
during streaming / compiling. */
|
|
3149
|
+
.segment-artifact-file:not([data-state="ready"]) .aparte-art-file__btn[data-action="download"] {
|
|
3150
|
+
display: none;
|
|
3151
|
+
}
|
|
3152
|
+
.aparte-art-file__preview-pane table {
|
|
3153
|
+
border-collapse: collapse;
|
|
3154
|
+
width: 100%;
|
|
3155
|
+
font-size: 0.82rem;
|
|
3156
|
+
font-family: inherit;
|
|
3157
|
+
}
|
|
3158
|
+
.aparte-art-file__preview-pane th,
|
|
3159
|
+
.aparte-art-file__preview-pane td {
|
|
3160
|
+
border: 1px solid rgba(0,0,0,0.10);
|
|
3161
|
+
padding: 6px 10px;
|
|
3162
|
+
text-align: left;
|
|
3163
|
+
white-space: nowrap;
|
|
3164
|
+
color: #1f2937;
|
|
3165
|
+
background: #fff;
|
|
3166
|
+
}
|
|
3167
|
+
.aparte-art-file__preview-pane tr:nth-child(odd) td { background: #f9fafb; }
|
|
3168
|
+
.aparte-art-file__preview-pane tr:first-child td {
|
|
3169
|
+
background: #f3f4f6;
|
|
3170
|
+
font-weight: 600;
|
|
3171
|
+
position: sticky; top: 0;
|
|
3172
|
+
color: #111827;
|
|
3173
|
+
}
|
|
3174
|
+
.aparte-art-file__preview-empty {
|
|
3175
|
+
padding: 20px; text-align: center;
|
|
3176
|
+
color: var(--aparte-text-muted, rgba(0,0,0,0.5));
|
|
3177
|
+
font-size: 0.85rem; font-style: italic;
|
|
3178
|
+
}
|
|
3179
|
+
.segment-artifact-file[data-state="error"] .aparte-art-file__icon {
|
|
3180
|
+
background: linear-gradient(135deg, #c0392b, #7d1f17);
|
|
3181
|
+
}
|
|
3182
|
+
.aparte-art-file__error {
|
|
3183
|
+
padding: 16px 18px;
|
|
3184
|
+
background: rgba(239, 68, 68, 0.05);
|
|
3185
|
+
border-top: 1px solid rgba(239, 68, 68, 0.2);
|
|
3186
|
+
color: var(--aparte-text, inherit);
|
|
3187
|
+
}
|
|
3188
|
+
.aparte-art-file__error-title {
|
|
3189
|
+
font-weight: 600;
|
|
3190
|
+
font-size: 0.9rem;
|
|
3191
|
+
margin-bottom: 6px;
|
|
3192
|
+
color: #b91c1c;
|
|
3193
|
+
}
|
|
3194
|
+
.aparte-art-file__error-msg {
|
|
3195
|
+
font-family: 'JetBrains Mono', 'Fira Code', Consolas, monospace;
|
|
3196
|
+
font-size: 0.78rem;
|
|
3197
|
+
padding: 6px 10px;
|
|
3198
|
+
background: rgba(0,0,0,0.04);
|
|
3199
|
+
border-radius: 4px;
|
|
3200
|
+
margin-bottom: 8px;
|
|
3201
|
+
color: #7f1d1d;
|
|
3202
|
+
word-break: break-word;
|
|
3203
|
+
}
|
|
3204
|
+
.aparte-art-file__error-hint {
|
|
3205
|
+
font-size: 0.78rem;
|
|
3206
|
+
color: var(--aparte-text-muted, rgba(0,0,0,0.55));
|
|
3207
|
+
font-style: italic;
|
|
3208
|
+
}
|
|
3209
|
+
`
|
|
3210
|
+
};
|
|
2994
3211
|
function languageForKind(kind) {
|
|
2995
3212
|
if (kind === "react") return "jsx";
|
|
2996
3213
|
if (kind === "markdown") return "md";
|
|
@@ -3030,19 +3247,6 @@ function downloadTextArtifact(segment) {
|
|
|
3030
3247
|
document.body.removeChild(a);
|
|
3031
3248
|
setTimeout(() => URL.revokeObjectURL(url), 1e3);
|
|
3032
3249
|
}
|
|
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
3250
|
function slugifyForFilename(text) {
|
|
3047
3251
|
const lower = text.trim().toLowerCase();
|
|
3048
3252
|
let out = "";
|
|
@@ -3063,63 +3267,86 @@ function slugifyForFilename(text) {
|
|
|
3063
3267
|
if (out.endsWith("-")) out = out.slice(0, -1);
|
|
3064
3268
|
return out;
|
|
3065
3269
|
}
|
|
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
|
-
|
|
3270
|
+
const latestSegment = /* @__PURE__ */ new WeakMap();
|
|
3271
|
+
function mountPreviewFrame(element, fallback) {
|
|
3272
|
+
const pane = element.querySelector('.aparte-art-card__pane[data-pane="preview"]');
|
|
3273
|
+
if (!pane || pane.querySelector("iframe")) return;
|
|
3274
|
+
const segment = latestSegment.get(element) ?? fallback;
|
|
3275
|
+
const kind = segment.artifactType || deriveArtifactKind(segment.mimeType ?? "", "text");
|
|
3276
|
+
if (!PREVIEWABLE_KINDS.has(kind)) return;
|
|
3277
|
+
const title = segment.title?.trim() || labelForKind(kind);
|
|
3278
|
+
const build = contextConfig().getArtifactPreviewBuilder() ?? buildSafePreviewDocument;
|
|
3279
|
+
const srcdoc = build(kind, stripCodeFences(segment.content || ""), title);
|
|
3280
|
+
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>`;
|
|
3281
|
+
}
|
|
3282
|
+
const registries = /* @__PURE__ */ new WeakMap();
|
|
3283
|
+
function registryFor(config) {
|
|
3284
|
+
let reg = registries.get(config);
|
|
3285
|
+
if (!reg) {
|
|
3286
|
+
reg = { renderers: /* @__PURE__ */ new Map(), defaultsInstalled: false, defaultsDeclined: false };
|
|
3287
|
+
registries.set(config, reg);
|
|
3288
|
+
}
|
|
3289
|
+
return reg;
|
|
3290
|
+
}
|
|
3291
|
+
function effectiveRenderers(config) {
|
|
3292
|
+
const own = registryFor(config);
|
|
3293
|
+
if (config === aparteGlobalConfig || own.defaultsDeclined) return own.renderers;
|
|
3294
|
+
const merged = new Map(registryFor(aparteGlobalConfig).renderers);
|
|
3295
|
+
for (const [type, renderer] of own.renderers) merged.set(type, renderer);
|
|
3296
|
+
return merged;
|
|
3297
|
+
}
|
|
3298
|
+
let styleElement = null;
|
|
3299
|
+
function registerSegmentRenderer(renderer, config = contextConfig()) {
|
|
3300
|
+
registryFor(config).renderers.set(renderer.type, renderer);
|
|
3301
|
+
injectRendererStyles(config);
|
|
3302
|
+
}
|
|
3303
|
+
function declineDefaultRenderers(config = contextConfig()) {
|
|
3304
|
+
registryFor(config).defaultsDeclined = true;
|
|
3305
|
+
}
|
|
3306
|
+
function installDefaultRenderersOnce(config = contextConfig()) {
|
|
3307
|
+
const reg = registryFor(config);
|
|
3308
|
+
if (reg.defaultsInstalled || reg.defaultsDeclined) return;
|
|
3309
|
+
reg.defaultsInstalled = true;
|
|
3310
|
+
for (const renderer of DEFAULT_RENDERERS) {
|
|
3311
|
+
if (!reg.renderers.has(renderer.type)) reg.renderers.set(renderer.type, renderer);
|
|
3099
3312
|
}
|
|
3313
|
+
injectRendererStyles(config);
|
|
3314
|
+
installArtifactReadyHook();
|
|
3100
3315
|
}
|
|
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";
|
|
3316
|
+
function unregisterSegmentRenderer(type, config = contextConfig()) {
|
|
3317
|
+
registryFor(config).renderers.delete(type);
|
|
3106
3318
|
}
|
|
3107
|
-
function
|
|
3108
|
-
|
|
3109
|
-
|
|
3110
|
-
|
|
3111
|
-
|
|
3112
|
-
|
|
3113
|
-
|
|
3114
|
-
|
|
3115
|
-
|
|
3116
|
-
|
|
3117
|
-
|
|
3118
|
-
|
|
3119
|
-
|
|
3120
|
-
|
|
3319
|
+
function getSegmentRenderer(type, config = contextConfig()) {
|
|
3320
|
+
return effectiveRenderers(config).get(type);
|
|
3321
|
+
}
|
|
3322
|
+
function getAllRenderers(config = contextConfig()) {
|
|
3323
|
+
return Array.from(effectiveRenderers(config).values());
|
|
3324
|
+
}
|
|
3325
|
+
function collectRendererStyles(config = contextConfig()) {
|
|
3326
|
+
return Array.from(registryFor(config).renderers.values()).map((r) => r.getStyles?.() || "").filter(Boolean).join("\n");
|
|
3327
|
+
}
|
|
3328
|
+
const injectedRendererStyles = /* @__PURE__ */ new Set();
|
|
3329
|
+
function injectRendererStyles(config = contextConfig()) {
|
|
3330
|
+
if (typeof document === "undefined") return;
|
|
3331
|
+
if (!styleElement || !styleElement.isConnected) {
|
|
3332
|
+
styleElement = document.createElement("style");
|
|
3333
|
+
styleElement.id = "aparte-renderer-styles";
|
|
3334
|
+
document.head.appendChild(styleElement);
|
|
3335
|
+
styleElement.textContent = Array.from(injectedRendererStyles).join("\n");
|
|
3121
3336
|
}
|
|
3122
|
-
|
|
3337
|
+
let added = false;
|
|
3338
|
+
for (const renderer of registryFor(config).renderers.values()) {
|
|
3339
|
+
const css = renderer.getStyles?.();
|
|
3340
|
+
if (!css || injectedRendererStyles.has(css)) continue;
|
|
3341
|
+
injectedRendererStyles.add(css);
|
|
3342
|
+
added = true;
|
|
3343
|
+
}
|
|
3344
|
+
if (added) styleElement.textContent = Array.from(injectedRendererStyles).join("\n");
|
|
3345
|
+
}
|
|
3346
|
+
function registerDefaultRenderers(config = contextConfig()) {
|
|
3347
|
+
registryFor(config).defaultsInstalled = true;
|
|
3348
|
+
for (const renderer of DEFAULT_RENDERERS) registerSegmentRenderer(renderer, config);
|
|
3349
|
+
installArtifactReadyHook();
|
|
3123
3350
|
}
|
|
3124
3351
|
const DEFAULT_RENDERERS = [
|
|
3125
3352
|
textRenderer,
|
|
@@ -3191,7 +3418,7 @@ function applyRetention(messages, tree, maxMessages) {
|
|
|
3191
3418
|
};
|
|
3192
3419
|
return { messages: trimmedFlat, tree: trimmedTree };
|
|
3193
3420
|
}
|
|
3194
|
-
class
|
|
3421
|
+
class AparteConversationManager {
|
|
3195
3422
|
_adapter;
|
|
3196
3423
|
_conversations = [];
|
|
3197
3424
|
_activeId = null;
|
|
@@ -3243,7 +3470,7 @@ class ConversationManager {
|
|
|
3243
3470
|
async createNew(title = "New Chat") {
|
|
3244
3471
|
const now = Date.now();
|
|
3245
3472
|
const conv = {
|
|
3246
|
-
id:
|
|
3473
|
+
id: uuid(),
|
|
3247
3474
|
title,
|
|
3248
3475
|
createdAt: now,
|
|
3249
3476
|
updatedAt: now,
|
|
@@ -3374,7 +3601,7 @@ class ConversationManager {
|
|
|
3374
3601
|
try {
|
|
3375
3602
|
l(snapshot);
|
|
3376
3603
|
} catch (err) {
|
|
3377
|
-
console.error("[
|
|
3604
|
+
console.error("[AparteConversationManager] listener threw:", err);
|
|
3378
3605
|
}
|
|
3379
3606
|
});
|
|
3380
3607
|
}
|
|
@@ -3384,12 +3611,9 @@ class ConversationManager {
|
|
|
3384
3611
|
return text.trim() || "New Chat";
|
|
3385
3612
|
}
|
|
3386
3613
|
}
|
|
3387
|
-
function attachmentId() {
|
|
3388
|
-
return typeof crypto !== "undefined" && crypto.randomUUID ? crypto.randomUUID() : `${Date.now()}-${Math.random().toString(36).slice(2)}`;
|
|
3389
|
-
}
|
|
3390
3614
|
function filesToAttachments(files) {
|
|
3391
3615
|
return files.map((file) => ({
|
|
3392
|
-
id:
|
|
3616
|
+
id: uuid(),
|
|
3393
3617
|
name: file.name,
|
|
3394
3618
|
type: file.type || "application/octet-stream",
|
|
3395
3619
|
url: URL.createObjectURL(file),
|
|
@@ -3397,6 +3621,13 @@ function filesToAttachments(files) {
|
|
|
3397
3621
|
blob: file
|
|
3398
3622
|
}));
|
|
3399
3623
|
}
|
|
3624
|
+
function revokeAttachmentUrls(attachments) {
|
|
3625
|
+
if (!attachments?.length) return;
|
|
3626
|
+
if (typeof URL === "undefined" || typeof URL.revokeObjectURL !== "function") return;
|
|
3627
|
+
for (const attachment of attachments) {
|
|
3628
|
+
if (attachment.url?.startsWith("blob:")) URL.revokeObjectURL(attachment.url);
|
|
3629
|
+
}
|
|
3630
|
+
}
|
|
3400
3631
|
class AparteConversationController {
|
|
3401
3632
|
_binding;
|
|
3402
3633
|
_options;
|
|
@@ -3428,7 +3659,30 @@ class AparteConversationController {
|
|
|
3428
3659
|
* conversation lifecycle yet.
|
|
3429
3660
|
*/
|
|
3430
3661
|
_manager() {
|
|
3431
|
-
|
|
3662
|
+
if (this._options.manager) return this._options.manager;
|
|
3663
|
+
const scoped = resolveConfig(this._binding.host);
|
|
3664
|
+
const manager = scoped.getConversationManager();
|
|
3665
|
+
if (!manager) this._warnManagerOnTheWrongConfig(scoped);
|
|
3666
|
+
return manager;
|
|
3667
|
+
}
|
|
3668
|
+
/**
|
|
3669
|
+
* Degraded mode is legitimate — a host that never opted into the conversation
|
|
3670
|
+
* lifecycle has no manager and should not be nagged. Registering one on the
|
|
3671
|
+
* WRONG config is not: the four wrapper hooks used to write it to the global
|
|
3672
|
+
* singleton unconditionally, so `config` + persistence looked like it worked
|
|
3673
|
+
* (the optimistic UI still appends) and saved nothing at all.
|
|
3674
|
+
*
|
|
3675
|
+
* Warned once per controller, and only in the case that is unambiguously a
|
|
3676
|
+
* mistake: no manager here, but one on the global.
|
|
3677
|
+
*/
|
|
3678
|
+
_warnedNoManager = false;
|
|
3679
|
+
_warnManagerOnTheWrongConfig(scoped) {
|
|
3680
|
+
if (this._warnedNoManager) return;
|
|
3681
|
+
if (scoped === aparteGlobalConfig || !aparteGlobalConfig.getConversationManager()) return;
|
|
3682
|
+
this._warnedNoManager = true;
|
|
3683
|
+
console.warn(
|
|
3684
|
+
"[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."
|
|
3685
|
+
);
|
|
3432
3686
|
}
|
|
3433
3687
|
/**
|
|
3434
3688
|
* Attach event listeners to the binding host. Returns an unbind function.
|
|
@@ -3443,7 +3697,7 @@ class AparteConversationController {
|
|
|
3443
3697
|
const files = evt.detail?.files;
|
|
3444
3698
|
const attachments = files?.length ? filesToAttachments(files) : void 0;
|
|
3445
3699
|
const userMsg = {
|
|
3446
|
-
id:
|
|
3700
|
+
id: uuid(),
|
|
3447
3701
|
role: "user",
|
|
3448
3702
|
content,
|
|
3449
3703
|
timestamp: evt.detail?.timestamp ?? Date.now(),
|
|
@@ -3510,8 +3764,12 @@ class AparteConversationController {
|
|
|
3510
3764
|
return () => this.unbind();
|
|
3511
3765
|
}
|
|
3512
3766
|
/** Detach event listeners. */
|
|
3767
|
+
/** Release handle for the hydration-retry subscription (see setConversationId). */
|
|
3768
|
+
_stopHydrationRetry = null;
|
|
3513
3769
|
unbind() {
|
|
3514
3770
|
this._persistActive();
|
|
3771
|
+
this._stopHydrationRetry?.();
|
|
3772
|
+
this._stopHydrationRetry = null;
|
|
3515
3773
|
const host = this._binding.host;
|
|
3516
3774
|
if (this._onSendCapture) {
|
|
3517
3775
|
host.removeEventListener("aparte-send", this._onSendCapture, { capture: true });
|
|
@@ -3589,8 +3847,10 @@ class AparteConversationController {
|
|
|
3589
3847
|
const conv = manager?.conversations.find((c) => c.id === id);
|
|
3590
3848
|
if (!manager || !conv) {
|
|
3591
3849
|
if (manager && !manager.initialized) {
|
|
3850
|
+
this._stopHydrationRetry?.();
|
|
3592
3851
|
const stop = manager.subscribe(() => {
|
|
3593
3852
|
stop();
|
|
3853
|
+
this._stopHydrationRetry = null;
|
|
3594
3854
|
if (manager.conversations.some((c) => c.id === id)) {
|
|
3595
3855
|
void this.setConversationId(id);
|
|
3596
3856
|
} else if (this._activeId === null) {
|
|
@@ -3599,6 +3859,7 @@ class AparteConversationController {
|
|
|
3599
3859
|
this._binding.clearMessages();
|
|
3600
3860
|
}
|
|
3601
3861
|
});
|
|
3862
|
+
this._stopHydrationRetry = stop;
|
|
3602
3863
|
return;
|
|
3603
3864
|
}
|
|
3604
3865
|
console.warn("[ConversationController] unknown id or no manager — clearing. manager:", !!manager, "conv:", !!conv);
|
|
@@ -3680,6 +3941,9 @@ class AparteConversationController {
|
|
|
3680
3941
|
await manager.addMessage(convId, userMsg);
|
|
3681
3942
|
}
|
|
3682
3943
|
}
|
|
3944
|
+
function cssEscape(value) {
|
|
3945
|
+
return value.replace(/["\\]/g, "\\$&");
|
|
3946
|
+
}
|
|
3683
3947
|
class AparteChatHost {
|
|
3684
3948
|
binding;
|
|
3685
3949
|
options;
|
|
@@ -3786,7 +4050,7 @@ class AparteChatHost {
|
|
|
3786
4050
|
const convBinding = {
|
|
3787
4051
|
hostId: this.binding.hostId,
|
|
3788
4052
|
host,
|
|
3789
|
-
// Controller-driven flushes mirror
|
|
4053
|
+
// Controller-driven flushes mirror AparteConversationManager state, not
|
|
3790
4054
|
// user intent — deliberately NOT emitted as `messagesChange` to
|
|
3791
4055
|
// avoid a feedback loop with hosts using a two-way `messages` input.
|
|
3792
4056
|
setMessages: (msgs) => {
|
|
@@ -4173,7 +4437,7 @@ class AparteChatHost {
|
|
|
4173
4437
|
const vp = this.binding.viewport;
|
|
4174
4438
|
if (!vp) return null;
|
|
4175
4439
|
return vp.querySelector(
|
|
4176
|
-
`aparte-chat-bubble[message-id="${id}"]`
|
|
4440
|
+
`aparte-chat-bubble[message-id="${cssEscape(id)}"]`
|
|
4177
4441
|
);
|
|
4178
4442
|
}
|
|
4179
4443
|
_lastBubble() {
|
|
@@ -4257,31 +4521,34 @@ class AparteChatHost {
|
|
|
4257
4521
|
});
|
|
4258
4522
|
}
|
|
4259
4523
|
}
|
|
4260
|
-
function assertNever(value) {
|
|
4261
|
-
throw new Error(`Unhandled union member: ${JSON.stringify(value)}`);
|
|
4262
|
-
}
|
|
4263
4524
|
function dispatchLifecycleEvent(target, name, detail) {
|
|
4264
|
-
|
|
4525
|
+
const id = target.id || void 0;
|
|
4526
|
+
const stamped = detail && typeof detail === "object" ? { targetId: id, ...detail } : detail;
|
|
4527
|
+
target.dispatchEvent(new CustomEvent(name, { bubbles: true, composed: true, detail: stamped }));
|
|
4265
4528
|
}
|
|
4266
4529
|
function dispatchArtifactLifecycle(target, messageId, segment, progress, isFinal) {
|
|
4267
4530
|
const id = segment.id;
|
|
4268
4531
|
const content = segment.content ?? "";
|
|
4269
|
-
|
|
4270
|
-
if (seen === void 0) {
|
|
4532
|
+
if (progress.get(id) === void 0) {
|
|
4271
4533
|
target.dispatchEvent(new CustomEvent("aparte-artifact-start", {
|
|
4272
4534
|
bubbles: true,
|
|
4273
4535
|
composed: true,
|
|
4274
|
-
detail: {
|
|
4536
|
+
detail: {
|
|
4537
|
+
messageId,
|
|
4538
|
+
segmentId: id,
|
|
4539
|
+
mimeType: segment.mimeType,
|
|
4540
|
+
artifactType: segment.artifactType,
|
|
4541
|
+
title: segment.title
|
|
4542
|
+
}
|
|
4275
4543
|
}));
|
|
4276
4544
|
progress.set(id, 0);
|
|
4277
4545
|
}
|
|
4278
4546
|
const lastLen = progress.get(id) ?? 0;
|
|
4279
4547
|
if (content.length > lastLen) {
|
|
4280
|
-
const chunk = content.slice(lastLen);
|
|
4281
4548
|
target.dispatchEvent(new CustomEvent("aparte-artifact-delta", {
|
|
4282
4549
|
bubbles: true,
|
|
4283
4550
|
composed: true,
|
|
4284
|
-
detail: { segmentId: id, chunk }
|
|
4551
|
+
detail: { segmentId: id, chunk: content.slice(lastLen) }
|
|
4285
4552
|
}));
|
|
4286
4553
|
progress.set(id, content.length);
|
|
4287
4554
|
}
|
|
@@ -4289,10 +4556,187 @@ function dispatchArtifactLifecycle(target, messageId, segment, progress, isFinal
|
|
|
4289
4556
|
target.dispatchEvent(new CustomEvent("aparte-artifact-ready", {
|
|
4290
4557
|
bubbles: true,
|
|
4291
4558
|
composed: true,
|
|
4292
|
-
detail: {
|
|
4559
|
+
detail: {
|
|
4560
|
+
messageId,
|
|
4561
|
+
segmentId: id,
|
|
4562
|
+
mimeType: segment.mimeType,
|
|
4563
|
+
artifactType: segment.artifactType,
|
|
4564
|
+
title: segment.title,
|
|
4565
|
+
content
|
|
4566
|
+
}
|
|
4293
4567
|
}));
|
|
4294
4568
|
}
|
|
4295
4569
|
}
|
|
4570
|
+
const INLINE_MAX_LINES = 15;
|
|
4571
|
+
const XML_OPEN_TAG = "<artifact";
|
|
4572
|
+
function partialXmlOpenTagLength(text) {
|
|
4573
|
+
const max = Math.min(text.length, XML_OPEN_TAG.length - 1);
|
|
4574
|
+
for (let k = max; k > 0; k--) {
|
|
4575
|
+
if (text.endsWith(XML_OPEN_TAG.slice(0, k))) return k;
|
|
4576
|
+
}
|
|
4577
|
+
return 0;
|
|
4578
|
+
}
|
|
4579
|
+
function feedXmlArtifactDelta(delta, xml, ctx) {
|
|
4580
|
+
const { targetElement, messageId, textParser, streamingSegmentIds, artifactProgress, artifactXmlHint } = ctx;
|
|
4581
|
+
let remaining = delta;
|
|
4582
|
+
const emitChatText = (text, syncActive = true) => {
|
|
4583
|
+
if (!text) return;
|
|
4584
|
+
const r = textParser.parse(text);
|
|
4585
|
+
for (const seg of r.segments) {
|
|
4586
|
+
if (!streamingSegmentIds.has(seg.id)) {
|
|
4587
|
+
targetElement.addSegment?.(seg);
|
|
4588
|
+
streamingSegmentIds.add(seg.id);
|
|
4589
|
+
} else if ("content" in seg) {
|
|
4590
|
+
targetElement.updateSegment?.(seg.id, { content: seg.content });
|
|
4591
|
+
}
|
|
4592
|
+
}
|
|
4593
|
+
const active = textParser.getState().activeSegment;
|
|
4594
|
+
if (syncActive && active) {
|
|
4595
|
+
if (!streamingSegmentIds.has(active.id)) {
|
|
4596
|
+
targetElement.addSegment?.(active);
|
|
4597
|
+
streamingSegmentIds.add(active.id);
|
|
4598
|
+
} else {
|
|
4599
|
+
targetElement.updateSegment?.(active.id, { content: active.content });
|
|
4600
|
+
}
|
|
4601
|
+
} else if (!r.segments.length && !active) ;
|
|
4602
|
+
};
|
|
4603
|
+
while (remaining.length > 0) {
|
|
4604
|
+
if (xml.state === "normal") {
|
|
4605
|
+
const tagStart = remaining.indexOf(XML_OPEN_TAG);
|
|
4606
|
+
if (tagStart === -1) {
|
|
4607
|
+
const held = partialXmlOpenTagLength(remaining);
|
|
4608
|
+
if (held > 0) {
|
|
4609
|
+
emitChatText(remaining.slice(0, remaining.length - held), false);
|
|
4610
|
+
xml.scanBuf = remaining.slice(remaining.length - held);
|
|
4611
|
+
xml.state = "scanning";
|
|
4612
|
+
} else {
|
|
4613
|
+
emitChatText(remaining);
|
|
4614
|
+
}
|
|
4615
|
+
remaining = "";
|
|
4616
|
+
} else {
|
|
4617
|
+
emitChatText(remaining.slice(0, tagStart), false);
|
|
4618
|
+
xml.scanBuf = "";
|
|
4619
|
+
remaining = remaining.slice(tagStart);
|
|
4620
|
+
xml.state = "scanning";
|
|
4621
|
+
}
|
|
4622
|
+
} else if (xml.state === "scanning") {
|
|
4623
|
+
xml.scanBuf += remaining;
|
|
4624
|
+
remaining = "";
|
|
4625
|
+
const cmp = Math.min(xml.scanBuf.length, XML_OPEN_TAG.length);
|
|
4626
|
+
if (xml.scanBuf.slice(0, cmp) !== XML_OPEN_TAG.slice(0, cmp)) {
|
|
4627
|
+
emitChatText(xml.scanBuf);
|
|
4628
|
+
xml.scanBuf = "";
|
|
4629
|
+
xml.state = "normal";
|
|
4630
|
+
continue;
|
|
4631
|
+
}
|
|
4632
|
+
const gtIdx = xml.scanBuf.indexOf(">");
|
|
4633
|
+
if (gtIdx !== -1) {
|
|
4634
|
+
const tag = xml.scanBuf.slice(0, gtIdx + 1);
|
|
4635
|
+
const mimeMatch = /mimeType=['"]([^'"]+)['"]/.exec(tag);
|
|
4636
|
+
const titleMatch = /title=['"]([^'"]+)['"]/.exec(tag);
|
|
4637
|
+
xml.mime = mimeMatch?.[1] ?? artifactXmlHint.mimeType;
|
|
4638
|
+
xml.title = titleMatch?.[1] ?? artifactXmlHint.kind;
|
|
4639
|
+
xml.kind = deriveArtifactKind(xml.mime, artifactXmlHint.kind);
|
|
4640
|
+
xml.segId = `artifact-xml-${uuid()}`;
|
|
4641
|
+
xml.content = "";
|
|
4642
|
+
const openSeg = {
|
|
4643
|
+
id: xml.segId,
|
|
4644
|
+
type: "artifact",
|
|
4645
|
+
mimeType: xml.mime,
|
|
4646
|
+
artifactType: xml.kind,
|
|
4647
|
+
title: xml.title,
|
|
4648
|
+
content: ""
|
|
4649
|
+
};
|
|
4650
|
+
targetElement.addSegment?.(openSeg);
|
|
4651
|
+
streamingSegmentIds.add(xml.segId);
|
|
4652
|
+
dispatchArtifactLifecycle(targetElement, messageId, openSeg, artifactProgress, false);
|
|
4653
|
+
xml.state = "in-artifact";
|
|
4654
|
+
remaining = xml.scanBuf.slice(gtIdx + 1);
|
|
4655
|
+
xml.scanBuf = "";
|
|
4656
|
+
}
|
|
4657
|
+
} else {
|
|
4658
|
+
const CLOSE = "</artifact>";
|
|
4659
|
+
const combined = xml.closeBuf + remaining;
|
|
4660
|
+
const closeIdx = combined.indexOf(CLOSE);
|
|
4661
|
+
if (closeIdx !== -1) {
|
|
4662
|
+
xml.content += combined.slice(0, closeIdx);
|
|
4663
|
+
const lineCount = xml.content.split("\n").length;
|
|
4664
|
+
const isInline = lineCount < INLINE_MAX_LINES;
|
|
4665
|
+
const finalSeg = {
|
|
4666
|
+
id: xml.segId,
|
|
4667
|
+
mimeType: xml.mime,
|
|
4668
|
+
artifactType: xml.kind,
|
|
4669
|
+
title: xml.title,
|
|
4670
|
+
content: xml.content
|
|
4671
|
+
};
|
|
4672
|
+
targetElement.updateSegment?.(xml.segId, { content: xml.content, inline: isInline });
|
|
4673
|
+
dispatchArtifactLifecycle(targetElement, messageId, finalSeg, artifactProgress, true);
|
|
4674
|
+
xml.state = "normal";
|
|
4675
|
+
xml.closeBuf = "";
|
|
4676
|
+
remaining = combined.slice(closeIdx + CLOSE.length);
|
|
4677
|
+
} else {
|
|
4678
|
+
const safeLen = Math.max(0, combined.length - CLOSE.length + 1);
|
|
4679
|
+
const safe = combined.slice(0, safeLen);
|
|
4680
|
+
xml.content += safe;
|
|
4681
|
+
xml.closeBuf = combined.slice(safeLen);
|
|
4682
|
+
remaining = "";
|
|
4683
|
+
if (xml.segId) {
|
|
4684
|
+
targetElement.updateSegment?.(xml.segId, { content: xml.content });
|
|
4685
|
+
dispatchArtifactLifecycle(targetElement, messageId, {
|
|
4686
|
+
id: xml.segId,
|
|
4687
|
+
mimeType: xml.mime,
|
|
4688
|
+
artifactType: xml.kind,
|
|
4689
|
+
title: xml.title,
|
|
4690
|
+
content: xml.content
|
|
4691
|
+
}, artifactProgress, false);
|
|
4692
|
+
}
|
|
4693
|
+
}
|
|
4694
|
+
}
|
|
4695
|
+
}
|
|
4696
|
+
}
|
|
4697
|
+
function finalizeXmlArtifact(xml, ctx) {
|
|
4698
|
+
const { targetElement, messageId, artifactProgress, artifactXmlHint } = ctx;
|
|
4699
|
+
if (xml.state === "scanning") {
|
|
4700
|
+
const held = xml.scanBuf;
|
|
4701
|
+
xml.scanBuf = "";
|
|
4702
|
+
xml.state = "normal";
|
|
4703
|
+
if (held) {
|
|
4704
|
+
const r = ctx.textParser.parse(held);
|
|
4705
|
+
for (const seg of r.segments) {
|
|
4706
|
+
if (!ctx.streamingSegmentIds.has(seg.id)) {
|
|
4707
|
+
targetElement.addSegment?.(seg);
|
|
4708
|
+
ctx.streamingSegmentIds.add(seg.id);
|
|
4709
|
+
} else if ("content" in seg) {
|
|
4710
|
+
targetElement.updateSegment?.(seg.id, { content: seg.content });
|
|
4711
|
+
}
|
|
4712
|
+
}
|
|
4713
|
+
const active = ctx.textParser.getState().activeSegment;
|
|
4714
|
+
if (active) {
|
|
4715
|
+
if (!ctx.streamingSegmentIds.has(active.id)) {
|
|
4716
|
+
targetElement.addSegment?.(active);
|
|
4717
|
+
ctx.streamingSegmentIds.add(active.id);
|
|
4718
|
+
} else {
|
|
4719
|
+
targetElement.updateSegment?.(active.id, { content: active.content });
|
|
4720
|
+
}
|
|
4721
|
+
}
|
|
4722
|
+
}
|
|
4723
|
+
return;
|
|
4724
|
+
}
|
|
4725
|
+
if (artifactXmlHint && xml.state === "in-artifact" && xml.segId) {
|
|
4726
|
+
xml.content += xml.closeBuf;
|
|
4727
|
+
const lineCount = xml.content.split("\n").length;
|
|
4728
|
+
const isInline = lineCount < INLINE_MAX_LINES;
|
|
4729
|
+
targetElement.updateSegment?.(xml.segId, { content: xml.content, inline: isInline });
|
|
4730
|
+
dispatchArtifactLifecycle(targetElement, messageId, {
|
|
4731
|
+
id: xml.segId,
|
|
4732
|
+
mimeType: xml.mime,
|
|
4733
|
+
artifactType: xml.kind,
|
|
4734
|
+
title: xml.title,
|
|
4735
|
+
content: xml.content
|
|
4736
|
+
}, artifactProgress, true);
|
|
4737
|
+
console.warn("[AparteClient] XML artifact finalized without closing tag — content may be partial.");
|
|
4738
|
+
}
|
|
4739
|
+
}
|
|
4296
4740
|
function createStreamAdapter(opts) {
|
|
4297
4741
|
const { target, config, messageId, artifactHint } = opts;
|
|
4298
4742
|
let parser = new AparteStreamParser();
|
|
@@ -4322,7 +4766,7 @@ function createStreamAdapter(opts) {
|
|
|
4322
4766
|
thinkingContent += e.delta;
|
|
4323
4767
|
if (!thinkingId) {
|
|
4324
4768
|
const seg = {
|
|
4325
|
-
id: `think-${
|
|
4769
|
+
id: `think-${uuid()}`,
|
|
4326
4770
|
type: "thinking",
|
|
4327
4771
|
content: thinkingContent,
|
|
4328
4772
|
collapsed: true,
|
|
@@ -4347,12 +4791,10 @@ function createStreamAdapter(opts) {
|
|
|
4347
4791
|
if (!streaming.has(segment.id)) {
|
|
4348
4792
|
target.addSegment?.(segment);
|
|
4349
4793
|
streaming.add(segment.id);
|
|
4794
|
+
} else if ("content" in segment) {
|
|
4795
|
+
target.updateSegment?.(segment.id, { content: segment.content });
|
|
4350
4796
|
}
|
|
4351
4797
|
}
|
|
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
4798
|
break;
|
|
4357
4799
|
}
|
|
4358
4800
|
const result = parser.parse(e.delta);
|
|
@@ -4394,10 +4836,7 @@ function createStreamAdapter(opts) {
|
|
|
4394
4836
|
dispatchArtifactLifecycle(target, messageId, active, artifactProgress, false);
|
|
4395
4837
|
}
|
|
4396
4838
|
}
|
|
4397
|
-
} else if (result.segments.length === 0)
|
|
4398
|
-
if (target.typeName) target.typeName(e.delta);
|
|
4399
|
-
else target.updateLastMessage?.(e.delta, { append: true });
|
|
4400
|
-
}
|
|
4839
|
+
} else if (result.segments.length === 0) ;
|
|
4401
4840
|
break;
|
|
4402
4841
|
}
|
|
4403
4842
|
case "text-flush": {
|
|
@@ -4517,7 +4956,7 @@ function createStreamAdapter(opts) {
|
|
|
4517
4956
|
case "turn-limit-exceeded":
|
|
4518
4957
|
if (e.scope === "global") {
|
|
4519
4958
|
target.addSegment?.({
|
|
4520
|
-
id: `max-turns-${
|
|
4959
|
+
id: `max-turns-${uuid()}`,
|
|
4521
4960
|
type: "error",
|
|
4522
4961
|
content: `Stopped after ${e.limit} tool calls to prevent an infinite loop.`,
|
|
4523
4962
|
details: "MAX_TURNS_EXCEEDED"
|
|
@@ -4527,7 +4966,7 @@ function createStreamAdapter(opts) {
|
|
|
4527
4966
|
}
|
|
4528
4967
|
break;
|
|
4529
4968
|
case "phase-advance":
|
|
4530
|
-
target.addSegment?.({ id: `pw-${
|
|
4969
|
+
target.addSegment?.({ id: `pw-${uuid()}`, type: "pipeline-waiting" });
|
|
4531
4970
|
break;
|
|
4532
4971
|
case "run-aborted":
|
|
4533
4972
|
dispatchLifecycleEvent(target, "aparte-message-aborted", { messageId });
|
|
@@ -4564,6 +5003,10 @@ function readableToAsyncIterable(stream, signal) {
|
|
|
4564
5003
|
},
|
|
4565
5004
|
async return() {
|
|
4566
5005
|
signal.removeEventListener("abort", onAbort);
|
|
5006
|
+
try {
|
|
5007
|
+
await reader.cancel();
|
|
5008
|
+
} catch {
|
|
5009
|
+
}
|
|
4567
5010
|
try {
|
|
4568
5011
|
reader.releaseLock();
|
|
4569
5012
|
} catch {
|
|
@@ -4574,7 +5017,20 @@ function readableToAsyncIterable(stream, signal) {
|
|
|
4574
5017
|
}
|
|
4575
5018
|
};
|
|
4576
5019
|
}
|
|
4577
|
-
const
|
|
5020
|
+
const DEFAULT_TOOL_HANDLER_TIMEOUT_MS = 5 * 60 * 1e3;
|
|
5021
|
+
function withToolTimeout(work, controller, timeoutMs) {
|
|
5022
|
+
let timer;
|
|
5023
|
+
const timedOut = new Promise((_resolve, reject) => {
|
|
5024
|
+
timer = setTimeout(() => {
|
|
5025
|
+
controller.abort();
|
|
5026
|
+
reject(Object.assign(new Error(`tool handler exceeded ${timeoutMs}ms`), { name: "AbortError" }));
|
|
5027
|
+
}, timeoutMs);
|
|
5028
|
+
});
|
|
5029
|
+
return {
|
|
5030
|
+
raced: Promise.race([work(controller.signal), timedOut]),
|
|
5031
|
+
done: () => clearTimeout(timer)
|
|
5032
|
+
};
|
|
5033
|
+
}
|
|
4578
5034
|
class AparteClient {
|
|
4579
5035
|
_boundHandler = null;
|
|
4580
5036
|
_boundAbortHandler = null;
|
|
@@ -4583,6 +5039,30 @@ class AparteClient {
|
|
|
4583
5039
|
_boundEditHandler = null;
|
|
4584
5040
|
_activeToolControllers = /* @__PURE__ */ new Set();
|
|
4585
5041
|
_isAborted = false;
|
|
5042
|
+
/**
|
|
5043
|
+
* Does this window event belong to this client?
|
|
5044
|
+
*
|
|
5045
|
+
* One rule for all five handlers, because it used to be four near-copies and
|
|
5046
|
+
* one omission: `aparte-compact` had no guard at all, so in the two-client
|
|
5047
|
+
* layout the JSDoc documents, a single compact event made BOTH clients run —
|
|
5048
|
+
* two paid summarisation calls against whichever chat the DOM scan found
|
|
5049
|
+
* first, and a global reset that wiped the other conversation.
|
|
5050
|
+
*
|
|
5051
|
+
* A scoped client also answers only events ADDRESSED to it. The old guard let
|
|
5052
|
+
* an untargeted event through to every scoped client, which turned one
|
|
5053
|
+
* broadcast into an action on every chat on the page.
|
|
5054
|
+
*/
|
|
5055
|
+
_isForThisInstance(e) {
|
|
5056
|
+
if (!e) return true;
|
|
5057
|
+
const detail = e.detail;
|
|
5058
|
+
const scope = this.options.scopeToTargetId;
|
|
5059
|
+
if (scope) return detail?.targetId === scope;
|
|
5060
|
+
if (this._config === aparteGlobalConfig) return true;
|
|
5061
|
+
const target = this._resolveTarget(detail?.targetId);
|
|
5062
|
+
if (!target) return true;
|
|
5063
|
+
const owner = resolveConfig(target);
|
|
5064
|
+
return owner === this._config || owner === aparteGlobalConfig;
|
|
5065
|
+
}
|
|
4586
5066
|
/** Aborts the in-flight vendor/transport fetch when the user stops a stream. */
|
|
4587
5067
|
_streamController = null;
|
|
4588
5068
|
options;
|
|
@@ -4593,11 +5073,11 @@ class AparteClient {
|
|
|
4593
5073
|
autoRegister: true,
|
|
4594
5074
|
...options
|
|
4595
5075
|
};
|
|
4596
|
-
this._config = options.config ??
|
|
5076
|
+
this._config = options.config ?? aparteGlobalConfig;
|
|
4597
5077
|
if (this.options.autoRegister) {
|
|
4598
|
-
registerDefaultRenderers();
|
|
5078
|
+
registerDefaultRenderers(this._config);
|
|
4599
5079
|
} else {
|
|
4600
|
-
declineDefaultRenderers();
|
|
5080
|
+
declineDefaultRenderers(this._config);
|
|
4601
5081
|
}
|
|
4602
5082
|
this._setupListeners();
|
|
4603
5083
|
}
|
|
@@ -4610,10 +5090,7 @@ class AparteClient {
|
|
|
4610
5090
|
this._boundHandler = (e) => {
|
|
4611
5091
|
const event = e;
|
|
4612
5092
|
if (event.type !== "aparte-send") return;
|
|
4613
|
-
if (this.
|
|
4614
|
-
const evtTargetId = event.detail?.targetId;
|
|
4615
|
-
if (evtTargetId && evtTargetId !== this.options.scopeToTargetId) return;
|
|
4616
|
-
}
|
|
5093
|
+
if (!this._isForThisInstance(event)) return;
|
|
4617
5094
|
this._isAborted = false;
|
|
4618
5095
|
for (const controller of this._activeToolControllers) {
|
|
4619
5096
|
controller.abort();
|
|
@@ -4634,27 +5111,23 @@ class AparteClient {
|
|
|
4634
5111
|
}
|
|
4635
5112
|
if (!this._boundAbortHandler) {
|
|
4636
5113
|
this._boundAbortHandler = (e) => {
|
|
4637
|
-
if (this.
|
|
4638
|
-
const evtTargetId = e?.detail?.targetId;
|
|
4639
|
-
if (evtTargetId && evtTargetId !== this.options.scopeToTargetId) return;
|
|
4640
|
-
}
|
|
5114
|
+
if (!this._isForThisInstance(e)) return;
|
|
4641
5115
|
this.abort();
|
|
4642
5116
|
};
|
|
4643
5117
|
}
|
|
4644
5118
|
window.addEventListener("aparte-abort", this._boundAbortHandler);
|
|
4645
5119
|
if (!this._boundCompactHandler) {
|
|
4646
|
-
this._boundCompactHandler = () => {
|
|
4647
|
-
|
|
5120
|
+
this._boundCompactHandler = (e) => {
|
|
5121
|
+
if (!this._isForThisInstance(e)) return;
|
|
5122
|
+
const detail = e.detail;
|
|
5123
|
+
void this.compact(detail?.targetId);
|
|
4648
5124
|
};
|
|
4649
5125
|
}
|
|
4650
5126
|
window.addEventListener("aparte-compact", this._boundCompactHandler);
|
|
4651
5127
|
if (!this._boundRetryHandler) {
|
|
4652
5128
|
this._boundRetryHandler = (e) => {
|
|
4653
5129
|
const evt = e;
|
|
4654
|
-
if (this.
|
|
4655
|
-
const evtTargetId = evt.detail?.targetId;
|
|
4656
|
-
if (evtTargetId && evtTargetId !== this.options.scopeToTargetId) return;
|
|
4657
|
-
}
|
|
5130
|
+
if (!this._isForThisInstance(evt)) return;
|
|
4658
5131
|
void this._handleRetry(evt);
|
|
4659
5132
|
};
|
|
4660
5133
|
}
|
|
@@ -4662,10 +5135,7 @@ class AparteClient {
|
|
|
4662
5135
|
if (!this._boundEditHandler) {
|
|
4663
5136
|
this._boundEditHandler = (e) => {
|
|
4664
5137
|
const evt = e;
|
|
4665
|
-
if (this.
|
|
4666
|
-
const evtTargetId = evt.detail?.targetId;
|
|
4667
|
-
if (evtTargetId && evtTargetId !== this.options.scopeToTargetId) return;
|
|
4668
|
-
}
|
|
5138
|
+
if (!this._isForThisInstance(evt)) return;
|
|
4669
5139
|
void this._handleEdit(evt);
|
|
4670
5140
|
};
|
|
4671
5141
|
}
|
|
@@ -4675,6 +5145,7 @@ class AparteClient {
|
|
|
4675
5145
|
* Stop listening.
|
|
4676
5146
|
*/
|
|
4677
5147
|
stop() {
|
|
5148
|
+
this.abort();
|
|
4678
5149
|
if (!this._boundHandler) return;
|
|
4679
5150
|
window.removeEventListener("aparte-send", this._boundHandler);
|
|
4680
5151
|
this._boundHandler = null;
|
|
@@ -4718,7 +5189,27 @@ class AparteClient {
|
|
|
4718
5189
|
* approval to `{ approved: false }` — there is no timeout, since a human may
|
|
4719
5190
|
* take any amount of time to decide.
|
|
4720
5191
|
*/
|
|
4721
|
-
|
|
5192
|
+
/**
|
|
5193
|
+
* Await the human's Approve/Reject for one tool call.
|
|
5194
|
+
*
|
|
5195
|
+
* `target` is not decoration: it is the scope of the consent. The listener sits
|
|
5196
|
+
* on `document` and used to accept any `aparte-tool-decision` whose
|
|
5197
|
+
* `detail.toolCallId` matched — and that id is the tool-call id the MODEL chose.
|
|
5198
|
+
* The built-in buttons dispatch with `bubbles: true, composed: true`, so on a
|
|
5199
|
+
* page with two chats a click aimed at one tool could satisfy the gate awaiting
|
|
5200
|
+
* a different tool in a different conversation. The consented action and the
|
|
5201
|
+
* executed action came apart, which is the entire failure mode an approval gate
|
|
5202
|
+
* exists to prevent — and the handler behind it is arbitrary consumer code.
|
|
5203
|
+
*
|
|
5204
|
+
* The check is DOM CONTAINMENT rather than a `targetId` string comparison. A
|
|
5205
|
+
* model can choose an id; it cannot choose where in the tree the click happened.
|
|
5206
|
+
* It also needs no change on the dispatch side, so a consumer's own Approve
|
|
5207
|
+
* button keeps working as long as it fires from inside its own chat.
|
|
5208
|
+
*
|
|
5209
|
+
* The request half of this handshake was hardened with `targetId` for exactly
|
|
5210
|
+
* this hazard. This is its sibling, and it was missed.
|
|
5211
|
+
*/
|
|
5212
|
+
_awaitToolDecision(toolCallId, signal, target) {
|
|
4722
5213
|
return new Promise((resolve) => {
|
|
4723
5214
|
if (signal.aborted) {
|
|
4724
5215
|
resolve({ approved: false });
|
|
@@ -4731,6 +5222,7 @@ class AparteClient {
|
|
|
4731
5222
|
const onDecision = (e) => {
|
|
4732
5223
|
const detail = e.detail;
|
|
4733
5224
|
if (detail?.toolCallId !== toolCallId) return;
|
|
5225
|
+
if (target && e.target instanceof Node && e.target !== document && !target.contains(e.target)) return;
|
|
4734
5226
|
cleanup();
|
|
4735
5227
|
resolve({ approved: detail?.approved === true, payload: detail?.payload });
|
|
4736
5228
|
};
|
|
@@ -4744,7 +5236,7 @@ class AparteClient {
|
|
|
4744
5236
|
}
|
|
4745
5237
|
/**
|
|
4746
5238
|
* Resolve the auth for a provider: `options.keyResolver` takes precedence,
|
|
4747
|
-
* then the
|
|
5239
|
+
* then the aparteGlobalConfig key channel (`setKeyProvider`) so a key registered
|
|
4748
5240
|
* there reaches the request. One key source on the happy path.
|
|
4749
5241
|
*/
|
|
4750
5242
|
async _resolveAuth(providerId) {
|
|
@@ -4781,12 +5273,24 @@ class AparteClient {
|
|
|
4781
5273
|
if (baseRequest.toolChoice === "none") {
|
|
4782
5274
|
baseRequest = { ...baseRequest, tools: void 0 };
|
|
4783
5275
|
}
|
|
4784
|
-
this._isAborted
|
|
4785
|
-
|
|
5276
|
+
if (this._isAborted) {
|
|
5277
|
+
dispatchLifecycleEvent(targetElement, "aparte-message-aborted", { messageId });
|
|
5278
|
+
return;
|
|
5279
|
+
}
|
|
5280
|
+
dispatchLifecycleEvent(targetElement, "aparte-message-start", { messageId, role: "assistant" });
|
|
4786
5281
|
try {
|
|
4787
5282
|
const usage = await this._streamLoop(targetElement, messageId, provider, baseRequest, authConfig);
|
|
4788
|
-
this.
|
|
5283
|
+
if (this._isAborted) {
|
|
5284
|
+
this._updateMessage(targetElement, messageId, { status: "completed" });
|
|
5285
|
+
} else {
|
|
5286
|
+
dispatchLifecycleEvent(targetElement, "aparte-message-done", { messageId, role: "assistant", usage });
|
|
5287
|
+
}
|
|
4789
5288
|
} catch (error) {
|
|
5289
|
+
if (this._isAborted) {
|
|
5290
|
+
dispatchLifecycleEvent(targetElement, "aparte-message-aborted", { messageId });
|
|
5291
|
+
this._updateMessage(targetElement, messageId, { status: "completed" });
|
|
5292
|
+
return;
|
|
5293
|
+
}
|
|
4790
5294
|
const aparteError = AparteError.from(error, AparteErrorCode.UNKNOWN_ERROR);
|
|
4791
5295
|
this._handleLifecycleError(targetElement, messageId, aparteError);
|
|
4792
5296
|
}
|
|
@@ -4798,20 +5302,9 @@ class AparteClient {
|
|
|
4798
5302
|
* Triggered programmatically or by dispatching `window.dispatchEvent(new CustomEvent('aparte-compact'))`.
|
|
4799
5303
|
* Dispatches `aparte-compact-done` on window when complete, or `aparte-compact-error` on failure.
|
|
4800
5304
|
*/
|
|
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
|
-
}
|
|
5305
|
+
async compact(targetId) {
|
|
5306
|
+
let target = this._resolveTarget(targetId);
|
|
5307
|
+
if (target && typeof target.getMessages !== "function") target = null;
|
|
4815
5308
|
if (!target) {
|
|
4816
5309
|
window.dispatchEvent(new CustomEvent("aparte-compact-error", { detail: { error: "No aparte-chat target found" } }));
|
|
4817
5310
|
return;
|
|
@@ -4860,7 +5353,14 @@ class AparteClient {
|
|
|
4860
5353
|
modelId: config.defaultModel || "",
|
|
4861
5354
|
stream: false
|
|
4862
5355
|
};
|
|
4863
|
-
const
|
|
5356
|
+
const compactController = new AbortController();
|
|
5357
|
+
this._streamController = compactController;
|
|
5358
|
+
const response = await this._config.getTransport().chat(
|
|
5359
|
+
provider,
|
|
5360
|
+
summarizeRequest,
|
|
5361
|
+
authConfig,
|
|
5362
|
+
{ providerId: provider.id, signal: compactController.signal }
|
|
5363
|
+
);
|
|
4864
5364
|
let summary;
|
|
4865
5365
|
if (typeof response === "string") {
|
|
4866
5366
|
summary = response;
|
|
@@ -4878,10 +5378,12 @@ class AparteClient {
|
|
|
4878
5378
|
if (!summary.trim()) {
|
|
4879
5379
|
throw new Error("Empty summary returned by model");
|
|
4880
5380
|
}
|
|
4881
|
-
|
|
5381
|
+
const clearAll = target.clearAll;
|
|
5382
|
+
if (typeof clearAll === "function") clearAll.call(target);
|
|
5383
|
+
else window.dispatchEvent(new CustomEvent("aparte-reset"));
|
|
4882
5384
|
await new Promise((resolve) => setTimeout(resolve, 50));
|
|
4883
5385
|
target.appendMessage?.({
|
|
4884
|
-
id:
|
|
5386
|
+
id: uuid(),
|
|
4885
5387
|
role: "assistant",
|
|
4886
5388
|
content: `📝 **Conversation summary**
|
|
4887
5389
|
|
|
@@ -4904,17 +5406,41 @@ ${summary}`,
|
|
|
4904
5406
|
* using the same conversation history minus the retried reply.
|
|
4905
5407
|
*/
|
|
4906
5408
|
/**
|
|
4907
|
-
*
|
|
4908
|
-
*
|
|
4909
|
-
* send / retry / edit
|
|
4910
|
-
* the initial send while retry
|
|
5409
|
+
* The registered tools, unless the current model says it cannot call one.
|
|
5410
|
+
*
|
|
5411
|
+
* Single source for the gate, so send / retry / edit cannot drift — that drift
|
|
5412
|
+
* is what once shipped `tools` on the initial send while retry and edit omitted
|
|
5413
|
+
* them.
|
|
5414
|
+
*
|
|
5415
|
+
* The gate used to be "the model DECLARES function_calling", defaulting to
|
|
5416
|
+
* stripping — and that made the whole tool surface unreachable on the primary
|
|
5417
|
+
* documented path. Three things had to line up for a tool to be sent, and one
|
|
5418
|
+
* never did: the app sets a model id by hand (`setModelConfig`) or a selector
|
|
5419
|
+
* sets it from a fetched list, and in both cases the model's declared
|
|
5420
|
+
* capabilities are commonly unknown. `undefined` capabilities then read as "no
|
|
5421
|
+
* function calling", so `tools: []` went on the wire while `getTools()` held
|
|
5422
|
+
* the tool the app had explicitly registered. The model answered, correctly,
|
|
5423
|
+
* that it had no such tool, and nothing anywhere said why.
|
|
5424
|
+
*
|
|
5425
|
+
* So the question is now "did this model say it cannot", not "did it say it
|
|
5426
|
+
* can". Registering a tool is an explicit act by the app; silently dropping it
|
|
5427
|
+
* because a `/models` listing is terse is the library second-guessing the
|
|
5428
|
+
* developer. A model that declares its capabilities and omits function calling
|
|
5429
|
+
* is still honoured — that is a statement, and it is respected.
|
|
5430
|
+
*
|
|
5431
|
+
* Failure modes, both ways round: over-sending means a model that cannot use
|
|
5432
|
+
* tools does not call one, or its own endpoint rejects the array with an error
|
|
5433
|
+
* the developer can read. Under-sending was silent, total, and looked like a
|
|
5434
|
+
* lying model.
|
|
4911
5435
|
*/
|
|
4912
5436
|
_toolsForCurrentModel() {
|
|
4913
|
-
const
|
|
4914
|
-
|
|
5437
|
+
const capabilities = this._config.getCurrentModel()?.capabilities;
|
|
5438
|
+
const declinesTools = capabilities ? !capabilities.includes("function_calling") : false;
|
|
5439
|
+
return declinesTools ? [] : this._config.getTools();
|
|
4915
5440
|
}
|
|
4916
5441
|
async _handleRetry(event) {
|
|
4917
5442
|
const { messageId, targetId } = event.detail ?? {};
|
|
5443
|
+
this._isAborted = false;
|
|
4918
5444
|
if (!messageId) return;
|
|
4919
5445
|
const targetElement = this._resolveTarget(targetId);
|
|
4920
5446
|
if (!targetElement) {
|
|
@@ -4925,9 +5451,9 @@ ${summary}`,
|
|
|
4925
5451
|
const retryIdx = allMessages.findIndex((m) => m.id === messageId);
|
|
4926
5452
|
const retryMsg = retryIdx >= 0 ? allMessages[retryIdx] : void 0;
|
|
4927
5453
|
const sliceEnd = retryMsg?.role === "user" ? retryIdx + 1 : retryIdx;
|
|
4928
|
-
const historyMessages = retryIdx
|
|
5454
|
+
const historyMessages = retryIdx >= 0 ? allMessages.slice(0, sliceEnd) : allMessages;
|
|
4929
5455
|
const newMsg = {
|
|
4930
|
-
id:
|
|
5456
|
+
id: uuid(),
|
|
4931
5457
|
role: "assistant",
|
|
4932
5458
|
content: "",
|
|
4933
5459
|
status: "pending",
|
|
@@ -4960,6 +5486,7 @@ ${summary}`,
|
|
|
4960
5486
|
*/
|
|
4961
5487
|
async _handleEdit(event) {
|
|
4962
5488
|
const { messageId, content: newContent, targetId } = event.detail ?? {};
|
|
5489
|
+
this._isAborted = false;
|
|
4963
5490
|
if (!messageId || newContent === void 0) return;
|
|
4964
5491
|
const targetElement = this._resolveTarget(targetId);
|
|
4965
5492
|
if (!targetElement) {
|
|
@@ -4988,7 +5515,7 @@ ${summary}`,
|
|
|
4988
5515
|
const provider = this._config.getAIProvider(providerId);
|
|
4989
5516
|
if (!provider) return;
|
|
4990
5517
|
const authConfig = await this._resolveAuth(providerId);
|
|
4991
|
-
const newMessageId =
|
|
5518
|
+
const newMessageId = uuid();
|
|
4992
5519
|
targetElement.appendMessage?.({
|
|
4993
5520
|
id: newMessageId,
|
|
4994
5521
|
role: "assistant",
|
|
@@ -5051,6 +5578,12 @@ ${summary}`,
|
|
|
5051
5578
|
}
|
|
5052
5579
|
async _handleSend(event) {
|
|
5053
5580
|
const { content, modelId, providerId: explicitProviderId } = event.detail;
|
|
5581
|
+
if (this._config.getRequireModelSelection() && !this._config.hasSelectedModel() && !modelId) {
|
|
5582
|
+
console.warn(
|
|
5583
|
+
"[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."
|
|
5584
|
+
);
|
|
5585
|
+
return;
|
|
5586
|
+
}
|
|
5054
5587
|
let targetElement = null;
|
|
5055
5588
|
const targetId = event.detail?.targetId;
|
|
5056
5589
|
if (targetId) {
|
|
@@ -5093,7 +5626,7 @@ ${summary}`,
|
|
|
5093
5626
|
console.warn("[AparteClient] ⚠️ No target element found with appendMessage support. Provide a targetResolver in AparteClientOptions or ensure the composer has a `target` attribute.");
|
|
5094
5627
|
return;
|
|
5095
5628
|
}
|
|
5096
|
-
const messageId =
|
|
5629
|
+
const messageId = uuid();
|
|
5097
5630
|
const config = this._config.getModelConfig();
|
|
5098
5631
|
const providerId = explicitProviderId || config.defaultProvider;
|
|
5099
5632
|
if (!providerId) {
|
|
@@ -5177,10 +5710,69 @@ ${summary}`,
|
|
|
5177
5710
|
content: this._extractText(m)
|
|
5178
5711
|
})).filter((m) => m.content.length > 0);
|
|
5179
5712
|
}
|
|
5713
|
+
/**
|
|
5714
|
+
* What the model is told it said last turn — reconstructed from what was
|
|
5715
|
+
* RENDERED, not from the raw-text field.
|
|
5716
|
+
*
|
|
5717
|
+
* The order used to be the other way round, and it silently corrupted the
|
|
5718
|
+
* context on the most common opening in a coding chat: a reply starting with a
|
|
5719
|
+
* code fence was sent back as three backticks and nothing else. Three
|
|
5720
|
+
* correct-in-isolation decisions composed into it:
|
|
5721
|
+
*
|
|
5722
|
+
* 1. the parser withholds an ambiguous prefix (``` ``` ```, `` ` ``, `<`)
|
|
5723
|
+
* waiting for the next delta, and creates no active segment while it waits;
|
|
5724
|
+
* 2. so `_streamLoop` sees zero segments, concludes the parser produced
|
|
5725
|
+
* nothing, and appends the raw delta straight to `message.content`;
|
|
5726
|
+
* 3. and this method preferred `content`.
|
|
5727
|
+
*
|
|
5728
|
+
* The bubble hides its content element as soon as segments exist, so the UI was
|
|
5729
|
+
* perfect and only the NEXT request showed it. No unit test, no example and
|
|
5730
|
+
* no browser test could see it.
|
|
5731
|
+
*
|
|
5732
|
+
* `content` stays as the fallback, and that is not vestigial: a non-streaming
|
|
5733
|
+
* transport writes the whole reply there and creates no segments at all.
|
|
5734
|
+
*/
|
|
5180
5735
|
_extractText(message) {
|
|
5736
|
+
const rendered = this._segmentsToText(message.segments);
|
|
5737
|
+
if (rendered) return rendered;
|
|
5181
5738
|
if (typeof message.content === "string" && message.content) return message.content;
|
|
5182
|
-
|
|
5183
|
-
|
|
5739
|
+
return "";
|
|
5740
|
+
}
|
|
5741
|
+
/**
|
|
5742
|
+
* Segments → the text a model can read back.
|
|
5743
|
+
*
|
|
5744
|
+
* Fences and the language tag are kept. Without them the model re-reads its own
|
|
5745
|
+
* code as prose — which is how it starts explaining a snippet it believes it
|
|
5746
|
+
* wrote in English. Artifacts are included for the same reason: dropping them
|
|
5747
|
+
* made an artifact the model had just produced invisible on the very next turn,
|
|
5748
|
+
* so it could not be asked to change the thing it had built.
|
|
5749
|
+
*
|
|
5750
|
+
* `thinking` stays out on purpose — it is the model's own scratchpad, and most
|
|
5751
|
+
* APIs neither want it back nor bill for it kindly.
|
|
5752
|
+
*/
|
|
5753
|
+
_segmentsToText(segments) {
|
|
5754
|
+
if (!segments?.length) return "";
|
|
5755
|
+
const fence = "```";
|
|
5756
|
+
const parts = [];
|
|
5757
|
+
for (const segment of segments) {
|
|
5758
|
+
const content = segment.content ?? "";
|
|
5759
|
+
if (segment.type === "text") {
|
|
5760
|
+
if (content) parts.push(content);
|
|
5761
|
+
} else if (segment.type === "code") {
|
|
5762
|
+
const lang = segment.language ?? "";
|
|
5763
|
+
parts.push(`${fence}${lang}
|
|
5764
|
+
${content}
|
|
5765
|
+
${fence}`);
|
|
5766
|
+
} else if (segment.type === "artifact") {
|
|
5767
|
+
const title = segment.title ?? "artifact";
|
|
5768
|
+
const kind = segment.artifactType ?? "";
|
|
5769
|
+
parts.push(`${fence}${kind}
|
|
5770
|
+
<!-- artifact: ${title} -->
|
|
5771
|
+
${content}
|
|
5772
|
+
${fence}`);
|
|
5773
|
+
}
|
|
5774
|
+
}
|
|
5775
|
+
return parts.join("\n").trim();
|
|
5184
5776
|
}
|
|
5185
5777
|
/**
|
|
5186
5778
|
* Which of the send's pending files get inlined into the request: the
|
|
@@ -5238,135 +5830,6 @@ ${content}
|
|
|
5238
5830
|
);
|
|
5239
5831
|
return parts.filter((p) => p !== null);
|
|
5240
5832
|
}
|
|
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
5833
|
/**
|
|
5371
5834
|
* Turn-1 forced tool call. When `toolChoice = { name, input }`
|
|
5372
5835
|
* (orchestrator-driven), execute the handler directly instead of consulting
|
|
@@ -5380,7 +5843,7 @@ ${content}
|
|
|
5380
5843
|
if (!(turns === 1 && toolChoice && typeof toolChoice === "object" && toolChoice.input !== void 0)) {
|
|
5381
5844
|
return { baseRequest, skip: false };
|
|
5382
5845
|
}
|
|
5383
|
-
const syntheticId =
|
|
5846
|
+
const syntheticId = uuid();
|
|
5384
5847
|
const syntheticCall = { id: syntheticId, name: toolChoice.name, input: toolChoice.input };
|
|
5385
5848
|
const toolSeg = {
|
|
5386
5849
|
id: `tool-${syntheticId}`,
|
|
@@ -5403,9 +5866,16 @@ ${content}
|
|
|
5403
5866
|
}
|
|
5404
5867
|
const controller = new AbortController();
|
|
5405
5868
|
this._activeToolControllers.add(controller);
|
|
5406
|
-
const
|
|
5869
|
+
const { raced, done: clearToolTimeout } = withToolTimeout(
|
|
5870
|
+
(sig) => handler(syntheticCall, sig, {
|
|
5871
|
+
target: targetElement,
|
|
5872
|
+
config: this._config
|
|
5873
|
+
}),
|
|
5874
|
+
controller,
|
|
5875
|
+
this.options.toolTimeoutMs ?? DEFAULT_TOOL_HANDLER_TIMEOUT_MS
|
|
5876
|
+
);
|
|
5407
5877
|
try {
|
|
5408
|
-
const result = await
|
|
5878
|
+
const result = await raced;
|
|
5409
5879
|
targetElement.updateSegment?.(toolSeg.id, { status: "resolved", result: result.content });
|
|
5410
5880
|
messages.push({ role: "tool_call", content: "", toolCalls: [syntheticCall] });
|
|
5411
5881
|
messages.push({ role: "tool_result", content: result.content, toolCallId: syntheticId });
|
|
@@ -5417,7 +5887,7 @@ ${content}
|
|
|
5417
5887
|
}
|
|
5418
5888
|
throw err;
|
|
5419
5889
|
} finally {
|
|
5420
|
-
|
|
5890
|
+
clearToolTimeout();
|
|
5421
5891
|
this._activeToolControllers.delete(controller);
|
|
5422
5892
|
}
|
|
5423
5893
|
}
|
|
@@ -5435,7 +5905,7 @@ ${content}
|
|
|
5435
5905
|
if (event.name === "create_artifact") {
|
|
5436
5906
|
const input = event.input;
|
|
5437
5907
|
const mimeType = input.mimeType ?? "text/plain";
|
|
5438
|
-
const kind =
|
|
5908
|
+
const kind = deriveArtifactKind(mimeType, "text");
|
|
5439
5909
|
const artifactSeg = {
|
|
5440
5910
|
id: `artifact-${event.id}`,
|
|
5441
5911
|
type: "artifact",
|
|
@@ -5445,7 +5915,7 @@ ${content}
|
|
|
5445
5915
|
content: input.content ?? ""
|
|
5446
5916
|
};
|
|
5447
5917
|
targetElement.addSegment?.(artifactSeg);
|
|
5448
|
-
|
|
5918
|
+
dispatchArtifactLifecycle(targetElement, messageId, artifactSeg, artifactProgress, true);
|
|
5449
5919
|
messages.push({
|
|
5450
5920
|
role: "tool_call",
|
|
5451
5921
|
content: "",
|
|
@@ -5499,13 +5969,13 @@ ${content}
|
|
|
5499
5969
|
const approvalController = new AbortController();
|
|
5500
5970
|
this._activeToolControllers.add(approvalController);
|
|
5501
5971
|
targetElement.updateSegment?.(toolSeg.id, { status: "awaiting-approval" });
|
|
5502
|
-
targetElement
|
|
5503
|
-
|
|
5504
|
-
|
|
5505
|
-
|
|
5506
|
-
})
|
|
5972
|
+
dispatchLifecycleEvent(targetElement, "aparte-tool-approval-request", {
|
|
5973
|
+
toolCallId: event.id,
|
|
5974
|
+
toolName: event.name,
|
|
5975
|
+
input: event.input
|
|
5976
|
+
});
|
|
5507
5977
|
let decision;
|
|
5508
|
-
const resolveApproval = this.options.approvalResolver ?? ((id, sig) => this._awaitToolDecision(id, sig));
|
|
5978
|
+
const resolveApproval = this.options.approvalResolver ?? ((id, sig) => this._awaitToolDecision(id, sig, targetElement));
|
|
5509
5979
|
try {
|
|
5510
5980
|
decision = await resolveApproval(event.id, approvalController.signal);
|
|
5511
5981
|
} finally {
|
|
@@ -5535,12 +6005,17 @@ ${content}
|
|
|
5535
6005
|
}
|
|
5536
6006
|
const controller = new AbortController();
|
|
5537
6007
|
this._activeToolControllers.add(controller);
|
|
5538
|
-
const
|
|
5539
|
-
|
|
5540
|
-
const result = await handler(
|
|
6008
|
+
const { raced, done: clearToolTimeout } = withToolTimeout(
|
|
6009
|
+
(sig) => handler(
|
|
5541
6010
|
{ id: event.id, name: event.name, input: effectiveInput },
|
|
5542
|
-
|
|
5543
|
-
|
|
6011
|
+
sig,
|
|
6012
|
+
{ target: targetElement, config: this._config }
|
|
6013
|
+
),
|
|
6014
|
+
controller,
|
|
6015
|
+
this.options.toolTimeoutMs ?? DEFAULT_TOOL_HANDLER_TIMEOUT_MS
|
|
6016
|
+
);
|
|
6017
|
+
try {
|
|
6018
|
+
const result = await raced;
|
|
5544
6019
|
targetElement.updateSegment?.(toolSeg.id, { status: "resolved", result: result.content });
|
|
5545
6020
|
const existingToolCallMsg = messages.find(
|
|
5546
6021
|
(m) => m.role === "tool_call" && m.toolCalls?.some((tc) => tc.id === event.id)
|
|
@@ -5566,7 +6041,7 @@ ${content}
|
|
|
5566
6041
|
throw err;
|
|
5567
6042
|
}
|
|
5568
6043
|
} finally {
|
|
5569
|
-
|
|
6044
|
+
clearToolTimeout();
|
|
5570
6045
|
this._activeToolControllers.delete(controller);
|
|
5571
6046
|
}
|
|
5572
6047
|
} else {
|
|
@@ -5576,7 +6051,12 @@ ${content}
|
|
|
5576
6051
|
}
|
|
5577
6052
|
return { continueLoop };
|
|
5578
6053
|
}
|
|
6054
|
+
/**
|
|
6055
|
+
* Stream loop: runs one provider.chat() call and repeats if a tool was called.
|
|
6056
|
+
* Maintains a running messages array to inject tool_call / tool_result turns.
|
|
6057
|
+
*/
|
|
5579
6058
|
async _streamLoop(targetElement, messageId, provider, baseRequest, authConfig) {
|
|
6059
|
+
this._streamController?.abort();
|
|
5580
6060
|
const streamController = new AbortController();
|
|
5581
6061
|
this._streamController = streamController;
|
|
5582
6062
|
if (this.options.streamRunner) {
|
|
@@ -5595,14 +6075,14 @@ ${content}
|
|
|
5595
6075
|
}
|
|
5596
6076
|
while (continueLoop) {
|
|
5597
6077
|
if (this._isAborted) {
|
|
5598
|
-
|
|
6078
|
+
dispatchLifecycleEvent(targetElement, "aparte-message-aborted", { messageId });
|
|
5599
6079
|
break;
|
|
5600
6080
|
}
|
|
5601
6081
|
turns++;
|
|
5602
6082
|
if (turns > globalMaxTurns) {
|
|
5603
6083
|
console.warn(`[AparteClient] maxTurns (${globalMaxTurns}) exceeded — stopping loop.`);
|
|
5604
6084
|
targetElement.addSegment?.({
|
|
5605
|
-
id: `max-turns-${
|
|
6085
|
+
id: `max-turns-${uuid()}`,
|
|
5606
6086
|
type: "error",
|
|
5607
6087
|
content: `Stopped after ${globalMaxTurns} tool calls to prevent an infinite loop.`,
|
|
5608
6088
|
details: "MAX_TURNS_EXCEEDED"
|
|
@@ -5630,7 +6110,17 @@ ${content}
|
|
|
5630
6110
|
const request = { ...baseRequest, messages: phaseMessages, _meta: phaseMeta };
|
|
5631
6111
|
const response = await this._config.getTransport().chat(provider, request, authConfig, { providerId: provider.id, signal: streamController.signal });
|
|
5632
6112
|
if (typeof response === "string") {
|
|
5633
|
-
|
|
6113
|
+
const wholeParser = new AparteStreamParser();
|
|
6114
|
+
const parsed = [
|
|
6115
|
+
...wholeParser.parse(response).segments,
|
|
6116
|
+
...wholeParser.finalize()
|
|
6117
|
+
];
|
|
6118
|
+
if (parsed.length > 0) {
|
|
6119
|
+
for (const segment of parsed) targetElement.addSegment?.(segment);
|
|
6120
|
+
this._updateMessage(targetElement, messageId, { status: "completed" });
|
|
6121
|
+
} else {
|
|
6122
|
+
this._updateMessage(targetElement, messageId, { content: response, status: "completed" });
|
|
6123
|
+
}
|
|
5634
6124
|
return void 0;
|
|
5635
6125
|
}
|
|
5636
6126
|
const reader = response.getReader();
|
|
@@ -5646,7 +6136,7 @@ ${content}
|
|
|
5646
6136
|
let rawSegId = null;
|
|
5647
6137
|
let rawContent = "";
|
|
5648
6138
|
if (artifactRawHint) {
|
|
5649
|
-
rawSegId = `artifact-raw-${
|
|
6139
|
+
rawSegId = `artifact-raw-${uuid()}`;
|
|
5650
6140
|
const rawSeg = {
|
|
5651
6141
|
id: rawSegId,
|
|
5652
6142
|
type: "artifact",
|
|
@@ -5657,7 +6147,7 @@ ${content}
|
|
|
5657
6147
|
};
|
|
5658
6148
|
targetElement.addSegment?.(rawSeg);
|
|
5659
6149
|
streamingSegmentIds.add(rawSegId);
|
|
5660
|
-
|
|
6150
|
+
dispatchArtifactLifecycle(targetElement, messageId, rawSeg, artifactProgress, false);
|
|
5661
6151
|
}
|
|
5662
6152
|
const artifactXmlHint = baseRequest._meta?.artifactXml;
|
|
5663
6153
|
const xmlCtx = {
|
|
@@ -5672,25 +6162,32 @@ ${content}
|
|
|
5672
6162
|
};
|
|
5673
6163
|
let precedingText = "";
|
|
5674
6164
|
const toolCallsThisTurn = [];
|
|
6165
|
+
const bailOnAbort = () => {
|
|
6166
|
+
if (!this._isAborted) return false;
|
|
6167
|
+
try {
|
|
6168
|
+
void reader.cancel();
|
|
6169
|
+
} catch {
|
|
6170
|
+
}
|
|
6171
|
+
dispatchLifecycleEvent(targetElement, "aparte-message-aborted", { messageId });
|
|
6172
|
+
continueLoop = false;
|
|
6173
|
+
return true;
|
|
6174
|
+
};
|
|
6175
|
+
let streamDrained = false;
|
|
5675
6176
|
try {
|
|
5676
6177
|
while (true) {
|
|
5677
|
-
if (
|
|
5678
|
-
|
|
5679
|
-
|
|
5680
|
-
|
|
5681
|
-
|
|
5682
|
-
this._dispatchLifecycleEvent(targetElement, "aparte-message-aborted", { messageId });
|
|
5683
|
-
continueLoop = false;
|
|
6178
|
+
if (bailOnAbort()) break;
|
|
6179
|
+
const { done, value: event } = await reader.read();
|
|
6180
|
+
if (bailOnAbort()) break;
|
|
6181
|
+
if (done) {
|
|
6182
|
+
streamDrained = true;
|
|
5684
6183
|
break;
|
|
5685
6184
|
}
|
|
5686
|
-
const { done, value: event } = await reader.read();
|
|
5687
|
-
if (done) break;
|
|
5688
6185
|
switch (event.type) {
|
|
5689
6186
|
case "thinking": {
|
|
5690
6187
|
thinkingContent += event.delta;
|
|
5691
6188
|
if (!thinkingSegmentId) {
|
|
5692
6189
|
const seg = {
|
|
5693
|
-
id: `think-${
|
|
6190
|
+
id: `think-${uuid()}`,
|
|
5694
6191
|
type: "thinking",
|
|
5695
6192
|
content: thinkingContent,
|
|
5696
6193
|
collapsed: true,
|
|
@@ -5713,7 +6210,7 @@ ${content}
|
|
|
5713
6210
|
if (artifactRawHint && rawSegId) {
|
|
5714
6211
|
rawContent += event.delta;
|
|
5715
6212
|
targetElement.updateSegment?.(rawSegId, { content: rawContent });
|
|
5716
|
-
|
|
6213
|
+
dispatchArtifactLifecycle(targetElement, messageId, {
|
|
5717
6214
|
id: rawSegId,
|
|
5718
6215
|
type: "artifact",
|
|
5719
6216
|
mimeType: artifactRawHint.mimeType,
|
|
@@ -5724,7 +6221,7 @@ ${content}
|
|
|
5724
6221
|
break;
|
|
5725
6222
|
}
|
|
5726
6223
|
if (artifactXmlHint) {
|
|
5727
|
-
|
|
6224
|
+
feedXmlArtifactDelta(event.delta, xmlCtx, {
|
|
5728
6225
|
targetElement,
|
|
5729
6226
|
messageId,
|
|
5730
6227
|
textParser,
|
|
@@ -5756,7 +6253,7 @@ ${content}
|
|
|
5756
6253
|
targetElement.updateSegment?.(segment.id, { content: segment.content });
|
|
5757
6254
|
}
|
|
5758
6255
|
if (segment.type === "artifact") {
|
|
5759
|
-
|
|
6256
|
+
dispatchArtifactLifecycle(targetElement, messageId, segment, artifactProgress, true);
|
|
5760
6257
|
}
|
|
5761
6258
|
}
|
|
5762
6259
|
const active = textParser.getState().activeSegment;
|
|
@@ -5765,17 +6262,15 @@ ${content}
|
|
|
5765
6262
|
targetElement.addSegment?.(active);
|
|
5766
6263
|
streamingSegmentIds.add(active.id);
|
|
5767
6264
|
if (active.type === "artifact") {
|
|
5768
|
-
|
|
6265
|
+
dispatchArtifactLifecycle(targetElement, messageId, active, artifactProgress, false);
|
|
5769
6266
|
}
|
|
5770
6267
|
} else {
|
|
5771
6268
|
targetElement.updateSegment?.(active.id, { content: active.content });
|
|
5772
6269
|
if (active.type === "artifact") {
|
|
5773
|
-
|
|
6270
|
+
dispatchArtifactLifecycle(targetElement, messageId, active, artifactProgress, false);
|
|
5774
6271
|
}
|
|
5775
6272
|
}
|
|
5776
6273
|
} else if (result.segments.length === 0) {
|
|
5777
|
-
if (targetElement.typeName) targetElement.typeName(event.delta);
|
|
5778
|
-
else targetElement.updateLastMessage?.(event.delta, { append: true });
|
|
5779
6274
|
}
|
|
5780
6275
|
break;
|
|
5781
6276
|
}
|
|
@@ -5792,16 +6287,22 @@ ${content}
|
|
|
5792
6287
|
case "done":
|
|
5793
6288
|
if (event.usage) lastUsage = event.usage;
|
|
5794
6289
|
break;
|
|
5795
|
-
default:
|
|
5796
|
-
|
|
6290
|
+
default: {
|
|
6291
|
+
const exhaustive = event;
|
|
6292
|
+
void exhaustive;
|
|
6293
|
+
this._warnUnknownStreamEvent(event);
|
|
6294
|
+
break;
|
|
6295
|
+
}
|
|
5797
6296
|
}
|
|
6297
|
+
if (!continueLoop) break;
|
|
5798
6298
|
}
|
|
6299
|
+
finalizeXmlArtifact(xmlCtx, { targetElement, messageId, artifactProgress, artifactXmlHint, textParser, streamingSegmentIds });
|
|
5799
6300
|
const finals = textParser.finalize();
|
|
5800
6301
|
if (artifactRawHint && rawSegId) {
|
|
5801
6302
|
const lineCount = rawContent.split("\n").length;
|
|
5802
6303
|
const isInline = lineCount < 15;
|
|
5803
6304
|
targetElement.updateSegment?.(rawSegId, { content: rawContent, inline: isInline });
|
|
5804
|
-
|
|
6305
|
+
dispatchArtifactLifecycle(targetElement, messageId, {
|
|
5805
6306
|
id: rawSegId,
|
|
5806
6307
|
type: "artifact",
|
|
5807
6308
|
mimeType: artifactRawHint.mimeType,
|
|
@@ -5811,22 +6312,6 @@ ${content}
|
|
|
5811
6312
|
inline: isInline
|
|
5812
6313
|
}, artifactProgress, true);
|
|
5813
6314
|
}
|
|
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
6315
|
if (artifactHint && !artifactPromoted) {
|
|
5831
6316
|
const codeIdx = finals.findIndex((s) => s.type === "code");
|
|
5832
6317
|
if (codeIdx !== -1) {
|
|
@@ -5853,7 +6338,7 @@ ${content}
|
|
|
5853
6338
|
targetElement.updateSegment?.(s.id, { content: s.content });
|
|
5854
6339
|
}
|
|
5855
6340
|
if (s.type === "artifact") {
|
|
5856
|
-
|
|
6341
|
+
dispatchArtifactLifecycle(targetElement, messageId, s, artifactProgress, true);
|
|
5857
6342
|
}
|
|
5858
6343
|
}
|
|
5859
6344
|
if (toolCallsThisTurn.length === 0) {
|
|
@@ -5862,13 +6347,19 @@ ${content}
|
|
|
5862
6347
|
messages.push({ role: "assistant", content: precedingText.trim() });
|
|
5863
6348
|
}
|
|
5864
6349
|
pipelineIndex++;
|
|
5865
|
-
const pwId = `pw-${
|
|
6350
|
+
const pwId = `pw-${uuid()}`;
|
|
5866
6351
|
targetElement.addSegment?.({ id: pwId, type: "pipeline-waiting" });
|
|
5867
6352
|
} else {
|
|
5868
6353
|
continueLoop = false;
|
|
5869
6354
|
}
|
|
5870
6355
|
}
|
|
5871
6356
|
} finally {
|
|
6357
|
+
if (!streamDrained) {
|
|
6358
|
+
try {
|
|
6359
|
+
await reader.cancel();
|
|
6360
|
+
} catch {
|
|
6361
|
+
}
|
|
6362
|
+
}
|
|
5872
6363
|
reader.releaseLock();
|
|
5873
6364
|
}
|
|
5874
6365
|
}
|
|
@@ -5908,12 +6399,17 @@ ${content}
|
|
|
5908
6399
|
const response = await this._config.getTransport().chat(provider, request, authConfig, { providerId: provider.id, signal });
|
|
5909
6400
|
return typeof response === "string" ? response : readableToAsyncIterable(response, signal);
|
|
5910
6401
|
};
|
|
5911
|
-
const toolLookup = (name) =>
|
|
6402
|
+
const toolLookup = (name) => {
|
|
6403
|
+
const handler = this._config.getToolHandler(name);
|
|
6404
|
+
if (!handler) return void 0;
|
|
6405
|
+
const context = { target: targetElement, config: this._config };
|
|
6406
|
+
return (call, sig) => handler(call, sig, context);
|
|
6407
|
+
};
|
|
5912
6408
|
const toolConfigLookup = (name) => {
|
|
5913
6409
|
const tool = this._config.getTools().find((t) => t.name === name);
|
|
5914
6410
|
return tool ? { maxTurns: tool.maxTurns, needsApproval: tool.needsApproval } : void 0;
|
|
5915
6411
|
};
|
|
5916
|
-
const approvalResolver = this.options.approvalResolver ?? ((id, sig) => this._awaitToolDecision(id, sig));
|
|
6412
|
+
const approvalResolver = this.options.approvalResolver ?? ((id, sig) => this._awaitToolDecision(id, sig, targetElement));
|
|
5917
6413
|
const usage = await streamRunner({
|
|
5918
6414
|
messageId,
|
|
5919
6415
|
baseRequest,
|
|
@@ -5924,9 +6420,14 @@ ${content}
|
|
|
5924
6420
|
emitter,
|
|
5925
6421
|
signal,
|
|
5926
6422
|
maxTurns: this.options.maxTurns,
|
|
6423
|
+
// Forwarded, because the option's own JSDoc promises it means one thing
|
|
6424
|
+
// whichever loop runs — and the fix that introduced it only wired the
|
|
6425
|
+
// inline path, so setting it was silently ignored here. Exactly the
|
|
6426
|
+
// asymmetry the option was added to remove.
|
|
6427
|
+
toolTimeoutMs: this.options.toolTimeoutMs,
|
|
5927
6428
|
// Match the inline loop's id conventions: prefixed artifact ids, but a
|
|
5928
6429
|
// BARE uuid for the synthetic tool (the adapter renders `tool-<id>`).
|
|
5929
|
-
idGen: (prefix) => prefix === "synthetic-tool" ?
|
|
6430
|
+
idGen: (prefix) => prefix === "synthetic-tool" ? uuid() : `${prefix}-${uuid()}`
|
|
5930
6431
|
});
|
|
5931
6432
|
return usage ?? void 0;
|
|
5932
6433
|
}
|
|
@@ -5943,15 +6444,16 @@ ${content}
|
|
|
5943
6444
|
*/
|
|
5944
6445
|
_handleLifecycleError(target, messageId, error) {
|
|
5945
6446
|
const errorSegment = {
|
|
5946
|
-
id: `error-${
|
|
6447
|
+
id: `error-${uuid()}`,
|
|
5947
6448
|
type: "error",
|
|
5948
6449
|
content: error.message,
|
|
5949
6450
|
details: error.code
|
|
5950
6451
|
};
|
|
5951
6452
|
if (target.updateMessage) {
|
|
6453
|
+
const rendered = target.getMessages?.()?.find((m) => m.id === messageId)?.segments ?? [];
|
|
5952
6454
|
target.updateMessage(messageId, {
|
|
5953
6455
|
status: "error",
|
|
5954
|
-
segments: [errorSegment]
|
|
6456
|
+
segments: [...rendered, errorSegment]
|
|
5955
6457
|
});
|
|
5956
6458
|
} else if (target.appendMessage) {
|
|
5957
6459
|
target.appendMessage({
|
|
@@ -5962,65 +6464,17 @@ ${content}
|
|
|
5962
6464
|
segments: [errorSegment]
|
|
5963
6465
|
});
|
|
5964
6466
|
}
|
|
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
|
-
}));
|
|
6467
|
+
dispatchLifecycleEvent(target, "aparte-message-error", { messageId, error });
|
|
5976
6468
|
}
|
|
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
|
-
}
|
|
6469
|
+
/** Warn once per client for an unrecognised stream event, then carry on. */
|
|
6470
|
+
_warnedUnknownEvents = /* @__PURE__ */ new Set();
|
|
6471
|
+
_warnUnknownStreamEvent(event) {
|
|
6472
|
+
const type = String(event?.type ?? "undefined");
|
|
6473
|
+
if (this._warnedUnknownEvents.has(type)) return;
|
|
6474
|
+
this._warnedUnknownEvents.add(type);
|
|
6475
|
+
console.warn(
|
|
6476
|
+
`[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.`
|
|
6477
|
+
);
|
|
6024
6478
|
}
|
|
6025
6479
|
}
|
|
6026
6480
|
function findHead(node) {
|
|
@@ -6028,7 +6482,7 @@ function findHead(node) {
|
|
|
6028
6482
|
if ("current" in node) return node;
|
|
6029
6483
|
return null;
|
|
6030
6484
|
}
|
|
6031
|
-
class
|
|
6485
|
+
class AparteMessageRepository {
|
|
6032
6486
|
_nodes = /* @__PURE__ */ new Map();
|
|
6033
6487
|
_head = null;
|
|
6034
6488
|
_root = { next: null, children: [] };
|
|
@@ -6095,7 +6549,7 @@ class MessageRepository {
|
|
|
6095
6549
|
const existing = this._nodes.get(message.id);
|
|
6096
6550
|
const parentNode = parentId ? this._nodes.get(parentId) : void 0;
|
|
6097
6551
|
if (parentId !== null && parentNode === void 0) {
|
|
6098
|
-
throw new Error(`
|
|
6552
|
+
throw new Error(`AparteMessageRepository: parent "${parentId}" not found.`);
|
|
6099
6553
|
}
|
|
6100
6554
|
const parent = parentNode ?? null;
|
|
6101
6555
|
if (existing) {
|
|
@@ -6260,7 +6714,7 @@ class MessageRepository {
|
|
|
6260
6714
|
node.next = null;
|
|
6261
6715
|
}
|
|
6262
6716
|
}
|
|
6263
|
-
const
|
|
6717
|
+
const APARTE_DEFAULT_UI_EVENTS = [
|
|
6264
6718
|
"aparte-send",
|
|
6265
6719
|
"aparte-action",
|
|
6266
6720
|
"aparte-retry",
|
|
@@ -6273,8 +6727,9 @@ function applyElementProps(el2, props, transformValue = (value) => value) {
|
|
|
6273
6727
|
for (const [key, value] of Object.entries(props)) {
|
|
6274
6728
|
if (key.startsWith("--")) {
|
|
6275
6729
|
el2.style.setProperty(key, String(value));
|
|
6276
|
-
} else if (key.startsWith("on")
|
|
6277
|
-
|
|
6730
|
+
} else if (key.startsWith("on")) {
|
|
6731
|
+
el2.removeAttribute(key);
|
|
6732
|
+
} else if (value === null || value === void 0 || value === false) {
|
|
6278
6733
|
el2.removeAttribute(key);
|
|
6279
6734
|
} else if (value === true) {
|
|
6280
6735
|
el2.setAttribute(key, "");
|
|
@@ -6295,17 +6750,30 @@ function el(tag, className, text) {
|
|
|
6295
6750
|
if (text != null) node.textContent = text;
|
|
6296
6751
|
return node;
|
|
6297
6752
|
}
|
|
6298
|
-
function fieldHeader(parent, field) {
|
|
6299
|
-
|
|
6753
|
+
function fieldHeader(parent, field, skipTitle) {
|
|
6754
|
+
let titleId;
|
|
6755
|
+
if (field.title && field.title.trim() !== skipTitle?.trim()) {
|
|
6756
|
+
const title = el("p", "aparte-elic-title", field.title);
|
|
6757
|
+
titleId = `elic-title-${uuid()}`;
|
|
6758
|
+
title.id = titleId;
|
|
6759
|
+
parent.appendChild(title);
|
|
6760
|
+
}
|
|
6300
6761
|
if (field.description) parent.appendChild(el("p", "aparte-elic-desc", field.description));
|
|
6762
|
+
return titleId;
|
|
6301
6763
|
}
|
|
6302
|
-
function
|
|
6764
|
+
function labelGroup(list, opts) {
|
|
6765
|
+
list.setAttribute("role", opts.multiple ? "group" : "radiogroup");
|
|
6766
|
+
if (opts.titleId) list.setAttribute("aria-labelledby", opts.titleId);
|
|
6767
|
+
else if (opts.fallbackLabel) list.setAttribute("aria-label", opts.fallbackLabel);
|
|
6768
|
+
}
|
|
6769
|
+
function buildEnumField(field, onChange, fallbackLabel) {
|
|
6303
6770
|
const wrap = el("div", "aparte-elic-field aparte-elic-enum");
|
|
6304
|
-
fieldHeader(wrap, field);
|
|
6771
|
+
const titleId = fieldHeader(wrap, field, fallbackLabel);
|
|
6305
6772
|
const list = el("div", "aparte-elic-options");
|
|
6306
|
-
|
|
6773
|
+
labelGroup(list, { multiple: field.multiple, titleId, fallbackLabel });
|
|
6774
|
+
const name = `elic-${uuid()}`;
|
|
6307
6775
|
const type = field.multiple ? "checkbox" : "radio";
|
|
6308
|
-
const allowOther = field.allowOther ??
|
|
6776
|
+
const allowOther = field.allowOther ?? contextConfig().getElicitationOptions().allowOther;
|
|
6309
6777
|
const defaults = new Set(Array.isArray(field.default) ? field.default : field.default != null ? [field.default] : []);
|
|
6310
6778
|
const buildOption = (value, label, description, recommended) => {
|
|
6311
6779
|
const row = el("label", "aparte-elic-option" + (recommended ? " aparte-elic-option--recommended" : ""));
|
|
@@ -6331,12 +6799,13 @@ function buildEnumField(field, onChange) {
|
|
|
6331
6799
|
control.name = name;
|
|
6332
6800
|
control.value = "__other__";
|
|
6333
6801
|
const body = el("span", "aparte-elic-option-body");
|
|
6334
|
-
|
|
6802
|
+
const t = contextConfig();
|
|
6803
|
+
body.appendChild(el("span", "aparte-elic-option-title", t.t("elicitationOther")));
|
|
6335
6804
|
otherText = el("input", "aparte-elic-other-input");
|
|
6336
6805
|
otherText.type = "text";
|
|
6337
|
-
otherText.placeholder = "
|
|
6806
|
+
otherText.placeholder = t.t("elicitationOtherPlaceholder");
|
|
6338
6807
|
otherText.style.display = "none";
|
|
6339
|
-
otherText.setAttribute("aria-label", "
|
|
6808
|
+
otherText.setAttribute("aria-label", t.t("elicitationOtherLabel"));
|
|
6340
6809
|
body.appendChild(otherText);
|
|
6341
6810
|
row.append(control, body);
|
|
6342
6811
|
list.appendChild(row);
|
|
@@ -6374,11 +6843,12 @@ function buildEnumField(field, onChange) {
|
|
|
6374
6843
|
focus: () => controls()[0]?.focus()
|
|
6375
6844
|
};
|
|
6376
6845
|
}
|
|
6377
|
-
function buildBooleanField(field, onChange) {
|
|
6846
|
+
function buildBooleanField(field, onChange, fallbackLabel) {
|
|
6378
6847
|
const wrap = el("div", "aparte-elic-field aparte-elic-boolean");
|
|
6379
|
-
fieldHeader(wrap, field);
|
|
6848
|
+
const titleId = fieldHeader(wrap, field, fallbackLabel);
|
|
6380
6849
|
const list = el("div", "aparte-elic-options");
|
|
6381
|
-
|
|
6850
|
+
labelGroup(list, { titleId, fallbackLabel });
|
|
6851
|
+
const name = `elic-${uuid()}`;
|
|
6382
6852
|
const mk = (val, label) => {
|
|
6383
6853
|
const row = el("label", "aparte-elic-option");
|
|
6384
6854
|
const control = el("input", "aparte-elic-control");
|
|
@@ -6391,8 +6861,9 @@ function buildBooleanField(field, onChange) {
|
|
|
6391
6861
|
row.append(control, body);
|
|
6392
6862
|
list.appendChild(row);
|
|
6393
6863
|
};
|
|
6394
|
-
|
|
6395
|
-
mk("
|
|
6864
|
+
const t = contextConfig();
|
|
6865
|
+
mk("true", field.trueLabel ?? t.t("elicitationYes"));
|
|
6866
|
+
mk("false", field.falseLabel ?? t.t("elicitationNo"));
|
|
6396
6867
|
wrap.appendChild(list);
|
|
6397
6868
|
list.addEventListener("change", onChange);
|
|
6398
6869
|
const checked = () => list.querySelector("input:checked");
|
|
@@ -6403,15 +6874,15 @@ function buildBooleanField(field, onChange) {
|
|
|
6403
6874
|
focus: () => list.querySelector("input")?.focus()
|
|
6404
6875
|
};
|
|
6405
6876
|
}
|
|
6406
|
-
function buildStringField(field, onChange) {
|
|
6877
|
+
function buildStringField(field, onChange, fallbackLabel) {
|
|
6407
6878
|
const wrap = el("div", "aparte-elic-field aparte-elic-string");
|
|
6408
|
-
fieldHeader(wrap, field);
|
|
6879
|
+
fieldHeader(wrap, field, fallbackLabel);
|
|
6409
6880
|
const input = field.multiline ? el("textarea", "aparte-elic-text") : el("input", "aparte-elic-text");
|
|
6410
6881
|
if (!field.multiline) input.type = "text";
|
|
6411
6882
|
if (field.placeholder) input.setAttribute("placeholder", field.placeholder);
|
|
6412
6883
|
if (field.default) input.value = field.default;
|
|
6413
6884
|
if (field.maxLength != null) input.setAttribute("maxlength", String(field.maxLength));
|
|
6414
|
-
input.setAttribute("aria-label", field.title ?? field.description ??
|
|
6885
|
+
input.setAttribute("aria-label", field.title ?? field.description ?? fallbackLabel ?? contextConfig().t("elicitationAnswerLabel"));
|
|
6415
6886
|
wrap.appendChild(input);
|
|
6416
6887
|
input.addEventListener("input", onChange);
|
|
6417
6888
|
const required = field.required ?? true;
|
|
@@ -6427,96 +6898,182 @@ function buildStringField(field, onChange) {
|
|
|
6427
6898
|
focus: () => input.focus()
|
|
6428
6899
|
};
|
|
6429
6900
|
}
|
|
6430
|
-
function buildField(field, onChange) {
|
|
6901
|
+
function buildField(field, onChange, fallbackLabel, key) {
|
|
6902
|
+
const custom = contextConfig().getElicitationFieldRenderer()?.(field, { key, notifyChange: onChange });
|
|
6903
|
+
if (custom) {
|
|
6904
|
+
return {
|
|
6905
|
+
el: custom.el,
|
|
6906
|
+
getValue: () => custom.getValue(),
|
|
6907
|
+
isComplete: () => custom.isComplete(),
|
|
6908
|
+
focus: () => custom.focus?.()
|
|
6909
|
+
};
|
|
6910
|
+
}
|
|
6431
6911
|
switch (field.type) {
|
|
6432
6912
|
case "enum":
|
|
6433
|
-
return buildEnumField(field, onChange);
|
|
6913
|
+
return buildEnumField(field, onChange, fallbackLabel);
|
|
6434
6914
|
case "boolean":
|
|
6435
|
-
return buildBooleanField(field, onChange);
|
|
6915
|
+
return buildBooleanField(field, onChange, fallbackLabel);
|
|
6436
6916
|
case "string":
|
|
6437
|
-
return buildStringField(field, onChange);
|
|
6917
|
+
return buildStringField(field, onChange, fallbackLabel);
|
|
6438
6918
|
}
|
|
6439
6919
|
}
|
|
6440
6920
|
function buildElicitationPanel(message, schema, onChange) {
|
|
6441
6921
|
const panel = el("div", "aparte-elic-panel");
|
|
6442
6922
|
if (message) panel.appendChild(el("p", "aparte-elic-message", message));
|
|
6923
|
+
const body = el("div", "aparte-elic-body");
|
|
6924
|
+
panel.appendChild(body);
|
|
6925
|
+
const dismiss = el("div", "aparte-elic-dismiss");
|
|
6926
|
+
panel.appendChild(dismiss);
|
|
6443
6927
|
if (schema.type === "object") {
|
|
6928
|
+
let show = function(index) {
|
|
6929
|
+
current = index;
|
|
6930
|
+
fields.forEach((f, i) => {
|
|
6931
|
+
f.field.el.hidden = i !== index;
|
|
6932
|
+
});
|
|
6933
|
+
syncNav();
|
|
6934
|
+
};
|
|
6444
6935
|
const entries = Object.entries(schema.properties);
|
|
6445
6936
|
const requiredKeys = new Set(schema.required ?? entries.map(([k]) => k));
|
|
6937
|
+
const cfg = contextConfig();
|
|
6938
|
+
let syncNav = () => {
|
|
6939
|
+
};
|
|
6940
|
+
const notify = () => {
|
|
6941
|
+
syncNav();
|
|
6942
|
+
onChange();
|
|
6943
|
+
};
|
|
6446
6944
|
const fields = entries.map(([key, field2]) => {
|
|
6447
|
-
const built = buildField(field2,
|
|
6945
|
+
const built = buildField(field2, notify, void 0, key);
|
|
6448
6946
|
if (!field2.title && !built.el.querySelector(".aparte-elic-title")) {
|
|
6449
6947
|
built.el.insertBefore(el("p", "aparte-elic-title", key), built.el.firstChild);
|
|
6450
6948
|
}
|
|
6451
|
-
|
|
6452
|
-
return { key, field: built, required: requiredKeys.has(key) };
|
|
6949
|
+
body.appendChild(built.el);
|
|
6950
|
+
return { key, field: built, required: requiredKeys.has(key), header: field2.header };
|
|
6453
6951
|
});
|
|
6454
|
-
|
|
6952
|
+
const isComplete = () => fields.every((f) => !f.required || f.field.isComplete());
|
|
6953
|
+
const api = {
|
|
6455
6954
|
el: panel,
|
|
6955
|
+
dismiss,
|
|
6456
6956
|
getContent: () => Object.fromEntries(fields.map((f) => [f.key, f.field.getValue()])),
|
|
6457
|
-
isComplete
|
|
6458
|
-
focus: () => fields[0]?.field.focus()
|
|
6957
|
+
isComplete,
|
|
6958
|
+
focus: () => fields[0]?.field.focus(),
|
|
6959
|
+
// A form of one behaves like a single field; only a real form steps.
|
|
6960
|
+
mode: () => "submit",
|
|
6961
|
+
canProceed: isComplete,
|
|
6962
|
+
proceed: () => {
|
|
6963
|
+
}
|
|
6964
|
+
};
|
|
6965
|
+
if (fields.length < 2 || cfg.getElicitationOptions().layout !== "stepped") return api;
|
|
6966
|
+
panel.classList.add("aparte-elic-panel--stepped");
|
|
6967
|
+
let current = 0;
|
|
6968
|
+
const steps = el("div", "aparte-elic-steps");
|
|
6969
|
+
steps.setAttribute("role", "tablist");
|
|
6970
|
+
const chips = fields.map((f, i) => {
|
|
6971
|
+
const chip = el("button", "aparte-elic-step");
|
|
6972
|
+
chip.type = "button";
|
|
6973
|
+
chip.setAttribute("role", "tab");
|
|
6974
|
+
chip.textContent = f.header?.trim() || String(i + 1);
|
|
6975
|
+
chip.addEventListener("click", () => {
|
|
6976
|
+
show(i);
|
|
6977
|
+
f.field.focus();
|
|
6978
|
+
});
|
|
6979
|
+
steps.appendChild(chip);
|
|
6980
|
+
return chip;
|
|
6981
|
+
});
|
|
6982
|
+
panel.insertBefore(steps, body);
|
|
6983
|
+
syncNav = () => {
|
|
6984
|
+
chips.forEach((chip, i) => {
|
|
6985
|
+
chip.setAttribute("aria-selected", String(i === current));
|
|
6986
|
+
chip.toggleAttribute("data-answered", fields[i].field.isComplete());
|
|
6987
|
+
});
|
|
6988
|
+
};
|
|
6989
|
+
show(0);
|
|
6990
|
+
const last = () => current === fields.length - 1;
|
|
6991
|
+
return {
|
|
6992
|
+
...api,
|
|
6993
|
+
focus: () => fields[current]?.field.focus(),
|
|
6994
|
+
mode: () => last() ? "submit" : "advance",
|
|
6995
|
+
// Advancing needs only THIS question answered; submitting needs them all,
|
|
6996
|
+
// which is what stops the last question from accepting a form with a hole
|
|
6997
|
+
// in it.
|
|
6998
|
+
canProceed: () => last() ? isComplete() : !!fields[current]?.field.isComplete(),
|
|
6999
|
+
proceed: () => {
|
|
7000
|
+
if (last()) return;
|
|
7001
|
+
show(current + 1);
|
|
7002
|
+
fields[current]?.field.focus();
|
|
7003
|
+
}
|
|
6459
7004
|
};
|
|
6460
7005
|
}
|
|
6461
|
-
const field = buildField(schema, onChange);
|
|
6462
|
-
|
|
7006
|
+
const field = buildField(schema, onChange, message);
|
|
7007
|
+
body.appendChild(field.el);
|
|
6463
7008
|
return {
|
|
6464
7009
|
el: panel,
|
|
7010
|
+
dismiss,
|
|
6465
7011
|
getContent: () => field.getValue(),
|
|
6466
7012
|
isComplete: () => field.isComplete(),
|
|
6467
|
-
focus: () => field.focus()
|
|
7013
|
+
focus: () => field.focus(),
|
|
7014
|
+
mode: () => "submit",
|
|
7015
|
+
canProceed: () => field.isComplete(),
|
|
7016
|
+
proceed: () => {
|
|
7017
|
+
}
|
|
6468
7018
|
};
|
|
6469
7019
|
}
|
|
6470
7020
|
function requestUserInput(request) {
|
|
6471
7021
|
return resolveConfig(request.target ?? null).requestUserInput(request);
|
|
6472
7022
|
}
|
|
6473
7023
|
export {
|
|
7024
|
+
uuid as $,
|
|
6474
7025
|
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
|
-
|
|
7026
|
+
createStreamAdapter as B,
|
|
7027
|
+
cssEscape as C,
|
|
7028
|
+
declineDefaultRenderers as D,
|
|
7029
|
+
defaultSanitizer as E,
|
|
7030
|
+
deriveArtifactKind as F,
|
|
7031
|
+
detachConfig as G,
|
|
7032
|
+
escapeAttr as H,
|
|
7033
|
+
escapeHtml as I,
|
|
7034
|
+
filesToAttachments as J,
|
|
7035
|
+
getAllRenderers as K,
|
|
7036
|
+
getSegmentRenderer as L,
|
|
7037
|
+
installDefaultRenderersOnce as M,
|
|
7038
|
+
isAwaitingReply as N,
|
|
7039
|
+
isFormatAdapter as O,
|
|
7040
|
+
isSafeUrl as P,
|
|
7041
|
+
parseAparteEventStream as Q,
|
|
7042
|
+
parseMarkdownToSegments as R,
|
|
7043
|
+
populateBubbleFromMessage as S,
|
|
7044
|
+
readableToAsyncIterable as T,
|
|
7045
|
+
registerDefaultRenderers as U,
|
|
7046
|
+
registerSegmentRenderer as V,
|
|
7047
|
+
requestUserInput as W,
|
|
7048
|
+
resolveConfig as X,
|
|
7049
|
+
revokeAttachmentUrls as Y,
|
|
7050
|
+
runWithConfig as Z,
|
|
7051
|
+
unregisterSegmentRenderer as _,
|
|
7052
|
+
APARTE_DEFAULT_BUBBLE_ACTIONS as a,
|
|
7053
|
+
APARTE_DEFAULT_HOST_HANDLERS as b,
|
|
7054
|
+
APARTE_DEFAULT_ICON_FALLBACKS as c,
|
|
7055
|
+
APARTE_DEFAULT_LOCALE as d,
|
|
7056
|
+
APARTE_DEFAULT_SKELETON_FALLBACKS as e,
|
|
7057
|
+
APARTE_DEFAULT_UI_EVENTS as f,
|
|
7058
|
+
APARTE_HOST_ATTR as g,
|
|
7059
|
+
AparteBackendTransport as h,
|
|
7060
|
+
AparteChatHost as i,
|
|
7061
|
+
AparteClient as j,
|
|
7062
|
+
AparteConfig as k,
|
|
7063
|
+
AparteConversationController as l,
|
|
7064
|
+
AparteConversationManager as m,
|
|
7065
|
+
AparteDirectTransport as n,
|
|
7066
|
+
AparteError as o,
|
|
7067
|
+
AparteErrorCode as p,
|
|
7068
|
+
AparteMessageRepository as q,
|
|
7069
|
+
AparteStreamParser as r,
|
|
7070
|
+
aparteGlobalConfig as s,
|
|
7071
|
+
applyElementProps as t,
|
|
7072
|
+
attachConfig as u,
|
|
7073
|
+
buildElicitationPanel as v,
|
|
7074
|
+
collectRendererStyles as w,
|
|
7075
|
+
contentToText as x,
|
|
7076
|
+
contextConfig as y,
|
|
7077
|
+
createAparteChatHandler as z
|
|
6521
7078
|
};
|
|
6522
|
-
//# sourceMappingURL=index-
|
|
7079
|
+
//# sourceMappingURL=index-BieFbKeW.js.map
|