@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.
Files changed (65) hide show
  1. package/README.md +62 -6
  2. package/dist/ai/{proposals.d.ts → proposalSigning.d.ts} +1 -1
  3. package/dist/ai/{proposals.js → proposalSigning.js} +6 -11
  4. package/dist/ai/providerOptions.d.ts +17 -1
  5. package/dist/ai/providerOptions.js +35 -7
  6. package/dist/ai/providerRuntime.d.ts +2 -2
  7. package/dist/ai/providerRuntime.js +50 -16
  8. package/dist/ai/sensitiveData.js +2 -6
  9. package/dist/components/AIInput.d.ts +2 -1
  10. package/dist/components/AIInput.js +327 -41
  11. package/dist/components/AIInput.module.css +41 -6
  12. package/dist/components/APIKeyField.d.ts +3 -0
  13. package/dist/components/{AIApiKeyField.js → APIKeyField.js} +3 -4
  14. package/dist/components/ActionToast.d.ts +30 -0
  15. package/dist/components/{AIActionProposalList.js → ActionToast.js} +78 -8
  16. package/dist/components/{AIActionProposalList.module.css → ActionToast.module.css} +49 -5
  17. package/dist/components/AuditLogList.d.ts +32 -0
  18. package/dist/components/AuditLogList.js +67 -0
  19. package/dist/components/AuditLogList.module.css +152 -0
  20. package/dist/components/DiffDialog.d.ts +19 -0
  21. package/dist/components/DiffDialog.js +396 -0
  22. package/dist/components/DiffDialog.module.css +333 -0
  23. package/dist/components/Icons.d.ts +9 -0
  24. package/dist/components/Icons.js +193 -0
  25. package/dist/components/MentionPopover.d.ts +19 -0
  26. package/dist/components/{CollectionMentionPopover.js → MentionPopover.js} +11 -4
  27. package/dist/components/{CollectionMentionPopover.module.css → MentionPopover.module.css} +0 -2
  28. package/dist/components/hooks/useAISettings.d.ts +6 -4
  29. package/dist/components/hooks/useAISettings.js +18 -3
  30. package/dist/components/hooks/useDocumentMentionSuggestions.d.ts +3 -3
  31. package/dist/components/hooks/useDocumentMentionSuggestions.js +2 -2
  32. package/dist/exports/client.d.ts +4 -2
  33. package/dist/exports/client.js +4 -2
  34. package/dist/handlers/applyActionHandler.d.ts +8 -0
  35. package/dist/handlers/applyActionHandler.js +563 -0
  36. package/dist/handlers/auditLogHandler.d.ts +6 -0
  37. package/dist/handlers/auditLogHandler.js +39 -0
  38. package/dist/handlers/chatHandler.d.ts +42 -0
  39. package/dist/handlers/chatHandler.js +634 -0
  40. package/dist/handlers/mentionSuggestionHandler.d.ts +7 -0
  41. package/dist/handlers/mentionSuggestionHandler.js +97 -0
  42. package/dist/handlers/proposalDiffHandler.d.ts +7 -0
  43. package/dist/handlers/proposalDiffHandler.js +208 -0
  44. package/dist/index.d.ts +6 -6
  45. package/dist/index.js +151 -8
  46. package/dist/payload/collectionPermissions.d.ts +14 -13
  47. package/dist/payload/collectionPermissions.js +3 -2
  48. package/dist/payload/normalizeData.d.ts +8 -0
  49. package/dist/payload/normalizeData.js +25 -3
  50. package/dist/payload/schemaContext.d.ts +79 -9
  51. package/dist/payload/schemaContext.js +231 -22
  52. package/dist/payload/shared.d.ts +33 -0
  53. package/dist/payload/shared.js +114 -0
  54. package/package.json +7 -2
  55. package/dist/components/AIActionProposalList.d.ts +0 -25
  56. package/dist/components/AIApiKeyField.d.ts +0 -2
  57. package/dist/components/CollectionMentionPopover.d.ts +0 -16
  58. package/dist/components/hooks/utils.d.ts +0 -1
  59. package/dist/components/hooks/utils.js +0 -3
  60. package/dist/endpoints/aiApplyActionEndpointHandler.d.ts +0 -7
  61. package/dist/endpoints/aiApplyActionEndpointHandler.js +0 -169
  62. package/dist/endpoints/aiChatEndpointHandler.d.ts +0 -35
  63. package/dist/endpoints/aiChatEndpointHandler.js +0 -321
  64. package/dist/endpoints/aiMentionSuggestionsEndpointHandler.d.ts +0 -7
  65. 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 { AIActionProposalList } from "./AIActionProposalList.js";
7
+ import { ActionToast } from "./ActionToast.js";
8
8
  import styles from "./AIInput.module.css";
9
- import { CollectionMentionPopover } from "./CollectionMentionPopover.js";
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
- export const AIInput = ()=>{
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 getCaretOffset = (element)=>{
296
+ const getTextBeforeCaret = (element)=>{
125
297
  const selection = window.getSelection();
126
- if (!selection || selection.rangeCount === 0) return 0;
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().length;
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 = (value, caretPosition)=>{
173
- const valueBeforeCaret = value.slice(0, caretPosition);
174
- const match = /(?:^|\s)@([\w-]*)$/.exec(valueBeforeCaret);
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
- const atIndex = valueBeforeCaret.lastIndexOf("@");
181
- setMentionQuery(match[1] || "");
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 beforeMention = prompt.slice(0, mentionRange.start);
206
- const afterMention = prompt.slice(mentionRange.end);
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}${promptText} ${afterMention}`);
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
- setProposals([]);
268
- setResponse("");
269
- throw new Error(result.error || "AI request failed");
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
- proposal
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(value, getCaretOffset(event.currentTarget));
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 ? /*#__PURE__*/ React.createElement(CollectionMentionPopover, {
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
- }) : null), /*#__PURE__*/ React.createElement("div", {
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
- }, isLoading ? "Sending..." : "Send")), /*#__PURE__*/ React.createElement(AIActionProposalList, {
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: 16px;
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: 34px;
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
- min-height: 40px;
183
- padding: 0 18px;
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
  }
@@ -0,0 +1,3 @@
1
+ import type { TextFieldClientComponent } from "payload";
2
+ declare const APIKeyField: TextFieldClientComponent;
3
+ export default APIKeyField;
@@ -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
- export const AIApiKeyField = ({ field, inputRef, path, readOnly })=>{
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;