@absolutejs/voice 0.0.22-beta.189 → 0.0.22-beta.190
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.
- package/dist/angular/index.d.ts +1 -0
- package/dist/angular/index.js +277 -148
- package/dist/angular/voice-live-ops.service.d.ts +11 -0
- package/dist/client/index.d.ts +4 -0
- package/dist/client/index.js +1385 -105
- package/dist/client/liveOps.d.ts +22 -0
- package/dist/client/liveOpsWidget.d.ts +23 -0
- package/dist/index.d.ts +2 -0
- package/dist/index.js +252 -59
- package/dist/liveOps.d.ts +122 -0
- package/dist/react/index.d.ts +1 -0
- package/dist/react/index.js +119 -16
- package/dist/react/useVoiceLiveOps.d.ts +9 -0
- package/dist/svelte/createVoiceLiveOps.d.ts +13 -0
- package/dist/svelte/index.d.ts +1 -0
- package/dist/svelte/index.js +1389 -101
- package/dist/vue/index.d.ts +1 -0
- package/dist/vue/index.js +165 -44
- package/dist/vue/useVoiceLiveOps.d.ts +9 -0
- package/package.json +1 -1
package/dist/svelte/index.js
CHANGED
|
@@ -760,6 +760,1293 @@ var createVoiceOpsActionCenter = (options = {}) => {
|
|
|
760
760
|
subscribe: store.subscribe
|
|
761
761
|
};
|
|
762
762
|
};
|
|
763
|
+
// src/client/liveOps.ts
|
|
764
|
+
var postVoiceLiveOpsAction = async (input, options = {}) => {
|
|
765
|
+
if (!input.sessionId) {
|
|
766
|
+
throw new Error("Start a voice session before running live ops actions.");
|
|
767
|
+
}
|
|
768
|
+
const fetchImpl = options.fetch ?? globalThis.fetch;
|
|
769
|
+
const response = await fetchImpl(options.actionPath ?? "/api/voice/live-ops/action", {
|
|
770
|
+
body: JSON.stringify(input),
|
|
771
|
+
headers: {
|
|
772
|
+
"Content-Type": "application/json"
|
|
773
|
+
},
|
|
774
|
+
method: "POST"
|
|
775
|
+
});
|
|
776
|
+
const payload = await response.json().catch(() => null);
|
|
777
|
+
if (!response.ok || !payload?.ok) {
|
|
778
|
+
const message = payload && typeof payload === "object" && "error" in payload ? String(payload.error) : `Voice live ops action failed: HTTP ${response.status}`;
|
|
779
|
+
throw new Error(message);
|
|
780
|
+
}
|
|
781
|
+
return payload;
|
|
782
|
+
};
|
|
783
|
+
var createVoiceLiveOpsStore = (options = {}) => {
|
|
784
|
+
const listeners = new Set;
|
|
785
|
+
let closed = false;
|
|
786
|
+
let snapshot = {
|
|
787
|
+
error: null,
|
|
788
|
+
isRunning: false
|
|
789
|
+
};
|
|
790
|
+
const emit = () => {
|
|
791
|
+
for (const listener of listeners) {
|
|
792
|
+
listener();
|
|
793
|
+
}
|
|
794
|
+
};
|
|
795
|
+
const run = async (input) => {
|
|
796
|
+
if (closed) {
|
|
797
|
+
return snapshot.lastResult;
|
|
798
|
+
}
|
|
799
|
+
snapshot = {
|
|
800
|
+
...snapshot,
|
|
801
|
+
error: null,
|
|
802
|
+
isRunning: true,
|
|
803
|
+
runningAction: input.action
|
|
804
|
+
};
|
|
805
|
+
emit();
|
|
806
|
+
try {
|
|
807
|
+
const result = await postVoiceLiveOpsAction(input, options);
|
|
808
|
+
await options.onControl?.(result);
|
|
809
|
+
snapshot = {
|
|
810
|
+
...snapshot,
|
|
811
|
+
error: null,
|
|
812
|
+
isRunning: false,
|
|
813
|
+
lastResult: result,
|
|
814
|
+
runningAction: undefined,
|
|
815
|
+
updatedAt: Date.now()
|
|
816
|
+
};
|
|
817
|
+
emit();
|
|
818
|
+
return result;
|
|
819
|
+
} catch (error) {
|
|
820
|
+
snapshot = {
|
|
821
|
+
...snapshot,
|
|
822
|
+
error: error instanceof Error ? error.message : String(error),
|
|
823
|
+
isRunning: false,
|
|
824
|
+
runningAction: undefined,
|
|
825
|
+
updatedAt: Date.now()
|
|
826
|
+
};
|
|
827
|
+
emit();
|
|
828
|
+
throw error;
|
|
829
|
+
}
|
|
830
|
+
};
|
|
831
|
+
const close = () => {
|
|
832
|
+
closed = true;
|
|
833
|
+
listeners.clear();
|
|
834
|
+
};
|
|
835
|
+
return {
|
|
836
|
+
close,
|
|
837
|
+
getServerSnapshot: () => snapshot,
|
|
838
|
+
getSnapshot: () => snapshot,
|
|
839
|
+
run,
|
|
840
|
+
subscribe: (listener) => {
|
|
841
|
+
listeners.add(listener);
|
|
842
|
+
return () => {
|
|
843
|
+
listeners.delete(listener);
|
|
844
|
+
};
|
|
845
|
+
}
|
|
846
|
+
};
|
|
847
|
+
};
|
|
848
|
+
|
|
849
|
+
// src/liveOps.ts
|
|
850
|
+
import { Elysia } from "elysia";
|
|
851
|
+
|
|
852
|
+
// src/audit.ts
|
|
853
|
+
var includes = (filter, value) => {
|
|
854
|
+
if (!filter) {
|
|
855
|
+
return true;
|
|
856
|
+
}
|
|
857
|
+
if (!value) {
|
|
858
|
+
return false;
|
|
859
|
+
}
|
|
860
|
+
return Array.isArray(filter) ? filter.includes(value) : filter === value;
|
|
861
|
+
};
|
|
862
|
+
var createVoiceAuditEvent = (event) => ({
|
|
863
|
+
...event,
|
|
864
|
+
at: event.at ?? Date.now(),
|
|
865
|
+
id: event.id ?? crypto.randomUUID()
|
|
866
|
+
});
|
|
867
|
+
var filterVoiceAuditEvents = (events, filter = {}) => {
|
|
868
|
+
const sorted = events.filter((event) => {
|
|
869
|
+
if (!includes(filter.type, event.type)) {
|
|
870
|
+
return false;
|
|
871
|
+
}
|
|
872
|
+
if (!includes(filter.outcome, event.outcome)) {
|
|
873
|
+
return false;
|
|
874
|
+
}
|
|
875
|
+
if (filter.actorId && event.actor?.id !== filter.actorId) {
|
|
876
|
+
return false;
|
|
877
|
+
}
|
|
878
|
+
if (filter.resourceId && event.resource?.id !== filter.resourceId) {
|
|
879
|
+
return false;
|
|
880
|
+
}
|
|
881
|
+
if (filter.resourceType && event.resource?.type !== filter.resourceType) {
|
|
882
|
+
return false;
|
|
883
|
+
}
|
|
884
|
+
if (filter.sessionId && event.sessionId !== filter.sessionId) {
|
|
885
|
+
return false;
|
|
886
|
+
}
|
|
887
|
+
if (filter.traceId && event.traceId !== filter.traceId) {
|
|
888
|
+
return false;
|
|
889
|
+
}
|
|
890
|
+
if (typeof filter.after === "number" && event.at <= filter.after) {
|
|
891
|
+
return false;
|
|
892
|
+
}
|
|
893
|
+
if (typeof filter.afterOrAt === "number" && event.at < filter.afterOrAt) {
|
|
894
|
+
return false;
|
|
895
|
+
}
|
|
896
|
+
if (typeof filter.before === "number" && event.at >= filter.before) {
|
|
897
|
+
return false;
|
|
898
|
+
}
|
|
899
|
+
if (typeof filter.beforeOrAt === "number" && event.at > filter.beforeOrAt) {
|
|
900
|
+
return false;
|
|
901
|
+
}
|
|
902
|
+
return true;
|
|
903
|
+
}).sort((left, right) => left.at - right.at || left.id.localeCompare(right.id));
|
|
904
|
+
return typeof filter.limit === "number" && filter.limit >= 0 ? sorted.slice(0, filter.limit) : sorted;
|
|
905
|
+
};
|
|
906
|
+
var createVoiceMemoryAuditEventStore = () => {
|
|
907
|
+
const events = new Map;
|
|
908
|
+
return {
|
|
909
|
+
append: (event) => {
|
|
910
|
+
const stored = createVoiceAuditEvent(event);
|
|
911
|
+
events.set(stored.id, stored);
|
|
912
|
+
return stored;
|
|
913
|
+
},
|
|
914
|
+
get: (id) => events.get(id),
|
|
915
|
+
list: (filter) => filterVoiceAuditEvents([...events.values()], filter)
|
|
916
|
+
};
|
|
917
|
+
};
|
|
918
|
+
var recordVoiceAuditEvent = (store, event) => store.append(createVoiceAuditEvent(event));
|
|
919
|
+
var recordVoiceProviderAuditEvent = (input) => recordVoiceAuditEvent(input.store, {
|
|
920
|
+
action: `${input.kind}.provider.call`,
|
|
921
|
+
actor: input.actor,
|
|
922
|
+
metadata: input.metadata,
|
|
923
|
+
outcome: input.outcome,
|
|
924
|
+
payload: {
|
|
925
|
+
cost: input.cost,
|
|
926
|
+
elapsedMs: input.elapsedMs,
|
|
927
|
+
error: input.error,
|
|
928
|
+
kind: input.kind,
|
|
929
|
+
model: input.model,
|
|
930
|
+
provider: input.provider
|
|
931
|
+
},
|
|
932
|
+
resource: {
|
|
933
|
+
id: input.provider,
|
|
934
|
+
type: "provider"
|
|
935
|
+
},
|
|
936
|
+
sessionId: input.sessionId,
|
|
937
|
+
traceId: input.traceId,
|
|
938
|
+
type: "provider.call"
|
|
939
|
+
});
|
|
940
|
+
var recordVoiceToolAuditEvent = (input) => recordVoiceAuditEvent(input.store, {
|
|
941
|
+
action: "tool.call",
|
|
942
|
+
actor: input.actor,
|
|
943
|
+
metadata: input.metadata,
|
|
944
|
+
outcome: input.outcome,
|
|
945
|
+
payload: {
|
|
946
|
+
elapsedMs: input.elapsedMs,
|
|
947
|
+
error: input.error,
|
|
948
|
+
toolCallId: input.toolCallId,
|
|
949
|
+
toolName: input.toolName
|
|
950
|
+
},
|
|
951
|
+
resource: {
|
|
952
|
+
id: input.toolName,
|
|
953
|
+
type: "tool"
|
|
954
|
+
},
|
|
955
|
+
sessionId: input.sessionId,
|
|
956
|
+
traceId: input.traceId,
|
|
957
|
+
type: "tool.call"
|
|
958
|
+
});
|
|
959
|
+
var recordVoiceHandoffAuditEvent = (input) => recordVoiceAuditEvent(input.store, {
|
|
960
|
+
action: "handoff",
|
|
961
|
+
actor: input.actor,
|
|
962
|
+
metadata: input.metadata,
|
|
963
|
+
outcome: input.outcome,
|
|
964
|
+
payload: {
|
|
965
|
+
fromAgentId: input.fromAgentId,
|
|
966
|
+
reason: input.reason,
|
|
967
|
+
target: input.target,
|
|
968
|
+
toAgentId: input.toAgentId
|
|
969
|
+
},
|
|
970
|
+
resource: {
|
|
971
|
+
id: input.toAgentId ?? input.target,
|
|
972
|
+
type: "handoff"
|
|
973
|
+
},
|
|
974
|
+
sessionId: input.sessionId,
|
|
975
|
+
traceId: input.traceId,
|
|
976
|
+
type: "handoff"
|
|
977
|
+
});
|
|
978
|
+
var recordVoiceRetentionAuditEvent = (input) => recordVoiceAuditEvent(input.store, {
|
|
979
|
+
action: input.dryRun ? "retention.plan" : "retention.apply",
|
|
980
|
+
actor: input.actor ?? {
|
|
981
|
+
id: "voice-retention",
|
|
982
|
+
kind: "system"
|
|
983
|
+
},
|
|
984
|
+
metadata: input.metadata,
|
|
985
|
+
outcome: "success",
|
|
986
|
+
payload: {
|
|
987
|
+
deletedCount: input.report.deletedCount,
|
|
988
|
+
dryRun: input.dryRun,
|
|
989
|
+
scopes: input.report.scopes
|
|
990
|
+
},
|
|
991
|
+
resource: {
|
|
992
|
+
type: "retention-policy"
|
|
993
|
+
},
|
|
994
|
+
type: "retention.policy"
|
|
995
|
+
});
|
|
996
|
+
var recordVoiceOperatorAuditEvent = (input) => recordVoiceAuditEvent(input.store, {
|
|
997
|
+
action: input.action,
|
|
998
|
+
actor: input.actor,
|
|
999
|
+
metadata: input.metadata,
|
|
1000
|
+
outcome: input.outcome ?? "success",
|
|
1001
|
+
payload: input.payload,
|
|
1002
|
+
resource: input.resource,
|
|
1003
|
+
sessionId: input.sessionId,
|
|
1004
|
+
traceId: input.traceId,
|
|
1005
|
+
type: "operator.action"
|
|
1006
|
+
});
|
|
1007
|
+
var createVoiceAuditLogger = (store) => ({
|
|
1008
|
+
handoff: (input) => recordVoiceHandoffAuditEvent({ ...input, store }),
|
|
1009
|
+
operatorAction: (input) => recordVoiceOperatorAuditEvent({ ...input, store }),
|
|
1010
|
+
providerCall: (input) => recordVoiceProviderAuditEvent({ ...input, store }),
|
|
1011
|
+
record: (event) => recordVoiceAuditEvent(store, event),
|
|
1012
|
+
retention: (input) => recordVoiceRetentionAuditEvent({ ...input, store }),
|
|
1013
|
+
toolCall: (input) => recordVoiceToolAuditEvent({ ...input, store })
|
|
1014
|
+
});
|
|
1015
|
+
|
|
1016
|
+
// src/trace.ts
|
|
1017
|
+
var createVoiceTraceEventId = (event) => [
|
|
1018
|
+
event.sessionId,
|
|
1019
|
+
event.turnId ?? "session",
|
|
1020
|
+
event.type,
|
|
1021
|
+
String(event.at ?? Date.now()),
|
|
1022
|
+
crypto.randomUUID()
|
|
1023
|
+
].map(encodeURIComponent).join(":");
|
|
1024
|
+
var createVoiceTraceEvent = (event) => ({
|
|
1025
|
+
...event,
|
|
1026
|
+
at: event.at,
|
|
1027
|
+
id: event.id ?? createVoiceTraceEventId({
|
|
1028
|
+
at: event.at,
|
|
1029
|
+
sessionId: event.sessionId,
|
|
1030
|
+
turnId: event.turnId,
|
|
1031
|
+
type: event.type
|
|
1032
|
+
})
|
|
1033
|
+
});
|
|
1034
|
+
var createVoiceTraceSinkDeliveryId = (events) => {
|
|
1035
|
+
const firstEvent = events[0];
|
|
1036
|
+
return [
|
|
1037
|
+
firstEvent?.sessionId ?? "trace",
|
|
1038
|
+
firstEvent?.traceId ?? "sink",
|
|
1039
|
+
String(firstEvent?.at ?? Date.now()),
|
|
1040
|
+
crypto.randomUUID()
|
|
1041
|
+
].map(encodeURIComponent).join(":");
|
|
1042
|
+
};
|
|
1043
|
+
var createVoiceTraceSinkDeliveryRecord = (input) => {
|
|
1044
|
+
const createdAt = input.createdAt ?? Date.now();
|
|
1045
|
+
return {
|
|
1046
|
+
createdAt,
|
|
1047
|
+
deliveredAt: input.deliveredAt,
|
|
1048
|
+
deliveryAttempts: input.deliveryAttempts,
|
|
1049
|
+
deliveryError: input.deliveryError,
|
|
1050
|
+
deliveryStatus: input.deliveryStatus ?? "pending",
|
|
1051
|
+
events: input.events,
|
|
1052
|
+
id: input.id ?? createVoiceTraceSinkDeliveryId(input.events),
|
|
1053
|
+
sinkDeliveries: input.sinkDeliveries,
|
|
1054
|
+
updatedAt: input.updatedAt ?? createdAt
|
|
1055
|
+
};
|
|
1056
|
+
};
|
|
1057
|
+
var matchesTraceFilter = (event, filter) => {
|
|
1058
|
+
if (filter.sessionId !== undefined && event.sessionId !== filter.sessionId) {
|
|
1059
|
+
return false;
|
|
1060
|
+
}
|
|
1061
|
+
if (filter.turnId !== undefined && event.turnId !== filter.turnId) {
|
|
1062
|
+
return false;
|
|
1063
|
+
}
|
|
1064
|
+
if (filter.scenarioId !== undefined && event.scenarioId !== filter.scenarioId) {
|
|
1065
|
+
return false;
|
|
1066
|
+
}
|
|
1067
|
+
if (filter.traceId !== undefined && event.traceId !== filter.traceId) {
|
|
1068
|
+
return false;
|
|
1069
|
+
}
|
|
1070
|
+
if (filter.type !== undefined) {
|
|
1071
|
+
const types = Array.isArray(filter.type) ? filter.type : [filter.type];
|
|
1072
|
+
if (!types.includes(event.type)) {
|
|
1073
|
+
return false;
|
|
1074
|
+
}
|
|
1075
|
+
}
|
|
1076
|
+
return true;
|
|
1077
|
+
};
|
|
1078
|
+
var filterVoiceTraceEvents = (events, filter = {}) => {
|
|
1079
|
+
const sorted = events.filter((event) => matchesTraceFilter(event, filter)).sort((left, right) => left.at - right.at || left.id.localeCompare(right.id));
|
|
1080
|
+
return typeof filter.limit === "number" && filter.limit >= 0 ? sorted.slice(0, filter.limit) : sorted;
|
|
1081
|
+
};
|
|
1082
|
+
var isPruneTimeMatch = (event, options) => {
|
|
1083
|
+
if (typeof options.before === "number" && event.at >= options.before) {
|
|
1084
|
+
return false;
|
|
1085
|
+
}
|
|
1086
|
+
if (typeof options.beforeOrAt === "number" && event.at > options.beforeOrAt) {
|
|
1087
|
+
return false;
|
|
1088
|
+
}
|
|
1089
|
+
return true;
|
|
1090
|
+
};
|
|
1091
|
+
var selectVoiceTraceEventsForPrune = (events, options = {}) => {
|
|
1092
|
+
let candidates = filterVoiceTraceEvents(events, options.filter).filter((event) => isPruneTimeMatch(event, options));
|
|
1093
|
+
if (typeof options.keepNewest === "number" && options.keepNewest >= 0) {
|
|
1094
|
+
const newestIds = new Set([...candidates].sort((left, right) => right.at - left.at || right.id.localeCompare(left.id)).slice(0, options.keepNewest).map((event) => event.id));
|
|
1095
|
+
candidates = candidates.filter((event) => !newestIds.has(event.id));
|
|
1096
|
+
}
|
|
1097
|
+
return typeof options.limit === "number" && options.limit >= 0 ? candidates.slice(0, options.limit) : candidates;
|
|
1098
|
+
};
|
|
1099
|
+
var pruneVoiceTraceEvents = async (options) => {
|
|
1100
|
+
const events = await options.store.list(options.filter);
|
|
1101
|
+
const deleted = selectVoiceTraceEventsForPrune(events, options);
|
|
1102
|
+
if (!options.dryRun) {
|
|
1103
|
+
await Promise.all(deleted.map((event) => options.store.remove(event.id)));
|
|
1104
|
+
}
|
|
1105
|
+
return {
|
|
1106
|
+
deleted,
|
|
1107
|
+
deletedCount: deleted.length,
|
|
1108
|
+
dryRun: Boolean(options.dryRun),
|
|
1109
|
+
scannedCount: events.length
|
|
1110
|
+
};
|
|
1111
|
+
};
|
|
1112
|
+
var sleep = async (delayMs) => {
|
|
1113
|
+
if (delayMs <= 0) {
|
|
1114
|
+
return;
|
|
1115
|
+
}
|
|
1116
|
+
await new Promise((resolve) => setTimeout(resolve, delayMs));
|
|
1117
|
+
};
|
|
1118
|
+
var toHex = (bytes) => Array.from(bytes, (byte) => byte.toString(16).padStart(2, "0")).join("");
|
|
1119
|
+
var signVoiceTraceSinkBody = async (input) => {
|
|
1120
|
+
const encoder = new TextEncoder;
|
|
1121
|
+
const key = await crypto.subtle.importKey("raw", encoder.encode(input.secret), {
|
|
1122
|
+
hash: "SHA-256",
|
|
1123
|
+
name: "HMAC"
|
|
1124
|
+
}, false, ["sign"]);
|
|
1125
|
+
const payload = encoder.encode(`${input.timestamp}.${input.body}`);
|
|
1126
|
+
const signature = await crypto.subtle.sign("HMAC", key, payload);
|
|
1127
|
+
return `sha256=${toHex(new Uint8Array(signature))}`;
|
|
1128
|
+
};
|
|
1129
|
+
var createVoiceTraceSinkDeliveryError = (input) => {
|
|
1130
|
+
if (input.response) {
|
|
1131
|
+
const statusText = input.response.statusText?.trim();
|
|
1132
|
+
return `Attempt ${input.attempt} failed with trace sink response ${input.response.status}${statusText ? ` ${statusText}` : ""}.`;
|
|
1133
|
+
}
|
|
1134
|
+
if (input.error instanceof Error) {
|
|
1135
|
+
return `Attempt ${input.attempt} failed: ${input.error.message}`;
|
|
1136
|
+
}
|
|
1137
|
+
return `Attempt ${input.attempt} failed: ${String(input.error)}`;
|
|
1138
|
+
};
|
|
1139
|
+
var normalizeVoiceTraceS3KeyPrefix = (prefix) => prefix?.trim().replace(/^\/+|\/+$/g, "") ?? "voice/trace-deliveries";
|
|
1140
|
+
var createVoiceTraceS3ObjectKey = (prefix, events) => {
|
|
1141
|
+
const firstEvent = events[0];
|
|
1142
|
+
const safeSessionId = encodeURIComponent(firstEvent?.sessionId ?? "trace");
|
|
1143
|
+
const safeEventId = encodeURIComponent(firstEvent?.id ?? crypto.randomUUID());
|
|
1144
|
+
return `${prefix}/${safeSessionId}/${Date.now()}-${safeEventId}.json`;
|
|
1145
|
+
};
|
|
1146
|
+
var resolveVoiceS3DeliveredTo = (options, key) => {
|
|
1147
|
+
const bucket = options.bucket;
|
|
1148
|
+
return bucket ? `s3://${bucket}/${key}` : `s3://${key}`;
|
|
1149
|
+
};
|
|
1150
|
+
var aggregateVoiceTraceSinkDeliveryStatus = (deliveries) => {
|
|
1151
|
+
const statuses = Object.values(deliveries).map((delivery) => delivery.status);
|
|
1152
|
+
if (statuses.length === 0 || statuses.every((status) => status === "skipped")) {
|
|
1153
|
+
return "skipped";
|
|
1154
|
+
}
|
|
1155
|
+
if (statuses.some((status) => status === "failed")) {
|
|
1156
|
+
return "failed";
|
|
1157
|
+
}
|
|
1158
|
+
return "delivered";
|
|
1159
|
+
};
|
|
1160
|
+
var createVoiceTraceHTTPSink = (options) => ({
|
|
1161
|
+
deliver: async ({ events }) => {
|
|
1162
|
+
const fetchImpl = options.fetch ?? globalThis.fetch;
|
|
1163
|
+
if (typeof fetchImpl !== "function") {
|
|
1164
|
+
return {
|
|
1165
|
+
attempts: 0,
|
|
1166
|
+
deliveredTo: options.url,
|
|
1167
|
+
error: "Trace sink delivery failed: fetch is not available in this runtime.",
|
|
1168
|
+
eventCount: events.length,
|
|
1169
|
+
status: "failed"
|
|
1170
|
+
};
|
|
1171
|
+
}
|
|
1172
|
+
const maxRetries = Math.max(0, options.retries ?? 0);
|
|
1173
|
+
const backoffMs = Math.max(0, options.backoffMs ?? 250);
|
|
1174
|
+
const timeoutMs = Math.max(0, options.timeoutMs ?? 1e4);
|
|
1175
|
+
const payload = options.body ? await options.body({ events }) : {
|
|
1176
|
+
eventCount: events.length,
|
|
1177
|
+
events,
|
|
1178
|
+
source: "absolutejs-voice"
|
|
1179
|
+
};
|
|
1180
|
+
const body = JSON.stringify(payload);
|
|
1181
|
+
let lastError = "Trace sink delivery failed.";
|
|
1182
|
+
for (let attempt = 1;attempt <= maxRetries + 1; attempt += 1) {
|
|
1183
|
+
let controller;
|
|
1184
|
+
let timeout;
|
|
1185
|
+
try {
|
|
1186
|
+
const headers = {
|
|
1187
|
+
"content-type": "application/json",
|
|
1188
|
+
...options.headers
|
|
1189
|
+
};
|
|
1190
|
+
if (options.signingSecret) {
|
|
1191
|
+
const timestamp = String(Date.now());
|
|
1192
|
+
headers["x-absolutejs-timestamp"] = timestamp;
|
|
1193
|
+
headers["x-absolutejs-signature"] = await signVoiceTraceSinkBody({
|
|
1194
|
+
body,
|
|
1195
|
+
secret: options.signingSecret,
|
|
1196
|
+
timestamp
|
|
1197
|
+
});
|
|
1198
|
+
}
|
|
1199
|
+
controller = timeoutMs > 0 ? new AbortController : undefined;
|
|
1200
|
+
if (controller && timeoutMs > 0) {
|
|
1201
|
+
timeout = setTimeout(() => controller?.abort(), timeoutMs);
|
|
1202
|
+
}
|
|
1203
|
+
const response = await fetchImpl(options.url, {
|
|
1204
|
+
body,
|
|
1205
|
+
headers,
|
|
1206
|
+
method: options.method ?? "POST",
|
|
1207
|
+
signal: controller?.signal
|
|
1208
|
+
});
|
|
1209
|
+
if (response.ok) {
|
|
1210
|
+
let responseBody;
|
|
1211
|
+
try {
|
|
1212
|
+
responseBody = await response.clone().json();
|
|
1213
|
+
} catch {
|
|
1214
|
+
responseBody = undefined;
|
|
1215
|
+
}
|
|
1216
|
+
return {
|
|
1217
|
+
attempts: attempt,
|
|
1218
|
+
deliveredAt: Date.now(),
|
|
1219
|
+
deliveredTo: options.url,
|
|
1220
|
+
eventCount: events.length,
|
|
1221
|
+
responseBody,
|
|
1222
|
+
status: "delivered"
|
|
1223
|
+
};
|
|
1224
|
+
}
|
|
1225
|
+
lastError = createVoiceTraceSinkDeliveryError({
|
|
1226
|
+
attempt,
|
|
1227
|
+
response
|
|
1228
|
+
});
|
|
1229
|
+
} catch (error) {
|
|
1230
|
+
lastError = createVoiceTraceSinkDeliveryError({
|
|
1231
|
+
attempt,
|
|
1232
|
+
error
|
|
1233
|
+
});
|
|
1234
|
+
} finally {
|
|
1235
|
+
if (timeout) {
|
|
1236
|
+
clearTimeout(timeout);
|
|
1237
|
+
}
|
|
1238
|
+
}
|
|
1239
|
+
if (attempt <= maxRetries) {
|
|
1240
|
+
await sleep(backoffMs * attempt);
|
|
1241
|
+
}
|
|
1242
|
+
}
|
|
1243
|
+
return {
|
|
1244
|
+
attempts: maxRetries + 1,
|
|
1245
|
+
deliveredTo: options.url,
|
|
1246
|
+
error: lastError,
|
|
1247
|
+
eventCount: events.length,
|
|
1248
|
+
status: "failed"
|
|
1249
|
+
};
|
|
1250
|
+
},
|
|
1251
|
+
eventTypes: options.eventTypes,
|
|
1252
|
+
id: options.id,
|
|
1253
|
+
kind: options.kind ?? "http"
|
|
1254
|
+
});
|
|
1255
|
+
var createVoiceTraceS3Sink = (options) => {
|
|
1256
|
+
const client = options.client ?? new Bun.S3Client(options);
|
|
1257
|
+
const keyPrefix = normalizeVoiceTraceS3KeyPrefix(options.keyPrefix);
|
|
1258
|
+
return {
|
|
1259
|
+
deliver: async ({ events }) => {
|
|
1260
|
+
const key = createVoiceTraceS3ObjectKey(keyPrefix, events);
|
|
1261
|
+
const payload = options.body ? await options.body({ events, key }) : {
|
|
1262
|
+
eventCount: events.length,
|
|
1263
|
+
events,
|
|
1264
|
+
key,
|
|
1265
|
+
source: "absolutejs-voice"
|
|
1266
|
+
};
|
|
1267
|
+
try {
|
|
1268
|
+
const file = client.file(key, options);
|
|
1269
|
+
await file.write(JSON.stringify(payload), {
|
|
1270
|
+
type: options.contentType ?? "application/json"
|
|
1271
|
+
});
|
|
1272
|
+
return {
|
|
1273
|
+
attempts: 1,
|
|
1274
|
+
deliveredAt: Date.now(),
|
|
1275
|
+
deliveredTo: resolveVoiceS3DeliveredTo(options, key),
|
|
1276
|
+
eventCount: events.length,
|
|
1277
|
+
responseBody: { key },
|
|
1278
|
+
status: "delivered"
|
|
1279
|
+
};
|
|
1280
|
+
} catch (error) {
|
|
1281
|
+
return {
|
|
1282
|
+
attempts: 1,
|
|
1283
|
+
deliveredTo: resolveVoiceS3DeliveredTo(options, key),
|
|
1284
|
+
error: error instanceof Error ? error.message : String(error),
|
|
1285
|
+
eventCount: events.length,
|
|
1286
|
+
status: "failed"
|
|
1287
|
+
};
|
|
1288
|
+
}
|
|
1289
|
+
},
|
|
1290
|
+
eventTypes: options.eventTypes,
|
|
1291
|
+
id: options.id,
|
|
1292
|
+
kind: options.kind ?? "s3"
|
|
1293
|
+
};
|
|
1294
|
+
};
|
|
1295
|
+
var deliverVoiceTraceEventsToSinks = async (input) => {
|
|
1296
|
+
const events = input.redact ? redactVoiceTraceEvents(input.events, input.redact) : input.events;
|
|
1297
|
+
const sinkDeliveries = {};
|
|
1298
|
+
for (const sink of input.sinks) {
|
|
1299
|
+
const sinkEvents = sink.eventTypes?.length ? events.filter((event) => sink.eventTypes?.includes(event.type)) : events;
|
|
1300
|
+
if (sinkEvents.length === 0) {
|
|
1301
|
+
sinkDeliveries[sink.id] = {
|
|
1302
|
+
attempts: 0,
|
|
1303
|
+
eventCount: 0,
|
|
1304
|
+
status: "skipped"
|
|
1305
|
+
};
|
|
1306
|
+
continue;
|
|
1307
|
+
}
|
|
1308
|
+
try {
|
|
1309
|
+
sinkDeliveries[sink.id] = await sink.deliver({
|
|
1310
|
+
events: sinkEvents
|
|
1311
|
+
});
|
|
1312
|
+
} catch (error) {
|
|
1313
|
+
sinkDeliveries[sink.id] = {
|
|
1314
|
+
attempts: 1,
|
|
1315
|
+
error: error instanceof Error ? error.message : String(error),
|
|
1316
|
+
eventCount: sinkEvents.length,
|
|
1317
|
+
status: "failed"
|
|
1318
|
+
};
|
|
1319
|
+
}
|
|
1320
|
+
}
|
|
1321
|
+
return {
|
|
1322
|
+
deliveredAt: Date.now(),
|
|
1323
|
+
eventCount: events.length,
|
|
1324
|
+
sinkDeliveries,
|
|
1325
|
+
status: aggregateVoiceTraceSinkDeliveryStatus(sinkDeliveries)
|
|
1326
|
+
};
|
|
1327
|
+
};
|
|
1328
|
+
var createVoiceTraceSinkStore = (options) => {
|
|
1329
|
+
const deliver = async (event) => {
|
|
1330
|
+
const result = await deliverVoiceTraceEventsToSinks({
|
|
1331
|
+
events: [event],
|
|
1332
|
+
redact: options.redact,
|
|
1333
|
+
sinks: options.sinks
|
|
1334
|
+
});
|
|
1335
|
+
await options.onDelivery?.(result);
|
|
1336
|
+
};
|
|
1337
|
+
return {
|
|
1338
|
+
append: async (event) => {
|
|
1339
|
+
const stored = await options.store.append(event);
|
|
1340
|
+
if (options.deliveryQueue) {
|
|
1341
|
+
const delivery2 = createVoiceTraceSinkDeliveryRecord({
|
|
1342
|
+
events: [stored]
|
|
1343
|
+
});
|
|
1344
|
+
await options.deliveryQueue.set(delivery2.id, delivery2);
|
|
1345
|
+
return stored;
|
|
1346
|
+
}
|
|
1347
|
+
const delivery = deliver(stored);
|
|
1348
|
+
if (options.awaitDelivery) {
|
|
1349
|
+
await delivery;
|
|
1350
|
+
} else {
|
|
1351
|
+
delivery.catch((error) => {
|
|
1352
|
+
options.onError?.(error);
|
|
1353
|
+
});
|
|
1354
|
+
}
|
|
1355
|
+
return stored;
|
|
1356
|
+
},
|
|
1357
|
+
get: (id) => options.store.get(id),
|
|
1358
|
+
list: (filter) => options.store.list(filter),
|
|
1359
|
+
remove: (id) => options.store.remove(id)
|
|
1360
|
+
};
|
|
1361
|
+
};
|
|
1362
|
+
var createVoiceMemoryTraceSinkDeliveryStore = () => {
|
|
1363
|
+
const deliveries = new Map;
|
|
1364
|
+
return {
|
|
1365
|
+
get: async (id) => deliveries.get(id),
|
|
1366
|
+
list: async () => [...deliveries.values()].sort((left, right) => left.createdAt - right.createdAt || left.id.localeCompare(right.id)),
|
|
1367
|
+
remove: async (id) => {
|
|
1368
|
+
deliveries.delete(id);
|
|
1369
|
+
},
|
|
1370
|
+
set: async (id, delivery) => {
|
|
1371
|
+
deliveries.set(id, delivery);
|
|
1372
|
+
}
|
|
1373
|
+
};
|
|
1374
|
+
};
|
|
1375
|
+
var createVoiceMemoryTraceEventStore = () => {
|
|
1376
|
+
const events = new Map;
|
|
1377
|
+
const append = async (event) => {
|
|
1378
|
+
const stored = createVoiceTraceEvent(event);
|
|
1379
|
+
events.set(stored.id, stored);
|
|
1380
|
+
return stored;
|
|
1381
|
+
};
|
|
1382
|
+
const get = async (id) => events.get(id);
|
|
1383
|
+
const list = async (filter) => filterVoiceTraceEvents([...events.values()], filter);
|
|
1384
|
+
const remove = async (id) => {
|
|
1385
|
+
events.delete(id);
|
|
1386
|
+
};
|
|
1387
|
+
return { append, get, list, remove };
|
|
1388
|
+
};
|
|
1389
|
+
var exportVoiceTrace = async (input) => {
|
|
1390
|
+
const events = await input.store.list(input.filter);
|
|
1391
|
+
return {
|
|
1392
|
+
exportedAt: Date.now(),
|
|
1393
|
+
events: input.redact ? redactVoiceTraceEvents(events, input.redact) : events,
|
|
1394
|
+
filter: input.filter,
|
|
1395
|
+
redacted: Boolean(input.redact)
|
|
1396
|
+
};
|
|
1397
|
+
};
|
|
1398
|
+
var toNumber = (value) => typeof value === "number" && Number.isFinite(value) ? value : 0;
|
|
1399
|
+
var escapeHtml3 = (value) => value.replace(/&/g, "&").replace(/</g, "<").replace(/>/g, ">").replace(/"/g, """);
|
|
1400
|
+
var formatTraceValue = (value) => {
|
|
1401
|
+
if (value === undefined || value === null) {
|
|
1402
|
+
return "";
|
|
1403
|
+
}
|
|
1404
|
+
if (typeof value === "string") {
|
|
1405
|
+
return value;
|
|
1406
|
+
}
|
|
1407
|
+
if (typeof value === "number" || typeof value === "boolean") {
|
|
1408
|
+
return String(value);
|
|
1409
|
+
}
|
|
1410
|
+
try {
|
|
1411
|
+
return JSON.stringify(value);
|
|
1412
|
+
} catch {
|
|
1413
|
+
return String(value);
|
|
1414
|
+
}
|
|
1415
|
+
};
|
|
1416
|
+
var DEFAULT_REDACTION_KEYS = [
|
|
1417
|
+
"apiKey",
|
|
1418
|
+
"authorization",
|
|
1419
|
+
"creditCard",
|
|
1420
|
+
"email",
|
|
1421
|
+
"externalId",
|
|
1422
|
+
"password",
|
|
1423
|
+
"phone",
|
|
1424
|
+
"secret",
|
|
1425
|
+
"ssn",
|
|
1426
|
+
"token"
|
|
1427
|
+
];
|
|
1428
|
+
var DEFAULT_REDACTION_TEXT_KEYS = [
|
|
1429
|
+
"assistantText",
|
|
1430
|
+
"content",
|
|
1431
|
+
"error",
|
|
1432
|
+
"reason",
|
|
1433
|
+
"summary",
|
|
1434
|
+
"text"
|
|
1435
|
+
];
|
|
1436
|
+
var EMAIL_PATTERN = /\b[A-Z0-9._%+-]+@[A-Z0-9.-]+\.[A-Z]{2,}\b/gi;
|
|
1437
|
+
var PHONE_PATTERN = /(?<!\d)(?:\+?1[\s.-]?)?(?:\(?\d{3}\)?[\s.-]?)\d{3}[\s.-]?\d{4}(?!\d)/g;
|
|
1438
|
+
var normalizeRedactionKey = (key) => key.trim().toLowerCase().replace(/[^a-z0-9]/g, "");
|
|
1439
|
+
var resolveVoiceTraceRedactionOptions = (options = {}) => ({
|
|
1440
|
+
keys: typeof options === "boolean" ? DEFAULT_REDACTION_KEYS : options.keys ?? DEFAULT_REDACTION_KEYS,
|
|
1441
|
+
redactEmails: typeof options === "boolean" ? true : options.redactEmails ?? true,
|
|
1442
|
+
redactPhoneNumbers: typeof options === "boolean" ? true : options.redactPhoneNumbers ?? true,
|
|
1443
|
+
redactText: typeof options === "boolean" ? true : options.redactText ?? true,
|
|
1444
|
+
replacement: typeof options === "boolean" ? "[redacted]" : options.replacement ?? "[redacted]",
|
|
1445
|
+
textKeys: typeof options === "boolean" ? DEFAULT_REDACTION_TEXT_KEYS : options.textKeys ?? DEFAULT_REDACTION_TEXT_KEYS
|
|
1446
|
+
});
|
|
1447
|
+
var resolveReplacement = (input) => typeof input.options.replacement === "function" ? input.options.replacement({
|
|
1448
|
+
key: input.key,
|
|
1449
|
+
path: input.path,
|
|
1450
|
+
value: input.value
|
|
1451
|
+
}) : input.options.replacement;
|
|
1452
|
+
var redactVoiceTraceText = (value, options = {}, input = {}) => {
|
|
1453
|
+
const resolved = resolveVoiceTraceRedactionOptions(options);
|
|
1454
|
+
let redacted = value;
|
|
1455
|
+
const replacement = resolveReplacement({
|
|
1456
|
+
key: input.key,
|
|
1457
|
+
options: resolved,
|
|
1458
|
+
path: input.path ?? [],
|
|
1459
|
+
value
|
|
1460
|
+
});
|
|
1461
|
+
if (resolved.redactEmails) {
|
|
1462
|
+
redacted = redacted.replace(EMAIL_PATTERN, replacement);
|
|
1463
|
+
}
|
|
1464
|
+
if (resolved.redactPhoneNumbers) {
|
|
1465
|
+
redacted = redacted.replace(PHONE_PATTERN, replacement);
|
|
1466
|
+
}
|
|
1467
|
+
return redacted;
|
|
1468
|
+
};
|
|
1469
|
+
var redactTraceValue = (value, options, path) => {
|
|
1470
|
+
const key = path.at(-1);
|
|
1471
|
+
const normalizedKey = key ? normalizeRedactionKey(key) : undefined;
|
|
1472
|
+
const sensitiveKeys = new Set(options.keys.map(normalizeRedactionKey));
|
|
1473
|
+
const textKeys = new Set(options.textKeys.map(normalizeRedactionKey));
|
|
1474
|
+
if (normalizedKey && sensitiveKeys.has(normalizedKey) && (value === null || ["boolean", "number", "string", "undefined"].includes(typeof value))) {
|
|
1475
|
+
return resolveReplacement({
|
|
1476
|
+
key,
|
|
1477
|
+
options,
|
|
1478
|
+
path,
|
|
1479
|
+
value: String(value ?? "")
|
|
1480
|
+
});
|
|
1481
|
+
}
|
|
1482
|
+
if (typeof value === "string") {
|
|
1483
|
+
const shouldRedactText = options.redactText && (!normalizedKey || textKeys.has(normalizedKey) || path.length === 0);
|
|
1484
|
+
return shouldRedactText ? redactVoiceTraceText(value, options, {
|
|
1485
|
+
key,
|
|
1486
|
+
path
|
|
1487
|
+
}) : value;
|
|
1488
|
+
}
|
|
1489
|
+
if (Array.isArray(value)) {
|
|
1490
|
+
return value.map((item, index) => redactTraceValue(item, options, [...path, String(index)]));
|
|
1491
|
+
}
|
|
1492
|
+
if (typeof value === "object" && value) {
|
|
1493
|
+
return Object.fromEntries(Object.entries(value).map(([entryKey, entryValue]) => [
|
|
1494
|
+
entryKey,
|
|
1495
|
+
redactTraceValue(entryValue, options, [...path, entryKey])
|
|
1496
|
+
]));
|
|
1497
|
+
}
|
|
1498
|
+
return value;
|
|
1499
|
+
};
|
|
1500
|
+
var redactVoiceTraceEvent = (event, options = {}) => {
|
|
1501
|
+
const resolved = resolveVoiceTraceRedactionOptions(options);
|
|
1502
|
+
return {
|
|
1503
|
+
...event,
|
|
1504
|
+
metadata: redactTraceValue(event.metadata, resolved, ["metadata"]),
|
|
1505
|
+
payload: redactTraceValue(event.payload, resolved, ["payload"])
|
|
1506
|
+
};
|
|
1507
|
+
};
|
|
1508
|
+
var redactVoiceTraceEvents = (events, options = {}) => events.map((event) => redactVoiceTraceEvent(event, options));
|
|
1509
|
+
var summarizeVoiceTrace = (events) => {
|
|
1510
|
+
const sorted = filterVoiceTraceEvents(events);
|
|
1511
|
+
const firstEvent = sorted[0];
|
|
1512
|
+
const lastEvent = sorted.at(-1);
|
|
1513
|
+
const lifecycleEvents = sorted.filter((event) => event.type === "call.lifecycle");
|
|
1514
|
+
const startEvent = lifecycleEvents.find((event) => event.payload.type === "start");
|
|
1515
|
+
const endEvent = lifecycleEvents.toReversed().find((event) => event.payload.type === "end");
|
|
1516
|
+
const costEvents = sorted.filter((event) => event.type === "turn.cost");
|
|
1517
|
+
const toolEvents = sorted.filter((event) => event.type === "agent.tool");
|
|
1518
|
+
const startedAt = startEvent?.at ?? firstEvent?.at;
|
|
1519
|
+
const endedAt = endEvent?.at ?? lastEvent?.at;
|
|
1520
|
+
const failed = sorted.some((event) => event.type === "session.error") || endEvent?.payload.disposition === "failed";
|
|
1521
|
+
return {
|
|
1522
|
+
assistantReplyCount: sorted.filter((event) => event.type === "turn.assistant").length,
|
|
1523
|
+
callDurationMs: startedAt !== undefined && endedAt !== undefined ? Math.max(0, endedAt - startedAt) : undefined,
|
|
1524
|
+
cost: {
|
|
1525
|
+
estimatedRelativeCostUnits: costEvents.reduce((total, event) => total + toNumber(event.payload.estimatedRelativeCostUnits), 0),
|
|
1526
|
+
totalBillableAudioMs: costEvents.reduce((total, event) => total + toNumber(event.payload.totalBillableAudioMs), 0)
|
|
1527
|
+
},
|
|
1528
|
+
endedAt,
|
|
1529
|
+
errorCount: sorted.filter((event) => event.type === "session.error").length,
|
|
1530
|
+
eventCount: sorted.length,
|
|
1531
|
+
failed,
|
|
1532
|
+
handoffCount: sorted.filter((event) => event.type === "agent.handoff").length,
|
|
1533
|
+
modelCallCount: sorted.filter((event) => event.type === "agent.model").length,
|
|
1534
|
+
sessionId: firstEvent?.sessionId,
|
|
1535
|
+
startedAt,
|
|
1536
|
+
toolCallCount: toolEvents.length,
|
|
1537
|
+
toolErrorCount: toolEvents.filter((event) => event.payload.status === "error").length,
|
|
1538
|
+
traceId: firstEvent?.traceId,
|
|
1539
|
+
transcriptCount: sorted.filter((event) => event.type === "turn.transcript").length,
|
|
1540
|
+
turnCount: sorted.filter((event) => event.type === "turn.committed").length
|
|
1541
|
+
};
|
|
1542
|
+
};
|
|
1543
|
+
var evaluateVoiceTrace = (events, options = {}) => {
|
|
1544
|
+
const summary = summarizeVoiceTrace(events);
|
|
1545
|
+
const issues = [];
|
|
1546
|
+
const maxHandoffs = options.maxHandoffs ?? 3;
|
|
1547
|
+
const maxToolErrors = options.maxToolErrors ?? 0;
|
|
1548
|
+
const maxModelCallsPerTurn = options.maxModelCallsPerTurn ?? 6;
|
|
1549
|
+
const turnCountForRatio = Math.max(1, summary.turnCount);
|
|
1550
|
+
if (options.requireCompletedCall !== false && !summary.endedAt) {
|
|
1551
|
+
issues.push({
|
|
1552
|
+
code: "call-not-ended",
|
|
1553
|
+
message: "Trace does not include a call end lifecycle event.",
|
|
1554
|
+
severity: "warning"
|
|
1555
|
+
});
|
|
1556
|
+
}
|
|
1557
|
+
if (summary.failed) {
|
|
1558
|
+
issues.push({
|
|
1559
|
+
code: "session-error",
|
|
1560
|
+
message: "Trace contains a session error or failed call disposition.",
|
|
1561
|
+
severity: "error"
|
|
1562
|
+
});
|
|
1563
|
+
}
|
|
1564
|
+
if (options.requireTranscript !== false && summary.transcriptCount === 0) {
|
|
1565
|
+
issues.push({
|
|
1566
|
+
code: "missing-transcript",
|
|
1567
|
+
message: "Trace does not include any transcript events.",
|
|
1568
|
+
severity: "error"
|
|
1569
|
+
});
|
|
1570
|
+
}
|
|
1571
|
+
if (options.requireTurn !== false && summary.turnCount === 0) {
|
|
1572
|
+
issues.push({
|
|
1573
|
+
code: "missing-turn",
|
|
1574
|
+
message: "Trace does not include any committed turns.",
|
|
1575
|
+
severity: "error"
|
|
1576
|
+
});
|
|
1577
|
+
}
|
|
1578
|
+
if (options.requireAssistantReply !== false && summary.turnCount > 0 && summary.assistantReplyCount === 0) {
|
|
1579
|
+
issues.push({
|
|
1580
|
+
code: "missing-assistant-reply",
|
|
1581
|
+
message: "Trace has committed turns but no assistant replies.",
|
|
1582
|
+
severity: "warning"
|
|
1583
|
+
});
|
|
1584
|
+
}
|
|
1585
|
+
if (summary.toolErrorCount > maxToolErrors) {
|
|
1586
|
+
issues.push({
|
|
1587
|
+
code: "tool-errors",
|
|
1588
|
+
message: `Trace has ${summary.toolErrorCount} tool error(s), above the allowed ${maxToolErrors}.`,
|
|
1589
|
+
severity: "error"
|
|
1590
|
+
});
|
|
1591
|
+
}
|
|
1592
|
+
if (summary.handoffCount > maxHandoffs) {
|
|
1593
|
+
issues.push({
|
|
1594
|
+
code: "too-many-handoffs",
|
|
1595
|
+
message: `Trace has ${summary.handoffCount} handoff(s), above the allowed ${maxHandoffs}.`,
|
|
1596
|
+
severity: "warning"
|
|
1597
|
+
});
|
|
1598
|
+
}
|
|
1599
|
+
if (summary.modelCallCount / turnCountForRatio > maxModelCallsPerTurn) {
|
|
1600
|
+
issues.push({
|
|
1601
|
+
code: "too-many-model-calls",
|
|
1602
|
+
message: `Trace averages more than ${maxModelCallsPerTurn} model calls per committed turn.`,
|
|
1603
|
+
severity: "warning"
|
|
1604
|
+
});
|
|
1605
|
+
}
|
|
1606
|
+
return {
|
|
1607
|
+
issues,
|
|
1608
|
+
pass: !issues.some((issue) => issue.severity === "error"),
|
|
1609
|
+
summary
|
|
1610
|
+
};
|
|
1611
|
+
};
|
|
1612
|
+
var renderTraceEventMarkdown = (event, startedAt) => {
|
|
1613
|
+
const offset = startedAt === undefined ? `${event.at}` : `+${Math.max(0, event.at - startedAt)}ms`;
|
|
1614
|
+
const label = `- ${offset} [${event.type}]`;
|
|
1615
|
+
switch (event.type) {
|
|
1616
|
+
case "turn.transcript":
|
|
1617
|
+
return `${label} ${event.payload.isFinal ? "final" : "partial"} "${formatTraceValue(event.payload.text)}"`;
|
|
1618
|
+
case "turn.committed":
|
|
1619
|
+
return `${label} committed "${formatTraceValue(event.payload.text)}"`;
|
|
1620
|
+
case "turn.assistant":
|
|
1621
|
+
return event.payload.text ? `${label} assistant "${formatTraceValue(event.payload.text)}"` : `${label} ${formatTraceValue(event.payload.status)}`;
|
|
1622
|
+
case "agent.tool":
|
|
1623
|
+
return `${label} ${formatTraceValue(event.payload.toolName)} ${formatTraceValue(event.payload.status)}`;
|
|
1624
|
+
case "agent.handoff":
|
|
1625
|
+
return `${label} ${formatTraceValue(event.payload.fromAgentId)} -> ${formatTraceValue(event.payload.targetAgentId)}`;
|
|
1626
|
+
case "session.error":
|
|
1627
|
+
return `${label} ${formatTraceValue(event.payload.error)}`;
|
|
1628
|
+
case "call.lifecycle":
|
|
1629
|
+
return `${label} ${formatTraceValue(event.payload.type)} ${formatTraceValue(event.payload.disposition)}`.trim();
|
|
1630
|
+
default:
|
|
1631
|
+
return `${label} ${formatTraceValue(event.payload)}`;
|
|
1632
|
+
}
|
|
1633
|
+
};
|
|
1634
|
+
var renderVoiceTraceMarkdown = (events, options = {}) => {
|
|
1635
|
+
const sorted = filterVoiceTraceEvents(options.redact ? redactVoiceTraceEvents(events, options.redact) : events);
|
|
1636
|
+
const summary = summarizeVoiceTrace(sorted);
|
|
1637
|
+
const evaluation = evaluateVoiceTrace(sorted, options.evaluation);
|
|
1638
|
+
const lines = [
|
|
1639
|
+
`# ${options.title ?? `Voice Trace ${summary.sessionId ?? ""}`.trim()}`,
|
|
1640
|
+
"",
|
|
1641
|
+
`Pass: ${evaluation.pass ? "yes" : "no"}`,
|
|
1642
|
+
`Session: ${summary.sessionId ?? "unknown"}`,
|
|
1643
|
+
`Events: ${summary.eventCount}`,
|
|
1644
|
+
`Turns: ${summary.turnCount}`,
|
|
1645
|
+
`Transcripts: ${summary.transcriptCount}`,
|
|
1646
|
+
`Assistant replies: ${summary.assistantReplyCount}`,
|
|
1647
|
+
`Model calls: ${summary.modelCallCount}`,
|
|
1648
|
+
`Tool calls: ${summary.toolCallCount}`,
|
|
1649
|
+
`Handoffs: ${summary.handoffCount}`,
|
|
1650
|
+
`Errors: ${summary.errorCount}`,
|
|
1651
|
+
`Estimated cost units: ${summary.cost.estimatedRelativeCostUnits}`,
|
|
1652
|
+
""
|
|
1653
|
+
];
|
|
1654
|
+
if (evaluation.issues.length > 0) {
|
|
1655
|
+
lines.push("## Issues", "");
|
|
1656
|
+
for (const issue of evaluation.issues) {
|
|
1657
|
+
lines.push(`- [${issue.severity}] ${issue.code}: ${issue.message}`);
|
|
1658
|
+
}
|
|
1659
|
+
lines.push("");
|
|
1660
|
+
}
|
|
1661
|
+
lines.push("## Timeline", "");
|
|
1662
|
+
for (const event of sorted) {
|
|
1663
|
+
lines.push(renderTraceEventMarkdown(event, summary.startedAt));
|
|
1664
|
+
}
|
|
1665
|
+
return lines.join(`
|
|
1666
|
+
`);
|
|
1667
|
+
};
|
|
1668
|
+
var renderVoiceTraceHTML = (events, options = {}) => {
|
|
1669
|
+
const markdown = renderVoiceTraceMarkdown(events, options);
|
|
1670
|
+
const renderEvents = options.redact ? redactVoiceTraceEvents(events, options.redact) : events;
|
|
1671
|
+
const summary = summarizeVoiceTrace(renderEvents);
|
|
1672
|
+
const evaluation = evaluateVoiceTrace(renderEvents, options.evaluation);
|
|
1673
|
+
const eventRows = filterVoiceTraceEvents(renderEvents).map((event) => {
|
|
1674
|
+
const offset = summary.startedAt === undefined ? event.at : Math.max(0, event.at - summary.startedAt);
|
|
1675
|
+
return [
|
|
1676
|
+
"<tr>",
|
|
1677
|
+
`<td>${escapeHtml3(String(offset))}</td>`,
|
|
1678
|
+
`<td>${escapeHtml3(event.type)}</td>`,
|
|
1679
|
+
`<td>${escapeHtml3(event.turnId ?? "")}</td>`,
|
|
1680
|
+
`<td><code>${escapeHtml3(JSON.stringify(event.payload))}</code></td>`,
|
|
1681
|
+
"</tr>"
|
|
1682
|
+
].join("");
|
|
1683
|
+
}).join(`
|
|
1684
|
+
`);
|
|
1685
|
+
return [
|
|
1686
|
+
"<!doctype html>",
|
|
1687
|
+
'<html lang="en">',
|
|
1688
|
+
"<head>",
|
|
1689
|
+
'<meta charset="utf-8" />',
|
|
1690
|
+
'<meta name="viewport" content="width=device-width, initial-scale=1" />',
|
|
1691
|
+
`<title>${escapeHtml3(options.title ?? "Voice Trace")}</title>`,
|
|
1692
|
+
"<style>",
|
|
1693
|
+
"body{font-family:ui-sans-serif,system-ui,sans-serif;margin:2rem;line-height:1.45;background:#f8f7f2;color:#181713}",
|
|
1694
|
+
"main{max-width:1100px;margin:auto}",
|
|
1695
|
+
".summary{display:grid;grid-template-columns:repeat(auto-fit,minmax(160px,1fr));gap:.75rem;margin:1rem 0}",
|
|
1696
|
+
".card{background:white;border:1px solid #ded9cc;border-radius:12px;padding:1rem}",
|
|
1697
|
+
".pass{color:#126b3a}.fail{color:#9d2222}",
|
|
1698
|
+
"table{border-collapse:collapse;width:100%;background:white;border:1px solid #ded9cc}",
|
|
1699
|
+
"th,td{border-bottom:1px solid #eee8dc;padding:.65rem;text-align:left;vertical-align:top}",
|
|
1700
|
+
"code{white-space:pre-wrap;word-break:break-word}",
|
|
1701
|
+
"pre{background:#181713;color:#f8f7f2;padding:1rem;border-radius:12px;overflow:auto}",
|
|
1702
|
+
"</style>",
|
|
1703
|
+
"</head>",
|
|
1704
|
+
"<body><main>",
|
|
1705
|
+
`<h1>${escapeHtml3(options.title ?? `Voice Trace ${summary.sessionId ?? ""}`.trim())}</h1>`,
|
|
1706
|
+
`<p class="${evaluation.pass ? "pass" : "fail"}">QA: ${evaluation.pass ? "pass" : "fail"}</p>`,
|
|
1707
|
+
'<section class="summary">',
|
|
1708
|
+
`<div class="card"><strong>Events</strong><br>${summary.eventCount}</div>`,
|
|
1709
|
+
`<div class="card"><strong>Turns</strong><br>${summary.turnCount}</div>`,
|
|
1710
|
+
`<div class="card"><strong>Transcripts</strong><br>${summary.transcriptCount}</div>`,
|
|
1711
|
+
`<div class="card"><strong>Tool errors</strong><br>${summary.toolErrorCount}</div>`,
|
|
1712
|
+
`<div class="card"><strong>Cost units</strong><br>${summary.cost.estimatedRelativeCostUnits}</div>`,
|
|
1713
|
+
"</section>",
|
|
1714
|
+
"<h2>Timeline</h2>",
|
|
1715
|
+
"<table><thead><tr><th>Offset ms</th><th>Type</th><th>Turn</th><th>Payload</th></tr></thead><tbody>",
|
|
1716
|
+
eventRows,
|
|
1717
|
+
"</tbody></table>",
|
|
1718
|
+
"<h2>Markdown Export</h2>",
|
|
1719
|
+
`<pre>${escapeHtml3(markdown)}</pre>`,
|
|
1720
|
+
"</main></body></html>"
|
|
1721
|
+
].join(`
|
|
1722
|
+
`);
|
|
1723
|
+
};
|
|
1724
|
+
var buildVoiceTraceReplay = (events, options = {}) => ({
|
|
1725
|
+
evaluation: evaluateVoiceTrace(options.redact ? redactVoiceTraceEvents(events, options.redact) : events, options.evaluation),
|
|
1726
|
+
html: renderVoiceTraceHTML(events, options),
|
|
1727
|
+
markdown: renderVoiceTraceMarkdown(events, options),
|
|
1728
|
+
summary: summarizeVoiceTrace(options.redact ? redactVoiceTraceEvents(events, options.redact) : events)
|
|
1729
|
+
});
|
|
1730
|
+
|
|
1731
|
+
// src/liveOps.ts
|
|
1732
|
+
var VOICE_LIVE_OPS_ACTIONS = [
|
|
1733
|
+
"assign",
|
|
1734
|
+
"create-task",
|
|
1735
|
+
"escalate",
|
|
1736
|
+
"force-handoff",
|
|
1737
|
+
"inject-instruction",
|
|
1738
|
+
"operator-takeover",
|
|
1739
|
+
"pause-assistant",
|
|
1740
|
+
"resume-assistant",
|
|
1741
|
+
"tag"
|
|
1742
|
+
];
|
|
1743
|
+
var isVoiceLiveOpsAction = (value) => typeof value === "string" && VOICE_LIVE_OPS_ACTIONS.includes(value);
|
|
1744
|
+
var toStringValue = (value) => typeof value === "string" && value.trim() ? value.trim() : undefined;
|
|
1745
|
+
var createVoiceMemoryLiveOpsControlStore = () => {
|
|
1746
|
+
const states = new Map;
|
|
1747
|
+
return {
|
|
1748
|
+
get: (sessionId) => states.get(sessionId),
|
|
1749
|
+
set: (sessionId, state) => {
|
|
1750
|
+
states.set(sessionId, state);
|
|
1751
|
+
}
|
|
1752
|
+
};
|
|
1753
|
+
};
|
|
1754
|
+
var getVoiceLiveOpsControlStatus = (action) => {
|
|
1755
|
+
switch (action) {
|
|
1756
|
+
case "force-handoff":
|
|
1757
|
+
return "handoff-forced";
|
|
1758
|
+
case "inject-instruction":
|
|
1759
|
+
return "instruction-injected";
|
|
1760
|
+
case "operator-takeover":
|
|
1761
|
+
return "operator-takeover";
|
|
1762
|
+
case "pause-assistant":
|
|
1763
|
+
return "assistant-paused";
|
|
1764
|
+
case "resume-assistant":
|
|
1765
|
+
return "assistant-resumed";
|
|
1766
|
+
default:
|
|
1767
|
+
return "recorded";
|
|
1768
|
+
}
|
|
1769
|
+
};
|
|
1770
|
+
var buildVoiceLiveOpsControlState = (input) => ({
|
|
1771
|
+
assistantPaused: input.action === "pause-assistant" || input.action === "operator-takeover" || input.action === "force-handoff" ? true : input.action === "resume-assistant" ? false : input.previous?.assistantPaused ?? false,
|
|
1772
|
+
handoffTarget: input.action === "force-handoff" ? input.tag : input.previous?.handoffTarget,
|
|
1773
|
+
injectedInstruction: input.action === "inject-instruction" ? input.detail : input.previous?.injectedInstruction,
|
|
1774
|
+
lastAction: input.action,
|
|
1775
|
+
lastUpdatedAt: input.at ?? Date.now(),
|
|
1776
|
+
operator: input.assignee,
|
|
1777
|
+
operatorTakeover: input.action === "operator-takeover" ? true : input.action === "resume-assistant" ? false : input.previous?.operatorTakeover ?? false,
|
|
1778
|
+
status: getVoiceLiveOpsControlStatus(input.action),
|
|
1779
|
+
tag: input.tag
|
|
1780
|
+
});
|
|
1781
|
+
var createVoiceLiveOpsController = (options = {}) => {
|
|
1782
|
+
const store = options.store ?? createVoiceMemoryLiveOpsControlStore();
|
|
1783
|
+
const perform = async (input) => {
|
|
1784
|
+
if (!input.sessionId) {
|
|
1785
|
+
throw new Error("Voice live ops action requires sessionId.");
|
|
1786
|
+
}
|
|
1787
|
+
if (!isVoiceLiveOpsAction(input.action)) {
|
|
1788
|
+
throw new Error("Voice live ops action is not supported.");
|
|
1789
|
+
}
|
|
1790
|
+
const at = Date.now();
|
|
1791
|
+
const assignee = input.assignee ?? options.defaultAssignee ?? "operator";
|
|
1792
|
+
const tag = input.tag ?? options.defaultTag ?? "live-ops";
|
|
1793
|
+
const detail = input.detail ?? options.defaultDetail ?? input.action;
|
|
1794
|
+
const previous = await store.get(input.sessionId);
|
|
1795
|
+
const control = buildVoiceLiveOpsControlState({
|
|
1796
|
+
...input,
|
|
1797
|
+
assignee,
|
|
1798
|
+
at,
|
|
1799
|
+
detail,
|
|
1800
|
+
previous,
|
|
1801
|
+
tag
|
|
1802
|
+
});
|
|
1803
|
+
await store.set(input.sessionId, control);
|
|
1804
|
+
const traceId = `voice-live-ops:${input.sessionId}:${input.action}:${at}`;
|
|
1805
|
+
await Promise.all([
|
|
1806
|
+
options.audit?.append(createVoiceAuditEvent({
|
|
1807
|
+
action: `voice.live_ops.${input.action}`,
|
|
1808
|
+
actor: {
|
|
1809
|
+
id: assignee,
|
|
1810
|
+
kind: "operator",
|
|
1811
|
+
name: assignee
|
|
1812
|
+
},
|
|
1813
|
+
at,
|
|
1814
|
+
metadata: {
|
|
1815
|
+
source: "voice-live-ops",
|
|
1816
|
+
tag
|
|
1817
|
+
},
|
|
1818
|
+
outcome: "success",
|
|
1819
|
+
payload: {
|
|
1820
|
+
action: input.action,
|
|
1821
|
+
assignee,
|
|
1822
|
+
control,
|
|
1823
|
+
detail,
|
|
1824
|
+
tag
|
|
1825
|
+
},
|
|
1826
|
+
resource: {
|
|
1827
|
+
id: input.sessionId,
|
|
1828
|
+
type: "voice.session"
|
|
1829
|
+
},
|
|
1830
|
+
sessionId: input.sessionId,
|
|
1831
|
+
traceId,
|
|
1832
|
+
type: "operator.action"
|
|
1833
|
+
})),
|
|
1834
|
+
options.trace?.append(createVoiceTraceEvent({
|
|
1835
|
+
at,
|
|
1836
|
+
metadata: {
|
|
1837
|
+
source: "voice-live-ops",
|
|
1838
|
+
tag
|
|
1839
|
+
},
|
|
1840
|
+
payload: {
|
|
1841
|
+
action: input.action,
|
|
1842
|
+
assignee,
|
|
1843
|
+
control,
|
|
1844
|
+
detail,
|
|
1845
|
+
status: "success",
|
|
1846
|
+
tag
|
|
1847
|
+
},
|
|
1848
|
+
sessionId: input.sessionId,
|
|
1849
|
+
traceId,
|
|
1850
|
+
type: "operator.action"
|
|
1851
|
+
}))
|
|
1852
|
+
]);
|
|
1853
|
+
const result = {
|
|
1854
|
+
action: input.action,
|
|
1855
|
+
control,
|
|
1856
|
+
ok: true,
|
|
1857
|
+
sessionId: input.sessionId
|
|
1858
|
+
};
|
|
1859
|
+
await options.onAction?.({
|
|
1860
|
+
...result,
|
|
1861
|
+
assignee,
|
|
1862
|
+
detail,
|
|
1863
|
+
tag
|
|
1864
|
+
});
|
|
1865
|
+
return result;
|
|
1866
|
+
};
|
|
1867
|
+
return {
|
|
1868
|
+
get: (sessionId) => store.get(sessionId),
|
|
1869
|
+
perform,
|
|
1870
|
+
store
|
|
1871
|
+
};
|
|
1872
|
+
};
|
|
1873
|
+
var readVoiceLiveOpsActionInput = async (request) => {
|
|
1874
|
+
const body = await request.json().catch(() => null);
|
|
1875
|
+
if (!body || typeof body !== "object") {
|
|
1876
|
+
throw new Error("Voice live ops action requires a JSON body.");
|
|
1877
|
+
}
|
|
1878
|
+
const record = body;
|
|
1879
|
+
const action = record.action;
|
|
1880
|
+
const sessionId = toStringValue(record.sessionId);
|
|
1881
|
+
if (!sessionId || !isVoiceLiveOpsAction(action)) {
|
|
1882
|
+
throw new Error("Voice live ops action requires valid sessionId and action.");
|
|
1883
|
+
}
|
|
1884
|
+
return {
|
|
1885
|
+
action,
|
|
1886
|
+
assignee: toStringValue(record.assignee),
|
|
1887
|
+
detail: toStringValue(record.detail),
|
|
1888
|
+
sessionId,
|
|
1889
|
+
tag: toStringValue(record.tag)
|
|
1890
|
+
};
|
|
1891
|
+
};
|
|
1892
|
+
var createVoiceLiveOpsRoutes = (options = {}) => {
|
|
1893
|
+
const controller = createVoiceLiveOpsController(options);
|
|
1894
|
+
const path = options.path ?? "/api/voice/live-ops/action";
|
|
1895
|
+
const controlPath = options.controlPath ?? "/api/voice/live-ops/control/:sessionId";
|
|
1896
|
+
return new Elysia({
|
|
1897
|
+
name: options.name ?? "absolutejs-voice-live-ops"
|
|
1898
|
+
}).post(path, async ({ request, set }) => {
|
|
1899
|
+
try {
|
|
1900
|
+
return await controller.perform(await readVoiceLiveOpsActionInput(request));
|
|
1901
|
+
} catch (error) {
|
|
1902
|
+
set.status = 400;
|
|
1903
|
+
return {
|
|
1904
|
+
error: error instanceof Error ? error.message : String(error),
|
|
1905
|
+
ok: false
|
|
1906
|
+
};
|
|
1907
|
+
}
|
|
1908
|
+
}).get(controlPath, async ({ params }) => {
|
|
1909
|
+
const sessionId = params.sessionId;
|
|
1910
|
+
return {
|
|
1911
|
+
control: await controller.get(sessionId),
|
|
1912
|
+
ok: true,
|
|
1913
|
+
sessionId
|
|
1914
|
+
};
|
|
1915
|
+
});
|
|
1916
|
+
};
|
|
1917
|
+
|
|
1918
|
+
// src/client/liveOpsWidget.ts
|
|
1919
|
+
var ACTION_LABELS = {
|
|
1920
|
+
assign: "Assign",
|
|
1921
|
+
"create-task": "Create task",
|
|
1922
|
+
escalate: "Escalate",
|
|
1923
|
+
"force-handoff": "Force handoff",
|
|
1924
|
+
"inject-instruction": "Inject instruction",
|
|
1925
|
+
"operator-takeover": "Take over",
|
|
1926
|
+
"pause-assistant": "Pause assistant",
|
|
1927
|
+
"resume-assistant": "Resume assistant",
|
|
1928
|
+
tag: "Tag"
|
|
1929
|
+
};
|
|
1930
|
+
var escapeHtml4 = (value) => value.replaceAll("&", "&").replaceAll("<", "<").replaceAll(">", ">").replaceAll('"', """).replaceAll("'", "'");
|
|
1931
|
+
var createVoiceLiveOpsInput = (action, input) => ({
|
|
1932
|
+
action,
|
|
1933
|
+
assignee: input.assignee,
|
|
1934
|
+
detail: input.detail,
|
|
1935
|
+
sessionId: input.sessionId ?? "",
|
|
1936
|
+
tag: input.tag
|
|
1937
|
+
});
|
|
1938
|
+
var renderVoiceLiveOpsHTML = (snapshot, options = {}) => {
|
|
1939
|
+
const sessionId = options.getSessionId?.() ?? "";
|
|
1940
|
+
const disabled = snapshot.isRunning || !sessionId;
|
|
1941
|
+
const actions = VOICE_LIVE_OPS_ACTIONS.map((action) => `<button type="button" data-absolute-voice-live-ops-action="${escapeHtml4(action)}"${disabled ? " disabled" : ""}>${escapeHtml4(snapshot.runningAction === action ? "Running..." : ACTION_LABELS[action])}</button>`).join("");
|
|
1942
|
+
const result = snapshot.error ? `<p class="absolute-voice-live-ops__error">${escapeHtml4(snapshot.error)}</p>` : snapshot.lastResult ? `<p class="absolute-voice-live-ops__result">Recorded ${escapeHtml4(snapshot.lastResult.action)}. Control: ${escapeHtml4(snapshot.lastResult.control.status)}.</p>` : '<p class="absolute-voice-live-ops__result">No live ops action has run yet.</p>';
|
|
1943
|
+
return `<section class="absolute-voice-live-ops">
|
|
1944
|
+
<header class="absolute-voice-live-ops__header">
|
|
1945
|
+
<span>${escapeHtml4(options.title ?? "Live Ops")}</span>
|
|
1946
|
+
<strong>${escapeHtml4(sessionId || "No active session")}</strong>
|
|
1947
|
+
</header>
|
|
1948
|
+
<p class="absolute-voice-live-ops__description">${escapeHtml4(options.description ?? "Pause, resume, take over, force handoff, or inject operator instructions during a live voice session.")}</p>
|
|
1949
|
+
<label><span>Operator</span><input data-absolute-voice-live-ops-assignee value="${escapeHtml4(options.defaultAssignee ?? "operator")}" /></label>
|
|
1950
|
+
<label><span>Tag / handoff target</span><input data-absolute-voice-live-ops-tag value="${escapeHtml4(options.defaultTag ?? "live-ops")}" /></label>
|
|
1951
|
+
<label><span>Detail / instruction</span><input data-absolute-voice-live-ops-detail value="${escapeHtml4(options.defaultDetail ?? "Operator marked this live session.")}" /></label>
|
|
1952
|
+
<div class="absolute-voice-live-ops__actions">${actions}</div>
|
|
1953
|
+
${result}
|
|
1954
|
+
</section>`;
|
|
1955
|
+
};
|
|
1956
|
+
var getVoiceLiveOpsCSS = () => `.absolute-voice-live-ops{border:1px solid #f59e0b66;border-radius:20px;background:#111827;color:#f8fafc;padding:18px;font-family:inherit}.absolute-voice-live-ops__header{align-items:start;display:flex;gap:12px;justify-content:space-between}.absolute-voice-live-ops__header span{color:#fbbf24;font-size:12px;font-weight:900;letter-spacing:.08em;text-transform:uppercase}.absolute-voice-live-ops__header strong{font-size:18px;overflow-wrap:anywhere}.absolute-voice-live-ops__description,.absolute-voice-live-ops__result{color:#cbd5e1}.absolute-voice-live-ops label{display:grid;gap:6px;margin-top:12px}.absolute-voice-live-ops label span{color:#94a3b8;font-size:13px}.absolute-voice-live-ops input{background:#020617;border:1px solid #f59e0b66;border-radius:12px;color:#f8fafc;font:inherit;padding:10px 12px}.absolute-voice-live-ops__actions{display:flex;flex-wrap:wrap;gap:8px;margin-top:14px}.absolute-voice-live-ops__actions button{background:#f59e0b;border:0;border-radius:999px;color:#111827;cursor:pointer;font:inherit;font-weight:900;padding:8px 12px}.absolute-voice-live-ops__actions button:disabled{cursor:not-allowed;opacity:.5}.absolute-voice-live-ops__error{color:#fecaca;font-weight:800}`;
|
|
1957
|
+
var mountVoiceLiveOps = (element, options = {}) => {
|
|
1958
|
+
const store = createVoiceLiveOpsStore(options);
|
|
1959
|
+
let assignee = options.defaultAssignee ?? "operator";
|
|
1960
|
+
let detail = options.defaultDetail ?? "Operator marked this live session.";
|
|
1961
|
+
let tag = options.defaultTag ?? "live-ops";
|
|
1962
|
+
const syncInputs = () => {
|
|
1963
|
+
const assigneeInput = element.querySelector("[data-absolute-voice-live-ops-assignee]");
|
|
1964
|
+
const detailInput = element.querySelector("[data-absolute-voice-live-ops-detail]");
|
|
1965
|
+
const tagInput = element.querySelector("[data-absolute-voice-live-ops-tag]");
|
|
1966
|
+
if (assigneeInput instanceof HTMLInputElement) {
|
|
1967
|
+
assignee = assigneeInput.value;
|
|
1968
|
+
}
|
|
1969
|
+
if (detailInput instanceof HTMLInputElement) {
|
|
1970
|
+
detail = detailInput.value;
|
|
1971
|
+
}
|
|
1972
|
+
if (tagInput instanceof HTMLInputElement) {
|
|
1973
|
+
tag = tagInput.value;
|
|
1974
|
+
}
|
|
1975
|
+
};
|
|
1976
|
+
const render = () => {
|
|
1977
|
+
element.innerHTML = renderVoiceLiveOpsHTML(store.getSnapshot(), {
|
|
1978
|
+
...options,
|
|
1979
|
+
defaultAssignee: assignee,
|
|
1980
|
+
defaultDetail: detail,
|
|
1981
|
+
defaultTag: tag
|
|
1982
|
+
});
|
|
1983
|
+
};
|
|
1984
|
+
const unsubscribe = store.subscribe(render);
|
|
1985
|
+
const handleInput = () => syncInputs();
|
|
1986
|
+
const handleClick = (event) => {
|
|
1987
|
+
const target = event.target;
|
|
1988
|
+
if (!(target instanceof Element)) {
|
|
1989
|
+
return;
|
|
1990
|
+
}
|
|
1991
|
+
const button = target.closest("[data-absolute-voice-live-ops-action]");
|
|
1992
|
+
const action = button?.getAttribute("data-absolute-voice-live-ops-action");
|
|
1993
|
+
if (!action) {
|
|
1994
|
+
return;
|
|
1995
|
+
}
|
|
1996
|
+
syncInputs();
|
|
1997
|
+
store.run(createVoiceLiveOpsInput(action, {
|
|
1998
|
+
assignee,
|
|
1999
|
+
detail,
|
|
2000
|
+
sessionId: options.getSessionId?.(),
|
|
2001
|
+
tag
|
|
2002
|
+
})).catch(() => {});
|
|
2003
|
+
};
|
|
2004
|
+
element.addEventListener?.("click", handleClick);
|
|
2005
|
+
element.addEventListener?.("input", handleInput);
|
|
2006
|
+
render();
|
|
2007
|
+
return {
|
|
2008
|
+
close: () => {
|
|
2009
|
+
element.removeEventListener?.("click", handleClick);
|
|
2010
|
+
element.removeEventListener?.("input", handleInput);
|
|
2011
|
+
unsubscribe();
|
|
2012
|
+
store.close();
|
|
2013
|
+
},
|
|
2014
|
+
run: store.run
|
|
2015
|
+
};
|
|
2016
|
+
};
|
|
2017
|
+
var defineVoiceLiveOpsElement = (tagName = "absolute-voice-live-ops", options = {}) => {
|
|
2018
|
+
if (typeof window === "undefined" || typeof customElements === "undefined" || customElements.get(tagName)) {
|
|
2019
|
+
return;
|
|
2020
|
+
}
|
|
2021
|
+
customElements.define(tagName, class AbsoluteVoiceLiveOpsElement extends HTMLElement {
|
|
2022
|
+
mounted;
|
|
2023
|
+
connectedCallback() {
|
|
2024
|
+
this.mounted = mountVoiceLiveOps(this, {
|
|
2025
|
+
...options,
|
|
2026
|
+
description: this.getAttribute("description") ?? options.description,
|
|
2027
|
+
getSessionId: options.getSessionId ?? (() => this.getAttribute("session-id") ?? undefined),
|
|
2028
|
+
title: this.getAttribute("title") ?? options.title
|
|
2029
|
+
});
|
|
2030
|
+
}
|
|
2031
|
+
disconnectedCallback() {
|
|
2032
|
+
this.mounted?.close();
|
|
2033
|
+
this.mounted = undefined;
|
|
2034
|
+
}
|
|
2035
|
+
});
|
|
2036
|
+
};
|
|
2037
|
+
|
|
2038
|
+
// src/svelte/createVoiceLiveOps.ts
|
|
2039
|
+
var createVoiceLiveOps = (options = {}) => {
|
|
2040
|
+
const store = createVoiceLiveOpsStore(options);
|
|
2041
|
+
return {
|
|
2042
|
+
close: store.close,
|
|
2043
|
+
getHTML: () => renderVoiceLiveOpsHTML(store.getSnapshot(), options),
|
|
2044
|
+
getSnapshot: store.getSnapshot,
|
|
2045
|
+
mount: (element) => mountVoiceLiveOps(element, options),
|
|
2046
|
+
run: store.run,
|
|
2047
|
+
subscribe: store.subscribe
|
|
2048
|
+
};
|
|
2049
|
+
};
|
|
763
2050
|
// src/client/opsStatus.ts
|
|
764
2051
|
var fetchVoiceOpsStatus = async (path = "/api/voice/ops-status", options = {}) => {
|
|
765
2052
|
const fetchImpl = options.fetch ?? globalThis.fetch;
|
|
@@ -849,7 +2136,7 @@ var SURFACE_LABELS = {
|
|
|
849
2136
|
sessions: "Sessions",
|
|
850
2137
|
workflows: "Workflows"
|
|
851
2138
|
};
|
|
852
|
-
var
|
|
2139
|
+
var escapeHtml5 = (value) => value.replaceAll("&", "&").replaceAll("<", "<").replaceAll(">", ">").replaceAll('"', """).replaceAll("'", "'");
|
|
853
2140
|
var readNumber = (value, key) => value && typeof value === "object" && (key in value) ? Number(value[key] ?? 0) : 0;
|
|
854
2141
|
var surfaceDetail = (surface) => {
|
|
855
2142
|
const total = readNumber(surface, "total");
|
|
@@ -904,24 +2191,24 @@ var createVoiceOpsStatusViewModel = (snapshot, options = {}) => {
|
|
|
904
2191
|
};
|
|
905
2192
|
var renderVoiceOpsStatusHTML = (snapshot, options = {}) => {
|
|
906
2193
|
const model = createVoiceOpsStatusViewModel(snapshot, options);
|
|
907
|
-
const surfaces = model.surfaces.length ? model.surfaces.map((surface) => `<li class="absolute-voice-ops-status__surface absolute-voice-ops-status__surface--${
|
|
908
|
-
<span>${
|
|
909
|
-
<strong>${
|
|
2194
|
+
const surfaces = model.surfaces.length ? model.surfaces.map((surface) => `<li class="absolute-voice-ops-status__surface absolute-voice-ops-status__surface--${escapeHtml5(surface.status)}">
|
|
2195
|
+
<span>${escapeHtml5(surface.label)}</span>
|
|
2196
|
+
<strong>${escapeHtml5(surface.detail)}</strong>
|
|
910
2197
|
</li>`).join("") : '<li class="absolute-voice-ops-status__surface"><span>Status</span><strong>Waiting for first check</strong></li>';
|
|
911
|
-
const links = model.links.length ? `<nav class="absolute-voice-ops-status__links">${model.links.slice(0, 4).map((link) => `<a href="${
|
|
912
|
-
return `<section class="absolute-voice-ops-status absolute-voice-ops-status--${
|
|
2198
|
+
const links = model.links.length ? `<nav class="absolute-voice-ops-status__links">${model.links.slice(0, 4).map((link) => `<a href="${escapeHtml5(link.href)}">${escapeHtml5(link.label)}</a>`).join("")}</nav>` : "";
|
|
2199
|
+
return `<section class="absolute-voice-ops-status absolute-voice-ops-status--${escapeHtml5(model.status)}">
|
|
913
2200
|
<header class="absolute-voice-ops-status__header">
|
|
914
|
-
<span class="absolute-voice-ops-status__eyebrow">${
|
|
915
|
-
<strong class="absolute-voice-ops-status__label">${
|
|
2201
|
+
<span class="absolute-voice-ops-status__eyebrow">${escapeHtml5(model.title)}</span>
|
|
2202
|
+
<strong class="absolute-voice-ops-status__label">${escapeHtml5(model.label)}</strong>
|
|
916
2203
|
</header>
|
|
917
|
-
<p class="absolute-voice-ops-status__description">${
|
|
2204
|
+
<p class="absolute-voice-ops-status__description">${escapeHtml5(model.description)}</p>
|
|
918
2205
|
<div class="absolute-voice-ops-status__summary">
|
|
919
2206
|
<span>${model.passed} passing</span>
|
|
920
2207
|
<span>${Math.max(model.total - model.passed, 0)} failing</span>
|
|
921
2208
|
<span>${model.total} checks</span>
|
|
922
2209
|
</div>
|
|
923
2210
|
<ul class="absolute-voice-ops-status__surfaces">${surfaces}</ul>
|
|
924
|
-
${model.error ? `<p class="absolute-voice-ops-status__error">${
|
|
2211
|
+
${model.error ? `<p class="absolute-voice-ops-status__error">${escapeHtml5(model.error)}</p>` : ""}
|
|
925
2212
|
${links}
|
|
926
2213
|
</section>`;
|
|
927
2214
|
};
|
|
@@ -1056,7 +2343,7 @@ var createVoiceProviderSimulationControlsStore = (options) => {
|
|
|
1056
2343
|
};
|
|
1057
2344
|
|
|
1058
2345
|
// src/client/providerSimulationControlsWidget.ts
|
|
1059
|
-
var
|
|
2346
|
+
var escapeHtml6 = (value) => value.replaceAll("&", "&").replaceAll("<", "<").replaceAll(">", ">").replaceAll('"', """).replaceAll("'", "'");
|
|
1060
2347
|
var formatKind = (kind) => (kind ?? "stt").toUpperCase();
|
|
1061
2348
|
var createVoiceProviderSimulationControlsViewModel = (snapshot, options) => {
|
|
1062
2349
|
const configuredProviders = options.providers.filter((provider) => provider.configured !== false);
|
|
@@ -1076,18 +2363,18 @@ var createVoiceProviderSimulationControlsViewModel = (snapshot, options) => {
|
|
|
1076
2363
|
};
|
|
1077
2364
|
var renderVoiceProviderSimulationControlsHTML = (snapshot, options) => {
|
|
1078
2365
|
const model = createVoiceProviderSimulationControlsViewModel(snapshot, options);
|
|
1079
|
-
const failureButtons = model.failureProviders.map((provider) => `<button type="button" data-voice-provider-fail="${
|
|
1080
|
-
const recoveryButtons = model.providers.map((provider) => `<button type="button" data-voice-provider-recover="${
|
|
2366
|
+
const failureButtons = model.failureProviders.map((provider) => `<button type="button" data-voice-provider-fail="${escapeHtml6(provider.provider)}"${!model.canSimulateFailure || snapshot.isRunning ? " disabled" : ""}>Simulate ${escapeHtml6(provider.provider)} ${escapeHtml6(formatKind(options.kind))} failure</button>`).join("");
|
|
2367
|
+
const recoveryButtons = model.providers.map((provider) => `<button type="button" data-voice-provider-recover="${escapeHtml6(provider.provider)}"${snapshot.isRunning ? " disabled" : ""}>Mark ${escapeHtml6(provider.provider)} recovered</button>`).join("");
|
|
1081
2368
|
return `<section class="absolute-voice-provider-simulation absolute-voice-provider-simulation--${snapshot.error ? "error" : snapshot.isRunning ? "running" : "ready"}">
|
|
1082
2369
|
<header class="absolute-voice-provider-simulation__header">
|
|
1083
|
-
<span class="absolute-voice-provider-simulation__eyebrow">${
|
|
1084
|
-
<strong class="absolute-voice-provider-simulation__label">${
|
|
2370
|
+
<span class="absolute-voice-provider-simulation__eyebrow">${escapeHtml6(model.title)}</span>
|
|
2371
|
+
<strong class="absolute-voice-provider-simulation__label">${escapeHtml6(model.label)}</strong>
|
|
1085
2372
|
</header>
|
|
1086
|
-
<p class="absolute-voice-provider-simulation__description">${
|
|
1087
|
-
${model.canSimulateFailure ? "" : `<p class="absolute-voice-provider-simulation__empty">${
|
|
2373
|
+
<p class="absolute-voice-provider-simulation__description">${escapeHtml6(model.description)}</p>
|
|
2374
|
+
${model.canSimulateFailure ? "" : `<p class="absolute-voice-provider-simulation__empty">${escapeHtml6(options.fallbackRequiredMessage ?? "Configure fallback providers before simulating failure.")}</p>`}
|
|
1088
2375
|
<div class="absolute-voice-provider-simulation__actions">${failureButtons}${recoveryButtons}</div>
|
|
1089
|
-
${snapshot.error ? `<p class="absolute-voice-provider-simulation__error">${
|
|
1090
|
-
${model.resultText ? `<pre class="absolute-voice-provider-simulation__result">${
|
|
2376
|
+
${snapshot.error ? `<p class="absolute-voice-provider-simulation__error">${escapeHtml6(snapshot.error)}</p>` : ""}
|
|
2377
|
+
${model.resultText ? `<pre class="absolute-voice-provider-simulation__result">${escapeHtml6(model.resultText)}</pre>` : ""}
|
|
1091
2378
|
</section>`;
|
|
1092
2379
|
};
|
|
1093
2380
|
var bindVoiceProviderSimulationControls = (element, store) => {
|
|
@@ -1244,7 +2531,7 @@ var createVoiceProviderCapabilitiesStore = (path = "/api/provider-capabilities",
|
|
|
1244
2531
|
// src/client/providerCapabilitiesWidget.ts
|
|
1245
2532
|
var DEFAULT_TITLE4 = "Provider Capabilities";
|
|
1246
2533
|
var DEFAULT_DESCRIPTION4 = "Configured, selected, and healthy voice providers for this deployment.";
|
|
1247
|
-
var
|
|
2534
|
+
var escapeHtml7 = (value) => value.replaceAll("&", "&").replaceAll("<", "<").replaceAll(">", ">").replaceAll('"', """).replaceAll("'", "'");
|
|
1248
2535
|
var formatProvider = (provider) => provider.split(/[-_\s]+/).filter(Boolean).map((part) => `${part[0]?.toUpperCase() ?? ""}${part.slice(1)}`).join(" ") || provider;
|
|
1249
2536
|
var formatKind2 = (kind) => kind.toUpperCase();
|
|
1250
2537
|
var formatStatus = (status) => status.split("-").map((part) => `${part[0]?.toUpperCase() ?? ""}${part.slice(1)}`).join(" ");
|
|
@@ -1299,25 +2586,25 @@ var createVoiceProviderCapabilitiesViewModel = (snapshot, options = {}) => {
|
|
|
1299
2586
|
};
|
|
1300
2587
|
var renderVoiceProviderCapabilitiesHTML = (snapshot, options = {}) => {
|
|
1301
2588
|
const model = createVoiceProviderCapabilitiesViewModel(snapshot, options);
|
|
1302
|
-
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--${
|
|
2589
|
+
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--${escapeHtml7(capability.status)}">
|
|
1303
2590
|
<header>
|
|
1304
|
-
<strong>${
|
|
1305
|
-
<span>${
|
|
2591
|
+
<strong>${escapeHtml7(capability.label)}</strong>
|
|
2592
|
+
<span>${escapeHtml7(formatStatus(capability.status))}</span>
|
|
1306
2593
|
</header>
|
|
1307
|
-
<p>${
|
|
2594
|
+
<p>${escapeHtml7(capability.detail)}</p>
|
|
1308
2595
|
<dl>${capability.rows.map((row) => `<div>
|
|
1309
|
-
<dt>${
|
|
1310
|
-
<dd>${
|
|
2596
|
+
<dt>${escapeHtml7(row.label)}</dt>
|
|
2597
|
+
<dd>${escapeHtml7(row.value)}</dd>
|
|
1311
2598
|
</div>`).join("")}</dl>
|
|
1312
2599
|
</article>`).join("")}</div>` : '<p class="absolute-voice-provider-capabilities__empty">Configure provider capabilities to see deployment coverage.</p>';
|
|
1313
|
-
return `<section class="absolute-voice-provider-capabilities absolute-voice-provider-capabilities--${
|
|
2600
|
+
return `<section class="absolute-voice-provider-capabilities absolute-voice-provider-capabilities--${escapeHtml7(model.status)}">
|
|
1314
2601
|
<header class="absolute-voice-provider-capabilities__header">
|
|
1315
|
-
<span class="absolute-voice-provider-capabilities__eyebrow">${
|
|
1316
|
-
<strong class="absolute-voice-provider-capabilities__label">${
|
|
2602
|
+
<span class="absolute-voice-provider-capabilities__eyebrow">${escapeHtml7(model.title)}</span>
|
|
2603
|
+
<strong class="absolute-voice-provider-capabilities__label">${escapeHtml7(model.label)}</strong>
|
|
1317
2604
|
</header>
|
|
1318
|
-
<p class="absolute-voice-provider-capabilities__description">${
|
|
2605
|
+
<p class="absolute-voice-provider-capabilities__description">${escapeHtml7(model.description)}</p>
|
|
1319
2606
|
${capabilities}
|
|
1320
|
-
${model.error ? `<p class="absolute-voice-provider-capabilities__error">${
|
|
2607
|
+
${model.error ? `<p class="absolute-voice-provider-capabilities__error">${escapeHtml7(model.error)}</p>` : ""}
|
|
1321
2608
|
</section>`;
|
|
1322
2609
|
};
|
|
1323
2610
|
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}`;
|
|
@@ -1445,7 +2732,7 @@ var createVoiceProviderContractsStore = (path = "/api/provider-contracts", optio
|
|
|
1445
2732
|
// src/client/providerContractsWidget.ts
|
|
1446
2733
|
var DEFAULT_TITLE5 = "Provider Contracts";
|
|
1447
2734
|
var DEFAULT_DESCRIPTION5 = "Production contract coverage for provider env, latency, fallback, streaming, and capabilities.";
|
|
1448
|
-
var
|
|
2735
|
+
var escapeHtml8 = (value) => value.replaceAll("&", "&").replaceAll("<", "<").replaceAll(">", ">").replaceAll('"', """).replaceAll("'", "'");
|
|
1449
2736
|
var formatProvider2 = (provider) => provider.split(/[-_\s]+/).filter(Boolean).map((part) => `${part[0]?.toUpperCase() ?? ""}${part.slice(1)}`).join(" ") || provider;
|
|
1450
2737
|
var formatStatus2 = (status) => status.split("-").map((part) => `${part[0]?.toUpperCase() ?? ""}${part.slice(1)}`).join(" ");
|
|
1451
2738
|
var contractDetail = (row) => {
|
|
@@ -1489,26 +2776,26 @@ var createVoiceProviderContractsViewModel = (snapshot, options = {}) => {
|
|
|
1489
2776
|
};
|
|
1490
2777
|
var renderVoiceProviderContractsHTML = (snapshot, options = {}) => {
|
|
1491
2778
|
const model = createVoiceProviderContractsViewModel(snapshot, options);
|
|
1492
|
-
const rows = model.rows.length ? `<div class="absolute-voice-provider-contracts__rows">${model.rows.map((row) => `<article class="absolute-voice-provider-contracts__row absolute-voice-provider-contracts__row--${
|
|
2779
|
+
const rows = model.rows.length ? `<div class="absolute-voice-provider-contracts__rows">${model.rows.map((row) => `<article class="absolute-voice-provider-contracts__row absolute-voice-provider-contracts__row--${escapeHtml8(row.status)}">
|
|
1493
2780
|
<header>
|
|
1494
|
-
<strong>${
|
|
1495
|
-
<span>${
|
|
2781
|
+
<strong>${escapeHtml8(row.label)}</strong>
|
|
2782
|
+
<span>${escapeHtml8(formatStatus2(row.status))}</span>
|
|
1496
2783
|
</header>
|
|
1497
|
-
<p>${
|
|
1498
|
-
${row.remediations.length ? `<ul class="absolute-voice-provider-contracts__remediations">${row.remediations.map((remediation) => `<li>${remediation.href ? `<a href="${
|
|
2784
|
+
<p>${escapeHtml8(row.detail)}</p>
|
|
2785
|
+
${row.remediations.length ? `<ul class="absolute-voice-provider-contracts__remediations">${row.remediations.map((remediation) => `<li>${remediation.href ? `<a href="${escapeHtml8(remediation.href)}">${escapeHtml8(remediation.label)}</a>` : `<strong>${escapeHtml8(remediation.label)}</strong>`}<span>${escapeHtml8(remediation.detail)}</span></li>`).join("")}</ul>` : ""}
|
|
1499
2786
|
<dl>${row.rows.map((item) => `<div>
|
|
1500
|
-
<dt>${
|
|
1501
|
-
<dd>${
|
|
2787
|
+
<dt>${escapeHtml8(item.label)}</dt>
|
|
2788
|
+
<dd>${escapeHtml8(item.value)}</dd>
|
|
1502
2789
|
</div>`).join("")}</dl>
|
|
1503
2790
|
</article>`).join("")}</div>` : '<p class="absolute-voice-provider-contracts__empty">Configure provider contracts to see production coverage.</p>';
|
|
1504
|
-
return `<section class="absolute-voice-provider-contracts absolute-voice-provider-contracts--${
|
|
2791
|
+
return `<section class="absolute-voice-provider-contracts absolute-voice-provider-contracts--${escapeHtml8(model.status)}">
|
|
1505
2792
|
<header class="absolute-voice-provider-contracts__header">
|
|
1506
|
-
<span class="absolute-voice-provider-contracts__eyebrow">${
|
|
1507
|
-
<strong class="absolute-voice-provider-contracts__label">${
|
|
2793
|
+
<span class="absolute-voice-provider-contracts__eyebrow">${escapeHtml8(model.title)}</span>
|
|
2794
|
+
<strong class="absolute-voice-provider-contracts__label">${escapeHtml8(model.label)}</strong>
|
|
1508
2795
|
</header>
|
|
1509
|
-
<p class="absolute-voice-provider-contracts__description">${
|
|
2796
|
+
<p class="absolute-voice-provider-contracts__description">${escapeHtml8(model.description)}</p>
|
|
1510
2797
|
${rows}
|
|
1511
|
-
${model.error ? `<p class="absolute-voice-provider-contracts__error">${
|
|
2798
|
+
${model.error ? `<p class="absolute-voice-provider-contracts__error">${escapeHtml8(model.error)}</p>` : ""}
|
|
1512
2799
|
</section>`;
|
|
1513
2800
|
};
|
|
1514
2801
|
var getVoiceProviderContractsCSS = () => `.absolute-voice-provider-contracts{border:1px solid #b8dcc7;border-radius:20px;background:#f7fff9;color:#09140d;padding:18px;box-shadow:0 18px 40px rgba(21,83,45,.12);font-family:inherit}.absolute-voice-provider-contracts--error,.absolute-voice-provider-contracts--warning{border-color:#f2a7a7;background:#fff7f4}.absolute-voice-provider-contracts__header,.absolute-voice-provider-contracts__row header{align-items:start;display:flex;gap:12px;justify-content:space-between}.absolute-voice-provider-contracts__eyebrow{color:#166534;font-size:12px;font-weight:800;letter-spacing:.08em;text-transform:uppercase}.absolute-voice-provider-contracts__label{font-size:24px;line-height:1}.absolute-voice-provider-contracts__description,.absolute-voice-provider-contracts__row p,.absolute-voice-provider-contracts__row dt,.absolute-voice-provider-contracts__empty{color:#405448}.absolute-voice-provider-contracts__rows{display:grid;gap:12px;margin-top:14px}.absolute-voice-provider-contracts__row{background:#fff;border:1px solid #d6eadb;border-radius:16px;padding:14px}.absolute-voice-provider-contracts__row--pass{border-color:#86efac}.absolute-voice-provider-contracts__row--warn,.absolute-voice-provider-contracts__row--fail{border-color:#f2a7a7}.absolute-voice-provider-contracts__row p{margin:10px 0}.absolute-voice-provider-contracts__remediations{display:grid;gap:8px;list-style:none;margin:0 0 10px;padding:0}.absolute-voice-provider-contracts__remediations li{background:#fff7ed;border:1px solid #fed7aa;border-radius:12px;display:grid;gap:3px;padding:8px}.absolute-voice-provider-contracts__remediations a,.absolute-voice-provider-contracts__remediations strong{color:#9a3412}.absolute-voice-provider-contracts__remediations span{color:#7c2d12}.absolute-voice-provider-contracts__row dl{display:grid;gap:8px;grid-template-columns:repeat(2,minmax(0,1fr));margin:0}.absolute-voice-provider-contracts__row div{background:#f7fff9;border:1px solid #d6eadb;border-radius:12px;padding:8px}.absolute-voice-provider-contracts__row dt{font-size:12px}.absolute-voice-provider-contracts__row dd{font-weight:800;margin:4px 0 0}.absolute-voice-provider-contracts__error{color:#9f1239;font-weight:700}`;
|
|
@@ -2283,7 +3570,7 @@ var createVoiceProviderStatusStore = (path = "/api/provider-status", options = {
|
|
|
2283
3570
|
// src/client/providerStatusWidget.ts
|
|
2284
3571
|
var DEFAULT_TITLE6 = "Voice Providers";
|
|
2285
3572
|
var DEFAULT_DESCRIPTION6 = "Live provider health, fallback counts, latency, and suppression state from your self-hosted trace store.";
|
|
2286
|
-
var
|
|
3573
|
+
var escapeHtml9 = (value) => value.replaceAll("&", "&").replaceAll("<", "<").replaceAll(">", ">").replaceAll('"', """).replaceAll("'", "'");
|
|
2287
3574
|
var formatProvider3 = (provider) => provider.split(/[-_\s]+/).filter(Boolean).map((part) => `${part[0]?.toUpperCase() ?? ""}${part.slice(1)}`).join(" ") || provider;
|
|
2288
3575
|
var formatStatus3 = (status) => status.split("-").map((part) => `${part[0]?.toUpperCase() ?? ""}${part.slice(1)}`).join(" ");
|
|
2289
3576
|
var formatLatency = (value) => typeof value === "number" ? `${value}ms` : "No samples";
|
|
@@ -2339,25 +3626,25 @@ var createVoiceProviderStatusViewModel = (snapshot, options = {}) => {
|
|
|
2339
3626
|
};
|
|
2340
3627
|
var renderVoiceProviderStatusHTML = (snapshot, options = {}) => {
|
|
2341
3628
|
const model = createVoiceProviderStatusViewModel(snapshot, options);
|
|
2342
|
-
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--${
|
|
3629
|
+
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--${escapeHtml9(provider.status)}">
|
|
2343
3630
|
<header>
|
|
2344
|
-
<strong>${
|
|
2345
|
-
<span>${
|
|
3631
|
+
<strong>${escapeHtml9(provider.label)}</strong>
|
|
3632
|
+
<span>${escapeHtml9(formatStatus3(provider.status))}</span>
|
|
2346
3633
|
</header>
|
|
2347
|
-
<p>${
|
|
3634
|
+
<p>${escapeHtml9(provider.detail)}</p>
|
|
2348
3635
|
<dl>${provider.rows.map((row) => `<div>
|
|
2349
|
-
<dt>${
|
|
2350
|
-
<dd>${
|
|
3636
|
+
<dt>${escapeHtml9(row.label)}</dt>
|
|
3637
|
+
<dd>${escapeHtml9(row.value)}</dd>
|
|
2351
3638
|
</div>`).join("")}</dl>
|
|
2352
3639
|
</article>`).join("")}</div>` : '<p class="absolute-voice-provider-status__empty">Run voice traffic to see provider health.</p>';
|
|
2353
|
-
return `<section class="absolute-voice-provider-status absolute-voice-provider-status--${
|
|
3640
|
+
return `<section class="absolute-voice-provider-status absolute-voice-provider-status--${escapeHtml9(model.status)}">
|
|
2354
3641
|
<header class="absolute-voice-provider-status__header">
|
|
2355
|
-
<span class="absolute-voice-provider-status__eyebrow">${
|
|
2356
|
-
<strong class="absolute-voice-provider-status__label">${
|
|
3642
|
+
<span class="absolute-voice-provider-status__eyebrow">${escapeHtml9(model.title)}</span>
|
|
3643
|
+
<strong class="absolute-voice-provider-status__label">${escapeHtml9(model.label)}</strong>
|
|
2357
3644
|
</header>
|
|
2358
|
-
<p class="absolute-voice-provider-status__description">${
|
|
3645
|
+
<p class="absolute-voice-provider-status__description">${escapeHtml9(model.description)}</p>
|
|
2359
3646
|
${providers}
|
|
2360
|
-
${model.error ? `<p class="absolute-voice-provider-status__error">${
|
|
3647
|
+
${model.error ? `<p class="absolute-voice-provider-status__error">${escapeHtml9(model.error)}</p>` : ""}
|
|
2361
3648
|
</section>`;
|
|
2362
3649
|
};
|
|
2363
3650
|
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}`;
|
|
@@ -2490,7 +3777,7 @@ var createVoiceRoutingStatusStore = (path = "/api/routing/latest", options = {})
|
|
|
2490
3777
|
// src/client/routingStatusWidget.ts
|
|
2491
3778
|
var DEFAULT_TITLE7 = "Voice Routing";
|
|
2492
3779
|
var DEFAULT_DESCRIPTION7 = "Latest provider routing decision from the self-hosted trace store.";
|
|
2493
|
-
var
|
|
3780
|
+
var escapeHtml10 = (value) => value.replaceAll("&", "&").replaceAll("<", "<").replaceAll(">", ">").replaceAll('"', """).replaceAll("'", "'");
|
|
2494
3781
|
var formatValue = (value, fallback = "None") => typeof value === "string" && value.trim() ? value : typeof value === "number" && Number.isFinite(value) ? String(value) : fallback;
|
|
2495
3782
|
var createVoiceRoutingStatusViewModel = (snapshot, options = {}) => {
|
|
2496
3783
|
const decision = snapshot.decision;
|
|
@@ -2527,17 +3814,17 @@ var createVoiceRoutingStatusViewModel = (snapshot, options = {}) => {
|
|
|
2527
3814
|
var renderVoiceRoutingStatusHTML = (snapshot, options = {}) => {
|
|
2528
3815
|
const model = createVoiceRoutingStatusViewModel(snapshot, options);
|
|
2529
3816
|
const rows = model.rows.length ? `<div class="absolute-voice-routing-status__grid">${model.rows.map((row) => `<div>
|
|
2530
|
-
<span>${
|
|
2531
|
-
<strong>${
|
|
3817
|
+
<span>${escapeHtml10(row.label)}</span>
|
|
3818
|
+
<strong>${escapeHtml10(row.value)}</strong>
|
|
2532
3819
|
</div>`).join("")}</div>` : '<p class="absolute-voice-routing-status__empty">Start a voice session to see the selected provider.</p>';
|
|
2533
|
-
return `<section class="absolute-voice-routing-status absolute-voice-routing-status--${
|
|
3820
|
+
return `<section class="absolute-voice-routing-status absolute-voice-routing-status--${escapeHtml10(model.status)}">
|
|
2534
3821
|
<header class="absolute-voice-routing-status__header">
|
|
2535
|
-
<span class="absolute-voice-routing-status__eyebrow">${
|
|
2536
|
-
<strong class="absolute-voice-routing-status__label">${
|
|
3822
|
+
<span class="absolute-voice-routing-status__eyebrow">${escapeHtml10(model.title)}</span>
|
|
3823
|
+
<strong class="absolute-voice-routing-status__label">${escapeHtml10(model.label)}</strong>
|
|
2537
3824
|
</header>
|
|
2538
|
-
<p class="absolute-voice-routing-status__description">${
|
|
3825
|
+
<p class="absolute-voice-routing-status__description">${escapeHtml10(model.description)}</p>
|
|
2539
3826
|
${rows}
|
|
2540
|
-
${model.error ? `<p class="absolute-voice-routing-status__error">${
|
|
3827
|
+
${model.error ? `<p class="absolute-voice-routing-status__error">${escapeHtml10(model.error)}</p>` : ""}
|
|
2541
3828
|
</section>`;
|
|
2542
3829
|
};
|
|
2543
3830
|
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}`;
|
|
@@ -2670,7 +3957,7 @@ var createVoiceTraceTimelineStore = (path = "/api/voice-traces", options = {}) =
|
|
|
2670
3957
|
// src/client/traceTimelineWidget.ts
|
|
2671
3958
|
var DEFAULT_TITLE8 = "Voice Traces";
|
|
2672
3959
|
var DEFAULT_DESCRIPTION8 = "Latest call timelines with provider latency, fallbacks, handoffs, and errors from your self-hosted trace store.";
|
|
2673
|
-
var
|
|
3960
|
+
var escapeHtml11 = (value) => value.replaceAll("&", "&").replaceAll("<", "<").replaceAll(">", ">").replaceAll('"', """).replaceAll("'", "'");
|
|
2674
3961
|
var formatMs = (value) => typeof value === "number" ? `${value}ms` : "n/a";
|
|
2675
3962
|
var formatProviders = (session) => session.providers.length ? session.providers.map((provider) => provider.provider).join(", ") : "No providers";
|
|
2676
3963
|
var createVoiceTraceTimelineViewModel = (snapshot, options = {}) => {
|
|
@@ -2700,27 +3987,27 @@ var renderVoiceTraceTimelineWidgetHTML = (snapshot, options = {}) => {
|
|
|
2700
3987
|
const model = createVoiceTraceTimelineViewModel(snapshot, options);
|
|
2701
3988
|
const sessions = model.sessions.length ? `<div class="absolute-voice-trace-timeline__sessions">${model.sessions.map((session) => {
|
|
2702
3989
|
const supportLinks = [
|
|
2703
|
-
`<a href="${
|
|
2704
|
-
session.operationsRecordHref ? `<a href="${
|
|
2705
|
-
session.incidentBundleHref ? `<a href="${
|
|
3990
|
+
`<a href="${escapeHtml11(session.detailHref)}">Open timeline</a>`,
|
|
3991
|
+
session.operationsRecordHref ? `<a href="${escapeHtml11(session.operationsRecordHref)}">Open operations record</a>` : undefined,
|
|
3992
|
+
session.incidentBundleHref ? `<a href="${escapeHtml11(session.incidentBundleHref)}">Export incident bundle</a>` : undefined
|
|
2706
3993
|
].filter(Boolean).join("");
|
|
2707
|
-
return `<article class="absolute-voice-trace-timeline__session absolute-voice-trace-timeline__session--${
|
|
3994
|
+
return `<article class="absolute-voice-trace-timeline__session absolute-voice-trace-timeline__session--${escapeHtml11(session.status)}">
|
|
2708
3995
|
<header>
|
|
2709
|
-
<strong>${
|
|
2710
|
-
<span>${
|
|
3996
|
+
<strong>${escapeHtml11(session.sessionId)}</strong>
|
|
3997
|
+
<span>${escapeHtml11(session.status)}</span>
|
|
2711
3998
|
</header>
|
|
2712
|
-
<p>${
|
|
3999
|
+
<p>${escapeHtml11(session.label)} \xB7 ${escapeHtml11(session.durationLabel)} \xB7 ${escapeHtml11(session.providerLabel)}</p>
|
|
2713
4000
|
<p class="absolute-voice-trace-timeline__actions">${supportLinks}</p>
|
|
2714
4001
|
</article>`;
|
|
2715
4002
|
}).join("")}</div>` : '<p class="absolute-voice-trace-timeline__empty">Run a voice session to see call timelines.</p>';
|
|
2716
|
-
return `<section class="absolute-voice-trace-timeline absolute-voice-trace-timeline--${
|
|
4003
|
+
return `<section class="absolute-voice-trace-timeline absolute-voice-trace-timeline--${escapeHtml11(model.status)}">
|
|
2717
4004
|
<header class="absolute-voice-trace-timeline__header">
|
|
2718
|
-
<span class="absolute-voice-trace-timeline__eyebrow">${
|
|
2719
|
-
<strong class="absolute-voice-trace-timeline__label">${
|
|
4005
|
+
<span class="absolute-voice-trace-timeline__eyebrow">${escapeHtml11(model.title)}</span>
|
|
4006
|
+
<strong class="absolute-voice-trace-timeline__label">${escapeHtml11(model.label)}</strong>
|
|
2720
4007
|
</header>
|
|
2721
|
-
<p class="absolute-voice-trace-timeline__description">${
|
|
4008
|
+
<p class="absolute-voice-trace-timeline__description">${escapeHtml11(model.description)}</p>
|
|
2722
4009
|
${sessions}
|
|
2723
|
-
${model.error ? `<p class="absolute-voice-trace-timeline__error">${
|
|
4010
|
+
${model.error ? `<p class="absolute-voice-trace-timeline__error">${escapeHtml11(model.error)}</p>` : ""}
|
|
2724
4011
|
</section>`;
|
|
2725
4012
|
};
|
|
2726
4013
|
var getVoiceTraceTimelineCSS = () => `.absolute-voice-trace-timeline{border:1px solid #bad7d3;border-radius:20px;background:#f3fffb;color:#09201c;padding:18px;box-shadow:0 18px 40px rgba(9,32,28,.12);font-family:inherit}.absolute-voice-trace-timeline--error,.absolute-voice-trace-timeline--failed{border-color:#f2a7a7;background:#fff5f3}.absolute-voice-trace-timeline--warning{border-color:#fbbf24;background:#fffaf0}.absolute-voice-trace-timeline__header,.absolute-voice-trace-timeline__session header{align-items:start;display:flex;gap:12px;justify-content:space-between}.absolute-voice-trace-timeline__eyebrow{color:#17665b;font-size:12px;font-weight:800;letter-spacing:.08em;text-transform:uppercase}.absolute-voice-trace-timeline__label{font-size:24px;line-height:1}.absolute-voice-trace-timeline__description,.absolute-voice-trace-timeline__session p,.absolute-voice-trace-timeline__empty{color:#35544f}.absolute-voice-trace-timeline__sessions{display:grid;gap:12px;margin-top:14px}.absolute-voice-trace-timeline__session{background:#fff;border:1px solid #cfe7e2;border-radius:16px;padding:14px}.absolute-voice-trace-timeline__session--failed{border-color:#f2a7a7}.absolute-voice-trace-timeline__session--warning{border-color:#fbbf24}.absolute-voice-trace-timeline__session p{margin:10px 0}.absolute-voice-trace-timeline__actions{display:flex;flex-wrap:wrap;gap:10px}.absolute-voice-trace-timeline__session a{color:#0f766e;font-weight:800}.absolute-voice-trace-timeline__empty{margin:14px 0 0}.absolute-voice-trace-timeline__error{color:#9f1239;font-weight:700}`;
|
|
@@ -2882,7 +4169,7 @@ var createVoiceTurnLatencyStore = (path = "/api/turn-latency", options = {}) =>
|
|
|
2882
4169
|
var DEFAULT_TITLE9 = "Turn Latency";
|
|
2883
4170
|
var DEFAULT_DESCRIPTION9 = "Per-turn timing from first transcript to commit and assistant response start.";
|
|
2884
4171
|
var DEFAULT_PROOF_LABEL = "Run latency proof";
|
|
2885
|
-
var
|
|
4172
|
+
var escapeHtml12 = (value) => value.replaceAll("&", "&").replaceAll("<", "<").replaceAll(">", ">").replaceAll('"', """).replaceAll("'", "'");
|
|
2886
4173
|
var formatMs2 = (value) => typeof value === "number" ? `${Math.round(value)}ms` : "n/a";
|
|
2887
4174
|
var createVoiceTurnLatencyViewModel = (snapshot, options = {}) => {
|
|
2888
4175
|
const turns = (snapshot.report?.turns ?? []).map((turn) => ({
|
|
@@ -2910,25 +4197,25 @@ var createVoiceTurnLatencyViewModel = (snapshot, options = {}) => {
|
|
|
2910
4197
|
};
|
|
2911
4198
|
var renderVoiceTurnLatencyHTML = (snapshot, options = {}) => {
|
|
2912
4199
|
const model = createVoiceTurnLatencyViewModel(snapshot, options);
|
|
2913
|
-
const turns = model.turns.length ? `<div class="absolute-voice-turn-latency__turns">${model.turns.map((turn) => `<article class="absolute-voice-turn-latency__turn absolute-voice-turn-latency__turn--${
|
|
4200
|
+
const turns = model.turns.length ? `<div class="absolute-voice-turn-latency__turns">${model.turns.map((turn) => `<article class="absolute-voice-turn-latency__turn absolute-voice-turn-latency__turn--${escapeHtml12(turn.status)}">
|
|
2914
4201
|
<header>
|
|
2915
|
-
<strong>${
|
|
2916
|
-
<span>${
|
|
4202
|
+
<strong>${escapeHtml12(turn.label)}</strong>
|
|
4203
|
+
<span>${escapeHtml12(turn.status)}</span>
|
|
2917
4204
|
</header>
|
|
2918
4205
|
<dl>${turn.rows.map((row) => `<div>
|
|
2919
|
-
<dt>${
|
|
2920
|
-
<dd>${
|
|
4206
|
+
<dt>${escapeHtml12(row.label)}</dt>
|
|
4207
|
+
<dd>${escapeHtml12(row.value)}</dd>
|
|
2921
4208
|
</div>`).join("")}</dl>
|
|
2922
4209
|
</article>`).join("")}</div>` : '<p class="absolute-voice-turn-latency__empty">Complete a voice turn to see latency diagnostics.</p>';
|
|
2923
|
-
return `<section class="absolute-voice-turn-latency absolute-voice-turn-latency--${
|
|
4210
|
+
return `<section class="absolute-voice-turn-latency absolute-voice-turn-latency--${escapeHtml12(model.status)}">
|
|
2924
4211
|
<header class="absolute-voice-turn-latency__header">
|
|
2925
|
-
<span class="absolute-voice-turn-latency__eyebrow">${
|
|
2926
|
-
<strong class="absolute-voice-turn-latency__label">${
|
|
4212
|
+
<span class="absolute-voice-turn-latency__eyebrow">${escapeHtml12(model.title)}</span>
|
|
4213
|
+
<strong class="absolute-voice-turn-latency__label">${escapeHtml12(model.label)}</strong>
|
|
2927
4214
|
</header>
|
|
2928
|
-
<p class="absolute-voice-turn-latency__description">${
|
|
2929
|
-
${model.showProofAction ? `<button class="absolute-voice-turn-latency__proof" data-absolute-voice-turn-latency-proof type="button">${
|
|
4215
|
+
<p class="absolute-voice-turn-latency__description">${escapeHtml12(model.description)}</p>
|
|
4216
|
+
${model.showProofAction ? `<button class="absolute-voice-turn-latency__proof" data-absolute-voice-turn-latency-proof type="button">${escapeHtml12(model.proofLabel ?? DEFAULT_PROOF_LABEL)}</button>` : ""}
|
|
2930
4217
|
${turns}
|
|
2931
|
-
${model.error ? `<p class="absolute-voice-turn-latency__error">${
|
|
4218
|
+
${model.error ? `<p class="absolute-voice-turn-latency__error">${escapeHtml12(model.error)}</p>` : ""}
|
|
2932
4219
|
</section>`;
|
|
2933
4220
|
};
|
|
2934
4221
|
var mountVoiceTurnLatency = (element, path = "/api/turn-latency", options = {}) => {
|
|
@@ -3069,7 +4356,7 @@ var createVoiceTurnQualityStore = (path = "/api/turn-quality", options = {}) =>
|
|
|
3069
4356
|
// src/client/turnQualityWidget.ts
|
|
3070
4357
|
var DEFAULT_TITLE10 = "Turn Quality";
|
|
3071
4358
|
var DEFAULT_DESCRIPTION10 = "Per-turn STT confidence, fallback selection, corrections, and transcript coverage.";
|
|
3072
|
-
var
|
|
4359
|
+
var escapeHtml13 = (value) => value.replaceAll("&", "&").replaceAll("<", "<").replaceAll(">", ">").replaceAll('"', """).replaceAll("'", "'");
|
|
3073
4360
|
var formatConfidence = (value) => typeof value === "number" ? `${Math.round(value * 100)}%` : "n/a";
|
|
3074
4361
|
var formatMaybe = (value) => value === undefined || value === "" ? "n/a" : String(value);
|
|
3075
4362
|
var getTurnDetail = (turn) => {
|
|
@@ -3119,25 +4406,25 @@ var createVoiceTurnQualityViewModel = (snapshot, options = {}) => {
|
|
|
3119
4406
|
};
|
|
3120
4407
|
var renderVoiceTurnQualityHTML = (snapshot, options = {}) => {
|
|
3121
4408
|
const model = createVoiceTurnQualityViewModel(snapshot, options);
|
|
3122
|
-
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--${
|
|
4409
|
+
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--${escapeHtml13(turn.status)}">
|
|
3123
4410
|
<header>
|
|
3124
|
-
<strong>${
|
|
3125
|
-
<span>${
|
|
4411
|
+
<strong>${escapeHtml13(turn.label)}</strong>
|
|
4412
|
+
<span>${escapeHtml13(turn.status)}</span>
|
|
3126
4413
|
</header>
|
|
3127
|
-
<p>${
|
|
4414
|
+
<p>${escapeHtml13(turn.detail)}</p>
|
|
3128
4415
|
<dl>${turn.rows.map((row) => `<div>
|
|
3129
|
-
<dt>${
|
|
3130
|
-
<dd>${
|
|
4416
|
+
<dt>${escapeHtml13(row.label)}</dt>
|
|
4417
|
+
<dd>${escapeHtml13(row.value)}</dd>
|
|
3131
4418
|
</div>`).join("")}</dl>
|
|
3132
4419
|
</article>`).join("")}</div>` : '<p class="absolute-voice-turn-quality__empty">Complete a voice turn to see STT quality diagnostics.</p>';
|
|
3133
|
-
return `<section class="absolute-voice-turn-quality absolute-voice-turn-quality--${
|
|
4420
|
+
return `<section class="absolute-voice-turn-quality absolute-voice-turn-quality--${escapeHtml13(model.status)}">
|
|
3134
4421
|
<header class="absolute-voice-turn-quality__header">
|
|
3135
|
-
<span class="absolute-voice-turn-quality__eyebrow">${
|
|
3136
|
-
<strong class="absolute-voice-turn-quality__label">${
|
|
4422
|
+
<span class="absolute-voice-turn-quality__eyebrow">${escapeHtml13(model.title)}</span>
|
|
4423
|
+
<strong class="absolute-voice-turn-quality__label">${escapeHtml13(model.label)}</strong>
|
|
3137
4424
|
</header>
|
|
3138
|
-
<p class="absolute-voice-turn-quality__description">${
|
|
4425
|
+
<p class="absolute-voice-turn-quality__description">${escapeHtml13(model.description)}</p>
|
|
3139
4426
|
${turns}
|
|
3140
|
-
${model.error ? `<p class="absolute-voice-turn-quality__error">${
|
|
4427
|
+
${model.error ? `<p class="absolute-voice-turn-quality__error">${escapeHtml13(model.error)}</p>` : ""}
|
|
3141
4428
|
</section>`;
|
|
3142
4429
|
};
|
|
3143
4430
|
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}`;
|
|
@@ -3921,6 +5208,7 @@ export {
|
|
|
3921
5208
|
createVoiceProviderCapabilities,
|
|
3922
5209
|
createVoiceOpsStatus,
|
|
3923
5210
|
createVoiceOpsActionCenter,
|
|
5211
|
+
createVoiceLiveOps,
|
|
3924
5212
|
createVoiceDeliveryRuntime,
|
|
3925
5213
|
createVoiceController,
|
|
3926
5214
|
createVoiceCampaignDialerProof
|