@fraza/module-ai 0.0.0-stage → 0.4.23

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 (54) hide show
  1. package/CHANGELOG.md +158 -0
  2. package/LICENSE +21 -0
  3. package/README.md +72 -2
  4. package/THIRD-PARTY-NOTICES.md +11 -0
  5. package/changelog.json +478 -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 +43 -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/seed.server.js +205 -0
  48. package/dist/server.js +19 -0
  49. package/dist/source.js +4 -0
  50. package/dist/studio.js +205 -0
  51. package/dist/test-prompt.server.js +24 -0
  52. package/dist/version.js +4 -0
  53. package/dist/widgets.server.js +29 -0
  54. package/package.json +67 -4
@@ -0,0 +1,457 @@
1
+ "use client";
2
+ import { Fragment, jsx, jsxs } from "react/jsx-runtime";
3
+ import React, { useCallback, useEffect, useLayoutEffect, useMemo, useRef, useState } from "react";
4
+ import { AI_CLASSES, Button, ConfirmDialog, 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 { hasHtmlField, REQUEST_MAX } from "../studio.js";
9
+ import { AI_API } from "./paths.js";
10
+ import { BAR_CLASS, LABEL_CLASS, StudioClose, StudioLabel, WINDOW_CLASS } from "./StudioBar.client.js";
11
+ import StudioComposer, { SendButton, StudioBox } from "./StudioComposer.client.js";
12
+ import StudioFields from "./StudioFields.client.js";
13
+ import StudioPlan from "./StudioPlan.client.js";
14
+ import { CONTENT_TRANSITION, WORKING_CONTENT, WorkingGlows, WorkingWord } from "./StudioWorking.client.js";
15
+ import { firstRequest, groupHints, hasValue, mergeVersion, planOf, planToSend, WORK_WORDS } from "./studio-compose.js";
16
+ import { fieldsOf } from "./studio-diff.js";
17
+ const START_KEY = "fraza-ai-studio-start:";
18
+ const WRITE_FROM_PLAN = "R\xE9dige le contenu complet en suivant ce plan.";
19
+ const WRITE_NOW = "R\xE9dige le contenu complet maintenant, sans plan.";
20
+ const REVIEW_PLAN = "Propose une nouvelle structure pour ce contenu.";
21
+ const EASE = "cubic-bezier(0.22, 1, 0.36, 1)";
22
+ const MORPH = 700;
23
+ function rectIn(el, base) {
24
+ if (!el || !base) return null;
25
+ const r = el.getBoundingClientRect();
26
+ return { left: r.left - base.left, top: r.top - base.top, width: r.width, height: r.height };
27
+ }
28
+ function ghostOut(win, snapshot, keyframes, options) {
29
+ if (!win || !snapshot || !snapshot.node.animate) return;
30
+ const node = snapshot.node;
31
+ Object.assign(node.style, {
32
+ position: "absolute",
33
+ left: `${snapshot.rect.left}px`,
34
+ top: `${snapshot.rect.top}px`,
35
+ width: `${snapshot.rect.width}px`,
36
+ margin: "0",
37
+ pointerEvents: "none",
38
+ zIndex: "20"
39
+ });
40
+ node.setAttribute("aria-hidden", "true");
41
+ node.removeAttribute("id");
42
+ win.appendChild(node);
43
+ const animation = node.animate(keyframes, { easing: EASE, fill: "forwards", ...options });
44
+ animation.onfinish = () => node.remove();
45
+ animation.oncancel = () => node.remove();
46
+ }
47
+ function flipFrom(el, from, base, options = {}, extra = {}) {
48
+ if (!(el == null ? void 0 : el.animate) || !from) return;
49
+ const to = rectIn(el, base);
50
+ el.animate([
51
+ { transform: `translate(${from.left - to.left}px, ${from.top - to.top}px)`, ...extra.from ?? {} },
52
+ { transform: "none", ...extra.to ?? {} }
53
+ ], { duration: MORPH, easing: EASE, fill: "backwards", ...options });
54
+ }
55
+ function readStart(name) {
56
+ try {
57
+ return window.localStorage.getItem(START_KEY + name) === "write" ? "write" : "plan";
58
+ } catch {
59
+ return "plan";
60
+ }
61
+ }
62
+ function saveStart(name, value) {
63
+ try {
64
+ window.localStorage.setItem(START_KEY + name, value);
65
+ } catch {
66
+ }
67
+ }
68
+ function reducedMotion() {
69
+ var _a;
70
+ return typeof window !== "undefined" && ((_a = window.matchMedia) == null ? void 0 : _a.call(window, "(prefers-reduced-motion: reduce)").matches);
71
+ }
72
+ function filledValues(values) {
73
+ return Object.fromEntries(Object.entries(values).filter(([, value]) => hasValue(value)));
74
+ }
75
+ function listOf(labels) {
76
+ if (labels.length <= 1) return labels.join("");
77
+ return `${labels.slice(0, -1).join(", ")} et ${labels.at(-1)}`;
78
+ }
79
+ function StepPill({ step, active, onClick }) {
80
+ return /* @__PURE__ */ jsx(
81
+ "button",
82
+ {
83
+ type: "button",
84
+ "aria-current": active ? "step" : void 0,
85
+ onClick,
86
+ className: `h-7 rounded-full border px-2.5 text-[12px] transition-colors ${active ? "border-accent bg-accent-soft font-medium text-accent" : "border-line text-fg-secondary hover:border-line-strong hover:text-fg"}`,
87
+ children: step.label
88
+ }
89
+ );
90
+ }
91
+ function StudioCompose({ action, scope, context, current = {}, onApply, onClose }) {
92
+ var _a;
93
+ const toast = useToast();
94
+ const fields = useMemo(() => fieldsOf(action == null ? void 0 : action.output), [action]);
95
+ const planned = useMemo(() => hasHtmlField(action == null ? void 0 : action.output), [action]);
96
+ const hints = useMemo(() => (action == null ? void 0 : action.hints) ?? [], [action]);
97
+ const hintGroups = useMemo(() => groupHints(hints), [hints]);
98
+ const suggestions = ((action == null ? void 0 : action.suggestions) ?? []).filter((suggestion) => suggestion.variants === 1);
99
+ const [start, setStart] = useState(() => planned ? readStart(action.name) : "write");
100
+ const [ticked, setTicked] = useState(() => /* @__PURE__ */ new Set());
101
+ const [draft, setDraft] = useState("");
102
+ const [chat, setChat] = useState([]);
103
+ const [asks, setAsks] = useState([]);
104
+ const [steps, setSteps] = useState(() => [{ label: "D\xE9part", values: { ...current } }]);
105
+ const [at, setAt] = useState(0);
106
+ const [plan, setPlan] = useState(null);
107
+ const [working, setWorking] = useState(false);
108
+ const [confirming, setConfirming] = useState(false);
109
+ const [startError, setStartError] = useState(null);
110
+ const [pending, setPending] = useState(null);
111
+ const listRef = useRef(null);
112
+ const winRef = useRef(null);
113
+ const questionRef = useRef(null);
114
+ const composerRef = useRef(null);
115
+ const waitBubbleRef = useRef(null);
116
+ const waitLineRef = useRef(null);
117
+ const firstBubbleRef = useRef(null);
118
+ const firstMessageRef = useRef(null);
119
+ const dividerRef = useRef(null);
120
+ const fieldRef = useRef(null);
121
+ const barRef = useRef(null);
122
+ const contentRef = useRef(null);
123
+ const toWait = useRef(null);
124
+ const toSplit = useRef(null);
125
+ const answered = steps.length > 1;
126
+ const waiting = !answered && chat.length > 0;
127
+ const started = chat.length > 0;
128
+ const step = steps[at];
129
+ const values = step.values;
130
+ const showingPlan = Boolean(plan) && step.plan === plan;
131
+ const versions = steps.slice(0, at + 1).filter((item, index) => index > 0 && !item.plan).length;
132
+ const workKind = pending === "plan" ? "plan" : versions === 0 ? "write" : "rework";
133
+ useEffect(() => {
134
+ var _a2;
135
+ (_a2 = listRef.current) == null ? void 0 : _a2.scrollTo({ top: listRef.current.scrollHeight, behavior: "smooth" });
136
+ }, [chat, working]);
137
+ useLayoutEffect(() => {
138
+ var _a2;
139
+ if (!waiting || !toWait.current) return;
140
+ const before = toWait.current;
141
+ toWait.current = null;
142
+ if (reducedMotion()) return;
143
+ const base = (_a2 = winRef.current) == null ? void 0 : _a2.getBoundingClientRect();
144
+ ghostOut(winRef.current, before.composer, [
145
+ { opacity: 1, transform: "none", filter: "blur(0)" },
146
+ { opacity: 0, transform: "scale(.97)", filter: "blur(4px)" }
147
+ ], { duration: 450 });
148
+ flipFrom(questionRef.current, before.question, base, { duration: 500 });
149
+ }, [waiting]);
150
+ useLayoutEffect(() => {
151
+ var _a2, _b, _c, _d, _e, _f, _g, _h, _i;
152
+ if (!answered || !toSplit.current) return;
153
+ const before = toSplit.current;
154
+ toSplit.current = null;
155
+ if (reducedMotion()) return;
156
+ const base = (_a2 = winRef.current) == null ? void 0 : _a2.getBoundingClientRect();
157
+ ghostOut(winRef.current, before.question, [
158
+ { opacity: 1, transform: "none", filter: "blur(0)" },
159
+ { opacity: 0, transform: "translateY(-24px)", filter: "blur(6px)" }
160
+ ], { duration: MORPH });
161
+ flipFrom(firstBubbleRef.current, before.bubble, base);
162
+ flipFrom(firstMessageRef.current, before.line, base, {}, { from: { opacity: 0 }, to: { opacity: 1 } });
163
+ (_c = (_b = dividerRef.current) == null ? void 0 : _b.animate) == null ? void 0 : _c.call(_b, [{ transform: "scaleY(0)" }, { transform: "scaleY(1)" }], { duration: MORPH, delay: 80, easing: EASE, fill: "backwards" });
164
+ (_e = (_d = fieldRef.current) == null ? void 0 : _d.animate) == null ? void 0 : _e.call(_d, [{ opacity: 0, transform: "translateY(24px)" }, { opacity: 1, transform: "none" }], { duration: MORPH, delay: 220, easing: EASE, fill: "backwards" });
165
+ (_g = (_f = barRef.current) == null ? void 0 : _f.animate) == null ? void 0 : _g.call(_f, [{ opacity: 0 }, { opacity: 1 }], { duration: MORPH, delay: 300, easing: EASE, fill: "backwards" });
166
+ const content = contentRef.current;
167
+ (_h = content == null ? void 0 : content.animate) == null ? void 0 : _h.call(content, [{ opacity: 0, transform: "translateX(40px)" }, { opacity: 1, transform: "none" }], { duration: MORPH, delay: 280, easing: EASE, fill: "backwards" });
168
+ [...((_i = content == null ? void 0 : content.firstElementChild) == null ? void 0 : _i.children) ?? []].slice(0, 6).forEach((card, index) => {
169
+ var _a3;
170
+ (_a3 = card.animate) == null ? void 0 : _a3.call(card, [{ opacity: 0, transform: "translateY(12px)" }, { opacity: 1, transform: "none" }], { duration: 500, delay: 420 + index * 100, easing: EASE, fill: "backwards" });
171
+ });
172
+ }, [answered]);
173
+ const pushStep = useCallback((next) => {
174
+ setSteps((all) => [...all.slice(0, at + 1), next]);
175
+ setAt(at + 1);
176
+ }, [at]);
177
+ const send = useCallback(async (request, { phase = null, sendPlan = null, shown = request } = {}) => {
178
+ var _a2, _b;
179
+ const text = request.trim();
180
+ if (!text || working) return;
181
+ const index = chat.length;
182
+ setChat((all) => [...all, { ask: shown.trim() }]);
183
+ setDraft("");
184
+ setWorking(true);
185
+ setPending(phase);
186
+ const previous = filledValues(values);
187
+ try {
188
+ const data = await apiFetch(`${AI_API}/run`, {
189
+ method: "POST",
190
+ body: {
191
+ prompt: action.name,
192
+ input: { request: text },
193
+ scope,
194
+ context: typeof context === "function" ? context() : context ?? null,
195
+ turn: {
196
+ asks,
197
+ previous: Object.keys(previous).length > 0 ? previous : null,
198
+ variants: 1,
199
+ ...phase ? { phase } : {},
200
+ ...sendPlan ? { plan: sendPlan } : {}
201
+ }
202
+ }
203
+ });
204
+ if (steps.length === 1) {
205
+ const base = (_a2 = winRef.current) == null ? void 0 : _a2.getBoundingClientRect();
206
+ toSplit.current = {
207
+ question: questionRef.current ? { node: questionRef.current.cloneNode(true), rect: rectIn(questionRef.current, base) } : null,
208
+ bubble: rectIn(waitBubbleRef.current, base),
209
+ line: rectIn(waitLineRef.current, base)
210
+ };
211
+ }
212
+ setAsks((all) => [...all, text]);
213
+ setChat((all) => all.map((item, i) => i === index ? { ...item, message: data.data.message } : item));
214
+ if (phase === "plan") {
215
+ const next = planOf(data.data);
216
+ setPlan(next);
217
+ pushStep({ label: "Plan", values, plan: next });
218
+ } else {
219
+ const version = ((_b = data.data.versions) == null ? void 0 : _b[0]) ?? {};
220
+ pushStep({ label: `V${versions + 1}`, values: mergeVersion(fields, values, version) });
221
+ }
222
+ } catch (error) {
223
+ const message = error.message || "L'IA n'a pas pu r\xE9pondre.";
224
+ if (steps.length === 1) {
225
+ setChat([]);
226
+ setStartError(message);
227
+ } else {
228
+ setChat((all) => all.map((item, i) => i === index ? { ...item, error: message } : item));
229
+ }
230
+ setDraft(shown);
231
+ } finally {
232
+ setWorking(false);
233
+ setPending(null);
234
+ }
235
+ }, [action, asks, chat.length, context, fields, pushStep, scope, steps.length, values, versions, working]);
236
+ const begin = useCallback((event) => {
237
+ var _a2;
238
+ event == null ? void 0 : event.preventDefault();
239
+ if (!draft.trim() || working) return;
240
+ const base = (_a2 = winRef.current) == null ? void 0 : _a2.getBoundingClientRect();
241
+ toWait.current = {
242
+ composer: composerRef.current ? { node: composerRef.current.cloneNode(true), rect: rectIn(composerRef.current, base) } : null,
243
+ question: rectIn(questionRef.current, base)
244
+ };
245
+ setStartError(null);
246
+ const request = firstRequest(draft, hints, ticked);
247
+ if (planned) saveStart(action.name, start);
248
+ send(request, { phase: planned && start === "plan" ? "plan" : null, shown: draft });
249
+ }, [action, draft, hints, planned, send, start, ticked, working]);
250
+ const toggleHint = useCallback((label) => {
251
+ setTicked((all) => {
252
+ const next = new Set(all);
253
+ if (next.has(label)) next.delete(label);
254
+ else next.add(label);
255
+ return next;
256
+ });
257
+ }, []);
258
+ const pickInGroup = useCallback((group, label) => {
259
+ setTicked((all) => {
260
+ const next = new Set(all);
261
+ for (const hint of group.hints) next.delete(hint.label);
262
+ if (label) next.add(label);
263
+ return next;
264
+ });
265
+ }, []);
266
+ const setValue = useCallback((key, value) => {
267
+ setSteps((all) => all.map((item, i) => i === at ? { ...item, values: { ...item.values, [key]: value } } : item));
268
+ }, [at]);
269
+ const changePlan = useCallback((next) => {
270
+ setPlan(next);
271
+ setSteps((all) => all.map((item, i) => i === at ? { ...item, plan: next } : item));
272
+ }, [at]);
273
+ const writeFromPlan = useCallback(() => {
274
+ if (!plan) return;
275
+ send(WRITE_FROM_PLAN, { sendPlan: planToSend(plan), shown: "R\xE9diger \xE0 partir de ce plan" });
276
+ }, [plan, send]);
277
+ const writeWithoutPlan = useCallback(() => {
278
+ send(WRITE_NOW, { shown: "\xC9crire sans plan" });
279
+ }, [send]);
280
+ const sendDraft = useCallback(() => {
281
+ if (showingPlan) send(draft, { phase: "plan", sendPlan: planToSend(plan) });
282
+ else send(draft);
283
+ }, [draft, plan, send, showingPlan]);
284
+ const pick = useCallback((index) => {
285
+ setAt(index);
286
+ if (steps[index].plan) setPlan(steps[index].plan);
287
+ }, [steps]);
288
+ const apply = useCallback(() => {
289
+ const out = filledValues(values);
290
+ if (Object.keys(out).length === 0) return;
291
+ onApply(out);
292
+ onClose();
293
+ toast.success("Le contenu est mis dans le formulaire. Relire avant d'enregistrer.");
294
+ }, [onApply, onClose, toast, values]);
295
+ const requestClose = useCallback(() => {
296
+ if (started) setConfirming(true);
297
+ else onClose();
298
+ }, [onClose, started]);
299
+ const canApply = !working && !showingPlan && Object.keys(filledValues(values)).length > 0;
300
+ const question = /* @__PURE__ */ jsxs("div", { ref: questionRef, className: "flex flex-col items-center gap-2 text-center", children: [
301
+ /* @__PURE__ */ jsx("p", { className: `m-0 text-[34px] leading-[42px] text-fg ${AI_CLASSES.voice}`, children: "Qu'est-ce qu'on \xE9crit ?" }),
302
+ /* @__PURE__ */ jsx("p", { className: "m-0 text-[13.5px] text-fg-muted", children: "Le sujet, \xE0 qui il s'adresse, ce qu'il doit faire passer." })
303
+ ] });
304
+ const middle = /* @__PURE__ */ jsx("div", { className: "flex flex-1 items-center justify-center overflow-y-auto px-6 pb-24 pt-8", children: waiting ? /* @__PURE__ */ jsxs("div", { className: "flex w-full max-w-[680px] flex-col items-center gap-6", children: [
305
+ question,
306
+ /* @__PURE__ */ jsx("div", { ref: waitBubbleRef, className: `${AI_CLASSES.arrive} max-w-[520px] self-center whitespace-pre-line rounded-[14px_14px_4px_14px] border border-line bg-surface-muted px-4 py-3 text-[15px] leading-[22px] text-fg`, children: (_a = chat[0]) == null ? void 0 : _a.ask }),
307
+ /* @__PURE__ */ jsxs("div", { ref: waitLineRef, className: `${AI_CLASSES.arrive} flex items-center gap-2.5 [animation-delay:150ms]`, children: [
308
+ /* @__PURE__ */ jsx("span", { "aria-hidden": "true", className: "fraza-ai-pulse h-2 w-2 rounded-full bg-accent" }),
309
+ /* @__PURE__ */ jsx(WorkingWord, { words: WORK_WORDS[workKind], className: "text-[16px] text-fg-muted" })
310
+ ] })
311
+ ] }) : /* @__PURE__ */ jsxs("form", { onSubmit: begin, className: `${AI_CLASSES.arrive} flex w-full max-w-[680px] flex-col gap-6`, children: [
312
+ question,
313
+ startError && /* @__PURE__ */ jsx("p", { role: "alert", className: "m-0 text-center text-[13px] text-danger", children: startError }),
314
+ /* @__PURE__ */ jsx("div", { ref: composerRef, children: /* @__PURE__ */ jsx(
315
+ StudioComposer,
316
+ {
317
+ draft,
318
+ onDraft: setDraft,
319
+ maxLength: REQUEST_MAX,
320
+ onSubmit: begin,
321
+ groups: hintGroups.groups,
322
+ alone: hintGroups.alone,
323
+ ticked,
324
+ onPick: pickInGroup,
325
+ onToggle: toggleHint,
326
+ planned,
327
+ start,
328
+ onStart: setStart,
329
+ footer: /* @__PURE__ */ jsxs("p", { className: "m-0 text-balance text-center text-[12px] leading-[18px] text-fg-muted", children: [
330
+ "L'IA remplira ",
331
+ listOf(fields.map((field) => field.label)),
332
+ "."
333
+ ] })
334
+ }
335
+ ) })
336
+ ] }) });
337
+ const chips = /* @__PURE__ */ jsxs("div", { className: "flex flex-wrap gap-1.5", children: [
338
+ planned && !showingPlan && versions > 0 && /* @__PURE__ */ jsx(
339
+ "button",
340
+ {
341
+ type: "button",
342
+ disabled: working,
343
+ onClick: () => send(REVIEW_PLAN, { phase: "plan", shown: "Revoir le plan" }),
344
+ className: "h-7 rounded-full border border-accent-line bg-accent-soft px-2.5 text-[12.5px] text-accent transition-colors disabled:opacity-50",
345
+ children: "Revoir le plan"
346
+ }
347
+ ),
348
+ !showingPlan && versions > 0 && suggestions.map((suggestion) => /* @__PURE__ */ jsx(
349
+ "button",
350
+ {
351
+ type: "button",
352
+ disabled: working,
353
+ onClick: () => send(suggestion.request, { shown: suggestion.label }),
354
+ className: "h-7 rounded-full border border-line bg-surface-muted px-2.5 text-[12.5px] text-fg-secondary transition-colors hover:border-accent-line hover:bg-accent-soft hover:text-fg disabled:opacity-50",
355
+ children: suggestion.label
356
+ },
357
+ suggestion.label
358
+ ))
359
+ ] });
360
+ const conversation = /* @__PURE__ */ jsxs(Fragment, { children: [
361
+ /* @__PURE__ */ jsxs("div", { ref: listRef, className: "flex min-h-0 flex-1 flex-col gap-3.5 overflow-y-auto px-5 py-5", children: [
362
+ chat.map((item, index) => /* @__PURE__ */ jsxs(React.Fragment, { children: [
363
+ /* @__PURE__ */ jsx("div", { ref: index === 0 ? firstBubbleRef : void 0, className: "max-w-[260px] self-end whitespace-pre-line rounded-[12px_12px_4px_12px] border border-line bg-surface-muted px-3 py-2 text-[13px] leading-[19px] text-fg", children: item.ask }),
364
+ item.error && /* @__PURE__ */ jsx("p", { className: "m-0 text-[12.5px] text-danger", children: item.error }),
365
+ item.message && /* @__PURE__ */ jsxs("div", { ref: index === 0 ? firstMessageRef : void 0, className: `${index === 0 ? "" : AI_CLASSES.arrive} flex items-start gap-2.5`, children: [
366
+ /* @__PURE__ */ jsx("span", { "aria-hidden": "true", className: "mt-1 flex h-5 w-5 shrink-0 items-center justify-center rounded-[6px] bg-accent-soft text-accent", children: /* @__PURE__ */ jsx(SparklesIcon, { className: "h-3 w-3" }) }),
367
+ /* @__PURE__ */ jsx("p", { className: `m-0 text-[15px] leading-[22px] text-fg ${AI_CLASSES.voice}`, children: item.message })
368
+ ] })
369
+ ] }, index)),
370
+ working && /* @__PURE__ */ jsxs("div", { className: "flex items-center gap-2.5", children: [
371
+ /* @__PURE__ */ jsx("span", { "aria-hidden": "true", className: "fraza-ai-pulse mx-1.5 h-2 w-2 rounded-full bg-accent" }),
372
+ /* @__PURE__ */ jsx(WorkingWord, { words: WORK_WORDS[workKind], className: "text-[15px] text-fg-muted" })
373
+ ] })
374
+ ] }),
375
+ /* @__PURE__ */ jsxs("div", { ref: fieldRef, className: "flex flex-col gap-2.5 px-5 pb-5 pt-2", children: [
376
+ chips,
377
+ /* @__PURE__ */ jsx("form", { onSubmit: (event) => {
378
+ event.preventDefault();
379
+ sendDraft();
380
+ }, children: /* @__PURE__ */ jsxs(StudioBox, { rounded: "rounded-[14px]", children: [
381
+ /* @__PURE__ */ jsx(
382
+ "textarea",
383
+ {
384
+ rows: 3,
385
+ value: draft,
386
+ maxLength: REQUEST_MAX,
387
+ "aria-label": "Demande",
388
+ placeholder: showingPlan ? "Changer le plan, un autre angle\u2026" : "Qu'est-ce qu'on change ?",
389
+ onChange: (event) => setDraft(event.target.value),
390
+ onKeyDown: (event) => {
391
+ if (event.key === "Enter" && !event.shiftKey) {
392
+ event.preventDefault();
393
+ sendDraft();
394
+ }
395
+ },
396
+ className: "relative block w-full resize-none border-0 bg-transparent px-3 pt-2.5 text-[13.5px] leading-5 text-fg outline-none placeholder:text-fg-subtle"
397
+ }
398
+ ),
399
+ /* @__PURE__ */ jsxs("div", { className: "relative flex items-center justify-between px-3 pb-2 pt-1", children: [
400
+ /* @__PURE__ */ jsx("span", { className: "text-[11.5px] text-fg-subtle", children: "Entr\xE9e pour envoyer" }),
401
+ /* @__PURE__ */ jsx(SendButton, { label: "Envoyer", size: "h-8 w-8", disabled: working || !draft.trim() })
402
+ ] })
403
+ ] }) })
404
+ ] })
405
+ ] });
406
+ return /* @__PURE__ */ jsxs(Fragment, { children: [
407
+ /* @__PURE__ */ jsx(Modal, { variant: "ai", bare: true, size: "full", isOpen: true, onClose: requestClose, closable: !working, title: (action == null ? void 0 : action.heading) ?? "Le studio", children: /* @__PURE__ */ jsx("div", { ref: winRef, className: `${WINDOW_CLASS} relative`, children: !answered ? /* @__PURE__ */ jsxs("div", { className: "flex min-w-0 flex-1 flex-col", children: [
408
+ /* @__PURE__ */ jsxs("div", { className: "flex h-14 shrink-0 items-center justify-between pl-5 pr-3", children: [
409
+ /* @__PURE__ */ jsx(StudioLabel, { label: (action == null ? void 0 : action.label) ?? "Le studio" }),
410
+ /* @__PURE__ */ jsx(StudioClose, { onClose: requestClose, disabled: working })
411
+ ] }),
412
+ middle
413
+ ] }) : /* @__PURE__ */ jsxs(Fragment, { children: [
414
+ /* @__PURE__ */ jsx("span", { ref: dividerRef, "aria-hidden": "true", className: "pointer-events-none absolute bottom-0 left-[339px] top-0 z-10 w-px origin-top bg-line" }),
415
+ /* @__PURE__ */ jsxs("aside", { "aria-label": "Conversation", className: "flex w-[340px] shrink-0 flex-col", children: [
416
+ /* @__PURE__ */ jsx("div", { className: LABEL_CLASS, children: /* @__PURE__ */ jsx(StudioLabel, { label: (action == null ? void 0 : action.label) ?? "Le studio" }) }),
417
+ conversation
418
+ ] }),
419
+ /* @__PURE__ */ jsxs("section", { "aria-label": "Le contenu", className: "flex min-w-0 flex-1 flex-col", children: [
420
+ /* @__PURE__ */ jsxs("div", { ref: barRef, className: BAR_CLASS, children: [
421
+ /* @__PURE__ */ jsx("nav", { "aria-label": "Historique", className: "flex min-w-0 items-center gap-1.5 overflow-x-auto", children: steps.map((item, index) => /* @__PURE__ */ jsxs(React.Fragment, { children: [
422
+ index > 0 && /* @__PURE__ */ jsx("span", { "aria-hidden": "true", className: "h-px w-3 bg-line-strong" }),
423
+ /* @__PURE__ */ jsx(StepPill, { step: item, active: index === at, onClick: () => pick(index) })
424
+ ] }, index)) }),
425
+ /* @__PURE__ */ jsx(Button, { variant: "ghost", size: "sm", disabled: working || at === 0, onClick: () => pick(at - 1), children: "Annuler l'\xE9tape" }),
426
+ /* @__PURE__ */ jsx("span", { className: "flex-1" }),
427
+ showingPlan ? /* @__PURE__ */ jsxs(Fragment, { children: [
428
+ /* @__PURE__ */ jsx(Button, { variant: "secondary", size: "sm", disabled: working, onClick: writeWithoutPlan, children: "\xC9crire sans plan" }),
429
+ /* @__PURE__ */ jsx(Button, { variant: "ai", size: "sm", icon: SparklesIcon, disabled: working || plan.sections.every((section) => !section.title.trim()), onClick: writeFromPlan, children: "R\xE9diger \xE0 partir de ce plan" })
430
+ ] }) : /* @__PURE__ */ jsx(Button, { variant: "ai", size: "sm", onClick: apply, disabled: !canApply, children: "Appliquer" }),
431
+ /* @__PURE__ */ jsx(StudioClose, { onClose: requestClose, disabled: working })
432
+ ] }),
433
+ /* @__PURE__ */ jsxs("div", { className: "relative flex min-h-0 flex-1 flex-col", children: [
434
+ /* @__PURE__ */ jsx("div", { ref: contentRef, className: `relative min-h-0 flex-1 overflow-y-auto bg-canvas ${CONTENT_TRANSITION} ${working ? WORKING_CONTENT : ""}`, children: showingPlan ? /* @__PURE__ */ jsx(StudioPlan, { plan, onChange: changePlan, disabled: working }) : at === 0 ? /* @__PURE__ */ jsx("div", { className: "flex h-full items-center justify-center", children: /* @__PURE__ */ jsx("span", { className: `text-[16px] text-fg-muted ${AI_CLASSES.voice}`, children: working ? "L'IA pr\xE9pare le contenu\u2026" : "Le contenu s'affichera ici." }) }) : /* @__PURE__ */ jsx(StudioFields, { fields, values, onChange: setValue, busy: working }) }),
435
+ /* @__PURE__ */ jsx(WorkingGlows, { active: working })
436
+ ] })
437
+ ] })
438
+ ] }) }) }),
439
+ /* @__PURE__ */ jsx(
440
+ ConfirmDialog,
441
+ {
442
+ isOpen: confirming,
443
+ title: "Fermer le studio ?",
444
+ description: "La conversation et le contenu non appliqu\xE9 seront perdus.",
445
+ confirmLabel: "Fermer",
446
+ onConfirm: () => {
447
+ setConfirming(false);
448
+ onClose();
449
+ },
450
+ onCancel: () => setConfirming(false)
451
+ }
452
+ )
453
+ ] });
454
+ }
455
+ export {
456
+ StudioCompose as default
457
+ };