@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.
@@ -0,0 +1,155 @@
1
+ import * as React from "react";
2
+ import clsx from "clsx";
3
+ import { ConfigForm, RuleBuilder } from "@echothink-ui/forms";
4
+ import type { FormValues, RuleDefinition } from "@echothink-ui/forms";
5
+
6
+ export interface QCThreshold {
7
+ id: string;
8
+ label: string;
9
+ operator: string;
10
+ value: number | string;
11
+ }
12
+
13
+ export interface QCProfile {
14
+ id: string;
15
+ language: string;
16
+ thresholds: QCThreshold[];
17
+ }
18
+
19
+ export interface QCProfileEditorProps {
20
+ profile: QCProfile;
21
+ onChange?: (profile: QCProfile) => void;
22
+ onSubmit?: (profile: QCProfile) => void;
23
+ className?: string;
24
+ }
25
+
26
+ const operatorOptions = [
27
+ { value: "lt", label: "Less than" },
28
+ { value: "lte", label: "Less than or equal" },
29
+ { value: "gt", label: "Greater than" },
30
+ { value: "gte", label: "Greater than or equal" },
31
+ { value: "eq", label: "Equals" },
32
+ { value: "between", label: "Between" }
33
+ ];
34
+
35
+ export function QCProfileEditor({ profile, onChange, onSubmit, className }: QCProfileEditorProps) {
36
+ const values = React.useMemo(() => valuesFromProfile(profile), [profile]);
37
+ const rules = React.useMemo(() => rulesFromProfile(profile), [profile]);
38
+
39
+ return (
40
+ <div className={clsx("eth-quality-qc-profile-editor", className)} data-eth-component="QCProfileEditor">
41
+ <ConfigForm
42
+ title="QC profile"
43
+ description={`Profile ${profile.id}`}
44
+ sections={[
45
+ {
46
+ id: "profile",
47
+ title: "Thresholds",
48
+ description: "Language and threshold values used by the automated QC job.",
49
+ fields: [
50
+ {
51
+ name: "language",
52
+ type: "text",
53
+ label: "Language",
54
+ required: true
55
+ },
56
+ ...profile.thresholds.flatMap((threshold) => [
57
+ {
58
+ name: operatorKey(threshold.id),
59
+ type: "select" as const,
60
+ label: `${threshold.label} operator`,
61
+ options: operatorOptions
62
+ },
63
+ {
64
+ name: valueKey(threshold.id),
65
+ type: typeof threshold.value === "number" ? ("number" as const) : ("text" as const),
66
+ label: `${threshold.label} value`
67
+ }
68
+ ])
69
+ ]
70
+ }
71
+ ]}
72
+ values={values}
73
+ submitLabel="Save profile"
74
+ onChange={(nextValues) => onChange?.(profileFromValues(profile, nextValues))}
75
+ onSubmit={(nextValues) => onSubmit?.(profileFromValues(profile, nextValues))}
76
+ />
77
+ <RuleBuilder
78
+ className="eth-quality-qc-profile-editor__rules"
79
+ rules={rules}
80
+ variables={profile.thresholds.map((threshold) => threshold.id)}
81
+ onChange={(nextRules) => onChange?.(profileFromRules(profile, nextRules))}
82
+ />
83
+ </div>
84
+ );
85
+ }
86
+
87
+ function valuesFromProfile(profile: QCProfile): FormValues {
88
+ return profile.thresholds.reduce<FormValues>(
89
+ (values, threshold) => ({
90
+ ...values,
91
+ [operatorKey(threshold.id)]: threshold.operator,
92
+ [valueKey(threshold.id)]: threshold.value
93
+ }),
94
+ { language: profile.language }
95
+ );
96
+ }
97
+
98
+ function profileFromValues(profile: QCProfile, values: FormValues): QCProfile {
99
+ return {
100
+ ...profile,
101
+ language: String(values.language ?? ""),
102
+ thresholds: profile.thresholds.map((threshold) => ({
103
+ ...threshold,
104
+ operator: String(values[operatorKey(threshold.id)] ?? threshold.operator),
105
+ value: normalizeThresholdValue(threshold.value, values[valueKey(threshold.id)])
106
+ }))
107
+ };
108
+ }
109
+
110
+ function rulesFromProfile(profile: QCProfile): RuleDefinition[] {
111
+ return profile.thresholds.map((threshold) => ({
112
+ id: threshold.id,
113
+ when: {
114
+ kind: "leaf",
115
+ field: threshold.id,
116
+ op: threshold.operator,
117
+ value: threshold.value
118
+ },
119
+ then: {
120
+ type: "require-approval",
121
+ target: "manual-review"
122
+ }
123
+ }));
124
+ }
125
+
126
+ function profileFromRules(profile: QCProfile, rules: RuleDefinition[]): QCProfile {
127
+ return {
128
+ ...profile,
129
+ thresholds: profile.thresholds.map((threshold) => {
130
+ const rule = rules.find((item) => item.id === threshold.id);
131
+ if (!rule) return threshold;
132
+ return {
133
+ ...threshold,
134
+ operator: rule.when.op ?? threshold.operator,
135
+ value: normalizeThresholdValue(threshold.value, rule.when.value)
136
+ };
137
+ })
138
+ };
139
+ }
140
+
141
+ function normalizeThresholdValue(previousValue: string | number, nextValue: unknown) {
142
+ if (typeof previousValue === "number") {
143
+ const numeric = Number(nextValue);
144
+ return Number.isNaN(numeric) ? previousValue : numeric;
145
+ }
146
+ return String(nextValue ?? "");
147
+ }
148
+
149
+ function operatorKey(id: string) {
150
+ return `${id}.operator`;
151
+ }
152
+
153
+ function valueKey(id: string) {
154
+ return `${id}.value`;
155
+ }
@@ -0,0 +1,35 @@
1
+ import * as React from "react";
2
+ import clsx from "clsx";
3
+ import { Badge, Surface } from "@echothink-ui/core";
4
+ import type { EthSeverity } from "@echothink-ui/core";
5
+
6
+ export interface QCRoutingEvent {
7
+ at: string;
8
+ label: string;
9
+ severity?: EthSeverity;
10
+ }
11
+
12
+ export interface QCRoutingTimelineProps {
13
+ events: QCRoutingEvent[];
14
+ className?: string;
15
+ }
16
+
17
+ export function QCRoutingTimeline({ events, className }: QCRoutingTimelineProps) {
18
+ return (
19
+ <Surface
20
+ className={clsx("eth-quality-qc-routing-timeline", className)}
21
+ title="Routing timeline"
22
+ data-eth-component="QCRoutingTimeline"
23
+ >
24
+ <ol className="eth-quality-qc-routing-timeline__events">
25
+ {events.map((event) => (
26
+ <li key={`${event.at}-${event.label}`}>
27
+ <time>{event.at}</time>
28
+ <Badge severity={event.severity ?? "info"}>{event.severity ?? "info"}</Badge>
29
+ <span>{event.label}</span>
30
+ </li>
31
+ ))}
32
+ </ol>
33
+ </Surface>
34
+ );
35
+ }
package/src/index.tsx ADDED
@@ -0,0 +1,14 @@
1
+ export * from "./components/QCMetricsInspector";
2
+ export * from "./components/QCProfileEditor";
3
+ export * from "./components/QCJobMonitor";
4
+ export * from "./components/QCDecisionBadge";
5
+ export * from "./components/QCRoutingTimeline";
6
+
7
+ export const QualityComponentNames = [
8
+ "QCMetricsInspector",
9
+ "QCProfileEditor",
10
+ "QCJobMonitor",
11
+ "QCDecisionBadge",
12
+ "QCRoutingTimeline"
13
+ ] as const;
14
+ export type QualityComponentName = (typeof QualityComponentNames)[number];