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

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,18 @@
1
+ import type { VoiceControllerOptions } from "../types";
2
+ import { type VoiceWidgetLabels, type VoiceWidgetTheme, type VoiceWidgetViewModel } from "../client/voiceWidgetView";
3
+ export type CreateVoiceWidgetServiceOptions = VoiceControllerOptions & {
4
+ labels?: VoiceWidgetLabels;
5
+ theme?: VoiceWidgetTheme;
6
+ title?: string;
7
+ };
8
+ export declare class VoiceWidgetService {
9
+ connect<TResult = unknown>(path: string, options?: CreateVoiceWidgetServiceOptions): {
10
+ close: () => void;
11
+ endCall: () => void;
12
+ getHTML: () => string;
13
+ mute: () => void;
14
+ startCall: () => Promise<void>;
15
+ unmute: () => Promise<void>;
16
+ viewModel: import("@angular/core").Signal<VoiceWidgetViewModel>;
17
+ };
18
+ }
@@ -0,0 +1,50 @@
1
+ import type { VoiceAgentUIState } from "../agentState";
2
+ import type { VoiceControllerState } from "../types";
3
+ export type VoiceWidgetTheme = {
4
+ accent?: string;
5
+ background?: string;
6
+ errorAccent?: string;
7
+ fontFamily?: string;
8
+ foreground?: string;
9
+ radius?: number | string;
10
+ };
11
+ export type VoiceWidgetLabels = {
12
+ callEnded?: string;
13
+ connecting?: string;
14
+ endCall?: string;
15
+ idle?: string;
16
+ listening?: string;
17
+ mute?: string;
18
+ speaking?: string;
19
+ startCall?: string;
20
+ thinking?: string;
21
+ unmute?: string;
22
+ };
23
+ export type VoiceWidgetViewModelInput = {
24
+ labels?: VoiceWidgetLabels;
25
+ state: Pick<VoiceControllerState, "assistantAudio" | "error" | "isConnected" | "isRecording" | "partial" | "status" | "turns">;
26
+ theme?: VoiceWidgetTheme;
27
+ title?: string;
28
+ };
29
+ export type VoiceWidgetViewModel = {
30
+ agentState: VoiceAgentUIState;
31
+ classes: {
32
+ container: string;
33
+ dot: string;
34
+ };
35
+ controls: {
36
+ canEnd: boolean;
37
+ canMute: boolean;
38
+ canStart: boolean;
39
+ };
40
+ errorMessage?: string;
41
+ labels: Required<VoiceWidgetLabels>;
42
+ partial?: string;
43
+ statusLabel: string;
44
+ theme: Required<VoiceWidgetTheme>;
45
+ title: string;
46
+ };
47
+ export declare const DEFAULT_VOICE_WIDGET_THEME: Required<VoiceWidgetTheme>;
48
+ export declare const DEFAULT_VOICE_WIDGET_LABELS: Required<VoiceWidgetLabels>;
49
+ export declare const createVoiceWidgetViewModel: (input: VoiceWidgetViewModelInput) => VoiceWidgetViewModel;
50
+ export declare const renderVoiceWidgetHTML: (model: VoiceWidgetViewModel) => string;
@@ -1,33 +1,13 @@
1
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
- };
2
+ import { type VoiceWidgetLabels, type VoiceWidgetTheme, type VoiceWidgetViewModel } from "../client/voiceWidgetView";
3
+ export type { VoiceWidgetLabels, VoiceWidgetTheme, VoiceWidgetViewModel };
22
4
  export type VoiceWidgetProps = {
23
5
  className?: string;
24
6
  controllerOptions?: VoiceControllerOptions;
25
7
  labels?: VoiceWidgetLabels;
26
- /** Voice runtime URL. Default '/voice'. */
27
- path?: string;
28
- /** Optional callback for diagnostic events surfaced by the controller. */
29
8
  onError?: (error: string) => void;
9
+ path?: string;
30
10
  theme?: VoiceWidgetTheme;
31
11
  title?: string;
32
12
  };
33
- export declare const VoiceWidget: ({ className, controllerOptions, labels: labelsOverride, onError, path, theme: themeOverride, title, }: VoiceWidgetProps) => import("react/jsx-runtime").JSX.Element;
13
+ export declare const VoiceWidget: ({ className, controllerOptions, labels, onError, path, theme, title, }: VoiceWidgetProps) => import("react/jsx-runtime").JSX.Element;
@@ -12494,9 +12494,8 @@ var voiceAgentUIStateOrder = [
12494
12494
  "speaking"
12495
12495
  ];
12496
12496
 
12497
- // src/react/VoiceWidget.tsx
12498
- import { jsxDEV as jsxDEV22 } from "react/jsx-dev-runtime";
12499
- var DEFAULT_THEME = {
12497
+ // src/client/voiceWidgetView.ts
12498
+ var DEFAULT_VOICE_WIDGET_THEME = {
12500
12499
  accent: "#3b82f6",
12501
12500
  background: "#0f172a",
12502
12501
  errorAccent: "#ef4444",
@@ -12504,7 +12503,7 @@ var DEFAULT_THEME = {
12504
12503
  foreground: "#f8fafc",
12505
12504
  radius: 16
12506
12505
  };
12507
- var DEFAULT_LABELS = {
12506
+ var DEFAULT_VOICE_WIDGET_LABELS = {
12508
12507
  callEnded: "Call ended",
12509
12508
  connecting: "Connecting\u2026",
12510
12509
  endCall: "End call",
@@ -12528,83 +12527,134 @@ var stateLabel = (state, labels) => {
12528
12527
  return labels.idle;
12529
12528
  }
12530
12529
  };
12530
+ var createVoiceWidgetViewModel = (input) => {
12531
+ const theme = { ...DEFAULT_VOICE_WIDGET_THEME, ...input.theme };
12532
+ const labels = { ...DEFAULT_VOICE_WIDGET_LABELS, ...input.labels };
12533
+ const lastAssistantAt = input.state.assistantAudio.at(-1)?.receivedAt;
12534
+ const lastTranscriptAt = input.state.turns.at(-1)?.committedAt;
12535
+ const agentState = deriveVoiceAgentUIState({
12536
+ hasActivePartial: input.state.partial.length > 0,
12537
+ isConnected: input.state.isConnected,
12538
+ isPlaying: false,
12539
+ isRecording: input.state.isRecording,
12540
+ lastAssistantAt,
12541
+ lastTranscriptAt
12542
+ });
12543
+ const connecting = !input.state.isConnected && input.state.status !== "idle" && !input.state.error;
12544
+ const statusLabel2 = input.state.error ? "Error" : connecting ? labels.connecting : input.state.status === "completed" ? labels.callEnded : stateLabel(agentState, labels);
12545
+ return {
12546
+ agentState,
12547
+ classes: {
12548
+ container: `absolute-voice-widget absolute-voice-widget--${agentState}`,
12549
+ dot: `absolute-voice-widget__dot${input.state.error ? " absolute-voice-widget__dot--error" : ""}`
12550
+ },
12551
+ controls: {
12552
+ canEnd: input.state.isConnected,
12553
+ canMute: input.state.isRecording,
12554
+ canStart: !input.state.isRecording && input.state.status !== "completed"
12555
+ },
12556
+ errorMessage: input.state.error ?? undefined,
12557
+ labels,
12558
+ partial: input.state.partial || undefined,
12559
+ statusLabel: statusLabel2,
12560
+ theme,
12561
+ title: input.title ?? "Voice"
12562
+ };
12563
+ };
12564
+ var escapeHtml27 = (value) => value.replaceAll("&", "&amp;").replaceAll("<", "&lt;").replaceAll(">", "&gt;").replaceAll('"', "&quot;").replaceAll("'", "&#39;");
12531
12565
  var resolveRadius = (radius) => typeof radius === "number" ? `${radius}px` : radius;
12566
+ var renderVoiceWidgetHTML = (model) => {
12567
+ const t = model.theme;
12568
+ const containerStyle = `background:${t.background};border-radius:${resolveRadius(t.radius)};color:${t.foreground};font-family:${t.fontFamily};min-width:240px;padding:20px 22px;`;
12569
+ const dotStyle = `background:${model.errorMessage ? t.errorAccent : model.agentState === "idle" ? "rgba(148,163,184,0.6)" : t.accent};border-radius:50%;height:10px;width:10px;`;
12570
+ const buttons = [];
12571
+ if (model.controls.canStart) {
12572
+ buttons.push(`<button type="button" data-action="start" style="background:${t.accent};border:none;border-radius:12px;color:${t.foreground};cursor:pointer;font-size:14px;font-weight:500;padding:10px 14px;">${escapeHtml27(model.labels.startCall)}</button>`);
12573
+ }
12574
+ if (model.controls.canMute) {
12575
+ buttons.push(`<button type="button" data-action="mute" style="background:transparent;border:1px solid rgba(255,255,255,0.18);border-radius:12px;color:${t.foreground};cursor:pointer;font-size:14px;font-weight:500;padding:10px 14px;">${escapeHtml27(model.labels.mute)}</button>`);
12576
+ }
12577
+ if (model.controls.canEnd) {
12578
+ buttons.push(`<button type="button" data-action="end" style="background:${t.errorAccent};border:none;border-radius:12px;color:${t.foreground};cursor:pointer;font-size:14px;font-weight:500;padding:10px 14px;">${escapeHtml27(model.labels.endCall)}</button>`);
12579
+ }
12580
+ return `<div role="region" aria-live="polite" data-agent-state="${model.agentState}" class="${escapeHtml27(model.classes.container)}" style="${containerStyle}">
12581
+ <div style="align-items:center;display:flex;gap:10px;margin-bottom:12px;">
12582
+ <span aria-hidden="true" class="${escapeHtml27(model.classes.dot)}" style="${dotStyle}"></span>
12583
+ <strong style="font-size:15px;">${escapeHtml27(model.title)}</strong>
12584
+ <span style="font-size:13px;margin-left:auto;opacity:0.7;">${escapeHtml27(model.statusLabel)}</span>
12585
+ </div>
12586
+ ${model.partial ? `<p style="font-size:13px;margin:8px 0 12px;opacity:0.85;word-break:break-word;">\u201C${escapeHtml27(model.partial)}\u201D</p>` : ""}
12587
+ <div style="display:flex;gap:10px;">${buttons.join("")}</div>
12588
+ ${model.errorMessage ? `<p style="color:${t.errorAccent};font-size:12px;margin-top:12px;">${escapeHtml27(model.errorMessage)}</p>` : ""}
12589
+ </div>`;
12590
+ };
12591
+
12592
+ // src/react/VoiceWidget.tsx
12593
+ import { jsxDEV as jsxDEV22 } from "react/jsx-dev-runtime";
12594
+ var resolveRadius2 = (radius) => typeof radius === "number" ? `${radius}px` : radius;
12595
+ var buttonStyle = (variant, theme) => ({
12596
+ background: variant === "primary" ? theme.accent : variant === "danger" ? theme.errorAccent : "transparent",
12597
+ border: variant === "secondary" ? `1px solid rgba(255,255,255,0.18)` : "none",
12598
+ borderRadius: 12,
12599
+ color: theme.foreground,
12600
+ cursor: "pointer",
12601
+ fontSize: 14,
12602
+ fontWeight: 500,
12603
+ padding: "10px 14px"
12604
+ });
12532
12605
  var VoiceWidget = ({
12533
12606
  className,
12534
12607
  controllerOptions,
12535
- labels: labelsOverride,
12608
+ labels,
12536
12609
  onError,
12537
12610
  path = "/voice",
12538
- theme: themeOverride,
12611
+ theme,
12539
12612
  title
12540
12613
  }) => {
12541
- const theme = { ...DEFAULT_THEME, ...themeOverride };
12542
- const labels = { ...DEFAULT_LABELS, ...labelsOverride };
12543
12614
  const lastErrorRef = useRef26(null);
12544
12615
  const controller = useVoiceController(path, controllerOptions);
12545
12616
  if (controller.error && controller.error !== lastErrorRef.current) {
12546
12617
  lastErrorRef.current = controller.error;
12547
12618
  onError?.(controller.error);
12548
12619
  }
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;
12620
+ const model = useMemo(() => createVoiceWidgetViewModel({
12621
+ labels,
12622
+ state: {
12623
+ assistantAudio: controller.assistantAudio,
12624
+ error: controller.error,
12625
+ isConnected: controller.isConnected,
12626
+ isRecording: controller.isRecording,
12627
+ partial: controller.partial,
12628
+ status: controller.status,
12629
+ turns: controller.turns
12630
+ },
12631
+ theme,
12632
+ title
12633
+ }), [
12634
+ controller.assistantAudio,
12635
+ controller.error,
12636
+ controller.isConnected,
12637
+ controller.isRecording,
12638
+ controller.partial,
12639
+ controller.status,
12640
+ controller.turns,
12641
+ labels,
12642
+ theme,
12643
+ title
12644
+ ]);
12602
12645
  return /* @__PURE__ */ jsxDEV22("div", {
12603
12646
  "aria-live": "polite",
12604
- className,
12605
- "data-agent-state": agentState,
12647
+ className: className ?? model.classes.container,
12648
+ "data-agent-state": model.agentState,
12606
12649
  role: "region",
12607
- style: containerStyle,
12650
+ style: {
12651
+ background: model.theme.background,
12652
+ borderRadius: resolveRadius2(model.theme.radius),
12653
+ color: model.theme.foreground,
12654
+ fontFamily: model.theme.fontFamily,
12655
+ minWidth: 240,
12656
+ padding: "20px 22px"
12657
+ },
12608
12658
  children: [
12609
12659
  /* @__PURE__ */ jsxDEV22("div", {
12610
12660
  style: {
@@ -12616,11 +12666,16 @@ var VoiceWidget = ({
12616
12666
  children: [
12617
12667
  /* @__PURE__ */ jsxDEV22("span", {
12618
12668
  "aria-hidden": "true",
12619
- style: statusDotStyle
12669
+ style: {
12670
+ background: model.errorMessage ? model.theme.errorAccent : model.agentState === "idle" ? "rgba(148, 163, 184, 0.6)" : model.theme.accent,
12671
+ borderRadius: "50%",
12672
+ height: 10,
12673
+ width: 10
12674
+ }
12620
12675
  }, undefined, false, undefined, this),
12621
12676
  /* @__PURE__ */ jsxDEV22("strong", {
12622
12677
  style: { fontSize: 15 },
12623
- children: title ?? "Voice"
12678
+ children: model.title
12624
12679
  }, undefined, false, undefined, this),
12625
12680
  /* @__PURE__ */ jsxDEV22("span", {
12626
12681
  style: {
@@ -12628,11 +12683,11 @@ var VoiceWidget = ({
12628
12683
  marginLeft: "auto",
12629
12684
  opacity: 0.7
12630
12685
  },
12631
- children: controller.error ? "Error" : connecting ? labels.connecting : controller.status === "completed" ? labels.callEnded : stateLabel(agentState, labels)
12686
+ children: model.statusLabel
12632
12687
  }, undefined, false, undefined, this)
12633
12688
  ]
12634
12689
  }, undefined, true, undefined, this),
12635
- controller.partial ? /* @__PURE__ */ jsxDEV22("p", {
12690
+ model.partial ? /* @__PURE__ */ jsxDEV22("p", {
12636
12691
  style: {
12637
12692
  fontSize: 13,
12638
12693
  margin: "8px 0 12px",
@@ -12641,40 +12696,44 @@ var VoiceWidget = ({
12641
12696
  },
12642
12697
  children: [
12643
12698
  "\u201C",
12644
- controller.partial,
12699
+ model.partial,
12645
12700
  "\u201D"
12646
12701
  ]
12647
12702
  }, undefined, true, undefined, this) : null,
12648
12703
  /* @__PURE__ */ jsxDEV22("div", {
12649
12704
  style: { display: "flex", gap: 10 },
12650
12705
  children: [
12651
- showStart ? /* @__PURE__ */ jsxDEV22("button", {
12652
- onClick: handleStart,
12653
- style: buttonStyle("primary"),
12706
+ model.controls.canStart ? /* @__PURE__ */ jsxDEV22("button", {
12707
+ onClick: () => {
12708
+ controller.startRecording();
12709
+ },
12710
+ style: buttonStyle("primary", model.theme),
12654
12711
  type: "button",
12655
- children: labels.startCall
12712
+ children: model.labels.startCall
12656
12713
  }, undefined, false, undefined, this) : null,
12657
- showStop ? /* @__PURE__ */ jsxDEV22("button", {
12658
- onClick: handleStop,
12659
- style: buttonStyle("secondary"),
12714
+ model.controls.canMute ? /* @__PURE__ */ jsxDEV22("button", {
12715
+ onClick: () => controller.stopRecording(),
12716
+ style: buttonStyle("secondary", model.theme),
12660
12717
  type: "button",
12661
- children: labels.mute
12718
+ children: model.labels.mute
12662
12719
  }, undefined, false, undefined, this) : null,
12663
- controller.isConnected ? /* @__PURE__ */ jsxDEV22("button", {
12664
- onClick: handleEnd,
12665
- style: buttonStyle("danger"),
12720
+ model.controls.canEnd ? /* @__PURE__ */ jsxDEV22("button", {
12721
+ onClick: () => {
12722
+ controller.close();
12723
+ },
12724
+ style: buttonStyle("danger", model.theme),
12666
12725
  type: "button",
12667
- children: labels.endCall
12726
+ children: model.labels.endCall
12668
12727
  }, undefined, false, undefined, this) : null
12669
12728
  ]
12670
12729
  }, undefined, true, undefined, this),
12671
- controller.error ? /* @__PURE__ */ jsxDEV22("p", {
12730
+ model.errorMessage ? /* @__PURE__ */ jsxDEV22("p", {
12672
12731
  style: {
12673
- color: theme.errorAccent,
12732
+ color: model.theme.errorAccent,
12674
12733
  fontSize: 12,
12675
12734
  marginTop: 12
12676
12735
  },
12677
- children: controller.error
12736
+ children: model.errorMessage
12678
12737
  }, undefined, false, undefined, this) : null
12679
12738
  ]
12680
12739
  }, undefined, true, undefined, this);
@@ -0,0 +1,19 @@
1
+ import type { VoiceControllerOptions } from "../types";
2
+ import { type VoiceWidgetLabels, type VoiceWidgetTheme, type VoiceWidgetViewModel } from "../client/voiceWidgetView";
3
+ export type CreateVoiceWidgetOptions = VoiceControllerOptions & {
4
+ labels?: VoiceWidgetLabels;
5
+ theme?: VoiceWidgetTheme;
6
+ title?: string;
7
+ };
8
+ export declare const createVoiceWidget: <TResult = unknown>(path: string, options?: CreateVoiceWidgetOptions) => {
9
+ close: () => void;
10
+ endCall: () => void;
11
+ getHTML: () => string;
12
+ getSnapshot: () => import("..").VoiceControllerState<TResult>;
13
+ getViewModel: () => VoiceWidgetViewModel;
14
+ mute: () => void;
15
+ startCall: () => Promise<void>;
16
+ subscribe: (subscriber: () => void) => () => void;
17
+ unmute: () => Promise<void>;
18
+ };
19
+ export type { VoiceWidgetLabels, VoiceWidgetTheme, VoiceWidgetViewModel, } from "../client/voiceWidgetView";
@@ -1,4 +1,6 @@
1
1
  export { createVoiceCampaignDialerProof } from "./createVoiceCampaignDialerProof";
2
+ export { createVoiceWidget } from "./createVoiceWidget";
3
+ export type { CreateVoiceWidgetOptions, VoiceWidgetLabels, VoiceWidgetTheme, VoiceWidgetViewModel, } from "./createVoiceWidget";
2
4
  export { createVoiceDeliveryRuntime } from "./createVoiceDeliveryRuntime";
3
5
  export { createVoiceOpsActionCenter } from "./createVoiceOpsActionCenter";
4
6
  export { createVoiceLiveOps } from "./createVoiceLiveOps";