@mvriu5/payload-ai 1.2.0 → 1.3.2

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (51) hide show
  1. package/README.md +42 -0
  2. package/dist/components/Icons.d.ts +0 -8
  3. package/dist/components/Icons.js +4 -216
  4. package/dist/components/{ActionToast.d.ts → action-toast/ActionToast.d.ts} +1 -2
  5. package/dist/components/{ActionToast.js → action-toast/ActionToast.js} +25 -34
  6. package/dist/components/{ActionToast.module.css → action-toast/ActionToast.module.css} +0 -79
  7. package/dist/components/ai-input/AIInput.js +433 -0
  8. package/dist/components/{AIInput.module.css → ai-input/AIInput.module.css} +111 -43
  9. package/dist/components/ai-input/badge.d.ts +14 -0
  10. package/dist/components/ai-input/badge.js +85 -0
  11. package/dist/components/{AuditLogList.d.ts → audit-log-list/AuditLogList.d.ts} +4 -8
  12. package/dist/components/{AuditLogList.js → audit-log-list/AuditLogList.js} +49 -21
  13. package/dist/components/{AuditLogList.module.css → audit-log-list/AuditLogList.module.css} +11 -65
  14. package/dist/components/dashboard/Dashboard.d.ts +2 -0
  15. package/dist/components/dashboard/Dashboard.js +15 -0
  16. package/dist/components/dashboard/Dashboard.module.css +13 -0
  17. package/dist/components/{DiffDialog.d.ts → diff-dialog/DiffDialog.d.ts} +2 -2
  18. package/dist/components/{DiffDialog.js → diff-dialog/DiffDialog.js} +200 -189
  19. package/dist/components/{DiffDialog.module.css → diff-dialog/DiffDialog.module.css} +17 -35
  20. package/dist/components/hooks/useAIChatStream.d.ts +39 -0
  21. package/dist/components/hooks/useAIChatStream.js +217 -0
  22. package/dist/components/hooks/useAISettings.js +26 -25
  23. package/dist/components/hooks/useAuditLog.d.ts +11 -0
  24. package/dist/components/hooks/useAuditLog.js +41 -0
  25. package/dist/components/hooks/useDocumentMentionSuggestions.d.ts +1 -1
  26. package/dist/components/hooks/useDocumentMentionSuggestions.js +31 -27
  27. package/dist/components/hooks/useMentions.d.ts +58 -0
  28. package/dist/components/hooks/useMentions.js +276 -0
  29. package/dist/components/hooks/usePluginConfig.d.ts +52 -0
  30. package/dist/components/hooks/usePluginConfig.js +20 -0
  31. package/dist/components/{MentionPopover.d.ts → mention-popover/MentionPopover.d.ts} +2 -4
  32. package/dist/components/{MentionPopover.js → mention-popover/MentionPopover.js} +1 -8
  33. package/dist/components/{MentionPopover.module.css → mention-popover/MentionPopover.module.css} +1 -1
  34. package/dist/exports/client.d.ts +2 -2
  35. package/dist/exports/client.js +2 -2
  36. package/dist/handlers/applyActionHandler.js +27 -7
  37. package/dist/handlers/chatHandler.js +302 -58
  38. package/dist/handlers/mediaUploadHandler.d.ts +7 -0
  39. package/dist/handlers/mediaUploadHandler.js +99 -0
  40. package/dist/handlers/mentionSuggestionHandler.js +16 -14
  41. package/dist/handlers/proposalDiffHandler.js +41 -29
  42. package/dist/index.d.ts +6 -0
  43. package/dist/index.js +39 -7
  44. package/dist/payload/collectionPermissions.js +3 -1
  45. package/dist/payload/normalizeData.d.ts +0 -6
  46. package/dist/payload/normalizeData.js +25 -13
  47. package/dist/payload/proposalData.js +4 -1
  48. package/dist/payload/schemaContext.js +98 -43
  49. package/package.json +6 -6
  50. package/dist/components/AIInput.js +0 -896
  51. /package/dist/components/{AIInput.d.ts → ai-input/AIInput.d.ts} +0 -0
@@ -1,896 +0,0 @@
1
- "use client";
2
- import { jsx as _jsx, jsxs as _jsxs, Fragment as _Fragment } from "react/jsx-runtime";
3
- import { useConfig } from "@payloadcms/ui";
4
- import { formatAdminURL } from "payload/shared";
5
- import { useCallback, useEffect, useMemo, useRef, useState } from "react";
6
- import { getResolvedAIModelConfig } from "../ai/providerOptions.js";
7
- import { getSerializableLabel, isInternalCollection } from "../payload/shared.js";
8
- import { ActionToast } from "./ActionToast.js";
9
- import styles from "./AIInput.module.css";
10
- import { MentionPopover } from "./MentionPopover.js";
11
- import { ClaudeIcon, GoogleGeminiIcon, MistralAiIcon, OpenaiIcon, OpenrouterIcon, Send } from "./Icons.js";
12
- import { RecentChangesList } from "./AuditLogList.js";
13
- import { useAISettings } from "./hooks/useAISettings.js";
14
- import { useDocumentMentionSuggestions } from "./hooks/useDocumentMentionSuggestions.js";
15
- const collectBlockOptions = ({ fields, parent })=>{
16
- const options = [];
17
- for (const field of fields){
18
- if (field.type === "blocks" && field.blocks) {
19
- for (const block of field.blocks){
20
- options.push({
21
- label: getSerializableLabel(block.labels?.singular, block.slug),
22
- parent,
23
- slug: block.slug,
24
- type: "block"
25
- });
26
- options.push(...collectBlockOptions({
27
- fields: block.fields || [],
28
- parent: `${parent}/${block.slug}`
29
- }));
30
- }
31
- }
32
- if (field.fields) {
33
- options.push(...collectBlockOptions({
34
- fields: field.fields,
35
- parent
36
- }));
37
- }
38
- }
39
- return options;
40
- };
41
- const getProviderIcon = (provider)=>{
42
- const iconProps = {
43
- "aria-hidden": true,
44
- className: styles.selectProviderIcon
45
- };
46
- switch(provider){
47
- case "claude":
48
- return /*#__PURE__*/ _jsx(ClaudeIcon, {
49
- ...iconProps
50
- });
51
- case "google":
52
- return /*#__PURE__*/ _jsx(GoogleGeminiIcon, {
53
- ...iconProps
54
- });
55
- case "mistral":
56
- return /*#__PURE__*/ _jsx(MistralAiIcon, {
57
- ...iconProps
58
- });
59
- case "openai":
60
- return /*#__PURE__*/ _jsx(OpenaiIcon, {
61
- ...iconProps
62
- });
63
- case "openrouter":
64
- return /*#__PURE__*/ _jsx(OpenrouterIcon, {
65
- ...iconProps
66
- });
67
- default:
68
- return null;
69
- }
70
- };
71
- const parseSSEEvent = (chunk)=>{
72
- const lines = chunk.split("\n");
73
- let eventName = "";
74
- const dataLines = [];
75
- for (const line of lines){
76
- if (line.startsWith("event:")) {
77
- eventName = line.slice(6).trim();
78
- continue;
79
- }
80
- if (line.startsWith("data:")) {
81
- dataLines.push(line.slice(5).trim());
82
- }
83
- }
84
- if (!eventName) return null;
85
- try {
86
- const data = JSON.parse(dataLines.join("\n"));
87
- if (eventName !== "text" && eventName !== "proposals" && eventName !== "error" && eventName !== "done" && eventName !== "debug") {
88
- return null;
89
- }
90
- return {
91
- data,
92
- event: eventName
93
- };
94
- } catch {
95
- return null;
96
- }
97
- };
98
- const sanitizeResponseText = (value)=>value.replace(/\*\*/g, "");
99
- const getDebugReasonLabel = (reason)=>{
100
- switch(reason){
101
- case "model_did_not_call_tool":
102
- return "Model did not create a proposal tool call.";
103
- case "proposal_created":
104
- return "Proposal created.";
105
- case "tool_validation_failed":
106
- return "Tool validation failed before a proposal could be created.";
107
- case "write_intent_without_tool_call":
108
- return "The selected model did not produce the required proposal tool call for this content change.";
109
- default:
110
- return "Unknown";
111
- }
112
- };
113
- const getApplyDebugReasonLabel = (reason)=>{
114
- switch(reason){
115
- case "unauthorized":
116
- return "Request was not authorized.";
117
- case "missing_proposal":
118
- return "No proposal was submitted to apply.";
119
- case "invalid_signature":
120
- return "Proposal signature was invalid or expired.";
121
- case "invalid_proposal_shape":
122
- return "Proposal shape was invalid.";
123
- case "sensitive_data_in_data":
124
- case "sensitive_data_in_localized_data":
125
- return "Proposal contained sensitive data.";
126
- case "unknown_global":
127
- return "Target global was not found.";
128
- case "unknown_or_disallowed_collection":
129
- return "Target collection is unknown or not allowed.";
130
- case "invalid_collection_write_shape":
131
- return "Proposal data does not match the target collection schema.";
132
- case "invalid_global_write_shape":
133
- return "Proposal data does not match the target global schema.";
134
- case "localized_create_without_locales":
135
- return "Localized create proposal had no locale entries.";
136
- case "missing_auth_password":
137
- return "Auth create proposal was missing a password.";
138
- case "missing_auth_email":
139
- return "Auth create proposal was missing an email.";
140
- case "payload_operation_failed":
141
- return "Payload rejected the write operation.";
142
- default:
143
- return "Unknown";
144
- }
145
- };
146
- const getChatDebugMessage = (debugInfo)=>{
147
- if (debugInfo.toolFailures?.length) {
148
- return debugInfo.toolFailures[0]?.message || getDebugReasonLabel(debugInfo.reason);
149
- }
150
- return getDebugReasonLabel(debugInfo.reason);
151
- };
152
- const isMentionBoundary = (character)=>{
153
- return character === undefined || /\s/.test(character);
154
- };
155
- const getActiveMentionRange = (valueBeforeCaret)=>{
156
- const caretPosition = valueBeforeCaret.length;
157
- for(let index = valueBeforeCaret.length - 1; index >= 0; index -= 1){
158
- const character = valueBeforeCaret[index];
159
- if (character === "@") {
160
- const previousCharacter = valueBeforeCaret[index - 1];
161
- const query = valueBeforeCaret.slice(index + 1);
162
- if (!isMentionBoundary(previousCharacter)) return null;
163
- if (!/^[\w-]*$/.test(query)) return null;
164
- return {
165
- query,
166
- range: {
167
- end: caretPosition,
168
- start: index
169
- }
170
- };
171
- }
172
- if (isMentionBoundary(character)) break;
173
- }
174
- return null;
175
- };
176
- const svgNamespace = "http://www.w3.org/2000/svg";
177
- const auditLogCollectionSlug = "payload-ai-auditlog";
178
- const responseOnlyToastCooldownMs = 10000;
179
- const appendSvgPath = (svg, d)=>{
180
- const path = document.createElementNS(svgNamespace, "path");
181
- path.setAttribute("d", d);
182
- svg.append(path);
183
- };
184
- const createBadgeIcon = (type)=>{
185
- if (type === "locale") return null;
186
- const svg = document.createElementNS(svgNamespace, "svg");
187
- svg.setAttribute("aria-hidden", "true");
188
- svg.setAttribute("class", styles.badgeIcon);
189
- svg.setAttribute("fill", "none");
190
- svg.setAttribute("stroke", "currentColor");
191
- svg.setAttribute("stroke-linecap", "round");
192
- svg.setAttribute("stroke-linejoin", "round");
193
- svg.setAttribute("stroke-width", "2");
194
- svg.setAttribute("viewBox", "0 0 24 24");
195
- if (type === "collection") {
196
- 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");
197
- }
198
- if (type === "doc") {
199
- appendSvgPath(svg, "M14 3v4a1 1 0 0 0 1 1h4");
200
- 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");
201
- }
202
- if (type === "global") {
203
- appendSvgPath(svg, "M3 12a9 9 0 1 0 18 0a9 9 0 0 0 -18 0");
204
- appendSvgPath(svg, "M3.6 9h16.8");
205
- appendSvgPath(svg, "M3.6 15h16.8");
206
- appendSvgPath(svg, "M11.5 3a17 17 0 0 0 0 18");
207
- appendSvgPath(svg, "M12.5 3a17 17 0 0 1 0 18");
208
- }
209
- if (type === "block") {
210
- 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");
211
- 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");
212
- }
213
- return svg;
214
- };
215
- const createBadgePrefix = (suggestion)=>{
216
- const prefix = document.createElement("span");
217
- const icon = createBadgeIcon(suggestion.type);
218
- prefix.className = styles.prefix;
219
- if (icon) {
220
- prefix.append(icon);
221
- }
222
- if (suggestion.type === "doc") {
223
- prefix.append(document.createTextNode(`${suggestion.collection || "document"}:`));
224
- } else if (suggestion.type === "locale") {
225
- prefix.textContent = "locale:";
226
- }
227
- return prefix;
228
- };
229
- const AIInput = ()=>{
230
- const { config } = useConfig();
231
- const editorRef = useRef(null);
232
- const mentionPopoverRef = useRef(null);
233
- const [prompt, setPrompt] = useState("");
234
- const configuredModels = config.admin?.custom?.payloadAiPlugin?.models;
235
- const aiModelConfig = useMemo(()=>getResolvedAIModelConfig(configuredModels), [
236
- configuredModels
237
- ]);
238
- const { selectedModel, setSelectedModel, settingsProvider } = useAISettings({
239
- adminUserSlug: config.admin?.user,
240
- apiRoute: config.routes.api,
241
- defaultModels: aiModelConfig.defaults
242
- });
243
- const [mentionQuery, setMentionQuery] = useState("");
244
- const [mentionRange, setMentionRange] = useState(null);
245
- const [mentionPopoverPosition, setMentionPopoverPosition] = useState(null);
246
- const [mentions, setMentions] = useState([]);
247
- const [appliedProposalIndexes, setAppliedProposalIndexes] = useState([]);
248
- const [response, setResponse] = useState("");
249
- const [tokenUsage, setTokenUsage] = useState(null);
250
- const [chatDebugInfo, setChatDebugInfo] = useState(null);
251
- const [applyDebugInfo, setApplyDebugInfo] = useState(null);
252
- const [error, setError] = useState("");
253
- const [proposals, setProposals] = useState([]);
254
- const [appliedChanges, setAppliedChanges] = useState([]);
255
- const [isLoading, setIsLoading] = useState(false);
256
- const [isApplying, setIsApplying] = useState(false);
257
- const enabledCollectionSlugs = config.admin?.custom?.payloadAiPlugin?.collectionSlugs;
258
- const enabledCollectionSlugSet = useMemo(()=>enabledCollectionSlugs ? new Set(enabledCollectionSlugs) : null, [
259
- enabledCollectionSlugs
260
- ]);
261
- const isCollectionMentionEnabled = (slug)=>!enabledCollectionSlugSet || enabledCollectionSlugSet.has(slug);
262
- const recentChangesEndpoint = useMemo(()=>formatAdminURL({
263
- apiRoute: config.routes.api,
264
- path: "/ai-audit-log"
265
- }), [
266
- config.routes.api
267
- ]);
268
- const allChangesURL = useMemo(()=>`${config.routes.admin || "/admin"}/collections/${auditLogCollectionSlug}`, [
269
- config.routes.admin
270
- ]);
271
- const loadRecentChanges = useCallback(async ()=>{
272
- const res = await fetch(recentChangesEndpoint);
273
- const result = await res.json().catch(()=>null);
274
- if (res.ok && result?.changes) {
275
- setAppliedChanges(result.changes.slice(0, 10));
276
- }
277
- }, [
278
- recentChangesEndpoint
279
- ]);
280
- useEffect(()=>{
281
- void loadRecentChanges().catch(()=>undefined);
282
- }, [
283
- loadRecentChanges
284
- ]);
285
- useEffect(()=>{
286
- if (isLoading || error || proposals.length > 0 || !response) return;
287
- const timeout = window.setTimeout(()=>{
288
- setResponse("");
289
- clearInput();
290
- }, responseOnlyToastCooldownMs);
291
- return ()=>window.clearTimeout(timeout);
292
- }, [
293
- error,
294
- isLoading,
295
- proposals.length,
296
- response
297
- ]);
298
- const collections = config.collections.filter((collection)=>!isInternalCollection(collection.slug)).filter((collection)=>isCollectionMentionEnabled(collection.slug)).map((collection)=>({
299
- label: getSerializableLabel(collection.labels?.singular, collection.slug),
300
- slug: collection.slug,
301
- type: "collection"
302
- }));
303
- const globals = config.globals?.map((global)=>({
304
- label: getSerializableLabel(global.label, global.slug),
305
- slug: global.slug,
306
- type: "global"
307
- })) || [];
308
- const localizationConfig = config.localization;
309
- const localesConfig = localizationConfig?.locales ?? [];
310
- const locales = localesConfig.flatMap((locale)=>{
311
- if (typeof locale === "string") {
312
- return [
313
- {
314
- isDefault: locale === localizationConfig?.defaultLocale,
315
- label: locale,
316
- slug: locale,
317
- type: "locale"
318
- }
319
- ];
320
- }
321
- if (!locale || typeof locale !== "object") return [];
322
- const slug = typeof locale.code === "string" ? locale.code : typeof locale.label === "string" ? locale.label : null;
323
- if (!slug) return [];
324
- return [
325
- {
326
- isDefault: slug === localizationConfig?.defaultLocale,
327
- label: getSerializableLabel(locale.label, slug),
328
- slug,
329
- type: "locale"
330
- }
331
- ];
332
- });
333
- const blocks = [
334
- ...config.collections.filter((collection)=>isCollectionMentionEnabled(collection.slug)).flatMap((collection)=>collectBlockOptions({
335
- fields: collection.fields,
336
- parent: collection.slug
337
- })),
338
- ...config.globals?.flatMap((global)=>collectBlockOptions({
339
- fields: global.fields,
340
- parent: global.slug
341
- })) || []
342
- ];
343
- const mentionOptions = [
344
- ...collections,
345
- ...globals,
346
- ...blocks,
347
- ...locales
348
- ];
349
- const normalizedMentionQuery = mentionQuery.toLowerCase();
350
- const filteredCollections = collections.filter((collection)=>collection.slug.toLowerCase().includes(normalizedMentionQuery) || collection.label.toLowerCase().includes(normalizedMentionQuery));
351
- const filteredMentionOptions = mentionOptions.filter((option)=>option.slug.toLowerCase().includes(normalizedMentionQuery) || option.label.toLowerCase().includes(normalizedMentionQuery));
352
- const documentSuggestionCollection = filteredCollections.length === 1 ? filteredCollections[0]?.slug : null;
353
- const { documentSuggestions, resetDocumentSuggestions } = useDocumentMentionSuggestions({
354
- apiRoute: config.routes.api,
355
- documentSuggestionCollection,
356
- mentionQuery,
357
- mentionRange
358
- });
359
- const mentionSuggestions = [
360
- ...filteredMentionOptions,
361
- ...documentSuggestions
362
- ];
363
- const shouldShowChatDebugInfo = Boolean(chatDebugInfo) && (Boolean(error) || chatDebugInfo?.reason !== "proposal_created");
364
- const shouldShowApplyDebugInfo = Boolean(applyDebugInfo) && Boolean(error);
365
- const actionToastDescription = response;
366
- const getTextBeforeCaret = (element)=>{
367
- const selection = window.getSelection();
368
- if (!selection || selection.rangeCount === 0) return "";
369
- const range = selection.getRangeAt(0);
370
- const clonedRange = range.cloneRange();
371
- clonedRange.selectNodeContents(element);
372
- clonedRange.setEnd(range.endContainer, range.endOffset);
373
- return clonedRange.toString();
374
- };
375
- const getTextNodeAtOffset = (element, offset)=>{
376
- const walker = document.createTreeWalker(element, NodeFilter.SHOW_TEXT);
377
- let currentOffset = 0;
378
- let node = walker.nextNode();
379
- while(node){
380
- const nextOffset = currentOffset + (node.textContent?.length || 0);
381
- if (offset <= nextOffset) {
382
- return {
383
- node,
384
- offset: offset - currentOffset
385
- };
386
- }
387
- currentOffset = nextOffset;
388
- node = walker.nextNode();
389
- }
390
- const textNode = document.createTextNode("");
391
- element.append(textNode);
392
- return {
393
- node: textNode,
394
- offset: 0
395
- };
396
- };
397
- const updateMentionPopoverPosition = useCallback((range)=>{
398
- const editor = editorRef.current;
399
- const popover = mentionPopoverRef.current;
400
- if (!editor || !popover || !range) {
401
- setMentionPopoverPosition(null);
402
- return;
403
- }
404
- const startPosition = getTextNodeAtOffset(editor, range.start);
405
- const anchorRange = document.createRange();
406
- anchorRange.setStart(startPosition.node, startPosition.offset);
407
- anchorRange.setEnd(startPosition.node, startPosition.offset);
408
- const editorRect = editor.getBoundingClientRect();
409
- const anchorRect = anchorRange.getBoundingClientRect();
410
- const fallbackLeft = Math.max(0, anchorRect.left - editorRect.left + 12);
411
- const fallbackTop = Math.max(0, anchorRect.bottom - editorRect.top + 20);
412
- const popoverWidth = popover.offsetWidth || 260;
413
- const maxLeft = Math.max(0, editor.clientWidth - popoverWidth);
414
- setMentionPopoverPosition({
415
- left: Math.min(fallbackLeft, maxLeft),
416
- top: fallbackTop
417
- });
418
- }, []);
419
- const replaceTextRangeWithBadge = ({ badge, editor, end, start })=>{
420
- const startPosition = getTextNodeAtOffset(editor, start);
421
- const endPosition = getTextNodeAtOffset(editor, end);
422
- const range = document.createRange();
423
- const trailingSpace = document.createTextNode(" ");
424
- range.setStart(startPosition.node, startPosition.offset);
425
- range.setEnd(endPosition.node, endPosition.offset);
426
- range.deleteContents();
427
- range.insertNode(trailingSpace);
428
- range.insertNode(badge);
429
- const selection = window.getSelection();
430
- const caretRange = document.createRange();
431
- caretRange.setStartAfter(trailingSpace);
432
- caretRange.collapse(true);
433
- selection?.removeAllRanges();
434
- selection?.addRange(caretRange);
435
- };
436
- const updateMentionState = (valueBeforeCaret)=>{
437
- const activeMention = getActiveMentionRange(valueBeforeCaret);
438
- if (!activeMention) {
439
- setMentionQuery("");
440
- setMentionRange(null);
441
- setMentionPopoverPosition(null);
442
- return;
443
- }
444
- setMentionQuery(activeMention.query);
445
- setMentionRange(activeMention.range);
446
- };
447
- useEffect(()=>{
448
- if (!mentionRange) {
449
- setMentionPopoverPosition(null);
450
- return;
451
- }
452
- updateMentionPopoverPosition(mentionRange);
453
- }, [
454
- mentionRange,
455
- mentionSuggestions.length,
456
- updateMentionPopoverPosition
457
- ]);
458
- useEffect(()=>{
459
- if (!mentionRange) return;
460
- const updatePosition = ()=>updateMentionPopoverPosition(mentionRange);
461
- window.addEventListener("resize", updatePosition);
462
- window.addEventListener("scroll", updatePosition, true);
463
- return ()=>{
464
- window.removeEventListener("resize", updatePosition);
465
- window.removeEventListener("scroll", updatePosition, true);
466
- };
467
- }, [
468
- mentionRange,
469
- updateMentionPopoverPosition
470
- ]);
471
- const clearInput = ()=>{
472
- setPrompt("");
473
- setMentions([]);
474
- if (editorRef.current) editorRef.current.textContent = "";
475
- };
476
- const getProposalViewURL = (proposal)=>{
477
- const adminRoute = config.routes.admin || "/admin";
478
- if (proposal.action === "updateGlobal" && proposal.slug) {
479
- return `${adminRoute}/globals/${proposal.slug}`;
480
- }
481
- if (proposal.collection && proposal.id) {
482
- return `${adminRoute}/collections/${proposal.collection}/${proposal.id}`;
483
- }
484
- return null;
485
- };
486
- const insertMention = (suggestion)=>{
487
- const editor = editorRef.current;
488
- if (!mentionRange || !editor) return;
489
- const currentValue = editor.innerText;
490
- const beforeMention = currentValue.slice(0, mentionRange.start);
491
- const afterMention = currentValue.slice(mentionRange.end);
492
- const badgeType = suggestion.type === "doc" ? "document" : suggestion.type;
493
- const badgePrefix = `${badgeType}:`;
494
- const badgeText = `${badgePrefix} ${suggestion.label}`;
495
- const badge = document.createElement("span");
496
- badge.className = [
497
- styles.badge,
498
- styles[suggestion.type],
499
- styles.inlineBadge
500
- ].join(" ");
501
- badge.contentEditable = "false";
502
- badge.append(createBadgePrefix(suggestion), Object.assign(document.createElement("span"), {
503
- className: styles.name,
504
- textContent: suggestion.label
505
- }));
506
- replaceTextRangeWithBadge({
507
- badge,
508
- editor,
509
- end: mentionRange.end,
510
- start: mentionRange.start
511
- });
512
- editor.focus();
513
- setPrompt(`${beforeMention}${badgeText} ${afterMention}`);
514
- setMentions((currentMentions)=>{
515
- 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);
516
- if (mentionExists) return currentMentions;
517
- return [
518
- ...currentMentions,
519
- suggestion
520
- ];
521
- });
522
- setMentionQuery("");
523
- setMentionRange(null);
524
- setMentionPopoverPosition(null);
525
- resetDocumentSuggestions();
526
- };
527
- const handleSubmit = async ()=>{
528
- const trimmedPrompt = prompt.trim();
529
- if (!trimmedPrompt) return;
530
- setIsLoading(true);
531
- setAppliedProposalIndexes([]);
532
- setError("");
533
- setProposals([]);
534
- setResponse("");
535
- setTokenUsage(null);
536
- setChatDebugInfo(null);
537
- setApplyDebugInfo(null);
538
- try {
539
- const res = await fetch(formatAdminURL({
540
- apiRoute: config.routes.api,
541
- path: "/ai-chat"
542
- }), {
543
- body: JSON.stringify({
544
- mentions,
545
- model: selectedModel,
546
- prompt: trimmedPrompt
547
- }),
548
- headers: {
549
- "Content-Type": "application/json"
550
- },
551
- method: "POST"
552
- });
553
- if (!res.ok) {
554
- const result = await res.json().catch(()=>null);
555
- throw new Error(result?.error || "AI request failed");
556
- }
557
- if (!res.body) {
558
- throw new Error("AI response stream is unavailable");
559
- }
560
- const reader = res.body.getReader();
561
- const decoder = new TextDecoder();
562
- let buffer = "";
563
- let finalDebugInfo = null;
564
- let receivedProposals = [];
565
- let receivedText = "";
566
- let receivedVisibleText = "";
567
- while(true){
568
- const { done, value } = await reader.read();
569
- if (done) break;
570
- buffer += decoder.decode(value, {
571
- stream: true
572
- });
573
- const chunks = buffer.split("\n\n");
574
- buffer = chunks.pop() || "";
575
- for (const chunk of chunks){
576
- const event = parseSSEEvent(chunk);
577
- if (!event) continue;
578
- if (event.event === "text") {
579
- if (event.data.delta) {
580
- const nextDelta = sanitizeResponseText(event.data.delta || "");
581
- receivedText += nextDelta;
582
- receivedVisibleText += nextDelta.replace(/\s+/g, "");
583
- setResponse((current)=>current + nextDelta);
584
- }
585
- continue;
586
- }
587
- if (event.event === "proposals") {
588
- receivedProposals = event.data.proposals || [];
589
- setProposals(receivedProposals);
590
- setTokenUsage(event.data.usage || null);
591
- continue;
592
- }
593
- if (event.event === "debug") {
594
- finalDebugInfo = event.data;
595
- setChatDebugInfo(event.data);
596
- if ((event.data.proposalCount || 0) === 0 && !receivedVisibleText) {
597
- // Show specific tool validation message if present, otherwise a generic no‑action message
598
- if (event.data.reason === "tool_validation_failed") {
599
- const msg = getChatDebugMessage(event.data);
600
- setResponse(msg);
601
- // Auto‑clear after the standard toast timeout
602
- window.setTimeout(()=>setResponse(""), responseOnlyToastCooldownMs);
603
- } else {
604
- setResponse("No action needed");
605
- }
606
- }
607
- continue;
608
- }
609
- if (event.event === "error") {
610
- throw new Error(event.data.error || "AI request failed");
611
- }
612
- }
613
- }
614
- const finalEvent = buffer.trim() ? parseSSEEvent(buffer.trim()) : null;
615
- if (finalEvent?.event === "proposals") {
616
- receivedProposals = finalEvent.data.proposals || [];
617
- setProposals(receivedProposals);
618
- setTokenUsage(finalEvent.data.usage || null);
619
- }
620
- if (finalEvent?.event === "debug") {
621
- finalDebugInfo = finalEvent.data;
622
- setChatDebugInfo(finalEvent.data);
623
- if ((finalEvent.data.proposalCount || 0) === 0 && !receivedVisibleText) {
624
- // Same logic as above for the final event
625
- if (finalEvent.data.reason === "tool_validation_failed") {
626
- setResponse(getChatDebugMessage(finalEvent.data));
627
- } else {
628
- setResponse("No action needed");
629
- }
630
- }
631
- }
632
- if (finalEvent?.event === "error") {
633
- throw new Error(finalEvent.data.error || "AI request failed");
634
- }
635
- if (receivedProposals.length === 0) {
636
- if (finalDebugInfo) {
637
- const debugMessage = getChatDebugMessage(finalDebugInfo);
638
- const isMeaningfulVisibleText = receivedVisibleText.length >= 12;
639
- const trimmedReceivedText = receivedText.trim();
640
- if (!isMeaningfulVisibleText || trimmedReceivedText.length < 12) {
641
- setResponse(debugMessage);
642
- } else {
643
- setResponse((current)=>current.trim() || debugMessage);
644
- }
645
- } else {
646
- // Fallback when no debug info is provided (e.g., tool validation failures without a debug event)
647
- setResponse("No action needed");
648
- }
649
- clearInput();
650
- }
651
- } catch (err) {
652
- setProposals([]);
653
- setResponse("");
654
- setTokenUsage(null);
655
- setApplyDebugInfo(null);
656
- setError(err instanceof Error ? err.message : "AI request failed");
657
- } finally{
658
- setIsLoading(false);
659
- }
660
- };
661
- const handleApplyProposal = async (proposal)=>{
662
- setIsApplying(true);
663
- setError("");
664
- try {
665
- const res = await fetch(formatAdminURL({
666
- apiRoute: config.routes.api,
667
- path: "/ai-apply-action"
668
- }), {
669
- body: JSON.stringify({
670
- aiResponse: response,
671
- prompt,
672
- proposal,
673
- tokenUsage
674
- }),
675
- headers: {
676
- "Content-Type": "application/json"
677
- },
678
- method: "POST"
679
- });
680
- const result = await res.json();
681
- if (!res.ok) {
682
- setProposals([]);
683
- setResponse("");
684
- setApplyDebugInfo(result.debug || null);
685
- throw new Error(result.error || "Could not apply proposal");
686
- }
687
- setAppliedProposalIndexes([]);
688
- setError("");
689
- setProposals([]);
690
- setResponse("");
691
- setTokenUsage(null);
692
- setChatDebugInfo(null);
693
- setApplyDebugInfo(null);
694
- if (result.change) {
695
- setAppliedChanges((current)=>[
696
- result.change,
697
- ...current
698
- ].slice(0, 10));
699
- }
700
- void loadRecentChanges().catch(()=>undefined);
701
- clearInput();
702
- } catch (err) {
703
- setError(err instanceof Error ? err.message : "Could not apply proposal");
704
- } finally{
705
- setIsApplying(false);
706
- }
707
- };
708
- return /*#__PURE__*/ _jsxs("div", {
709
- className: styles.chatLayout,
710
- children: [
711
- /*#__PURE__*/ _jsxs("div", {
712
- className: styles.chat,
713
- children: [
714
- /*#__PURE__*/ _jsx("div", {
715
- className: styles.chatHeader,
716
- children: /*#__PURE__*/ _jsxs("div", {
717
- children: [
718
- /*#__PURE__*/ _jsx("h2", {
719
- className: styles.chatTitle,
720
- children: "AI Assistant"
721
- }),
722
- /*#__PURE__*/ _jsx("p", {
723
- className: styles.chatDescription,
724
- children: "Ask AI to draft, improve, or analyze content."
725
- })
726
- ]
727
- })
728
- }),
729
- /*#__PURE__*/ _jsxs("div", {
730
- className: styles.chatInputRow,
731
- children: [
732
- /*#__PURE__*/ _jsx("div", {
733
- className: styles.chatInputSurface,
734
- children: /*#__PURE__*/ _jsx("div", {
735
- className: styles.chatInput,
736
- contentEditable: true,
737
- "data-placeholder": "Ask AI...",
738
- onInput: (event)=>{
739
- const value = event.currentTarget.innerText;
740
- setPrompt(value);
741
- if (!value.trim()) {
742
- setMentions([]);
743
- }
744
- updateMentionState(getTextBeforeCaret(event.currentTarget));
745
- },
746
- onKeyDown: (event)=>{
747
- if (event.key === "ArrowDown" && mentionRange && mentionSuggestions.length > 0) {
748
- const firstOption = mentionPopoverRef.current?.querySelector("button");
749
- if (firstOption) {
750
- event.preventDefault();
751
- firstOption.focus();
752
- return;
753
- }
754
- }
755
- if (event.key === "Enter" && !event.shiftKey) {
756
- event.preventDefault();
757
- void handleSubmit();
758
- }
759
- },
760
- ref: editorRef,
761
- role: "textbox",
762
- suppressContentEditableWarning: true
763
- })
764
- }),
765
- mentionRange && /*#__PURE__*/ _jsx(MentionPopover, {
766
- containerRef: mentionPopoverRef,
767
- onSelect: insertMention,
768
- style: mentionPopoverPosition ? {
769
- left: `${mentionPopoverPosition.left}px`,
770
- top: `${mentionPopoverPosition.top}px`
771
- } : undefined,
772
- suggestions: mentionSuggestions
773
- })
774
- ]
775
- }),
776
- /*#__PURE__*/ _jsxs("div", {
777
- className: styles.chatActionsRow,
778
- children: [
779
- /*#__PURE__*/ _jsx("div", {
780
- className: styles.settings,
781
- children: /*#__PURE__*/ _jsxs("label", {
782
- className: styles.setting,
783
- children: [
784
- /*#__PURE__*/ _jsx("span", {
785
- className: styles.settingLabel,
786
- children: "Model"
787
- }),
788
- /*#__PURE__*/ _jsxs("div", {
789
- className: styles.selectWrapper,
790
- children: [
791
- getProviderIcon(settingsProvider),
792
- /*#__PURE__*/ _jsxs("select", {
793
- className: styles.select,
794
- disabled: !settingsProvider,
795
- onChange: (event)=>setSelectedModel(event.target.value),
796
- value: selectedModel,
797
- children: [
798
- !settingsProvider && /*#__PURE__*/ _jsx("option", {
799
- value: "",
800
- children: "Select provider in account settings"
801
- }),
802
- settingsProvider && aiModelConfig.providers[settingsProvider].map((model)=>/*#__PURE__*/ _jsx("option", {
803
- value: model.value,
804
- children: model.label
805
- }, model.value))
806
- ]
807
- })
808
- ]
809
- })
810
- ]
811
- })
812
- }),
813
- /*#__PURE__*/ _jsxs("button", {
814
- className: styles.chatButton,
815
- disabled: !prompt.trim() || !settingsProvider || !selectedModel || isLoading || Boolean(error) || Boolean(actionToastDescription) || proposals.length > 0,
816
- onClick: ()=>void handleSubmit(),
817
- type: "button",
818
- children: [
819
- /*#__PURE__*/ _jsx(Send, {
820
- width: 14,
821
- height: 14
822
- }),
823
- isLoading ? "Sending..." : "Send"
824
- ]
825
- })
826
- ]
827
- }),
828
- /*#__PURE__*/ _jsx(ActionToast, {
829
- apiRoute: config.routes.api,
830
- appliedProposalIndexes: appliedProposalIndexes,
831
- description: actionToastDescription,
832
- error: error,
833
- getViewURL: getProposalViewURL,
834
- isApplying: isApplying,
835
- onDismiss: ()=>{
836
- setAppliedProposalIndexes([]);
837
- setError("");
838
- setProposals([]);
839
- setResponse("");
840
- setTokenUsage(null);
841
- setChatDebugInfo(null);
842
- setApplyDebugInfo(null);
843
- clearInput();
844
- },
845
- onDismissError: ()=>{
846
- setError("");
847
- },
848
- onApply: (proposal, _index)=>void handleApplyProposal(proposal),
849
- proposals: proposals,
850
- prompt: prompt,
851
- tokenUsage: tokenUsage
852
- }),
853
- shouldShowApplyDebugInfo && applyDebugInfo && /*#__PURE__*/ _jsxs("div", {
854
- className: styles.debugInfo,
855
- children: [
856
- /*#__PURE__*/ _jsx("strong", {
857
- children: "Apply debug"
858
- }),
859
- /*#__PURE__*/ _jsx("br", {}),
860
- "Reason: ",
861
- getApplyDebugReasonLabel(applyDebugInfo.reason),
862
- /*#__PURE__*/ _jsx("br", {}),
863
- "Phase: ",
864
- applyDebugInfo.phase,
865
- /*#__PURE__*/ _jsx("br", {}),
866
- "Target: ",
867
- applyDebugInfo.collection || applyDebugInfo.slug || "unknown",
868
- applyDebugInfo.id ? /*#__PURE__*/ _jsxs(_Fragment, {
869
- children: [
870
- /*#__PURE__*/ _jsx("br", {}),
871
- "ID: ",
872
- applyDebugInfo.id
873
- ]
874
- }) : null,
875
- applyDebugInfo.details ? /*#__PURE__*/ _jsxs(_Fragment, {
876
- children: [
877
- /*#__PURE__*/ _jsx("br", {}),
878
- "Details:",
879
- /*#__PURE__*/ _jsx("pre", {
880
- className: styles.debugDetails,
881
- children: JSON.stringify(applyDebugInfo.details, null, 2)
882
- })
883
- ]
884
- }) : null
885
- ]
886
- })
887
- ]
888
- }),
889
- /*#__PURE__*/ _jsx(RecentChangesList, {
890
- allChangesURL: allChangesURL,
891
- changes: appliedChanges
892
- })
893
- ]
894
- });
895
- };
896
- export default AIInput;