@smooai/smooth-operator 0.1.0 → 0.2.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (81) hide show
  1. package/README.md +18 -4
  2. package/dist/react/components/SmoothChat.d.ts +35 -0
  3. package/dist/react/components/SmoothChat.d.ts.map +1 -0
  4. package/dist/react/components/SmoothChat.js +26 -0
  5. package/dist/react/components/SmoothChat.js.map +1 -0
  6. package/dist/react/components/parts.d.ts +30 -0
  7. package/dist/react/components/parts.d.ts.map +1 -0
  8. package/dist/react/components/parts.js +67 -0
  9. package/dist/react/components/parts.js.map +1 -0
  10. package/dist/react/index.d.ts +27 -0
  11. package/dist/react/index.d.ts.map +1 -0
  12. package/dist/react/index.js +26 -0
  13. package/dist/react/index.js.map +1 -0
  14. package/dist/react/provider.d.ts +27 -0
  15. package/dist/react/provider.d.ts.map +1 -0
  16. package/dist/react/provider.js +25 -0
  17. package/dist/react/provider.js.map +1 -0
  18. package/dist/react/response.d.ts +19 -0
  19. package/dist/react/response.d.ts.map +1 -0
  20. package/dist/react/response.js +53 -0
  21. package/dist/react/response.js.map +1 -0
  22. package/dist/react/theme.d.ts +64 -0
  23. package/dist/react/theme.d.ts.map +1 -0
  24. package/dist/react/theme.js +38 -0
  25. package/dist/react/theme.js.map +1 -0
  26. package/dist/react/types.d.ts +24 -0
  27. package/dist/react/types.d.ts.map +1 -0
  28. package/dist/react/types.js +2 -0
  29. package/dist/react/types.js.map +1 -0
  30. package/dist/react/use-conversation.d.ts +59 -0
  31. package/dist/react/use-conversation.d.ts.map +1 -0
  32. package/dist/react/use-conversation.js +154 -0
  33. package/dist/react/use-conversation.js.map +1 -0
  34. package/dist/widget/chat-widget.iife.js +1301 -0
  35. package/dist/widget/chat-widget.iife.js.map +1 -0
  36. package/dist/widget/config.d.ts +73 -0
  37. package/dist/widget/config.d.ts.map +1 -0
  38. package/dist/widget/config.js +30 -0
  39. package/dist/widget/config.js.map +1 -0
  40. package/dist/widget/conversation.d.ts +50 -0
  41. package/dist/widget/conversation.d.ts.map +1 -0
  42. package/dist/widget/conversation.js +173 -0
  43. package/dist/widget/conversation.js.map +1 -0
  44. package/dist/widget/element.d.ts +94 -0
  45. package/dist/widget/element.d.ts.map +1 -0
  46. package/dist/widget/element.js +377 -0
  47. package/dist/widget/element.js.map +1 -0
  48. package/dist/widget/index.d.ts +25 -0
  49. package/dist/widget/index.d.ts.map +1 -0
  50. package/dist/widget/index.js +24 -0
  51. package/dist/widget/index.js.map +1 -0
  52. package/dist/widget/logo.d.ts +9 -0
  53. package/dist/widget/logo.d.ts.map +1 -0
  54. package/dist/widget/logo.js +10 -0
  55. package/dist/widget/logo.js.map +1 -0
  56. package/dist/widget/standalone.d.ts +25 -0
  57. package/dist/widget/standalone.d.ts.map +1 -0
  58. package/dist/widget/standalone.js +15 -0
  59. package/dist/widget/standalone.js.map +1 -0
  60. package/dist/widget/styles.d.ts +12 -0
  61. package/dist/widget/styles.d.ts.map +1 -0
  62. package/dist/widget/styles.js +262 -0
  63. package/dist/widget/styles.js.map +1 -0
  64. package/package.json +44 -6
  65. package/src/react/components/SmoothChat.tsx +57 -0
  66. package/src/react/components/parts.tsx +141 -0
  67. package/src/react/index.ts +30 -0
  68. package/src/react/provider.tsx +40 -0
  69. package/src/react/response.ts +57 -0
  70. package/src/react/styles.css +231 -0
  71. package/src/react/theme.ts +92 -0
  72. package/src/react/types.ts +27 -0
  73. package/src/react/use-conversation.ts +203 -0
  74. package/src/widget/config.ts +102 -0
  75. package/src/widget/conversation.ts +212 -0
  76. package/src/widget/element.ts +420 -0
  77. package/src/widget/index.ts +37 -0
  78. package/src/widget/logo.ts +9 -0
  79. package/src/widget/smooth-logo.svg +32 -0
  80. package/src/widget/standalone.ts +33 -0
  81. package/src/widget/styles.ts +265 -0
@@ -0,0 +1,1301 @@
1
+ var SmoothAgentChat = (function(exports) {
2
+ Object.defineProperty(exports, Symbol.toStringTag, { value: "Module" });
3
+ //#region src/widget/config.ts
4
+ /** Resolve a partial config against the built-in defaults. */
5
+ function resolveConfig(config) {
6
+ const theme = config.theme ?? {};
7
+ const primary = theme.primary ?? "#00a6a6";
8
+ const primaryText = theme.primaryText ?? "#f8fafc";
9
+ return {
10
+ endpoint: config.endpoint,
11
+ mode: config.mode ?? "popover",
12
+ agentId: config.agentId,
13
+ agentName: config.agentName ?? "Assistant",
14
+ userName: config.userName,
15
+ userEmail: config.userEmail,
16
+ placeholder: config.placeholder ?? "Type a message…",
17
+ greeting: config.greeting ?? "Hi! How can I help you today?",
18
+ connectionErrorMessage: config.connectionErrorMessage ?? "We couldn't reach the chat. Please try again in a moment.",
19
+ startOpen: config.startOpen ?? false,
20
+ theme: {
21
+ text: theme.text ?? "#f8fafc",
22
+ background: theme.background ?? "#040d30",
23
+ primary,
24
+ primaryText,
25
+ assistantBubble: theme.assistantBubble ?? "#06134b",
26
+ assistantBubbleText: theme.assistantBubbleText ?? "#f8fafc",
27
+ userBubble: theme.userBubble ?? primary,
28
+ userBubbleText: theme.userBubbleText ?? primaryText,
29
+ border: theme.border ?? "#0a1f7a"
30
+ }
31
+ };
32
+ }
33
+ //#endregion
34
+ //#region src/transport.ts
35
+ const WS_CONNECTING = 0;
36
+ const WS_OPEN = 1;
37
+ const WS_CLOSING = 2;
38
+ /** Default connect timeout (ms) for the WebSocket transport. */
39
+ const DEFAULT_CONNECT_TIMEOUT = 3e4;
40
+ /**
41
+ * Default transport backed by a `WebSocket`-like object. By default it uses the
42
+ * global `WebSocket`; pass a `factory` to inject one (e.g. the `ws` package on
43
+ * Node, or a mock in tests).
44
+ */
45
+ var WebSocketTransport = class {
46
+ socket = null;
47
+ url;
48
+ factory;
49
+ connectTimeout;
50
+ messageHandlers = /* @__PURE__ */ new Set();
51
+ closeHandlers = /* @__PURE__ */ new Set();
52
+ errorHandlers = /* @__PURE__ */ new Set();
53
+ constructor(url, factory, connectTimeout = DEFAULT_CONNECT_TIMEOUT) {
54
+ this.url = url;
55
+ this.connectTimeout = connectTimeout;
56
+ if (factory) this.factory = factory;
57
+ else {
58
+ const G = globalThis;
59
+ if (!G.WebSocket) throw new Error("No global WebSocket available; pass a WebSocketFactory to WebSocketTransport.");
60
+ const Ctor = G.WebSocket;
61
+ this.factory = (u) => new Ctor(u);
62
+ }
63
+ }
64
+ get state() {
65
+ if (!this.socket) return "closed";
66
+ switch (this.socket.readyState) {
67
+ case WS_CONNECTING: return "connecting";
68
+ case WS_OPEN: return "open";
69
+ case WS_CLOSING: return "closing";
70
+ default: return "closed";
71
+ }
72
+ }
73
+ connect() {
74
+ if (this.socket && this.socket.readyState === WS_OPEN) return Promise.resolve();
75
+ if (this.socket && this.socket.readyState !== WS_OPEN) {
76
+ const stale = this.socket;
77
+ this.socket = null;
78
+ try {
79
+ stale.close();
80
+ } catch {}
81
+ }
82
+ return new Promise((resolve, reject) => {
83
+ const socket = this.factory(this.url);
84
+ this.socket = socket;
85
+ let settled = false;
86
+ const timer = this.connectTimeout > 0 ? setTimeout(() => {
87
+ if (settled) return;
88
+ settled = true;
89
+ if (this.socket === socket) this.socket = null;
90
+ try {
91
+ socket.close();
92
+ } catch {}
93
+ reject(/* @__PURE__ */ new Error(`WebSocket connect to ${this.url} timed out after ${this.connectTimeout}ms`));
94
+ }, this.connectTimeout) : void 0;
95
+ socket.addEventListener("open", () => {
96
+ if (this.socket !== socket) return;
97
+ if (settled) return;
98
+ settled = true;
99
+ if (timer) clearTimeout(timer);
100
+ resolve();
101
+ });
102
+ socket.addEventListener("error", (ev) => {
103
+ if (this.socket !== socket) return;
104
+ for (const h of this.errorHandlers) h(ev);
105
+ if (!settled && this.state !== "open") {
106
+ settled = true;
107
+ if (timer) clearTimeout(timer);
108
+ if (this.socket === socket) this.socket = null;
109
+ try {
110
+ socket.close();
111
+ } catch {}
112
+ reject(ev instanceof Error ? ev : /* @__PURE__ */ new Error("WebSocket connection error"));
113
+ }
114
+ });
115
+ socket.addEventListener("close", (ev) => {
116
+ if (this.socket !== socket) return;
117
+ if (timer) clearTimeout(timer);
118
+ for (const h of this.closeHandlers) h({
119
+ code: ev.code,
120
+ reason: ev.reason
121
+ });
122
+ });
123
+ socket.addEventListener("message", (ev) => {
124
+ if (this.socket !== socket) return;
125
+ const data = typeof ev.data === "string" ? ev.data : String(ev.data);
126
+ for (const h of this.messageHandlers) h(data);
127
+ });
128
+ });
129
+ }
130
+ send(data) {
131
+ if (!this.socket || this.socket.readyState !== WS_OPEN) throw new Error(`Cannot send: transport is "${this.state}"`);
132
+ this.socket.send(data);
133
+ }
134
+ close(code, reason) {
135
+ this.socket?.close(code, reason);
136
+ }
137
+ onMessage(handler) {
138
+ this.messageHandlers.add(handler);
139
+ return () => this.messageHandlers.delete(handler);
140
+ }
141
+ onClose(handler) {
142
+ this.closeHandlers.add(handler);
143
+ return () => this.closeHandlers.delete(handler);
144
+ }
145
+ onError(handler) {
146
+ this.errorHandlers.add(handler);
147
+ return () => this.errorHandlers.delete(handler);
148
+ }
149
+ };
150
+ //#endregion
151
+ //#region src/types.ts
152
+ /** Every server→client `type` discriminator value. */
153
+ const EVENT_TYPES = [
154
+ "immediate_response",
155
+ "eventual_response",
156
+ "stream_chunk",
157
+ "stream_token",
158
+ "keepalive",
159
+ "write_confirmation_required",
160
+ "otp_verification_required",
161
+ "otp_sent",
162
+ "otp_verified",
163
+ "otp_invalid",
164
+ "error",
165
+ "pong"
166
+ ];
167
+ /** True if `frame` looks like any server event (has a known `type` discriminator). */
168
+ function isServerEvent(frame) {
169
+ return typeof frame === "object" && frame !== null && "type" in frame && typeof frame.type === "string" && EVENT_TYPES.includes(frame.type);
170
+ }
171
+ //#endregion
172
+ //#region src/client.ts
173
+ /**
174
+ * SmoothAgentClient — a minimal, idiomatic, transport-agnostic client for the
175
+ * smooth-operator WebSocket protocol.
176
+ *
177
+ * Design goals
178
+ * ------------
179
+ * - **Transport-agnostic.** The client never touches a real socket directly; it
180
+ * talks to an injectable {@link Transport}. The default ({@link WebSocketTransport})
181
+ * uses the global `WebSocket`, but tests inject a mock and Node can inject `ws`.
182
+ * - **Request/response correlation by `requestId`.** Every action gets a generated
183
+ * `requestId`; the client routes incoming events back to the originating call.
184
+ * - **Streaming as an async iterator.** `sendMessage` returns a {@link MessageTurn}
185
+ * that is both awaitable (resolves with the terminal `eventual_response`) and
186
+ * async-iterable (yields each `stream_token` / `stream_chunk` / HITL event in
187
+ * order). This models the `stream_token`/`stream_chunk` → `eventual_response`
188
+ * flow without forcing a callback style on the caller.
189
+ * - **No live server required.** Correctness is fully unit-testable with a mock
190
+ * transport (see `test/client.test.ts`).
191
+ */
192
+ /** A timeout that yields no terminal event. */
193
+ var RequestTimeoutError = class extends Error {
194
+ constructor(requestId, ms) {
195
+ super(`Request ${requestId} timed out after ${ms}ms`);
196
+ this.name = "RequestTimeoutError";
197
+ }
198
+ };
199
+ /**
200
+ * A streaming turn that received no terminal `eventual_response` / `error` within the
201
+ * configured {@link SmoothAgentClientOptions.turnTimeout}. The turn rejects with this
202
+ * and its async iteration throws it, so a stuck server can never hang the caller.
203
+ */
204
+ var TurnTimeoutError = class extends Error {
205
+ requestId;
206
+ constructor(requestId, ms) {
207
+ super(`Turn ${requestId} timed out after ${ms}ms without a terminal response`);
208
+ this.name = "TurnTimeoutError";
209
+ this.requestId = requestId;
210
+ }
211
+ };
212
+ /** A protocol-level error event surfaced as a throwable. */
213
+ var ProtocolError = class extends Error {
214
+ code;
215
+ requestId;
216
+ constructor(code, message, requestId) {
217
+ super(message);
218
+ this.name = "ProtocolError";
219
+ this.code = code;
220
+ this.requestId = requestId;
221
+ }
222
+ };
223
+ /**
224
+ * A streaming message turn. Await it for the terminal {@link EventualResponse},
225
+ * or async-iterate it to receive every intermediate event in arrival order.
226
+ *
227
+ * ```ts
228
+ * const turn = client.sendMessage({ sessionId, message: 'hi' });
229
+ * for await (const ev of turn) {
230
+ * if (ev.type === 'stream_token') process.stdout.write(ev.token ?? '');
231
+ * }
232
+ * const final = await turn; // EventualResponse
233
+ * ```
234
+ */
235
+ var MessageTurn = class {
236
+ /** The requestId this turn is correlated on. */
237
+ requestId;
238
+ queue = [];
239
+ waiter = null;
240
+ done = false;
241
+ finalEvent = null;
242
+ error = null;
243
+ settled;
244
+ settle;
245
+ fail;
246
+ onClose;
247
+ timeoutTimer;
248
+ constructor(requestId, onClose, turnTimeout = 0) {
249
+ this.requestId = requestId;
250
+ this.onClose = onClose;
251
+ this.settled = new Promise((resolve, reject) => {
252
+ this.settle = resolve;
253
+ this.fail = reject;
254
+ });
255
+ this.settled.catch(() => {});
256
+ if (turnTimeout > 0) this.timeoutTimer = setTimeout(() => {
257
+ this.finish(null, new TurnTimeoutError(this.requestId, turnTimeout));
258
+ }, turnTimeout);
259
+ }
260
+ /** Feed an event into the turn (called by the client's dispatcher). */
261
+ push(event) {
262
+ if (this.done) return;
263
+ if (event.type === "error") {
264
+ const code = event.data?.error?.code ?? "INTERNAL_ERROR";
265
+ const message = event.data?.error?.message ?? "Unknown protocol error";
266
+ this.deliver(event);
267
+ this.finish(null, new ProtocolError(code, message, this.requestId));
268
+ return;
269
+ }
270
+ this.deliver(event);
271
+ if (event.type === "eventual_response") this.finish(event, null);
272
+ }
273
+ /** Force-close the turn (e.g. on disconnect) with an error. */
274
+ abort(err) {
275
+ if (this.done) return;
276
+ this.finish(null, err);
277
+ }
278
+ deliver(event) {
279
+ if (this.waiter) {
280
+ const w = this.waiter;
281
+ this.waiter = null;
282
+ w.resolve({
283
+ value: event,
284
+ done: false
285
+ });
286
+ } else this.queue.push(event);
287
+ }
288
+ finish(final, err) {
289
+ if (this.done) return;
290
+ this.done = true;
291
+ this.finalEvent = final;
292
+ this.error = err;
293
+ if (this.timeoutTimer) {
294
+ clearTimeout(this.timeoutTimer);
295
+ this.timeoutTimer = void 0;
296
+ }
297
+ this.onClose();
298
+ if (err) this.fail(err);
299
+ else if (final) this.settle(final);
300
+ if (this.waiter) {
301
+ const w = this.waiter;
302
+ this.waiter = null;
303
+ if (err) w.reject(err);
304
+ else w.resolve({
305
+ value: void 0,
306
+ done: true
307
+ });
308
+ }
309
+ }
310
+ [Symbol.asyncIterator]() {
311
+ return { next: () => {
312
+ if (this.queue.length > 0) return Promise.resolve({
313
+ value: this.queue.shift(),
314
+ done: false
315
+ });
316
+ if (this.done) {
317
+ if (this.error) return Promise.reject(this.error);
318
+ return Promise.resolve({
319
+ value: void 0,
320
+ done: true
321
+ });
322
+ }
323
+ return new Promise((resolve, reject) => {
324
+ this.waiter = {
325
+ resolve,
326
+ reject
327
+ };
328
+ });
329
+ } };
330
+ }
331
+ then(onfulfilled, onrejected) {
332
+ return this.settled.then(onfulfilled, onrejected);
333
+ }
334
+ };
335
+ var SmoothAgentClient = class {
336
+ transport;
337
+ generateRequestId;
338
+ requestTimeout;
339
+ turnTimeout;
340
+ /** requestId → single-response waiter (create_session, get_session, ping, …). */
341
+ pending = /* @__PURE__ */ new Map();
342
+ /** requestId → active streaming turn (send_message, and HITL resumes). */
343
+ turns = /* @__PURE__ */ new Map();
344
+ /** Unsolicited-event listeners (keepalive, server-push). */
345
+ listeners = /* @__PURE__ */ new Set();
346
+ unsubscribe = [];
347
+ constructor(options) {
348
+ this.transport = options.transport ?? new WebSocketTransport(options.url, options.webSocketFactory);
349
+ this.requestTimeout = options.requestTimeout ?? 3e4;
350
+ this.turnTimeout = options.turnTimeout ?? 12e4;
351
+ this.generateRequestId = options.generateRequestId ?? (() => `req-${globalThis.crypto?.randomUUID?.() ?? Math.random().toString(36).slice(2)}`);
352
+ this.unsubscribe.push(this.transport.onMessage((data) => this.handleFrame(data)));
353
+ this.unsubscribe.push(this.transport.onClose(() => this.failAll(/* @__PURE__ */ new Error("Transport closed"))));
354
+ }
355
+ /** Open the underlying transport. */
356
+ async connect() {
357
+ await this.transport.connect();
358
+ }
359
+ /** Close the transport and reject all in-flight work. */
360
+ disconnect(reason = "client disconnect") {
361
+ this.failAll(new Error(reason));
362
+ for (const u of this.unsubscribe) u();
363
+ this.unsubscribe = [];
364
+ this.transport.close(1e3, reason);
365
+ }
366
+ /** Subscribe to unsolicited / uncorrelated server events (e.g. keepalive). */
367
+ onEvent(listener) {
368
+ this.listeners.add(listener);
369
+ return () => this.listeners.delete(listener);
370
+ }
371
+ /** Start a new conversation session. Resolves with the session descriptor. */
372
+ async createConversationSession(req) {
373
+ return extractImmediateData(await this.request({
374
+ action: "create_conversation_session",
375
+ ...req
376
+ }));
377
+ }
378
+ /** Fetch a session snapshot by ID. */
379
+ async getSession(req) {
380
+ return extractImmediateData(await this.request({
381
+ action: "get_session",
382
+ ...req
383
+ }));
384
+ }
385
+ /** Fetch a page of conversation messages. */
386
+ async getMessages(req) {
387
+ return extractImmediateData(await this.request({
388
+ action: "get_conversation_messages",
389
+ ...req
390
+ }));
391
+ }
392
+ /** Keepalive ping. Resolves with the server timestamp from the `pong` event. */
393
+ async ping() {
394
+ const event = await this.request({ action: "ping" });
395
+ if (event.type === "pong") return event.timestamp ?? event.data?.timestamp ?? Date.now();
396
+ return Date.now();
397
+ }
398
+ /**
399
+ * Submit a user message and return a {@link MessageTurn}: await it for the
400
+ * terminal `eventual_response`, or async-iterate it for the streaming events.
401
+ */
402
+ sendMessage(req) {
403
+ const requestId = this.generateRequestId();
404
+ const turn = new MessageTurn(requestId, () => this.turns.delete(requestId), this.turnTimeout);
405
+ this.turns.set(requestId, turn);
406
+ try {
407
+ this.transport.send(JSON.stringify({
408
+ action: "send_message",
409
+ requestId,
410
+ ...req
411
+ }));
412
+ } catch (err) {
413
+ this.turns.delete(requestId);
414
+ turn.abort(err);
415
+ }
416
+ return turn;
417
+ }
418
+ /**
419
+ * Approve or reject a pending tool write, resuming the paused turn identified
420
+ * by `requestId`. The resumed streaming events flow back into the original
421
+ * {@link MessageTurn} for that `requestId`.
422
+ */
423
+ confirmToolAction(req) {
424
+ this.transport.send(JSON.stringify({
425
+ action: "confirm_tool_action",
426
+ ...req
427
+ }));
428
+ }
429
+ /**
430
+ * Submit an OTP code, resuming the paused turn identified by `requestId`.
431
+ * The resumed streaming events flow back into the original {@link MessageTurn}.
432
+ */
433
+ verifyOtp(req) {
434
+ this.transport.send(JSON.stringify({
435
+ action: "verify_otp",
436
+ ...req
437
+ }));
438
+ }
439
+ /** Send an action that expects a single correlated response event. */
440
+ request(action) {
441
+ const requestId = action.requestId ?? this.generateRequestId();
442
+ const frame = {
443
+ ...action,
444
+ requestId
445
+ };
446
+ return new Promise((resolve, reject) => {
447
+ const timer = this.requestTimeout > 0 ? setTimeout(() => {
448
+ this.pending.delete(requestId);
449
+ reject(new RequestTimeoutError(requestId, this.requestTimeout));
450
+ }, this.requestTimeout) : void 0;
451
+ this.pending.set(requestId, {
452
+ resolve,
453
+ reject,
454
+ timer
455
+ });
456
+ try {
457
+ this.transport.send(JSON.stringify(frame));
458
+ } catch (err) {
459
+ if (timer) clearTimeout(timer);
460
+ this.pending.delete(requestId);
461
+ reject(err);
462
+ }
463
+ });
464
+ }
465
+ /** Parse and route an incoming frame to the right consumer. */
466
+ handleFrame(data) {
467
+ let frame;
468
+ try {
469
+ frame = JSON.parse(data);
470
+ } catch {
471
+ return;
472
+ }
473
+ if (!isServerEvent(frame)) return;
474
+ const event = frame;
475
+ const requestId = event.requestId;
476
+ if (requestId && this.turns.has(requestId)) {
477
+ this.turns.get(requestId).push(event);
478
+ return;
479
+ }
480
+ if (requestId && this.pending.has(requestId)) {
481
+ const pending = this.pending.get(requestId);
482
+ this.pending.delete(requestId);
483
+ if (pending.timer) clearTimeout(pending.timer);
484
+ if (event.type === "error") {
485
+ const code = event.data?.error?.code ?? "INTERNAL_ERROR";
486
+ const message = event.data?.error?.message ?? "Unknown protocol error";
487
+ pending.reject(new ProtocolError(code, message, requestId));
488
+ } else pending.resolve(event);
489
+ return;
490
+ }
491
+ for (const l of this.listeners) l(event);
492
+ }
493
+ failAll(err) {
494
+ for (const [, p] of this.pending) {
495
+ if (p.timer) clearTimeout(p.timer);
496
+ p.reject(err);
497
+ }
498
+ this.pending.clear();
499
+ for (const [, turn] of this.turns) turn.abort(err);
500
+ this.turns.clear();
501
+ }
502
+ };
503
+ /** Pull the typed `data` payload out of an `immediate_response` event. */
504
+ function extractImmediateData(event) {
505
+ if (event.type === "immediate_response") return event.data;
506
+ if ("data" in event && event.data && typeof event.data === "object") return event.data;
507
+ throw new ProtocolError("UNEXPECTED_EVENT", `Expected immediate_response, got "${event.type}"`, event.requestId);
508
+ }
509
+ //#endregion
510
+ //#region src/widget/conversation.ts
511
+ /**
512
+ * ConversationController — the bridge between the widget UI and the
513
+ * `@smooai/smooth-operator` protocol client.
514
+ *
515
+ * This is the piece that was rewired: the original smooai widget spoke to
516
+ * `@smooai/realtime`; here every protocol action goes through {@link SmoothAgentClient}.
517
+ * The wire shapes are identical (the protocol was lifted from `@smooai/realtime`),
518
+ * so the swap is purely at the client-library boundary.
519
+ *
520
+ * Flow:
521
+ * 1. `connect()` → opens the WebSocket transport and `create_conversation_session`.
522
+ * 2. `send(text)` → `send_message`, streaming `stream_token` deltas into the
523
+ * in-progress assistant message, then the terminal
524
+ * `eventual_response`.
525
+ *
526
+ * The controller is UI-agnostic: it emits typed events and the view renders them.
527
+ */
528
+ /** Pull the final assistant text out of an `eventual_response` data payload. */
529
+ function extractFinalText(response) {
530
+ if (!response || typeof response !== "object") return null;
531
+ const r = response;
532
+ if (Array.isArray(r.responseParts)) return r.responseParts.filter((p) => typeof p === "string").join("\n\n");
533
+ return null;
534
+ }
535
+ /**
536
+ * Pull the grounding {@link Citation}s out of a terminal `eventual_response`.
537
+ *
538
+ * The protocol client types these (`eventual_response.data.data.citations`),
539
+ * but they're optional and back-compatible — absent when the turn used no
540
+ * knowledge sources. We read them defensively (tolerating their total absence,
541
+ * non-array shapes, and missing fields) so a server that doesn't emit them, or
542
+ * an older client, can't break rendering. Each citation always carries
543
+ * `id`/`title`/`snippet`/`score`; `url` is present only for web-sourced docs.
544
+ */
545
+ function extractCitations(inner) {
546
+ if (!inner || typeof inner !== "object") return [];
547
+ const raw = inner.citations;
548
+ if (!Array.isArray(raw)) return [];
549
+ const out = [];
550
+ for (const c of raw) {
551
+ if (!c || typeof c !== "object") continue;
552
+ const obj = c;
553
+ const id = typeof obj.id === "string" ? obj.id : "";
554
+ const title = typeof obj.title === "string" ? obj.title : id || "Source";
555
+ const snippet = typeof obj.snippet === "string" ? obj.snippet : "";
556
+ const url = typeof obj.url === "string" && obj.url ? obj.url : void 0;
557
+ const score = typeof obj.score === "number" ? obj.score : 0;
558
+ out.push({
559
+ id,
560
+ title,
561
+ snippet,
562
+ score,
563
+ url
564
+ });
565
+ }
566
+ return out;
567
+ }
568
+ var ConversationController = class {
569
+ config;
570
+ events;
571
+ client = null;
572
+ sessionId = null;
573
+ messages = [];
574
+ status = "idle";
575
+ seq = 0;
576
+ constructor(config, events) {
577
+ this.config = config;
578
+ this.events = events;
579
+ }
580
+ get connectionStatus() {
581
+ return this.status;
582
+ }
583
+ nextId(prefix) {
584
+ this.seq += 1;
585
+ return `${prefix}-${this.seq}-${Date.now().toString(36)}`;
586
+ }
587
+ setStatus(status, detail) {
588
+ this.status = status;
589
+ this.events.onStatus(status, detail);
590
+ }
591
+ emitMessages() {
592
+ this.events.onMessages(this.messages.map((m) => ({ ...m })));
593
+ }
594
+ /** Open the transport and create a conversation session. Idempotent. */
595
+ async connect() {
596
+ if (this.status === "connecting" || this.status === "ready") return;
597
+ this.setStatus("connecting");
598
+ try {
599
+ this.client = new SmoothAgentClient({ url: this.config.endpoint });
600
+ await this.client.connect();
601
+ const session = await this.client.createConversationSession({
602
+ agentId: this.config.agentId,
603
+ userName: this.config.userName,
604
+ userEmail: this.config.userEmail
605
+ });
606
+ this.sessionId = session.sessionId;
607
+ this.setStatus("ready");
608
+ } catch (err) {
609
+ this.setStatus("error", err instanceof Error ? err.message : String(err));
610
+ throw err;
611
+ }
612
+ }
613
+ /**
614
+ * Submit a user message. Appends the user bubble immediately, then streams the
615
+ * assistant reply token-by-token, finalizing on `eventual_response`.
616
+ */
617
+ async send(text) {
618
+ const trimmed = text.trim();
619
+ if (!trimmed) return;
620
+ if (!this.client || !this.sessionId || this.status !== "ready") await this.connect();
621
+ if (!this.client || !this.sessionId) throw new Error("Conversation is not connected");
622
+ this.messages.push({
623
+ id: this.nextId("u"),
624
+ role: "user",
625
+ text: trimmed,
626
+ streaming: false
627
+ });
628
+ const assistant = {
629
+ id: this.nextId("a"),
630
+ role: "assistant",
631
+ text: "",
632
+ streaming: true
633
+ };
634
+ this.messages.push(assistant);
635
+ this.emitMessages();
636
+ try {
637
+ const turn = this.client.sendMessage({
638
+ sessionId: this.sessionId,
639
+ message: trimmed,
640
+ stream: true
641
+ });
642
+ for await (const event of turn) if (event.type === "stream_token") {
643
+ const token = event.token ?? event.data?.token ?? "";
644
+ if (token) {
645
+ assistant.text += token;
646
+ this.emitMessages();
647
+ }
648
+ }
649
+ const inner = (await turn).data?.data;
650
+ const finalText = extractFinalText(inner?.response);
651
+ if (finalText && finalText.length > assistant.text.length) assistant.text = finalText;
652
+ if (!assistant.text) assistant.text = "(no response)";
653
+ const citations = extractCitations(inner);
654
+ if (citations.length > 0) assistant.citations = citations;
655
+ assistant.streaming = false;
656
+ this.emitMessages();
657
+ } catch (err) {
658
+ assistant.streaming = false;
659
+ const message = err instanceof ProtocolError ? `Error: ${err.message}` : this.config.connectionErrorMessage ?? "We couldn't reach the chat.";
660
+ assistant.text = assistant.text ? `${assistant.text}\n\n${message}` : message;
661
+ this.emitMessages();
662
+ this.setStatus("error", err instanceof Error ? err.message : String(err));
663
+ }
664
+ }
665
+ /** Tear down the underlying client. */
666
+ disconnect() {
667
+ this.client?.disconnect("widget closed");
668
+ this.client = null;
669
+ this.sessionId = null;
670
+ this.setStatus("closed");
671
+ }
672
+ };
673
+ //#endregion
674
+ //#region src/widget/logo.ts
675
+ /**
676
+ * The Smooth logo, inlined as an SVG string so the full-page header can render
677
+ * it without a separate network fetch (the IIFE bundle is self-contained).
678
+ *
679
+ * GENERATED from `assets/smooth-logo.svg` — do not edit by hand. Regenerate with:
680
+ * node -e ... (see the commit that added this file)
681
+ */
682
+ const SMOOTH_LOGO_SVG = "<?xml version=\"1.0\" encoding=\"UTF-8\"?>\n<svg id=\"Layer_1\" data-name=\"Layer 1\" xmlns=\"http://www.w3.org/2000/svg\" xmlns:xlink=\"http://www.w3.org/1999/xlink\" viewBox=\"0 0 550 135\">\n <defs>\n <style>\n .cls-1 {\n fill: url(#linear-gradient-3);\n }\n\n .cls-2 {\n fill: url(#linear-gradient-2);\n }\n\n .cls-3 {\n fill: url(#linear-gradient);\n fill-rule: evenodd;\n }\n </style>\n <linearGradient id=\"linear-gradient\" x1=\"115.59\" y1=\"112.81\" x2=\"25.08\" y2=\"22.3\" gradientUnits=\"userSpaceOnUse\">\n <stop offset=\".3\" stop-color=\"#f49f0a\"/>\n <stop offset=\".79\" stop-color=\"#fb7a4d\"/>\n <stop offset=\"1\" stop-color=\"#ff6b6c\"/>\n </linearGradient>\n <linearGradient id=\"linear-gradient-2\" x1=\"360.91\" y1=\"152.01\" x2=\"202.32\" y2=\"-6.59\" xlink:href=\"#linear-gradient\"/>\n <linearGradient id=\"linear-gradient-3\" x1=\"443.91\" y1=\"30.15\" x2=\"531.36\" y2=\"117.59\" gradientUnits=\"userSpaceOnUse\">\n <stop offset=\".43\" stop-color=\"#00a6a6\"/>\n <stop offset=\"1\" stop-color=\"#1238dd\"/>\n </linearGradient>\n </defs>\n <path class=\"cls-3\" d=\"M48.28,14.96c-12.39,5.21-22.54,14.64-28.65,26.61-6.12,11.97-7.8,25.72-4.77,38.81,3.04,13.09,10.6,24.69,21.36,32.75,10.76,8.06,24.02,12.05,37.44,11.28,13.42-.77,26.13-6.26,35.9-15.5,9.76-9.24,15.95-21.63,17.46-34.99,1.51-13.36-1.74-26.82-9.19-38.01-1.07-1.61-.64-3.78.97-4.85,1.61-1.07,3.78-.64,4.85.97,8.36,12.56,12.02,27.68,10.32,42.67-1.7,15-8.64,28.91-19.61,39.28-10.96,10.37-25.24,16.54-40.31,17.4-15.07.87-29.96-3.62-42.04-12.66-12.08-9.05-20.58-22.07-23.99-36.77-3.41-14.7-1.51-30.14,5.35-43.58,6.87-13.44,18.26-24.02,32.17-29.87,13.91-5.85,29.44-6.6,43.85-2.11,1.85.57,2.88,2.54,2.3,4.38-.57,1.85-2.54,2.88-4.38,2.3-12.83-4-26.67-3.33-39.06,1.88ZM111.39,19.75c0,2.07-1.68,3.75-3.75,3.75s-3.75-1.68-3.75-3.75,1.68-3.75,3.75-3.75,3.75,1.68,3.75,3.75ZM64.64,59.93c0,1.91,2.39,2.56,7.69,3.88,3.89.97,6.6,2.18,8.15,3.63,1.53,1.45,2.29,3.53,2.29,6.25,0,3.57-1.03,6.26-3.11,8.08-2.07,1.82-5.09,2.73-9.09,2.73h-9.6c-1.97,0-3.57-1.6-3.59-3.57-.01-1.99,1.6-3.61,3.59-3.61h9.41c3.15-.12,4.79-.95,4.91-2.47,0-1.3-1.03-2.21-3.07-2.73-6.91-1.72-11.11-3.44-12.6-5.15-1.48-1.71-2.23-3.77-2.23-6.19,0-6.59,3.2-9.85,9.59-9.8h10.77c1.99,0,3.6,1.61,3.6,3.59s-1.61,3.59-3.6,3.59h-9.69c-1.83,0-3.43.06-3.43,1.77Z\"/>\n <path class=\"cls-2\" d=\"M205.52,48.44h-8.86c-.44-3.75-2.23-6.65-5.38-8.72-3.16-2.07-7.03-3.1-11.6-3.1h0c-3.35,0-6.27.54-8.78,1.62-2.49,1.09-4.44,2.59-5.84,4.48-1.39,1.89-2.08,4.05-2.08,6.46h0c0,2.01.49,3.75,1.46,5.2.97,1.44,2.22,2.63,3.74,3.58,1.53.95,3.13,1.72,4.8,2.32,1.68.6,3.22,1.09,4.62,1.46h0l7.68,2.06c1.97.52,4.17,1.23,6.6,2.14,2.43.92,4.75,2.16,6.98,3.72,2.23,1.56,4.07,3.56,5.52,6,1.45,2.44,2.18,5.43,2.18,8.98h0c0,4.08-1.07,7.77-3.2,11.08-2.12,3.29-5.22,5.91-9.3,7.86-4.08,1.95-9.02,2.92-14.82,2.92h0c-5.43,0-10.11-.87-14.06-2.62-3.95-1.75-7.05-4.19-9.3-7.32-2.25-3.12-3.53-6.75-3.84-10.88h9.46c.25,2.85,1.22,5.21,2.9,7.06,1.69,1.87,3.83,3.25,6.42,4.14,2.6.89,5.41,1.34,8.42,1.34h0c3.49,0,6.63-.57,9.4-1.72,2.79-1.13,4.99-2.73,6.62-4.8,1.63-2.05,2.44-4.46,2.44-7.22h0c0-2.51-.7-4.55-2.1-6.12-1.41-1.57-3.26-2.85-5.54-3.84-2.29-.99-4.77-1.85-7.44-2.58h0l-9.3-2.66c-5.91-1.71-10.59-4.13-14.04-7.28-3.44-3.16-5.16-7.29-5.16-12.38h0c0-4.23,1.15-7.93,3.46-11.1,2.29-3.16,5.39-5.62,9.3-7.38,3.91-1.76,8.27-2.64,13.08-2.64h0c4.88,0,9.21.87,13,2.6,3.8,1.73,6.81,4.11,9.04,7.12,2.23,3,3.4,6.41,3.52,10.22h0ZM229.16,105.18h-8.72v-56.74h8.42v8.86h.74c1.19-3.03,3.1-5.38,5.74-7.06,2.63-1.69,5.79-2.54,9.48-2.54h0c3.75,0,6.87.85,9.36,2.54,2.51,1.68,4.46,4.03,5.86,7.06h.58c1.45-2.92,3.63-5.25,6.54-7,2.91-1.73,6.39-2.6,10.46-2.6h0c5.07,0,9.21,1.58,12.44,4.74,3.23,3.17,4.84,8.09,4.84,14.76h0v37.98h-8.72v-37.98c0-4.19-1.14-7.18-3.42-8.98-2.29-1.79-4.99-2.68-8.1-2.68h0c-3.99,0-7.07,1.2-9.26,3.6-2.2,2.4-3.3,5.43-3.3,9.1h0v36.94h-8.86v-38.86c0-3.23-1.05-5.83-3.14-7.82-2.09-1.97-4.79-2.96-8.08-2.96h0c-2.27,0-4.38.6-6.34,1.8-1.96,1.21-3.53,2.88-4.72,5-1.2,2.13-1.8,4.59-1.8,7.38h0v35.46ZM333.9,106.36h0c-5.12,0-9.61-1.22-13.46-3.66-3.85-2.44-6.86-5.85-9.02-10.24-2.15-4.37-3.22-9.49-3.22-15.36h0c0-5.91,1.07-11.07,3.22-15.48,2.16-4.4,5.17-7.82,9.02-10.26,3.85-2.44,8.34-3.66,13.46-3.66h0c5.12,0,9.61,1.22,13.46,3.66,3.85,2.44,6.86,5.86,9.02,10.26,2.15,4.41,3.22,9.57,3.22,15.48h0c0,5.87-1.07,10.99-3.22,15.36-2.16,4.39-5.17,7.8-9.02,10.24-3.85,2.44-8.34,3.66-13.46,3.66ZM333.9,98.52h0c3.89,0,7.09-.99,9.6-2.98,2.52-2,4.38-4.63,5.58-7.88,1.21-3.25,1.82-6.77,1.82-10.56h0c0-3.79-.61-7.32-1.82-10.6-1.2-3.27-3.06-5.91-5.58-7.94-2.51-2.01-5.71-3.02-9.6-3.02h0c-3.89,0-7.09,1.01-9.6,3.02-2.51,2.03-4.37,4.67-5.58,7.94-1.2,3.28-1.8,6.81-1.8,10.6h0c0,3.79.6,7.31,1.8,10.56,1.21,3.25,3.07,5.88,5.58,7.88,2.51,1.99,5.71,2.98,9.6,2.98ZM395.94,106.36h0c-5.12,0-9.61-1.22-13.46-3.66-3.85-2.44-6.85-5.85-9-10.24-2.16-4.37-3.24-9.49-3.24-15.36h0c0-5.91,1.08-11.07,3.24-15.48,2.15-4.4,5.15-7.82,9-10.26,3.85-2.44,8.34-3.66,13.46-3.66h0c5.12,0,9.61,1.22,13.46,3.66,3.85,2.44,6.86,5.86,9.02,10.26,2.16,4.41,3.24,9.57,3.24,15.48h0c0,5.87-1.08,10.99-3.24,15.36-2.16,4.39-5.17,7.8-9.02,10.24-3.85,2.44-8.34,3.66-13.46,3.66ZM395.94,98.52h0c3.89,0,7.09-.99,9.6-2.98,2.52-2,4.38-4.63,5.58-7.88,1.21-3.25,1.82-6.77,1.82-10.56h0c0-3.79-.61-7.32-1.82-10.6-1.2-3.27-3.06-5.91-5.58-7.94-2.51-2.01-5.71-3.02-9.6-3.02h0c-3.88,0-7.08,1.01-9.6,3.02-2.51,2.03-4.37,4.67-5.58,7.94-1.2,3.28-1.8,6.81-1.8,10.6h0c0,3.79.6,7.31,1.8,10.56,1.21,3.25,3.07,5.88,5.58,7.88,2.52,1.99,5.72,2.98,9.6,2.98Z\"/>\n <path class=\"cls-1\" d=\"M467.88,48.02v13.28h-35.79v-13.28h35.79ZM439.68,34.38h17.89v53.42c0,1.5.36,2.62,1.08,3.36.72.74,1.88,1.1,3.49,1.1.62,0,1.48-.07,2.59-.21,1.11-.14,1.91-.27,2.38-.41l2.31,13.02c-2.02.58-3.97.97-5.84,1.18-1.88.21-3.66.31-5.33.31-6.08,0-10.7-1.43-13.84-4.28-3.15-2.85-4.72-7.01-4.72-12.48v-55.01ZM506.59,72.63v32.71h-17.89V28.95h17.53v33.53h-1.13c1.4-4.55,3.6-8.21,6.59-11,2.99-2.79,7.01-4.18,12.07-4.18,4,0,7.48.89,10.46,2.67,2.97,1.78,5.28,4.29,6.92,7.54,1.64,3.25,2.46,7.02,2.46,11.33v36.5h-17.89v-33.02c0-3.21-.82-5.73-2.46-7.56-1.64-1.83-3.93-2.74-6.87-2.74-1.92,0-3.62.42-5.1,1.26-1.49.84-2.64,2.04-3.46,3.61-.82,1.57-1.23,3.49-1.23,5.74Z\"/>\n</svg>";
683
+ //#endregion
684
+ //#region src/widget/styles.ts
685
+ /**
686
+ * Render the widget's scoped stylesheet. All theme values are injected as CSS
687
+ * custom properties on `:host` so they can be overridden per-instance and so the
688
+ * styles below stay static. Kept deliberately framework-light — no Tailwind, no
689
+ * runtime CSS-in-JS; just a string the web component drops into its shadow root.
690
+ *
691
+ * `mode` switches the host positioning + panel sizing between the floating
692
+ * popover (default) and the full-page layout (fills its container/viewport).
693
+ */
694
+ function buildStyles(theme, mode = "popover") {
695
+ return `
696
+ :host {
697
+ --sac-text: ${theme.text};
698
+ --sac-bg: ${theme.background};
699
+ --sac-primary: ${theme.primary};
700
+ --sac-primary-text: ${theme.primaryText};
701
+ --sac-assistant-bubble: ${theme.assistantBubble};
702
+ --sac-assistant-bubble-text: ${theme.assistantBubbleText};
703
+ --sac-user-bubble: ${theme.userBubble};
704
+ --sac-user-bubble-text: ${theme.userBubbleText};
705
+ --sac-border: ${theme.border};
706
+
707
+ ${mode === "fullpage" ? `/* Full-page: fill the host's box (the element should be sized by its
708
+ container, or it falls back to filling the viewport). */
709
+ display: block;
710
+ position: relative;
711
+ width: 100%;
712
+ height: 100%;
713
+ min-height: 100vh;` : `/* Popover: float in the bottom-right corner. */
714
+ position: fixed;
715
+ bottom: 20px;
716
+ right: 20px;
717
+ z-index: 2147483000;`}
718
+ font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, Helvetica, Arial, sans-serif;
719
+ }
720
+
721
+ * { box-sizing: border-box; }
722
+
723
+ .launcher {
724
+ width: 56px;
725
+ height: 56px;
726
+ border-radius: 50%;
727
+ border: none;
728
+ cursor: pointer;
729
+ background: var(--sac-primary);
730
+ color: var(--sac-primary-text);
731
+ box-shadow: 0 4px 16px rgba(0, 0, 0, 0.25);
732
+ display: flex;
733
+ align-items: center;
734
+ justify-content: center;
735
+ font-size: 24px;
736
+ transition: transform 0.15s ease;
737
+ }
738
+ .launcher:hover { transform: scale(1.05); }
739
+
740
+ .panel {
741
+ width: 360px;
742
+ max-width: calc(100vw - 40px);
743
+ height: 520px;
744
+ max-height: calc(100vh - 40px);
745
+ display: flex;
746
+ flex-direction: column;
747
+ background: var(--sac-bg);
748
+ color: var(--sac-text);
749
+ border: 1px solid var(--sac-border);
750
+ border-radius: 14px;
751
+ overflow: hidden;
752
+ box-shadow: 0 12px 40px rgba(0, 0, 0, 0.35);
753
+ }
754
+
755
+ /* Full-page: the panel becomes the whole surface — no floating box, no shadow,
756
+ no rounded corners; it fills the host. */
757
+ .panel.fullpage {
758
+ width: 100%;
759
+ height: 100%;
760
+ min-height: 100vh;
761
+ max-width: none;
762
+ max-height: none;
763
+ border: none;
764
+ border-radius: 0;
765
+ box-shadow: none;
766
+ }
767
+
768
+ .header {
769
+ display: flex;
770
+ align-items: center;
771
+ justify-content: space-between;
772
+ padding: 12px 14px;
773
+ background: var(--sac-primary);
774
+ color: var(--sac-primary-text);
775
+ }
776
+ .header .brand { display: flex; align-items: center; gap: 10px; min-width: 0; }
777
+ .header .logo { height: 24px; width: auto; display: block; }
778
+ .header .title { font-weight: 600; font-size: 15px; }
779
+ .header .status { font-size: 11px; opacity: 0.85; }
780
+ .header .powered {
781
+ font-size: 10px;
782
+ opacity: 0.7;
783
+ letter-spacing: 0.02em;
784
+ }
785
+ .header .close {
786
+ background: transparent;
787
+ border: none;
788
+ color: inherit;
789
+ cursor: pointer;
790
+ font-size: 18px;
791
+ line-height: 1;
792
+ padding: 4px;
793
+ }
794
+
795
+ /* Full-page header: taller, logo-led, centered max-width content row. */
796
+ .panel.fullpage .header { padding: 14px 20px; }
797
+ .panel.fullpage .logo { height: 30px; }
798
+
799
+ .messages {
800
+ flex: 1;
801
+ overflow-y: auto;
802
+ padding: 14px;
803
+ display: flex;
804
+ flex-direction: column;
805
+ gap: 10px;
806
+ }
807
+
808
+ .bubble {
809
+ max-width: 80%;
810
+ padding: 9px 12px;
811
+ border-radius: 12px;
812
+ font-size: 14px;
813
+ line-height: 1.4;
814
+ white-space: pre-wrap;
815
+ word-break: break-word;
816
+ }
817
+ .bubble.assistant {
818
+ align-self: flex-start;
819
+ background: var(--sac-assistant-bubble);
820
+ color: var(--sac-assistant-bubble-text);
821
+ border-bottom-left-radius: 4px;
822
+ }
823
+ .bubble.user {
824
+ align-self: flex-end;
825
+ background: var(--sac-user-bubble);
826
+ color: var(--sac-user-bubble-text);
827
+ border-bottom-right-radius: 4px;
828
+ }
829
+ .bubble.greeting { opacity: 0.85; font-style: italic; }
830
+
831
+ /* Full-page: center the conversation in a readable column and let bubbles
832
+ breathe a little wider. */
833
+ .panel.fullpage .messages {
834
+ padding: 24px 20px;
835
+ align-items: stretch;
836
+ }
837
+ .panel.fullpage .messages > * {
838
+ width: 100%;
839
+ max-width: 760px;
840
+ margin-left: auto;
841
+ margin-right: auto;
842
+ }
843
+ .panel.fullpage .bubble { max-width: 100%; }
844
+ .panel.fullpage .bubble.user { align-self: flex-end; max-width: 80%; margin-right: auto; }
845
+ .panel.fullpage .bubble.assistant { align-self: flex-start; max-width: 100%; }
846
+
847
+ /* Sources panel — rendered under an assistant bubble whose terminal
848
+ eventual_response carried citations. */
849
+ .sources {
850
+ align-self: flex-start;
851
+ max-width: 80%;
852
+ margin-top: -4px;
853
+ font-size: 12.5px;
854
+ color: var(--sac-text);
855
+ }
856
+ .panel.fullpage .sources { max-width: 100%; }
857
+ .sources details { background: transparent; }
858
+ .sources summary {
859
+ cursor: pointer;
860
+ font-weight: 600;
861
+ opacity: 0.85;
862
+ list-style: none;
863
+ user-select: none;
864
+ padding: 2px 0;
865
+ }
866
+ .sources summary::-webkit-details-marker { display: none; }
867
+ .sources summary::before {
868
+ content: '▸';
869
+ display: inline-block;
870
+ margin-right: 6px;
871
+ transition: transform 0.15s ease;
872
+ }
873
+ .sources details[open] summary::before { transform: rotate(90deg); }
874
+ .sources ol {
875
+ margin: 6px 0 0;
876
+ padding-left: 0;
877
+ list-style: none;
878
+ display: flex;
879
+ flex-direction: column;
880
+ gap: 8px;
881
+ }
882
+ .sources li {
883
+ border-left: 2px solid var(--sac-primary);
884
+ padding-left: 10px;
885
+ }
886
+ .sources .src-title {
887
+ color: var(--sac-primary);
888
+ text-decoration: none;
889
+ font-weight: 600;
890
+ word-break: break-word;
891
+ }
892
+ .sources a.src-title:hover { text-decoration: underline; }
893
+ .sources span.src-title { color: var(--sac-text); opacity: 0.95; }
894
+ .sources .src-snippet {
895
+ display: block;
896
+ margin-top: 2px;
897
+ opacity: 0.7;
898
+ line-height: 1.4;
899
+ white-space: normal;
900
+ }
901
+
902
+ .cursor::after {
903
+ content: '▋';
904
+ margin-left: 1px;
905
+ animation: sac-blink 1s steps(2, start) infinite;
906
+ }
907
+ @keyframes sac-blink { to { visibility: hidden; } }
908
+
909
+ .composer {
910
+ display: flex;
911
+ gap: 8px;
912
+ padding: 10px;
913
+ border-top: 1px solid var(--sac-border);
914
+ }
915
+ .composer textarea {
916
+ flex: 1;
917
+ resize: none;
918
+ border: 1px solid var(--sac-border);
919
+ border-radius: 8px;
920
+ padding: 8px 10px;
921
+ font-family: inherit;
922
+ font-size: 14px;
923
+ background: transparent;
924
+ color: var(--sac-text);
925
+ max-height: 96px;
926
+ line-height: 1.4;
927
+ }
928
+ .composer textarea:focus { outline: 1px solid var(--sac-primary); }
929
+ .composer button {
930
+ border: none;
931
+ border-radius: 8px;
932
+ padding: 0 14px;
933
+ cursor: pointer;
934
+ background: var(--sac-primary);
935
+ color: var(--sac-primary-text);
936
+ font-weight: 600;
937
+ font-size: 14px;
938
+ }
939
+ .composer button:disabled { opacity: 0.5; cursor: default; }
940
+
941
+ .hidden { display: none !important; }
942
+ `;
943
+ }
944
+ //#endregion
945
+ //#region src/widget/element.ts
946
+ const ELEMENT_TAG = "smooth-agent-chat";
947
+ const OBSERVED = [
948
+ "endpoint",
949
+ "agent-id",
950
+ "agent-name",
951
+ "placeholder",
952
+ "greeting",
953
+ "start-open",
954
+ "mode"
955
+ ];
956
+ /**
957
+ * Return `url` only if it is a valid absolute `http(s)` URL, else `null`.
958
+ *
959
+ * SECURITY: citation URLs originate from indexed content (web / GitHub
960
+ * connectors), which can be attacker-influenceable. Assigning an arbitrary
961
+ * string to `<a>.href` allows `javascript:`/`data:`/`vbscript:` URLs that
962
+ * execute on click — a stored-XSS vector. Only http(s) links are rendered as
963
+ * anchors; anything else falls back to plain text.
964
+ */
965
+ function safeHttpUrl(url) {
966
+ if (!url) return null;
967
+ try {
968
+ const parsed = new URL(url);
969
+ return parsed.protocol === "http:" || parsed.protocol === "https:" ? parsed.href : null;
970
+ } catch {
971
+ return null;
972
+ }
973
+ }
974
+ var SmoothAgentChatElement = class extends HTMLElement {
975
+ static get observedAttributes() {
976
+ return OBSERVED;
977
+ }
978
+ root;
979
+ controller = null;
980
+ overrides = {};
981
+ open = false;
982
+ messages = [];
983
+ status = "idle";
984
+ mounted = false;
985
+ panelEl = null;
986
+ launcherEl = null;
987
+ messagesEl = null;
988
+ statusEl = null;
989
+ inputEl = null;
990
+ sendBtn = null;
991
+ constructor() {
992
+ super();
993
+ this.root = this.attachShadow({ mode: "open" });
994
+ }
995
+ connectedCallback() {
996
+ this.mounted = true;
997
+ this.render();
998
+ }
999
+ disconnectedCallback() {
1000
+ this.mounted = false;
1001
+ this.controller?.disconnect();
1002
+ this.controller = null;
1003
+ }
1004
+ attributeChangedCallback() {
1005
+ if (this.mounted) this.render();
1006
+ }
1007
+ /**
1008
+ * Programmatically merge config overrides (endpoint, agentId, theme, …). Values
1009
+ * set here take precedence over HTML attributes. Re-renders the widget.
1010
+ */
1011
+ configure(config) {
1012
+ this.overrides = {
1013
+ ...this.overrides,
1014
+ ...config
1015
+ };
1016
+ if (config.theme) this.overrides.theme = {
1017
+ ...this.overrides.theme ?? {},
1018
+ ...config.theme
1019
+ };
1020
+ if (this.mounted) this.render();
1021
+ }
1022
+ /** Open the chat panel. */
1023
+ openChat() {
1024
+ this.open = true;
1025
+ this.syncOpenState();
1026
+ this.controller?.connect().catch(() => {});
1027
+ }
1028
+ /** Collapse the chat panel back to the launcher. */
1029
+ closeChat() {
1030
+ this.open = false;
1031
+ this.syncOpenState();
1032
+ }
1033
+ readConfig() {
1034
+ const endpoint = this.overrides.endpoint ?? this.getAttribute("endpoint") ?? "";
1035
+ const agentId = this.overrides.agentId ?? this.getAttribute("agent-id") ?? "";
1036
+ if (!endpoint || !agentId) return null;
1037
+ const theme = this.overrides.theme;
1038
+ const modeAttr = this.getAttribute("mode");
1039
+ return {
1040
+ endpoint,
1041
+ mode: this.overrides.mode ?? (modeAttr === "fullpage" ? "fullpage" : modeAttr === "popover" ? "popover" : void 0) ?? "popover",
1042
+ agentId,
1043
+ agentName: this.overrides.agentName ?? this.getAttribute("agent-name") ?? void 0,
1044
+ userName: this.overrides.userName,
1045
+ userEmail: this.overrides.userEmail,
1046
+ placeholder: this.overrides.placeholder ?? this.getAttribute("placeholder") ?? void 0,
1047
+ greeting: this.overrides.greeting ?? this.getAttribute("greeting") ?? void 0,
1048
+ connectionErrorMessage: this.overrides.connectionErrorMessage,
1049
+ startOpen: this.overrides.startOpen ?? this.hasAttribute("start-open"),
1050
+ theme
1051
+ };
1052
+ }
1053
+ render() {
1054
+ const config = this.readConfig();
1055
+ if (!config) {
1056
+ this.root.innerHTML = "";
1057
+ return;
1058
+ }
1059
+ const resolved = resolveConfig(config);
1060
+ if (!this.controller) {
1061
+ this.controller = new ConversationController(config, {
1062
+ onMessages: (messages) => {
1063
+ this.messages = messages;
1064
+ this.renderMessages(resolved.greeting);
1065
+ },
1066
+ onStatus: (status) => {
1067
+ this.status = status;
1068
+ this.renderStatus();
1069
+ this.renderComposerState();
1070
+ }
1071
+ });
1072
+ if (resolved.startOpen) this.open = true;
1073
+ }
1074
+ const fullpage = resolved.mode === "fullpage";
1075
+ if (fullpage) this.open = true;
1076
+ const style = document.createElement("style");
1077
+ style.textContent = buildStyles(resolved.theme, resolved.mode);
1078
+ const headerBrand = fullpage ? `<div class="brand">
1079
+ <span class="logo-wrap">${SMOOTH_LOGO_SVG}</span>
1080
+ <div>
1081
+ <div class="title">${escapeHtml(resolved.agentName)}</div>
1082
+ <div class="status"></div>
1083
+ </div>
1084
+ </div>
1085
+ <div class="powered">powered by smooth-operator</div>` : `<div class="brand">
1086
+ <div>
1087
+ <div class="title">${escapeHtml(resolved.agentName)}</div>
1088
+ <div class="status"></div>
1089
+ </div>
1090
+ </div>
1091
+ <button class="close" aria-label="Close chat">×</button>`;
1092
+ const container = document.createElement("div");
1093
+ container.innerHTML = `
1094
+ ${fullpage ? "" : "<button class=\"launcher\" part=\"launcher\" aria-label=\"Open chat\">💬</button>"}
1095
+ <div class="panel${fullpage ? " fullpage" : " hidden"}" part="panel" role="${fullpage ? "region" : "dialog"}" aria-label="${escapeHtml(resolved.agentName)} chat">
1096
+ <div class="header">
1097
+ ${headerBrand}
1098
+ </div>
1099
+ <div class="messages"></div>
1100
+ <div class="composer">
1101
+ <textarea rows="1" placeholder="${escapeHtml(resolved.placeholder)}"></textarea>
1102
+ <button class="send" type="button">Send</button>
1103
+ </div>
1104
+ </div>
1105
+ `;
1106
+ const logoSvg = container.querySelector(".logo-wrap svg");
1107
+ if (logoSvg) logoSvg.setAttribute("class", "logo");
1108
+ this.root.replaceChildren(style, container);
1109
+ this.launcherEl = container.querySelector(".launcher");
1110
+ this.panelEl = container.querySelector(".panel");
1111
+ this.messagesEl = container.querySelector(".messages");
1112
+ this.statusEl = container.querySelector(".status");
1113
+ this.inputEl = container.querySelector("textarea");
1114
+ this.sendBtn = container.querySelector(".send");
1115
+ this.launcherEl?.addEventListener("click", () => this.openChat());
1116
+ container.querySelector(".close")?.addEventListener("click", () => this.closeChat());
1117
+ this.sendBtn?.addEventListener("click", () => this.submit());
1118
+ this.inputEl?.addEventListener("keydown", (ev) => {
1119
+ if (ev.key === "Enter" && !ev.shiftKey) {
1120
+ ev.preventDefault();
1121
+ this.submit();
1122
+ }
1123
+ });
1124
+ if (fullpage) this.controller?.connect().catch(() => {});
1125
+ this.syncOpenState();
1126
+ this.renderMessages(resolved.greeting);
1127
+ this.renderStatus();
1128
+ this.renderComposerState();
1129
+ }
1130
+ syncOpenState() {
1131
+ if (this.panelEl?.classList.contains("fullpage")) {
1132
+ this.inputEl?.focus();
1133
+ return;
1134
+ }
1135
+ this.panelEl?.classList.toggle("hidden", !this.open);
1136
+ this.launcherEl?.classList.toggle("hidden", this.open);
1137
+ if (this.open) this.inputEl?.focus();
1138
+ }
1139
+ renderMessages(greeting) {
1140
+ if (!this.messagesEl) return;
1141
+ this.messagesEl.replaceChildren();
1142
+ if (this.messages.length === 0 && greeting) {
1143
+ const g = document.createElement("div");
1144
+ g.className = "bubble assistant greeting";
1145
+ g.textContent = greeting;
1146
+ this.messagesEl.appendChild(g);
1147
+ }
1148
+ for (const msg of this.messages) {
1149
+ const el = document.createElement("div");
1150
+ el.className = `bubble ${msg.role}`;
1151
+ if (msg.streaming && !msg.text) el.classList.add("cursor");
1152
+ else if (msg.streaming) {
1153
+ el.classList.add("cursor");
1154
+ el.textContent = msg.text;
1155
+ } else el.textContent = msg.text;
1156
+ this.messagesEl.appendChild(el);
1157
+ if (msg.role === "assistant" && !msg.streaming && msg.citations && msg.citations.length > 0) this.messagesEl.appendChild(this.renderSources(msg.citations));
1158
+ }
1159
+ this.messagesEl.scrollTop = this.messagesEl.scrollHeight;
1160
+ }
1161
+ /**
1162
+ * Build the collapsible "Sources (N)" block for an assistant message's
1163
+ * citations. Each source renders its `title` (linked to `citation.url` when
1164
+ * present — `target=_blank rel=noopener` — plain text otherwise) plus the
1165
+ * grounding `snippet`. Built with DOM APIs (not innerHTML) so citation text
1166
+ * can't inject markup.
1167
+ */
1168
+ renderSources(citations) {
1169
+ const wrap = document.createElement("div");
1170
+ wrap.className = "sources";
1171
+ wrap.setAttribute("part", "sources");
1172
+ const details = document.createElement("details");
1173
+ details.open = true;
1174
+ const summary = document.createElement("summary");
1175
+ summary.textContent = `Sources (${citations.length})`;
1176
+ details.appendChild(summary);
1177
+ const list = document.createElement("ol");
1178
+ for (const c of citations) {
1179
+ const li = document.createElement("li");
1180
+ let titleEl;
1181
+ const safeUrl = safeHttpUrl(c.url);
1182
+ if (safeUrl) {
1183
+ const a = document.createElement("a");
1184
+ a.className = "src-title";
1185
+ a.href = safeUrl;
1186
+ a.target = "_blank";
1187
+ a.rel = "noopener noreferrer";
1188
+ titleEl = a;
1189
+ } else {
1190
+ titleEl = document.createElement("span");
1191
+ titleEl.className = "src-title";
1192
+ }
1193
+ titleEl.textContent = c.title || c.id || "Source";
1194
+ li.appendChild(titleEl);
1195
+ if (c.snippet) {
1196
+ const snip = document.createElement("span");
1197
+ snip.className = "src-snippet";
1198
+ snip.textContent = c.snippet;
1199
+ li.appendChild(snip);
1200
+ }
1201
+ list.appendChild(li);
1202
+ }
1203
+ details.appendChild(list);
1204
+ wrap.appendChild(details);
1205
+ return wrap;
1206
+ }
1207
+ renderStatus() {
1208
+ if (!this.statusEl) return;
1209
+ const label = {
1210
+ idle: "",
1211
+ connecting: "Connecting…",
1212
+ ready: "Online",
1213
+ error: "Connection issue",
1214
+ closed: "Disconnected"
1215
+ };
1216
+ this.statusEl.textContent = label[this.status];
1217
+ }
1218
+ renderComposerState() {
1219
+ const busy = this.status === "connecting";
1220
+ if (this.sendBtn) this.sendBtn.disabled = busy;
1221
+ if (this.inputEl) this.inputEl.disabled = busy;
1222
+ }
1223
+ submit() {
1224
+ if (!this.inputEl || !this.controller) return;
1225
+ const text = this.inputEl.value;
1226
+ if (!text.trim()) return;
1227
+ this.inputEl.value = "";
1228
+ this.controller.send(text);
1229
+ }
1230
+ };
1231
+ function escapeHtml(value) {
1232
+ return value.replace(/[&<>"']/g, (c) => {
1233
+ switch (c) {
1234
+ case "&": return "&amp;";
1235
+ case "<": return "&lt;";
1236
+ case ">": return "&gt;";
1237
+ case "\"": return "&quot;";
1238
+ default: return "&#39;";
1239
+ }
1240
+ });
1241
+ }
1242
+ /** Register the custom element once. Safe to call multiple times. */
1243
+ function defineChatWidget() {
1244
+ if (typeof customElements !== "undefined" && !customElements.get("smooth-agent-chat")) customElements.define(ELEMENT_TAG, SmoothAgentChatElement);
1245
+ }
1246
+ /**
1247
+ * Programmatically create, configure, and append a widget to the page.
1248
+ * Returns the element so the host can drive `openChat()` / `closeChat()`.
1249
+ */
1250
+ function mountChatWidget(config, target = document.body) {
1251
+ defineChatWidget();
1252
+ const el = document.createElement(ELEMENT_TAG);
1253
+ el.configure(config);
1254
+ target.appendChild(el);
1255
+ return el;
1256
+ }
1257
+ /**
1258
+ * Ergonomic helper for the full-page layout: mounts a `<smooth-agent-chat>` in
1259
+ * `mode: "fullpage"` (no launcher — the chat fills its container/viewport with a
1260
+ * Smooth-branded header, a scrollable message list, and an input bar) and
1261
+ * returns the element.
1262
+ *
1263
+ * `target` defaults to `document.body`; pass a sized container to embed the
1264
+ * full-page chat inside a layout region (e.g. a `/chat` route shell or an
1265
+ * iframe). The `mode` is forced to `"fullpage"` regardless of the passed config.
1266
+ *
1267
+ * ```ts
1268
+ * mountFullPageChat({ endpoint: 'wss://…/ws', agentId: '…', agentName: 'Support' });
1269
+ * ```
1270
+ */
1271
+ function mountFullPageChat(config, target = document.body) {
1272
+ return mountChatWidget({
1273
+ ...config,
1274
+ mode: "fullpage"
1275
+ }, target);
1276
+ }
1277
+ //#endregion
1278
+ //#region src/widget/standalone.ts
1279
+ defineChatWidget();
1280
+ /** Convenience alias matching the global API surface (`SmoothAgentChat.mount`). */
1281
+ function mount(config, target) {
1282
+ return mountChatWidget(config, target);
1283
+ }
1284
+ /**
1285
+ * Full-page convenience alias (`SmoothAgentChat.mountFullPage`): mounts the chat
1286
+ * in `mode: "fullpage"` so it fills its container/viewport with no launcher.
1287
+ */
1288
+ function mountFullPage(config, target) {
1289
+ return mountFullPageChat(config, target);
1290
+ }
1291
+ //#endregion
1292
+ exports.SmoothAgentChatElement = SmoothAgentChatElement;
1293
+ exports.defineChatWidget = defineChatWidget;
1294
+ exports.mount = mount;
1295
+ exports.mountChatWidget = mountChatWidget;
1296
+ exports.mountFullPage = mountFullPage;
1297
+ exports.mountFullPageChat = mountFullPageChat;
1298
+ return exports;
1299
+ })({});
1300
+
1301
+ //# sourceMappingURL=chat-widget.iife.js.map