@mvriu5/payload-ai 0.5.8 → 0.5.10
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 +62 -6
- package/dist/ai/{proposals.d.ts → proposalSigning.d.ts} +1 -1
- package/dist/ai/{proposals.js → proposalSigning.js} +6 -11
- package/dist/ai/providerOptions.d.ts +17 -1
- package/dist/ai/providerOptions.js +35 -7
- package/dist/ai/providerRuntime.d.ts +2 -2
- package/dist/ai/providerRuntime.js +50 -16
- package/dist/ai/sensitiveData.js +2 -6
- package/dist/components/AIInput.d.ts +2 -1
- package/dist/components/AIInput.js +327 -41
- package/dist/components/AIInput.module.css +41 -6
- package/dist/components/APIKeyField.d.ts +3 -0
- package/dist/components/{AIApiKeyField.js → APIKeyField.js} +3 -4
- package/dist/components/ActionToast.d.ts +30 -0
- package/dist/components/{AIActionProposalList.js → ActionToast.js} +78 -8
- package/dist/components/{AIActionProposalList.module.css → ActionToast.module.css} +49 -5
- package/dist/components/AuditLogList.d.ts +32 -0
- package/dist/components/AuditLogList.js +67 -0
- package/dist/components/AuditLogList.module.css +152 -0
- package/dist/components/DiffDialog.d.ts +19 -0
- package/dist/components/DiffDialog.js +396 -0
- package/dist/components/DiffDialog.module.css +333 -0
- package/dist/components/Icons.d.ts +9 -0
- package/dist/components/Icons.js +193 -0
- package/dist/components/MentionPopover.d.ts +19 -0
- package/dist/components/{CollectionMentionPopover.js → MentionPopover.js} +11 -4
- package/dist/components/{CollectionMentionPopover.module.css → MentionPopover.module.css} +0 -2
- package/dist/components/hooks/useAISettings.d.ts +6 -4
- package/dist/components/hooks/useAISettings.js +18 -3
- package/dist/components/hooks/useDocumentMentionSuggestions.d.ts +3 -3
- package/dist/components/hooks/useDocumentMentionSuggestions.js +2 -2
- package/dist/exports/client.d.ts +4 -2
- package/dist/exports/client.js +4 -2
- package/dist/handlers/applyActionHandler.d.ts +8 -0
- package/dist/handlers/applyActionHandler.js +563 -0
- package/dist/handlers/auditLogHandler.d.ts +6 -0
- package/dist/handlers/auditLogHandler.js +39 -0
- package/dist/handlers/chatHandler.d.ts +42 -0
- package/dist/handlers/chatHandler.js +634 -0
- package/dist/handlers/mentionSuggestionHandler.d.ts +7 -0
- package/dist/handlers/mentionSuggestionHandler.js +97 -0
- package/dist/handlers/proposalDiffHandler.d.ts +7 -0
- package/dist/handlers/proposalDiffHandler.js +208 -0
- package/dist/index.d.ts +6 -6
- package/dist/index.js +151 -8
- package/dist/payload/collectionPermissions.d.ts +14 -13
- package/dist/payload/collectionPermissions.js +3 -2
- package/dist/payload/normalizeData.d.ts +8 -0
- package/dist/payload/normalizeData.js +25 -3
- package/dist/payload/schemaContext.d.ts +79 -9
- package/dist/payload/schemaContext.js +231 -22
- package/dist/payload/shared.d.ts +33 -0
- package/dist/payload/shared.js +114 -0
- package/package.json +7 -2
- package/dist/components/AIActionProposalList.d.ts +0 -25
- package/dist/components/AIApiKeyField.d.ts +0 -2
- package/dist/components/CollectionMentionPopover.d.ts +0 -16
- package/dist/components/hooks/utils.d.ts +0 -1
- package/dist/components/hooks/utils.js +0 -3
- package/dist/endpoints/aiApplyActionEndpointHandler.d.ts +0 -7
- package/dist/endpoints/aiApplyActionEndpointHandler.js +0 -169
- package/dist/endpoints/aiChatEndpointHandler.d.ts +0 -35
- package/dist/endpoints/aiChatEndpointHandler.js +0 -321
- package/dist/endpoints/aiMentionSuggestionsEndpointHandler.d.ts +0 -7
- package/dist/endpoints/aiMentionSuggestionsEndpointHandler.js +0 -63
|
@@ -1,13 +1,14 @@
|
|
|
1
1
|
"use client";
|
|
2
2
|
import { useConfig } from "@payloadcms/ui";
|
|
3
3
|
import { formatAdminURL } from "payload/shared";
|
|
4
|
-
import { useMemo, useRef, useState } from "react";
|
|
4
|
+
import { useCallback, useEffect, useMemo, useRef, useState } from "react";
|
|
5
5
|
import { getResolvedAIModelConfig } from "../ai/providerOptions.js";
|
|
6
6
|
import { getSerializableLabel, isInternalCollection } from "../payload/shared.js";
|
|
7
|
-
import {
|
|
7
|
+
import { ActionToast } from "./ActionToast.js";
|
|
8
8
|
import styles from "./AIInput.module.css";
|
|
9
|
-
import {
|
|
10
|
-
import { ClaudeIcon, GoogleGeminiIcon, MistralAiIcon, OpenaiIcon } from "./Icons.js";
|
|
9
|
+
import { MentionPopover } from "./MentionPopover.js";
|
|
10
|
+
import { ClaudeIcon, GoogleGeminiIcon, MistralAiIcon, OpenaiIcon, OpenrouterIcon, Send } from "./Icons.js";
|
|
11
|
+
import { RecentChangesList } from "./AuditLogList.js";
|
|
11
12
|
import { useAISettings } from "./hooks/useAISettings.js";
|
|
12
13
|
import { useDocumentMentionSuggestions } from "./hooks/useDocumentMentionSuggestions.js";
|
|
13
14
|
const collectBlockOptions = ({ fields, parent })=>{
|
|
@@ -50,11 +51,117 @@ const getProviderIcon = (provider)=>{
|
|
|
50
51
|
return /*#__PURE__*/ React.createElement(MistralAiIcon, iconProps);
|
|
51
52
|
case "openai":
|
|
52
53
|
return /*#__PURE__*/ React.createElement(OpenaiIcon, iconProps);
|
|
54
|
+
case "openrouter":
|
|
55
|
+
return /*#__PURE__*/ React.createElement(OpenrouterIcon, iconProps);
|
|
53
56
|
default:
|
|
54
57
|
return null;
|
|
55
58
|
}
|
|
56
59
|
};
|
|
57
|
-
|
|
60
|
+
const parseSSEEvent = (chunk)=>{
|
|
61
|
+
const lines = chunk.split("\n");
|
|
62
|
+
let eventName = "";
|
|
63
|
+
const dataLines = [];
|
|
64
|
+
for (const line of lines){
|
|
65
|
+
if (line.startsWith("event:")) {
|
|
66
|
+
eventName = line.slice(6).trim();
|
|
67
|
+
continue;
|
|
68
|
+
}
|
|
69
|
+
if (line.startsWith("data:")) {
|
|
70
|
+
dataLines.push(line.slice(5).trim());
|
|
71
|
+
}
|
|
72
|
+
}
|
|
73
|
+
if (!eventName) return null;
|
|
74
|
+
try {
|
|
75
|
+
const data = JSON.parse(dataLines.join("\n"));
|
|
76
|
+
if (eventName !== "text" && eventName !== "proposals" && eventName !== "error" && eventName !== "done") {
|
|
77
|
+
return null;
|
|
78
|
+
}
|
|
79
|
+
return {
|
|
80
|
+
data,
|
|
81
|
+
event: eventName
|
|
82
|
+
};
|
|
83
|
+
} catch {
|
|
84
|
+
return null;
|
|
85
|
+
}
|
|
86
|
+
};
|
|
87
|
+
const sanitizeResponseText = (value)=>value.replace(/\*\*/g, "");
|
|
88
|
+
const isMentionBoundary = (character)=>{
|
|
89
|
+
return character === undefined || /\s/.test(character);
|
|
90
|
+
};
|
|
91
|
+
const getActiveMentionRange = (valueBeforeCaret)=>{
|
|
92
|
+
const caretPosition = valueBeforeCaret.length;
|
|
93
|
+
for(let index = valueBeforeCaret.length - 1; index >= 0; index -= 1){
|
|
94
|
+
const character = valueBeforeCaret[index];
|
|
95
|
+
if (character === "@") {
|
|
96
|
+
const previousCharacter = valueBeforeCaret[index - 1];
|
|
97
|
+
const query = valueBeforeCaret.slice(index + 1);
|
|
98
|
+
if (!isMentionBoundary(previousCharacter)) return null;
|
|
99
|
+
if (!/^[\w-]*$/.test(query)) return null;
|
|
100
|
+
return {
|
|
101
|
+
query,
|
|
102
|
+
range: {
|
|
103
|
+
end: caretPosition,
|
|
104
|
+
start: index
|
|
105
|
+
}
|
|
106
|
+
};
|
|
107
|
+
}
|
|
108
|
+
if (isMentionBoundary(character)) break;
|
|
109
|
+
}
|
|
110
|
+
return null;
|
|
111
|
+
};
|
|
112
|
+
const svgNamespace = "http://www.w3.org/2000/svg";
|
|
113
|
+
const auditLogCollectionSlug = "payload-ai-auditlog";
|
|
114
|
+
const appendSvgPath = (svg, d)=>{
|
|
115
|
+
const path = document.createElementNS(svgNamespace, "path");
|
|
116
|
+
path.setAttribute("d", d);
|
|
117
|
+
svg.append(path);
|
|
118
|
+
};
|
|
119
|
+
const createBadgeIcon = (type)=>{
|
|
120
|
+
if (type === "locale") return null;
|
|
121
|
+
const svg = document.createElementNS(svgNamespace, "svg");
|
|
122
|
+
svg.setAttribute("aria-hidden", "true");
|
|
123
|
+
svg.setAttribute("class", styles.badgeIcon);
|
|
124
|
+
svg.setAttribute("fill", "none");
|
|
125
|
+
svg.setAttribute("stroke", "currentColor");
|
|
126
|
+
svg.setAttribute("stroke-linecap", "round");
|
|
127
|
+
svg.setAttribute("stroke-linejoin", "round");
|
|
128
|
+
svg.setAttribute("stroke-width", "2");
|
|
129
|
+
svg.setAttribute("viewBox", "0 0 24 24");
|
|
130
|
+
if (type === "collection") {
|
|
131
|
+
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");
|
|
132
|
+
}
|
|
133
|
+
if (type === "doc") {
|
|
134
|
+
appendSvgPath(svg, "M14 3v4a1 1 0 0 0 1 1h4");
|
|
135
|
+
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");
|
|
136
|
+
}
|
|
137
|
+
if (type === "global") {
|
|
138
|
+
appendSvgPath(svg, "M3 12a9 9 0 1 0 18 0a9 9 0 0 0 -18 0");
|
|
139
|
+
appendSvgPath(svg, "M3.6 9h16.8");
|
|
140
|
+
appendSvgPath(svg, "M3.6 15h16.8");
|
|
141
|
+
appendSvgPath(svg, "M11.5 3a17 17 0 0 0 0 18");
|
|
142
|
+
appendSvgPath(svg, "M12.5 3a17 17 0 0 1 0 18");
|
|
143
|
+
}
|
|
144
|
+
if (type === "block") {
|
|
145
|
+
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");
|
|
146
|
+
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");
|
|
147
|
+
}
|
|
148
|
+
return svg;
|
|
149
|
+
};
|
|
150
|
+
const createBadgePrefix = (suggestion)=>{
|
|
151
|
+
const prefix = document.createElement("span");
|
|
152
|
+
const icon = createBadgeIcon(suggestion.type);
|
|
153
|
+
prefix.className = styles.prefix;
|
|
154
|
+
if (icon) {
|
|
155
|
+
prefix.append(icon);
|
|
156
|
+
}
|
|
157
|
+
if (suggestion.type === "doc") {
|
|
158
|
+
prefix.append(document.createTextNode(`${suggestion.collection || "document"}:`));
|
|
159
|
+
} else if (suggestion.type === "locale") {
|
|
160
|
+
prefix.textContent = "locale:";
|
|
161
|
+
}
|
|
162
|
+
return prefix;
|
|
163
|
+
};
|
|
164
|
+
const AIInput = ()=>{
|
|
58
165
|
const { config } = useConfig();
|
|
59
166
|
const editorRef = useRef(null);
|
|
60
167
|
const mentionPopoverRef = useRef(null);
|
|
@@ -70,11 +177,14 @@ export const AIInput = ()=>{
|
|
|
70
177
|
});
|
|
71
178
|
const [mentionQuery, setMentionQuery] = useState("");
|
|
72
179
|
const [mentionRange, setMentionRange] = useState(null);
|
|
180
|
+
const [mentionPopoverPosition, setMentionPopoverPosition] = useState(null);
|
|
73
181
|
const [mentions, setMentions] = useState([]);
|
|
74
182
|
const [appliedProposalIndexes, setAppliedProposalIndexes] = useState([]);
|
|
75
183
|
const [response, setResponse] = useState("");
|
|
184
|
+
const [tokenUsage, setTokenUsage] = useState(null);
|
|
76
185
|
const [error, setError] = useState("");
|
|
77
186
|
const [proposals, setProposals] = useState([]);
|
|
187
|
+
const [appliedChanges, setAppliedChanges] = useState([]);
|
|
78
188
|
const [isLoading, setIsLoading] = useState(false);
|
|
79
189
|
const [isApplying, setIsApplying] = useState(false);
|
|
80
190
|
const enabledCollectionSlugs = config.admin?.custom?.payloadAiPlugin?.collectionSlugs;
|
|
@@ -82,6 +192,42 @@ export const AIInput = ()=>{
|
|
|
82
192
|
enabledCollectionSlugs
|
|
83
193
|
]);
|
|
84
194
|
const isCollectionMentionEnabled = (slug)=>!enabledCollectionSlugSet || enabledCollectionSlugSet.has(slug);
|
|
195
|
+
const recentChangesEndpoint = useMemo(()=>formatAdminURL({
|
|
196
|
+
apiRoute: config.routes.api,
|
|
197
|
+
path: "/ai-audit-log"
|
|
198
|
+
}), [
|
|
199
|
+
config.routes.api
|
|
200
|
+
]);
|
|
201
|
+
const allChangesURL = useMemo(()=>`${config.routes.admin || "/admin"}/collections/${auditLogCollectionSlug}`, [
|
|
202
|
+
config.routes.admin
|
|
203
|
+
]);
|
|
204
|
+
const loadRecentChanges = useCallback(async ()=>{
|
|
205
|
+
const res = await fetch(recentChangesEndpoint);
|
|
206
|
+
const result = await res.json().catch(()=>null);
|
|
207
|
+
if (res.ok && result?.changes) {
|
|
208
|
+
setAppliedChanges(result.changes.slice(0, 10));
|
|
209
|
+
}
|
|
210
|
+
}, [
|
|
211
|
+
recentChangesEndpoint
|
|
212
|
+
]);
|
|
213
|
+
useEffect(()=>{
|
|
214
|
+
void loadRecentChanges().catch(()=>undefined);
|
|
215
|
+
}, [
|
|
216
|
+
loadRecentChanges
|
|
217
|
+
]);
|
|
218
|
+
useEffect(()=>{
|
|
219
|
+
if (isLoading || error || proposals.length > 0 || !response) return;
|
|
220
|
+
const timeout = window.setTimeout(()=>{
|
|
221
|
+
setResponse("");
|
|
222
|
+
clearInput();
|
|
223
|
+
}, 5000);
|
|
224
|
+
return ()=>window.clearTimeout(timeout);
|
|
225
|
+
}, [
|
|
226
|
+
error,
|
|
227
|
+
isLoading,
|
|
228
|
+
proposals.length,
|
|
229
|
+
response
|
|
230
|
+
]);
|
|
85
231
|
const collections = config.collections.filter((collection)=>!isInternalCollection(collection.slug)).filter((collection)=>isCollectionMentionEnabled(collection.slug)).map((collection)=>({
|
|
86
232
|
label: getSerializableLabel(collection.labels?.singular, collection.slug),
|
|
87
233
|
slug: collection.slug,
|
|
@@ -92,6 +238,31 @@ export const AIInput = ()=>{
|
|
|
92
238
|
slug: global.slug,
|
|
93
239
|
type: "global"
|
|
94
240
|
})) || [];
|
|
241
|
+
const localizationConfig = config.localization;
|
|
242
|
+
const localesConfig = localizationConfig?.locales ?? [];
|
|
243
|
+
const locales = localesConfig.flatMap((locale)=>{
|
|
244
|
+
if (typeof locale === "string") {
|
|
245
|
+
return [
|
|
246
|
+
{
|
|
247
|
+
isDefault: locale === localizationConfig?.defaultLocale,
|
|
248
|
+
label: locale,
|
|
249
|
+
slug: locale,
|
|
250
|
+
type: "locale"
|
|
251
|
+
}
|
|
252
|
+
];
|
|
253
|
+
}
|
|
254
|
+
if (!locale || typeof locale !== "object") return [];
|
|
255
|
+
const slug = typeof locale.code === "string" ? locale.code : typeof locale.label === "string" ? locale.label : null;
|
|
256
|
+
if (!slug) return [];
|
|
257
|
+
return [
|
|
258
|
+
{
|
|
259
|
+
isDefault: slug === localizationConfig?.defaultLocale,
|
|
260
|
+
label: getSerializableLabel(locale.label, slug),
|
|
261
|
+
slug,
|
|
262
|
+
type: "locale"
|
|
263
|
+
}
|
|
264
|
+
];
|
|
265
|
+
});
|
|
95
266
|
const blocks = [
|
|
96
267
|
...config.collections.filter((collection)=>isCollectionMentionEnabled(collection.slug)).flatMap((collection)=>collectBlockOptions({
|
|
97
268
|
fields: collection.fields,
|
|
@@ -105,11 +276,12 @@ export const AIInput = ()=>{
|
|
|
105
276
|
const mentionOptions = [
|
|
106
277
|
...collections,
|
|
107
278
|
...globals,
|
|
108
|
-
...blocks
|
|
279
|
+
...blocks,
|
|
280
|
+
...locales
|
|
109
281
|
];
|
|
110
282
|
const normalizedMentionQuery = mentionQuery.toLowerCase();
|
|
111
283
|
const filteredCollections = collections.filter((collection)=>collection.slug.toLowerCase().includes(normalizedMentionQuery) || collection.label.toLowerCase().includes(normalizedMentionQuery));
|
|
112
|
-
const filteredMentionOptions = mentionOptions.filter((option)=>option.slug.toLowerCase().includes(normalizedMentionQuery));
|
|
284
|
+
const filteredMentionOptions = mentionOptions.filter((option)=>option.slug.toLowerCase().includes(normalizedMentionQuery) || option.label.toLowerCase().includes(normalizedMentionQuery));
|
|
113
285
|
const documentSuggestionCollection = filteredCollections.length === 1 ? filteredCollections[0]?.slug : null;
|
|
114
286
|
const { documentSuggestions, resetDocumentSuggestions } = useDocumentMentionSuggestions({
|
|
115
287
|
apiRoute: config.routes.api,
|
|
@@ -121,14 +293,14 @@ export const AIInput = ()=>{
|
|
|
121
293
|
...filteredMentionOptions,
|
|
122
294
|
...documentSuggestions
|
|
123
295
|
];
|
|
124
|
-
const
|
|
296
|
+
const getTextBeforeCaret = (element)=>{
|
|
125
297
|
const selection = window.getSelection();
|
|
126
|
-
if (!selection || selection.rangeCount === 0) return
|
|
298
|
+
if (!selection || selection.rangeCount === 0) return "";
|
|
127
299
|
const range = selection.getRangeAt(0);
|
|
128
300
|
const clonedRange = range.cloneRange();
|
|
129
301
|
clonedRange.selectNodeContents(element);
|
|
130
302
|
clonedRange.setEnd(range.endContainer, range.endOffset);
|
|
131
|
-
return clonedRange.toString()
|
|
303
|
+
return clonedRange.toString();
|
|
132
304
|
};
|
|
133
305
|
const getTextNodeAtOffset = (element, offset)=>{
|
|
134
306
|
const walker = document.createTreeWalker(element, NodeFilter.SHOW_TEXT);
|
|
@@ -152,6 +324,28 @@ export const AIInput = ()=>{
|
|
|
152
324
|
offset: 0
|
|
153
325
|
};
|
|
154
326
|
};
|
|
327
|
+
const updateMentionPopoverPosition = useCallback((range)=>{
|
|
328
|
+
const editor = editorRef.current;
|
|
329
|
+
const popover = mentionPopoverRef.current;
|
|
330
|
+
if (!editor || !popover || !range) {
|
|
331
|
+
setMentionPopoverPosition(null);
|
|
332
|
+
return;
|
|
333
|
+
}
|
|
334
|
+
const startPosition = getTextNodeAtOffset(editor, range.start);
|
|
335
|
+
const anchorRange = document.createRange();
|
|
336
|
+
anchorRange.setStart(startPosition.node, startPosition.offset);
|
|
337
|
+
anchorRange.setEnd(startPosition.node, startPosition.offset);
|
|
338
|
+
const editorRect = editor.getBoundingClientRect();
|
|
339
|
+
const anchorRect = anchorRange.getBoundingClientRect();
|
|
340
|
+
const fallbackLeft = Math.max(0, anchorRect.left - editorRect.left + 12);
|
|
341
|
+
const fallbackTop = Math.max(0, anchorRect.bottom - editorRect.top + 20);
|
|
342
|
+
const popoverWidth = popover.offsetWidth || 260;
|
|
343
|
+
const maxLeft = Math.max(0, editor.clientWidth - popoverWidth);
|
|
344
|
+
setMentionPopoverPosition({
|
|
345
|
+
left: Math.min(fallbackLeft, maxLeft),
|
|
346
|
+
top: fallbackTop
|
|
347
|
+
});
|
|
348
|
+
}, []);
|
|
155
349
|
const replaceTextRangeWithBadge = ({ badge, editor, end, start })=>{
|
|
156
350
|
const startPosition = getTextNodeAtOffset(editor, start);
|
|
157
351
|
const endPosition = getTextNodeAtOffset(editor, end);
|
|
@@ -169,21 +363,41 @@ export const AIInput = ()=>{
|
|
|
169
363
|
selection?.removeAllRanges();
|
|
170
364
|
selection?.addRange(caretRange);
|
|
171
365
|
};
|
|
172
|
-
const updateMentionState = (
|
|
173
|
-
const
|
|
174
|
-
|
|
175
|
-
if (!match || typeof match.index !== "number") {
|
|
366
|
+
const updateMentionState = (valueBeforeCaret)=>{
|
|
367
|
+
const activeMention = getActiveMentionRange(valueBeforeCaret);
|
|
368
|
+
if (!activeMention) {
|
|
176
369
|
setMentionQuery("");
|
|
177
370
|
setMentionRange(null);
|
|
371
|
+
setMentionPopoverPosition(null);
|
|
178
372
|
return;
|
|
179
373
|
}
|
|
180
|
-
|
|
181
|
-
|
|
182
|
-
setMentionRange({
|
|
183
|
-
end: caretPosition,
|
|
184
|
-
start: atIndex
|
|
185
|
-
});
|
|
374
|
+
setMentionQuery(activeMention.query);
|
|
375
|
+
setMentionRange(activeMention.range);
|
|
186
376
|
};
|
|
377
|
+
useEffect(()=>{
|
|
378
|
+
if (!mentionRange) {
|
|
379
|
+
setMentionPopoverPosition(null);
|
|
380
|
+
return;
|
|
381
|
+
}
|
|
382
|
+
updateMentionPopoverPosition(mentionRange);
|
|
383
|
+
}, [
|
|
384
|
+
mentionRange,
|
|
385
|
+
mentionSuggestions.length,
|
|
386
|
+
updateMentionPopoverPosition
|
|
387
|
+
]);
|
|
388
|
+
useEffect(()=>{
|
|
389
|
+
if (!mentionRange) return;
|
|
390
|
+
const updatePosition = ()=>updateMentionPopoverPosition(mentionRange);
|
|
391
|
+
window.addEventListener("resize", updatePosition);
|
|
392
|
+
window.addEventListener("scroll", updatePosition, true);
|
|
393
|
+
return ()=>{
|
|
394
|
+
window.removeEventListener("resize", updatePosition);
|
|
395
|
+
window.removeEventListener("scroll", updatePosition, true);
|
|
396
|
+
};
|
|
397
|
+
}, [
|
|
398
|
+
mentionRange,
|
|
399
|
+
updateMentionPopoverPosition
|
|
400
|
+
]);
|
|
187
401
|
const clearInput = ()=>{
|
|
188
402
|
setPrompt("");
|
|
189
403
|
setMentions([]);
|
|
@@ -202,12 +416,12 @@ export const AIInput = ()=>{
|
|
|
202
416
|
const insertMention = (suggestion)=>{
|
|
203
417
|
const editor = editorRef.current;
|
|
204
418
|
if (!mentionRange || !editor) return;
|
|
205
|
-
const
|
|
206
|
-
const
|
|
419
|
+
const currentValue = editor.innerText;
|
|
420
|
+
const beforeMention = currentValue.slice(0, mentionRange.start);
|
|
421
|
+
const afterMention = currentValue.slice(mentionRange.end);
|
|
207
422
|
const badgeType = suggestion.type === "doc" ? "document" : suggestion.type;
|
|
208
423
|
const badgePrefix = `${badgeType}:`;
|
|
209
424
|
const badgeText = `${badgePrefix} ${suggestion.label}`;
|
|
210
|
-
const promptText = suggestion.type === "doc" ? `${badgeText} (${suggestion.collection}/${suggestion.id})` : badgeText;
|
|
211
425
|
const badge = document.createElement("span");
|
|
212
426
|
badge.className = [
|
|
213
427
|
styles.badge,
|
|
@@ -215,10 +429,7 @@ export const AIInput = ()=>{
|
|
|
215
429
|
styles.inlineBadge
|
|
216
430
|
].join(" ");
|
|
217
431
|
badge.contentEditable = "false";
|
|
218
|
-
badge.append(Object.assign(document.createElement("span"), {
|
|
219
|
-
className: styles.prefix,
|
|
220
|
-
textContent: `${badgePrefix} `
|
|
221
|
-
}), Object.assign(document.createElement("span"), {
|
|
432
|
+
badge.append(createBadgePrefix(suggestion), Object.assign(document.createElement("span"), {
|
|
222
433
|
className: styles.name,
|
|
223
434
|
textContent: suggestion.label
|
|
224
435
|
}));
|
|
@@ -229,7 +440,7 @@ export const AIInput = ()=>{
|
|
|
229
440
|
start: mentionRange.start
|
|
230
441
|
});
|
|
231
442
|
editor.focus();
|
|
232
|
-
setPrompt(`${beforeMention}${
|
|
443
|
+
setPrompt(`${beforeMention}${badgeText} ${afterMention}`);
|
|
233
444
|
setMentions((currentMentions)=>{
|
|
234
445
|
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);
|
|
235
446
|
if (mentionExists) return currentMentions;
|
|
@@ -240,6 +451,7 @@ export const AIInput = ()=>{
|
|
|
240
451
|
});
|
|
241
452
|
setMentionQuery("");
|
|
242
453
|
setMentionRange(null);
|
|
454
|
+
setMentionPopoverPosition(null);
|
|
243
455
|
resetDocumentSuggestions();
|
|
244
456
|
};
|
|
245
457
|
const handleSubmit = async ()=>{
|
|
@@ -247,6 +459,10 @@ export const AIInput = ()=>{
|
|
|
247
459
|
if (!trimmedPrompt) return;
|
|
248
460
|
setIsLoading(true);
|
|
249
461
|
setAppliedProposalIndexes([]);
|
|
462
|
+
setError("");
|
|
463
|
+
setProposals([]);
|
|
464
|
+
setResponse("");
|
|
465
|
+
setTokenUsage(null);
|
|
250
466
|
try {
|
|
251
467
|
const res = await fetch(formatAdminURL({
|
|
252
468
|
apiRoute: config.routes.api,
|
|
@@ -262,18 +478,61 @@ export const AIInput = ()=>{
|
|
|
262
478
|
},
|
|
263
479
|
method: "POST"
|
|
264
480
|
});
|
|
265
|
-
const result = await res.json();
|
|
266
481
|
if (!res.ok) {
|
|
267
|
-
|
|
268
|
-
|
|
269
|
-
|
|
482
|
+
const result = await res.json().catch(()=>null);
|
|
483
|
+
throw new Error(result?.error || "AI request failed");
|
|
484
|
+
}
|
|
485
|
+
if (!res.body) {
|
|
486
|
+
throw new Error("AI response stream is unavailable");
|
|
487
|
+
}
|
|
488
|
+
const reader = res.body.getReader();
|
|
489
|
+
const decoder = new TextDecoder();
|
|
490
|
+
let buffer = "";
|
|
491
|
+
let receivedProposals = [];
|
|
492
|
+
while(true){
|
|
493
|
+
const { done, value } = await reader.read();
|
|
494
|
+
if (done) break;
|
|
495
|
+
buffer += decoder.decode(value, {
|
|
496
|
+
stream: true
|
|
497
|
+
});
|
|
498
|
+
const chunks = buffer.split("\n\n");
|
|
499
|
+
buffer = chunks.pop() || "";
|
|
500
|
+
for (const chunk of chunks){
|
|
501
|
+
const event = parseSSEEvent(chunk);
|
|
502
|
+
if (!event) continue;
|
|
503
|
+
if (event.event === "text") {
|
|
504
|
+
if (event.data.delta) {
|
|
505
|
+
setResponse((current)=>current + sanitizeResponseText(event.data.delta || ""));
|
|
506
|
+
}
|
|
507
|
+
continue;
|
|
508
|
+
}
|
|
509
|
+
if (event.event === "proposals") {
|
|
510
|
+
receivedProposals = event.data.proposals || [];
|
|
511
|
+
setProposals(receivedProposals);
|
|
512
|
+
setTokenUsage(event.data.usage || null);
|
|
513
|
+
continue;
|
|
514
|
+
}
|
|
515
|
+
if (event.event === "error") {
|
|
516
|
+
throw new Error(event.data.error || "AI request failed");
|
|
517
|
+
}
|
|
518
|
+
}
|
|
519
|
+
}
|
|
520
|
+
const finalEvent = buffer.trim() ? parseSSEEvent(buffer.trim()) : null;
|
|
521
|
+
if (finalEvent?.event === "proposals") {
|
|
522
|
+
receivedProposals = finalEvent.data.proposals || [];
|
|
523
|
+
setProposals(receivedProposals);
|
|
524
|
+
setTokenUsage(finalEvent.data.usage || null);
|
|
525
|
+
}
|
|
526
|
+
if (finalEvent?.event === "error") {
|
|
527
|
+
throw new Error(finalEvent.data.error || "AI request failed");
|
|
528
|
+
}
|
|
529
|
+
if (receivedProposals.length === 0) {
|
|
530
|
+
clearInput();
|
|
270
531
|
}
|
|
271
|
-
setError("");
|
|
272
|
-
setResponse(result.text || "");
|
|
273
|
-
setProposals(result.proposals || []);
|
|
274
532
|
} catch (err) {
|
|
275
533
|
setProposals([]);
|
|
276
534
|
setResponse("");
|
|
535
|
+
setTokenUsage(null);
|
|
277
536
|
setError(err instanceof Error ? err.message : "AI request failed");
|
|
278
537
|
} finally{
|
|
279
538
|
setIsLoading(false);
|
|
@@ -288,7 +547,10 @@ export const AIInput = ()=>{
|
|
|
288
547
|
path: "/ai-apply-action"
|
|
289
548
|
}), {
|
|
290
549
|
body: JSON.stringify({
|
|
291
|
-
|
|
550
|
+
aiResponse: response,
|
|
551
|
+
prompt,
|
|
552
|
+
proposal,
|
|
553
|
+
tokenUsage
|
|
292
554
|
}),
|
|
293
555
|
headers: {
|
|
294
556
|
"Content-Type": "application/json"
|
|
@@ -305,6 +567,14 @@ export const AIInput = ()=>{
|
|
|
305
567
|
setError("");
|
|
306
568
|
setProposals([]);
|
|
307
569
|
setResponse("");
|
|
570
|
+
setTokenUsage(null);
|
|
571
|
+
if (result.change) {
|
|
572
|
+
setAppliedChanges((current)=>[
|
|
573
|
+
result.change,
|
|
574
|
+
...current
|
|
575
|
+
].slice(0, 10));
|
|
576
|
+
}
|
|
577
|
+
void loadRecentChanges().catch(()=>undefined);
|
|
308
578
|
clearInput();
|
|
309
579
|
} catch (err) {
|
|
310
580
|
setError(err instanceof Error ? err.message : "Could not apply proposal");
|
|
@@ -313,6 +583,8 @@ export const AIInput = ()=>{
|
|
|
313
583
|
}
|
|
314
584
|
};
|
|
315
585
|
return /*#__PURE__*/ React.createElement("div", {
|
|
586
|
+
className: styles.chatLayout
|
|
587
|
+
}, /*#__PURE__*/ React.createElement("div", {
|
|
316
588
|
className: styles.chat
|
|
317
589
|
}, /*#__PURE__*/ React.createElement("div", {
|
|
318
590
|
className: styles.chatHeader
|
|
@@ -334,7 +606,7 @@ export const AIInput = ()=>{
|
|
|
334
606
|
if (!value.trim()) {
|
|
335
607
|
setMentions([]);
|
|
336
608
|
}
|
|
337
|
-
updateMentionState(
|
|
609
|
+
updateMentionState(getTextBeforeCaret(event.currentTarget));
|
|
338
610
|
},
|
|
339
611
|
onKeyDown: (event)=>{
|
|
340
612
|
if (event.key === "ArrowDown" && mentionRange && mentionSuggestions.length > 0) {
|
|
@@ -353,11 +625,15 @@ export const AIInput = ()=>{
|
|
|
353
625
|
ref: editorRef,
|
|
354
626
|
role: "textbox",
|
|
355
627
|
suppressContentEditableWarning: true
|
|
356
|
-
})), mentionRange
|
|
628
|
+
})), mentionRange && /*#__PURE__*/ React.createElement(MentionPopover, {
|
|
357
629
|
containerRef: mentionPopoverRef,
|
|
358
630
|
onSelect: insertMention,
|
|
631
|
+
style: mentionPopoverPosition ? {
|
|
632
|
+
left: `${mentionPopoverPosition.left}px`,
|
|
633
|
+
top: `${mentionPopoverPosition.top}px`
|
|
634
|
+
} : undefined,
|
|
359
635
|
suggestions: mentionSuggestions
|
|
360
|
-
})
|
|
636
|
+
})), /*#__PURE__*/ React.createElement("div", {
|
|
361
637
|
className: styles.chatActionsRow
|
|
362
638
|
}, /*#__PURE__*/ React.createElement("div", {
|
|
363
639
|
className: styles.settings
|
|
@@ -382,7 +658,11 @@ export const AIInput = ()=>{
|
|
|
382
658
|
disabled: !prompt.trim() || !settingsProvider || !selectedModel || isLoading,
|
|
383
659
|
onClick: ()=>void handleSubmit(),
|
|
384
660
|
type: "button"
|
|
385
|
-
},
|
|
661
|
+
}, /*#__PURE__*/ React.createElement(Send, {
|
|
662
|
+
width: 14,
|
|
663
|
+
height: 14
|
|
664
|
+
}), isLoading ? "Sending..." : "Send")), /*#__PURE__*/ React.createElement(ActionToast, {
|
|
665
|
+
apiRoute: config.routes.api,
|
|
386
666
|
appliedProposalIndexes: appliedProposalIndexes,
|
|
387
667
|
description: response,
|
|
388
668
|
error: error,
|
|
@@ -393,12 +673,18 @@ export const AIInput = ()=>{
|
|
|
393
673
|
setError("");
|
|
394
674
|
setProposals([]);
|
|
395
675
|
setResponse("");
|
|
676
|
+
setTokenUsage(null);
|
|
396
677
|
clearInput();
|
|
397
678
|
},
|
|
398
679
|
onDismissError: ()=>{
|
|
399
680
|
setError("");
|
|
400
681
|
},
|
|
401
682
|
onApply: (proposal, _index)=>void handleApplyProposal(proposal),
|
|
402
|
-
proposals: proposals
|
|
683
|
+
proposals: proposals,
|
|
684
|
+
tokenUsage: tokenUsage
|
|
685
|
+
})), /*#__PURE__*/ React.createElement(RecentChangesList, {
|
|
686
|
+
allChangesURL: allChangesURL,
|
|
687
|
+
changes: appliedChanges
|
|
403
688
|
}));
|
|
404
689
|
};
|
|
690
|
+
export default AIInput;
|
|
@@ -1,8 +1,15 @@
|
|
|
1
|
+
.chatLayout {
|
|
2
|
+
align-items: start;
|
|
3
|
+
display: grid;
|
|
4
|
+
gap: 16px;
|
|
5
|
+
grid-template-columns: minmax(0, 1fr) minmax(0, 1fr);
|
|
6
|
+
margin-bottom: 24px;
|
|
7
|
+
max-width: 1280px;
|
|
8
|
+
}
|
|
9
|
+
|
|
1
10
|
.chat {
|
|
2
11
|
border: 1px solid var(--theme-elevation-150);
|
|
3
12
|
border-radius: 8px;
|
|
4
|
-
margin-bottom: 24px;
|
|
5
|
-
max-width: 920px;
|
|
6
13
|
padding: 20px;
|
|
7
14
|
}
|
|
8
15
|
|
|
@@ -52,7 +59,7 @@
|
|
|
52
59
|
|
|
53
60
|
.selectProviderIcon {
|
|
54
61
|
color: var(--theme-text);
|
|
55
|
-
height:
|
|
62
|
+
height: 12px;
|
|
56
63
|
left: 10px;
|
|
57
64
|
pointer-events: none;
|
|
58
65
|
position: absolute;
|
|
@@ -67,7 +74,7 @@
|
|
|
67
74
|
border-radius: 4px;
|
|
68
75
|
color: var(--theme-text);
|
|
69
76
|
font: inherit;
|
|
70
|
-
min-height:
|
|
77
|
+
min-height: 24px;
|
|
71
78
|
min-width: 160px;
|
|
72
79
|
padding: 0 28px 0 34px;
|
|
73
80
|
}
|
|
@@ -145,7 +152,16 @@
|
|
|
145
152
|
}
|
|
146
153
|
|
|
147
154
|
.prefix {
|
|
155
|
+
align-items: center;
|
|
148
156
|
color: var(--theme-elevation-500);
|
|
157
|
+
display: inline-flex;
|
|
158
|
+
gap: 3px;
|
|
159
|
+
}
|
|
160
|
+
|
|
161
|
+
.badgeIcon {
|
|
162
|
+
flex: 0 0 auto;
|
|
163
|
+
height: 12px;
|
|
164
|
+
width: 12px;
|
|
149
165
|
}
|
|
150
166
|
|
|
151
167
|
.name {
|
|
@@ -172,15 +188,29 @@
|
|
|
172
188
|
border-color: color-mix(in srgb, #7a5c9e 28%, var(--theme-elevation-150));
|
|
173
189
|
}
|
|
174
190
|
|
|
191
|
+
.locale {
|
|
192
|
+
background: color-mix(in srgb, #2f5da8 12%, var(--theme-bg));
|
|
193
|
+
border-color: color-mix(in srgb, #2f5da8 28%, var(--theme-elevation-150));
|
|
194
|
+
}
|
|
195
|
+
|
|
175
196
|
.chatButton {
|
|
197
|
+
align-items: center;
|
|
176
198
|
background: var(--theme-text);
|
|
177
199
|
border: 0;
|
|
178
200
|
border-radius: 4px;
|
|
179
201
|
color: var(--theme-bg);
|
|
180
202
|
cursor: pointer;
|
|
203
|
+
display: inline-flex;
|
|
181
204
|
font: inherit;
|
|
182
|
-
|
|
183
|
-
|
|
205
|
+
gap: 6px;
|
|
206
|
+
justify-content: center;
|
|
207
|
+
line-height: 24px;
|
|
208
|
+
min-height: 24px;
|
|
209
|
+
padding: 0 14px;
|
|
210
|
+
}
|
|
211
|
+
|
|
212
|
+
.chatButton svg {
|
|
213
|
+
flex: 0 0 auto;
|
|
184
214
|
}
|
|
185
215
|
|
|
186
216
|
.chatButton:disabled {
|
|
@@ -220,6 +250,10 @@
|
|
|
220
250
|
}
|
|
221
251
|
|
|
222
252
|
@media (max-width: 768px) {
|
|
253
|
+
.chatLayout {
|
|
254
|
+
grid-template-columns: minmax(0, 1fr);
|
|
255
|
+
}
|
|
256
|
+
|
|
223
257
|
.chatHeader {
|
|
224
258
|
flex-direction: column;
|
|
225
259
|
gap: 12px;
|
|
@@ -251,4 +285,5 @@
|
|
|
251
285
|
.chatButton {
|
|
252
286
|
align-self: stretch;
|
|
253
287
|
}
|
|
288
|
+
|
|
254
289
|
}
|
|
@@ -7,15 +7,13 @@ const getFieldClassName = ({ className, isReadOnly, showError })=>[
|
|
|
7
7
|
showError ? "error" : null,
|
|
8
8
|
isReadOnly ? "read-only" : null
|
|
9
9
|
].filter(Boolean).join(" ");
|
|
10
|
-
|
|
10
|
+
const APIKeyField = ({ field, inputRef, path, readOnly })=>{
|
|
11
11
|
const { disabled, errorMessage, setValue, showError, value } = useField({
|
|
12
12
|
path
|
|
13
13
|
});
|
|
14
14
|
const fieldID = `field-${path.replace(/\./g, "__")}`;
|
|
15
15
|
const isReadOnly = readOnly || disabled || field.admin?.disabled;
|
|
16
|
-
const handleChange = (event)=>
|
|
17
|
-
setValue(event.target.value);
|
|
18
|
-
};
|
|
16
|
+
const handleChange = (event)=>setValue(event.target.value);
|
|
19
17
|
return /*#__PURE__*/ React.createElement("div", {
|
|
20
18
|
className: getFieldClassName({
|
|
21
19
|
className: field.admin?.className,
|
|
@@ -45,3 +43,4 @@ export const AIApiKeyField = ({ field, inputRef, path, readOnly })=>{
|
|
|
45
43
|
className: "field-description"
|
|
46
44
|
}, field.admin?.description?.toString())));
|
|
47
45
|
};
|
|
46
|
+
export default APIKeyField;
|