@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.
- package/CHANGELOG.md +158 -0
- package/LICENSE +21 -0
- package/README.md +72 -2
- package/THIRD-PARTY-NOTICES.md +11 -0
- package/changelog.json +478 -0
- package/dist/admin/AiActions.client.js +166 -0
- package/dist/admin/AiChoices.client.js +67 -0
- package/dist/admin/AiInputForm.client.js +57 -0
- package/dist/admin/AiPage.client.js +181 -0
- package/dist/admin/AiResults.client.js +76 -0
- package/dist/admin/AiStudio.client.js +453 -0
- package/dist/admin/AiUsageWidget.client.js +28 -0
- package/dist/admin/StudioBar.client.js +33 -0
- package/dist/admin/StudioCompose.client.js +457 -0
- package/dist/admin/StudioComposer.client.js +217 -0
- package/dist/admin/StudioFields.client.js +124 -0
- package/dist/admin/StudioPlan.client.js +94 -0
- package/dist/admin/StudioWorking.client.js +157 -0
- package/dist/admin/choices.js +22 -0
- package/dist/admin/format.js +19 -0
- package/dist/admin/input-form.js +59 -0
- package/dist/admin/paths.js +4 -0
- package/dist/admin/studio-compose.js +94 -0
- package/dist/admin/studio-diff.js +240 -0
- package/dist/admin/widgets.js +14 -0
- package/dist/admin.js +9 -0
- package/dist/api.server.js +155 -0
- package/dist/config.server.js +113 -0
- package/dist/cron.server.js +26 -0
- package/dist/db.server.js +65 -0
- package/dist/index.js +43 -0
- package/dist/integration.server.js +28 -0
- package/dist/migrations.js +4 -0
- package/dist/permissions.js +9 -0
- package/dist/prompts.server.js +149 -0
- package/dist/providers/anthropic.server.js +42 -0
- package/dist/providers/chat-completions.server.js +63 -0
- package/dist/providers/google.server.js +52 -0
- package/dist/providers/http.server.js +98 -0
- package/dist/providers/index.server.js +22 -0
- package/dist/providers/ollama.server.js +45 -0
- package/dist/register-server.js +34 -0
- package/dist/results.server.js +74 -0
- package/dist/run.server.js +166 -0
- package/dist/runs.server.js +106 -0
- package/dist/schema.js +190 -0
- package/dist/seed.server.js +205 -0
- package/dist/server.js +19 -0
- package/dist/source.js +4 -0
- package/dist/studio.js +205 -0
- package/dist/test-prompt.server.js +24 -0
- package/dist/version.js +4 -0
- package/dist/widgets.server.js +29 -0
- package/package.json +67 -4
|
@@ -0,0 +1,453 @@
|
|
|
1
|
+
"use client";
|
|
2
|
+
import { Fragment, jsx, jsxs } from "react/jsx-runtime";
|
|
3
|
+
import React, { useCallback, useEffect, 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 StudioCompose from "./StudioCompose.client.js";
|
|
12
|
+
import { SendButton, StudioBox } from "./StudioComposer.client.js";
|
|
13
|
+
import StudioPlan from "./StudioPlan.client.js";
|
|
14
|
+
import { CONTENT_TRANSITION, WORKING_CONTENT, WorkingGlows, WorkingWord } from "./StudioWorking.client.js";
|
|
15
|
+
import { isEmptyValues, planOf, planToSend, WORK_WORDS } from "./studio-compose.js";
|
|
16
|
+
import { compareVersion, fieldsOf } from "./studio-diff.js";
|
|
17
|
+
const REVIEW_PLAN = "Propose une nouvelle structure pour ce contenu.";
|
|
18
|
+
const WRITE_FROM_PLAN = "R\xE9\xE9cris le contenu en suivant ce plan. Garde ce qui fonctionne d\xE9j\xE0.";
|
|
19
|
+
const SEGMENT = {
|
|
20
|
+
same: "",
|
|
21
|
+
del: "rounded-[3px] bg-danger-soft px-px text-danger line-through decoration-danger/60",
|
|
22
|
+
add: "rounded-[3px] bg-accent-soft px-px text-fg",
|
|
23
|
+
addClean: "underline decoration-accent/60 decoration-1 underline-offset-4"
|
|
24
|
+
};
|
|
25
|
+
const BLOCK_STYLE = {
|
|
26
|
+
h1: "text-[20px] leading-[28px] font-semibold text-fg",
|
|
27
|
+
h2: "text-[18px] leading-[26px] font-semibold text-fg",
|
|
28
|
+
h3: "text-[16px] leading-[24px] font-semibold text-fg",
|
|
29
|
+
h4: "text-[15px] leading-[22px] font-semibold text-fg",
|
|
30
|
+
blockquote: "border-l-2 border-line-strong pl-3 italic",
|
|
31
|
+
ul: "whitespace-pre-line",
|
|
32
|
+
ol: "whitespace-pre-line"
|
|
33
|
+
};
|
|
34
|
+
function shownSegments(segments, kept, view) {
|
|
35
|
+
if (!kept) return segments.filter(([kind]) => kind !== "add").map(([, text]) => ["same", text]);
|
|
36
|
+
if (view === "clean") return segments.filter(([kind]) => kind !== "del").map(([kind, text]) => [kind === "add" ? "addClean" : "same", text]);
|
|
37
|
+
return segments;
|
|
38
|
+
}
|
|
39
|
+
function Segments({ segments }) {
|
|
40
|
+
return segments.map(([kind, text], index) => /* @__PURE__ */ jsx("span", { className: SEGMENT[kind], children: text }, index));
|
|
41
|
+
}
|
|
42
|
+
function Piece({ piece, kept, current, view, titleLike, onPick }) {
|
|
43
|
+
const changed = piece.kind !== "same";
|
|
44
|
+
const style = titleLike ? "text-[22px] leading-[30px] font-semibold tracking-[-0.01em] text-fg" : BLOCK_STYLE[piece.tag] ?? "text-[14.5px] leading-[24px] text-fg-secondary";
|
|
45
|
+
if (piece.marker) {
|
|
46
|
+
return /* @__PURE__ */ jsx("span", { className: "inline-flex w-fit items-center gap-1.5 rounded-control border border-dashed border-line-strong px-2.5 py-1 text-[12px] text-fg-muted", children: "\xC9l\xE9ment gard\xE9 tel quel (image, vid\xE9o, bloc\u2026)" });
|
|
47
|
+
}
|
|
48
|
+
const body = /* @__PURE__ */ jsx(Segments, { segments: changed ? shownSegments(piece.segments, kept, view) : piece.segments });
|
|
49
|
+
if (!changed) return /* @__PURE__ */ jsx("div", { className: style, children: body });
|
|
50
|
+
return /* @__PURE__ */ jsxs(
|
|
51
|
+
"div",
|
|
52
|
+
{
|
|
53
|
+
role: "button",
|
|
54
|
+
tabIndex: -1,
|
|
55
|
+
onClick: onPick,
|
|
56
|
+
className: `relative -ml-4 cursor-pointer rounded-control py-0.5 pl-4 pr-1 transition-colors ${current ? "bg-accent-soft/40" : "hover:bg-surface-muted"}`,
|
|
57
|
+
children: [
|
|
58
|
+
/* @__PURE__ */ jsx("span", { "aria-hidden": "true", className: `absolute bottom-1 left-0 top-1 w-[2px] rounded-full transition-colors ${kept ? "bg-accent" : "bg-line-strong"}` }),
|
|
59
|
+
/* @__PURE__ */ jsx("div", { className: `${style} transition-opacity ${kept ? "" : "opacity-60"}`, children: body })
|
|
60
|
+
]
|
|
61
|
+
}
|
|
62
|
+
);
|
|
63
|
+
}
|
|
64
|
+
function Toggle({ options, value, onChange }) {
|
|
65
|
+
return /* @__PURE__ */ jsx("div", { className: "flex items-center gap-0.5 rounded-control border border-line bg-surface-muted p-0.5", children: options.map(([key, label]) => /* @__PURE__ */ jsx(
|
|
66
|
+
"button",
|
|
67
|
+
{
|
|
68
|
+
type: "button",
|
|
69
|
+
"aria-pressed": value === key,
|
|
70
|
+
onClick: () => onChange(key),
|
|
71
|
+
className: `h-7 rounded-[6px] px-2.5 text-[12.5px] font-medium transition-colors ${value === key ? "bg-surface text-fg shadow-[inset_0_0_0_1px_var(--color-line-strong)]" : "text-fg-muted hover:text-fg"}`,
|
|
72
|
+
children: label
|
|
73
|
+
},
|
|
74
|
+
key
|
|
75
|
+
)) });
|
|
76
|
+
}
|
|
77
|
+
function Kbd({ children }) {
|
|
78
|
+
return /* @__PURE__ */ jsx("kbd", { className: "ml-1 inline-flex h-[18px] min-w-[18px] items-center justify-center rounded-[4px] border border-current/30 px-1 font-sans text-[10.5px] opacity-70", children });
|
|
79
|
+
}
|
|
80
|
+
function cardTitle(fields, version) {
|
|
81
|
+
for (const field of fields) {
|
|
82
|
+
const value = version == null ? void 0 : version[field.key];
|
|
83
|
+
if (field.kind === "text" && typeof value === "string" && value.trim() && !value.includes("<")) return value;
|
|
84
|
+
}
|
|
85
|
+
return "Le texte retravaill\xE9";
|
|
86
|
+
}
|
|
87
|
+
function AiStudio(props) {
|
|
88
|
+
const { isOpen, action, scope, context, current, onApply, onClose } = props;
|
|
89
|
+
const [composing] = useState(() => isEmptyValues(fieldsOf(action == null ? void 0 : action.output), current));
|
|
90
|
+
if (!isOpen) return null;
|
|
91
|
+
if (composing) return /* @__PURE__ */ jsx(StudioCompose, { action, scope, context, current, onApply, onClose });
|
|
92
|
+
return /* @__PURE__ */ jsx(StudioRevise, { ...props });
|
|
93
|
+
}
|
|
94
|
+
function StudioRevise({ isOpen, action, scope, context, current, onApply, onClose }) {
|
|
95
|
+
var _a;
|
|
96
|
+
const toast = useToast();
|
|
97
|
+
const fields = useMemo(() => fieldsOf(action == null ? void 0 : action.output), [action]);
|
|
98
|
+
const planned = useMemo(() => hasHtmlField(action == null ? void 0 : action.output), [action]);
|
|
99
|
+
const [plan, setPlan] = useState(null);
|
|
100
|
+
const [planOpen, setPlanOpen] = useState(false);
|
|
101
|
+
const [pending, setPending] = useState(null);
|
|
102
|
+
const [turns, setTurns] = useState([]);
|
|
103
|
+
const [shown, setShown] = useState(null);
|
|
104
|
+
const [kept, setKept] = useState({});
|
|
105
|
+
const [cursor, setCursor] = useState(0);
|
|
106
|
+
const [view, setView] = useState("diff");
|
|
107
|
+
const [draft, setDraft] = useState("");
|
|
108
|
+
const [working, setWorking] = useState(false);
|
|
109
|
+
const [confirming, setConfirming] = useState(false);
|
|
110
|
+
const listRef = useRef(null);
|
|
111
|
+
const docRef = useRef(null);
|
|
112
|
+
useEffect(() => {
|
|
113
|
+
if (!isOpen) return;
|
|
114
|
+
setTurns([]);
|
|
115
|
+
setShown(null);
|
|
116
|
+
setKept({});
|
|
117
|
+
setCursor(0);
|
|
118
|
+
setView("diff");
|
|
119
|
+
setDraft("");
|
|
120
|
+
setConfirming(false);
|
|
121
|
+
setPlan(null);
|
|
122
|
+
setPlanOpen(false);
|
|
123
|
+
}, [isOpen, action]);
|
|
124
|
+
useEffect(() => {
|
|
125
|
+
var _a2;
|
|
126
|
+
(_a2 = listRef.current) == null ? void 0 : _a2.scrollTo({ top: listRef.current.scrollHeight, behavior: "smooth" });
|
|
127
|
+
}, [turns, working]);
|
|
128
|
+
const shownKey = shown ? `${shown.turn}.${shown.version}` : null;
|
|
129
|
+
const shownTurn = shown ? turns[shown.turn] : null;
|
|
130
|
+
const version = ((_a = shownTurn == null ? void 0 : shownTurn.versions) == null ? void 0 : _a[shown.version]) ?? null;
|
|
131
|
+
const base = (shownTurn == null ? void 0 : shownTurn.base) ?? current;
|
|
132
|
+
const carried = useMemo(() => (shownTurn == null ? void 0 : shownTurn.carried) ?? {}, [shownTurn]);
|
|
133
|
+
const keptHere = useMemo(() => shownKey ? kept[shownKey] ?? {} : {}, [kept, shownKey]);
|
|
134
|
+
const result = useMemo(() => compareVersion(fields, base, version ?? {}, keptHere), [fields, base, version, keptHere]);
|
|
135
|
+
const total = useMemo(() => ({ ...carried, ...result.merged }), [carried, result.merged]);
|
|
136
|
+
const changes = version ? result.changes : [];
|
|
137
|
+
const keptCount = changes.filter((change) => keptHere[change.id] !== false).length;
|
|
138
|
+
const at = Math.min(cursor, Math.max(0, changes.length - 1));
|
|
139
|
+
const currentChange = changes[at] ?? null;
|
|
140
|
+
const setKeep = useCallback((id, value) => {
|
|
141
|
+
if (!shownKey) return;
|
|
142
|
+
setKept((all) => ({ ...all, [shownKey]: { ...all[shownKey] ?? {}, [id]: value } }));
|
|
143
|
+
}, [shownKey]);
|
|
144
|
+
const decide = useCallback((value) => {
|
|
145
|
+
if (!currentChange) return;
|
|
146
|
+
setKeep(currentChange.id, value);
|
|
147
|
+
setCursor(Math.min(changes.length - 1, at + 1));
|
|
148
|
+
}, [at, changes.length, currentChange, setKeep]);
|
|
149
|
+
const setAll = useCallback((value) => {
|
|
150
|
+
if (!shownKey) return;
|
|
151
|
+
setKept((all) => ({ ...all, [shownKey]: Object.fromEntries(changes.map((change) => [change.id, value])) }));
|
|
152
|
+
}, [changes, shownKey]);
|
|
153
|
+
const send = useCallback(async (request, variants = 1, { phase = null, plan: sendPlan = null, shown: shown2 = request } = {}) => {
|
|
154
|
+
const text = request.trim();
|
|
155
|
+
if (!text || working) return;
|
|
156
|
+
const asks = turns.filter((turn) => turn.versions || turn.planned).map((turn) => turn.request ?? turn.ask);
|
|
157
|
+
const index = turns.length;
|
|
158
|
+
const previous = version ? total : {};
|
|
159
|
+
setTurns((all) => [...all, { ask: shown2.trim(), request: text, carried: previous, base: { ...current, ...previous } }]);
|
|
160
|
+
setDraft("");
|
|
161
|
+
setWorking(true);
|
|
162
|
+
setPending(phase);
|
|
163
|
+
try {
|
|
164
|
+
const data = await apiFetch(`${AI_API}/run`, {
|
|
165
|
+
method: "POST",
|
|
166
|
+
body: {
|
|
167
|
+
prompt: action.name,
|
|
168
|
+
input: { request: text },
|
|
169
|
+
scope,
|
|
170
|
+
context: typeof context === "function" ? context() : context ?? null,
|
|
171
|
+
turn: {
|
|
172
|
+
asks,
|
|
173
|
+
previous: Object.keys(previous).length > 0 ? previous : null,
|
|
174
|
+
variants,
|
|
175
|
+
...phase ? { phase } : {},
|
|
176
|
+
...sendPlan ? { plan: sendPlan } : {}
|
|
177
|
+
}
|
|
178
|
+
}
|
|
179
|
+
});
|
|
180
|
+
if (phase === "plan") {
|
|
181
|
+
setTurns((all) => all.map((turn, i) => i === index ? { ...turn, message: data.data.message, planned: true } : turn));
|
|
182
|
+
setPlan(planOf(data.data));
|
|
183
|
+
setPlanOpen(true);
|
|
184
|
+
return;
|
|
185
|
+
}
|
|
186
|
+
setTurns((all) => all.map((turn, i) => i === index ? { ...turn, message: data.data.message, versions: data.data.versions } : turn));
|
|
187
|
+
setShown({ turn: index, version: 0 });
|
|
188
|
+
setCursor(0);
|
|
189
|
+
setView("diff");
|
|
190
|
+
setPlanOpen(false);
|
|
191
|
+
} catch (error) {
|
|
192
|
+
setTurns((all) => all.map((turn, i) => i === index ? { ...turn, error: error.message || "L'IA n'a pas pu r\xE9pondre." } : turn));
|
|
193
|
+
setDraft(shown2);
|
|
194
|
+
} finally {
|
|
195
|
+
setWorking(false);
|
|
196
|
+
setPending(null);
|
|
197
|
+
}
|
|
198
|
+
}, [action, context, current, scope, total, turns, version, working]);
|
|
199
|
+
const apply = useCallback(() => {
|
|
200
|
+
if (Object.keys(total).length === 0) return;
|
|
201
|
+
onApply(total);
|
|
202
|
+
onClose();
|
|
203
|
+
toast.success("Les changements gard\xE9s sont appliqu\xE9s au formulaire. Relire avant d'enregistrer.");
|
|
204
|
+
}, [onApply, onClose, total, toast]);
|
|
205
|
+
const requestClose = useCallback(() => {
|
|
206
|
+
if (turns.length > 0) setConfirming(true);
|
|
207
|
+
else onClose();
|
|
208
|
+
}, [onClose, turns.length]);
|
|
209
|
+
const onDocKey = useCallback((event) => {
|
|
210
|
+
if (!currentChange) return;
|
|
211
|
+
if (event.key === "ArrowDown") {
|
|
212
|
+
event.preventDefault();
|
|
213
|
+
setCursor(Math.min(changes.length - 1, at + 1));
|
|
214
|
+
} else if (event.key === "ArrowUp") {
|
|
215
|
+
event.preventDefault();
|
|
216
|
+
setCursor(Math.max(0, at - 1));
|
|
217
|
+
} else if (event.key === "Enter") {
|
|
218
|
+
event.preventDefault();
|
|
219
|
+
decide(true);
|
|
220
|
+
} else if (event.key === "Backspace") {
|
|
221
|
+
event.preventDefault();
|
|
222
|
+
decide(false);
|
|
223
|
+
}
|
|
224
|
+
}, [at, changes.length, currentChange, decide]);
|
|
225
|
+
const suggestions = (action == null ? void 0 : action.suggestions) ?? [];
|
|
226
|
+
const showingPlan = planOpen && plan !== null;
|
|
227
|
+
return /* @__PURE__ */ jsxs(Fragment, { children: [
|
|
228
|
+
/* @__PURE__ */ jsx(Modal, { variant: "ai", bare: true, size: "full", isOpen, onClose: requestClose, closable: !working, title: (action == null ? void 0 : action.heading) ?? "Le studio", children: /* @__PURE__ */ jsxs("div", { className: WINDOW_CLASS, children: [
|
|
229
|
+
/* @__PURE__ */ jsxs("aside", { "aria-label": "Conversation", className: "flex w-[340px] shrink-0 flex-col border-r border-line", children: [
|
|
230
|
+
/* @__PURE__ */ jsx("div", { className: LABEL_CLASS, children: /* @__PURE__ */ jsx(StudioLabel, { label: (action == null ? void 0 : action.label) ?? "Le studio" }) }),
|
|
231
|
+
/* @__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: [
|
|
232
|
+
turns.length === 0 && /* @__PURE__ */ jsxs("div", { className: `${AI_CLASSES.arrive} flex flex-col gap-1.5`, children: [
|
|
233
|
+
/* @__PURE__ */ jsx("p", { className: `m-0 text-[22px] leading-[29px] text-fg ${AI_CLASSES.voice}`, children: "Qu'est-ce qu'on change ?" }),
|
|
234
|
+
/* @__PURE__ */ jsx("p", { className: "m-0 text-[12.5px] text-fg-muted", children: "L'IA propose, on garde ce qui pla\xEEt." })
|
|
235
|
+
] }),
|
|
236
|
+
turns.map((turn, index) => /* @__PURE__ */ jsxs(React.Fragment, { children: [
|
|
237
|
+
/* @__PURE__ */ jsx("div", { className: `${AI_CLASSES.arrive} 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: turn.ask }),
|
|
238
|
+
turn.error && /* @__PURE__ */ jsx("p", { className: "m-0 text-[12.5px] text-danger", children: turn.error }),
|
|
239
|
+
turn.planned && /* @__PURE__ */ jsxs("div", { className: `${AI_CLASSES.arrive} flex items-start gap-2.5`, children: [
|
|
240
|
+
/* @__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" }) }),
|
|
241
|
+
/* @__PURE__ */ jsxs("div", { className: "flex min-w-0 flex-1 flex-col gap-2", children: [
|
|
242
|
+
/* @__PURE__ */ jsx("p", { className: `m-0 text-[15px] leading-[22px] text-fg ${AI_CLASSES.voice}`, children: turn.message }),
|
|
243
|
+
/* @__PURE__ */ jsxs(
|
|
244
|
+
"button",
|
|
245
|
+
{
|
|
246
|
+
type: "button",
|
|
247
|
+
"aria-pressed": showingPlan,
|
|
248
|
+
onClick: () => setPlanOpen(true),
|
|
249
|
+
className: `flex items-center justify-between rounded-control border px-3 py-2 text-left text-[11.5px] text-fg-muted transition-colors ${showingPlan ? AI_CLASSES.optionPicked : AI_CLASSES.option}`,
|
|
250
|
+
children: [
|
|
251
|
+
/* @__PURE__ */ jsxs("span", { children: [
|
|
252
|
+
"Plan \xB7 ",
|
|
253
|
+
(plan == null ? void 0 : plan.sections.length) ?? 0,
|
|
254
|
+
" sections"
|
|
255
|
+
] }),
|
|
256
|
+
/* @__PURE__ */ jsx("span", { className: "text-accent", children: showingPlan ? "Affich\xE9" : "Voir" })
|
|
257
|
+
]
|
|
258
|
+
}
|
|
259
|
+
)
|
|
260
|
+
] })
|
|
261
|
+
] }),
|
|
262
|
+
turn.versions && /* @__PURE__ */ jsxs("div", { className: `${AI_CLASSES.arrive} flex items-start gap-2.5`, children: [
|
|
263
|
+
/* @__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" }) }),
|
|
264
|
+
/* @__PURE__ */ jsxs("div", { className: "flex min-w-0 flex-1 flex-col gap-2", children: [
|
|
265
|
+
/* @__PURE__ */ jsx("p", { className: `m-0 text-[15px] leading-[22px] text-fg ${AI_CLASSES.voice}`, children: turn.message }),
|
|
266
|
+
turn.versions.map((item, v) => {
|
|
267
|
+
const active = (shown == null ? void 0 : shown.turn) === index && (shown == null ? void 0 : shown.version) === v;
|
|
268
|
+
const count = compareVersion(fields, turn.base ?? current, item, {}).changes.length;
|
|
269
|
+
return /* @__PURE__ */ jsxs(
|
|
270
|
+
"button",
|
|
271
|
+
{
|
|
272
|
+
type: "button",
|
|
273
|
+
"aria-pressed": active,
|
|
274
|
+
onClick: () => {
|
|
275
|
+
setShown({ turn: index, version: v });
|
|
276
|
+
setCursor(0);
|
|
277
|
+
},
|
|
278
|
+
className: `flex flex-col gap-1 rounded-control border px-3 py-2 text-left transition-colors ${active ? AI_CLASSES.optionPicked : AI_CLASSES.option}`,
|
|
279
|
+
children: [
|
|
280
|
+
/* @__PURE__ */ jsxs("span", { className: "flex items-center justify-between text-[11.5px] text-fg-muted", children: [
|
|
281
|
+
/* @__PURE__ */ jsxs("span", { children: [
|
|
282
|
+
turn.versions.length > 1 ? `Version ${String.fromCharCode(65 + v)} \xB7 ` : "",
|
|
283
|
+
count,
|
|
284
|
+
" changement",
|
|
285
|
+
count > 1 ? "s" : ""
|
|
286
|
+
] }),
|
|
287
|
+
/* @__PURE__ */ jsx("span", { className: "text-accent", children: active ? "Affich\xE9e" : "Voir" })
|
|
288
|
+
] }),
|
|
289
|
+
/* @__PURE__ */ jsx("span", { className: "line-clamp-2 text-[13px] font-medium leading-[18px] text-fg", children: cardTitle(fields, item) })
|
|
290
|
+
]
|
|
291
|
+
},
|
|
292
|
+
v
|
|
293
|
+
);
|
|
294
|
+
})
|
|
295
|
+
] })
|
|
296
|
+
] })
|
|
297
|
+
] }, index)),
|
|
298
|
+
working && /* @__PURE__ */ jsxs("div", { className: "flex items-center gap-2.5", children: [
|
|
299
|
+
/* @__PURE__ */ jsx("span", { "aria-hidden": "true", className: "fraza-ai-pulse mx-1.5 h-2 w-2 rounded-full bg-accent" }),
|
|
300
|
+
/* @__PURE__ */ jsx(WorkingWord, { words: WORK_WORDS[pending === "plan" ? "plan" : "rework"], className: "text-[15px] text-fg-muted" })
|
|
301
|
+
] })
|
|
302
|
+
] }),
|
|
303
|
+
/* @__PURE__ */ jsxs("div", { className: "flex flex-col gap-2.5 px-5 pb-5 pt-2", children: [
|
|
304
|
+
(suggestions.length > 0 || planned) && /* @__PURE__ */ jsxs("div", { className: "flex flex-wrap gap-1.5", children: [
|
|
305
|
+
planned && /* @__PURE__ */ jsx(
|
|
306
|
+
"button",
|
|
307
|
+
{
|
|
308
|
+
type: "button",
|
|
309
|
+
disabled: working,
|
|
310
|
+
onClick: () => send(REVIEW_PLAN, 1, { phase: "plan", shown: "Revoir le plan" }),
|
|
311
|
+
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",
|
|
312
|
+
children: "Revoir le plan"
|
|
313
|
+
}
|
|
314
|
+
),
|
|
315
|
+
suggestions.map((suggestion) => /* @__PURE__ */ jsx(
|
|
316
|
+
"button",
|
|
317
|
+
{
|
|
318
|
+
type: "button",
|
|
319
|
+
disabled: working,
|
|
320
|
+
onClick: () => send(suggestion.request, suggestion.variants),
|
|
321
|
+
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",
|
|
322
|
+
children: suggestion.label
|
|
323
|
+
},
|
|
324
|
+
suggestion.label
|
|
325
|
+
))
|
|
326
|
+
] }),
|
|
327
|
+
/* @__PURE__ */ jsx("form", { onSubmit: (event) => {
|
|
328
|
+
event.preventDefault();
|
|
329
|
+
send(draft);
|
|
330
|
+
}, children: /* @__PURE__ */ jsxs(StudioBox, { rounded: "rounded-[14px]", children: [
|
|
331
|
+
/* @__PURE__ */ jsx(
|
|
332
|
+
"textarea",
|
|
333
|
+
{
|
|
334
|
+
rows: 3,
|
|
335
|
+
value: draft,
|
|
336
|
+
maxLength: REQUEST_MAX,
|
|
337
|
+
"aria-label": "Demande",
|
|
338
|
+
placeholder: "Ce qui ne va pas, ce qui manque, le ton voulu\u2026",
|
|
339
|
+
onChange: (event) => setDraft(event.target.value),
|
|
340
|
+
onKeyDown: (event) => {
|
|
341
|
+
if (event.key === "Enter" && !event.shiftKey) {
|
|
342
|
+
event.preventDefault();
|
|
343
|
+
send(draft);
|
|
344
|
+
}
|
|
345
|
+
},
|
|
346
|
+
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"
|
|
347
|
+
}
|
|
348
|
+
),
|
|
349
|
+
/* @__PURE__ */ jsxs("div", { className: "relative flex items-center justify-between px-3 pb-2 pt-1", children: [
|
|
350
|
+
/* @__PURE__ */ jsx("span", { className: "text-[11.5px] text-fg-subtle", children: "Entr\xE9e pour envoyer" }),
|
|
351
|
+
/* @__PURE__ */ jsx(SendButton, { label: "Envoyer", size: "h-8 w-8", disabled: working || !draft.trim() })
|
|
352
|
+
] })
|
|
353
|
+
] }) })
|
|
354
|
+
] })
|
|
355
|
+
] }),
|
|
356
|
+
/* @__PURE__ */ jsxs("section", { "aria-label": "Le contenu", className: "relative flex min-w-0 flex-1 flex-col", children: [
|
|
357
|
+
/* @__PURE__ */ jsxs("div", { className: BAR_CLASS, children: [
|
|
358
|
+
showingPlan ? /* @__PURE__ */ jsx(Button, { variant: "secondary", size: "sm", disabled: working, onClick: () => setPlanOpen(false), children: "Retour au contenu" }) : version && changes.length > 0 ? /* @__PURE__ */ jsxs(Fragment, { children: [
|
|
359
|
+
/* @__PURE__ */ jsx(Toggle, { options: [["diff", "Changements"], ["clean", "R\xE9sultat"]], value: view, onChange: setView }),
|
|
360
|
+
/* @__PURE__ */ jsxs("span", { className: "text-[12.5px] text-fg-muted", children: [
|
|
361
|
+
keptCount,
|
|
362
|
+
" sur ",
|
|
363
|
+
changes.length,
|
|
364
|
+
" gard\xE9s"
|
|
365
|
+
] }),
|
|
366
|
+
/* @__PURE__ */ jsx("button", { type: "button", onClick: () => setAll(true), className: "text-[12.5px] text-accent hover:underline", children: "Tout garder" }),
|
|
367
|
+
/* @__PURE__ */ jsx("button", { type: "button", onClick: () => setAll(false), className: "text-[12.5px] text-fg-muted hover:text-fg", children: "Tout refuser" })
|
|
368
|
+
] }) : /* @__PURE__ */ jsx("span", { className: "truncate text-[12.5px] text-fg-muted", children: version ? "Cette version ne change rien." : "Rien n'est enregistr\xE9 avant la relecture." }),
|
|
369
|
+
/* @__PURE__ */ jsx("span", { className: "flex-1" }),
|
|
370
|
+
showingPlan ? /* @__PURE__ */ jsx(
|
|
371
|
+
Button,
|
|
372
|
+
{
|
|
373
|
+
variant: "ai",
|
|
374
|
+
size: "sm",
|
|
375
|
+
icon: SparklesIcon,
|
|
376
|
+
disabled: working || plan.sections.every((section) => !section.title.trim()),
|
|
377
|
+
onClick: () => send(WRITE_FROM_PLAN, 1, { plan: planToSend(plan), shown: "R\xE9\xE9crire selon ce plan" }),
|
|
378
|
+
children: "R\xE9\xE9crire selon ce plan"
|
|
379
|
+
}
|
|
380
|
+
) : /* @__PURE__ */ jsx(Button, { variant: "ai", size: "sm", onClick: apply, disabled: working || Object.keys(total).length === 0, children: keptCount > 0 ? `Appliquer ${keptCount} changement${keptCount > 1 ? "s" : ""}` : "Appliquer" }),
|
|
381
|
+
/* @__PURE__ */ jsx(StudioClose, { onClose: requestClose, disabled: working })
|
|
382
|
+
] }),
|
|
383
|
+
showingPlan && /* @__PURE__ */ jsx("div", { className: "min-h-0 flex-1 overflow-y-auto bg-canvas", children: /* @__PURE__ */ jsx(StudioPlan, { plan, onChange: setPlan, disabled: working }) }),
|
|
384
|
+
!showingPlan && /* @__PURE__ */ jsxs(Fragment, { children: [
|
|
385
|
+
/* @__PURE__ */ jsxs("div", { className: "relative flex min-h-0 flex-1 flex-col", children: [
|
|
386
|
+
/* @__PURE__ */ jsx("div", { ref: docRef, tabIndex: 0, onKeyDown: onDocKey, className: "relative min-h-0 flex-1 overflow-y-auto outline-none", children: /* @__PURE__ */ jsx("div", { className: `mx-auto flex max-w-[680px] flex-col gap-6 px-6 py-6 ${CONTENT_TRANSITION} ${working ? WORKING_CONTENT : ""}`, children: fields.map((field, fieldIndex) => {
|
|
387
|
+
const compared = result.compared[field.key];
|
|
388
|
+
if (!compared || compared.pieces.every((piece) => piece.segments.length === 0)) return null;
|
|
389
|
+
return /* @__PURE__ */ jsxs("div", { className: "flex flex-col gap-2", children: [
|
|
390
|
+
/* @__PURE__ */ jsx("span", { className: "text-[11.5px] font-medium text-fg-subtle", children: field.label }),
|
|
391
|
+
/* @__PURE__ */ jsx("div", { className: "flex flex-col gap-3", children: compared.pieces.map((piece) => {
|
|
392
|
+
const index = changes.findIndex((change) => change.id === piece.id);
|
|
393
|
+
return /* @__PURE__ */ jsx(
|
|
394
|
+
Piece,
|
|
395
|
+
{
|
|
396
|
+
piece,
|
|
397
|
+
view,
|
|
398
|
+
kept: keptHere[piece.id] !== false,
|
|
399
|
+
current: index === at && index !== -1,
|
|
400
|
+
titleLike: fieldIndex === 0 && !compared.html,
|
|
401
|
+
onPick: () => {
|
|
402
|
+
var _a2;
|
|
403
|
+
setCursor(index);
|
|
404
|
+
(_a2 = docRef.current) == null ? void 0 : _a2.focus();
|
|
405
|
+
}
|
|
406
|
+
},
|
|
407
|
+
piece.id
|
|
408
|
+
);
|
|
409
|
+
}) })
|
|
410
|
+
] }, field.key);
|
|
411
|
+
}) }) }),
|
|
412
|
+
/* @__PURE__ */ jsx(WorkingGlows, { active: working })
|
|
413
|
+
] }),
|
|
414
|
+
currentChange && /* @__PURE__ */ jsx("div", { className: "flex items-center gap-4 border-t border-line px-6 py-3", children: /* @__PURE__ */ jsxs("div", { className: "flex min-w-0 items-center gap-3", children: [
|
|
415
|
+
/* @__PURE__ */ jsx("div", { className: "flex items-center gap-1", "aria-hidden": "true", children: changes.map((change, index) => /* @__PURE__ */ jsx("span", { className: `h-1.5 rounded-full transition-all ${index === at ? "w-4 bg-accent" : `w-1.5 ${keptHere[change.id] !== false ? "bg-accent/45" : "bg-line-strong"}`}` }, change.id)) }),
|
|
416
|
+
/* @__PURE__ */ jsxs("span", { className: "truncate text-[12.5px] text-fg-muted", children: [
|
|
417
|
+
at + 1,
|
|
418
|
+
" sur ",
|
|
419
|
+
changes.length,
|
|
420
|
+
" \xB7 ",
|
|
421
|
+
currentChange.field.label
|
|
422
|
+
] }),
|
|
423
|
+
/* @__PURE__ */ jsxs(Button, { variant: "secondary", size: "sm", onClick: () => decide(false), children: [
|
|
424
|
+
"Refuser",
|
|
425
|
+
/* @__PURE__ */ jsx(Kbd, { children: "\u232B" })
|
|
426
|
+
] }),
|
|
427
|
+
/* @__PURE__ */ jsxs(Button, { variant: "secondary", size: "sm", onClick: () => decide(true), children: [
|
|
428
|
+
"Garder",
|
|
429
|
+
/* @__PURE__ */ jsx(Kbd, { children: "\u21B5" })
|
|
430
|
+
] })
|
|
431
|
+
] }) })
|
|
432
|
+
] })
|
|
433
|
+
] })
|
|
434
|
+
] }) }),
|
|
435
|
+
/* @__PURE__ */ jsx(
|
|
436
|
+
ConfirmDialog,
|
|
437
|
+
{
|
|
438
|
+
isOpen: confirming,
|
|
439
|
+
title: "Fermer le studio ?",
|
|
440
|
+
description: "La conversation et les changements non appliqu\xE9s seront perdus.",
|
|
441
|
+
confirmLabel: "Fermer",
|
|
442
|
+
onConfirm: () => {
|
|
443
|
+
setConfirming(false);
|
|
444
|
+
onClose();
|
|
445
|
+
},
|
|
446
|
+
onCancel: () => setConfirming(false)
|
|
447
|
+
}
|
|
448
|
+
)
|
|
449
|
+
] });
|
|
450
|
+
}
|
|
451
|
+
export {
|
|
452
|
+
AiStudio as default
|
|
453
|
+
};
|
|
@@ -0,0 +1,28 @@
|
|
|
1
|
+
"use client";
|
|
2
|
+
import { jsx } from "react/jsx-runtime";
|
|
3
|
+
import React from "react";
|
|
4
|
+
import { registerWidget } from "@fraza/core/features/admin";
|
|
5
|
+
import { useAdminHref } from "@fraza/core/features/admin/components";
|
|
6
|
+
import { StatCard } from "@fraza/core/shared/components";
|
|
7
|
+
import { SOURCE } from "../source.js";
|
|
8
|
+
import { formatTokens } from "./format.js";
|
|
9
|
+
import { AI_WIDGETS } from "./widgets.js";
|
|
10
|
+
function AiUsageWidget({ data, loading }) {
|
|
11
|
+
const adminHref = useAdminHref();
|
|
12
|
+
if (!loading && !data) return null;
|
|
13
|
+
const total = data ? data.inputTokens + data.outputTokens : 0;
|
|
14
|
+
const change = !data ? void 0 : !data.configured ? "IA non configur\xE9e" : data.errors > 0 ? `${data.errors} appels en \xE9chec sur ${data.calls}` : `${data.calls} appels`;
|
|
15
|
+
return /* @__PURE__ */ jsx(
|
|
16
|
+
StatCard,
|
|
17
|
+
{
|
|
18
|
+
title: "Jetons d'IA ce mois-ci",
|
|
19
|
+
value: data ? formatTokens(total) : "",
|
|
20
|
+
change,
|
|
21
|
+
tone: data && (!data.configured || data.errors > 0) ? "warning" : "default",
|
|
22
|
+
href: adminHref("ai"),
|
|
23
|
+
linkLabel: "Voir la page IA",
|
|
24
|
+
loading
|
|
25
|
+
}
|
|
26
|
+
);
|
|
27
|
+
}
|
|
28
|
+
registerWidget({ ...AI_WIDGETS.USAGE, Component: AiUsageWidget }, { source: SOURCE });
|
|
@@ -0,0 +1,33 @@
|
|
|
1
|
+
"use client";
|
|
2
|
+
import { jsx, jsxs } from "react/jsx-runtime";
|
|
3
|
+
import React from "react";
|
|
4
|
+
import { SparklesIcon } from "@fraza/icons/SparklesIcon";
|
|
5
|
+
function StudioLabel({ label }) {
|
|
6
|
+
return /* @__PURE__ */ jsxs("span", { className: "flex min-w-0 items-center gap-2", children: [
|
|
7
|
+
/* @__PURE__ */ jsx("span", { "aria-hidden": "true", className: "flex h-[22px] w-[22px] shrink-0 items-center justify-center rounded-[6px] bg-accent-soft text-accent", children: /* @__PURE__ */ jsx(SparklesIcon, { className: "h-3 w-3" }) }),
|
|
8
|
+
/* @__PURE__ */ jsx("span", { className: "truncate text-[13px] font-medium text-fg", children: label })
|
|
9
|
+
] });
|
|
10
|
+
}
|
|
11
|
+
function StudioClose({ onClose, disabled = false }) {
|
|
12
|
+
return /* @__PURE__ */ jsx(
|
|
13
|
+
"button",
|
|
14
|
+
{
|
|
15
|
+
type: "button",
|
|
16
|
+
"aria-label": "Fermer",
|
|
17
|
+
disabled,
|
|
18
|
+
onClick: onClose,
|
|
19
|
+
className: "flex h-8 w-8 shrink-0 items-center justify-center rounded-control text-fg-muted transition-colors hover:bg-line-soft hover:text-fg disabled:opacity-40",
|
|
20
|
+
children: /* @__PURE__ */ jsx("svg", { width: "16", height: "16", viewBox: "0 0 24 24", fill: "none", stroke: "currentColor", strokeWidth: "1.8", strokeLinecap: "round", "aria-hidden": "true", children: /* @__PURE__ */ jsx("path", { d: "M6 6l12 12M18 6L6 18" }) })
|
|
21
|
+
}
|
|
22
|
+
);
|
|
23
|
+
}
|
|
24
|
+
const WINDOW_CLASS = "flex h-[min(820px,calc(90dvh-2px))]";
|
|
25
|
+
const LABEL_CLASS = "flex h-14 shrink-0 items-center border-b border-line px-5";
|
|
26
|
+
const BAR_CLASS = "flex h-14 shrink-0 items-center gap-3 border-b border-line pl-5 pr-3";
|
|
27
|
+
export {
|
|
28
|
+
BAR_CLASS,
|
|
29
|
+
LABEL_CLASS,
|
|
30
|
+
StudioClose,
|
|
31
|
+
StudioLabel,
|
|
32
|
+
WINDOW_CLASS
|
|
33
|
+
};
|