@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/index.cjs ADDED
@@ -0,0 +1,1423 @@
1
+ 'use strict';
2
+
3
+ Object.defineProperty(exports, '__esModule', { value: true });
4
+
5
+ // src/errors.ts
6
+ var NexorError = class extends Error {
7
+ constructor(message) {
8
+ super(message);
9
+ this.name = "NexorError";
10
+ }
11
+ };
12
+ var NexorAPIError = class extends NexorError {
13
+ constructor(opts) {
14
+ super(opts.message);
15
+ this.name = "NexorAPIError";
16
+ this.status = opts.status;
17
+ this.code = opts.code ?? statusToCode(opts.status);
18
+ this.body = opts.body;
19
+ this.requestId = opts.requestId;
20
+ }
21
+ };
22
+ var NexorAuthError = class extends NexorAPIError {
23
+ constructor(opts) {
24
+ super({ status: 401, code: "unauthorized", ...opts });
25
+ this.name = "NexorAuthError";
26
+ }
27
+ };
28
+ var NexorValidationError = class extends NexorAPIError {
29
+ constructor(opts) {
30
+ super({ status: 400, code: "validation_error", ...opts });
31
+ this.name = "NexorValidationError";
32
+ }
33
+ };
34
+ var NexorNetworkError = class extends NexorError {
35
+ constructor(message, cause) {
36
+ super(message);
37
+ this.name = "NexorNetworkError";
38
+ this.cause = cause;
39
+ }
40
+ };
41
+ function statusToCode(status) {
42
+ if (status === 400) return "validation_error";
43
+ if (status === 401) return "unauthorized";
44
+ if (status === 403) return "forbidden";
45
+ if (status === 404) return "not_found";
46
+ if (status === 409) return "conflict";
47
+ if (status === 422) return "unprocessable_entity";
48
+ if (status === 429) return "rate_limited";
49
+ if (status >= 500) return "server_error";
50
+ return "http_error";
51
+ }
52
+
53
+ // src/http.ts
54
+ var DEFAULT_BASE_URL = "https://api.getnexor.ai";
55
+ var DEFAULT_TIMEOUT_MS = 3e4;
56
+ var DEFAULT_MAX_RETRIES = 2;
57
+ var SDK_VERSION = "0.1.0";
58
+ function resolveConfig(opts) {
59
+ if (!opts || typeof opts.apiKey !== "string" || opts.apiKey.trim() === "") {
60
+ throw new Error(
61
+ "Nexor SDK: `apiKey` is required. Get one in the dashboard at https://app.getnexor.ai"
62
+ );
63
+ }
64
+ const fetchImpl = opts.fetch ?? (typeof globalThis !== "undefined" && typeof globalThis.fetch === "function" ? globalThis.fetch.bind(globalThis) : void 0);
65
+ if (!fetchImpl) {
66
+ throw new Error(
67
+ "Nexor SDK: no global fetch found. Pass a `fetch` implementation via init() or upgrade to Node 18+."
68
+ );
69
+ }
70
+ const ua = `nexor-sdk-js/${SDK_VERSION}${opts.userAgentSuffix ? ` ${opts.userAgentSuffix}` : ""}`;
71
+ return {
72
+ apiKey: opts.apiKey.trim(),
73
+ baseUrl: stripTrailingSlash(opts.baseUrl ?? DEFAULT_BASE_URL),
74
+ timeoutMs: opts.timeoutMs ?? DEFAULT_TIMEOUT_MS,
75
+ maxRetries: opts.maxRetries ?? DEFAULT_MAX_RETRIES,
76
+ fetchImpl,
77
+ userAgent: ua
78
+ };
79
+ }
80
+ async function request(cfg, req) {
81
+ const url = buildUrl(cfg.baseUrl, req.path, req.query);
82
+ const headers = {
83
+ "X-API-Key": cfg.apiKey,
84
+ Accept: "application/json",
85
+ "User-Agent": cfg.userAgent
86
+ };
87
+ if (req.options?.idempotencyKey) {
88
+ headers["Idempotency-Key"] = req.options.idempotencyKey;
89
+ }
90
+ let body;
91
+ if (req.body !== void 0) {
92
+ headers["Content-Type"] = "application/json";
93
+ body = JSON.stringify(req.body);
94
+ }
95
+ const timeoutMs = req.options?.timeoutMs ?? cfg.timeoutMs;
96
+ let lastErr;
97
+ for (let attempt = 0; attempt <= cfg.maxRetries; attempt++) {
98
+ const controller = new AbortController();
99
+ const externalSignal = req.options?.signal;
100
+ const onAbort = () => controller.abort();
101
+ if (externalSignal) {
102
+ if (externalSignal.aborted) controller.abort();
103
+ else externalSignal.addEventListener("abort", onAbort);
104
+ }
105
+ const timeoutId = setTimeout(() => controller.abort(), timeoutMs);
106
+ try {
107
+ const res = await cfg.fetchImpl(url, {
108
+ method: req.method,
109
+ headers,
110
+ body,
111
+ signal: controller.signal
112
+ });
113
+ clearTimeout(timeoutId);
114
+ externalSignal?.removeEventListener("abort", onAbort);
115
+ const requestId = res.headers.get("x-request-id") ?? void 0;
116
+ if (res.ok) {
117
+ if (res.status === 204) return void 0;
118
+ const text = await res.text();
119
+ if (!text) return void 0;
120
+ try {
121
+ return JSON.parse(text);
122
+ } catch {
123
+ throw new NexorAPIError({
124
+ status: res.status,
125
+ message: "Failed to parse JSON response",
126
+ body: text,
127
+ requestId
128
+ });
129
+ }
130
+ }
131
+ const errBody = await safeJson(res);
132
+ const message = pickMessage(errBody) ?? `HTTP ${res.status}`;
133
+ const err = makeError(res.status, message, errBody, requestId);
134
+ if (shouldRetry(res.status) && attempt < cfg.maxRetries) {
135
+ await sleep(backoffMs(attempt, res));
136
+ lastErr = err;
137
+ continue;
138
+ }
139
+ throw err;
140
+ } catch (err) {
141
+ clearTimeout(timeoutId);
142
+ externalSignal?.removeEventListener("abort", onAbort);
143
+ if (err instanceof NexorAPIError) throw err;
144
+ const isAbort = err instanceof Error && (err.name === "AbortError" || /abort/i.test(err.message));
145
+ if (isAbort && externalSignal?.aborted) throw err;
146
+ lastErr = err;
147
+ if (attempt < cfg.maxRetries) {
148
+ await sleep(backoffMs(attempt));
149
+ continue;
150
+ }
151
+ throw new NexorNetworkError(
152
+ isAbort ? `Request timed out after ${timeoutMs}ms` : `Network error: ${err?.message ?? String(err)}`,
153
+ err
154
+ );
155
+ }
156
+ }
157
+ throw lastErr ?? new NexorNetworkError("Request failed");
158
+ }
159
+ function makeError(status, message, body, requestId) {
160
+ if (status === 401 || status === 403) {
161
+ return new NexorAuthError({ message, body, requestId });
162
+ }
163
+ if (status === 400 || status === 422) {
164
+ return new NexorValidationError({ message, body, requestId });
165
+ }
166
+ return new NexorAPIError({ status, message, body, requestId });
167
+ }
168
+ function pickMessage(body) {
169
+ if (!body || typeof body !== "object") return void 0;
170
+ const b = body;
171
+ if (typeof b.message === "string") return b.message;
172
+ if (typeof b.error === "string") return b.error;
173
+ return void 0;
174
+ }
175
+ async function safeJson(res) {
176
+ try {
177
+ const txt = await res.text();
178
+ if (!txt) return null;
179
+ try {
180
+ return JSON.parse(txt);
181
+ } catch {
182
+ return txt;
183
+ }
184
+ } catch {
185
+ return null;
186
+ }
187
+ }
188
+ function shouldRetry(status) {
189
+ return status === 429 || status >= 500 && status <= 599;
190
+ }
191
+ function backoffMs(attempt, res) {
192
+ if (res) {
193
+ const ra = res.headers.get("retry-after");
194
+ if (ra) {
195
+ const n = Number(ra);
196
+ if (!Number.isNaN(n) && n > 0) return Math.min(n * 1e3, 1e4);
197
+ }
198
+ }
199
+ const base = 300 * 2 ** attempt;
200
+ return base + Math.floor(Math.random() * 200);
201
+ }
202
+ function sleep(ms) {
203
+ return new Promise((r) => setTimeout(r, ms));
204
+ }
205
+ function stripTrailingSlash(s) {
206
+ return s.endsWith("/") ? s.slice(0, -1) : s;
207
+ }
208
+ function buildUrl(baseUrl, path, query) {
209
+ const p = path.startsWith("/") ? path : `/${path}`;
210
+ const qs = encodeQuery(query);
211
+ return `${baseUrl}${p}${qs ? `?${qs}` : ""}`;
212
+ }
213
+ function encodeQuery(query) {
214
+ if (!query) return "";
215
+ const parts = [];
216
+ for (const [k, v] of Object.entries(query)) {
217
+ if (v === void 0 || v === null) continue;
218
+ if (Array.isArray(v)) {
219
+ if (v.length === 0) continue;
220
+ parts.push(`${encodeURIComponent(k)}=${encodeURIComponent(v.join(","))}`);
221
+ } else {
222
+ parts.push(`${encodeURIComponent(k)}=${encodeURIComponent(String(v))}`);
223
+ }
224
+ }
225
+ return parts.join("&");
226
+ }
227
+
228
+ // src/client.ts
229
+ var PUBLIC_PREFIX = "/api/public";
230
+ var NexorClient = class {
231
+ constructor(opts) {
232
+ this.cfg = resolveConfig(opts);
233
+ }
234
+ // ─── Leads ───────────────────────────────────────────────────────────────
235
+ /** Create a single lead, optionally assigning it to a workflow. */
236
+ createLead(input, options) {
237
+ return request(this.cfg, {
238
+ method: "POST",
239
+ path: `${PUBLIC_PREFIX}/leads`,
240
+ body: input,
241
+ options
242
+ });
243
+ }
244
+ /** Create many leads in one request (max 1000). */
245
+ createLeadsBulk(inputs, options) {
246
+ return request(this.cfg, {
247
+ method: "POST",
248
+ path: `${PUBLIC_PREFIX}/leads`,
249
+ body: inputs,
250
+ options
251
+ });
252
+ }
253
+ /** Update an existing lead. metadata is shallow-merged server-side. */
254
+ updateLead(leadId, updates, options) {
255
+ return request(this.cfg, {
256
+ method: "PUT",
257
+ path: `${PUBLIC_PREFIX}/leads/${encodeURIComponent(leadId)}`,
258
+ body: updates,
259
+ options
260
+ });
261
+ }
262
+ /** Fetch a lead with its captured variables and per-channel engagement. */
263
+ getLead(leadId, options) {
264
+ return request(this.cfg, {
265
+ method: "GET",
266
+ path: `${PUBLIC_PREFIX}/leads/${encodeURIComponent(leadId)}`,
267
+ options
268
+ });
269
+ }
270
+ /** Full chronological history: messages, transcripts, activity. */
271
+ getLeadHistory(leadId, params = {}, options) {
272
+ const channel = Array.isArray(params.channel) ? params.channel : params.channel ? [params.channel] : void 0;
273
+ return request(this.cfg, {
274
+ method: "GET",
275
+ path: `${PUBLIC_PREFIX}/leads/${encodeURIComponent(leadId)}/history`,
276
+ query: { channel, limit: params.limit, offset: params.offset },
277
+ options
278
+ });
279
+ }
280
+ /** Has automation been paused (human takeover) for this lead? */
281
+ isLeadPaused(leadId, params = {}, options) {
282
+ return request(this.cfg, {
283
+ method: "GET",
284
+ path: `${PUBLIC_PREFIX}/leads/${encodeURIComponent(leadId)}/is_paused`,
285
+ query: { workflow_id: params.workflow_id },
286
+ options
287
+ });
288
+ }
289
+ /** Pause automation (engage human takeover) on a specific workflow run. */
290
+ stopAutomation(leadId, input, options) {
291
+ return request(this.cfg, {
292
+ method: "POST",
293
+ path: `${PUBLIC_PREFIX}/leads/${encodeURIComponent(leadId)}/automation/stop`,
294
+ body: input,
295
+ options
296
+ });
297
+ }
298
+ /** Resume automation; pass resume_cadence: true to restart scheduled touches. */
299
+ resumeAutomation(leadId, input, options) {
300
+ return request(this.cfg, {
301
+ method: "POST",
302
+ path: `${PUBLIC_PREFIX}/leads/${encodeURIComponent(leadId)}/automation/resume`,
303
+ body: input,
304
+ options
305
+ });
306
+ }
307
+ /**
308
+ * Replace the tag set on a lead (identified by lead_id OR email).
309
+ * Tags missing from the input are unassigned; new tags are find-or-created.
310
+ */
311
+ syncLeadTags(input, options) {
312
+ return request(this.cfg, {
313
+ method: "POST",
314
+ path: `${PUBLIC_PREFIX}/leads/tags`,
315
+ body: input,
316
+ options
317
+ });
318
+ }
319
+ /** All meetings on file for a lead (optionally filter by status). */
320
+ listLeadMeetings(leadId, params = {}, options) {
321
+ const status = Array.isArray(params.status) ? params.status.join(",") : params.status;
322
+ return request(this.cfg, {
323
+ method: "GET",
324
+ path: `${PUBLIC_PREFIX}/leads/${encodeURIComponent(leadId)}/meetings`,
325
+ query: { status },
326
+ options
327
+ });
328
+ }
329
+ // ─── Workflows / Campaigns / Templates ──────────────────────────────────
330
+ /** Active workflows in your account. */
331
+ listWorkflows(options) {
332
+ return request(this.cfg, {
333
+ method: "GET",
334
+ path: `${PUBLIC_PREFIX}/workflows`,
335
+ options
336
+ });
337
+ }
338
+ /** Campaigns in your account. */
339
+ listCampaigns(options) {
340
+ return request(this.cfg, {
341
+ method: "GET",
342
+ path: `${PUBLIC_PREFIX}/campaigns`,
343
+ options
344
+ });
345
+ }
346
+ /** Approved WhatsApp templates. */
347
+ listTemplates(params = {}, options) {
348
+ return request(this.cfg, {
349
+ method: "GET",
350
+ path: `${PUBLIC_PREFIX}/templates`,
351
+ query: { status: params.status, category: params.category },
352
+ options
353
+ });
354
+ }
355
+ // ─── Messages ───────────────────────────────────────────────────────────
356
+ /** Send a one-off message (WhatsApp/email) or trigger a call. */
357
+ sendMessage(input, options) {
358
+ return request(this.cfg, {
359
+ method: "POST",
360
+ path: `${PUBLIC_PREFIX}/messages`,
361
+ body: input,
362
+ options
363
+ });
364
+ }
365
+ // ─── Meetings ───────────────────────────────────────────────────────────
366
+ /** Log a booked meeting against an existing lead (matched by email). */
367
+ createMeeting(input, options) {
368
+ return request(this.cfg, {
369
+ method: "POST",
370
+ path: `${PUBLIC_PREFIX}/meetings`,
371
+ body: input,
372
+ options
373
+ });
374
+ }
375
+ /** Push a note-taker transcript / summary / action items to a meeting. */
376
+ createMeetingNotes(input, options) {
377
+ return request(this.cfg, {
378
+ method: "POST",
379
+ path: `${PUBLIC_PREFIX}/meeting-notes`,
380
+ body: input,
381
+ options
382
+ });
383
+ }
384
+ // ─── Chat (used by initChat widget) ─────────────────────────────────────
385
+ /**
386
+ * Send a turn in a browser-embedded chat session. Backed by
387
+ * POST /api/public/chat on the Nexor API.
388
+ *
389
+ * Most users will not call this directly — `initChat()` handles it.
390
+ */
391
+ chat(input, options) {
392
+ return request(this.cfg, {
393
+ method: "POST",
394
+ path: `${PUBLIC_PREFIX}/chat`,
395
+ body: input,
396
+ options
397
+ });
398
+ }
399
+ };
400
+
401
+ // src/chat/config.ts
402
+ function applyDefaults(o) {
403
+ return {
404
+ workflowId: o.workflowId,
405
+ title: o.title ?? "Chat with us",
406
+ subtitle: o.subtitle ?? "We typically reply in a few minutes",
407
+ greeting: o.greeting ?? "Hi there! \u{1F44B} How can we help?",
408
+ errorText: o.errorText ?? "Sorry, something went wrong. Please try again in a moment.",
409
+ locale: o.locale ?? (typeof navigator !== "undefined" ? navigator.language : void 0),
410
+ accentColor: o.accentColor ?? "#111827",
411
+ accentTextColor: o.accentTextColor ?? "#ffffff",
412
+ showBranding: o.showBranding ?? true,
413
+ position: o.position ?? "bottom-right",
414
+ openOnLoad: o.openOnLoad ?? false,
415
+ debounceMs: o.debounceMs ?? 700,
416
+ container: o.container,
417
+ systemPrompt: o.systemPrompt,
418
+ clientPrompt: o.clientPrompt,
419
+ lead: o.lead,
420
+ capture: {
421
+ fields: o.capture?.fields ?? ["first_name", "email"],
422
+ mode: o.capture?.mode ?? "before",
423
+ label: o.capture?.label ?? "Tell us a bit about you to get started:",
424
+ submitLabel: o.capture?.submitLabel ?? "Start chat"
425
+ },
426
+ metadata: o.metadata,
427
+ onOpen: o.onOpen,
428
+ onClose: o.onClose,
429
+ onMessage: o.onMessage,
430
+ onLeadCaptured: o.onLeadCaptured,
431
+ onError: o.onError
432
+ };
433
+ }
434
+ function needsCapture(cfg) {
435
+ if (cfg.capture.mode === "skip") return false;
436
+ if (cfg.lead) {
437
+ const have = cfg.lead;
438
+ return cfg.capture.fields.some((f) => !have[f]);
439
+ }
440
+ return true;
441
+ }
442
+
443
+ // src/chat/session.ts
444
+ var SESSION_KEY = "nexor.chat.session_id";
445
+ var SESSION_RETENTION_MS = 30 * 24 * 60 * 60 * 1e3;
446
+ function ensureSessionId() {
447
+ const now = Date.now();
448
+ try {
449
+ const rec = readRecord();
450
+ if (rec && now - rec.ts < SESSION_RETENTION_MS) {
451
+ writeRecord(rec.id, now);
452
+ return rec.id;
453
+ }
454
+ } catch {
455
+ }
456
+ const id = `sess_${randomHex(16)}`;
457
+ try {
458
+ writeRecord(id, now);
459
+ } catch {
460
+ }
461
+ return id;
462
+ }
463
+ function touchSession() {
464
+ try {
465
+ const rec = readRecord();
466
+ if (rec) writeRecord(rec.id, Date.now());
467
+ } catch {
468
+ }
469
+ }
470
+ function readRecord() {
471
+ const raw = window.localStorage.getItem(SESSION_KEY);
472
+ if (!raw) return null;
473
+ try {
474
+ const o = JSON.parse(raw);
475
+ if (o && typeof o.id === "string" && typeof o.ts === "number") return o;
476
+ } catch {
477
+ }
478
+ if (raw.startsWith("sess_")) return { id: raw, ts: Date.now() };
479
+ return null;
480
+ }
481
+ function writeRecord(id, ts) {
482
+ window.localStorage.setItem(SESSION_KEY, JSON.stringify({ id, ts }));
483
+ }
484
+ function randomHex(bytes) {
485
+ const arr = new Uint8Array(bytes);
486
+ if (typeof crypto !== "undefined" && crypto.getRandomValues) {
487
+ crypto.getRandomValues(arr);
488
+ } else {
489
+ for (let i = 0; i < bytes; i++) arr[i] = Math.floor(Math.random() * 256);
490
+ }
491
+ return Array.from(arr, (b) => b.toString(16).padStart(2, "0")).join("");
492
+ }
493
+
494
+ // src/chat/styles.ts
495
+ var widgetCss = (accent, accentText) => `
496
+ .nexor-chat,
497
+ .nexor-chat * {
498
+ box-sizing: border-box;
499
+ font-family: -apple-system, BlinkMacSystemFont, "Segoe UI", Roboto, "Helvetica Neue", Arial, sans-serif;
500
+ }
501
+
502
+ .nexor-chat {
503
+ position: fixed;
504
+ z-index: 2147483640;
505
+ bottom: 20px;
506
+ right: 20px;
507
+ font-size: 14px;
508
+ line-height: 1.4;
509
+ color: #111;
510
+ }
511
+
512
+ .nexor-chat[data-position="bottom-left"] {
513
+ right: auto;
514
+ left: 20px;
515
+ }
516
+
517
+ /* \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 */
518
+ .nexor-chat__launcher {
519
+ width: 56px;
520
+ height: 56px;
521
+ border-radius: 9999px;
522
+ background: ${accent};
523
+ color: ${accentText};
524
+ border: none;
525
+ cursor: pointer;
526
+ display: flex;
527
+ align-items: center;
528
+ justify-content: center;
529
+ box-shadow: 0 6px 18px rgba(0, 0, 0, 0.18);
530
+ transition: transform 160ms cubic-bezier(.2,.7,.2,1.1),
531
+ box-shadow 160ms ease,
532
+ background 200ms ease;
533
+ padding: 0;
534
+ position: relative;
535
+ animation: nexor-launcher-in 360ms cubic-bezier(.2,.7,.2,1.1) both;
536
+ }
537
+ .nexor-chat__launcher:hover {
538
+ transform: translateY(-2px) scale(1.04);
539
+ box-shadow: 0 10px 24px rgba(0, 0, 0, 0.24);
540
+ }
541
+ .nexor-chat__launcher:active {
542
+ transform: translateY(0) scale(0.97);
543
+ transition-duration: 80ms;
544
+ }
545
+ .nexor-chat__launcher:focus-visible {
546
+ outline: 2px solid ${accent};
547
+ outline-offset: 2px;
548
+ }
549
+ .nexor-chat[data-open="true"] .nexor-chat__launcher {
550
+ transform: scale(0.85);
551
+ opacity: 0.75;
552
+ }
553
+ .nexor-chat__launcher svg { width: 24px; height: 24px; transition: transform 200ms ease; }
554
+ .nexor-chat[data-open="true"] .nexor-chat__launcher svg { transform: rotate(8deg); }
555
+
556
+ .nexor-chat__unread {
557
+ position: absolute;
558
+ top: -4px;
559
+ right: -4px;
560
+ min-width: 18px;
561
+ height: 18px;
562
+ background: #ef4444;
563
+ color: #fff;
564
+ border-radius: 9999px;
565
+ font-size: 11px;
566
+ font-weight: 600;
567
+ padding: 0 5px;
568
+ display: flex;
569
+ align-items: center;
570
+ justify-content: center;
571
+ border: 2px solid #fff;
572
+ animation: nexor-pop-in 300ms cubic-bezier(.2,.7,.2,1.1) both;
573
+ }
574
+
575
+ @keyframes nexor-launcher-in {
576
+ from { transform: translateY(20px) scale(0.6); opacity: 0; }
577
+ to { transform: translateY(0) scale(1); opacity: 1; }
578
+ }
579
+ @keyframes nexor-pop-in {
580
+ from { transform: scale(0); }
581
+ to { transform: scale(1); }
582
+ }
583
+
584
+ /* \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 */
585
+ .nexor-chat__panel {
586
+ position: absolute;
587
+ bottom: 72px;
588
+ right: 0;
589
+ width: 360px;
590
+ max-width: calc(100vw - 32px);
591
+ height: 540px;
592
+ max-height: calc(100vh - 120px);
593
+ background: #fff;
594
+ border-radius: 14px;
595
+ box-shadow: 0 24px 60px rgba(0, 0, 0, 0.22);
596
+ display: flex;
597
+ flex-direction: column;
598
+ overflow: hidden;
599
+ transform: translateY(14px) scale(0.96);
600
+ opacity: 0;
601
+ pointer-events: none;
602
+ transition: transform 220ms cubic-bezier(.2,.7,.2,1.1),
603
+ opacity 220ms ease;
604
+ border: 1px solid rgba(0, 0, 0, 0.06);
605
+ /* Use visibility so screen readers don't read collapsed content */
606
+ visibility: hidden;
607
+ }
608
+ .nexor-chat[data-position="bottom-left"] .nexor-chat__panel {
609
+ right: auto;
610
+ left: 0;
611
+ }
612
+ .nexor-chat[data-open="true"] .nexor-chat__panel {
613
+ transform: translateY(0) scale(1);
614
+ opacity: 1;
615
+ pointer-events: auto;
616
+ visibility: visible;
617
+ }
618
+
619
+ .nexor-chat__header {
620
+ background: ${accent};
621
+ color: ${accentText};
622
+ padding: 14px 16px;
623
+ display: flex;
624
+ align-items: center;
625
+ justify-content: space-between;
626
+ gap: 12px;
627
+ }
628
+ .nexor-chat__title {
629
+ font-weight: 600;
630
+ font-size: 15px;
631
+ margin: 0;
632
+ }
633
+ .nexor-chat__subtitle {
634
+ font-size: 12px;
635
+ opacity: 0.85;
636
+ margin: 2px 0 0;
637
+ }
638
+ .nexor-chat__status-dot {
639
+ display: inline-block;
640
+ width: 8px;
641
+ height: 8px;
642
+ border-radius: 9999px;
643
+ background: #34d399;
644
+ margin-right: 6px;
645
+ vertical-align: 1px;
646
+ animation: nexor-pulse 2.4s ease-in-out infinite;
647
+ box-shadow: 0 0 0 0 rgba(52, 211, 153, 0.6);
648
+ }
649
+ @keyframes nexor-pulse {
650
+ 0%, 100% { box-shadow: 0 0 0 0 rgba(52, 211, 153, 0.55); }
651
+ 50% { box-shadow: 0 0 0 6px rgba(52, 211, 153, 0); }
652
+ }
653
+ .nexor-chat__close {
654
+ background: transparent;
655
+ border: none;
656
+ color: inherit;
657
+ cursor: pointer;
658
+ padding: 4px;
659
+ border-radius: 6px;
660
+ opacity: 0.9;
661
+ transition: background 140ms ease, transform 140ms ease;
662
+ }
663
+ .nexor-chat__close:hover { background: rgba(255,255,255,0.18); opacity: 1; }
664
+ .nexor-chat__close:active { transform: scale(0.92); }
665
+ .nexor-chat__close svg { width: 18px; height: 18px; display: block; }
666
+
667
+ /* \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 */
668
+ .nexor-chat__body {
669
+ flex: 1;
670
+ overflow-y: auto;
671
+ padding: 14px;
672
+ background: #f6f7f9;
673
+ display: flex;
674
+ flex-direction: column;
675
+ gap: 8px;
676
+ scroll-behavior: smooth;
677
+ /* Prevent layout shift when scrollbar appears */
678
+ scrollbar-gutter: stable;
679
+ }
680
+ .nexor-chat__body::-webkit-scrollbar { width: 6px; }
681
+ .nexor-chat__body::-webkit-scrollbar-thumb {
682
+ background: rgba(0,0,0,0.15);
683
+ border-radius: 9999px;
684
+ }
685
+
686
+ .nexor-chat__msg {
687
+ max-width: 80%;
688
+ padding: 8px 12px;
689
+ border-radius: 14px;
690
+ white-space: pre-wrap;
691
+ word-wrap: break-word;
692
+ animation: nexor-msg-in 240ms cubic-bezier(.2,.7,.2,1.1) both;
693
+ will-change: transform, opacity;
694
+ }
695
+ .nexor-chat__msg--bot {
696
+ background: #fff;
697
+ align-self: flex-start;
698
+ border: 1px solid rgba(0,0,0,0.06);
699
+ border-bottom-left-radius: 4px;
700
+ box-shadow: 0 1px 2px rgba(0,0,0,0.04);
701
+ }
702
+ .nexor-chat__msg--user {
703
+ background: ${accent};
704
+ color: ${accentText};
705
+ align-self: flex-end;
706
+ border-bottom-right-radius: 4px;
707
+ animation-name: nexor-msg-in-user;
708
+ }
709
+ .nexor-chat__msg--system {
710
+ background: transparent;
711
+ color: #6b7280;
712
+ font-size: 12px;
713
+ align-self: center;
714
+ text-align: center;
715
+ max-width: 100%;
716
+ animation-name: nexor-fade-in;
717
+ }
718
+ .nexor-chat__msg-time {
719
+ display: block;
720
+ margin-top: 4px;
721
+ font-size: 10px;
722
+ line-height: 1;
723
+ opacity: 0.55;
724
+ text-align: right;
725
+ }
726
+ .nexor-chat__msg--bot .nexor-chat__msg-time { text-align: left; }
727
+
728
+ @keyframes nexor-msg-in {
729
+ from { transform: translateY(8px) scale(0.96); opacity: 0; }
730
+ to { transform: translateY(0) scale(1); opacity: 1; }
731
+ }
732
+ @keyframes nexor-msg-in-user {
733
+ from { transform: translateY(8px) translateX(8px) scale(0.96); opacity: 0; }
734
+ to { transform: translateY(0) translateX(0) scale(1); opacity: 1; }
735
+ }
736
+ @keyframes nexor-fade-in {
737
+ from { opacity: 0; }
738
+ to { opacity: 1; }
739
+ }
740
+
741
+ .nexor-chat__typing {
742
+ align-self: flex-start;
743
+ background: #fff;
744
+ border: 1px solid rgba(0,0,0,0.06);
745
+ border-radius: 14px;
746
+ border-bottom-left-radius: 4px;
747
+ padding: 10px 14px;
748
+ display: inline-flex;
749
+ gap: 4px;
750
+ animation: nexor-msg-in 240ms cubic-bezier(.2,.7,.2,1.1) both;
751
+ }
752
+ .nexor-chat__typing span {
753
+ width: 6px; height: 6px; border-radius: 9999px;
754
+ background: #9ca3af;
755
+ display: inline-block;
756
+ animation: nexor-blink 1.2s infinite ease-in-out;
757
+ }
758
+ .nexor-chat__typing span:nth-child(2) { animation-delay: 0.15s; }
759
+ .nexor-chat__typing span:nth-child(3) { animation-delay: 0.30s; }
760
+ @keyframes nexor-blink {
761
+ 0%, 60%, 100% { opacity: 0.3; transform: translateY(0); }
762
+ 30% { opacity: 1; transform: translateY(-3px); }
763
+ }
764
+
765
+ /* \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 */
766
+ .nexor-chat__capture {
767
+ background: #fff;
768
+ border: 1px solid rgba(0,0,0,0.06);
769
+ border-radius: 12px;
770
+ padding: 12px;
771
+ display: flex;
772
+ flex-direction: column;
773
+ gap: 8px;
774
+ margin-top: 4px;
775
+ animation: nexor-msg-in 280ms cubic-bezier(.2,.7,.2,1.1) both;
776
+ box-shadow: 0 2px 8px rgba(0,0,0,0.05);
777
+ }
778
+ .nexor-chat__capture-label {
779
+ font-size: 12px;
780
+ color: #6b7280;
781
+ margin: 0;
782
+ }
783
+ .nexor-chat__capture-row {
784
+ display: flex;
785
+ gap: 8px;
786
+ }
787
+ .nexor-chat__capture-row > * { flex: 1; }
788
+ .nexor-chat__input,
789
+ .nexor-chat__capture input {
790
+ font: inherit;
791
+ padding: 8px 10px;
792
+ border: 1px solid #d1d5db;
793
+ border-radius: 8px;
794
+ background: #fff;
795
+ color: #111;
796
+ outline: none;
797
+ width: 100%;
798
+ transition: border-color 140ms ease, box-shadow 140ms ease;
799
+ }
800
+ .nexor-chat__input:focus,
801
+ .nexor-chat__capture input:focus {
802
+ border-color: ${accent};
803
+ box-shadow: 0 0 0 3px ${accent}33;
804
+ }
805
+ .nexor-chat__capture-submit {
806
+ background: ${accent};
807
+ color: ${accentText};
808
+ border: none;
809
+ border-radius: 8px;
810
+ padding: 8px 12px;
811
+ font: inherit;
812
+ font-weight: 600;
813
+ cursor: pointer;
814
+ transition: transform 140ms ease, filter 140ms ease;
815
+ }
816
+ .nexor-chat__capture-submit:hover { filter: brightness(1.08); }
817
+ .nexor-chat__capture-submit:active { transform: scale(0.97); }
818
+ .nexor-chat__capture-submit[disabled] { opacity: 0.6; cursor: not-allowed; }
819
+
820
+ /* \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 */
821
+ .nexor-chat__composer {
822
+ display: flex;
823
+ gap: 8px;
824
+ padding: 10px;
825
+ background: #fff;
826
+ border-top: 1px solid rgba(0,0,0,0.06);
827
+ align-items: stretch;
828
+ }
829
+ .nexor-chat__send {
830
+ background: ${accent};
831
+ color: ${accentText};
832
+ border: none;
833
+ border-radius: 8px;
834
+ padding: 0 14px;
835
+ font: inherit;
836
+ font-weight: 600;
837
+ cursor: pointer;
838
+ min-width: 44px;
839
+ display: flex;
840
+ align-items: center;
841
+ justify-content: center;
842
+ transition: transform 140ms ease, filter 140ms ease;
843
+ }
844
+ .nexor-chat__send:hover { filter: brightness(1.08); }
845
+ .nexor-chat__send:active { transform: scale(0.95); }
846
+ .nexor-chat__send[disabled] {
847
+ opacity: 0.6;
848
+ cursor: not-allowed;
849
+ transform: none;
850
+ }
851
+ .nexor-chat__send svg {
852
+ width: 18px;
853
+ height: 18px;
854
+ transition: transform 200ms ease;
855
+ }
856
+ .nexor-chat__send:not([disabled]):hover svg { transform: translateX(2px); }
857
+
858
+ /* Small spinner inside the send button while a turn is in flight */
859
+ .nexor-chat__send--loading svg { display: none; }
860
+ .nexor-chat__send--loading::after {
861
+ content: "";
862
+ width: 14px;
863
+ height: 14px;
864
+ border-radius: 9999px;
865
+ border: 2px solid currentColor;
866
+ border-top-color: transparent;
867
+ animation: nexor-spin 700ms linear infinite;
868
+ }
869
+ @keyframes nexor-spin {
870
+ to { transform: rotate(360deg); }
871
+ }
872
+
873
+ .nexor-chat__footer {
874
+ text-align: center;
875
+ font-size: 11px;
876
+ color: #9ca3af;
877
+ padding: 6px;
878
+ background: #fff;
879
+ border-top: 1px solid rgba(0,0,0,0.04);
880
+ }
881
+ .nexor-chat__footer a {
882
+ color: inherit;
883
+ text-decoration: none;
884
+ font-weight: 600;
885
+ }
886
+ .nexor-chat__footer a:hover { text-decoration: underline; }
887
+
888
+ /* Respect users who request reduced motion. */
889
+ @media (prefers-reduced-motion: reduce) {
890
+ .nexor-chat,
891
+ .nexor-chat * {
892
+ animation-duration: 0.001ms !important;
893
+ transition-duration: 0.001ms !important;
894
+ }
895
+ .nexor-chat__status-dot { animation: none; }
896
+ }
897
+ `;
898
+
899
+ // src/chat/dom.ts
900
+ var STYLE_ATTR = "data-nexor-chat-styles";
901
+ function injectStyles(accent, accentText) {
902
+ if (document.querySelector(`style[${STYLE_ATTR}]`)) return;
903
+ const style = document.createElement("style");
904
+ style.setAttribute(STYLE_ATTR, "1");
905
+ style.textContent = widgetCss(accent, accentText);
906
+ document.head.appendChild(style);
907
+ }
908
+ function buildLauncher() {
909
+ const el = document.createElement("button");
910
+ el.type = "button";
911
+ el.className = "nexor-chat__launcher";
912
+ el.setAttribute("aria-label", "Open chat");
913
+ el.innerHTML = `
914
+ <svg viewBox="0 0 24 24" fill="none" xmlns="http://www.w3.org/2000/svg" aria-hidden="true">
915
+ <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"
916
+ stroke="currentColor" stroke-width="1.8" stroke-linejoin="round"/>
917
+ </svg>
918
+ `;
919
+ const badge = document.createElement("span");
920
+ badge.className = "nexor-chat__unread";
921
+ badge.style.display = "none";
922
+ el.appendChild(badge);
923
+ return { el, badge };
924
+ }
925
+ function buildPanel(cfg) {
926
+ const el = document.createElement("div");
927
+ el.className = "nexor-chat__panel";
928
+ el.setAttribute("role", "dialog");
929
+ el.setAttribute("aria-label", cfg.title);
930
+ const { header, closeBtn } = buildHeader(cfg);
931
+ const { body, captureForm, captureInputs } = buildBody(cfg);
932
+ const { form, input, send } = buildComposer();
933
+ const footer = cfg.showBranding ? buildFooter() : null;
934
+ el.appendChild(header);
935
+ el.appendChild(body);
936
+ el.appendChild(form);
937
+ if (footer) el.appendChild(footer);
938
+ return { el, closeBtn, body, form, input, send, captureForm, captureInputs };
939
+ }
940
+ function buildHeader(cfg) {
941
+ const header = document.createElement("div");
942
+ header.className = "nexor-chat__header";
943
+ const subtitleHtml = cfg.subtitle ? `<p class="nexor-chat__subtitle">
944
+ <span class="nexor-chat__status-dot" aria-hidden="true"></span>${escapeHtml(cfg.subtitle)}
945
+ </p>` : "";
946
+ header.innerHTML = `
947
+ <div>
948
+ <p class="nexor-chat__title">${escapeHtml(cfg.title)}</p>
949
+ ${subtitleHtml}
950
+ </div>
951
+ `;
952
+ const closeBtn = document.createElement("button");
953
+ closeBtn.type = "button";
954
+ closeBtn.className = "nexor-chat__close";
955
+ closeBtn.setAttribute("aria-label", "Close chat");
956
+ closeBtn.innerHTML = `
957
+ <svg viewBox="0 0 24 24" fill="none" xmlns="http://www.w3.org/2000/svg" aria-hidden="true">
958
+ <path d="M6 6l12 12M6 18L18 6" stroke="currentColor" stroke-width="2" stroke-linecap="round"/>
959
+ </svg>
960
+ `;
961
+ header.appendChild(closeBtn);
962
+ return { header, closeBtn };
963
+ }
964
+ function buildBody(cfg) {
965
+ const body = document.createElement("div");
966
+ body.className = "nexor-chat__body";
967
+ const captureInputs = {};
968
+ let captureForm = null;
969
+ if (needsCapture(cfg)) {
970
+ captureForm = buildCaptureForm(cfg, captureInputs);
971
+ body.appendChild(captureForm);
972
+ }
973
+ return { body, captureForm, captureInputs };
974
+ }
975
+ function buildCaptureForm(cfg, inputsOut) {
976
+ const form = document.createElement("form");
977
+ form.className = "nexor-chat__capture";
978
+ form.noValidate = true;
979
+ const label = document.createElement("p");
980
+ label.className = "nexor-chat__capture-label";
981
+ label.textContent = cfg.capture.label;
982
+ form.appendChild(label);
983
+ const fields = cfg.capture.fields;
984
+ if (fields.includes("first_name") || fields.includes("last_name")) {
985
+ const row = document.createElement("div");
986
+ row.className = "nexor-chat__capture-row";
987
+ if (fields.includes("first_name")) {
988
+ inputsOut.first_name = makeInput("first_name", "First name", "text", cfg.lead?.first_name);
989
+ row.appendChild(inputsOut.first_name);
990
+ }
991
+ if (fields.includes("last_name")) {
992
+ inputsOut.last_name = makeInput("last_name", "Last name", "text", cfg.lead?.last_name);
993
+ row.appendChild(inputsOut.last_name);
994
+ }
995
+ if (row.childElementCount > 0) form.appendChild(row);
996
+ }
997
+ if (fields.includes("email")) {
998
+ inputsOut.email = makeInput("email", "Email", "email", cfg.lead?.email);
999
+ form.appendChild(inputsOut.email);
1000
+ }
1001
+ if (fields.includes("phone")) {
1002
+ inputsOut.phone = makeInput("phone", "Phone", "tel", cfg.lead?.phone);
1003
+ form.appendChild(inputsOut.phone);
1004
+ }
1005
+ const submit = document.createElement("button");
1006
+ submit.type = "submit";
1007
+ submit.className = "nexor-chat__capture-submit";
1008
+ submit.textContent = cfg.capture.submitLabel;
1009
+ form.appendChild(submit);
1010
+ return form;
1011
+ }
1012
+ function buildComposer() {
1013
+ const form = document.createElement("form");
1014
+ form.className = "nexor-chat__composer";
1015
+ const input = document.createElement("input");
1016
+ input.type = "text";
1017
+ input.className = "nexor-chat__input";
1018
+ input.placeholder = "Type a message\u2026";
1019
+ input.autocomplete = "off";
1020
+ const send = document.createElement("button");
1021
+ send.type = "submit";
1022
+ send.className = "nexor-chat__send";
1023
+ send.setAttribute("aria-label", "Send message");
1024
+ send.innerHTML = `
1025
+ <svg viewBox="0 0 24 24" fill="none" xmlns="http://www.w3.org/2000/svg" aria-hidden="true">
1026
+ <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"
1027
+ stroke="currentColor" stroke-width="1.6" stroke-linejoin="round" fill="currentColor"/>
1028
+ </svg>
1029
+ `;
1030
+ form.appendChild(input);
1031
+ form.appendChild(send);
1032
+ return { form, input, send };
1033
+ }
1034
+ function buildFooter() {
1035
+ const el = document.createElement("div");
1036
+ el.className = "nexor-chat__footer";
1037
+ el.innerHTML = `Powered by <a href="https://www.getnexor.ai" target="_blank" rel="noopener noreferrer">Nexor</a>`;
1038
+ return el;
1039
+ }
1040
+ function buildMessage(role, text, ts, locale) {
1041
+ const el = document.createElement("div");
1042
+ el.className = `nexor-chat__msg nexor-chat__msg--${role}`;
1043
+ const body = document.createElement("span");
1044
+ body.className = "nexor-chat__msg-text";
1045
+ body.textContent = text;
1046
+ el.appendChild(body);
1047
+ if (role !== "system" && ts) {
1048
+ const time = document.createElement("time");
1049
+ time.className = "nexor-chat__msg-time";
1050
+ time.dataset.ts = String(ts);
1051
+ time.textContent = formatRelativeTime(ts, locale);
1052
+ el.appendChild(time);
1053
+ }
1054
+ return el;
1055
+ }
1056
+ function formatRelativeTime(ts, locale, now = Date.now()) {
1057
+ try {
1058
+ const diff = Math.max(0, now - ts);
1059
+ const min = Math.round(diff / 6e4);
1060
+ const hr = Math.round(diff / 36e5);
1061
+ const day = Math.round(diff / 864e5);
1062
+ const rtf = new Intl.RelativeTimeFormat(locale, { numeric: "auto" });
1063
+ if (diff < 45e3) return rtf.format(0, "second");
1064
+ if (min < 60) return rtf.format(-min, "minute");
1065
+ if (hr < 24) return rtf.format(-hr, "hour");
1066
+ if (day < 7) return rtf.format(-day, "day");
1067
+ return new Date(ts).toLocaleDateString(locale, {
1068
+ day: "numeric",
1069
+ month: "short"
1070
+ });
1071
+ } catch {
1072
+ try {
1073
+ return new Date(ts).toLocaleTimeString(locale, {
1074
+ hour: "2-digit",
1075
+ minute: "2-digit"
1076
+ });
1077
+ } catch {
1078
+ return "";
1079
+ }
1080
+ }
1081
+ }
1082
+ function buildTypingIndicator() {
1083
+ const el = document.createElement("div");
1084
+ el.className = "nexor-chat__typing";
1085
+ el.innerHTML = "<span></span><span></span><span></span>";
1086
+ return el;
1087
+ }
1088
+ function makeInput(name, placeholder, type, value) {
1089
+ const i = document.createElement("input");
1090
+ i.name = name;
1091
+ i.type = type;
1092
+ i.placeholder = placeholder;
1093
+ if (value) i.value = value;
1094
+ return i;
1095
+ }
1096
+ function escapeHtml(s) {
1097
+ return s.replace(/&/g, "&amp;").replace(/</g, "&lt;").replace(/>/g, "&gt;").replace(/"/g, "&quot;").replace(/'/g, "&#039;");
1098
+ }
1099
+
1100
+ // src/chat/index.ts
1101
+ var TYPING_DELAY_MS = 220;
1102
+ function initChat(client, options) {
1103
+ ensureBrowser();
1104
+ if (!options || !options.workflowId) {
1105
+ throw new Error("Nexor SDK: initChat() requires `workflowId`.");
1106
+ }
1107
+ const cfg = applyDefaults(options);
1108
+ injectStyles(cfg.accentColor, cfg.accentTextColor);
1109
+ const sessionId = ensureSessionId();
1110
+ const historyKey = `nexor.chat.history.${sessionId}`;
1111
+ const root = buildRoot(cfg);
1112
+ const launcher = buildLauncher();
1113
+ const panel = buildPanel(cfg);
1114
+ root.appendChild(launcher.el);
1115
+ root.appendChild(panel.el);
1116
+ (cfg.container ?? document.body).appendChild(root);
1117
+ const state = {
1118
+ isOpen: false,
1119
+ unread: 0,
1120
+ leadId: void 0,
1121
+ captured: !needsCapture(cfg),
1122
+ pendingLead: { ...cfg.lead ?? {} },
1123
+ history: []
1124
+ };
1125
+ launcher.el.addEventListener("click", toggle);
1126
+ panel.closeBtn.addEventListener("click", close);
1127
+ panel.form.addEventListener("submit", onComposerSubmit);
1128
+ panel.captureForm?.addEventListener("submit", onCaptureSubmit);
1129
+ syncComposerLock();
1130
+ const restored = loadHistory();
1131
+ if (restored.length) {
1132
+ state.history = restored;
1133
+ for (const m of restored) appendBubble(m.role === "user" ? "user" : "bot", m.text, m.ts);
1134
+ scrollToBottom();
1135
+ }
1136
+ if (cfg.openOnLoad) open();
1137
+ function open() {
1138
+ if (state.isOpen) return;
1139
+ state.isOpen = true;
1140
+ root.setAttribute("data-open", "true");
1141
+ clearUnread();
1142
+ if (state.history.length === 0 && cfg.greeting && state.captured) {
1143
+ appendBot(cfg.greeting);
1144
+ }
1145
+ panel.input.focus();
1146
+ cfg.onOpen?.();
1147
+ }
1148
+ function close() {
1149
+ if (!state.isOpen) return;
1150
+ state.isOpen = false;
1151
+ root.setAttribute("data-open", "false");
1152
+ cfg.onClose?.();
1153
+ }
1154
+ function toggle() {
1155
+ state.isOpen ? close() : open();
1156
+ }
1157
+ function onCaptureSubmit(e) {
1158
+ e.preventDefault();
1159
+ const collected = readCaptureFields(panel);
1160
+ const required = cfg.capture.fields;
1161
+ for (const f of required) {
1162
+ if (!collected[f]) {
1163
+ panel.captureInputs[f]?.focus();
1164
+ return;
1165
+ }
1166
+ }
1167
+ state.pendingLead = { ...state.pendingLead, ...collected };
1168
+ state.captured = true;
1169
+ panel.captureForm?.remove();
1170
+ syncComposerLock();
1171
+ panel.input.focus();
1172
+ if (cfg.greeting && state.history.length === 0) appendBot(cfg.greeting);
1173
+ }
1174
+ const send = {
1175
+ pending: [],
1176
+ timer: 0,
1177
+ typingTimer: 0,
1178
+ inFlight: false,
1179
+ typing: null
1180
+ };
1181
+ function onComposerSubmit(e) {
1182
+ e.preventDefault();
1183
+ const text = panel.input.value.trim();
1184
+ if (!text) return;
1185
+ panel.input.value = "";
1186
+ queueMessage(text);
1187
+ }
1188
+ function queueMessage(text) {
1189
+ if (!state.captured) {
1190
+ panel.captureInputs.first_name?.focus();
1191
+ return;
1192
+ }
1193
+ appendUser(text);
1194
+ send.pending.push(text);
1195
+ scheduleFlush();
1196
+ }
1197
+ function scheduleFlush() {
1198
+ if (send.timer) window.clearTimeout(send.timer);
1199
+ send.timer = window.setTimeout(() => void flush(), cfg.debounceMs);
1200
+ }
1201
+ async function flush() {
1202
+ if (send.inFlight || send.pending.length === 0) return;
1203
+ const batch = send.pending.splice(0, send.pending.length);
1204
+ const message = batch.join("\n");
1205
+ send.inFlight = true;
1206
+ send.typingTimer = window.setTimeout(() => {
1207
+ send.typing = appendNode(buildTypingIndicator());
1208
+ }, TYPING_DELAY_MS);
1209
+ try {
1210
+ const res = await client.chat({
1211
+ workflow_id: cfg.workflowId,
1212
+ session_id: sessionId,
1213
+ message,
1214
+ system_prompt: cfg.systemPrompt,
1215
+ client_prompt: cfg.clientPrompt,
1216
+ lead: Object.keys(state.pendingLead).length ? state.pendingLead : void 0,
1217
+ metadata: cfg.metadata
1218
+ });
1219
+ if (res.lead_id && !state.leadId) {
1220
+ state.leadId = res.lead_id;
1221
+ cfg.onLeadCaptured?.(state.leadId);
1222
+ }
1223
+ const reply = (res.reply ?? "").trim();
1224
+ if (reply) {
1225
+ appendBot(reply);
1226
+ } else {
1227
+ appendSystem(cfg.errorText);
1228
+ cfg.onError?.(new Error("Nexor SDK: empty reply from server"));
1229
+ }
1230
+ } catch (err) {
1231
+ appendSystem(cfg.errorText);
1232
+ cfg.onError?.(err);
1233
+ } finally {
1234
+ window.clearTimeout(send.typingTimer);
1235
+ send.typing?.remove();
1236
+ send.typing = null;
1237
+ send.inFlight = false;
1238
+ panel.input.focus();
1239
+ if (send.pending.length) scheduleFlush();
1240
+ }
1241
+ }
1242
+ function appendUser(text) {
1243
+ const ts = Date.now();
1244
+ appendBubble("user", text, ts);
1245
+ state.history.push({ role: "user", text, ts });
1246
+ saveHistory();
1247
+ cfg.onMessage?.({ role: "user", text });
1248
+ }
1249
+ function appendBot(text) {
1250
+ const ts = Date.now();
1251
+ appendBubble("bot", text, ts);
1252
+ state.history.push({ role: "bot", text, ts });
1253
+ saveHistory();
1254
+ if (!state.isOpen) bumpUnread();
1255
+ cfg.onMessage?.({ role: "bot", text });
1256
+ }
1257
+ function saveHistory() {
1258
+ try {
1259
+ window.localStorage.setItem(
1260
+ historyKey,
1261
+ JSON.stringify({ ts: Date.now(), history: state.history })
1262
+ );
1263
+ touchSession();
1264
+ } catch {
1265
+ }
1266
+ }
1267
+ function loadHistory() {
1268
+ try {
1269
+ const raw = window.localStorage.getItem(historyKey);
1270
+ if (!raw) return [];
1271
+ const o = JSON.parse(raw);
1272
+ if (!o || !Array.isArray(o.history)) return [];
1273
+ if (Date.now() - (o.ts || 0) > SESSION_RETENTION_MS) {
1274
+ window.localStorage.removeItem(historyKey);
1275
+ return [];
1276
+ }
1277
+ return o.history;
1278
+ } catch {
1279
+ return [];
1280
+ }
1281
+ }
1282
+ function appendSystem(text) {
1283
+ appendBubble("system", text);
1284
+ }
1285
+ function appendBubble(role, text, ts) {
1286
+ return appendNode(buildMessage(role, text, ts, cfg.locale));
1287
+ }
1288
+ const refreshTimer = window.setInterval(() => {
1289
+ panel.body.querySelectorAll(".nexor-chat__msg-time[data-ts]").forEach((el) => {
1290
+ const ts = Number(el.dataset.ts);
1291
+ if (ts) el.textContent = formatRelativeTime(ts, cfg.locale);
1292
+ });
1293
+ }, 6e4);
1294
+ function appendNode(node) {
1295
+ panel.body.appendChild(node);
1296
+ scrollToBottom();
1297
+ return node;
1298
+ }
1299
+ function scrollToBottom() {
1300
+ panel.body.scrollTop = panel.body.scrollHeight;
1301
+ }
1302
+ function bumpUnread() {
1303
+ state.unread++;
1304
+ launcher.badge.textContent = String(state.unread);
1305
+ launcher.badge.style.display = "flex";
1306
+ }
1307
+ function clearUnread() {
1308
+ state.unread = 0;
1309
+ launcher.badge.style.display = "none";
1310
+ }
1311
+ function syncComposerLock() {
1312
+ const lock = !state.captured;
1313
+ panel.input.disabled = lock;
1314
+ panel.send.disabled = lock;
1315
+ panel.input.placeholder = lock ? "Complete the form above to start\u2026" : "Type a message\u2026";
1316
+ }
1317
+ return {
1318
+ open,
1319
+ close,
1320
+ toggle,
1321
+ // Programmatic send goes through the same debounce/coalesce path. Resolves
1322
+ // immediately — the message is queued, not necessarily sent yet.
1323
+ send: (text) => {
1324
+ queueMessage(text);
1325
+ return Promise.resolve();
1326
+ },
1327
+ destroy: () => {
1328
+ if (send.timer) window.clearTimeout(send.timer);
1329
+ if (send.typingTimer) window.clearTimeout(send.typingTimer);
1330
+ window.clearInterval(refreshTimer);
1331
+ root.remove();
1332
+ },
1333
+ getSessionId: () => sessionId
1334
+ };
1335
+ }
1336
+ function ensureBrowser() {
1337
+ if (typeof window === "undefined" || typeof document === "undefined") {
1338
+ throw new Error("Nexor SDK: initChat() requires a browser environment.");
1339
+ }
1340
+ }
1341
+ function buildRoot(cfg) {
1342
+ const root = document.createElement("div");
1343
+ root.className = "nexor-chat";
1344
+ root.setAttribute("data-position", cfg.position);
1345
+ root.setAttribute("data-open", "false");
1346
+ return root;
1347
+ }
1348
+ function readCaptureFields(panel) {
1349
+ const out = {};
1350
+ const inputs = panel.captureInputs;
1351
+ if (inputs.first_name) out.first_name = inputs.first_name.value.trim();
1352
+ if (inputs.last_name) out.last_name = inputs.last_name.value.trim();
1353
+ if (inputs.email) out.email = inputs.email.value.trim();
1354
+ if (inputs.phone) out.phone = inputs.phone.value.trim();
1355
+ return out;
1356
+ }
1357
+
1358
+ // src/index.ts
1359
+ function createClient(opts) {
1360
+ return new NexorClient(opts);
1361
+ }
1362
+ var _client = null;
1363
+ function getClient() {
1364
+ if (!_client) {
1365
+ throw new Error(
1366
+ "Nexor SDK: call `nexor.init({ apiKey })` before using the API."
1367
+ );
1368
+ }
1369
+ return _client;
1370
+ }
1371
+ function init(opts) {
1372
+ _client = new NexorClient(opts);
1373
+ return _client;
1374
+ }
1375
+ function isInitialized() {
1376
+ return _client !== null;
1377
+ }
1378
+ function reset() {
1379
+ _client = null;
1380
+ }
1381
+ var nexor = {
1382
+ init,
1383
+ isInitialized,
1384
+ reset,
1385
+ get client() {
1386
+ return getClient();
1387
+ },
1388
+ // Leads
1389
+ createLead: ((...args) => getClient().createLead(...args)),
1390
+ createLeadsBulk: ((...args) => getClient().createLeadsBulk(...args)),
1391
+ updateLead: ((...args) => getClient().updateLead(...args)),
1392
+ getLead: ((...args) => getClient().getLead(...args)),
1393
+ getLeadHistory: ((...args) => getClient().getLeadHistory(...args)),
1394
+ isLeadPaused: ((...args) => getClient().isLeadPaused(...args)),
1395
+ stopAutomation: ((...args) => getClient().stopAutomation(...args)),
1396
+ resumeAutomation: ((...args) => getClient().resumeAutomation(...args)),
1397
+ syncLeadTags: ((...args) => getClient().syncLeadTags(...args)),
1398
+ listLeadMeetings: ((...args) => getClient().listLeadMeetings(...args)),
1399
+ // Workflows / Campaigns / Templates
1400
+ listWorkflows: ((...args) => getClient().listWorkflows(...args)),
1401
+ listCampaigns: ((...args) => getClient().listCampaigns(...args)),
1402
+ listTemplates: ((...args) => getClient().listTemplates(...args)),
1403
+ // Messages
1404
+ sendMessage: ((...args) => getClient().sendMessage(...args)),
1405
+ // Meetings
1406
+ createMeeting: ((...args) => getClient().createMeeting(...args)),
1407
+ createMeetingNotes: ((...args) => getClient().createMeetingNotes(...args)),
1408
+ // Chat widget
1409
+ initChat: (options) => initChat(getClient(), options)
1410
+ };
1411
+ var src_default = nexor;
1412
+
1413
+ exports.NexorAPIError = NexorAPIError;
1414
+ exports.NexorAuthError = NexorAuthError;
1415
+ exports.NexorClient = NexorClient;
1416
+ exports.NexorError = NexorError;
1417
+ exports.NexorNetworkError = NexorNetworkError;
1418
+ exports.NexorValidationError = NexorValidationError;
1419
+ exports.createClient = createClient;
1420
+ exports.default = src_default;
1421
+ exports.nexor = nexor;
1422
+ //# sourceMappingURL=index.cjs.map
1423
+ //# sourceMappingURL=index.cjs.map