@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.
@@ -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;AAyBnD,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,CAsPlD,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 l, useEcoAiErrorReporter as u } from "../../hooks/useEcoAiMonitoring.js";
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 d, AiErrorNotice as f, CitedReadingsList as p } from "./AiShared.js";
9
- import { useMemo as m, useState as h } from "react";
10
- import { Activity as g, ScanSearch as _ } from "lucide-react";
11
- import { Fragment as v, jsx as y, jsxs as b } from "react/jsx-runtime";
12
- import { Button as x, GlassCard as S, Label as C, Select as w, SelectContent as T, SelectItem as E, SelectTrigger as D, SelectValue as O } from "@burdenoff/fe-libs/ui";
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 k = {
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
- }, A = {
19
+ }, j = {
20
20
  SPIKE: ["ecoAi.anomalies.kindSpike", "Spikes"],
21
21
  DROP: ["ecoAi.anomalies.kindDrop", "Drops"],
22
22
  FLATLINE: ["ecoAi.anomalies.kindFlatline", "Flatlines"]
23
- }, j = ({ siteId: j, parameters: M, sensors: N = [] }) => {
24
- let P = i(), [F, I] = h(""), [L, R] = h("30d"), z = F || M[0] || "", B = l(), V = u(), [H, U] = h(null), [W, G] = h(null), [K, q] = h(null), J = r({
25
- siteId: j,
26
- parameter: H?.parameter ?? z,
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
- }), Y = m(() => J.data?.items ?? [], [J.data]), X = m(() => H ? o(H.anomalies) : [], [H]), Z = (e) => e && N.find((t) => t.id === e)?.name || null;
32
- return /* @__PURE__ */ y("div", {
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__ */ b(S, {
34
+ children: /* @__PURE__ */ x(C, {
35
35
  treatment: "plain",
36
36
  className: "p-5",
37
37
  children: [
38
- M.length === 0 ? /* @__PURE__ */ y("p", {
38
+ N.length === 0 ? /* @__PURE__ */ b("p", {
39
39
  className: "text-sm text-text-secondary",
40
- children: P("ecoAi.anomalies.noParameters", "Record measurements at this site to analyse them for anomalies.")
41
- }) : /* @__PURE__ */ b(v, { children: [/* @__PURE__ */ b("div", {
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__ */ b("div", {
44
+ /* @__PURE__ */ x("div", {
45
45
  className: "grid gap-1.5",
46
- children: [/* @__PURE__ */ y(C, {
46
+ children: [/* @__PURE__ */ b(w, {
47
47
  htmlFor: "ecoAiAnomalyParam",
48
- children: P("ecoAi.anomalies.parameter", "Parameter")
49
- }), /* @__PURE__ */ b(w, {
50
- value: z,
48
+ children: F("ecoAi.anomalies.parameter", "Parameter")
49
+ }), /* @__PURE__ */ x(T, {
50
+ value: B,
51
51
  onValueChange: (e) => {
52
- I(e), U(null), G(null);
52
+ L(e), W(null), K(null);
53
53
  },
54
- children: [/* @__PURE__ */ y(D, {
54
+ children: [/* @__PURE__ */ b(O, {
55
55
  id: "ecoAiAnomalyParam",
56
56
  className: "w-48",
57
- children: /* @__PURE__ */ y(O, {})
58
- }), /* @__PURE__ */ y(T, { children: M.map((e) => /* @__PURE__ */ y(E, {
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__ */ b("div", {
64
+ /* @__PURE__ */ x("div", {
65
65
  className: "grid gap-1.5",
66
- children: [/* @__PURE__ */ y(C, {
66
+ children: [/* @__PURE__ */ b(w, {
67
67
  htmlFor: "ecoAiAnomalyPeriod",
68
- children: P("ecoAi.anomalies.period", "Period")
69
- }), /* @__PURE__ */ b(w, {
70
- value: L,
68
+ children: F("ecoAi.anomalies.period", "Period")
69
+ }), /* @__PURE__ */ x(T, {
70
+ value: R,
71
71
  onValueChange: (e) => {
72
- R(e), U(null), G(null);
72
+ z(e), W(null), K(null);
73
73
  },
74
- children: [/* @__PURE__ */ y(D, {
74
+ children: [/* @__PURE__ */ b(O, {
75
75
  id: "ecoAiAnomalyPeriod",
76
76
  className: "w-44",
77
- children: /* @__PURE__ */ y(O, {})
78
- }), /* @__PURE__ */ y(T, { children: a.map((e) => /* @__PURE__ */ y(E, {
77
+ children: /* @__PURE__ */ b(k, {})
78
+ }), /* @__PURE__ */ b(E, { children: a.map((e) => /* @__PURE__ */ b(D, {
79
79
  value: e,
80
- children: P(k[e][0], k[e][1])
80
+ children: F(A[e][0], A[e][1])
81
81
  }, e)) })]
82
82
  })]
83
83
  }),
84
- /* @__PURE__ */ b(x, {
84
+ /* @__PURE__ */ x(S, {
85
85
  onClick: async () => {
86
- if (z) {
87
- G(null), q(null);
86
+ if (B) {
87
+ K(null), J(null);
88
88
  try {
89
- U(await B.mutateAsync({
90
- siteId: j,
91
- parameter: z,
92
- ...s(L)
89
+ W(await V.mutateAsync({
90
+ siteId: M,
91
+ parameter: B,
92
+ ...s(R)
93
93
  }));
94
94
  } catch (e) {
95
- U(null), G(V(e, "EcoAiDetectAnomalies", P("ecoAi.anomalies.failed", "Anomaly detection failed.")));
95
+ W(null), K(H(e, "EcoAiDetectAnomalies", F("ecoAi.anomalies.failed", "Anomaly detection failed.")));
96
96
  }
97
97
  }
98
98
  },
99
- disabled: !z || B.isPending,
99
+ disabled: !B || V.isPending,
100
100
  "data-testid": "eco-ai-anomalies-run",
101
- children: [/* @__PURE__ */ y(_, { className: "mr-1.5 h-4 w-4" }), B.isPending ? P("ecoAi.anomalies.running", "Analysing…") : P("ecoAi.anomalies.run", "Detect anomalies")]
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__ */ y(d, { className: "mt-2" })] }),
105
- W && /* @__PURE__ */ y(f, {
106
- error: W,
104
+ }), /* @__PURE__ */ b(f, { className: "mt-2" })] }),
105
+ G && /* @__PURE__ */ b(p, {
106
+ error: G,
107
107
  className: "mt-4"
108
108
  }),
109
- H && H.status === "INSUFFICIENT_DATA" && /* @__PURE__ */ b("div", {
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__ */ y("p", {
113
+ /* @__PURE__ */ b("p", {
114
114
  className: "text-sm font-semibold text-text-primary",
115
- children: P("ecoAi.anomalies.notEnoughData", "Not enough data")
115
+ children: F("ecoAi.anomalies.notEnoughData", "Not enough data")
116
116
  }),
117
- /* @__PURE__ */ y("p", {
117
+ /* @__PURE__ */ b("p", {
118
118
  className: "mt-1 text-sm text-text-secondary",
119
- children: P("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: H.readingsAnalysed,
121
- parameter: H.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
- H.notes.length > 0 && /* @__PURE__ */ y("ul", {
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: H.notes.map((e, t) => /* @__PURE__ */ y("li", { children: e }, `${t}:${e}`))
126
+ children: Q.map((e, t) => /* @__PURE__ */ b("li", { children: e }, `${t}:${e}`))
127
127
  })
128
128
  ]
129
129
  }),
130
- H && H.status === "OK" && /* @__PURE__ */ b("div", {
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__ */ b("p", {
134
+ /* @__PURE__ */ x("p", {
135
135
  className: "text-xs text-text-tertiary",
136
- children: [P("ecoAi.anomalies.summary", "{{count}} reading(s) analysed · baseline median {{median}} {{unit}}", {
137
- count: H.readingsAnalysed,
138
- median: t(H.baseline?.median ?? null),
139
- unit: H.unit ?? ""
140
- }), H.truncated && ` · ${P("ecoAi.anomalies.truncated", "the series was truncated to the most recent readings")}`]
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
- X.length === 0 ? /* @__PURE__ */ b("p", {
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__ */ y(g, { className: "h-4 w-4 text-status-success-text" }), P("ecoAi.anomalies.none", "No spikes, drops or flatlines in this period.")]
146
- }) : X.map((r) => /* @__PURE__ */ b("div", { children: [/* @__PURE__ */ b("h3", {
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__ */ y(n, { status: r.kind }),
150
- P(A[r.kind]?.[0] ?? r.kind, A[r.kind]?.[1] ?? r.kind),
151
- /* @__PURE__ */ b("span", {
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__ */ y("ul", {
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 = K === n.measurementId;
164
- return /* @__PURE__ */ b("li", {
163
+ let r = q === n.measurementId;
164
+ return /* @__PURE__ */ x("li", {
165
165
  "data-testid": "eco-ai-anomaly",
166
- children: [/* @__PURE__ */ b("button", {
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: () => q(r ? null : n.measurementId),
171
- children: [/* @__PURE__ */ b("span", {
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
- H.unit ?? "",
177
- Z(n.sensorId) && /* @__PURE__ */ y("span", {
176
+ U.unit ?? "",
177
+ $(n.sensorId) && /* @__PURE__ */ b("span", {
178
178
  className: "ml-2 text-xs text-text-tertiary",
179
- children: Z(n.sensorId)
179
+ children: $(n.sensorId)
180
180
  })
181
181
  ]
182
- }), /* @__PURE__ */ b("span", {
182
+ }), /* @__PURE__ */ x("span", {
183
183
  className: "flex items-center gap-3 text-xs text-text-tertiary",
184
184
  children: [
185
- /* @__PURE__ */ y("span", { children: n.kind === "FLATLINE" ? P("ecoAi.anomalies.runLength", "run of {{n}}", { n: t(n.score, { maximumFractionDigits: 1 }) }) : P("ecoAi.anomalies.score", "z {{z}}", { z: t(n.score, { maximumFractionDigits: 1 }) }) }),
186
- /* @__PURE__ */ y("span", { children: e(n.recordedAt) }),
187
- /* @__PURE__ */ y("span", { children: P("ecoAi.anomalies.citedCount", "{{n}} cited", { n: n.evidenceMeasurementIds.length }) })
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__ */ y("div", {
190
+ }), r && /* @__PURE__ */ b("div", {
191
191
  className: "px-3 pb-3",
192
- children: /* @__PURE__ */ y(p, {
193
- readings: c(n.evidenceMeasurementIds, Y),
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
- H.notes.length > 0 && /* @__PURE__ */ y("ul", {
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: H.notes.map((e, t) => /* @__PURE__ */ y("li", { children: e }, `${t}:${e}`))
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 { j as AiAnomalyPanel };
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":"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"}
@@ -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,EAKL,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,CAqH1D,CAAC"}
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 i, useEcoAiProposeThreshold as a } from "../../hooks/useEcoAiMonitoring.js";
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 o, AiErrorNotice as s } from "./AiShared.js";
6
- import { useState as c } from "react";
7
- import { Wand2 as l } from "lucide-react";
8
- import { jsx as u, jsxs as d } from "react/jsx-runtime";
9
- import { Button as f } from "@burdenoff/fe-libs/ui";
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 p = ({ siteId: p, parameter: m, onSuggest: h }) => {
12
- let g = e(), _ = a(), v = i(), [y, b] = c(null), [x, S] = c(null), C = m.trim(), w = !!(p && C), T = async () => {
13
- if (w) {
14
- S(null), b(null);
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 _.mutateAsync({
17
- siteId: p,
18
- parameter: C
16
+ let e = await v.mutateAsync({
17
+ siteId: m,
18
+ parameter: w
19
19
  });
20
- b(e);
20
+ x(e);
21
21
  let t = r(e);
22
- t && h(t);
22
+ t && g(t);
23
23
  } catch (e) {
24
- S(v(e, "EcoAiProposeThreshold", g("ecoAi.suggest.failed", "Could not suggest a threshold.")));
24
+ C(y(e, "EcoAiProposeThreshold", _("ecoAi.suggest.failed", "Could not suggest a threshold.")));
25
25
  }
26
26
  }
27
- }, E = y ? r(y) : null;
28
- return /* @__PURE__ */ d("div", {
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__ */ d("div", {
32
+ /* @__PURE__ */ f("div", {
33
33
  className: "flex flex-wrap items-center justify-between gap-2",
34
- children: [/* @__PURE__ */ d(f, {
34
+ children: [/* @__PURE__ */ f(p, {
35
35
  type: "button",
36
36
  size: "sm",
37
37
  variant: "outline",
38
- onClick: T,
39
- disabled: !w || _.isPending,
38
+ onClick: E,
39
+ disabled: !T || v.isPending,
40
40
  "data-testid": "eco-ai-suggest-run",
41
- children: [/* @__PURE__ */ u(l, { className: "mr-1.5 h-4 w-4" }), _.isPending ? g("ecoAi.suggest.running", "Suggesting…") : g("ecoAi.suggest.run", "Suggest from history")]
42
- }), /* @__PURE__ */ u(o, {})]
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
- !w && /* @__PURE__ */ u("p", {
44
+ !T && /* @__PURE__ */ d("p", {
45
45
  className: "text-xs text-text-tertiary",
46
- children: g("ecoAi.suggest.needInputs", "Pick a site and parameter to suggest a range from its readings.")
46
+ children: _("ecoAi.suggest.needInputs", "Pick a site and parameter to suggest a range from its readings.")
47
47
  }),
48
- x && /* @__PURE__ */ u(s, { error: x }),
49
- y && !E && /* @__PURE__ */ d("div", {
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__ */ u("p", {
52
+ children: [/* @__PURE__ */ d("p", {
53
53
  className: "font-medium text-text-primary",
54
- children: g("ecoAi.suggest.notEnoughData", "Not enough history to suggest a range")
55
- }), /* @__PURE__ */ u("p", {
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: g("ecoAi.suggest.sample", "{{n}} reading(s) in the last {{days}} days.", {
58
- n: y.sampleSize,
59
- days: y.lookbackDays
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
- y && E && /* @__PURE__ */ d("div", {
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__ */ u("p", { children: g("ecoAi.suggest.applied", "Filled in {{min}}–{{max}} {{unit}} (outside range). Review it, then save the threshold.", {
67
- min: E.minValue,
68
- max: E.maxValue,
69
- unit: E.unit ?? ""
70
- }) }), /* @__PURE__ */ u("p", {
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: g("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(y.historicalBreachRate),
75
- n: y.sampleSize,
76
- days: y.lookbackDays,
77
- p5: y.p5 == null ? "—" : t(y.p5),
78
- p50: y.p50 == null ? "—" : t(y.p50),
79
- p95: y.p95 == null ? "—" : t(y.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
- y && y.notes.length > 0 && /* @__PURE__ */ u("ul", {
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: y.notes.map((e, t) => /* @__PURE__ */ u("li", { children: e }, `${t}:${e}`))
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 { p as AiThresholdSuggest };
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 {proposal && proposal.notes.length > 0 && (\n <ul className=\"list-disc space-y-0.5 pl-5 text-xs text-text-tertiary\">\n {proposal.notes.map((n, i) => (\n <li key={`${i}:${n}`}>{n}</li>\n ))}\n </ul>\n )}\n </div>\n );\n};\n"],"mappings":";;;;;;;;;;AAgCA,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;CAEzD,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,KAAY,EAAS,MAAM,SAAS,KACnC,kBAAC,MAAD;IAAI,WAAU;cACX,EAAS,MAAM,KAAK,GAAG,MACtB,kBAAC,MAAD,EAAA,UAAuB,EAAM,GAApB,GAAG,EAAE,GAAG,GAAY,CAC9B;GACC,CAAA;EAEH;;AAET"}
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 o } from "../../hooks/useEcoImpactHubApi.js";
4
- import { useTr as ee } from "../../hooks/useTr.js";
5
- import { formatConfidence as te, matchingThresholds as ne, resolveCitedReadings as s } from "../../utils/aiMonitoring.js";
6
- import { useEcoAiErrorReporter as c, useEcoAiTriageAlert as l } from "../../hooks/useEcoAiMonitoring.js";
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 u, AiErrorNotice as d, CitedReadingsList as re } from "./AiShared.js";
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 C, ResponsiveDialogHeader as w, ResponsiveDialogTitle as T, Select as E, SelectContent as D, SelectItem as O, SelectTrigger as k, SelectValue as A } from "@burdenoff/fe-libs/ui";
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 j = (n) => n.direction === "OUTSIDE_RANGE" ? `${t(n.minValue)}–${t(n.maxValue)} ${n.unit}` : `${e(n.direction)} ${t(n.limitValue)} ${n.unit}`, M = ({ measurement: e, onClose: t, onCitedReadings: n }) => /* @__PURE__ */ v(x, {
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(N, {
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
- }), N = ({ measurement: x, onClose: S, onCitedReadings: M }) => {
26
- let N = ee(), P = o(x.siteId), F = f(() => ne(x, P.data?.items ?? []), [x, P.data]), [I, L] = p(""), R = I || F[0]?.id || "", z = r({
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
- }), B = i({ siteId: x.siteId }), V = l(), H = a(), U = c(), [W, G] = p(null), [K, q] = p(null), [J, Y] = p(null), [oe, X] = p(!1), [se, Z] = p(!1), Q = f(() => {
34
- let e = z.data?.items ?? [];
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
- }, [z.data, x]), $ = f(() => W ? s(W.evidenceMeasurementIds, Q) : [], [W, Q]);
36
+ }, [R.data, x]), $ = f(() => U ? o(U.evidenceMeasurementIds, Q) : [], [U, Q]);
37
37
  return /* @__PURE__ */ y(_, { children: [
38
- /* @__PURE__ */ y(w, { children: [/* @__PURE__ */ y(T, {
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
- }), N("ecoAi.triage.title", "Triage alert")]
44
- }), /* @__PURE__ */ v(ae, { children: N("ecoAi.triage.description", "Check whether this {{parameter}} reading of {{value}} {{unit}} is a real event or a sensor fault.", {
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
- P.isLoading ? /* @__PURE__ */ v("p", {
53
+ N.isLoading ? /* @__PURE__ */ v("p", {
54
54
  className: "text-sm text-text-secondary",
55
- children: N("ecoAi.triage.loadingThresholds", "Loading thresholds…")
56
- }) : F.length === 0 ? /* @__PURE__ */ v("p", {
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: N("ecoAi.triage.noThreshold", "No alert threshold covers {{parameter}} at this site, so there is nothing to triage against.", { parameter: x.parameter })
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: N("ecoAi.triage.threshold", "Threshold")
64
- }), /* @__PURE__ */ y(E, {
65
- value: R,
66
- onValueChange: L,
67
- children: [/* @__PURE__ */ v(k, {
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(A, {})
70
- }), /* @__PURE__ */ v(D, { children: F.map((t) => /* @__PURE__ */ y(O, {
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
- j(t),
73
+ A(t),
74
74
  " · ",
75
75
  e(t.severity),
76
- t.isActive ? "" : ` · ${N("ecoAi.triage.inactive", "inactive")}`
76
+ t.isActive ? "" : ` · ${M("ecoAi.triage.inactive", "inactive")}`
77
77
  ]
78
78
  }, t.id)) })]
79
79
  })]
80
80
  }),
81
- K && /* @__PURE__ */ v(d, { error: K }),
82
- W && /* @__PURE__ */ y("div", {
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: W.classification }),
89
+ /* @__PURE__ */ v(n, { status: U.classification }),
90
90
  /* @__PURE__ */ v("span", {
91
91
  className: "text-sm text-text-secondary",
92
- children: N("ecoAi.triage.confidence", "Confidence {{value}}", { value: te(W.confidence) })
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(W.recommendation)]
96
+ children: ["· ", e(U.recommendation)]
97
97
  }),
98
- !W.breachesThreshold && /* @__PURE__ */ v("span", {
98
+ !U.breachesThreshold && /* @__PURE__ */ v("span", {
99
99
  className: "text-xs text-text-tertiary",
100
- children: N("ecoAi.triage.noLongerBreaches", "This reading does not breach the selected threshold.")
100
+ children: M("ecoAi.triage.noLongerBreaches", "This reading does not breach the selected threshold.")
101
101
  })
102
102
  ]
103
103
  }),
104
- W.rationale.length > 0 && /* @__PURE__ */ y("div", { children: [/* @__PURE__ */ v("h3", {
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: N("ecoAi.triage.rationale", "Why")
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: W.rationale.map((e, t) => /* @__PURE__ */ v("li", { children: e }, `${t}:${e}`))
109
+ children: U.rationale.map((e, t) => /* @__PURE__ */ v("li", { children: e }, `${t}:${e}`))
110
110
  })] }),
111
- W.signals.length > 0 && /* @__PURE__ */ y("div", { children: [/* @__PURE__ */ v("h3", {
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: N("ecoAi.triage.signals", "Signals")
113
+ children: M("ecoAi.triage.signals", "Signals")
114
114
  }), /* @__PURE__ */ v("ul", {
115
115
  className: "space-y-1.5 text-sm",
116
- children: W.signals.map((e) => /* @__PURE__ */ y("li", {
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: N("ecoAi.triage.citedReadings", "Cited readings ({{count}})", { count: $.length })
130
- }), /* @__PURE__ */ v(re, {
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
- W.proposedAction && !se && /* @__PURE__ */ y("div", {
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: N("ecoAi.triage.proposalTitle", "Suggested action: mark {{sensor}} as {{status}}", {
146
- sensor: ((e) => e && B.data?.items?.find((t) => t.id === e)?.name || N("ecoAi.triage.unnamedSensor", "the sensor"))(W.proposedAction.sensorId),
147
- status: e(W.proposedAction.targetStatus)
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: W.proposedAction.reason
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: N("ecoAi.triage.proposalNotApplied", "Nothing has changed yet. The sensor status changes only if you apply it.")
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
- oe ? /* @__PURE__ */ y("p", {
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" }), N("ecoAi.triage.applied", "Sensor marked as {{status}}.", { status: e(W.proposedAction.targetStatus) })]
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 = W?.proposedAction;
170
+ let e = U?.proposedAction;
171
171
  if (e) {
172
- Y(null);
172
+ J(null);
173
173
  try {
174
- await H.mutateAsync({
174
+ await V.mutateAsync({
175
175
  id: e.sensorId,
176
176
  status: e.targetStatus
177
177
  }), X(!0);
178
178
  } catch (e) {
179
- Y(e instanceof Error ? e.message : N("ecoAi.triage.applyFailed", "Could not update the sensor."));
179
+ J(e instanceof Error ? e.message : M("ecoAi.triage.applyFailed", "Could not update the sensor."));
180
180
  }
181
181
  }
182
182
  },
183
- disabled: H.isPending,
183
+ disabled: V.isPending,
184
184
  "data-testid": "eco-ai-decision-apply",
185
- children: H.isPending ? N("ecoAi.triage.applying", "Applying…") : N("ecoAi.triage.apply", "Mark sensor {{status}}", { status: e(W.proposedAction.targetStatus) })
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: N("ecoAi.triage.dismiss", "Dismiss")
190
+ children: M("ecoAi.triage.dismiss", "Dismiss")
191
191
  })]
192
192
  }),
193
- J && /* @__PURE__ */ v("p", {
193
+ q && /* @__PURE__ */ v("p", {
194
194
  className: "mt-2 text-sm text-status-error-text",
195
- children: J
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(C, {
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(u, {}), /* @__PURE__ */ y("div", {
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: N("ecoAi.close", "Close")
210
+ children: M("ecoAi.close", "Close")
211
211
  }), /* @__PURE__ */ v(b, {
212
212
  onClick: async () => {
213
- if (R) {
214
- q(null), G(null), X(!1), Z(!1), Y(null);
213
+ if (L) {
214
+ K(null), W(null), X(!1), Z(!1), J(null);
215
215
  try {
216
- let e = await V.mutateAsync({
217
- thresholdId: R,
216
+ let e = await B.mutateAsync({
217
+ thresholdId: L,
218
218
  measurementId: x.id
219
219
  });
220
- G(e), M?.(e.evidenceMeasurementIds);
220
+ W(e), j?.(e.evidenceMeasurementIds);
221
221
  } catch (e) {
222
- q(U(e, "EcoAiTriageAlert", N("ecoAi.triage.failed", "Triage failed.")));
222
+ K(H(e, "EcoAiTriageAlert", M("ecoAi.triage.failed", "Triage failed.")));
223
223
  }
224
224
  }
225
225
  },
226
- disabled: !R || V.isPending,
226
+ disabled: !L || B.isPending,
227
227
  "data-testid": "eco-ai-triage-run",
228
- children: V.isPending ? N("ecoAi.triage.running", "Triaging…") : W ? N("ecoAi.triage.rerun", "Run again") : N("ecoAi.triage.run", "Run triage")
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 { M as AiTriageDialog };
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":";;;;;;;;;;;;;AA0DA,IAAM,KAAgB,MACpB,EAAE,cAAc,kBACZ,GAAG,EAAa,EAAE,QAAQ,EAAE,GAAG,EAAa,EAAE,QAAQ,EAAE,GAAG,EAAE,SAC7D,GAAG,EAAgB,EAAE,SAAS,EAAE,GAAG,EAAa,EAAE,UAAU,EAAE,GAAG,EAAE,QAE5D,KAA2C,EACtD,gBACA,YACA,yBAEA,kBAAC,GAAD;CAAkB,MAAM,MAAgB;CAAM,eAAe,MAAS,CAAC,KAAQ,EAAQ;WACrF,kBAAC,GAAD;EAAyB,WAAU;YAChC,KACC,kBAAC,GAAD;GAEe;GACJ;GACQ;EAClB,GAJM,EAAY,EAIlB;CAEoB,CAAA;AACT,CAAA,GAGd,KAIA,EAAE,gBAAa,YAAS,yBAAsB;CAClD,IAAM,IAAK,GAAM,GACX,IAAkB,GAAiB,EAAY,MAAM,GACrD,IAAa,QACX,GAAmB,GAAa,EAAgB,MAAM,SAAS,CAAC,CAAC,GACvE,CAAC,GAAa,EAAgB,IAAI,CACpC,GACM,CAAC,GAAmB,KAAwB,EAAiB,EAAE,GAC/D,IAAc,KAAqB,EAAW,IAAI,MAAM,IAGxD,IAAe,EAAmB;EACtC,QAAQ,EAAY;EACpB,WAAW,EAAY;EACvB,YAAY;GAAE,OAAO;GAAK,QAAQ;EAAE;CACtC,CAAC,GACK,IAAe,EAAc,EAAE,QAAQ,EAAY,OAAO,CAAC,GAE3D,IAAS,EAAoB,GAC7B,IAAkB,EAAsB,GACxC,IAAgB,EAAsB,GAEtC,CAAC,GAAQ,KAAa,EAAsC,IAAI,GAChE,CAAC,GAAS,KAAc,EAAgC,IAAI,GAC5D,CAAC,GAAY,KAAiB,EAAwB,IAAI,GAC1D,CAAC,GAAS,KAAc,EAAS,EAAK,GACtC,CAAC,IAAW,KAAgB,EAAS,EAAK,GAE1C,IAAW,QAAc;EAC7B,IAAM,IAAQ,EAAa,MAAM,SAAS,CAAC;EAC3C,OAAO,EAAM,MAAM,MAAM,EAAE,OAAO,EAAY,EAAE,IAAI,IAAQ,CAAC,GAAa,GAAG,CAAK;CACpF,GAAG,CAAC,EAAa,MAAM,CAAW,CAAC,GAC7B,IAAQ,QACL,IAAS,EAAqB,EAAO,wBAAwB,CAAQ,IAAI,CAAC,GACjF,CAAC,GAAQ,CAAQ,CACnB;CAuCA,OACE,kBAAA,GAAA,EAAA,UAAA;EACE,kBAAC,GAAD,EAAA,UAAA,CACE,kBAAC,GAAD;GAAuB,WAAU;aAAjC,CACE,kBAAC,GAAD;IAAU,WAAU;IAAsB,eAAY;GAAQ,CAAA,GAC7D,EAAG,sBAAsB,cAAc,CACnB;MACvB,kBAAC,IAAD,EAAA,UACG,EACC,4BACA,qGACA;GACE,WAAW,EAAY;GACvB,OAAO,EAAa,EAAY,KAAK;GACrC,MAAM,EAAY;EACpB,CACF,EAC2B,CAAA,CACP,EAAA,CAAA;EAExB,kBAAC,OAAD;GAAK,WAAU;GAAkB,eAAY;aAA7C;IAEG,EAAgB,YACf,kBAAC,KAAD;KAAG,WAAU;eACV,EAAG,kCAAkC,qBAAqB;IAC1D,CAAA,IACD,EAAW,WAAW,IACxB,kBAAC,KAAD;KAAG,WAAU;eACV,EACC,4BACA,gGACA,EAAE,WAAW,EAAY,UAAU,CACrC;IACC,CAAA,IAEH,kBAAC,OAAD;KAAK,WAAU;eAAf,CACE,kBAAC,IAAD;MAAO,SAAQ;gBACZ,EAAG,0BAA0B,WAAW;KACpC,CAAA,GACP,kBAAC,GAAD;MAAQ,OAAO;MAAa,eAAe;gBAA3C,CACE,kBAAC,GAAD;OAAe,IAAG;iBAChB,kBAAC,GAAD,CAAc,CAAA;MACD,CAAA,GACf,kBAAC,GAAD,EAAA,UACG,EAAW,KAAK,MACf,kBAAC,GAAD;OAAuB,OAAO,EAAE;iBAAhC;QACG,EAAa,CAAC;QAAE;QAAI,EAAgB,EAAE,QAAQ;QAC9C,EAAE,WAAW,KAAK,MAAM,EAAG,yBAAyB,UAAU;OACrD;SAHK,EAAE,EAGP,CACb,EACY,CAAA,CACT;OACL;;IAGN,KAAW,kBAAC,GAAD,EAAe,OAAO,EAAU,CAAA;IAG3C,KACC,kBAAC,OAAD;KAAK,WAAU;KAAa,eAAY;eAAxC;MACE,kBAAC,OAAD;OAAK,WAAU;iBAAf;QACE,kBAAC,GAAD,EAAa,QAAQ,EAAO,eAAiB,CAAA;QAC7C,kBAAC,QAAD;SAAM,WAAU;mBACb,EAAG,2BAA2B,wBAAwB,EACrD,OAAO,GAAiB,EAAO,UAAU,EAC3C,CAAC;QACG,CAAA;QACN,kBAAC,QAAD;SAAM,WAAU;mBAAhB,CAA8C,MACzC,EAAgB,EAAO,cAAc,CACpC;;QACL,CAAC,EAAO,qBACP,kBAAC,QAAD;SAAM,WAAU;mBACb,EACC,iCACA,sDACF;QACI,CAAA;OAEL;;MAEJ,EAAO,UAAU,SAAS,KACzB,kBAAC,OAAD,EAAA,UAAA,CACE,kBAAC,MAAD;OAAI,WAAU;iBACX,EAAG,0BAA0B,KAAK;MACjC,CAAA,GACJ,kBAAC,MAAD;OAAI,WAAU;iBACX,EAAO,UAAU,KAAK,GAAM,MAC3B,kBAAC,MAAD,EAAA,UAA0B,EAAS,GAA1B,GAAG,EAAE,GAAG,GAAkB,CACpC;MACC,CAAA,CACD,EAAA,CAAA;MAGN,EAAO,QAAQ,SAAS,KACvB,kBAAC,OAAD,EAAA,UAAA,CACE,kBAAC,MAAD;OAAI,WAAU;iBACX,EAAG,wBAAwB,SAAS;MACnC,CAAA,GACJ,kBAAC,MAAD;OAAI,WAAU;iBACX,EAAO,QAAQ,KAAK,MACnB,kBAAC,MAAD;QAAiB,WAAU;kBAA3B,CACE,kBAAC,GAAD;SAAa,QAAQ,EAAE;SAAS,WAAU;QAAY,CAAA,GACtD,kBAAC,QAAD;SAAM,WAAU;mBAAuB,EAAE;QAAa,CAAA,CACpD;UAHK,EAAE,IAGP,CACL;MACC,CAAA,CACD,EAAA,CAAA;MAGP,kBAAC,OAAD,EAAA,UAAA,CACE,kBAAC,MAAD;OAAI,WAAU;iBACX,EAAG,8BAA8B,8BAA8B,EAC9D,OAAO,EAAM,OACf,CAAC;MACC,CAAA,GACJ,kBAAC,GAAD;OAAmB,UAAU;OAAO,SAAS,EAAY;MAAK,CAAA,CAC3D,EAAA,CAAA;MAGJ,EAAO,kBAAkB,CAAC,MACzB,kBAAC,OAAD;OACE,eAAY;OACZ,WAAU;iBAFZ;QAIE,kBAAC,OAAD;SAAK,WAAU;mBAAf,CACE,kBAAC,GAAD,EAAa,WAAU,mDAAoD,CAAA,GAC3E,kBAAC,OAAD;UAAK,WAAU;oBAAf;WACE,kBAAC,KAAD;YAAG,WAAU;sBACV,EACC,8BACA,mDACA;aACE,UA1KJ,MACjB,KAAM,EAAa,MAAM,OAAO,MAAM,MAAM,EAAE,OAAO,CAAE,GAAG,QAC3D,EAAG,8BAA8B,YAAY,GAwKJ,EAAO,eAAe,QAAQ;aACjD,QAAQ,EAAgB,EAAO,eAAe,YAAY;YAC5D,CACF;WACC,CAAA;WACH,kBAAC,KAAD;YAAG,WAAU;sBACV,EAAO,eAAe;WACtB,CAAA;WAEF,CAAC,KACA,kBAAC,KAAD;YAAG,WAAU;sBACV,EACC,mCACA,0EACF;WACC,CAAA;UAEF;WACF;;QACJ,IACC,kBAAC,KAAD;SACE,WAAU;SACV,eAAY;mBAFd,CAIE,kBAAC,GAAD,EAAc,WAAU,UAAW,CAAA,GAClC,EAAG,wBAAwB,gCAAgC,EAC1D,QAAQ,EAAgB,EAAO,eAAe,YAAY,EAC5D,CAAC,CACA;aAEH,kBAAC,OAAD;SAAK,WAAU;mBAAf,CACE,kBAAC,GAAD;UACE,MAAK;UACL,SAAS,YArLK;WAChC,IAAM,IAAS,GAAQ;WAClB,OACL;cAAc,IAAI;YAClB,IAAI;aAEF,AADA,MAAM,EAAgB,YAAY;cAAE,IAAI,EAAO;cAAU,QAAQ,EAAO;aAAa,CAAC,GACtF,EAAW,EAAI;YACjB,SAAS,GAAK;aACZ,EACE,aAAe,QACX,EAAI,UACJ,EAAG,4BAA4B,8BAA8B,CACnE;YACF;WAVkB;UAWpB;UAwKoB,UAAU,EAAgB;UAC1B,eAAY;oBAEX,EAAgB,YACb,EAAG,yBAAyB,WAAW,IACvC,EAAG,sBAAsB,0BAA0B,EACjD,QAAQ,EAAgB,EAAO,eAAe,YAAY,EAC5D,CAAC;SACC,CAAA,GACR,kBAAC,GAAD;UAAQ,MAAK;UAAK,SAAQ;UAAQ,eAAe,EAAa,EAAI;oBAC/D,EAAG,wBAAwB,SAAS;SAC/B,CAAA,CACL;;QAEN,KAAc,kBAAC,KAAD;SAAG,WAAU;mBAAuC;QAAc,CAAA;OAC9E;;KAEJ;;GAEJ;;EAEL,kBAAC,IAAD;GAAwB,WAAU;aAAlC,CACE,kBAAC,GAAD,CAAe,CAAA,GACf,kBAAC,OAAD;IAAK,WAAU;cAAf,CACE,kBAAC,GAAD;KAAQ,SAAQ;KAAU,SAAS;eAChC,EAAG,eAAe,OAAO;IACpB,CAAA,GACR,kBAAC,GAAD;KACE,SAAS,YApOW;MACvB,OAKL;OAJA,EAAW,IAAI,GACf,EAAU,IAAI,GACd,EAAW,EAAK,GAChB,EAAa,EAAK,GAClB,EAAc,IAAI;OAClB,IAAI;QACF,IAAM,IAAU,MAAM,EAAO,YAAY;SAAE;SAAa,eAAe,EAAY;QAAG,CAAC;QAEvF,AADA,EAAU,CAAO,GACjB,IAAkB,EAAQ,sBAAsB;OAClD,SAAS,GAAK;QACZ,EACE,EAAc,GAAK,oBAAoB,EAAG,uBAAuB,gBAAgB,CAAC,CACpF;OACF;MATkB;KAUpB;KAqNU,UAAU,CAAC,KAAe,EAAO;KACjC,eAAY;eAEX,EAAO,YACJ,EAAG,wBAAwB,WAAW,IACtC,IACE,EAAG,sBAAsB,WAAW,IACpC,EAAG,oBAAoB,YAAY;IACnC,CAAA,CACL;KACiB;;CACxB,EAAA,CAAA;AAEN"}
@@ -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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"}
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@burdenoff/microfe-ecoimpacthub",
3
- "version": "2026.1030.1",
3
+ "version": "2026.1030.2",
4
4
  "description": "Burdenoff EcoImpactHub Microfrontend",
5
5
  "type": "module",
6
6
  "files": [