@burdenoff/microfe-ecoimpacthub 2026.1030.1 → 2026.1030.2
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/ecoimpacthub/components/ai/AiAnomalyPanel.d.ts.map +1 -1
- package/dist/ecoimpacthub/components/ai/AiAnomalyPanel.js +92 -92
- package/dist/ecoimpacthub/components/ai/AiAnomalyPanel.js.map +1 -1
- package/dist/ecoimpacthub/components/ai/AiThresholdSuggest.d.ts.map +1 -1
- package/dist/ecoimpacthub/components/ai/AiThresholdSuggest.js +51 -51
- package/dist/ecoimpacthub/components/ai/AiThresholdSuggest.js.map +1 -1
- package/dist/ecoimpacthub/components/ai/AiTriageDialog.js +74 -74
- package/dist/ecoimpacthub/components/ai/AiTriageDialog.js.map +1 -1
- package/dist/ecoimpacthub/utils/aiMonitoring.d.ts +1 -0
- package/dist/ecoimpacthub/utils/aiMonitoring.d.ts.map +1 -1
- package/dist/ecoimpacthub/utils/aiMonitoring.js +5 -1
- package/dist/ecoimpacthub/utils/aiMonitoring.js.map +1 -1
- package/package.json +1 -1
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"AiAnomalyPanel.d.ts","sourceRoot":"","sources":["../../../../src/ecoimpacthub/components/ai/AiAnomalyPanel.tsx"],"names":[],"mappings":"AAAA;;;;;;;;GAQG;AAEH,OAAO,EAAqB,KAAK,EAAE,EAAE,MAAM,OAAO,CAAC;
|
|
1
|
+
{"version":3,"file":"AiAnomalyPanel.d.ts","sourceRoot":"","sources":["../../../../src/ecoimpacthub/components/ai/AiAnomalyPanel.tsx"],"names":[],"mappings":"AAAA;;;;;;;;GAQG;AAEH,OAAO,EAAqB,KAAK,EAAE,EAAE,MAAM,OAAO,CAAC;AA0BnD,OAAO,KAAK,EAA6B,SAAS,EAAE,MAAM,aAAa,CAAC;AAGxE,UAAU,mBAAmB;IAC3B,MAAM,EAAE,MAAM,CAAC;IACf,0DAA0D;IAC1D,UAAU,EAAE,MAAM,EAAE,CAAC;IACrB,OAAO,CAAC,EAAE,SAAS,EAAE,CAAC;CACvB;AAeD,eAAO,MAAM,cAAc,EAAE,EAAE,CAAC,mBAAmB,CAuPlD,CAAC"}
|
|
@@ -2,153 +2,153 @@ import { formatDateTime as e, formatNumber as t } from "../../utils/formatters.j
|
|
|
2
2
|
import { StatusBadge as n } from "../StatusBadge.js";
|
|
3
3
|
import { useEcoMeasurements as r } from "../../hooks/useEcoImpactHubApi.js";
|
|
4
4
|
import { useTr as i } from "../../hooks/useTr.js";
|
|
5
|
-
import { ANOMALY_PERIOD_PRESETS as a, groupAnomalies as o, periodRange as s, resolveCitedReadings as c } from "../../utils/aiMonitoring.js";
|
|
6
|
-
import { useEcoAiDetectAnomalies as
|
|
5
|
+
import { ANOMALY_PERIOD_PRESETS as a, groupAnomalies as o, periodRange as s, resolveCitedReadings as c, userFacingAiNotes as l } from "../../utils/aiMonitoring.js";
|
|
6
|
+
import { useEcoAiDetectAnomalies as u, useEcoAiErrorReporter as d } from "../../hooks/useEcoAiMonitoring.js";
|
|
7
7
|
import "../../hooks/index.js";
|
|
8
|
-
import { AiCreditHint as
|
|
9
|
-
import { useMemo as
|
|
10
|
-
import { Activity as
|
|
11
|
-
import { Fragment as
|
|
12
|
-
import { Button as
|
|
8
|
+
import { AiCreditHint as f, AiErrorNotice as p, CitedReadingsList as m } from "./AiShared.js";
|
|
9
|
+
import { useMemo as h, useState as g } from "react";
|
|
10
|
+
import { Activity as _, ScanSearch as v } from "lucide-react";
|
|
11
|
+
import { Fragment as y, jsx as b, jsxs as x } from "react/jsx-runtime";
|
|
12
|
+
import { Button as S, GlassCard as C, Label as w, Select as T, SelectContent as E, SelectItem as D, SelectTrigger as O, SelectValue as k } from "@burdenoff/fe-libs/ui";
|
|
13
13
|
//#region src/ecoimpacthub/components/ai/AiAnomalyPanel.tsx
|
|
14
|
-
var
|
|
14
|
+
var A = {
|
|
15
15
|
"7d": ["ecoAi.anomalies.period7d", "Last 7 days"],
|
|
16
16
|
"30d": ["ecoAi.anomalies.period30d", "Last 30 days"],
|
|
17
17
|
"90d": ["ecoAi.anomalies.period90d", "Last 90 days"],
|
|
18
18
|
"365d": ["ecoAi.anomalies.period365d", "Last 12 months"]
|
|
19
|
-
},
|
|
19
|
+
}, j = {
|
|
20
20
|
SPIKE: ["ecoAi.anomalies.kindSpike", "Spikes"],
|
|
21
21
|
DROP: ["ecoAi.anomalies.kindDrop", "Drops"],
|
|
22
22
|
FLATLINE: ["ecoAi.anomalies.kindFlatline", "Flatlines"]
|
|
23
|
-
},
|
|
24
|
-
let
|
|
25
|
-
siteId:
|
|
26
|
-
parameter:
|
|
23
|
+
}, M = ({ siteId: M, parameters: N, sensors: P = [] }) => {
|
|
24
|
+
let F = i(), [I, L] = g(""), [R, z] = g("30d"), B = I || N[0] || "", V = u(), H = d(), [U, W] = g(null), [G, K] = g(null), [q, J] = g(null), Y = r({
|
|
25
|
+
siteId: M,
|
|
26
|
+
parameter: U?.parameter ?? B,
|
|
27
27
|
pagination: {
|
|
28
28
|
limit: 100,
|
|
29
29
|
offset: 0
|
|
30
30
|
}
|
|
31
|
-
}),
|
|
32
|
-
return /* @__PURE__ */
|
|
31
|
+
}), X = h(() => Y.data?.items ?? [], [Y.data]), Z = h(() => U ? o(U.anomalies) : [], [U]), Q = h(() => l(U?.notes), [U]), $ = (e) => e && P.find((t) => t.id === e)?.name || null;
|
|
32
|
+
return /* @__PURE__ */ b("div", {
|
|
33
33
|
"data-testid": "eco-ai-anomalies",
|
|
34
|
-
children: /* @__PURE__ */
|
|
34
|
+
children: /* @__PURE__ */ x(C, {
|
|
35
35
|
treatment: "plain",
|
|
36
36
|
className: "p-5",
|
|
37
37
|
children: [
|
|
38
|
-
|
|
38
|
+
N.length === 0 ? /* @__PURE__ */ b("p", {
|
|
39
39
|
className: "text-sm text-text-secondary",
|
|
40
|
-
children:
|
|
41
|
-
}) : /* @__PURE__ */
|
|
40
|
+
children: F("ecoAi.anomalies.noParameters", "Record measurements at this site to analyse them for anomalies.")
|
|
41
|
+
}) : /* @__PURE__ */ x(y, { children: [/* @__PURE__ */ x("div", {
|
|
42
42
|
className: "flex flex-wrap items-end gap-3",
|
|
43
43
|
children: [
|
|
44
|
-
/* @__PURE__ */
|
|
44
|
+
/* @__PURE__ */ x("div", {
|
|
45
45
|
className: "grid gap-1.5",
|
|
46
|
-
children: [/* @__PURE__ */
|
|
46
|
+
children: [/* @__PURE__ */ b(w, {
|
|
47
47
|
htmlFor: "ecoAiAnomalyParam",
|
|
48
|
-
children:
|
|
49
|
-
}), /* @__PURE__ */
|
|
50
|
-
value:
|
|
48
|
+
children: F("ecoAi.anomalies.parameter", "Parameter")
|
|
49
|
+
}), /* @__PURE__ */ x(T, {
|
|
50
|
+
value: B,
|
|
51
51
|
onValueChange: (e) => {
|
|
52
|
-
|
|
52
|
+
L(e), W(null), K(null);
|
|
53
53
|
},
|
|
54
|
-
children: [/* @__PURE__ */
|
|
54
|
+
children: [/* @__PURE__ */ b(O, {
|
|
55
55
|
id: "ecoAiAnomalyParam",
|
|
56
56
|
className: "w-48",
|
|
57
|
-
children: /* @__PURE__ */
|
|
58
|
-
}), /* @__PURE__ */
|
|
57
|
+
children: /* @__PURE__ */ b(k, {})
|
|
58
|
+
}), /* @__PURE__ */ b(E, { children: N.map((e) => /* @__PURE__ */ b(D, {
|
|
59
59
|
value: e,
|
|
60
60
|
children: e
|
|
61
61
|
}, e)) })]
|
|
62
62
|
})]
|
|
63
63
|
}),
|
|
64
|
-
/* @__PURE__ */
|
|
64
|
+
/* @__PURE__ */ x("div", {
|
|
65
65
|
className: "grid gap-1.5",
|
|
66
|
-
children: [/* @__PURE__ */
|
|
66
|
+
children: [/* @__PURE__ */ b(w, {
|
|
67
67
|
htmlFor: "ecoAiAnomalyPeriod",
|
|
68
|
-
children:
|
|
69
|
-
}), /* @__PURE__ */
|
|
70
|
-
value:
|
|
68
|
+
children: F("ecoAi.anomalies.period", "Period")
|
|
69
|
+
}), /* @__PURE__ */ x(T, {
|
|
70
|
+
value: R,
|
|
71
71
|
onValueChange: (e) => {
|
|
72
|
-
|
|
72
|
+
z(e), W(null), K(null);
|
|
73
73
|
},
|
|
74
|
-
children: [/* @__PURE__ */
|
|
74
|
+
children: [/* @__PURE__ */ b(O, {
|
|
75
75
|
id: "ecoAiAnomalyPeriod",
|
|
76
76
|
className: "w-44",
|
|
77
|
-
children: /* @__PURE__ */
|
|
78
|
-
}), /* @__PURE__ */
|
|
77
|
+
children: /* @__PURE__ */ b(k, {})
|
|
78
|
+
}), /* @__PURE__ */ b(E, { children: a.map((e) => /* @__PURE__ */ b(D, {
|
|
79
79
|
value: e,
|
|
80
|
-
children:
|
|
80
|
+
children: F(A[e][0], A[e][1])
|
|
81
81
|
}, e)) })]
|
|
82
82
|
})]
|
|
83
83
|
}),
|
|
84
|
-
/* @__PURE__ */
|
|
84
|
+
/* @__PURE__ */ x(S, {
|
|
85
85
|
onClick: async () => {
|
|
86
|
-
if (
|
|
87
|
-
|
|
86
|
+
if (B) {
|
|
87
|
+
K(null), J(null);
|
|
88
88
|
try {
|
|
89
|
-
|
|
90
|
-
siteId:
|
|
91
|
-
parameter:
|
|
92
|
-
...s(
|
|
89
|
+
W(await V.mutateAsync({
|
|
90
|
+
siteId: M,
|
|
91
|
+
parameter: B,
|
|
92
|
+
...s(R)
|
|
93
93
|
}));
|
|
94
94
|
} catch (e) {
|
|
95
|
-
|
|
95
|
+
W(null), K(H(e, "EcoAiDetectAnomalies", F("ecoAi.anomalies.failed", "Anomaly detection failed.")));
|
|
96
96
|
}
|
|
97
97
|
}
|
|
98
98
|
},
|
|
99
|
-
disabled: !
|
|
99
|
+
disabled: !B || V.isPending,
|
|
100
100
|
"data-testid": "eco-ai-anomalies-run",
|
|
101
|
-
children: [/* @__PURE__ */
|
|
101
|
+
children: [/* @__PURE__ */ b(v, { className: "mr-1.5 h-4 w-4" }), V.isPending ? F("ecoAi.anomalies.running", "Analysing…") : F("ecoAi.anomalies.run", "Detect anomalies")]
|
|
102
102
|
})
|
|
103
103
|
]
|
|
104
|
-
}), /* @__PURE__ */
|
|
105
|
-
|
|
106
|
-
error:
|
|
104
|
+
}), /* @__PURE__ */ b(f, { className: "mt-2" })] }),
|
|
105
|
+
G && /* @__PURE__ */ b(p, {
|
|
106
|
+
error: G,
|
|
107
107
|
className: "mt-4"
|
|
108
108
|
}),
|
|
109
|
-
|
|
109
|
+
U && U.status === "INSUFFICIENT_DATA" && /* @__PURE__ */ x("div", {
|
|
110
110
|
className: "mt-4 rounded-lg bg-bg-sunken/60 p-4",
|
|
111
111
|
"data-testid": "eco-ai-anomalies-insufficient",
|
|
112
112
|
children: [
|
|
113
|
-
/* @__PURE__ */
|
|
113
|
+
/* @__PURE__ */ b("p", {
|
|
114
114
|
className: "text-sm font-semibold text-text-primary",
|
|
115
|
-
children:
|
|
115
|
+
children: F("ecoAi.anomalies.notEnoughData", "Not enough data")
|
|
116
116
|
}),
|
|
117
|
-
/* @__PURE__ */
|
|
117
|
+
/* @__PURE__ */ b("p", {
|
|
118
118
|
className: "mt-1 text-sm text-text-secondary",
|
|
119
|
-
children:
|
|
120
|
-
count:
|
|
121
|
-
parameter:
|
|
119
|
+
children: F("ecoAi.anomalies.notEnoughDataBody", "Only {{count}} reading(s) of {{parameter}} in this period, too few to judge what is normal. No trend is shown.", {
|
|
120
|
+
count: U.readingsAnalysed,
|
|
121
|
+
parameter: U.parameter
|
|
122
122
|
})
|
|
123
123
|
}),
|
|
124
|
-
|
|
124
|
+
Q.length > 0 && /* @__PURE__ */ b("ul", {
|
|
125
125
|
className: "mt-2 list-disc space-y-0.5 pl-5 text-xs text-text-tertiary",
|
|
126
|
-
children:
|
|
126
|
+
children: Q.map((e, t) => /* @__PURE__ */ b("li", { children: e }, `${t}:${e}`))
|
|
127
127
|
})
|
|
128
128
|
]
|
|
129
129
|
}),
|
|
130
|
-
|
|
130
|
+
U && U.status === "OK" && /* @__PURE__ */ x("div", {
|
|
131
131
|
className: "mt-4 grid gap-4",
|
|
132
132
|
"data-testid": "eco-ai-anomalies-result",
|
|
133
133
|
children: [
|
|
134
|
-
/* @__PURE__ */
|
|
134
|
+
/* @__PURE__ */ x("p", {
|
|
135
135
|
className: "text-xs text-text-tertiary",
|
|
136
|
-
children: [
|
|
137
|
-
count:
|
|
138
|
-
median: t(
|
|
139
|
-
unit:
|
|
140
|
-
}),
|
|
136
|
+
children: [F("ecoAi.anomalies.summary", "{{count}} reading(s) analysed · baseline median {{median}} {{unit}}", {
|
|
137
|
+
count: U.readingsAnalysed,
|
|
138
|
+
median: t(U.baseline?.median ?? null),
|
|
139
|
+
unit: U.unit ?? ""
|
|
140
|
+
}), U.truncated && ` · ${F("ecoAi.anomalies.truncated", "the series was truncated to the most recent readings")}`]
|
|
141
141
|
}),
|
|
142
|
-
|
|
142
|
+
Z.length === 0 ? /* @__PURE__ */ x("p", {
|
|
143
143
|
className: "flex items-center gap-2 text-sm text-text-secondary",
|
|
144
144
|
"data-testid": "eco-ai-anomalies-none",
|
|
145
|
-
children: [/* @__PURE__ */
|
|
146
|
-
}) :
|
|
145
|
+
children: [/* @__PURE__ */ b(_, { className: "h-4 w-4 text-status-success-text" }), F("ecoAi.anomalies.none", "No spikes, drops or flatlines in this period.")]
|
|
146
|
+
}) : Z.map((r) => /* @__PURE__ */ x("div", { children: [/* @__PURE__ */ x("h3", {
|
|
147
147
|
className: "mb-2 flex items-center gap-2 text-sm font-semibold text-text-primary",
|
|
148
148
|
children: [
|
|
149
|
-
/* @__PURE__ */
|
|
150
|
-
|
|
151
|
-
/* @__PURE__ */
|
|
149
|
+
/* @__PURE__ */ b(n, { status: r.kind }),
|
|
150
|
+
F(j[r.kind]?.[0] ?? r.kind, j[r.kind]?.[1] ?? r.kind),
|
|
151
|
+
/* @__PURE__ */ x("span", {
|
|
152
152
|
className: "text-text-tertiary",
|
|
153
153
|
children: [
|
|
154
154
|
"(",
|
|
@@ -157,49 +157,49 @@ var k = {
|
|
|
157
157
|
]
|
|
158
158
|
})
|
|
159
159
|
]
|
|
160
|
-
}), /* @__PURE__ */
|
|
160
|
+
}), /* @__PURE__ */ b("ul", {
|
|
161
161
|
className: "divide-y divide-border-seam/60 rounded-lg border border-border-seam",
|
|
162
162
|
children: r.items.map((n) => {
|
|
163
|
-
let r =
|
|
164
|
-
return /* @__PURE__ */
|
|
163
|
+
let r = q === n.measurementId;
|
|
164
|
+
return /* @__PURE__ */ x("li", {
|
|
165
165
|
"data-testid": "eco-ai-anomaly",
|
|
166
|
-
children: [/* @__PURE__ */
|
|
166
|
+
children: [/* @__PURE__ */ x("button", {
|
|
167
167
|
type: "button",
|
|
168
168
|
className: "flex w-full items-center justify-between gap-3 px-3 py-2 text-left text-sm hover:bg-bg-sunken/60",
|
|
169
169
|
"aria-expanded": r,
|
|
170
|
-
onClick: () =>
|
|
171
|
-
children: [/* @__PURE__ */
|
|
170
|
+
onClick: () => J(r ? null : n.measurementId),
|
|
171
|
+
children: [/* @__PURE__ */ x("span", {
|
|
172
172
|
className: "tabular-nums text-text-primary",
|
|
173
173
|
children: [
|
|
174
174
|
t(n.value),
|
|
175
175
|
" ",
|
|
176
|
-
|
|
177
|
-
|
|
176
|
+
U.unit ?? "",
|
|
177
|
+
$(n.sensorId) && /* @__PURE__ */ b("span", {
|
|
178
178
|
className: "ml-2 text-xs text-text-tertiary",
|
|
179
|
-
children:
|
|
179
|
+
children: $(n.sensorId)
|
|
180
180
|
})
|
|
181
181
|
]
|
|
182
|
-
}), /* @__PURE__ */
|
|
182
|
+
}), /* @__PURE__ */ x("span", {
|
|
183
183
|
className: "flex items-center gap-3 text-xs text-text-tertiary",
|
|
184
184
|
children: [
|
|
185
|
-
/* @__PURE__ */
|
|
186
|
-
/* @__PURE__ */
|
|
187
|
-
/* @__PURE__ */
|
|
185
|
+
/* @__PURE__ */ b("span", { children: n.kind === "FLATLINE" ? F("ecoAi.anomalies.runLength", "run of {{n}}", { n: t(n.score, { maximumFractionDigits: 1 }) }) : F("ecoAi.anomalies.score", "z {{z}}", { z: t(n.score, { maximumFractionDigits: 1 }) }) }),
|
|
186
|
+
/* @__PURE__ */ b("span", { children: e(n.recordedAt) }),
|
|
187
|
+
/* @__PURE__ */ b("span", { children: F("ecoAi.anomalies.citedCount", "{{n}} cited", { n: n.evidenceMeasurementIds.length }) })
|
|
188
188
|
]
|
|
189
189
|
})]
|
|
190
|
-
}), r && /* @__PURE__ */
|
|
190
|
+
}), r && /* @__PURE__ */ b("div", {
|
|
191
191
|
className: "px-3 pb-3",
|
|
192
|
-
children: /* @__PURE__ */
|
|
193
|
-
readings: c(n.evidenceMeasurementIds,
|
|
192
|
+
children: /* @__PURE__ */ b(m, {
|
|
193
|
+
readings: c(n.evidenceMeasurementIds, X),
|
|
194
194
|
focusId: n.measurementId
|
|
195
195
|
})
|
|
196
196
|
})]
|
|
197
197
|
}, n.measurementId);
|
|
198
198
|
})
|
|
199
199
|
})] }, r.kind)),
|
|
200
|
-
|
|
200
|
+
Q.length > 0 && /* @__PURE__ */ b("ul", {
|
|
201
201
|
className: "list-disc space-y-0.5 pl-5 text-xs text-text-tertiary",
|
|
202
|
-
children:
|
|
202
|
+
children: Q.map((e, t) => /* @__PURE__ */ b("li", { children: e }, `${t}:${e}`))
|
|
203
203
|
})
|
|
204
204
|
]
|
|
205
205
|
})
|
|
@@ -208,6 +208,6 @@ var k = {
|
|
|
208
208
|
});
|
|
209
209
|
};
|
|
210
210
|
//#endregion
|
|
211
|
-
export {
|
|
211
|
+
export { M as AiAnomalyPanel };
|
|
212
212
|
|
|
213
213
|
//# sourceMappingURL=AiAnomalyPanel.js.map
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"AiAnomalyPanel.js","names":[],"sources":["../../../../src/ecoimpacthub/components/ai/AiAnomalyPanel.tsx"],"sourcesContent":["/**\n * AI anomaly panel (site detail)\n * @module ecoimpacthub/components/ai\n *\n * Runs `ecoAiDetectAnomalies` for one parameter at a site over a chosen\n * period and lists spikes, drops and flatlines with the readings each one\n * cites. INSUFFICIENT_DATA renders an explicit \"Not enough data\" state and\n * never a trend. Detection is metered, so it runs only on the user's click.\n */\n\nimport { useMemo, useState, type FC } from 'react';\nimport { Activity, ScanSearch } from 'lucide-react';\nimport {\n Button,\n GlassCard,\n Label,\n Select,\n SelectContent,\n SelectItem,\n SelectTrigger,\n SelectValue,\n} from '@burdenoff/fe-libs/ui';\n\nimport { StatusBadge } from '../StatusBadge';\nimport { useEcoAiDetectAnomalies, useEcoAiErrorReporter, useEcoMeasurements } from '../../hooks';\nimport { useTr } from '../../hooks/useTr';\nimport {\n ANOMALY_PERIOD_PRESETS,\n groupAnomalies,\n periodRange,\n resolveCitedReadings,\n type AnomalyPeriodPreset,\n type EcoAiErrorInfo,\n} from '../../utils/aiMonitoring';\nimport { formatDateTime, formatNumber } from '../../utils/formatters';\nimport type { EcoAnomalyDetectionResult, EcoSensor } from '../../types';\nimport { AiCreditHint, AiErrorNotice, CitedReadingsList } from './AiShared';\n\ninterface AiAnomalyPanelProps {\n siteId: string;\n /** Parameters the site has readings or thresholds for. */\n parameters: string[];\n sensors?: EcoSensor[];\n}\n\nconst PERIOD_LABELS: Record<AnomalyPeriodPreset, [string, string]> = {\n '7d': ['ecoAi.anomalies.period7d', 'Last 7 days'],\n '30d': ['ecoAi.anomalies.period30d', 'Last 30 days'],\n '90d': ['ecoAi.anomalies.period90d', 'Last 90 days'],\n '365d': ['ecoAi.anomalies.period365d', 'Last 12 months'],\n};\n\nconst KIND_LABELS: Record<string, [string, string]> = {\n SPIKE: ['ecoAi.anomalies.kindSpike', 'Spikes'],\n DROP: ['ecoAi.anomalies.kindDrop', 'Drops'],\n FLATLINE: ['ecoAi.anomalies.kindFlatline', 'Flatlines'],\n};\n\nexport const AiAnomalyPanel: FC<AiAnomalyPanelProps> = ({ siteId, parameters, sensors = [] }) => {\n const tr = useTr();\n const [parameter, setParameter] = useState<string>('');\n const [period, setPeriod] = useState<AnomalyPeriodPreset>('30d');\n const activeParameter = parameter || parameters[0] || '';\n\n const detect = useEcoAiDetectAnomalies();\n const reportAiError = useEcoAiErrorReporter();\n const [result, setResult] = useState<EcoAnomalyDetectionResult | null>(null);\n const [aiError, setAiError] = useState<EcoAiErrorInfo | null>(null);\n const [expanded, setExpanded] = useState<string | null>(null);\n\n // Readings for the analysed parameter resolve cited ids to values.\n const historyQuery = useEcoMeasurements({\n siteId,\n parameter: result?.parameter ?? activeParameter,\n pagination: { limit: 100, offset: 0 },\n });\n const readings = useMemo(() => historyQuery.data?.items ?? [], [historyQuery.data]);\n const groups = useMemo(() => (result ? groupAnomalies(result.anomalies) : []), [result]);\n const sensorName = (id?: string | null) => (id && sensors.find((s) => s.id === id)?.name) || null;\n\n const run = async () => {\n if (!activeParameter) return;\n setAiError(null);\n setExpanded(null);\n try {\n const out = await detect.mutateAsync({\n siteId,\n parameter: activeParameter,\n ...periodRange(period),\n });\n setResult(out);\n } catch (err) {\n setResult(null);\n setAiError(\n reportAiError(\n err,\n 'EcoAiDetectAnomalies',\n tr('ecoAi.anomalies.failed', 'Anomaly detection failed.')\n )\n );\n }\n };\n\n return (\n <div data-testid=\"eco-ai-anomalies\">\n <GlassCard treatment=\"plain\" className=\"p-5\">\n {parameters.length === 0 ? (\n <p className=\"text-sm text-text-secondary\">\n {tr(\n 'ecoAi.anomalies.noParameters',\n 'Record measurements at this site to analyse them for anomalies.'\n )}\n </p>\n ) : (\n <>\n <div className=\"flex flex-wrap items-end gap-3\">\n <div className=\"grid gap-1.5\">\n <Label htmlFor=\"ecoAiAnomalyParam\">\n {tr('ecoAi.anomalies.parameter', 'Parameter')}\n </Label>\n <Select\n value={activeParameter}\n onValueChange={(v) => {\n setParameter(v);\n setResult(null);\n setAiError(null);\n }}\n >\n <SelectTrigger id=\"ecoAiAnomalyParam\" className=\"w-48\">\n <SelectValue />\n </SelectTrigger>\n <SelectContent>\n {parameters.map((p) => (\n <SelectItem key={p} value={p}>\n {p}\n </SelectItem>\n ))}\n </SelectContent>\n </Select>\n </div>\n <div className=\"grid gap-1.5\">\n <Label htmlFor=\"ecoAiAnomalyPeriod\">{tr('ecoAi.anomalies.period', 'Period')}</Label>\n <Select\n value={period}\n onValueChange={(v) => {\n setPeriod(v as AnomalyPeriodPreset);\n setResult(null);\n setAiError(null);\n }}\n >\n <SelectTrigger id=\"ecoAiAnomalyPeriod\" className=\"w-44\">\n <SelectValue />\n </SelectTrigger>\n <SelectContent>\n {ANOMALY_PERIOD_PRESETS.map((p) => (\n <SelectItem key={p} value={p}>\n {tr(PERIOD_LABELS[p][0], PERIOD_LABELS[p][1])}\n </SelectItem>\n ))}\n </SelectContent>\n </Select>\n </div>\n <Button\n onClick={run}\n disabled={!activeParameter || detect.isPending}\n data-testid=\"eco-ai-anomalies-run\"\n >\n <ScanSearch className=\"mr-1.5 h-4 w-4\" />\n {detect.isPending\n ? tr('ecoAi.anomalies.running', 'Analysing…')\n : tr('ecoAi.anomalies.run', 'Detect anomalies')}\n </Button>\n </div>\n <AiCreditHint className=\"mt-2\" />\n </>\n )}\n\n {aiError && <AiErrorNotice error={aiError} className=\"mt-4\" />}\n\n {result && result.status === 'INSUFFICIENT_DATA' && (\n <div\n className=\"mt-4 rounded-lg bg-bg-sunken/60 p-4\"\n data-testid=\"eco-ai-anomalies-insufficient\"\n >\n <p className=\"text-sm font-semibold text-text-primary\">\n {tr('ecoAi.anomalies.notEnoughData', 'Not enough data')}\n </p>\n <p className=\"mt-1 text-sm text-text-secondary\">\n {tr(\n 'ecoAi.anomalies.notEnoughDataBody',\n 'Only {{count}} reading(s) of {{parameter}} in this period, too few to judge what is normal. No trend is shown.',\n { count: result.readingsAnalysed, parameter: result.parameter }\n )}\n </p>\n {result.notes.length > 0 && (\n <ul className=\"mt-2 list-disc space-y-0.5 pl-5 text-xs text-text-tertiary\">\n {result.notes.map((n, i) => (\n <li key={`${i}:${n}`}>{n}</li>\n ))}\n </ul>\n )}\n </div>\n )}\n\n {result && result.status === 'OK' && (\n <div className=\"mt-4 grid gap-4\" data-testid=\"eco-ai-anomalies-result\">\n <p className=\"text-xs text-text-tertiary\">\n {tr(\n 'ecoAi.anomalies.summary',\n '{{count}} reading(s) analysed · baseline median {{median}} {{unit}}',\n {\n count: result.readingsAnalysed,\n median: formatNumber(result.baseline?.median ?? null),\n unit: result.unit ?? '',\n }\n )}\n {result.truncated &&\n ` · ${tr('ecoAi.anomalies.truncated', 'the series was truncated to the most recent readings')}`}\n </p>\n\n {groups.length === 0 ? (\n <p\n className=\"flex items-center gap-2 text-sm text-text-secondary\"\n data-testid=\"eco-ai-anomalies-none\"\n >\n <Activity className=\"h-4 w-4 text-status-success-text\" />\n {tr('ecoAi.anomalies.none', 'No spikes, drops or flatlines in this period.')}\n </p>\n ) : (\n groups.map((group) => (\n <div key={group.kind}>\n <h3 className=\"mb-2 flex items-center gap-2 text-sm font-semibold text-text-primary\">\n <StatusBadge status={group.kind} />\n {tr(\n KIND_LABELS[group.kind]?.[0] ?? group.kind,\n KIND_LABELS[group.kind]?.[1] ?? group.kind\n )}\n <span className=\"text-text-tertiary\">({group.items.length})</span>\n </h3>\n <ul className=\"divide-y divide-border-seam/60 rounded-lg border border-border-seam\">\n {group.items.map((a) => {\n const open = expanded === a.measurementId;\n return (\n <li key={a.measurementId} data-testid=\"eco-ai-anomaly\">\n <button\n type=\"button\"\n className=\"flex w-full items-center justify-between gap-3 px-3 py-2 text-left text-sm hover:bg-bg-sunken/60\"\n aria-expanded={open}\n onClick={() => setExpanded(open ? null : a.measurementId)}\n >\n <span className=\"tabular-nums text-text-primary\">\n {formatNumber(a.value)} {result.unit ?? ''}\n {sensorName(a.sensorId) && (\n <span className=\"ml-2 text-xs text-text-tertiary\">\n {sensorName(a.sensorId)}\n </span>\n )}\n </span>\n <span className=\"flex items-center gap-3 text-xs text-text-tertiary\">\n <span>\n {a.kind === 'FLATLINE'\n ? tr('ecoAi.anomalies.runLength', 'run of {{n}}', {\n n: formatNumber(a.score, { maximumFractionDigits: 1 }),\n })\n : tr('ecoAi.anomalies.score', 'z {{z}}', {\n z: formatNumber(a.score, { maximumFractionDigits: 1 }),\n })}\n </span>\n <span>{formatDateTime(a.recordedAt)}</span>\n <span>\n {tr('ecoAi.anomalies.citedCount', '{{n}} cited', {\n n: a.evidenceMeasurementIds.length,\n })}\n </span>\n </span>\n </button>\n {open && (\n <div className=\"px-3 pb-3\">\n <CitedReadingsList\n readings={resolveCitedReadings(a.evidenceMeasurementIds, readings)}\n focusId={a.measurementId}\n />\n </div>\n )}\n </li>\n );\n })}\n </ul>\n </div>\n ))\n )}\n\n {result.notes.length > 0 && (\n <ul className=\"list-disc space-y-0.5 pl-5 text-xs text-text-tertiary\">\n {result.notes.map((n, i) => (\n <li key={`${i}:${n}`}>{n}</li>\n ))}\n </ul>\n )}\n </div>\n )}\n </GlassCard>\n </div>\n );\n};\n"],"mappings":"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"}
|
|
1
|
+
{"version":3,"file":"AiAnomalyPanel.js","names":[],"sources":["../../../../src/ecoimpacthub/components/ai/AiAnomalyPanel.tsx"],"sourcesContent":["/**\n * AI anomaly panel (site detail)\n * @module ecoimpacthub/components/ai\n *\n * Runs `ecoAiDetectAnomalies` for one parameter at a site over a chosen\n * period and lists spikes, drops and flatlines with the readings each one\n * cites. INSUFFICIENT_DATA renders an explicit \"Not enough data\" state and\n * never a trend. Detection is metered, so it runs only on the user's click.\n */\n\nimport { useMemo, useState, type FC } from 'react';\nimport { Activity, ScanSearch } from 'lucide-react';\nimport {\n Button,\n GlassCard,\n Label,\n Select,\n SelectContent,\n SelectItem,\n SelectTrigger,\n SelectValue,\n} from '@burdenoff/fe-libs/ui';\n\nimport { StatusBadge } from '../StatusBadge';\nimport { useEcoAiDetectAnomalies, useEcoAiErrorReporter, useEcoMeasurements } from '../../hooks';\nimport { useTr } from '../../hooks/useTr';\nimport {\n ANOMALY_PERIOD_PRESETS,\n groupAnomalies,\n periodRange,\n resolveCitedReadings,\n userFacingAiNotes,\n type AnomalyPeriodPreset,\n type EcoAiErrorInfo,\n} from '../../utils/aiMonitoring';\nimport { formatDateTime, formatNumber } from '../../utils/formatters';\nimport type { EcoAnomalyDetectionResult, EcoSensor } from '../../types';\nimport { AiCreditHint, AiErrorNotice, CitedReadingsList } from './AiShared';\n\ninterface AiAnomalyPanelProps {\n siteId: string;\n /** Parameters the site has readings or thresholds for. */\n parameters: string[];\n sensors?: EcoSensor[];\n}\n\nconst PERIOD_LABELS: Record<AnomalyPeriodPreset, [string, string]> = {\n '7d': ['ecoAi.anomalies.period7d', 'Last 7 days'],\n '30d': ['ecoAi.anomalies.period30d', 'Last 30 days'],\n '90d': ['ecoAi.anomalies.period90d', 'Last 90 days'],\n '365d': ['ecoAi.anomalies.period365d', 'Last 12 months'],\n};\n\nconst KIND_LABELS: Record<string, [string, string]> = {\n SPIKE: ['ecoAi.anomalies.kindSpike', 'Spikes'],\n DROP: ['ecoAi.anomalies.kindDrop', 'Drops'],\n FLATLINE: ['ecoAi.anomalies.kindFlatline', 'Flatlines'],\n};\n\nexport const AiAnomalyPanel: FC<AiAnomalyPanelProps> = ({ siteId, parameters, sensors = [] }) => {\n const tr = useTr();\n const [parameter, setParameter] = useState<string>('');\n const [period, setPeriod] = useState<AnomalyPeriodPreset>('30d');\n const activeParameter = parameter || parameters[0] || '';\n\n const detect = useEcoAiDetectAnomalies();\n const reportAiError = useEcoAiErrorReporter();\n const [result, setResult] = useState<EcoAnomalyDetectionResult | null>(null);\n const [aiError, setAiError] = useState<EcoAiErrorInfo | null>(null);\n const [expanded, setExpanded] = useState<string | null>(null);\n\n // Readings for the analysed parameter resolve cited ids to values.\n const historyQuery = useEcoMeasurements({\n siteId,\n parameter: result?.parameter ?? activeParameter,\n pagination: { limit: 100, offset: 0 },\n });\n const readings = useMemo(() => historyQuery.data?.items ?? [], [historyQuery.data]);\n const groups = useMemo(() => (result ? groupAnomalies(result.anomalies) : []), [result]);\n const resultNotes = useMemo(() => userFacingAiNotes(result?.notes), [result]);\n const sensorName = (id?: string | null) => (id && sensors.find((s) => s.id === id)?.name) || null;\n\n const run = async () => {\n if (!activeParameter) return;\n setAiError(null);\n setExpanded(null);\n try {\n const out = await detect.mutateAsync({\n siteId,\n parameter: activeParameter,\n ...periodRange(period),\n });\n setResult(out);\n } catch (err) {\n setResult(null);\n setAiError(\n reportAiError(\n err,\n 'EcoAiDetectAnomalies',\n tr('ecoAi.anomalies.failed', 'Anomaly detection failed.')\n )\n );\n }\n };\n\n return (\n <div data-testid=\"eco-ai-anomalies\">\n <GlassCard treatment=\"plain\" className=\"p-5\">\n {parameters.length === 0 ? (\n <p className=\"text-sm text-text-secondary\">\n {tr(\n 'ecoAi.anomalies.noParameters',\n 'Record measurements at this site to analyse them for anomalies.'\n )}\n </p>\n ) : (\n <>\n <div className=\"flex flex-wrap items-end gap-3\">\n <div className=\"grid gap-1.5\">\n <Label htmlFor=\"ecoAiAnomalyParam\">\n {tr('ecoAi.anomalies.parameter', 'Parameter')}\n </Label>\n <Select\n value={activeParameter}\n onValueChange={(v) => {\n setParameter(v);\n setResult(null);\n setAiError(null);\n }}\n >\n <SelectTrigger id=\"ecoAiAnomalyParam\" className=\"w-48\">\n <SelectValue />\n </SelectTrigger>\n <SelectContent>\n {parameters.map((p) => (\n <SelectItem key={p} value={p}>\n {p}\n </SelectItem>\n ))}\n </SelectContent>\n </Select>\n </div>\n <div className=\"grid gap-1.5\">\n <Label htmlFor=\"ecoAiAnomalyPeriod\">{tr('ecoAi.anomalies.period', 'Period')}</Label>\n <Select\n value={period}\n onValueChange={(v) => {\n setPeriod(v as AnomalyPeriodPreset);\n setResult(null);\n setAiError(null);\n }}\n >\n <SelectTrigger id=\"ecoAiAnomalyPeriod\" className=\"w-44\">\n <SelectValue />\n </SelectTrigger>\n <SelectContent>\n {ANOMALY_PERIOD_PRESETS.map((p) => (\n <SelectItem key={p} value={p}>\n {tr(PERIOD_LABELS[p][0], PERIOD_LABELS[p][1])}\n </SelectItem>\n ))}\n </SelectContent>\n </Select>\n </div>\n <Button\n onClick={run}\n disabled={!activeParameter || detect.isPending}\n data-testid=\"eco-ai-anomalies-run\"\n >\n <ScanSearch className=\"mr-1.5 h-4 w-4\" />\n {detect.isPending\n ? tr('ecoAi.anomalies.running', 'Analysing…')\n : tr('ecoAi.anomalies.run', 'Detect anomalies')}\n </Button>\n </div>\n <AiCreditHint className=\"mt-2\" />\n </>\n )}\n\n {aiError && <AiErrorNotice error={aiError} className=\"mt-4\" />}\n\n {result && result.status === 'INSUFFICIENT_DATA' && (\n <div\n className=\"mt-4 rounded-lg bg-bg-sunken/60 p-4\"\n data-testid=\"eco-ai-anomalies-insufficient\"\n >\n <p className=\"text-sm font-semibold text-text-primary\">\n {tr('ecoAi.anomalies.notEnoughData', 'Not enough data')}\n </p>\n <p className=\"mt-1 text-sm text-text-secondary\">\n {tr(\n 'ecoAi.anomalies.notEnoughDataBody',\n 'Only {{count}} reading(s) of {{parameter}} in this period, too few to judge what is normal. No trend is shown.',\n { count: result.readingsAnalysed, parameter: result.parameter }\n )}\n </p>\n {resultNotes.length > 0 && (\n <ul className=\"mt-2 list-disc space-y-0.5 pl-5 text-xs text-text-tertiary\">\n {resultNotes.map((n, i) => (\n <li key={`${i}:${n}`}>{n}</li>\n ))}\n </ul>\n )}\n </div>\n )}\n\n {result && result.status === 'OK' && (\n <div className=\"mt-4 grid gap-4\" data-testid=\"eco-ai-anomalies-result\">\n <p className=\"text-xs text-text-tertiary\">\n {tr(\n 'ecoAi.anomalies.summary',\n '{{count}} reading(s) analysed · baseline median {{median}} {{unit}}',\n {\n count: result.readingsAnalysed,\n median: formatNumber(result.baseline?.median ?? null),\n unit: result.unit ?? '',\n }\n )}\n {result.truncated &&\n ` · ${tr('ecoAi.anomalies.truncated', 'the series was truncated to the most recent readings')}`}\n </p>\n\n {groups.length === 0 ? (\n <p\n className=\"flex items-center gap-2 text-sm text-text-secondary\"\n data-testid=\"eco-ai-anomalies-none\"\n >\n <Activity className=\"h-4 w-4 text-status-success-text\" />\n {tr('ecoAi.anomalies.none', 'No spikes, drops or flatlines in this period.')}\n </p>\n ) : (\n groups.map((group) => (\n <div key={group.kind}>\n <h3 className=\"mb-2 flex items-center gap-2 text-sm font-semibold text-text-primary\">\n <StatusBadge status={group.kind} />\n {tr(\n KIND_LABELS[group.kind]?.[0] ?? group.kind,\n KIND_LABELS[group.kind]?.[1] ?? group.kind\n )}\n <span className=\"text-text-tertiary\">({group.items.length})</span>\n </h3>\n <ul className=\"divide-y divide-border-seam/60 rounded-lg border border-border-seam\">\n {group.items.map((a) => {\n const open = expanded === a.measurementId;\n return (\n <li key={a.measurementId} data-testid=\"eco-ai-anomaly\">\n <button\n type=\"button\"\n className=\"flex w-full items-center justify-between gap-3 px-3 py-2 text-left text-sm hover:bg-bg-sunken/60\"\n aria-expanded={open}\n onClick={() => setExpanded(open ? null : a.measurementId)}\n >\n <span className=\"tabular-nums text-text-primary\">\n {formatNumber(a.value)} {result.unit ?? ''}\n {sensorName(a.sensorId) && (\n <span className=\"ml-2 text-xs text-text-tertiary\">\n {sensorName(a.sensorId)}\n </span>\n )}\n </span>\n <span className=\"flex items-center gap-3 text-xs text-text-tertiary\">\n <span>\n {a.kind === 'FLATLINE'\n ? tr('ecoAi.anomalies.runLength', 'run of {{n}}', {\n n: formatNumber(a.score, { maximumFractionDigits: 1 }),\n })\n : tr('ecoAi.anomalies.score', 'z {{z}}', {\n z: formatNumber(a.score, { maximumFractionDigits: 1 }),\n })}\n </span>\n <span>{formatDateTime(a.recordedAt)}</span>\n <span>\n {tr('ecoAi.anomalies.citedCount', '{{n}} cited', {\n n: a.evidenceMeasurementIds.length,\n })}\n </span>\n </span>\n </button>\n {open && (\n <div className=\"px-3 pb-3\">\n <CitedReadingsList\n readings={resolveCitedReadings(a.evidenceMeasurementIds, readings)}\n focusId={a.measurementId}\n />\n </div>\n )}\n </li>\n );\n })}\n </ul>\n </div>\n ))\n )}\n\n {resultNotes.length > 0 && (\n <ul className=\"list-disc space-y-0.5 pl-5 text-xs text-text-tertiary\">\n {resultNotes.map((n, i) => (\n <li key={`${i}:${n}`}>{n}</li>\n ))}\n </ul>\n )}\n </div>\n )}\n </GlassCard>\n </div>\n );\n};\n"],"mappings":";;;;;;;;;;;;;AA8CA,IAAM,IAA+D;CACnE,MAAM,CAAC,4BAA4B,aAAa;CAChD,OAAO,CAAC,6BAA6B,cAAc;CACnD,OAAO,CAAC,6BAA6B,cAAc;CACnD,QAAQ,CAAC,8BAA8B,gBAAgB;AACzD,GAEM,IAAgD;CACpD,OAAO,CAAC,6BAA6B,QAAQ;CAC7C,MAAM,CAAC,4BAA4B,OAAO;CAC1C,UAAU,CAAC,gCAAgC,WAAW;AACxD,GAEa,KAA2C,EAAE,WAAQ,eAAY,aAAU,CAAC,QAAQ;CAC/F,IAAM,IAAK,EAAM,GACX,CAAC,GAAW,KAAgB,EAAiB,EAAE,GAC/C,CAAC,GAAQ,KAAa,EAA8B,KAAK,GACzD,IAAkB,KAAa,EAAW,MAAM,IAEhD,IAAS,EAAwB,GACjC,IAAgB,EAAsB,GACtC,CAAC,GAAQ,KAAa,EAA2C,IAAI,GACrE,CAAC,GAAS,KAAc,EAAgC,IAAI,GAC5D,CAAC,GAAU,KAAe,EAAwB,IAAI,GAGtD,IAAe,EAAmB;EACtC;EACA,WAAW,GAAQ,aAAa;EAChC,YAAY;GAAE,OAAO;GAAK,QAAQ;EAAE;CACtC,CAAC,GACK,IAAW,QAAc,EAAa,MAAM,SAAS,CAAC,GAAG,CAAC,EAAa,IAAI,CAAC,GAC5E,IAAS,QAAe,IAAS,EAAe,EAAO,SAAS,IAAI,CAAC,GAAI,CAAC,CAAM,CAAC,GACjF,IAAc,QAAc,EAAkB,GAAQ,KAAK,GAAG,CAAC,CAAM,CAAC,GACtE,KAAc,MAAwB,KAAM,EAAQ,MAAM,MAAM,EAAE,OAAO,CAAE,GAAG,QAAS;CAyB7F,OACE,kBAAC,OAAD;EAAK,eAAY;YACf,kBAAC,GAAD;GAAW,WAAU;GAAQ,WAAU;aAAvC;IACG,EAAW,WAAW,IACrB,kBAAC,KAAD;KAAG,WAAU;eACV,EACC,gCACA,iEACF;IACC,CAAA,IAEH,kBAAA,GAAA,EAAA,UAAA,CACE,kBAAC,OAAD;KAAK,WAAU;eAAf;MACE,kBAAC,OAAD;OAAK,WAAU;iBAAf,CACE,kBAAC,GAAD;QAAO,SAAQ;kBACZ,EAAG,6BAA6B,WAAW;OACvC,CAAA,GACP,kBAAC,GAAD;QACE,OAAO;QACP,gBAAgB,MAAM;SAGpB,AAFA,EAAa,CAAC,GACd,EAAU,IAAI,GACd,EAAW,IAAI;QACjB;kBANF,CAQE,kBAAC,GAAD;SAAe,IAAG;SAAoB,WAAU;mBAC9C,kBAAC,GAAD,CAAc,CAAA;QACD,CAAA,GACf,kBAAC,GAAD,EAAA,UACG,EAAW,KAAK,MACf,kBAAC,GAAD;SAAoB,OAAO;mBACxB;QACS,GAFK,CAEL,CACb,EACY,CAAA,CACT;SACL;;MACL,kBAAC,OAAD;OAAK,WAAU;iBAAf,CACE,kBAAC,GAAD;QAAO,SAAQ;kBAAsB,EAAG,0BAA0B,QAAQ;OAAS,CAAA,GACnF,kBAAC,GAAD;QACE,OAAO;QACP,gBAAgB,MAAM;SAGpB,AAFA,EAAU,CAAwB,GAClC,EAAU,IAAI,GACd,EAAW,IAAI;QACjB;kBANF,CAQE,kBAAC,GAAD;SAAe,IAAG;SAAqB,WAAU;mBAC/C,kBAAC,GAAD,CAAc,CAAA;QACD,CAAA,GACf,kBAAC,GAAD,EAAA,UACG,EAAuB,KAAK,MAC3B,kBAAC,GAAD;SAAoB,OAAO;mBACxB,EAAG,EAAc,GAAG,IAAI,EAAc,GAAG,EAAE;QAClC,GAFK,CAEL,CACb,EACY,CAAA,CACT;SACL;;MACL,kBAAC,GAAD;OACE,SAAS,YAnFC;QACjB,OAEL;SADA,EAAW,IAAI,GACf,EAAY,IAAI;SAChB,IAAI;UAMF,EAAU,MALQ,EAAO,YAAY;WACnC;WACA,WAAW;WACX,GAAG,EAAY,CAAM;UACvB,CAAC,CACY;SACf,SAAS,GAAK;UAEZ,AADA,EAAU,IAAI,GACd,EACE,EACE,GACA,wBACA,EAAG,0BAA0B,2BAA2B,CAC1D,CACF;SACF;QAjBgB;OAkBlB;OA+Dc,UAAU,CAAC,KAAmB,EAAO;OACrC,eAAY;iBAHd,CAKE,kBAAC,GAAD,EAAY,WAAU,iBAAkB,CAAA,GACvC,EAAO,YACJ,EAAG,2BAA2B,YAAY,IAC1C,EAAG,uBAAuB,kBAAkB,CAC1C;;KACL;QACL,kBAAC,GAAD,EAAc,WAAU,OAAQ,CAAA,CAChC,EAAA,CAAA;IAGH,KAAW,kBAAC,GAAD;KAAe,OAAO;KAAS,WAAU;IAAQ,CAAA;IAE5D,KAAU,EAAO,WAAW,uBAC3B,kBAAC,OAAD;KACE,WAAU;KACV,eAAY;eAFd;MAIE,kBAAC,KAAD;OAAG,WAAU;iBACV,EAAG,iCAAiC,iBAAiB;MACrD,CAAA;MACH,kBAAC,KAAD;OAAG,WAAU;iBACV,EACC,qCACA,kHACA;QAAE,OAAO,EAAO;QAAkB,WAAW,EAAO;OAAU,CAChE;MACC,CAAA;MACF,EAAY,SAAS,KACpB,kBAAC,MAAD;OAAI,WAAU;iBACX,EAAY,KAAK,GAAG,MACnB,kBAAC,MAAD,EAAA,UAAuB,EAAM,GAApB,GAAG,EAAE,GAAG,GAAY,CAC9B;MACC,CAAA;KAEH;;IAGN,KAAU,EAAO,WAAW,QAC3B,kBAAC,OAAD;KAAK,WAAU;KAAkB,eAAY;eAA7C;MACE,kBAAC,KAAD;OAAG,WAAU;iBAAb,CACG,EACC,2BACA,uEACA;QACE,OAAO,EAAO;QACd,QAAQ,EAAa,EAAO,UAAU,UAAU,IAAI;QACpD,MAAM,EAAO,QAAQ;OACvB,CACF,GACC,EAAO,aACN,MAAM,EAAG,6BAA6B,sDAAsD,GAC7F;;MAEF,EAAO,WAAW,IACjB,kBAAC,KAAD;OACE,WAAU;OACV,eAAY;iBAFd,CAIE,kBAAC,GAAD,EAAU,WAAU,mCAAoC,CAAA,GACvD,EAAG,wBAAwB,+CAA+C,CAC1E;WAEH,EAAO,KAAK,MACV,kBAAC,OAAD,EAAA,UAAA,CACE,kBAAC,MAAD;OAAI,WAAU;iBAAd;QACE,kBAAC,GAAD,EAAa,QAAQ,EAAM,KAAO,CAAA;QACjC,EACC,EAAY,EAAM,QAAQ,MAAM,EAAM,MACtC,EAAY,EAAM,QAAQ,MAAM,EAAM,IACxC;QACA,kBAAC,QAAD;SAAM,WAAU;mBAAhB;UAAqC;UAAE,EAAM,MAAM;UAAO;SAAO;;OAC/D;UACJ,kBAAC,MAAD;OAAI,WAAU;iBACX,EAAM,MAAM,KAAK,MAAM;QACtB,IAAM,IAAO,MAAa,EAAE;QAC5B,OACE,kBAAC,MAAD;SAA0B,eAAY;mBAAtC,CACE,kBAAC,UAAD;UACE,MAAK;UACL,WAAU;UACV,iBAAe;UACf,eAAe,EAAY,IAAO,OAAO,EAAE,aAAa;oBAJ1D,CAME,kBAAC,QAAD;WAAM,WAAU;qBAAhB;YACG,EAAa,EAAE,KAAK;YAAE;YAAE,EAAO,QAAQ;YACvC,EAAW,EAAE,QAAQ,KACpB,kBAAC,QAAD;aAAM,WAAU;uBACb,EAAW,EAAE,QAAQ;YAClB,CAAA;WAEJ;cACN,kBAAC,QAAD;WAAM,WAAU;qBAAhB;YACE,kBAAC,QAAD,EAAA,UACG,EAAE,SAAS,aACR,EAAG,6BAA6B,gBAAgB,EAC9C,GAAG,EAAa,EAAE,OAAO,EAAE,uBAAuB,EAAE,CAAC,EACvD,CAAC,IACD,EAAG,yBAAyB,WAAW,EACrC,GAAG,EAAa,EAAE,OAAO,EAAE,uBAAuB,EAAE,CAAC,EACvD,CAAC,EACD,CAAA;YACN,kBAAC,QAAD,EAAA,UAAO,EAAe,EAAE,UAAU,EAAQ,CAAA;YAC1C,kBAAC,QAAD,EAAA,UACG,EAAG,8BAA8B,eAAe,EAC/C,GAAG,EAAE,uBAAuB,OAC9B,CAAC,EACG,CAAA;WACF;YACA;aACP,KACC,kBAAC,OAAD;UAAK,WAAU;oBACb,kBAAC,GAAD;WACE,UAAU,EAAqB,EAAE,wBAAwB,CAAQ;WACjE,SAAS,EAAE;UACZ,CAAA;SACE,CAAA,CAEL;WAzCK,EAAE,aAyCP;OAER,CAAC;MACC,CAAA,CACD,EAAA,GA1DK,EAAM,IA0DX,CACN;MAGF,EAAY,SAAS,KACpB,kBAAC,MAAD;OAAI,WAAU;iBACX,EAAY,KAAK,GAAG,MACnB,kBAAC,MAAD,EAAA,UAAuB,EAAM,GAApB,GAAG,EAAE,GAAG,GAAY,CAC9B;MACC,CAAA;KAEH;;GAEE;;CACR,CAAA;AAET"}
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"AiThresholdSuggest.d.ts","sourceRoot":"","sources":["../../../../src/ecoimpacthub/components/ai/AiThresholdSuggest.tsx"],"names":[],"mappings":"AAAA;;;;;;;;GAQG;AAEH,OAAO,EAAY,KAAK,EAAE,EAAE,MAAM,OAAO,CAAC;AAM1C,OAAO,
|
|
1
|
+
{"version":3,"file":"AiThresholdSuggest.d.ts","sourceRoot":"","sources":["../../../../src/ecoimpacthub/components/ai/AiThresholdSuggest.tsx"],"names":[],"mappings":"AAAA;;;;;;;;GAQG;AAEH,OAAO,EAAY,KAAK,EAAE,EAAE,MAAM,OAAO,CAAC;AAM1C,OAAO,EAML,KAAK,gBAAgB,EACtB,MAAM,0BAA0B,CAAC;AAIlC,UAAU,uBAAuB;IAC/B,MAAM,EAAE,MAAM,CAAC;IACf,SAAS,EAAE,MAAM,CAAC;IAClB,SAAS,EAAE,CAAC,OAAO,EAAE,gBAAgB,KAAK,IAAI,CAAC;CAChD;AAED,eAAO,MAAM,kBAAkB,EAAE,EAAE,CAAC,uBAAuB,CAsH1D,CAAC"}
|
|
@@ -1,93 +1,93 @@
|
|
|
1
1
|
import { useTr as e } from "../../hooks/useTr.js";
|
|
2
|
-
import { formatBound as t, formatBreachRate as n, proposalToPrefill as r } from "../../utils/aiMonitoring.js";
|
|
3
|
-
import { useEcoAiErrorReporter as
|
|
2
|
+
import { formatBound as t, formatBreachRate as n, proposalToPrefill as r, userFacingAiNotes as i } from "../../utils/aiMonitoring.js";
|
|
3
|
+
import { useEcoAiErrorReporter as a, useEcoAiProposeThreshold as o } from "../../hooks/useEcoAiMonitoring.js";
|
|
4
4
|
import "../../hooks/index.js";
|
|
5
|
-
import { AiCreditHint as
|
|
6
|
-
import { useState as
|
|
7
|
-
import { Wand2 as
|
|
8
|
-
import { jsx as
|
|
9
|
-
import { Button as
|
|
5
|
+
import { AiCreditHint as s, AiErrorNotice as c } from "./AiShared.js";
|
|
6
|
+
import { useState as l } from "react";
|
|
7
|
+
import { Wand2 as u } from "lucide-react";
|
|
8
|
+
import { jsx as d, jsxs as f } from "react/jsx-runtime";
|
|
9
|
+
import { Button as p } from "@burdenoff/fe-libs/ui";
|
|
10
10
|
//#region src/ecoimpacthub/components/ai/AiThresholdSuggest.tsx
|
|
11
|
-
var
|
|
12
|
-
let
|
|
13
|
-
if (
|
|
14
|
-
|
|
11
|
+
var m = ({ siteId: m, parameter: h, onSuggest: g }) => {
|
|
12
|
+
let _ = e(), v = o(), y = a(), [b, x] = l(null), [S, C] = l(null), w = h.trim(), T = !!(m && w), E = async () => {
|
|
13
|
+
if (T) {
|
|
14
|
+
C(null), x(null);
|
|
15
15
|
try {
|
|
16
|
-
let e = await
|
|
17
|
-
siteId:
|
|
18
|
-
parameter:
|
|
16
|
+
let e = await v.mutateAsync({
|
|
17
|
+
siteId: m,
|
|
18
|
+
parameter: w
|
|
19
19
|
});
|
|
20
|
-
|
|
20
|
+
x(e);
|
|
21
21
|
let t = r(e);
|
|
22
|
-
t &&
|
|
22
|
+
t && g(t);
|
|
23
23
|
} catch (e) {
|
|
24
|
-
|
|
24
|
+
C(y(e, "EcoAiProposeThreshold", _("ecoAi.suggest.failed", "Could not suggest a threshold.")));
|
|
25
25
|
}
|
|
26
26
|
}
|
|
27
|
-
},
|
|
28
|
-
return /* @__PURE__ */
|
|
27
|
+
}, D = b ? r(b) : null, O = i(b?.notes);
|
|
28
|
+
return /* @__PURE__ */ f("div", {
|
|
29
29
|
className: "grid gap-2 rounded-lg border border-dashed border-border-seam p-3",
|
|
30
30
|
"data-testid": "eco-ai-suggest",
|
|
31
31
|
children: [
|
|
32
|
-
/* @__PURE__ */
|
|
32
|
+
/* @__PURE__ */ f("div", {
|
|
33
33
|
className: "flex flex-wrap items-center justify-between gap-2",
|
|
34
|
-
children: [/* @__PURE__ */
|
|
34
|
+
children: [/* @__PURE__ */ f(p, {
|
|
35
35
|
type: "button",
|
|
36
36
|
size: "sm",
|
|
37
37
|
variant: "outline",
|
|
38
|
-
onClick:
|
|
39
|
-
disabled: !
|
|
38
|
+
onClick: E,
|
|
39
|
+
disabled: !T || v.isPending,
|
|
40
40
|
"data-testid": "eco-ai-suggest-run",
|
|
41
|
-
children: [/* @__PURE__ */ u
|
|
42
|
-
}), /* @__PURE__ */
|
|
41
|
+
children: [/* @__PURE__ */ d(u, { className: "mr-1.5 h-4 w-4" }), v.isPending ? _("ecoAi.suggest.running", "Suggesting…") : _("ecoAi.suggest.run", "Suggest from history")]
|
|
42
|
+
}), /* @__PURE__ */ d(s, {})]
|
|
43
43
|
}),
|
|
44
|
-
!
|
|
44
|
+
!T && /* @__PURE__ */ d("p", {
|
|
45
45
|
className: "text-xs text-text-tertiary",
|
|
46
|
-
children:
|
|
46
|
+
children: _("ecoAi.suggest.needInputs", "Pick a site and parameter to suggest a range from its readings.")
|
|
47
47
|
}),
|
|
48
|
-
|
|
49
|
-
|
|
48
|
+
S && /* @__PURE__ */ d(c, { error: S }),
|
|
49
|
+
b && !D && /* @__PURE__ */ f("div", {
|
|
50
50
|
className: "text-sm text-text-secondary",
|
|
51
51
|
"data-testid": "eco-ai-suggest-insufficient",
|
|
52
|
-
children: [/* @__PURE__ */
|
|
52
|
+
children: [/* @__PURE__ */ d("p", {
|
|
53
53
|
className: "font-medium text-text-primary",
|
|
54
|
-
children:
|
|
55
|
-
}), /* @__PURE__ */
|
|
54
|
+
children: _("ecoAi.suggest.notEnoughData", "Not enough history to suggest a range")
|
|
55
|
+
}), /* @__PURE__ */ d("p", {
|
|
56
56
|
className: "mt-0.5 text-xs",
|
|
57
|
-
children:
|
|
58
|
-
n:
|
|
59
|
-
days:
|
|
57
|
+
children: _("ecoAi.suggest.sample", "{{n}} reading(s) in the last {{days}} days.", {
|
|
58
|
+
n: b.sampleSize,
|
|
59
|
+
days: b.lookbackDays
|
|
60
60
|
})
|
|
61
61
|
})]
|
|
62
62
|
}),
|
|
63
|
-
|
|
63
|
+
b && D && /* @__PURE__ */ f("div", {
|
|
64
64
|
className: "text-sm text-text-secondary",
|
|
65
65
|
"data-testid": "eco-ai-suggest-result",
|
|
66
|
-
children: [/* @__PURE__ */
|
|
67
|
-
min:
|
|
68
|
-
max:
|
|
69
|
-
unit:
|
|
70
|
-
}) }), /* @__PURE__ */
|
|
66
|
+
children: [/* @__PURE__ */ d("p", { children: _("ecoAi.suggest.applied", "Filled in {{min}}–{{max}} {{unit}} (outside range). Review it, then save the threshold.", {
|
|
67
|
+
min: D.minValue,
|
|
68
|
+
max: D.maxValue,
|
|
69
|
+
unit: D.unit ?? ""
|
|
70
|
+
}) }), /* @__PURE__ */ d("p", {
|
|
71
71
|
className: "mt-1 text-xs text-text-tertiary",
|
|
72
72
|
"data-testid": "eco-ai-suggest-breach-rate",
|
|
73
|
-
children:
|
|
74
|
-
rate: n(
|
|
75
|
-
n:
|
|
76
|
-
days:
|
|
77
|
-
p5:
|
|
78
|
-
p50:
|
|
79
|
-
p95:
|
|
73
|
+
children: _("ecoAi.suggest.breachRate", "This band would have flagged {{rate}} of {{n}} readings over the last {{days}} days (p5 {{p5}}, median {{p50}}, p95 {{p95}}).", {
|
|
74
|
+
rate: n(b.historicalBreachRate),
|
|
75
|
+
n: b.sampleSize,
|
|
76
|
+
days: b.lookbackDays,
|
|
77
|
+
p5: b.p5 == null ? "—" : t(b.p5),
|
|
78
|
+
p50: b.p50 == null ? "—" : t(b.p50),
|
|
79
|
+
p95: b.p95 == null ? "—" : t(b.p95)
|
|
80
80
|
})
|
|
81
81
|
})]
|
|
82
82
|
}),
|
|
83
|
-
|
|
83
|
+
O.length > 0 && /* @__PURE__ */ d("ul", {
|
|
84
84
|
className: "list-disc space-y-0.5 pl-5 text-xs text-text-tertiary",
|
|
85
|
-
children:
|
|
85
|
+
children: O.map((e, t) => /* @__PURE__ */ d("li", { children: e }, `${t}:${e}`))
|
|
86
86
|
})
|
|
87
87
|
]
|
|
88
88
|
});
|
|
89
89
|
};
|
|
90
90
|
//#endregion
|
|
91
|
-
export {
|
|
91
|
+
export { m as AiThresholdSuggest };
|
|
92
92
|
|
|
93
93
|
//# sourceMappingURL=AiThresholdSuggest.js.map
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"AiThresholdSuggest.js","names":[],"sources":["../../../../src/ecoimpacthub/components/ai/AiThresholdSuggest.tsx"],"sourcesContent":["/**\n * \"Suggest from history\" for the threshold create dialogs\n * @module ecoimpacthub/components/ai\n *\n * Calls `ecoAiProposeThreshold` (read-only) and hands the OUTSIDE_RANGE band\n * to the dialog through `onSuggest`, which pre-fills min / max / unit. The\n * proposal is never saved here: the threshold exists only once the user\n * submits the create dialog (`ecoCreateThreshold`).\n */\n\nimport { useState, type FC } from 'react';\nimport { Wand2 } from 'lucide-react';\nimport { Button } from '@burdenoff/fe-libs/ui';\n\nimport { useEcoAiErrorReporter, useEcoAiProposeThreshold } from '../../hooks';\nimport { useTr } from '../../hooks/useTr';\nimport {\n formatBound,\n formatBreachRate,\n proposalToPrefill,\n type EcoAiErrorInfo,\n type ThresholdPrefill,\n} from '../../utils/aiMonitoring';\nimport type { EcoThresholdProposal } from '../../types';\nimport { AiCreditHint, AiErrorNotice } from './AiShared';\n\ninterface AiThresholdSuggestProps {\n siteId: string;\n parameter: string;\n onSuggest: (prefill: ThresholdPrefill) => void;\n}\n\nexport const AiThresholdSuggest: FC<AiThresholdSuggestProps> = ({\n siteId,\n parameter,\n onSuggest,\n}) => {\n const tr = useTr();\n const propose = useEcoAiProposeThreshold();\n const reportAiError = useEcoAiErrorReporter();\n const [proposal, setProposal] = useState<EcoThresholdProposal | null>(null);\n const [aiError, setAiError] = useState<EcoAiErrorInfo | null>(null);\n\n const trimmed = parameter.trim();\n const ready = Boolean(siteId && trimmed);\n\n const suggest = async () => {\n if (!ready) return;\n setAiError(null);\n setProposal(null);\n try {\n const out = await propose.mutateAsync({ siteId, parameter: trimmed });\n setProposal(out);\n const prefill = proposalToPrefill(out);\n if (prefill) onSuggest(prefill);\n } catch (err) {\n setAiError(\n reportAiError(\n err,\n 'EcoAiProposeThreshold',\n tr('ecoAi.suggest.failed', 'Could not suggest a threshold.')\n )\n );\n }\n };\n\n const prefill = proposal ? proposalToPrefill(proposal) : null;\n\n return (\n <div\n className=\"grid gap-2 rounded-lg border border-dashed border-border-seam p-3\"\n data-testid=\"eco-ai-suggest\"\n >\n <div className=\"flex flex-wrap items-center justify-between gap-2\">\n <Button\n type=\"button\"\n size=\"sm\"\n variant=\"outline\"\n onClick={suggest}\n disabled={!ready || propose.isPending}\n data-testid=\"eco-ai-suggest-run\"\n >\n <Wand2 className=\"mr-1.5 h-4 w-4\" />\n {propose.isPending\n ? tr('ecoAi.suggest.running', 'Suggesting…')\n : tr('ecoAi.suggest.run', 'Suggest from history')}\n </Button>\n <AiCreditHint />\n </div>\n {!ready && (\n <p className=\"text-xs text-text-tertiary\">\n {tr(\n 'ecoAi.suggest.needInputs',\n 'Pick a site and parameter to suggest a range from its readings.'\n )}\n </p>\n )}\n\n {aiError && <AiErrorNotice error={aiError} />}\n\n {proposal && !prefill && (\n <div className=\"text-sm text-text-secondary\" data-testid=\"eco-ai-suggest-insufficient\">\n <p className=\"font-medium text-text-primary\">\n {tr('ecoAi.suggest.notEnoughData', 'Not enough history to suggest a range')}\n </p>\n <p className=\"mt-0.5 text-xs\">\n {tr('ecoAi.suggest.sample', '{{n}} reading(s) in the last {{days}} days.', {\n n: proposal.sampleSize,\n days: proposal.lookbackDays,\n })}\n </p>\n </div>\n )}\n\n {proposal && prefill && (\n <div className=\"text-sm text-text-secondary\" data-testid=\"eco-ai-suggest-result\">\n <p>\n {tr(\n 'ecoAi.suggest.applied',\n 'Filled in {{min}}–{{max}} {{unit}} (outside range). Review it, then save the threshold.',\n { min: prefill.minValue, max: prefill.maxValue, unit: prefill.unit ?? '' }\n )}\n </p>\n <p className=\"mt-1 text-xs text-text-tertiary\" data-testid=\"eco-ai-suggest-breach-rate\">\n {tr(\n 'ecoAi.suggest.breachRate',\n 'This band would have flagged {{rate}} of {{n}} readings over the last {{days}} days (p5 {{p5}}, median {{p50}}, p95 {{p95}}).',\n {\n rate: formatBreachRate(proposal.historicalBreachRate),\n n: proposal.sampleSize,\n days: proposal.lookbackDays,\n p5: proposal.p5 != null ? formatBound(proposal.p5) : '—',\n p50: proposal.p50 != null ? formatBound(proposal.p50) : '—',\n p95: proposal.p95 != null ? formatBound(proposal.p95) : '—',\n }\n )}\n </p>\n </div>\n )}\n\n {
|
|
1
|
+
{"version":3,"file":"AiThresholdSuggest.js","names":[],"sources":["../../../../src/ecoimpacthub/components/ai/AiThresholdSuggest.tsx"],"sourcesContent":["/**\n * \"Suggest from history\" for the threshold create dialogs\n * @module ecoimpacthub/components/ai\n *\n * Calls `ecoAiProposeThreshold` (read-only) and hands the OUTSIDE_RANGE band\n * to the dialog through `onSuggest`, which pre-fills min / max / unit. The\n * proposal is never saved here: the threshold exists only once the user\n * submits the create dialog (`ecoCreateThreshold`).\n */\n\nimport { useState, type FC } from 'react';\nimport { Wand2 } from 'lucide-react';\nimport { Button } from '@burdenoff/fe-libs/ui';\n\nimport { useEcoAiErrorReporter, useEcoAiProposeThreshold } from '../../hooks';\nimport { useTr } from '../../hooks/useTr';\nimport {\n formatBound,\n formatBreachRate,\n proposalToPrefill,\n userFacingAiNotes,\n type EcoAiErrorInfo,\n type ThresholdPrefill,\n} from '../../utils/aiMonitoring';\nimport type { EcoThresholdProposal } from '../../types';\nimport { AiCreditHint, AiErrorNotice } from './AiShared';\n\ninterface AiThresholdSuggestProps {\n siteId: string;\n parameter: string;\n onSuggest: (prefill: ThresholdPrefill) => void;\n}\n\nexport const AiThresholdSuggest: FC<AiThresholdSuggestProps> = ({\n siteId,\n parameter,\n onSuggest,\n}) => {\n const tr = useTr();\n const propose = useEcoAiProposeThreshold();\n const reportAiError = useEcoAiErrorReporter();\n const [proposal, setProposal] = useState<EcoThresholdProposal | null>(null);\n const [aiError, setAiError] = useState<EcoAiErrorInfo | null>(null);\n\n const trimmed = parameter.trim();\n const ready = Boolean(siteId && trimmed);\n\n const suggest = async () => {\n if (!ready) return;\n setAiError(null);\n setProposal(null);\n try {\n const out = await propose.mutateAsync({ siteId, parameter: trimmed });\n setProposal(out);\n const prefill = proposalToPrefill(out);\n if (prefill) onSuggest(prefill);\n } catch (err) {\n setAiError(\n reportAiError(\n err,\n 'EcoAiProposeThreshold',\n tr('ecoAi.suggest.failed', 'Could not suggest a threshold.')\n )\n );\n }\n };\n\n const prefill = proposal ? proposalToPrefill(proposal) : null;\n const proposalNotes = userFacingAiNotes(proposal?.notes);\n\n return (\n <div\n className=\"grid gap-2 rounded-lg border border-dashed border-border-seam p-3\"\n data-testid=\"eco-ai-suggest\"\n >\n <div className=\"flex flex-wrap items-center justify-between gap-2\">\n <Button\n type=\"button\"\n size=\"sm\"\n variant=\"outline\"\n onClick={suggest}\n disabled={!ready || propose.isPending}\n data-testid=\"eco-ai-suggest-run\"\n >\n <Wand2 className=\"mr-1.5 h-4 w-4\" />\n {propose.isPending\n ? tr('ecoAi.suggest.running', 'Suggesting…')\n : tr('ecoAi.suggest.run', 'Suggest from history')}\n </Button>\n <AiCreditHint />\n </div>\n {!ready && (\n <p className=\"text-xs text-text-tertiary\">\n {tr(\n 'ecoAi.suggest.needInputs',\n 'Pick a site and parameter to suggest a range from its readings.'\n )}\n </p>\n )}\n\n {aiError && <AiErrorNotice error={aiError} />}\n\n {proposal && !prefill && (\n <div className=\"text-sm text-text-secondary\" data-testid=\"eco-ai-suggest-insufficient\">\n <p className=\"font-medium text-text-primary\">\n {tr('ecoAi.suggest.notEnoughData', 'Not enough history to suggest a range')}\n </p>\n <p className=\"mt-0.5 text-xs\">\n {tr('ecoAi.suggest.sample', '{{n}} reading(s) in the last {{days}} days.', {\n n: proposal.sampleSize,\n days: proposal.lookbackDays,\n })}\n </p>\n </div>\n )}\n\n {proposal && prefill && (\n <div className=\"text-sm text-text-secondary\" data-testid=\"eco-ai-suggest-result\">\n <p>\n {tr(\n 'ecoAi.suggest.applied',\n 'Filled in {{min}}–{{max}} {{unit}} (outside range). Review it, then save the threshold.',\n { min: prefill.minValue, max: prefill.maxValue, unit: prefill.unit ?? '' }\n )}\n </p>\n <p className=\"mt-1 text-xs text-text-tertiary\" data-testid=\"eco-ai-suggest-breach-rate\">\n {tr(\n 'ecoAi.suggest.breachRate',\n 'This band would have flagged {{rate}} of {{n}} readings over the last {{days}} days (p5 {{p5}}, median {{p50}}, p95 {{p95}}).',\n {\n rate: formatBreachRate(proposal.historicalBreachRate),\n n: proposal.sampleSize,\n days: proposal.lookbackDays,\n p5: proposal.p5 != null ? formatBound(proposal.p5) : '—',\n p50: proposal.p50 != null ? formatBound(proposal.p50) : '—',\n p95: proposal.p95 != null ? formatBound(proposal.p95) : '—',\n }\n )}\n </p>\n </div>\n )}\n\n {proposalNotes.length > 0 && (\n <ul className=\"list-disc space-y-0.5 pl-5 text-xs text-text-tertiary\">\n {proposalNotes.map((n, i) => (\n <li key={`${i}:${n}`}>{n}</li>\n ))}\n </ul>\n )}\n </div>\n );\n};\n"],"mappings":";;;;;;;;;;AAiCA,IAAa,KAAmD,EAC9D,WACA,cACA,mBACI;CACJ,IAAM,IAAK,EAAM,GACX,IAAU,EAAyB,GACnC,IAAgB,EAAsB,GACtC,CAAC,GAAU,KAAe,EAAsC,IAAI,GACpE,CAAC,GAAS,KAAc,EAAgC,IAAI,GAE5D,IAAU,EAAU,KAAK,GACzB,IAAQ,GAAQ,KAAU,IAE1B,IAAU,YAAY;EACrB,OAEL;GADA,EAAW,IAAI,GACf,EAAY,IAAI;GAChB,IAAI;IACF,IAAM,IAAM,MAAM,EAAQ,YAAY;KAAE;KAAQ,WAAW;IAAQ,CAAC;IACpE,EAAY,CAAG;IACf,IAAM,IAAU,EAAkB,CAAG;IACrC,AAAI,KAAS,EAAU,CAAO;GAChC,SAAS,GAAK;IACZ,EACE,EACE,GACA,yBACA,EAAG,wBAAwB,gCAAgC,CAC7D,CACF;GACF;EAdgB;CAelB,GAEM,IAAU,IAAW,EAAkB,CAAQ,IAAI,MACnD,IAAgB,EAAkB,GAAU,KAAK;CAEvD,OACE,kBAAC,OAAD;EACE,WAAU;EACV,eAAY;YAFd;GAIE,kBAAC,OAAD;IAAK,WAAU;cAAf,CACE,kBAAC,GAAD;KACE,MAAK;KACL,MAAK;KACL,SAAQ;KACR,SAAS;KACT,UAAU,CAAC,KAAS,EAAQ;KAC5B,eAAY;eANd,CAQE,kBAAC,GAAD,EAAO,WAAU,iBAAkB,CAAA,GAClC,EAAQ,YACL,EAAG,yBAAyB,aAAa,IACzC,EAAG,qBAAqB,sBAAsB,CAC5C;QACR,kBAAC,GAAD,CAAe,CAAA,CACZ;;GACJ,CAAC,KACA,kBAAC,KAAD;IAAG,WAAU;cACV,EACC,4BACA,iEACF;GACC,CAAA;GAGJ,KAAW,kBAAC,GAAD,EAAe,OAAO,EAAU,CAAA;GAE3C,KAAY,CAAC,KACZ,kBAAC,OAAD;IAAK,WAAU;IAA8B,eAAY;cAAzD,CACE,kBAAC,KAAD;KAAG,WAAU;eACV,EAAG,+BAA+B,uCAAuC;IACzE,CAAA,GACH,kBAAC,KAAD;KAAG,WAAU;eACV,EAAG,wBAAwB,+CAA+C;MACzE,GAAG,EAAS;MACZ,MAAM,EAAS;KACjB,CAAC;IACA,CAAA,CACA;;GAGN,KAAY,KACX,kBAAC,OAAD;IAAK,WAAU;IAA8B,eAAY;cAAzD,CACE,kBAAC,KAAD,EAAA,UACG,EACC,yBACA,2FACA;KAAE,KAAK,EAAQ;KAAU,KAAK,EAAQ;KAAU,MAAM,EAAQ,QAAQ;IAAG,CAC3E,EACC,CAAA,GACH,kBAAC,KAAD;KAAG,WAAU;KAAkC,eAAY;eACxD,EACC,4BACA,iIACA;MACE,MAAM,EAAiB,EAAS,oBAAoB;MACpD,GAAG,EAAS;MACZ,MAAM,EAAS;MACf,IAAI,EAAS,MAAM,OAAkC,MAA3B,EAAY,EAAS,EAAE;MACjD,KAAK,EAAS,OAAO,OAAmC,MAA5B,EAAY,EAAS,GAAG;MACpD,KAAK,EAAS,OAAO,OAAmC,MAA5B,EAAY,EAAS,GAAG;KACtD,CACF;IACC,CAAA,CACA;;GAGN,EAAc,SAAS,KACtB,kBAAC,MAAD;IAAI,WAAU;cACX,EAAc,KAAK,GAAG,MACrB,kBAAC,MAAD,EAAA,UAAuB,EAAM,GAApB,GAAG,EAAE,GAAG,GAAY,CAC9B;GACC,CAAA;EAEH;;AAET"}
|
|
@@ -1,47 +1,47 @@
|
|
|
1
1
|
import { formatEnumLabel as e, formatNumber as t } from "../../utils/formatters.js";
|
|
2
2
|
import { StatusBadge as n } from "../StatusBadge.js";
|
|
3
|
-
import { useEcoMeasurements as r, useEcoSensors as i, useEcoSetSensorStatus as a, useEcoThresholds as
|
|
4
|
-
import { useTr as
|
|
5
|
-
import { formatConfidence as
|
|
6
|
-
import { useEcoAiErrorReporter as
|
|
3
|
+
import { useEcoMeasurements as r, useEcoSensors as i, useEcoSetSensorStatus as a, useEcoThresholds as ee } from "../../hooks/useEcoImpactHubApi.js";
|
|
4
|
+
import { useTr as te } from "../../hooks/useTr.js";
|
|
5
|
+
import { formatConfidence as ne, matchingThresholds as re, resolveCitedReadings as o } from "../../utils/aiMonitoring.js";
|
|
6
|
+
import { useEcoAiErrorReporter as s, useEcoAiTriageAlert as c } from "../../hooks/useEcoAiMonitoring.js";
|
|
7
7
|
import "../../hooks/index.js";
|
|
8
|
-
import { AiCreditHint as
|
|
8
|
+
import { AiCreditHint as l, AiErrorNotice as u, CitedReadingsList as d } from "./AiShared.js";
|
|
9
9
|
import { useMemo as f, useState as p } from "react";
|
|
10
10
|
import { CheckCircle2 as m, ShieldAlert as h, Sparkles as g } from "lucide-react";
|
|
11
11
|
import { Fragment as _, jsx as v, jsxs as y } from "react/jsx-runtime";
|
|
12
|
-
import { Button as b, Label as ie, ResponsiveDialog as x, ResponsiveDialogContent as S, ResponsiveDialogDescription as ae, ResponsiveDialogFooter as
|
|
12
|
+
import { Button as b, Label as ie, ResponsiveDialog as x, ResponsiveDialogContent as S, ResponsiveDialogDescription as ae, ResponsiveDialogFooter as oe, ResponsiveDialogHeader as C, ResponsiveDialogTitle as w, Select as T, SelectContent as E, SelectItem as D, SelectTrigger as O, SelectValue as k } from "@burdenoff/fe-libs/ui";
|
|
13
13
|
//#region src/ecoimpacthub/components/ai/AiTriageDialog.tsx
|
|
14
|
-
var
|
|
14
|
+
var A = (n) => n.direction === "OUTSIDE_RANGE" ? `${t(n.minValue)}–${t(n.maxValue)} ${n.unit}` : `${e(n.direction)} ${t(n.limitValue)} ${n.unit}`, j = ({ measurement: e, onClose: t, onCitedReadings: n }) => /* @__PURE__ */ v(x, {
|
|
15
15
|
open: e !== null,
|
|
16
16
|
onOpenChange: (e) => !e && t(),
|
|
17
17
|
children: /* @__PURE__ */ v(S, {
|
|
18
18
|
className: "max-h-[90vh] overflow-y-auto sm:max-w-2xl",
|
|
19
|
-
children: e && /* @__PURE__ */ v(
|
|
19
|
+
children: e && /* @__PURE__ */ v(M, {
|
|
20
20
|
measurement: e,
|
|
21
21
|
onClose: t,
|
|
22
22
|
onCitedReadings: n
|
|
23
23
|
}, e.id)
|
|
24
24
|
})
|
|
25
|
-
}),
|
|
26
|
-
let
|
|
25
|
+
}), M = ({ measurement: x, onClose: S, onCitedReadings: j }) => {
|
|
26
|
+
let M = te(), N = ee(x.siteId), P = f(() => re(x, N.data?.items ?? []), [x, N.data]), [F, I] = p(""), L = F || P[0]?.id || "", R = r({
|
|
27
27
|
siteId: x.siteId,
|
|
28
28
|
parameter: x.parameter,
|
|
29
29
|
pagination: {
|
|
30
30
|
limit: 100,
|
|
31
31
|
offset: 0
|
|
32
32
|
}
|
|
33
|
-
}),
|
|
34
|
-
let e =
|
|
33
|
+
}), z = i({ siteId: x.siteId }), B = c(), V = a(), H = s(), [U, W] = p(null), [G, K] = p(null), [q, J] = p(null), [Y, X] = p(!1), [se, Z] = p(!1), Q = f(() => {
|
|
34
|
+
let e = R.data?.items ?? [];
|
|
35
35
|
return e.some((e) => e.id === x.id) ? e : [x, ...e];
|
|
36
|
-
}, [
|
|
36
|
+
}, [R.data, x]), $ = f(() => U ? o(U.evidenceMeasurementIds, Q) : [], [U, Q]);
|
|
37
37
|
return /* @__PURE__ */ y(_, { children: [
|
|
38
|
-
/* @__PURE__ */ y(
|
|
38
|
+
/* @__PURE__ */ y(C, { children: [/* @__PURE__ */ y(w, {
|
|
39
39
|
className: "flex items-center gap-2",
|
|
40
40
|
children: [/* @__PURE__ */ v(g, {
|
|
41
41
|
className: "h-4 w-4 text-accent",
|
|
42
42
|
"aria-hidden": "true"
|
|
43
|
-
}),
|
|
44
|
-
}), /* @__PURE__ */ v(ae, { children:
|
|
43
|
+
}), M("ecoAi.triage.title", "Triage alert")]
|
|
44
|
+
}), /* @__PURE__ */ v(ae, { children: M("ecoAi.triage.description", "Check whether this {{parameter}} reading of {{value}} {{unit}} is a real event or a sensor fault.", {
|
|
45
45
|
parameter: x.parameter,
|
|
46
46
|
value: t(x.value),
|
|
47
47
|
unit: x.unit
|
|
@@ -50,70 +50,70 @@ var j = (n) => n.direction === "OUTSIDE_RANGE" ? `${t(n.minValue)}–${t(n.maxVa
|
|
|
50
50
|
className: "grid gap-4 py-2",
|
|
51
51
|
"data-testid": "eco-ai-triage",
|
|
52
52
|
children: [
|
|
53
|
-
|
|
53
|
+
N.isLoading ? /* @__PURE__ */ v("p", {
|
|
54
54
|
className: "text-sm text-text-secondary",
|
|
55
|
-
children:
|
|
56
|
-
}) :
|
|
55
|
+
children: M("ecoAi.triage.loadingThresholds", "Loading thresholds…")
|
|
56
|
+
}) : P.length === 0 ? /* @__PURE__ */ v("p", {
|
|
57
57
|
className: "rounded-lg bg-bg-sunken/60 p-3 text-sm text-text-secondary",
|
|
58
|
-
children:
|
|
58
|
+
children: M("ecoAi.triage.noThreshold", "No alert threshold covers {{parameter}} at this site, so there is nothing to triage against.", { parameter: x.parameter })
|
|
59
59
|
}) : /* @__PURE__ */ y("div", {
|
|
60
60
|
className: "grid gap-1.5",
|
|
61
61
|
children: [/* @__PURE__ */ v(ie, {
|
|
62
62
|
htmlFor: "ecoAiTriageThreshold",
|
|
63
|
-
children:
|
|
64
|
-
}), /* @__PURE__ */ y(
|
|
65
|
-
value:
|
|
66
|
-
onValueChange:
|
|
67
|
-
children: [/* @__PURE__ */ v(
|
|
63
|
+
children: M("ecoAi.triage.threshold", "Threshold")
|
|
64
|
+
}), /* @__PURE__ */ y(T, {
|
|
65
|
+
value: L,
|
|
66
|
+
onValueChange: I,
|
|
67
|
+
children: [/* @__PURE__ */ v(O, {
|
|
68
68
|
id: "ecoAiTriageThreshold",
|
|
69
|
-
children: /* @__PURE__ */ v(
|
|
70
|
-
}), /* @__PURE__ */ v(
|
|
69
|
+
children: /* @__PURE__ */ v(k, {})
|
|
70
|
+
}), /* @__PURE__ */ v(E, { children: P.map((t) => /* @__PURE__ */ y(D, {
|
|
71
71
|
value: t.id,
|
|
72
72
|
children: [
|
|
73
|
-
|
|
73
|
+
A(t),
|
|
74
74
|
" · ",
|
|
75
75
|
e(t.severity),
|
|
76
|
-
t.isActive ? "" : ` · ${
|
|
76
|
+
t.isActive ? "" : ` · ${M("ecoAi.triage.inactive", "inactive")}`
|
|
77
77
|
]
|
|
78
78
|
}, t.id)) })]
|
|
79
79
|
})]
|
|
80
80
|
}),
|
|
81
|
-
|
|
82
|
-
|
|
81
|
+
G && /* @__PURE__ */ v(u, { error: G }),
|
|
82
|
+
U && /* @__PURE__ */ y("div", {
|
|
83
83
|
className: "grid gap-4",
|
|
84
84
|
"data-testid": "eco-ai-triage-result",
|
|
85
85
|
children: [
|
|
86
86
|
/* @__PURE__ */ y("div", {
|
|
87
87
|
className: "flex flex-wrap items-center gap-3",
|
|
88
88
|
children: [
|
|
89
|
-
/* @__PURE__ */ v(n, { status:
|
|
89
|
+
/* @__PURE__ */ v(n, { status: U.classification }),
|
|
90
90
|
/* @__PURE__ */ v("span", {
|
|
91
91
|
className: "text-sm text-text-secondary",
|
|
92
|
-
children:
|
|
92
|
+
children: M("ecoAi.triage.confidence", "Confidence {{value}}", { value: ne(U.confidence) })
|
|
93
93
|
}),
|
|
94
94
|
/* @__PURE__ */ y("span", {
|
|
95
95
|
className: "text-sm text-text-secondary",
|
|
96
|
-
children: ["· ", e(
|
|
96
|
+
children: ["· ", e(U.recommendation)]
|
|
97
97
|
}),
|
|
98
|
-
!
|
|
98
|
+
!U.breachesThreshold && /* @__PURE__ */ v("span", {
|
|
99
99
|
className: "text-xs text-text-tertiary",
|
|
100
|
-
children:
|
|
100
|
+
children: M("ecoAi.triage.noLongerBreaches", "This reading does not breach the selected threshold.")
|
|
101
101
|
})
|
|
102
102
|
]
|
|
103
103
|
}),
|
|
104
|
-
|
|
104
|
+
U.rationale.length > 0 && /* @__PURE__ */ y("div", { children: [/* @__PURE__ */ v("h3", {
|
|
105
105
|
className: "mb-1.5 text-sm font-semibold text-text-primary",
|
|
106
|
-
children:
|
|
106
|
+
children: M("ecoAi.triage.rationale", "Why")
|
|
107
107
|
}), /* @__PURE__ */ v("ul", {
|
|
108
108
|
className: "list-disc space-y-1 pl-5 text-sm text-text-secondary",
|
|
109
|
-
children:
|
|
109
|
+
children: U.rationale.map((e, t) => /* @__PURE__ */ v("li", { children: e }, `${t}:${e}`))
|
|
110
110
|
})] }),
|
|
111
|
-
|
|
111
|
+
U.signals.length > 0 && /* @__PURE__ */ y("div", { children: [/* @__PURE__ */ v("h3", {
|
|
112
112
|
className: "mb-1.5 text-sm font-semibold text-text-primary",
|
|
113
|
-
children:
|
|
113
|
+
children: M("ecoAi.triage.signals", "Signals")
|
|
114
114
|
}), /* @__PURE__ */ v("ul", {
|
|
115
115
|
className: "space-y-1.5 text-sm",
|
|
116
|
-
children:
|
|
116
|
+
children: U.signals.map((e) => /* @__PURE__ */ y("li", {
|
|
117
117
|
className: "flex items-start gap-2",
|
|
118
118
|
children: [/* @__PURE__ */ v(n, {
|
|
119
119
|
status: e.favours,
|
|
@@ -126,12 +126,12 @@ var j = (n) => n.direction === "OUTSIDE_RANGE" ? `${t(n.minValue)}–${t(n.maxVa
|
|
|
126
126
|
})] }),
|
|
127
127
|
/* @__PURE__ */ y("div", { children: [/* @__PURE__ */ v("h3", {
|
|
128
128
|
className: "mb-1.5 text-sm font-semibold text-text-primary",
|
|
129
|
-
children:
|
|
130
|
-
}), /* @__PURE__ */ v(
|
|
129
|
+
children: M("ecoAi.triage.citedReadings", "Cited readings ({{count}})", { count: $.length })
|
|
130
|
+
}), /* @__PURE__ */ v(d, {
|
|
131
131
|
readings: $,
|
|
132
132
|
focusId: x.id
|
|
133
133
|
})] }),
|
|
134
|
-
|
|
134
|
+
U.proposedAction && !se && /* @__PURE__ */ y("div", {
|
|
135
135
|
"data-testid": "eco-ai-decision-card",
|
|
136
136
|
className: "rounded-xl border border-border-seam bg-bg-sunken/60 p-4",
|
|
137
137
|
children: [
|
|
@@ -142,57 +142,57 @@ var j = (n) => n.direction === "OUTSIDE_RANGE" ? `${t(n.minValue)}–${t(n.maxVa
|
|
|
142
142
|
children: [
|
|
143
143
|
/* @__PURE__ */ v("p", {
|
|
144
144
|
className: "text-sm font-semibold text-text-primary",
|
|
145
|
-
children:
|
|
146
|
-
sensor: ((e) => e &&
|
|
147
|
-
status: e(
|
|
145
|
+
children: M("ecoAi.triage.proposalTitle", "Suggested action: mark {{sensor}} as {{status}}", {
|
|
146
|
+
sensor: ((e) => e && z.data?.items?.find((t) => t.id === e)?.name || M("ecoAi.triage.unnamedSensor", "the sensor"))(U.proposedAction.sensorId),
|
|
147
|
+
status: e(U.proposedAction.targetStatus)
|
|
148
148
|
})
|
|
149
149
|
}),
|
|
150
150
|
/* @__PURE__ */ v("p", {
|
|
151
151
|
className: "mt-1 text-sm text-text-secondary",
|
|
152
|
-
children:
|
|
152
|
+
children: U.proposedAction.reason
|
|
153
153
|
}),
|
|
154
|
-
/* @__PURE__ */ v("p", {
|
|
154
|
+
!Y && /* @__PURE__ */ v("p", {
|
|
155
155
|
className: "mt-1 text-xs text-text-tertiary",
|
|
156
|
-
children:
|
|
156
|
+
children: M("ecoAi.triage.proposalNotApplied", "Nothing has changed yet. The sensor status changes only if you apply it.")
|
|
157
157
|
})
|
|
158
158
|
]
|
|
159
159
|
})]
|
|
160
160
|
}),
|
|
161
|
-
|
|
161
|
+
Y ? /* @__PURE__ */ y("p", {
|
|
162
162
|
className: "mt-3 flex items-center gap-1.5 text-sm text-status-success-text",
|
|
163
163
|
"data-testid": "eco-ai-decision-applied",
|
|
164
|
-
children: [/* @__PURE__ */ v(m, { className: "h-4 w-4" }),
|
|
164
|
+
children: [/* @__PURE__ */ v(m, { className: "h-4 w-4" }), M("ecoAi.triage.applied", "Sensor marked as {{status}}.", { status: e(U.proposedAction.targetStatus) })]
|
|
165
165
|
}) : /* @__PURE__ */ y("div", {
|
|
166
166
|
className: "mt-3 flex flex-wrap gap-2",
|
|
167
167
|
children: [/* @__PURE__ */ v(b, {
|
|
168
168
|
size: "sm",
|
|
169
169
|
onClick: async () => {
|
|
170
|
-
let e =
|
|
170
|
+
let e = U?.proposedAction;
|
|
171
171
|
if (e) {
|
|
172
|
-
|
|
172
|
+
J(null);
|
|
173
173
|
try {
|
|
174
|
-
await
|
|
174
|
+
await V.mutateAsync({
|
|
175
175
|
id: e.sensorId,
|
|
176
176
|
status: e.targetStatus
|
|
177
177
|
}), X(!0);
|
|
178
178
|
} catch (e) {
|
|
179
|
-
|
|
179
|
+
J(e instanceof Error ? e.message : M("ecoAi.triage.applyFailed", "Could not update the sensor."));
|
|
180
180
|
}
|
|
181
181
|
}
|
|
182
182
|
},
|
|
183
|
-
disabled:
|
|
183
|
+
disabled: V.isPending,
|
|
184
184
|
"data-testid": "eco-ai-decision-apply",
|
|
185
|
-
children:
|
|
185
|
+
children: V.isPending ? M("ecoAi.triage.applying", "Applying…") : M("ecoAi.triage.apply", "Mark sensor {{status}}", { status: e(U.proposedAction.targetStatus) })
|
|
186
186
|
}), /* @__PURE__ */ v(b, {
|
|
187
187
|
size: "sm",
|
|
188
188
|
variant: "ghost",
|
|
189
189
|
onClick: () => Z(!0),
|
|
190
|
-
children:
|
|
190
|
+
children: M("ecoAi.triage.dismiss", "Dismiss")
|
|
191
191
|
})]
|
|
192
192
|
}),
|
|
193
|
-
|
|
193
|
+
q && /* @__PURE__ */ v("p", {
|
|
194
194
|
className: "mt-2 text-sm text-status-error-text",
|
|
195
|
-
children:
|
|
195
|
+
children: q
|
|
196
196
|
})
|
|
197
197
|
]
|
|
198
198
|
})
|
|
@@ -200,38 +200,38 @@ var j = (n) => n.direction === "OUTSIDE_RANGE" ? `${t(n.minValue)}–${t(n.maxVa
|
|
|
200
200
|
})
|
|
201
201
|
]
|
|
202
202
|
}),
|
|
203
|
-
/* @__PURE__ */ y(
|
|
203
|
+
/* @__PURE__ */ y(oe, {
|
|
204
204
|
className: "flex-col items-stretch gap-2 sm:flex-row sm:items-center sm:justify-between",
|
|
205
|
-
children: [/* @__PURE__ */ v(
|
|
205
|
+
children: [/* @__PURE__ */ v(l, {}), /* @__PURE__ */ y("div", {
|
|
206
206
|
className: "flex gap-2 sm:justify-end",
|
|
207
207
|
children: [/* @__PURE__ */ v(b, {
|
|
208
208
|
variant: "outline",
|
|
209
209
|
onClick: S,
|
|
210
|
-
children:
|
|
210
|
+
children: M("ecoAi.close", "Close")
|
|
211
211
|
}), /* @__PURE__ */ v(b, {
|
|
212
212
|
onClick: async () => {
|
|
213
|
-
if (
|
|
214
|
-
|
|
213
|
+
if (L) {
|
|
214
|
+
K(null), W(null), X(!1), Z(!1), J(null);
|
|
215
215
|
try {
|
|
216
|
-
let e = await
|
|
217
|
-
thresholdId:
|
|
216
|
+
let e = await B.mutateAsync({
|
|
217
|
+
thresholdId: L,
|
|
218
218
|
measurementId: x.id
|
|
219
219
|
});
|
|
220
|
-
|
|
220
|
+
W(e), j?.(e.evidenceMeasurementIds);
|
|
221
221
|
} catch (e) {
|
|
222
|
-
|
|
222
|
+
K(H(e, "EcoAiTriageAlert", M("ecoAi.triage.failed", "Triage failed.")));
|
|
223
223
|
}
|
|
224
224
|
}
|
|
225
225
|
},
|
|
226
|
-
disabled: !
|
|
226
|
+
disabled: !L || B.isPending,
|
|
227
227
|
"data-testid": "eco-ai-triage-run",
|
|
228
|
-
children:
|
|
228
|
+
children: B.isPending ? M("ecoAi.triage.running", "Triaging…") : U ? M("ecoAi.triage.rerun", "Run again") : M("ecoAi.triage.run", "Run triage")
|
|
229
229
|
})]
|
|
230
230
|
})]
|
|
231
231
|
})
|
|
232
232
|
] });
|
|
233
233
|
};
|
|
234
234
|
//#endregion
|
|
235
|
-
export {
|
|
235
|
+
export { j as AiTriageDialog };
|
|
236
236
|
|
|
237
237
|
//# sourceMappingURL=AiTriageDialog.js.map
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"AiTriageDialog.js","names":[],"sources":["../../../../src/ecoimpacthub/components/ai/AiTriageDialog.tsx"],"sourcesContent":["/**\n * AI alert triage dialog\n * @module ecoimpacthub/components/ai\n *\n * Opens from a breaching reading. The user picks the threshold it breached\n * (auto-selected when only one applies) and runs `ecoAiTriageAlert`. The\n * verdict shows classification, confidence, rationale, signals and the cited\n * readings. A `proposedAction` renders as a decision card: the sensor status\n * changes only when the user clicks its button, via the existing\n * `ecoSetSensorStatus` mutation. Nothing is applied automatically.\n */\n\nimport { useMemo, useState, type FC } from 'react';\nimport { CheckCircle2, ShieldAlert, Sparkles } from 'lucide-react';\nimport {\n Button,\n Label,\n Select,\n SelectContent,\n SelectItem,\n SelectTrigger,\n SelectValue,\n ResponsiveDialog,\n ResponsiveDialogContent,\n ResponsiveDialogHeader,\n ResponsiveDialogTitle,\n ResponsiveDialogDescription,\n ResponsiveDialogFooter,\n} from '@burdenoff/fe-libs/ui';\n\nimport { StatusBadge } from '../StatusBadge';\nimport {\n useEcoAiErrorReporter,\n useEcoAiTriageAlert,\n useEcoMeasurements,\n useEcoSensors,\n useEcoSetSensorStatus,\n useEcoThresholds,\n} from '../../hooks';\nimport { useTr } from '../../hooks/useTr';\nimport {\n formatConfidence,\n matchingThresholds,\n resolveCitedReadings,\n type EcoAiErrorInfo,\n} from '../../utils/aiMonitoring';\nimport { formatEnumLabel, formatNumber } from '../../utils/formatters';\nimport type { EcoAlertThreshold, EcoAlertTriageResult, EcoMeasurement } from '../../types';\nimport { AiCreditHint, AiErrorNotice, CitedReadingsList } from './AiShared';\n\ninterface AiTriageDialogProps {\n /** The breaching reading to triage; the dialog is closed when null. */\n measurement: EcoMeasurement | null;\n onClose: () => void;\n /** Called with every reading the verdict cites, to highlight table rows. */\n onCitedReadings?: (ids: string[]) => void;\n}\n\nconst describeRule = (t: EcoAlertThreshold): string =>\n t.direction === 'OUTSIDE_RANGE'\n ? `${formatNumber(t.minValue)}–${formatNumber(t.maxValue)} ${t.unit}`\n : `${formatEnumLabel(t.direction)} ${formatNumber(t.limitValue)} ${t.unit}`;\n\nexport const AiTriageDialog: FC<AiTriageDialogProps> = ({\n measurement,\n onClose,\n onCitedReadings,\n}) => (\n <ResponsiveDialog open={measurement !== null} onOpenChange={(open) => !open && onClose()}>\n <ResponsiveDialogContent className=\"max-h-[90vh] overflow-y-auto sm:max-w-2xl\">\n {measurement && (\n <TriageBody\n key={measurement.id}\n measurement={measurement}\n onClose={onClose}\n onCitedReadings={onCitedReadings}\n />\n )}\n </ResponsiveDialogContent>\n </ResponsiveDialog>\n);\n\nconst TriageBody: FC<{\n measurement: EcoMeasurement;\n onClose: () => void;\n onCitedReadings?: (ids: string[]) => void;\n}> = ({ measurement, onClose, onCitedReadings }) => {\n const tr = useTr();\n const thresholdsQuery = useEcoThresholds(measurement.siteId);\n const candidates = useMemo(\n () => matchingThresholds(measurement, thresholdsQuery.data?.items ?? []),\n [measurement, thresholdsQuery.data]\n );\n const [pickedThresholdId, setPickedThresholdId] = useState<string>('');\n const thresholdId = pickedThresholdId || candidates[0]?.id || '';\n\n // Readings for this site + parameter resolve the cited ids to values.\n const historyQuery = useEcoMeasurements({\n siteId: measurement.siteId,\n parameter: measurement.parameter,\n pagination: { limit: 100, offset: 0 },\n });\n const sensorsQuery = useEcoSensors({ siteId: measurement.siteId });\n\n const triage = useEcoAiTriageAlert();\n const setSensorStatus = useEcoSetSensorStatus();\n const reportAiError = useEcoAiErrorReporter();\n\n const [result, setResult] = useState<EcoAlertTriageResult | null>(null);\n const [aiError, setAiError] = useState<EcoAiErrorInfo | null>(null);\n const [applyError, setApplyError] = useState<string | null>(null);\n const [applied, setApplied] = useState(false);\n const [dismissed, setDismissed] = useState(false);\n\n const readings = useMemo(() => {\n const items = historyQuery.data?.items ?? [];\n return items.some((m) => m.id === measurement.id) ? items : [measurement, ...items];\n }, [historyQuery.data, measurement]);\n const cited = useMemo(\n () => (result ? resolveCitedReadings(result.evidenceMeasurementIds, readings) : []),\n [result, readings]\n );\n const sensorName = (id: string | null | undefined): string =>\n (id && sensorsQuery.data?.items?.find((s) => s.id === id)?.name) ||\n tr('ecoAi.triage.unnamedSensor', 'the sensor');\n\n const runTriage = async () => {\n if (!thresholdId) return;\n setAiError(null);\n setResult(null);\n setApplied(false);\n setDismissed(false);\n setApplyError(null);\n try {\n const verdict = await triage.mutateAsync({ thresholdId, measurementId: measurement.id });\n setResult(verdict);\n onCitedReadings?.(verdict.evidenceMeasurementIds);\n } catch (err) {\n setAiError(\n reportAiError(err, 'EcoAiTriageAlert', tr('ecoAi.triage.failed', 'Triage failed.'))\n );\n }\n };\n\n const applyProposal = async () => {\n const action = result?.proposedAction;\n if (!action) return;\n setApplyError(null);\n try {\n await setSensorStatus.mutateAsync({ id: action.sensorId, status: action.targetStatus });\n setApplied(true);\n } catch (err) {\n setApplyError(\n err instanceof Error\n ? err.message\n : tr('ecoAi.triage.applyFailed', 'Could not update the sensor.')\n );\n }\n };\n\n return (\n <>\n <ResponsiveDialogHeader>\n <ResponsiveDialogTitle className=\"flex items-center gap-2\">\n <Sparkles className=\"h-4 w-4 text-accent\" aria-hidden=\"true\" />\n {tr('ecoAi.triage.title', 'Triage alert')}\n </ResponsiveDialogTitle>\n <ResponsiveDialogDescription>\n {tr(\n 'ecoAi.triage.description',\n 'Check whether this {{parameter}} reading of {{value}} {{unit}} is a real event or a sensor fault.',\n {\n parameter: measurement.parameter,\n value: formatNumber(measurement.value),\n unit: measurement.unit,\n }\n )}\n </ResponsiveDialogDescription>\n </ResponsiveDialogHeader>\n\n <div className=\"grid gap-4 py-2\" data-testid=\"eco-ai-triage\">\n {/* Threshold picker */}\n {thresholdsQuery.isLoading ? (\n <p className=\"text-sm text-text-secondary\">\n {tr('ecoAi.triage.loadingThresholds', 'Loading thresholds…')}\n </p>\n ) : candidates.length === 0 ? (\n <p className=\"rounded-lg bg-bg-sunken/60 p-3 text-sm text-text-secondary\">\n {tr(\n 'ecoAi.triage.noThreshold',\n 'No alert threshold covers {{parameter}} at this site, so there is nothing to triage against.',\n { parameter: measurement.parameter }\n )}\n </p>\n ) : (\n <div className=\"grid gap-1.5\">\n <Label htmlFor=\"ecoAiTriageThreshold\">\n {tr('ecoAi.triage.threshold', 'Threshold')}\n </Label>\n <Select value={thresholdId} onValueChange={setPickedThresholdId}>\n <SelectTrigger id=\"ecoAiTriageThreshold\">\n <SelectValue />\n </SelectTrigger>\n <SelectContent>\n {candidates.map((t) => (\n <SelectItem key={t.id} value={t.id}>\n {describeRule(t)} · {formatEnumLabel(t.severity)}\n {t.isActive ? '' : ` · ${tr('ecoAi.triage.inactive', 'inactive')}`}\n </SelectItem>\n ))}\n </SelectContent>\n </Select>\n </div>\n )}\n\n {aiError && <AiErrorNotice error={aiError} />}\n\n {/* Verdict */}\n {result && (\n <div className=\"grid gap-4\" data-testid=\"eco-ai-triage-result\">\n <div className=\"flex flex-wrap items-center gap-3\">\n <StatusBadge status={result.classification} />\n <span className=\"text-sm text-text-secondary\">\n {tr('ecoAi.triage.confidence', 'Confidence {{value}}', {\n value: formatConfidence(result.confidence),\n })}\n </span>\n <span className=\"text-sm text-text-secondary\">\n · {formatEnumLabel(result.recommendation)}\n </span>\n {!result.breachesThreshold && (\n <span className=\"text-xs text-text-tertiary\">\n {tr(\n 'ecoAi.triage.noLongerBreaches',\n 'This reading does not breach the selected threshold.'\n )}\n </span>\n )}\n </div>\n\n {result.rationale.length > 0 && (\n <div>\n <h3 className=\"mb-1.5 text-sm font-semibold text-text-primary\">\n {tr('ecoAi.triage.rationale', 'Why')}\n </h3>\n <ul className=\"list-disc space-y-1 pl-5 text-sm text-text-secondary\">\n {result.rationale.map((line, i) => (\n <li key={`${i}:${line}`}>{line}</li>\n ))}\n </ul>\n </div>\n )}\n\n {result.signals.length > 0 && (\n <div>\n <h3 className=\"mb-1.5 text-sm font-semibold text-text-primary\">\n {tr('ecoAi.triage.signals', 'Signals')}\n </h3>\n <ul className=\"space-y-1.5 text-sm\">\n {result.signals.map((s) => (\n <li key={s.code} className=\"flex items-start gap-2\">\n <StatusBadge status={s.favours} className=\"shrink-0\" />\n <span className=\"text-text-secondary\">{s.detail}</span>\n </li>\n ))}\n </ul>\n </div>\n )}\n\n <div>\n <h3 className=\"mb-1.5 text-sm font-semibold text-text-primary\">\n {tr('ecoAi.triage.citedReadings', 'Cited readings ({{count}})', {\n count: cited.length,\n })}\n </h3>\n <CitedReadingsList readings={cited} focusId={measurement.id} />\n </div>\n\n {/* Decision card: applied only on the user's click. */}\n {result.proposedAction && !dismissed && (\n <div\n data-testid=\"eco-ai-decision-card\"\n className=\"rounded-xl border border-border-seam bg-bg-sunken/60 p-4\"\n >\n <div className=\"flex items-start gap-2\">\n <ShieldAlert className=\"mt-0.5 h-4 w-4 shrink-0 text-status-warning-text\" />\n <div className=\"min-w-0\">\n <p className=\"text-sm font-semibold text-text-primary\">\n {tr(\n 'ecoAi.triage.proposalTitle',\n 'Suggested action: mark {{sensor}} as {{status}}',\n {\n sensor: sensorName(result.proposedAction.sensorId),\n status: formatEnumLabel(result.proposedAction.targetStatus),\n }\n )}\n </p>\n <p className=\"mt-1 text-sm text-text-secondary\">\n {result.proposedAction.reason}\n </p>\n <p className=\"mt-1 text-xs text-text-tertiary\">\n {tr(\n 'ecoAi.triage.proposalNotApplied',\n 'Nothing has changed yet. The sensor status changes only if you apply it.'\n )}\n </p>\n </div>\n </div>\n {applied ? (\n <p\n className=\"mt-3 flex items-center gap-1.5 text-sm text-status-success-text\"\n data-testid=\"eco-ai-decision-applied\"\n >\n <CheckCircle2 className=\"h-4 w-4\" />\n {tr('ecoAi.triage.applied', 'Sensor marked as {{status}}.', {\n status: formatEnumLabel(result.proposedAction.targetStatus),\n })}\n </p>\n ) : (\n <div className=\"mt-3 flex flex-wrap gap-2\">\n <Button\n size=\"sm\"\n onClick={applyProposal}\n disabled={setSensorStatus.isPending}\n data-testid=\"eco-ai-decision-apply\"\n >\n {setSensorStatus.isPending\n ? tr('ecoAi.triage.applying', 'Applying…')\n : tr('ecoAi.triage.apply', 'Mark sensor {{status}}', {\n status: formatEnumLabel(result.proposedAction.targetStatus),\n })}\n </Button>\n <Button size=\"sm\" variant=\"ghost\" onClick={() => setDismissed(true)}>\n {tr('ecoAi.triage.dismiss', 'Dismiss')}\n </Button>\n </div>\n )}\n {applyError && <p className=\"mt-2 text-sm text-status-error-text\">{applyError}</p>}\n </div>\n )}\n </div>\n )}\n </div>\n\n <ResponsiveDialogFooter className=\"flex-col items-stretch gap-2 sm:flex-row sm:items-center sm:justify-between\">\n <AiCreditHint />\n <div className=\"flex gap-2 sm:justify-end\">\n <Button variant=\"outline\" onClick={onClose}>\n {tr('ecoAi.close', 'Close')}\n </Button>\n <Button\n onClick={runTriage}\n disabled={!thresholdId || triage.isPending}\n data-testid=\"eco-ai-triage-run\"\n >\n {triage.isPending\n ? tr('ecoAi.triage.running', 'Triaging…')\n : result\n ? tr('ecoAi.triage.rerun', 'Run again')\n : tr('ecoAi.triage.run', 'Run triage')}\n </Button>\n </div>\n </ResponsiveDialogFooter>\n </>\n );\n};\n"],"mappings":"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"}
|
|
1
|
+
{"version":3,"file":"AiTriageDialog.js","names":[],"sources":["../../../../src/ecoimpacthub/components/ai/AiTriageDialog.tsx"],"sourcesContent":["/**\n * AI alert triage dialog\n * @module ecoimpacthub/components/ai\n *\n * Opens from a breaching reading. The user picks the threshold it breached\n * (auto-selected when only one applies) and runs `ecoAiTriageAlert`. The\n * verdict shows classification, confidence, rationale, signals and the cited\n * readings. A `proposedAction` renders as a decision card: the sensor status\n * changes only when the user clicks its button, via the existing\n * `ecoSetSensorStatus` mutation. Nothing is applied automatically.\n */\n\nimport { useMemo, useState, type FC } from 'react';\nimport { CheckCircle2, ShieldAlert, Sparkles } from 'lucide-react';\nimport {\n Button,\n Label,\n Select,\n SelectContent,\n SelectItem,\n SelectTrigger,\n SelectValue,\n ResponsiveDialog,\n ResponsiveDialogContent,\n ResponsiveDialogHeader,\n ResponsiveDialogTitle,\n ResponsiveDialogDescription,\n ResponsiveDialogFooter,\n} from '@burdenoff/fe-libs/ui';\n\nimport { StatusBadge } from '../StatusBadge';\nimport {\n useEcoAiErrorReporter,\n useEcoAiTriageAlert,\n useEcoMeasurements,\n useEcoSensors,\n useEcoSetSensorStatus,\n useEcoThresholds,\n} from '../../hooks';\nimport { useTr } from '../../hooks/useTr';\nimport {\n formatConfidence,\n matchingThresholds,\n resolveCitedReadings,\n type EcoAiErrorInfo,\n} from '../../utils/aiMonitoring';\nimport { formatEnumLabel, formatNumber } from '../../utils/formatters';\nimport type { EcoAlertThreshold, EcoAlertTriageResult, EcoMeasurement } from '../../types';\nimport { AiCreditHint, AiErrorNotice, CitedReadingsList } from './AiShared';\n\ninterface AiTriageDialogProps {\n /** The breaching reading to triage; the dialog is closed when null. */\n measurement: EcoMeasurement | null;\n onClose: () => void;\n /** Called with every reading the verdict cites, to highlight table rows. */\n onCitedReadings?: (ids: string[]) => void;\n}\n\nconst describeRule = (t: EcoAlertThreshold): string =>\n t.direction === 'OUTSIDE_RANGE'\n ? `${formatNumber(t.minValue)}–${formatNumber(t.maxValue)} ${t.unit}`\n : `${formatEnumLabel(t.direction)} ${formatNumber(t.limitValue)} ${t.unit}`;\n\nexport const AiTriageDialog: FC<AiTriageDialogProps> = ({\n measurement,\n onClose,\n onCitedReadings,\n}) => (\n <ResponsiveDialog open={measurement !== null} onOpenChange={(open) => !open && onClose()}>\n <ResponsiveDialogContent className=\"max-h-[90vh] overflow-y-auto sm:max-w-2xl\">\n {measurement && (\n <TriageBody\n key={measurement.id}\n measurement={measurement}\n onClose={onClose}\n onCitedReadings={onCitedReadings}\n />\n )}\n </ResponsiveDialogContent>\n </ResponsiveDialog>\n);\n\nconst TriageBody: FC<{\n measurement: EcoMeasurement;\n onClose: () => void;\n onCitedReadings?: (ids: string[]) => void;\n}> = ({ measurement, onClose, onCitedReadings }) => {\n const tr = useTr();\n const thresholdsQuery = useEcoThresholds(measurement.siteId);\n const candidates = useMemo(\n () => matchingThresholds(measurement, thresholdsQuery.data?.items ?? []),\n [measurement, thresholdsQuery.data]\n );\n const [pickedThresholdId, setPickedThresholdId] = useState<string>('');\n const thresholdId = pickedThresholdId || candidates[0]?.id || '';\n\n // Readings for this site + parameter resolve the cited ids to values.\n const historyQuery = useEcoMeasurements({\n siteId: measurement.siteId,\n parameter: measurement.parameter,\n pagination: { limit: 100, offset: 0 },\n });\n const sensorsQuery = useEcoSensors({ siteId: measurement.siteId });\n\n const triage = useEcoAiTriageAlert();\n const setSensorStatus = useEcoSetSensorStatus();\n const reportAiError = useEcoAiErrorReporter();\n\n const [result, setResult] = useState<EcoAlertTriageResult | null>(null);\n const [aiError, setAiError] = useState<EcoAiErrorInfo | null>(null);\n const [applyError, setApplyError] = useState<string | null>(null);\n const [applied, setApplied] = useState(false);\n const [dismissed, setDismissed] = useState(false);\n\n const readings = useMemo(() => {\n const items = historyQuery.data?.items ?? [];\n return items.some((m) => m.id === measurement.id) ? items : [measurement, ...items];\n }, [historyQuery.data, measurement]);\n const cited = useMemo(\n () => (result ? resolveCitedReadings(result.evidenceMeasurementIds, readings) : []),\n [result, readings]\n );\n const sensorName = (id: string | null | undefined): string =>\n (id && sensorsQuery.data?.items?.find((s) => s.id === id)?.name) ||\n tr('ecoAi.triage.unnamedSensor', 'the sensor');\n\n const runTriage = async () => {\n if (!thresholdId) return;\n setAiError(null);\n setResult(null);\n setApplied(false);\n setDismissed(false);\n setApplyError(null);\n try {\n const verdict = await triage.mutateAsync({ thresholdId, measurementId: measurement.id });\n setResult(verdict);\n onCitedReadings?.(verdict.evidenceMeasurementIds);\n } catch (err) {\n setAiError(\n reportAiError(err, 'EcoAiTriageAlert', tr('ecoAi.triage.failed', 'Triage failed.'))\n );\n }\n };\n\n const applyProposal = async () => {\n const action = result?.proposedAction;\n if (!action) return;\n setApplyError(null);\n try {\n await setSensorStatus.mutateAsync({ id: action.sensorId, status: action.targetStatus });\n setApplied(true);\n } catch (err) {\n setApplyError(\n err instanceof Error\n ? err.message\n : tr('ecoAi.triage.applyFailed', 'Could not update the sensor.')\n );\n }\n };\n\n return (\n <>\n <ResponsiveDialogHeader>\n <ResponsiveDialogTitle className=\"flex items-center gap-2\">\n <Sparkles className=\"h-4 w-4 text-accent\" aria-hidden=\"true\" />\n {tr('ecoAi.triage.title', 'Triage alert')}\n </ResponsiveDialogTitle>\n <ResponsiveDialogDescription>\n {tr(\n 'ecoAi.triage.description',\n 'Check whether this {{parameter}} reading of {{value}} {{unit}} is a real event or a sensor fault.',\n {\n parameter: measurement.parameter,\n value: formatNumber(measurement.value),\n unit: measurement.unit,\n }\n )}\n </ResponsiveDialogDescription>\n </ResponsiveDialogHeader>\n\n <div className=\"grid gap-4 py-2\" data-testid=\"eco-ai-triage\">\n {/* Threshold picker */}\n {thresholdsQuery.isLoading ? (\n <p className=\"text-sm text-text-secondary\">\n {tr('ecoAi.triage.loadingThresholds', 'Loading thresholds…')}\n </p>\n ) : candidates.length === 0 ? (\n <p className=\"rounded-lg bg-bg-sunken/60 p-3 text-sm text-text-secondary\">\n {tr(\n 'ecoAi.triage.noThreshold',\n 'No alert threshold covers {{parameter}} at this site, so there is nothing to triage against.',\n { parameter: measurement.parameter }\n )}\n </p>\n ) : (\n <div className=\"grid gap-1.5\">\n <Label htmlFor=\"ecoAiTriageThreshold\">\n {tr('ecoAi.triage.threshold', 'Threshold')}\n </Label>\n <Select value={thresholdId} onValueChange={setPickedThresholdId}>\n <SelectTrigger id=\"ecoAiTriageThreshold\">\n <SelectValue />\n </SelectTrigger>\n <SelectContent>\n {candidates.map((t) => (\n <SelectItem key={t.id} value={t.id}>\n {describeRule(t)} · {formatEnumLabel(t.severity)}\n {t.isActive ? '' : ` · ${tr('ecoAi.triage.inactive', 'inactive')}`}\n </SelectItem>\n ))}\n </SelectContent>\n </Select>\n </div>\n )}\n\n {aiError && <AiErrorNotice error={aiError} />}\n\n {/* Verdict */}\n {result && (\n <div className=\"grid gap-4\" data-testid=\"eco-ai-triage-result\">\n <div className=\"flex flex-wrap items-center gap-3\">\n <StatusBadge status={result.classification} />\n <span className=\"text-sm text-text-secondary\">\n {tr('ecoAi.triage.confidence', 'Confidence {{value}}', {\n value: formatConfidence(result.confidence),\n })}\n </span>\n <span className=\"text-sm text-text-secondary\">\n · {formatEnumLabel(result.recommendation)}\n </span>\n {!result.breachesThreshold && (\n <span className=\"text-xs text-text-tertiary\">\n {tr(\n 'ecoAi.triage.noLongerBreaches',\n 'This reading does not breach the selected threshold.'\n )}\n </span>\n )}\n </div>\n\n {result.rationale.length > 0 && (\n <div>\n <h3 className=\"mb-1.5 text-sm font-semibold text-text-primary\">\n {tr('ecoAi.triage.rationale', 'Why')}\n </h3>\n <ul className=\"list-disc space-y-1 pl-5 text-sm text-text-secondary\">\n {result.rationale.map((line, i) => (\n <li key={`${i}:${line}`}>{line}</li>\n ))}\n </ul>\n </div>\n )}\n\n {result.signals.length > 0 && (\n <div>\n <h3 className=\"mb-1.5 text-sm font-semibold text-text-primary\">\n {tr('ecoAi.triage.signals', 'Signals')}\n </h3>\n <ul className=\"space-y-1.5 text-sm\">\n {result.signals.map((s) => (\n <li key={s.code} className=\"flex items-start gap-2\">\n <StatusBadge status={s.favours} className=\"shrink-0\" />\n <span className=\"text-text-secondary\">{s.detail}</span>\n </li>\n ))}\n </ul>\n </div>\n )}\n\n <div>\n <h3 className=\"mb-1.5 text-sm font-semibold text-text-primary\">\n {tr('ecoAi.triage.citedReadings', 'Cited readings ({{count}})', {\n count: cited.length,\n })}\n </h3>\n <CitedReadingsList readings={cited} focusId={measurement.id} />\n </div>\n\n {/* Decision card: applied only on the user's click. */}\n {result.proposedAction && !dismissed && (\n <div\n data-testid=\"eco-ai-decision-card\"\n className=\"rounded-xl border border-border-seam bg-bg-sunken/60 p-4\"\n >\n <div className=\"flex items-start gap-2\">\n <ShieldAlert className=\"mt-0.5 h-4 w-4 shrink-0 text-status-warning-text\" />\n <div className=\"min-w-0\">\n <p className=\"text-sm font-semibold text-text-primary\">\n {tr(\n 'ecoAi.triage.proposalTitle',\n 'Suggested action: mark {{sensor}} as {{status}}',\n {\n sensor: sensorName(result.proposedAction.sensorId),\n status: formatEnumLabel(result.proposedAction.targetStatus),\n }\n )}\n </p>\n <p className=\"mt-1 text-sm text-text-secondary\">\n {result.proposedAction.reason}\n </p>\n {/* Only true until the user applies the proposal. */}\n {!applied && (\n <p className=\"mt-1 text-xs text-text-tertiary\">\n {tr(\n 'ecoAi.triage.proposalNotApplied',\n 'Nothing has changed yet. The sensor status changes only if you apply it.'\n )}\n </p>\n )}\n </div>\n </div>\n {applied ? (\n <p\n className=\"mt-3 flex items-center gap-1.5 text-sm text-status-success-text\"\n data-testid=\"eco-ai-decision-applied\"\n >\n <CheckCircle2 className=\"h-4 w-4\" />\n {tr('ecoAi.triage.applied', 'Sensor marked as {{status}}.', {\n status: formatEnumLabel(result.proposedAction.targetStatus),\n })}\n </p>\n ) : (\n <div className=\"mt-3 flex flex-wrap gap-2\">\n <Button\n size=\"sm\"\n onClick={applyProposal}\n disabled={setSensorStatus.isPending}\n data-testid=\"eco-ai-decision-apply\"\n >\n {setSensorStatus.isPending\n ? tr('ecoAi.triage.applying', 'Applying…')\n : tr('ecoAi.triage.apply', 'Mark sensor {{status}}', {\n status: formatEnumLabel(result.proposedAction.targetStatus),\n })}\n </Button>\n <Button size=\"sm\" variant=\"ghost\" onClick={() => setDismissed(true)}>\n {tr('ecoAi.triage.dismiss', 'Dismiss')}\n </Button>\n </div>\n )}\n {applyError && <p className=\"mt-2 text-sm text-status-error-text\">{applyError}</p>}\n </div>\n )}\n </div>\n )}\n </div>\n\n <ResponsiveDialogFooter className=\"flex-col items-stretch gap-2 sm:flex-row sm:items-center sm:justify-between\">\n <AiCreditHint />\n <div className=\"flex gap-2 sm:justify-end\">\n <Button variant=\"outline\" onClick={onClose}>\n {tr('ecoAi.close', 'Close')}\n </Button>\n <Button\n onClick={runTriage}\n disabled={!thresholdId || triage.isPending}\n data-testid=\"eco-ai-triage-run\"\n >\n {triage.isPending\n ? tr('ecoAi.triage.running', 'Triaging…')\n : result\n ? tr('ecoAi.triage.rerun', 'Run again')\n : tr('ecoAi.triage.run', 'Run triage')}\n </Button>\n </div>\n </ResponsiveDialogFooter>\n </>\n );\n};\n"],"mappings":"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"}
|
|
@@ -52,4 +52,5 @@ export declare function formatBound(value: number): string;
|
|
|
52
52
|
export declare function proposalToPrefill(proposal: EcoThresholdProposal): ThresholdPrefill | null;
|
|
53
53
|
/** Historical breach share (0..1) as a percentage with one decimal. */
|
|
54
54
|
export declare function formatBreachRate(rate: number | null | undefined): string;
|
|
55
|
+
export declare function userFacingAiNotes(notes: readonly string[] | null | undefined): string[];
|
|
55
56
|
//# sourceMappingURL=aiMonitoring.d.ts.map
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"aiMonitoring.d.ts","sourceRoot":"","sources":["../../../src/ecoimpacthub/utils/aiMonitoring.ts"],"names":[],"mappings":"AAAA;;;;GAIG;AASH,OAAO,KAAK,EACV,iBAAiB,EACjB,mBAAmB,EACnB,cAAc,EACd,cAAc,EACd,oBAAoB,EACrB,MAAM,UAAU,CAAC;AAIlB,MAAM,MAAM,cAAc,GAAG,OAAO,GAAG,UAAU,GAAG,OAAO,CAAC;AAE5D,MAAM,WAAW,cAAc;IAC7B,IAAI,EAAE,cAAc,CAAC;IACrB,OAAO,EAAE,MAAM,CAAC;CACjB;AAKD;;;GAGG;AACH,wBAAgB,kBAAkB,CAAC,GAAG,EAAE,OAAO,EAAE,QAAQ,EAAE,MAAM,GAAG,cAAc,CAkCjF;AAID;;;GAGG;AACH,wBAAgB,kBAAkB,CAChC,WAAW,EAAE,IAAI,CAAC,cAAc,EAAE,QAAQ,GAAG,WAAW,CAAC,EACzD,UAAU,EAAE,SAAS,iBAAiB,EAAE,GACvC,iBAAiB,EAAE,CAIrB;AAED,MAAM,WAAW,YAAY;IAC3B,EAAE,EAAE,MAAM,CAAC;IACX,WAAW,EAAE,cAAc,GAAG,IAAI,CAAC;CACpC;AAED,6EAA6E;AAC7E,wBAAgB,oBAAoB,CAClC,GAAG,EAAE,SAAS,MAAM,EAAE,EACtB,QAAQ,EAAE,SAAS,cAAc,EAAE,GAClC,YAAY,EAAE,CAUhB;AAED,mEAAmE;AACnE,wBAAgB,gBAAgB,CAAC,UAAU,EAAE,MAAM,GAAG,MAAM,CAI3D;AAID,MAAM,MAAM,mBAAmB,GAAG,IAAI,GAAG,KAAK,GAAG,KAAK,GAAG,MAAM,CAAC;AAEhE,eAAO,MAAM,sBAAsB,EAAE,SAAS,mBAAmB,EAAiC,CAAC;AAWnG,gEAAgE;AAChE,wBAAgB,WAAW,CACzB,MAAM,EAAE,mBAAmB,EAC3B,GAAG,GAAE,IAAiB,GACrB;IAAE,WAAW,EAAE,MAAM,CAAC;IAAC,SAAS,EAAE,MAAM,CAAA;CAAE,CAM5C;AAED,6EAA6E;AAC7E,wBAAgB,gBAAgB,CAC9B,YAAY,EAAE,SAAS,IAAI,CAAC,cAAc,EAAE,WAAW,CAAC,EAAE,EAC1D,UAAU,GAAE,SAAS,IAAI,CAAC,iBAAiB,EAAE,WAAW,CAAC,EAAO,GAC/D,MAAM,EAAE,CAKV;AAID,0EAA0E;AAC1E,wBAAgB,cAAc,CAC5B,SAAS,EAAE,SAAS,mBAAmB,EAAE,GACxC;IAAE,IAAI,EAAE,cAAc,CAAC;IAAC,KAAK,EAAE,mBAAmB,EAAE,CAAA;CAAE,EAAE,CAO1D;AAID,MAAM,WAAW,gBAAgB;IAC/B,SAAS,EAAE,eAAe,CAAC;IAC3B,QAAQ,EAAE,MAAM,CAAC;IACjB,QAAQ,EAAE,MAAM,CAAC;IACjB,IAAI,EAAE,MAAM,GAAG,IAAI,CAAC;CACrB;AAED,sEAAsE;AACtE,wBAAgB,WAAW,CAAC,KAAK,EAAE,MAAM,GAAG,MAAM,CAEjD;AAED;;;GAGG;AACH,wBAAgB,iBAAiB,CAAC,QAAQ,EAAE,oBAAoB,GAAG,gBAAgB,GAAG,IAAI,CAUzF;AAED,uEAAuE;AACvE,wBAAgB,gBAAgB,CAAC,IAAI,EAAE,MAAM,GAAG,IAAI,GAAG,SAAS,GAAG,MAAM,CAIxE"}
|
|
1
|
+
{"version":3,"file":"aiMonitoring.d.ts","sourceRoot":"","sources":["../../../src/ecoimpacthub/utils/aiMonitoring.ts"],"names":[],"mappings":"AAAA;;;;GAIG;AASH,OAAO,KAAK,EACV,iBAAiB,EACjB,mBAAmB,EACnB,cAAc,EACd,cAAc,EACd,oBAAoB,EACrB,MAAM,UAAU,CAAC;AAIlB,MAAM,MAAM,cAAc,GAAG,OAAO,GAAG,UAAU,GAAG,OAAO,CAAC;AAE5D,MAAM,WAAW,cAAc;IAC7B,IAAI,EAAE,cAAc,CAAC;IACrB,OAAO,EAAE,MAAM,CAAC;CACjB;AAKD;;;GAGG;AACH,wBAAgB,kBAAkB,CAAC,GAAG,EAAE,OAAO,EAAE,QAAQ,EAAE,MAAM,GAAG,cAAc,CAkCjF;AAID;;;GAGG;AACH,wBAAgB,kBAAkB,CAChC,WAAW,EAAE,IAAI,CAAC,cAAc,EAAE,QAAQ,GAAG,WAAW,CAAC,EACzD,UAAU,EAAE,SAAS,iBAAiB,EAAE,GACvC,iBAAiB,EAAE,CAIrB;AAED,MAAM,WAAW,YAAY;IAC3B,EAAE,EAAE,MAAM,CAAC;IACX,WAAW,EAAE,cAAc,GAAG,IAAI,CAAC;CACpC;AAED,6EAA6E;AAC7E,wBAAgB,oBAAoB,CAClC,GAAG,EAAE,SAAS,MAAM,EAAE,EACtB,QAAQ,EAAE,SAAS,cAAc,EAAE,GAClC,YAAY,EAAE,CAUhB;AAED,mEAAmE;AACnE,wBAAgB,gBAAgB,CAAC,UAAU,EAAE,MAAM,GAAG,MAAM,CAI3D;AAID,MAAM,MAAM,mBAAmB,GAAG,IAAI,GAAG,KAAK,GAAG,KAAK,GAAG,MAAM,CAAC;AAEhE,eAAO,MAAM,sBAAsB,EAAE,SAAS,mBAAmB,EAAiC,CAAC;AAWnG,gEAAgE;AAChE,wBAAgB,WAAW,CACzB,MAAM,EAAE,mBAAmB,EAC3B,GAAG,GAAE,IAAiB,GACrB;IAAE,WAAW,EAAE,MAAM,CAAC;IAAC,SAAS,EAAE,MAAM,CAAA;CAAE,CAM5C;AAED,6EAA6E;AAC7E,wBAAgB,gBAAgB,CAC9B,YAAY,EAAE,SAAS,IAAI,CAAC,cAAc,EAAE,WAAW,CAAC,EAAE,EAC1D,UAAU,GAAE,SAAS,IAAI,CAAC,iBAAiB,EAAE,WAAW,CAAC,EAAO,GAC/D,MAAM,EAAE,CAKV;AAID,0EAA0E;AAC1E,wBAAgB,cAAc,CAC5B,SAAS,EAAE,SAAS,mBAAmB,EAAE,GACxC;IAAE,IAAI,EAAE,cAAc,CAAC;IAAC,KAAK,EAAE,mBAAmB,EAAE,CAAA;CAAE,EAAE,CAO1D;AAID,MAAM,WAAW,gBAAgB;IAC/B,SAAS,EAAE,eAAe,CAAC;IAC3B,QAAQ,EAAE,MAAM,CAAC;IACjB,QAAQ,EAAE,MAAM,CAAC;IACjB,IAAI,EAAE,MAAM,GAAG,IAAI,CAAC;CACrB;AAED,sEAAsE;AACtE,wBAAgB,WAAW,CAAC,KAAK,EAAE,MAAM,GAAG,MAAM,CAEjD;AAED;;;GAGG;AACH,wBAAgB,iBAAiB,CAAC,QAAQ,EAAE,oBAAoB,GAAG,gBAAgB,GAAG,IAAI,CAUzF;AAED,uEAAuE;AACvE,wBAAgB,gBAAgB,CAAC,IAAI,EAAE,MAAM,GAAG,IAAI,GAAG,SAAS,GAAG,MAAM,CAIxE;AAWD,wBAAgB,iBAAiB,CAAC,KAAK,EAAE,SAAS,MAAM,EAAE,GAAG,IAAI,GAAG,SAAS,GAAG,MAAM,EAAE,CAGvF"}
|
|
@@ -94,7 +94,11 @@ function v(e) {
|
|
|
94
94
|
let t = Math.min(1, Math.max(0, e)) * 100;
|
|
95
95
|
return `${Number(t.toFixed(1))}%`;
|
|
96
96
|
}
|
|
97
|
+
var y = /\beco[A-Z][A-Za-z]+\b/;
|
|
98
|
+
function b(e) {
|
|
99
|
+
return e ? e.filter((e) => e.trim().length > 0 && !y.test(e)) : [];
|
|
100
|
+
}
|
|
97
101
|
//#endregion
|
|
98
|
-
export { l as ANOMALY_PERIOD_PRESETS, a as classifyEcoAiError, g as formatBound, v as formatBreachRate, c as formatConfidence, h as groupAnomalies, o as matchingThresholds, p as parameterOptions, f as periodRange, _ as proposalToPrefill, s as resolveCitedReadings };
|
|
102
|
+
export { l as ANOMALY_PERIOD_PRESETS, a as classifyEcoAiError, g as formatBound, v as formatBreachRate, c as formatConfidence, h as groupAnomalies, o as matchingThresholds, p as parameterOptions, f as periodRange, _ as proposalToPrefill, s as resolveCitedReadings, b as userFacingAiNotes };
|
|
99
103
|
|
|
100
104
|
//# sourceMappingURL=aiMonitoring.js.map
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"aiMonitoring.js","names":[],"sources":["../../../src/ecoimpacthub/utils/aiMonitoring.ts"],"sourcesContent":["/**\n * Pure helpers behind the AI monitoring UI (triage decision card, anomaly\n * panel, threshold suggestion). Kept free of React so they are unit-tested.\n * @module ecoimpacthub/utils\n */\n\nimport {\n buildQuotaExhaustedMessage,\n extractQuotaExhaustedExtensions,\n} from '@burdenoff/fe-libs/shared/utils';\n\nimport { graphQLErrorsOf } from './graphqlErrors';\n\nimport type {\n EcoAlertThreshold,\n EcoAnomalyCandidate,\n EcoAnomalyKind,\n EcoMeasurement,\n EcoThresholdProposal,\n} from '../types';\n\n// ── Errors ──────────────────────────────────────────────────────────────────\n\nexport type EcoAiErrorKind = 'quota' | 'disabled' | 'other';\n\nexport interface EcoAiErrorInfo {\n kind: EcoAiErrorKind;\n message: string;\n}\n\nconst FEATURE_DISABLED_CODE = 'FEATURE_DISABLED';\nconst AI_CREDITS_QUOTA_SUFFIX = '.ai.credits';\n\n/**\n * Classify a failed AI monitoring call. QUOTA_EXHAUSTED and FEATURE_DISABLED\n * get dedicated copy; everything else keeps the backend's own message.\n */\nexport function classifyEcoAiError(err: unknown, fallback: string): EcoAiErrorInfo {\n const quota = extractQuotaExhaustedExtensions(err);\n if (quota) {\n // `ecoimpacthub.ai.credits` is the meter's machine name; say what it is.\n const quotaName = quota.quotaName?.endsWith(AI_CREDITS_QUOTA_SUFFIX)\n ? 'AI credits'\n : quota.quotaName;\n return {\n kind: 'quota',\n message: buildQuotaExhaustedMessage({ ...quota, quotaName, field: undefined }),\n };\n }\n\n const errors = graphQLErrorsOf(err);\n const disabled = errors.find((e) => e.extensions?.code === FEATURE_DISABLED_CODE);\n const message = err instanceof Error && err.message ? err.message : fallback;\n if (disabled || message.includes(FEATURE_DISABLED_CODE)) {\n // The svc localises the message itself (monitoring.errors.aiMonitoringDisabled).\n const serverMessage = disabled?.message;\n return {\n kind: 'disabled',\n message:\n typeof serverMessage === 'string' && serverMessage.trim()\n ? serverMessage\n : 'AI monitoring is turned off for this workspace.',\n };\n }\n\n const userMessage = errors.find((e) => typeof e.extensions?.userMessage === 'string')?.extensions\n ?.userMessage;\n return {\n kind: 'other',\n message: typeof userMessage === 'string' && userMessage.trim() ? userMessage : message,\n };\n}\n\n// ── Triage ──────────────────────────────────────────────────────────────────\n\n/**\n * Thresholds a measurement can be triaged against: same site and parameter\n * (the svc rejects any other pairing), active ones first.\n */\nexport function matchingThresholds(\n measurement: Pick<EcoMeasurement, 'siteId' | 'parameter'>,\n thresholds: readonly EcoAlertThreshold[]\n): EcoAlertThreshold[] {\n return thresholds\n .filter((t) => t.siteId === measurement.siteId && t.parameter === measurement.parameter)\n .sort((a, b) => Number(b.isActive) - Number(a.isActive));\n}\n\nexport interface CitedReading {\n id: string;\n measurement: EcoMeasurement | null;\n}\n\n/** Resolve cited measurement ids against the readings loaded on the page. */\nexport function resolveCitedReadings(\n ids: readonly string[],\n readings: readonly EcoMeasurement[]\n): CitedReading[] {\n const byId = new Map(readings.map((m) => [m.id, m] as const));\n const seen = new Set<string>();\n const out: CitedReading[] = [];\n for (const id of ids) {\n if (seen.has(id)) continue;\n seen.add(id);\n out.push({ id, measurement: byId.get(id) ?? null });\n }\n return out;\n}\n\n/** Confidence (0..1) as a whole percentage, e.g. 0.834 → \"83%\". */\nexport function formatConfidence(confidence: number): string {\n if (!Number.isFinite(confidence)) return '—';\n const clamped = Math.min(1, Math.max(0, confidence));\n return `${Math.round(clamped * 100)}%`;\n}\n\n// ── Anomalies ───────────────────────────────────────────────────────────────\n\nexport type AnomalyPeriodPreset = '7d' | '30d' | '90d' | '365d';\n\nexport const ANOMALY_PERIOD_PRESETS: readonly AnomalyPeriodPreset[] = ['7d', '30d', '90d', '365d'];\n\nconst PRESET_DAYS: Record<AnomalyPeriodPreset, number> = {\n '7d': 7,\n '30d': 30,\n '90d': 90,\n '365d': 365,\n};\n\nconst DAY_MS = 86_400_000;\n\n/** ISO period ending `now` (the svc accepts up to 366 days). */\nexport function periodRange(\n preset: AnomalyPeriodPreset,\n now: Date = new Date()\n): { periodStart: string; periodEnd: string } {\n const end = now.getTime();\n return {\n periodStart: new Date(end - PRESET_DAYS[preset] * DAY_MS).toISOString(),\n periodEnd: new Date(end).toISOString(),\n };\n}\n\n/** Distinct parameters a site has data or rules for, sorted for a picker. */\nexport function parameterOptions(\n measurements: readonly Pick<EcoMeasurement, 'parameter'>[],\n thresholds: readonly Pick<EcoAlertThreshold, 'parameter'>[] = []\n): string[] {\n const set = new Set<string>();\n for (const m of measurements) if (m.parameter.trim()) set.add(m.parameter);\n for (const t of thresholds) if (t.parameter.trim()) set.add(t.parameter);\n return [...set].sort((a, b) => a.localeCompare(b));\n}\n\nconst KIND_ORDER: readonly EcoAnomalyKind[] = ['SPIKE', 'DROP', 'FLATLINE'];\n\n/** Anomalies grouped by kind (spikes, drops, flatlines), newest first. */\nexport function groupAnomalies(\n anomalies: readonly EcoAnomalyCandidate[]\n): { kind: EcoAnomalyKind; items: EcoAnomalyCandidate[] }[] {\n return KIND_ORDER.map((kind) => ({\n kind,\n items: anomalies\n .filter((a) => a.kind === kind)\n .sort((a, b) => Date.parse(b.recordedAt) - Date.parse(a.recordedAt)),\n })).filter((g) => g.items.length > 0);\n}\n\n// ── Threshold proposal ──────────────────────────────────────────────────────\n\nexport interface ThresholdPrefill {\n direction: 'OUTSIDE_RANGE';\n minValue: string;\n maxValue: string;\n unit: string | null;\n}\n\n/** Round to 6 significant digits and print without exponent noise. */\nexport function formatBound(value: number): string {\n return String(Number(value.toPrecision(6)));\n}\n\n/**\n * Form values for the threshold create dialog, or null when the proposal has\n * no usable band (INSUFFICIENT_DATA, or a bound missing).\n */\nexport function proposalToPrefill(proposal: EcoThresholdProposal): ThresholdPrefill | null {\n if (proposal.status !== 'OK') return null;\n if (proposal.minValue == null || proposal.maxValue == null) return null;\n if (!Number.isFinite(proposal.minValue) || !Number.isFinite(proposal.maxValue)) return null;\n return {\n direction: 'OUTSIDE_RANGE',\n minValue: formatBound(proposal.minValue),\n maxValue: formatBound(proposal.maxValue),\n unit: proposal.unit?.trim() ? proposal.unit : null,\n };\n}\n\n/** Historical breach share (0..1) as a percentage with one decimal. */\nexport function formatBreachRate(rate: number | null | undefined): string {\n if (rate == null || !Number.isFinite(rate)) return '—';\n const pct = Math.min(1, Math.max(0, rate)) * 100;\n return `${Number(pct.toFixed(1))}%`;\n}\n"],"mappings":"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"}
|
|
1
|
+
{"version":3,"file":"aiMonitoring.js","names":[],"sources":["../../../src/ecoimpacthub/utils/aiMonitoring.ts"],"sourcesContent":["/**\n * Pure helpers behind the AI monitoring UI (triage decision card, anomaly\n * panel, threshold suggestion). Kept free of React so they are unit-tested.\n * @module ecoimpacthub/utils\n */\n\nimport {\n buildQuotaExhaustedMessage,\n extractQuotaExhaustedExtensions,\n} from '@burdenoff/fe-libs/shared/utils';\n\nimport { graphQLErrorsOf } from './graphqlErrors';\n\nimport type {\n EcoAlertThreshold,\n EcoAnomalyCandidate,\n EcoAnomalyKind,\n EcoMeasurement,\n EcoThresholdProposal,\n} from '../types';\n\n// ── Errors ──────────────────────────────────────────────────────────────────\n\nexport type EcoAiErrorKind = 'quota' | 'disabled' | 'other';\n\nexport interface EcoAiErrorInfo {\n kind: EcoAiErrorKind;\n message: string;\n}\n\nconst FEATURE_DISABLED_CODE = 'FEATURE_DISABLED';\nconst AI_CREDITS_QUOTA_SUFFIX = '.ai.credits';\n\n/**\n * Classify a failed AI monitoring call. QUOTA_EXHAUSTED and FEATURE_DISABLED\n * get dedicated copy; everything else keeps the backend's own message.\n */\nexport function classifyEcoAiError(err: unknown, fallback: string): EcoAiErrorInfo {\n const quota = extractQuotaExhaustedExtensions(err);\n if (quota) {\n // `ecoimpacthub.ai.credits` is the meter's machine name; say what it is.\n const quotaName = quota.quotaName?.endsWith(AI_CREDITS_QUOTA_SUFFIX)\n ? 'AI credits'\n : quota.quotaName;\n return {\n kind: 'quota',\n message: buildQuotaExhaustedMessage({ ...quota, quotaName, field: undefined }),\n };\n }\n\n const errors = graphQLErrorsOf(err);\n const disabled = errors.find((e) => e.extensions?.code === FEATURE_DISABLED_CODE);\n const message = err instanceof Error && err.message ? err.message : fallback;\n if (disabled || message.includes(FEATURE_DISABLED_CODE)) {\n // The svc localises the message itself (monitoring.errors.aiMonitoringDisabled).\n const serverMessage = disabled?.message;\n return {\n kind: 'disabled',\n message:\n typeof serverMessage === 'string' && serverMessage.trim()\n ? serverMessage\n : 'AI monitoring is turned off for this workspace.',\n };\n }\n\n const userMessage = errors.find((e) => typeof e.extensions?.userMessage === 'string')?.extensions\n ?.userMessage;\n return {\n kind: 'other',\n message: typeof userMessage === 'string' && userMessage.trim() ? userMessage : message,\n };\n}\n\n// ── Triage ──────────────────────────────────────────────────────────────────\n\n/**\n * Thresholds a measurement can be triaged against: same site and parameter\n * (the svc rejects any other pairing), active ones first.\n */\nexport function matchingThresholds(\n measurement: Pick<EcoMeasurement, 'siteId' | 'parameter'>,\n thresholds: readonly EcoAlertThreshold[]\n): EcoAlertThreshold[] {\n return thresholds\n .filter((t) => t.siteId === measurement.siteId && t.parameter === measurement.parameter)\n .sort((a, b) => Number(b.isActive) - Number(a.isActive));\n}\n\nexport interface CitedReading {\n id: string;\n measurement: EcoMeasurement | null;\n}\n\n/** Resolve cited measurement ids against the readings loaded on the page. */\nexport function resolveCitedReadings(\n ids: readonly string[],\n readings: readonly EcoMeasurement[]\n): CitedReading[] {\n const byId = new Map(readings.map((m) => [m.id, m] as const));\n const seen = new Set<string>();\n const out: CitedReading[] = [];\n for (const id of ids) {\n if (seen.has(id)) continue;\n seen.add(id);\n out.push({ id, measurement: byId.get(id) ?? null });\n }\n return out;\n}\n\n/** Confidence (0..1) as a whole percentage, e.g. 0.834 → \"83%\". */\nexport function formatConfidence(confidence: number): string {\n if (!Number.isFinite(confidence)) return '—';\n const clamped = Math.min(1, Math.max(0, confidence));\n return `${Math.round(clamped * 100)}%`;\n}\n\n// ── Anomalies ───────────────────────────────────────────────────────────────\n\nexport type AnomalyPeriodPreset = '7d' | '30d' | '90d' | '365d';\n\nexport const ANOMALY_PERIOD_PRESETS: readonly AnomalyPeriodPreset[] = ['7d', '30d', '90d', '365d'];\n\nconst PRESET_DAYS: Record<AnomalyPeriodPreset, number> = {\n '7d': 7,\n '30d': 30,\n '90d': 90,\n '365d': 365,\n};\n\nconst DAY_MS = 86_400_000;\n\n/** ISO period ending `now` (the svc accepts up to 366 days). */\nexport function periodRange(\n preset: AnomalyPeriodPreset,\n now: Date = new Date()\n): { periodStart: string; periodEnd: string } {\n const end = now.getTime();\n return {\n periodStart: new Date(end - PRESET_DAYS[preset] * DAY_MS).toISOString(),\n periodEnd: new Date(end).toISOString(),\n };\n}\n\n/** Distinct parameters a site has data or rules for, sorted for a picker. */\nexport function parameterOptions(\n measurements: readonly Pick<EcoMeasurement, 'parameter'>[],\n thresholds: readonly Pick<EcoAlertThreshold, 'parameter'>[] = []\n): string[] {\n const set = new Set<string>();\n for (const m of measurements) if (m.parameter.trim()) set.add(m.parameter);\n for (const t of thresholds) if (t.parameter.trim()) set.add(t.parameter);\n return [...set].sort((a, b) => a.localeCompare(b));\n}\n\nconst KIND_ORDER: readonly EcoAnomalyKind[] = ['SPIKE', 'DROP', 'FLATLINE'];\n\n/** Anomalies grouped by kind (spikes, drops, flatlines), newest first. */\nexport function groupAnomalies(\n anomalies: readonly EcoAnomalyCandidate[]\n): { kind: EcoAnomalyKind; items: EcoAnomalyCandidate[] }[] {\n return KIND_ORDER.map((kind) => ({\n kind,\n items: anomalies\n .filter((a) => a.kind === kind)\n .sort((a, b) => Date.parse(b.recordedAt) - Date.parse(a.recordedAt)),\n })).filter((g) => g.items.length > 0);\n}\n\n// ── Threshold proposal ──────────────────────────────────────────────────────\n\nexport interface ThresholdPrefill {\n direction: 'OUTSIDE_RANGE';\n minValue: string;\n maxValue: string;\n unit: string | null;\n}\n\n/** Round to 6 significant digits and print without exponent noise. */\nexport function formatBound(value: number): string {\n return String(Number(value.toPrecision(6)));\n}\n\n/**\n * Form values for the threshold create dialog, or null when the proposal has\n * no usable band (INSUFFICIENT_DATA, or a bound missing).\n */\nexport function proposalToPrefill(proposal: EcoThresholdProposal): ThresholdPrefill | null {\n if (proposal.status !== 'OK') return null;\n if (proposal.minValue == null || proposal.maxValue == null) return null;\n if (!Number.isFinite(proposal.minValue) || !Number.isFinite(proposal.maxValue)) return null;\n return {\n direction: 'OUTSIDE_RANGE',\n minValue: formatBound(proposal.minValue),\n maxValue: formatBound(proposal.maxValue),\n unit: proposal.unit?.trim() ? proposal.unit : null,\n };\n}\n\n/** Historical breach share (0..1) as a percentage with one decimal. */\nexport function formatBreachRate(rate: number | null | undefined): string {\n if (rate == null || !Number.isFinite(rate)) return '—';\n const pct = Math.min(1, Math.max(0, rate)) * 100;\n return `${Number(pct.toFixed(1))}%`;\n}\n\n/**\n * The backend writes run `notes` for every caller, including agents and MCP\n * clients, so some of them name the GraphQL operation to call next (for\n * example \"create it with ecoCreateThreshold after review\"). Those notes are\n * instructions for API callers; the UI states the same thing in its own copy,\n * so they are dropped here instead of showing an operation name to end users.\n */\nconst API_OPERATION_NAME = /\\beco[A-Z][A-Za-z]+\\b/;\n\nexport function userFacingAiNotes(notes: readonly string[] | null | undefined): string[] {\n if (!notes) return [];\n return notes.filter((note) => note.trim().length > 0 && !API_OPERATION_NAME.test(note));\n}\n"],"mappings":"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"}
|