@echothink-ui/quality 0.1.0

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/index.js ADDED
@@ -0,0 +1,329 @@
1
+ // src/components/QCMetricsInspector.tsx
2
+ import * as React from "react";
3
+ import clsx from "clsx";
4
+ import { Badge, StatusDot, Surface } from "@echothink-ui/core";
5
+ import { DataTable } from "@echothink-ui/data";
6
+ import { jsx, jsxs } from "react/jsx-runtime";
7
+ function QCMetricsInspector({ metrics, decision, className }) {
8
+ const columns = React.useMemo(
9
+ () => [
10
+ { key: "label", header: "Metric" },
11
+ {
12
+ key: "value",
13
+ header: "Value",
14
+ render: (metric) => `${metric.value}${metric.unit ? ` ${metric.unit}` : ""}`
15
+ },
16
+ {
17
+ key: "threshold",
18
+ header: "Threshold",
19
+ render: (metric) => thresholdLabel(metric.threshold)
20
+ },
21
+ {
22
+ key: "status",
23
+ header: "Status",
24
+ render: (metric) => /* @__PURE__ */ jsx(StatusDot, { status: metricStatus(metric.status), label: metricStatusLabel(metric.status) })
25
+ }
26
+ ],
27
+ []
28
+ );
29
+ return /* @__PURE__ */ jsxs(
30
+ Surface,
31
+ {
32
+ className: clsx("eth-quality-qc-metrics-inspector", className),
33
+ title: "QC metrics",
34
+ subtitle: decision ? "Automated decision available" : void 0,
35
+ "data-eth-component": "QCMetricsInspector",
36
+ children: [
37
+ decision ? /* @__PURE__ */ jsxs("div", { className: "eth-quality-qc-metrics-inspector__decision", children: [
38
+ /* @__PURE__ */ jsx(Badge, { severity: decisionSeverity(decision.status), children: decisionLabel(decision.status) }),
39
+ decision.reasons?.length ? /* @__PURE__ */ jsx("ul", { children: decision.reasons.map((reason) => /* @__PURE__ */ jsx("li", { children: reason }, reason)) }) : null
40
+ ] }) : null,
41
+ /* @__PURE__ */ jsx(DataTable, { rows: metrics, columns })
42
+ ]
43
+ }
44
+ );
45
+ }
46
+ function thresholdLabel(threshold) {
47
+ if (!threshold) return "-";
48
+ if (threshold.min !== void 0 && threshold.max !== void 0) {
49
+ return `${threshold.min} - ${threshold.max}`;
50
+ }
51
+ if (threshold.min !== void 0) return `>= ${threshold.min}`;
52
+ if (threshold.max !== void 0) return `<= ${threshold.max}`;
53
+ return "-";
54
+ }
55
+ function metricStatus(status) {
56
+ if (status === "pass") return "succeeded";
57
+ if (status === "fail") return "failed";
58
+ if (status === "warning") return "warning";
59
+ return "approval-required";
60
+ }
61
+ function metricStatusLabel(status) {
62
+ if (status === "manual-review") return "Manual review";
63
+ return status.charAt(0).toUpperCase() + status.slice(1);
64
+ }
65
+ function decisionSeverity(status) {
66
+ if (status === "pass") return "success";
67
+ if (status === "fail") return "danger";
68
+ return "warning";
69
+ }
70
+ function decisionLabel(status) {
71
+ if (status === "manual-review-required") return "Manual review required";
72
+ return status.charAt(0).toUpperCase() + status.slice(1);
73
+ }
74
+
75
+ // src/components/QCProfileEditor.tsx
76
+ import * as React2 from "react";
77
+ import clsx2 from "clsx";
78
+ import { ConfigForm, RuleBuilder } from "@echothink-ui/forms";
79
+ import { jsx as jsx2, jsxs as jsxs2 } from "react/jsx-runtime";
80
+ var operatorOptions = [
81
+ { value: "lt", label: "Less than" },
82
+ { value: "lte", label: "Less than or equal" },
83
+ { value: "gt", label: "Greater than" },
84
+ { value: "gte", label: "Greater than or equal" },
85
+ { value: "eq", label: "Equals" },
86
+ { value: "between", label: "Between" }
87
+ ];
88
+ function QCProfileEditor({ profile, onChange, onSubmit, className }) {
89
+ const values = React2.useMemo(() => valuesFromProfile(profile), [profile]);
90
+ const rules = React2.useMemo(() => rulesFromProfile(profile), [profile]);
91
+ return /* @__PURE__ */ jsxs2("div", { className: clsx2("eth-quality-qc-profile-editor", className), "data-eth-component": "QCProfileEditor", children: [
92
+ /* @__PURE__ */ jsx2(
93
+ ConfigForm,
94
+ {
95
+ title: "QC profile",
96
+ description: `Profile ${profile.id}`,
97
+ sections: [
98
+ {
99
+ id: "profile",
100
+ title: "Thresholds",
101
+ description: "Language and threshold values used by the automated QC job.",
102
+ fields: [
103
+ {
104
+ name: "language",
105
+ type: "text",
106
+ label: "Language",
107
+ required: true
108
+ },
109
+ ...profile.thresholds.flatMap((threshold) => [
110
+ {
111
+ name: operatorKey(threshold.id),
112
+ type: "select",
113
+ label: `${threshold.label} operator`,
114
+ options: operatorOptions
115
+ },
116
+ {
117
+ name: valueKey(threshold.id),
118
+ type: typeof threshold.value === "number" ? "number" : "text",
119
+ label: `${threshold.label} value`
120
+ }
121
+ ])
122
+ ]
123
+ }
124
+ ],
125
+ values,
126
+ submitLabel: "Save profile",
127
+ onChange: (nextValues) => onChange?.(profileFromValues(profile, nextValues)),
128
+ onSubmit: (nextValues) => onSubmit?.(profileFromValues(profile, nextValues))
129
+ }
130
+ ),
131
+ /* @__PURE__ */ jsx2(
132
+ RuleBuilder,
133
+ {
134
+ className: "eth-quality-qc-profile-editor__rules",
135
+ rules,
136
+ variables: profile.thresholds.map((threshold) => threshold.id),
137
+ onChange: (nextRules) => onChange?.(profileFromRules(profile, nextRules))
138
+ }
139
+ )
140
+ ] });
141
+ }
142
+ function valuesFromProfile(profile) {
143
+ return profile.thresholds.reduce(
144
+ (values, threshold) => ({
145
+ ...values,
146
+ [operatorKey(threshold.id)]: threshold.operator,
147
+ [valueKey(threshold.id)]: threshold.value
148
+ }),
149
+ { language: profile.language }
150
+ );
151
+ }
152
+ function profileFromValues(profile, values) {
153
+ return {
154
+ ...profile,
155
+ language: String(values.language ?? ""),
156
+ thresholds: profile.thresholds.map((threshold) => ({
157
+ ...threshold,
158
+ operator: String(values[operatorKey(threshold.id)] ?? threshold.operator),
159
+ value: normalizeThresholdValue(threshold.value, values[valueKey(threshold.id)])
160
+ }))
161
+ };
162
+ }
163
+ function rulesFromProfile(profile) {
164
+ return profile.thresholds.map((threshold) => ({
165
+ id: threshold.id,
166
+ when: {
167
+ kind: "leaf",
168
+ field: threshold.id,
169
+ op: threshold.operator,
170
+ value: threshold.value
171
+ },
172
+ then: {
173
+ type: "require-approval",
174
+ target: "manual-review"
175
+ }
176
+ }));
177
+ }
178
+ function profileFromRules(profile, rules) {
179
+ return {
180
+ ...profile,
181
+ thresholds: profile.thresholds.map((threshold) => {
182
+ const rule = rules.find((item) => item.id === threshold.id);
183
+ if (!rule) return threshold;
184
+ return {
185
+ ...threshold,
186
+ operator: rule.when.op ?? threshold.operator,
187
+ value: normalizeThresholdValue(threshold.value, rule.when.value)
188
+ };
189
+ })
190
+ };
191
+ }
192
+ function normalizeThresholdValue(previousValue, nextValue) {
193
+ if (typeof previousValue === "number") {
194
+ const numeric = Number(nextValue);
195
+ return Number.isNaN(numeric) ? previousValue : numeric;
196
+ }
197
+ return String(nextValue ?? "");
198
+ }
199
+ function operatorKey(id) {
200
+ return `${id}.operator`;
201
+ }
202
+ function valueKey(id) {
203
+ return `${id}.value`;
204
+ }
205
+
206
+ // src/components/QCJobMonitor.tsx
207
+ import * as React3 from "react";
208
+ import clsx3 from "clsx";
209
+ import { StatusDot as StatusDot2, Surface as Surface2 } from "@echothink-ui/core";
210
+ import { JobQueuePanel } from "@echothink-ui/admin";
211
+ import { DataTable as DataTable2 } from "@echothink-ui/data";
212
+ import { jsx as jsx3, jsxs as jsxs3 } from "react/jsx-runtime";
213
+ function QCJobMonitor({ jobs, className }) {
214
+ const columns = React3.useMemo(
215
+ () => [
216
+ { key: "recordingId", header: "Recording" },
217
+ {
218
+ key: "status",
219
+ header: "Status",
220
+ render: (job) => /* @__PURE__ */ jsx3(StatusDot2, { status: job.status, label: job.status })
221
+ },
222
+ { key: "queuedAt", header: "Queued" },
223
+ { key: "durationMs", header: "Duration", render: (job) => formatDuration(job.durationMs) },
224
+ { key: "profile", header: "Profile", render: (job) => job.profile ?? "default" }
225
+ ],
226
+ []
227
+ );
228
+ return /* @__PURE__ */ jsxs3(
229
+ Surface2,
230
+ {
231
+ className: clsx3("eth-quality-qc-job-monitor", className),
232
+ title: "QC job monitor",
233
+ "data-eth-component": "QCJobMonitor",
234
+ children: [
235
+ /* @__PURE__ */ jsx3(JobQueuePanel, { queues: queuesFromJobs(jobs) }),
236
+ /* @__PURE__ */ jsx3(DataTable2, { rows: jobs, columns })
237
+ ]
238
+ }
239
+ );
240
+ }
241
+ function queuesFromJobs(jobs) {
242
+ const grouped = /* @__PURE__ */ new Map();
243
+ for (const job of jobs) {
244
+ const key = job.profile ?? "default";
245
+ grouped.set(key, [...grouped.get(key) ?? [], job]);
246
+ }
247
+ return Array.from(grouped.entries()).map(([profile, profileJobs]) => ({
248
+ id: profile,
249
+ name: `${profile} QC`,
250
+ depth: profileJobs.filter((job) => job.status === "queued").length,
251
+ processing: profileJobs.filter((job) => job.status === "running" || job.status === "in-progress").length,
252
+ failed: profileJobs.filter((job) => job.status === "failed").length,
253
+ retryRate: retryRate(profileJobs)
254
+ }));
255
+ }
256
+ function retryRate(jobs) {
257
+ if (!jobs.length) return 0;
258
+ return Math.round(jobs.filter((job) => job.status === "failed").length / jobs.length * 100);
259
+ }
260
+ function formatDuration(durationMs) {
261
+ if (durationMs === void 0) return "-";
262
+ if (durationMs < 1e3) return `${durationMs}ms`;
263
+ const seconds = Math.round(durationMs / 1e3);
264
+ return `${seconds}s`;
265
+ }
266
+
267
+ // src/components/QCDecisionBadge.tsx
268
+ import clsx4 from "clsx";
269
+ import { Badge as Badge2 } from "@echothink-ui/core";
270
+ import { jsx as jsx4 } from "react/jsx-runtime";
271
+ function QCDecisionBadge({ decision, reason, className }) {
272
+ return /* @__PURE__ */ jsx4(
273
+ "span",
274
+ {
275
+ className: clsx4("eth-quality-qc-decision-badge", className),
276
+ title: reason,
277
+ "data-eth-component": "QCDecisionBadge",
278
+ children: /* @__PURE__ */ jsx4(Badge2, { severity: decisionSeverity2(decision), children: decisionLabel2(decision) })
279
+ }
280
+ );
281
+ }
282
+ function decisionSeverity2(decision) {
283
+ if (decision === "pass") return "success";
284
+ if (decision === "fail") return "danger";
285
+ if (decision === "overridden") return "info";
286
+ return "warning";
287
+ }
288
+ function decisionLabel2(decision) {
289
+ if (decision === "manual-review-required") return "Manual review required";
290
+ return decision.charAt(0).toUpperCase() + decision.slice(1);
291
+ }
292
+
293
+ // src/components/QCRoutingTimeline.tsx
294
+ import clsx5 from "clsx";
295
+ import { Badge as Badge3, Surface as Surface3 } from "@echothink-ui/core";
296
+ import { jsx as jsx5, jsxs as jsxs4 } from "react/jsx-runtime";
297
+ function QCRoutingTimeline({ events, className }) {
298
+ return /* @__PURE__ */ jsx5(
299
+ Surface3,
300
+ {
301
+ className: clsx5("eth-quality-qc-routing-timeline", className),
302
+ title: "Routing timeline",
303
+ "data-eth-component": "QCRoutingTimeline",
304
+ children: /* @__PURE__ */ jsx5("ol", { className: "eth-quality-qc-routing-timeline__events", children: events.map((event) => /* @__PURE__ */ jsxs4("li", { children: [
305
+ /* @__PURE__ */ jsx5("time", { children: event.at }),
306
+ /* @__PURE__ */ jsx5(Badge3, { severity: event.severity ?? "info", children: event.severity ?? "info" }),
307
+ /* @__PURE__ */ jsx5("span", { children: event.label })
308
+ ] }, `${event.at}-${event.label}`)) })
309
+ }
310
+ );
311
+ }
312
+
313
+ // src/index.tsx
314
+ var QualityComponentNames = [
315
+ "QCMetricsInspector",
316
+ "QCProfileEditor",
317
+ "QCJobMonitor",
318
+ "QCDecisionBadge",
319
+ "QCRoutingTimeline"
320
+ ];
321
+ export {
322
+ QCDecisionBadge,
323
+ QCJobMonitor,
324
+ QCMetricsInspector,
325
+ QCProfileEditor,
326
+ QCRoutingTimeline,
327
+ QualityComponentNames
328
+ };
329
+ //# sourceMappingURL=index.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"sources":["../src/components/QCMetricsInspector.tsx","../src/components/QCProfileEditor.tsx","../src/components/QCJobMonitor.tsx","../src/components/QCDecisionBadge.tsx","../src/components/QCRoutingTimeline.tsx","../src/index.tsx"],"sourcesContent":["import * as React from \"react\";\nimport clsx from \"clsx\";\nimport { Badge, StatusDot, Surface } from \"@echothink-ui/core\";\nimport type { EthOperationalStatus, EthSeverity } from \"@echothink-ui/core\";\nimport { DataTable } from \"@echothink-ui/data\";\nimport type { DataColumn } from \"@echothink-ui/data\";\n\nexport interface QCMetric extends Record<string, unknown> {\n id: string;\n label: string;\n value: number | string;\n unit?: string;\n threshold?: { min?: number; max?: number };\n status: \"pass\" | \"fail\" | \"warning\" | \"manual-review\";\n}\n\nexport interface QCDecision {\n status: \"pass\" | \"fail\" | \"manual-review-required\";\n reasons?: string[];\n}\n\nexport interface QCMetricsInspectorProps {\n metrics: QCMetric[];\n decision?: QCDecision;\n className?: string;\n}\n\nexport function QCMetricsInspector({ metrics, decision, className }: QCMetricsInspectorProps) {\n const columns = React.useMemo<DataColumn<QCMetric>[]>(\n () => [\n { key: \"label\", header: \"Metric\" },\n {\n key: \"value\",\n header: \"Value\",\n render: (metric) => `${metric.value}${metric.unit ? ` ${metric.unit}` : \"\"}`\n },\n {\n key: \"threshold\",\n header: \"Threshold\",\n render: (metric) => thresholdLabel(metric.threshold)\n },\n {\n key: \"status\",\n header: \"Status\",\n render: (metric) => (\n <StatusDot status={metricStatus(metric.status)} label={metricStatusLabel(metric.status)} />\n )\n }\n ],\n []\n );\n\n return (\n <Surface\n className={clsx(\"eth-quality-qc-metrics-inspector\", className)}\n title=\"QC metrics\"\n subtitle={decision ? \"Automated decision available\" : undefined}\n data-eth-component=\"QCMetricsInspector\"\n >\n {decision ? (\n <div className=\"eth-quality-qc-metrics-inspector__decision\">\n <Badge severity={decisionSeverity(decision.status)}>{decisionLabel(decision.status)}</Badge>\n {decision.reasons?.length ? (\n <ul>\n {decision.reasons.map((reason) => (\n <li key={reason}>{reason}</li>\n ))}\n </ul>\n ) : null}\n </div>\n ) : null}\n <DataTable rows={metrics} columns={columns} />\n </Surface>\n );\n}\n\nfunction thresholdLabel(threshold?: QCMetric[\"threshold\"]) {\n if (!threshold) return \"-\";\n if (threshold.min !== undefined && threshold.max !== undefined) {\n return `${threshold.min} - ${threshold.max}`;\n }\n if (threshold.min !== undefined) return `>= ${threshold.min}`;\n if (threshold.max !== undefined) return `<= ${threshold.max}`;\n return \"-\";\n}\n\nfunction metricStatus(status: QCMetric[\"status\"]): EthOperationalStatus {\n if (status === \"pass\") return \"succeeded\";\n if (status === \"fail\") return \"failed\";\n if (status === \"warning\") return \"warning\";\n return \"approval-required\";\n}\n\nfunction metricStatusLabel(status: QCMetric[\"status\"]) {\n if (status === \"manual-review\") return \"Manual review\";\n return status.charAt(0).toUpperCase() + status.slice(1);\n}\n\nfunction decisionSeverity(status: QCDecision[\"status\"]): EthSeverity {\n if (status === \"pass\") return \"success\";\n if (status === \"fail\") return \"danger\";\n return \"warning\";\n}\n\nfunction decisionLabel(status: QCDecision[\"status\"]) {\n if (status === \"manual-review-required\") return \"Manual review required\";\n return status.charAt(0).toUpperCase() + status.slice(1);\n}\n","import * as React from \"react\";\nimport clsx from \"clsx\";\nimport { ConfigForm, RuleBuilder } from \"@echothink-ui/forms\";\nimport type { FormValues, RuleDefinition } from \"@echothink-ui/forms\";\n\nexport interface QCThreshold {\n id: string;\n label: string;\n operator: string;\n value: number | string;\n}\n\nexport interface QCProfile {\n id: string;\n language: string;\n thresholds: QCThreshold[];\n}\n\nexport interface QCProfileEditorProps {\n profile: QCProfile;\n onChange?: (profile: QCProfile) => void;\n onSubmit?: (profile: QCProfile) => void;\n className?: string;\n}\n\nconst operatorOptions = [\n { value: \"lt\", label: \"Less than\" },\n { value: \"lte\", label: \"Less than or equal\" },\n { value: \"gt\", label: \"Greater than\" },\n { value: \"gte\", label: \"Greater than or equal\" },\n { value: \"eq\", label: \"Equals\" },\n { value: \"between\", label: \"Between\" }\n];\n\nexport function QCProfileEditor({ profile, onChange, onSubmit, className }: QCProfileEditorProps) {\n const values = React.useMemo(() => valuesFromProfile(profile), [profile]);\n const rules = React.useMemo(() => rulesFromProfile(profile), [profile]);\n\n return (\n <div className={clsx(\"eth-quality-qc-profile-editor\", className)} data-eth-component=\"QCProfileEditor\">\n <ConfigForm\n title=\"QC profile\"\n description={`Profile ${profile.id}`}\n sections={[\n {\n id: \"profile\",\n title: \"Thresholds\",\n description: \"Language and threshold values used by the automated QC job.\",\n fields: [\n {\n name: \"language\",\n type: \"text\",\n label: \"Language\",\n required: true\n },\n ...profile.thresholds.flatMap((threshold) => [\n {\n name: operatorKey(threshold.id),\n type: \"select\" as const,\n label: `${threshold.label} operator`,\n options: operatorOptions\n },\n {\n name: valueKey(threshold.id),\n type: typeof threshold.value === \"number\" ? (\"number\" as const) : (\"text\" as const),\n label: `${threshold.label} value`\n }\n ])\n ]\n }\n ]}\n values={values}\n submitLabel=\"Save profile\"\n onChange={(nextValues) => onChange?.(profileFromValues(profile, nextValues))}\n onSubmit={(nextValues) => onSubmit?.(profileFromValues(profile, nextValues))}\n />\n <RuleBuilder\n className=\"eth-quality-qc-profile-editor__rules\"\n rules={rules}\n variables={profile.thresholds.map((threshold) => threshold.id)}\n onChange={(nextRules) => onChange?.(profileFromRules(profile, nextRules))}\n />\n </div>\n );\n}\n\nfunction valuesFromProfile(profile: QCProfile): FormValues {\n return profile.thresholds.reduce<FormValues>(\n (values, threshold) => ({\n ...values,\n [operatorKey(threshold.id)]: threshold.operator,\n [valueKey(threshold.id)]: threshold.value\n }),\n { language: profile.language }\n );\n}\n\nfunction profileFromValues(profile: QCProfile, values: FormValues): QCProfile {\n return {\n ...profile,\n language: String(values.language ?? \"\"),\n thresholds: profile.thresholds.map((threshold) => ({\n ...threshold,\n operator: String(values[operatorKey(threshold.id)] ?? threshold.operator),\n value: normalizeThresholdValue(threshold.value, values[valueKey(threshold.id)])\n }))\n };\n}\n\nfunction rulesFromProfile(profile: QCProfile): RuleDefinition[] {\n return profile.thresholds.map((threshold) => ({\n id: threshold.id,\n when: {\n kind: \"leaf\",\n field: threshold.id,\n op: threshold.operator,\n value: threshold.value\n },\n then: {\n type: \"require-approval\",\n target: \"manual-review\"\n }\n }));\n}\n\nfunction profileFromRules(profile: QCProfile, rules: RuleDefinition[]): QCProfile {\n return {\n ...profile,\n thresholds: profile.thresholds.map((threshold) => {\n const rule = rules.find((item) => item.id === threshold.id);\n if (!rule) return threshold;\n return {\n ...threshold,\n operator: rule.when.op ?? threshold.operator,\n value: normalizeThresholdValue(threshold.value, rule.when.value)\n };\n })\n };\n}\n\nfunction normalizeThresholdValue(previousValue: string | number, nextValue: unknown) {\n if (typeof previousValue === \"number\") {\n const numeric = Number(nextValue);\n return Number.isNaN(numeric) ? previousValue : numeric;\n }\n return String(nextValue ?? \"\");\n}\n\nfunction operatorKey(id: string) {\n return `${id}.operator`;\n}\n\nfunction valueKey(id: string) {\n return `${id}.value`;\n}\n","import * as React from \"react\";\nimport clsx from \"clsx\";\nimport { StatusDot, Surface } from \"@echothink-ui/core\";\nimport type { EthOperationalStatus } from \"@echothink-ui/core\";\nimport { JobQueuePanel } from \"@echothink-ui/admin\";\nimport { DataTable } from \"@echothink-ui/data\";\nimport type { DataColumn } from \"@echothink-ui/data\";\n\nexport interface QCJob extends Record<string, unknown> {\n id: string;\n recordingId: string;\n status: EthOperationalStatus;\n queuedAt: string;\n durationMs?: number;\n profile?: string;\n}\n\nexport interface QCJobMonitorProps {\n jobs: QCJob[];\n className?: string;\n}\n\nexport function QCJobMonitor({ jobs, className }: QCJobMonitorProps) {\n const columns = React.useMemo<DataColumn<QCJob>[]>(\n () => [\n { key: \"recordingId\", header: \"Recording\" },\n {\n key: \"status\",\n header: \"Status\",\n render: (job) => <StatusDot status={job.status} label={job.status} />\n },\n { key: \"queuedAt\", header: \"Queued\" },\n { key: \"durationMs\", header: \"Duration\", render: (job) => formatDuration(job.durationMs) },\n { key: \"profile\", header: \"Profile\", render: (job) => job.profile ?? \"default\" }\n ],\n []\n );\n\n return (\n <Surface\n className={clsx(\"eth-quality-qc-job-monitor\", className)}\n title=\"QC job monitor\"\n data-eth-component=\"QCJobMonitor\"\n >\n <JobQueuePanel queues={queuesFromJobs(jobs)} />\n <DataTable rows={jobs} columns={columns} />\n </Surface>\n );\n}\n\nfunction queuesFromJobs(jobs: QCJob[]) {\n const grouped = new Map<string, QCJob[]>();\n for (const job of jobs) {\n const key = job.profile ?? \"default\";\n grouped.set(key, [...(grouped.get(key) ?? []), job]);\n }\n return Array.from(grouped.entries()).map(([profile, profileJobs]) => ({\n id: profile,\n name: `${profile} QC`,\n depth: profileJobs.filter((job) => job.status === \"queued\").length,\n processing: profileJobs.filter((job) => job.status === \"running\" || job.status === \"in-progress\").length,\n failed: profileJobs.filter((job) => job.status === \"failed\").length,\n retryRate: retryRate(profileJobs)\n }));\n}\n\nfunction retryRate(jobs: QCJob[]) {\n if (!jobs.length) return 0;\n return Math.round((jobs.filter((job) => job.status === \"failed\").length / jobs.length) * 100);\n}\n\nfunction formatDuration(durationMs?: number) {\n if (durationMs === undefined) return \"-\";\n if (durationMs < 1000) return `${durationMs}ms`;\n const seconds = Math.round(durationMs / 1000);\n return `${seconds}s`;\n}\n","import * as React from \"react\";\nimport clsx from \"clsx\";\nimport { Badge } from \"@echothink-ui/core\";\nimport type { EthSeverity } from \"@echothink-ui/core\";\n\nexport interface QCDecisionBadgeProps {\n decision: \"pass\" | \"fail\" | \"manual-review-required\" | \"overridden\";\n reason?: string;\n className?: string;\n}\n\nexport function QCDecisionBadge({ decision, reason, className }: QCDecisionBadgeProps) {\n return (\n <span\n className={clsx(\"eth-quality-qc-decision-badge\", className)}\n title={reason}\n data-eth-component=\"QCDecisionBadge\"\n >\n <Badge severity={decisionSeverity(decision)}>{decisionLabel(decision)}</Badge>\n </span>\n );\n}\n\nfunction decisionSeverity(decision: QCDecisionBadgeProps[\"decision\"]): EthSeverity {\n if (decision === \"pass\") return \"success\";\n if (decision === \"fail\") return \"danger\";\n if (decision === \"overridden\") return \"info\";\n return \"warning\";\n}\n\nfunction decisionLabel(decision: QCDecisionBadgeProps[\"decision\"]) {\n if (decision === \"manual-review-required\") return \"Manual review required\";\n return decision.charAt(0).toUpperCase() + decision.slice(1);\n}\n","import * as React from \"react\";\nimport clsx from \"clsx\";\nimport { Badge, Surface } from \"@echothink-ui/core\";\nimport type { EthSeverity } from \"@echothink-ui/core\";\n\nexport interface QCRoutingEvent {\n at: string;\n label: string;\n severity?: EthSeverity;\n}\n\nexport interface QCRoutingTimelineProps {\n events: QCRoutingEvent[];\n className?: string;\n}\n\nexport function QCRoutingTimeline({ events, className }: QCRoutingTimelineProps) {\n return (\n <Surface\n className={clsx(\"eth-quality-qc-routing-timeline\", className)}\n title=\"Routing timeline\"\n data-eth-component=\"QCRoutingTimeline\"\n >\n <ol className=\"eth-quality-qc-routing-timeline__events\">\n {events.map((event) => (\n <li key={`${event.at}-${event.label}`}>\n <time>{event.at}</time>\n <Badge severity={event.severity ?? \"info\"}>{event.severity ?? \"info\"}</Badge>\n <span>{event.label}</span>\n </li>\n ))}\n </ol>\n </Surface>\n );\n}\n","export * from \"./components/QCMetricsInspector\";\nexport * from \"./components/QCProfileEditor\";\nexport * from \"./components/QCJobMonitor\";\nexport * from \"./components/QCDecisionBadge\";\nexport * from \"./components/QCRoutingTimeline\";\n\nexport const QualityComponentNames = [\n \"QCMetricsInspector\",\n \"QCProfileEditor\",\n \"QCJobMonitor\",\n \"QCDecisionBadge\",\n \"QCRoutingTimeline\"\n] as const;\nexport type QualityComponentName = (typeof QualityComponentNames)[number];\n"],"mappings":"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","names":["React","clsx","jsx","jsxs","React","clsx","StatusDot","Surface","DataTable","jsx","jsxs","clsx","Badge","jsx","decisionSeverity","decisionLabel","clsx","Badge","Surface","jsx","jsxs"]}
package/package.json ADDED
@@ -0,0 +1,42 @@
1
+ {
2
+ "name": "@echothink-ui/quality",
3
+ "version": "0.1.0",
4
+ "private": false,
5
+ "type": "module",
6
+ "sideEffects": false,
7
+ "main": "./dist/index.cjs",
8
+ "module": "./dist/index.js",
9
+ "types": "./dist/index.d.ts",
10
+ "exports": {
11
+ ".": {
12
+ "types": "./dist/index.d.ts",
13
+ "import": "./dist/index.js",
14
+ "require": "./dist/index.cjs"
15
+ }
16
+ },
17
+ "files": [
18
+ "dist",
19
+ "src",
20
+ "README.md"
21
+ ],
22
+ "peerDependencies": {
23
+ "react": ">=18.3.0",
24
+ "react-dom": ">=18.3.0"
25
+ },
26
+ "dependencies": {
27
+ "clsx": "^2.1.1",
28
+ "@echothink-ui/data": "0.2.0",
29
+ "@echothink-ui/core": "0.2.0",
30
+ "@echothink-ui/forms": "0.2.0",
31
+ "@echothink-ui/admin": "0.1.0"
32
+ },
33
+ "publishConfig": {
34
+ "access": "public"
35
+ },
36
+ "scripts": {
37
+ "build": "tsup src/index.tsx --format esm,cjs --sourcemap --clean --external react --external react-dom && tsc -p tsconfig.json --declaration --emitDeclarationOnly --noEmit false --outDir dist",
38
+ "typecheck": "tsc -p tsconfig.json --noEmit",
39
+ "test": "vitest run --config ../../vitest.config.ts --passWithNoTests",
40
+ "lint": "eslint src"
41
+ }
42
+ }
@@ -0,0 +1,34 @@
1
+ import * as React from "react";
2
+ import clsx from "clsx";
3
+ import { Badge } from "@echothink-ui/core";
4
+ import type { EthSeverity } from "@echothink-ui/core";
5
+
6
+ export interface QCDecisionBadgeProps {
7
+ decision: "pass" | "fail" | "manual-review-required" | "overridden";
8
+ reason?: string;
9
+ className?: string;
10
+ }
11
+
12
+ export function QCDecisionBadge({ decision, reason, className }: QCDecisionBadgeProps) {
13
+ return (
14
+ <span
15
+ className={clsx("eth-quality-qc-decision-badge", className)}
16
+ title={reason}
17
+ data-eth-component="QCDecisionBadge"
18
+ >
19
+ <Badge severity={decisionSeverity(decision)}>{decisionLabel(decision)}</Badge>
20
+ </span>
21
+ );
22
+ }
23
+
24
+ function decisionSeverity(decision: QCDecisionBadgeProps["decision"]): EthSeverity {
25
+ if (decision === "pass") return "success";
26
+ if (decision === "fail") return "danger";
27
+ if (decision === "overridden") return "info";
28
+ return "warning";
29
+ }
30
+
31
+ function decisionLabel(decision: QCDecisionBadgeProps["decision"]) {
32
+ if (decision === "manual-review-required") return "Manual review required";
33
+ return decision.charAt(0).toUpperCase() + decision.slice(1);
34
+ }
@@ -0,0 +1,77 @@
1
+ import * as React from "react";
2
+ import clsx from "clsx";
3
+ import { StatusDot, Surface } from "@echothink-ui/core";
4
+ import type { EthOperationalStatus } from "@echothink-ui/core";
5
+ import { JobQueuePanel } from "@echothink-ui/admin";
6
+ import { DataTable } from "@echothink-ui/data";
7
+ import type { DataColumn } from "@echothink-ui/data";
8
+
9
+ export interface QCJob extends Record<string, unknown> {
10
+ id: string;
11
+ recordingId: string;
12
+ status: EthOperationalStatus;
13
+ queuedAt: string;
14
+ durationMs?: number;
15
+ profile?: string;
16
+ }
17
+
18
+ export interface QCJobMonitorProps {
19
+ jobs: QCJob[];
20
+ className?: string;
21
+ }
22
+
23
+ export function QCJobMonitor({ jobs, className }: QCJobMonitorProps) {
24
+ const columns = React.useMemo<DataColumn<QCJob>[]>(
25
+ () => [
26
+ { key: "recordingId", header: "Recording" },
27
+ {
28
+ key: "status",
29
+ header: "Status",
30
+ render: (job) => <StatusDot status={job.status} label={job.status} />
31
+ },
32
+ { key: "queuedAt", header: "Queued" },
33
+ { key: "durationMs", header: "Duration", render: (job) => formatDuration(job.durationMs) },
34
+ { key: "profile", header: "Profile", render: (job) => job.profile ?? "default" }
35
+ ],
36
+ []
37
+ );
38
+
39
+ return (
40
+ <Surface
41
+ className={clsx("eth-quality-qc-job-monitor", className)}
42
+ title="QC job monitor"
43
+ data-eth-component="QCJobMonitor"
44
+ >
45
+ <JobQueuePanel queues={queuesFromJobs(jobs)} />
46
+ <DataTable rows={jobs} columns={columns} />
47
+ </Surface>
48
+ );
49
+ }
50
+
51
+ function queuesFromJobs(jobs: QCJob[]) {
52
+ const grouped = new Map<string, QCJob[]>();
53
+ for (const job of jobs) {
54
+ const key = job.profile ?? "default";
55
+ grouped.set(key, [...(grouped.get(key) ?? []), job]);
56
+ }
57
+ return Array.from(grouped.entries()).map(([profile, profileJobs]) => ({
58
+ id: profile,
59
+ name: `${profile} QC`,
60
+ depth: profileJobs.filter((job) => job.status === "queued").length,
61
+ processing: profileJobs.filter((job) => job.status === "running" || job.status === "in-progress").length,
62
+ failed: profileJobs.filter((job) => job.status === "failed").length,
63
+ retryRate: retryRate(profileJobs)
64
+ }));
65
+ }
66
+
67
+ function retryRate(jobs: QCJob[]) {
68
+ if (!jobs.length) return 0;
69
+ return Math.round((jobs.filter((job) => job.status === "failed").length / jobs.length) * 100);
70
+ }
71
+
72
+ function formatDuration(durationMs?: number) {
73
+ if (durationMs === undefined) return "-";
74
+ if (durationMs < 1000) return `${durationMs}ms`;
75
+ const seconds = Math.round(durationMs / 1000);
76
+ return `${seconds}s`;
77
+ }
@@ -0,0 +1,108 @@
1
+ import * as React from "react";
2
+ import clsx from "clsx";
3
+ import { Badge, StatusDot, Surface } from "@echothink-ui/core";
4
+ import type { EthOperationalStatus, EthSeverity } from "@echothink-ui/core";
5
+ import { DataTable } from "@echothink-ui/data";
6
+ import type { DataColumn } from "@echothink-ui/data";
7
+
8
+ export interface QCMetric extends Record<string, unknown> {
9
+ id: string;
10
+ label: string;
11
+ value: number | string;
12
+ unit?: string;
13
+ threshold?: { min?: number; max?: number };
14
+ status: "pass" | "fail" | "warning" | "manual-review";
15
+ }
16
+
17
+ export interface QCDecision {
18
+ status: "pass" | "fail" | "manual-review-required";
19
+ reasons?: string[];
20
+ }
21
+
22
+ export interface QCMetricsInspectorProps {
23
+ metrics: QCMetric[];
24
+ decision?: QCDecision;
25
+ className?: string;
26
+ }
27
+
28
+ export function QCMetricsInspector({ metrics, decision, className }: QCMetricsInspectorProps) {
29
+ const columns = React.useMemo<DataColumn<QCMetric>[]>(
30
+ () => [
31
+ { key: "label", header: "Metric" },
32
+ {
33
+ key: "value",
34
+ header: "Value",
35
+ render: (metric) => `${metric.value}${metric.unit ? ` ${metric.unit}` : ""}`
36
+ },
37
+ {
38
+ key: "threshold",
39
+ header: "Threshold",
40
+ render: (metric) => thresholdLabel(metric.threshold)
41
+ },
42
+ {
43
+ key: "status",
44
+ header: "Status",
45
+ render: (metric) => (
46
+ <StatusDot status={metricStatus(metric.status)} label={metricStatusLabel(metric.status)} />
47
+ )
48
+ }
49
+ ],
50
+ []
51
+ );
52
+
53
+ return (
54
+ <Surface
55
+ className={clsx("eth-quality-qc-metrics-inspector", className)}
56
+ title="QC metrics"
57
+ subtitle={decision ? "Automated decision available" : undefined}
58
+ data-eth-component="QCMetricsInspector"
59
+ >
60
+ {decision ? (
61
+ <div className="eth-quality-qc-metrics-inspector__decision">
62
+ <Badge severity={decisionSeverity(decision.status)}>{decisionLabel(decision.status)}</Badge>
63
+ {decision.reasons?.length ? (
64
+ <ul>
65
+ {decision.reasons.map((reason) => (
66
+ <li key={reason}>{reason}</li>
67
+ ))}
68
+ </ul>
69
+ ) : null}
70
+ </div>
71
+ ) : null}
72
+ <DataTable rows={metrics} columns={columns} />
73
+ </Surface>
74
+ );
75
+ }
76
+
77
+ function thresholdLabel(threshold?: QCMetric["threshold"]) {
78
+ if (!threshold) return "-";
79
+ if (threshold.min !== undefined && threshold.max !== undefined) {
80
+ return `${threshold.min} - ${threshold.max}`;
81
+ }
82
+ if (threshold.min !== undefined) return `>= ${threshold.min}`;
83
+ if (threshold.max !== undefined) return `<= ${threshold.max}`;
84
+ return "-";
85
+ }
86
+
87
+ function metricStatus(status: QCMetric["status"]): EthOperationalStatus {
88
+ if (status === "pass") return "succeeded";
89
+ if (status === "fail") return "failed";
90
+ if (status === "warning") return "warning";
91
+ return "approval-required";
92
+ }
93
+
94
+ function metricStatusLabel(status: QCMetric["status"]) {
95
+ if (status === "manual-review") return "Manual review";
96
+ return status.charAt(0).toUpperCase() + status.slice(1);
97
+ }
98
+
99
+ function decisionSeverity(status: QCDecision["status"]): EthSeverity {
100
+ if (status === "pass") return "success";
101
+ if (status === "fail") return "danger";
102
+ return "warning";
103
+ }
104
+
105
+ function decisionLabel(status: QCDecision["status"]) {
106
+ if (status === "manual-review-required") return "Manual review required";
107
+ return status.charAt(0).toUpperCase() + status.slice(1);
108
+ }