@absolutejs/voice 0.0.22-beta.66 → 0.0.22-beta.68

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.
@@ -1495,9 +1495,299 @@ var VoiceRoutingStatus = ({
1495
1495
  ]
1496
1496
  }, undefined, true, undefined, this);
1497
1497
  };
1498
- // src/react/useVoiceStream.tsx
1498
+ // src/react/useVoiceTurnQuality.tsx
1499
1499
  import { useEffect as useEffect6, useRef as useRef6, useSyncExternalStore as useSyncExternalStore6 } from "react";
1500
1500
 
1501
+ // src/client/turnQuality.ts
1502
+ var fetchVoiceTurnQuality = async (path = "/api/turn-quality", options = {}) => {
1503
+ const fetchImpl = options.fetch ?? globalThis.fetch;
1504
+ const response = await fetchImpl(path);
1505
+ if (!response.ok) {
1506
+ throw new Error(`Voice turn quality failed: HTTP ${response.status}`);
1507
+ }
1508
+ return await response.json();
1509
+ };
1510
+ var createVoiceTurnQualityStore = (path = "/api/turn-quality", options = {}) => {
1511
+ const listeners = new Set;
1512
+ let closed = false;
1513
+ let timer;
1514
+ let snapshot = {
1515
+ error: null,
1516
+ isLoading: false
1517
+ };
1518
+ const emit = () => {
1519
+ for (const listener of listeners) {
1520
+ listener();
1521
+ }
1522
+ };
1523
+ const refresh = async () => {
1524
+ if (closed) {
1525
+ return snapshot.report;
1526
+ }
1527
+ snapshot = {
1528
+ ...snapshot,
1529
+ error: null,
1530
+ isLoading: true
1531
+ };
1532
+ emit();
1533
+ try {
1534
+ const report = await fetchVoiceTurnQuality(path, options);
1535
+ snapshot = {
1536
+ error: null,
1537
+ isLoading: false,
1538
+ report,
1539
+ updatedAt: Date.now()
1540
+ };
1541
+ emit();
1542
+ return report;
1543
+ } catch (error) {
1544
+ snapshot = {
1545
+ ...snapshot,
1546
+ error: error instanceof Error ? error.message : String(error),
1547
+ isLoading: false
1548
+ };
1549
+ emit();
1550
+ throw error;
1551
+ }
1552
+ };
1553
+ const close = () => {
1554
+ closed = true;
1555
+ if (timer) {
1556
+ clearInterval(timer);
1557
+ timer = undefined;
1558
+ }
1559
+ listeners.clear();
1560
+ };
1561
+ if (options.intervalMs && options.intervalMs > 0) {
1562
+ timer = setInterval(() => {
1563
+ refresh().catch(() => {});
1564
+ }, options.intervalMs);
1565
+ }
1566
+ return {
1567
+ close,
1568
+ getServerSnapshot: () => snapshot,
1569
+ getSnapshot: () => snapshot,
1570
+ refresh,
1571
+ subscribe: (listener) => {
1572
+ listeners.add(listener);
1573
+ return () => {
1574
+ listeners.delete(listener);
1575
+ };
1576
+ }
1577
+ };
1578
+ };
1579
+
1580
+ // src/react/useVoiceTurnQuality.tsx
1581
+ var useVoiceTurnQuality = (path = "/api/turn-quality", options = {}) => {
1582
+ const storeRef = useRef6(null);
1583
+ if (!storeRef.current) {
1584
+ storeRef.current = createVoiceTurnQualityStore(path, options);
1585
+ }
1586
+ const store = storeRef.current;
1587
+ useEffect6(() => {
1588
+ store.refresh().catch(() => {});
1589
+ return () => store.close();
1590
+ }, [store]);
1591
+ return {
1592
+ ...useSyncExternalStore6(store.subscribe, store.getSnapshot, store.getServerSnapshot),
1593
+ refresh: store.refresh
1594
+ };
1595
+ };
1596
+
1597
+ // src/client/turnQualityWidget.ts
1598
+ var DEFAULT_TITLE5 = "Turn Quality";
1599
+ var DEFAULT_DESCRIPTION5 = "Per-turn STT confidence, fallback selection, corrections, and transcript coverage.";
1600
+ var escapeHtml6 = (value) => value.replaceAll("&", "&amp;").replaceAll("<", "&lt;").replaceAll(">", "&gt;").replaceAll('"', "&quot;").replaceAll("'", "&#39;");
1601
+ var formatConfidence = (value) => typeof value === "number" ? `${Math.round(value * 100)}%` : "n/a";
1602
+ var formatMaybe = (value) => value === undefined || value === "" ? "n/a" : String(value);
1603
+ var getTurnDetail = (turn) => {
1604
+ if (turn.status === "fail") {
1605
+ return "Empty or unusable committed turn; inspect transcripts and adapter events.";
1606
+ }
1607
+ if (turn.fallbackUsed) {
1608
+ return `Fallback STT selected${turn.fallbackSelectionReason ? ` by ${turn.fallbackSelectionReason}` : ""}.`;
1609
+ }
1610
+ if (turn.correctionChanged) {
1611
+ return `Correction changed the turn${turn.correctionProvider ? ` via ${turn.correctionProvider}` : ""}.`;
1612
+ }
1613
+ if (turn.status === "warn") {
1614
+ return "Turn completed with quality warnings.";
1615
+ }
1616
+ if (turn.status === "unknown") {
1617
+ return "No quality diagnostics were recorded for this turn.";
1618
+ }
1619
+ return "Turn quality looks healthy.";
1620
+ };
1621
+ var createVoiceTurnQualityViewModel = (snapshot, options = {}) => {
1622
+ const turns = (snapshot.report?.turns ?? []).map((turn) => ({
1623
+ ...turn,
1624
+ detail: getTurnDetail(turn),
1625
+ label: turn.text || "Empty turn",
1626
+ rows: [
1627
+ { label: "Source", value: turn.source ?? "unknown" },
1628
+ { label: "Confidence", value: formatConfidence(turn.averageConfidence) },
1629
+ { label: "Fallback", value: turn.fallbackUsed ? "Yes" : "No" },
1630
+ { label: "Correction", value: turn.correctionChanged ? "Changed" : "None" },
1631
+ { label: "Transcripts", value: `${turn.selectedTranscriptCount} selected` },
1632
+ { label: "Cost", value: formatMaybe(turn.costUnits) }
1633
+ ]
1634
+ }));
1635
+ const warningCount = snapshot.report?.warnings ?? turns.filter((turn) => turn.status === "warn").length;
1636
+ const failedCount = snapshot.report?.failed ?? turns.filter((turn) => turn.status === "fail").length;
1637
+ return {
1638
+ description: options.description ?? DEFAULT_DESCRIPTION5,
1639
+ error: snapshot.error,
1640
+ isLoading: snapshot.isLoading,
1641
+ label: snapshot.error ? "Unavailable" : turns.length ? failedCount > 0 ? `${failedCount} failed` : warningCount > 0 ? `${warningCount} warnings` : `${turns.length} healthy` : snapshot.isLoading ? "Checking" : "No turns",
1642
+ status: snapshot.error ? "error" : turns.length ? failedCount > 0 || warningCount > 0 ? "warning" : "ready" : snapshot.isLoading ? "loading" : "empty",
1643
+ title: options.title ?? DEFAULT_TITLE5,
1644
+ turns,
1645
+ updatedAt: snapshot.updatedAt
1646
+ };
1647
+ };
1648
+ var renderVoiceTurnQualityHTML = (snapshot, options = {}) => {
1649
+ const model = createVoiceTurnQualityViewModel(snapshot, options);
1650
+ const turns = model.turns.length ? `<div class="absolute-voice-turn-quality__turns">${model.turns.map((turn) => `<article class="absolute-voice-turn-quality__turn absolute-voice-turn-quality__turn--${escapeHtml6(turn.status)}">
1651
+ <header>
1652
+ <strong>${escapeHtml6(turn.label)}</strong>
1653
+ <span>${escapeHtml6(turn.status)}</span>
1654
+ </header>
1655
+ <p>${escapeHtml6(turn.detail)}</p>
1656
+ <dl>${turn.rows.map((row) => `<div>
1657
+ <dt>${escapeHtml6(row.label)}</dt>
1658
+ <dd>${escapeHtml6(row.value)}</dd>
1659
+ </div>`).join("")}</dl>
1660
+ </article>`).join("")}</div>` : '<p class="absolute-voice-turn-quality__empty">Complete a voice turn to see STT quality diagnostics.</p>';
1661
+ return `<section class="absolute-voice-turn-quality absolute-voice-turn-quality--${escapeHtml6(model.status)}">
1662
+ <header class="absolute-voice-turn-quality__header">
1663
+ <span class="absolute-voice-turn-quality__eyebrow">${escapeHtml6(model.title)}</span>
1664
+ <strong class="absolute-voice-turn-quality__label">${escapeHtml6(model.label)}</strong>
1665
+ </header>
1666
+ <p class="absolute-voice-turn-quality__description">${escapeHtml6(model.description)}</p>
1667
+ ${turns}
1668
+ ${model.error ? `<p class="absolute-voice-turn-quality__error">${escapeHtml6(model.error)}</p>` : ""}
1669
+ </section>`;
1670
+ };
1671
+ var getVoiceTurnQualityCSS = () => `.absolute-voice-turn-quality{border:1px solid #e4d1a3;border-radius:20px;background:#fff9eb;color:#17120a;padding:18px;box-shadow:0 18px 40px rgba(73,48,14,.12);font-family:inherit}.absolute-voice-turn-quality--error,.absolute-voice-turn-quality--warning{border-color:#f2a7a7;background:#fff5f3}.absolute-voice-turn-quality__header,.absolute-voice-turn-quality__turn header{align-items:start;display:flex;gap:12px;justify-content:space-between}.absolute-voice-turn-quality__eyebrow{color:#8a5a0a;font-size:12px;font-weight:800;letter-spacing:.08em;text-transform:uppercase}.absolute-voice-turn-quality__label{font-size:24px;line-height:1}.absolute-voice-turn-quality__description,.absolute-voice-turn-quality__turn p,.absolute-voice-turn-quality__turn dt,.absolute-voice-turn-quality__empty{color:#5a4930}.absolute-voice-turn-quality__turns{display:grid;gap:12px;margin-top:14px}.absolute-voice-turn-quality__turn{background:#fff;border:1px solid #f0dfba;border-radius:16px;padding:14px}.absolute-voice-turn-quality__turn--pass{border-color:#86efac}.absolute-voice-turn-quality__turn--warn,.absolute-voice-turn-quality__turn--unknown{border-color:#fbbf24}.absolute-voice-turn-quality__turn--fail{border-color:#f2a7a7}.absolute-voice-turn-quality__turn p{margin:10px 0}.absolute-voice-turn-quality__turn dl{display:grid;gap:8px;grid-template-columns:repeat(2,minmax(0,1fr));margin:0}.absolute-voice-turn-quality__turn div{background:#fff9eb;border:1px solid #f0dfba;border-radius:12px;padding:8px}.absolute-voice-turn-quality__turn dt{font-size:12px}.absolute-voice-turn-quality__turn dd{font-weight:800;margin:4px 0 0}.absolute-voice-turn-quality__empty{margin:14px 0 0}.absolute-voice-turn-quality__error{color:#9f1239;font-weight:700}`;
1672
+ var mountVoiceTurnQuality = (element, path = "/api/turn-quality", options = {}) => {
1673
+ const store = createVoiceTurnQualityStore(path, options);
1674
+ const render = () => {
1675
+ element.innerHTML = renderVoiceTurnQualityHTML(store.getSnapshot(), options);
1676
+ };
1677
+ const unsubscribe = store.subscribe(render);
1678
+ render();
1679
+ store.refresh().catch(() => {});
1680
+ return {
1681
+ close: () => {
1682
+ unsubscribe();
1683
+ store.close();
1684
+ },
1685
+ refresh: store.refresh
1686
+ };
1687
+ };
1688
+ var defineVoiceTurnQualityElement = (tagName = "absolute-voice-turn-quality") => {
1689
+ if (typeof window === "undefined" || typeof customElements === "undefined" || customElements.get(tagName)) {
1690
+ return;
1691
+ }
1692
+ customElements.define(tagName, class AbsoluteVoiceTurnQualityElement extends HTMLElement {
1693
+ mounted;
1694
+ connectedCallback() {
1695
+ const intervalMs = Number(this.getAttribute("interval-ms") ?? 5000);
1696
+ this.mounted = mountVoiceTurnQuality(this, this.getAttribute("path") ?? "/api/turn-quality", {
1697
+ description: this.getAttribute("description") ?? undefined,
1698
+ intervalMs: Number.isFinite(intervalMs) ? intervalMs : 5000,
1699
+ title: this.getAttribute("title") ?? undefined
1700
+ });
1701
+ }
1702
+ disconnectedCallback() {
1703
+ this.mounted?.close();
1704
+ this.mounted = undefined;
1705
+ }
1706
+ });
1707
+ };
1708
+
1709
+ // src/react/VoiceTurnQuality.tsx
1710
+ import { jsxDEV as jsxDEV6 } from "react/jsx-dev-runtime";
1711
+ var VoiceTurnQuality = ({
1712
+ className,
1713
+ path = "/api/turn-quality",
1714
+ ...options
1715
+ }) => {
1716
+ const snapshot = useVoiceTurnQuality(path, options);
1717
+ const model = createVoiceTurnQualityViewModel(snapshot, options);
1718
+ return /* @__PURE__ */ jsxDEV6("section", {
1719
+ className: [
1720
+ "absolute-voice-turn-quality",
1721
+ `absolute-voice-turn-quality--${model.status}`,
1722
+ className
1723
+ ].filter(Boolean).join(" "),
1724
+ children: [
1725
+ /* @__PURE__ */ jsxDEV6("header", {
1726
+ className: "absolute-voice-turn-quality__header",
1727
+ children: [
1728
+ /* @__PURE__ */ jsxDEV6("span", {
1729
+ className: "absolute-voice-turn-quality__eyebrow",
1730
+ children: model.title
1731
+ }, undefined, false, undefined, this),
1732
+ /* @__PURE__ */ jsxDEV6("strong", {
1733
+ className: "absolute-voice-turn-quality__label",
1734
+ children: model.label
1735
+ }, undefined, false, undefined, this)
1736
+ ]
1737
+ }, undefined, true, undefined, this),
1738
+ /* @__PURE__ */ jsxDEV6("p", {
1739
+ className: "absolute-voice-turn-quality__description",
1740
+ children: model.description
1741
+ }, undefined, false, undefined, this),
1742
+ model.turns.length ? /* @__PURE__ */ jsxDEV6("div", {
1743
+ className: "absolute-voice-turn-quality__turns",
1744
+ children: model.turns.map((turn) => /* @__PURE__ */ jsxDEV6("article", {
1745
+ className: [
1746
+ "absolute-voice-turn-quality__turn",
1747
+ `absolute-voice-turn-quality__turn--${turn.status}`
1748
+ ].join(" "),
1749
+ children: [
1750
+ /* @__PURE__ */ jsxDEV6("header", {
1751
+ children: [
1752
+ /* @__PURE__ */ jsxDEV6("strong", {
1753
+ children: turn.label
1754
+ }, undefined, false, undefined, this),
1755
+ /* @__PURE__ */ jsxDEV6("span", {
1756
+ children: turn.status
1757
+ }, undefined, false, undefined, this)
1758
+ ]
1759
+ }, undefined, true, undefined, this),
1760
+ /* @__PURE__ */ jsxDEV6("p", {
1761
+ children: turn.detail
1762
+ }, undefined, false, undefined, this),
1763
+ /* @__PURE__ */ jsxDEV6("dl", {
1764
+ children: turn.rows.map((row) => /* @__PURE__ */ jsxDEV6("div", {
1765
+ children: [
1766
+ /* @__PURE__ */ jsxDEV6("dt", {
1767
+ children: row.label
1768
+ }, undefined, false, undefined, this),
1769
+ /* @__PURE__ */ jsxDEV6("dd", {
1770
+ children: row.value
1771
+ }, undefined, false, undefined, this)
1772
+ ]
1773
+ }, row.label, true, undefined, this))
1774
+ }, undefined, false, undefined, this)
1775
+ ]
1776
+ }, `${turn.sessionId}:${turn.turnId}`, true, undefined, this))
1777
+ }, undefined, false, undefined, this) : /* @__PURE__ */ jsxDEV6("p", {
1778
+ className: "absolute-voice-turn-quality__empty",
1779
+ children: "Complete a voice turn to see STT quality diagnostics."
1780
+ }, undefined, false, undefined, this),
1781
+ model.error ? /* @__PURE__ */ jsxDEV6("p", {
1782
+ className: "absolute-voice-turn-quality__error",
1783
+ children: model.error
1784
+ }, undefined, false, undefined, this) : null
1785
+ ]
1786
+ }, undefined, true, undefined, this);
1787
+ };
1788
+ // src/react/useVoiceStream.tsx
1789
+ import { useEffect as useEffect7, useRef as useRef7, useSyncExternalStore as useSyncExternalStore7 } from "react";
1790
+
1501
1791
  // src/client/actions.ts
1502
1792
  var normalizeErrorMessage = (value) => {
1503
1793
  if (typeof value === "string" && value.trim()) {
@@ -2024,13 +2314,13 @@ var EMPTY_SNAPSHOT = {
2024
2314
  turns: []
2025
2315
  };
2026
2316
  var useVoiceStream = (path, options = {}) => {
2027
- const streamRef = useRef6(null);
2317
+ const streamRef = useRef7(null);
2028
2318
  if (!streamRef.current) {
2029
2319
  streamRef.current = createVoiceStream(path, options);
2030
2320
  }
2031
2321
  const stream = streamRef.current;
2032
- useEffect6(() => () => stream.close(), [stream]);
2033
- const snapshot = useSyncExternalStore6(stream.subscribe, stream.getSnapshot, stream.getServerSnapshot) ?? EMPTY_SNAPSHOT;
2322
+ useEffect7(() => () => stream.close(), [stream]);
2323
+ const snapshot = useSyncExternalStore7(stream.subscribe, stream.getSnapshot, stream.getServerSnapshot) ?? EMPTY_SNAPSHOT;
2034
2324
  return {
2035
2325
  ...snapshot,
2036
2326
  callControl: (message) => stream.callControl(message),
@@ -2040,7 +2330,7 @@ var useVoiceStream = (path, options = {}) => {
2040
2330
  };
2041
2331
  };
2042
2332
  // src/react/useVoiceController.tsx
2043
- import { useEffect as useEffect7, useRef as useRef7, useSyncExternalStore as useSyncExternalStore7 } from "react";
2333
+ import { useEffect as useEffect8, useRef as useRef8, useSyncExternalStore as useSyncExternalStore8 } from "react";
2044
2334
 
2045
2335
  // src/client/htmx.ts
2046
2336
  var DEFAULT_EVENT_NAME = "voice-refresh";
@@ -2687,13 +2977,13 @@ var EMPTY_SNAPSHOT2 = {
2687
2977
  turns: []
2688
2978
  };
2689
2979
  var useVoiceController = (path, options = {}) => {
2690
- const controllerRef = useRef7(null);
2980
+ const controllerRef = useRef8(null);
2691
2981
  if (!controllerRef.current) {
2692
2982
  controllerRef.current = createVoiceController(path, options);
2693
2983
  }
2694
2984
  const controller = controllerRef.current;
2695
- useEffect7(() => () => controller.close(), [controller]);
2696
- const snapshot = useSyncExternalStore7(controller.subscribe, controller.getSnapshot, controller.getServerSnapshot) ?? EMPTY_SNAPSHOT2;
2985
+ useEffect8(() => () => controller.close(), [controller]);
2986
+ const snapshot = useSyncExternalStore8(controller.subscribe, controller.getSnapshot, controller.getServerSnapshot) ?? EMPTY_SNAPSHOT2;
2697
2987
  return {
2698
2988
  ...snapshot,
2699
2989
  bindHTMX: controller.bindHTMX,
@@ -2707,7 +2997,7 @@ var useVoiceController = (path, options = {}) => {
2707
2997
  };
2708
2998
  };
2709
2999
  // src/react/useVoiceWorkflowStatus.tsx
2710
- import { useEffect as useEffect8, useRef as useRef8, useSyncExternalStore as useSyncExternalStore8 } from "react";
3000
+ import { useEffect as useEffect9, useRef as useRef9, useSyncExternalStore as useSyncExternalStore9 } from "react";
2711
3001
 
2712
3002
  // src/client/workflowStatus.ts
2713
3003
  var fetchVoiceWorkflowStatus = async (path = "/evals/scenarios/json", options = {}) => {
@@ -2790,22 +3080,23 @@ var createVoiceWorkflowStatusStore = (path = "/evals/scenarios/json", options =
2790
3080
 
2791
3081
  // src/react/useVoiceWorkflowStatus.tsx
2792
3082
  var useVoiceWorkflowStatus = (path = "/evals/scenarios/json", options = {}) => {
2793
- const storeRef = useRef8(null);
3083
+ const storeRef = useRef9(null);
2794
3084
  if (!storeRef.current) {
2795
3085
  storeRef.current = createVoiceWorkflowStatusStore(path, options);
2796
3086
  }
2797
3087
  const store = storeRef.current;
2798
- useEffect8(() => {
3088
+ useEffect9(() => {
2799
3089
  store.refresh().catch(() => {});
2800
3090
  return () => store.close();
2801
3091
  }, [store]);
2802
3092
  return {
2803
- ...useSyncExternalStore8(store.subscribe, store.getSnapshot, store.getServerSnapshot),
3093
+ ...useSyncExternalStore9(store.subscribe, store.getSnapshot, store.getServerSnapshot),
2804
3094
  refresh: store.refresh
2805
3095
  };
2806
3096
  };
2807
3097
  export {
2808
3098
  useVoiceWorkflowStatus,
3099
+ useVoiceTurnQuality,
2809
3100
  useVoiceStream,
2810
3101
  useVoiceRoutingStatus,
2811
3102
  useVoiceProviderStatus,
@@ -2813,6 +3104,7 @@ export {
2813
3104
  useVoiceProviderCapabilities,
2814
3105
  useVoiceController,
2815
3106
  useVoiceAppKitStatus,
3107
+ VoiceTurnQuality,
2816
3108
  VoiceRoutingStatus,
2817
3109
  VoiceProviderStatus,
2818
3110
  VoiceProviderSimulationControls,
@@ -0,0 +1,8 @@
1
+ import { type VoiceTurnQualityClientOptions } from '../client/turnQuality';
2
+ export declare const useVoiceTurnQuality: (path?: string, options?: VoiceTurnQualityClientOptions) => {
3
+ refresh: () => Promise<import("..").VoiceTurnQualityReport | undefined>;
4
+ error: string | null;
5
+ isLoading: boolean;
6
+ report?: import("..").VoiceTurnQualityReport;
7
+ updatedAt?: number;
8
+ };
@@ -0,0 +1,10 @@
1
+ import { type VoiceTurnQualityWidgetOptions } from '../client/turnQualityWidget';
2
+ export declare const createVoiceTurnQuality: (path?: string, options?: VoiceTurnQualityWidgetOptions) => {
3
+ getHTML: () => string;
4
+ getViewModel: () => import("../client").VoiceTurnQualityViewModel;
5
+ close: () => void;
6
+ getServerSnapshot: () => import("../client").VoiceTurnQualitySnapshot;
7
+ getSnapshot: () => import("../client").VoiceTurnQualitySnapshot;
8
+ refresh: () => Promise<import("..").VoiceTurnQualityReport | undefined>;
9
+ subscribe: (listener: () => void) => () => void;
10
+ };
@@ -5,5 +5,6 @@ export { createVoiceProviderCapabilities } from './createVoiceProviderCapabiliti
5
5
  export { createVoiceStream } from './createVoiceStream';
6
6
  export { createVoiceProviderStatus } from './createVoiceProviderStatus';
7
7
  export { createVoiceRoutingStatus } from './createVoiceRoutingStatus';
8
+ export { createVoiceTurnQuality } from './createVoiceTurnQuality';
8
9
  export { createVoiceWorkflowStatus } from './createVoiceWorkflowStatus';
9
10
  export { createVoiceController } from '../client/controller';
@@ -1580,6 +1580,206 @@ var createVoiceRoutingStatus = (path = "/api/routing/latest", options = {}) => {
1580
1580
  getViewModel: () => createVoiceRoutingStatusViewModel(store.getSnapshot(), options)
1581
1581
  };
1582
1582
  };
1583
+ // src/client/turnQuality.ts
1584
+ var fetchVoiceTurnQuality = async (path = "/api/turn-quality", options = {}) => {
1585
+ const fetchImpl = options.fetch ?? globalThis.fetch;
1586
+ const response = await fetchImpl(path);
1587
+ if (!response.ok) {
1588
+ throw new Error(`Voice turn quality failed: HTTP ${response.status}`);
1589
+ }
1590
+ return await response.json();
1591
+ };
1592
+ var createVoiceTurnQualityStore = (path = "/api/turn-quality", options = {}) => {
1593
+ const listeners = new Set;
1594
+ let closed = false;
1595
+ let timer;
1596
+ let snapshot = {
1597
+ error: null,
1598
+ isLoading: false
1599
+ };
1600
+ const emit = () => {
1601
+ for (const listener of listeners) {
1602
+ listener();
1603
+ }
1604
+ };
1605
+ const refresh = async () => {
1606
+ if (closed) {
1607
+ return snapshot.report;
1608
+ }
1609
+ snapshot = {
1610
+ ...snapshot,
1611
+ error: null,
1612
+ isLoading: true
1613
+ };
1614
+ emit();
1615
+ try {
1616
+ const report = await fetchVoiceTurnQuality(path, options);
1617
+ snapshot = {
1618
+ error: null,
1619
+ isLoading: false,
1620
+ report,
1621
+ updatedAt: Date.now()
1622
+ };
1623
+ emit();
1624
+ return report;
1625
+ } catch (error) {
1626
+ snapshot = {
1627
+ ...snapshot,
1628
+ error: error instanceof Error ? error.message : String(error),
1629
+ isLoading: false
1630
+ };
1631
+ emit();
1632
+ throw error;
1633
+ }
1634
+ };
1635
+ const close = () => {
1636
+ closed = true;
1637
+ if (timer) {
1638
+ clearInterval(timer);
1639
+ timer = undefined;
1640
+ }
1641
+ listeners.clear();
1642
+ };
1643
+ if (options.intervalMs && options.intervalMs > 0) {
1644
+ timer = setInterval(() => {
1645
+ refresh().catch(() => {});
1646
+ }, options.intervalMs);
1647
+ }
1648
+ return {
1649
+ close,
1650
+ getServerSnapshot: () => snapshot,
1651
+ getSnapshot: () => snapshot,
1652
+ refresh,
1653
+ subscribe: (listener) => {
1654
+ listeners.add(listener);
1655
+ return () => {
1656
+ listeners.delete(listener);
1657
+ };
1658
+ }
1659
+ };
1660
+ };
1661
+
1662
+ // src/client/turnQualityWidget.ts
1663
+ var DEFAULT_TITLE5 = "Turn Quality";
1664
+ var DEFAULT_DESCRIPTION5 = "Per-turn STT confidence, fallback selection, corrections, and transcript coverage.";
1665
+ var escapeHtml6 = (value) => value.replaceAll("&", "&amp;").replaceAll("<", "&lt;").replaceAll(">", "&gt;").replaceAll('"', "&quot;").replaceAll("'", "&#39;");
1666
+ var formatConfidence = (value) => typeof value === "number" ? `${Math.round(value * 100)}%` : "n/a";
1667
+ var formatMaybe = (value) => value === undefined || value === "" ? "n/a" : String(value);
1668
+ var getTurnDetail = (turn) => {
1669
+ if (turn.status === "fail") {
1670
+ return "Empty or unusable committed turn; inspect transcripts and adapter events.";
1671
+ }
1672
+ if (turn.fallbackUsed) {
1673
+ return `Fallback STT selected${turn.fallbackSelectionReason ? ` by ${turn.fallbackSelectionReason}` : ""}.`;
1674
+ }
1675
+ if (turn.correctionChanged) {
1676
+ return `Correction changed the turn${turn.correctionProvider ? ` via ${turn.correctionProvider}` : ""}.`;
1677
+ }
1678
+ if (turn.status === "warn") {
1679
+ return "Turn completed with quality warnings.";
1680
+ }
1681
+ if (turn.status === "unknown") {
1682
+ return "No quality diagnostics were recorded for this turn.";
1683
+ }
1684
+ return "Turn quality looks healthy.";
1685
+ };
1686
+ var createVoiceTurnQualityViewModel = (snapshot, options = {}) => {
1687
+ const turns = (snapshot.report?.turns ?? []).map((turn) => ({
1688
+ ...turn,
1689
+ detail: getTurnDetail(turn),
1690
+ label: turn.text || "Empty turn",
1691
+ rows: [
1692
+ { label: "Source", value: turn.source ?? "unknown" },
1693
+ { label: "Confidence", value: formatConfidence(turn.averageConfidence) },
1694
+ { label: "Fallback", value: turn.fallbackUsed ? "Yes" : "No" },
1695
+ { label: "Correction", value: turn.correctionChanged ? "Changed" : "None" },
1696
+ { label: "Transcripts", value: `${turn.selectedTranscriptCount} selected` },
1697
+ { label: "Cost", value: formatMaybe(turn.costUnits) }
1698
+ ]
1699
+ }));
1700
+ const warningCount = snapshot.report?.warnings ?? turns.filter((turn) => turn.status === "warn").length;
1701
+ const failedCount = snapshot.report?.failed ?? turns.filter((turn) => turn.status === "fail").length;
1702
+ return {
1703
+ description: options.description ?? DEFAULT_DESCRIPTION5,
1704
+ error: snapshot.error,
1705
+ isLoading: snapshot.isLoading,
1706
+ label: snapshot.error ? "Unavailable" : turns.length ? failedCount > 0 ? `${failedCount} failed` : warningCount > 0 ? `${warningCount} warnings` : `${turns.length} healthy` : snapshot.isLoading ? "Checking" : "No turns",
1707
+ status: snapshot.error ? "error" : turns.length ? failedCount > 0 || warningCount > 0 ? "warning" : "ready" : snapshot.isLoading ? "loading" : "empty",
1708
+ title: options.title ?? DEFAULT_TITLE5,
1709
+ turns,
1710
+ updatedAt: snapshot.updatedAt
1711
+ };
1712
+ };
1713
+ var renderVoiceTurnQualityHTML = (snapshot, options = {}) => {
1714
+ const model = createVoiceTurnQualityViewModel(snapshot, options);
1715
+ const turns = model.turns.length ? `<div class="absolute-voice-turn-quality__turns">${model.turns.map((turn) => `<article class="absolute-voice-turn-quality__turn absolute-voice-turn-quality__turn--${escapeHtml6(turn.status)}">
1716
+ <header>
1717
+ <strong>${escapeHtml6(turn.label)}</strong>
1718
+ <span>${escapeHtml6(turn.status)}</span>
1719
+ </header>
1720
+ <p>${escapeHtml6(turn.detail)}</p>
1721
+ <dl>${turn.rows.map((row) => `<div>
1722
+ <dt>${escapeHtml6(row.label)}</dt>
1723
+ <dd>${escapeHtml6(row.value)}</dd>
1724
+ </div>`).join("")}</dl>
1725
+ </article>`).join("")}</div>` : '<p class="absolute-voice-turn-quality__empty">Complete a voice turn to see STT quality diagnostics.</p>';
1726
+ return `<section class="absolute-voice-turn-quality absolute-voice-turn-quality--${escapeHtml6(model.status)}">
1727
+ <header class="absolute-voice-turn-quality__header">
1728
+ <span class="absolute-voice-turn-quality__eyebrow">${escapeHtml6(model.title)}</span>
1729
+ <strong class="absolute-voice-turn-quality__label">${escapeHtml6(model.label)}</strong>
1730
+ </header>
1731
+ <p class="absolute-voice-turn-quality__description">${escapeHtml6(model.description)}</p>
1732
+ ${turns}
1733
+ ${model.error ? `<p class="absolute-voice-turn-quality__error">${escapeHtml6(model.error)}</p>` : ""}
1734
+ </section>`;
1735
+ };
1736
+ var getVoiceTurnQualityCSS = () => `.absolute-voice-turn-quality{border:1px solid #e4d1a3;border-radius:20px;background:#fff9eb;color:#17120a;padding:18px;box-shadow:0 18px 40px rgba(73,48,14,.12);font-family:inherit}.absolute-voice-turn-quality--error,.absolute-voice-turn-quality--warning{border-color:#f2a7a7;background:#fff5f3}.absolute-voice-turn-quality__header,.absolute-voice-turn-quality__turn header{align-items:start;display:flex;gap:12px;justify-content:space-between}.absolute-voice-turn-quality__eyebrow{color:#8a5a0a;font-size:12px;font-weight:800;letter-spacing:.08em;text-transform:uppercase}.absolute-voice-turn-quality__label{font-size:24px;line-height:1}.absolute-voice-turn-quality__description,.absolute-voice-turn-quality__turn p,.absolute-voice-turn-quality__turn dt,.absolute-voice-turn-quality__empty{color:#5a4930}.absolute-voice-turn-quality__turns{display:grid;gap:12px;margin-top:14px}.absolute-voice-turn-quality__turn{background:#fff;border:1px solid #f0dfba;border-radius:16px;padding:14px}.absolute-voice-turn-quality__turn--pass{border-color:#86efac}.absolute-voice-turn-quality__turn--warn,.absolute-voice-turn-quality__turn--unknown{border-color:#fbbf24}.absolute-voice-turn-quality__turn--fail{border-color:#f2a7a7}.absolute-voice-turn-quality__turn p{margin:10px 0}.absolute-voice-turn-quality__turn dl{display:grid;gap:8px;grid-template-columns:repeat(2,minmax(0,1fr));margin:0}.absolute-voice-turn-quality__turn div{background:#fff9eb;border:1px solid #f0dfba;border-radius:12px;padding:8px}.absolute-voice-turn-quality__turn dt{font-size:12px}.absolute-voice-turn-quality__turn dd{font-weight:800;margin:4px 0 0}.absolute-voice-turn-quality__empty{margin:14px 0 0}.absolute-voice-turn-quality__error{color:#9f1239;font-weight:700}`;
1737
+ var mountVoiceTurnQuality = (element, path = "/api/turn-quality", options = {}) => {
1738
+ const store = createVoiceTurnQualityStore(path, options);
1739
+ const render = () => {
1740
+ element.innerHTML = renderVoiceTurnQualityHTML(store.getSnapshot(), options);
1741
+ };
1742
+ const unsubscribe = store.subscribe(render);
1743
+ render();
1744
+ store.refresh().catch(() => {});
1745
+ return {
1746
+ close: () => {
1747
+ unsubscribe();
1748
+ store.close();
1749
+ },
1750
+ refresh: store.refresh
1751
+ };
1752
+ };
1753
+ var defineVoiceTurnQualityElement = (tagName = "absolute-voice-turn-quality") => {
1754
+ if (typeof window === "undefined" || typeof customElements === "undefined" || customElements.get(tagName)) {
1755
+ return;
1756
+ }
1757
+ customElements.define(tagName, class AbsoluteVoiceTurnQualityElement extends HTMLElement {
1758
+ mounted;
1759
+ connectedCallback() {
1760
+ const intervalMs = Number(this.getAttribute("interval-ms") ?? 5000);
1761
+ this.mounted = mountVoiceTurnQuality(this, this.getAttribute("path") ?? "/api/turn-quality", {
1762
+ description: this.getAttribute("description") ?? undefined,
1763
+ intervalMs: Number.isFinite(intervalMs) ? intervalMs : 5000,
1764
+ title: this.getAttribute("title") ?? undefined
1765
+ });
1766
+ }
1767
+ disconnectedCallback() {
1768
+ this.mounted?.close();
1769
+ this.mounted = undefined;
1770
+ }
1771
+ });
1772
+ };
1773
+
1774
+ // src/svelte/createVoiceTurnQuality.ts
1775
+ var createVoiceTurnQuality = (path = "/api/turn-quality", options = {}) => {
1776
+ const store = createVoiceTurnQualityStore(path, options);
1777
+ return {
1778
+ ...store,
1779
+ getHTML: () => renderVoiceTurnQualityHTML(store.getSnapshot(), options),
1780
+ getViewModel: () => createVoiceTurnQualityViewModel(store.getSnapshot(), options)
1781
+ };
1782
+ };
1583
1783
  // src/client/workflowStatus.ts
1584
1784
  var fetchVoiceWorkflowStatus = async (path = "/evals/scenarios/json", options = {}) => {
1585
1785
  const fetchImpl = options.fetch ?? globalThis.fetch;
@@ -2292,6 +2492,7 @@ var createVoiceController = (path, options = {}) => {
2292
2492
  };
2293
2493
  export {
2294
2494
  createVoiceWorkflowStatus,
2495
+ createVoiceTurnQuality,
2295
2496
  createVoiceStream2 as createVoiceStream,
2296
2497
  createVoiceRoutingStatus,
2297
2498
  createVoiceProviderStatus,