@burdenoff/microfe-mybodyshield 2026.821.1 → 2026.827.1
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/dist/mybodyshield/pages/ai/AiControlsPage.d.ts.map +1 -1
- package/dist/mybodyshield/pages/ai/AiControlsPage.js +167 -108
- package/dist/mybodyshield/pages/ai/AiControlsPage.js.map +1 -1
- package/dist/mybodyshield/pages/coaching/CoachingPage.d.ts.map +1 -1
- package/dist/mybodyshield/pages/coaching/CoachingPage.js +222 -151
- package/dist/mybodyshield/pages/coaching/CoachingPage.js.map +1 -1
- package/dist/mybodyshield/pages/insights/InsightsCharts.d.ts +18 -0
- package/dist/mybodyshield/pages/insights/InsightsCharts.d.ts.map +1 -0
- package/dist/mybodyshield/pages/insights/InsightsCharts.js +549 -0
- package/dist/mybodyshield/pages/insights/InsightsCharts.js.map +1 -0
- package/dist/mybodyshield/pages/insights/InsightsPage.d.ts.map +1 -1
- package/dist/mybodyshield/pages/insights/InsightsPage.js +202 -86
- package/dist/mybodyshield/pages/insights/InsightsPage.js.map +1 -1
- package/dist/node_modules/recharts/es6/cartesian/Line.js +473 -0
- package/dist/node_modules/recharts/es6/cartesian/Line.js.map +1 -0
- package/dist/node_modules/recharts/es6/cartesian/Scatter.js +368 -0
- package/dist/node_modules/recharts/es6/cartesian/Scatter.js.map +1 -0
- package/dist/node_modules/recharts/es6/cartesian/ZAxis.js +103 -0
- package/dist/node_modules/recharts/es6/cartesian/ZAxis.js.map +1 -0
- package/dist/node_modules/recharts/es6/chart/BarChart.js +24 -0
- package/dist/node_modules/recharts/es6/chart/BarChart.js.map +1 -0
- package/dist/node_modules/recharts/es6/chart/ComposedChart.js +38 -0
- package/dist/node_modules/recharts/es6/chart/ComposedChart.js.map +1 -0
- package/dist/node_modules/recharts/es6/util/DataUtils.js +12 -2
- package/dist/node_modules/recharts/es6/util/DataUtils.js.map +1 -1
- package/dist/node_modules/recharts/es6/util/ScatterUtils.js +48 -0
- package/dist/node_modules/recharts/es6/util/ScatterUtils.js.map +1 -0
- package/dist/stories/InsightsCharts.stories.d.ts +15 -0
- package/dist/stories/InsightsCharts.stories.d.ts.map +1 -0
- package/package.json +1 -1
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"AiControlsPage.d.ts","sourceRoot":"","sources":["../../../../src/mybodyshield/pages/ai/AiControlsPage.tsx"],"names":[],"mappings":"AAAA;;;;;;;GAOG;
|
|
1
|
+
{"version":3,"file":"AiControlsPage.d.ts","sourceRoot":"","sources":["../../../../src/mybodyshield/pages/ai/AiControlsPage.tsx"],"names":[],"mappings":"AAAA;;;;;;;GAOG;AAgcH,wBAAgB,cAAc,gCA2C7B"}
|
|
@@ -3,225 +3,284 @@ import { MetricsCard as t } from "../../components/MetricsCard.js";
|
|
|
3
3
|
import { formatDateTime as n, formatEnumLabel as r, formatRelativeTime as i } from "../../utils/formatters.js";
|
|
4
4
|
import { ErrorView as a, LoadingView as o, Section as s } from "../../components/StateViews.js";
|
|
5
5
|
import { DataTable as c } from "../../components/DataTable.js";
|
|
6
|
-
import { useMybAiOperationLogs as l,
|
|
6
|
+
import { useMybAiOperationLogs as l, useMybAiProcessingConsent as u, useMybAiSettings as d, useMybSetAiProcessingConsent as f, useMybUpdateAiSettings as p } from "../../hooks/useMyBodyShieldApi.js";
|
|
7
7
|
import "../../hooks/index.js";
|
|
8
8
|
import "../../components/index.js";
|
|
9
|
-
import {
|
|
10
|
-
import {
|
|
11
|
-
import {
|
|
12
|
-
import {
|
|
9
|
+
import { IconChip as m } from "../_shared/visuals.js";
|
|
10
|
+
import { useMemo as h, useState as g } from "react";
|
|
11
|
+
import { Activity as _, Bot as v, KeyRound as y, Save as b, ShieldCheck as x, ShieldOff as S, Sparkles as C } from "lucide-react";
|
|
12
|
+
import { jsx as w, jsxs as T } from "react/jsx-runtime";
|
|
13
|
+
import { Button as E, GlassCard as D, Input as O, Label as k, Select as A, SelectContent as j, SelectItem as M, SelectTrigger as N, SelectValue as P, Spinner as F, Switch as I } from "@burdenoff/fe-libs/ui";
|
|
13
14
|
//#region src/mybodyshield/pages/ai/AiControlsPage.tsx
|
|
14
|
-
var
|
|
15
|
+
var L = [
|
|
15
16
|
"GEMINI",
|
|
16
17
|
"OPENAI",
|
|
17
18
|
"ANTHROPIC"
|
|
18
|
-
],
|
|
19
|
+
], R = {
|
|
19
20
|
GEMINI: "Google Gemini",
|
|
20
21
|
OPENAI: "OpenAI",
|
|
21
22
|
ANTHROPIC: "Anthropic"
|
|
22
|
-
},
|
|
23
|
+
}, z = {
|
|
23
24
|
GEMINI: "gemini-2.5-flash",
|
|
24
25
|
OPENAI: "gpt-4o-mini",
|
|
25
26
|
ANTHROPIC: "claude-3-5-haiku-latest"
|
|
26
|
-
},
|
|
27
|
+
}, B = (e) => ({
|
|
27
28
|
provider: e.provider,
|
|
28
29
|
modelName: e.modelName,
|
|
29
30
|
dailyAllowance: String(e.dailyAllowance),
|
|
30
31
|
providerKey: "",
|
|
31
32
|
clearKey: !1
|
|
32
33
|
});
|
|
33
|
-
function
|
|
34
|
-
let
|
|
34
|
+
function V() {
|
|
35
|
+
let e = u(), t = f();
|
|
36
|
+
if (e.isLoading) return /* @__PURE__ */ w(o, { label: "Loading AI-processing consent…" });
|
|
37
|
+
if (e.isError) return /* @__PURE__ */ w(a, {
|
|
38
|
+
message: "We couldn't load your AI-processing consent.",
|
|
39
|
+
onRetry: () => e.refetch()
|
|
40
|
+
});
|
|
41
|
+
let n = e.data?.enabled ?? !1;
|
|
42
|
+
return /* @__PURE__ */ w(D, {
|
|
43
|
+
treatment: "plain",
|
|
44
|
+
className: "p-5",
|
|
45
|
+
children: /* @__PURE__ */ T("div", {
|
|
46
|
+
className: "flex flex-wrap items-start justify-between gap-4",
|
|
47
|
+
children: [/* @__PURE__ */ T("div", {
|
|
48
|
+
className: "flex min-w-0 items-start gap-3",
|
|
49
|
+
children: [/* @__PURE__ */ w(m, { icon: w(n ? x : S, { className: "h-5 w-5" }) }), /* @__PURE__ */ T("div", {
|
|
50
|
+
className: "min-w-0",
|
|
51
|
+
children: [
|
|
52
|
+
/* @__PURE__ */ w("p", {
|
|
53
|
+
className: "text-sm font-medium text-text-primary",
|
|
54
|
+
children: n ? "AI may read your health data" : "AI processing is withdrawn — summaries will not run"
|
|
55
|
+
}),
|
|
56
|
+
/* @__PURE__ */ w("p", {
|
|
57
|
+
className: "mt-0.5 text-sm text-text-secondary",
|
|
58
|
+
children: n ? "The provider and model below are used to turn your logged training, nutrition, recovery and body-metric data into plain-language summaries." : "The provider and model below are saved, but no AI summary is generated while consent is withdrawn. Rule-based guidance and every manual logging path keep working."
|
|
59
|
+
}),
|
|
60
|
+
e.data?.withdrawnAt && !n && /* @__PURE__ */ T("p", {
|
|
61
|
+
className: "mt-1 text-xs text-text-tertiary",
|
|
62
|
+
children: [
|
|
63
|
+
"Withdrawn ",
|
|
64
|
+
i(e.data.withdrawnAt),
|
|
65
|
+
"."
|
|
66
|
+
]
|
|
67
|
+
}),
|
|
68
|
+
e.data?.acceptedAt && n && /* @__PURE__ */ T("p", {
|
|
69
|
+
className: "mt-1 text-xs text-text-tertiary",
|
|
70
|
+
children: [
|
|
71
|
+
"Consent given ",
|
|
72
|
+
i(e.data.acceptedAt),
|
|
73
|
+
"."
|
|
74
|
+
]
|
|
75
|
+
})
|
|
76
|
+
]
|
|
77
|
+
})]
|
|
78
|
+
}), /* @__PURE__ */ w(I, {
|
|
79
|
+
checked: n,
|
|
80
|
+
onCheckedChange: (e) => t.mutate(e),
|
|
81
|
+
disabled: t.isPending,
|
|
82
|
+
"aria-label": "Allow AI to read my health data"
|
|
83
|
+
})]
|
|
84
|
+
})
|
|
85
|
+
});
|
|
86
|
+
}
|
|
87
|
+
function H({ settings: l, logsQuery: u }) {
|
|
88
|
+
let d = p(), [f, m] = g(() => B(l)), [x, S] = g(null), [I, H] = g(!1), U = h(() => {
|
|
35
89
|
let { dailyAllowance: e, dailyAllowanceUsed: t } = l;
|
|
36
90
|
return e <= 0 ? 0 : Math.min(100, Math.round(t / e * 100));
|
|
37
|
-
}, [l]),
|
|
38
|
-
|
|
91
|
+
}, [l]), W = l.dailyAllowanceUsed >= l.dailyAllowance, G = (e) => {
|
|
92
|
+
m((t) => ({
|
|
39
93
|
...t,
|
|
40
94
|
provider: e,
|
|
41
|
-
modelName: t.modelName ===
|
|
95
|
+
modelName: t.modelName === z[t.provider] ? z[e] : t.modelName
|
|
42
96
|
}));
|
|
43
97
|
};
|
|
44
|
-
return /* @__PURE__ */
|
|
98
|
+
return /* @__PURE__ */ T(e, {
|
|
45
99
|
title: "AI controls",
|
|
46
100
|
description: "Choose your AI provider, model and daily allowance.",
|
|
47
|
-
actions: /* @__PURE__ */
|
|
101
|
+
actions: /* @__PURE__ */ T(E, {
|
|
48
102
|
className: "gap-2",
|
|
49
103
|
onClick: async () => {
|
|
50
|
-
|
|
51
|
-
let e = Number(
|
|
104
|
+
S(null), H(!1);
|
|
105
|
+
let e = Number(f.dailyAllowance);
|
|
52
106
|
if (!Number.isInteger(e) || e < 0 || e > 1e4) {
|
|
53
|
-
|
|
107
|
+
S("Daily allowance must be an integer between 0 and 10,000.");
|
|
54
108
|
return;
|
|
55
109
|
}
|
|
56
|
-
if (!
|
|
57
|
-
|
|
110
|
+
if (!f.modelName.trim()) {
|
|
111
|
+
S("Model name is required.");
|
|
58
112
|
return;
|
|
59
113
|
}
|
|
60
114
|
let t = {
|
|
61
|
-
provider:
|
|
62
|
-
modelName:
|
|
115
|
+
provider: f.provider,
|
|
116
|
+
modelName: f.modelName.trim(),
|
|
63
117
|
dailyAllowance: e
|
|
64
118
|
};
|
|
65
|
-
|
|
119
|
+
f.clearKey ? t.providerKey = "" : f.providerKey.trim() && (t.providerKey = f.providerKey.trim());
|
|
66
120
|
try {
|
|
67
|
-
await
|
|
121
|
+
await d.mutateAsync(t), H(!0), m((e) => ({
|
|
68
122
|
...e,
|
|
69
123
|
providerKey: "",
|
|
70
124
|
clearKey: !1
|
|
71
125
|
}));
|
|
72
126
|
} catch (e) {
|
|
73
|
-
|
|
127
|
+
S(e instanceof Error ? e.message : "Failed to save AI settings.");
|
|
74
128
|
}
|
|
75
129
|
},
|
|
76
|
-
disabled:
|
|
77
|
-
children: [
|
|
130
|
+
disabled: d.isPending,
|
|
131
|
+
children: [d.isPending ? /* @__PURE__ */ w(F, { size: "sm" }) : /* @__PURE__ */ w(b, { className: "h-4 w-4" }), "Save settings"]
|
|
78
132
|
}),
|
|
79
133
|
children: [
|
|
80
|
-
/* @__PURE__ */
|
|
134
|
+
/* @__PURE__ */ w(s, {
|
|
135
|
+
title: "AI-processing consent",
|
|
136
|
+
description: "AI summaries only run while you allow AI to read your health data.",
|
|
137
|
+
children: /* @__PURE__ */ w(V, {})
|
|
138
|
+
}),
|
|
139
|
+
/* @__PURE__ */ T("div", {
|
|
81
140
|
className: "grid grid-cols-1 gap-4 sm:grid-cols-3",
|
|
82
141
|
children: [
|
|
83
|
-
/* @__PURE__ */
|
|
84
|
-
icon: /* @__PURE__ */
|
|
142
|
+
/* @__PURE__ */ w(t, {
|
|
143
|
+
icon: /* @__PURE__ */ w(C, { className: "h-5 w-5" }),
|
|
85
144
|
value: l.provider,
|
|
86
145
|
label: "Current provider"
|
|
87
146
|
}),
|
|
88
|
-
/* @__PURE__ */
|
|
89
|
-
icon: /* @__PURE__ */
|
|
147
|
+
/* @__PURE__ */ w(t, {
|
|
148
|
+
icon: /* @__PURE__ */ w(v, { className: "h-5 w-5" }),
|
|
90
149
|
value: l.modelName,
|
|
91
150
|
label: "Current model"
|
|
92
151
|
}),
|
|
93
|
-
/* @__PURE__ */
|
|
94
|
-
icon: /* @__PURE__ */
|
|
152
|
+
/* @__PURE__ */ w(t, {
|
|
153
|
+
icon: /* @__PURE__ */ w(_, { className: "h-5 w-5" }),
|
|
95
154
|
value: `${l.dailyAllowanceUsed} / ${l.dailyAllowance}`,
|
|
96
155
|
label: "Daily allowance used",
|
|
97
|
-
accent:
|
|
98
|
-
footer: /* @__PURE__ */
|
|
156
|
+
accent: W ? "error" : U >= 80 ? "warning" : "success",
|
|
157
|
+
footer: /* @__PURE__ */ T("p", {
|
|
99
158
|
className: "text-xs text-text-tertiary",
|
|
100
159
|
children: ["Resets ", i(l.allowanceResetAt)]
|
|
101
160
|
})
|
|
102
161
|
})
|
|
103
162
|
]
|
|
104
163
|
}),
|
|
105
|
-
/* @__PURE__ */
|
|
164
|
+
/* @__PURE__ */ w(s, {
|
|
106
165
|
title: "Configuration",
|
|
107
166
|
description: "Changes apply to your account only.",
|
|
108
|
-
children: /* @__PURE__ */
|
|
167
|
+
children: /* @__PURE__ */ T(D, {
|
|
109
168
|
treatment: "plain",
|
|
110
169
|
className: "p-6",
|
|
111
170
|
children: [
|
|
112
|
-
/* @__PURE__ */
|
|
171
|
+
/* @__PURE__ */ T("div", {
|
|
113
172
|
className: "grid gap-6 md:grid-cols-2",
|
|
114
173
|
children: [
|
|
115
|
-
/* @__PURE__ */
|
|
174
|
+
/* @__PURE__ */ T("div", {
|
|
116
175
|
className: "space-y-1.5",
|
|
117
176
|
children: [
|
|
118
|
-
/* @__PURE__ */
|
|
177
|
+
/* @__PURE__ */ w(k, {
|
|
119
178
|
htmlFor: "myb-ai-provider",
|
|
120
179
|
children: "AI provider"
|
|
121
180
|
}),
|
|
122
|
-
/* @__PURE__ */
|
|
123
|
-
value:
|
|
124
|
-
onValueChange: (e) =>
|
|
125
|
-
children: [/* @__PURE__ */
|
|
181
|
+
/* @__PURE__ */ T(A, {
|
|
182
|
+
value: f.provider,
|
|
183
|
+
onValueChange: (e) => G(e),
|
|
184
|
+
children: [/* @__PURE__ */ w(N, {
|
|
126
185
|
id: "myb-ai-provider",
|
|
127
|
-
children: /* @__PURE__ */
|
|
128
|
-
}), /* @__PURE__ */
|
|
186
|
+
children: /* @__PURE__ */ w(P, {})
|
|
187
|
+
}), /* @__PURE__ */ w(j, { children: L.map((e) => /* @__PURE__ */ w(M, {
|
|
129
188
|
value: e,
|
|
130
|
-
children:
|
|
189
|
+
children: R[e]
|
|
131
190
|
}, e)) })]
|
|
132
191
|
}),
|
|
133
|
-
/* @__PURE__ */
|
|
192
|
+
/* @__PURE__ */ w("p", {
|
|
134
193
|
className: "text-xs text-text-tertiary",
|
|
135
194
|
children: "The provider used for AI-generated guidance summaries."
|
|
136
195
|
})
|
|
137
196
|
]
|
|
138
197
|
}),
|
|
139
|
-
/* @__PURE__ */
|
|
198
|
+
/* @__PURE__ */ T("div", {
|
|
140
199
|
className: "space-y-1.5",
|
|
141
200
|
children: [
|
|
142
|
-
/* @__PURE__ */
|
|
201
|
+
/* @__PURE__ */ w(k, {
|
|
143
202
|
htmlFor: "myb-ai-model",
|
|
144
203
|
children: "Model name"
|
|
145
204
|
}),
|
|
146
|
-
/* @__PURE__ */
|
|
205
|
+
/* @__PURE__ */ w(O, {
|
|
147
206
|
id: "myb-ai-model",
|
|
148
|
-
value:
|
|
149
|
-
onChange: (e) =>
|
|
207
|
+
value: f.modelName,
|
|
208
|
+
onChange: (e) => m((t) => ({
|
|
150
209
|
...t,
|
|
151
210
|
modelName: e.target.value
|
|
152
211
|
})),
|
|
153
|
-
placeholder:
|
|
212
|
+
placeholder: z[f.provider]
|
|
154
213
|
}),
|
|
155
|
-
/* @__PURE__ */
|
|
214
|
+
/* @__PURE__ */ T("p", {
|
|
156
215
|
className: "text-xs text-text-tertiary",
|
|
157
216
|
children: [
|
|
158
217
|
"e.g. ",
|
|
159
|
-
|
|
218
|
+
z[f.provider],
|
|
160
219
|
". Must be valid for the chosen provider."
|
|
161
220
|
]
|
|
162
221
|
})
|
|
163
222
|
]
|
|
164
223
|
}),
|
|
165
|
-
/* @__PURE__ */
|
|
224
|
+
/* @__PURE__ */ T("div", {
|
|
166
225
|
className: "space-y-1.5",
|
|
167
226
|
children: [
|
|
168
|
-
/* @__PURE__ */
|
|
227
|
+
/* @__PURE__ */ w(k, {
|
|
169
228
|
htmlFor: "myb-ai-allowance",
|
|
170
229
|
children: "Daily allowance"
|
|
171
230
|
}),
|
|
172
|
-
/* @__PURE__ */
|
|
231
|
+
/* @__PURE__ */ w(O, {
|
|
173
232
|
id: "myb-ai-allowance",
|
|
174
233
|
type: "number",
|
|
175
234
|
min: 0,
|
|
176
235
|
max: 1e4,
|
|
177
236
|
step: 1,
|
|
178
|
-
value:
|
|
179
|
-
onChange: (e) =>
|
|
237
|
+
value: f.dailyAllowance,
|
|
238
|
+
onChange: (e) => m((t) => ({
|
|
180
239
|
...t,
|
|
181
240
|
dailyAllowance: e.target.value
|
|
182
241
|
}))
|
|
183
242
|
}),
|
|
184
|
-
/* @__PURE__ */
|
|
243
|
+
/* @__PURE__ */ w("p", {
|
|
185
244
|
className: "text-xs text-text-tertiary",
|
|
186
245
|
children: "Maximum AI operations counted against your account per UTC day."
|
|
187
246
|
})
|
|
188
247
|
]
|
|
189
248
|
}),
|
|
190
|
-
/* @__PURE__ */
|
|
249
|
+
/* @__PURE__ */ T("div", {
|
|
191
250
|
className: "space-y-1.5",
|
|
192
251
|
children: [
|
|
193
|
-
/* @__PURE__ */
|
|
252
|
+
/* @__PURE__ */ w(k, {
|
|
194
253
|
htmlFor: "myb-ai-key",
|
|
195
|
-
children: /* @__PURE__ */
|
|
254
|
+
children: /* @__PURE__ */ T("span", {
|
|
196
255
|
className: "inline-flex items-center gap-1.5",
|
|
197
|
-
children: [/* @__PURE__ */ y
|
|
256
|
+
children: [/* @__PURE__ */ w(y, { className: "h-3.5 w-3.5" }), "Provider API key"]
|
|
198
257
|
})
|
|
199
258
|
}),
|
|
200
|
-
/* @__PURE__ */
|
|
259
|
+
/* @__PURE__ */ w(O, {
|
|
201
260
|
id: "myb-ai-key",
|
|
202
261
|
type: "password",
|
|
203
|
-
value:
|
|
204
|
-
onChange: (e) =>
|
|
262
|
+
value: f.providerKey,
|
|
263
|
+
onChange: (e) => m((t) => ({
|
|
205
264
|
...t,
|
|
206
265
|
providerKey: e.target.value,
|
|
207
266
|
clearKey: !1
|
|
208
267
|
})),
|
|
209
268
|
placeholder: l.hasProviderKey ? "•••••••• (saved)" : "Optional",
|
|
210
|
-
disabled:
|
|
269
|
+
disabled: f.clearKey
|
|
211
270
|
}),
|
|
212
|
-
/* @__PURE__ */
|
|
271
|
+
/* @__PURE__ */ T("div", {
|
|
213
272
|
className: "flex items-center gap-2",
|
|
214
|
-
children: [/* @__PURE__ */
|
|
273
|
+
children: [/* @__PURE__ */ w("input", {
|
|
215
274
|
id: "myb-ai-clear-key",
|
|
216
275
|
type: "checkbox",
|
|
217
|
-
checked:
|
|
218
|
-
onChange: (e) =>
|
|
276
|
+
checked: f.clearKey,
|
|
277
|
+
onChange: (e) => m((t) => ({
|
|
219
278
|
...t,
|
|
220
279
|
clearKey: e.target.checked,
|
|
221
280
|
providerKey: ""
|
|
222
281
|
})),
|
|
223
282
|
className: "h-4 w-4 rounded border-border-seam text-accent focus:ring-accent"
|
|
224
|
-
}), /* @__PURE__ */
|
|
283
|
+
}), /* @__PURE__ */ w(k, {
|
|
225
284
|
htmlFor: "myb-ai-clear-key",
|
|
226
285
|
className: "text-xs font-normal text-text-secondary",
|
|
227
286
|
children: "Clear saved key and fall back to the workspace default"
|
|
@@ -231,49 +290,49 @@ function F({ settings: l, logsQuery: u }) {
|
|
|
231
290
|
})
|
|
232
291
|
]
|
|
233
292
|
}),
|
|
234
|
-
|
|
293
|
+
I && /* @__PURE__ */ w("p", {
|
|
235
294
|
className: "mt-4 text-sm text-status-success-text",
|
|
236
295
|
children: "AI settings saved."
|
|
237
296
|
}),
|
|
238
|
-
|
|
297
|
+
x && /* @__PURE__ */ w("p", {
|
|
239
298
|
className: "mt-4 text-sm text-status-error-text",
|
|
240
|
-
children:
|
|
299
|
+
children: x
|
|
241
300
|
})
|
|
242
301
|
]
|
|
243
302
|
})
|
|
244
303
|
}),
|
|
245
|
-
/* @__PURE__ */
|
|
304
|
+
/* @__PURE__ */ w(s, {
|
|
246
305
|
title: "Operation history",
|
|
247
306
|
description: "Redacted audit log of AI operations for your account.",
|
|
248
|
-
children: u.isLoading ? /* @__PURE__ */
|
|
307
|
+
children: u.isLoading ? /* @__PURE__ */ w(o, { label: "Loading operation history…" }) : u.isError ? /* @__PURE__ */ w(a, {
|
|
249
308
|
message: "We couldn't load the operation log.",
|
|
250
309
|
onRetry: () => u.refetch()
|
|
251
|
-
}) : u.data?.items.length === 0 ? /* @__PURE__ */
|
|
310
|
+
}) : u.data?.items.length === 0 ? /* @__PURE__ */ T(D, {
|
|
252
311
|
treatment: "plain",
|
|
253
312
|
className: "p-8 text-center",
|
|
254
313
|
children: [
|
|
255
|
-
/* @__PURE__ */
|
|
256
|
-
/* @__PURE__ */
|
|
314
|
+
/* @__PURE__ */ w(C, { className: "mx-auto h-8 w-8 text-text-tertiary" }),
|
|
315
|
+
/* @__PURE__ */ w("p", {
|
|
257
316
|
className: "mt-3 text-sm font-medium text-text-primary",
|
|
258
317
|
children: "No AI operations yet"
|
|
259
318
|
}),
|
|
260
|
-
/* @__PURE__ */
|
|
319
|
+
/* @__PURE__ */ w("p", {
|
|
261
320
|
className: "mt-1 text-xs text-text-tertiary",
|
|
262
321
|
children: "Operations appear here once AI-generated guidance runs."
|
|
263
322
|
})
|
|
264
323
|
]
|
|
265
|
-
}) : /* @__PURE__ */
|
|
324
|
+
}) : /* @__PURE__ */ w(c, {
|
|
266
325
|
columns: [
|
|
267
326
|
{
|
|
268
327
|
key: "createdAt",
|
|
269
328
|
header: "Time",
|
|
270
329
|
sortValue: (e) => new Date(e.createdAt).getTime(),
|
|
271
|
-
render: (e) => /* @__PURE__ */
|
|
330
|
+
render: (e) => /* @__PURE__ */ T("div", {
|
|
272
331
|
className: "text-text-primary",
|
|
273
|
-
children: [/* @__PURE__ */
|
|
332
|
+
children: [/* @__PURE__ */ w("span", {
|
|
274
333
|
className: "block text-sm",
|
|
275
334
|
children: n(e.createdAt)
|
|
276
|
-
}), /* @__PURE__ */
|
|
335
|
+
}), /* @__PURE__ */ w("span", {
|
|
277
336
|
className: "text-xs text-text-tertiary",
|
|
278
337
|
children: i(e.createdAt)
|
|
279
338
|
})]
|
|
@@ -282,7 +341,7 @@ function F({ settings: l, logsQuery: u }) {
|
|
|
282
341
|
{
|
|
283
342
|
key: "operationType",
|
|
284
343
|
header: "Operation",
|
|
285
|
-
render: (e) => /* @__PURE__ */
|
|
344
|
+
render: (e) => /* @__PURE__ */ w("span", {
|
|
286
345
|
className: "text-text-secondary",
|
|
287
346
|
children: r(e.operationType)
|
|
288
347
|
})
|
|
@@ -290,10 +349,10 @@ function F({ settings: l, logsQuery: u }) {
|
|
|
290
349
|
{
|
|
291
350
|
key: "provider",
|
|
292
351
|
header: "Provider / Model",
|
|
293
|
-
render: (e) => /* @__PURE__ */
|
|
352
|
+
render: (e) => /* @__PURE__ */ T("div", { children: [/* @__PURE__ */ w("span", {
|
|
294
353
|
className: "block text-sm text-text-primary",
|
|
295
|
-
children:
|
|
296
|
-
}), /* @__PURE__ */
|
|
354
|
+
children: R[e.provider]
|
|
355
|
+
}), /* @__PURE__ */ w("span", {
|
|
297
356
|
className: "text-xs text-text-tertiary",
|
|
298
357
|
children: e.modelName
|
|
299
358
|
})] })
|
|
@@ -302,7 +361,7 @@ function F({ settings: l, logsQuery: u }) {
|
|
|
302
361
|
key: "tokens",
|
|
303
362
|
header: "Tokens",
|
|
304
363
|
align: "right",
|
|
305
|
-
render: (e) => /* @__PURE__ */
|
|
364
|
+
render: (e) => /* @__PURE__ */ T("span", {
|
|
306
365
|
className: "tabular-nums text-text-secondary",
|
|
307
366
|
children: [
|
|
308
367
|
e.inputTokens ?? "—",
|
|
@@ -314,7 +373,7 @@ function F({ settings: l, logsQuery: u }) {
|
|
|
314
373
|
{
|
|
315
374
|
key: "status",
|
|
316
375
|
header: "Status",
|
|
317
|
-
render: (e) => /* @__PURE__ */
|
|
376
|
+
render: (e) => /* @__PURE__ */ w("span", {
|
|
318
377
|
className: `inline-flex items-center rounded-full px-2 py-0.5 text-xs font-medium ${e.status === "SUCCESS" ? "bg-status-success-bg text-status-success-text" : "bg-status-error-bg text-status-error-text"}`,
|
|
319
378
|
children: r(e.status)
|
|
320
379
|
})
|
|
@@ -323,10 +382,10 @@ function F({ settings: l, logsQuery: u }) {
|
|
|
323
382
|
key: "error",
|
|
324
383
|
header: "Error",
|
|
325
384
|
hideOnMobile: !0,
|
|
326
|
-
render: (e) => e.errorMessage ? /* @__PURE__ */
|
|
385
|
+
render: (e) => e.errorMessage ? /* @__PURE__ */ w("span", {
|
|
327
386
|
className: "text-xs text-status-error-text",
|
|
328
387
|
children: e.errorMessage
|
|
329
|
-
}) : /* @__PURE__ */
|
|
388
|
+
}) : /* @__PURE__ */ w("span", {
|
|
330
389
|
className: "text-text-tertiary",
|
|
331
390
|
children: "—"
|
|
332
391
|
})
|
|
@@ -343,36 +402,36 @@ function F({ settings: l, logsQuery: u }) {
|
|
|
343
402
|
]
|
|
344
403
|
});
|
|
345
404
|
}
|
|
346
|
-
function
|
|
347
|
-
let t =
|
|
348
|
-
if (t.isLoading) return /* @__PURE__ */
|
|
405
|
+
function U() {
|
|
406
|
+
let t = d(), n = l();
|
|
407
|
+
if (t.isLoading) return /* @__PURE__ */ w(e, {
|
|
349
408
|
title: "AI controls",
|
|
350
|
-
children: /* @__PURE__ */
|
|
409
|
+
children: /* @__PURE__ */ w(o, { label: "Loading AI settings…" })
|
|
351
410
|
});
|
|
352
|
-
if (t.isError) return /* @__PURE__ */
|
|
411
|
+
if (t.isError) return /* @__PURE__ */ w(e, {
|
|
353
412
|
title: "AI controls",
|
|
354
|
-
children: /* @__PURE__ */
|
|
413
|
+
children: /* @__PURE__ */ w(a, {
|
|
355
414
|
title: "Could not load AI settings",
|
|
356
415
|
message: "We couldn't fetch your AI configuration. Try again in a moment.",
|
|
357
416
|
onRetry: () => t.refetch()
|
|
358
417
|
})
|
|
359
418
|
});
|
|
360
419
|
let r = t.data;
|
|
361
|
-
if (!r) return /* @__PURE__ */
|
|
420
|
+
if (!r) return /* @__PURE__ */ w(e, {
|
|
362
421
|
title: "AI controls",
|
|
363
|
-
children: /* @__PURE__ */
|
|
422
|
+
children: /* @__PURE__ */ w(a, {
|
|
364
423
|
title: "Could not load AI settings",
|
|
365
424
|
message: "We couldn't fetch your AI configuration. Try again in a moment.",
|
|
366
425
|
onRetry: () => t.refetch()
|
|
367
426
|
})
|
|
368
427
|
});
|
|
369
428
|
let i = `${r.id}:${r.updatedAt}`;
|
|
370
|
-
return /* @__PURE__ */
|
|
429
|
+
return /* @__PURE__ */ w(H, {
|
|
371
430
|
settings: r,
|
|
372
431
|
logsQuery: n
|
|
373
432
|
}, i);
|
|
374
433
|
}
|
|
375
434
|
//#endregion
|
|
376
|
-
export {
|
|
435
|
+
export { U as AiControlsPage };
|
|
377
436
|
|
|
378
437
|
//# sourceMappingURL=AiControlsPage.js.map
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"AiControlsPage.js","names":[],"sources":["../../../../src/mybodyshield/pages/ai/AiControlsPage.tsx"],"sourcesContent":["/**\n * AI controls — provider, model, daily allowance and operation history.\n * @module mybodyshield/pages/ai\n *\n * BOFF-5949: per-member AI settings surface. Lets the member choose a provider\n * (Gemini / OpenAI / Anthropic), the model name, a daily operation allowance,\n * and an optional provider API key. Shows the redacted audit log of AI ops.\n */\n\nimport { useMemo, useState } from 'react';\nimport { Sparkles, KeyRound, Bot, Activity, Save } from 'lucide-react';\nimport {\n Button,\n Input,\n Label,\n Select,\n SelectTrigger,\n SelectContent,\n SelectItem,\n SelectValue,\n GlassCard,\n Spinner,\n} from '@burdenoff/fe-libs/ui';\nimport {\n PageLayout,\n MetricsCard,\n LoadingView,\n ErrorView,\n Section,\n DataTable,\n type Column,\n} from '../../components';\nimport { useMybAiSettings, useMybUpdateAiSettings, useMybAiOperationLogs } from '../../hooks';\nimport type {\n MybAiProvider,\n MybAiSettings,\n MybAiOperationLog,\n MybAiOperationLogConnection,\n MybUpdateAiSettingsInput,\n} from '../../types';\nimport { formatDateTime, formatRelativeTime, formatEnumLabel } from '../../utils/formatters';\nimport type { UseQueryResult } from '@tanstack/react-query';\n\nconst AI_PROVIDERS: MybAiProvider[] = ['GEMINI', 'OPENAI', 'ANTHROPIC'];\n\nconst PROVIDER_LABELS: Record<MybAiProvider, string> = {\n GEMINI: 'Google Gemini',\n OPENAI: 'OpenAI',\n ANTHROPIC: 'Anthropic',\n};\n\nconst DEFAULT_MODELS: Record<MybAiProvider, string> = {\n GEMINI: 'gemini-2.5-flash',\n OPENAI: 'gpt-4o-mini',\n ANTHROPIC: 'claude-3-5-haiku-latest',\n};\n\ninterface AiSettingsForm {\n provider: MybAiProvider;\n modelName: string;\n dailyAllowance: string;\n providerKey: string;\n clearKey: boolean;\n}\n\nconst makeFormFromSettings = (settings: MybAiSettings): AiSettingsForm => ({\n provider: settings.provider,\n modelName: settings.modelName,\n dailyAllowance: String(settings.dailyAllowance),\n providerKey: '',\n clearKey: false,\n});\n\ninterface AiControlsPageInnerProps {\n settings: MybAiSettings;\n logsQuery: UseQueryResult<MybAiOperationLogConnection, Error>;\n}\n\nfunction AiControlsPageInner({ settings, logsQuery }: AiControlsPageInnerProps) {\n const updateMutation = useMybUpdateAiSettings();\n const [form, setForm] = useState<AiSettingsForm>(() => makeFormFromSettings(settings));\n const [err, setErr] = useState<string | null>(null);\n const [saved, setSaved] = useState(false);\n\n const usagePercent = useMemo(() => {\n const { dailyAllowance, dailyAllowanceUsed } = settings;\n if (dailyAllowance <= 0) return 0;\n return Math.min(100, Math.round((dailyAllowanceUsed / dailyAllowance) * 100));\n }, [settings]);\n\n const isExhausted = settings.dailyAllowanceUsed >= settings.dailyAllowance;\n\n const handleProviderChange = (provider: MybAiProvider) => {\n setForm((f) => ({\n ...f,\n provider,\n modelName:\n f.modelName === DEFAULT_MODELS[f.provider] ? DEFAULT_MODELS[provider] : f.modelName,\n }));\n };\n\n const submit = async () => {\n setErr(null);\n setSaved(false);\n\n const allowance = Number(form.dailyAllowance);\n if (!Number.isInteger(allowance) || allowance < 0 || allowance > 10_000) {\n setErr('Daily allowance must be an integer between 0 and 10,000.');\n return;\n }\n if (!form.modelName.trim()) {\n setErr('Model name is required.');\n return;\n }\n\n const input: MybUpdateAiSettingsInput = {\n provider: form.provider,\n modelName: form.modelName.trim(),\n dailyAllowance: allowance,\n };\n\n if (form.clearKey) {\n input.providerKey = '';\n } else if (form.providerKey.trim()) {\n input.providerKey = form.providerKey.trim();\n }\n\n try {\n await updateMutation.mutateAsync(input);\n setSaved(true);\n setForm((f) => ({ ...f, providerKey: '', clearKey: false }));\n } catch (e) {\n setErr(e instanceof Error ? e.message : 'Failed to save AI settings.');\n }\n };\n\n const logColumns: Column<MybAiOperationLog>[] = [\n {\n key: 'createdAt',\n header: 'Time',\n sortValue: (r) => new Date(r.createdAt).getTime(),\n render: (r) => (\n <div className=\"text-text-primary\">\n <span className=\"block text-sm\">{formatDateTime(r.createdAt)}</span>\n <span className=\"text-xs text-text-tertiary\">{formatRelativeTime(r.createdAt)}</span>\n </div>\n ),\n },\n {\n key: 'operationType',\n header: 'Operation',\n render: (r) => (\n <span className=\"text-text-secondary\">{formatEnumLabel(r.operationType)}</span>\n ),\n },\n {\n key: 'provider',\n header: 'Provider / Model',\n render: (r) => (\n <div>\n <span className=\"block text-sm text-text-primary\">{PROVIDER_LABELS[r.provider]}</span>\n <span className=\"text-xs text-text-tertiary\">{r.modelName}</span>\n </div>\n ),\n },\n {\n key: 'tokens',\n header: 'Tokens',\n align: 'right',\n render: (r) => (\n <span className=\"tabular-nums text-text-secondary\">\n {r.inputTokens ?? '—'} / {r.outputTokens ?? '—'}\n </span>\n ),\n },\n {\n key: 'status',\n header: 'Status',\n render: (r) => (\n <span\n className={`inline-flex items-center rounded-full px-2 py-0.5 text-xs font-medium ${\n r.status === 'SUCCESS'\n ? 'bg-status-success-bg text-status-success-text'\n : 'bg-status-error-bg text-status-error-text'\n }`}\n >\n {formatEnumLabel(r.status)}\n </span>\n ),\n },\n {\n key: 'error',\n header: 'Error',\n hideOnMobile: true,\n render: (r) =>\n r.errorMessage ? (\n <span className=\"text-xs text-status-error-text\">{r.errorMessage}</span>\n ) : (\n <span className=\"text-text-tertiary\">—</span>\n ),\n },\n ];\n\n return (\n <PageLayout\n title=\"AI controls\"\n description=\"Choose your AI provider, model and daily allowance.\"\n actions={\n <Button className=\"gap-2\" onClick={submit} disabled={updateMutation.isPending}>\n {updateMutation.isPending ? <Spinner size=\"sm\" /> : <Save className=\"h-4 w-4\" />}\n Save settings\n </Button>\n }\n >\n {/* KPI grid */}\n <div className=\"grid grid-cols-1 gap-4 sm:grid-cols-3\">\n <MetricsCard\n icon={<Sparkles className=\"h-5 w-5\" />}\n value={settings.provider}\n label=\"Current provider\"\n />\n <MetricsCard\n icon={<Bot className=\"h-5 w-5\" />}\n value={settings.modelName}\n label=\"Current model\"\n />\n <MetricsCard\n icon={<Activity className=\"h-5 w-5\" />}\n value={`${settings.dailyAllowanceUsed} / ${settings.dailyAllowance}`}\n label=\"Daily allowance used\"\n accent={isExhausted ? 'error' : usagePercent >= 80 ? 'warning' : 'success'}\n footer={\n <p className=\"text-xs text-text-tertiary\">\n Resets {formatRelativeTime(settings.allowanceResetAt)}\n </p>\n }\n />\n </div>\n\n {/* Settings form */}\n <Section title=\"Configuration\" description=\"Changes apply to your account only.\">\n <GlassCard treatment=\"plain\" className=\"p-6\">\n <div className=\"grid gap-6 md:grid-cols-2\">\n <div className=\"space-y-1.5\">\n <Label htmlFor=\"myb-ai-provider\">AI provider</Label>\n <Select\n value={form.provider}\n onValueChange={(v) => handleProviderChange(v as MybAiProvider)}\n >\n <SelectTrigger id=\"myb-ai-provider\">\n <SelectValue />\n </SelectTrigger>\n <SelectContent>\n {AI_PROVIDERS.map((p) => (\n <SelectItem key={p} value={p}>\n {PROVIDER_LABELS[p]}\n </SelectItem>\n ))}\n </SelectContent>\n </Select>\n <p className=\"text-xs text-text-tertiary\">\n The provider used for AI-generated guidance summaries.\n </p>\n </div>\n\n <div className=\"space-y-1.5\">\n <Label htmlFor=\"myb-ai-model\">Model name</Label>\n <Input\n id=\"myb-ai-model\"\n value={form.modelName}\n onChange={(e) => setForm((f) => ({ ...f, modelName: e.target.value }))}\n placeholder={DEFAULT_MODELS[form.provider]}\n />\n <p className=\"text-xs text-text-tertiary\">\n e.g. {DEFAULT_MODELS[form.provider]}. Must be valid for the chosen provider.\n </p>\n </div>\n\n <div className=\"space-y-1.5\">\n <Label htmlFor=\"myb-ai-allowance\">Daily allowance</Label>\n <Input\n id=\"myb-ai-allowance\"\n type=\"number\"\n min={0}\n max={10000}\n step={1}\n value={form.dailyAllowance}\n onChange={(e) => setForm((f) => ({ ...f, dailyAllowance: e.target.value }))}\n />\n <p className=\"text-xs text-text-tertiary\">\n Maximum AI operations counted against your account per UTC day.\n </p>\n </div>\n\n <div className=\"space-y-1.5\">\n <Label htmlFor=\"myb-ai-key\">\n <span className=\"inline-flex items-center gap-1.5\">\n <KeyRound className=\"h-3.5 w-3.5\" />\n Provider API key\n </span>\n </Label>\n <Input\n id=\"myb-ai-key\"\n type=\"password\"\n value={form.providerKey}\n onChange={(e) =>\n setForm((f) => ({ ...f, providerKey: e.target.value, clearKey: false }))\n }\n placeholder={settings.hasProviderKey ? '•••••••• (saved)' : 'Optional'}\n disabled={form.clearKey}\n />\n <div className=\"flex items-center gap-2\">\n <input\n id=\"myb-ai-clear-key\"\n type=\"checkbox\"\n checked={form.clearKey}\n onChange={(e) =>\n setForm((f) => ({ ...f, clearKey: e.target.checked, providerKey: '' }))\n }\n className=\"h-4 w-4 rounded border-border-seam text-accent focus:ring-accent\"\n />\n <Label\n htmlFor=\"myb-ai-clear-key\"\n className=\"text-xs font-normal text-text-secondary\"\n >\n Clear saved key and fall back to the workspace default\n </Label>\n </div>\n </div>\n </div>\n\n {saved && <p className=\"mt-4 text-sm text-status-success-text\">AI settings saved.</p>}\n {err && <p className=\"mt-4 text-sm text-status-error-text\">{err}</p>}\n </GlassCard>\n </Section>\n\n {/* Operation log */}\n <Section\n title=\"Operation history\"\n description=\"Redacted audit log of AI operations for your account.\"\n >\n {logsQuery.isLoading ? (\n <LoadingView label=\"Loading operation history…\" />\n ) : logsQuery.isError ? (\n <ErrorView\n message=\"We couldn't load the operation log.\"\n onRetry={() => logsQuery.refetch()}\n />\n ) : logsQuery.data?.items.length === 0 ? (\n <GlassCard treatment=\"plain\" className=\"p-8 text-center\">\n <Sparkles className=\"mx-auto h-8 w-8 text-text-tertiary\" />\n <p className=\"mt-3 text-sm font-medium text-text-primary\">No AI operations yet</p>\n <p className=\"mt-1 text-xs text-text-tertiary\">\n Operations appear here once AI-generated guidance runs.\n </p>\n </GlassCard>\n ) : (\n <DataTable\n columns={logColumns}\n rows={logsQuery.data?.items ?? []}\n rowKey={(r) => r.id}\n defaultSort={{ key: 'createdAt', dir: 'desc' }}\n />\n )}\n </Section>\n </PageLayout>\n );\n}\n\nexport function AiControlsPage() {\n const settingsQuery = useMybAiSettings();\n const logsQuery = useMybAiOperationLogs();\n\n if (settingsQuery.isLoading) {\n return (\n <PageLayout title=\"AI controls\">\n <LoadingView label=\"Loading AI settings…\" />\n </PageLayout>\n );\n }\n\n if (settingsQuery.isError) {\n return (\n <PageLayout title=\"AI controls\">\n <ErrorView\n title=\"Could not load AI settings\"\n message=\"We couldn't fetch your AI configuration. Try again in a moment.\"\n onRetry={() => settingsQuery.refetch()}\n />\n </PageLayout>\n );\n }\n\n const settings = settingsQuery.data;\n if (!settings) {\n return (\n <PageLayout title=\"AI controls\">\n <ErrorView\n title=\"Could not load AI settings\"\n message=\"We couldn't fetch your AI configuration. Try again in a moment.\"\n onRetry={() => settingsQuery.refetch()}\n />\n </PageLayout>\n );\n }\n\n // Key the inner component on the settings revision so form state re-seeds\n // when the source settings change, without synchronously setting state in\n // an effect (which triggers cascading renders).\n const settingsKey = `${settings.id}:${settings.updatedAt}`;\n\n return <AiControlsPageInner key={settingsKey} settings={settings} logsQuery={logsQuery} />;\n}\n"],"mappings":"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"}
|
|
1
|
+
{"version":3,"file":"AiControlsPage.js","names":[],"sources":["../../../../src/mybodyshield/pages/ai/AiControlsPage.tsx"],"sourcesContent":["/**\n * AI controls — provider, model, daily allowance and operation history.\n * @module mybodyshield/pages/ai\n *\n * BOFF-5949: per-member AI settings surface. Lets the member choose a provider\n * (Gemini / OpenAI / Anthropic), the model name, a daily operation allowance,\n * and an optional provider API key. Shows the redacted audit log of AI ops.\n */\n\nimport { useMemo, useState } from 'react';\nimport { Sparkles, KeyRound, Bot, Activity, Save, ShieldCheck, ShieldOff } from 'lucide-react';\nimport {\n Button,\n Input,\n Label,\n Select,\n SelectTrigger,\n SelectContent,\n SelectItem,\n SelectValue,\n Switch,\n GlassCard,\n Spinner,\n} from '@burdenoff/fe-libs/ui';\nimport {\n PageLayout,\n MetricsCard,\n LoadingView,\n ErrorView,\n Section,\n DataTable,\n type Column,\n} from '../../components';\nimport {\n useMybAiSettings,\n useMybUpdateAiSettings,\n useMybAiOperationLogs,\n useMybAiProcessingConsent,\n useMybSetAiProcessingConsent,\n} from '../../hooks';\nimport type {\n MybAiProvider,\n MybAiSettings,\n MybAiOperationLog,\n MybAiOperationLogConnection,\n MybUpdateAiSettingsInput,\n} from '../../types';\nimport { formatDateTime, formatRelativeTime, formatEnumLabel } from '../../utils/formatters';\nimport { IconChip } from '../_shared/visuals';\nimport type { UseQueryResult } from '@tanstack/react-query';\n\nconst AI_PROVIDERS: MybAiProvider[] = ['GEMINI', 'OPENAI', 'ANTHROPIC'];\n\nconst PROVIDER_LABELS: Record<MybAiProvider, string> = {\n GEMINI: 'Google Gemini',\n OPENAI: 'OpenAI',\n ANTHROPIC: 'Anthropic',\n};\n\nconst DEFAULT_MODELS: Record<MybAiProvider, string> = {\n GEMINI: 'gemini-2.5-flash',\n OPENAI: 'gpt-4o-mini',\n ANTHROPIC: 'claude-3-5-haiku-latest',\n};\n\ninterface AiSettingsForm {\n provider: MybAiProvider;\n modelName: string;\n dailyAllowance: string;\n providerKey: string;\n clearKey: boolean;\n}\n\nconst makeFormFromSettings = (settings: MybAiSettings): AiSettingsForm => ({\n provider: settings.provider,\n modelName: settings.modelName,\n dailyAllowance: String(settings.dailyAllowance),\n providerKey: '',\n clearKey: false,\n});\n\n/**\n * AI-processing consent gate (BOFF-3838).\n *\n * The settings on this page only take effect while consent is granted: the\n * service checks `mybMyAiProcessingConsent` before it will generate a guidance\n * summary, and returns a null summary without it. Surfacing the state here (and\n * not only under Settings) stops a member from configuring a provider and model\n * that can never run. Reads and writes the same query key as the Settings page,\n * so the two stay in sync.\n */\nfunction AiConsentGate() {\n const consent = useMybAiProcessingConsent();\n const setConsent = useMybSetAiProcessingConsent();\n\n if (consent.isLoading) {\n return <LoadingView label=\"Loading AI-processing consent…\" />;\n }\n\n if (consent.isError) {\n return (\n <ErrorView\n message=\"We couldn't load your AI-processing consent.\"\n onRetry={() => consent.refetch()}\n />\n );\n }\n\n const enabled = consent.data?.enabled ?? false;\n\n return (\n <GlassCard treatment=\"plain\" className=\"p-5\">\n <div className=\"flex flex-wrap items-start justify-between gap-4\">\n <div className=\"flex min-w-0 items-start gap-3\">\n <IconChip\n icon={enabled ? <ShieldCheck className=\"h-5 w-5\" /> : <ShieldOff className=\"h-5 w-5\" />}\n />\n <div className=\"min-w-0\">\n <p className=\"text-sm font-medium text-text-primary\">\n {enabled\n ? 'AI may read your health data'\n : 'AI processing is withdrawn — summaries will not run'}\n </p>\n <p className=\"mt-0.5 text-sm text-text-secondary\">\n {enabled\n ? 'The provider and model below are used to turn your logged training, nutrition, recovery and body-metric data into plain-language summaries.'\n : 'The provider and model below are saved, but no AI summary is generated while consent is withdrawn. Rule-based guidance and every manual logging path keep working.'}\n </p>\n {consent.data?.withdrawnAt && !enabled && (\n <p className=\"mt-1 text-xs text-text-tertiary\">\n Withdrawn {formatRelativeTime(consent.data.withdrawnAt)}.\n </p>\n )}\n {consent.data?.acceptedAt && enabled && (\n <p className=\"mt-1 text-xs text-text-tertiary\">\n Consent given {formatRelativeTime(consent.data.acceptedAt)}.\n </p>\n )}\n </div>\n </div>\n <Switch\n checked={enabled}\n onCheckedChange={(checked) => setConsent.mutate(checked)}\n disabled={setConsent.isPending}\n aria-label=\"Allow AI to read my health data\"\n />\n </div>\n </GlassCard>\n );\n}\n\ninterface AiControlsPageInnerProps {\n settings: MybAiSettings;\n logsQuery: UseQueryResult<MybAiOperationLogConnection, Error>;\n}\n\nfunction AiControlsPageInner({ settings, logsQuery }: AiControlsPageInnerProps) {\n const updateMutation = useMybUpdateAiSettings();\n const [form, setForm] = useState<AiSettingsForm>(() => makeFormFromSettings(settings));\n const [err, setErr] = useState<string | null>(null);\n const [saved, setSaved] = useState(false);\n\n const usagePercent = useMemo(() => {\n const { dailyAllowance, dailyAllowanceUsed } = settings;\n if (dailyAllowance <= 0) return 0;\n return Math.min(100, Math.round((dailyAllowanceUsed / dailyAllowance) * 100));\n }, [settings]);\n\n const isExhausted = settings.dailyAllowanceUsed >= settings.dailyAllowance;\n\n const handleProviderChange = (provider: MybAiProvider) => {\n setForm((f) => ({\n ...f,\n provider,\n modelName:\n f.modelName === DEFAULT_MODELS[f.provider] ? DEFAULT_MODELS[provider] : f.modelName,\n }));\n };\n\n const submit = async () => {\n setErr(null);\n setSaved(false);\n\n const allowance = Number(form.dailyAllowance);\n if (!Number.isInteger(allowance) || allowance < 0 || allowance > 10_000) {\n setErr('Daily allowance must be an integer between 0 and 10,000.');\n return;\n }\n if (!form.modelName.trim()) {\n setErr('Model name is required.');\n return;\n }\n\n const input: MybUpdateAiSettingsInput = {\n provider: form.provider,\n modelName: form.modelName.trim(),\n dailyAllowance: allowance,\n };\n\n if (form.clearKey) {\n input.providerKey = '';\n } else if (form.providerKey.trim()) {\n input.providerKey = form.providerKey.trim();\n }\n\n try {\n await updateMutation.mutateAsync(input);\n setSaved(true);\n setForm((f) => ({ ...f, providerKey: '', clearKey: false }));\n } catch (e) {\n setErr(e instanceof Error ? e.message : 'Failed to save AI settings.');\n }\n };\n\n const logColumns: Column<MybAiOperationLog>[] = [\n {\n key: 'createdAt',\n header: 'Time',\n sortValue: (r) => new Date(r.createdAt).getTime(),\n render: (r) => (\n <div className=\"text-text-primary\">\n <span className=\"block text-sm\">{formatDateTime(r.createdAt)}</span>\n <span className=\"text-xs text-text-tertiary\">{formatRelativeTime(r.createdAt)}</span>\n </div>\n ),\n },\n {\n key: 'operationType',\n header: 'Operation',\n render: (r) => (\n <span className=\"text-text-secondary\">{formatEnumLabel(r.operationType)}</span>\n ),\n },\n {\n key: 'provider',\n header: 'Provider / Model',\n render: (r) => (\n <div>\n <span className=\"block text-sm text-text-primary\">{PROVIDER_LABELS[r.provider]}</span>\n <span className=\"text-xs text-text-tertiary\">{r.modelName}</span>\n </div>\n ),\n },\n {\n key: 'tokens',\n header: 'Tokens',\n align: 'right',\n render: (r) => (\n <span className=\"tabular-nums text-text-secondary\">\n {r.inputTokens ?? '—'} / {r.outputTokens ?? '—'}\n </span>\n ),\n },\n {\n key: 'status',\n header: 'Status',\n render: (r) => (\n <span\n className={`inline-flex items-center rounded-full px-2 py-0.5 text-xs font-medium ${\n r.status === 'SUCCESS'\n ? 'bg-status-success-bg text-status-success-text'\n : 'bg-status-error-bg text-status-error-text'\n }`}\n >\n {formatEnumLabel(r.status)}\n </span>\n ),\n },\n {\n key: 'error',\n header: 'Error',\n hideOnMobile: true,\n render: (r) =>\n r.errorMessage ? (\n <span className=\"text-xs text-status-error-text\">{r.errorMessage}</span>\n ) : (\n <span className=\"text-text-tertiary\">—</span>\n ),\n },\n ];\n\n return (\n <PageLayout\n title=\"AI controls\"\n description=\"Choose your AI provider, model and daily allowance.\"\n actions={\n <Button className=\"gap-2\" onClick={submit} disabled={updateMutation.isPending}>\n {updateMutation.isPending ? <Spinner size=\"sm\" /> : <Save className=\"h-4 w-4\" />}\n Save settings\n </Button>\n }\n >\n {/* Consent gate — settings below only take effect while this is on. */}\n <Section\n title=\"AI-processing consent\"\n description=\"AI summaries only run while you allow AI to read your health data.\"\n >\n <AiConsentGate />\n </Section>\n\n {/* KPI grid */}\n <div className=\"grid grid-cols-1 gap-4 sm:grid-cols-3\">\n <MetricsCard\n icon={<Sparkles className=\"h-5 w-5\" />}\n value={settings.provider}\n label=\"Current provider\"\n />\n <MetricsCard\n icon={<Bot className=\"h-5 w-5\" />}\n value={settings.modelName}\n label=\"Current model\"\n />\n <MetricsCard\n icon={<Activity className=\"h-5 w-5\" />}\n value={`${settings.dailyAllowanceUsed} / ${settings.dailyAllowance}`}\n label=\"Daily allowance used\"\n accent={isExhausted ? 'error' : usagePercent >= 80 ? 'warning' : 'success'}\n footer={\n <p className=\"text-xs text-text-tertiary\">\n Resets {formatRelativeTime(settings.allowanceResetAt)}\n </p>\n }\n />\n </div>\n\n {/* Settings form */}\n <Section title=\"Configuration\" description=\"Changes apply to your account only.\">\n <GlassCard treatment=\"plain\" className=\"p-6\">\n <div className=\"grid gap-6 md:grid-cols-2\">\n <div className=\"space-y-1.5\">\n <Label htmlFor=\"myb-ai-provider\">AI provider</Label>\n <Select\n value={form.provider}\n onValueChange={(v) => handleProviderChange(v as MybAiProvider)}\n >\n <SelectTrigger id=\"myb-ai-provider\">\n <SelectValue />\n </SelectTrigger>\n <SelectContent>\n {AI_PROVIDERS.map((p) => (\n <SelectItem key={p} value={p}>\n {PROVIDER_LABELS[p]}\n </SelectItem>\n ))}\n </SelectContent>\n </Select>\n <p className=\"text-xs text-text-tertiary\">\n The provider used for AI-generated guidance summaries.\n </p>\n </div>\n\n <div className=\"space-y-1.5\">\n <Label htmlFor=\"myb-ai-model\">Model name</Label>\n <Input\n id=\"myb-ai-model\"\n value={form.modelName}\n onChange={(e) => setForm((f) => ({ ...f, modelName: e.target.value }))}\n placeholder={DEFAULT_MODELS[form.provider]}\n />\n <p className=\"text-xs text-text-tertiary\">\n e.g. {DEFAULT_MODELS[form.provider]}. Must be valid for the chosen provider.\n </p>\n </div>\n\n <div className=\"space-y-1.5\">\n <Label htmlFor=\"myb-ai-allowance\">Daily allowance</Label>\n <Input\n id=\"myb-ai-allowance\"\n type=\"number\"\n min={0}\n max={10000}\n step={1}\n value={form.dailyAllowance}\n onChange={(e) => setForm((f) => ({ ...f, dailyAllowance: e.target.value }))}\n />\n <p className=\"text-xs text-text-tertiary\">\n Maximum AI operations counted against your account per UTC day.\n </p>\n </div>\n\n <div className=\"space-y-1.5\">\n <Label htmlFor=\"myb-ai-key\">\n <span className=\"inline-flex items-center gap-1.5\">\n <KeyRound className=\"h-3.5 w-3.5\" />\n Provider API key\n </span>\n </Label>\n <Input\n id=\"myb-ai-key\"\n type=\"password\"\n value={form.providerKey}\n onChange={(e) =>\n setForm((f) => ({ ...f, providerKey: e.target.value, clearKey: false }))\n }\n placeholder={settings.hasProviderKey ? '•••••••• (saved)' : 'Optional'}\n disabled={form.clearKey}\n />\n <div className=\"flex items-center gap-2\">\n <input\n id=\"myb-ai-clear-key\"\n type=\"checkbox\"\n checked={form.clearKey}\n onChange={(e) =>\n setForm((f) => ({ ...f, clearKey: e.target.checked, providerKey: '' }))\n }\n className=\"h-4 w-4 rounded border-border-seam text-accent focus:ring-accent\"\n />\n <Label\n htmlFor=\"myb-ai-clear-key\"\n className=\"text-xs font-normal text-text-secondary\"\n >\n Clear saved key and fall back to the workspace default\n </Label>\n </div>\n </div>\n </div>\n\n {saved && <p className=\"mt-4 text-sm text-status-success-text\">AI settings saved.</p>}\n {err && <p className=\"mt-4 text-sm text-status-error-text\">{err}</p>}\n </GlassCard>\n </Section>\n\n {/* Operation log */}\n <Section\n title=\"Operation history\"\n description=\"Redacted audit log of AI operations for your account.\"\n >\n {logsQuery.isLoading ? (\n <LoadingView label=\"Loading operation history…\" />\n ) : logsQuery.isError ? (\n <ErrorView\n message=\"We couldn't load the operation log.\"\n onRetry={() => logsQuery.refetch()}\n />\n ) : logsQuery.data?.items.length === 0 ? (\n <GlassCard treatment=\"plain\" className=\"p-8 text-center\">\n <Sparkles className=\"mx-auto h-8 w-8 text-text-tertiary\" />\n <p className=\"mt-3 text-sm font-medium text-text-primary\">No AI operations yet</p>\n <p className=\"mt-1 text-xs text-text-tertiary\">\n Operations appear here once AI-generated guidance runs.\n </p>\n </GlassCard>\n ) : (\n <DataTable\n columns={logColumns}\n rows={logsQuery.data?.items ?? []}\n rowKey={(r) => r.id}\n defaultSort={{ key: 'createdAt', dir: 'desc' }}\n />\n )}\n </Section>\n </PageLayout>\n );\n}\n\nexport function AiControlsPage() {\n const settingsQuery = useMybAiSettings();\n const logsQuery = useMybAiOperationLogs();\n\n if (settingsQuery.isLoading) {\n return (\n <PageLayout title=\"AI controls\">\n <LoadingView label=\"Loading AI settings…\" />\n </PageLayout>\n );\n }\n\n if (settingsQuery.isError) {\n return (\n <PageLayout title=\"AI controls\">\n <ErrorView\n title=\"Could not load AI settings\"\n message=\"We couldn't fetch your AI configuration. Try again in a moment.\"\n onRetry={() => settingsQuery.refetch()}\n />\n </PageLayout>\n );\n }\n\n const settings = settingsQuery.data;\n if (!settings) {\n return (\n <PageLayout title=\"AI controls\">\n <ErrorView\n title=\"Could not load AI settings\"\n message=\"We couldn't fetch your AI configuration. Try again in a moment.\"\n onRetry={() => settingsQuery.refetch()}\n />\n </PageLayout>\n );\n }\n\n // Key the inner component on the settings revision so form state re-seeds\n // when the source settings change, without synchronously setting state in\n // an effect (which triggers cascading renders).\n const settingsKey = `${settings.id}:${settings.updatedAt}`;\n\n return <AiControlsPageInner key={settingsKey} settings={settings} logsQuery={logsQuery} />;\n}\n"],"mappings":"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"}
|