@absolutejs/voice 0.0.22-beta.65 → 0.0.22-beta.67

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,8 @@
1
+ import { type VoiceProviderCapabilitiesClientOptions } from '../client/providerCapabilities';
2
+ export declare const useVoiceProviderCapabilities: <TProvider extends string = string>(path?: string, options?: VoiceProviderCapabilitiesClientOptions) => {
3
+ refresh: () => Promise<import("..").VoiceProviderCapabilityReport<TProvider> | undefined>;
4
+ error: string | null;
5
+ isLoading: boolean;
6
+ report?: import("..").VoiceProviderCapabilityReport<TProvider> | undefined;
7
+ updatedAt?: number;
8
+ };
@@ -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 VoiceProviderCapabilitiesWidgetOptions } from '../client/providerCapabilitiesWidget';
2
+ export declare const createVoiceProviderCapabilities: <TProvider extends string = string>(path?: string, options?: VoiceProviderCapabilitiesWidgetOptions) => {
3
+ getHTML: () => string;
4
+ getViewModel: () => import("../client").VoiceProviderCapabilitiesViewModel<TProvider>;
5
+ close: () => void;
6
+ getServerSnapshot: () => import("../client").VoiceProviderCapabilitiesSnapshot<TProvider>;
7
+ getSnapshot: () => import("../client").VoiceProviderCapabilitiesSnapshot<TProvider>;
8
+ refresh: () => Promise<import("..").VoiceProviderCapabilityReport<TProvider> | undefined>;
9
+ subscribe: (listener: () => void) => () => void;
10
+ };
@@ -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
+ };
@@ -1,8 +1,10 @@
1
1
  export { createVoiceAppKitStatus } from './createVoiceAppKitStatus';
2
2
  export { createVoiceOpsStatus } from './createVoiceOpsStatus';
3
3
  export { createVoiceProviderSimulationControls } from './createVoiceProviderSimulationControls';
4
+ export { createVoiceProviderCapabilities } from './createVoiceProviderCapabilities';
4
5
  export { createVoiceStream } from './createVoiceStream';
5
6
  export { createVoiceProviderStatus } from './createVoiceProviderStatus';
6
7
  export { createVoiceRoutingStatus } from './createVoiceRoutingStatus';
8
+ export { createVoiceTurnQuality } from './createVoiceTurnQuality';
7
9
  export { createVoiceWorkflowStatus } from './createVoiceWorkflowStatus';
8
10
  export { createVoiceController } from '../client/controller';
@@ -473,6 +473,211 @@ var createVoiceProviderSimulationControls = (options) => {
473
473
  getViewModel: () => createVoiceProviderSimulationControlsViewModel(store.getSnapshot(), options)
474
474
  };
475
475
  };
476
+ // src/client/providerCapabilities.ts
477
+ var fetchVoiceProviderCapabilities = async (path = "/api/provider-capabilities", options = {}) => {
478
+ const fetchImpl = options.fetch ?? globalThis.fetch;
479
+ const response = await fetchImpl(path);
480
+ if (!response.ok) {
481
+ throw new Error(`Voice provider capabilities failed: HTTP ${response.status}`);
482
+ }
483
+ return await response.json();
484
+ };
485
+ var createVoiceProviderCapabilitiesStore = (path = "/api/provider-capabilities", options = {}) => {
486
+ const listeners = new Set;
487
+ let closed = false;
488
+ let timer;
489
+ let snapshot = {
490
+ error: null,
491
+ isLoading: false
492
+ };
493
+ const emit = () => {
494
+ for (const listener of listeners) {
495
+ listener();
496
+ }
497
+ };
498
+ const refresh = async () => {
499
+ if (closed) {
500
+ return snapshot.report;
501
+ }
502
+ snapshot = {
503
+ ...snapshot,
504
+ error: null,
505
+ isLoading: true
506
+ };
507
+ emit();
508
+ try {
509
+ const report = await fetchVoiceProviderCapabilities(path, options);
510
+ snapshot = {
511
+ error: null,
512
+ isLoading: false,
513
+ report,
514
+ updatedAt: Date.now()
515
+ };
516
+ emit();
517
+ return report;
518
+ } catch (error) {
519
+ snapshot = {
520
+ ...snapshot,
521
+ error: error instanceof Error ? error.message : String(error),
522
+ isLoading: false
523
+ };
524
+ emit();
525
+ throw error;
526
+ }
527
+ };
528
+ const close = () => {
529
+ closed = true;
530
+ if (timer) {
531
+ clearInterval(timer);
532
+ timer = undefined;
533
+ }
534
+ listeners.clear();
535
+ };
536
+ if (options.intervalMs && options.intervalMs > 0) {
537
+ timer = setInterval(() => {
538
+ refresh().catch(() => {});
539
+ }, options.intervalMs);
540
+ }
541
+ return {
542
+ close,
543
+ getServerSnapshot: () => snapshot,
544
+ getSnapshot: () => snapshot,
545
+ refresh,
546
+ subscribe: (listener) => {
547
+ listeners.add(listener);
548
+ return () => {
549
+ listeners.delete(listener);
550
+ };
551
+ }
552
+ };
553
+ };
554
+
555
+ // src/client/providerCapabilitiesWidget.ts
556
+ var DEFAULT_TITLE2 = "Provider Capabilities";
557
+ var DEFAULT_DESCRIPTION2 = "Configured, selected, and healthy voice providers for this deployment.";
558
+ var escapeHtml3 = (value) => value.replaceAll("&", "&amp;").replaceAll("<", "&lt;").replaceAll(">", "&gt;").replaceAll('"', "&quot;").replaceAll("'", "&#39;");
559
+ var formatProvider = (provider) => provider.split(/[-_\s]+/).filter(Boolean).map((part) => `${part[0]?.toUpperCase() ?? ""}${part.slice(1)}`).join(" ") || provider;
560
+ var formatKind2 = (kind) => kind.toUpperCase();
561
+ var formatStatus = (status) => status.split("-").map((part) => `${part[0]?.toUpperCase() ?? ""}${part.slice(1)}`).join(" ");
562
+ var getCapabilityDetail = (capability) => {
563
+ if (!capability.configured) {
564
+ return "Not configured in this deployment.";
565
+ }
566
+ if (capability.selected) {
567
+ return `Selected ${capability.kind.toUpperCase()} provider for new sessions.`;
568
+ }
569
+ if (capability.health?.status === "healthy") {
570
+ return "Configured and healthy fallback candidate.";
571
+ }
572
+ if (capability.health?.status === "idle") {
573
+ return "Configured; no traffic observed yet.";
574
+ }
575
+ if (capability.health?.lastError) {
576
+ return capability.health.lastError;
577
+ }
578
+ return "Configured and available.";
579
+ };
580
+ var isWarningStatus = (status) => status === "degraded" || status === "rate-limited" || status === "suppressed" || status === "unconfigured";
581
+ var createVoiceProviderCapabilitiesViewModel = (snapshot, options = {}) => {
582
+ const capabilities = (snapshot.report?.capabilities ?? []).map((capability) => ({
583
+ ...capability,
584
+ detail: getCapabilityDetail(capability),
585
+ label: `${formatProvider(capability.provider)} ${formatKind2(capability.kind)}`,
586
+ rows: [
587
+ { label: "Status", value: formatStatus(capability.status) },
588
+ { label: "Selected", value: capability.selected ? "Yes" : "No" },
589
+ { label: "Model", value: capability.model ?? "Default" },
590
+ {
591
+ label: "Features",
592
+ value: capability.features?.join(", ") || "Not specified"
593
+ },
594
+ { label: "Runs", value: String(capability.health?.runCount ?? 0) },
595
+ { label: "Errors", value: String(capability.health?.errorCount ?? 0) }
596
+ ]
597
+ }));
598
+ const warningCount = capabilities.filter((capability) => isWarningStatus(capability.status)).length;
599
+ const selectedCount = snapshot.report?.selected ?? capabilities.filter((capability) => capability.selected).length;
600
+ return {
601
+ capabilities,
602
+ description: options.description ?? DEFAULT_DESCRIPTION2,
603
+ error: snapshot.error,
604
+ isLoading: snapshot.isLoading,
605
+ label: snapshot.error ? "Unavailable" : capabilities.length ? warningCount > 0 ? `${warningCount} needs attention` : `${selectedCount} selected` : snapshot.isLoading ? "Checking" : "No capabilities",
606
+ status: snapshot.error ? "error" : capabilities.length ? warningCount > 0 ? "warning" : "ready" : snapshot.isLoading ? "loading" : "empty",
607
+ title: options.title ?? DEFAULT_TITLE2,
608
+ updatedAt: snapshot.updatedAt
609
+ };
610
+ };
611
+ var renderVoiceProviderCapabilitiesHTML = (snapshot, options = {}) => {
612
+ const model = createVoiceProviderCapabilitiesViewModel(snapshot, options);
613
+ const capabilities = model.capabilities.length ? `<div class="absolute-voice-provider-capabilities__providers">${model.capabilities.map((capability) => `<article class="absolute-voice-provider-capabilities__provider absolute-voice-provider-capabilities__provider--${escapeHtml3(capability.status)}">
614
+ <header>
615
+ <strong>${escapeHtml3(capability.label)}</strong>
616
+ <span>${escapeHtml3(formatStatus(capability.status))}</span>
617
+ </header>
618
+ <p>${escapeHtml3(capability.detail)}</p>
619
+ <dl>${capability.rows.map((row) => `<div>
620
+ <dt>${escapeHtml3(row.label)}</dt>
621
+ <dd>${escapeHtml3(row.value)}</dd>
622
+ </div>`).join("")}</dl>
623
+ </article>`).join("")}</div>` : '<p class="absolute-voice-provider-capabilities__empty">Configure provider capabilities to see deployment coverage.</p>';
624
+ return `<section class="absolute-voice-provider-capabilities absolute-voice-provider-capabilities--${escapeHtml3(model.status)}">
625
+ <header class="absolute-voice-provider-capabilities__header">
626
+ <span class="absolute-voice-provider-capabilities__eyebrow">${escapeHtml3(model.title)}</span>
627
+ <strong class="absolute-voice-provider-capabilities__label">${escapeHtml3(model.label)}</strong>
628
+ </header>
629
+ <p class="absolute-voice-provider-capabilities__description">${escapeHtml3(model.description)}</p>
630
+ ${capabilities}
631
+ ${model.error ? `<p class="absolute-voice-provider-capabilities__error">${escapeHtml3(model.error)}</p>` : ""}
632
+ </section>`;
633
+ };
634
+ var getVoiceProviderCapabilitiesCSS = () => `.absolute-voice-provider-capabilities{border:1px solid #bfd7ea;border-radius:20px;background:#f6fbff;color:#08131f;padding:18px;box-shadow:0 18px 40px rgba(14,51,78,.12);font-family:inherit}.absolute-voice-provider-capabilities--error,.absolute-voice-provider-capabilities--warning{border-color:#f2a7a7;background:#fff5f3}.absolute-voice-provider-capabilities__header,.absolute-voice-provider-capabilities__provider header{align-items:start;display:flex;gap:12px;justify-content:space-between}.absolute-voice-provider-capabilities__eyebrow{color:#255f85;font-size:12px;font-weight:800;letter-spacing:.08em;text-transform:uppercase}.absolute-voice-provider-capabilities__label{font-size:24px;line-height:1}.absolute-voice-provider-capabilities__description,.absolute-voice-provider-capabilities__provider p,.absolute-voice-provider-capabilities__provider dt,.absolute-voice-provider-capabilities__empty{color:#405467}.absolute-voice-provider-capabilities__providers{display:grid;gap:12px;margin-top:14px}.absolute-voice-provider-capabilities__provider{background:#fff;border:1px solid #d7e7f3;border-radius:16px;padding:14px}.absolute-voice-provider-capabilities__provider--selected,.absolute-voice-provider-capabilities__provider--healthy{border-color:#86efac}.absolute-voice-provider-capabilities__provider--degraded,.absolute-voice-provider-capabilities__provider--rate-limited,.absolute-voice-provider-capabilities__provider--suppressed,.absolute-voice-provider-capabilities__provider--unconfigured{border-color:#f2a7a7}.absolute-voice-provider-capabilities__provider p{margin:10px 0}.absolute-voice-provider-capabilities__provider dl{display:grid;gap:8px;grid-template-columns:repeat(2,minmax(0,1fr));margin:0}.absolute-voice-provider-capabilities__provider div{background:#f6fbff;border:1px solid #d7e7f3;border-radius:12px;padding:8px}.absolute-voice-provider-capabilities__provider dt{font-size:12px}.absolute-voice-provider-capabilities__provider dd{font-weight:800;margin:4px 0 0}.absolute-voice-provider-capabilities__empty{margin:14px 0 0}.absolute-voice-provider-capabilities__error{color:#9f1239;font-weight:700}`;
635
+ var mountVoiceProviderCapabilities = (element, path = "/api/provider-capabilities", options = {}) => {
636
+ const store = createVoiceProviderCapabilitiesStore(path, options);
637
+ const render = () => {
638
+ element.innerHTML = renderVoiceProviderCapabilitiesHTML(store.getSnapshot(), options);
639
+ };
640
+ const unsubscribe = store.subscribe(render);
641
+ render();
642
+ store.refresh().catch(() => {});
643
+ return {
644
+ close: () => {
645
+ unsubscribe();
646
+ store.close();
647
+ },
648
+ refresh: store.refresh
649
+ };
650
+ };
651
+ var defineVoiceProviderCapabilitiesElement = (tagName = "absolute-voice-provider-capabilities") => {
652
+ if (typeof window === "undefined" || typeof customElements === "undefined" || customElements.get(tagName)) {
653
+ return;
654
+ }
655
+ customElements.define(tagName, class AbsoluteVoiceProviderCapabilitiesElement extends HTMLElement {
656
+ mounted;
657
+ connectedCallback() {
658
+ const intervalMs = Number(this.getAttribute("interval-ms") ?? 5000);
659
+ this.mounted = mountVoiceProviderCapabilities(this, this.getAttribute("path") ?? "/api/provider-capabilities", {
660
+ description: this.getAttribute("description") ?? undefined,
661
+ intervalMs: Number.isFinite(intervalMs) ? intervalMs : 5000,
662
+ title: this.getAttribute("title") ?? undefined
663
+ });
664
+ }
665
+ disconnectedCallback() {
666
+ this.mounted?.close();
667
+ this.mounted = undefined;
668
+ }
669
+ });
670
+ };
671
+
672
+ // src/svelte/createVoiceProviderCapabilities.ts
673
+ var createVoiceProviderCapabilities = (path = "/api/provider-capabilities", options = {}) => {
674
+ const store = createVoiceProviderCapabilitiesStore(path, options);
675
+ return {
676
+ ...store,
677
+ getHTML: () => renderVoiceProviderCapabilitiesHTML(store.getSnapshot(), options),
678
+ getViewModel: () => createVoiceProviderCapabilitiesViewModel(store.getSnapshot(), options)
679
+ };
680
+ };
476
681
  // src/client/actions.ts
477
682
  var normalizeErrorMessage = (value) => {
478
683
  if (typeof value === "string" && value.trim()) {
@@ -1069,11 +1274,11 @@ var createVoiceProviderStatusStore = (path = "/api/provider-status", options = {
1069
1274
  };
1070
1275
 
1071
1276
  // src/client/providerStatusWidget.ts
1072
- var DEFAULT_TITLE2 = "Voice Providers";
1073
- var DEFAULT_DESCRIPTION2 = "Live provider health, fallback counts, latency, and suppression state from your self-hosted trace store.";
1074
- var escapeHtml3 = (value) => value.replaceAll("&", "&amp;").replaceAll("<", "&lt;").replaceAll(">", "&gt;").replaceAll('"', "&quot;").replaceAll("'", "&#39;");
1075
- var formatProvider = (provider) => provider.split(/[-_\s]+/).filter(Boolean).map((part) => `${part[0]?.toUpperCase() ?? ""}${part.slice(1)}`).join(" ") || provider;
1076
- var formatStatus = (status) => status.split("-").map((part) => `${part[0]?.toUpperCase() ?? ""}${part.slice(1)}`).join(" ");
1277
+ var DEFAULT_TITLE3 = "Voice Providers";
1278
+ var DEFAULT_DESCRIPTION3 = "Live provider health, fallback counts, latency, and suppression state from your self-hosted trace store.";
1279
+ var escapeHtml4 = (value) => value.replaceAll("&", "&amp;").replaceAll("<", "&lt;").replaceAll(">", "&gt;").replaceAll('"', "&quot;").replaceAll("'", "&#39;");
1280
+ var formatProvider2 = (provider) => provider.split(/[-_\s]+/).filter(Boolean).map((part) => `${part[0]?.toUpperCase() ?? ""}${part.slice(1)}`).join(" ") || provider;
1281
+ var formatStatus2 = (status) => status.split("-").map((part) => `${part[0]?.toUpperCase() ?? ""}${part.slice(1)}`).join(" ");
1077
1282
  var formatLatency = (value) => typeof value === "number" ? `${value}ms` : "No samples";
1078
1283
  var formatSuppression = (value) => typeof value === "number" ? `${Math.ceil(value / 1000)}s` : "None";
1079
1284
  var getProviderDetail = (provider) => {
@@ -1094,12 +1299,12 @@ var getProviderDetail = (provider) => {
1094
1299
  }
1095
1300
  return "No provider traffic observed yet.";
1096
1301
  };
1097
- var isWarningStatus = (status) => status === "degraded" || status === "rate-limited" || status === "recoverable" || status === "suppressed";
1302
+ var isWarningStatus2 = (status) => status === "degraded" || status === "rate-limited" || status === "recoverable" || status === "suppressed";
1098
1303
  var createVoiceProviderStatusViewModel = (snapshot, options = {}) => {
1099
1304
  const providers = snapshot.providers.map((provider) => ({
1100
1305
  ...provider,
1101
1306
  detail: getProviderDetail(provider),
1102
- label: `${formatProvider(provider.provider)}${provider.recommended ? " recommended" : ""}`,
1307
+ label: `${formatProvider2(provider.provider)}${provider.recommended ? " recommended" : ""}`,
1103
1308
  rows: [
1104
1309
  { label: "Runs", value: String(provider.runCount) },
1105
1310
  { label: "Avg latency", value: formatLatency(provider.averageElapsedMs) },
@@ -1112,40 +1317,40 @@ var createVoiceProviderStatusViewModel = (snapshot, options = {}) => {
1112
1317
  }
1113
1318
  ]
1114
1319
  }));
1115
- const warningCount = providers.filter((provider) => isWarningStatus(provider.status)).length;
1320
+ const warningCount = providers.filter((provider) => isWarningStatus2(provider.status)).length;
1116
1321
  const healthyCount = providers.filter((provider) => provider.status === "healthy").length;
1117
1322
  return {
1118
- description: options.description ?? DEFAULT_DESCRIPTION2,
1323
+ description: options.description ?? DEFAULT_DESCRIPTION3,
1119
1324
  error: snapshot.error,
1120
1325
  isLoading: snapshot.isLoading,
1121
1326
  label: snapshot.error ? "Unavailable" : providers.length ? warningCount > 0 ? `${warningCount} needs attention` : `${healthyCount} healthy` : snapshot.isLoading ? "Checking" : "No provider traffic",
1122
1327
  providers,
1123
1328
  status: snapshot.error ? "error" : providers.length ? warningCount > 0 ? "warning" : "ready" : snapshot.isLoading ? "loading" : "empty",
1124
- title: options.title ?? DEFAULT_TITLE2,
1329
+ title: options.title ?? DEFAULT_TITLE3,
1125
1330
  updatedAt: snapshot.updatedAt
1126
1331
  };
1127
1332
  };
1128
1333
  var renderVoiceProviderStatusHTML = (snapshot, options = {}) => {
1129
1334
  const model = createVoiceProviderStatusViewModel(snapshot, options);
1130
- const providers = model.providers.length ? `<div class="absolute-voice-provider-status__providers">${model.providers.map((provider) => `<article class="absolute-voice-provider-status__provider absolute-voice-provider-status__provider--${escapeHtml3(provider.status)}">
1335
+ const providers = model.providers.length ? `<div class="absolute-voice-provider-status__providers">${model.providers.map((provider) => `<article class="absolute-voice-provider-status__provider absolute-voice-provider-status__provider--${escapeHtml4(provider.status)}">
1131
1336
  <header>
1132
- <strong>${escapeHtml3(provider.label)}</strong>
1133
- <span>${escapeHtml3(formatStatus(provider.status))}</span>
1337
+ <strong>${escapeHtml4(provider.label)}</strong>
1338
+ <span>${escapeHtml4(formatStatus2(provider.status))}</span>
1134
1339
  </header>
1135
- <p>${escapeHtml3(provider.detail)}</p>
1340
+ <p>${escapeHtml4(provider.detail)}</p>
1136
1341
  <dl>${provider.rows.map((row) => `<div>
1137
- <dt>${escapeHtml3(row.label)}</dt>
1138
- <dd>${escapeHtml3(row.value)}</dd>
1342
+ <dt>${escapeHtml4(row.label)}</dt>
1343
+ <dd>${escapeHtml4(row.value)}</dd>
1139
1344
  </div>`).join("")}</dl>
1140
1345
  </article>`).join("")}</div>` : '<p class="absolute-voice-provider-status__empty">Run voice traffic to see provider health.</p>';
1141
- return `<section class="absolute-voice-provider-status absolute-voice-provider-status--${escapeHtml3(model.status)}">
1346
+ return `<section class="absolute-voice-provider-status absolute-voice-provider-status--${escapeHtml4(model.status)}">
1142
1347
  <header class="absolute-voice-provider-status__header">
1143
- <span class="absolute-voice-provider-status__eyebrow">${escapeHtml3(model.title)}</span>
1144
- <strong class="absolute-voice-provider-status__label">${escapeHtml3(model.label)}</strong>
1348
+ <span class="absolute-voice-provider-status__eyebrow">${escapeHtml4(model.title)}</span>
1349
+ <strong class="absolute-voice-provider-status__label">${escapeHtml4(model.label)}</strong>
1145
1350
  </header>
1146
- <p class="absolute-voice-provider-status__description">${escapeHtml3(model.description)}</p>
1351
+ <p class="absolute-voice-provider-status__description">${escapeHtml4(model.description)}</p>
1147
1352
  ${providers}
1148
- ${model.error ? `<p class="absolute-voice-provider-status__error">${escapeHtml3(model.error)}</p>` : ""}
1353
+ ${model.error ? `<p class="absolute-voice-provider-status__error">${escapeHtml4(model.error)}</p>` : ""}
1149
1354
  </section>`;
1150
1355
  };
1151
1356
  var getVoiceProviderStatusCSS = () => `.absolute-voice-provider-status{border:1px solid #d8d2c4;border-radius:20px;background:#fffaf0;color:#16130d;padding:18px;box-shadow:0 18px 40px rgba(47,37,18,.12);font-family:inherit}.absolute-voice-provider-status--error,.absolute-voice-provider-status--warning{border-color:#f2a7a7;background:#fff5f3}.absolute-voice-provider-status__header,.absolute-voice-provider-status__provider header{align-items:start;display:flex;gap:12px;justify-content:space-between}.absolute-voice-provider-status__eyebrow{color:#73664f;font-size:12px;font-weight:800;letter-spacing:.08em;text-transform:uppercase}.absolute-voice-provider-status__label{font-size:24px;line-height:1}.absolute-voice-provider-status__description,.absolute-voice-provider-status__provider p,.absolute-voice-provider-status__provider dt,.absolute-voice-provider-status__empty{color:#514733}.absolute-voice-provider-status__providers{display:grid;gap:12px;margin-top:14px}.absolute-voice-provider-status__provider{background:#fff;border:1px solid #eee4d2;border-radius:16px;padding:14px}.absolute-voice-provider-status__provider--degraded,.absolute-voice-provider-status__provider--rate-limited,.absolute-voice-provider-status__provider--suppressed{border-color:#f2a7a7}.absolute-voice-provider-status__provider--recoverable{border-color:#fbbf24}.absolute-voice-provider-status__provider p{margin:10px 0}.absolute-voice-provider-status__provider dl{display:grid;gap:8px;grid-template-columns:repeat(2,minmax(0,1fr));margin:0}.absolute-voice-provider-status__provider div{background:#fffaf0;border:1px solid #eee4d2;border-radius:12px;padding:8px}.absolute-voice-provider-status__provider dt{font-size:12px}.absolute-voice-provider-status__provider dd{font-weight:800;margin:4px 0 0}.absolute-voice-provider-status__empty{margin:14px 0 0}.absolute-voice-provider-status__error{color:#9f1239;font-weight:700}`;
@@ -1276,9 +1481,9 @@ var createVoiceRoutingStatusStore = (path = "/api/routing/latest", options = {})
1276
1481
  };
1277
1482
 
1278
1483
  // src/client/routingStatusWidget.ts
1279
- var DEFAULT_TITLE3 = "Voice Routing";
1280
- var DEFAULT_DESCRIPTION3 = "Latest provider routing decision from the self-hosted trace store.";
1281
- var escapeHtml4 = (value) => value.replaceAll("&", "&amp;").replaceAll("<", "&lt;").replaceAll(">", "&gt;").replaceAll('"', "&quot;").replaceAll("'", "&#39;");
1484
+ var DEFAULT_TITLE4 = "Voice Routing";
1485
+ var DEFAULT_DESCRIPTION4 = "Latest provider routing decision from the self-hosted trace store.";
1486
+ var escapeHtml5 = (value) => value.replaceAll("&", "&amp;").replaceAll("<", "&lt;").replaceAll(">", "&gt;").replaceAll('"', "&quot;").replaceAll("'", "&#39;");
1282
1487
  var formatValue = (value, fallback = "None") => typeof value === "string" && value.trim() ? value : typeof value === "number" && Number.isFinite(value) ? String(value) : fallback;
1283
1488
  var createVoiceRoutingStatusViewModel = (snapshot, options = {}) => {
1284
1489
  const decision = snapshot.decision;
@@ -1302,30 +1507,30 @@ var createVoiceRoutingStatusViewModel = (snapshot, options = {}) => {
1302
1507
  ] : [];
1303
1508
  return {
1304
1509
  decision,
1305
- description: options.description ?? DEFAULT_DESCRIPTION3,
1510
+ description: options.description ?? DEFAULT_DESCRIPTION4,
1306
1511
  error: snapshot.error,
1307
1512
  isLoading: snapshot.isLoading,
1308
1513
  label: snapshot.error ? "Unavailable" : decision ? `${formatValue(decision.kind).toUpperCase()} ${formatValue(decision.status, "unknown")}` : snapshot.isLoading ? "Checking" : "No routing yet",
1309
1514
  rows,
1310
1515
  status: snapshot.error ? "error" : decision ? "ready" : snapshot.isLoading ? "loading" : "empty",
1311
- title: options.title ?? DEFAULT_TITLE3,
1516
+ title: options.title ?? DEFAULT_TITLE4,
1312
1517
  updatedAt: snapshot.updatedAt
1313
1518
  };
1314
1519
  };
1315
1520
  var renderVoiceRoutingStatusHTML = (snapshot, options = {}) => {
1316
1521
  const model = createVoiceRoutingStatusViewModel(snapshot, options);
1317
1522
  const rows = model.rows.length ? `<div class="absolute-voice-routing-status__grid">${model.rows.map((row) => `<div>
1318
- <span>${escapeHtml4(row.label)}</span>
1319
- <strong>${escapeHtml4(row.value)}</strong>
1523
+ <span>${escapeHtml5(row.label)}</span>
1524
+ <strong>${escapeHtml5(row.value)}</strong>
1320
1525
  </div>`).join("")}</div>` : '<p class="absolute-voice-routing-status__empty">Start a voice session to see the selected provider.</p>';
1321
- return `<section class="absolute-voice-routing-status absolute-voice-routing-status--${escapeHtml4(model.status)}">
1526
+ return `<section class="absolute-voice-routing-status absolute-voice-routing-status--${escapeHtml5(model.status)}">
1322
1527
  <header class="absolute-voice-routing-status__header">
1323
- <span class="absolute-voice-routing-status__eyebrow">${escapeHtml4(model.title)}</span>
1324
- <strong class="absolute-voice-routing-status__label">${escapeHtml4(model.label)}</strong>
1528
+ <span class="absolute-voice-routing-status__eyebrow">${escapeHtml5(model.title)}</span>
1529
+ <strong class="absolute-voice-routing-status__label">${escapeHtml5(model.label)}</strong>
1325
1530
  </header>
1326
- <p class="absolute-voice-routing-status__description">${escapeHtml4(model.description)}</p>
1531
+ <p class="absolute-voice-routing-status__description">${escapeHtml5(model.description)}</p>
1327
1532
  ${rows}
1328
- ${model.error ? `<p class="absolute-voice-routing-status__error">${escapeHtml4(model.error)}</p>` : ""}
1533
+ ${model.error ? `<p class="absolute-voice-routing-status__error">${escapeHtml5(model.error)}</p>` : ""}
1329
1534
  </section>`;
1330
1535
  };
1331
1536
  var getVoiceRoutingStatusCSS = () => `.absolute-voice-routing-status{border:1px solid #d8d2c4;border-radius:20px;background:#fffaf0;color:#16130d;padding:18px;box-shadow:0 18px 40px rgba(47,37,18,.12);font-family:inherit}.absolute-voice-routing-status--error{border-color:#f2a7a7;background:#fff5f3}.absolute-voice-routing-status__header{align-items:start;display:flex;gap:12px;justify-content:space-between}.absolute-voice-routing-status__eyebrow{color:#73664f;font-size:12px;font-weight:800;letter-spacing:.08em;text-transform:uppercase}.absolute-voice-routing-status__label{font-size:24px;line-height:1}.absolute-voice-routing-status__description{color:#514733;margin:12px 0 0}.absolute-voice-routing-status__grid{display:grid;gap:8px;grid-template-columns:repeat(2,minmax(0,1fr));margin-top:14px}.absolute-voice-routing-status__grid div{background:#fff;border:1px solid #eee4d2;border-radius:14px;padding:10px 12px}.absolute-voice-routing-status__grid span{color:#655944;display:block;font-size:12px;margin-bottom:4px}.absolute-voice-routing-status__grid strong{overflow-wrap:anywhere}.absolute-voice-routing-status__empty{color:#655944;margin:14px 0 0}.absolute-voice-routing-status__error{color:#9f1239;font-weight:700}`;
@@ -1375,6 +1580,206 @@ var createVoiceRoutingStatus = (path = "/api/routing/latest", options = {}) => {
1375
1580
  getViewModel: () => createVoiceRoutingStatusViewModel(store.getSnapshot(), options)
1376
1581
  };
1377
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
+ };
1378
1783
  // src/client/workflowStatus.ts
1379
1784
  var fetchVoiceWorkflowStatus = async (path = "/evals/scenarios/json", options = {}) => {
1380
1785
  const fetchImpl = options.fetch ?? globalThis.fetch;
@@ -2087,10 +2492,12 @@ var createVoiceController = (path, options = {}) => {
2087
2492
  };
2088
2493
  export {
2089
2494
  createVoiceWorkflowStatus,
2495
+ createVoiceTurnQuality,
2090
2496
  createVoiceStream2 as createVoiceStream,
2091
2497
  createVoiceRoutingStatus,
2092
2498
  createVoiceProviderStatus,
2093
2499
  createVoiceProviderSimulationControls,
2500
+ createVoiceProviderCapabilities,
2094
2501
  createVoiceOpsStatus,
2095
2502
  createVoiceController,
2096
2503
  createVoiceAppKitStatus