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

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-OUTD6CWN.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-DAYBNBVA.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.2" })
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-OUTD6CWN.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-OUTD6CWN.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-DAYBNBVA.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);
@@ -2653,7 +2709,7 @@ function WiredPanelInner({
2653
2709
  const backendUrl = typeof import.meta !== "undefined" && import.meta.env?.VITE_BACKEND_URL || "";
2654
2710
  const url = backendUrl ? `${backendUrl}/api/feedback` : "/api/feedback";
2655
2711
  try {
2656
- await fetch(url, {
2712
+ const res = await fetch(url, {
2657
2713
  method: "POST",
2658
2714
  headers: { "Content-Type": "application/json" },
2659
2715
  body: JSON.stringify({
@@ -2665,11 +2721,11 @@ function WiredPanelInner({
2665
2721
  timings: lastTimings ?? void 0,
2666
2722
  route: window.location.pathname,
2667
2723
  copilotName: config.copilotName,
2668
- kitVersion: "5.1.1"
2724
+ kitVersion: "5.1.2"
2669
2725
  })
2670
2726
  });
2671
- setFeedbackSentTurn(target.turnNumber);
2672
- setTimeout(() => setFeedbackSentTurn(null), 2e3);
2727
+ const body = await res.json().catch(() => ({ ticketId: void 0 }));
2728
+ setFeedbackSentTurns((prev) => ({ ...prev, [target.turnNumber]: body.ticketId || "\u2713" }));
2673
2729
  } catch {
2674
2730
  }
2675
2731
  setFeedbackTarget(null);
@@ -2729,7 +2785,7 @@ function WiredPanelInner({
2729
2785
  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
2786
  }
2731
2787
  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 }),
2788
+ /* @__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
2789
  /* @__PURE__ */ jsx9(AnimatePresence5, { children: showSettings && /* @__PURE__ */ jsx9(Suspense, { fallback: null, children: /* @__PURE__ */ jsx9(VoiceSettingsView2, { onBack: toggleSettings, onVolumeChange: applyVolume }) }) })
2734
2790
  ] });
2735
2791
  }
@@ -3375,7 +3431,7 @@ function VoiceWaveformCanvas({
3375
3431
  }
3376
3432
 
3377
3433
  // src/components/VoiceOverlay.tsx
3378
- import { Fragment as Fragment2, jsx as jsx16, jsxs as jsxs11 } from "react/jsx-runtime";
3434
+ import { Fragment as Fragment3, jsx as jsx16, jsxs as jsxs11 } from "react/jsx-runtime";
3379
3435
  var OVERLAY_Z = 2147483647;
3380
3436
  var SPRING_BOUNCY = { type: "spring", damping: 20, stiffness: 180 };
3381
3437
  var SPRING_SMOOTH = { type: "spring", damping: 28, stiffness: 160 };
@@ -3553,7 +3609,7 @@ function OverlayPortal({
3553
3609
  }) {
3554
3610
  if (!isOpen) return null;
3555
3611
  return createPortal2(
3556
- /* @__PURE__ */ jsxs11(Fragment2, { children: [
3612
+ /* @__PURE__ */ jsxs11(Fragment3, { children: [
3557
3613
  /* @__PURE__ */ jsx16(
3558
3614
  "svg",
3559
3615
  {