@smooai/chat-widget 0.12.0 → 0.13.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 +1 -0
- package/dist/chat-widget.global.js +205 -12
- package/dist/chat-widget.global.js.map +1 -1
- package/dist/index.d.ts +72 -1
- package/dist/index.d.ts.map +1 -1
- package/dist/index.js +205 -12
- package/dist/index.js.map +1 -1
- package/package.json +1 -1
- package/src/config.ts +11 -0
- package/src/conversation.ts +93 -1
- package/src/element.ts +138 -14
- package/src/index.ts +2 -0
- package/src/styles.ts +42 -0
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@smooai/chat-widget",
|
|
3
|
-
"version": "0.
|
|
3
|
+
"version": "0.13.0",
|
|
4
4
|
"description": "Embeddable AI chat as a framework-light web component — the Aurora Glass design, streaming replies, grounded sources, and per-brand theming. Speaks the smooth-operator WebSocket protocol.",
|
|
5
5
|
"license": "MIT",
|
|
6
6
|
"author": "SmooAI",
|
package/src/config.ts
CHANGED
|
@@ -141,6 +141,16 @@ export interface ChatWidgetConfig {
|
|
|
141
141
|
* `require*` flags are ignored and the pre-chat form is skipped.
|
|
142
142
|
*/
|
|
143
143
|
allowAnonymous?: boolean;
|
|
144
|
+
/**
|
|
145
|
+
* Show the agent's tool activity (grep / read_file / bash / knowledge_search…)
|
|
146
|
+
* as inline chips interleaved with its prose, mirroring the smooth daemon SPA.
|
|
147
|
+
*
|
|
148
|
+
* Defaults to **`false`**: for a customer-facing support widget, surfacing raw
|
|
149
|
+
* tool calls to an end-user is usually undesirable, so tool activity is hidden
|
|
150
|
+
* and only the assistant's prose renders. Enable it for internal / power-user
|
|
151
|
+
* surfaces where seeing what the agent did mid-turn is valuable.
|
|
152
|
+
*/
|
|
153
|
+
showToolActivity?: boolean;
|
|
144
154
|
/** Theme overrides. */
|
|
145
155
|
theme?: ChatWidgetTheme;
|
|
146
156
|
}
|
|
@@ -194,6 +204,7 @@ export function resolveConfig(config: ChatWidgetConfig): ResolvedConfig {
|
|
|
194
204
|
collectConsent: config.collectConsent ?? true,
|
|
195
205
|
allowChatRestore: config.allowChatRestore ?? true,
|
|
196
206
|
allowAnonymous: config.allowAnonymous ?? false,
|
|
207
|
+
showToolActivity: config.showToolActivity ?? false,
|
|
197
208
|
theme: {
|
|
198
209
|
text: theme.text ?? '#f8fafc',
|
|
199
210
|
background: theme.background ?? '#040d30',
|
package/src/conversation.ts
CHANGED
|
@@ -61,6 +61,32 @@ export type { Citation };
|
|
|
61
61
|
|
|
62
62
|
export type Role = 'user' | 'assistant';
|
|
63
63
|
|
|
64
|
+
/**
|
|
65
|
+
* One tool invocation within an assistant turn. Mirrors the smooth daemon SPA's
|
|
66
|
+
* `ToolCall` (`crates/smooth-web/web/src/operator.ts`): opens `done: false` on the
|
|
67
|
+
* tool call and resolves on the tool result.
|
|
68
|
+
*/
|
|
69
|
+
export interface ToolCall {
|
|
70
|
+
/** Stable id for keyed rendering (assigned when the call opens). */
|
|
71
|
+
id: string;
|
|
72
|
+
name: string;
|
|
73
|
+
/** Raw arguments, JSON-stringified. */
|
|
74
|
+
args: string;
|
|
75
|
+
/** Present once the tool resolves. */
|
|
76
|
+
result?: string;
|
|
77
|
+
isError?: boolean;
|
|
78
|
+
done: boolean;
|
|
79
|
+
}
|
|
80
|
+
|
|
81
|
+
/**
|
|
82
|
+
* One ordered segment of an assistant turn: a run of prose, or a tool call.
|
|
83
|
+
* Preserves the interleave order the model produced (say a bit → call a tool →
|
|
84
|
+
* say a bit → …) so the UI can render tool chips INLINE where the model called
|
|
85
|
+
* them. Mirrors the daemon SPA's `MessageBlock`. Only populated when the widget
|
|
86
|
+
* is configured with `showToolActivity: true`.
|
|
87
|
+
*/
|
|
88
|
+
export type MessageBlock = { kind: 'text'; text: string } | { kind: 'tool'; tool: ToolCall };
|
|
89
|
+
|
|
64
90
|
export interface ChatMessage {
|
|
65
91
|
id: string;
|
|
66
92
|
role: Role;
|
|
@@ -68,6 +94,12 @@ export interface ChatMessage {
|
|
|
68
94
|
text: string;
|
|
69
95
|
/** True while an assistant message is still streaming. */
|
|
70
96
|
streaming: boolean;
|
|
97
|
+
/**
|
|
98
|
+
* Ordered text + tool segments, interleaved as the model produced them. Present
|
|
99
|
+
* only on assistant messages when `showToolActivity` is enabled (absent
|
|
100
|
+
* otherwise — the default popover renders `text` alone, byte-for-byte unchanged).
|
|
101
|
+
*/
|
|
102
|
+
blocks?: MessageBlock[];
|
|
71
103
|
/**
|
|
72
104
|
* Sources that grounded an assistant answer, when the terminal
|
|
73
105
|
* `eventual_response` carried any. Optional + back-compatible: absent when
|
|
@@ -244,6 +276,52 @@ function wireMessageToChat(m: WireMessage, idx: number): ChatMessage | null {
|
|
|
244
276
|
return { id: typeof m.id === 'string' ? m.id : `hist-${idx}`, role, text, streaming: false };
|
|
245
277
|
}
|
|
246
278
|
|
|
279
|
+
let toolSeq = 0;
|
|
280
|
+
const nextToolId = (): string => `tool-${++toolSeq}`;
|
|
281
|
+
|
|
282
|
+
/** Grow the trailing text block, or open a new one if the last block was a tool. */
|
|
283
|
+
function growTextBlock(blocks: MessageBlock[], text: string): void {
|
|
284
|
+
if (!text) return;
|
|
285
|
+
const last = blocks[blocks.length - 1];
|
|
286
|
+
if (last && last.kind === 'text') last.text += text;
|
|
287
|
+
else blocks.push({ kind: 'text', text });
|
|
288
|
+
}
|
|
289
|
+
|
|
290
|
+
/**
|
|
291
|
+
* Fold a `stream_chunk` node-state into the ordered block list, returning `true`
|
|
292
|
+
* when the chunk carried tool activity.
|
|
293
|
+
*
|
|
294
|
+
* Tool activity rides `state.rawResponse.toolCall` / `state.rawResponse.toolResult`
|
|
295
|
+
* — **NOT** `state.toolResult`. Reading the wrong path leaves every chip stuck on
|
|
296
|
+
* "running…" forever (the exact bug the daemon SPA hit and this mirror avoids).
|
|
297
|
+
*/
|
|
298
|
+
function applyToolChunk(blocks: MessageBlock[], state: unknown): boolean {
|
|
299
|
+
const raw = (state as { rawResponse?: unknown } | null | undefined)?.rawResponse;
|
|
300
|
+
if (!raw || typeof raw !== 'object') return false;
|
|
301
|
+
const call = (raw as { toolCall?: { name?: string; arguments?: unknown } }).toolCall;
|
|
302
|
+
const res = (raw as { toolResult?: { name?: string; isError?: boolean; result?: unknown } }).toolResult;
|
|
303
|
+
if (call) {
|
|
304
|
+
const args = typeof call.arguments === 'string' ? call.arguments : JSON.stringify(call.arguments ?? {});
|
|
305
|
+
blocks.push({ kind: 'tool', tool: { id: nextToolId(), name: call.name ?? '', args, done: false } });
|
|
306
|
+
return true;
|
|
307
|
+
}
|
|
308
|
+
if (res) {
|
|
309
|
+
const result = typeof res.result === 'string' ? res.result : JSON.stringify(res.result ?? '');
|
|
310
|
+
// Complete the most-recent still-open tool block matching this name.
|
|
311
|
+
for (let i = blocks.length - 1; i >= 0; i--) {
|
|
312
|
+
const b = blocks[i];
|
|
313
|
+
if (b && b.kind === 'tool' && b.tool.name === (res.name ?? '') && !b.tool.done) {
|
|
314
|
+
b.tool.done = true;
|
|
315
|
+
b.tool.isError = !!res.isError;
|
|
316
|
+
b.tool.result = result;
|
|
317
|
+
break;
|
|
318
|
+
}
|
|
319
|
+
}
|
|
320
|
+
return true;
|
|
321
|
+
}
|
|
322
|
+
return false;
|
|
323
|
+
}
|
|
324
|
+
|
|
247
325
|
export class ConversationController {
|
|
248
326
|
private readonly config: ChatWidgetConfig;
|
|
249
327
|
private readonly events: ConversationEvents;
|
|
@@ -674,7 +752,8 @@ export class ConversationController {
|
|
|
674
752
|
this.messages.push({ id: this.nextId('u'), role: 'user', text: trimmed, streaming: false });
|
|
675
753
|
|
|
676
754
|
// 2. Placeholder assistant bubble we grow as tokens arrive.
|
|
677
|
-
const
|
|
755
|
+
const showTools = this.config.showToolActivity === true;
|
|
756
|
+
const assistant: ChatMessage = { id: this.nextId('a'), role: 'assistant', text: '', streaming: true, blocks: showTools ? [] : undefined };
|
|
678
757
|
this.messages.push(assistant);
|
|
679
758
|
this.emitMessages();
|
|
680
759
|
|
|
@@ -687,6 +766,14 @@ export class ConversationController {
|
|
|
687
766
|
const token = event.token ?? event.data?.token ?? '';
|
|
688
767
|
if (token) {
|
|
689
768
|
assistant.text += token;
|
|
769
|
+
// Grow the trailing text block so prose interleaves with any
|
|
770
|
+
// tool chips in the order the model produced them.
|
|
771
|
+
if (showTools && assistant.blocks) growTextBlock(assistant.blocks, token);
|
|
772
|
+
this.emitMessages();
|
|
773
|
+
}
|
|
774
|
+
} else if (showTools && event.type === 'stream_chunk') {
|
|
775
|
+
// Tool activity (gated). Read state.rawResponse.toolCall/.toolResult.
|
|
776
|
+
if (assistant.blocks && applyToolChunk(assistant.blocks, event.data?.state)) {
|
|
690
777
|
this.emitMessages();
|
|
691
778
|
}
|
|
692
779
|
} else {
|
|
@@ -715,6 +802,11 @@ export class ConversationController {
|
|
|
715
802
|
if (suggestions.length > 0) {
|
|
716
803
|
assistant.suggestions = suggestions;
|
|
717
804
|
}
|
|
805
|
+
// Only keep blocks for turns that actually invoked a tool — a prose-only
|
|
806
|
+
// turn drops back to the normal markdown text path (with the final text).
|
|
807
|
+
if (assistant.blocks && !assistant.blocks.some((b) => b.kind === 'tool')) {
|
|
808
|
+
assistant.blocks = undefined;
|
|
809
|
+
}
|
|
718
810
|
assistant.streaming = false;
|
|
719
811
|
this.emitMessages();
|
|
720
812
|
} catch (err) {
|
package/src/element.ts
CHANGED
|
@@ -17,7 +17,16 @@
|
|
|
17
17
|
import { AsYouType, isValidPhoneNumber, parsePhoneNumber } from 'libphonenumber-js/min';
|
|
18
18
|
import type { ChatWidgetConfig, ChatWidgetMode, ChatWidgetTheme } from './config.js';
|
|
19
19
|
import { needsUserInfo, resolveConfig } from './config.js';
|
|
20
|
-
import {
|
|
20
|
+
import {
|
|
21
|
+
type ChatMessage,
|
|
22
|
+
type Citation,
|
|
23
|
+
type ConnectionStatus,
|
|
24
|
+
ConversationController,
|
|
25
|
+
type IdentityRestore,
|
|
26
|
+
type Interrupt,
|
|
27
|
+
SUPPORTED_INTERACTION_CAPABILITIES,
|
|
28
|
+
type ToolCall,
|
|
29
|
+
} from './conversation.js';
|
|
21
30
|
import { SMOOTH_ICON_SVG } from './logo.js';
|
|
22
31
|
import { cleanCitationSnippet, escapeHtml, renderMarkdown, safeHttpUrl } from './markdown.js';
|
|
23
32
|
import { buildStyles } from './styles.js';
|
|
@@ -52,7 +61,7 @@ function phoneToE164(value: string): string | null {
|
|
|
52
61
|
/** Public smooth-operator repo — the "powered by" header tag + footer link here. */
|
|
53
62
|
const SMOOTH_OPERATOR_URL = 'https://github.com/SmooAI/smooth-operator';
|
|
54
63
|
|
|
55
|
-
const OBSERVED = ['endpoint', 'agent-id', 'agent-name', 'logo-url', 'placeholder', 'greeting', 'start-open', 'mode', 'hide-branding'] as const;
|
|
64
|
+
const OBSERVED = ['endpoint', 'agent-id', 'agent-name', 'logo-url', 'placeholder', 'greeting', 'start-open', 'mode', 'hide-branding', 'show-tool-activity'] as const;
|
|
56
65
|
|
|
57
66
|
/**
|
|
58
67
|
* Inline SVG icons (static, trusted strings — never interpolated with user data).
|
|
@@ -75,6 +84,8 @@ const ICON = {
|
|
|
75
84
|
user: `<svg viewBox="0 0 24 24" fill="none" xmlns="http://www.w3.org/2000/svg"><circle cx="12" cy="8.2" r="3.4" stroke="currentColor" stroke-width="1.7"/><path d="M5.5 19.5c.8-3.1 3.4-4.8 6.5-4.8s5.7 1.7 6.5 4.8" stroke="currentColor" stroke-width="1.7" stroke-linecap="round"/></svg>`,
|
|
76
85
|
/** Tool-confirmation interrupt — a shield. */
|
|
77
86
|
shield: `<svg viewBox="0 0 24 24" fill="none" xmlns="http://www.w3.org/2000/svg"><path d="M12 3 5 6v5c0 4.4 3 7.2 7 8.5 4-1.3 7-4.1 7-8.5V6l-7-3Z" stroke="currentColor" stroke-width="1.7" stroke-linejoin="round"/><path d="m9 11.5 2 2 4-4" stroke="currentColor" stroke-width="1.7" stroke-linecap="round" stroke-linejoin="round"/></svg>`,
|
|
87
|
+
/** Tool-activity chip — a wrench. */
|
|
88
|
+
tool: `<svg viewBox="0 0 24 24" fill="none" xmlns="http://www.w3.org/2000/svg"><path d="M14.7 6.3a3.5 3.5 0 0 0-4.6 4.3l-5 5a1.6 1.6 0 0 0 2.3 2.3l5-5a3.5 3.5 0 0 0 4.3-4.6l-2 2-1.7-.3-.3-1.7 2-2Z" stroke="currentColor" stroke-width="1.5" stroke-linejoin="round"/></svg>`,
|
|
78
89
|
} as const;
|
|
79
90
|
|
|
80
91
|
/**
|
|
@@ -292,6 +303,10 @@ export class SmoothAgentChatElement extends HTMLElement {
|
|
|
292
303
|
/** How many chars of {@link streamTarget} are currently shown. */
|
|
293
304
|
private displayedLength = 0;
|
|
294
305
|
private rafId = 0;
|
|
306
|
+
/** Block structure signature of the last-rendered streaming message (tool chips
|
|
307
|
+
* only — text growth doesn't change it), so a chip add/resolve forces a rebuild
|
|
308
|
+
* while plain trailing-text growth stays on the fast reveal path. */
|
|
309
|
+
private prevBlockSig = '';
|
|
295
310
|
|
|
296
311
|
constructor() {
|
|
297
312
|
super();
|
|
@@ -376,6 +391,7 @@ export class SmoothAgentChatElement extends HTMLElement {
|
|
|
376
391
|
collectConsent: this.overrides.collectConsent,
|
|
377
392
|
allowChatRestore: this.overrides.allowChatRestore,
|
|
378
393
|
allowAnonymous: this.overrides.allowAnonymous,
|
|
394
|
+
showToolActivity: this.overrides.showToolActivity ?? this.hasAttribute('show-tool-activity'),
|
|
379
395
|
theme,
|
|
380
396
|
};
|
|
381
397
|
}
|
|
@@ -1046,14 +1062,19 @@ export class SmoothAgentChatElement extends HTMLElement {
|
|
|
1046
1062
|
// finalize transition (streaming → done) needs the markdown render + sources
|
|
1047
1063
|
last.streaming !== prevLast.streaming ||
|
|
1048
1064
|
// first token after the typing indicator needs the bubble swapped in
|
|
1049
|
-
(!!last.streaming && !prevLast.text && !!last.text)
|
|
1065
|
+
(!!last.streaming && !prevLast.text && !!last.text) ||
|
|
1066
|
+
// a tool chip was added or resolved (text growth alone doesn't change this)
|
|
1067
|
+
this.blockSig(last) !== this.prevBlockSig;
|
|
1050
1068
|
|
|
1051
1069
|
this.messages = messages;
|
|
1052
|
-
|
|
1053
|
-
|
|
1054
|
-
|
|
1055
|
-
//
|
|
1056
|
-
|
|
1070
|
+
this.prevBlockSig = this.blockSig(last);
|
|
1071
|
+
|
|
1072
|
+
if (!structural && last && last.streaming && this.tailKey(last) === this.streamMsgId) {
|
|
1073
|
+
// Fast path: the streaming (trailing) text just grew. Bump the reveal
|
|
1074
|
+
// target and let the rAF loop catch up — no DOM rebuild, no per-token
|
|
1075
|
+
// reflow. `tailText` is the live trailing text block for a tool turn, or
|
|
1076
|
+
// the whole message for a plain-prose turn.
|
|
1077
|
+
this.streamTarget = this.tailText(last);
|
|
1057
1078
|
this.ensureRevealLoop();
|
|
1058
1079
|
return;
|
|
1059
1080
|
}
|
|
@@ -1061,6 +1082,32 @@ export class SmoothAgentChatElement extends HTMLElement {
|
|
|
1061
1082
|
this.renderMessages();
|
|
1062
1083
|
}
|
|
1063
1084
|
|
|
1085
|
+
/** True when an assistant message's turn invoked at least one tool. */
|
|
1086
|
+
private hasToolBlocks(m?: ChatMessage): boolean {
|
|
1087
|
+
return !!m?.blocks?.some((b) => b.kind === 'tool');
|
|
1088
|
+
}
|
|
1089
|
+
|
|
1090
|
+
/** Signature that changes only when a tool chip is added or resolved (text growth is 'x'). */
|
|
1091
|
+
private blockSig(m?: ChatMessage): string {
|
|
1092
|
+
if (!m?.blocks) return '';
|
|
1093
|
+
return m.blocks.map((b) => (b.kind === 'tool' ? `t:${b.tool.id}:${b.tool.done ? 1 : 0}` : 'x')).join('|');
|
|
1094
|
+
}
|
|
1095
|
+
|
|
1096
|
+
/** The live (last) text block for a tool turn, else the whole message text. */
|
|
1097
|
+
private tailText(m: ChatMessage): string {
|
|
1098
|
+
if (this.hasToolBlocks(m) && m.blocks) {
|
|
1099
|
+
const last = m.blocks[m.blocks.length - 1];
|
|
1100
|
+
return last?.kind === 'text' ? last.text : '';
|
|
1101
|
+
}
|
|
1102
|
+
return m.text;
|
|
1103
|
+
}
|
|
1104
|
+
|
|
1105
|
+
/** Reveal-binding key — composite for a tool-turn tail (so a new trailing block rebinds), else msg id. */
|
|
1106
|
+
private tailKey(m: ChatMessage): string {
|
|
1107
|
+
if (this.hasToolBlocks(m) && m.blocks) return `${m.id}#${m.blocks.length - 1}`;
|
|
1108
|
+
return m.id;
|
|
1109
|
+
}
|
|
1110
|
+
|
|
1064
1111
|
private renderMessages(): void {
|
|
1065
1112
|
if (!this.messagesEl) return;
|
|
1066
1113
|
this.resetReveal();
|
|
@@ -1087,6 +1134,16 @@ export class SmoothAgentChatElement extends HTMLElement {
|
|
|
1087
1134
|
}
|
|
1088
1135
|
|
|
1089
1136
|
for (const msg of this.messages) {
|
|
1137
|
+
// Tool-activity turns (gated by `showToolActivity`) render as an ordered
|
|
1138
|
+
// strip of prose bubbles + inline tool chips instead of one bubble.
|
|
1139
|
+
if (msg.role === 'assistant' && this.hasToolBlocks(msg)) {
|
|
1140
|
+
this.messagesEl.appendChild(this.buildRow('assistant', this.renderAssistantBlocks(msg)));
|
|
1141
|
+
if (!msg.streaming && msg.citations && msg.citations.length > 0) {
|
|
1142
|
+
this.messagesEl.appendChild(this.renderSources(msg.citations));
|
|
1143
|
+
}
|
|
1144
|
+
continue;
|
|
1145
|
+
}
|
|
1146
|
+
|
|
1090
1147
|
const bubble = document.createElement('div');
|
|
1091
1148
|
bubble.className = `bubble ${msg.role}`;
|
|
1092
1149
|
if (msg.role === 'assistant' && msg.streaming && !msg.text) {
|
|
@@ -1168,22 +1225,89 @@ export class SmoothAgentChatElement extends HTMLElement {
|
|
|
1168
1225
|
* doesn't restart the reveal from zero), then resumes the loop.
|
|
1169
1226
|
*/
|
|
1170
1227
|
private bindReveal(msg: ChatMessage, bubble: HTMLElement): void {
|
|
1171
|
-
|
|
1228
|
+
// `tailKey`/`tailText` collapse to the message id + full text for a plain
|
|
1229
|
+
// prose turn, and to the live trailing text block for a tool turn — so both
|
|
1230
|
+
// the single streaming bubble and a tool turn's trailing prose share this path.
|
|
1231
|
+
const key = this.tailKey(msg);
|
|
1232
|
+
const target = this.tailText(msg);
|
|
1233
|
+
const carryOver = key === this.streamMsgId ? Math.min(this.displayedLength, target.length) : 0;
|
|
1172
1234
|
this.streamBubbleEl = bubble;
|
|
1173
|
-
this.streamMsgId =
|
|
1174
|
-
this.streamTarget =
|
|
1235
|
+
this.streamMsgId = key;
|
|
1236
|
+
this.streamTarget = target;
|
|
1175
1237
|
this.displayedLength = carryOver;
|
|
1176
1238
|
|
|
1177
1239
|
if (this.prefersReducedMotion()) {
|
|
1178
1240
|
// No animation: show everything immediately.
|
|
1179
|
-
this.displayedLength =
|
|
1180
|
-
bubble.textContent =
|
|
1241
|
+
this.displayedLength = target.length;
|
|
1242
|
+
bubble.textContent = target;
|
|
1181
1243
|
return;
|
|
1182
1244
|
}
|
|
1183
|
-
bubble.textContent =
|
|
1245
|
+
bubble.textContent = target.slice(0, this.displayedLength);
|
|
1184
1246
|
this.ensureRevealLoop();
|
|
1185
1247
|
}
|
|
1186
1248
|
|
|
1249
|
+
/**
|
|
1250
|
+
* Render a tool-activity assistant turn as an ordered strip: prose bubbles and
|
|
1251
|
+
* inline tool chips in the order the model produced them (mirrors the daemon
|
|
1252
|
+
* SPA's `blocks`). The live trailing text block (while streaming) binds to the
|
|
1253
|
+
* rAF reveal; earlier/finalized text blocks render as sanitized markdown.
|
|
1254
|
+
*/
|
|
1255
|
+
private renderAssistantBlocks(msg: ChatMessage): HTMLElement {
|
|
1256
|
+
const wrap = document.createElement('div');
|
|
1257
|
+
wrap.className = 'blocks';
|
|
1258
|
+
const blocks = msg.blocks ?? [];
|
|
1259
|
+
const lastIdx = blocks.length - 1;
|
|
1260
|
+
blocks.forEach((block, i) => {
|
|
1261
|
+
if (block.kind === 'tool') {
|
|
1262
|
+
wrap.appendChild(this.buildToolChip(block.tool));
|
|
1263
|
+
return;
|
|
1264
|
+
}
|
|
1265
|
+
const bubble = document.createElement('div');
|
|
1266
|
+
bubble.className = 'bubble assistant';
|
|
1267
|
+
if (msg.streaming && i === lastIdx) {
|
|
1268
|
+
// Live trailing prose → plain text + cursor, driven by the reveal loop.
|
|
1269
|
+
bubble.classList.add('cursor');
|
|
1270
|
+
this.bindReveal(msg, bubble);
|
|
1271
|
+
} else {
|
|
1272
|
+
// Settled prose → sanitized markdown (same allowlisted renderer as bubbles).
|
|
1273
|
+
bubble.classList.add('md');
|
|
1274
|
+
bubble.innerHTML = renderMarkdown(block.text);
|
|
1275
|
+
}
|
|
1276
|
+
wrap.appendChild(bubble);
|
|
1277
|
+
});
|
|
1278
|
+
return wrap;
|
|
1279
|
+
}
|
|
1280
|
+
|
|
1281
|
+
/**
|
|
1282
|
+
* A single tool-activity chip: icon + tool name + status (running… / done / error),
|
|
1283
|
+
* with a truncated args preview. Tool name/args are set via `textContent` so a
|
|
1284
|
+
* tool payload can never inject markup.
|
|
1285
|
+
*/
|
|
1286
|
+
private buildToolChip(tool: ToolCall): HTMLElement {
|
|
1287
|
+
const chip = document.createElement('div');
|
|
1288
|
+
chip.className = `toolchip ${tool.done ? (tool.isError ? 'error' : 'done') : 'running'}`;
|
|
1289
|
+
chip.setAttribute('part', 'tool-chip');
|
|
1290
|
+
|
|
1291
|
+
const icon = document.createElement('span');
|
|
1292
|
+
icon.className = 'ti';
|
|
1293
|
+
icon.innerHTML = ICON.tool; // static, trusted
|
|
1294
|
+
const name = document.createElement('span');
|
|
1295
|
+
name.className = 'tn';
|
|
1296
|
+
name.textContent = tool.name || 'tool';
|
|
1297
|
+
const status = document.createElement('span');
|
|
1298
|
+
status.className = 'ts';
|
|
1299
|
+
status.textContent = tool.done ? (tool.isError ? 'error' : 'done') : 'running…';
|
|
1300
|
+
chip.append(icon, name, status);
|
|
1301
|
+
|
|
1302
|
+
if (tool.args && tool.args !== '{}' && tool.args !== '""') {
|
|
1303
|
+
const args = document.createElement('span');
|
|
1304
|
+
args.className = 'ta';
|
|
1305
|
+
args.textContent = tool.args.length > 80 ? `${tool.args.slice(0, 80)}…` : tool.args;
|
|
1306
|
+
chip.appendChild(args);
|
|
1307
|
+
}
|
|
1308
|
+
return chip;
|
|
1309
|
+
}
|
|
1310
|
+
|
|
1187
1311
|
/** Start the rAF loop if it isn't already running. */
|
|
1188
1312
|
private ensureRevealLoop(): void {
|
|
1189
1313
|
if (this.prefersReducedMotion() || typeof requestAnimationFrame !== 'function') {
|
package/src/index.ts
CHANGED
package/src/styles.ts
CHANGED
|
@@ -29,6 +29,7 @@ export function buildStyles(theme: ResolvedTheme, mode: ChatWidgetMode = 'popove
|
|
|
29
29
|
--sac-bg: ${theme.background};
|
|
30
30
|
--sac-primary: ${theme.primary};
|
|
31
31
|
--sac-primary-text: ${theme.primaryText};
|
|
32
|
+
--sac-secondary: ${theme.secondary};
|
|
32
33
|
--sac-assistant-bubble: ${theme.assistantBubble};
|
|
33
34
|
--sac-assistant-bubble-text: ${theme.assistantBubbleText};
|
|
34
35
|
--sac-user-bubble: ${theme.userBubble};
|
|
@@ -349,6 +350,47 @@ ${
|
|
|
349
350
|
}
|
|
350
351
|
@keyframes sac-blink { to { opacity: 0 } }
|
|
351
352
|
|
|
353
|
+
/* Interleaved tool-activity strip (gated by showToolActivity): prose bubbles
|
|
354
|
+
and inline tool chips stacked in the order the model produced them. */
|
|
355
|
+
.blocks { display: flex; flex-direction: column; align-items: flex-start; gap: 7px; }
|
|
356
|
+
.blocks .bubble { align-self: flex-start; }
|
|
357
|
+
.toolchip {
|
|
358
|
+
display: inline-flex;
|
|
359
|
+
align-items: center;
|
|
360
|
+
gap: 6px;
|
|
361
|
+
max-width: 100%;
|
|
362
|
+
padding: 5px 10px;
|
|
363
|
+
border-radius: 99px;
|
|
364
|
+
font-size: 12px;
|
|
365
|
+
line-height: 1.3;
|
|
366
|
+
color: color-mix(in srgb, var(--sac-text) 78%, transparent);
|
|
367
|
+
background: color-mix(in srgb, var(--sac-text) 6%, transparent);
|
|
368
|
+
border: 1px solid color-mix(in srgb, var(--sac-text) 12%, transparent);
|
|
369
|
+
animation: sac-msg-in .3s var(--sac-ease) both;
|
|
370
|
+
}
|
|
371
|
+
.toolchip .ti { display: inline-flex; flex: none; opacity: .8; }
|
|
372
|
+
.toolchip .ti svg { width: 13px; height: 13px; }
|
|
373
|
+
.toolchip .tn { font-weight: 600; letter-spacing: .01em; }
|
|
374
|
+
.toolchip .ts { opacity: .7; }
|
|
375
|
+
.toolchip .ta {
|
|
376
|
+
font-family: ui-monospace, SFMono-Regular, Menlo, monospace;
|
|
377
|
+
font-size: 11px;
|
|
378
|
+
opacity: .6;
|
|
379
|
+
overflow: hidden;
|
|
380
|
+
text-overflow: ellipsis;
|
|
381
|
+
white-space: nowrap;
|
|
382
|
+
min-width: 0;
|
|
383
|
+
}
|
|
384
|
+
.toolchip.running { border-color: color-mix(in srgb, var(--sac-primary) 45%, transparent); }
|
|
385
|
+
.toolchip.running .ts { color: var(--sac-primary); opacity: 1; animation: sac-typing 1.3s ease-in-out infinite; }
|
|
386
|
+
.toolchip.done .ts::before { content: '✓ '; }
|
|
387
|
+
.toolchip.error {
|
|
388
|
+
color: var(--sac-secondary);
|
|
389
|
+
border-color: color-mix(in srgb, var(--sac-secondary) 50%, transparent);
|
|
390
|
+
background: color-mix(in srgb, var(--sac-secondary) 10%, transparent);
|
|
391
|
+
}
|
|
392
|
+
.toolchip.error .ts::before { content: '! '; }
|
|
393
|
+
|
|
352
394
|
/* ─────────────── Rendered markdown (assistant bubbles / snippets) ─────────── */
|
|
353
395
|
/* The renderer (markdown.ts) emits a small allowlisted set of tags; these rules
|
|
354
396
|
keep them legible inside the tight Aurora-Glass bubble + citation card. */
|