@unctad-ai/voice-agent-ui 5.0.0 → 5.0.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.
@@ -5,7 +5,7 @@ import {
5
5
  SliderSetting,
6
6
  ToggleSetting,
7
7
  VoiceSettingsView
8
- } from "./chunk-SRZ6RHED.js";
8
+ } from "./chunk-K3JJWWRQ.js";
9
9
  export {
10
10
  Divider,
11
11
  SelectSetting,
@@ -14,4 +14,4 @@ export {
14
14
  ToggleSetting,
15
15
  VoiceSettingsView as default
16
16
  };
17
- //# sourceMappingURL=VoiceSettingsView-AMFLQL3Z.js.map
17
+ //# sourceMappingURL=VoiceSettingsView-QY7TEHYZ.js.map
@@ -1505,7 +1505,7 @@ function VoiceSettingsView({ onBack, onVolumeChange }) {
1505
1505
  ) : /* @__PURE__ */ jsx3("span", {}),
1506
1506
  /* @__PURE__ */ jsxs2("span", { children: [
1507
1507
  "Kit v",
1508
- /* @__PURE__ */ jsx3("span", { style: { fontWeight: 500, color: "#6b7280" }, children: "5.0.0" })
1508
+ /* @__PURE__ */ jsx3("span", { style: { fontWeight: 500, color: "#6b7280" }, children: "5.0.1" })
1509
1509
  ] })
1510
1510
  ] }) })
1511
1511
  ]
@@ -1597,4 +1597,4 @@ export {
1597
1597
  SettingsSection,
1598
1598
  Divider
1599
1599
  };
1600
- //# sourceMappingURL=chunk-SRZ6RHED.js.map
1600
+ //# sourceMappingURL=chunk-K3JJWWRQ.js.map
package/dist/index.js CHANGED
@@ -8,7 +8,7 @@ import {
8
8
  VoiceSettingsProvider,
9
9
  VoiceSettingsView,
10
10
  useVoiceSettings
11
- } from "./chunk-SRZ6RHED.js";
11
+ } from "./chunk-K3JJWWRQ.js";
12
12
 
13
13
  // src/VoiceAgentProvider.tsx
14
14
  import { SiteConfigProvider } from "@unctad-ai/voice-agent-core";
@@ -1665,7 +1665,9 @@ function PipelineMetricsBar({
1665
1665
 
1666
1666
  // src/components/GlassCopilotPanel.tsx
1667
1667
  import { jsx as jsx9, jsxs as jsxs8 } from "react/jsx-runtime";
1668
- var VoiceSettingsView2 = lazy(() => import("./VoiceSettingsView-AMFLQL3Z.js"));
1668
+ var VoiceSettingsView2 = lazy(() => import("./VoiceSettingsView-QY7TEHYZ.js"));
1669
+ var RETRY_INITIAL_MS = 3e3;
1670
+ var RETRY_MAX_MS = 3e4;
1669
1671
  var STATE_LABELS = {
1670
1672
  IDLE: "Tap mic to speak",
1671
1673
  LISTENING: "Listening...",
@@ -1680,7 +1682,7 @@ var ARIA_LIVE_LABELS = {
1680
1682
  speaking: "Playing response",
1681
1683
  error: "An error occurred"
1682
1684
  };
1683
- function CopilotFAB({ onClick, portraitSrc }) {
1685
+ function CopilotFAB({ onClick, portraitSrc, isOffline = false }) {
1684
1686
  const { colors } = useSiteConfig4();
1685
1687
  return /* @__PURE__ */ jsx9(
1686
1688
  motion5.button,
@@ -1699,7 +1701,7 @@ function CopilotFAB({ onClick, portraitSrc }) {
1699
1701
  },
1700
1702
  "aria-label": "Open voice assistant",
1701
1703
  "data-testid": "voice-agent-fab",
1702
- children: /* @__PURE__ */ jsx9("div", { className: "agent-fab-border", style: { width: 54, height: 54, "--agent-primary": colors.primary }, children: /* @__PURE__ */ jsx9("div", { className: "agent-fab-border-inner", children: portraitSrc ? /* @__PURE__ */ jsx9(
1704
+ children: /* @__PURE__ */ jsx9("div", { className: "agent-fab-border", style: { width: 54, height: 54, "--agent-primary": isOffline ? "#9ca3af" : colors.primary, animation: isOffline ? "none" : void 0 }, children: /* @__PURE__ */ jsx9("div", { className: "agent-fab-border-inner", children: portraitSrc ? /* @__PURE__ */ jsx9(
1703
1705
  "img",
1704
1706
  {
1705
1707
  src: portraitSrc,
@@ -1736,6 +1738,7 @@ function CollapsedBar({
1736
1738
  onClose,
1737
1739
  onRetry,
1738
1740
  isRetrying = false,
1741
+ retryCountdown,
1739
1742
  voiceError,
1740
1743
  micPaused = false,
1741
1744
  onMicToggle,
@@ -1807,8 +1810,8 @@ function CollapsedBar({
1807
1810
  letterSpacing: "0.01em"
1808
1811
  },
1809
1812
  children: isOffline ? /* @__PURE__ */ jsxs8("span", { className: "inline-flex items-center gap-1", children: [
1810
- "Offline",
1811
- onRetry && /* @__PURE__ */ jsx9(
1813
+ retryCountdown ? `Retrying in ${retryCountdown}s` : "Offline",
1814
+ onRetry && !retryCountdown && /* @__PURE__ */ jsx9(
1812
1815
  "button",
1813
1816
  {
1814
1817
  onClick: (e) => {
@@ -1931,15 +1934,17 @@ function ComposerBar({
1931
1934
  }, [disabled]);
1932
1935
  useEffect6(() => {
1933
1936
  if (mode === "text") {
1934
- requestAnimationFrame(() => inputRef.current?.focus());
1937
+ const timer = setTimeout(() => inputRef.current?.focus(), 200);
1938
+ return () => clearTimeout(timer);
1935
1939
  }
1936
1940
  }, [mode]);
1937
1941
  const handleSubmit = () => {
1942
+ if (disabled) return;
1938
1943
  const trimmed = text.trim();
1939
1944
  if (!trimmed) return;
1940
1945
  onTextSubmit(trimmed);
1941
1946
  setText("");
1942
- if (!disabled) setMode("voice");
1947
+ requestAnimationFrame(() => inputRef.current?.focus());
1943
1948
  };
1944
1949
  const handleCancel = () => {
1945
1950
  setText("");
@@ -2062,6 +2067,7 @@ function ComposerBar({
2062
2067
  type: "text",
2063
2068
  value: text,
2064
2069
  onChange: (e) => setText(e.target.value),
2070
+ disabled,
2065
2071
  onKeyDown: (e) => {
2066
2072
  if (e.key === "Enter") handleSubmit();
2067
2073
  if (e.key === "Escape") handleCancel();
@@ -2080,7 +2086,7 @@ function ComposerBar({
2080
2086
  pill.style.boxShadow = "none";
2081
2087
  }
2082
2088
  },
2083
- placeholder: "Ask about services...",
2089
+ placeholder: disabled ? "Reconnecting..." : "Ask about services...",
2084
2090
  "aria-label": "Type your question",
2085
2091
  "data-testid": "voice-agent-input",
2086
2092
  className: "w-full",
@@ -2090,7 +2096,9 @@ function ComposerBar({
2090
2096
  background: "transparent",
2091
2097
  border: "none",
2092
2098
  outline: "none",
2093
- padding: 0
2099
+ padding: 0,
2100
+ opacity: disabled ? 0.5 : 1,
2101
+ cursor: disabled ? "not-allowed" : void 0
2094
2102
  }
2095
2103
  }
2096
2104
  )
@@ -2161,6 +2169,7 @@ function ExpandedContent({
2161
2169
  onInteraction,
2162
2170
  onRetry,
2163
2171
  isRetrying = false,
2172
+ retryCountdown,
2164
2173
  lastTimings,
2165
2174
  showPipelineMetrics,
2166
2175
  pipelineMetricsAutoHideMs,
@@ -2256,7 +2265,7 @@ function ExpandedContent({
2256
2265
  /* @__PURE__ */ jsx9(PipelineMetricsBar, { timings: lastTimings ?? null, show: showPipelineMetrics, autoHideMs: pipelineMetricsAutoHideMs }),
2257
2266
  isOffline && onRetry && /* @__PURE__ */ jsx9("div", { style: { padding: "0 16px 8px", display: "flex", justifyContent: "center" }, children: /* @__PURE__ */ jsxs8(motion5.button, { whileHover: { scale: 1.04 }, whileTap: { scale: 0.96 }, onClick: onRetry, disabled: isRetrying, className: "inline-flex items-center gap-2 rounded-full cursor-pointer transition-colors", style: { padding: "8px 18px", fontSize: "13px", fontWeight: 500, color: isRetrying ? "rgba(0,0,0,0.35)" : "rgba(220,38,38,0.8)", backgroundColor: isRetrying ? "rgba(0,0,0,0.04)" : "rgba(220,38,38,0.08)", border: "1px solid", borderColor: isRetrying ? "rgba(0,0,0,0.06)" : "rgba(220,38,38,0.15)" }, "aria-label": "Retry connection", children: [
2258
2267
  /* @__PURE__ */ jsx9(motion5.span, { animate: isRetrying ? { rotate: 360 } : { rotate: 0 }, transition: isRetrying ? { duration: 0.8, repeat: Infinity, ease: "linear" } : { duration: 0.3 }, style: { display: "flex", alignItems: "center", justifyContent: "center" }, children: /* @__PURE__ */ jsx9(RotateCw, { style: { width: 14, height: 14 } }) }),
2259
- isRetrying ? "Checking..." : "Retry connection"
2268
+ isRetrying ? "Checking..." : retryCountdown ? `Retrying in ${retryCountdown}s...` : "Retry connection"
2260
2269
  ] }) }),
2261
2270
  voiceError !== "network_error" && /* @__PURE__ */ jsx9("div", { style: { padding: "0 16px 8px" }, children: /* @__PURE__ */ jsx9(VoiceErrorDisplay, { error: voiceError, onDismiss: dismissError }) }),
2262
2271
  /* @__PURE__ */ jsx9("div", { style: { padding: "0 16px 8px" }, children: /* @__PURE__ */ jsx9(VoiceToolCard, { result: toolResult, onDismiss: onToolDismiss, variant: "capsule" }) })
@@ -2344,28 +2353,70 @@ function WiredPanelInner({
2344
2353
  const autoStartedRef = useRef4(false);
2345
2354
  const pollTimerRef = useRef4(null);
2346
2355
  const cancelledRef = useRef4(false);
2356
+ const [retryCountdown, setRetryCountdown] = useState5(null);
2357
+ const backoffDelayRef = useRef4(RETRY_INITIAL_MS);
2358
+ const countdownTimerRef = useRef4(null);
2359
+ const dismissErrorRef = useRef4(dismissError);
2360
+ useEffect6(() => {
2361
+ dismissErrorRef.current = dismissError;
2362
+ }, [dismissError]);
2363
+ const autoListenRef = useRef4(settings.autoListen);
2364
+ useEffect6(() => {
2365
+ autoListenRef.current = settings.autoListen;
2366
+ }, [settings.autoListen]);
2367
+ const clearCountdown = useCallback3(() => {
2368
+ if (countdownTimerRef.current) {
2369
+ clearInterval(countdownTimerRef.current);
2370
+ countdownTimerRef.current = null;
2371
+ }
2372
+ setRetryCountdown(null);
2373
+ }, []);
2374
+ const runHealthCheckRef = useRef4(() => {
2375
+ });
2376
+ const scheduleRetry = useCallback3((delayMs) => {
2377
+ if (pollTimerRef.current) clearTimeout(pollTimerRef.current);
2378
+ clearCountdown();
2379
+ const seconds = Math.ceil(delayMs / 1e3);
2380
+ setRetryCountdown(seconds);
2381
+ let remaining = seconds;
2382
+ countdownTimerRef.current = setInterval(() => {
2383
+ remaining -= 1;
2384
+ if (remaining <= 0) {
2385
+ clearCountdown();
2386
+ return;
2387
+ }
2388
+ setRetryCountdown(remaining);
2389
+ }, 1e3);
2390
+ pollTimerRef.current = setTimeout(() => runHealthCheckRef.current(), delayMs);
2391
+ backoffDelayRef.current = Math.min(delayMs * 2, RETRY_MAX_MS);
2392
+ }, [clearCountdown]);
2347
2393
  const runHealthCheck = useCallback3(() => {
2394
+ clearCountdown();
2348
2395
  checkBackendHealth().then(({ available }) => {
2349
2396
  if (cancelledRef.current) return;
2350
2397
  setBackendDown(!available);
2351
2398
  if (available) {
2352
- dismissError();
2353
- if (!autoStartedRef.current && settings.autoListen) {
2399
+ backoffDelayRef.current = RETRY_INITIAL_MS;
2400
+ dismissErrorRef.current();
2401
+ if (!autoStartedRef.current && autoListenRef.current) {
2354
2402
  autoStartedRef.current = true;
2355
2403
  startRef.current();
2356
2404
  }
2357
2405
  } else {
2358
- if (pollTimerRef.current) clearTimeout(pollTimerRef.current);
2359
- pollTimerRef.current = setTimeout(runHealthCheck, RECOVERY_POLL_MS);
2406
+ scheduleRetry(backoffDelayRef.current);
2360
2407
  }
2361
2408
  });
2362
- }, [dismissError, settings.autoListen]);
2409
+ }, [scheduleRetry, clearCountdown]);
2410
+ useEffect6(() => {
2411
+ runHealthCheckRef.current = runHealthCheck;
2412
+ }, [runHealthCheck]);
2363
2413
  useEffect6(() => {
2364
2414
  cancelledRef.current = false;
2365
2415
  runHealthCheck();
2366
2416
  return () => {
2367
2417
  cancelledRef.current = true;
2368
2418
  if (pollTimerRef.current) clearTimeout(pollTimerRef.current);
2419
+ if (countdownTimerRef.current) clearInterval(countdownTimerRef.current);
2369
2420
  };
2370
2421
  }, [runHealthCheck]);
2371
2422
  const onStateChangeRef = useRef4(onStateChange);
@@ -2443,23 +2494,23 @@ function WiredPanelInner({
2443
2494
  const handleRetryClick = useCallback3(() => {
2444
2495
  if (isRetrying) return;
2445
2496
  setIsRetrying(true);
2497
+ backoffDelayRef.current = RETRY_INITIAL_MS;
2498
+ clearCountdown();
2446
2499
  checkBackendHealth().then(({ available }) => {
2447
2500
  if (cancelledRef.current) return;
2448
2501
  setIsRetrying(false);
2449
2502
  setBackendDown(!available);
2450
2503
  if (available) {
2451
- dismissError();
2452
- if (!autoStartedRef.current && settings.autoListen) {
2504
+ dismissErrorRef.current();
2505
+ if (!autoStartedRef.current && autoListenRef.current) {
2453
2506
  autoStartedRef.current = true;
2454
2507
  startRef.current();
2455
2508
  }
2456
- }
2457
- if (!available) {
2458
- if (pollTimerRef.current) clearTimeout(pollTimerRef.current);
2459
- pollTimerRef.current = setTimeout(runHealthCheck, RECOVERY_POLL_MS);
2509
+ } else {
2510
+ scheduleRetry(backoffDelayRef.current);
2460
2511
  }
2461
2512
  }).catch(() => setIsRetrying(false));
2462
- }, [isRetrying, runHealthCheck, dismissError, settings.autoListen]);
2513
+ }, [isRetrying, clearCountdown, scheduleRetry]);
2463
2514
  const onExpandRef = useRef4(onExpand);
2464
2515
  useEffect6(() => {
2465
2516
  onExpandRef.current = onExpand;
@@ -2476,10 +2527,10 @@ function WiredPanelInner({
2476
2527
  const [showSettings, setShowSettings] = useState5(false);
2477
2528
  const toggleSettings = useCallback3(() => setShowSettings((p) => !p), []);
2478
2529
  if (panelState === "collapsed") {
2479
- return /* @__PURE__ */ jsx9(CollapsedBar, { orbState, getAmplitude, analyser, voiceState: state, onExpand, onClose, onRetry: handleRetryClick, isRetrying, voiceError: effectiveError, micPaused, onMicToggle: handleMicToggle, ttsEnabled: settings.ttsEnabled, copilotName: config.copilotName, portraitSrc: resolvedPortrait });
2530
+ return /* @__PURE__ */ jsx9(CollapsedBar, { orbState, getAmplitude, analyser, voiceState: state, onExpand, onClose, onRetry: handleRetryClick, isRetrying, retryCountdown, voiceError: effectiveError, micPaused, onMicToggle: handleMicToggle, ttsEnabled: settings.ttsEnabled, copilotName: config.copilotName, portraitSrc: resolvedPortrait });
2480
2531
  }
2481
2532
  return /* @__PURE__ */ jsxs8("div", { className: "relative h-full", children: [
2482
- /* @__PURE__ */ jsx9(ExpandedContent, { orbState, getAmplitude, analyser, voiceState: state, messages, isTyping, toolResult, voiceError: effectiveError, dismissError, onCollapse, onClose, onTextSubmit: handleTextSubmit, onMicToggle: handleMicToggle, micPaused, onToolDismiss: () => setToolResult(null), onInteraction: bumpActivity, onRetry: handleRetryClick, isRetrying, lastTimings, showPipelineMetrics: settings.showPipelineMetrics, pipelineMetricsAutoHideMs: settings.pipelineMetricsAutoHideMs, showSettings, onSettingsToggle: toggleSettings, ttsEnabled: settings.ttsEnabled, copilotName: config.copilotName, portraitSrc: resolvedPortrait, onStartMic: handleMicToggle, onSwitchToKeyboard: handleSwitchToKeyboard, switchToTextRef }),
2533
+ /* @__PURE__ */ jsx9(ExpandedContent, { orbState, getAmplitude, analyser, voiceState: state, messages, isTyping, toolResult, voiceError: effectiveError, dismissError, onCollapse, onClose, onTextSubmit: handleTextSubmit, onMicToggle: handleMicToggle, micPaused, onToolDismiss: () => setToolResult(null), onInteraction: bumpActivity, onRetry: handleRetryClick, isRetrying, retryCountdown, lastTimings, showPipelineMetrics: settings.showPipelineMetrics, pipelineMetricsAutoHideMs: settings.pipelineMetricsAutoHideMs, showSettings, onSettingsToggle: toggleSettings, ttsEnabled: settings.ttsEnabled, copilotName: config.copilotName, portraitSrc: resolvedPortrait, onStartMic: handleMicToggle, onSwitchToKeyboard: handleSwitchToKeyboard, switchToTextRef }),
2483
2534
  /* @__PURE__ */ jsx9(AnimatePresence5, { children: showSettings && /* @__PURE__ */ jsx9(Suspense, { fallback: null, children: /* @__PURE__ */ jsx9(VoiceSettingsView2, { onBack: toggleSettings, onVolumeChange: applyVolume }) }) })
2484
2535
  ] });
2485
2536
  }
@@ -2506,6 +2557,22 @@ function GlassCopilotPanel({ isOpen: isOpenProp, onOpen: onOpenProp, onClose: on
2506
2557
  const handleExpand = useCallback3(() => {
2507
2558
  setInternalState("expanded");
2508
2559
  }, []);
2560
+ const [fabOffline, setFabOffline] = useState5(false);
2561
+ useEffect6(() => {
2562
+ if (isOpen) return;
2563
+ let cancelled = false;
2564
+ const check = () => {
2565
+ checkBackendHealth().then(({ available }) => {
2566
+ if (!cancelled) setFabOffline(!available);
2567
+ });
2568
+ };
2569
+ check();
2570
+ const timer = setInterval(check, RECOVERY_POLL_MS);
2571
+ return () => {
2572
+ cancelled = true;
2573
+ clearInterval(timer);
2574
+ };
2575
+ }, [isOpen]);
2509
2576
  const isVisible = panelState !== "hidden";
2510
2577
  const isExpanded = panelState === "expanded";
2511
2578
  const targetHeight = isExpanded ? PANEL_EXPANDED_HEIGHT : PANEL_COLLAPSED_HEIGHT;
@@ -2537,7 +2604,7 @@ function GlassCopilotPanel({ isOpen: isOpenProp, onOpen: onOpenProp, onClose: on
2537
2604
  ] }) }) }),
2538
2605
  /* @__PURE__ */ jsx9("span", { "aria-live": "polite", "aria-atomic": "true", style: { position: "absolute", width: 1, height: 1, padding: 0, margin: -1, overflow: "hidden", clip: "rect(0,0,0,0)", whiteSpace: "nowrap", borderWidth: 0 }, children: ariaAnnouncement }),
2539
2606
  /* @__PURE__ */ jsxs8(AnimatePresence5, { children: [
2540
- !isVisible && /* @__PURE__ */ jsx9(motion5.div, { ref: fabRef, initial: { scale: 0, opacity: 0 }, animate: { scale: 1, opacity: 1 }, exit: { scale: 0, opacity: 0 }, transition: { duration: 0.25, ease: "easeOut" }, className: "fixed", style: { bottom: PANEL_BOTTOM, right: PANEL_RIGHT, zIndex: PANEL_Z_INDEX }, children: /* @__PURE__ */ jsx9(CopilotFAB, { onClick: handleOpen, portraitSrc: resolvedPortrait }) }, "copilot-fab"),
2607
+ !isVisible && /* @__PURE__ */ jsx9(motion5.div, { ref: fabRef, initial: { scale: 0, opacity: 0 }, animate: { scale: 1, opacity: 1 }, exit: { scale: 0, opacity: 0 }, transition: { duration: 0.25, ease: "easeOut" }, className: "fixed", style: { bottom: PANEL_BOTTOM, right: PANEL_RIGHT, zIndex: PANEL_Z_INDEX }, children: /* @__PURE__ */ jsx9(CopilotFAB, { onClick: handleOpen, portraitSrc: resolvedPortrait, isOffline: fabOffline }) }, "copilot-fab"),
2541
2608
  isVisible && /* @__PURE__ */ jsxs8(
2542
2609
  motion5.div,
2543
2610
  {