@absolutejs/voice 0.0.22-beta.493 → 0.0.22-beta.494

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.
@@ -0,0 +1,33 @@
1
+ import type { VoiceControllerOptions } from "../types";
2
+ export type VoiceWidgetTheme = {
3
+ accent?: string;
4
+ background?: string;
5
+ errorAccent?: string;
6
+ fontFamily?: string;
7
+ foreground?: string;
8
+ radius?: number | string;
9
+ };
10
+ export type VoiceWidgetLabels = {
11
+ callEnded?: string;
12
+ connecting?: string;
13
+ endCall?: string;
14
+ idle?: string;
15
+ listening?: string;
16
+ mute?: string;
17
+ speaking?: string;
18
+ startCall?: string;
19
+ thinking?: string;
20
+ unmute?: string;
21
+ };
22
+ export type VoiceWidgetProps = {
23
+ className?: string;
24
+ controllerOptions?: VoiceControllerOptions;
25
+ labels?: VoiceWidgetLabels;
26
+ /** Voice runtime URL. Default '/voice'. */
27
+ path?: string;
28
+ /** Optional callback for diagnostic events surfaced by the controller. */
29
+ onError?: (error: string) => void;
30
+ theme?: VoiceWidgetTheme;
31
+ title?: string;
32
+ };
33
+ export declare const VoiceWidget: ({ className, controllerOptions, labels: labelsOverride, onError, path, theme: themeOverride, title, }: VoiceWidgetProps) => import("react/jsx-runtime").JSX.Element;
@@ -35,6 +35,8 @@ export { useVoiceDeliveryRuntime } from "./useVoiceDeliveryRuntime";
35
35
  export { useVoiceCampaignDialerProof } from "./useVoiceCampaignDialerProof";
36
36
  export { useVoiceStream } from "./useVoiceStream";
37
37
  export { useVoiceController } from "./useVoiceController";
38
+ export { VoiceWidget } from "./VoiceWidget";
39
+ export type { VoiceWidgetLabels, VoiceWidgetProps, VoiceWidgetTheme, } from "./VoiceWidget";
38
40
  export { useVoiceProviderStatus } from "./useVoiceProviderStatus";
39
41
  export { useVoiceProviderCapabilities } from "./useVoiceProviderCapabilities";
40
42
  export { useVoiceProviderContracts } from "./useVoiceProviderContracts";
@@ -12450,8 +12450,237 @@ var useVoiceController = (path, options = {}) => {
12450
12450
  toggleRecording: () => controller.toggleRecording()
12451
12451
  };
12452
12452
  };
12453
+ // src/react/VoiceWidget.tsx
12454
+ import { useMemo, useRef as useRef26 } from "react";
12455
+
12456
+ // src/agentState.ts
12457
+ var deriveVoiceAgentUIState = (input) => {
12458
+ if (!input.isConnected) {
12459
+ return "idle";
12460
+ }
12461
+ if (input.isPlaying) {
12462
+ return "speaking";
12463
+ }
12464
+ if (input.isRecording && input.hasActivePartial) {
12465
+ return "listening";
12466
+ }
12467
+ if (input.isRecording) {
12468
+ return "listening";
12469
+ }
12470
+ if (input.lastTranscriptAt && !input.lastAssistantAt) {
12471
+ return "thinking";
12472
+ }
12473
+ if (input.lastTranscriptAt && input.lastAssistantAt && input.lastTranscriptAt > input.lastAssistantAt) {
12474
+ return "thinking";
12475
+ }
12476
+ return "idle";
12477
+ };
12478
+ var describeVoiceAgentUIState = (state) => {
12479
+ switch (state) {
12480
+ case "idle":
12481
+ return "Idle";
12482
+ case "listening":
12483
+ return "Listening";
12484
+ case "speaking":
12485
+ return "Speaking";
12486
+ case "thinking":
12487
+ return "Thinking";
12488
+ }
12489
+ };
12490
+ var voiceAgentUIStateOrder = [
12491
+ "idle",
12492
+ "listening",
12493
+ "thinking",
12494
+ "speaking"
12495
+ ];
12496
+
12497
+ // src/react/VoiceWidget.tsx
12498
+ import { jsxDEV as jsxDEV22 } from "react/jsx-dev-runtime";
12499
+ var DEFAULT_THEME = {
12500
+ accent: "#3b82f6",
12501
+ background: "#0f172a",
12502
+ errorAccent: "#ef4444",
12503
+ fontFamily: 'ui-sans-serif, system-ui, -apple-system, "Segoe UI", Roboto, "Helvetica Neue", Arial, sans-serif',
12504
+ foreground: "#f8fafc",
12505
+ radius: 16
12506
+ };
12507
+ var DEFAULT_LABELS = {
12508
+ callEnded: "Call ended",
12509
+ connecting: "Connecting\u2026",
12510
+ endCall: "End call",
12511
+ idle: "Idle",
12512
+ listening: "Listening",
12513
+ mute: "Mute",
12514
+ speaking: "Speaking",
12515
+ startCall: "Start call",
12516
+ thinking: "Thinking",
12517
+ unmute: "Unmute"
12518
+ };
12519
+ var stateLabel = (state, labels) => {
12520
+ switch (state) {
12521
+ case "listening":
12522
+ return labels.listening;
12523
+ case "speaking":
12524
+ return labels.speaking;
12525
+ case "thinking":
12526
+ return labels.thinking;
12527
+ case "idle":
12528
+ return labels.idle;
12529
+ }
12530
+ };
12531
+ var resolveRadius = (radius) => typeof radius === "number" ? `${radius}px` : radius;
12532
+ var VoiceWidget = ({
12533
+ className,
12534
+ controllerOptions,
12535
+ labels: labelsOverride,
12536
+ onError,
12537
+ path = "/voice",
12538
+ theme: themeOverride,
12539
+ title
12540
+ }) => {
12541
+ const theme = { ...DEFAULT_THEME, ...themeOverride };
12542
+ const labels = { ...DEFAULT_LABELS, ...labelsOverride };
12543
+ const lastErrorRef = useRef26(null);
12544
+ const controller = useVoiceController(path, controllerOptions);
12545
+ if (controller.error && controller.error !== lastErrorRef.current) {
12546
+ lastErrorRef.current = controller.error;
12547
+ onError?.(controller.error);
12548
+ }
12549
+ const lastAssistantAt = useMemo(() => {
12550
+ const last = controller.assistantAudio.at(-1);
12551
+ return last?.receivedAt;
12552
+ }, [controller.assistantAudio]);
12553
+ const lastTranscriptAt = useMemo(() => {
12554
+ const lastTurn = controller.turns.at(-1);
12555
+ return lastTurn?.committedAt;
12556
+ }, [controller.turns]);
12557
+ const agentState = deriveVoiceAgentUIState({
12558
+ hasActivePartial: controller.partial.length > 0,
12559
+ isConnected: controller.isConnected,
12560
+ isPlaying: false,
12561
+ isRecording: controller.isRecording,
12562
+ lastAssistantAt,
12563
+ lastTranscriptAt
12564
+ });
12565
+ const connecting = !controller.isConnected && controller.status !== "idle" && !controller.error;
12566
+ const containerStyle = {
12567
+ background: theme.background,
12568
+ borderRadius: resolveRadius(theme.radius),
12569
+ color: theme.foreground,
12570
+ fontFamily: theme.fontFamily,
12571
+ minWidth: 240,
12572
+ padding: "20px 22px"
12573
+ };
12574
+ const statusDotStyle = {
12575
+ background: controller.error ? theme.errorAccent : agentState === "idle" ? "rgba(148, 163, 184, 0.6)" : theme.accent,
12576
+ borderRadius: "50%",
12577
+ boxShadow: agentState === "speaking" ? `0 0 12px ${theme.accent}` : undefined,
12578
+ height: 10,
12579
+ width: 10
12580
+ };
12581
+ const buttonStyle = (variant) => ({
12582
+ background: variant === "primary" ? theme.accent : variant === "danger" ? theme.errorAccent : "transparent",
12583
+ border: variant === "secondary" ? `1px solid rgba(255,255,255,0.18)` : "none",
12584
+ borderRadius: 12,
12585
+ color: theme.foreground,
12586
+ cursor: "pointer",
12587
+ fontSize: 14,
12588
+ fontWeight: 500,
12589
+ padding: "10px 14px"
12590
+ });
12591
+ const handleStart = () => {
12592
+ controller.startRecording();
12593
+ };
12594
+ const handleStop = () => {
12595
+ controller.stopRecording();
12596
+ };
12597
+ const handleEnd = () => {
12598
+ controller.close();
12599
+ };
12600
+ const showStart = !controller.isRecording && controller.status !== "completed";
12601
+ const showStop = controller.isRecording;
12602
+ return /* @__PURE__ */ jsxDEV22("div", {
12603
+ "aria-live": "polite",
12604
+ className,
12605
+ "data-agent-state": agentState,
12606
+ role: "region",
12607
+ style: containerStyle,
12608
+ children: [
12609
+ /* @__PURE__ */ jsxDEV22("div", {
12610
+ style: {
12611
+ alignItems: "center",
12612
+ display: "flex",
12613
+ gap: 10,
12614
+ marginBottom: 12
12615
+ },
12616
+ children: [
12617
+ /* @__PURE__ */ jsxDEV22("span", {
12618
+ "aria-hidden": "true",
12619
+ style: statusDotStyle
12620
+ }, undefined, false, undefined, this),
12621
+ /* @__PURE__ */ jsxDEV22("strong", {
12622
+ style: { fontSize: 15 },
12623
+ children: title ?? "Voice"
12624
+ }, undefined, false, undefined, this),
12625
+ /* @__PURE__ */ jsxDEV22("span", {
12626
+ style: {
12627
+ fontSize: 13,
12628
+ marginLeft: "auto",
12629
+ opacity: 0.7
12630
+ },
12631
+ children: controller.error ? "Error" : connecting ? labels.connecting : controller.status === "completed" ? labels.callEnded : stateLabel(agentState, labels)
12632
+ }, undefined, false, undefined, this)
12633
+ ]
12634
+ }, undefined, true, undefined, this),
12635
+ controller.partial ? /* @__PURE__ */ jsxDEV22("p", {
12636
+ style: {
12637
+ fontSize: 13,
12638
+ margin: "8px 0 12px",
12639
+ opacity: 0.85,
12640
+ wordBreak: "break-word"
12641
+ },
12642
+ children: [
12643
+ "\u201C",
12644
+ controller.partial,
12645
+ "\u201D"
12646
+ ]
12647
+ }, undefined, true, undefined, this) : null,
12648
+ /* @__PURE__ */ jsxDEV22("div", {
12649
+ style: { display: "flex", gap: 10 },
12650
+ children: [
12651
+ showStart ? /* @__PURE__ */ jsxDEV22("button", {
12652
+ onClick: handleStart,
12653
+ style: buttonStyle("primary"),
12654
+ type: "button",
12655
+ children: labels.startCall
12656
+ }, undefined, false, undefined, this) : null,
12657
+ showStop ? /* @__PURE__ */ jsxDEV22("button", {
12658
+ onClick: handleStop,
12659
+ style: buttonStyle("secondary"),
12660
+ type: "button",
12661
+ children: labels.mute
12662
+ }, undefined, false, undefined, this) : null,
12663
+ controller.isConnected ? /* @__PURE__ */ jsxDEV22("button", {
12664
+ onClick: handleEnd,
12665
+ style: buttonStyle("danger"),
12666
+ type: "button",
12667
+ children: labels.endCall
12668
+ }, undefined, false, undefined, this) : null
12669
+ ]
12670
+ }, undefined, true, undefined, this),
12671
+ controller.error ? /* @__PURE__ */ jsxDEV22("p", {
12672
+ style: {
12673
+ color: theme.errorAccent,
12674
+ fontSize: 12,
12675
+ marginTop: 12
12676
+ },
12677
+ children: controller.error
12678
+ }, undefined, false, undefined, this) : null
12679
+ ]
12680
+ }, undefined, true, undefined, this);
12681
+ };
12453
12682
  // src/react/useVoiceWorkflowStatus.tsx
12454
- import { useEffect as useEffect26, useRef as useRef26, useSyncExternalStore as useSyncExternalStore26 } from "react";
12683
+ import { useEffect as useEffect26, useRef as useRef27, useSyncExternalStore as useSyncExternalStore26 } from "react";
12455
12684
 
12456
12685
  // src/client/workflowStatus.ts
12457
12686
  var fetchVoiceWorkflowStatus = async (path = "/evals/scenarios/json", options = {}) => {
@@ -12534,7 +12763,7 @@ var createVoiceWorkflowStatusStore = (path = "/evals/scenarios/json", options =
12534
12763
 
12535
12764
  // src/react/useVoiceWorkflowStatus.tsx
12536
12765
  var useVoiceWorkflowStatus = (path = "/evals/scenarios/json", options = {}) => {
12537
- const storeRef = useRef26(null);
12766
+ const storeRef = useRef27(null);
12538
12767
  if (!storeRef.current) {
12539
12768
  storeRef.current = createVoiceWorkflowStatusStore(path, options);
12540
12769
  }
@@ -12575,6 +12804,7 @@ export {
12575
12804
  useVoiceCampaignDialerProof,
12576
12805
  useVoiceCallDebugger,
12577
12806
  useVoiceAgentSquadStatus,
12807
+ VoiceWidget,
12578
12808
  VoiceTurnQuality,
12579
12809
  VoiceTurnLatency,
12580
12810
  VoiceTraceTimeline,
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@absolutejs/voice",
3
- "version": "0.0.22-beta.493",
3
+ "version": "0.0.22-beta.494",
4
4
  "description": "Voice primitives and Elysia plugin for AbsoluteJS",
5
5
  "repository": {
6
6
  "type": "git",