@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.
Files changed (69) 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 +210 -39
  11. package/dist/components/AIInput.module.css +15 -1
  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} +10 -8
  16. package/dist/components/AuditLogList.d.ts +32 -0
  17. package/dist/components/{RecentChangesList.js → AuditLogList.js} +13 -12
  18. package/dist/components/{RecentChangesList.module.css → AuditLogList.module.css} +18 -1
  19. package/dist/components/DiffDialog.d.ts +10 -3
  20. package/dist/components/DiffDialog.js +164 -78
  21. package/dist/components/DiffDialog.module.css +86 -2
  22. package/dist/components/Icons.d.ts +5 -0
  23. package/dist/components/Icons.js +109 -3
  24. package/dist/components/MentionPopover.d.ts +19 -0
  25. package/dist/components/{CollectionMentionPopover.js → MentionPopover.js} +11 -4
  26. package/dist/components/{CollectionMentionPopover.module.css → MentionPopover.module.css} +0 -2
  27. package/dist/components/hooks/useAISettings.d.ts +6 -4
  28. package/dist/components/hooks/useAISettings.js +18 -3
  29. package/dist/components/hooks/useDocumentMentionSuggestions.d.ts +3 -3
  30. package/dist/components/hooks/useDocumentMentionSuggestions.js +2 -2
  31. package/dist/exports/client.d.ts +4 -2
  32. package/dist/exports/client.js +4 -2
  33. package/dist/handlers/applyActionHandler.d.ts +8 -0
  34. package/dist/handlers/applyActionHandler.js +563 -0
  35. package/dist/handlers/auditLogHandler.d.ts +6 -0
  36. package/dist/{endpoints/aiRecentChangesEndpointHandler.js → handlers/auditLogHandler.js} +12 -8
  37. package/dist/handlers/chatHandler.d.ts +42 -0
  38. package/dist/handlers/chatHandler.js +634 -0
  39. package/dist/handlers/mentionSuggestionHandler.d.ts +7 -0
  40. package/dist/handlers/mentionSuggestionHandler.js +97 -0
  41. package/dist/handlers/proposalDiffHandler.d.ts +7 -0
  42. package/dist/handlers/proposalDiffHandler.js +208 -0
  43. package/dist/index.d.ts +6 -6
  44. package/dist/index.js +50 -18
  45. package/dist/payload/collectionPermissions.d.ts +14 -13
  46. package/dist/payload/collectionPermissions.js +3 -2
  47. package/dist/payload/normalizeData.d.ts +8 -0
  48. package/dist/payload/normalizeData.js +25 -3
  49. package/dist/payload/schemaContext.d.ts +43 -16
  50. package/dist/payload/schemaContext.js +139 -18
  51. package/dist/payload/shared.d.ts +33 -0
  52. package/dist/payload/shared.js +114 -0
  53. package/package.json +7 -2
  54. package/dist/components/AIActionProposalList.d.ts +0 -26
  55. package/dist/components/AIApiKeyField.d.ts +0 -2
  56. package/dist/components/CollectionMentionPopover.d.ts +0 -16
  57. package/dist/components/RecentChangesList.d.ts +0 -18
  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 -8
  61. package/dist/endpoints/aiApplyActionEndpointHandler.js +0 -365
  62. package/dist/endpoints/aiChatEndpointHandler.d.ts +0 -35
  63. package/dist/endpoints/aiChatEndpointHandler.js +0 -382
  64. package/dist/endpoints/aiMentionSuggestionsEndpointHandler.d.ts +0 -7
  65. package/dist/endpoints/aiMentionSuggestionsEndpointHandler.js +0 -63
  66. package/dist/endpoints/aiProposalDiffEndpointHandler.d.ts +0 -7
  67. package/dist/endpoints/aiProposalDiffEndpointHandler.js +0 -138
  68. package/dist/endpoints/aiRecentChangesEndpointHandler.d.ts +0 -6
  69. /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 { 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, Send } from "./Icons.js";
11
- import { RecentChangesList } from "./RecentChangesList.js";
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
- export const AIInput = ()=>{
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-recent-changes"
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 getCaretOffset = (element)=>{
296
+ const getTextBeforeCaret = (element)=>{
188
297
  const selection = window.getSelection();
189
- if (!selection || selection.rangeCount === 0) return 0;
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().length;
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 = (value, caretPosition)=>{
236
- const valueBeforeCaret = value.slice(0, caretPosition);
237
- const match = /(?:^|\s)@([\w-]*)$/.exec(valueBeforeCaret);
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
- const atIndex = valueBeforeCaret.lastIndexOf("@");
244
- setMentionQuery(match[1] || "");
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 beforeMention = prompt.slice(0, mentionRange.start);
269
- 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);
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}${promptText} ${afterMention}`);
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
- setProposals(event.data.proposals || []);
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
- setProposals(finalEvent.data.proposals || []);
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
- proposal
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, 12));
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(value, getCaretOffset(event.currentTarget));
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 ? /*#__PURE__*/ React.createElement(CollectionMentionPopover, {
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
- }) : null), /*#__PURE__*/ React.createElement("div", {
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(AIActionProposalList, {
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: stretch;
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);
@@ -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;
@@ -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 "./AIActionProposalList.module.css";
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 AIActionProposalList = ({ apiRoute, appliedProposalIndexes, description, error, getViewURL, isApplying, onDismiss, onDismissError, onApply, proposals })=>{
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 ? /*#__PURE__*/ React.createElement("details", {
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)) : null)), proposals.map((proposal, index)=>{
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 ? /*#__PURE__*/ React.createElement("div", {
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")) : null, activeDiff ? /*#__PURE__*/ React.createElement(DiffDialog, {
158
+ }, "Dismiss")), activeDiff && /*#__PURE__*/ React.createElement(DiffDialog, {
158
159
  diff: activeDiff.diff,
159
160
  onClose: ()=>setActiveDiff(null),
160
- proposal: activeDiff.proposal
161
- }) : null);
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 {};