@wallavi/widget 1.12.7 → 1.13.1

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.js CHANGED
@@ -92,6 +92,83 @@ async function getFreshClerkToken() {
92
92
  }
93
93
  }
94
94
 
95
+ // src/lib/page-context.ts
96
+ function candidatesFrom(actions) {
97
+ const out = [];
98
+ for (const action of actions) {
99
+ const title = action.displayName ?? action.name;
100
+ const target = action.steps?.find((s) => s.action === "navigate")?.value;
101
+ if (!title || typeof target !== "string" || !target.startsWith("/")) {
102
+ continue;
103
+ }
104
+ const [path = "", search = ""] = target.split("?");
105
+ out.push({
106
+ title,
107
+ segments: path.split("/").filter(Boolean),
108
+ query: new URLSearchParams(search)
109
+ });
110
+ }
111
+ return out;
112
+ }
113
+ var PARAM = /^\[(\w+)\]$/;
114
+ var LOCALE = /^[a-z]{2}(?:-[a-z]{2,4})?$/i;
115
+ function offsetsFor(pathSegments, routeLength) {
116
+ const offsets = [];
117
+ if (pathSegments.length === routeLength) offsets.push(0);
118
+ if (pathSegments.length === routeLength + 1 && LOCALE.test(pathSegments[0] ?? "")) {
119
+ offsets.push(1);
120
+ }
121
+ return offsets;
122
+ }
123
+ function safeDecode(segment) {
124
+ try {
125
+ return decodeURIComponent(segment);
126
+ } catch {
127
+ return null;
128
+ }
129
+ }
130
+ function score(candidate, pathSegments, search) {
131
+ const { segments, query } = candidate;
132
+ if (segments.length === 0) return null;
133
+ for (const [key, value] of query) {
134
+ if (search.get(key) !== value) return null;
135
+ }
136
+ for (const offset of offsetsFor(pathSegments, segments.length)) {
137
+ const params = {};
138
+ let points = Array.from(query.keys()).length;
139
+ let matched = true;
140
+ for (let i = 0; i < segments.length && matched; i++) {
141
+ const pattern = segments[i];
142
+ const actual = safeDecode(pathSegments[offset + i]);
143
+ const param = PARAM.exec(pattern);
144
+ if (actual === null) matched = false;
145
+ else if (param) params[param[1]] = actual;
146
+ else if (pattern === actual) points += 2;
147
+ else matched = false;
148
+ }
149
+ if (matched) return { score: points, params };
150
+ }
151
+ return null;
152
+ }
153
+ function derivePageContext(location, actions) {
154
+ if (!actions?.length) return void 0;
155
+ const pathSegments = location.pathname.split("/").filter(Boolean);
156
+ const search = new URLSearchParams(location.search);
157
+ let best = null;
158
+ for (const candidate of candidatesFrom(actions)) {
159
+ const result = score(candidate, pathSegments, search);
160
+ if (result && (!best || result.score > best.score)) {
161
+ best = { title: candidate.title, ...result };
162
+ }
163
+ }
164
+ if (!best) return void 0;
165
+ return {
166
+ url: `${location.pathname}${location.search}`,
167
+ title: best.title,
168
+ ...Object.keys(best.params).length > 0 ? { params: best.params } : {}
169
+ };
170
+ }
171
+
95
172
  // src/lib/types.ts
96
173
  function getContrastColor(hex) {
97
174
  const clean = hex.replace("#", "");
@@ -105,7 +182,7 @@ function formatToolName(name) {
105
182
  return name.replace(/([A-Z])/g, " $1").replace(/_/g, " ").trim();
106
183
  }
107
184
 
108
- // src/core/stream-protocol.ts
185
+ // ../protocol/src/index.ts
109
186
  var STREAM_DELIMITER = "\u03B6\u236E";
110
187
  async function consumeStream(body, handler) {
111
188
  const reader = body.getReader();
@@ -121,9 +198,9 @@ async function consumeStream(body, handler) {
121
198
  if (!raw) continue;
122
199
  try {
123
200
  const parsed = JSON.parse(raw);
124
- const proto = parsed.data?.uiMessageProtocol;
125
- if (proto) {
201
+ if (parsed.data?.uiMessageProtocol) {
126
202
  eventCount++;
203
+ const proto = parsed.data.uiMessageProtocol;
127
204
  if (proto.type === "text-delta") {
128
205
  textAccumulator += proto.delta ?? "";
129
206
  }
@@ -273,12 +350,321 @@ function applyUiEventToMessages(prev, proto, msgId) {
273
350
  copy[idx] = msg;
274
351
  return copy;
275
352
  }
276
- var API_URL = process.env.NEXT_PUBLIC_API_URL ?? "https://wallavi-production.up.railway.app";
353
+
354
+ // src/core/declarative-actions.ts
355
+ function sanitizeSelector(selector) {
356
+ if (!selector || typeof selector !== "string") return null;
357
+ const lower = selector.toLowerCase();
358
+ if (lower.includes("<") || lower.includes(">") || lower.includes("script") || lower.includes("iframe") || lower.includes("onload") || lower.includes("onerror") || lower.includes("javascript:")) {
359
+ return null;
360
+ }
361
+ const cleanRegex = /^[a-zA-Z0-9\s\.\-#_\[\]="':]+$/;
362
+ if (!cleanRegex.test(selector)) {
363
+ return null;
364
+ }
365
+ return selector;
366
+ }
367
+ function sanitizeEventName(name) {
368
+ if (!name || typeof name !== "string") return null;
369
+ const nameRegex = /^[a-zA-Z0-9\-_:]+$/;
370
+ if (!nameRegex.test(name)) return null;
371
+ const nativeEvents = /* @__PURE__ */ new Set([
372
+ "click",
373
+ "dblclick",
374
+ "mouseup",
375
+ "mousedown",
376
+ "mouseover",
377
+ "mouseout",
378
+ "submit",
379
+ "reset",
380
+ "change",
381
+ "select",
382
+ "keydown",
383
+ "keypress",
384
+ "keyup",
385
+ "load",
386
+ "unload",
387
+ "abort",
388
+ "error",
389
+ "resize",
390
+ "scroll",
391
+ "contextmenu"
392
+ ]);
393
+ if (nativeEvents.has(name.toLowerCase())) return null;
394
+ return name;
395
+ }
396
+ function sanitizeUrl(url) {
397
+ if (!url || typeof url !== "string") return null;
398
+ const trimmed = url.trim();
399
+ const lower = trimmed.toLowerCase();
400
+ if (trimmed.startsWith("//")) {
401
+ return null;
402
+ }
403
+ if (lower.startsWith("javascript:") || lower.startsWith("data:")) {
404
+ return null;
405
+ }
406
+ if (trimmed.startsWith("/")) {
407
+ return trimmed;
408
+ }
409
+ if (trimmed.startsWith("http://") || trimmed.startsWith("https://")) {
410
+ return trimmed;
411
+ }
412
+ return null;
413
+ }
414
+ async function waitForElement(selector, retries = 5, delayMs = 100) {
415
+ const cleanId = selector.startsWith("#") ? selector.substring(1) : selector;
416
+ for (let i = 0; i < retries; i++) {
417
+ try {
418
+ const el = document.querySelector(selector);
419
+ if (el) return el;
420
+ } catch (e) {
421
+ console.error(e);
422
+ }
423
+ try {
424
+ const byId = document.getElementById(cleanId);
425
+ if (byId) return byId;
426
+ } catch (e) {
427
+ console.error(e);
428
+ }
429
+ if (!selector.startsWith("#") && !selector.startsWith(".") && !selector.startsWith("[")) {
430
+ try {
431
+ const byPrependedId = document.querySelector(`#${selector}`);
432
+ if (byPrependedId) return byPrependedId;
433
+ } catch (e) {
434
+ console.error(e);
435
+ }
436
+ }
437
+ await new Promise((resolve) => setTimeout(resolve, delayMs));
438
+ }
439
+ return null;
440
+ }
441
+ var HEX_COLOR_REGEX = /^#([A-Fa-f0-9]{3}|[A-Fa-f0-9]{6})$/;
442
+ function sanitizeColor(color, fallback = "#6366f1") {
443
+ if (!color || typeof color !== "string") return fallback;
444
+ const trimmed = color.trim();
445
+ return HEX_COLOR_REGEX.test(trimmed) ? trimmed : fallback;
446
+ }
447
+ function injectCopilotStyles(color = "#6366f1") {
448
+ if (typeof document === "undefined") return;
449
+ const safeColor = sanitizeColor(color, "#6366f1");
450
+ let style = document.getElementById(
451
+ "wallavi-copilot-styles"
452
+ );
453
+ if (!style) {
454
+ style = document.createElement("style");
455
+ style.id = "wallavi-copilot-styles";
456
+ document.head.appendChild(style);
457
+ }
458
+ if (style.dataset.color === safeColor) return;
459
+ style.dataset.color = safeColor;
460
+ style.textContent = `
461
+ @keyframes wallavi-pulse-beacon {
462
+ 0% {
463
+ box-shadow: 0 0 0 0 rgba(99, 102, 241, 0.7);
464
+ outline: 2px solid ${safeColor};
465
+ }
466
+ 70% {
467
+ box-shadow: 0 0 0 10px rgba(99, 102, 241, 0);
468
+ outline: 2px solid rgba(99, 102, 241, 0.3);
469
+ }
470
+ 100% {
471
+ box-shadow: 0 0 0 0 rgba(99, 102, 241, 0);
472
+ outline: 2px solid transparent;
473
+ }
474
+ }
475
+ .wallavi-element-beacon {
476
+ animation: wallavi-pulse-beacon 1.8s ease-out infinite !important;
477
+ outline-offset: 3px !important;
478
+ border-radius: inherit;
479
+ transition: outline 0.2s ease, box-shadow 0.2s ease !important;
480
+ }
481
+ @keyframes wallavi-flash-touch-anim {
482
+ 0% {
483
+ outline: 3px solid ${safeColor};
484
+ outline-offset: 2px;
485
+ filter: brightness(1.08);
486
+ }
487
+ 100% {
488
+ outline: 2px solid transparent;
489
+ outline-offset: 6px;
490
+ filter: none;
491
+ }
492
+ }
493
+ .wallavi-flash-touch {
494
+ animation: wallavi-flash-touch-anim 0.5s cubic-bezier(0.16, 1, 0.3, 1) forwards !important;
495
+ }
496
+ `;
497
+ document.head.appendChild(style);
498
+ }
499
+ function flashElement(el, color) {
500
+ injectCopilotStyles(color);
501
+ el.classList.remove("wallavi-flash-touch");
502
+ void el.offsetWidth;
503
+ el.classList.add("wallavi-flash-touch");
504
+ setTimeout(() => {
505
+ el.classList.remove("wallavi-flash-touch");
506
+ }, 600);
507
+ }
508
+ function highlightElement(el, color, durationMs = 3e3) {
509
+ injectCopilotStyles(color);
510
+ el.classList.add("wallavi-element-beacon");
511
+ setTimeout(() => {
512
+ el.classList.remove("wallavi-element-beacon");
513
+ }, durationMs);
514
+ }
515
+ var delay = (ms) => new Promise((resolve) => setTimeout(resolve, ms));
516
+ async function executeDeclarativeSteps(steps, onNavigate, copilotColor) {
517
+ if (typeof window === "undefined" || !Array.isArray(steps)) return;
518
+ const safeCopilotColor = sanitizeColor(copilotColor, "#6366f1");
519
+ for (const step of steps) {
520
+ try {
521
+ const { action } = step;
522
+ switch (action) {
523
+ case "scroll_to":
524
+ case "scroll_into_view": {
525
+ const selector = sanitizeSelector(step.selector);
526
+ if (!selector) break;
527
+ const el = await waitForElement(selector);
528
+ if (el) {
529
+ flashElement(el, safeCopilotColor);
530
+ el.scrollIntoView({ behavior: "smooth", block: "center" });
531
+ }
532
+ break;
533
+ }
534
+ case "click": {
535
+ const selector = sanitizeSelector(step.selector);
536
+ if (!selector) break;
537
+ const el = await waitForElement(selector);
538
+ if (el) {
539
+ flashElement(el, safeCopilotColor);
540
+ await delay(80);
541
+ el.click();
542
+ }
543
+ break;
544
+ }
545
+ case "focus": {
546
+ const selector = sanitizeSelector(step.selector);
547
+ if (!selector) break;
548
+ const el = await waitForElement(selector);
549
+ if (el) {
550
+ flashElement(el, safeCopilotColor);
551
+ el.focus();
552
+ }
553
+ break;
554
+ }
555
+ case "highlight": {
556
+ const selector = sanitizeSelector(step.selector);
557
+ if (!selector) break;
558
+ const el = await waitForElement(selector);
559
+ if (el) {
560
+ highlightElement(
561
+ el,
562
+ sanitizeColor(step.color, safeCopilotColor),
563
+ step.durationMs ?? 3e3
564
+ );
565
+ }
566
+ break;
567
+ }
568
+ case "wait": {
569
+ const parsedVal = typeof step.value === "number" ? step.value : parseInt(String(step.value ?? "500"), 10);
570
+ const ms = step.delayMs ?? (Number.isNaN(parsedVal) ? 500 : parsedVal);
571
+ await delay(ms);
572
+ break;
573
+ }
574
+ case "add_class": {
575
+ const selector = sanitizeSelector(step.selector);
576
+ const value = step.value;
577
+ if (!selector || !value) break;
578
+ const el = await waitForElement(selector);
579
+ if (el) el.classList.add(...value.split(/\s+/).filter(Boolean));
580
+ break;
581
+ }
582
+ case "remove_class": {
583
+ const selector = sanitizeSelector(step.selector);
584
+ const value = step.value;
585
+ if (!selector || !value) break;
586
+ const el = await waitForElement(selector);
587
+ if (el) el.classList.remove(...value.split(/\s+/).filter(Boolean));
588
+ break;
589
+ }
590
+ case "fill_value": {
591
+ const selector = sanitizeSelector(step.selector);
592
+ const value = step.value;
593
+ if (!selector) break;
594
+ const el = await waitForElement(selector);
595
+ if (el && (el instanceof HTMLInputElement || el instanceof HTMLTextAreaElement || el instanceof HTMLSelectElement)) {
596
+ if (el instanceof HTMLInputElement) {
597
+ const type = el.type.toLowerCase();
598
+ if (type === "password" || type === "hidden") {
599
+ console.warn(
600
+ `[Wallavi Widget] Blocked fill_value action on sensitive field: ${type}`
601
+ );
602
+ break;
603
+ }
604
+ }
605
+ const isReadOnly = (el instanceof HTMLInputElement || el instanceof HTMLTextAreaElement) && el.readOnly;
606
+ if (el.disabled || isReadOnly) {
607
+ console.warn(
608
+ "[Wallavi Widget] Blocked fill_value action on disabled/readOnly element"
609
+ );
610
+ break;
611
+ }
612
+ flashElement(el, safeCopilotColor);
613
+ el.value = value ?? "";
614
+ el.dispatchEvent(new Event("input", { bubbles: true }));
615
+ el.dispatchEvent(new Event("change", { bubbles: true }));
616
+ }
617
+ break;
618
+ }
619
+ case "dispatch_event": {
620
+ const eventName = sanitizeEventName(step.value);
621
+ if (!eventName) break;
622
+ const detail = step.payload ?? {};
623
+ window.dispatchEvent(new CustomEvent(eventName, { detail }));
624
+ break;
625
+ }
626
+ case "navigate": {
627
+ const url = sanitizeUrl(step.value);
628
+ if (url) {
629
+ if (onNavigate) {
630
+ onNavigate(url);
631
+ } else {
632
+ window.location.href = url;
633
+ }
634
+ }
635
+ break;
636
+ }
637
+ default: {
638
+ const unknownAction = step.action;
639
+ console.warn(
640
+ `[Wallavi Widget] Unknown declarative action: ${unknownAction}`
641
+ );
642
+ }
643
+ }
644
+ await delay(120);
645
+ } catch (err) {
646
+ console.error(
647
+ "[Wallavi Widget] Failed to execute declarative step:",
648
+ step,
649
+ err
650
+ );
651
+ }
652
+ }
653
+ }
654
+
655
+ // src/lib/api-url.ts
656
+ var WALLAVI_PRODUCTION_API_URL = "https://wallavi-production.up.railway.app";
657
+ function resolveApiUrl(explicit) {
658
+ return explicit ?? process.env.NEXT_PUBLIC_API_URL ?? WALLAVI_PRODUCTION_API_URL;
659
+ }
660
+
661
+ // src/hooks/use-voice-call.ts
277
662
  function useVoiceCall({
278
663
  agentId,
279
664
  threadId,
280
665
  workspaceId,
281
- customBackend
666
+ customBackend,
667
+ apiUrl
282
668
  }) {
283
669
  const [active, setActive] = react.useState(false);
284
670
  const [token, setToken] = react.useState(null);
@@ -291,7 +677,7 @@ function useVoiceCall({
291
677
  setError(null);
292
678
  try {
293
679
  const isPrivate = Boolean(workspaceId);
294
- const url = isPrivate ? `${API_URL}/api/threads/${threadId}/livekit-token?agentId=${encodeURIComponent(agentId)}` : `${API_URL}/api/chat/livekit-token?agentId=${encodeURIComponent(agentId)}&threadId=${encodeURIComponent(threadId)}`;
680
+ const url = isPrivate ? `${resolveApiUrl(apiUrl)}/api/threads/${threadId}/livekit-token?agentId=${encodeURIComponent(agentId)}` : `${resolveApiUrl(apiUrl)}/api/chat/livekit-token?agentId=${encodeURIComponent(agentId)}&threadId=${encodeURIComponent(threadId)}`;
295
681
  const token2 = isPrivate ? await getFreshClerkToken() : null;
296
682
  const res = await fetch(url, {
297
683
  headers: {
@@ -307,7 +693,7 @@ function useVoiceCall({
307
693
  setError(err.message);
308
694
  }
309
695
  setLoading(false);
310
- }, [agentId, threadId, workspaceId, customBackend]);
696
+ }, [agentId, threadId, workspaceId, customBackend, apiUrl]);
311
697
  const stop = react.useCallback(() => {
312
698
  setActive(false);
313
699
  setToken(null);
@@ -324,13 +710,33 @@ function useVoiceCall({
324
710
  };
325
711
  }
326
712
 
713
+ // src/lib/user-headers.ts
714
+ var warnedHeadersFailure = false;
715
+ async function resolveUserHeaders(userContext) {
716
+ const source = userContext?.headers;
717
+ if (!source) return {};
718
+ if (typeof source !== "function") return source;
719
+ try {
720
+ return await source() ?? {};
721
+ } catch (err) {
722
+ if (!warnedHeadersFailure) {
723
+ warnedHeadersFailure = true;
724
+ console.warn(
725
+ "[wallavi-widget] userContext.headers threw; sending without host headers",
726
+ err
727
+ );
728
+ }
729
+ return {};
730
+ }
731
+ }
732
+
327
733
  // src/hooks/use-chat.ts
328
- var API_URL2 = process.env.NEXT_PUBLIC_API_URL ?? "https://wallavi-production.up.railway.app";
329
734
  function newId() {
330
735
  return Math.random().toString(36).slice(2, 10);
331
736
  }
332
737
  function useChat({
333
738
  agentId,
739
+ apiUrl,
334
740
  workspaceId = "",
335
741
  envId,
336
742
  source = "playground",
@@ -338,13 +744,20 @@ function useChat({
338
744
  persist = false,
339
745
  onNavigate,
340
746
  playgroundOverrides,
341
- customBackend
747
+ customBackend,
748
+ copilotColor,
749
+ resolvePageContext
342
750
  }) {
751
+ const baseUrl = resolveApiUrl(apiUrl);
343
752
  const userId = userContext?.userId;
344
753
  const persistKey = persist ? userId ? `wallavi_${agentId}_${userId}` : `wallavi_${agentId}` : null;
345
754
  const onNavigateRef = react.useRef(onNavigate);
755
+ const resolvePageContextRef = react.useRef(resolvePageContext);
756
+ const copilotColorRef = react.useRef(copilotColor);
346
757
  react.useEffect(() => {
347
758
  onNavigateRef.current = onNavigate;
759
+ resolvePageContextRef.current = resolvePageContext;
760
+ copilotColorRef.current = copilotColor;
348
761
  });
349
762
  const [messages, setMessages] = react.useState(() => {
350
763
  if (!persistKey || typeof window === "undefined") return [];
@@ -438,7 +851,11 @@ function useChat({
438
851
  return;
439
852
  }
440
853
  if (proto.type === "client-action") {
441
- executeDeclarativeSteps(proto.steps, onNavigateRef.current);
854
+ executeDeclarativeSteps(
855
+ proto.steps,
856
+ onNavigateRef.current,
857
+ copilotColorRef.current
858
+ );
442
859
  return;
443
860
  }
444
861
  if (proto.type === "debug-trace") {
@@ -454,10 +871,18 @@ function useChat({
454
871
  );
455
872
  const fetchAndStream = react.useCallback(
456
873
  async (opts) => {
457
- const { input: userInput, msgId, extraMetadata, attachments } = opts;
874
+ const {
875
+ input: userInput,
876
+ msgId,
877
+ extraMetadata,
878
+ attachments,
879
+ pickerSelection
880
+ } = opts;
458
881
  const isPrivate = Boolean(workspaceId);
459
882
  const token = isPrivate ? await getFreshClerkToken() : null;
460
- const url = isPrivate ? `${API_URL2}/api/threads/${threadId}/stream` : `${API_URL2}/api/chat/stream`;
883
+ const hostHeaders = await resolveUserHeaders(userContext);
884
+ const pageContext = userContext?.pageContext ?? resolvePageContextRef.current?.();
885
+ const url = isPrivate ? `${baseUrl}/api/threads/${threadId}/stream` : `${baseUrl}/api/chat/stream`;
461
886
  const res = await fetch(url, {
462
887
  method: "POST",
463
888
  headers: {
@@ -474,14 +899,17 @@ function useChat({
474
899
  } : { threadId },
475
900
  source,
476
901
  ...attachments?.length ? { attachments } : {},
902
+ // Its own field: userMetadata keys starting with __ belong to the
903
+ // engine and the API rejects them.
904
+ ...pickerSelection ? { pickerSelection } : {},
477
905
  ...userContext?.userName ? { userName: userContext.userName } : {},
478
906
  ...userContext?.userEmail ? { userEmail: userContext.userEmail } : {},
479
907
  userMetadata: {
480
908
  ...userContext?.metadata ?? {},
481
- ...userContext?.pageContext ? { pageContext: userContext.pageContext } : {},
909
+ ...pageContext ? { pageContext } : {},
482
910
  headers: {
483
911
  ...token ? { Authorization: `Bearer ${token}` } : {},
484
- ...userContext?.headers ?? {},
912
+ ...hostHeaders,
485
913
  ...userContext?.metadata?.headers ?? {}
486
914
  },
487
915
  ...extraMetadata ?? {}
@@ -526,6 +954,7 @@ function useChat({
526
954
  }
527
955
  },
528
956
  [
957
+ baseUrl,
529
958
  agentId,
530
959
  workspaceId,
531
960
  envId,
@@ -540,7 +969,8 @@ function useChat({
540
969
  agentId,
541
970
  threadId,
542
971
  workspaceId,
543
- customBackend
972
+ customBackend,
973
+ apiUrl: baseUrl
544
974
  });
545
975
  react.useEffect(() => {
546
976
  if (customBackend || !persistKey) return;
@@ -589,7 +1019,7 @@ function useChat({
589
1019
  try {
590
1020
  const isPrivate = Boolean(workspaceId);
591
1021
  const token = isPrivate ? await getFreshClerkToken() : null;
592
- const url = isPrivate ? `${API_URL2}/api/threads/${threadId}/messages` : `${API_URL2}/api/chat/messages?agentId=${encodeURIComponent(agentId)}&threadId=${encodeURIComponent(threadId)}`;
1022
+ const url = isPrivate ? `${baseUrl}/api/threads/${threadId}/messages` : `${baseUrl}/api/chat/messages?agentId=${encodeURIComponent(agentId)}&threadId=${encodeURIComponent(threadId)}`;
593
1023
  const res = await fetch(url, {
594
1024
  headers: {
595
1025
  ...token ? { Authorization: `Bearer ${token}` } : {}
@@ -653,7 +1083,15 @@ function useChat({
653
1083
  }
654
1084
  }
655
1085
  }
656
- }, [threadId, agentId, workspaceId, envId, persistKey, fetchAndStream]);
1086
+ }, [
1087
+ baseUrl,
1088
+ threadId,
1089
+ agentId,
1090
+ workspaceId,
1091
+ envId,
1092
+ persistKey,
1093
+ fetchAndStream
1094
+ ]);
657
1095
  const reset = react.useCallback(() => {
658
1096
  setMessages([]);
659
1097
  setInput("");
@@ -776,9 +1214,7 @@ function useChat({
776
1214
  await fetchAndStream({
777
1215
  input: label,
778
1216
  msgId: assistantMsgId,
779
- extraMetadata: {
780
- __pickerSelection: { pickerId, paramName, value, label }
781
- }
1217
+ pickerSelection: { pickerId, paramName, value, label }
782
1218
  });
783
1219
  } catch {
784
1220
  setMessages((prev) => {
@@ -837,106 +1273,6 @@ function useChat({
837
1273
  setSelectedContext
838
1274
  };
839
1275
  }
840
- async function waitForElement(selector, retries = 5, delayMs = 100) {
841
- const cleanId = selector.startsWith("#") ? selector.substring(1) : selector;
842
- for (let i = 0; i < retries; i++) {
843
- try {
844
- const el = document.querySelector(selector);
845
- if (el) return el;
846
- } catch (e) {
847
- }
848
- try {
849
- const byId = document.getElementById(cleanId);
850
- if (byId) return byId;
851
- } catch (e) {
852
- }
853
- if (!selector.startsWith("#") && !selector.startsWith(".") && !selector.startsWith("[")) {
854
- try {
855
- const byPrependedId = document.querySelector(`#${selector}`);
856
- if (byPrependedId) return byPrependedId;
857
- } catch (e) {
858
- }
859
- }
860
- await new Promise((resolve) => setTimeout(resolve, delayMs));
861
- }
862
- return null;
863
- }
864
- async function executeDeclarativeSteps(steps, onNavigate) {
865
- if (typeof window === "undefined" || !Array.isArray(steps)) return;
866
- for (const step of steps) {
867
- try {
868
- const { action, selector, value, payload } = step;
869
- switch (action) {
870
- case "scroll_into_view": {
871
- if (!selector) break;
872
- const el = await waitForElement(selector);
873
- el?.scrollIntoView({ behavior: "smooth", block: "center" });
874
- break;
875
- }
876
- case "click": {
877
- if (!selector) break;
878
- const el = await waitForElement(selector);
879
- el?.click();
880
- break;
881
- }
882
- case "focus": {
883
- if (!selector) break;
884
- const el = await waitForElement(selector);
885
- el?.focus();
886
- break;
887
- }
888
- case "add_class": {
889
- if (!selector || !value) break;
890
- const el = await waitForElement(selector);
891
- if (el) el.classList.add(...value.split(/\s+/).filter(Boolean));
892
- break;
893
- }
894
- case "remove_class": {
895
- if (!selector || !value) break;
896
- const el = await waitForElement(selector);
897
- if (el) el.classList.remove(...value.split(/\s+/).filter(Boolean));
898
- break;
899
- }
900
- case "fill_value": {
901
- if (!selector) break;
902
- const el = await waitForElement(selector);
903
- if (el) {
904
- el.value = value ?? "";
905
- el.dispatchEvent(new Event("input", { bubbles: true }));
906
- el.dispatchEvent(new Event("change", { bubbles: true }));
907
- }
908
- break;
909
- }
910
- case "dispatch_event": {
911
- if (!value) break;
912
- const detail = payload ?? {};
913
- window.dispatchEvent(new CustomEvent(value, { detail }));
914
- break;
915
- }
916
- case "navigate": {
917
- if (value) {
918
- if (onNavigate) {
919
- onNavigate(value);
920
- } else {
921
- window.location.href = value;
922
- }
923
- }
924
- break;
925
- }
926
- default:
927
- console.warn(
928
- `[Wallavi Widget] Unknown declarative action: ${action}`
929
- );
930
- }
931
- } catch (err) {
932
- console.error(
933
- "[Wallavi Widget] Failed to execute declarative step:",
934
- step,
935
- err
936
- );
937
- }
938
- }
939
- }
940
1276
  function getPreferredMimeType() {
941
1277
  if (typeof MediaRecorder === "undefined") return "";
942
1278
  const candidates = [
@@ -953,7 +1289,6 @@ function mimeTypeToExtension(mimeType) {
953
1289
  if (mimeType.includes("mp4")) return "mp4";
954
1290
  return "webm";
955
1291
  }
956
- var DEFAULT_API_URL = process.env.NEXT_PUBLIC_API_URL ?? "https://wallavi-production.up.railway.app";
957
1292
  function useVoice({
958
1293
  agentId,
959
1294
  apiUrl,
@@ -966,7 +1301,7 @@ function useVoice({
966
1301
  const streamRef = react.useRef(null);
967
1302
  const errorTimerRef = react.useRef(null);
968
1303
  const isSupported = typeof window !== "undefined" && typeof MediaRecorder !== "undefined" && !!navigator?.mediaDevices?.getUserMedia;
969
- const base = apiUrl ?? DEFAULT_API_URL;
1304
+ const base = resolveApiUrl(apiUrl);
970
1305
  const transcribeBlob = react.useCallback(
971
1306
  async (blob, mimeType) => {
972
1307
  setVoiceState("transcribing");
@@ -1045,7 +1380,6 @@ function useVoice({
1045
1380
  }, []);
1046
1381
  return { voiceState, isSupported, start, stop };
1047
1382
  }
1048
- var DEFAULT_API_URL2 = process.env.NEXT_PUBLIC_API_URL ?? "https://wallavi-production.up.railway.app";
1049
1383
  function makeId() {
1050
1384
  return `att_${Date.now().toString(36)}_${Math.random().toString(36).slice(2, 7)}`;
1051
1385
  }
@@ -1055,7 +1389,7 @@ function useAttachments({
1055
1389
  maxFiles = 5
1056
1390
  }) {
1057
1391
  const [attachments, setAttachments] = react.useState([]);
1058
- const base = apiUrl ?? DEFAULT_API_URL2;
1392
+ const base = resolveApiUrl(apiUrl);
1059
1393
  const uploadOne = react.useCallback(
1060
1394
  async (file, id) => {
1061
1395
  const form = new FormData();
@@ -2850,6 +3184,20 @@ function VoiceOverlay({
2850
3184
  }
2851
3185
  ) });
2852
3186
  }
3187
+
3188
+ // src/lib/unique-keys.ts
3189
+ function uniqueKeys(items, baseKey) {
3190
+ const seen = /* @__PURE__ */ new Map();
3191
+ return items.map((item) => {
3192
+ const base = baseKey(item);
3193
+ const count = seen.get(base) ?? 0;
3194
+ seen.set(base, count + 1);
3195
+ return count === 0 ? base : `${base}#${count}`;
3196
+ });
3197
+ }
3198
+ var actionKey = (action) => `${action.name}:${JSON.stringify(action.steps ?? [])}`;
3199
+ var docKey = (doc) => `${doc.name}:${doc.url ?? ""}`;
3200
+ var topicKey = (topic) => topic.name;
2853
3201
  function SvgIcon({
2854
3202
  className,
2855
3203
  strokeWidth = "1.8",
@@ -2923,11 +3271,11 @@ function urlRelevance(docUrl, pageUrl) {
2923
3271
  return 50;
2924
3272
  const docParts = docClean.split(/[/?#]/).filter(Boolean);
2925
3273
  const pageParts = pageClean.split(/[/?#]/).filter(Boolean);
2926
- let score = 0;
3274
+ let score2 = 0;
2927
3275
  for (const part of pageParts) {
2928
- if (docParts.includes(part)) score += 10;
3276
+ if (docParts.includes(part)) score2 += 10;
2929
3277
  }
2930
- return score;
3278
+ return score2;
2931
3279
  }
2932
3280
  function topicToQuestion(topic, locale) {
2933
3281
  const name = topic.name.trim();
@@ -3018,9 +3366,9 @@ function getContextKeywords(pageUrl, pageTitle, pageParams, pageVars, metadata)
3018
3366
  return Array.from(keywords);
3019
3367
  }
3020
3368
  function computeRelevanceScore(item, keywords, pageUrl) {
3021
- let score = 0;
3369
+ let score2 = 0;
3022
3370
  if (item.url && pageUrl) {
3023
- score += urlRelevance(item.url, pageUrl);
3371
+ score2 += urlRelevance(item.url, pageUrl);
3024
3372
  }
3025
3373
  const itemName = (item.name || "").toLowerCase().normalize("NFD").replace(/[\u0300-\u036f]/g, "");
3026
3374
  const itemDesc = (item.description || item.summary || "").toLowerCase().normalize("NFD").replace(/[\u0300-\u036f]/g, "");
@@ -3028,33 +3376,41 @@ function computeRelevanceScore(item, keywords, pageUrl) {
3028
3376
  const nameWords = itemName.split(/[^a-z0-9]/).filter(Boolean);
3029
3377
  for (const keyword of keywords) {
3030
3378
  if (itemName.includes(keyword)) {
3031
- score += 30;
3379
+ score2 += 30;
3032
3380
  if (nameWords.includes(keyword)) {
3033
- score += 20;
3381
+ score2 += 20;
3034
3382
  }
3035
3383
  }
3036
3384
  if (itemDesc.includes(keyword)) {
3037
- score += 15;
3385
+ score2 += 15;
3038
3386
  }
3039
3387
  if (itemUrl.includes(keyword)) {
3040
- score += 20;
3388
+ score2 += 20;
3041
3389
  }
3042
3390
  }
3043
- if (item.keywords) {
3044
- const allItemKeywords = [
3045
- ...item.keywords.en || [],
3046
- ...item.keywords.es || [],
3047
- ...item.keywords.fr || []
3048
- ].map(
3391
+ if (item.keywords && typeof item.keywords === "object") {
3392
+ const rawList = [];
3393
+ if (Array.isArray(item.keywords)) {
3394
+ rawList.push(...item.keywords);
3395
+ } else {
3396
+ const kwRecord = item.keywords;
3397
+ for (const lang of ["en", "es", "fr"]) {
3398
+ const langVal = kwRecord[lang];
3399
+ if (Array.isArray(langVal)) {
3400
+ rawList.push(...langVal);
3401
+ }
3402
+ }
3403
+ }
3404
+ const allItemKeywords = rawList.filter((k) => typeof k === "string" && k.trim().length > 0).map(
3049
3405
  (k) => k.toLowerCase().normalize("NFD").replace(/[\u0300-\u036f]/g, "")
3050
3406
  );
3051
3407
  for (const keyword of keywords) {
3052
3408
  if (allItemKeywords.includes(keyword)) {
3053
- score += 45;
3409
+ score2 += 45;
3054
3410
  }
3055
3411
  }
3056
3412
  }
3057
- return score;
3413
+ return score2;
3058
3414
  }
3059
3415
  var STAGGER_MS = 50;
3060
3416
  var cardBase = "ww-group ww-w-full ww-rounded-xl ww-border ww-border-border ww-bg-background ww-text-left ww-shadow-[0_1px_2px_rgba(0,0,0,0.03)] hover:ww-bg-muted/20 hover:ww-border-border/80 hover:ww-shadow-md active:ww-scale-[0.98] ww-transition-all ww-duration-200 ww-animate-in ww-fade-in ww-slide-in-from-bottom-1 focus-visible:ww-ring-2 focus-visible:ww-ring-ring/40 focus-visible:ww-outline-none";
@@ -3267,6 +3623,9 @@ function CommandPanel({
3267
3623
  }));
3268
3624
  return scored.sort((a, b) => b.score - a.score);
3269
3625
  }, [clientActions, pageUrl, pageTitle, pageParams, pageVars, metadata]);
3626
+ const actionKeys = uniqueKeys(sortedActions, actionKey);
3627
+ const docKeys = uniqueKeys(relevantDocs, docKey);
3628
+ const topicKeys = uniqueKeys(topicQuestions, topicKey);
3270
3629
  const hasActions = sortedActions.length > 0;
3271
3630
  const hasDocs = relevantDocs.length > 0;
3272
3631
  const hasTopics = topicQuestions.length > 0;
@@ -3288,7 +3647,7 @@ function CommandPanel({
3288
3647
  accentColor,
3289
3648
  onExecute: () => handleExecute(action.steps)
3290
3649
  },
3291
- action.name
3650
+ actionKeys[i]
3292
3651
  )) })
3293
3652
  ] }),
3294
3653
  hasDocs && /* @__PURE__ */ jsxRuntime.jsxs("section", { children: [
@@ -3301,7 +3660,7 @@ function CommandPanel({
3301
3660
  onSend,
3302
3661
  locale
3303
3662
  },
3304
- `${doc.name}-${doc.url ?? i}`
3663
+ docKeys[i]
3305
3664
  )) })
3306
3665
  ] }),
3307
3666
  hasTopics && /* @__PURE__ */ jsxRuntime.jsxs("section", { children: [
@@ -3313,7 +3672,7 @@ function CommandPanel({
3313
3672
  index: i,
3314
3673
  onSend
3315
3674
  },
3316
- topic.name
3675
+ topicKeys[i]
3317
3676
  )) })
3318
3677
  ] })
3319
3678
  ] }),
@@ -3360,7 +3719,7 @@ function CommandPanel({
3360
3719
  accentColor,
3361
3720
  onExecute: () => handleExecute(action.steps)
3362
3721
  },
3363
- action.name
3722
+ actionKeys[i]
3364
3723
  )) })
3365
3724
  ] }),
3366
3725
  hasDocs && /* @__PURE__ */ jsxRuntime.jsxs("section", { children: [
@@ -3373,7 +3732,7 @@ function CommandPanel({
3373
3732
  onSend,
3374
3733
  locale
3375
3734
  },
3376
- `${doc.name}-${doc.url ?? i}`
3735
+ docKeys[i]
3377
3736
  )) })
3378
3737
  ] }),
3379
3738
  hasTopics && /* @__PURE__ */ jsxRuntime.jsxs("section", { children: [
@@ -3386,13 +3745,14 @@ function CommandPanel({
3386
3745
  compact: true,
3387
3746
  onSend
3388
3747
  },
3389
- topic.name
3748
+ topicKeys[i]
3390
3749
  )) })
3391
3750
  ] })
3392
3751
  ] });
3393
3752
  }
3394
3753
  function ChatWidget({
3395
3754
  agentId,
3755
+ apiUrl,
3396
3756
  workspaceId,
3397
3757
  agentName,
3398
3758
  displayName,
@@ -3453,8 +3813,10 @@ function ChatWidget({
3453
3813
  window.addEventListener("message", handleMessage);
3454
3814
  return () => window.removeEventListener("message", handleMessage);
3455
3815
  }, []);
3816
+ const resolvePageContext = () => userContext?.pageContext ?? (typeof window === "undefined" ? void 0 : derivePageContext(window.location, clientActions));
3456
3817
  const chat = useChat({
3457
3818
  agentId,
3819
+ apiUrl,
3458
3820
  workspaceId,
3459
3821
  envId,
3460
3822
  source,
@@ -3462,10 +3824,13 @@ function ChatWidget({
3462
3824
  persist,
3463
3825
  onNavigate,
3464
3826
  playgroundOverrides,
3465
- customBackend
3827
+ customBackend,
3828
+ copilotColor: userMessageColor,
3829
+ resolvePageContext: () => resolvePageContext()
3466
3830
  });
3467
3831
  const voice = useVoice({
3468
3832
  agentId,
3833
+ apiUrl,
3469
3834
  onTranscript: (text) => {
3470
3835
  if (voiceAutoSend) {
3471
3836
  void chat.send(text);
@@ -3474,7 +3839,7 @@ function ChatWidget({
3474
3839
  }
3475
3840
  }
3476
3841
  });
3477
- const attachmentHook = useAttachments({ agentId });
3842
+ const attachmentHook = useAttachments({ agentId, apiUrl });
3478
3843
  const debugTraceLenRef = react.useRef(0);
3479
3844
  react.useEffect(() => {
3480
3845
  if (!onDebugTrace || chat.debugTraces.length <= debugTraceLenRef.current)
@@ -3574,9 +3939,9 @@ function ChatWidget({
3574
3939
  const showInlineCommandPanel = showCommandPanel && !showSidebar && chat.messages.length === 0 && (showQuickActions && clientActions.length > 0 || showRagTopics && ragTopics.length > 0 || showRagDocuments && ragDocuments.length > 0);
3575
3940
  const handleExecuteAction = react.useCallback(
3576
3941
  (steps) => {
3577
- executeDeclarativeSteps(steps, onNavigate);
3942
+ executeDeclarativeSteps(steps, onNavigate, userMessageColor);
3578
3943
  },
3579
- [onNavigate]
3944
+ [onNavigate, userMessageColor]
3580
3945
  );
3581
3946
  const [activeActionIndex, setActiveActionIndex] = react.useState(-1);
3582
3947
  const query = chat.input.toLowerCase().trim();
@@ -3633,11 +3998,12 @@ function ChatWidget({
3633
3998
  },
3634
3999
  [matchingActions, activeActionIndex, handleExecuteAction, chat]
3635
4000
  );
4001
+ const pageContext = resolvePageContext();
3636
4002
  const commandPanelProps = {
3637
- pageUrl: userContext?.pageContext?.url,
3638
- pageTitle: userContext?.pageContext?.title,
3639
- pageParams: userContext?.pageContext?.params,
3640
- pageVars: userContext?.pageContext?.vars,
4003
+ pageUrl: pageContext?.url,
4004
+ pageTitle: pageContext?.title,
4005
+ pageParams: pageContext?.params,
4006
+ pageVars: pageContext?.vars,
3641
4007
  metadata: userContext?.metadata,
3642
4008
  clientActions: showQuickActions ? clientActions : [],
3643
4009
  ragTopics: showRagTopics ? ragTopics : [],
@@ -3957,7 +4323,6 @@ function ChatWidget({
3957
4323
  }
3958
4324
  );
3959
4325
  }
3960
- var WALLAVI_PUBLIC_API = "https://wallavi-production.up.railway.app";
3961
4326
  var EMPTY = {
3962
4327
  remoteConfig: {},
3963
4328
  bubbleIconUrl: void 0,
@@ -3971,7 +4336,37 @@ var EMPTY = {
3971
4336
  panelHeight: 580,
3972
4337
  loading: false
3973
4338
  };
3974
- function useAutoConfig(agentId, enabled) {
4339
+ function toChatProps(cfg) {
4340
+ const remote = {};
4341
+ const set = (key, value) => {
4342
+ if (value !== null && value !== void 0) remote[key] = value;
4343
+ };
4344
+ set("agentName", cfg.agentName);
4345
+ set("profilePicture", cfg.profilePicture);
4346
+ set("displayName", cfg.displayName);
4347
+ set("theme", cfg.theme);
4348
+ set("userMessageColor", cfg.userMessageColor);
4349
+ if (cfg.initialMessages?.length) remote.initialMessages = cfg.initialMessages;
4350
+ if (cfg.suggestedMessages?.length)
4351
+ remote.suggestedMessages = cfg.suggestedMessages;
4352
+ set("messagePlaceholder", cfg.messagePlaceholder);
4353
+ set("watermark", cfg.watermark);
4354
+ set("footer", cfg.footer);
4355
+ set("showThinking", cfg.showThinking);
4356
+ set("regenerateMessage", cfg.regenerateMessage);
4357
+ set("widgetLayout", cfg.widgetLayout);
4358
+ set("enableVoice", cfg.enableVoice);
4359
+ set("showCommandPanel", cfg.showCommandPanel);
4360
+ set("showRagTopics", cfg.showRagTopics);
4361
+ set("showRagDocuments", cfg.showRagDocuments);
4362
+ set("showQuickActions", cfg.showQuickActions);
4363
+ set("clientActions", cfg.clientActions);
4364
+ set("ragTopics", cfg.ragTopics);
4365
+ set("ragDocuments", cfg.ragDocuments);
4366
+ set("locale", cfg.locale);
4367
+ return remote;
4368
+ }
4369
+ function useAutoConfig(agentId, enabled, apiUrl) {
3975
4370
  const [result, setResult] = react.useState(() => ({
3976
4371
  ...EMPTY,
3977
4372
  loading: enabled && Boolean(agentId)
@@ -3982,61 +4377,34 @@ function useAutoConfig(agentId, enabled) {
3982
4377
  return;
3983
4378
  }
3984
4379
  let cancelled = false;
3985
- fetch(`${WALLAVI_PUBLIC_API}/api/public/widget/${agentId}`).then((r) => r.json()).then((body) => {
4380
+ setResult({ ...EMPTY, loading: true });
4381
+ fetch(`${resolveApiUrl(apiUrl)}/api/public/widget/${agentId}`).then((r) => r.json()).then((body) => {
3986
4382
  if (cancelled) return;
3987
- const cfg = body?.data ?? {};
3988
- const remote = {};
3989
- if (cfg.profilePicture != null)
3990
- remote.profilePicture = cfg.profilePicture;
3991
- if (cfg.displayName != null)
3992
- remote.displayName = cfg.displayName;
3993
- if (cfg.theme) remote.theme = cfg.theme;
3994
- if (cfg.userMessageColor)
3995
- remote.userMessageColor = cfg.userMessageColor;
3996
- if (Array.isArray(cfg.initialMessages) && cfg.initialMessages.length > 0)
3997
- remote.initialMessages = cfg.initialMessages;
3998
- if (Array.isArray(cfg.suggestedMessages))
3999
- remote.suggestedMessages = cfg.suggestedMessages;
4000
- if (cfg.messagePlaceholder != null)
4001
- remote.messagePlaceholder = cfg.messagePlaceholder;
4002
- if (cfg.watermark != null) remote.watermark = cfg.watermark;
4003
- if (cfg.footer != null) remote.footer = cfg.footer;
4004
- if (cfg.showThinking != null)
4005
- remote.showThinking = cfg.showThinking;
4006
- if (cfg.regenerateMessage != null)
4007
- remote.regenerateMessage = cfg.regenerateMessage;
4008
- if (cfg.widgetLayout)
4009
- remote.widgetLayout = cfg.widgetLayout;
4010
- if (cfg.enableVoice != null)
4011
- remote.enableVoice = cfg.enableVoice;
4012
- if (cfg.clientActions)
4013
- remote.clientActions = cfg.clientActions;
4014
- if (Array.isArray(cfg.ragTopics))
4015
- remote.ragTopics = cfg.ragTopics;
4016
- if (Array.isArray(cfg.ragDocuments))
4017
- remote.ragDocuments = cfg.ragDocuments;
4018
- if (cfg.locale != null)
4019
- remote.locale = cfg.locale;
4383
+ const cfg = body?.data;
4384
+ if (!cfg) {
4385
+ setResult(EMPTY);
4386
+ return;
4387
+ }
4020
4388
  setResult({
4021
- remoteConfig: remote,
4389
+ remoteConfig: toChatProps(cfg),
4022
4390
  bubbleIconUrl: cfg.chatIcon || cfg.profilePicture || void 0,
4023
4391
  autoOpen: Boolean(cfg.autoOpen),
4024
4392
  keyboardShortcut: Boolean(cfg.keyboardShortcut),
4025
4393
  position: cfg.alignChatBubbleButton === "left" ? "bottom-left" : "bottom-right",
4026
4394
  widgetLayout: cfg.widgetLayout === "center" ? "center" : "bubble",
4027
- clientActions: Array.isArray(cfg.clientActions) ? cfg.clientActions : [],
4028
- bubbleSize: typeof cfg.bubbleSize === "number" ? cfg.bubbleSize : 52,
4029
- panelWidth: typeof cfg.panelWidth === "number" ? cfg.panelWidth : 360,
4030
- panelHeight: typeof cfg.panelHeight === "number" ? cfg.panelHeight : 580,
4395
+ clientActions: cfg.clientActions ?? [],
4396
+ bubbleSize: cfg.bubbleSize ?? 52,
4397
+ panelWidth: cfg.panelWidth ?? 360,
4398
+ panelHeight: cfg.panelHeight ?? 580,
4031
4399
  loading: false
4032
4400
  });
4033
4401
  }).catch(() => {
4034
- if (!cancelled) setResult((r) => ({ ...r, loading: false }));
4402
+ if (!cancelled) setResult(EMPTY);
4035
4403
  });
4036
4404
  return () => {
4037
4405
  cancelled = true;
4038
4406
  };
4039
- }, [agentId, enabled]);
4407
+ }, [agentId, enabled, apiUrl]);
4040
4408
  return result;
4041
4409
  }
4042
4410
  function toWidgetMsg(m) {
@@ -4117,6 +4485,38 @@ function useSupportChat({
4117
4485
  },
4118
4486
  [base]
4119
4487
  );
4488
+ function loadAblyScript() {
4489
+ return new Promise((resolve, reject) => {
4490
+ if (typeof window === "undefined") {
4491
+ resolve(null);
4492
+ return;
4493
+ }
4494
+ if (window.Ably) {
4495
+ resolve(window.Ably);
4496
+ return;
4497
+ }
4498
+ const existing = document.querySelector("script[data-ably-sdk]");
4499
+ if (existing) {
4500
+ existing.addEventListener("load", () => resolve(window.Ably));
4501
+ existing.addEventListener(
4502
+ "error",
4503
+ () => reject(new Error("Ably CDN load failed"))
4504
+ );
4505
+ return;
4506
+ }
4507
+ const script = document.createElement("script");
4508
+ script.src = "https://cdn.ably.com/lib/ably.min-2.js";
4509
+ script.crossOrigin = "anonymous";
4510
+ script.async = true;
4511
+ script.setAttribute("data-ably-sdk", "true");
4512
+ script.onload = () => resolve(window.Ably);
4513
+ script.onerror = () => {
4514
+ script.remove();
4515
+ reject(new Error("Ably CDN load failed"));
4516
+ };
4517
+ document.head.appendChild(script);
4518
+ });
4519
+ }
4120
4520
  react.useEffect(() => {
4121
4521
  if (!enabled || !session) return;
4122
4522
  void loadMessages(session);
@@ -4131,7 +4531,8 @@ function useSupportChat({
4131
4531
  );
4132
4532
  if (!res.ok) return;
4133
4533
  const { tokenRequest, channel: channelName } = await res.json();
4134
- const AblyLib = (await import('ably')).default;
4534
+ const AblyLib = await loadAblyScript();
4535
+ if (!AblyLib) return;
4135
4536
  ablyClient = new AblyLib.Realtime({
4136
4537
  authCallback: (_, cb) => cb(null, tokenRequest)
4137
4538
  });
@@ -4439,9 +4840,9 @@ function BubbleWidget({
4439
4840
  height: heightProp,
4440
4841
  expandedWidth = 640,
4441
4842
  expandedHeight = "calc(100vh - 100px)",
4442
- keyboardShortcut: keyboardShortcutProp = false,
4843
+ keyboardShortcut: keyboardShortcutProp,
4443
4844
  shortcutKey = "k",
4444
- autoOpen: autoOpenProp = false,
4845
+ autoOpen: autoOpenProp,
4445
4846
  bubbleIconUrl: bubbleIconUrlProp,
4446
4847
  bubbleSize: bubbleSizeProp,
4447
4848
  panelClassName,
@@ -4482,24 +4883,25 @@ function BubbleWidget({
4482
4883
  }, []);
4483
4884
  const remote = useAutoConfig(
4484
4885
  inboxToken ? "" : chatProps.agentId ?? "",
4485
- !inboxToken && autoConfig
4886
+ !inboxToken && autoConfig,
4887
+ chatProps.apiUrl
4486
4888
  );
4487
- const resolvedPosition = remote.position ?? positionProp;
4488
- const resolvedLayout = remote.widgetLayout ?? chatProps.widgetLayout ?? "bubble";
4489
- const resolvedBubbleIcon = remote.bubbleIconUrl ?? bubbleIconUrlProp;
4490
- const resolvedAutoOpen = remote.autoOpen || autoOpenProp;
4491
- const resolvedKeyboardShortcut = remote.keyboardShortcut || keyboardShortcutProp;
4492
- const resolvedBubbleSize = remote.bubbleSize ?? bubbleSizeProp;
4493
- const resolvedWidth = remote.panelWidth ?? widthProp;
4494
- const resolvedHeight = remote.panelHeight ?? heightProp;
4889
+ const resolvedPosition = positionProp ?? remote.position;
4890
+ const resolvedLayout = chatProps.widgetLayout ?? remote.widgetLayout;
4891
+ const resolvedBubbleIcon = bubbleIconUrlProp ?? remote.bubbleIconUrl;
4892
+ const resolvedAutoOpen = autoOpenProp ?? remote.autoOpen;
4893
+ const resolvedKeyboardShortcut = keyboardShortcutProp ?? remote.keyboardShortcut;
4894
+ const resolvedBubbleSize = bubbleSizeProp ?? remote.bubbleSize;
4895
+ const resolvedWidth = widthProp ?? remote.panelWidth;
4896
+ const resolvedHeight = heightProp ?? remote.panelHeight;
4495
4897
  const definedChatProps = Object.fromEntries(
4496
4898
  Object.entries(chatProps).filter(([, v]) => v !== void 0)
4497
4899
  );
4498
4900
  const mergedConfig = {
4499
- ...definedChatProps,
4500
4901
  ...remote.remoteConfig,
4902
+ ...definedChatProps,
4501
4903
  agentId: chatProps.agentId ?? "",
4502
- agentName: remote.remoteConfig.agentName ?? chatProps.agentName ?? "Asistente",
4904
+ agentName: chatProps.agentName ?? remote.remoteConfig.agentName ?? "Asistente",
4503
4905
  source: chatProps.source ?? remote.remoteConfig.source ?? "web"
4504
4906
  };
4505
4907
  const supportBackend = useSupportChat(
@@ -4515,6 +4917,10 @@ function BubbleWidget({
4515
4917
  react.useEffect(() => {
4516
4918
  setOpenRef.current = setOpen;
4517
4919
  });
4920
+ const openRef = react.useRef(open);
4921
+ react.useEffect(() => {
4922
+ openRef.current = open;
4923
+ });
4518
4924
  react.useEffect(() => {
4519
4925
  if (!resolvedAutoOpen || autoOpenedRef.current) return;
4520
4926
  const dismissedUntil = Number(localStorage.getItem(KEY_DISMISSED) ?? 0);
@@ -4524,15 +4930,29 @@ function BubbleWidget({
4524
4930
  }
4525
4931
  }, [resolvedAutoOpen]);
4526
4932
  react.useEffect(() => {
4527
- if (!resolvedKeyboardShortcut) return;
4933
+ const onToggle = () => setOpenRef.current((v) => !v);
4934
+ const onOpen = () => setOpenRef.current(true);
4935
+ const onClose = () => setOpenRef.current(false);
4936
+ window.addEventListener("wallavi:toggle-assistant", onToggle);
4937
+ window.addEventListener("wallavi:open-assistant", onOpen);
4938
+ window.addEventListener("wallavi:close-assistant", onClose);
4528
4939
  const onKey = (e) => {
4529
- if ((e.metaKey || e.ctrlKey) && e.key.toLowerCase() === shortcutKey.toLowerCase()) {
4940
+ if (resolvedKeyboardShortcut && (e.metaKey || e.ctrlKey) && e.key.toLowerCase() === shortcutKey.toLowerCase()) {
4530
4941
  e.preventDefault();
4531
4942
  setOpenRef.current((v) => !v);
4943
+ } else if (e.key === "Escape" && openRef.current) {
4944
+ e.preventDefault();
4945
+ e.stopPropagation();
4946
+ setOpenRef.current(false);
4532
4947
  }
4533
4948
  };
4534
4949
  window.addEventListener("keydown", onKey);
4535
- return () => window.removeEventListener("keydown", onKey);
4950
+ return () => {
4951
+ window.removeEventListener("wallavi:toggle-assistant", onToggle);
4952
+ window.removeEventListener("wallavi:open-assistant", onOpen);
4953
+ window.removeEventListener("wallavi:close-assistant", onClose);
4954
+ window.removeEventListener("keydown", onKey);
4955
+ };
4536
4956
  }, [resolvedKeyboardShortcut, shortcutKey]);
4537
4957
  react.useEffect(() => {
4538
4958
  if (!open) return;
@@ -4587,7 +5007,8 @@ function BubbleWidget({
4587
5007
  display: "flex",
4588
5008
  flexDirection: "column",
4589
5009
  alignItems: isLeft ? "flex-start" : "flex-end",
4590
- gap: 12
5010
+ gap: 12,
5011
+ pointerEvents: !open && hideBubble ? "none" : void 0
4591
5012
  },
4592
5013
  children: [
4593
5014
  /* @__PURE__ */ jsxRuntime.jsx(
@@ -4604,12 +5025,14 @@ function BubbleWidget({
4604
5025
  zIndex: 9999,
4605
5026
  width: panelWidth,
4606
5027
  height: panelHeight,
4607
- transition: "width 0.3s ease, height 0.3s ease"
5028
+ transition: "width 0.3s ease, height 0.3s ease",
5029
+ pointerEvents: "auto"
4608
5030
  } : {
4609
5031
  display: open ? "block" : "none",
4610
5032
  width: panelWidth,
4611
5033
  height: panelHeight,
4612
- transition: "width 0.3s ease, height 0.3s ease"
5034
+ transition: "width 0.3s ease, height 0.3s ease",
5035
+ pointerEvents: "auto"
4613
5036
  },
4614
5037
  children: /* @__PURE__ */ jsxRuntime.jsx(
4615
5038
  ChatWidget,