@unctad-ai/voice-agent-ui 5.1.1 → 5.1.3

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-2Y43PT5D.js";
8
+ } from "./chunk-LOZIYINE.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-2CCGBBY4.js.map
17
+ //# sourceMappingURL=VoiceSettingsView-UL3TSETS.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.1" })
1905
+ /* @__PURE__ */ jsx3("span", { style: { fontWeight: 500, color: "#6b7280" }, children: "5.1.3" })
1906
1906
  ] })
1907
1907
  ] }) })
1908
1908
  ]
@@ -1994,4 +1994,4 @@ export {
1994
1994
  SettingsSection,
1995
1995
  Divider
1996
1996
  };
1997
- //# sourceMappingURL=chunk-2Y43PT5D.js.map
1997
+ //# sourceMappingURL=chunk-LOZIYINE.js.map
package/dist/index.d.ts CHANGED
@@ -225,10 +225,10 @@ interface VoiceTranscriptProps {
225
225
  onSwitchToKeyboard?: () => void;
226
226
  /** Callback when user reports a bad assistant response */
227
227
  onReport?: (turnNumber: number, assistantMessage: string, userMessage?: string) => void;
228
- /** Turn number of the most recently sent feedback (shows "Sent" confirmation) */
229
- feedbackSentTurn?: number | null;
228
+ /** Map of turn numbers to ticket IDs for sent feedback */
229
+ feedbackSentTurns?: Record<number, string>;
230
230
  }
231
- declare function VoiceTranscript({ messages, isTyping, variant, voiceError, voiceState, onStartMic, onSwitchToKeyboard, onReport, feedbackSentTurn, }: VoiceTranscriptProps): react_jsx_runtime.JSX.Element;
231
+ declare function VoiceTranscript({ messages, isTyping, variant, voiceError, voiceState, onStartMic, onSwitchToKeyboard, onReport, feedbackSentTurns, }: VoiceTranscriptProps): react_jsx_runtime.JSX.Element;
232
232
 
233
233
  interface VoiceWaveformCanvasProps {
234
234
  analyserNode: AnalyserNode | null;
package/dist/index.js CHANGED
@@ -8,7 +8,7 @@ import {
8
8
  VoiceSettingsProvider,
9
9
  VoiceSettingsView,
10
10
  useVoiceSettings
11
- } from "./chunk-2Y43PT5D.js";
11
+ } from "./chunk-LOZIYINE.js";
12
12
 
13
13
  // src/VoiceAgentProvider.tsx
14
14
  import { SiteConfigProvider } from "@unctad-ai/voice-agent-core";
@@ -665,8 +665,8 @@ import {
665
665
  DEFAULT_FONT_FAMILY,
666
666
  useSiteConfig as useSiteConfig3
667
667
  } from "@unctad-ai/voice-agent-core";
668
- import { ArrowRight, PenLine, MousePointerClick, Search, Info, ChevronDown, Mic, Keyboard, Flag } from "lucide-react";
669
- import { jsx as jsx4, jsxs as jsxs3 } from "react/jsx-runtime";
668
+ import { ArrowRight, PenLine, MousePointerClick, Search, Info, ChevronDown, Mic, Keyboard, Flag, Copy, Check } from "lucide-react";
669
+ import { Fragment, jsx as jsx4, jsxs as jsxs3 } from "react/jsx-runtime";
670
670
  function cleanForDisplay(text) {
671
671
  return text.replace(/\[(laugh|chuckle|sigh|gasp|cough|clear throat|sniff|groan|shush)\]/gi, "").replace(/\*\*(.*?)\*\*/g, "$1").replace(/^\s*\*\s+/gm, "- ").replace(/\*(.*?)\*/g, "$1").replace(/`([^`]+)`/g, "$1").replace(/^#{1,6}\s+/gm, "").replace(/<br\s*\/?>/gi, "\n").replace(/<[^>]+>/g, "").replace(/\|[-:\s|]+\|/g, "").replace(/\|/g, "\n").replace(/\p{Emoji_Presentation}|\p{Extended_Pictographic}/gu, "").replace(/[ \t]{3,}/g, " ").replace(/\n{3,}/g, "\n\n").replace(/^[\s-]{3,}$/gm, "").trim();
672
672
  }
@@ -834,6 +834,80 @@ function isDisplayWorthy(msg) {
834
834
  if (alphaOnly.length === 0) return false;
835
835
  return true;
836
836
  }
837
+ function FeedbackPill({ isSent, ticketId, onReport }) {
838
+ const [copied, setCopied] = useState3(false);
839
+ const [showTicket, setShowTicket] = useState3(false);
840
+ useEffect3(() => {
841
+ if (!isSent) {
842
+ setShowTicket(false);
843
+ return;
844
+ }
845
+ const timer = setTimeout(() => setShowTicket(true), 2e3);
846
+ return () => clearTimeout(timer);
847
+ }, [isSent]);
848
+ const phase = !isSent ? "idle" : showTicket ? "ticket" : "sent";
849
+ return /* @__PURE__ */ jsx4(
850
+ "span",
851
+ {
852
+ role: phase === "idle" ? "button" : void 0,
853
+ onClick: phase === "idle" ? onReport : void 0,
854
+ className: "voice-feedback-pill",
855
+ style: {
856
+ marginTop: 2,
857
+ fontSize: 11,
858
+ padding: "2px 8px",
859
+ borderRadius: 8,
860
+ border: phase === "sent" ? "1px solid rgba(22,163,74,0.3)" : phase === "ticket" ? "1px solid rgba(217,119,6,0.3)" : "1px solid rgba(0,0,0,0.12)",
861
+ background: "transparent",
862
+ color: phase === "sent" ? "#16a34a" : phase === "ticket" ? "#92400e" : "rgba(0,0,0,0.30)",
863
+ cursor: phase === "idle" ? "pointer" : "default",
864
+ display: "inline-flex",
865
+ alignItems: "center",
866
+ gap: 3,
867
+ opacity: isSent ? 1 : 0,
868
+ transition: "opacity 0.15s, color 0.4s, border-color 0.4s",
869
+ fontFamily: phase === "ticket" ? "monospace" : "inherit"
870
+ },
871
+ children: phase === "sent" ? /* @__PURE__ */ jsxs3(Fragment, { children: [
872
+ /* @__PURE__ */ jsx4(Check, { size: 10, strokeWidth: 2 }),
873
+ "Sent"
874
+ ] }) : phase === "ticket" && ticketId ? /* @__PURE__ */ jsxs3(Fragment, { children: [
875
+ "Ticket ",
876
+ ticketId,
877
+ /* @__PURE__ */ jsx4(
878
+ "span",
879
+ {
880
+ role: "button",
881
+ "aria-label": "Copy ticket ID",
882
+ onClick: () => {
883
+ navigator.clipboard.writeText(ticketId).then(() => {
884
+ setCopied(true);
885
+ setTimeout(() => setCopied(false), 1500);
886
+ }).catch(() => {
887
+ });
888
+ },
889
+ style: {
890
+ cursor: "pointer",
891
+ display: "inline-flex",
892
+ alignItems: "center",
893
+ marginLeft: 3,
894
+ gap: 2,
895
+ color: copied ? "#16a34a" : "#92400e",
896
+ transition: "color 0.15s"
897
+ },
898
+ children: copied ? /* @__PURE__ */ jsxs3(Fragment, { children: [
899
+ /* @__PURE__ */ jsx4(Check, { size: 10, strokeWidth: 2 }),
900
+ " Copied"
901
+ ] }) : /* @__PURE__ */ jsx4(Copy, { size: 10, strokeWidth: 2 })
902
+ }
903
+ )
904
+ ] }) : /* @__PURE__ */ jsxs3(Fragment, { children: [
905
+ /* @__PURE__ */ jsx4(Flag, { size: 10, strokeWidth: 2 }),
906
+ "Feedback"
907
+ ] })
908
+ }
909
+ );
910
+ }
837
911
  function VoiceTranscript({
838
912
  messages,
839
913
  isTyping,
@@ -843,7 +917,7 @@ function VoiceTranscript({
843
917
  onStartMic,
844
918
  onSwitchToKeyboard,
845
919
  onReport,
846
- feedbackSentTurn
920
+ feedbackSentTurns
847
921
  }) {
848
922
  const config = useSiteConfig3();
849
923
  const fontFamily = config.fontFamily ?? DEFAULT_FONT_FAMILY;
@@ -1035,35 +1109,17 @@ function VoiceTranscript({
1035
1109
  ),
1036
1110
  isAI && (!isLast || !isTyping) && onReport && (() => {
1037
1111
  const turnNumber = visible.slice(0, idx + 1).filter((m) => m.role === "assistant").length;
1038
- const isSent = feedbackSentTurn === turnNumber;
1039
- return /* @__PURE__ */ jsxs3(
1040
- "button",
1112
+ const ticketId = feedbackSentTurns?.[turnNumber] ?? null;
1113
+ const isSent = ticketId !== null;
1114
+ return /* @__PURE__ */ jsx4(
1115
+ FeedbackPill,
1041
1116
  {
1042
- disabled: isSent,
1043
- onClick: () => {
1117
+ isSent,
1118
+ ticketId,
1119
+ onReport: () => {
1044
1120
  const userMsg = visible.slice(0, idx).reverse().find((m) => m.role === "user");
1045
1121
  onReport(turnNumber, msg.text, userMsg?.text);
1046
- },
1047
- className: "voice-feedback-pill",
1048
- style: {
1049
- marginTop: 2,
1050
- fontSize: 11,
1051
- padding: "2px 8px",
1052
- borderRadius: 8,
1053
- border: isSent ? "1px solid rgba(22,163,74,0.3)" : "1px solid rgba(0,0,0,0.12)",
1054
- background: "transparent",
1055
- color: isSent ? "#16a34a" : "rgba(0,0,0,0.30)",
1056
- cursor: isSent ? "default" : "pointer",
1057
- display: "inline-flex",
1058
- alignItems: "center",
1059
- gap: 3,
1060
- opacity: isSent ? 1 : 0,
1061
- transition: "opacity 0.15s, color 0.15s"
1062
- },
1063
- children: [
1064
- /* @__PURE__ */ jsx4(Flag, { size: 10, strokeWidth: 2 }),
1065
- isSent ? "Sent" : "Feedback"
1066
- ]
1122
+ }
1067
1123
  }
1068
1124
  );
1069
1125
  })()
@@ -1394,7 +1450,7 @@ function EmptyStateGraphic({ primaryColor, voiceState, onStartMic, onSwitchToKey
1394
1450
  import { useEffect as useEffect4 } from "react";
1395
1451
  import { motion as motion2, AnimatePresence as AnimatePresence2 } from "motion/react";
1396
1452
  import { ArrowUpRight } from "lucide-react";
1397
- import { Fragment, jsx as jsx5, jsxs as jsxs4 } from "react/jsx-runtime";
1453
+ import { Fragment as Fragment2, jsx as jsx5, jsxs as jsxs4 } from "react/jsx-runtime";
1398
1454
  var AUTO_DISMISS_MS = 8e3;
1399
1455
  function VoiceToolCard({
1400
1456
  result,
@@ -1438,7 +1494,7 @@ function VoiceToolCard({
1438
1494
  onAction?.(result);
1439
1495
  }
1440
1496
  },
1441
- children: isCapsule ? /* @__PURE__ */ jsxs4(Fragment, { children: [
1497
+ children: isCapsule ? /* @__PURE__ */ jsxs4(Fragment2, { children: [
1442
1498
  /* @__PURE__ */ jsx5(
1443
1499
  ArrowUpRight,
1444
1500
  {
@@ -1454,7 +1510,7 @@ function VoiceToolCard({
1454
1510
  children: result.displayText
1455
1511
  }
1456
1512
  )
1457
- ] }) : /* @__PURE__ */ jsxs4(Fragment, { children: [
1513
+ ] }) : /* @__PURE__ */ jsxs4(Fragment2, { children: [
1458
1514
  /* @__PURE__ */ jsx5("p", { className: "text-white/50 text-xs uppercase tracking-wider mb-1", children: result.name }),
1459
1515
  /* @__PURE__ */ jsx5("p", { className: "text-white text-sm leading-relaxed", children: result.displayText })
1460
1516
  ] })
@@ -1708,7 +1764,7 @@ function PipelineMetricsBar({
1708
1764
 
1709
1765
  // src/components/GlassCopilotPanel.tsx
1710
1766
  import { jsx as jsx9, jsxs as jsxs8 } from "react/jsx-runtime";
1711
- var VoiceSettingsView2 = lazy(() => import("./VoiceSettingsView-2CCGBBY4.js"));
1767
+ var VoiceSettingsView2 = lazy(() => import("./VoiceSettingsView-UL3TSETS.js"));
1712
1768
  var RETRY_INITIAL_MS = 3e3;
1713
1769
  var RETRY_MAX_MS = 3e4;
1714
1770
  var STATE_LABELS = {
@@ -2329,7 +2385,7 @@ function ExpandedContent({
2329
2385
  onSwitchToKeyboard,
2330
2386
  switchToTextRef,
2331
2387
  onReport,
2332
- feedbackSentTurn,
2388
+ feedbackSentTurns,
2333
2389
  feedbackTarget,
2334
2390
  onFeedbackSubmit,
2335
2391
  onFeedbackCancel
@@ -2412,7 +2468,7 @@ function ExpandedContent({
2412
2468
  }
2413
2469
  ),
2414
2470
  /* @__PURE__ */ jsxs8("div", { style: { flex: 1, minHeight: 0, display: "flex", flexDirection: "column", overflow: "hidden" }, children: [
2415
- /* @__PURE__ */ jsx9("div", { "data-testid": "voice-agent-transcript", style: { flex: 1, minHeight: 0, display: "flex", flexDirection: "column" }, children: /* @__PURE__ */ jsx9(VoiceTranscript, { messages, isTyping, variant: "panel", voiceError, voiceState, onStartMic, onSwitchToKeyboard, onReport, feedbackSentTurn }) }),
2471
+ /* @__PURE__ */ jsx9("div", { "data-testid": "voice-agent-transcript", style: { flex: 1, minHeight: 0, display: "flex", flexDirection: "column" }, children: /* @__PURE__ */ jsx9(VoiceTranscript, { messages, isTyping, variant: "panel", voiceError, voiceState, onStartMic, onSwitchToKeyboard, onReport, feedbackSentTurns }) }),
2416
2472
  /* @__PURE__ */ jsxs8("div", { style: { flexShrink: 0 }, children: [
2417
2473
  /* @__PURE__ */ jsx9(PipelineMetricsBar, { timings: lastTimings ?? null, show: showPipelineMetrics, autoHideMs: pipelineMetricsAutoHideMs }),
2418
2474
  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: [
@@ -2501,7 +2557,7 @@ function WiredPanelInner({
2501
2557
  const { state, start, stop, messages, getAmplitude, analyser, sendTextMessage, voiceError, dismissError, lastTimings, sessionId, applyVolume, settings } = useVoiceAgent({ settings: voiceSettings, volumeRef, speedRef });
2502
2558
  const [toolResult, setToolResult] = useState5(null);
2503
2559
  const [feedbackTarget, setFeedbackTarget] = useState5(null);
2504
- const [feedbackSentTurn, setFeedbackSentTurn] = useState5(null);
2560
+ const [feedbackSentTurns, setFeedbackSentTurns] = useState5({});
2505
2561
  const orbState = voiceStateToOrbState(state);
2506
2562
  const [backendDown, setBackendDown] = useState5(false);
2507
2563
  const autoStartedRef = useRef4(false);
@@ -2617,19 +2673,30 @@ function WiredPanelInner({
2617
2673
  useEffect6(() => {
2618
2674
  if (panelState === "hidden") stopRef.current(true);
2619
2675
  }, [panelState]);
2676
+ const micOpen = state === "LISTENING" || state === "USER_SPEAKING";
2677
+ const stateRef = useRef4(state);
2678
+ stateRef.current = state;
2620
2679
  useEffect6(() => {
2621
2680
  if (panelState === "hidden" || micPaused || showSettings) return;
2622
- if (state === "PROCESSING" || state === "AI_SPEAKING") return;
2623
- const timer = setTimeout(() => {
2624
- stopRef.current();
2625
- if (hasConversationRef.current) {
2626
- setMicPaused(true);
2627
- } else {
2628
- onCollapseRef.current();
2629
- }
2630
- }, settings.idleTimeoutMs);
2681
+ if (!micOpen) return;
2682
+ let timer;
2683
+ const schedule = () => {
2684
+ timer = setTimeout(() => {
2685
+ if (stateRef.current === "USER_SPEAKING") {
2686
+ schedule();
2687
+ return;
2688
+ }
2689
+ stopRef.current();
2690
+ if (hasConversationRef.current) {
2691
+ setMicPaused(true);
2692
+ } else {
2693
+ onCollapseRef.current();
2694
+ }
2695
+ }, settings.idleTimeoutMs);
2696
+ };
2697
+ schedule();
2631
2698
  return () => clearTimeout(timer);
2632
- }, [state, panelState, micPaused, showSettings, activity, settings.idleTimeoutMs]);
2699
+ }, [micOpen, panelState, micPaused, showSettings, activity, settings.idleTimeoutMs]);
2633
2700
  useEffect6(() => {
2634
2701
  if (!micPaused || panelState !== "expanded" || showSettings) return;
2635
2702
  if (settings.panelCollapseTimeoutMs === 0) return;
@@ -2653,7 +2720,7 @@ function WiredPanelInner({
2653
2720
  const backendUrl = typeof import.meta !== "undefined" && import.meta.env?.VITE_BACKEND_URL || "";
2654
2721
  const url = backendUrl ? `${backendUrl}/api/feedback` : "/api/feedback";
2655
2722
  try {
2656
- await fetch(url, {
2723
+ const res = await fetch(url, {
2657
2724
  method: "POST",
2658
2725
  headers: { "Content-Type": "application/json" },
2659
2726
  body: JSON.stringify({
@@ -2665,11 +2732,11 @@ function WiredPanelInner({
2665
2732
  timings: lastTimings ?? void 0,
2666
2733
  route: window.location.pathname,
2667
2734
  copilotName: config.copilotName,
2668
- kitVersion: "5.1.1"
2735
+ kitVersion: "5.1.3"
2669
2736
  })
2670
2737
  });
2671
- setFeedbackSentTurn(target.turnNumber);
2672
- setTimeout(() => setFeedbackSentTurn(null), 2e3);
2738
+ const body = await res.json().catch(() => ({ ticketId: void 0 }));
2739
+ setFeedbackSentTurns((prev) => ({ ...prev, [target.turnNumber]: body.ticketId || "\u2713" }));
2673
2740
  } catch {
2674
2741
  }
2675
2742
  setFeedbackTarget(null);
@@ -2729,7 +2796,7 @@ function WiredPanelInner({
2729
2796
  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 });
2730
2797
  }
2731
2798
  return /* @__PURE__ */ jsxs8("div", { className: "relative h-full", children: [
2732
- /* @__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, onReport: handleReport, feedbackSentTurn, feedbackTarget, onFeedbackSubmit: handleFeedbackSubmit, onFeedbackCancel: handleFeedbackCancel }),
2799
+ /* @__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, onReport: handleReport, feedbackSentTurns, feedbackTarget, onFeedbackSubmit: handleFeedbackSubmit, onFeedbackCancel: handleFeedbackCancel }),
2733
2800
  /* @__PURE__ */ jsx9(AnimatePresence5, { children: showSettings && /* @__PURE__ */ jsx9(Suspense, { fallback: null, children: /* @__PURE__ */ jsx9(VoiceSettingsView2, { onBack: toggleSettings, onVolumeChange: applyVolume }) }) })
2734
2801
  ] });
2735
2802
  }
@@ -3375,7 +3442,7 @@ function VoiceWaveformCanvas({
3375
3442
  }
3376
3443
 
3377
3444
  // src/components/VoiceOverlay.tsx
3378
- import { Fragment as Fragment2, jsx as jsx16, jsxs as jsxs11 } from "react/jsx-runtime";
3445
+ import { Fragment as Fragment3, jsx as jsx16, jsxs as jsxs11 } from "react/jsx-runtime";
3379
3446
  var OVERLAY_Z = 2147483647;
3380
3447
  var SPRING_BOUNCY = { type: "spring", damping: 20, stiffness: 180 };
3381
3448
  var SPRING_SMOOTH = { type: "spring", damping: 28, stiffness: 160 };
@@ -3553,7 +3620,7 @@ function OverlayPortal({
3553
3620
  }) {
3554
3621
  if (!isOpen) return null;
3555
3622
  return createPortal2(
3556
- /* @__PURE__ */ jsxs11(Fragment2, { children: [
3623
+ /* @__PURE__ */ jsxs11(Fragment3, { children: [
3557
3624
  /* @__PURE__ */ jsx16(
3558
3625
  "svg",
3559
3626
  {