@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,57 @@
1
+ "use client";
2
+ import { jsx } from "react/jsx-runtime";
3
+ import React from "react";
4
+ import { Checkbox, Input, Select, Textarea } from "@fraza/core/shared/components";
5
+ function AiInputForm({ fields, values, errors, onChange, disabled = false }) {
6
+ const set = (key, value) => onChange({ ...values, [key]: value });
7
+ return /* @__PURE__ */ jsx("div", { className: "flex flex-col gap-4", children: fields.map((field) => {
8
+ const common = {
9
+ label: field.label,
10
+ description: field.description ?? void 0,
11
+ error: errors[field.key],
12
+ required: field.required,
13
+ disabled
14
+ };
15
+ if (field.kind === "boolean") {
16
+ return /* @__PURE__ */ jsx(
17
+ Checkbox,
18
+ {
19
+ label: field.label,
20
+ checked: Boolean(values[field.key]),
21
+ onChange: (checked) => set(field.key, checked),
22
+ disabled
23
+ },
24
+ field.key
25
+ );
26
+ }
27
+ if (field.kind === "select") {
28
+ return /* @__PURE__ */ jsx(
29
+ Select,
30
+ {
31
+ ...common,
32
+ value: values[field.key] ?? "",
33
+ onChange: (value) => set(field.key, value),
34
+ options: field.options.map((option) => ({ value: option, label: option }))
35
+ },
36
+ field.key
37
+ );
38
+ }
39
+ if (field.kind === "text") {
40
+ const long = field.maxLength === null || field.maxLength > 1e3;
41
+ return /* @__PURE__ */ jsx(Textarea, { ...common, rows: long ? 7 : 4, resizable: true, value: values[field.key] ?? "", onChange: (value) => set(field.key, value) }, field.key);
42
+ }
43
+ return /* @__PURE__ */ jsx(
44
+ Input,
45
+ {
46
+ ...common,
47
+ type: field.kind === "number" ? "number" : "text",
48
+ value: values[field.key] ?? "",
49
+ onChange: (value) => set(field.key, value)
50
+ },
51
+ field.key
52
+ );
53
+ }) });
54
+ }
55
+ export {
56
+ AiInputForm as default
57
+ };
@@ -0,0 +1,181 @@
1
+ "use client";
2
+ import { jsx, jsxs } from "react/jsx-runtime";
3
+ import React, { useCallback, useEffect, useState } from "react";
4
+ import { AdminHeader } from "@fraza/core/features/admin/components";
5
+ import { Badge, Button, Card, FilterTabs, RelativeDate, StatCard, Table, TABLE_MIN_HEIGHT } from "@fraza/core/shared/components";
6
+ import { apiFetch } from "@fraza/core/shared/http";
7
+ import { useToast } from "@fraza/core/toast";
8
+ import { formatDuration, formatTokens, TRIGGER_LABELS } from "./format.js";
9
+ import { AI_API } from "./paths.js";
10
+ const PAGE_SIZE = 25;
11
+ const USAGE_DAYS = 30;
12
+ const STATUS_TABS = [
13
+ { key: "all", label: "Tous" },
14
+ { key: "success", label: "R\xE9ussis" },
15
+ { key: "error", label: "En \xE9chec" }
16
+ ];
17
+ function ConfigCard({ status, onTest, testing }) {
18
+ const config = status == null ? void 0 : status.config;
19
+ const rows = config ? [
20
+ ["Fournisseur", config.providerLabel ?? config.provider ?? "Non d\xE9fini"],
21
+ ["Mod\xE8le", config.model ?? "Non d\xE9fini"],
22
+ ["Cl\xE9 d'API", config.hasKey ? "Pr\xE9sente" : "Absente"],
23
+ ...config.baseUrl ? [["Adresse", config.baseUrl]] : []
24
+ ] : [];
25
+ return /* @__PURE__ */ jsx(
26
+ Card,
27
+ {
28
+ header: /* @__PURE__ */ jsxs("div", { className: "mb-4 flex items-start justify-between gap-3", children: [
29
+ /* @__PURE__ */ jsxs("div", { className: "min-w-0", children: [
30
+ /* @__PURE__ */ jsx("h3", { className: "text-[15px] font-semibold leading-5 text-fg", children: "Configuration" }),
31
+ /* @__PURE__ */ jsx("p", { className: "mt-1 text-[12.5px] text-fg-muted", children: "Lue dans les variables d'environnement FRAZA_MODULE_AI_*." })
32
+ ] }),
33
+ config && /* @__PURE__ */ jsx(Button, { variant: "secondary", size: "sm", onClick: onTest, loading: testing, disabled: !config.configured || testing, children: "Tester" })
34
+ ] }),
35
+ children: !config ? /* @__PURE__ */ jsx("p", { className: "text-[13px] text-fg-muted", children: "Chargement\u2026" }) : /* @__PURE__ */ jsxs("div", { className: "flex flex-col gap-3", children: [
36
+ /* @__PURE__ */ jsx("dl", { className: "grid grid-cols-1 gap-x-6 gap-y-2 sm:grid-cols-2", children: rows.map(([label, value]) => /* @__PURE__ */ jsxs("div", { className: "flex flex-col gap-0.5", children: [
37
+ /* @__PURE__ */ jsx("dt", { className: "text-[12px] text-fg-muted", children: label }),
38
+ /* @__PURE__ */ jsx("dd", { className: "break-words text-[13px] text-fg", children: value })
39
+ ] }, label)) }),
40
+ config.problem && /* @__PURE__ */ jsx("p", { className: "text-[13px] text-danger-ink", children: config.problem }),
41
+ status.unmatchedCustomizations.length > 0 && /* @__PURE__ */ jsxs("p", { className: "text-[13px] text-warning-ink", children: [
42
+ "ai.js ajuste des prompts que personne ne d\xE9clare : ",
43
+ status.unmatchedCustomizations.join(", "),
44
+ ". V\xE9rifier leur nom, ou installer le module qui les fournit."
45
+ ] })
46
+ ] })
47
+ }
48
+ );
49
+ }
50
+ function UsageBlock({ usage }) {
51
+ const totals = usage == null ? void 0 : usage.totals;
52
+ return /* @__PURE__ */ jsxs("div", { className: "flex flex-col gap-3", children: [
53
+ /* @__PURE__ */ jsxs("div", { className: "grid grid-cols-1 gap-3 sm:grid-cols-3", children: [
54
+ /* @__PURE__ */ jsx(StatCard, { title: `Jetons envoy\xE9s, ${USAGE_DAYS} jours`, value: totals ? formatTokens(totals.inputTokens) : "", loading: !usage }),
55
+ /* @__PURE__ */ jsx(StatCard, { title: `Jetons re\xE7us, ${USAGE_DAYS} jours`, value: totals ? formatTokens(totals.outputTokens) : "", loading: !usage }),
56
+ /* @__PURE__ */ jsx(StatCard, { title: "Appels en \xE9chec", value: totals ? `${totals.errors} sur ${totals.calls}` : "", tone: (totals == null ? void 0 : totals.errors) ? "warning" : "default", loading: !usage })
57
+ ] }),
58
+ (usage == null ? void 0 : usage.byPrompt.length) > 0 && /* @__PURE__ */ jsx(Card, { title: "Par prompt", padding: "sm", children: /* @__PURE__ */ jsx("ul", { className: "flex flex-col divide-y divide-line-soft", children: usage.byPrompt.map((row) => /* @__PURE__ */ jsxs("li", { className: "flex items-center justify-between gap-3 py-2 text-[13px]", children: [
59
+ /* @__PURE__ */ jsx("span", { className: "truncate font-mono text-fg", children: row.prompt }),
60
+ /* @__PURE__ */ jsxs("span", { className: "shrink-0 text-fg-muted", children: [
61
+ row.calls,
62
+ " appels \xB7 ",
63
+ formatTokens(row.inputTokens + row.outputTokens),
64
+ " jetons"
65
+ ] })
66
+ ] }, row.prompt)) }) })
67
+ ] });
68
+ }
69
+ function PromptsCard({ prompts }) {
70
+ return /* @__PURE__ */ jsx(Card, { title: "Prompts d\xE9clar\xE9s", subtitle: "Par les modules et par le fichier ai.js du projet.", children: !prompts ? /* @__PURE__ */ jsx("p", { className: "text-[13px] text-fg-muted", children: "Chargement\u2026" }) : prompts.length === 0 ? /* @__PURE__ */ jsx("p", { className: "text-[13px] text-fg-muted", children: "Aucun prompt pour l'instant. Les modules compatibles et le fichier ai.js en d\xE9clarent." }) : /* @__PURE__ */ jsx("ul", { className: "flex flex-col divide-y divide-line-soft", children: prompts.map((prompt) => /* @__PURE__ */ jsxs("li", { className: "flex flex-wrap items-center gap-x-3 gap-y-1 py-2 text-[13px]", children: [
71
+ /* @__PURE__ */ jsx("span", { className: "font-mono text-fg", children: prompt.name }),
72
+ prompt.label && /* @__PURE__ */ jsx("span", { className: "text-fg-secondary", children: prompt.label }),
73
+ /* @__PURE__ */ jsxs("span", { className: "text-fg-muted", children: [
74
+ prompt.target ? `\xC9cran ${prompt.target}` : "Code seulement",
75
+ " \xB7 ",
76
+ prompt.source
77
+ ] }),
78
+ prompt.customized && /* @__PURE__ */ jsx(Badge, { dot: false, children: "Ajust\xE9 par ai.js" }),
79
+ !prompt.enabled && /* @__PURE__ */ jsx(Badge, { variant: "cancelled", children: "D\xE9sactiv\xE9" })
80
+ ] }, prompt.name)) }) });
81
+ }
82
+ function useRuns(status, page, reload) {
83
+ const toast = useToast();
84
+ const [state, setState] = useState({ runs: [], total: 0, loading: true });
85
+ useEffect(() => {
86
+ let cancelled = false;
87
+ setState((s) => ({ ...s, loading: true }));
88
+ const query = new URLSearchParams({ page: String(page), limit: String(PAGE_SIZE), ...status ? { status } : {} });
89
+ apiFetch(`${AI_API}/runs?${query}`).then((data) => {
90
+ if (!cancelled) setState({ runs: data.runs, total: data.pagination.total, loading: false });
91
+ }).catch((error) => {
92
+ if (cancelled) return;
93
+ setState({ runs: [], total: 0, loading: false });
94
+ toast.error(error.message || "L'historique n'a pas pu \xEAtre charg\xE9.");
95
+ });
96
+ return () => {
97
+ cancelled = true;
98
+ };
99
+ }, [status, page, reload, toast]);
100
+ return state;
101
+ }
102
+ const RUN_COLUMNS = [
103
+ { key: "prompt", label: "Prompt", mobile: "title", render: (r) => /* @__PURE__ */ jsx("span", { className: "font-mono text-fg", children: r.prompt }), mobileRender: (r) => r.prompt },
104
+ {
105
+ key: "status",
106
+ label: "R\xE9sultat",
107
+ render: (r) => r.status === "success" ? /* @__PURE__ */ jsx(Badge, { variant: "success", children: "R\xE9ussi" }) : /* @__PURE__ */ jsxs("span", { className: "flex flex-col gap-0.5", children: [
108
+ /* @__PURE__ */ jsx(Badge, { variant: "danger", children: "\xC9chec" }),
109
+ /* @__PURE__ */ jsx("span", { className: "max-w-[320px] truncate text-[12px] text-fg-muted", title: r.errorMessage ?? "", children: r.errorMessage })
110
+ ] })
111
+ },
112
+ { key: "trigger", label: "Origine", render: (r) => TRIGGER_LABELS[r.trigger] ?? r.trigger },
113
+ { key: "model", label: "Mod\xE8le", className: "w-full max-w-0", render: (r) => /* @__PURE__ */ jsx("span", { className: "block truncate", title: r.model ?? "", children: r.model ?? "\u2014" }) },
114
+ { key: "tokens", label: "Jetons", render: (r) => r.inputTokens === null && r.outputTokens === null ? "\u2014" : `${formatTokens(r.inputTokens)} / ${formatTokens(r.outputTokens)}` },
115
+ { key: "duration", label: "Dur\xE9e", render: (r) => formatDuration(r.durationMs) },
116
+ { key: "createdAt", label: "Date", mobile: "subtitle", render: (r) => /* @__PURE__ */ jsx(RelativeDate, { date: r.createdAt }), mobileRender: (r) => /* @__PURE__ */ jsx(RelativeDate, { date: r.createdAt }) }
117
+ ];
118
+ function AiPage() {
119
+ const toast = useToast();
120
+ const [status, setStatus] = useState(null);
121
+ const [usage, setUsage] = useState(null);
122
+ const [testing, setTesting] = useState(false);
123
+ const [filter, setFilter] = useState("all");
124
+ const [page, setPage] = useState(1);
125
+ const [reload, setReload] = useState(0);
126
+ const runs = useRuns(filter === "all" ? "" : filter, page, reload);
127
+ useEffect(() => {
128
+ apiFetch(`${AI_API}/status`).then(setStatus).catch((error) => toast.error(error.message || "La configuration n'a pas pu \xEAtre lue."));
129
+ }, [toast]);
130
+ useEffect(() => {
131
+ apiFetch(`${AI_API}/usage?days=${USAGE_DAYS}`).then((data) => setUsage(data.usage)).catch((error) => toast.error(error.message || "La consommation n'a pas pu \xEAtre lue."));
132
+ }, [toast, reload]);
133
+ const test = useCallback(async () => {
134
+ var _a;
135
+ setTesting(true);
136
+ try {
137
+ const data = await apiFetch(`${AI_API}/test`, { method: "POST" });
138
+ toast.success(`Le fournisseur r\xE9pond. ${((_a = data.data) == null ? void 0 : _a.message) ?? ""}`.trim());
139
+ } catch (error) {
140
+ toast.error(error.message || "Le test a \xE9chou\xE9.");
141
+ } finally {
142
+ setTesting(false);
143
+ setReload((n) => n + 1);
144
+ setPage(1);
145
+ }
146
+ }, [toast]);
147
+ return /* @__PURE__ */ jsxs("div", { className: "flex flex-col gap-6", children: [
148
+ /* @__PURE__ */ jsx(AdminHeader, { title: "IA", description: "Fournisseur d'IA, prompts d\xE9clar\xE9s, jetons consomm\xE9s et historique des appels." }),
149
+ /* @__PURE__ */ jsx(ConfigCard, { status, onTest: test, testing }),
150
+ /* @__PURE__ */ jsx(UsageBlock, { usage }),
151
+ /* @__PURE__ */ jsx(PromptsCard, { prompts: (status == null ? void 0 : status.prompts) ?? null }),
152
+ /* @__PURE__ */ jsxs("div", { className: "flex flex-col gap-3", children: [
153
+ /* @__PURE__ */ jsxs("div", { className: "flex flex-col justify-between gap-3 sm:flex-row sm:items-center", children: [
154
+ /* @__PURE__ */ jsx("h2", { className: "text-[15px] font-semibold text-fg", children: "Historique des appels" }),
155
+ /* @__PURE__ */ jsx(FilterTabs, { tabs: STATUS_TABS, value: filter, onChange: (value) => {
156
+ setFilter(value);
157
+ setPage(1);
158
+ } })
159
+ ] }),
160
+ /* @__PURE__ */ jsx(Card, { padding: "none", children: /* @__PURE__ */ jsx(
161
+ Table,
162
+ {
163
+ columns: RUN_COLUMNS,
164
+ data: runs.runs,
165
+ loading: runs.loading,
166
+ minHeight: TABLE_MIN_HEIGHT,
167
+ currentPage: page,
168
+ totalPages: Math.max(1, Math.ceil(runs.total / PAGE_SIZE)),
169
+ onPageChange: setPage,
170
+ limit: PAGE_SIZE,
171
+ total: runs.total,
172
+ emptyMessage: "Aucun appel",
173
+ emptyDescription: "Les appels au fournisseur appara\xEEtront ici, sans le texte envoy\xE9 ni la r\xE9ponse."
174
+ }
175
+ ) })
176
+ ] })
177
+ ] });
178
+ }
179
+ export {
180
+ AiPage as default
181
+ };
@@ -0,0 +1,76 @@
1
+ "use client";
2
+ import { jsx, jsxs } from "react/jsx-runtime";
3
+ import React, { useCallback, useEffect, useState } from "react";
4
+ import { AI_CLASSES, Badge, Button } from "@fraza/core/shared/components";
5
+ import { apiFetch } from "@fraza/core/shared/http";
6
+ import { useToast } from "@fraza/core/toast";
7
+ import { AI_API } from "./paths.js";
8
+ function formatValue(value) {
9
+ if (value === null || value === void 0 || value === "") return "\u2014";
10
+ if (Array.isArray(value)) return value.map(formatValue).join(", ");
11
+ if (typeof value === "boolean") return value ? "Oui" : "Non";
12
+ if (typeof value === "object") return JSON.stringify(value);
13
+ return String(value);
14
+ }
15
+ function Answer({ data, labels }) {
16
+ return /* @__PURE__ */ jsx("dl", { className: "flex flex-col gap-2", children: Object.entries(data).map(([key, value]) => /* @__PURE__ */ jsxs("div", { className: "flex flex-col gap-0.5", children: [
17
+ /* @__PURE__ */ jsx("dt", { className: "text-[12px] text-fg-muted", children: labels[key] ?? key }),
18
+ /* @__PURE__ */ jsx("dd", { className: `whitespace-pre-line break-words text-[15px] leading-[21px] text-fg ${AI_CLASSES.voice}`, children: formatValue(value) })
19
+ ] }, key)) });
20
+ }
21
+ function AiResults({ target, id, labels = {}, onApply }) {
22
+ const toast = useToast();
23
+ const [results, setResults] = useState([]);
24
+ const [busy, setBusy] = useState(null);
25
+ useEffect(() => {
26
+ let cancelled = false;
27
+ const query = new URLSearchParams({ target, id: String(id) });
28
+ apiFetch(`${AI_API}/results?${query}`).then((data) => {
29
+ if (!cancelled) setResults((data == null ? void 0 : data.results) ?? []);
30
+ }).catch(() => {
31
+ if (!cancelled) setResults([]);
32
+ });
33
+ return () => {
34
+ cancelled = true;
35
+ };
36
+ }, [target, id]);
37
+ const dismiss = useCallback(async (result, used) => {
38
+ setBusy(result.id);
39
+ try {
40
+ await apiFetch(`${AI_API}/results/${result.id}/dismiss`, { method: "POST" });
41
+ setResults((current) => current.filter((r) => r.id !== result.id));
42
+ if (used) toast.success("La proposition est appliqu\xE9e. Relire avant d'enregistrer.");
43
+ } catch (error) {
44
+ toast.error(error.message || "La proposition n'a pas pu \xEAtre retir\xE9e.");
45
+ } finally {
46
+ setBusy(null);
47
+ }
48
+ }, [toast]);
49
+ if (results.length === 0) return null;
50
+ return /* @__PURE__ */ jsx("div", { className: "flex flex-col gap-3", children: results.map((result) => (
51
+ // The answer waits in the AI's voice, on a card edged with the accent.
52
+ /* @__PURE__ */ jsx("div", { className: `${AI_CLASSES.arrive} rounded-card border ${AI_CLASSES.card}`, children: /* @__PURE__ */ jsxs("div", { className: "p-4", children: [
53
+ /* @__PURE__ */ jsx("div", { className: "mb-2 flex items-center gap-2", children: /* @__PURE__ */ jsx(Badge, { variant: "ai", children: "Propos\xE9 par l'IA" }) }),
54
+ /* @__PURE__ */ jsx(Answer, { data: result.data, labels }),
55
+ /* @__PURE__ */ jsxs("div", { className: "mt-3 flex justify-end gap-2", children: [
56
+ /* @__PURE__ */ jsx(Button, { variant: "ghost", size: "sm", disabled: busy === result.id, onClick: () => dismiss(result, false), children: "Ignorer" }),
57
+ onApply && /* @__PURE__ */ jsx(
58
+ Button,
59
+ {
60
+ variant: "secondary",
61
+ size: "sm",
62
+ disabled: busy === result.id,
63
+ onClick: () => {
64
+ onApply(result.data, result);
65
+ dismiss(result, true);
66
+ },
67
+ children: "Appliquer"
68
+ }
69
+ )
70
+ ] })
71
+ ] }) }, result.id)
72
+ )) });
73
+ }
74
+ export {
75
+ AiResults as default
76
+ };