@mvriu5/payload-ai 0.5.9 → 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 +210 -39
- package/dist/components/AIInput.module.css +15 -1
- 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} +10 -8
- package/dist/components/AuditLogList.d.ts +32 -0
- package/dist/components/{RecentChangesList.js → AuditLogList.js} +13 -12
- package/dist/components/{RecentChangesList.module.css → AuditLogList.module.css} +18 -1
- package/dist/components/DiffDialog.d.ts +10 -3
- package/dist/components/DiffDialog.js +164 -78
- package/dist/components/DiffDialog.module.css +86 -2
- package/dist/components/Icons.d.ts +5 -0
- package/dist/components/Icons.js +109 -3
- 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/{endpoints/aiRecentChangesEndpointHandler.js → handlers/auditLogHandler.js} +12 -8
- 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 +50 -18
- 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 +43 -16
- package/dist/payload/schemaContext.js +139 -18
- 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 -26
- package/dist/components/AIApiKeyField.d.ts +0 -2
- package/dist/components/CollectionMentionPopover.d.ts +0 -16
- package/dist/components/RecentChangesList.d.ts +0 -18
- package/dist/components/hooks/utils.d.ts +0 -1
- package/dist/components/hooks/utils.js +0 -3
- package/dist/endpoints/aiApplyActionEndpointHandler.d.ts +0 -8
- package/dist/endpoints/aiApplyActionEndpointHandler.js +0 -365
- package/dist/endpoints/aiChatEndpointHandler.d.ts +0 -35
- package/dist/endpoints/aiChatEndpointHandler.js +0 -382
- package/dist/endpoints/aiMentionSuggestionsEndpointHandler.d.ts +0 -7
- package/dist/endpoints/aiMentionSuggestionsEndpointHandler.js +0 -63
- package/dist/endpoints/aiProposalDiffEndpointHandler.d.ts +0 -7
- package/dist/endpoints/aiProposalDiffEndpointHandler.js +0 -138
- package/dist/endpoints/aiRecentChangesEndpointHandler.d.ts +0 -6
- /package/dist/components/{AIActionProposalList.module.css → ActionToast.module.css} +0 -0
|
@@ -4,11 +4,11 @@ import { formatAdminURL } from "payload/shared";
|
|
|
4
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, Send } from "./Icons.js";
|
|
11
|
-
import { RecentChangesList } from "./
|
|
9
|
+
import { MentionPopover } from "./MentionPopover.js";
|
|
10
|
+
import { ClaudeIcon, GoogleGeminiIcon, MistralAiIcon, OpenaiIcon, OpenrouterIcon, Send } from "./Icons.js";
|
|
11
|
+
import { RecentChangesList } from "./AuditLogList.js";
|
|
12
12
|
import { useAISettings } from "./hooks/useAISettings.js";
|
|
13
13
|
import { useDocumentMentionSuggestions } from "./hooks/useDocumentMentionSuggestions.js";
|
|
14
14
|
const collectBlockOptions = ({ fields, parent })=>{
|
|
@@ -51,6 +51,8 @@ const getProviderIcon = (provider)=>{
|
|
|
51
51
|
return /*#__PURE__*/ React.createElement(MistralAiIcon, iconProps);
|
|
52
52
|
case "openai":
|
|
53
53
|
return /*#__PURE__*/ React.createElement(OpenaiIcon, iconProps);
|
|
54
|
+
case "openrouter":
|
|
55
|
+
return /*#__PURE__*/ React.createElement(OpenrouterIcon, iconProps);
|
|
54
56
|
default:
|
|
55
57
|
return null;
|
|
56
58
|
}
|
|
@@ -83,7 +85,83 @@ const parseSSEEvent = (chunk)=>{
|
|
|
83
85
|
}
|
|
84
86
|
};
|
|
85
87
|
const sanitizeResponseText = (value)=>value.replace(/\*\*/g, "");
|
|
86
|
-
|
|
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 = ()=>{
|
|
87
165
|
const { config } = useConfig();
|
|
88
166
|
const editorRef = useRef(null);
|
|
89
167
|
const mentionPopoverRef = useRef(null);
|
|
@@ -99,9 +177,11 @@ export const AIInput = ()=>{
|
|
|
99
177
|
});
|
|
100
178
|
const [mentionQuery, setMentionQuery] = useState("");
|
|
101
179
|
const [mentionRange, setMentionRange] = useState(null);
|
|
180
|
+
const [mentionPopoverPosition, setMentionPopoverPosition] = useState(null);
|
|
102
181
|
const [mentions, setMentions] = useState([]);
|
|
103
182
|
const [appliedProposalIndexes, setAppliedProposalIndexes] = useState([]);
|
|
104
183
|
const [response, setResponse] = useState("");
|
|
184
|
+
const [tokenUsage, setTokenUsage] = useState(null);
|
|
105
185
|
const [error, setError] = useState("");
|
|
106
186
|
const [proposals, setProposals] = useState([]);
|
|
107
187
|
const [appliedChanges, setAppliedChanges] = useState([]);
|
|
@@ -114,15 +194,18 @@ export const AIInput = ()=>{
|
|
|
114
194
|
const isCollectionMentionEnabled = (slug)=>!enabledCollectionSlugSet || enabledCollectionSlugSet.has(slug);
|
|
115
195
|
const recentChangesEndpoint = useMemo(()=>formatAdminURL({
|
|
116
196
|
apiRoute: config.routes.api,
|
|
117
|
-
path: "/ai-
|
|
197
|
+
path: "/ai-audit-log"
|
|
118
198
|
}), [
|
|
119
199
|
config.routes.api
|
|
120
200
|
]);
|
|
201
|
+
const allChangesURL = useMemo(()=>`${config.routes.admin || "/admin"}/collections/${auditLogCollectionSlug}`, [
|
|
202
|
+
config.routes.admin
|
|
203
|
+
]);
|
|
121
204
|
const loadRecentChanges = useCallback(async ()=>{
|
|
122
205
|
const res = await fetch(recentChangesEndpoint);
|
|
123
206
|
const result = await res.json().catch(()=>null);
|
|
124
207
|
if (res.ok && result?.changes) {
|
|
125
|
-
setAppliedChanges(result.changes);
|
|
208
|
+
setAppliedChanges(result.changes.slice(0, 10));
|
|
126
209
|
}
|
|
127
210
|
}, [
|
|
128
211
|
recentChangesEndpoint
|
|
@@ -155,6 +238,31 @@ export const AIInput = ()=>{
|
|
|
155
238
|
slug: global.slug,
|
|
156
239
|
type: "global"
|
|
157
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
|
+
});
|
|
158
266
|
const blocks = [
|
|
159
267
|
...config.collections.filter((collection)=>isCollectionMentionEnabled(collection.slug)).flatMap((collection)=>collectBlockOptions({
|
|
160
268
|
fields: collection.fields,
|
|
@@ -168,11 +276,12 @@ export const AIInput = ()=>{
|
|
|
168
276
|
const mentionOptions = [
|
|
169
277
|
...collections,
|
|
170
278
|
...globals,
|
|
171
|
-
...blocks
|
|
279
|
+
...blocks,
|
|
280
|
+
...locales
|
|
172
281
|
];
|
|
173
282
|
const normalizedMentionQuery = mentionQuery.toLowerCase();
|
|
174
283
|
const filteredCollections = collections.filter((collection)=>collection.slug.toLowerCase().includes(normalizedMentionQuery) || collection.label.toLowerCase().includes(normalizedMentionQuery));
|
|
175
|
-
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));
|
|
176
285
|
const documentSuggestionCollection = filteredCollections.length === 1 ? filteredCollections[0]?.slug : null;
|
|
177
286
|
const { documentSuggestions, resetDocumentSuggestions } = useDocumentMentionSuggestions({
|
|
178
287
|
apiRoute: config.routes.api,
|
|
@@ -184,14 +293,14 @@ export const AIInput = ()=>{
|
|
|
184
293
|
...filteredMentionOptions,
|
|
185
294
|
...documentSuggestions
|
|
186
295
|
];
|
|
187
|
-
const
|
|
296
|
+
const getTextBeforeCaret = (element)=>{
|
|
188
297
|
const selection = window.getSelection();
|
|
189
|
-
if (!selection || selection.rangeCount === 0) return
|
|
298
|
+
if (!selection || selection.rangeCount === 0) return "";
|
|
190
299
|
const range = selection.getRangeAt(0);
|
|
191
300
|
const clonedRange = range.cloneRange();
|
|
192
301
|
clonedRange.selectNodeContents(element);
|
|
193
302
|
clonedRange.setEnd(range.endContainer, range.endOffset);
|
|
194
|
-
return clonedRange.toString()
|
|
303
|
+
return clonedRange.toString();
|
|
195
304
|
};
|
|
196
305
|
const getTextNodeAtOffset = (element, offset)=>{
|
|
197
306
|
const walker = document.createTreeWalker(element, NodeFilter.SHOW_TEXT);
|
|
@@ -215,6 +324,28 @@ export const AIInput = ()=>{
|
|
|
215
324
|
offset: 0
|
|
216
325
|
};
|
|
217
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
|
+
}, []);
|
|
218
349
|
const replaceTextRangeWithBadge = ({ badge, editor, end, start })=>{
|
|
219
350
|
const startPosition = getTextNodeAtOffset(editor, start);
|
|
220
351
|
const endPosition = getTextNodeAtOffset(editor, end);
|
|
@@ -232,21 +363,41 @@ export const AIInput = ()=>{
|
|
|
232
363
|
selection?.removeAllRanges();
|
|
233
364
|
selection?.addRange(caretRange);
|
|
234
365
|
};
|
|
235
|
-
const updateMentionState = (
|
|
236
|
-
const
|
|
237
|
-
|
|
238
|
-
if (!match || typeof match.index !== "number") {
|
|
366
|
+
const updateMentionState = (valueBeforeCaret)=>{
|
|
367
|
+
const activeMention = getActiveMentionRange(valueBeforeCaret);
|
|
368
|
+
if (!activeMention) {
|
|
239
369
|
setMentionQuery("");
|
|
240
370
|
setMentionRange(null);
|
|
371
|
+
setMentionPopoverPosition(null);
|
|
241
372
|
return;
|
|
242
373
|
}
|
|
243
|
-
|
|
244
|
-
|
|
245
|
-
setMentionRange({
|
|
246
|
-
end: caretPosition,
|
|
247
|
-
start: atIndex
|
|
248
|
-
});
|
|
374
|
+
setMentionQuery(activeMention.query);
|
|
375
|
+
setMentionRange(activeMention.range);
|
|
249
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
|
+
]);
|
|
250
401
|
const clearInput = ()=>{
|
|
251
402
|
setPrompt("");
|
|
252
403
|
setMentions([]);
|
|
@@ -265,12 +416,12 @@ export const AIInput = ()=>{
|
|
|
265
416
|
const insertMention = (suggestion)=>{
|
|
266
417
|
const editor = editorRef.current;
|
|
267
418
|
if (!mentionRange || !editor) return;
|
|
268
|
-
const
|
|
269
|
-
const
|
|
419
|
+
const currentValue = editor.innerText;
|
|
420
|
+
const beforeMention = currentValue.slice(0, mentionRange.start);
|
|
421
|
+
const afterMention = currentValue.slice(mentionRange.end);
|
|
270
422
|
const badgeType = suggestion.type === "doc" ? "document" : suggestion.type;
|
|
271
423
|
const badgePrefix = `${badgeType}:`;
|
|
272
424
|
const badgeText = `${badgePrefix} ${suggestion.label}`;
|
|
273
|
-
const promptText = suggestion.type === "doc" ? `${badgeText} (${suggestion.collection}/${suggestion.id})` : badgeText;
|
|
274
425
|
const badge = document.createElement("span");
|
|
275
426
|
badge.className = [
|
|
276
427
|
styles.badge,
|
|
@@ -278,10 +429,7 @@ export const AIInput = ()=>{
|
|
|
278
429
|
styles.inlineBadge
|
|
279
430
|
].join(" ");
|
|
280
431
|
badge.contentEditable = "false";
|
|
281
|
-
badge.append(Object.assign(document.createElement("span"), {
|
|
282
|
-
className: styles.prefix,
|
|
283
|
-
textContent: `${badgePrefix} `
|
|
284
|
-
}), Object.assign(document.createElement("span"), {
|
|
432
|
+
badge.append(createBadgePrefix(suggestion), Object.assign(document.createElement("span"), {
|
|
285
433
|
className: styles.name,
|
|
286
434
|
textContent: suggestion.label
|
|
287
435
|
}));
|
|
@@ -292,7 +440,7 @@ export const AIInput = ()=>{
|
|
|
292
440
|
start: mentionRange.start
|
|
293
441
|
});
|
|
294
442
|
editor.focus();
|
|
295
|
-
setPrompt(`${beforeMention}${
|
|
443
|
+
setPrompt(`${beforeMention}${badgeText} ${afterMention}`);
|
|
296
444
|
setMentions((currentMentions)=>{
|
|
297
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);
|
|
298
446
|
if (mentionExists) return currentMentions;
|
|
@@ -303,6 +451,7 @@ export const AIInput = ()=>{
|
|
|
303
451
|
});
|
|
304
452
|
setMentionQuery("");
|
|
305
453
|
setMentionRange(null);
|
|
454
|
+
setMentionPopoverPosition(null);
|
|
306
455
|
resetDocumentSuggestions();
|
|
307
456
|
};
|
|
308
457
|
const handleSubmit = async ()=>{
|
|
@@ -313,6 +462,7 @@ export const AIInput = ()=>{
|
|
|
313
462
|
setError("");
|
|
314
463
|
setProposals([]);
|
|
315
464
|
setResponse("");
|
|
465
|
+
setTokenUsage(null);
|
|
316
466
|
try {
|
|
317
467
|
const res = await fetch(formatAdminURL({
|
|
318
468
|
apiRoute: config.routes.api,
|
|
@@ -338,6 +488,7 @@ export const AIInput = ()=>{
|
|
|
338
488
|
const reader = res.body.getReader();
|
|
339
489
|
const decoder = new TextDecoder();
|
|
340
490
|
let buffer = "";
|
|
491
|
+
let receivedProposals = [];
|
|
341
492
|
while(true){
|
|
342
493
|
const { done, value } = await reader.read();
|
|
343
494
|
if (done) break;
|
|
@@ -356,7 +507,9 @@ export const AIInput = ()=>{
|
|
|
356
507
|
continue;
|
|
357
508
|
}
|
|
358
509
|
if (event.event === "proposals") {
|
|
359
|
-
|
|
510
|
+
receivedProposals = event.data.proposals || [];
|
|
511
|
+
setProposals(receivedProposals);
|
|
512
|
+
setTokenUsage(event.data.usage || null);
|
|
360
513
|
continue;
|
|
361
514
|
}
|
|
362
515
|
if (event.event === "error") {
|
|
@@ -366,14 +519,20 @@ export const AIInput = ()=>{
|
|
|
366
519
|
}
|
|
367
520
|
const finalEvent = buffer.trim() ? parseSSEEvent(buffer.trim()) : null;
|
|
368
521
|
if (finalEvent?.event === "proposals") {
|
|
369
|
-
|
|
522
|
+
receivedProposals = finalEvent.data.proposals || [];
|
|
523
|
+
setProposals(receivedProposals);
|
|
524
|
+
setTokenUsage(finalEvent.data.usage || null);
|
|
370
525
|
}
|
|
371
526
|
if (finalEvent?.event === "error") {
|
|
372
527
|
throw new Error(finalEvent.data.error || "AI request failed");
|
|
373
528
|
}
|
|
529
|
+
if (receivedProposals.length === 0) {
|
|
530
|
+
clearInput();
|
|
531
|
+
}
|
|
374
532
|
} catch (err) {
|
|
375
533
|
setProposals([]);
|
|
376
534
|
setResponse("");
|
|
535
|
+
setTokenUsage(null);
|
|
377
536
|
setError(err instanceof Error ? err.message : "AI request failed");
|
|
378
537
|
} finally{
|
|
379
538
|
setIsLoading(false);
|
|
@@ -388,7 +547,10 @@ export const AIInput = ()=>{
|
|
|
388
547
|
path: "/ai-apply-action"
|
|
389
548
|
}), {
|
|
390
549
|
body: JSON.stringify({
|
|
391
|
-
|
|
550
|
+
aiResponse: response,
|
|
551
|
+
prompt,
|
|
552
|
+
proposal,
|
|
553
|
+
tokenUsage
|
|
392
554
|
}),
|
|
393
555
|
headers: {
|
|
394
556
|
"Content-Type": "application/json"
|
|
@@ -405,11 +567,12 @@ export const AIInput = ()=>{
|
|
|
405
567
|
setError("");
|
|
406
568
|
setProposals([]);
|
|
407
569
|
setResponse("");
|
|
570
|
+
setTokenUsage(null);
|
|
408
571
|
if (result.change) {
|
|
409
572
|
setAppliedChanges((current)=>[
|
|
410
573
|
result.change,
|
|
411
574
|
...current
|
|
412
|
-
].slice(0,
|
|
575
|
+
].slice(0, 10));
|
|
413
576
|
}
|
|
414
577
|
void loadRecentChanges().catch(()=>undefined);
|
|
415
578
|
clearInput();
|
|
@@ -443,7 +606,7 @@ export const AIInput = ()=>{
|
|
|
443
606
|
if (!value.trim()) {
|
|
444
607
|
setMentions([]);
|
|
445
608
|
}
|
|
446
|
-
updateMentionState(
|
|
609
|
+
updateMentionState(getTextBeforeCaret(event.currentTarget));
|
|
447
610
|
},
|
|
448
611
|
onKeyDown: (event)=>{
|
|
449
612
|
if (event.key === "ArrowDown" && mentionRange && mentionSuggestions.length > 0) {
|
|
@@ -462,11 +625,15 @@ export const AIInput = ()=>{
|
|
|
462
625
|
ref: editorRef,
|
|
463
626
|
role: "textbox",
|
|
464
627
|
suppressContentEditableWarning: true
|
|
465
|
-
})), mentionRange
|
|
628
|
+
})), mentionRange && /*#__PURE__*/ React.createElement(MentionPopover, {
|
|
466
629
|
containerRef: mentionPopoverRef,
|
|
467
630
|
onSelect: insertMention,
|
|
631
|
+
style: mentionPopoverPosition ? {
|
|
632
|
+
left: `${mentionPopoverPosition.left}px`,
|
|
633
|
+
top: `${mentionPopoverPosition.top}px`
|
|
634
|
+
} : undefined,
|
|
468
635
|
suggestions: mentionSuggestions
|
|
469
|
-
})
|
|
636
|
+
})), /*#__PURE__*/ React.createElement("div", {
|
|
470
637
|
className: styles.chatActionsRow
|
|
471
638
|
}, /*#__PURE__*/ React.createElement("div", {
|
|
472
639
|
className: styles.settings
|
|
@@ -494,7 +661,7 @@ export const AIInput = ()=>{
|
|
|
494
661
|
}, /*#__PURE__*/ React.createElement(Send, {
|
|
495
662
|
width: 14,
|
|
496
663
|
height: 14
|
|
497
|
-
}), isLoading ? "Sending..." : "Send")), /*#__PURE__*/ React.createElement(
|
|
664
|
+
}), isLoading ? "Sending..." : "Send")), /*#__PURE__*/ React.createElement(ActionToast, {
|
|
498
665
|
apiRoute: config.routes.api,
|
|
499
666
|
appliedProposalIndexes: appliedProposalIndexes,
|
|
500
667
|
description: response,
|
|
@@ -506,14 +673,18 @@ export const AIInput = ()=>{
|
|
|
506
673
|
setError("");
|
|
507
674
|
setProposals([]);
|
|
508
675
|
setResponse("");
|
|
676
|
+
setTokenUsage(null);
|
|
509
677
|
clearInput();
|
|
510
678
|
},
|
|
511
679
|
onDismissError: ()=>{
|
|
512
680
|
setError("");
|
|
513
681
|
},
|
|
514
682
|
onApply: (proposal, _index)=>void handleApplyProposal(proposal),
|
|
515
|
-
proposals: proposals
|
|
683
|
+
proposals: proposals,
|
|
684
|
+
tokenUsage: tokenUsage
|
|
516
685
|
})), /*#__PURE__*/ React.createElement(RecentChangesList, {
|
|
686
|
+
allChangesURL: allChangesURL,
|
|
517
687
|
changes: appliedChanges
|
|
518
688
|
}));
|
|
519
689
|
};
|
|
690
|
+
export default AIInput;
|
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
.chatLayout {
|
|
2
|
-
align-items:
|
|
2
|
+
align-items: start;
|
|
3
3
|
display: grid;
|
|
4
4
|
gap: 16px;
|
|
5
5
|
grid-template-columns: minmax(0, 1fr) minmax(0, 1fr);
|
|
@@ -152,7 +152,16 @@
|
|
|
152
152
|
}
|
|
153
153
|
|
|
154
154
|
.prefix {
|
|
155
|
+
align-items: center;
|
|
155
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;
|
|
156
165
|
}
|
|
157
166
|
|
|
158
167
|
.name {
|
|
@@ -179,6 +188,11 @@
|
|
|
179
188
|
border-color: color-mix(in srgb, #7a5c9e 28%, var(--theme-elevation-150));
|
|
180
189
|
}
|
|
181
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
|
+
|
|
182
196
|
.chatButton {
|
|
183
197
|
align-items: center;
|
|
184
198
|
background: var(--theme-text);
|
|
@@ -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;
|
|
@@ -0,0 +1,30 @@
|
|
|
1
|
+
export type ActionProposal = {
|
|
2
|
+
_aiSignature?: {
|
|
3
|
+
expiresAt: string;
|
|
4
|
+
value: string;
|
|
5
|
+
};
|
|
6
|
+
action: "create" | "delete" | "update" | "updateGlobal";
|
|
7
|
+
collection?: string;
|
|
8
|
+
id?: string;
|
|
9
|
+
label: string;
|
|
10
|
+
slug?: string;
|
|
11
|
+
};
|
|
12
|
+
type ActionToastProps = {
|
|
13
|
+
apiRoute: string;
|
|
14
|
+
appliedProposalIndexes: number[];
|
|
15
|
+
description?: string;
|
|
16
|
+
error?: string;
|
|
17
|
+
getViewURL?: (proposal: ActionProposal) => string | null;
|
|
18
|
+
isApplying: boolean;
|
|
19
|
+
onDismiss?: () => void;
|
|
20
|
+
onDismissError?: () => void;
|
|
21
|
+
onApply: (proposal: ActionProposal, index: number) => void;
|
|
22
|
+
proposals: ActionProposal[];
|
|
23
|
+
tokenUsage?: {
|
|
24
|
+
inputTokens?: number;
|
|
25
|
+
outputTokens?: number;
|
|
26
|
+
totalTokens?: number;
|
|
27
|
+
} | null;
|
|
28
|
+
};
|
|
29
|
+
export declare const ActionToast: ({ apiRoute, appliedProposalIndexes, description, error, getViewURL, isApplying, onDismiss, onDismissError, onApply, proposals, tokenUsage, }: ActionToastProps) => import("react").JSX.Element | null;
|
|
30
|
+
export {};
|
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import styles from "./
|
|
1
|
+
import styles from "./ActionToast.module.css";
|
|
2
2
|
import { redactSensitiveData } from "../ai/sensitiveData.js";
|
|
3
3
|
import { formatAdminURL } from "payload/shared";
|
|
4
4
|
import { useState } from "react";
|
|
@@ -19,7 +19,7 @@ const getSafeProposalDetails = (proposal)=>{
|
|
|
19
19
|
}
|
|
20
20
|
return redactedProposal;
|
|
21
21
|
};
|
|
22
|
-
export const
|
|
22
|
+
export const ActionToast = ({ apiRoute, appliedProposalIndexes, description, error, getViewURL, isApplying, onDismiss, onDismissError, onApply, proposals, tokenUsage })=>{
|
|
23
23
|
const [activeDiff, setActiveDiff] = useState(null);
|
|
24
24
|
const [diffError, setDiffError] = useState("");
|
|
25
25
|
const [loadingDiffIndex, setLoadingDiffIndex] = useState(null);
|
|
@@ -47,6 +47,7 @@ export const AIActionProposalList = ({ apiRoute, appliedProposalIndexes, descrip
|
|
|
47
47
|
throw new Error(result?.error || "Could not load proposal diff.");
|
|
48
48
|
}
|
|
49
49
|
setActiveDiff({
|
|
50
|
+
change: null,
|
|
50
51
|
diff: {
|
|
51
52
|
after: result.after,
|
|
52
53
|
before: result.before
|
|
@@ -77,13 +78,13 @@ export const AIActionProposalList = ({ apiRoute, appliedProposalIndexes, descrip
|
|
|
77
78
|
className: styles.label
|
|
78
79
|
}, "AI response"), /*#__PURE__*/ React.createElement("div", {
|
|
79
80
|
className: styles.description
|
|
80
|
-
}, descriptionPreview), isDescriptionTruncated
|
|
81
|
+
}, descriptionPreview), isDescriptionTruncated && /*#__PURE__*/ React.createElement("details", {
|
|
81
82
|
className: styles.details
|
|
82
83
|
}, /*#__PURE__*/ React.createElement("summary", {
|
|
83
84
|
className: styles.summary
|
|
84
85
|
}, "Full response"), /*#__PURE__*/ React.createElement("pre", {
|
|
85
86
|
className: styles.proposalDetails
|
|
86
|
-
}, description))
|
|
87
|
+
}, description)))), proposals.map((proposal, index)=>{
|
|
87
88
|
const isApplied = appliedProposalIndexes.includes(index);
|
|
88
89
|
const viewURL = getViewURL?.(proposal);
|
|
89
90
|
return /*#__PURE__*/ React.createElement("div", {
|
|
@@ -144,7 +145,7 @@ export const AIActionProposalList = ({ apiRoute, appliedProposalIndexes, descrip
|
|
|
144
145
|
height: 16,
|
|
145
146
|
width: 16
|
|
146
147
|
})))));
|
|
147
|
-
}), diffError
|
|
148
|
+
}), diffError && /*#__PURE__*/ React.createElement("div", {
|
|
148
149
|
className: `${styles.item} ${styles.errorItem}`
|
|
149
150
|
}, /*#__PURE__*/ React.createElement("div", null, /*#__PURE__*/ React.createElement("div", {
|
|
150
151
|
className: styles.label
|
|
@@ -154,9 +155,10 @@ export const AIActionProposalList = ({ apiRoute, appliedProposalIndexes, descrip
|
|
|
154
155
|
className: styles.button,
|
|
155
156
|
onClick: ()=>setDiffError(""),
|
|
156
157
|
type: "button"
|
|
157
|
-
}, "Dismiss"))
|
|
158
|
+
}, "Dismiss")), activeDiff && /*#__PURE__*/ React.createElement(DiffDialog, {
|
|
158
159
|
diff: activeDiff.diff,
|
|
159
160
|
onClose: ()=>setActiveDiff(null),
|
|
160
|
-
proposal: activeDiff.proposal
|
|
161
|
-
|
|
161
|
+
proposal: activeDiff.proposal,
|
|
162
|
+
tokenUsage: tokenUsage || undefined
|
|
163
|
+
}));
|
|
162
164
|
};
|
|
@@ -0,0 +1,32 @@
|
|
|
1
|
+
import type { ActionProposal } from "./ActionToast.js";
|
|
2
|
+
import { type ProposalDiff } from "./DiffDialog.js";
|
|
3
|
+
export type AppliedChange = {
|
|
4
|
+
action?: ActionProposal["action"] | null;
|
|
5
|
+
additions: number;
|
|
6
|
+
after?: unknown;
|
|
7
|
+
before?: unknown;
|
|
8
|
+
collection?: string | null;
|
|
9
|
+
createdAt?: string | null;
|
|
10
|
+
documentID?: string | null;
|
|
11
|
+
inputTokens?: number | null;
|
|
12
|
+
outputTokens?: number | null;
|
|
13
|
+
removals: number;
|
|
14
|
+
slug?: string | null;
|
|
15
|
+
targetType?: string | null;
|
|
16
|
+
totalTokens?: number | null;
|
|
17
|
+
title: string;
|
|
18
|
+
userID?: string | null;
|
|
19
|
+
userLabel?: string | null;
|
|
20
|
+
url?: string | null;
|
|
21
|
+
};
|
|
22
|
+
type AuditLogListProps = {
|
|
23
|
+
allChangesURL?: string;
|
|
24
|
+
changes: AppliedChange[];
|
|
25
|
+
};
|
|
26
|
+
export type ActiveDiff = {
|
|
27
|
+
change: AppliedChange | null;
|
|
28
|
+
diff: ProposalDiff;
|
|
29
|
+
proposal: ActionProposal;
|
|
30
|
+
};
|
|
31
|
+
export declare const RecentChangesList: ({ allChangesURL, changes }: AuditLogListProps) => import("react").JSX.Element;
|
|
32
|
+
export {};
|