@mvriu5/payload-ai 1.1.1 → 1.3.2
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/README.md +42 -0
- package/dist/components/Icons.d.ts +0 -8
- package/dist/components/Icons.js +4 -216
- package/dist/components/{ActionToast.d.ts → action-toast/ActionToast.d.ts} +2 -2
- package/dist/components/{ActionToast.js → action-toast/ActionToast.js} +27 -35
- package/dist/components/{ActionToast.module.css → action-toast/ActionToast.module.css} +0 -79
- package/dist/components/ai-input/AIInput.js +433 -0
- package/dist/components/{AIInput.module.css → ai-input/AIInput.module.css} +119 -42
- package/dist/components/ai-input/badge.d.ts +14 -0
- package/dist/components/ai-input/badge.js +85 -0
- package/dist/components/{AuditLogList.d.ts → audit-log-list/AuditLogList.d.ts} +4 -8
- package/dist/components/{AuditLogList.js → audit-log-list/AuditLogList.js} +49 -21
- package/dist/components/{AuditLogList.module.css → audit-log-list/AuditLogList.module.css} +11 -65
- package/dist/components/dashboard/Dashboard.d.ts +2 -0
- package/dist/components/dashboard/Dashboard.js +15 -0
- package/dist/components/dashboard/Dashboard.module.css +13 -0
- package/dist/components/{DiffDialog.d.ts → diff-dialog/DiffDialog.d.ts} +2 -2
- package/dist/components/{DiffDialog.js → diff-dialog/DiffDialog.js} +200 -189
- package/dist/components/{DiffDialog.module.css → diff-dialog/DiffDialog.module.css} +17 -35
- package/dist/components/hooks/useAIChatStream.d.ts +39 -0
- package/dist/components/hooks/useAIChatStream.js +217 -0
- package/dist/components/hooks/useAISettings.js +26 -25
- package/dist/components/hooks/useAuditLog.d.ts +11 -0
- package/dist/components/hooks/useAuditLog.js +41 -0
- package/dist/components/hooks/useDocumentMentionSuggestions.d.ts +1 -1
- package/dist/components/hooks/useDocumentMentionSuggestions.js +31 -27
- package/dist/components/hooks/useMentions.d.ts +58 -0
- package/dist/components/hooks/useMentions.js +276 -0
- package/dist/components/hooks/usePluginConfig.d.ts +52 -0
- package/dist/components/hooks/usePluginConfig.js +20 -0
- package/dist/components/{MentionPopover.d.ts → mention-popover/MentionPopover.d.ts} +2 -4
- package/dist/components/{MentionPopover.js → mention-popover/MentionPopover.js} +1 -8
- package/dist/components/{MentionPopover.module.css → mention-popover/MentionPopover.module.css} +1 -1
- package/dist/exports/client.d.ts +2 -2
- package/dist/exports/client.js +2 -2
- package/dist/handlers/applyActionHandler.js +341 -112
- package/dist/handlers/chatHandler.js +940 -84
- package/dist/handlers/mediaUploadHandler.d.ts +7 -0
- package/dist/handlers/mediaUploadHandler.js +99 -0
- package/dist/handlers/mentionSuggestionHandler.js +16 -14
- package/dist/handlers/proposalDiffHandler.js +117 -50
- package/dist/index.d.ts +6 -0
- package/dist/index.js +39 -7
- package/dist/payload/collectionPermissions.js +3 -1
- package/dist/payload/logging.d.ts +9 -0
- package/dist/payload/logging.js +14 -0
- package/dist/payload/normalizeData.d.ts +16 -12
- package/dist/payload/normalizeData.js +47 -14
- package/dist/payload/proposalData.d.ts +31 -0
- package/dist/payload/proposalData.js +578 -0
- package/dist/payload/schemaContext.d.ts +3 -7
- package/dist/payload/schemaContext.js +147 -66
- package/package.json +6 -6
- package/dist/components/AIInput.js +0 -747
- /package/dist/components/{AIInput.d.ts → ai-input/AIInput.d.ts} +0 -0
|
@@ -1,747 +0,0 @@
|
|
|
1
|
-
"use client";
|
|
2
|
-
import { jsx as _jsx, jsxs as _jsxs } from "react/jsx-runtime";
|
|
3
|
-
import { useConfig } from "@payloadcms/ui";
|
|
4
|
-
import { formatAdminURL } from "payload/shared";
|
|
5
|
-
import { useCallback, useEffect, useMemo, useRef, useState } from "react";
|
|
6
|
-
import { getResolvedAIModelConfig } from "../ai/providerOptions.js";
|
|
7
|
-
import { getSerializableLabel, isInternalCollection } from "../payload/shared.js";
|
|
8
|
-
import { ActionToast } from "./ActionToast.js";
|
|
9
|
-
import styles from "./AIInput.module.css";
|
|
10
|
-
import { MentionPopover } from "./MentionPopover.js";
|
|
11
|
-
import { ClaudeIcon, GoogleGeminiIcon, MistralAiIcon, OpenaiIcon, OpenrouterIcon, Send } from "./Icons.js";
|
|
12
|
-
import { RecentChangesList } from "./AuditLogList.js";
|
|
13
|
-
import { useAISettings } from "./hooks/useAISettings.js";
|
|
14
|
-
import { useDocumentMentionSuggestions } from "./hooks/useDocumentMentionSuggestions.js";
|
|
15
|
-
const collectBlockOptions = ({ fields, parent })=>{
|
|
16
|
-
const options = [];
|
|
17
|
-
for (const field of fields){
|
|
18
|
-
if (field.type === "blocks" && field.blocks) {
|
|
19
|
-
for (const block of field.blocks){
|
|
20
|
-
options.push({
|
|
21
|
-
label: getSerializableLabel(block.labels?.singular, block.slug),
|
|
22
|
-
parent,
|
|
23
|
-
slug: block.slug,
|
|
24
|
-
type: "block"
|
|
25
|
-
});
|
|
26
|
-
options.push(...collectBlockOptions({
|
|
27
|
-
fields: block.fields || [],
|
|
28
|
-
parent: `${parent}/${block.slug}`
|
|
29
|
-
}));
|
|
30
|
-
}
|
|
31
|
-
}
|
|
32
|
-
if (field.fields) {
|
|
33
|
-
options.push(...collectBlockOptions({
|
|
34
|
-
fields: field.fields,
|
|
35
|
-
parent
|
|
36
|
-
}));
|
|
37
|
-
}
|
|
38
|
-
}
|
|
39
|
-
return options;
|
|
40
|
-
};
|
|
41
|
-
const getProviderIcon = (provider)=>{
|
|
42
|
-
const iconProps = {
|
|
43
|
-
"aria-hidden": true,
|
|
44
|
-
className: styles.selectProviderIcon
|
|
45
|
-
};
|
|
46
|
-
switch(provider){
|
|
47
|
-
case "claude":
|
|
48
|
-
return /*#__PURE__*/ _jsx(ClaudeIcon, {
|
|
49
|
-
...iconProps
|
|
50
|
-
});
|
|
51
|
-
case "google":
|
|
52
|
-
return /*#__PURE__*/ _jsx(GoogleGeminiIcon, {
|
|
53
|
-
...iconProps
|
|
54
|
-
});
|
|
55
|
-
case "mistral":
|
|
56
|
-
return /*#__PURE__*/ _jsx(MistralAiIcon, {
|
|
57
|
-
...iconProps
|
|
58
|
-
});
|
|
59
|
-
case "openai":
|
|
60
|
-
return /*#__PURE__*/ _jsx(OpenaiIcon, {
|
|
61
|
-
...iconProps
|
|
62
|
-
});
|
|
63
|
-
case "openrouter":
|
|
64
|
-
return /*#__PURE__*/ _jsx(OpenrouterIcon, {
|
|
65
|
-
...iconProps
|
|
66
|
-
});
|
|
67
|
-
default:
|
|
68
|
-
return null;
|
|
69
|
-
}
|
|
70
|
-
};
|
|
71
|
-
const parseSSEEvent = (chunk)=>{
|
|
72
|
-
const lines = chunk.split("\n");
|
|
73
|
-
let eventName = "";
|
|
74
|
-
const dataLines = [];
|
|
75
|
-
for (const line of lines){
|
|
76
|
-
if (line.startsWith("event:")) {
|
|
77
|
-
eventName = line.slice(6).trim();
|
|
78
|
-
continue;
|
|
79
|
-
}
|
|
80
|
-
if (line.startsWith("data:")) {
|
|
81
|
-
dataLines.push(line.slice(5).trim());
|
|
82
|
-
}
|
|
83
|
-
}
|
|
84
|
-
if (!eventName) return null;
|
|
85
|
-
try {
|
|
86
|
-
const data = JSON.parse(dataLines.join("\n"));
|
|
87
|
-
if (eventName !== "text" && eventName !== "proposals" && eventName !== "error" && eventName !== "done") {
|
|
88
|
-
return null;
|
|
89
|
-
}
|
|
90
|
-
return {
|
|
91
|
-
data,
|
|
92
|
-
event: eventName
|
|
93
|
-
};
|
|
94
|
-
} catch {
|
|
95
|
-
return null;
|
|
96
|
-
}
|
|
97
|
-
};
|
|
98
|
-
const sanitizeResponseText = (value)=>value.replace(/\*\*/g, "");
|
|
99
|
-
const isMentionBoundary = (character)=>{
|
|
100
|
-
return character === undefined || /\s/.test(character);
|
|
101
|
-
};
|
|
102
|
-
const getActiveMentionRange = (valueBeforeCaret)=>{
|
|
103
|
-
const caretPosition = valueBeforeCaret.length;
|
|
104
|
-
for(let index = valueBeforeCaret.length - 1; index >= 0; index -= 1){
|
|
105
|
-
const character = valueBeforeCaret[index];
|
|
106
|
-
if (character === "@") {
|
|
107
|
-
const previousCharacter = valueBeforeCaret[index - 1];
|
|
108
|
-
const query = valueBeforeCaret.slice(index + 1);
|
|
109
|
-
if (!isMentionBoundary(previousCharacter)) return null;
|
|
110
|
-
if (!/^[\w-]*$/.test(query)) return null;
|
|
111
|
-
return {
|
|
112
|
-
query,
|
|
113
|
-
range: {
|
|
114
|
-
end: caretPosition,
|
|
115
|
-
start: index
|
|
116
|
-
}
|
|
117
|
-
};
|
|
118
|
-
}
|
|
119
|
-
if (isMentionBoundary(character)) break;
|
|
120
|
-
}
|
|
121
|
-
return null;
|
|
122
|
-
};
|
|
123
|
-
const svgNamespace = "http://www.w3.org/2000/svg";
|
|
124
|
-
const auditLogCollectionSlug = "payload-ai-auditlog";
|
|
125
|
-
const appendSvgPath = (svg, d)=>{
|
|
126
|
-
const path = document.createElementNS(svgNamespace, "path");
|
|
127
|
-
path.setAttribute("d", d);
|
|
128
|
-
svg.append(path);
|
|
129
|
-
};
|
|
130
|
-
const createBadgeIcon = (type)=>{
|
|
131
|
-
if (type === "locale") return null;
|
|
132
|
-
const svg = document.createElementNS(svgNamespace, "svg");
|
|
133
|
-
svg.setAttribute("aria-hidden", "true");
|
|
134
|
-
svg.setAttribute("class", styles.badgeIcon);
|
|
135
|
-
svg.setAttribute("fill", "none");
|
|
136
|
-
svg.setAttribute("stroke", "currentColor");
|
|
137
|
-
svg.setAttribute("stroke-linecap", "round");
|
|
138
|
-
svg.setAttribute("stroke-linejoin", "round");
|
|
139
|
-
svg.setAttribute("stroke-width", "2");
|
|
140
|
-
svg.setAttribute("viewBox", "0 0 24 24");
|
|
141
|
-
if (type === "collection") {
|
|
142
|
-
appendSvgPath(svg, "M5 4h4l3 3h7a2 2 0 0 1 2 2v8a2 2 0 0 1 -2 2h-14a2 2 0 0 1 -2 -2v-11a2 2 0 0 1 2 -2");
|
|
143
|
-
}
|
|
144
|
-
if (type === "doc") {
|
|
145
|
-
appendSvgPath(svg, "M14 3v4a1 1 0 0 0 1 1h4");
|
|
146
|
-
appendSvgPath(svg, "M17 21h-10a2 2 0 0 1 -2 -2v-14a2 2 0 0 1 2 -2h7l5 5v11a2 2 0 0 1 -2 2");
|
|
147
|
-
}
|
|
148
|
-
if (type === "global") {
|
|
149
|
-
appendSvgPath(svg, "M3 12a9 9 0 1 0 18 0a9 9 0 0 0 -18 0");
|
|
150
|
-
appendSvgPath(svg, "M3.6 9h16.8");
|
|
151
|
-
appendSvgPath(svg, "M3.6 15h16.8");
|
|
152
|
-
appendSvgPath(svg, "M11.5 3a17 17 0 0 0 0 18");
|
|
153
|
-
appendSvgPath(svg, "M12.5 3a17 17 0 0 1 0 18");
|
|
154
|
-
}
|
|
155
|
-
if (type === "block") {
|
|
156
|
-
appendSvgPath(svg, "M14 4a1 1 0 0 1 1 -1h5a1 1 0 0 1 1 1v5a1 1 0 0 1 -1 1h-5a1 1 0 0 1 -1 -1l0 -5");
|
|
157
|
-
appendSvgPath(svg, "M3 14h12a2 2 0 0 1 2 2v3a2 2 0 0 1 -2 2h-10a2 2 0 0 1 -2 -2v-10a2 2 0 0 1 2 -2h3a2 2 0 0 1 2 2v12");
|
|
158
|
-
}
|
|
159
|
-
return svg;
|
|
160
|
-
};
|
|
161
|
-
const createBadgePrefix = (suggestion)=>{
|
|
162
|
-
const prefix = document.createElement("span");
|
|
163
|
-
const icon = createBadgeIcon(suggestion.type);
|
|
164
|
-
prefix.className = styles.prefix;
|
|
165
|
-
if (icon) {
|
|
166
|
-
prefix.append(icon);
|
|
167
|
-
}
|
|
168
|
-
if (suggestion.type === "doc") {
|
|
169
|
-
prefix.append(document.createTextNode(`${suggestion.collection || "document"}:`));
|
|
170
|
-
} else if (suggestion.type === "locale") {
|
|
171
|
-
prefix.textContent = "locale:";
|
|
172
|
-
}
|
|
173
|
-
return prefix;
|
|
174
|
-
};
|
|
175
|
-
const AIInput = ()=>{
|
|
176
|
-
const { config } = useConfig();
|
|
177
|
-
const editorRef = useRef(null);
|
|
178
|
-
const mentionPopoverRef = useRef(null);
|
|
179
|
-
const [prompt, setPrompt] = useState("");
|
|
180
|
-
const configuredModels = config.admin?.custom?.payloadAiPlugin?.models;
|
|
181
|
-
const aiModelConfig = useMemo(()=>getResolvedAIModelConfig(configuredModels), [
|
|
182
|
-
configuredModels
|
|
183
|
-
]);
|
|
184
|
-
const { selectedModel, setSelectedModel, settingsProvider } = useAISettings({
|
|
185
|
-
adminUserSlug: config.admin?.user,
|
|
186
|
-
apiRoute: config.routes.api,
|
|
187
|
-
defaultModels: aiModelConfig.defaults
|
|
188
|
-
});
|
|
189
|
-
const [mentionQuery, setMentionQuery] = useState("");
|
|
190
|
-
const [mentionRange, setMentionRange] = useState(null);
|
|
191
|
-
const [mentionPopoverPosition, setMentionPopoverPosition] = useState(null);
|
|
192
|
-
const [mentions, setMentions] = useState([]);
|
|
193
|
-
const [appliedProposalIndexes, setAppliedProposalIndexes] = useState([]);
|
|
194
|
-
const [response, setResponse] = useState("");
|
|
195
|
-
const [tokenUsage, setTokenUsage] = useState(null);
|
|
196
|
-
const [error, setError] = useState("");
|
|
197
|
-
const [proposals, setProposals] = useState([]);
|
|
198
|
-
const [appliedChanges, setAppliedChanges] = useState([]);
|
|
199
|
-
const [isLoading, setIsLoading] = useState(false);
|
|
200
|
-
const [isApplying, setIsApplying] = useState(false);
|
|
201
|
-
const enabledCollectionSlugs = config.admin?.custom?.payloadAiPlugin?.collectionSlugs;
|
|
202
|
-
const enabledCollectionSlugSet = useMemo(()=>enabledCollectionSlugs ? new Set(enabledCollectionSlugs) : null, [
|
|
203
|
-
enabledCollectionSlugs
|
|
204
|
-
]);
|
|
205
|
-
const isCollectionMentionEnabled = (slug)=>!enabledCollectionSlugSet || enabledCollectionSlugSet.has(slug);
|
|
206
|
-
const recentChangesEndpoint = useMemo(()=>formatAdminURL({
|
|
207
|
-
apiRoute: config.routes.api,
|
|
208
|
-
path: "/ai-audit-log"
|
|
209
|
-
}), [
|
|
210
|
-
config.routes.api
|
|
211
|
-
]);
|
|
212
|
-
const allChangesURL = useMemo(()=>`${config.routes.admin || "/admin"}/collections/${auditLogCollectionSlug}`, [
|
|
213
|
-
config.routes.admin
|
|
214
|
-
]);
|
|
215
|
-
const loadRecentChanges = useCallback(async ()=>{
|
|
216
|
-
const res = await fetch(recentChangesEndpoint);
|
|
217
|
-
const result = await res.json().catch(()=>null);
|
|
218
|
-
if (res.ok && result?.changes) {
|
|
219
|
-
setAppliedChanges(result.changes.slice(0, 10));
|
|
220
|
-
}
|
|
221
|
-
}, [
|
|
222
|
-
recentChangesEndpoint
|
|
223
|
-
]);
|
|
224
|
-
useEffect(()=>{
|
|
225
|
-
void loadRecentChanges().catch(()=>undefined);
|
|
226
|
-
}, [
|
|
227
|
-
loadRecentChanges
|
|
228
|
-
]);
|
|
229
|
-
useEffect(()=>{
|
|
230
|
-
if (isLoading || error || proposals.length > 0 || !response) return;
|
|
231
|
-
const timeout = window.setTimeout(()=>{
|
|
232
|
-
setResponse("");
|
|
233
|
-
clearInput();
|
|
234
|
-
}, 5000);
|
|
235
|
-
return ()=>window.clearTimeout(timeout);
|
|
236
|
-
}, [
|
|
237
|
-
error,
|
|
238
|
-
isLoading,
|
|
239
|
-
proposals.length,
|
|
240
|
-
response
|
|
241
|
-
]);
|
|
242
|
-
const collections = config.collections.filter((collection)=>!isInternalCollection(collection.slug)).filter((collection)=>isCollectionMentionEnabled(collection.slug)).map((collection)=>({
|
|
243
|
-
label: getSerializableLabel(collection.labels?.singular, collection.slug),
|
|
244
|
-
slug: collection.slug,
|
|
245
|
-
type: "collection"
|
|
246
|
-
}));
|
|
247
|
-
const globals = config.globals?.map((global)=>({
|
|
248
|
-
label: getSerializableLabel(global.label, global.slug),
|
|
249
|
-
slug: global.slug,
|
|
250
|
-
type: "global"
|
|
251
|
-
})) || [];
|
|
252
|
-
const localizationConfig = config.localization;
|
|
253
|
-
const localesConfig = localizationConfig?.locales ?? [];
|
|
254
|
-
const locales = localesConfig.flatMap((locale)=>{
|
|
255
|
-
if (typeof locale === "string") {
|
|
256
|
-
return [
|
|
257
|
-
{
|
|
258
|
-
isDefault: locale === localizationConfig?.defaultLocale,
|
|
259
|
-
label: locale,
|
|
260
|
-
slug: locale,
|
|
261
|
-
type: "locale"
|
|
262
|
-
}
|
|
263
|
-
];
|
|
264
|
-
}
|
|
265
|
-
if (!locale || typeof locale !== "object") return [];
|
|
266
|
-
const slug = typeof locale.code === "string" ? locale.code : typeof locale.label === "string" ? locale.label : null;
|
|
267
|
-
if (!slug) return [];
|
|
268
|
-
return [
|
|
269
|
-
{
|
|
270
|
-
isDefault: slug === localizationConfig?.defaultLocale,
|
|
271
|
-
label: getSerializableLabel(locale.label, slug),
|
|
272
|
-
slug,
|
|
273
|
-
type: "locale"
|
|
274
|
-
}
|
|
275
|
-
];
|
|
276
|
-
});
|
|
277
|
-
const blocks = [
|
|
278
|
-
...config.collections.filter((collection)=>isCollectionMentionEnabled(collection.slug)).flatMap((collection)=>collectBlockOptions({
|
|
279
|
-
fields: collection.fields,
|
|
280
|
-
parent: collection.slug
|
|
281
|
-
})),
|
|
282
|
-
...config.globals?.flatMap((global)=>collectBlockOptions({
|
|
283
|
-
fields: global.fields,
|
|
284
|
-
parent: global.slug
|
|
285
|
-
})) || []
|
|
286
|
-
];
|
|
287
|
-
const mentionOptions = [
|
|
288
|
-
...collections,
|
|
289
|
-
...globals,
|
|
290
|
-
...blocks,
|
|
291
|
-
...locales
|
|
292
|
-
];
|
|
293
|
-
const normalizedMentionQuery = mentionQuery.toLowerCase();
|
|
294
|
-
const filteredCollections = collections.filter((collection)=>collection.slug.toLowerCase().includes(normalizedMentionQuery) || collection.label.toLowerCase().includes(normalizedMentionQuery));
|
|
295
|
-
const filteredMentionOptions = mentionOptions.filter((option)=>option.slug.toLowerCase().includes(normalizedMentionQuery) || option.label.toLowerCase().includes(normalizedMentionQuery));
|
|
296
|
-
const documentSuggestionCollection = filteredCollections.length === 1 ? filteredCollections[0]?.slug : null;
|
|
297
|
-
const { documentSuggestions, resetDocumentSuggestions } = useDocumentMentionSuggestions({
|
|
298
|
-
apiRoute: config.routes.api,
|
|
299
|
-
documentSuggestionCollection,
|
|
300
|
-
mentionQuery,
|
|
301
|
-
mentionRange
|
|
302
|
-
});
|
|
303
|
-
const mentionSuggestions = [
|
|
304
|
-
...filteredMentionOptions,
|
|
305
|
-
...documentSuggestions
|
|
306
|
-
];
|
|
307
|
-
const getTextBeforeCaret = (element)=>{
|
|
308
|
-
const selection = window.getSelection();
|
|
309
|
-
if (!selection || selection.rangeCount === 0) return "";
|
|
310
|
-
const range = selection.getRangeAt(0);
|
|
311
|
-
const clonedRange = range.cloneRange();
|
|
312
|
-
clonedRange.selectNodeContents(element);
|
|
313
|
-
clonedRange.setEnd(range.endContainer, range.endOffset);
|
|
314
|
-
return clonedRange.toString();
|
|
315
|
-
};
|
|
316
|
-
const getTextNodeAtOffset = (element, offset)=>{
|
|
317
|
-
const walker = document.createTreeWalker(element, NodeFilter.SHOW_TEXT);
|
|
318
|
-
let currentOffset = 0;
|
|
319
|
-
let node = walker.nextNode();
|
|
320
|
-
while(node){
|
|
321
|
-
const nextOffset = currentOffset + (node.textContent?.length || 0);
|
|
322
|
-
if (offset <= nextOffset) {
|
|
323
|
-
return {
|
|
324
|
-
node,
|
|
325
|
-
offset: offset - currentOffset
|
|
326
|
-
};
|
|
327
|
-
}
|
|
328
|
-
currentOffset = nextOffset;
|
|
329
|
-
node = walker.nextNode();
|
|
330
|
-
}
|
|
331
|
-
const textNode = document.createTextNode("");
|
|
332
|
-
element.append(textNode);
|
|
333
|
-
return {
|
|
334
|
-
node: textNode,
|
|
335
|
-
offset: 0
|
|
336
|
-
};
|
|
337
|
-
};
|
|
338
|
-
const updateMentionPopoverPosition = useCallback((range)=>{
|
|
339
|
-
const editor = editorRef.current;
|
|
340
|
-
const popover = mentionPopoverRef.current;
|
|
341
|
-
if (!editor || !popover || !range) {
|
|
342
|
-
setMentionPopoverPosition(null);
|
|
343
|
-
return;
|
|
344
|
-
}
|
|
345
|
-
const startPosition = getTextNodeAtOffset(editor, range.start);
|
|
346
|
-
const anchorRange = document.createRange();
|
|
347
|
-
anchorRange.setStart(startPosition.node, startPosition.offset);
|
|
348
|
-
anchorRange.setEnd(startPosition.node, startPosition.offset);
|
|
349
|
-
const editorRect = editor.getBoundingClientRect();
|
|
350
|
-
const anchorRect = anchorRange.getBoundingClientRect();
|
|
351
|
-
const fallbackLeft = Math.max(0, anchorRect.left - editorRect.left + 12);
|
|
352
|
-
const fallbackTop = Math.max(0, anchorRect.bottom - editorRect.top + 20);
|
|
353
|
-
const popoverWidth = popover.offsetWidth || 260;
|
|
354
|
-
const maxLeft = Math.max(0, editor.clientWidth - popoverWidth);
|
|
355
|
-
setMentionPopoverPosition({
|
|
356
|
-
left: Math.min(fallbackLeft, maxLeft),
|
|
357
|
-
top: fallbackTop
|
|
358
|
-
});
|
|
359
|
-
}, []);
|
|
360
|
-
const replaceTextRangeWithBadge = ({ badge, editor, end, start })=>{
|
|
361
|
-
const startPosition = getTextNodeAtOffset(editor, start);
|
|
362
|
-
const endPosition = getTextNodeAtOffset(editor, end);
|
|
363
|
-
const range = document.createRange();
|
|
364
|
-
const trailingSpace = document.createTextNode(" ");
|
|
365
|
-
range.setStart(startPosition.node, startPosition.offset);
|
|
366
|
-
range.setEnd(endPosition.node, endPosition.offset);
|
|
367
|
-
range.deleteContents();
|
|
368
|
-
range.insertNode(trailingSpace);
|
|
369
|
-
range.insertNode(badge);
|
|
370
|
-
const selection = window.getSelection();
|
|
371
|
-
const caretRange = document.createRange();
|
|
372
|
-
caretRange.setStartAfter(trailingSpace);
|
|
373
|
-
caretRange.collapse(true);
|
|
374
|
-
selection?.removeAllRanges();
|
|
375
|
-
selection?.addRange(caretRange);
|
|
376
|
-
};
|
|
377
|
-
const updateMentionState = (valueBeforeCaret)=>{
|
|
378
|
-
const activeMention = getActiveMentionRange(valueBeforeCaret);
|
|
379
|
-
if (!activeMention) {
|
|
380
|
-
setMentionQuery("");
|
|
381
|
-
setMentionRange(null);
|
|
382
|
-
setMentionPopoverPosition(null);
|
|
383
|
-
return;
|
|
384
|
-
}
|
|
385
|
-
setMentionQuery(activeMention.query);
|
|
386
|
-
setMentionRange(activeMention.range);
|
|
387
|
-
};
|
|
388
|
-
useEffect(()=>{
|
|
389
|
-
if (!mentionRange) {
|
|
390
|
-
setMentionPopoverPosition(null);
|
|
391
|
-
return;
|
|
392
|
-
}
|
|
393
|
-
updateMentionPopoverPosition(mentionRange);
|
|
394
|
-
}, [
|
|
395
|
-
mentionRange,
|
|
396
|
-
mentionSuggestions.length,
|
|
397
|
-
updateMentionPopoverPosition
|
|
398
|
-
]);
|
|
399
|
-
useEffect(()=>{
|
|
400
|
-
if (!mentionRange) return;
|
|
401
|
-
const updatePosition = ()=>updateMentionPopoverPosition(mentionRange);
|
|
402
|
-
window.addEventListener("resize", updatePosition);
|
|
403
|
-
window.addEventListener("scroll", updatePosition, true);
|
|
404
|
-
return ()=>{
|
|
405
|
-
window.removeEventListener("resize", updatePosition);
|
|
406
|
-
window.removeEventListener("scroll", updatePosition, true);
|
|
407
|
-
};
|
|
408
|
-
}, [
|
|
409
|
-
mentionRange,
|
|
410
|
-
updateMentionPopoverPosition
|
|
411
|
-
]);
|
|
412
|
-
const clearInput = ()=>{
|
|
413
|
-
setPrompt("");
|
|
414
|
-
setMentions([]);
|
|
415
|
-
if (editorRef.current) editorRef.current.textContent = "";
|
|
416
|
-
};
|
|
417
|
-
const getProposalViewURL = (proposal)=>{
|
|
418
|
-
const adminRoute = config.routes.admin || "/admin";
|
|
419
|
-
if (proposal.action === "updateGlobal" && proposal.slug) {
|
|
420
|
-
return `${adminRoute}/globals/${proposal.slug}`;
|
|
421
|
-
}
|
|
422
|
-
if (proposal.collection && proposal.id) {
|
|
423
|
-
return `${adminRoute}/collections/${proposal.collection}/${proposal.id}`;
|
|
424
|
-
}
|
|
425
|
-
return null;
|
|
426
|
-
};
|
|
427
|
-
const insertMention = (suggestion)=>{
|
|
428
|
-
const editor = editorRef.current;
|
|
429
|
-
if (!mentionRange || !editor) return;
|
|
430
|
-
const currentValue = editor.innerText;
|
|
431
|
-
const beforeMention = currentValue.slice(0, mentionRange.start);
|
|
432
|
-
const afterMention = currentValue.slice(mentionRange.end);
|
|
433
|
-
const badgeType = suggestion.type === "doc" ? "document" : suggestion.type;
|
|
434
|
-
const badgePrefix = `${badgeType}:`;
|
|
435
|
-
const badgeText = `${badgePrefix} ${suggestion.label}`;
|
|
436
|
-
const badge = document.createElement("span");
|
|
437
|
-
badge.className = [
|
|
438
|
-
styles.badge,
|
|
439
|
-
styles[suggestion.type],
|
|
440
|
-
styles.inlineBadge
|
|
441
|
-
].join(" ");
|
|
442
|
-
badge.contentEditable = "false";
|
|
443
|
-
badge.append(createBadgePrefix(suggestion), Object.assign(document.createElement("span"), {
|
|
444
|
-
className: styles.name,
|
|
445
|
-
textContent: suggestion.label
|
|
446
|
-
}));
|
|
447
|
-
replaceTextRangeWithBadge({
|
|
448
|
-
badge,
|
|
449
|
-
editor,
|
|
450
|
-
end: mentionRange.end,
|
|
451
|
-
start: mentionRange.start
|
|
452
|
-
});
|
|
453
|
-
editor.focus();
|
|
454
|
-
setPrompt(`${beforeMention}${badgeText} ${afterMention}`);
|
|
455
|
-
setMentions((currentMentions)=>{
|
|
456
|
-
const mentionExists = currentMentions.some((mention)=>mention.type === suggestion.type && mention.slug === suggestion.slug && mention.parent === suggestion.parent && mention.collection === suggestion.collection && mention.id === suggestion.id);
|
|
457
|
-
if (mentionExists) return currentMentions;
|
|
458
|
-
return [
|
|
459
|
-
...currentMentions,
|
|
460
|
-
suggestion
|
|
461
|
-
];
|
|
462
|
-
});
|
|
463
|
-
setMentionQuery("");
|
|
464
|
-
setMentionRange(null);
|
|
465
|
-
setMentionPopoverPosition(null);
|
|
466
|
-
resetDocumentSuggestions();
|
|
467
|
-
};
|
|
468
|
-
const handleSubmit = async ()=>{
|
|
469
|
-
const trimmedPrompt = prompt.trim();
|
|
470
|
-
if (!trimmedPrompt) return;
|
|
471
|
-
setIsLoading(true);
|
|
472
|
-
setAppliedProposalIndexes([]);
|
|
473
|
-
setError("");
|
|
474
|
-
setProposals([]);
|
|
475
|
-
setResponse("");
|
|
476
|
-
setTokenUsage(null);
|
|
477
|
-
try {
|
|
478
|
-
const res = await fetch(formatAdminURL({
|
|
479
|
-
apiRoute: config.routes.api,
|
|
480
|
-
path: "/ai-chat"
|
|
481
|
-
}), {
|
|
482
|
-
body: JSON.stringify({
|
|
483
|
-
mentions,
|
|
484
|
-
model: selectedModel,
|
|
485
|
-
prompt: trimmedPrompt
|
|
486
|
-
}),
|
|
487
|
-
headers: {
|
|
488
|
-
"Content-Type": "application/json"
|
|
489
|
-
},
|
|
490
|
-
method: "POST"
|
|
491
|
-
});
|
|
492
|
-
if (!res.ok) {
|
|
493
|
-
const result = await res.json().catch(()=>null);
|
|
494
|
-
throw new Error(result?.error || "AI request failed");
|
|
495
|
-
}
|
|
496
|
-
if (!res.body) {
|
|
497
|
-
throw new Error("AI response stream is unavailable");
|
|
498
|
-
}
|
|
499
|
-
const reader = res.body.getReader();
|
|
500
|
-
const decoder = new TextDecoder();
|
|
501
|
-
let buffer = "";
|
|
502
|
-
let receivedProposals = [];
|
|
503
|
-
while(true){
|
|
504
|
-
const { done, value } = await reader.read();
|
|
505
|
-
if (done) break;
|
|
506
|
-
buffer += decoder.decode(value, {
|
|
507
|
-
stream: true
|
|
508
|
-
});
|
|
509
|
-
const chunks = buffer.split("\n\n");
|
|
510
|
-
buffer = chunks.pop() || "";
|
|
511
|
-
for (const chunk of chunks){
|
|
512
|
-
const event = parseSSEEvent(chunk);
|
|
513
|
-
if (!event) continue;
|
|
514
|
-
if (event.event === "text") {
|
|
515
|
-
if (event.data.delta) {
|
|
516
|
-
setResponse((current)=>current + sanitizeResponseText(event.data.delta || ""));
|
|
517
|
-
}
|
|
518
|
-
continue;
|
|
519
|
-
}
|
|
520
|
-
if (event.event === "proposals") {
|
|
521
|
-
receivedProposals = event.data.proposals || [];
|
|
522
|
-
setProposals(receivedProposals);
|
|
523
|
-
setTokenUsage(event.data.usage || null);
|
|
524
|
-
continue;
|
|
525
|
-
}
|
|
526
|
-
if (event.event === "error") {
|
|
527
|
-
throw new Error(event.data.error || "AI request failed");
|
|
528
|
-
}
|
|
529
|
-
}
|
|
530
|
-
}
|
|
531
|
-
const finalEvent = buffer.trim() ? parseSSEEvent(buffer.trim()) : null;
|
|
532
|
-
if (finalEvent?.event === "proposals") {
|
|
533
|
-
receivedProposals = finalEvent.data.proposals || [];
|
|
534
|
-
setProposals(receivedProposals);
|
|
535
|
-
setTokenUsage(finalEvent.data.usage || null);
|
|
536
|
-
}
|
|
537
|
-
if (finalEvent?.event === "error") {
|
|
538
|
-
throw new Error(finalEvent.data.error || "AI request failed");
|
|
539
|
-
}
|
|
540
|
-
if (receivedProposals.length === 0) {
|
|
541
|
-
clearInput();
|
|
542
|
-
}
|
|
543
|
-
} catch (err) {
|
|
544
|
-
setProposals([]);
|
|
545
|
-
setResponse("");
|
|
546
|
-
setTokenUsage(null);
|
|
547
|
-
setError(err instanceof Error ? err.message : "AI request failed");
|
|
548
|
-
} finally{
|
|
549
|
-
setIsLoading(false);
|
|
550
|
-
}
|
|
551
|
-
};
|
|
552
|
-
const handleApplyProposal = async (proposal)=>{
|
|
553
|
-
setIsApplying(true);
|
|
554
|
-
setError("");
|
|
555
|
-
try {
|
|
556
|
-
const res = await fetch(formatAdminURL({
|
|
557
|
-
apiRoute: config.routes.api,
|
|
558
|
-
path: "/ai-apply-action"
|
|
559
|
-
}), {
|
|
560
|
-
body: JSON.stringify({
|
|
561
|
-
aiResponse: response,
|
|
562
|
-
prompt,
|
|
563
|
-
proposal,
|
|
564
|
-
tokenUsage
|
|
565
|
-
}),
|
|
566
|
-
headers: {
|
|
567
|
-
"Content-Type": "application/json"
|
|
568
|
-
},
|
|
569
|
-
method: "POST"
|
|
570
|
-
});
|
|
571
|
-
const result = await res.json();
|
|
572
|
-
if (!res.ok) {
|
|
573
|
-
setProposals([]);
|
|
574
|
-
setResponse("");
|
|
575
|
-
throw new Error(result.error || "Could not apply proposal");
|
|
576
|
-
}
|
|
577
|
-
setAppliedProposalIndexes([]);
|
|
578
|
-
setError("");
|
|
579
|
-
setProposals([]);
|
|
580
|
-
setResponse("");
|
|
581
|
-
setTokenUsage(null);
|
|
582
|
-
if (result.change) {
|
|
583
|
-
setAppliedChanges((current)=>[
|
|
584
|
-
result.change,
|
|
585
|
-
...current
|
|
586
|
-
].slice(0, 10));
|
|
587
|
-
}
|
|
588
|
-
void loadRecentChanges().catch(()=>undefined);
|
|
589
|
-
clearInput();
|
|
590
|
-
} catch (err) {
|
|
591
|
-
setError(err instanceof Error ? err.message : "Could not apply proposal");
|
|
592
|
-
} finally{
|
|
593
|
-
setIsApplying(false);
|
|
594
|
-
}
|
|
595
|
-
};
|
|
596
|
-
return /*#__PURE__*/ _jsxs("div", {
|
|
597
|
-
className: styles.chatLayout,
|
|
598
|
-
children: [
|
|
599
|
-
/*#__PURE__*/ _jsxs("div", {
|
|
600
|
-
className: styles.chat,
|
|
601
|
-
children: [
|
|
602
|
-
/*#__PURE__*/ _jsx("div", {
|
|
603
|
-
className: styles.chatHeader,
|
|
604
|
-
children: /*#__PURE__*/ _jsxs("div", {
|
|
605
|
-
children: [
|
|
606
|
-
/*#__PURE__*/ _jsx("h2", {
|
|
607
|
-
className: styles.chatTitle,
|
|
608
|
-
children: "AI Assistant"
|
|
609
|
-
}),
|
|
610
|
-
/*#__PURE__*/ _jsx("p", {
|
|
611
|
-
className: styles.chatDescription,
|
|
612
|
-
children: "Ask AI to draft, improve, or analyze content."
|
|
613
|
-
})
|
|
614
|
-
]
|
|
615
|
-
})
|
|
616
|
-
}),
|
|
617
|
-
/*#__PURE__*/ _jsxs("div", {
|
|
618
|
-
className: styles.chatInputRow,
|
|
619
|
-
children: [
|
|
620
|
-
/*#__PURE__*/ _jsx("div", {
|
|
621
|
-
className: styles.chatInputSurface,
|
|
622
|
-
children: /*#__PURE__*/ _jsx("div", {
|
|
623
|
-
className: styles.chatInput,
|
|
624
|
-
contentEditable: true,
|
|
625
|
-
"data-placeholder": "Ask AI...",
|
|
626
|
-
onInput: (event)=>{
|
|
627
|
-
const value = event.currentTarget.innerText;
|
|
628
|
-
setPrompt(value);
|
|
629
|
-
if (!value.trim()) {
|
|
630
|
-
setMentions([]);
|
|
631
|
-
}
|
|
632
|
-
updateMentionState(getTextBeforeCaret(event.currentTarget));
|
|
633
|
-
},
|
|
634
|
-
onKeyDown: (event)=>{
|
|
635
|
-
if (event.key === "ArrowDown" && mentionRange && mentionSuggestions.length > 0) {
|
|
636
|
-
const firstOption = mentionPopoverRef.current?.querySelector("button");
|
|
637
|
-
if (firstOption) {
|
|
638
|
-
event.preventDefault();
|
|
639
|
-
firstOption.focus();
|
|
640
|
-
return;
|
|
641
|
-
}
|
|
642
|
-
}
|
|
643
|
-
if (event.key === "Enter" && !event.shiftKey) {
|
|
644
|
-
event.preventDefault();
|
|
645
|
-
void handleSubmit();
|
|
646
|
-
}
|
|
647
|
-
},
|
|
648
|
-
ref: editorRef,
|
|
649
|
-
role: "textbox",
|
|
650
|
-
suppressContentEditableWarning: true
|
|
651
|
-
})
|
|
652
|
-
}),
|
|
653
|
-
mentionRange && /*#__PURE__*/ _jsx(MentionPopover, {
|
|
654
|
-
containerRef: mentionPopoverRef,
|
|
655
|
-
onSelect: insertMention,
|
|
656
|
-
style: mentionPopoverPosition ? {
|
|
657
|
-
left: `${mentionPopoverPosition.left}px`,
|
|
658
|
-
top: `${mentionPopoverPosition.top}px`
|
|
659
|
-
} : undefined,
|
|
660
|
-
suggestions: mentionSuggestions
|
|
661
|
-
})
|
|
662
|
-
]
|
|
663
|
-
}),
|
|
664
|
-
/*#__PURE__*/ _jsxs("div", {
|
|
665
|
-
className: styles.chatActionsRow,
|
|
666
|
-
children: [
|
|
667
|
-
/*#__PURE__*/ _jsx("div", {
|
|
668
|
-
className: styles.settings,
|
|
669
|
-
children: /*#__PURE__*/ _jsxs("label", {
|
|
670
|
-
className: styles.setting,
|
|
671
|
-
children: [
|
|
672
|
-
/*#__PURE__*/ _jsx("span", {
|
|
673
|
-
className: styles.settingLabel,
|
|
674
|
-
children: "Model"
|
|
675
|
-
}),
|
|
676
|
-
/*#__PURE__*/ _jsxs("div", {
|
|
677
|
-
className: styles.selectWrapper,
|
|
678
|
-
children: [
|
|
679
|
-
getProviderIcon(settingsProvider),
|
|
680
|
-
/*#__PURE__*/ _jsxs("select", {
|
|
681
|
-
className: styles.select,
|
|
682
|
-
disabled: !settingsProvider,
|
|
683
|
-
onChange: (event)=>setSelectedModel(event.target.value),
|
|
684
|
-
value: selectedModel,
|
|
685
|
-
children: [
|
|
686
|
-
!settingsProvider && /*#__PURE__*/ _jsx("option", {
|
|
687
|
-
value: "",
|
|
688
|
-
children: "Select provider in account settings"
|
|
689
|
-
}),
|
|
690
|
-
settingsProvider && aiModelConfig.providers[settingsProvider].map((model)=>/*#__PURE__*/ _jsx("option", {
|
|
691
|
-
value: model.value,
|
|
692
|
-
children: model.label
|
|
693
|
-
}, model.value))
|
|
694
|
-
]
|
|
695
|
-
})
|
|
696
|
-
]
|
|
697
|
-
})
|
|
698
|
-
]
|
|
699
|
-
})
|
|
700
|
-
}),
|
|
701
|
-
/*#__PURE__*/ _jsxs("button", {
|
|
702
|
-
className: styles.chatButton,
|
|
703
|
-
disabled: !prompt.trim() || !settingsProvider || !selectedModel || isLoading,
|
|
704
|
-
onClick: ()=>void handleSubmit(),
|
|
705
|
-
type: "button",
|
|
706
|
-
children: [
|
|
707
|
-
/*#__PURE__*/ _jsx(Send, {
|
|
708
|
-
width: 14,
|
|
709
|
-
height: 14
|
|
710
|
-
}),
|
|
711
|
-
isLoading ? "Sending..." : "Send"
|
|
712
|
-
]
|
|
713
|
-
})
|
|
714
|
-
]
|
|
715
|
-
}),
|
|
716
|
-
/*#__PURE__*/ _jsx(ActionToast, {
|
|
717
|
-
apiRoute: config.routes.api,
|
|
718
|
-
appliedProposalIndexes: appliedProposalIndexes,
|
|
719
|
-
description: response,
|
|
720
|
-
error: error,
|
|
721
|
-
getViewURL: getProposalViewURL,
|
|
722
|
-
isApplying: isApplying,
|
|
723
|
-
onDismiss: ()=>{
|
|
724
|
-
setAppliedProposalIndexes([]);
|
|
725
|
-
setError("");
|
|
726
|
-
setProposals([]);
|
|
727
|
-
setResponse("");
|
|
728
|
-
setTokenUsage(null);
|
|
729
|
-
clearInput();
|
|
730
|
-
},
|
|
731
|
-
onDismissError: ()=>{
|
|
732
|
-
setError("");
|
|
733
|
-
},
|
|
734
|
-
onApply: (proposal, _index)=>void handleApplyProposal(proposal),
|
|
735
|
-
proposals: proposals,
|
|
736
|
-
tokenUsage: tokenUsage
|
|
737
|
-
})
|
|
738
|
-
]
|
|
739
|
-
}),
|
|
740
|
-
/*#__PURE__*/ _jsx(RecentChangesList, {
|
|
741
|
-
allChangesURL: allChangesURL,
|
|
742
|
-
changes: appliedChanges
|
|
743
|
-
})
|
|
744
|
-
]
|
|
745
|
-
});
|
|
746
|
-
};
|
|
747
|
-
export default AIInput;
|