@unctad-ai/voice-agent-ui 5.1.2 → 5.2.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.
@@ -5,7 +5,7 @@ import {
5
5
  SliderSetting,
6
6
  ToggleSetting,
7
7
  VoiceSettingsView
8
- } from "./chunk-OUTD6CWN.js";
8
+ } from "./chunk-IMDRPZ6B.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-DAYBNBVA.js.map
17
+ //# sourceMappingURL=VoiceSettingsView-OPMI5HCP.js.map
@@ -1902,7 +1902,7 @@ function VoiceSettingsView({ onBack, onVolumeChange }) {
1902
1902
  ) : /* @__PURE__ */ jsx3("span", {}),
1903
1903
  /* @__PURE__ */ jsxs2("span", { children: [
1904
1904
  "Kit v",
1905
- /* @__PURE__ */ jsx3("span", { style: { fontWeight: 500, color: "#6b7280" }, children: "5.1.2" })
1905
+ /* @__PURE__ */ jsx3("span", { style: { fontWeight: 500, color: "#6b7280" }, children: "5.2.0" })
1906
1906
  ] })
1907
1907
  ] }) })
1908
1908
  ]
@@ -1994,4 +1994,4 @@ export {
1994
1994
  SettingsSection,
1995
1995
  Divider
1996
1996
  };
1997
- //# sourceMappingURL=chunk-OUTD6CWN.js.map
1997
+ //# sourceMappingURL=chunk-IMDRPZ6B.js.map
package/dist/index.js CHANGED
@@ -8,7 +8,7 @@ import {
8
8
  VoiceSettingsProvider,
9
9
  VoiceSettingsView,
10
10
  useVoiceSettings
11
- } from "./chunk-OUTD6CWN.js";
11
+ } from "./chunk-IMDRPZ6B.js";
12
12
 
13
13
  // src/VoiceAgentProvider.tsx
14
14
  import { SiteConfigProvider } from "@unctad-ai/voice-agent-core";
@@ -32,7 +32,7 @@ import {
32
32
  Component as Component2
33
33
  } from "react";
34
34
  import { createPortal } from "react-dom";
35
- import { motion as motion5, AnimatePresence as AnimatePresence5 } from "motion/react";
35
+ import { motion as motion5, AnimatePresence as AnimatePresence5, useReducedMotion as useReducedMotion2 } from "motion/react";
36
36
  import { ChevronDown as ChevronDown2, X, Mic as Mic3, ArrowUp, Keyboard as Keyboard2, RotateCw, Settings, VolumeX as VolumeX2, Flag as Flag2 } from "lucide-react";
37
37
  import {
38
38
  useVoiceAgent,
@@ -1764,7 +1764,7 @@ function PipelineMetricsBar({
1764
1764
 
1765
1765
  // src/components/GlassCopilotPanel.tsx
1766
1766
  import { jsx as jsx9, jsxs as jsxs8 } from "react/jsx-runtime";
1767
- var VoiceSettingsView2 = lazy(() => import("./VoiceSettingsView-DAYBNBVA.js"));
1767
+ var VoiceSettingsView2 = lazy(() => import("./VoiceSettingsView-OPMI5HCP.js"));
1768
1768
  var RETRY_INITIAL_MS = 3e3;
1769
1769
  var RETRY_MAX_MS = 3e4;
1770
1770
  var STATE_LABELS = {
@@ -1795,12 +1795,12 @@ function CopilotFAB({ onClick, portraitSrc, isOffline = false }) {
1795
1795
  border: "none",
1796
1796
  padding: 0,
1797
1797
  borderRadius: "50%",
1798
- width: 54,
1799
- height: 54
1798
+ width: 68,
1799
+ height: 68
1800
1800
  },
1801
1801
  "aria-label": "Open voice assistant",
1802
1802
  "data-testid": "voice-agent-fab",
1803
- 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(
1803
+ children: /* @__PURE__ */ jsx9("div", { className: "agent-fab-border", style: { width: 68, height: 68, "--agent-primary": isOffline ? "#9ca3af" : colors.primary, animation: isOffline ? "none" : void 0 }, children: /* @__PURE__ */ jsx9("div", { className: "agent-fab-border-inner", children: portraitSrc ? /* @__PURE__ */ jsx9(
1804
1804
  "img",
1805
1805
  {
1806
1806
  src: portraitSrc,
@@ -1819,7 +1819,7 @@ function CopilotFAB({ onClick, portraitSrc, isOffline = false }) {
1819
1819
  justifyContent: "center",
1820
1820
  backgroundColor: "#6b7280",
1821
1821
  color: "white",
1822
- fontSize: 20,
1822
+ fontSize: 24,
1823
1823
  fontWeight: 700
1824
1824
  },
1825
1825
  children: "AI"
@@ -1828,6 +1828,176 @@ function CopilotFAB({ onClick, portraitSrc, isOffline = false }) {
1828
1828
  }
1829
1829
  );
1830
1830
  }
1831
+ var FAB_GREETED_KEY = (name) => `voice-fab-greeted:${name}`;
1832
+ var FAB_LAST_SHOWN_KEY = (name) => `voice-fab-last-shown:${name}`;
1833
+ var FAB_COOLDOWN_MS = 30 * 60 * 1e3;
1834
+ var FAB_TOOLTIP_NARROW_QUERY = "(max-width: 479px)";
1835
+ function safeStorage(op) {
1836
+ try {
1837
+ return op();
1838
+ } catch {
1839
+ return null;
1840
+ }
1841
+ }
1842
+ function CopilotFABTooltip({ onClick }) {
1843
+ const { copilotName, colors, fabTooltip } = useSiteConfig4();
1844
+ const prefersReduced = useReducedMotion2();
1845
+ const [visible, setVisible] = useState5(false);
1846
+ const [dismissed, setDismissed] = useState5(false);
1847
+ const [isFirstVisit] = useState5(() => !safeStorage(() => localStorage.getItem(FAB_GREETED_KEY(copilotName))));
1848
+ useEffect6(() => {
1849
+ if (typeof window === "undefined") return;
1850
+ const mql = window.matchMedia(FAB_TOOLTIP_NARROW_QUERY);
1851
+ if (mql.matches) {
1852
+ setDismissed(true);
1853
+ return;
1854
+ }
1855
+ const handler = (e) => {
1856
+ if (e.matches) setDismissed(true);
1857
+ };
1858
+ mql.addEventListener("change", handler);
1859
+ return () => mql.removeEventListener("change", handler);
1860
+ }, []);
1861
+ useEffect6(() => {
1862
+ if (isFirstVisit) {
1863
+ const timer2 = setTimeout(() => {
1864
+ setVisible(true);
1865
+ safeStorage(() => localStorage.setItem(FAB_GREETED_KEY(copilotName), "true"));
1866
+ safeStorage(() => localStorage.setItem(FAB_LAST_SHOWN_KEY(copilotName), (/* @__PURE__ */ new Date()).toISOString()));
1867
+ }, 2e3);
1868
+ return () => clearTimeout(timer2);
1869
+ }
1870
+ const lastShown = safeStorage(() => localStorage.getItem(FAB_LAST_SHOWN_KEY(copilotName)));
1871
+ if (lastShown && Date.now() - new Date(lastShown).getTime() < FAB_COOLDOWN_MS) return;
1872
+ const timer = setTimeout(() => {
1873
+ setVisible(true);
1874
+ safeStorage(() => localStorage.setItem(FAB_LAST_SHOWN_KEY(copilotName), (/* @__PURE__ */ new Date()).toISOString()));
1875
+ }, 5e3);
1876
+ return () => clearTimeout(timer);
1877
+ }, [copilotName, isFirstVisit]);
1878
+ useEffect6(() => {
1879
+ if (!visible || isFirstVisit) return;
1880
+ const timer = setTimeout(() => setDismissed(true), 5e3);
1881
+ return () => clearTimeout(timer);
1882
+ }, [visible, isFirstVisit]);
1883
+ useEffect6(() => {
1884
+ if (!visible || dismissed || isFirstVisit) return;
1885
+ const handleScroll = () => setDismissed(true);
1886
+ window.addEventListener("scroll", handleScroll, { passive: true, once: true });
1887
+ return () => window.removeEventListener("scroll", handleScroll);
1888
+ }, [visible, dismissed, isFirstVisit]);
1889
+ const handleTryNow = () => {
1890
+ setDismissed(true);
1891
+ onClick();
1892
+ };
1893
+ const handleDismiss = (e) => {
1894
+ e.stopPropagation();
1895
+ setDismissed(true);
1896
+ };
1897
+ const firstVisitText = fabTooltip?.firstVisit ? fabTooltip.firstVisit.replaceAll("{name}", copilotName) : `I'm ${copilotName}, your virtual civil servant.
1898
+ How may I help you?`;
1899
+ const returnVisitText = fabTooltip?.returnVisit ?? "How may I help you?";
1900
+ const show = visible && !dismissed;
1901
+ return /* @__PURE__ */ jsx9(AnimatePresence5, { children: show && /* @__PURE__ */ jsxs8(
1902
+ motion5.div,
1903
+ {
1904
+ role: "status",
1905
+ "aria-live": "polite",
1906
+ "data-testid": "voice-agent-fab-tooltip",
1907
+ initial: prefersReduced ? { opacity: 0 } : { opacity: 0, x: 20 },
1908
+ animate: prefersReduced ? { opacity: 1 } : { opacity: 1, x: 0 },
1909
+ exit: prefersReduced ? { opacity: 0 } : { opacity: 0, x: 10 },
1910
+ transition: prefersReduced ? { duration: 0.2 } : { type: "spring", stiffness: 300, damping: 25 },
1911
+ onClick: isFirstVisit ? void 0 : handleTryNow,
1912
+ style: {
1913
+ display: "flex",
1914
+ alignItems: "center",
1915
+ gap: 0,
1916
+ cursor: isFirstVisit ? "default" : "pointer"
1917
+ },
1918
+ children: [
1919
+ /* @__PURE__ */ jsxs8(
1920
+ "div",
1921
+ {
1922
+ style: {
1923
+ background: "white",
1924
+ borderRadius: 20,
1925
+ padding: isFirstVisit ? "12px 20px" : "10px 18px",
1926
+ boxShadow: "0 2px 12px rgba(0,0,0,0.10), 0 0 0 1px rgba(0,0,0,0.04)",
1927
+ maxWidth: 340
1928
+ },
1929
+ children: [
1930
+ /* @__PURE__ */ jsx9(
1931
+ "div",
1932
+ {
1933
+ style: {
1934
+ fontSize: isFirstVisit ? 14 : 13,
1935
+ color: "#1a1a1a",
1936
+ fontWeight: 500,
1937
+ lineHeight: 1.4,
1938
+ whiteSpace: "pre-line"
1939
+ },
1940
+ children: isFirstVisit ? firstVisitText : returnVisitText
1941
+ }
1942
+ ),
1943
+ isFirstVisit && /* @__PURE__ */ jsxs8("div", { style: { display: "flex", gap: 8, marginTop: 8 }, children: [
1944
+ /* @__PURE__ */ jsx9(
1945
+ "button",
1946
+ {
1947
+ "data-testid": "voice-agent-fab-tooltip-cta",
1948
+ "aria-label": `Open ${copilotName} voice assistant`,
1949
+ onClick: handleTryNow,
1950
+ style: {
1951
+ padding: "6px 16px",
1952
+ background: colors.primary,
1953
+ color: "white",
1954
+ border: "none",
1955
+ borderRadius: 12,
1956
+ fontSize: 12,
1957
+ fontWeight: 600,
1958
+ cursor: "pointer"
1959
+ },
1960
+ children: "Try it now"
1961
+ }
1962
+ ),
1963
+ /* @__PURE__ */ jsx9(
1964
+ "button",
1965
+ {
1966
+ onClick: handleDismiss,
1967
+ style: {
1968
+ padding: "6px 12px",
1969
+ background: "transparent",
1970
+ color: "#6b7280",
1971
+ border: "none",
1972
+ borderRadius: 12,
1973
+ fontSize: 12,
1974
+ cursor: "pointer"
1975
+ },
1976
+ children: "Maybe later"
1977
+ }
1978
+ )
1979
+ ] })
1980
+ ]
1981
+ }
1982
+ ),
1983
+ /* @__PURE__ */ jsx9(
1984
+ "div",
1985
+ {
1986
+ style: {
1987
+ width: 0,
1988
+ height: 0,
1989
+ borderTop: "8px solid transparent",
1990
+ borderBottom: "8px solid transparent",
1991
+ borderLeft: "8px solid white",
1992
+ flexShrink: 0,
1993
+ filter: "drop-shadow(2px 0 1px rgba(0,0,0,0.06))"
1994
+ }
1995
+ }
1996
+ )
1997
+ ]
1998
+ }
1999
+ ) });
2000
+ }
1831
2001
  function CollapsedBar({
1832
2002
  orbState,
1833
2003
  getAmplitude,
@@ -2673,19 +2843,30 @@ function WiredPanelInner({
2673
2843
  useEffect6(() => {
2674
2844
  if (panelState === "hidden") stopRef.current(true);
2675
2845
  }, [panelState]);
2846
+ const micOpen = state === "LISTENING" || state === "USER_SPEAKING";
2847
+ const stateRef = useRef4(state);
2848
+ stateRef.current = state;
2676
2849
  useEffect6(() => {
2677
2850
  if (panelState === "hidden" || micPaused || showSettings) return;
2678
- if (state === "PROCESSING" || state === "AI_SPEAKING") return;
2679
- const timer = setTimeout(() => {
2680
- stopRef.current();
2681
- if (hasConversationRef.current) {
2682
- setMicPaused(true);
2683
- } else {
2684
- onCollapseRef.current();
2685
- }
2686
- }, settings.idleTimeoutMs);
2851
+ if (!micOpen) return;
2852
+ let timer;
2853
+ const schedule = () => {
2854
+ timer = setTimeout(() => {
2855
+ if (stateRef.current === "USER_SPEAKING") {
2856
+ schedule();
2857
+ return;
2858
+ }
2859
+ stopRef.current();
2860
+ if (hasConversationRef.current) {
2861
+ setMicPaused(true);
2862
+ } else {
2863
+ onCollapseRef.current();
2864
+ }
2865
+ }, settings.idleTimeoutMs);
2866
+ };
2867
+ schedule();
2687
2868
  return () => clearTimeout(timer);
2688
- }, [state, panelState, micPaused, showSettings, activity, settings.idleTimeoutMs]);
2869
+ }, [micOpen, panelState, micPaused, showSettings, activity, settings.idleTimeoutMs]);
2689
2870
  useEffect6(() => {
2690
2871
  if (!micPaused || panelState !== "expanded" || showSettings) return;
2691
2872
  if (settings.panelCollapseTimeoutMs === 0) return;
@@ -2721,7 +2902,7 @@ function WiredPanelInner({
2721
2902
  timings: lastTimings ?? void 0,
2722
2903
  route: window.location.pathname,
2723
2904
  copilotName: config.copilotName,
2724
- kitVersion: "5.1.2"
2905
+ kitVersion: "5.2.0"
2725
2906
  })
2726
2907
  });
2727
2908
  const body = await res.json().catch(() => ({ ticketId: void 0 }));
@@ -2859,7 +3040,10 @@ function GlassCopilotPanel({ isOpen: isOpenProp, onOpen: onOpenProp, onClose: on
2859
3040
  ] }) }) }),
2860
3041
  /* @__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 }),
2861
3042
  /* @__PURE__ */ jsxs8(AnimatePresence5, { children: [
2862
- !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"),
3043
+ !isVisible && /* @__PURE__ */ jsxs8(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, display: "flex", alignItems: "flex-end", gap: 12 }, children: [
3044
+ /* @__PURE__ */ jsx9(CopilotFABTooltip, { onClick: handleOpen }),
3045
+ /* @__PURE__ */ jsx9(CopilotFAB, { onClick: handleOpen, portraitSrc: resolvedPortrait, isOffline: fabOffline })
3046
+ ] }, "copilot-fab"),
2863
3047
  isVisible && /* @__PURE__ */ jsxs8(
2864
3048
  motion5.div,
2865
3049
  {
@@ -2869,9 +3053,9 @@ function GlassCopilotPanel({ isOpen: isOpenProp, onOpen: onOpenProp, onClose: on
2869
3053
  "aria-label": "Voice Assistant",
2870
3054
  "aria-modal": "false",
2871
3055
  "data-testid": "voice-agent-panel",
2872
- initial: { width: 48, height: 48, borderRadius: 24, opacity: 0, scale: 0.9 },
3056
+ initial: { width: 68, height: 68, borderRadius: 34, opacity: 0, scale: 0.9 },
2873
3057
  animate: { width: PANEL_WIDTH, height: Math.min(targetHeight, window.innerHeight - 48), borderRadius: PANEL_BORDER_RADIUS, opacity: 1, scale: 1, transition: SPRING_PANEL },
2874
- exit: { width: 48, height: 48, borderRadius: 24, opacity: 0, scale: 0.95, transition: SPRING_PANEL_EXIT },
3058
+ exit: { width: 68, height: 68, borderRadius: 34, opacity: 0, scale: 0.95, transition: SPRING_PANEL_EXIT },
2875
3059
  className: "fixed",
2876
3060
  style: { bottom: PANEL_BOTTOM, right: PANEL_RIGHT, zIndex: PANEL_Z_INDEX, transformOrigin: "bottom right", maxWidth: "calc(100vw - 32px)", outline: "none", fontFamily: config.fontFamily ?? DEFAULT_FONT_FAMILY2 },
2877
3061
  children: [
@@ -2928,7 +3112,7 @@ function VoiceA11yAnnouncer({ isOpen, orbState }) {
2928
3112
  }
2929
3113
 
2930
3114
  // src/components/VoiceCopilotFAB.tsx
2931
- import { motion as motion6, AnimatePresence as AnimatePresence6, useReducedMotion as useReducedMotion2 } from "motion/react";
3115
+ import { motion as motion6, AnimatePresence as AnimatePresence6, useReducedMotion as useReducedMotion3 } from "motion/react";
2932
3116
  import { Mic as Mic4 } from "lucide-react";
2933
3117
  import { useSiteConfig as useSiteConfig5 } from "@unctad-ai/voice-agent-core";
2934
3118
  import { jsx as jsx11 } from "react/jsx-runtime";
@@ -2938,7 +3122,7 @@ function VoiceCopilotFAB({
2938
3122
  isOverlayOpen,
2939
3123
  onMouseEnter
2940
3124
  }) {
2941
- const prefersReduced = useReducedMotion2();
3125
+ const prefersReduced = useReducedMotion3();
2942
3126
  const { colors } = useSiteConfig5();
2943
3127
  return /* @__PURE__ */ jsx11(AnimatePresence6, { children: !isOverlayOpen && /* @__PURE__ */ jsx11(
2944
3128
  motion6.div,