@getnexorai/sdk 0.1.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/dist/chat.cjs ADDED
@@ -0,0 +1,962 @@
1
+ 'use strict';
2
+
3
+ // src/chat/config.ts
4
+ function applyDefaults(o) {
5
+ return {
6
+ workflowId: o.workflowId,
7
+ title: o.title ?? "Chat with us",
8
+ subtitle: o.subtitle ?? "We typically reply in a few minutes",
9
+ greeting: o.greeting ?? "Hi there! \u{1F44B} How can we help?",
10
+ errorText: o.errorText ?? "Sorry, something went wrong. Please try again in a moment.",
11
+ locale: o.locale ?? (typeof navigator !== "undefined" ? navigator.language : void 0),
12
+ accentColor: o.accentColor ?? "#111827",
13
+ accentTextColor: o.accentTextColor ?? "#ffffff",
14
+ showBranding: o.showBranding ?? true,
15
+ position: o.position ?? "bottom-right",
16
+ openOnLoad: o.openOnLoad ?? false,
17
+ debounceMs: o.debounceMs ?? 700,
18
+ container: o.container,
19
+ systemPrompt: o.systemPrompt,
20
+ clientPrompt: o.clientPrompt,
21
+ lead: o.lead,
22
+ capture: {
23
+ fields: o.capture?.fields ?? ["first_name", "email"],
24
+ mode: o.capture?.mode ?? "before",
25
+ label: o.capture?.label ?? "Tell us a bit about you to get started:",
26
+ submitLabel: o.capture?.submitLabel ?? "Start chat"
27
+ },
28
+ metadata: o.metadata,
29
+ onOpen: o.onOpen,
30
+ onClose: o.onClose,
31
+ onMessage: o.onMessage,
32
+ onLeadCaptured: o.onLeadCaptured,
33
+ onError: o.onError
34
+ };
35
+ }
36
+ function needsCapture(cfg) {
37
+ if (cfg.capture.mode === "skip") return false;
38
+ if (cfg.lead) {
39
+ const have = cfg.lead;
40
+ return cfg.capture.fields.some((f) => !have[f]);
41
+ }
42
+ return true;
43
+ }
44
+
45
+ // src/chat/session.ts
46
+ var SESSION_KEY = "nexor.chat.session_id";
47
+ var SESSION_RETENTION_MS = 30 * 24 * 60 * 60 * 1e3;
48
+ function ensureSessionId() {
49
+ const now = Date.now();
50
+ try {
51
+ const rec = readRecord();
52
+ if (rec && now - rec.ts < SESSION_RETENTION_MS) {
53
+ writeRecord(rec.id, now);
54
+ return rec.id;
55
+ }
56
+ } catch {
57
+ }
58
+ const id = `sess_${randomHex(16)}`;
59
+ try {
60
+ writeRecord(id, now);
61
+ } catch {
62
+ }
63
+ return id;
64
+ }
65
+ function touchSession() {
66
+ try {
67
+ const rec = readRecord();
68
+ if (rec) writeRecord(rec.id, Date.now());
69
+ } catch {
70
+ }
71
+ }
72
+ function readRecord() {
73
+ const raw = window.localStorage.getItem(SESSION_KEY);
74
+ if (!raw) return null;
75
+ try {
76
+ const o = JSON.parse(raw);
77
+ if (o && typeof o.id === "string" && typeof o.ts === "number") return o;
78
+ } catch {
79
+ }
80
+ if (raw.startsWith("sess_")) return { id: raw, ts: Date.now() };
81
+ return null;
82
+ }
83
+ function writeRecord(id, ts) {
84
+ window.localStorage.setItem(SESSION_KEY, JSON.stringify({ id, ts }));
85
+ }
86
+ function randomHex(bytes) {
87
+ const arr = new Uint8Array(bytes);
88
+ if (typeof crypto !== "undefined" && crypto.getRandomValues) {
89
+ crypto.getRandomValues(arr);
90
+ } else {
91
+ for (let i = 0; i < bytes; i++) arr[i] = Math.floor(Math.random() * 256);
92
+ }
93
+ return Array.from(arr, (b) => b.toString(16).padStart(2, "0")).join("");
94
+ }
95
+
96
+ // src/chat/styles.ts
97
+ var widgetCss = (accent, accentText) => `
98
+ .nexor-chat,
99
+ .nexor-chat * {
100
+ box-sizing: border-box;
101
+ font-family: -apple-system, BlinkMacSystemFont, "Segoe UI", Roboto, "Helvetica Neue", Arial, sans-serif;
102
+ }
103
+
104
+ .nexor-chat {
105
+ position: fixed;
106
+ z-index: 2147483640;
107
+ bottom: 20px;
108
+ right: 20px;
109
+ font-size: 14px;
110
+ line-height: 1.4;
111
+ color: #111;
112
+ }
113
+
114
+ .nexor-chat[data-position="bottom-left"] {
115
+ right: auto;
116
+ left: 20px;
117
+ }
118
+
119
+ /* \u2500\u2500 Launcher bubble \u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500 */
120
+ .nexor-chat__launcher {
121
+ width: 56px;
122
+ height: 56px;
123
+ border-radius: 9999px;
124
+ background: ${accent};
125
+ color: ${accentText};
126
+ border: none;
127
+ cursor: pointer;
128
+ display: flex;
129
+ align-items: center;
130
+ justify-content: center;
131
+ box-shadow: 0 6px 18px rgba(0, 0, 0, 0.18);
132
+ transition: transform 160ms cubic-bezier(.2,.7,.2,1.1),
133
+ box-shadow 160ms ease,
134
+ background 200ms ease;
135
+ padding: 0;
136
+ position: relative;
137
+ animation: nexor-launcher-in 360ms cubic-bezier(.2,.7,.2,1.1) both;
138
+ }
139
+ .nexor-chat__launcher:hover {
140
+ transform: translateY(-2px) scale(1.04);
141
+ box-shadow: 0 10px 24px rgba(0, 0, 0, 0.24);
142
+ }
143
+ .nexor-chat__launcher:active {
144
+ transform: translateY(0) scale(0.97);
145
+ transition-duration: 80ms;
146
+ }
147
+ .nexor-chat__launcher:focus-visible {
148
+ outline: 2px solid ${accent};
149
+ outline-offset: 2px;
150
+ }
151
+ .nexor-chat[data-open="true"] .nexor-chat__launcher {
152
+ transform: scale(0.85);
153
+ opacity: 0.75;
154
+ }
155
+ .nexor-chat__launcher svg { width: 24px; height: 24px; transition: transform 200ms ease; }
156
+ .nexor-chat[data-open="true"] .nexor-chat__launcher svg { transform: rotate(8deg); }
157
+
158
+ .nexor-chat__unread {
159
+ position: absolute;
160
+ top: -4px;
161
+ right: -4px;
162
+ min-width: 18px;
163
+ height: 18px;
164
+ background: #ef4444;
165
+ color: #fff;
166
+ border-radius: 9999px;
167
+ font-size: 11px;
168
+ font-weight: 600;
169
+ padding: 0 5px;
170
+ display: flex;
171
+ align-items: center;
172
+ justify-content: center;
173
+ border: 2px solid #fff;
174
+ animation: nexor-pop-in 300ms cubic-bezier(.2,.7,.2,1.1) both;
175
+ }
176
+
177
+ @keyframes nexor-launcher-in {
178
+ from { transform: translateY(20px) scale(0.6); opacity: 0; }
179
+ to { transform: translateY(0) scale(1); opacity: 1; }
180
+ }
181
+ @keyframes nexor-pop-in {
182
+ from { transform: scale(0); }
183
+ to { transform: scale(1); }
184
+ }
185
+
186
+ /* \u2500\u2500 Panel \u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500 */
187
+ .nexor-chat__panel {
188
+ position: absolute;
189
+ bottom: 72px;
190
+ right: 0;
191
+ width: 360px;
192
+ max-width: calc(100vw - 32px);
193
+ height: 540px;
194
+ max-height: calc(100vh - 120px);
195
+ background: #fff;
196
+ border-radius: 14px;
197
+ box-shadow: 0 24px 60px rgba(0, 0, 0, 0.22);
198
+ display: flex;
199
+ flex-direction: column;
200
+ overflow: hidden;
201
+ transform: translateY(14px) scale(0.96);
202
+ opacity: 0;
203
+ pointer-events: none;
204
+ transition: transform 220ms cubic-bezier(.2,.7,.2,1.1),
205
+ opacity 220ms ease;
206
+ border: 1px solid rgba(0, 0, 0, 0.06);
207
+ /* Use visibility so screen readers don't read collapsed content */
208
+ visibility: hidden;
209
+ }
210
+ .nexor-chat[data-position="bottom-left"] .nexor-chat__panel {
211
+ right: auto;
212
+ left: 0;
213
+ }
214
+ .nexor-chat[data-open="true"] .nexor-chat__panel {
215
+ transform: translateY(0) scale(1);
216
+ opacity: 1;
217
+ pointer-events: auto;
218
+ visibility: visible;
219
+ }
220
+
221
+ .nexor-chat__header {
222
+ background: ${accent};
223
+ color: ${accentText};
224
+ padding: 14px 16px;
225
+ display: flex;
226
+ align-items: center;
227
+ justify-content: space-between;
228
+ gap: 12px;
229
+ }
230
+ .nexor-chat__title {
231
+ font-weight: 600;
232
+ font-size: 15px;
233
+ margin: 0;
234
+ }
235
+ .nexor-chat__subtitle {
236
+ font-size: 12px;
237
+ opacity: 0.85;
238
+ margin: 2px 0 0;
239
+ }
240
+ .nexor-chat__status-dot {
241
+ display: inline-block;
242
+ width: 8px;
243
+ height: 8px;
244
+ border-radius: 9999px;
245
+ background: #34d399;
246
+ margin-right: 6px;
247
+ vertical-align: 1px;
248
+ animation: nexor-pulse 2.4s ease-in-out infinite;
249
+ box-shadow: 0 0 0 0 rgba(52, 211, 153, 0.6);
250
+ }
251
+ @keyframes nexor-pulse {
252
+ 0%, 100% { box-shadow: 0 0 0 0 rgba(52, 211, 153, 0.55); }
253
+ 50% { box-shadow: 0 0 0 6px rgba(52, 211, 153, 0); }
254
+ }
255
+ .nexor-chat__close {
256
+ background: transparent;
257
+ border: none;
258
+ color: inherit;
259
+ cursor: pointer;
260
+ padding: 4px;
261
+ border-radius: 6px;
262
+ opacity: 0.9;
263
+ transition: background 140ms ease, transform 140ms ease;
264
+ }
265
+ .nexor-chat__close:hover { background: rgba(255,255,255,0.18); opacity: 1; }
266
+ .nexor-chat__close:active { transform: scale(0.92); }
267
+ .nexor-chat__close svg { width: 18px; height: 18px; display: block; }
268
+
269
+ /* \u2500\u2500 Body / messages \u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500 */
270
+ .nexor-chat__body {
271
+ flex: 1;
272
+ overflow-y: auto;
273
+ padding: 14px;
274
+ background: #f6f7f9;
275
+ display: flex;
276
+ flex-direction: column;
277
+ gap: 8px;
278
+ scroll-behavior: smooth;
279
+ /* Prevent layout shift when scrollbar appears */
280
+ scrollbar-gutter: stable;
281
+ }
282
+ .nexor-chat__body::-webkit-scrollbar { width: 6px; }
283
+ .nexor-chat__body::-webkit-scrollbar-thumb {
284
+ background: rgba(0,0,0,0.15);
285
+ border-radius: 9999px;
286
+ }
287
+
288
+ .nexor-chat__msg {
289
+ max-width: 80%;
290
+ padding: 8px 12px;
291
+ border-radius: 14px;
292
+ white-space: pre-wrap;
293
+ word-wrap: break-word;
294
+ animation: nexor-msg-in 240ms cubic-bezier(.2,.7,.2,1.1) both;
295
+ will-change: transform, opacity;
296
+ }
297
+ .nexor-chat__msg--bot {
298
+ background: #fff;
299
+ align-self: flex-start;
300
+ border: 1px solid rgba(0,0,0,0.06);
301
+ border-bottom-left-radius: 4px;
302
+ box-shadow: 0 1px 2px rgba(0,0,0,0.04);
303
+ }
304
+ .nexor-chat__msg--user {
305
+ background: ${accent};
306
+ color: ${accentText};
307
+ align-self: flex-end;
308
+ border-bottom-right-radius: 4px;
309
+ animation-name: nexor-msg-in-user;
310
+ }
311
+ .nexor-chat__msg--system {
312
+ background: transparent;
313
+ color: #6b7280;
314
+ font-size: 12px;
315
+ align-self: center;
316
+ text-align: center;
317
+ max-width: 100%;
318
+ animation-name: nexor-fade-in;
319
+ }
320
+ .nexor-chat__msg-time {
321
+ display: block;
322
+ margin-top: 4px;
323
+ font-size: 10px;
324
+ line-height: 1;
325
+ opacity: 0.55;
326
+ text-align: right;
327
+ }
328
+ .nexor-chat__msg--bot .nexor-chat__msg-time { text-align: left; }
329
+
330
+ @keyframes nexor-msg-in {
331
+ from { transform: translateY(8px) scale(0.96); opacity: 0; }
332
+ to { transform: translateY(0) scale(1); opacity: 1; }
333
+ }
334
+ @keyframes nexor-msg-in-user {
335
+ from { transform: translateY(8px) translateX(8px) scale(0.96); opacity: 0; }
336
+ to { transform: translateY(0) translateX(0) scale(1); opacity: 1; }
337
+ }
338
+ @keyframes nexor-fade-in {
339
+ from { opacity: 0; }
340
+ to { opacity: 1; }
341
+ }
342
+
343
+ .nexor-chat__typing {
344
+ align-self: flex-start;
345
+ background: #fff;
346
+ border: 1px solid rgba(0,0,0,0.06);
347
+ border-radius: 14px;
348
+ border-bottom-left-radius: 4px;
349
+ padding: 10px 14px;
350
+ display: inline-flex;
351
+ gap: 4px;
352
+ animation: nexor-msg-in 240ms cubic-bezier(.2,.7,.2,1.1) both;
353
+ }
354
+ .nexor-chat__typing span {
355
+ width: 6px; height: 6px; border-radius: 9999px;
356
+ background: #9ca3af;
357
+ display: inline-block;
358
+ animation: nexor-blink 1.2s infinite ease-in-out;
359
+ }
360
+ .nexor-chat__typing span:nth-child(2) { animation-delay: 0.15s; }
361
+ .nexor-chat__typing span:nth-child(3) { animation-delay: 0.30s; }
362
+ @keyframes nexor-blink {
363
+ 0%, 60%, 100% { opacity: 0.3; transform: translateY(0); }
364
+ 30% { opacity: 1; transform: translateY(-3px); }
365
+ }
366
+
367
+ /* \u2500\u2500 Lead capture form \u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500 */
368
+ .nexor-chat__capture {
369
+ background: #fff;
370
+ border: 1px solid rgba(0,0,0,0.06);
371
+ border-radius: 12px;
372
+ padding: 12px;
373
+ display: flex;
374
+ flex-direction: column;
375
+ gap: 8px;
376
+ margin-top: 4px;
377
+ animation: nexor-msg-in 280ms cubic-bezier(.2,.7,.2,1.1) both;
378
+ box-shadow: 0 2px 8px rgba(0,0,0,0.05);
379
+ }
380
+ .nexor-chat__capture-label {
381
+ font-size: 12px;
382
+ color: #6b7280;
383
+ margin: 0;
384
+ }
385
+ .nexor-chat__capture-row {
386
+ display: flex;
387
+ gap: 8px;
388
+ }
389
+ .nexor-chat__capture-row > * { flex: 1; }
390
+ .nexor-chat__input,
391
+ .nexor-chat__capture input {
392
+ font: inherit;
393
+ padding: 8px 10px;
394
+ border: 1px solid #d1d5db;
395
+ border-radius: 8px;
396
+ background: #fff;
397
+ color: #111;
398
+ outline: none;
399
+ width: 100%;
400
+ transition: border-color 140ms ease, box-shadow 140ms ease;
401
+ }
402
+ .nexor-chat__input:focus,
403
+ .nexor-chat__capture input:focus {
404
+ border-color: ${accent};
405
+ box-shadow: 0 0 0 3px ${accent}33;
406
+ }
407
+ .nexor-chat__capture-submit {
408
+ background: ${accent};
409
+ color: ${accentText};
410
+ border: none;
411
+ border-radius: 8px;
412
+ padding: 8px 12px;
413
+ font: inherit;
414
+ font-weight: 600;
415
+ cursor: pointer;
416
+ transition: transform 140ms ease, filter 140ms ease;
417
+ }
418
+ .nexor-chat__capture-submit:hover { filter: brightness(1.08); }
419
+ .nexor-chat__capture-submit:active { transform: scale(0.97); }
420
+ .nexor-chat__capture-submit[disabled] { opacity: 0.6; cursor: not-allowed; }
421
+
422
+ /* \u2500\u2500 Composer \u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500 */
423
+ .nexor-chat__composer {
424
+ display: flex;
425
+ gap: 8px;
426
+ padding: 10px;
427
+ background: #fff;
428
+ border-top: 1px solid rgba(0,0,0,0.06);
429
+ align-items: stretch;
430
+ }
431
+ .nexor-chat__send {
432
+ background: ${accent};
433
+ color: ${accentText};
434
+ border: none;
435
+ border-radius: 8px;
436
+ padding: 0 14px;
437
+ font: inherit;
438
+ font-weight: 600;
439
+ cursor: pointer;
440
+ min-width: 44px;
441
+ display: flex;
442
+ align-items: center;
443
+ justify-content: center;
444
+ transition: transform 140ms ease, filter 140ms ease;
445
+ }
446
+ .nexor-chat__send:hover { filter: brightness(1.08); }
447
+ .nexor-chat__send:active { transform: scale(0.95); }
448
+ .nexor-chat__send[disabled] {
449
+ opacity: 0.6;
450
+ cursor: not-allowed;
451
+ transform: none;
452
+ }
453
+ .nexor-chat__send svg {
454
+ width: 18px;
455
+ height: 18px;
456
+ transition: transform 200ms ease;
457
+ }
458
+ .nexor-chat__send:not([disabled]):hover svg { transform: translateX(2px); }
459
+
460
+ /* Small spinner inside the send button while a turn is in flight */
461
+ .nexor-chat__send--loading svg { display: none; }
462
+ .nexor-chat__send--loading::after {
463
+ content: "";
464
+ width: 14px;
465
+ height: 14px;
466
+ border-radius: 9999px;
467
+ border: 2px solid currentColor;
468
+ border-top-color: transparent;
469
+ animation: nexor-spin 700ms linear infinite;
470
+ }
471
+ @keyframes nexor-spin {
472
+ to { transform: rotate(360deg); }
473
+ }
474
+
475
+ .nexor-chat__footer {
476
+ text-align: center;
477
+ font-size: 11px;
478
+ color: #9ca3af;
479
+ padding: 6px;
480
+ background: #fff;
481
+ border-top: 1px solid rgba(0,0,0,0.04);
482
+ }
483
+ .nexor-chat__footer a {
484
+ color: inherit;
485
+ text-decoration: none;
486
+ font-weight: 600;
487
+ }
488
+ .nexor-chat__footer a:hover { text-decoration: underline; }
489
+
490
+ /* Respect users who request reduced motion. */
491
+ @media (prefers-reduced-motion: reduce) {
492
+ .nexor-chat,
493
+ .nexor-chat * {
494
+ animation-duration: 0.001ms !important;
495
+ transition-duration: 0.001ms !important;
496
+ }
497
+ .nexor-chat__status-dot { animation: none; }
498
+ }
499
+ `;
500
+
501
+ // src/chat/dom.ts
502
+ var STYLE_ATTR = "data-nexor-chat-styles";
503
+ function injectStyles(accent, accentText) {
504
+ if (document.querySelector(`style[${STYLE_ATTR}]`)) return;
505
+ const style = document.createElement("style");
506
+ style.setAttribute(STYLE_ATTR, "1");
507
+ style.textContent = widgetCss(accent, accentText);
508
+ document.head.appendChild(style);
509
+ }
510
+ function buildLauncher() {
511
+ const el = document.createElement("button");
512
+ el.type = "button";
513
+ el.className = "nexor-chat__launcher";
514
+ el.setAttribute("aria-label", "Open chat");
515
+ el.innerHTML = `
516
+ <svg viewBox="0 0 24 24" fill="none" xmlns="http://www.w3.org/2000/svg" aria-hidden="true">
517
+ <path d="M4 5.5C4 4.67 4.67 4 5.5 4h13c.83 0 1.5.67 1.5 1.5v9c0 .83-.67 1.5-1.5 1.5H9l-4 4V5.5Z"
518
+ stroke="currentColor" stroke-width="1.8" stroke-linejoin="round"/>
519
+ </svg>
520
+ `;
521
+ const badge = document.createElement("span");
522
+ badge.className = "nexor-chat__unread";
523
+ badge.style.display = "none";
524
+ el.appendChild(badge);
525
+ return { el, badge };
526
+ }
527
+ function buildPanel(cfg) {
528
+ const el = document.createElement("div");
529
+ el.className = "nexor-chat__panel";
530
+ el.setAttribute("role", "dialog");
531
+ el.setAttribute("aria-label", cfg.title);
532
+ const { header, closeBtn } = buildHeader(cfg);
533
+ const { body, captureForm, captureInputs } = buildBody(cfg);
534
+ const { form, input, send } = buildComposer();
535
+ const footer = cfg.showBranding ? buildFooter() : null;
536
+ el.appendChild(header);
537
+ el.appendChild(body);
538
+ el.appendChild(form);
539
+ if (footer) el.appendChild(footer);
540
+ return { el, closeBtn, body, form, input, send, captureForm, captureInputs };
541
+ }
542
+ function buildHeader(cfg) {
543
+ const header = document.createElement("div");
544
+ header.className = "nexor-chat__header";
545
+ const subtitleHtml = cfg.subtitle ? `<p class="nexor-chat__subtitle">
546
+ <span class="nexor-chat__status-dot" aria-hidden="true"></span>${escapeHtml(cfg.subtitle)}
547
+ </p>` : "";
548
+ header.innerHTML = `
549
+ <div>
550
+ <p class="nexor-chat__title">${escapeHtml(cfg.title)}</p>
551
+ ${subtitleHtml}
552
+ </div>
553
+ `;
554
+ const closeBtn = document.createElement("button");
555
+ closeBtn.type = "button";
556
+ closeBtn.className = "nexor-chat__close";
557
+ closeBtn.setAttribute("aria-label", "Close chat");
558
+ closeBtn.innerHTML = `
559
+ <svg viewBox="0 0 24 24" fill="none" xmlns="http://www.w3.org/2000/svg" aria-hidden="true">
560
+ <path d="M6 6l12 12M6 18L18 6" stroke="currentColor" stroke-width="2" stroke-linecap="round"/>
561
+ </svg>
562
+ `;
563
+ header.appendChild(closeBtn);
564
+ return { header, closeBtn };
565
+ }
566
+ function buildBody(cfg) {
567
+ const body = document.createElement("div");
568
+ body.className = "nexor-chat__body";
569
+ const captureInputs = {};
570
+ let captureForm = null;
571
+ if (needsCapture(cfg)) {
572
+ captureForm = buildCaptureForm(cfg, captureInputs);
573
+ body.appendChild(captureForm);
574
+ }
575
+ return { body, captureForm, captureInputs };
576
+ }
577
+ function buildCaptureForm(cfg, inputsOut) {
578
+ const form = document.createElement("form");
579
+ form.className = "nexor-chat__capture";
580
+ form.noValidate = true;
581
+ const label = document.createElement("p");
582
+ label.className = "nexor-chat__capture-label";
583
+ label.textContent = cfg.capture.label;
584
+ form.appendChild(label);
585
+ const fields = cfg.capture.fields;
586
+ if (fields.includes("first_name") || fields.includes("last_name")) {
587
+ const row = document.createElement("div");
588
+ row.className = "nexor-chat__capture-row";
589
+ if (fields.includes("first_name")) {
590
+ inputsOut.first_name = makeInput("first_name", "First name", "text", cfg.lead?.first_name);
591
+ row.appendChild(inputsOut.first_name);
592
+ }
593
+ if (fields.includes("last_name")) {
594
+ inputsOut.last_name = makeInput("last_name", "Last name", "text", cfg.lead?.last_name);
595
+ row.appendChild(inputsOut.last_name);
596
+ }
597
+ if (row.childElementCount > 0) form.appendChild(row);
598
+ }
599
+ if (fields.includes("email")) {
600
+ inputsOut.email = makeInput("email", "Email", "email", cfg.lead?.email);
601
+ form.appendChild(inputsOut.email);
602
+ }
603
+ if (fields.includes("phone")) {
604
+ inputsOut.phone = makeInput("phone", "Phone", "tel", cfg.lead?.phone);
605
+ form.appendChild(inputsOut.phone);
606
+ }
607
+ const submit = document.createElement("button");
608
+ submit.type = "submit";
609
+ submit.className = "nexor-chat__capture-submit";
610
+ submit.textContent = cfg.capture.submitLabel;
611
+ form.appendChild(submit);
612
+ return form;
613
+ }
614
+ function buildComposer() {
615
+ const form = document.createElement("form");
616
+ form.className = "nexor-chat__composer";
617
+ const input = document.createElement("input");
618
+ input.type = "text";
619
+ input.className = "nexor-chat__input";
620
+ input.placeholder = "Type a message\u2026";
621
+ input.autocomplete = "off";
622
+ const send = document.createElement("button");
623
+ send.type = "submit";
624
+ send.className = "nexor-chat__send";
625
+ send.setAttribute("aria-label", "Send message");
626
+ send.innerHTML = `
627
+ <svg viewBox="0 0 24 24" fill="none" xmlns="http://www.w3.org/2000/svg" aria-hidden="true">
628
+ <path d="M3.4 11.2 20.5 4.3c.7-.3 1.4.4 1.1 1.1l-6.9 17.1c-.3.8-1.4.8-1.8 0L10 14l-7.5-3c-.8-.3-.8-1.5 0-1.8Z"
629
+ stroke="currentColor" stroke-width="1.6" stroke-linejoin="round" fill="currentColor"/>
630
+ </svg>
631
+ `;
632
+ form.appendChild(input);
633
+ form.appendChild(send);
634
+ return { form, input, send };
635
+ }
636
+ function buildFooter() {
637
+ const el = document.createElement("div");
638
+ el.className = "nexor-chat__footer";
639
+ el.innerHTML = `Powered by <a href="https://www.getnexor.ai" target="_blank" rel="noopener noreferrer">Nexor</a>`;
640
+ return el;
641
+ }
642
+ function buildMessage(role, text, ts, locale) {
643
+ const el = document.createElement("div");
644
+ el.className = `nexor-chat__msg nexor-chat__msg--${role}`;
645
+ const body = document.createElement("span");
646
+ body.className = "nexor-chat__msg-text";
647
+ body.textContent = text;
648
+ el.appendChild(body);
649
+ if (role !== "system" && ts) {
650
+ const time = document.createElement("time");
651
+ time.className = "nexor-chat__msg-time";
652
+ time.dataset.ts = String(ts);
653
+ time.textContent = formatRelativeTime(ts, locale);
654
+ el.appendChild(time);
655
+ }
656
+ return el;
657
+ }
658
+ function formatRelativeTime(ts, locale, now = Date.now()) {
659
+ try {
660
+ const diff = Math.max(0, now - ts);
661
+ const min = Math.round(diff / 6e4);
662
+ const hr = Math.round(diff / 36e5);
663
+ const day = Math.round(diff / 864e5);
664
+ const rtf = new Intl.RelativeTimeFormat(locale, { numeric: "auto" });
665
+ if (diff < 45e3) return rtf.format(0, "second");
666
+ if (min < 60) return rtf.format(-min, "minute");
667
+ if (hr < 24) return rtf.format(-hr, "hour");
668
+ if (day < 7) return rtf.format(-day, "day");
669
+ return new Date(ts).toLocaleDateString(locale, {
670
+ day: "numeric",
671
+ month: "short"
672
+ });
673
+ } catch {
674
+ try {
675
+ return new Date(ts).toLocaleTimeString(locale, {
676
+ hour: "2-digit",
677
+ minute: "2-digit"
678
+ });
679
+ } catch {
680
+ return "";
681
+ }
682
+ }
683
+ }
684
+ function buildTypingIndicator() {
685
+ const el = document.createElement("div");
686
+ el.className = "nexor-chat__typing";
687
+ el.innerHTML = "<span></span><span></span><span></span>";
688
+ return el;
689
+ }
690
+ function makeInput(name, placeholder, type, value) {
691
+ const i = document.createElement("input");
692
+ i.name = name;
693
+ i.type = type;
694
+ i.placeholder = placeholder;
695
+ if (value) i.value = value;
696
+ return i;
697
+ }
698
+ function escapeHtml(s) {
699
+ return s.replace(/&/g, "&amp;").replace(/</g, "&lt;").replace(/>/g, "&gt;").replace(/"/g, "&quot;").replace(/'/g, "&#039;");
700
+ }
701
+
702
+ // src/chat/index.ts
703
+ var TYPING_DELAY_MS = 220;
704
+ function initChat(client, options) {
705
+ ensureBrowser();
706
+ if (!options || !options.workflowId) {
707
+ throw new Error("Nexor SDK: initChat() requires `workflowId`.");
708
+ }
709
+ const cfg = applyDefaults(options);
710
+ injectStyles(cfg.accentColor, cfg.accentTextColor);
711
+ const sessionId = ensureSessionId();
712
+ const historyKey = `nexor.chat.history.${sessionId}`;
713
+ const root = buildRoot(cfg);
714
+ const launcher = buildLauncher();
715
+ const panel = buildPanel(cfg);
716
+ root.appendChild(launcher.el);
717
+ root.appendChild(panel.el);
718
+ (cfg.container ?? document.body).appendChild(root);
719
+ const state = {
720
+ isOpen: false,
721
+ unread: 0,
722
+ leadId: void 0,
723
+ captured: !needsCapture(cfg),
724
+ pendingLead: { ...cfg.lead ?? {} },
725
+ history: []
726
+ };
727
+ launcher.el.addEventListener("click", toggle);
728
+ panel.closeBtn.addEventListener("click", close);
729
+ panel.form.addEventListener("submit", onComposerSubmit);
730
+ panel.captureForm?.addEventListener("submit", onCaptureSubmit);
731
+ syncComposerLock();
732
+ const restored = loadHistory();
733
+ if (restored.length) {
734
+ state.history = restored;
735
+ for (const m of restored) appendBubble(m.role === "user" ? "user" : "bot", m.text, m.ts);
736
+ scrollToBottom();
737
+ }
738
+ if (cfg.openOnLoad) open();
739
+ function open() {
740
+ if (state.isOpen) return;
741
+ state.isOpen = true;
742
+ root.setAttribute("data-open", "true");
743
+ clearUnread();
744
+ if (state.history.length === 0 && cfg.greeting && state.captured) {
745
+ appendBot(cfg.greeting);
746
+ }
747
+ panel.input.focus();
748
+ cfg.onOpen?.();
749
+ }
750
+ function close() {
751
+ if (!state.isOpen) return;
752
+ state.isOpen = false;
753
+ root.setAttribute("data-open", "false");
754
+ cfg.onClose?.();
755
+ }
756
+ function toggle() {
757
+ state.isOpen ? close() : open();
758
+ }
759
+ function onCaptureSubmit(e) {
760
+ e.preventDefault();
761
+ const collected = readCaptureFields(panel);
762
+ const required = cfg.capture.fields;
763
+ for (const f of required) {
764
+ if (!collected[f]) {
765
+ panel.captureInputs[f]?.focus();
766
+ return;
767
+ }
768
+ }
769
+ state.pendingLead = { ...state.pendingLead, ...collected };
770
+ state.captured = true;
771
+ panel.captureForm?.remove();
772
+ syncComposerLock();
773
+ panel.input.focus();
774
+ if (cfg.greeting && state.history.length === 0) appendBot(cfg.greeting);
775
+ }
776
+ const send = {
777
+ pending: [],
778
+ timer: 0,
779
+ typingTimer: 0,
780
+ inFlight: false,
781
+ typing: null
782
+ };
783
+ function onComposerSubmit(e) {
784
+ e.preventDefault();
785
+ const text = panel.input.value.trim();
786
+ if (!text) return;
787
+ panel.input.value = "";
788
+ queueMessage(text);
789
+ }
790
+ function queueMessage(text) {
791
+ if (!state.captured) {
792
+ panel.captureInputs.first_name?.focus();
793
+ return;
794
+ }
795
+ appendUser(text);
796
+ send.pending.push(text);
797
+ scheduleFlush();
798
+ }
799
+ function scheduleFlush() {
800
+ if (send.timer) window.clearTimeout(send.timer);
801
+ send.timer = window.setTimeout(() => void flush(), cfg.debounceMs);
802
+ }
803
+ async function flush() {
804
+ if (send.inFlight || send.pending.length === 0) return;
805
+ const batch = send.pending.splice(0, send.pending.length);
806
+ const message = batch.join("\n");
807
+ send.inFlight = true;
808
+ send.typingTimer = window.setTimeout(() => {
809
+ send.typing = appendNode(buildTypingIndicator());
810
+ }, TYPING_DELAY_MS);
811
+ try {
812
+ const res = await client.chat({
813
+ workflow_id: cfg.workflowId,
814
+ session_id: sessionId,
815
+ message,
816
+ system_prompt: cfg.systemPrompt,
817
+ client_prompt: cfg.clientPrompt,
818
+ lead: Object.keys(state.pendingLead).length ? state.pendingLead : void 0,
819
+ metadata: cfg.metadata
820
+ });
821
+ if (res.lead_id && !state.leadId) {
822
+ state.leadId = res.lead_id;
823
+ cfg.onLeadCaptured?.(state.leadId);
824
+ }
825
+ const reply = (res.reply ?? "").trim();
826
+ if (reply) {
827
+ appendBot(reply);
828
+ } else {
829
+ appendSystem(cfg.errorText);
830
+ cfg.onError?.(new Error("Nexor SDK: empty reply from server"));
831
+ }
832
+ } catch (err) {
833
+ appendSystem(cfg.errorText);
834
+ cfg.onError?.(err);
835
+ } finally {
836
+ window.clearTimeout(send.typingTimer);
837
+ send.typing?.remove();
838
+ send.typing = null;
839
+ send.inFlight = false;
840
+ panel.input.focus();
841
+ if (send.pending.length) scheduleFlush();
842
+ }
843
+ }
844
+ function appendUser(text) {
845
+ const ts = Date.now();
846
+ appendBubble("user", text, ts);
847
+ state.history.push({ role: "user", text, ts });
848
+ saveHistory();
849
+ cfg.onMessage?.({ role: "user", text });
850
+ }
851
+ function appendBot(text) {
852
+ const ts = Date.now();
853
+ appendBubble("bot", text, ts);
854
+ state.history.push({ role: "bot", text, ts });
855
+ saveHistory();
856
+ if (!state.isOpen) bumpUnread();
857
+ cfg.onMessage?.({ role: "bot", text });
858
+ }
859
+ function saveHistory() {
860
+ try {
861
+ window.localStorage.setItem(
862
+ historyKey,
863
+ JSON.stringify({ ts: Date.now(), history: state.history })
864
+ );
865
+ touchSession();
866
+ } catch {
867
+ }
868
+ }
869
+ function loadHistory() {
870
+ try {
871
+ const raw = window.localStorage.getItem(historyKey);
872
+ if (!raw) return [];
873
+ const o = JSON.parse(raw);
874
+ if (!o || !Array.isArray(o.history)) return [];
875
+ if (Date.now() - (o.ts || 0) > SESSION_RETENTION_MS) {
876
+ window.localStorage.removeItem(historyKey);
877
+ return [];
878
+ }
879
+ return o.history;
880
+ } catch {
881
+ return [];
882
+ }
883
+ }
884
+ function appendSystem(text) {
885
+ appendBubble("system", text);
886
+ }
887
+ function appendBubble(role, text, ts) {
888
+ return appendNode(buildMessage(role, text, ts, cfg.locale));
889
+ }
890
+ const refreshTimer = window.setInterval(() => {
891
+ panel.body.querySelectorAll(".nexor-chat__msg-time[data-ts]").forEach((el) => {
892
+ const ts = Number(el.dataset.ts);
893
+ if (ts) el.textContent = formatRelativeTime(ts, cfg.locale);
894
+ });
895
+ }, 6e4);
896
+ function appendNode(node) {
897
+ panel.body.appendChild(node);
898
+ scrollToBottom();
899
+ return node;
900
+ }
901
+ function scrollToBottom() {
902
+ panel.body.scrollTop = panel.body.scrollHeight;
903
+ }
904
+ function bumpUnread() {
905
+ state.unread++;
906
+ launcher.badge.textContent = String(state.unread);
907
+ launcher.badge.style.display = "flex";
908
+ }
909
+ function clearUnread() {
910
+ state.unread = 0;
911
+ launcher.badge.style.display = "none";
912
+ }
913
+ function syncComposerLock() {
914
+ const lock = !state.captured;
915
+ panel.input.disabled = lock;
916
+ panel.send.disabled = lock;
917
+ panel.input.placeholder = lock ? "Complete the form above to start\u2026" : "Type a message\u2026";
918
+ }
919
+ return {
920
+ open,
921
+ close,
922
+ toggle,
923
+ // Programmatic send goes through the same debounce/coalesce path. Resolves
924
+ // immediately — the message is queued, not necessarily sent yet.
925
+ send: (text) => {
926
+ queueMessage(text);
927
+ return Promise.resolve();
928
+ },
929
+ destroy: () => {
930
+ if (send.timer) window.clearTimeout(send.timer);
931
+ if (send.typingTimer) window.clearTimeout(send.typingTimer);
932
+ window.clearInterval(refreshTimer);
933
+ root.remove();
934
+ },
935
+ getSessionId: () => sessionId
936
+ };
937
+ }
938
+ function ensureBrowser() {
939
+ if (typeof window === "undefined" || typeof document === "undefined") {
940
+ throw new Error("Nexor SDK: initChat() requires a browser environment.");
941
+ }
942
+ }
943
+ function buildRoot(cfg) {
944
+ const root = document.createElement("div");
945
+ root.className = "nexor-chat";
946
+ root.setAttribute("data-position", cfg.position);
947
+ root.setAttribute("data-open", "false");
948
+ return root;
949
+ }
950
+ function readCaptureFields(panel) {
951
+ const out = {};
952
+ const inputs = panel.captureInputs;
953
+ if (inputs.first_name) out.first_name = inputs.first_name.value.trim();
954
+ if (inputs.last_name) out.last_name = inputs.last_name.value.trim();
955
+ if (inputs.email) out.email = inputs.email.value.trim();
956
+ if (inputs.phone) out.phone = inputs.phone.value.trim();
957
+ return out;
958
+ }
959
+
960
+ exports.initChat = initChat;
961
+ //# sourceMappingURL=chat.cjs.map
962
+ //# sourceMappingURL=chat.cjs.map