@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,59 @@
|
|
|
1
|
+
const ONE_LINE_MAX = 120;
|
|
2
|
+
function typesOf(node) {
|
|
3
|
+
return Array.isArray(node.type) ? node.type : [node.type];
|
|
4
|
+
}
|
|
5
|
+
function formFields(schema) {
|
|
6
|
+
if (!(schema == null ? void 0 : schema.properties)) return [];
|
|
7
|
+
return Object.entries(schema.properties).map(([key, node]) => {
|
|
8
|
+
const types = typesOf(node);
|
|
9
|
+
const type = types.find((t) => t !== "null");
|
|
10
|
+
const kind = type === "boolean" ? "boolean" : type === "number" || type === "integer" ? "number" : node.enum ? "select" : node.maxLength && node.maxLength <= ONE_LINE_MAX ? "line" : "text";
|
|
11
|
+
return {
|
|
12
|
+
key,
|
|
13
|
+
kind,
|
|
14
|
+
label: node.title ?? key,
|
|
15
|
+
description: node.description ?? null,
|
|
16
|
+
required: !types.includes("null") && kind !== "boolean",
|
|
17
|
+
options: node.enum ?? [],
|
|
18
|
+
maxLength: node.maxLength ?? null,
|
|
19
|
+
integer: type === "integer"
|
|
20
|
+
};
|
|
21
|
+
});
|
|
22
|
+
}
|
|
23
|
+
function initialValues(fields) {
|
|
24
|
+
return Object.fromEntries(fields.map((field) => [field.key, field.kind === "boolean" ? false : ""]));
|
|
25
|
+
}
|
|
26
|
+
function readValues(fields, values) {
|
|
27
|
+
const input = {};
|
|
28
|
+
const errors = {};
|
|
29
|
+
for (const field of fields) {
|
|
30
|
+
const raw = values[field.key];
|
|
31
|
+
if (field.kind === "boolean") {
|
|
32
|
+
input[field.key] = Boolean(raw);
|
|
33
|
+
continue;
|
|
34
|
+
}
|
|
35
|
+
const text = typeof raw === "string" ? raw.trim() : "";
|
|
36
|
+
if (text === "") {
|
|
37
|
+
if (field.required) errors[field.key] = "Ce champ est requis.";
|
|
38
|
+
else input[field.key] = null;
|
|
39
|
+
continue;
|
|
40
|
+
}
|
|
41
|
+
if (field.kind === "number") {
|
|
42
|
+
const number = Number(text.replace(",", "."));
|
|
43
|
+
if (!Number.isFinite(number) || field.integer && !Number.isInteger(number)) errors[field.key] = field.integer ? "Un nombre entier est attendu." : "Un nombre est attendu.";
|
|
44
|
+
else input[field.key] = number;
|
|
45
|
+
continue;
|
|
46
|
+
}
|
|
47
|
+
if (field.maxLength && [...text].length > field.maxLength) {
|
|
48
|
+
errors[field.key] = `${field.maxLength} caract\xE8res au plus.`;
|
|
49
|
+
continue;
|
|
50
|
+
}
|
|
51
|
+
input[field.key] = text;
|
|
52
|
+
}
|
|
53
|
+
return { input, errors };
|
|
54
|
+
}
|
|
55
|
+
export {
|
|
56
|
+
formFields,
|
|
57
|
+
initialValues,
|
|
58
|
+
readValues
|
|
59
|
+
};
|
|
@@ -0,0 +1,94 @@
|
|
|
1
|
+
const WORK_WORDS = Object.freeze({
|
|
2
|
+
write: Object.freeze(["Esquisse", "Premier jet", "Intertitres", "Mise en forme", "Transitions", "Exemples", "Fil conducteur", "Chapeau", "D\xE9veloppement", "Mise en mots"]),
|
|
3
|
+
rework: Object.freeze(["Relecture", "Peaufinage", "Accords", "Ponctuation", "Tournures", "Nuances", "Rythme", "Clart\xE9", "Reformulation", "Polissage", "Liaisons", "V\xE9rification", "Synonymes", "Ciselage"]),
|
|
4
|
+
plan: Object.freeze(["Structure", "Angle", "Sections", "Titres", "Ordre", "Fil conducteur", "\xC9quilibre", "Charpente"])
|
|
5
|
+
});
|
|
6
|
+
function hasValue(value) {
|
|
7
|
+
if (Array.isArray(value)) return value.length > 0;
|
|
8
|
+
if (typeof value === "string") return value.replace(/<[^>]+>/g, "").trim() !== "";
|
|
9
|
+
return value !== null && value !== void 0;
|
|
10
|
+
}
|
|
11
|
+
function isEmptyValues(fields, values) {
|
|
12
|
+
const texts = fields.filter((field) => field.kind === "text");
|
|
13
|
+
if (texts.length === 0) return false;
|
|
14
|
+
return texts.every((field) => !hasValue(values == null ? void 0 : values[field.key]));
|
|
15
|
+
}
|
|
16
|
+
function mergeVersion(fields, values, version) {
|
|
17
|
+
const out = { ...values };
|
|
18
|
+
for (const field of fields) {
|
|
19
|
+
const next = version == null ? void 0 : version[field.key];
|
|
20
|
+
if (next !== null && next !== void 0) out[field.key] = next;
|
|
21
|
+
}
|
|
22
|
+
return out;
|
|
23
|
+
}
|
|
24
|
+
function firstRequest(text, hints, ticked) {
|
|
25
|
+
const on = new Set(ticked);
|
|
26
|
+
const lines = hints.filter((hint) => on.has(hint.label)).map((hint) => `- ${hint.text}`);
|
|
27
|
+
const head = text.trim();
|
|
28
|
+
if (lines.length === 0) return head;
|
|
29
|
+
return [head, "", "Consignes :", ...lines].join("\n").trim();
|
|
30
|
+
}
|
|
31
|
+
function groupHints(hints) {
|
|
32
|
+
const groups = /* @__PURE__ */ new Map();
|
|
33
|
+
const alone = [];
|
|
34
|
+
for (const hint of hints) {
|
|
35
|
+
if (!hint.group) {
|
|
36
|
+
alone.push(hint);
|
|
37
|
+
continue;
|
|
38
|
+
}
|
|
39
|
+
if (!groups.has(hint.group)) groups.set(hint.group, { name: hint.group, hints: [] });
|
|
40
|
+
groups.get(hint.group).hints.push(hint);
|
|
41
|
+
}
|
|
42
|
+
return { groups: [...groups.values()], alone };
|
|
43
|
+
}
|
|
44
|
+
function layoutFields(fields) {
|
|
45
|
+
const title = fields.find((field) => field.kind === "text" && !field.html) ?? null;
|
|
46
|
+
const page = fields.filter((field) => field.html);
|
|
47
|
+
const sheet = fields.filter((field) => field !== title && !field.html);
|
|
48
|
+
return { title, page, sheet };
|
|
49
|
+
}
|
|
50
|
+
function planOf(answer, titleIndex = 0) {
|
|
51
|
+
const titles = Array.isArray(answer == null ? void 0 : answer.titles) ? answer.titles : [];
|
|
52
|
+
return {
|
|
53
|
+
titles,
|
|
54
|
+
title: titles[titleIndex] ?? titles[0] ?? "",
|
|
55
|
+
sections: ((answer == null ? void 0 : answer.sections) ?? []).map((section) => ({ title: section.title, summary: section.summary ?? "" }))
|
|
56
|
+
};
|
|
57
|
+
}
|
|
58
|
+
function planToSend(plan) {
|
|
59
|
+
return {
|
|
60
|
+
title: plan.title ?? "",
|
|
61
|
+
sections: plan.sections.filter((section) => section.title.trim()).map((section) => ({ title: section.title.trim(), summary: section.summary.trim() }))
|
|
62
|
+
};
|
|
63
|
+
}
|
|
64
|
+
function moveSection(plan, index, delta) {
|
|
65
|
+
const to = index + delta;
|
|
66
|
+
if (to < 0 || to >= plan.sections.length) return plan;
|
|
67
|
+
const sections = [...plan.sections];
|
|
68
|
+
[sections[index], sections[to]] = [sections[to], sections[index]];
|
|
69
|
+
return { ...plan, sections };
|
|
70
|
+
}
|
|
71
|
+
function removeSection(plan, index) {
|
|
72
|
+
return { ...plan, sections: plan.sections.filter((_, i) => i !== index) };
|
|
73
|
+
}
|
|
74
|
+
function addSection(plan) {
|
|
75
|
+
return { ...plan, sections: [...plan.sections, { title: "", summary: "" }] };
|
|
76
|
+
}
|
|
77
|
+
function renameSection(plan, index, changes) {
|
|
78
|
+
return { ...plan, sections: plan.sections.map((section, i) => i === index ? { ...section, ...changes } : section) };
|
|
79
|
+
}
|
|
80
|
+
export {
|
|
81
|
+
WORK_WORDS,
|
|
82
|
+
addSection,
|
|
83
|
+
firstRequest,
|
|
84
|
+
groupHints,
|
|
85
|
+
hasValue,
|
|
86
|
+
isEmptyValues,
|
|
87
|
+
layoutFields,
|
|
88
|
+
mergeVersion,
|
|
89
|
+
moveSection,
|
|
90
|
+
planOf,
|
|
91
|
+
planToSend,
|
|
92
|
+
removeSection,
|
|
93
|
+
renameSection
|
|
94
|
+
};
|
|
@@ -0,0 +1,240 @@
|
|
|
1
|
+
const DIFF_LIMIT = 25e4;
|
|
2
|
+
const BLOCK_TAGS = "p|h[1-6]|ul|ol|blockquote|pre|figure|div|table";
|
|
3
|
+
const OPEN_RE = new RegExp(`<(${BLOCK_TAGS}|hr)\\b[^>]*>`, "gi");
|
|
4
|
+
const HAS_BLOCK = new RegExp(`<(${BLOCK_TAGS}|li)\\b`, "i");
|
|
5
|
+
const MARKER = /^\s*\[\[bloc:[^\]\s]+\]\]\s*$/;
|
|
6
|
+
const ENTITIES = { amp: "&", lt: "<", gt: ">", quot: '"', apos: "'", nbsp: "\xA0", "#39": "'" };
|
|
7
|
+
function decode(text) {
|
|
8
|
+
return text.replace(/&(#\d+|#x[0-9a-f]+|[a-z]+\d*);/gi, (all, name) => {
|
|
9
|
+
if (name[0] === "#") {
|
|
10
|
+
const code = name[1] === "x" || name[1] === "X" ? parseInt(name.slice(2), 16) : parseInt(name.slice(1), 10);
|
|
11
|
+
return Number.isFinite(code) ? String.fromCodePoint(code) : all;
|
|
12
|
+
}
|
|
13
|
+
return ENTITIES[name.toLowerCase()] ?? all;
|
|
14
|
+
});
|
|
15
|
+
}
|
|
16
|
+
function htmlText(html) {
|
|
17
|
+
return decode(html.replace(/<\/(li|p|h[1-6])>/gi, "\n").replace(/<br\s*\/?>/gi, "\n").replace(/<[^>]+>/g, "")).replace(/[ \t]+\n/g, "\n").replace(/\n{2,}/g, "\n").trim();
|
|
18
|
+
}
|
|
19
|
+
function isHtml(value) {
|
|
20
|
+
return typeof value === "string" && HAS_BLOCK.test(value);
|
|
21
|
+
}
|
|
22
|
+
function htmlBlocks(html) {
|
|
23
|
+
const source = String(html ?? "");
|
|
24
|
+
const out = [];
|
|
25
|
+
const push = (piece, tag) => {
|
|
26
|
+
const text = htmlText(piece);
|
|
27
|
+
if (!text && tag !== "hr" && tag !== "figure") return;
|
|
28
|
+
out.push({ html: piece.trim(), tag, text, marker: MARKER.test(text) });
|
|
29
|
+
};
|
|
30
|
+
let last = 0;
|
|
31
|
+
OPEN_RE.lastIndex = 0;
|
|
32
|
+
let open;
|
|
33
|
+
while ((open = OPEN_RE.exec(source)) !== null) {
|
|
34
|
+
const tag = open[1].toLowerCase();
|
|
35
|
+
const between = source.slice(last, open.index);
|
|
36
|
+
if (between.trim()) push(`<p>${between.trim()}</p>`, "p");
|
|
37
|
+
const end = tag === "hr" ? OPEN_RE.lastIndex : closingEnd(source, tag, OPEN_RE.lastIndex);
|
|
38
|
+
push(source.slice(open.index, end), tag);
|
|
39
|
+
last = end;
|
|
40
|
+
OPEN_RE.lastIndex = end;
|
|
41
|
+
}
|
|
42
|
+
const rest = source.slice(last);
|
|
43
|
+
if (rest.trim()) push(`<p>${rest.trim()}</p>`, "p");
|
|
44
|
+
return out;
|
|
45
|
+
}
|
|
46
|
+
function closingEnd(source, tag, from) {
|
|
47
|
+
const re = new RegExp(`<(/?)${tag}\\b[^>]*>`, "gi");
|
|
48
|
+
re.lastIndex = from;
|
|
49
|
+
let depth = 1;
|
|
50
|
+
let match;
|
|
51
|
+
while ((match = re.exec(source)) !== null) {
|
|
52
|
+
depth += match[1] ? -1 : 1;
|
|
53
|
+
if (depth === 0) return re.lastIndex;
|
|
54
|
+
}
|
|
55
|
+
return source.length;
|
|
56
|
+
}
|
|
57
|
+
function tokens(text) {
|
|
58
|
+
return text.split(/(\s+)/).filter((token) => token !== "");
|
|
59
|
+
}
|
|
60
|
+
function lcsTable(a, b, same) {
|
|
61
|
+
const table = Array.from({ length: a.length + 1 }, () => new Uint32Array(b.length + 1));
|
|
62
|
+
for (let i = a.length - 1; i >= 0; i -= 1) {
|
|
63
|
+
for (let j = b.length - 1; j >= 0; j -= 1) {
|
|
64
|
+
table[i][j] = same(a[i], b[j]) ? table[i + 1][j + 1] + 1 : Math.max(table[i + 1][j], table[i][j + 1]);
|
|
65
|
+
}
|
|
66
|
+
}
|
|
67
|
+
return table;
|
|
68
|
+
}
|
|
69
|
+
function compact(segments) {
|
|
70
|
+
const out = [];
|
|
71
|
+
for (const [kind, text] of segments) {
|
|
72
|
+
const last = out[out.length - 1];
|
|
73
|
+
if (last && last[0] === kind) last[1] += text;
|
|
74
|
+
else out.push([kind, text]);
|
|
75
|
+
}
|
|
76
|
+
return out;
|
|
77
|
+
}
|
|
78
|
+
function wordDiff(before, after) {
|
|
79
|
+
const a = tokens(before ?? "");
|
|
80
|
+
const b = tokens(after ?? "");
|
|
81
|
+
if (a.length * b.length > DIFF_LIMIT) return compact([["del", before ?? ""], ["add", after ?? ""]].filter(([, t]) => t));
|
|
82
|
+
const table = lcsTable(a, b, (x, y) => x === y);
|
|
83
|
+
const out = [];
|
|
84
|
+
let i = 0;
|
|
85
|
+
let j = 0;
|
|
86
|
+
while (i < a.length && j < b.length) {
|
|
87
|
+
if (a[i] === b[j]) {
|
|
88
|
+
out.push(["same", a[i]]);
|
|
89
|
+
i += 1;
|
|
90
|
+
j += 1;
|
|
91
|
+
} else if (table[i + 1][j] >= table[i][j + 1]) {
|
|
92
|
+
out.push(["del", a[i]]);
|
|
93
|
+
i += 1;
|
|
94
|
+
} else {
|
|
95
|
+
out.push(["add", b[j]]);
|
|
96
|
+
j += 1;
|
|
97
|
+
}
|
|
98
|
+
}
|
|
99
|
+
while (i < a.length) out.push(["del", a[i++]]);
|
|
100
|
+
while (j < b.length) out.push(["add", b[j++]]);
|
|
101
|
+
return regions(out);
|
|
102
|
+
}
|
|
103
|
+
function regions(raw) {
|
|
104
|
+
const out = [];
|
|
105
|
+
let i = 0;
|
|
106
|
+
while (i < raw.length) {
|
|
107
|
+
if (raw[i][0] === "same") {
|
|
108
|
+
out.push(raw[i]);
|
|
109
|
+
i += 1;
|
|
110
|
+
continue;
|
|
111
|
+
}
|
|
112
|
+
let end = i;
|
|
113
|
+
for (let k = i; k < raw.length; k += 1) {
|
|
114
|
+
const [kind, text] = raw[k];
|
|
115
|
+
if (kind !== "same") {
|
|
116
|
+
end = k;
|
|
117
|
+
continue;
|
|
118
|
+
}
|
|
119
|
+
if (text.trim()) break;
|
|
120
|
+
}
|
|
121
|
+
let del = "";
|
|
122
|
+
let add = "";
|
|
123
|
+
for (let k = i; k <= end; k += 1) {
|
|
124
|
+
const [kind, text] = raw[k];
|
|
125
|
+
if (kind !== "add") del += text;
|
|
126
|
+
if (kind !== "del") add += text;
|
|
127
|
+
}
|
|
128
|
+
if (del) out.push(["del", del]);
|
|
129
|
+
if (add) out.push(["add", add]);
|
|
130
|
+
i = end + 1;
|
|
131
|
+
}
|
|
132
|
+
return compact(out);
|
|
133
|
+
}
|
|
134
|
+
function alignBlocks(before, after) {
|
|
135
|
+
const table = lcsTable(before, after, (x, y) => x.text === y.text);
|
|
136
|
+
const pairs = [];
|
|
137
|
+
let i = 0;
|
|
138
|
+
let j = 0;
|
|
139
|
+
const gapBefore = [];
|
|
140
|
+
const gapAfter = [];
|
|
141
|
+
const flush = () => {
|
|
142
|
+
const count = Math.max(gapBefore.length, gapAfter.length);
|
|
143
|
+
for (let k = 0; k < count; k += 1) {
|
|
144
|
+
const b = gapBefore[k] ?? null;
|
|
145
|
+
const a = gapAfter[k] ?? null;
|
|
146
|
+
pairs.push({ kind: b && a ? "change" : b ? "del" : "add", before: b, after: a });
|
|
147
|
+
}
|
|
148
|
+
gapBefore.length = 0;
|
|
149
|
+
gapAfter.length = 0;
|
|
150
|
+
};
|
|
151
|
+
while (i < before.length && j < after.length) {
|
|
152
|
+
if (before[i].text === after[j].text) {
|
|
153
|
+
flush();
|
|
154
|
+
pairs.push({ kind: "same", before: before[i], after: after[j] });
|
|
155
|
+
i += 1;
|
|
156
|
+
j += 1;
|
|
157
|
+
} else if (table[i + 1][j] >= table[i][j + 1]) {
|
|
158
|
+
gapBefore.push(before[i++]);
|
|
159
|
+
} else {
|
|
160
|
+
gapAfter.push(after[j++]);
|
|
161
|
+
}
|
|
162
|
+
}
|
|
163
|
+
while (i < before.length) gapBefore.push(before[i++]);
|
|
164
|
+
while (j < after.length) gapAfter.push(after[j++]);
|
|
165
|
+
flush();
|
|
166
|
+
return pairs;
|
|
167
|
+
}
|
|
168
|
+
function fieldsOf(output) {
|
|
169
|
+
return Object.entries((output == null ? void 0 : output.properties) ?? {}).map(([key, node]) => {
|
|
170
|
+
var _a;
|
|
171
|
+
const types = [node.type].flat();
|
|
172
|
+
const kind = types.includes("array") ? "list" : node.enum ? "choice" : "text";
|
|
173
|
+
const options = kind === "list" ? ((_a = node.items) == null ? void 0 : _a.enum) ?? null : node.enum ?? null;
|
|
174
|
+
return { key, label: node.title ?? key, kind, html: node.format === "html", options, maxLength: node.maxLength ?? null };
|
|
175
|
+
});
|
|
176
|
+
}
|
|
177
|
+
function sameValue(a, b) {
|
|
178
|
+
return JSON.stringify(a ?? null) === JSON.stringify(b ?? null);
|
|
179
|
+
}
|
|
180
|
+
function listText(value) {
|
|
181
|
+
if (Array.isArray(value)) return value.join(", ");
|
|
182
|
+
return value === null || value === void 0 ? "" : String(value);
|
|
183
|
+
}
|
|
184
|
+
function compareField(field, now, next) {
|
|
185
|
+
const unchanged = next === null || next === void 0 || sameValue(now, next);
|
|
186
|
+
if (field.kind === "text" && (isHtml(now) || isHtml(next))) {
|
|
187
|
+
const before = htmlBlocks(typeof now === "string" ? now : "");
|
|
188
|
+
const after = unchanged ? before : htmlBlocks(next);
|
|
189
|
+
const pieces = alignBlocks(before, after).map((pair, index) => {
|
|
190
|
+
const block = pair.after ?? pair.before;
|
|
191
|
+
const segments2 = pair.kind === "same" ? [["same", block.text]] : pair.kind === "add" ? [["add", pair.after.text]] : pair.kind === "del" ? [["del", pair.before.text]] : wordDiff(pair.before.text, pair.after.text);
|
|
192
|
+
return { id: `${field.key}.${index}`, kind: pair.kind, tag: block.tag, marker: block.marker, before: pair.before, after: pair.after, segments: segments2 };
|
|
193
|
+
});
|
|
194
|
+
return { html: true, changed: pieces.some((piece) => piece.kind !== "same"), pieces };
|
|
195
|
+
}
|
|
196
|
+
const text = field.kind === "text" ? now ?? "" : listText(now);
|
|
197
|
+
if (unchanged) {
|
|
198
|
+
return { html: false, changed: false, pieces: [{ id: field.key, kind: "same", segments: text ? [["same", text]] : [] }] };
|
|
199
|
+
}
|
|
200
|
+
const segments = field.kind === "text" ? wordDiff(String(now ?? ""), String(next)) : compact([["del", text], ["add", listText(next)]].filter(([, t]) => t));
|
|
201
|
+
return { html: false, changed: true, pieces: [{ id: field.key, kind: "change", segments, value: next }] };
|
|
202
|
+
}
|
|
203
|
+
function mergeField(compared, kept) {
|
|
204
|
+
if (!compared.changed) return void 0;
|
|
205
|
+
const isKept = (piece) => kept[piece.id] !== false;
|
|
206
|
+
if (!compared.pieces.some((piece) => piece.kind !== "same" && isKept(piece))) return void 0;
|
|
207
|
+
if (!compared.html) return compared.pieces[0].value;
|
|
208
|
+
return compared.pieces.map((piece) => {
|
|
209
|
+
if (piece.kind === "same") return piece.before.html;
|
|
210
|
+
if (piece.kind === "change") return isKept(piece) ? piece.after.html : piece.before.html;
|
|
211
|
+
if (piece.kind === "add") return isKept(piece) ? piece.after.html : "";
|
|
212
|
+
return isKept(piece) ? "" : piece.before.html;
|
|
213
|
+
}).filter(Boolean).join("\n");
|
|
214
|
+
}
|
|
215
|
+
function compareVersion(fields, current, version, kept) {
|
|
216
|
+
const compared = {};
|
|
217
|
+
const changes = [];
|
|
218
|
+
const merged = {};
|
|
219
|
+
for (const field of fields) {
|
|
220
|
+
const result = compareField(field, current == null ? void 0 : current[field.key], version == null ? void 0 : version[field.key]);
|
|
221
|
+
compared[field.key] = result;
|
|
222
|
+
for (const piece of result.pieces) {
|
|
223
|
+
if (piece.kind !== "same") changes.push({ ...piece, field });
|
|
224
|
+
}
|
|
225
|
+
const value = mergeField(result, kept);
|
|
226
|
+
if (value !== void 0) merged[field.key] = value;
|
|
227
|
+
}
|
|
228
|
+
return { compared, changes, merged };
|
|
229
|
+
}
|
|
230
|
+
export {
|
|
231
|
+
alignBlocks,
|
|
232
|
+
compareField,
|
|
233
|
+
compareVersion,
|
|
234
|
+
fieldsOf,
|
|
235
|
+
htmlBlocks,
|
|
236
|
+
htmlText,
|
|
237
|
+
isHtml,
|
|
238
|
+
mergeField,
|
|
239
|
+
wordDiff
|
|
240
|
+
};
|
|
@@ -0,0 +1,14 @@
|
|
|
1
|
+
import { AI_PERMISSIONS } from "../permissions.js";
|
|
2
|
+
const AI_WIDGETS = Object.freeze({
|
|
3
|
+
USAGE: Object.freeze({
|
|
4
|
+
id: "ai-usage",
|
|
5
|
+
title: "IA",
|
|
6
|
+
description: "Les jetons consomm\xE9s ce mois-ci et les appels en \xE9chec.",
|
|
7
|
+
permission: AI_PERMISSIONS.VIEW,
|
|
8
|
+
size: { default: 1, min: 1, max: 2 },
|
|
9
|
+
order: 80
|
|
10
|
+
})
|
|
11
|
+
});
|
|
12
|
+
export {
|
|
13
|
+
AI_WIDGETS
|
|
14
|
+
};
|
package/dist/admin.js
ADDED
|
@@ -0,0 +1,9 @@
|
|
|
1
|
+
"use client";
|
|
2
|
+
import { registerPage } from "@fraza/core/features/admin";
|
|
3
|
+
import { provideIntegration } from "@fraza/core/integrations";
|
|
4
|
+
import AiActions from "./admin/AiActions.client.js";
|
|
5
|
+
import AiResults from "./admin/AiResults.client.js";
|
|
6
|
+
import "./admin/AiUsageWidget.client.js";
|
|
7
|
+
import { SOURCE } from "./source.js";
|
|
8
|
+
registerPage({ slug: "ai", load: () => import("./admin/AiPage.client.js") }, { source: SOURCE });
|
|
9
|
+
provideIntegration("ai", { AiActions, AiResults }, { source: SOURCE });
|
|
@@ -0,0 +1,155 @@
|
|
|
1
|
+
import { apiError, apiSuccess, defineApiRoutes, readJsonObject } from "@fraza/core/api";
|
|
2
|
+
import { parsePagination } from "@fraza/core/database/sql";
|
|
3
|
+
import { fail } from "@fraza/core/shared/logger";
|
|
4
|
+
import { hasPermission } from "@fraza/core/users";
|
|
5
|
+
import { describeAiConfig } from "./config.server.js";
|
|
6
|
+
import { AI_PERMISSIONS } from "./permissions.js";
|
|
7
|
+
import { listPrompts, listUnmatchedCustomizations, promptsForTarget, resolvePrompt } from "./prompts.server.js";
|
|
8
|
+
import { dismissResult, getResults } from "./results.server.js";
|
|
9
|
+
import { outputSchemaOf, runPrompt } from "./run.server.js";
|
|
10
|
+
import { getUsage, listRuns } from "./runs.server.js";
|
|
11
|
+
import { readTurn } from "./studio.js";
|
|
12
|
+
import { TEST_PROMPT } from "./test-prompt.server.js";
|
|
13
|
+
const RUN_RATE_LIMIT = "ai_run";
|
|
14
|
+
const NAME_PATTERN = /^[a-z][a-z0-9-]*(\.[a-z][a-z0-9-]*)*$/;
|
|
15
|
+
const RUNS_PAGE = { defaultLimit: 50, max: 200 };
|
|
16
|
+
const USAGE_DAYS = { default: 30, max: 366 };
|
|
17
|
+
const DAY_MS = 24 * 60 * 60 * 1e3;
|
|
18
|
+
const FAILURE_STATUS = {
|
|
19
|
+
unknown_prompt: 404,
|
|
20
|
+
invalid_input: 400,
|
|
21
|
+
not_applicable: 400,
|
|
22
|
+
not_configured: 503,
|
|
23
|
+
prompt_error: 500,
|
|
24
|
+
rate_limited: 429,
|
|
25
|
+
cancelled: 400
|
|
26
|
+
};
|
|
27
|
+
function readScope(url) {
|
|
28
|
+
const raw = url.searchParams.get("scope");
|
|
29
|
+
if (!raw) return {};
|
|
30
|
+
try {
|
|
31
|
+
const scope = JSON.parse(raw);
|
|
32
|
+
return scope && typeof scope === "object" && !Array.isArray(scope) ? scope : null;
|
|
33
|
+
} catch {
|
|
34
|
+
return null;
|
|
35
|
+
}
|
|
36
|
+
}
|
|
37
|
+
async function mayRun(prompt, userId) {
|
|
38
|
+
return !prompt.permission || hasPermission(userId, prompt.permission);
|
|
39
|
+
}
|
|
40
|
+
async function handleActions(request, params, context) {
|
|
41
|
+
const url = new URL(request.url);
|
|
42
|
+
const target = url.searchParams.get("target") ?? "";
|
|
43
|
+
if (!NAME_PATTERN.test(target)) return apiError("Bad Request", "\xC9cran inconnu.");
|
|
44
|
+
const scope = readScope(url);
|
|
45
|
+
if (scope === null) return apiError("Bad Request", "Param\xE8tre scope invalide.");
|
|
46
|
+
const userId = context.session.user.id;
|
|
47
|
+
const actions = [];
|
|
48
|
+
for (const prompt of promptsForTarget(target)) {
|
|
49
|
+
if (!await mayRun(prompt, userId)) continue;
|
|
50
|
+
let output;
|
|
51
|
+
try {
|
|
52
|
+
output = await outputSchemaOf(prompt, scope);
|
|
53
|
+
} catch (err) {
|
|
54
|
+
fail(`AI: the output schema of ${prompt.name} could not be built`, err);
|
|
55
|
+
continue;
|
|
56
|
+
}
|
|
57
|
+
if (output === null) continue;
|
|
58
|
+
actions.push({
|
|
59
|
+
name: prompt.name,
|
|
60
|
+
label: prompt.label,
|
|
61
|
+
heading: prompt.heading ?? null,
|
|
62
|
+
description: prompt.description ?? null,
|
|
63
|
+
mode: prompt.mode,
|
|
64
|
+
input: prompt.input ?? null,
|
|
65
|
+
output,
|
|
66
|
+
suggestions: prompt.suggestions ?? [],
|
|
67
|
+
hints: prompt.hints ?? []
|
|
68
|
+
});
|
|
69
|
+
}
|
|
70
|
+
return apiSuccess({ actions, configured: describeAiConfig().configured });
|
|
71
|
+
}
|
|
72
|
+
function failureResponse(error) {
|
|
73
|
+
const status = FAILURE_STATUS[error.code] ?? 502;
|
|
74
|
+
return apiError(status, error.message, { code: error.code });
|
|
75
|
+
}
|
|
76
|
+
async function handleRun(request, params, context) {
|
|
77
|
+
const body = await readJsonObject(request);
|
|
78
|
+
const prompt = typeof body.prompt === "string" ? resolvePrompt(body.prompt) : null;
|
|
79
|
+
if (!prompt || !prompt.target) return apiError("Not Found", "Ce prompt n'existe pas ou a \xE9t\xE9 d\xE9sactiv\xE9.");
|
|
80
|
+
const userId = context.session.user.id;
|
|
81
|
+
if (!await mayRun(prompt, userId)) return apiError("Forbidden", "Ce prompt demande une permission que ce compte n'a pas.");
|
|
82
|
+
const scope = body.scope ?? {};
|
|
83
|
+
if (typeof scope !== "object" || Array.isArray(scope)) return apiError("Bad Request", "Param\xE8tre scope invalide.");
|
|
84
|
+
const turn = prompt.mode === "studio" ? readTurn(body.turn ?? {}) : void 0;
|
|
85
|
+
if (turn === null) return apiError("Bad Request", "Param\xE8tre turn invalide.");
|
|
86
|
+
const result = await runPrompt(prompt.name, {
|
|
87
|
+
input: body.input ?? null,
|
|
88
|
+
context: body.context ?? null,
|
|
89
|
+
scope,
|
|
90
|
+
turn,
|
|
91
|
+
userId,
|
|
92
|
+
trigger: "button",
|
|
93
|
+
signal: request.signal
|
|
94
|
+
});
|
|
95
|
+
if (!result.success) return failureResponse(result.error);
|
|
96
|
+
return apiSuccess({ success: true, data: result.data, model: result.model });
|
|
97
|
+
}
|
|
98
|
+
async function handleResults(request) {
|
|
99
|
+
const url = new URL(request.url);
|
|
100
|
+
const target = url.searchParams.get("target") ?? "";
|
|
101
|
+
const id = url.searchParams.get("id") ?? "";
|
|
102
|
+
if (!NAME_PATTERN.test(target) || id === "" || id.length > 120) return apiError("Bad Request", "Contenu inconnu.");
|
|
103
|
+
return apiSuccess({ results: await getResults(target, id) });
|
|
104
|
+
}
|
|
105
|
+
async function handleDismiss(request, params) {
|
|
106
|
+
if (!await dismissResult(params.id)) return apiError("Not Found", "Cette proposition n'existe plus.");
|
|
107
|
+
return apiSuccess({ success: true });
|
|
108
|
+
}
|
|
109
|
+
async function handleStatus() {
|
|
110
|
+
return apiSuccess({
|
|
111
|
+
config: describeAiConfig(),
|
|
112
|
+
prompts: listPrompts().filter((prompt) => prompt.name !== TEST_PROMPT),
|
|
113
|
+
unmatchedCustomizations: listUnmatchedCustomizations()
|
|
114
|
+
});
|
|
115
|
+
}
|
|
116
|
+
async function handleTest(request, params, context) {
|
|
117
|
+
const result = await runPrompt(TEST_PROMPT, { userId: context.session.user.id, trigger: "test", signal: request.signal });
|
|
118
|
+
if (!result.success) return failureResponse(result.error);
|
|
119
|
+
return apiSuccess({ success: true, model: result.model, usage: result.usage, data: result.data });
|
|
120
|
+
}
|
|
121
|
+
async function handleRuns(request) {
|
|
122
|
+
const url = new URL(request.url);
|
|
123
|
+
const status = url.searchParams.get("status") || null;
|
|
124
|
+
if (status && status !== "success" && status !== "error") return apiError("Bad Request", "Statut invalide.");
|
|
125
|
+
const { page, limit, offset } = parsePagination(url, RUNS_PAGE);
|
|
126
|
+
const { runs, total } = await listRuns({ limit, offset, status });
|
|
127
|
+
return apiSuccess({ runs, pagination: { page, limit, total } });
|
|
128
|
+
}
|
|
129
|
+
async function handleUsage(request) {
|
|
130
|
+
const raw = new URL(request.url).searchParams.get("days");
|
|
131
|
+
const days = raw === null ? USAGE_DAYS.default : Number(raw);
|
|
132
|
+
if (!Number.isInteger(days) || days < 1 || days > USAGE_DAYS.max) {
|
|
133
|
+
return apiError("Bad Request", `La p\xE9riode doit compter de 1 \xE0 ${USAGE_DAYS.max} jours.`);
|
|
134
|
+
}
|
|
135
|
+
const to = /* @__PURE__ */ new Date();
|
|
136
|
+
const from = new Date(to.getTime() - days * DAY_MS);
|
|
137
|
+
return apiSuccess({ usage: await getUsage({ from, to }) });
|
|
138
|
+
}
|
|
139
|
+
const use = { auth: "admin", permission: AI_PERMISSIONS.USE };
|
|
140
|
+
const view = { auth: "admin", permission: AI_PERMISSIONS.VIEW };
|
|
141
|
+
const routes = defineApiRoutes([
|
|
142
|
+
{ path: "/ai/actions", method: "GET", handler: handleActions, ...use },
|
|
143
|
+
// Each run costs tokens: its own budget, counted per account.
|
|
144
|
+
{ path: "/ai/run", method: "POST", handler: handleRun, ...use, rateLimit: RUN_RATE_LIMIT },
|
|
145
|
+
{ path: "/ai/results", method: "GET", handler: handleResults, ...use },
|
|
146
|
+
{ path: "/ai/results/:id/dismiss", method: "POST", handler: handleDismiss, ...use },
|
|
147
|
+
{ path: "/ai/status", method: "GET", handler: handleStatus, ...view },
|
|
148
|
+
{ path: "/ai/test", method: "POST", handler: handleTest, ...view, rateLimit: RUN_RATE_LIMIT },
|
|
149
|
+
{ path: "/ai/runs", method: "GET", handler: handleRuns, ...view },
|
|
150
|
+
{ path: "/ai/usage", method: "GET", handler: handleUsage, ...view }
|
|
151
|
+
]);
|
|
152
|
+
export {
|
|
153
|
+
RUN_RATE_LIMIT,
|
|
154
|
+
routes
|
|
155
|
+
};
|