@fraza/module-ai 0.0.0-stage → 0.4.22

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.
Files changed (53) hide show
  1. package/CHANGELOG.md +152 -0
  2. package/LICENSE +21 -0
  3. package/README.md +72 -2
  4. package/THIRD-PARTY-NOTICES.md +11 -0
  5. package/changelog.json +459 -0
  6. package/dist/admin/AiActions.client.js +166 -0
  7. package/dist/admin/AiChoices.client.js +67 -0
  8. package/dist/admin/AiInputForm.client.js +57 -0
  9. package/dist/admin/AiPage.client.js +181 -0
  10. package/dist/admin/AiResults.client.js +76 -0
  11. package/dist/admin/AiStudio.client.js +453 -0
  12. package/dist/admin/AiUsageWidget.client.js +28 -0
  13. package/dist/admin/StudioBar.client.js +33 -0
  14. package/dist/admin/StudioCompose.client.js +457 -0
  15. package/dist/admin/StudioComposer.client.js +217 -0
  16. package/dist/admin/StudioFields.client.js +124 -0
  17. package/dist/admin/StudioPlan.client.js +94 -0
  18. package/dist/admin/StudioWorking.client.js +157 -0
  19. package/dist/admin/choices.js +22 -0
  20. package/dist/admin/format.js +19 -0
  21. package/dist/admin/input-form.js +59 -0
  22. package/dist/admin/paths.js +4 -0
  23. package/dist/admin/studio-compose.js +94 -0
  24. package/dist/admin/studio-diff.js +240 -0
  25. package/dist/admin/widgets.js +14 -0
  26. package/dist/admin.js +9 -0
  27. package/dist/api.server.js +155 -0
  28. package/dist/config.server.js +113 -0
  29. package/dist/cron.server.js +26 -0
  30. package/dist/db.server.js +65 -0
  31. package/dist/index.js +41 -0
  32. package/dist/integration.server.js +28 -0
  33. package/dist/migrations.js +4 -0
  34. package/dist/permissions.js +9 -0
  35. package/dist/prompts.server.js +149 -0
  36. package/dist/providers/anthropic.server.js +42 -0
  37. package/dist/providers/chat-completions.server.js +63 -0
  38. package/dist/providers/google.server.js +52 -0
  39. package/dist/providers/http.server.js +98 -0
  40. package/dist/providers/index.server.js +22 -0
  41. package/dist/providers/ollama.server.js +45 -0
  42. package/dist/register-server.js +34 -0
  43. package/dist/results.server.js +74 -0
  44. package/dist/run.server.js +166 -0
  45. package/dist/runs.server.js +106 -0
  46. package/dist/schema.js +190 -0
  47. package/dist/server.js +19 -0
  48. package/dist/source.js +4 -0
  49. package/dist/studio.js +205 -0
  50. package/dist/test-prompt.server.js +24 -0
  51. package/dist/version.js +4 -0
  52. package/dist/widgets.server.js +29 -0
  53. package/package.json +67 -4
package/changelog.json ADDED
@@ -0,0 +1,459 @@
1
+ {
2
+ "name": "@fraza/module-ai",
3
+ "versions": [
4
+ {
5
+ "version": "0.4.22",
6
+ "date": "2026-11-08",
7
+ "breaking": [],
8
+ "envAdded": [],
9
+ "envRemoved": [],
10
+ "envRenamed": [],
11
+ "templateChanged": [],
12
+ "action": [],
13
+ "entries": []
14
+ },
15
+ {
16
+ "version": "0.4.21",
17
+ "date": "2026-11-07",
18
+ "breaking": [],
19
+ "envAdded": [],
20
+ "envRemoved": [],
21
+ "envRenamed": [],
22
+ "templateChanged": [],
23
+ "action": [],
24
+ "entries": [
25
+ {
26
+ "type": "feat",
27
+ "scope": "ui",
28
+ "subject": "add animated working status to ai studio",
29
+ "hash": "92e1667",
30
+ "breaking": false
31
+ }
32
+ ]
33
+ },
34
+ {
35
+ "version": "0.4.20",
36
+ "date": "2026-11-07",
37
+ "breaking": [],
38
+ "envAdded": [],
39
+ "envRemoved": [],
40
+ "envRenamed": [],
41
+ "templateChanged": [],
42
+ "action": [],
43
+ "entries": [
44
+ {
45
+ "type": "style",
46
+ "scope": "ui",
47
+ "subject": "move studio composer focus light inside the box",
48
+ "hash": "78f9481",
49
+ "breaking": false
50
+ }
51
+ ]
52
+ },
53
+ {
54
+ "version": "0.4.19",
55
+ "date": "2026-11-07",
56
+ "breaking": [],
57
+ "envAdded": [],
58
+ "envRemoved": [],
59
+ "envRenamed": [],
60
+ "templateChanged": [],
61
+ "action": [],
62
+ "entries": [
63
+ {
64
+ "type": "style",
65
+ "scope": "ui",
66
+ "subject": "soften studio composer focus glow and grain",
67
+ "hash": "b93cb0d",
68
+ "breaking": false
69
+ }
70
+ ]
71
+ },
72
+ {
73
+ "version": "0.4.18",
74
+ "date": "2026-11-07",
75
+ "breaking": [],
76
+ "envAdded": [],
77
+ "envRemoved": [],
78
+ "envRenamed": [],
79
+ "templateChanged": [],
80
+ "action": [],
81
+ "entries": [
82
+ {
83
+ "type": "style",
84
+ "scope": "ui",
85
+ "subject": "reshape studio composer glow to match the box",
86
+ "hash": "d54e21a",
87
+ "breaking": false
88
+ }
89
+ ]
90
+ },
91
+ {
92
+ "version": "0.4.17",
93
+ "date": "2026-11-07",
94
+ "breaking": [],
95
+ "envAdded": [],
96
+ "envRemoved": [],
97
+ "envRenamed": [],
98
+ "templateChanged": [],
99
+ "action": [],
100
+ "entries": [
101
+ {
102
+ "type": "style",
103
+ "scope": "ui",
104
+ "subject": "soften studio composer focus glow",
105
+ "hash": "7693879",
106
+ "breaking": false
107
+ }
108
+ ]
109
+ },
110
+ {
111
+ "version": "0.4.16",
112
+ "date": "2026-11-07",
113
+ "breaking": [],
114
+ "envAdded": [],
115
+ "envRemoved": [],
116
+ "envRenamed": [],
117
+ "templateChanged": [],
118
+ "action": [],
119
+ "entries": [
120
+ {
121
+ "type": "style",
122
+ "scope": "ui",
123
+ "subject": "soften studio composer glow and shadow",
124
+ "hash": "88946a9",
125
+ "breaking": false
126
+ }
127
+ ]
128
+ },
129
+ {
130
+ "version": "0.4.15",
131
+ "date": "2026-11-07",
132
+ "breaking": [],
133
+ "envAdded": [],
134
+ "envRemoved": [],
135
+ "envRenamed": [],
136
+ "templateChanged": [],
137
+ "action": [],
138
+ "entries": [
139
+ {
140
+ "type": "feat",
141
+ "scope": "admin",
142
+ "subject": "add start mode panel to studio composer",
143
+ "hash": "2cbd1f6",
144
+ "breaking": false
145
+ }
146
+ ]
147
+ },
148
+ {
149
+ "version": "0.4.14",
150
+ "date": "2026-11-07",
151
+ "breaking": [],
152
+ "envAdded": [],
153
+ "envRemoved": [],
154
+ "envRenamed": [],
155
+ "templateChanged": [],
156
+ "action": [],
157
+ "entries": [
158
+ {
159
+ "type": "refactor",
160
+ "scope": "ai",
161
+ "subject": "extract studio composer into its own component",
162
+ "hash": "4e74444",
163
+ "breaking": false
164
+ }
165
+ ]
166
+ },
167
+ {
168
+ "version": "0.4.13",
169
+ "date": "2026-11-07",
170
+ "breaking": [],
171
+ "envAdded": [],
172
+ "envRemoved": [],
173
+ "envRenamed": [],
174
+ "templateChanged": [],
175
+ "action": [],
176
+ "entries": [
177
+ {
178
+ "type": "style",
179
+ "scope": "ui",
180
+ "subject": "add bottom border to studio label bar",
181
+ "hash": "fc4d004",
182
+ "breaking": false
183
+ }
184
+ ]
185
+ },
186
+ {
187
+ "version": "0.4.12",
188
+ "date": "2026-11-07",
189
+ "breaking": [],
190
+ "envAdded": [],
191
+ "envRemoved": [],
192
+ "envRenamed": [],
193
+ "templateChanged": [],
194
+ "action": [],
195
+ "entries": [
196
+ {
197
+ "type": "refactor",
198
+ "scope": "admin",
199
+ "subject": "move studio bar and window layout into shared component",
200
+ "hash": "ea95fdf",
201
+ "breaking": false
202
+ }
203
+ ]
204
+ },
205
+ {
206
+ "version": "0.4.11",
207
+ "date": "2026-11-07",
208
+ "breaking": [],
209
+ "envAdded": [],
210
+ "envRemoved": [],
211
+ "envRenamed": [],
212
+ "templateChanged": [],
213
+ "action": [],
214
+ "entries": [
215
+ {
216
+ "type": "feat",
217
+ "scope": "studio",
218
+ "subject": "group hints into menus and animate the compose layout",
219
+ "hash": "662ef1c",
220
+ "breaking": false
221
+ }
222
+ ]
223
+ },
224
+ {
225
+ "version": "0.4.10",
226
+ "date": "2026-11-07",
227
+ "breaking": [],
228
+ "envAdded": [],
229
+ "envRemoved": [],
230
+ "envRenamed": [],
231
+ "templateChanged": [],
232
+ "action": [],
233
+ "entries": [
234
+ {
235
+ "type": "feat",
236
+ "scope": "ai",
237
+ "subject": "add studio compose mode and plan review",
238
+ "hash": "46ded16",
239
+ "breaking": false
240
+ }
241
+ ]
242
+ },
243
+ {
244
+ "version": "0.4.9",
245
+ "date": "2026-11-07",
246
+ "breaking": [],
247
+ "envAdded": [],
248
+ "envRemoved": [],
249
+ "envRenamed": [],
250
+ "templateChanged": [],
251
+ "action": [],
252
+ "entries": [
253
+ {
254
+ "type": "refactor",
255
+ "scope": "admin",
256
+ "subject": "rename ai sparkles icon to sparkles icon",
257
+ "hash": "3ccf3f6",
258
+ "breaking": false
259
+ },
260
+ {
261
+ "type": "feat",
262
+ "scope": "admin",
263
+ "subject": "add icon-only and size options to ai actions trigger",
264
+ "hash": "eaa0877",
265
+ "breaking": false
266
+ }
267
+ ]
268
+ },
269
+ {
270
+ "version": "0.4.8",
271
+ "date": "2026-11-07",
272
+ "breaking": [],
273
+ "envAdded": [],
274
+ "envRemoved": [],
275
+ "envRenamed": [],
276
+ "templateChanged": [],
277
+ "action": [],
278
+ "entries": [
279
+ {
280
+ "type": "refactor",
281
+ "scope": "ui",
282
+ "subject": "replace ChatSparkIcon with AiSparklesIcon",
283
+ "hash": "1dd3dee",
284
+ "breaking": false
285
+ }
286
+ ]
287
+ },
288
+ {
289
+ "version": "0.4.7",
290
+ "date": "2026-11-07",
291
+ "breaking": [],
292
+ "envAdded": [],
293
+ "envRemoved": [],
294
+ "envRenamed": [],
295
+ "templateChanged": [],
296
+ "action": [],
297
+ "entries": [
298
+ {
299
+ "type": "refactor",
300
+ "scope": "ui",
301
+ "subject": "rename ai apply wording to \"appliquer\" and \"laisser tel quel\"",
302
+ "hash": "471087a",
303
+ "breaking": false
304
+ }
305
+ ]
306
+ },
307
+ {
308
+ "version": "0.4.6",
309
+ "date": "2026-11-07",
310
+ "breaking": [],
311
+ "envAdded": [],
312
+ "envRemoved": [],
313
+ "envRenamed": [],
314
+ "templateChanged": [],
315
+ "action": [],
316
+ "entries": [
317
+ {
318
+ "type": "feat",
319
+ "scope": "ai",
320
+ "subject": "add studio mode for comparing and applying prompt versions",
321
+ "hash": "d2b427d",
322
+ "breaking": false
323
+ }
324
+ ]
325
+ },
326
+ {
327
+ "version": "0.4.5",
328
+ "date": "2026-11-07",
329
+ "breaking": [],
330
+ "envAdded": [],
331
+ "envRemoved": [],
332
+ "envRenamed": [],
333
+ "templateChanged": [],
334
+ "action": [],
335
+ "entries": [
336
+ {
337
+ "type": "feat",
338
+ "scope": "admin",
339
+ "subject": "enlarge and make resizable long text inputs in ai form",
340
+ "hash": "858b23c",
341
+ "breaking": false
342
+ }
343
+ ]
344
+ },
345
+ {
346
+ "version": "0.4.4",
347
+ "date": "2026-11-07",
348
+ "breaking": [],
349
+ "envAdded": [],
350
+ "envRemoved": [],
351
+ "envRenamed": [],
352
+ "templateChanged": [],
353
+ "action": [],
354
+ "entries": [
355
+ {
356
+ "type": "feat",
357
+ "scope": "ai",
358
+ "subject": "add configurable menu label with action descriptions",
359
+ "hash": "526a6f7",
360
+ "breaking": false
361
+ }
362
+ ]
363
+ },
364
+ {
365
+ "version": "0.4.3",
366
+ "date": "2026-11-07",
367
+ "breaking": [],
368
+ "envAdded": [],
369
+ "envRemoved": [],
370
+ "envRenamed": [],
371
+ "templateChanged": [],
372
+ "action": [],
373
+ "entries": [
374
+ {
375
+ "type": "refactor",
376
+ "scope": "admin",
377
+ "subject": "use ai trigger component for single action button",
378
+ "hash": "b79c212",
379
+ "breaking": false
380
+ }
381
+ ]
382
+ },
383
+ {
384
+ "version": "0.4.2",
385
+ "date": "2026-11-07",
386
+ "breaking": [],
387
+ "envAdded": [],
388
+ "envRemoved": [],
389
+ "envRenamed": [],
390
+ "templateChanged": [],
391
+ "action": [],
392
+ "entries": [
393
+ {
394
+ "type": "feat",
395
+ "scope": "ai",
396
+ "subject": "redesign ai modal with preview, steps and eyebrow",
397
+ "hash": "ec4ca14",
398
+ "breaking": false
399
+ }
400
+ ]
401
+ },
402
+ {
403
+ "version": "0.4.1",
404
+ "date": "2026-11-07",
405
+ "breaking": [],
406
+ "envAdded": [],
407
+ "envRemoved": [],
408
+ "envRenamed": [],
409
+ "templateChanged": [],
410
+ "action": [],
411
+ "entries": [
412
+ {
413
+ "type": "refactor",
414
+ "scope": "ai",
415
+ "subject": "use core ai design system in admin actions",
416
+ "hash": "44cf966",
417
+ "breaking": false
418
+ }
419
+ ]
420
+ },
421
+ {
422
+ "version": "0.4.0",
423
+ "date": "2026-11-07",
424
+ "breaking": [],
425
+ "envAdded": [
426
+ "FRAZA_MODULE_AI",
427
+ "FRAZA_MODULE_AI_API_KEY",
428
+ "FRAZA_MODULE_AI_BASE_URL",
429
+ "FRAZA_MODULE_AI_MODEL",
430
+ "FRAZA_MODULE_AI_PROVIDER",
431
+ "FRAZA_MODULE_AI_RETENTION_DAYS",
432
+ "FRAZA_MODULE_AI_TIMEOUT_MS"
433
+ ],
434
+ "envRemoved": [],
435
+ "envRenamed": [],
436
+ "templateChanged": [],
437
+ "action": [
438
+ "npx fraza-db migrate",
439
+ "npx fraza-doctor --fix"
440
+ ],
441
+ "entries": [
442
+ {
443
+ "type": "feat",
444
+ "scope": null,
445
+ "subject": "add ai module with providers, admin pages and prompts",
446
+ "hash": "6c49c51",
447
+ "breaking": false
448
+ },
449
+ {
450
+ "type": "other",
451
+ "scope": null,
452
+ "subject": "Initial commit",
453
+ "hash": "6240154",
454
+ "breaking": false
455
+ }
456
+ ]
457
+ }
458
+ ]
459
+ }
@@ -0,0 +1,166 @@
1
+ "use client";
2
+ import { Fragment, jsx, jsxs } from "react/jsx-runtime";
3
+ import React, { useCallback, useEffect, useId, useMemo, useState } from "react";
4
+ import { AiPreview, AiTrigger, AiWorking, Button, Modal } from "@fraza/core/shared/components";
5
+ import { apiFetch } from "@fraza/core/shared/http";
6
+ import { useToast } from "@fraza/core/toast";
7
+ import { SparklesIcon } from "@fraza/icons/SparklesIcon";
8
+ import AiChoices from "./AiChoices.client.js";
9
+ import AiInputForm from "./AiInputForm.client.js";
10
+ import AiStudio from "./AiStudio.client.js";
11
+ import { applyChoices } from "./choices.js";
12
+ import { formFields, initialValues, readValues } from "./input-form.js";
13
+ import { AI_API } from "./paths.js";
14
+ function useActions(target, scopeKey) {
15
+ const [actions, setActions] = useState([]);
16
+ useEffect(() => {
17
+ let cancelled = false;
18
+ const query = new URLSearchParams({ target, scope: scopeKey });
19
+ apiFetch(`${AI_API}/actions?${query}`).then((data) => {
20
+ if (!cancelled) setActions((data == null ? void 0 : data.configured) ? data.actions ?? [] : []);
21
+ }).catch(() => {
22
+ if (!cancelled) setActions([]);
23
+ });
24
+ return () => {
25
+ cancelled = true;
26
+ };
27
+ }, [target, scopeKey]);
28
+ return actions;
29
+ }
30
+ function stepsOf(output) {
31
+ return Object.entries((output == null ? void 0 : output.properties) ?? {}).map(([key, node]) => node.title ?? key);
32
+ }
33
+ const KEEP_NOTE = "Rien n'est enregistr\xE9 avant la relecture.";
34
+ function AiActions({ target, scope = {}, context, current, onApply, disabled = false, preview, menuLabel = "Actions d'IA", iconOnly = false, size }) {
35
+ const toast = useToast();
36
+ const formId = useId();
37
+ const scopeKey = useMemo(() => JSON.stringify(scope ?? {}), [scope]);
38
+ const actions = useActions(target, scopeKey);
39
+ const [active, setActive] = useState(null);
40
+ const [phase, setPhase] = useState(null);
41
+ const [values, setValues] = useState({});
42
+ const [errors, setErrors] = useState({});
43
+ const [answer, setAnswer] = useState(null);
44
+ const fields = useMemo(() => formFields(active == null ? void 0 : active.input), [active]);
45
+ const working = phase === "working";
46
+ const close = useCallback(() => {
47
+ setPhase(null);
48
+ setActive(null);
49
+ setAnswer(null);
50
+ setErrors({});
51
+ }, []);
52
+ const run = useCallback(async (action, input) => {
53
+ setActive(action);
54
+ setPhase("working");
55
+ try {
56
+ const data = await apiFetch(`${AI_API}/run`, {
57
+ method: "POST",
58
+ body: {
59
+ prompt: action.name,
60
+ input,
61
+ scope: JSON.parse(scopeKey),
62
+ context: typeof context === "function" ? context() : context ?? null
63
+ }
64
+ });
65
+ if (action.mode === "choose") {
66
+ setAnswer(data.data);
67
+ setPhase("choose");
68
+ return;
69
+ }
70
+ onApply == null ? void 0 : onApply(data.data);
71
+ close();
72
+ toast.success("Les champs ont \xE9t\xE9 remplis. Relire avant d'enregistrer.");
73
+ } catch (error) {
74
+ toast.error(error.message || "L'IA n'a pas pu r\xE9pondre.");
75
+ if (formFields(action.input).length > 0) setPhase("form");
76
+ else close();
77
+ }
78
+ }, [close, context, onApply, scopeKey, toast]);
79
+ const [studio, setStudio] = useState(null);
80
+ const start = useCallback((action) => {
81
+ if (action.mode === "studio") {
82
+ setStudio({ action, current: (typeof current === "function" ? current() : current) ?? {} });
83
+ return;
84
+ }
85
+ const actionFields = formFields(action.input);
86
+ if (actionFields.length === 0) {
87
+ run(action, null);
88
+ return;
89
+ }
90
+ setValues(initialValues(actionFields));
91
+ setErrors({});
92
+ setAnswer(null);
93
+ setActive(action);
94
+ setPhase("form");
95
+ }, [current, run]);
96
+ const submitForm = useCallback((event) => {
97
+ event == null ? void 0 : event.preventDefault();
98
+ const { input, errors: found } = readValues(fields, values);
99
+ setErrors(found);
100
+ if (Object.keys(found).length === 0) run(active, input);
101
+ }, [active, fields, run, values]);
102
+ const applyPicked = useCallback((picked) => {
103
+ onApply == null ? void 0 : onApply(applyChoices(active.output, answer, picked));
104
+ close();
105
+ toast.success("La proposition est appliqu\xE9e. Relire avant d'enregistrer.");
106
+ }, [active, answer, close, onApply, toast]);
107
+ if (actions.length === 0) return null;
108
+ const trigger = actions.length === 1 ? /* @__PURE__ */ jsx(AiTrigger, { disabled: disabled || working, iconOnly, size, onClick: () => start(actions[0]), children: actions[0].label }) : /* @__PURE__ */ jsx(
109
+ AiTrigger,
110
+ {
111
+ disabled: disabled || working,
112
+ iconOnly,
113
+ size,
114
+ items: actions.map((action) => ({ label: action.label, description: action.description ?? void 0, onClick: () => start(action) })),
115
+ children: menuLabel
116
+ }
117
+ );
118
+ const media = (preview == null ? void 0 : preview.src) ? /* @__PURE__ */ jsx(AiPreview, { src: preview.src, alt: preview.alt ?? "", label: preview.label }) : null;
119
+ const eyebrow = phase === "choose" ? "Propos\xE9 par l'IA" : phase === "working" ? "L'IA r\xE9dige" : active == null ? void 0 : active.label;
120
+ const title = phase === "working" ? active == null ? void 0 : active.label : (active == null ? void 0 : active.heading) ?? (active == null ? void 0 : active.label);
121
+ return /* @__PURE__ */ jsxs(Fragment, { children: [
122
+ trigger,
123
+ studio && /* @__PURE__ */ jsx(
124
+ AiStudio,
125
+ {
126
+ isOpen: true,
127
+ action: studio.action,
128
+ scope: JSON.parse(scopeKey),
129
+ context,
130
+ current: studio.current,
131
+ onApply: (values2) => onApply == null ? void 0 : onApply(values2),
132
+ onClose: () => setStudio(null)
133
+ }
134
+ ),
135
+ /* @__PURE__ */ jsxs(
136
+ Modal,
137
+ {
138
+ variant: "ai",
139
+ isOpen: phase !== null,
140
+ onClose: close,
141
+ closable: !working,
142
+ loading: working,
143
+ eyebrow,
144
+ title: title ?? "",
145
+ media,
146
+ description: phase === "form" ? (active == null ? void 0 : active.description) ?? void 0 : void 0,
147
+ size: phase === "choose" ? "lg" : "md",
148
+ footer: phase === "form" ? /* @__PURE__ */ jsxs("div", { className: "flex items-center justify-between gap-3", children: [
149
+ /* @__PURE__ */ jsx("span", { className: "text-[12px] text-fg-muted", children: KEEP_NOTE }),
150
+ /* @__PURE__ */ jsxs("div", { className: "flex gap-2", children: [
151
+ /* @__PURE__ */ jsx(Button, { variant: "secondary", onClick: close, children: "Annuler" }),
152
+ /* @__PURE__ */ jsx(Button, { variant: "ai", type: "submit", form: formId, icon: SparklesIcon, children: "G\xE9n\xE9rer" })
153
+ ] })
154
+ ] }) : null,
155
+ children: [
156
+ phase === "form" && /* @__PURE__ */ jsx("form", { id: formId, onSubmit: submitForm, noValidate: true, children: /* @__PURE__ */ jsx(AiInputForm, { fields, values, errors, onChange: setValues }) }),
157
+ working && /* @__PURE__ */ jsx(AiWorking, { steps: stepsOf(active == null ? void 0 : active.output) }),
158
+ phase === "choose" && answer && /* @__PURE__ */ jsx(AiChoices, { schema: active.output, data: answer, onApply: applyPicked, onCancel: close, note: KEEP_NOTE })
159
+ ]
160
+ }
161
+ )
162
+ ] });
163
+ }
164
+ export {
165
+ AiActions as default
166
+ };
@@ -0,0 +1,67 @@
1
+ "use client";
2
+ import { jsx, jsxs } from "react/jsx-runtime";
3
+ import React, { useState } from "react";
4
+ import { AI_CLASSES, Button } from "@fraza/core/shared/components";
5
+ import { CheckIcon } from "@fraza/icons/CheckIcon";
6
+ import { choiceGroups } from "./choices.js";
7
+ const SLUG = /^[a-z0-9]+(-[a-z0-9]+)*$/;
8
+ function Proposal({ text, picked, mono, delay, onPick }) {
9
+ return /* @__PURE__ */ jsxs(
10
+ "button",
11
+ {
12
+ type: "button",
13
+ "aria-pressed": picked,
14
+ onClick: onPick,
15
+ style: { animationDelay: `${delay}s` },
16
+ className: `${AI_CLASSES.arrive} flex min-h-10 w-full items-start gap-2.5 rounded-control border px-3 py-2.5 text-left text-fg transition-colors focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-ring focus-visible:ring-offset-2 focus-visible:ring-offset-surface ${picked ? AI_CLASSES.optionPicked : AI_CLASSES.option}`,
17
+ children: [
18
+ /* @__PURE__ */ jsx("span", { "aria-hidden": "true", className: `mt-0.5 flex h-4 w-4 shrink-0 items-center justify-center rounded-full border ${picked ? "border-accent bg-accent text-accent-fg" : "border-line-strong"}`, children: picked && /* @__PURE__ */ jsx(CheckIcon, { className: "h-2.5 w-2.5" }) }),
19
+ /* @__PURE__ */ jsx("span", { className: mono ? "break-all font-code text-[13px] leading-5" : `text-[15px] leading-[21px] ${AI_CLASSES.voice}`, children: text })
20
+ ]
21
+ }
22
+ );
23
+ }
24
+ function AiChoices({ schema, data, onApply, onCancel, note }) {
25
+ const groups = choiceGroups(schema, data);
26
+ const [picked, setPicked] = useState(() => Object.fromEntries(groups.map((group) => [group.key, 0])));
27
+ let order = 0;
28
+ return /* @__PURE__ */ jsxs("div", { className: "flex flex-col gap-5", children: [
29
+ groups.map((group) => {
30
+ const mono = group.options.every((option) => SLUG.test(option));
31
+ return /* @__PURE__ */ jsxs("fieldset", { className: "m-0 flex flex-col gap-1.5 border-0 p-0", children: [
32
+ /* @__PURE__ */ jsx("legend", { className: "mb-2 p-0 text-[12.5px] font-medium text-fg", children: group.title }),
33
+ group.options.map((option, index) => /* @__PURE__ */ jsx(
34
+ Proposal,
35
+ {
36
+ text: option,
37
+ mono,
38
+ picked: picked[group.key] === index,
39
+ delay: order++ * 0.12,
40
+ onPick: () => setPicked({ ...picked, [group.key]: index })
41
+ },
42
+ index
43
+ )),
44
+ /* @__PURE__ */ jsx(
45
+ "button",
46
+ {
47
+ type: "button",
48
+ "aria-pressed": picked[group.key] === null,
49
+ onClick: () => setPicked({ ...picked, [group.key]: null }),
50
+ className: `self-start px-0.5 py-1 text-[12.5px] transition-colors hover:text-fg ${picked[group.key] === null ? "text-fg" : "text-fg-muted"}`,
51
+ children: "Laisser tel quel"
52
+ }
53
+ )
54
+ ] }, group.key);
55
+ }),
56
+ /* @__PURE__ */ jsxs("div", { className: "flex items-center justify-between gap-3 pb-4", children: [
57
+ /* @__PURE__ */ jsx("span", { className: "text-[12px] text-fg-muted", children: note }),
58
+ /* @__PURE__ */ jsxs("div", { className: "flex gap-2", children: [
59
+ /* @__PURE__ */ jsx(Button, { variant: "secondary", onClick: onCancel, children: "Annuler" }),
60
+ /* @__PURE__ */ jsx(Button, { variant: "ai", onClick: () => onApply(picked), children: "Appliquer" })
61
+ ] })
62
+ ] })
63
+ ] });
64
+ }
65
+ export {
66
+ AiChoices as default
67
+ };