@wallavi/widget 1.12.7 → 1.13.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.mjs CHANGED
@@ -66,6 +66,83 @@ async function getFreshClerkToken() {
66
66
  }
67
67
  }
68
68
 
69
+ // src/lib/page-context.ts
70
+ function candidatesFrom(actions) {
71
+ const out = [];
72
+ for (const action of actions) {
73
+ const title = action.displayName ?? action.name;
74
+ const target = action.steps?.find((s) => s.action === "navigate")?.value;
75
+ if (!title || typeof target !== "string" || !target.startsWith("/")) {
76
+ continue;
77
+ }
78
+ const [path = "", search = ""] = target.split("?");
79
+ out.push({
80
+ title,
81
+ segments: path.split("/").filter(Boolean),
82
+ query: new URLSearchParams(search)
83
+ });
84
+ }
85
+ return out;
86
+ }
87
+ var PARAM = /^\[(\w+)\]$/;
88
+ var LOCALE = /^[a-z]{2}(?:-[a-z]{2,4})?$/i;
89
+ function offsetsFor(pathSegments, routeLength) {
90
+ const offsets = [];
91
+ if (pathSegments.length === routeLength) offsets.push(0);
92
+ if (pathSegments.length === routeLength + 1 && LOCALE.test(pathSegments[0] ?? "")) {
93
+ offsets.push(1);
94
+ }
95
+ return offsets;
96
+ }
97
+ function safeDecode(segment) {
98
+ try {
99
+ return decodeURIComponent(segment);
100
+ } catch {
101
+ return null;
102
+ }
103
+ }
104
+ function score(candidate, pathSegments, search) {
105
+ const { segments, query } = candidate;
106
+ if (segments.length === 0) return null;
107
+ for (const [key, value] of query) {
108
+ if (search.get(key) !== value) return null;
109
+ }
110
+ for (const offset of offsetsFor(pathSegments, segments.length)) {
111
+ const params = {};
112
+ let points = Array.from(query.keys()).length;
113
+ let matched = true;
114
+ for (let i = 0; i < segments.length && matched; i++) {
115
+ const pattern = segments[i];
116
+ const actual = safeDecode(pathSegments[offset + i]);
117
+ const param = PARAM.exec(pattern);
118
+ if (actual === null) matched = false;
119
+ else if (param) params[param[1]] = actual;
120
+ else if (pattern === actual) points += 2;
121
+ else matched = false;
122
+ }
123
+ if (matched) return { score: points, params };
124
+ }
125
+ return null;
126
+ }
127
+ function derivePageContext(location, actions) {
128
+ if (!actions?.length) return void 0;
129
+ const pathSegments = location.pathname.split("/").filter(Boolean);
130
+ const search = new URLSearchParams(location.search);
131
+ let best = null;
132
+ for (const candidate of candidatesFrom(actions)) {
133
+ const result = score(candidate, pathSegments, search);
134
+ if (result && (!best || result.score > best.score)) {
135
+ best = { title: candidate.title, ...result };
136
+ }
137
+ }
138
+ if (!best) return void 0;
139
+ return {
140
+ url: `${location.pathname}${location.search}`,
141
+ title: best.title,
142
+ ...Object.keys(best.params).length > 0 ? { params: best.params } : {}
143
+ };
144
+ }
145
+
69
146
  // src/lib/types.ts
70
147
  function getContrastColor(hex) {
71
148
  const clean = hex.replace("#", "");
@@ -79,7 +156,7 @@ function formatToolName(name) {
79
156
  return name.replace(/([A-Z])/g, " $1").replace(/_/g, " ").trim();
80
157
  }
81
158
 
82
- // src/core/stream-protocol.ts
159
+ // ../protocol/src/index.ts
83
160
  var STREAM_DELIMITER = "\u03B6\u236E";
84
161
  async function consumeStream(body, handler) {
85
162
  const reader = body.getReader();
@@ -95,9 +172,9 @@ async function consumeStream(body, handler) {
95
172
  if (!raw) continue;
96
173
  try {
97
174
  const parsed = JSON.parse(raw);
98
- const proto = parsed.data?.uiMessageProtocol;
99
- if (proto) {
175
+ if (parsed.data?.uiMessageProtocol) {
100
176
  eventCount++;
177
+ const proto = parsed.data.uiMessageProtocol;
101
178
  if (proto.type === "text-delta") {
102
179
  textAccumulator += proto.delta ?? "";
103
180
  }
@@ -247,12 +324,321 @@ function applyUiEventToMessages(prev, proto, msgId) {
247
324
  copy[idx] = msg;
248
325
  return copy;
249
326
  }
250
- var API_URL = process.env.NEXT_PUBLIC_API_URL ?? "https://wallavi-production.up.railway.app";
327
+
328
+ // src/core/declarative-actions.ts
329
+ function sanitizeSelector(selector) {
330
+ if (!selector || typeof selector !== "string") return null;
331
+ const lower = selector.toLowerCase();
332
+ if (lower.includes("<") || lower.includes(">") || lower.includes("script") || lower.includes("iframe") || lower.includes("onload") || lower.includes("onerror") || lower.includes("javascript:")) {
333
+ return null;
334
+ }
335
+ const cleanRegex = /^[a-zA-Z0-9\s\.\-#_\[\]="':]+$/;
336
+ if (!cleanRegex.test(selector)) {
337
+ return null;
338
+ }
339
+ return selector;
340
+ }
341
+ function sanitizeEventName(name) {
342
+ if (!name || typeof name !== "string") return null;
343
+ const nameRegex = /^[a-zA-Z0-9\-_:]+$/;
344
+ if (!nameRegex.test(name)) return null;
345
+ const nativeEvents = /* @__PURE__ */ new Set([
346
+ "click",
347
+ "dblclick",
348
+ "mouseup",
349
+ "mousedown",
350
+ "mouseover",
351
+ "mouseout",
352
+ "submit",
353
+ "reset",
354
+ "change",
355
+ "select",
356
+ "keydown",
357
+ "keypress",
358
+ "keyup",
359
+ "load",
360
+ "unload",
361
+ "abort",
362
+ "error",
363
+ "resize",
364
+ "scroll",
365
+ "contextmenu"
366
+ ]);
367
+ if (nativeEvents.has(name.toLowerCase())) return null;
368
+ return name;
369
+ }
370
+ function sanitizeUrl(url) {
371
+ if (!url || typeof url !== "string") return null;
372
+ const trimmed = url.trim();
373
+ const lower = trimmed.toLowerCase();
374
+ if (trimmed.startsWith("//")) {
375
+ return null;
376
+ }
377
+ if (lower.startsWith("javascript:") || lower.startsWith("data:")) {
378
+ return null;
379
+ }
380
+ if (trimmed.startsWith("/")) {
381
+ return trimmed;
382
+ }
383
+ if (trimmed.startsWith("http://") || trimmed.startsWith("https://")) {
384
+ return trimmed;
385
+ }
386
+ return null;
387
+ }
388
+ async function waitForElement(selector, retries = 5, delayMs = 100) {
389
+ const cleanId = selector.startsWith("#") ? selector.substring(1) : selector;
390
+ for (let i = 0; i < retries; i++) {
391
+ try {
392
+ const el = document.querySelector(selector);
393
+ if (el) return el;
394
+ } catch (e) {
395
+ console.error(e);
396
+ }
397
+ try {
398
+ const byId = document.getElementById(cleanId);
399
+ if (byId) return byId;
400
+ } catch (e) {
401
+ console.error(e);
402
+ }
403
+ if (!selector.startsWith("#") && !selector.startsWith(".") && !selector.startsWith("[")) {
404
+ try {
405
+ const byPrependedId = document.querySelector(`#${selector}`);
406
+ if (byPrependedId) return byPrependedId;
407
+ } catch (e) {
408
+ console.error(e);
409
+ }
410
+ }
411
+ await new Promise((resolve) => setTimeout(resolve, delayMs));
412
+ }
413
+ return null;
414
+ }
415
+ var HEX_COLOR_REGEX = /^#([A-Fa-f0-9]{3}|[A-Fa-f0-9]{6})$/;
416
+ function sanitizeColor(color, fallback = "#6366f1") {
417
+ if (!color || typeof color !== "string") return fallback;
418
+ const trimmed = color.trim();
419
+ return HEX_COLOR_REGEX.test(trimmed) ? trimmed : fallback;
420
+ }
421
+ function injectCopilotStyles(color = "#6366f1") {
422
+ if (typeof document === "undefined") return;
423
+ const safeColor = sanitizeColor(color, "#6366f1");
424
+ let style = document.getElementById(
425
+ "wallavi-copilot-styles"
426
+ );
427
+ if (!style) {
428
+ style = document.createElement("style");
429
+ style.id = "wallavi-copilot-styles";
430
+ document.head.appendChild(style);
431
+ }
432
+ if (style.dataset.color === safeColor) return;
433
+ style.dataset.color = safeColor;
434
+ style.textContent = `
435
+ @keyframes wallavi-pulse-beacon {
436
+ 0% {
437
+ box-shadow: 0 0 0 0 rgba(99, 102, 241, 0.7);
438
+ outline: 2px solid ${safeColor};
439
+ }
440
+ 70% {
441
+ box-shadow: 0 0 0 10px rgba(99, 102, 241, 0);
442
+ outline: 2px solid rgba(99, 102, 241, 0.3);
443
+ }
444
+ 100% {
445
+ box-shadow: 0 0 0 0 rgba(99, 102, 241, 0);
446
+ outline: 2px solid transparent;
447
+ }
448
+ }
449
+ .wallavi-element-beacon {
450
+ animation: wallavi-pulse-beacon 1.8s ease-out infinite !important;
451
+ outline-offset: 3px !important;
452
+ border-radius: inherit;
453
+ transition: outline 0.2s ease, box-shadow 0.2s ease !important;
454
+ }
455
+ @keyframes wallavi-flash-touch-anim {
456
+ 0% {
457
+ outline: 3px solid ${safeColor};
458
+ outline-offset: 2px;
459
+ filter: brightness(1.08);
460
+ }
461
+ 100% {
462
+ outline: 2px solid transparent;
463
+ outline-offset: 6px;
464
+ filter: none;
465
+ }
466
+ }
467
+ .wallavi-flash-touch {
468
+ animation: wallavi-flash-touch-anim 0.5s cubic-bezier(0.16, 1, 0.3, 1) forwards !important;
469
+ }
470
+ `;
471
+ document.head.appendChild(style);
472
+ }
473
+ function flashElement(el, color) {
474
+ injectCopilotStyles(color);
475
+ el.classList.remove("wallavi-flash-touch");
476
+ void el.offsetWidth;
477
+ el.classList.add("wallavi-flash-touch");
478
+ setTimeout(() => {
479
+ el.classList.remove("wallavi-flash-touch");
480
+ }, 600);
481
+ }
482
+ function highlightElement(el, color, durationMs = 3e3) {
483
+ injectCopilotStyles(color);
484
+ el.classList.add("wallavi-element-beacon");
485
+ setTimeout(() => {
486
+ el.classList.remove("wallavi-element-beacon");
487
+ }, durationMs);
488
+ }
489
+ var delay = (ms) => new Promise((resolve) => setTimeout(resolve, ms));
490
+ async function executeDeclarativeSteps(steps, onNavigate, copilotColor) {
491
+ if (typeof window === "undefined" || !Array.isArray(steps)) return;
492
+ const safeCopilotColor = sanitizeColor(copilotColor, "#6366f1");
493
+ for (const step of steps) {
494
+ try {
495
+ const { action } = step;
496
+ switch (action) {
497
+ case "scroll_to":
498
+ case "scroll_into_view": {
499
+ const selector = sanitizeSelector(step.selector);
500
+ if (!selector) break;
501
+ const el = await waitForElement(selector);
502
+ if (el) {
503
+ flashElement(el, safeCopilotColor);
504
+ el.scrollIntoView({ behavior: "smooth", block: "center" });
505
+ }
506
+ break;
507
+ }
508
+ case "click": {
509
+ const selector = sanitizeSelector(step.selector);
510
+ if (!selector) break;
511
+ const el = await waitForElement(selector);
512
+ if (el) {
513
+ flashElement(el, safeCopilotColor);
514
+ await delay(80);
515
+ el.click();
516
+ }
517
+ break;
518
+ }
519
+ case "focus": {
520
+ const selector = sanitizeSelector(step.selector);
521
+ if (!selector) break;
522
+ const el = await waitForElement(selector);
523
+ if (el) {
524
+ flashElement(el, safeCopilotColor);
525
+ el.focus();
526
+ }
527
+ break;
528
+ }
529
+ case "highlight": {
530
+ const selector = sanitizeSelector(step.selector);
531
+ if (!selector) break;
532
+ const el = await waitForElement(selector);
533
+ if (el) {
534
+ highlightElement(
535
+ el,
536
+ sanitizeColor(step.color, safeCopilotColor),
537
+ step.durationMs ?? 3e3
538
+ );
539
+ }
540
+ break;
541
+ }
542
+ case "wait": {
543
+ const parsedVal = typeof step.value === "number" ? step.value : parseInt(String(step.value ?? "500"), 10);
544
+ const ms = step.delayMs ?? (Number.isNaN(parsedVal) ? 500 : parsedVal);
545
+ await delay(ms);
546
+ break;
547
+ }
548
+ case "add_class": {
549
+ const selector = sanitizeSelector(step.selector);
550
+ const value = step.value;
551
+ if (!selector || !value) break;
552
+ const el = await waitForElement(selector);
553
+ if (el) el.classList.add(...value.split(/\s+/).filter(Boolean));
554
+ break;
555
+ }
556
+ case "remove_class": {
557
+ const selector = sanitizeSelector(step.selector);
558
+ const value = step.value;
559
+ if (!selector || !value) break;
560
+ const el = await waitForElement(selector);
561
+ if (el) el.classList.remove(...value.split(/\s+/).filter(Boolean));
562
+ break;
563
+ }
564
+ case "fill_value": {
565
+ const selector = sanitizeSelector(step.selector);
566
+ const value = step.value;
567
+ if (!selector) break;
568
+ const el = await waitForElement(selector);
569
+ if (el && (el instanceof HTMLInputElement || el instanceof HTMLTextAreaElement || el instanceof HTMLSelectElement)) {
570
+ if (el instanceof HTMLInputElement) {
571
+ const type = el.type.toLowerCase();
572
+ if (type === "password" || type === "hidden") {
573
+ console.warn(
574
+ `[Wallavi Widget] Blocked fill_value action on sensitive field: ${type}`
575
+ );
576
+ break;
577
+ }
578
+ }
579
+ const isReadOnly = (el instanceof HTMLInputElement || el instanceof HTMLTextAreaElement) && el.readOnly;
580
+ if (el.disabled || isReadOnly) {
581
+ console.warn(
582
+ "[Wallavi Widget] Blocked fill_value action on disabled/readOnly element"
583
+ );
584
+ break;
585
+ }
586
+ flashElement(el, safeCopilotColor);
587
+ el.value = value ?? "";
588
+ el.dispatchEvent(new Event("input", { bubbles: true }));
589
+ el.dispatchEvent(new Event("change", { bubbles: true }));
590
+ }
591
+ break;
592
+ }
593
+ case "dispatch_event": {
594
+ const eventName = sanitizeEventName(step.value);
595
+ if (!eventName) break;
596
+ const detail = step.payload ?? {};
597
+ window.dispatchEvent(new CustomEvent(eventName, { detail }));
598
+ break;
599
+ }
600
+ case "navigate": {
601
+ const url = sanitizeUrl(step.value);
602
+ if (url) {
603
+ if (onNavigate) {
604
+ onNavigate(url);
605
+ } else {
606
+ window.location.href = url;
607
+ }
608
+ }
609
+ break;
610
+ }
611
+ default: {
612
+ const unknownAction = step.action;
613
+ console.warn(
614
+ `[Wallavi Widget] Unknown declarative action: ${unknownAction}`
615
+ );
616
+ }
617
+ }
618
+ await delay(120);
619
+ } catch (err) {
620
+ console.error(
621
+ "[Wallavi Widget] Failed to execute declarative step:",
622
+ step,
623
+ err
624
+ );
625
+ }
626
+ }
627
+ }
628
+
629
+ // src/lib/api-url.ts
630
+ var WALLAVI_PRODUCTION_API_URL = "https://wallavi-production.up.railway.app";
631
+ function resolveApiUrl(explicit) {
632
+ return explicit ?? process.env.NEXT_PUBLIC_API_URL ?? WALLAVI_PRODUCTION_API_URL;
633
+ }
634
+
635
+ // src/hooks/use-voice-call.ts
251
636
  function useVoiceCall({
252
637
  agentId,
253
638
  threadId,
254
639
  workspaceId,
255
- customBackend
640
+ customBackend,
641
+ apiUrl
256
642
  }) {
257
643
  const [active, setActive] = useState(false);
258
644
  const [token, setToken] = useState(null);
@@ -265,7 +651,7 @@ function useVoiceCall({
265
651
  setError(null);
266
652
  try {
267
653
  const isPrivate = Boolean(workspaceId);
268
- 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)}`;
654
+ 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)}`;
269
655
  const token2 = isPrivate ? await getFreshClerkToken() : null;
270
656
  const res = await fetch(url, {
271
657
  headers: {
@@ -281,7 +667,7 @@ function useVoiceCall({
281
667
  setError(err.message);
282
668
  }
283
669
  setLoading(false);
284
- }, [agentId, threadId, workspaceId, customBackend]);
670
+ }, [agentId, threadId, workspaceId, customBackend, apiUrl]);
285
671
  const stop = useCallback(() => {
286
672
  setActive(false);
287
673
  setToken(null);
@@ -298,13 +684,33 @@ function useVoiceCall({
298
684
  };
299
685
  }
300
686
 
687
+ // src/lib/user-headers.ts
688
+ var warnedHeadersFailure = false;
689
+ async function resolveUserHeaders(userContext) {
690
+ const source = userContext?.headers;
691
+ if (!source) return {};
692
+ if (typeof source !== "function") return source;
693
+ try {
694
+ return await source() ?? {};
695
+ } catch (err) {
696
+ if (!warnedHeadersFailure) {
697
+ warnedHeadersFailure = true;
698
+ console.warn(
699
+ "[wallavi-widget] userContext.headers threw; sending without host headers",
700
+ err
701
+ );
702
+ }
703
+ return {};
704
+ }
705
+ }
706
+
301
707
  // src/hooks/use-chat.ts
302
- var API_URL2 = process.env.NEXT_PUBLIC_API_URL ?? "https://wallavi-production.up.railway.app";
303
708
  function newId() {
304
709
  return Math.random().toString(36).slice(2, 10);
305
710
  }
306
711
  function useChat({
307
712
  agentId,
713
+ apiUrl,
308
714
  workspaceId = "",
309
715
  envId,
310
716
  source = "playground",
@@ -312,13 +718,20 @@ function useChat({
312
718
  persist = false,
313
719
  onNavigate,
314
720
  playgroundOverrides,
315
- customBackend
721
+ customBackend,
722
+ copilotColor,
723
+ resolvePageContext
316
724
  }) {
725
+ const baseUrl = resolveApiUrl(apiUrl);
317
726
  const userId = userContext?.userId;
318
727
  const persistKey = persist ? userId ? `wallavi_${agentId}_${userId}` : `wallavi_${agentId}` : null;
319
728
  const onNavigateRef = useRef(onNavigate);
729
+ const resolvePageContextRef = useRef(resolvePageContext);
730
+ const copilotColorRef = useRef(copilotColor);
320
731
  useEffect(() => {
321
732
  onNavigateRef.current = onNavigate;
733
+ resolvePageContextRef.current = resolvePageContext;
734
+ copilotColorRef.current = copilotColor;
322
735
  });
323
736
  const [messages, setMessages] = useState(() => {
324
737
  if (!persistKey || typeof window === "undefined") return [];
@@ -412,7 +825,11 @@ function useChat({
412
825
  return;
413
826
  }
414
827
  if (proto.type === "client-action") {
415
- executeDeclarativeSteps(proto.steps, onNavigateRef.current);
828
+ executeDeclarativeSteps(
829
+ proto.steps,
830
+ onNavigateRef.current,
831
+ copilotColorRef.current
832
+ );
416
833
  return;
417
834
  }
418
835
  if (proto.type === "debug-trace") {
@@ -431,7 +848,9 @@ function useChat({
431
848
  const { input: userInput, msgId, extraMetadata, attachments } = opts;
432
849
  const isPrivate = Boolean(workspaceId);
433
850
  const token = isPrivate ? await getFreshClerkToken() : null;
434
- const url = isPrivate ? `${API_URL2}/api/threads/${threadId}/stream` : `${API_URL2}/api/chat/stream`;
851
+ const hostHeaders = await resolveUserHeaders(userContext);
852
+ const pageContext = userContext?.pageContext ?? resolvePageContextRef.current?.();
853
+ const url = isPrivate ? `${baseUrl}/api/threads/${threadId}/stream` : `${baseUrl}/api/chat/stream`;
435
854
  const res = await fetch(url, {
436
855
  method: "POST",
437
856
  headers: {
@@ -452,10 +871,10 @@ function useChat({
452
871
  ...userContext?.userEmail ? { userEmail: userContext.userEmail } : {},
453
872
  userMetadata: {
454
873
  ...userContext?.metadata ?? {},
455
- ...userContext?.pageContext ? { pageContext: userContext.pageContext } : {},
874
+ ...pageContext ? { pageContext } : {},
456
875
  headers: {
457
876
  ...token ? { Authorization: `Bearer ${token}` } : {},
458
- ...userContext?.headers ?? {},
877
+ ...hostHeaders,
459
878
  ...userContext?.metadata?.headers ?? {}
460
879
  },
461
880
  ...extraMetadata ?? {}
@@ -500,6 +919,7 @@ function useChat({
500
919
  }
501
920
  },
502
921
  [
922
+ baseUrl,
503
923
  agentId,
504
924
  workspaceId,
505
925
  envId,
@@ -514,7 +934,8 @@ function useChat({
514
934
  agentId,
515
935
  threadId,
516
936
  workspaceId,
517
- customBackend
937
+ customBackend,
938
+ apiUrl: baseUrl
518
939
  });
519
940
  useEffect(() => {
520
941
  if (customBackend || !persistKey) return;
@@ -563,7 +984,7 @@ function useChat({
563
984
  try {
564
985
  const isPrivate = Boolean(workspaceId);
565
986
  const token = isPrivate ? await getFreshClerkToken() : null;
566
- const url = isPrivate ? `${API_URL2}/api/threads/${threadId}/messages` : `${API_URL2}/api/chat/messages?agentId=${encodeURIComponent(agentId)}&threadId=${encodeURIComponent(threadId)}`;
987
+ const url = isPrivate ? `${baseUrl}/api/threads/${threadId}/messages` : `${baseUrl}/api/chat/messages?agentId=${encodeURIComponent(agentId)}&threadId=${encodeURIComponent(threadId)}`;
567
988
  const res = await fetch(url, {
568
989
  headers: {
569
990
  ...token ? { Authorization: `Bearer ${token}` } : {}
@@ -627,7 +1048,15 @@ function useChat({
627
1048
  }
628
1049
  }
629
1050
  }
630
- }, [threadId, agentId, workspaceId, envId, persistKey, fetchAndStream]);
1051
+ }, [
1052
+ baseUrl,
1053
+ threadId,
1054
+ agentId,
1055
+ workspaceId,
1056
+ envId,
1057
+ persistKey,
1058
+ fetchAndStream
1059
+ ]);
631
1060
  const reset = useCallback(() => {
632
1061
  setMessages([]);
633
1062
  setInput("");
@@ -811,106 +1240,6 @@ function useChat({
811
1240
  setSelectedContext
812
1241
  };
813
1242
  }
814
- async function waitForElement(selector, retries = 5, delayMs = 100) {
815
- const cleanId = selector.startsWith("#") ? selector.substring(1) : selector;
816
- for (let i = 0; i < retries; i++) {
817
- try {
818
- const el = document.querySelector(selector);
819
- if (el) return el;
820
- } catch (e) {
821
- }
822
- try {
823
- const byId = document.getElementById(cleanId);
824
- if (byId) return byId;
825
- } catch (e) {
826
- }
827
- if (!selector.startsWith("#") && !selector.startsWith(".") && !selector.startsWith("[")) {
828
- try {
829
- const byPrependedId = document.querySelector(`#${selector}`);
830
- if (byPrependedId) return byPrependedId;
831
- } catch (e) {
832
- }
833
- }
834
- await new Promise((resolve) => setTimeout(resolve, delayMs));
835
- }
836
- return null;
837
- }
838
- async function executeDeclarativeSteps(steps, onNavigate) {
839
- if (typeof window === "undefined" || !Array.isArray(steps)) return;
840
- for (const step of steps) {
841
- try {
842
- const { action, selector, value, payload } = step;
843
- switch (action) {
844
- case "scroll_into_view": {
845
- if (!selector) break;
846
- const el = await waitForElement(selector);
847
- el?.scrollIntoView({ behavior: "smooth", block: "center" });
848
- break;
849
- }
850
- case "click": {
851
- if (!selector) break;
852
- const el = await waitForElement(selector);
853
- el?.click();
854
- break;
855
- }
856
- case "focus": {
857
- if (!selector) break;
858
- const el = await waitForElement(selector);
859
- el?.focus();
860
- break;
861
- }
862
- case "add_class": {
863
- if (!selector || !value) break;
864
- const el = await waitForElement(selector);
865
- if (el) el.classList.add(...value.split(/\s+/).filter(Boolean));
866
- break;
867
- }
868
- case "remove_class": {
869
- if (!selector || !value) break;
870
- const el = await waitForElement(selector);
871
- if (el) el.classList.remove(...value.split(/\s+/).filter(Boolean));
872
- break;
873
- }
874
- case "fill_value": {
875
- if (!selector) break;
876
- const el = await waitForElement(selector);
877
- if (el) {
878
- el.value = value ?? "";
879
- el.dispatchEvent(new Event("input", { bubbles: true }));
880
- el.dispatchEvent(new Event("change", { bubbles: true }));
881
- }
882
- break;
883
- }
884
- case "dispatch_event": {
885
- if (!value) break;
886
- const detail = payload ?? {};
887
- window.dispatchEvent(new CustomEvent(value, { detail }));
888
- break;
889
- }
890
- case "navigate": {
891
- if (value) {
892
- if (onNavigate) {
893
- onNavigate(value);
894
- } else {
895
- window.location.href = value;
896
- }
897
- }
898
- break;
899
- }
900
- default:
901
- console.warn(
902
- `[Wallavi Widget] Unknown declarative action: ${action}`
903
- );
904
- }
905
- } catch (err) {
906
- console.error(
907
- "[Wallavi Widget] Failed to execute declarative step:",
908
- step,
909
- err
910
- );
911
- }
912
- }
913
- }
914
1243
  function getPreferredMimeType() {
915
1244
  if (typeof MediaRecorder === "undefined") return "";
916
1245
  const candidates = [
@@ -927,7 +1256,6 @@ function mimeTypeToExtension(mimeType) {
927
1256
  if (mimeType.includes("mp4")) return "mp4";
928
1257
  return "webm";
929
1258
  }
930
- var DEFAULT_API_URL = process.env.NEXT_PUBLIC_API_URL ?? "https://wallavi-production.up.railway.app";
931
1259
  function useVoice({
932
1260
  agentId,
933
1261
  apiUrl,
@@ -940,7 +1268,7 @@ function useVoice({
940
1268
  const streamRef = useRef(null);
941
1269
  const errorTimerRef = useRef(null);
942
1270
  const isSupported = typeof window !== "undefined" && typeof MediaRecorder !== "undefined" && !!navigator?.mediaDevices?.getUserMedia;
943
- const base = apiUrl ?? DEFAULT_API_URL;
1271
+ const base = resolveApiUrl(apiUrl);
944
1272
  const transcribeBlob = useCallback(
945
1273
  async (blob, mimeType) => {
946
1274
  setVoiceState("transcribing");
@@ -1019,7 +1347,6 @@ function useVoice({
1019
1347
  }, []);
1020
1348
  return { voiceState, isSupported, start, stop };
1021
1349
  }
1022
- var DEFAULT_API_URL2 = process.env.NEXT_PUBLIC_API_URL ?? "https://wallavi-production.up.railway.app";
1023
1350
  function makeId() {
1024
1351
  return `att_${Date.now().toString(36)}_${Math.random().toString(36).slice(2, 7)}`;
1025
1352
  }
@@ -1029,7 +1356,7 @@ function useAttachments({
1029
1356
  maxFiles = 5
1030
1357
  }) {
1031
1358
  const [attachments, setAttachments] = useState([]);
1032
- const base = apiUrl ?? DEFAULT_API_URL2;
1359
+ const base = resolveApiUrl(apiUrl);
1033
1360
  const uploadOne = useCallback(
1034
1361
  async (file, id) => {
1035
1362
  const form = new FormData();
@@ -2824,6 +3151,20 @@ function VoiceOverlay({
2824
3151
  }
2825
3152
  ) });
2826
3153
  }
3154
+
3155
+ // src/lib/unique-keys.ts
3156
+ function uniqueKeys(items, baseKey) {
3157
+ const seen = /* @__PURE__ */ new Map();
3158
+ return items.map((item) => {
3159
+ const base = baseKey(item);
3160
+ const count = seen.get(base) ?? 0;
3161
+ seen.set(base, count + 1);
3162
+ return count === 0 ? base : `${base}#${count}`;
3163
+ });
3164
+ }
3165
+ var actionKey = (action) => `${action.name}:${JSON.stringify(action.steps ?? [])}`;
3166
+ var docKey = (doc) => `${doc.name}:${doc.url ?? ""}`;
3167
+ var topicKey = (topic) => topic.name;
2827
3168
  function SvgIcon({
2828
3169
  className,
2829
3170
  strokeWidth = "1.8",
@@ -2897,11 +3238,11 @@ function urlRelevance(docUrl, pageUrl) {
2897
3238
  return 50;
2898
3239
  const docParts = docClean.split(/[/?#]/).filter(Boolean);
2899
3240
  const pageParts = pageClean.split(/[/?#]/).filter(Boolean);
2900
- let score = 0;
3241
+ let score2 = 0;
2901
3242
  for (const part of pageParts) {
2902
- if (docParts.includes(part)) score += 10;
3243
+ if (docParts.includes(part)) score2 += 10;
2903
3244
  }
2904
- return score;
3245
+ return score2;
2905
3246
  }
2906
3247
  function topicToQuestion(topic, locale) {
2907
3248
  const name = topic.name.trim();
@@ -2992,9 +3333,9 @@ function getContextKeywords(pageUrl, pageTitle, pageParams, pageVars, metadata)
2992
3333
  return Array.from(keywords);
2993
3334
  }
2994
3335
  function computeRelevanceScore(item, keywords, pageUrl) {
2995
- let score = 0;
3336
+ let score2 = 0;
2996
3337
  if (item.url && pageUrl) {
2997
- score += urlRelevance(item.url, pageUrl);
3338
+ score2 += urlRelevance(item.url, pageUrl);
2998
3339
  }
2999
3340
  const itemName = (item.name || "").toLowerCase().normalize("NFD").replace(/[\u0300-\u036f]/g, "");
3000
3341
  const itemDesc = (item.description || item.summary || "").toLowerCase().normalize("NFD").replace(/[\u0300-\u036f]/g, "");
@@ -3002,33 +3343,41 @@ function computeRelevanceScore(item, keywords, pageUrl) {
3002
3343
  const nameWords = itemName.split(/[^a-z0-9]/).filter(Boolean);
3003
3344
  for (const keyword of keywords) {
3004
3345
  if (itemName.includes(keyword)) {
3005
- score += 30;
3346
+ score2 += 30;
3006
3347
  if (nameWords.includes(keyword)) {
3007
- score += 20;
3348
+ score2 += 20;
3008
3349
  }
3009
3350
  }
3010
3351
  if (itemDesc.includes(keyword)) {
3011
- score += 15;
3352
+ score2 += 15;
3012
3353
  }
3013
3354
  if (itemUrl.includes(keyword)) {
3014
- score += 20;
3355
+ score2 += 20;
3015
3356
  }
3016
3357
  }
3017
- if (item.keywords) {
3018
- const allItemKeywords = [
3019
- ...item.keywords.en || [],
3020
- ...item.keywords.es || [],
3021
- ...item.keywords.fr || []
3022
- ].map(
3358
+ if (item.keywords && typeof item.keywords === "object") {
3359
+ const rawList = [];
3360
+ if (Array.isArray(item.keywords)) {
3361
+ rawList.push(...item.keywords);
3362
+ } else {
3363
+ const kwRecord = item.keywords;
3364
+ for (const lang of ["en", "es", "fr"]) {
3365
+ const langVal = kwRecord[lang];
3366
+ if (Array.isArray(langVal)) {
3367
+ rawList.push(...langVal);
3368
+ }
3369
+ }
3370
+ }
3371
+ const allItemKeywords = rawList.filter((k) => typeof k === "string" && k.trim().length > 0).map(
3023
3372
  (k) => k.toLowerCase().normalize("NFD").replace(/[\u0300-\u036f]/g, "")
3024
3373
  );
3025
3374
  for (const keyword of keywords) {
3026
3375
  if (allItemKeywords.includes(keyword)) {
3027
- score += 45;
3376
+ score2 += 45;
3028
3377
  }
3029
3378
  }
3030
3379
  }
3031
- return score;
3380
+ return score2;
3032
3381
  }
3033
3382
  var STAGGER_MS = 50;
3034
3383
  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";
@@ -3241,6 +3590,9 @@ function CommandPanel({
3241
3590
  }));
3242
3591
  return scored.sort((a, b) => b.score - a.score);
3243
3592
  }, [clientActions, pageUrl, pageTitle, pageParams, pageVars, metadata]);
3593
+ const actionKeys = uniqueKeys(sortedActions, actionKey);
3594
+ const docKeys = uniqueKeys(relevantDocs, docKey);
3595
+ const topicKeys = uniqueKeys(topicQuestions, topicKey);
3244
3596
  const hasActions = sortedActions.length > 0;
3245
3597
  const hasDocs = relevantDocs.length > 0;
3246
3598
  const hasTopics = topicQuestions.length > 0;
@@ -3262,7 +3614,7 @@ function CommandPanel({
3262
3614
  accentColor,
3263
3615
  onExecute: () => handleExecute(action.steps)
3264
3616
  },
3265
- action.name
3617
+ actionKeys[i]
3266
3618
  )) })
3267
3619
  ] }),
3268
3620
  hasDocs && /* @__PURE__ */ jsxs("section", { children: [
@@ -3275,7 +3627,7 @@ function CommandPanel({
3275
3627
  onSend,
3276
3628
  locale
3277
3629
  },
3278
- `${doc.name}-${doc.url ?? i}`
3630
+ docKeys[i]
3279
3631
  )) })
3280
3632
  ] }),
3281
3633
  hasTopics && /* @__PURE__ */ jsxs("section", { children: [
@@ -3287,7 +3639,7 @@ function CommandPanel({
3287
3639
  index: i,
3288
3640
  onSend
3289
3641
  },
3290
- topic.name
3642
+ topicKeys[i]
3291
3643
  )) })
3292
3644
  ] })
3293
3645
  ] }),
@@ -3334,7 +3686,7 @@ function CommandPanel({
3334
3686
  accentColor,
3335
3687
  onExecute: () => handleExecute(action.steps)
3336
3688
  },
3337
- action.name
3689
+ actionKeys[i]
3338
3690
  )) })
3339
3691
  ] }),
3340
3692
  hasDocs && /* @__PURE__ */ jsxs("section", { children: [
@@ -3347,7 +3699,7 @@ function CommandPanel({
3347
3699
  onSend,
3348
3700
  locale
3349
3701
  },
3350
- `${doc.name}-${doc.url ?? i}`
3702
+ docKeys[i]
3351
3703
  )) })
3352
3704
  ] }),
3353
3705
  hasTopics && /* @__PURE__ */ jsxs("section", { children: [
@@ -3360,13 +3712,14 @@ function CommandPanel({
3360
3712
  compact: true,
3361
3713
  onSend
3362
3714
  },
3363
- topic.name
3715
+ topicKeys[i]
3364
3716
  )) })
3365
3717
  ] })
3366
3718
  ] });
3367
3719
  }
3368
3720
  function ChatWidget({
3369
3721
  agentId,
3722
+ apiUrl,
3370
3723
  workspaceId,
3371
3724
  agentName,
3372
3725
  displayName,
@@ -3427,8 +3780,10 @@ function ChatWidget({
3427
3780
  window.addEventListener("message", handleMessage);
3428
3781
  return () => window.removeEventListener("message", handleMessage);
3429
3782
  }, []);
3783
+ const resolvePageContext = () => userContext?.pageContext ?? (typeof window === "undefined" ? void 0 : derivePageContext(window.location, clientActions));
3430
3784
  const chat = useChat({
3431
3785
  agentId,
3786
+ apiUrl,
3432
3787
  workspaceId,
3433
3788
  envId,
3434
3789
  source,
@@ -3436,10 +3791,13 @@ function ChatWidget({
3436
3791
  persist,
3437
3792
  onNavigate,
3438
3793
  playgroundOverrides,
3439
- customBackend
3794
+ customBackend,
3795
+ copilotColor: userMessageColor,
3796
+ resolvePageContext: () => resolvePageContext()
3440
3797
  });
3441
3798
  const voice = useVoice({
3442
3799
  agentId,
3800
+ apiUrl,
3443
3801
  onTranscript: (text) => {
3444
3802
  if (voiceAutoSend) {
3445
3803
  void chat.send(text);
@@ -3448,7 +3806,7 @@ function ChatWidget({
3448
3806
  }
3449
3807
  }
3450
3808
  });
3451
- const attachmentHook = useAttachments({ agentId });
3809
+ const attachmentHook = useAttachments({ agentId, apiUrl });
3452
3810
  const debugTraceLenRef = useRef(0);
3453
3811
  useEffect(() => {
3454
3812
  if (!onDebugTrace || chat.debugTraces.length <= debugTraceLenRef.current)
@@ -3548,9 +3906,9 @@ function ChatWidget({
3548
3906
  const showInlineCommandPanel = showCommandPanel && !showSidebar && chat.messages.length === 0 && (showQuickActions && clientActions.length > 0 || showRagTopics && ragTopics.length > 0 || showRagDocuments && ragDocuments.length > 0);
3549
3907
  const handleExecuteAction = useCallback(
3550
3908
  (steps) => {
3551
- executeDeclarativeSteps(steps, onNavigate);
3909
+ executeDeclarativeSteps(steps, onNavigate, userMessageColor);
3552
3910
  },
3553
- [onNavigate]
3911
+ [onNavigate, userMessageColor]
3554
3912
  );
3555
3913
  const [activeActionIndex, setActiveActionIndex] = useState(-1);
3556
3914
  const query = chat.input.toLowerCase().trim();
@@ -3607,11 +3965,12 @@ function ChatWidget({
3607
3965
  },
3608
3966
  [matchingActions, activeActionIndex, handleExecuteAction, chat]
3609
3967
  );
3968
+ const pageContext = resolvePageContext();
3610
3969
  const commandPanelProps = {
3611
- pageUrl: userContext?.pageContext?.url,
3612
- pageTitle: userContext?.pageContext?.title,
3613
- pageParams: userContext?.pageContext?.params,
3614
- pageVars: userContext?.pageContext?.vars,
3970
+ pageUrl: pageContext?.url,
3971
+ pageTitle: pageContext?.title,
3972
+ pageParams: pageContext?.params,
3973
+ pageVars: pageContext?.vars,
3615
3974
  metadata: userContext?.metadata,
3616
3975
  clientActions: showQuickActions ? clientActions : [],
3617
3976
  ragTopics: showRagTopics ? ragTopics : [],
@@ -3931,7 +4290,6 @@ function ChatWidget({
3931
4290
  }
3932
4291
  );
3933
4292
  }
3934
- var WALLAVI_PUBLIC_API = "https://wallavi-production.up.railway.app";
3935
4293
  var EMPTY = {
3936
4294
  remoteConfig: {},
3937
4295
  bubbleIconUrl: void 0,
@@ -3945,7 +4303,37 @@ var EMPTY = {
3945
4303
  panelHeight: 580,
3946
4304
  loading: false
3947
4305
  };
3948
- function useAutoConfig(agentId, enabled) {
4306
+ function toChatProps(cfg) {
4307
+ const remote = {};
4308
+ const set = (key, value) => {
4309
+ if (value !== null && value !== void 0) remote[key] = value;
4310
+ };
4311
+ set("agentName", cfg.agentName);
4312
+ set("profilePicture", cfg.profilePicture);
4313
+ set("displayName", cfg.displayName);
4314
+ set("theme", cfg.theme);
4315
+ set("userMessageColor", cfg.userMessageColor);
4316
+ if (cfg.initialMessages?.length) remote.initialMessages = cfg.initialMessages;
4317
+ if (cfg.suggestedMessages?.length)
4318
+ remote.suggestedMessages = cfg.suggestedMessages;
4319
+ set("messagePlaceholder", cfg.messagePlaceholder);
4320
+ set("watermark", cfg.watermark);
4321
+ set("footer", cfg.footer);
4322
+ set("showThinking", cfg.showThinking);
4323
+ set("regenerateMessage", cfg.regenerateMessage);
4324
+ set("widgetLayout", cfg.widgetLayout);
4325
+ set("enableVoice", cfg.enableVoice);
4326
+ set("showCommandPanel", cfg.showCommandPanel);
4327
+ set("showRagTopics", cfg.showRagTopics);
4328
+ set("showRagDocuments", cfg.showRagDocuments);
4329
+ set("showQuickActions", cfg.showQuickActions);
4330
+ set("clientActions", cfg.clientActions);
4331
+ set("ragTopics", cfg.ragTopics);
4332
+ set("ragDocuments", cfg.ragDocuments);
4333
+ set("locale", cfg.locale);
4334
+ return remote;
4335
+ }
4336
+ function useAutoConfig(agentId, enabled, apiUrl) {
3949
4337
  const [result, setResult] = useState(() => ({
3950
4338
  ...EMPTY,
3951
4339
  loading: enabled && Boolean(agentId)
@@ -3956,61 +4344,34 @@ function useAutoConfig(agentId, enabled) {
3956
4344
  return;
3957
4345
  }
3958
4346
  let cancelled = false;
3959
- fetch(`${WALLAVI_PUBLIC_API}/api/public/widget/${agentId}`).then((r) => r.json()).then((body) => {
4347
+ setResult({ ...EMPTY, loading: true });
4348
+ fetch(`${resolveApiUrl(apiUrl)}/api/public/widget/${agentId}`).then((r) => r.json()).then((body) => {
3960
4349
  if (cancelled) return;
3961
- const cfg = body?.data ?? {};
3962
- const remote = {};
3963
- if (cfg.profilePicture != null)
3964
- remote.profilePicture = cfg.profilePicture;
3965
- if (cfg.displayName != null)
3966
- remote.displayName = cfg.displayName;
3967
- if (cfg.theme) remote.theme = cfg.theme;
3968
- if (cfg.userMessageColor)
3969
- remote.userMessageColor = cfg.userMessageColor;
3970
- if (Array.isArray(cfg.initialMessages) && cfg.initialMessages.length > 0)
3971
- remote.initialMessages = cfg.initialMessages;
3972
- if (Array.isArray(cfg.suggestedMessages))
3973
- remote.suggestedMessages = cfg.suggestedMessages;
3974
- if (cfg.messagePlaceholder != null)
3975
- remote.messagePlaceholder = cfg.messagePlaceholder;
3976
- if (cfg.watermark != null) remote.watermark = cfg.watermark;
3977
- if (cfg.footer != null) remote.footer = cfg.footer;
3978
- if (cfg.showThinking != null)
3979
- remote.showThinking = cfg.showThinking;
3980
- if (cfg.regenerateMessage != null)
3981
- remote.regenerateMessage = cfg.regenerateMessage;
3982
- if (cfg.widgetLayout)
3983
- remote.widgetLayout = cfg.widgetLayout;
3984
- if (cfg.enableVoice != null)
3985
- remote.enableVoice = cfg.enableVoice;
3986
- if (cfg.clientActions)
3987
- remote.clientActions = cfg.clientActions;
3988
- if (Array.isArray(cfg.ragTopics))
3989
- remote.ragTopics = cfg.ragTopics;
3990
- if (Array.isArray(cfg.ragDocuments))
3991
- remote.ragDocuments = cfg.ragDocuments;
3992
- if (cfg.locale != null)
3993
- remote.locale = cfg.locale;
4350
+ const cfg = body?.data;
4351
+ if (!cfg) {
4352
+ setResult(EMPTY);
4353
+ return;
4354
+ }
3994
4355
  setResult({
3995
- remoteConfig: remote,
4356
+ remoteConfig: toChatProps(cfg),
3996
4357
  bubbleIconUrl: cfg.chatIcon || cfg.profilePicture || void 0,
3997
4358
  autoOpen: Boolean(cfg.autoOpen),
3998
4359
  keyboardShortcut: Boolean(cfg.keyboardShortcut),
3999
4360
  position: cfg.alignChatBubbleButton === "left" ? "bottom-left" : "bottom-right",
4000
4361
  widgetLayout: cfg.widgetLayout === "center" ? "center" : "bubble",
4001
- clientActions: Array.isArray(cfg.clientActions) ? cfg.clientActions : [],
4002
- bubbleSize: typeof cfg.bubbleSize === "number" ? cfg.bubbleSize : 52,
4003
- panelWidth: typeof cfg.panelWidth === "number" ? cfg.panelWidth : 360,
4004
- panelHeight: typeof cfg.panelHeight === "number" ? cfg.panelHeight : 580,
4362
+ clientActions: cfg.clientActions ?? [],
4363
+ bubbleSize: cfg.bubbleSize ?? 52,
4364
+ panelWidth: cfg.panelWidth ?? 360,
4365
+ panelHeight: cfg.panelHeight ?? 580,
4005
4366
  loading: false
4006
4367
  });
4007
4368
  }).catch(() => {
4008
- if (!cancelled) setResult((r) => ({ ...r, loading: false }));
4369
+ if (!cancelled) setResult(EMPTY);
4009
4370
  });
4010
4371
  return () => {
4011
4372
  cancelled = true;
4012
4373
  };
4013
- }, [agentId, enabled]);
4374
+ }, [agentId, enabled, apiUrl]);
4014
4375
  return result;
4015
4376
  }
4016
4377
  function toWidgetMsg(m) {
@@ -4091,6 +4452,38 @@ function useSupportChat({
4091
4452
  },
4092
4453
  [base]
4093
4454
  );
4455
+ function loadAblyScript() {
4456
+ return new Promise((resolve, reject) => {
4457
+ if (typeof window === "undefined") {
4458
+ resolve(null);
4459
+ return;
4460
+ }
4461
+ if (window.Ably) {
4462
+ resolve(window.Ably);
4463
+ return;
4464
+ }
4465
+ const existing = document.querySelector("script[data-ably-sdk]");
4466
+ if (existing) {
4467
+ existing.addEventListener("load", () => resolve(window.Ably));
4468
+ existing.addEventListener(
4469
+ "error",
4470
+ () => reject(new Error("Ably CDN load failed"))
4471
+ );
4472
+ return;
4473
+ }
4474
+ const script = document.createElement("script");
4475
+ script.src = "https://cdn.ably.com/lib/ably.min-2.js";
4476
+ script.crossOrigin = "anonymous";
4477
+ script.async = true;
4478
+ script.setAttribute("data-ably-sdk", "true");
4479
+ script.onload = () => resolve(window.Ably);
4480
+ script.onerror = () => {
4481
+ script.remove();
4482
+ reject(new Error("Ably CDN load failed"));
4483
+ };
4484
+ document.head.appendChild(script);
4485
+ });
4486
+ }
4094
4487
  useEffect(() => {
4095
4488
  if (!enabled || !session) return;
4096
4489
  void loadMessages(session);
@@ -4105,7 +4498,8 @@ function useSupportChat({
4105
4498
  );
4106
4499
  if (!res.ok) return;
4107
4500
  const { tokenRequest, channel: channelName } = await res.json();
4108
- const AblyLib = (await import('ably')).default;
4501
+ const AblyLib = await loadAblyScript();
4502
+ if (!AblyLib) return;
4109
4503
  ablyClient = new AblyLib.Realtime({
4110
4504
  authCallback: (_, cb) => cb(null, tokenRequest)
4111
4505
  });
@@ -4413,9 +4807,9 @@ function BubbleWidget({
4413
4807
  height: heightProp,
4414
4808
  expandedWidth = 640,
4415
4809
  expandedHeight = "calc(100vh - 100px)",
4416
- keyboardShortcut: keyboardShortcutProp = false,
4810
+ keyboardShortcut: keyboardShortcutProp,
4417
4811
  shortcutKey = "k",
4418
- autoOpen: autoOpenProp = false,
4812
+ autoOpen: autoOpenProp,
4419
4813
  bubbleIconUrl: bubbleIconUrlProp,
4420
4814
  bubbleSize: bubbleSizeProp,
4421
4815
  panelClassName,
@@ -4456,24 +4850,25 @@ function BubbleWidget({
4456
4850
  }, []);
4457
4851
  const remote = useAutoConfig(
4458
4852
  inboxToken ? "" : chatProps.agentId ?? "",
4459
- !inboxToken && autoConfig
4853
+ !inboxToken && autoConfig,
4854
+ chatProps.apiUrl
4460
4855
  );
4461
- const resolvedPosition = remote.position ?? positionProp;
4462
- const resolvedLayout = remote.widgetLayout ?? chatProps.widgetLayout ?? "bubble";
4463
- const resolvedBubbleIcon = remote.bubbleIconUrl ?? bubbleIconUrlProp;
4464
- const resolvedAutoOpen = remote.autoOpen || autoOpenProp;
4465
- const resolvedKeyboardShortcut = remote.keyboardShortcut || keyboardShortcutProp;
4466
- const resolvedBubbleSize = remote.bubbleSize ?? bubbleSizeProp;
4467
- const resolvedWidth = remote.panelWidth ?? widthProp;
4468
- const resolvedHeight = remote.panelHeight ?? heightProp;
4856
+ const resolvedPosition = positionProp ?? remote.position;
4857
+ const resolvedLayout = chatProps.widgetLayout ?? remote.widgetLayout;
4858
+ const resolvedBubbleIcon = bubbleIconUrlProp ?? remote.bubbleIconUrl;
4859
+ const resolvedAutoOpen = autoOpenProp ?? remote.autoOpen;
4860
+ const resolvedKeyboardShortcut = keyboardShortcutProp ?? remote.keyboardShortcut;
4861
+ const resolvedBubbleSize = bubbleSizeProp ?? remote.bubbleSize;
4862
+ const resolvedWidth = widthProp ?? remote.panelWidth;
4863
+ const resolvedHeight = heightProp ?? remote.panelHeight;
4469
4864
  const definedChatProps = Object.fromEntries(
4470
4865
  Object.entries(chatProps).filter(([, v]) => v !== void 0)
4471
4866
  );
4472
4867
  const mergedConfig = {
4473
- ...definedChatProps,
4474
4868
  ...remote.remoteConfig,
4869
+ ...definedChatProps,
4475
4870
  agentId: chatProps.agentId ?? "",
4476
- agentName: remote.remoteConfig.agentName ?? chatProps.agentName ?? "Asistente",
4871
+ agentName: chatProps.agentName ?? remote.remoteConfig.agentName ?? "Asistente",
4477
4872
  source: chatProps.source ?? remote.remoteConfig.source ?? "web"
4478
4873
  };
4479
4874
  const supportBackend = useSupportChat(
@@ -4489,6 +4884,10 @@ function BubbleWidget({
4489
4884
  useEffect(() => {
4490
4885
  setOpenRef.current = setOpen;
4491
4886
  });
4887
+ const openRef = useRef(open);
4888
+ useEffect(() => {
4889
+ openRef.current = open;
4890
+ });
4492
4891
  useEffect(() => {
4493
4892
  if (!resolvedAutoOpen || autoOpenedRef.current) return;
4494
4893
  const dismissedUntil = Number(localStorage.getItem(KEY_DISMISSED) ?? 0);
@@ -4498,15 +4897,29 @@ function BubbleWidget({
4498
4897
  }
4499
4898
  }, [resolvedAutoOpen]);
4500
4899
  useEffect(() => {
4501
- if (!resolvedKeyboardShortcut) return;
4900
+ const onToggle = () => setOpenRef.current((v) => !v);
4901
+ const onOpen = () => setOpenRef.current(true);
4902
+ const onClose = () => setOpenRef.current(false);
4903
+ window.addEventListener("wallavi:toggle-assistant", onToggle);
4904
+ window.addEventListener("wallavi:open-assistant", onOpen);
4905
+ window.addEventListener("wallavi:close-assistant", onClose);
4502
4906
  const onKey = (e) => {
4503
- if ((e.metaKey || e.ctrlKey) && e.key.toLowerCase() === shortcutKey.toLowerCase()) {
4907
+ if (resolvedKeyboardShortcut && (e.metaKey || e.ctrlKey) && e.key.toLowerCase() === shortcutKey.toLowerCase()) {
4504
4908
  e.preventDefault();
4505
4909
  setOpenRef.current((v) => !v);
4910
+ } else if (e.key === "Escape" && openRef.current) {
4911
+ e.preventDefault();
4912
+ e.stopPropagation();
4913
+ setOpenRef.current(false);
4506
4914
  }
4507
4915
  };
4508
4916
  window.addEventListener("keydown", onKey);
4509
- return () => window.removeEventListener("keydown", onKey);
4917
+ return () => {
4918
+ window.removeEventListener("wallavi:toggle-assistant", onToggle);
4919
+ window.removeEventListener("wallavi:open-assistant", onOpen);
4920
+ window.removeEventListener("wallavi:close-assistant", onClose);
4921
+ window.removeEventListener("keydown", onKey);
4922
+ };
4510
4923
  }, [resolvedKeyboardShortcut, shortcutKey]);
4511
4924
  useEffect(() => {
4512
4925
  if (!open) return;
@@ -4561,7 +4974,8 @@ function BubbleWidget({
4561
4974
  display: "flex",
4562
4975
  flexDirection: "column",
4563
4976
  alignItems: isLeft ? "flex-start" : "flex-end",
4564
- gap: 12
4977
+ gap: 12,
4978
+ pointerEvents: !open && hideBubble ? "none" : void 0
4565
4979
  },
4566
4980
  children: [
4567
4981
  /* @__PURE__ */ jsx(
@@ -4578,12 +4992,14 @@ function BubbleWidget({
4578
4992
  zIndex: 9999,
4579
4993
  width: panelWidth,
4580
4994
  height: panelHeight,
4581
- transition: "width 0.3s ease, height 0.3s ease"
4995
+ transition: "width 0.3s ease, height 0.3s ease",
4996
+ pointerEvents: "auto"
4582
4997
  } : {
4583
4998
  display: open ? "block" : "none",
4584
4999
  width: panelWidth,
4585
5000
  height: panelHeight,
4586
- transition: "width 0.3s ease, height 0.3s ease"
5001
+ transition: "width 0.3s ease, height 0.3s ease",
5002
+ pointerEvents: "auto"
4587
5003
  },
4588
5004
  children: /* @__PURE__ */ jsx(
4589
5005
  ChatWidget,