@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
@@ -0,0 +1,217 @@
1
+ "use client";
2
+ import { jsx, jsxs } from "react/jsx-runtime";
3
+ import React, { useEffect, useId, useRef, useState } from "react";
4
+ import { AI_CLASSES } from "@fraza/core/shared/components";
5
+ const BORDER = "linear-gradient(120deg, var(--color-accent), var(--color-accent-line) 35%, var(--color-line-strong) 70%)";
6
+ const LIGHT = "radial-gradient(70% 90% at 30% 0%, color-mix(in srgb, var(--color-accent) 16%, transparent), transparent 70%)";
7
+ const GRAIN_MASK = "radial-gradient(70% 90% at 30% 0%, #000, transparent 70%)";
8
+ function StudioBox({ rounded = "rounded-[18px]", className = "", children }) {
9
+ return /* @__PURE__ */ jsxs("div", { className: "group/box relative", children: [
10
+ /* @__PURE__ */ jsx("span", { "aria-hidden": "true", className: `pointer-events-none absolute inset-0 ${rounded} opacity-0 transition-opacity duration-300 ease-out group-focus-within/box:opacity-100 motion-reduce:transition-none`, style: { background: BORDER } }),
11
+ /* @__PURE__ */ jsxs("div", { className: `relative overflow-hidden ${rounded} border border-control-line bg-control bg-clip-padding transition-colors group-focus-within/box:border-transparent ${className}`, children: [
12
+ /* @__PURE__ */ jsx("span", { "aria-hidden": "true", className: "pointer-events-none absolute inset-0 opacity-0 transition-opacity duration-300 ease-out group-focus-within/box:opacity-100 motion-reduce:transition-none", style: { background: LIGHT } }),
13
+ /* @__PURE__ */ jsx(
14
+ "span",
15
+ {
16
+ "aria-hidden": "true",
17
+ className: "pointer-events-none absolute inset-0 opacity-0 transition-opacity duration-300 ease-out group-focus-within/box:opacity-[calc(var(--fraza-ai-grain-opacity,0.65)*0.77)] motion-reduce:transition-none forced-colors:hidden print:hidden",
18
+ style: { backgroundImage: "var(--fraza-ai-grain)", backgroundSize: "160px 160px", mixBlendMode: "overlay", WebkitMaskImage: GRAIN_MASK, maskImage: GRAIN_MASK }
19
+ }
20
+ ),
21
+ children
22
+ ] })
23
+ ] });
24
+ }
25
+ function SendButton({ label, disabled = false, size = "h-9 w-9" }) {
26
+ return /* @__PURE__ */ jsx(
27
+ "button",
28
+ {
29
+ type: "submit",
30
+ "aria-label": label,
31
+ title: label,
32
+ disabled,
33
+ className: `relative flex ${size} shrink-0 items-center justify-center rounded-full transition-colors ${AI_CLASSES.button}`,
34
+ children: /* @__PURE__ */ jsx("svg", { width: "16", height: "16", viewBox: "0 0 24 24", fill: "none", stroke: "currentColor", strokeWidth: "2.2", strokeLinecap: "round", strokeLinejoin: "round", "aria-hidden": "true", children: /* @__PURE__ */ jsx("path", { d: "M12 19V5M6 11l6-6 6 6" }) })
35
+ }
36
+ );
37
+ }
38
+ const STARTS = [
39
+ { key: "plan", label: "Plan d'abord", text: "On valide les titres et la structure, puis l'IA r\xE9dige." },
40
+ { key: "write", label: "\xC9crire tout de suite", text: "L'IA r\xE9dige directement, on retouche ensuite." }
41
+ ];
42
+ const PANEL = `${AI_CLASSES.arrive} absolute top-[calc(100%+8px)] z-10 rounded-card border border-line dark:border-line-strong bg-surface shadow-[0_8px_24px_rgba(0,0,0,0.12)] dark:shadow-[0_8px_24px_rgba(0,0,0,0.6)]`;
43
+ function SlidersIcon() {
44
+ return /* @__PURE__ */ jsxs("svg", { width: "14", height: "14", viewBox: "0 0 24 24", fill: "none", stroke: "currentColor", strokeWidth: "1.8", strokeLinecap: "round", "aria-hidden": "true", children: [
45
+ /* @__PURE__ */ jsx("path", { d: "M4 6h10M18 6h2M4 12h4M12 12h8M4 18h12" }),
46
+ /* @__PURE__ */ jsx("circle", { cx: "16", cy: "6", r: "2" }),
47
+ /* @__PURE__ */ jsx("circle", { cx: "10", cy: "12", r: "2" }),
48
+ /* @__PURE__ */ jsx("circle", { cx: "18", cy: "18", r: "2" })
49
+ ] });
50
+ }
51
+ function Segmented({ group, ticked, onPick }) {
52
+ const labelId = useId();
53
+ return /* @__PURE__ */ jsxs("div", { className: "flex items-center justify-between gap-4", children: [
54
+ /* @__PURE__ */ jsx("span", { id: labelId, className: "text-[12.5px] text-fg-muted", children: group.name }),
55
+ /* @__PURE__ */ jsx("div", { role: "radiogroup", "aria-labelledby": labelId, className: "flex rounded-control border border-line bg-surface-muted p-0.5", children: group.hints.map((hint) => {
56
+ const on = ticked.has(hint.label);
57
+ return /* @__PURE__ */ jsx(
58
+ "button",
59
+ {
60
+ type: "button",
61
+ role: "radio",
62
+ "aria-checked": on,
63
+ title: hint.text,
64
+ onClick: () => onPick(group, on ? null : hint.label),
65
+ className: `h-[26px] rounded-[6px] px-2.5 text-[12.5px] transition-colors ${on ? "bg-surface text-accent shadow-[inset_0_0_0_1px_var(--color-accent-line)]" : "text-fg-muted hover:text-fg"}`,
66
+ children: hint.label
67
+ },
68
+ hint.label
69
+ );
70
+ }) })
71
+ ] });
72
+ }
73
+ function HintSwitch({ hint, on, onToggle }) {
74
+ return /* @__PURE__ */ jsxs(
75
+ "button",
76
+ {
77
+ type: "button",
78
+ role: "switch",
79
+ "aria-checked": on,
80
+ title: hint.text,
81
+ onClick: onToggle,
82
+ className: "flex items-center justify-between gap-4 text-left text-[12.5px] text-fg-muted",
83
+ children: [
84
+ hint.label,
85
+ /* @__PURE__ */ jsx("span", { "aria-hidden": "true", className: `relative inline-block h-[18px] w-[30px] shrink-0 rounded-full transition-colors ${on ? "bg-accent" : "bg-line-strong"}`, children: /* @__PURE__ */ jsx("span", { className: `absolute top-[2px] h-[14px] w-[14px] rounded-full bg-surface transition-[left] ${on ? "left-[14px]" : "left-[2px]"}` }) })
86
+ ]
87
+ }
88
+ );
89
+ }
90
+ function StudioComposer({ draft, onDraft, maxLength, onSubmit, groups, alone, ticked, onPick, onToggle, planned, start, onStart, footer = null }) {
91
+ var _a;
92
+ const [open, setOpen] = useState(null);
93
+ const optionsId = useId();
94
+ const startId = useId();
95
+ const rootRef = useRef(null);
96
+ const optionsRef = useRef(null);
97
+ const startRef = useRef(null);
98
+ const hasOptions = groups.length > 0 || alone.length > 0;
99
+ const sendLabel = planned && start === "plan" ? "Proposer un plan" : "\xC9crire";
100
+ const startLabel = ((_a = STARTS.find((item) => item.key === start)) == null ? void 0 : _a.label) ?? STARTS[0].label;
101
+ useEffect(() => {
102
+ if (!open) return void 0;
103
+ const close = (event) => {
104
+ var _a2;
105
+ if (!((_a2 = rootRef.current) == null ? void 0 : _a2.contains(event.target))) setOpen(null);
106
+ };
107
+ document.addEventListener("mousedown", close);
108
+ return () => document.removeEventListener("mousedown", close);
109
+ }, [open]);
110
+ const toggle = (name) => setOpen((current) => current === name ? null : name);
111
+ return /* @__PURE__ */ jsxs("div", { className: "flex flex-col gap-5", children: [
112
+ /* @__PURE__ */ jsxs(
113
+ "div",
114
+ {
115
+ ref: rootRef,
116
+ className: "group relative",
117
+ onKeyDown: (event) => {
118
+ var _a2;
119
+ if (event.key === "Escape" && open) {
120
+ event.preventDefault();
121
+ event.stopPropagation();
122
+ (_a2 = (open === "start" ? startRef : optionsRef).current) == null ? void 0 : _a2.focus();
123
+ setOpen(null);
124
+ }
125
+ },
126
+ children: [
127
+ /* @__PURE__ */ jsxs(StudioBox, { className: "shadow-[0_8px_28px_rgba(0,0,0,0.12)]", children: [
128
+ /* @__PURE__ */ jsx(
129
+ "textarea",
130
+ {
131
+ rows: 3,
132
+ value: draft,
133
+ maxLength,
134
+ "aria-label": "Demande",
135
+ placeholder: "Un guide pour\u2026, une annonce de\u2026, un article sur\u2026",
136
+ onChange: (event) => onDraft(event.target.value),
137
+ onKeyDown: (event) => {
138
+ if (event.key === "Enter" && !event.shiftKey) onSubmit(event);
139
+ },
140
+ className: "relative block w-full resize-none border-0 bg-transparent px-5 pb-1.5 pt-[18px] text-[15.5px] leading-6 text-fg outline-none placeholder:text-fg-subtle"
141
+ }
142
+ ),
143
+ /* @__PURE__ */ jsxs("div", { className: "relative flex items-center gap-2 px-3 pb-2.5 pt-2", children: [
144
+ hasOptions && /* @__PURE__ */ jsxs(
145
+ "button",
146
+ {
147
+ ref: optionsRef,
148
+ type: "button",
149
+ "aria-expanded": open === "options",
150
+ "aria-controls": open === "options" ? optionsId : void 0,
151
+ onClick: () => toggle("options"),
152
+ className: `flex h-8 items-center gap-1.5 rounded-control border px-2.5 text-[13px] transition-colors ${open === "options" ? "border-line-strong bg-surface-muted text-fg" : "border-line text-fg-secondary hover:border-line-strong hover:text-fg"}`,
153
+ children: [
154
+ /* @__PURE__ */ jsx(SlidersIcon, {}),
155
+ "Options",
156
+ ticked.size > 0 && /* @__PURE__ */ jsx("span", { className: "inline-flex h-[18px] min-w-[18px] items-center justify-center rounded-full bg-accent px-1 text-[11px] font-medium text-accent-fg", children: ticked.size })
157
+ ]
158
+ }
159
+ ),
160
+ /* @__PURE__ */ jsx("span", { className: "flex-1" }),
161
+ planned && /* @__PURE__ */ jsxs(
162
+ "button",
163
+ {
164
+ ref: startRef,
165
+ type: "button",
166
+ "aria-haspopup": "true",
167
+ "aria-expanded": open === "start",
168
+ "aria-controls": open === "start" ? startId : void 0,
169
+ onClick: () => toggle("start"),
170
+ className: `flex h-8 items-center gap-1.5 rounded-control px-2.5 text-[13px] transition-colors ${open === "start" ? "bg-surface-muted text-fg" : "text-fg-secondary hover:bg-surface-muted hover:text-fg"}`,
171
+ children: [
172
+ startLabel,
173
+ /* @__PURE__ */ jsx("svg", { "aria-hidden": "true", width: "12", height: "12", viewBox: "0 0 24 24", fill: "none", stroke: "currentColor", strokeWidth: "2", className: `text-fg-muted transition-transform ${open === "start" ? "rotate-180" : ""}`, children: /* @__PURE__ */ jsx("path", { d: "M6 9l6 6 6-6" }) })
174
+ ]
175
+ }
176
+ ),
177
+ /* @__PURE__ */ jsx(SendButton, { label: sendLabel, disabled: !draft.trim() })
178
+ ] })
179
+ ] }),
180
+ open === "options" && /* @__PURE__ */ jsxs("div", { id: optionsId, role: "group", "aria-label": "Options", className: `${PANEL} left-0 flex w-[380px] max-w-full flex-col gap-3 p-3.5`, children: [
181
+ groups.map((group) => /* @__PURE__ */ jsx(Segmented, { group, ticked, onPick }, group.name)),
182
+ alone.map((hint) => /* @__PURE__ */ jsx(HintSwitch, { hint, on: ticked.has(hint.label), onToggle: () => onToggle(hint.label) }, hint.label))
183
+ ] }),
184
+ open === "start" && /* @__PURE__ */ jsx("div", { id: startId, role: "radiogroup", "aria-label": "Comment commencer", className: `${PANEL} right-0 flex w-[300px] max-w-full flex-col p-1.5`, children: STARTS.map((item) => /* @__PURE__ */ jsxs(
185
+ "button",
186
+ {
187
+ type: "button",
188
+ role: "radio",
189
+ "aria-checked": start === item.key,
190
+ onClick: () => {
191
+ var _a2;
192
+ onStart(item.key);
193
+ setOpen(null);
194
+ (_a2 = startRef.current) == null ? void 0 : _a2.focus();
195
+ },
196
+ className: `flex items-start gap-2.5 rounded-control px-2.5 py-2 text-left transition-colors ${start === item.key ? "bg-accent-soft" : "hover:bg-surface-muted"}`,
197
+ children: [
198
+ /* @__PURE__ */ jsx("svg", { "aria-hidden": "true", width: "14", height: "14", viewBox: "0 0 24 24", fill: "none", stroke: "currentColor", strokeWidth: "2.2", strokeLinecap: "round", strokeLinejoin: "round", className: `mt-[3px] shrink-0 text-accent ${start === item.key ? "" : "invisible"}`, children: /* @__PURE__ */ jsx("path", { d: "M5 12l5 5L20 7" }) }),
199
+ /* @__PURE__ */ jsxs("span", { className: "flex flex-col gap-0.5", children: [
200
+ /* @__PURE__ */ jsx("span", { className: `text-[13px] font-medium ${start === item.key ? "text-accent" : "text-fg"}`, children: item.label }),
201
+ /* @__PURE__ */ jsx("span", { className: "text-[12px] leading-[17px] text-fg-muted", children: item.text })
202
+ ] })
203
+ ]
204
+ },
205
+ item.key
206
+ )) })
207
+ ]
208
+ }
209
+ ),
210
+ footer && /* @__PURE__ */ jsx("div", { className: open ? "invisible" : "", children: footer })
211
+ ] });
212
+ }
213
+ export {
214
+ SendButton,
215
+ StudioBox,
216
+ StudioComposer as default
217
+ };
@@ -0,0 +1,124 @@
1
+ "use client";
2
+ import { jsx, jsxs } from "react/jsx-runtime";
3
+ import React, { lazy, Suspense, useEffect, useRef, useState } from "react";
4
+ import { blocksToHtml, htmlToBlocks } from "@fraza/core/shared/components/block-editor";
5
+ import { hasValue, layoutFields } from "./studio-compose.js";
6
+ const BlockEditor = lazy(() => import("@fraza/core/shared/components/BlockEditor"));
7
+ function HtmlEditor({ field, value, onChange }) {
8
+ const [blocks, setBlocks] = useState(() => htmlToBlocks(value ?? ""));
9
+ const sent = useRef(value ?? "");
10
+ useEffect(() => {
11
+ if ((value ?? "") === sent.current) return;
12
+ sent.current = value ?? "";
13
+ setBlocks(htmlToBlocks(value ?? ""));
14
+ }, [value]);
15
+ return /* @__PURE__ */ jsx(Suspense, { fallback: /* @__PURE__ */ jsx("div", { className: "h-[320px] animate-pulse rounded-control bg-surface-muted motion-reduce:animate-none" }), children: /* @__PURE__ */ jsx(
16
+ BlockEditor,
17
+ {
18
+ label: field.label,
19
+ value: blocks,
20
+ minHeight: 320,
21
+ onChange: (next) => {
22
+ setBlocks(next);
23
+ const html = blocksToHtml(next);
24
+ sent.current = html;
25
+ onChange(html);
26
+ }
27
+ }
28
+ ) });
29
+ }
30
+ function SheetField({ field, value, onChange }) {
31
+ const id = `studio-${field.key}`;
32
+ if (field.kind === "choice" || field.kind === "list" && field.options) {
33
+ const picked = new Set(field.kind === "list" ? Array.isArray(value) ? value : [] : [value].filter(Boolean));
34
+ const toggle = (option) => {
35
+ if (field.kind === "choice") return onChange(picked.has(option) ? null : option);
36
+ const next = new Set(picked);
37
+ if (next.has(option)) next.delete(option);
38
+ else next.add(option);
39
+ return onChange(field.options.filter((item) => next.has(item)));
40
+ };
41
+ return /* @__PURE__ */ jsxs("fieldset", { className: "m-0 flex flex-col gap-2 border-0 p-0", children: [
42
+ /* @__PURE__ */ jsx("legend", { className: "mb-2 text-[12px] text-fg-muted", children: field.label }),
43
+ /* @__PURE__ */ jsx("div", { className: "flex flex-wrap gap-1.5", children: (field.options ?? []).map((option) => /* @__PURE__ */ jsx(
44
+ "button",
45
+ {
46
+ type: "button",
47
+ "aria-pressed": picked.has(option),
48
+ onClick: () => toggle(option),
49
+ className: `h-7 rounded-full border px-2.5 text-[12.5px] transition-colors ${picked.has(option) ? "border-accent-line bg-accent-soft text-accent" : "border-line bg-surface text-fg-secondary hover:border-line-strong"}`,
50
+ children: option
51
+ },
52
+ option
53
+ )) })
54
+ ] });
55
+ }
56
+ const text = Array.isArray(value) ? value.join(", ") : value ?? "";
57
+ return /* @__PURE__ */ jsxs("div", { className: "flex flex-col gap-1.5", children: [
58
+ /* @__PURE__ */ jsx("label", { htmlFor: id, className: "text-[12px] text-fg-muted", children: field.label }),
59
+ /* @__PURE__ */ jsx(
60
+ "textarea",
61
+ {
62
+ id,
63
+ rows: field.maxLength && field.maxLength <= 80 ? 1 : 3,
64
+ value: text,
65
+ maxLength: field.maxLength ?? void 0,
66
+ onChange: (event) => onChange(field.kind === "list" ? event.target.value.split(",").map((item) => item.trim()).filter(Boolean) : event.target.value),
67
+ className: "resize-none rounded-control border border-control-line bg-control px-3 py-2 text-[13.5px] leading-5 text-fg outline-none focus:border-accent-line"
68
+ }
69
+ ),
70
+ field.maxLength && /* @__PURE__ */ jsxs("span", { className: "self-end text-[11px] tabular-nums text-fg-subtle", children: [
71
+ [...text].length,
72
+ " / ",
73
+ field.maxLength
74
+ ] })
75
+ ] });
76
+ }
77
+ function StudioFields({ fields, values, onChange, busy = false }) {
78
+ const { title, page, sheet } = layoutFields(fields);
79
+ const [open, setOpen] = useState(false);
80
+ const filled = sheet.filter((field) => hasValue(values[field.key])).length;
81
+ return /* @__PURE__ */ jsxs("div", { className: `mx-auto flex max-w-[760px] flex-col gap-4 px-6 py-6 ${busy ? "pointer-events-none" : ""}`, children: [
82
+ /* @__PURE__ */ jsxs("div", { className: "flex flex-col gap-5 rounded-card border border-line bg-surface p-6", children: [
83
+ title && /* @__PURE__ */ jsxs("div", { className: "flex flex-col gap-1", children: [
84
+ /* @__PURE__ */ jsx("label", { htmlFor: `studio-${title.key}`, className: "text-[12px] text-fg-muted", children: title.label }),
85
+ /* @__PURE__ */ jsx(
86
+ "input",
87
+ {
88
+ id: `studio-${title.key}`,
89
+ value: values[title.key] ?? "",
90
+ maxLength: title.maxLength ?? void 0,
91
+ onChange: (event) => onChange(title.key, event.target.value),
92
+ className: "border-0 bg-transparent p-0 text-[24px] font-semibold leading-[32px] tracking-[-0.01em] text-fg outline-none"
93
+ }
94
+ )
95
+ ] }),
96
+ page.map((field) => /* @__PURE__ */ jsx(HtmlEditor, { field, value: values[field.key], onChange: (html) => onChange(field.key, html) }, field.key))
97
+ ] }),
98
+ sheet.length > 0 && /* @__PURE__ */ jsxs("div", { className: "rounded-card border border-line bg-surface", children: [
99
+ /* @__PURE__ */ jsxs(
100
+ "button",
101
+ {
102
+ type: "button",
103
+ "aria-expanded": open,
104
+ onClick: () => setOpen(!open),
105
+ className: "flex w-full items-center gap-3 px-5 py-3.5 text-left",
106
+ children: [
107
+ /* @__PURE__ */ jsx("span", { className: "text-[13.5px] font-medium text-fg", children: "Fiche" }),
108
+ /* @__PURE__ */ jsx("span", { className: "min-w-0 flex-1 truncate text-[12.5px] text-fg-muted", children: sheet.map((field) => field.label).join(" \xB7 ") }),
109
+ /* @__PURE__ */ jsxs("span", { className: "text-[12px] tabular-nums text-fg-muted", children: [
110
+ filled,
111
+ " sur ",
112
+ sheet.length
113
+ ] }),
114
+ /* @__PURE__ */ jsx("svg", { width: "16", height: "16", viewBox: "0 0 24 24", fill: "none", stroke: "currentColor", strokeWidth: "1.8", "aria-hidden": "true", className: `text-fg-muted transition-transform ${open ? "rotate-180" : ""}`, children: /* @__PURE__ */ jsx("path", { d: "M6 9l6 6 6-6" }) })
115
+ ]
116
+ }
117
+ ),
118
+ open && /* @__PURE__ */ jsx("div", { className: "flex flex-col gap-4 border-t border-line px-5 py-4", children: sheet.map((field) => /* @__PURE__ */ jsx(SheetField, { field, value: values[field.key], onChange: (value) => onChange(field.key, value) }, field.key)) })
119
+ ] })
120
+ ] });
121
+ }
122
+ export {
123
+ StudioFields as default
124
+ };
@@ -0,0 +1,94 @@
1
+ "use client";
2
+ import { jsx, jsxs } from "react/jsx-runtime";
3
+ import React from "react";
4
+ import { AI_CLASSES } from "@fraza/core/shared/components";
5
+ import { addSection, moveSection, removeSection, renameSection } from "./studio-compose.js";
6
+ import { PLAN_SECTIONS, PLAN_SUMMARY_MAX, PLAN_TITLE_MAX } from "../studio.js";
7
+ function IconButton({ label, onClick, disabled, children }) {
8
+ return /* @__PURE__ */ jsx(
9
+ "button",
10
+ {
11
+ type: "button",
12
+ "aria-label": label,
13
+ title: label,
14
+ disabled,
15
+ onClick,
16
+ className: "flex h-7 w-7 items-center justify-center rounded-[6px] text-fg-muted transition-colors hover:bg-surface-muted hover:text-fg disabled:opacity-30 disabled:hover:bg-transparent",
17
+ children
18
+ }
19
+ );
20
+ }
21
+ const STROKE = { fill: "none", stroke: "currentColor", strokeWidth: 1.8, strokeLinecap: "round", strokeLinejoin: "round", "aria-hidden": true };
22
+ function StudioPlan({ plan, onChange, disabled = false }) {
23
+ return /* @__PURE__ */ jsxs("div", { className: "mx-auto flex max-w-[720px] flex-col gap-4 px-6 py-6", children: [
24
+ plan.titles.length > 0 && /* @__PURE__ */ jsxs("fieldset", { disabled, className: "m-0 flex flex-col gap-2 rounded-card border border-line bg-surface p-5", children: [
25
+ /* @__PURE__ */ jsx("legend", { className: "sr-only", children: "Titre" }),
26
+ /* @__PURE__ */ jsx("span", { "aria-hidden": "true", className: "text-[11.5px] font-medium text-fg-subtle", children: "Titre \xB7 choisir" }),
27
+ plan.titles.map((title, index) => {
28
+ const picked = plan.title === title;
29
+ return /* @__PURE__ */ jsx(
30
+ "button",
31
+ {
32
+ type: "button",
33
+ "aria-pressed": picked,
34
+ onClick: () => onChange({ ...plan, title }),
35
+ className: `rounded-control border px-3.5 py-2.5 text-left text-[16px] leading-[22px] text-fg transition-colors ${AI_CLASSES.voice} ${picked ? AI_CLASSES.optionPicked : AI_CLASSES.option}`,
36
+ children: title
37
+ },
38
+ index
39
+ );
40
+ })
41
+ ] }),
42
+ /* @__PURE__ */ jsxs("div", { className: "flex flex-col rounded-card border border-line bg-surface", children: [
43
+ /* @__PURE__ */ jsx("span", { className: "px-5 pb-2 pt-4 text-[11.5px] font-medium text-fg-subtle", children: "Plan" }),
44
+ /* @__PURE__ */ jsx("ol", { className: "m-0 list-none p-0", children: plan.sections.map((section, index) => /* @__PURE__ */ jsxs("li", { className: "flex items-start gap-3 border-t border-line px-5 py-3", children: [
45
+ /* @__PURE__ */ jsx("span", { "aria-hidden": "true", className: "mt-1.5 w-5 shrink-0 text-right text-[12px] tabular-nums text-fg-subtle", children: index + 1 }),
46
+ /* @__PURE__ */ jsxs("div", { className: "flex min-w-0 flex-1 flex-col gap-1", children: [
47
+ /* @__PURE__ */ jsx(
48
+ "input",
49
+ {
50
+ value: section.title,
51
+ maxLength: PLAN_TITLE_MAX,
52
+ disabled,
53
+ "aria-label": `Section ${index + 1} : intertitre`,
54
+ placeholder: "Intertitre",
55
+ onChange: (event) => onChange(renameSection(plan, index, { title: event.target.value })),
56
+ className: "border-0 bg-transparent p-0 text-[14px] font-medium leading-5 text-fg outline-none placeholder:text-fg-subtle"
57
+ }
58
+ ),
59
+ /* @__PURE__ */ jsx(
60
+ "textarea",
61
+ {
62
+ rows: 2,
63
+ value: section.summary,
64
+ maxLength: PLAN_SUMMARY_MAX,
65
+ disabled,
66
+ "aria-label": `Section ${index + 1} : ce qu'elle couvre`,
67
+ placeholder: "Ce que la section couvre",
68
+ onChange: (event) => onChange(renameSection(plan, index, { summary: event.target.value })),
69
+ className: "resize-none border-0 bg-transparent p-0 text-[12.5px] leading-[18px] text-fg-muted outline-none placeholder:text-fg-subtle"
70
+ }
71
+ )
72
+ ] }),
73
+ /* @__PURE__ */ jsxs("div", { className: "flex shrink-0 items-center", children: [
74
+ /* @__PURE__ */ jsx(IconButton, { label: "Monter la section", disabled: disabled || index === 0, onClick: () => onChange(moveSection(plan, index, -1)), children: /* @__PURE__ */ jsx("svg", { width: "14", height: "14", viewBox: "0 0 24 24", ...STROKE, children: /* @__PURE__ */ jsx("path", { d: "M6 15l6-6 6 6" }) }) }),
75
+ /* @__PURE__ */ jsx(IconButton, { label: "Descendre la section", disabled: disabled || index === plan.sections.length - 1, onClick: () => onChange(moveSection(plan, index, 1)), children: /* @__PURE__ */ jsx("svg", { width: "14", height: "14", viewBox: "0 0 24 24", ...STROKE, children: /* @__PURE__ */ jsx("path", { d: "M6 9l6 6 6-6" }) }) }),
76
+ /* @__PURE__ */ jsx(IconButton, { label: "Retirer la section", disabled: disabled || plan.sections.length === 1, onClick: () => onChange(removeSection(plan, index)), children: /* @__PURE__ */ jsx("svg", { width: "14", height: "14", viewBox: "0 0 24 24", ...STROKE, children: /* @__PURE__ */ jsx("path", { d: "M6 6l12 12M18 6L6 18" }) }) })
77
+ ] })
78
+ ] }, index)) }),
79
+ /* @__PURE__ */ jsx("div", { className: "border-t border-line px-5 py-2.5", children: /* @__PURE__ */ jsx(
80
+ "button",
81
+ {
82
+ type: "button",
83
+ disabled: disabled || plan.sections.length >= PLAN_SECTIONS,
84
+ onClick: () => onChange(addSection(plan)),
85
+ className: "text-[13px] text-fg-secondary hover:text-fg disabled:opacity-40",
86
+ children: "+ Ajouter une section"
87
+ }
88
+ ) })
89
+ ] })
90
+ ] });
91
+ }
92
+ export {
93
+ StudioPlan as default
94
+ };
@@ -0,0 +1,157 @@
1
+ "use client";
2
+ import { jsx, jsxs } from "react/jsx-runtime";
3
+ import React, { useEffect, useMemo, useRef, useState } from "react";
4
+ import { AI_CLASSES } from "@fraza/core/shared/components";
5
+ const WORKING_CONTENT = "opacity-75 blur-[1.5px]";
6
+ const CONTENT_TRANSITION = "transition-[opacity,filter] duration-[900ms] ease-[cubic-bezier(0.22,1,0.36,1)] motion-reduce:transition-none";
7
+ const COUNT = 5;
8
+ const LAP = 18;
9
+ const GRAIN_MASK = "linear-gradient(rgb(0 0 0 / .5), rgb(0 0 0 / .5)), var(--fraza-ai-grain)";
10
+ const GLOW = (tone) => `radial-gradient(closest-side, color-mix(in srgb, ${tone} 26%, transparent), color-mix(in srgb, ${tone} 6%, transparent) 55%, transparent)`;
11
+ const ACCENT = "var(--color-accent)";
12
+ const LIGHT = "color-mix(in srgb, var(--color-accent) 45%, #fff)";
13
+ const KEYFRAMES = `
14
+ @keyframes fraza-ai-wander { from { offset-distance: 0%; } to { offset-distance: 100%; } }
15
+ @keyframes fraza-ai-glow-breathe { from { opacity: var(--o1); } to { opacity: var(--o2); } }
16
+ @keyframes fraza-ai-glow-grow { from { transform: scale(var(--s1)); } to { transform: scale(var(--s2)); } }
17
+ @media (prefers-reduced-motion: reduce) {
18
+ .fraza-ai-wander, .fraza-ai-wander > span { animation: none !important; }
19
+ }`;
20
+ function points(w, h) {
21
+ const cells = [];
22
+ for (let x = 0; x < 3; x += 1) for (let y = 0; y < 3; y += 1) cells.push([x, y]);
23
+ for (let i = cells.length - 1; i > 0; i -= 1) {
24
+ const j = Math.floor(Math.random() * (i + 1));
25
+ [cells[i], cells[j]] = [cells[j], cells[i]];
26
+ }
27
+ const span = (cell, size) => Math.round((-0.12 + (cell + 0.1 + Math.random() * 0.8) * (1.24 / 3)) * size);
28
+ return cells.slice(0, 7).map(([x, y]) => [span(x, w), span(y, h)]);
29
+ }
30
+ function curve(pts) {
31
+ const n = pts.length;
32
+ const at = (i) => pts[(i + n) % n];
33
+ let d = `M ${pts[0][0]} ${pts[0][1]}`;
34
+ for (let i = 0; i < n; i += 1) {
35
+ const [p0, p1, p2, p3] = [at(i - 1), at(i), at(i + 1), at(i + 2)];
36
+ const c1 = [p1[0] + (p2[0] - p0[0]) / 6, p1[1] + (p2[1] - p0[1]) / 6];
37
+ const c2 = [p2[0] - (p3[0] - p1[0]) / 6, p2[1] - (p3[1] - p1[1]) / 6];
38
+ d += ` C ${Math.round(c1[0])} ${Math.round(c1[1])}, ${Math.round(c2[0])} ${Math.round(c2[1])}, ${p2[0]} ${p2[1]}`;
39
+ }
40
+ return `path('${d} Z')`;
41
+ }
42
+ function makeGlows(w, h) {
43
+ return Array.from({ length: COUNT }, (_, i) => {
44
+ const lap = LAP * (0.8 + Math.random() * 0.6);
45
+ return {
46
+ size: Math.round(460 + Math.random() * 220),
47
+ path: curve(points(w, h)),
48
+ lap,
49
+ delay: -Math.random() * lap,
50
+ pulse: 3 + Math.random() * 3,
51
+ grow: 4 + Math.random() * 4,
52
+ o1: 0.35 + Math.random() * 0.2,
53
+ o2: 0.75 + Math.random() * 0.25,
54
+ s1: 0.6 + Math.random() * 0.2,
55
+ s2: 1.15 + Math.random() * 0.3,
56
+ tone: i % 3 === 2 ? LIGHT : ACCENT
57
+ };
58
+ });
59
+ }
60
+ function WorkingGlows({ active }) {
61
+ const ref = useRef(null);
62
+ const [box, setBox] = useState(null);
63
+ useEffect(() => {
64
+ const node = ref.current;
65
+ if (!node) return void 0;
66
+ const read = () => setBox((old) => {
67
+ const w = Math.round(node.clientWidth);
68
+ const h = Math.round(node.clientHeight);
69
+ return old && Math.abs(old.w - w) < 40 && Math.abs(old.h - h) < 40 ? old : { w, h };
70
+ });
71
+ read();
72
+ if (typeof ResizeObserver === "undefined") return void 0;
73
+ const observer = new ResizeObserver(read);
74
+ observer.observe(node);
75
+ return () => observer.disconnect();
76
+ }, []);
77
+ const glows = useMemo(() => box ? makeGlows(box.w, box.h) : [], [box]);
78
+ return /* @__PURE__ */ jsxs(
79
+ "div",
80
+ {
81
+ ref,
82
+ "aria-hidden": "true",
83
+ className: `pointer-events-none absolute inset-0 overflow-hidden transition-opacity duration-[900ms] ease-out motion-reduce:transition-none forced-colors:hidden print:hidden ${active ? "opacity-100" : "opacity-0"}`,
84
+ style: { WebkitMaskImage: GRAIN_MASK, maskImage: GRAIN_MASK, WebkitMaskSize: "100% 100%, 160px 160px", maskSize: "100% 100%, 160px 160px", WebkitMaskComposite: "source-over", maskComposite: "add" },
85
+ children: [
86
+ /* @__PURE__ */ jsx("style", { children: KEYFRAMES }),
87
+ active && glows.map((glow, index) => /* @__PURE__ */ jsx(
88
+ "div",
89
+ {
90
+ className: "fraza-ai-wander absolute left-0 top-0",
91
+ style: {
92
+ width: glow.size,
93
+ height: glow.size,
94
+ offsetPath: glow.path,
95
+ offsetRotate: "0deg",
96
+ offsetAnchor: "center",
97
+ animation: `fraza-ai-wander ${glow.lap}s linear ${glow.delay}s infinite`
98
+ },
99
+ children: /* @__PURE__ */ jsx(
100
+ "span",
101
+ {
102
+ className: "absolute inset-0 rounded-full",
103
+ style: {
104
+ background: GLOW(glow.tone),
105
+ filter: "blur(24px)",
106
+ "--o1": glow.o1,
107
+ "--o2": glow.o2,
108
+ "--s1": glow.s1,
109
+ "--s2": glow.s2,
110
+ animation: `fraza-ai-glow-breathe ${glow.pulse}s ease-in-out ${glow.delay}s infinite alternate, fraza-ai-glow-grow ${glow.grow}s ease-in-out ${glow.delay}s infinite alternate`
111
+ }
112
+ }
113
+ )
114
+ },
115
+ index
116
+ ))
117
+ ]
118
+ }
119
+ );
120
+ }
121
+ function WorkingWord({ words, className = "" }) {
122
+ const [index, setIndex] = useState(() => Math.floor(Math.random() * words.length));
123
+ const [shown, setShown] = useState(true);
124
+ useEffect(() => {
125
+ if (words.length < 2) return void 0;
126
+ let swap;
127
+ const timer = setInterval(() => {
128
+ setShown(false);
129
+ swap = setTimeout(() => {
130
+ setIndex((current) => {
131
+ let next = current;
132
+ while (next === current) next = Math.floor(Math.random() * words.length);
133
+ return next;
134
+ });
135
+ setShown(true);
136
+ }, 350);
137
+ }, 2e3);
138
+ return () => {
139
+ clearInterval(timer);
140
+ clearTimeout(swap);
141
+ };
142
+ }, [words]);
143
+ return /* @__PURE__ */ jsx(
144
+ "span",
145
+ {
146
+ role: "status",
147
+ className: `inline-block transition-[opacity,filter,transform] duration-[350ms] ease-out motion-reduce:transition-none ${AI_CLASSES.voice} ${shown ? "translate-y-0 opacity-100 blur-0" : "translate-y-1 opacity-0 blur-[3px]"} ${className}`,
148
+ children: `${words[index % words.length]}\u2026`
149
+ }
150
+ );
151
+ }
152
+ export {
153
+ CONTENT_TRANSITION,
154
+ WORKING_CONTENT,
155
+ WorkingGlows,
156
+ WorkingWord
157
+ };
@@ -0,0 +1,22 @@
1
+ function isTextList(node) {
2
+ var _a, _b;
3
+ const itemTypes = Array.isArray((_a = node == null ? void 0 : node.items) == null ? void 0 : _a.type) ? node.items.type : [(_b = node == null ? void 0 : node.items) == null ? void 0 : _b.type];
4
+ const types = Array.isArray(node == null ? void 0 : node.type) ? node.type : [node == null ? void 0 : node.type];
5
+ return types.includes("array") && itemTypes.includes("string");
6
+ }
7
+ function choiceGroups(schema, data) {
8
+ if (!(schema == null ? void 0 : schema.properties) || !data) return [];
9
+ return Object.entries(schema.properties).filter(([key, node]) => isTextList(node) && Array.isArray(data[key])).map(([key, node]) => ({ key, title: node.title ?? node.description ?? key, options: data[key] }));
10
+ }
11
+ function applyChoices(schema, data, picked) {
12
+ const result = { ...data };
13
+ for (const group of choiceGroups(schema, data)) {
14
+ const index = picked[group.key];
15
+ result[group.key] = Number.isInteger(index) && index >= 0 && index < group.options.length ? group.options[index] : null;
16
+ }
17
+ return result;
18
+ }
19
+ export {
20
+ applyChoices,
21
+ choiceGroups
22
+ };
@@ -0,0 +1,19 @@
1
+ const NUMBER = new Intl.NumberFormat("fr-CA");
2
+ function formatTokens(value) {
3
+ return NUMBER.format(Number.isFinite(value) ? value : 0);
4
+ }
5
+ function formatDuration(ms) {
6
+ if (!Number.isFinite(ms)) return "\u2014";
7
+ if (ms < 1e3) return `${ms} ms`;
8
+ return `${(ms / 1e3).toLocaleString("fr-CA", { maximumFractionDigits: 1 })} s`;
9
+ }
10
+ const TRIGGER_LABELS = Object.freeze({
11
+ button: "Bouton",
12
+ code: "Code",
13
+ test: "Test"
14
+ });
15
+ export {
16
+ TRIGGER_LABELS,
17
+ formatDuration,
18
+ formatTokens
19
+ };