@smooai/chat-widget 0.3.0 → 0.5.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 CHANGED
@@ -1,20 +1,26 @@
1
- <div align="center">
1
+ <p align="center">
2
+ <a href="https://smoo.ai"><img src=".github/banner.png" alt="@smooai/chat-widget — Embeddable AI chat as a web component" width="100%" /></a>
3
+ </p>
2
4
 
3
- # @smooai/chat-widget
5
+ <p align="center">
6
+ <a href="https://www.npmjs.com/package/@smooai/chat-widget"><img src="https://img.shields.io/npm/v/@smooai/chat-widget?style=for-the-badge&color=00A6A6&label=npm&logo=npm&logoColor=white&labelColor=020618" alt="npm"></a>
7
+ <a href="https://smoo.ai/platform/support"><img src="https://img.shields.io/badge/Smoo_AI-platform-00A6A6?style=for-the-badge&labelColor=020618" alt="Smoo AI"></a>
8
+ <a href="./LICENSE"><img src="https://img.shields.io/badge/license-MIT-F49F0A?style=for-the-badge&labelColor=020618" alt="license"></a>
9
+ </p>
4
10
 
5
- ### Embeddable AI chat as a framework-light web component.
11
+ <p align="center">
12
+ <img src="https://img.shields.io/badge/gzip-~19%20kB-FF6B6C?style=flat-square" alt="bundle size">
13
+ <a href="https://github.com/SmooAI/chat-widget/actions/workflows/ci.yml"><img src="https://github.com/SmooAI/chat-widget/actions/workflows/ci.yml/badge.svg" alt="CI"></a>
14
+ </p>
6
15
 
7
- **Streaming replies · grounded sources · popover or full-page · inherits _your_ brand color.**
8
-
9
- [![npm](https://img.shields.io/npm/v/@smooai/chat-widget?color=00a6a6&label=npm)](https://www.npmjs.com/package/@smooai/chat-widget)
10
- [![CI](https://github.com/SmooAI/chat-widget/actions/workflows/ci.yml/badge.svg)](https://github.com/SmooAI/chat-widget/actions/workflows/ci.yml)
11
- [![bundle](https://img.shields.io/badge/gzip-~19%20kB-4f8bff)](https://github.com/SmooAI/chat-widget)
12
- [![license](https://img.shields.io/badge/license-MIT-8b5cf6)](./LICENSE)
13
-
14
- </div>
16
+ <p align="center">
17
+ <a href="#why-this-widget"><b>Features</b></a> &nbsp;·&nbsp; <a href="#install"><b>Install</b></a> &nbsp;·&nbsp; <a href="#quick-start"><b>Usage</b></a> &nbsp;·&nbsp; <a href="#theming"><b>Theming</b></a> &nbsp;·&nbsp; <a href="#part-of-smoo-ai"><b>Platform</b></a>
18
+ </p>
15
19
 
16
20
  ---
17
21
 
22
+ > A self-contained, streaming chat surface you drop into any site with one tag. No React on the host page, no CSS to import, ~19 kB gzipped, with the smooth-operator protocol client built in.
23
+
18
24
  `<smooth-agent-chat>` is a self-contained chat web component for the
19
25
  [smooth-operator](https://github.com/SmooAI/smooth-operator) protocol. Drop in one
20
26
  `<script>` tag (or import the ESM module) and you get a polished, streaming,
@@ -60,7 +66,36 @@ installed automatically.
60
66
 
61
67
  ## Quick start
62
68
 
63
- ### 1. Declarativea `<script>` tag + the element
69
+ ### 1. Recommendedthe deferred loader (best Core Web Vitals)
70
+
71
+ Include the tiny **loader** (~2 KB gzipped) eagerly. It does nothing expensive on
72
+ load — it waits until the page is past its critical render (`requestIdleCallback`
73
+ after `load`, **or** the visitor's first pointer/keydown/scroll, **or** an 8 s
74
+ fallback), and only *then* injects the full widget module. The widget never
75
+ competes with your page's LCP/TBT, but it's instantly there the moment a visitor
76
+ shows intent.
77
+
78
+ ```html
79
+ <script>
80
+ window.SmoothAgentChatConfig = {
81
+ endpoint: 'wss://your-host/ws',
82
+ agentId: '11111111-1111-4111-8111-111111111111',
83
+ agentName: 'Support',
84
+ greeting: 'Hi! How can I help?',
85
+ };
86
+ </script>
87
+ <script src="https://unpkg.com/@smooai/chat-widget/dist/chat-widget-loader.global.js" async></script>
88
+ ```
89
+
90
+ The loader resolves the widget module as a sibling of its own URL (override with
91
+ `data-src`), so a single CDN path is all you need. For the simplest installs you
92
+ can skip the config global and use `data-*` attributes on the loader tag
93
+ (`data-endpoint`, `data-agent-id`, `data-primary`, `data-mode`).
94
+
95
+ ### 2. Declarative — eager `<script>` + the element
96
+
97
+ Simplest, but the module evaluates during page load — prefer the loader above on
98
+ performance-sensitive (especially marketing) pages.
64
99
 
65
100
  ```html
66
101
  <script src="https://unpkg.com/@smooai/chat-widget/dist/chat-widget.global.js"></script>
@@ -75,7 +110,7 @@ installed automatically.
75
110
 
76
111
  The IIFE bundle auto-registers the element on load.
77
112
 
78
- ### 2. Programmatic — bundler / ESM
113
+ ### 3. Programmatic — bundler / ESM
79
114
 
80
115
  ```ts
81
116
  import { mountChatWidget } from '@smooai/chat-widget';
@@ -155,12 +190,15 @@ mountChatWidget({
155
190
  the collected name/email are attached to the conversation session (phone rides
156
191
  session metadata). Set **`allowAnonymous: true`** to skip the form.
157
192
 
158
- ### OTP & tool confirmation (programmatic)
193
+ ### OTP & tool confirmation (HITL)
159
194
 
160
- The `ConversationController` surfaces mid-turn pauses via an `onInterrupt`
161
- callback `otp_verification_required` and `write_confirmation_required`and
162
- resumes them with `verifyOtp(code)` / `confirmTool(approved)`. (A built-in dialog
163
- UI for these is on the roadmap; the protocol plumbing ships today.)
195
+ When a turn pauses for **OTP verification** or a **tool-write approval**, the
196
+ widget shows an inline overlay above the composer an OTP code prompt (with
197
+ masked destination + retry state) or an Approve/Decline confirmation and
198
+ resumes the turn automatically. Under the hood the `ConversationController`
199
+ surfaces `otp_verification_required` / `write_confirmation_required` via an
200
+ `onInterrupt` callback and resumes with `verifyOtp(code)` / `confirmTool(approved)`,
201
+ so you can also drive it programmatically.
164
202
 
165
203
  ## Full-page mode
166
204
 
@@ -232,6 +270,25 @@ pnpm test:e2e # Playwright live e2e (gated — see e2e/)
232
270
  Open `index.html` (e.g. `pnpm dlx serve .`) after a build for the interactive,
233
271
  backend-free showcase.
234
272
 
273
+ ## Part of Smoo AI
274
+
275
+ `@smooai/chat-widget` is built and open-sourced by **[Smoo AI](https://smoo.ai)** — the AI-powered business platform with AI built into every product: CRM, customer support, campaigns, field service, observability, and developer tools.
276
+
277
+ - 🚀 **Customer support on the platform** — [smoo.ai/platform/support](https://smoo.ai/platform/support)
278
+ - 🧰 **More open source from Smoo AI** — [smoo.ai/open-source](https://smoo.ai/open-source)
279
+ - 🧩 **Sibling repos** — [smooth-operator](https://github.com/SmooAI/smooth-operator) (the protocol it renders), [smooth-operator-core](https://github.com/SmooAI/smooth-operator-core) (the engine), [@smooai/config](https://github.com/SmooAI/config), [@smooai/logger](https://github.com/SmooAI/logger)
280
+
281
+ ## Contributing
282
+
283
+ Issues and PRs welcome. Run `pnpm check` (typecheck + test + build) before opening
284
+ a PR.
285
+
235
286
  ## License
236
287
 
237
- MIT © [SmooAI](https://github.com/SmooAI)
288
+ MIT © [Smoo AI](https://smoo.ai)
289
+
290
+ ---
291
+
292
+ <p align="center">
293
+ Built by <a href="https://smoo.ai"><strong>Smoo AI</strong></a> — AI built into every product.
294
+ </p>
@@ -0,0 +1,157 @@
1
+ (function() {
2
+ //#region src/loader-core.ts
3
+ /**
4
+ * Tiny, dependency-free deferred loader for `@smooai/chat-widget`.
5
+ *
6
+ * The recommended embed: a host includes this *small* script eagerly, and it
7
+ * defers injecting the real (heavier) `chat-widget.global.js` module until the
8
+ * page is past its critical render — so the widget never competes with the host
9
+ * page's LCP/TBT. It triggers the real load on the **first** of:
10
+ *
11
+ * - `window` `load` → `requestIdleCallback` (idle time), or
12
+ * - user intent (`pointerdown` / `keydown` / `scroll`), or
13
+ * - an 8s fallback.
14
+ *
15
+ * After the module loads (which registers `<smooth-agent-chat>` and exposes
16
+ * `window.SmoothAgentChat`), it mounts the widget with the host's config.
17
+ *
18
+ * Deliberately imports nothing from the widget itself — this file must stay a
19
+ * few hundred bytes so including it eagerly is free.
20
+ *
21
+ * ## Config
22
+ * Set a global before/with the loader (any `ChatWidgetConfig`, plus an optional
23
+ * `src` pointing at the module bundle):
24
+ *
25
+ * ```html
26
+ * <script>window.SmoothAgentChatConfig = { endpoint: 'wss://…/ws', agentId: '…' };<\/script>
27
+ * <script src="https://cdn/…/chat-widget-loader.global.js" async><\/script>
28
+ * ```
29
+ *
30
+ * Or, for the simplest installs, `data-*` attributes on the loader's own tag
31
+ * (`data-endpoint`, `data-agent-id`, `data-primary`, `data-mode`, `data-src`).
32
+ * If no config is found, the module is still loaded (registering the element) so
33
+ * a `<smooth-agent-chat …>` placed directly in markup upgrades itself.
34
+ */
35
+ /** Must match `ELEMENT_TAG` in `element.ts`; inlined to avoid importing it. */
36
+ const ELEMENT_TAG = "smooth-agent-chat";
37
+ /** Default module filename, resolved as a sibling of the loader script. */
38
+ const DEFAULT_MODULE = "chat-widget.global.js";
39
+ /**
40
+ * Resolve the module URL: explicit `src` (config or `data-src`) wins, else a
41
+ * sibling of the loader script, else the bare default filename.
42
+ */
43
+ function resolveModuleSrc(win, script) {
44
+ const fromConfig = win.SmoothAgentChatConfig?.src;
45
+ if (fromConfig) return fromConfig;
46
+ const fromAttr = script?.getAttribute("data-src");
47
+ if (fromAttr) return fromAttr;
48
+ const selfSrc = script?.src;
49
+ if (selfSrc) return selfSrc.replace(/[^/]*$/, DEFAULT_MODULE);
50
+ return DEFAULT_MODULE;
51
+ }
52
+ /**
53
+ * The widget mount config: the `SmoothAgentChatConfig` global (minus the
54
+ * loader-only `src`), or `data-*` attributes. `undefined` ⇒ no programmatic
55
+ * mount (a markup element will upgrade itself once the module registers it).
56
+ */
57
+ function resolveMountConfig(win, script) {
58
+ if (win.SmoothAgentChatConfig) {
59
+ const { src: _src, ...config } = win.SmoothAgentChatConfig;
60
+ return Object.keys(config).length > 0 ? config : void 0;
61
+ }
62
+ if (!script) return void 0;
63
+ const endpoint = script.getAttribute("data-endpoint");
64
+ const agentId = script.getAttribute("data-agent-id");
65
+ if (!endpoint && !agentId) return void 0;
66
+ const config = {};
67
+ if (endpoint) config.endpoint = endpoint;
68
+ if (agentId) config.agentId = agentId;
69
+ const primary = script.getAttribute("data-primary");
70
+ if (primary) config.theme = { primary };
71
+ const mode = script.getAttribute("data-mode");
72
+ if (mode) config.mode = mode;
73
+ return config;
74
+ }
75
+ /**
76
+ * Install the deferred loader. Idempotent per call; the IIFE entry
77
+ * (`loader.ts`) invokes it once on script execution.
78
+ */
79
+ function initChatWidgetLoader() {
80
+ const win = window;
81
+ const script = document.currentScript ?? null;
82
+ const moduleSrc = resolveModuleSrc(win, script);
83
+ let loaded = false;
84
+ let idleId;
85
+ let fallbackId;
86
+ function mountWhenReady() {
87
+ const config = resolveMountConfig(win, script);
88
+ if (!config) return;
89
+ const apply = () => {
90
+ try {
91
+ win.SmoothAgentChat?.mount(config);
92
+ } catch (error) {
93
+ console.error("[chat-widget loader] mount failed", error);
94
+ }
95
+ };
96
+ if (win.SmoothAgentChat) apply();
97
+ else if (window.customElements?.whenDefined) window.customElements.whenDefined(ELEMENT_TAG).then(apply, apply);
98
+ else apply();
99
+ }
100
+ function teardown() {
101
+ if (idleId !== void 0) {
102
+ window.cancelIdleCallback?.(idleId);
103
+ window.clearTimeout(idleId);
104
+ }
105
+ if (fallbackId !== void 0) window.clearTimeout(fallbackId);
106
+ window.removeEventListener("pointerdown", load);
107
+ window.removeEventListener("keydown", load);
108
+ window.removeEventListener("scroll", load);
109
+ }
110
+ function load() {
111
+ if (loaded) return;
112
+ loaded = true;
113
+ teardown();
114
+ const tag = document.createElement("script");
115
+ tag.src = moduleSrc;
116
+ tag.onload = mountWhenReady;
117
+ tag.onerror = () => console.error("[chat-widget loader] failed to load", moduleSrc);
118
+ document.head.appendChild(tag);
119
+ }
120
+ function schedule() {
121
+ if (window.requestIdleCallback) idleId = window.requestIdleCallback(() => load(), { timeout: 5e3 });
122
+ else idleId = window.setTimeout(load, 2500);
123
+ fallbackId = window.setTimeout(load, 8e3);
124
+ }
125
+ window.addEventListener("pointerdown", load, {
126
+ once: true,
127
+ passive: true
128
+ });
129
+ window.addEventListener("keydown", load, { once: true });
130
+ window.addEventListener("scroll", load, {
131
+ once: true,
132
+ passive: true
133
+ });
134
+ if (document.readyState === "complete") schedule();
135
+ else window.addEventListener("load", schedule, { once: true });
136
+ }
137
+ //#endregion
138
+ //#region src/loader.ts
139
+ /**
140
+ * Standalone IIFE entry for the **deferred loader** — built to
141
+ * `dist/chat-widget-loader.global.js`.
142
+ *
143
+ * This is the recommended embed: include it eagerly (it's tiny) and it lazily
144
+ * injects the real `chat-widget.global.js` module only once the host page is
145
+ * past its critical render (idle / user intent / 8s fallback), so the widget
146
+ * never competes with the host's LCP/TBT. See {@link initChatWidgetLoader}.
147
+ *
148
+ * ```html
149
+ * <script>window.SmoothAgentChatConfig = { endpoint: 'wss://…/ws', agentId: '…' };<\/script>
150
+ * <script src="https://cdn/…/chat-widget-loader.global.js" async><\/script>
151
+ * ```
152
+ */
153
+ initChatWidgetLoader();
154
+ //#endregion
155
+ })();
156
+
157
+ //# sourceMappingURL=chat-widget-loader.global.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"chat-widget-loader.global.js","names":[],"sources":["../src/loader-core.ts","../src/loader.ts"],"sourcesContent":["/**\n * Tiny, dependency-free deferred loader for `@smooai/chat-widget`.\n *\n * The recommended embed: a host includes this *small* script eagerly, and it\n * defers injecting the real (heavier) `chat-widget.global.js` module until the\n * page is past its critical render — so the widget never competes with the host\n * page's LCP/TBT. It triggers the real load on the **first** of:\n *\n * - `window` `load` → `requestIdleCallback` (idle time), or\n * - user intent (`pointerdown` / `keydown` / `scroll`), or\n * - an 8s fallback.\n *\n * After the module loads (which registers `<smooth-agent-chat>` and exposes\n * `window.SmoothAgentChat`), it mounts the widget with the host's config.\n *\n * Deliberately imports nothing from the widget itself — this file must stay a\n * few hundred bytes so including it eagerly is free.\n *\n * ## Config\n * Set a global before/with the loader (any `ChatWidgetConfig`, plus an optional\n * `src` pointing at the module bundle):\n *\n * ```html\n * <script>window.SmoothAgentChatConfig = { endpoint: 'wss://…/ws', agentId: '…' };</script>\n * <script src=\"https://cdn/…/chat-widget-loader.global.js\" async></script>\n * ```\n *\n * Or, for the simplest installs, `data-*` attributes on the loader's own tag\n * (`data-endpoint`, `data-agent-id`, `data-primary`, `data-mode`, `data-src`).\n * If no config is found, the module is still loaded (registering the element) so\n * a `<smooth-agent-chat …>` placed directly in markup upgrades itself.\n */\n\n/** Must match `ELEMENT_TAG` in `element.ts`; inlined to avoid importing it. */\nconst ELEMENT_TAG = 'smooth-agent-chat';\n/** Default module filename, resolved as a sibling of the loader script. */\nconst DEFAULT_MODULE = 'chat-widget.global.js';\n\ntype MountFn = (config: Record<string, unknown>, target?: HTMLElement) => unknown;\n\ninterface LoaderWindow extends Window {\n SmoothAgentChat?: { mount: MountFn };\n SmoothAgentChatConfig?: Record<string, unknown> & { src?: string };\n}\n\n/**\n * Resolve the module URL: explicit `src` (config or `data-src`) wins, else a\n * sibling of the loader script, else the bare default filename.\n */\nfunction resolveModuleSrc(win: LoaderWindow, script: HTMLScriptElement | null): string {\n const fromConfig = win.SmoothAgentChatConfig?.src;\n if (fromConfig) return fromConfig;\n const fromAttr = script?.getAttribute('data-src');\n if (fromAttr) return fromAttr;\n const selfSrc = script?.src;\n if (selfSrc) return selfSrc.replace(/[^/]*$/, DEFAULT_MODULE);\n return DEFAULT_MODULE;\n}\n\n/**\n * The widget mount config: the `SmoothAgentChatConfig` global (minus the\n * loader-only `src`), or `data-*` attributes. `undefined` ⇒ no programmatic\n * mount (a markup element will upgrade itself once the module registers it).\n */\nfunction resolveMountConfig(win: LoaderWindow, script: HTMLScriptElement | null): Record<string, unknown> | undefined {\n if (win.SmoothAgentChatConfig) {\n const { src: _src, ...config } = win.SmoothAgentChatConfig;\n // Only `src` (no real fields) ⇒ no programmatic mount; a markup element\n // upgrades itself once the module registers it.\n return Object.keys(config).length > 0 ? config : undefined;\n }\n if (!script) return undefined;\n const endpoint = script.getAttribute('data-endpoint');\n const agentId = script.getAttribute('data-agent-id');\n if (!endpoint && !agentId) return undefined;\n const config: Record<string, unknown> = {};\n if (endpoint) config.endpoint = endpoint;\n if (agentId) config.agentId = agentId;\n const primary = script.getAttribute('data-primary');\n if (primary) config.theme = { primary };\n const mode = script.getAttribute('data-mode');\n if (mode) config.mode = mode;\n return config;\n}\n\n/**\n * Install the deferred loader. Idempotent per call; the IIFE entry\n * (`loader.ts`) invokes it once on script execution.\n */\nexport function initChatWidgetLoader(): void {\n const win = window as LoaderWindow;\n const script = (document.currentScript as HTMLScriptElement | null) ?? null;\n const moduleSrc = resolveModuleSrc(win, script);\n\n let loaded = false;\n let idleId: number | undefined;\n let fallbackId: number | undefined;\n\n function mountWhenReady(): void {\n const config = resolveMountConfig(win, script);\n if (!config) return; // markup element upgrades itself; nothing to mount\n const apply = () => {\n try {\n win.SmoothAgentChat?.mount(config);\n } catch (error) {\n console.error('[chat-widget loader] mount failed', error);\n }\n };\n if (win.SmoothAgentChat) {\n apply();\n } else if (window.customElements?.whenDefined) {\n window.customElements.whenDefined(ELEMENT_TAG).then(apply, apply);\n } else {\n apply();\n }\n }\n\n function teardown(): void {\n if (idleId !== undefined) {\n // The id is either an idle handle or (fallback path) a timeout id;\n // both cancels are no-ops on the wrong kind, so calling both is safe.\n window.cancelIdleCallback?.(idleId);\n window.clearTimeout(idleId);\n }\n if (fallbackId !== undefined) window.clearTimeout(fallbackId);\n window.removeEventListener('pointerdown', load);\n window.removeEventListener('keydown', load);\n window.removeEventListener('scroll', load);\n }\n\n function load(): void {\n if (loaded) return;\n loaded = true;\n teardown();\n const tag = document.createElement('script');\n tag.src = moduleSrc;\n tag.onload = mountWhenReady;\n tag.onerror = () => console.error('[chat-widget loader] failed to load', moduleSrc);\n document.head.appendChild(tag);\n }\n\n function schedule(): void {\n if (window.requestIdleCallback) {\n idleId = window.requestIdleCallback(() => load(), { timeout: 5000 });\n } else {\n idleId = window.setTimeout(load, 2500) as unknown as number;\n }\n fallbackId = window.setTimeout(load, 8000) as unknown as number;\n }\n\n // User intent loads immediately (they're about to want the widget).\n window.addEventListener('pointerdown', load, { once: true, passive: true });\n window.addEventListener('keydown', load, { once: true });\n window.addEventListener('scroll', load, { once: true, passive: true });\n\n if (document.readyState === 'complete') {\n schedule();\n } else {\n window.addEventListener('load', schedule, { once: true });\n }\n}\n","/**\n * Standalone IIFE entry for the **deferred loader** — built to\n * `dist/chat-widget-loader.global.js`.\n *\n * This is the recommended embed: include it eagerly (it's tiny) and it lazily\n * injects the real `chat-widget.global.js` module only once the host page is\n * past its critical render (idle / user intent / 8s fallback), so the widget\n * never competes with the host's LCP/TBT. See {@link initChatWidgetLoader}.\n *\n * ```html\n * <script>window.SmoothAgentChatConfig = { endpoint: 'wss://…/ws', agentId: '…' };</script>\n * <script src=\"https://cdn/…/chat-widget-loader.global.js\" async></script>\n * ```\n */\nimport { initChatWidgetLoader } from './loader-core.js';\n\ninitChatWidgetLoader();\n"],"mappings":"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"}
@@ -1305,6 +1305,63 @@ var SmoothAgentChat = (function(exports) {
1305
1305
  transform: translateY(-1px);
1306
1306
  }
1307
1307
 
1308
+ /* ─────────────── OTP / tool-confirmation interrupt ────────────────── */
1309
+ .interrupt { padding: 0 14px; }
1310
+ .int-card {
1311
+ border: 1px solid color-mix(in srgb, var(--sac-primary) 35%, var(--sac-border));
1312
+ background: color-mix(in srgb, var(--sac-primary) 8%, var(--sac-surface-2));
1313
+ border-radius: 14px;
1314
+ padding: 12px 13px;
1315
+ animation: sac-msg-in .3s var(--sac-ease) both;
1316
+ }
1317
+ .int-head { display: flex; align-items: center; gap: 8px; }
1318
+ .int-ico { display: flex; color: var(--sac-primary); }
1319
+ .int-ico svg { width: 17px; height: 17px; }
1320
+ .int-title { font-size: 13.5px; font-weight: 650; }
1321
+ .int-desc { margin-top: 5px; font-size: 12.5px; line-height: 1.45; color: color-mix(in srgb, var(--sac-text) 80%, transparent); }
1322
+ .int-sent { margin-top: 6px; font-size: 11.5px; color: color-mix(in srgb, var(--sac-text) 60%, transparent); }
1323
+ .int-row { display: flex; gap: 8px; margin-top: 10px; }
1324
+ .int-input {
1325
+ flex: 1;
1326
+ min-width: 0;
1327
+ border: 1px solid color-mix(in srgb, var(--sac-border) 80%, transparent);
1328
+ background: var(--sac-bg);
1329
+ color: var(--sac-text);
1330
+ border-radius: 10px;
1331
+ padding: 9px 11px;
1332
+ font-family: inherit;
1333
+ font-size: 14px;
1334
+ letter-spacing: .14em;
1335
+ outline: none;
1336
+ transition: border-color .2s ease, box-shadow .2s ease;
1337
+ }
1338
+ .int-input:focus {
1339
+ border-color: color-mix(in srgb, var(--sac-primary) 60%, transparent);
1340
+ box-shadow: 0 0 0 4px color-mix(in srgb, var(--sac-primary) 14%, transparent);
1341
+ }
1342
+ .int-btn {
1343
+ border: 1px solid color-mix(in srgb, var(--sac-border) 80%, transparent);
1344
+ background: var(--sac-surface-2);
1345
+ color: var(--sac-text);
1346
+ border-radius: 10px;
1347
+ padding: 9px 14px;
1348
+ font-family: inherit;
1349
+ font-size: 13px;
1350
+ font-weight: 600;
1351
+ cursor: pointer;
1352
+ transition: transform .2s var(--sac-ease), background .2s ease, border-color .2s ease;
1353
+ }
1354
+ .int-btn:hover { transform: translateY(-1px); }
1355
+ .int-btn.primary {
1356
+ border: none;
1357
+ background: linear-gradient(150deg, var(--sac-primary), var(--sac-primary-2));
1358
+ color: var(--sac-primary-text);
1359
+ box-shadow: 0 6px 14px -6px color-mix(in srgb, var(--sac-primary) 65%, transparent);
1360
+ }
1361
+ .int-row .int-btn { flex: 1; }
1362
+ .int-row .int-input + .int-btn { flex: 0 0 auto; }
1363
+ .int-error { margin-top: 8px; font-size: 12px; color: #f87171; }
1364
+
1308
1365
  .hidden { display: none !important; }
1309
1366
 
1310
1367
  @media (prefers-reduced-motion: reduce) {
@@ -1339,7 +1396,11 @@ var SmoothAgentChat = (function(exports) {
1339
1396
  /** Send — an upward arrow. */
1340
1397
  send: `<svg viewBox="0 0 24 24" fill="none" xmlns="http://www.w3.org/2000/svg"><path d="M12 19V6M12 6l-5.5 5.5M12 6l5.5 5.5" stroke="currentColor" stroke-width="1.9" stroke-linecap="round" stroke-linejoin="round"/></svg>`,
1341
1398
  /** Sources disclosure caret. */
1342
- chev: `<svg width="11" height="11" viewBox="0 0 24 24" fill="none"><path d="m9 6 6 6-6 6" stroke="currentColor" stroke-width="2.2" stroke-linecap="round" stroke-linejoin="round"/></svg>`
1399
+ chev: `<svg width="11" height="11" viewBox="0 0 24 24" fill="none"><path d="m9 6 6 6-6 6" stroke="currentColor" stroke-width="2.2" stroke-linecap="round" stroke-linejoin="round"/></svg>`,
1400
+ /** OTP interrupt — a padlock. */
1401
+ lock: `<svg viewBox="0 0 24 24" fill="none" xmlns="http://www.w3.org/2000/svg"><rect x="5" y="10.5" width="14" height="9.5" rx="2.2" stroke="currentColor" stroke-width="1.7"/><path d="M8 10.5V8a4 4 0 0 1 8 0v2.5" stroke="currentColor" stroke-width="1.7"/></svg>`,
1402
+ /** Tool-confirmation interrupt — a shield. */
1403
+ 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>`
1343
1404
  };
1344
1405
  /**
1345
1406
  * Return `url` only if it is a valid absolute `http(s)` URL, else `null`.
@@ -1376,6 +1437,9 @@ var SmoothAgentChat = (function(exports) {
1376
1437
  hasSent = false;
1377
1438
  /** Starter prompts shown as chips in the empty state. */
1378
1439
  examplePrompts = [];
1440
+ /** Current mid-turn interrupt (OTP / tool-confirmation), or null. */
1441
+ interrupt = null;
1442
+ interruptEl = null;
1379
1443
  panelEl = null;
1380
1444
  launcherEl = null;
1381
1445
  messagesEl = null;
@@ -1468,6 +1532,10 @@ var SmoothAgentChat = (function(exports) {
1468
1532
  this.status = status;
1469
1533
  this.renderStatus();
1470
1534
  this.renderComposerState();
1535
+ },
1536
+ onInterrupt: (interrupt) => {
1537
+ this.interrupt = interrupt;
1538
+ this.renderInterrupt();
1471
1539
  }
1472
1540
  });
1473
1541
  if (resolved.startOpen) this.open = true;
@@ -1510,6 +1578,7 @@ var SmoothAgentChat = (function(exports) {
1510
1578
  </div>`;
1511
1579
  const chatHtml = `
1512
1580
  <div class="messages"></div>
1581
+ <div class="interrupt hidden"></div>
1513
1582
  <div class="composer-wrap">
1514
1583
  <div class="composer">
1515
1584
  <textarea rows="1" placeholder="${escapeHtml(resolved.placeholder)}"></textarea>
@@ -1536,6 +1605,7 @@ var SmoothAgentChat = (function(exports) {
1536
1605
  this.dotEl = container.querySelector(".dot");
1537
1606
  this.inputEl = container.querySelector("textarea");
1538
1607
  this.sendBtn = container.querySelector(".send");
1608
+ this.interruptEl = container.querySelector(".interrupt");
1539
1609
  this.launcherEl?.addEventListener("click", () => this.openChat());
1540
1610
  container.querySelector(".close")?.addEventListener("click", () => this.closeChat());
1541
1611
  this.sendBtn?.addEventListener("click", () => this.submit());
@@ -1556,6 +1626,97 @@ var SmoothAgentChat = (function(exports) {
1556
1626
  if (!gating) this.renderMessages(resolved.greeting);
1557
1627
  this.renderStatus();
1558
1628
  this.renderComposerState();
1629
+ this.renderInterrupt();
1630
+ }
1631
+ /**
1632
+ * Render (or clear) the mid-turn interrupt overlay above the composer:
1633
+ * an OTP code prompt or a tool-write confirmation. Server-supplied text is
1634
+ * set via `textContent` (never innerHTML); only static icons use innerHTML.
1635
+ */
1636
+ renderInterrupt() {
1637
+ const el = this.interruptEl;
1638
+ if (!el) return;
1639
+ el.replaceChildren();
1640
+ const it = this.interrupt;
1641
+ if (!it) {
1642
+ el.classList.add("hidden");
1643
+ return;
1644
+ }
1645
+ el.classList.remove("hidden");
1646
+ const card = document.createElement("div");
1647
+ card.className = "int-card";
1648
+ const head = document.createElement("div");
1649
+ head.className = "int-head";
1650
+ const ico = document.createElement("span");
1651
+ ico.className = "int-ico";
1652
+ ico.innerHTML = it.kind === "otp" ? ICON.lock : ICON.shield;
1653
+ const title = document.createElement("span");
1654
+ title.className = "int-title";
1655
+ title.textContent = it.kind === "otp" ? "Verification required" : "Confirm this action";
1656
+ head.append(ico, title);
1657
+ card.appendChild(head);
1658
+ if (it.actionDescription) {
1659
+ const desc = document.createElement("div");
1660
+ desc.className = "int-desc";
1661
+ desc.textContent = it.actionDescription;
1662
+ card.appendChild(desc);
1663
+ }
1664
+ if (it.kind === "otp") {
1665
+ if (it.sent?.maskedDestination) {
1666
+ const sent = document.createElement("div");
1667
+ sent.className = "int-sent";
1668
+ sent.textContent = `Code sent to ${it.sent.maskedDestination}${it.sent.channel ? ` via ${it.sent.channel}` : ""}.`;
1669
+ card.appendChild(sent);
1670
+ }
1671
+ const row = document.createElement("div");
1672
+ row.className = "int-row";
1673
+ const input = document.createElement("input");
1674
+ input.className = "int-input";
1675
+ input.type = "text";
1676
+ input.inputMode = "numeric";
1677
+ input.autocomplete = "one-time-code";
1678
+ input.placeholder = "Enter code";
1679
+ const submit = () => {
1680
+ const code = input.value.trim();
1681
+ if (code) this.controller?.verifyOtp(code);
1682
+ };
1683
+ input.addEventListener("keydown", (ev) => {
1684
+ if (ev.key === "Enter") {
1685
+ ev.preventDefault();
1686
+ submit();
1687
+ }
1688
+ });
1689
+ const verify = document.createElement("button");
1690
+ verify.className = "int-btn primary";
1691
+ verify.type = "button";
1692
+ verify.textContent = "Verify";
1693
+ verify.addEventListener("click", submit);
1694
+ row.append(input, verify);
1695
+ card.appendChild(row);
1696
+ if (it.error) {
1697
+ const err = document.createElement("div");
1698
+ err.className = "int-error";
1699
+ err.textContent = it.attemptsRemaining != null ? `${it.error} (${it.attemptsRemaining} left)` : it.error;
1700
+ card.appendChild(err);
1701
+ }
1702
+ queueMicrotask(() => input.focus());
1703
+ } else {
1704
+ const row = document.createElement("div");
1705
+ row.className = "int-row";
1706
+ const decline = document.createElement("button");
1707
+ decline.className = "int-btn";
1708
+ decline.type = "button";
1709
+ decline.textContent = "Decline";
1710
+ decline.addEventListener("click", () => this.controller?.confirmTool(false));
1711
+ const approve = document.createElement("button");
1712
+ approve.className = "int-btn primary";
1713
+ approve.type = "button";
1714
+ approve.textContent = "Approve";
1715
+ approve.addEventListener("click", () => this.controller?.confirmTool(true));
1716
+ row.append(decline, approve);
1717
+ card.appendChild(row);
1718
+ }
1719
+ el.appendChild(card);
1559
1720
  }
1560
1721
  /** Collect identity from the pre-chat form, then drop into the chat view. */
1561
1722
  handlePrechatSubmit(form) {