@mvriu5/payload-ai 1.1.1 → 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 (55) 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} +2 -2
  5. package/dist/components/{ActionToast.js → action-toast/ActionToast.js} +27 -35
  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} +119 -42
  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 +341 -112
  37. package/dist/handlers/chatHandler.js +940 -84
  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 +117 -50
  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/logging.d.ts +9 -0
  46. package/dist/payload/logging.js +14 -0
  47. package/dist/payload/normalizeData.d.ts +16 -12
  48. package/dist/payload/normalizeData.js +47 -14
  49. package/dist/payload/proposalData.d.ts +31 -0
  50. package/dist/payload/proposalData.js +578 -0
  51. package/dist/payload/schemaContext.d.ts +3 -7
  52. package/dist/payload/schemaContext.js +147 -66
  53. package/package.json +6 -6
  54. package/dist/components/AIInput.js +0 -747
  55. /package/dist/components/{AIInput.d.ts → ai-input/AIInput.d.ts} +0 -0
@@ -1,747 +0,0 @@
1
- "use client";
2
- import { jsx as _jsx, jsxs as _jsxs } 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") {
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 isMentionBoundary = (character)=>{
100
- return character === undefined || /\s/.test(character);
101
- };
102
- const getActiveMentionRange = (valueBeforeCaret)=>{
103
- const caretPosition = valueBeforeCaret.length;
104
- for(let index = valueBeforeCaret.length - 1; index >= 0; index -= 1){
105
- const character = valueBeforeCaret[index];
106
- if (character === "@") {
107
- const previousCharacter = valueBeforeCaret[index - 1];
108
- const query = valueBeforeCaret.slice(index + 1);
109
- if (!isMentionBoundary(previousCharacter)) return null;
110
- if (!/^[\w-]*$/.test(query)) return null;
111
- return {
112
- query,
113
- range: {
114
- end: caretPosition,
115
- start: index
116
- }
117
- };
118
- }
119
- if (isMentionBoundary(character)) break;
120
- }
121
- return null;
122
- };
123
- const svgNamespace = "http://www.w3.org/2000/svg";
124
- const auditLogCollectionSlug = "payload-ai-auditlog";
125
- const appendSvgPath = (svg, d)=>{
126
- const path = document.createElementNS(svgNamespace, "path");
127
- path.setAttribute("d", d);
128
- svg.append(path);
129
- };
130
- const createBadgeIcon = (type)=>{
131
- if (type === "locale") return null;
132
- const svg = document.createElementNS(svgNamespace, "svg");
133
- svg.setAttribute("aria-hidden", "true");
134
- svg.setAttribute("class", styles.badgeIcon);
135
- svg.setAttribute("fill", "none");
136
- svg.setAttribute("stroke", "currentColor");
137
- svg.setAttribute("stroke-linecap", "round");
138
- svg.setAttribute("stroke-linejoin", "round");
139
- svg.setAttribute("stroke-width", "2");
140
- svg.setAttribute("viewBox", "0 0 24 24");
141
- if (type === "collection") {
142
- 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");
143
- }
144
- if (type === "doc") {
145
- appendSvgPath(svg, "M14 3v4a1 1 0 0 0 1 1h4");
146
- 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");
147
- }
148
- if (type === "global") {
149
- appendSvgPath(svg, "M3 12a9 9 0 1 0 18 0a9 9 0 0 0 -18 0");
150
- appendSvgPath(svg, "M3.6 9h16.8");
151
- appendSvgPath(svg, "M3.6 15h16.8");
152
- appendSvgPath(svg, "M11.5 3a17 17 0 0 0 0 18");
153
- appendSvgPath(svg, "M12.5 3a17 17 0 0 1 0 18");
154
- }
155
- if (type === "block") {
156
- 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");
157
- 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");
158
- }
159
- return svg;
160
- };
161
- const createBadgePrefix = (suggestion)=>{
162
- const prefix = document.createElement("span");
163
- const icon = createBadgeIcon(suggestion.type);
164
- prefix.className = styles.prefix;
165
- if (icon) {
166
- prefix.append(icon);
167
- }
168
- if (suggestion.type === "doc") {
169
- prefix.append(document.createTextNode(`${suggestion.collection || "document"}:`));
170
- } else if (suggestion.type === "locale") {
171
- prefix.textContent = "locale:";
172
- }
173
- return prefix;
174
- };
175
- const AIInput = ()=>{
176
- const { config } = useConfig();
177
- const editorRef = useRef(null);
178
- const mentionPopoverRef = useRef(null);
179
- const [prompt, setPrompt] = useState("");
180
- const configuredModels = config.admin?.custom?.payloadAiPlugin?.models;
181
- const aiModelConfig = useMemo(()=>getResolvedAIModelConfig(configuredModels), [
182
- configuredModels
183
- ]);
184
- const { selectedModel, setSelectedModel, settingsProvider } = useAISettings({
185
- adminUserSlug: config.admin?.user,
186
- apiRoute: config.routes.api,
187
- defaultModels: aiModelConfig.defaults
188
- });
189
- const [mentionQuery, setMentionQuery] = useState("");
190
- const [mentionRange, setMentionRange] = useState(null);
191
- const [mentionPopoverPosition, setMentionPopoverPosition] = useState(null);
192
- const [mentions, setMentions] = useState([]);
193
- const [appliedProposalIndexes, setAppliedProposalIndexes] = useState([]);
194
- const [response, setResponse] = useState("");
195
- const [tokenUsage, setTokenUsage] = useState(null);
196
- const [error, setError] = useState("");
197
- const [proposals, setProposals] = useState([]);
198
- const [appliedChanges, setAppliedChanges] = useState([]);
199
- const [isLoading, setIsLoading] = useState(false);
200
- const [isApplying, setIsApplying] = useState(false);
201
- const enabledCollectionSlugs = config.admin?.custom?.payloadAiPlugin?.collectionSlugs;
202
- const enabledCollectionSlugSet = useMemo(()=>enabledCollectionSlugs ? new Set(enabledCollectionSlugs) : null, [
203
- enabledCollectionSlugs
204
- ]);
205
- const isCollectionMentionEnabled = (slug)=>!enabledCollectionSlugSet || enabledCollectionSlugSet.has(slug);
206
- const recentChangesEndpoint = useMemo(()=>formatAdminURL({
207
- apiRoute: config.routes.api,
208
- path: "/ai-audit-log"
209
- }), [
210
- config.routes.api
211
- ]);
212
- const allChangesURL = useMemo(()=>`${config.routes.admin || "/admin"}/collections/${auditLogCollectionSlug}`, [
213
- config.routes.admin
214
- ]);
215
- const loadRecentChanges = useCallback(async ()=>{
216
- const res = await fetch(recentChangesEndpoint);
217
- const result = await res.json().catch(()=>null);
218
- if (res.ok && result?.changes) {
219
- setAppliedChanges(result.changes.slice(0, 10));
220
- }
221
- }, [
222
- recentChangesEndpoint
223
- ]);
224
- useEffect(()=>{
225
- void loadRecentChanges().catch(()=>undefined);
226
- }, [
227
- loadRecentChanges
228
- ]);
229
- useEffect(()=>{
230
- if (isLoading || error || proposals.length > 0 || !response) return;
231
- const timeout = window.setTimeout(()=>{
232
- setResponse("");
233
- clearInput();
234
- }, 5000);
235
- return ()=>window.clearTimeout(timeout);
236
- }, [
237
- error,
238
- isLoading,
239
- proposals.length,
240
- response
241
- ]);
242
- const collections = config.collections.filter((collection)=>!isInternalCollection(collection.slug)).filter((collection)=>isCollectionMentionEnabled(collection.slug)).map((collection)=>({
243
- label: getSerializableLabel(collection.labels?.singular, collection.slug),
244
- slug: collection.slug,
245
- type: "collection"
246
- }));
247
- const globals = config.globals?.map((global)=>({
248
- label: getSerializableLabel(global.label, global.slug),
249
- slug: global.slug,
250
- type: "global"
251
- })) || [];
252
- const localizationConfig = config.localization;
253
- const localesConfig = localizationConfig?.locales ?? [];
254
- const locales = localesConfig.flatMap((locale)=>{
255
- if (typeof locale === "string") {
256
- return [
257
- {
258
- isDefault: locale === localizationConfig?.defaultLocale,
259
- label: locale,
260
- slug: locale,
261
- type: "locale"
262
- }
263
- ];
264
- }
265
- if (!locale || typeof locale !== "object") return [];
266
- const slug = typeof locale.code === "string" ? locale.code : typeof locale.label === "string" ? locale.label : null;
267
- if (!slug) return [];
268
- return [
269
- {
270
- isDefault: slug === localizationConfig?.defaultLocale,
271
- label: getSerializableLabel(locale.label, slug),
272
- slug,
273
- type: "locale"
274
- }
275
- ];
276
- });
277
- const blocks = [
278
- ...config.collections.filter((collection)=>isCollectionMentionEnabled(collection.slug)).flatMap((collection)=>collectBlockOptions({
279
- fields: collection.fields,
280
- parent: collection.slug
281
- })),
282
- ...config.globals?.flatMap((global)=>collectBlockOptions({
283
- fields: global.fields,
284
- parent: global.slug
285
- })) || []
286
- ];
287
- const mentionOptions = [
288
- ...collections,
289
- ...globals,
290
- ...blocks,
291
- ...locales
292
- ];
293
- const normalizedMentionQuery = mentionQuery.toLowerCase();
294
- const filteredCollections = collections.filter((collection)=>collection.slug.toLowerCase().includes(normalizedMentionQuery) || collection.label.toLowerCase().includes(normalizedMentionQuery));
295
- const filteredMentionOptions = mentionOptions.filter((option)=>option.slug.toLowerCase().includes(normalizedMentionQuery) || option.label.toLowerCase().includes(normalizedMentionQuery));
296
- const documentSuggestionCollection = filteredCollections.length === 1 ? filteredCollections[0]?.slug : null;
297
- const { documentSuggestions, resetDocumentSuggestions } = useDocumentMentionSuggestions({
298
- apiRoute: config.routes.api,
299
- documentSuggestionCollection,
300
- mentionQuery,
301
- mentionRange
302
- });
303
- const mentionSuggestions = [
304
- ...filteredMentionOptions,
305
- ...documentSuggestions
306
- ];
307
- const getTextBeforeCaret = (element)=>{
308
- const selection = window.getSelection();
309
- if (!selection || selection.rangeCount === 0) return "";
310
- const range = selection.getRangeAt(0);
311
- const clonedRange = range.cloneRange();
312
- clonedRange.selectNodeContents(element);
313
- clonedRange.setEnd(range.endContainer, range.endOffset);
314
- return clonedRange.toString();
315
- };
316
- const getTextNodeAtOffset = (element, offset)=>{
317
- const walker = document.createTreeWalker(element, NodeFilter.SHOW_TEXT);
318
- let currentOffset = 0;
319
- let node = walker.nextNode();
320
- while(node){
321
- const nextOffset = currentOffset + (node.textContent?.length || 0);
322
- if (offset <= nextOffset) {
323
- return {
324
- node,
325
- offset: offset - currentOffset
326
- };
327
- }
328
- currentOffset = nextOffset;
329
- node = walker.nextNode();
330
- }
331
- const textNode = document.createTextNode("");
332
- element.append(textNode);
333
- return {
334
- node: textNode,
335
- offset: 0
336
- };
337
- };
338
- const updateMentionPopoverPosition = useCallback((range)=>{
339
- const editor = editorRef.current;
340
- const popover = mentionPopoverRef.current;
341
- if (!editor || !popover || !range) {
342
- setMentionPopoverPosition(null);
343
- return;
344
- }
345
- const startPosition = getTextNodeAtOffset(editor, range.start);
346
- const anchorRange = document.createRange();
347
- anchorRange.setStart(startPosition.node, startPosition.offset);
348
- anchorRange.setEnd(startPosition.node, startPosition.offset);
349
- const editorRect = editor.getBoundingClientRect();
350
- const anchorRect = anchorRange.getBoundingClientRect();
351
- const fallbackLeft = Math.max(0, anchorRect.left - editorRect.left + 12);
352
- const fallbackTop = Math.max(0, anchorRect.bottom - editorRect.top + 20);
353
- const popoverWidth = popover.offsetWidth || 260;
354
- const maxLeft = Math.max(0, editor.clientWidth - popoverWidth);
355
- setMentionPopoverPosition({
356
- left: Math.min(fallbackLeft, maxLeft),
357
- top: fallbackTop
358
- });
359
- }, []);
360
- const replaceTextRangeWithBadge = ({ badge, editor, end, start })=>{
361
- const startPosition = getTextNodeAtOffset(editor, start);
362
- const endPosition = getTextNodeAtOffset(editor, end);
363
- const range = document.createRange();
364
- const trailingSpace = document.createTextNode(" ");
365
- range.setStart(startPosition.node, startPosition.offset);
366
- range.setEnd(endPosition.node, endPosition.offset);
367
- range.deleteContents();
368
- range.insertNode(trailingSpace);
369
- range.insertNode(badge);
370
- const selection = window.getSelection();
371
- const caretRange = document.createRange();
372
- caretRange.setStartAfter(trailingSpace);
373
- caretRange.collapse(true);
374
- selection?.removeAllRanges();
375
- selection?.addRange(caretRange);
376
- };
377
- const updateMentionState = (valueBeforeCaret)=>{
378
- const activeMention = getActiveMentionRange(valueBeforeCaret);
379
- if (!activeMention) {
380
- setMentionQuery("");
381
- setMentionRange(null);
382
- setMentionPopoverPosition(null);
383
- return;
384
- }
385
- setMentionQuery(activeMention.query);
386
- setMentionRange(activeMention.range);
387
- };
388
- useEffect(()=>{
389
- if (!mentionRange) {
390
- setMentionPopoverPosition(null);
391
- return;
392
- }
393
- updateMentionPopoverPosition(mentionRange);
394
- }, [
395
- mentionRange,
396
- mentionSuggestions.length,
397
- updateMentionPopoverPosition
398
- ]);
399
- useEffect(()=>{
400
- if (!mentionRange) return;
401
- const updatePosition = ()=>updateMentionPopoverPosition(mentionRange);
402
- window.addEventListener("resize", updatePosition);
403
- window.addEventListener("scroll", updatePosition, true);
404
- return ()=>{
405
- window.removeEventListener("resize", updatePosition);
406
- window.removeEventListener("scroll", updatePosition, true);
407
- };
408
- }, [
409
- mentionRange,
410
- updateMentionPopoverPosition
411
- ]);
412
- const clearInput = ()=>{
413
- setPrompt("");
414
- setMentions([]);
415
- if (editorRef.current) editorRef.current.textContent = "";
416
- };
417
- const getProposalViewURL = (proposal)=>{
418
- const adminRoute = config.routes.admin || "/admin";
419
- if (proposal.action === "updateGlobal" && proposal.slug) {
420
- return `${adminRoute}/globals/${proposal.slug}`;
421
- }
422
- if (proposal.collection && proposal.id) {
423
- return `${adminRoute}/collections/${proposal.collection}/${proposal.id}`;
424
- }
425
- return null;
426
- };
427
- const insertMention = (suggestion)=>{
428
- const editor = editorRef.current;
429
- if (!mentionRange || !editor) return;
430
- const currentValue = editor.innerText;
431
- const beforeMention = currentValue.slice(0, mentionRange.start);
432
- const afterMention = currentValue.slice(mentionRange.end);
433
- const badgeType = suggestion.type === "doc" ? "document" : suggestion.type;
434
- const badgePrefix = `${badgeType}:`;
435
- const badgeText = `${badgePrefix} ${suggestion.label}`;
436
- const badge = document.createElement("span");
437
- badge.className = [
438
- styles.badge,
439
- styles[suggestion.type],
440
- styles.inlineBadge
441
- ].join(" ");
442
- badge.contentEditable = "false";
443
- badge.append(createBadgePrefix(suggestion), Object.assign(document.createElement("span"), {
444
- className: styles.name,
445
- textContent: suggestion.label
446
- }));
447
- replaceTextRangeWithBadge({
448
- badge,
449
- editor,
450
- end: mentionRange.end,
451
- start: mentionRange.start
452
- });
453
- editor.focus();
454
- setPrompt(`${beforeMention}${badgeText} ${afterMention}`);
455
- setMentions((currentMentions)=>{
456
- 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);
457
- if (mentionExists) return currentMentions;
458
- return [
459
- ...currentMentions,
460
- suggestion
461
- ];
462
- });
463
- setMentionQuery("");
464
- setMentionRange(null);
465
- setMentionPopoverPosition(null);
466
- resetDocumentSuggestions();
467
- };
468
- const handleSubmit = async ()=>{
469
- const trimmedPrompt = prompt.trim();
470
- if (!trimmedPrompt) return;
471
- setIsLoading(true);
472
- setAppliedProposalIndexes([]);
473
- setError("");
474
- setProposals([]);
475
- setResponse("");
476
- setTokenUsage(null);
477
- try {
478
- const res = await fetch(formatAdminURL({
479
- apiRoute: config.routes.api,
480
- path: "/ai-chat"
481
- }), {
482
- body: JSON.stringify({
483
- mentions,
484
- model: selectedModel,
485
- prompt: trimmedPrompt
486
- }),
487
- headers: {
488
- "Content-Type": "application/json"
489
- },
490
- method: "POST"
491
- });
492
- if (!res.ok) {
493
- const result = await res.json().catch(()=>null);
494
- throw new Error(result?.error || "AI request failed");
495
- }
496
- if (!res.body) {
497
- throw new Error("AI response stream is unavailable");
498
- }
499
- const reader = res.body.getReader();
500
- const decoder = new TextDecoder();
501
- let buffer = "";
502
- let receivedProposals = [];
503
- while(true){
504
- const { done, value } = await reader.read();
505
- if (done) break;
506
- buffer += decoder.decode(value, {
507
- stream: true
508
- });
509
- const chunks = buffer.split("\n\n");
510
- buffer = chunks.pop() || "";
511
- for (const chunk of chunks){
512
- const event = parseSSEEvent(chunk);
513
- if (!event) continue;
514
- if (event.event === "text") {
515
- if (event.data.delta) {
516
- setResponse((current)=>current + sanitizeResponseText(event.data.delta || ""));
517
- }
518
- continue;
519
- }
520
- if (event.event === "proposals") {
521
- receivedProposals = event.data.proposals || [];
522
- setProposals(receivedProposals);
523
- setTokenUsage(event.data.usage || null);
524
- continue;
525
- }
526
- if (event.event === "error") {
527
- throw new Error(event.data.error || "AI request failed");
528
- }
529
- }
530
- }
531
- const finalEvent = buffer.trim() ? parseSSEEvent(buffer.trim()) : null;
532
- if (finalEvent?.event === "proposals") {
533
- receivedProposals = finalEvent.data.proposals || [];
534
- setProposals(receivedProposals);
535
- setTokenUsage(finalEvent.data.usage || null);
536
- }
537
- if (finalEvent?.event === "error") {
538
- throw new Error(finalEvent.data.error || "AI request failed");
539
- }
540
- if (receivedProposals.length === 0) {
541
- clearInput();
542
- }
543
- } catch (err) {
544
- setProposals([]);
545
- setResponse("");
546
- setTokenUsage(null);
547
- setError(err instanceof Error ? err.message : "AI request failed");
548
- } finally{
549
- setIsLoading(false);
550
- }
551
- };
552
- const handleApplyProposal = async (proposal)=>{
553
- setIsApplying(true);
554
- setError("");
555
- try {
556
- const res = await fetch(formatAdminURL({
557
- apiRoute: config.routes.api,
558
- path: "/ai-apply-action"
559
- }), {
560
- body: JSON.stringify({
561
- aiResponse: response,
562
- prompt,
563
- proposal,
564
- tokenUsage
565
- }),
566
- headers: {
567
- "Content-Type": "application/json"
568
- },
569
- method: "POST"
570
- });
571
- const result = await res.json();
572
- if (!res.ok) {
573
- setProposals([]);
574
- setResponse("");
575
- throw new Error(result.error || "Could not apply proposal");
576
- }
577
- setAppliedProposalIndexes([]);
578
- setError("");
579
- setProposals([]);
580
- setResponse("");
581
- setTokenUsage(null);
582
- if (result.change) {
583
- setAppliedChanges((current)=>[
584
- result.change,
585
- ...current
586
- ].slice(0, 10));
587
- }
588
- void loadRecentChanges().catch(()=>undefined);
589
- clearInput();
590
- } catch (err) {
591
- setError(err instanceof Error ? err.message : "Could not apply proposal");
592
- } finally{
593
- setIsApplying(false);
594
- }
595
- };
596
- return /*#__PURE__*/ _jsxs("div", {
597
- className: styles.chatLayout,
598
- children: [
599
- /*#__PURE__*/ _jsxs("div", {
600
- className: styles.chat,
601
- children: [
602
- /*#__PURE__*/ _jsx("div", {
603
- className: styles.chatHeader,
604
- children: /*#__PURE__*/ _jsxs("div", {
605
- children: [
606
- /*#__PURE__*/ _jsx("h2", {
607
- className: styles.chatTitle,
608
- children: "AI Assistant"
609
- }),
610
- /*#__PURE__*/ _jsx("p", {
611
- className: styles.chatDescription,
612
- children: "Ask AI to draft, improve, or analyze content."
613
- })
614
- ]
615
- })
616
- }),
617
- /*#__PURE__*/ _jsxs("div", {
618
- className: styles.chatInputRow,
619
- children: [
620
- /*#__PURE__*/ _jsx("div", {
621
- className: styles.chatInputSurface,
622
- children: /*#__PURE__*/ _jsx("div", {
623
- className: styles.chatInput,
624
- contentEditable: true,
625
- "data-placeholder": "Ask AI...",
626
- onInput: (event)=>{
627
- const value = event.currentTarget.innerText;
628
- setPrompt(value);
629
- if (!value.trim()) {
630
- setMentions([]);
631
- }
632
- updateMentionState(getTextBeforeCaret(event.currentTarget));
633
- },
634
- onKeyDown: (event)=>{
635
- if (event.key === "ArrowDown" && mentionRange && mentionSuggestions.length > 0) {
636
- const firstOption = mentionPopoverRef.current?.querySelector("button");
637
- if (firstOption) {
638
- event.preventDefault();
639
- firstOption.focus();
640
- return;
641
- }
642
- }
643
- if (event.key === "Enter" && !event.shiftKey) {
644
- event.preventDefault();
645
- void handleSubmit();
646
- }
647
- },
648
- ref: editorRef,
649
- role: "textbox",
650
- suppressContentEditableWarning: true
651
- })
652
- }),
653
- mentionRange && /*#__PURE__*/ _jsx(MentionPopover, {
654
- containerRef: mentionPopoverRef,
655
- onSelect: insertMention,
656
- style: mentionPopoverPosition ? {
657
- left: `${mentionPopoverPosition.left}px`,
658
- top: `${mentionPopoverPosition.top}px`
659
- } : undefined,
660
- suggestions: mentionSuggestions
661
- })
662
- ]
663
- }),
664
- /*#__PURE__*/ _jsxs("div", {
665
- className: styles.chatActionsRow,
666
- children: [
667
- /*#__PURE__*/ _jsx("div", {
668
- className: styles.settings,
669
- children: /*#__PURE__*/ _jsxs("label", {
670
- className: styles.setting,
671
- children: [
672
- /*#__PURE__*/ _jsx("span", {
673
- className: styles.settingLabel,
674
- children: "Model"
675
- }),
676
- /*#__PURE__*/ _jsxs("div", {
677
- className: styles.selectWrapper,
678
- children: [
679
- getProviderIcon(settingsProvider),
680
- /*#__PURE__*/ _jsxs("select", {
681
- className: styles.select,
682
- disabled: !settingsProvider,
683
- onChange: (event)=>setSelectedModel(event.target.value),
684
- value: selectedModel,
685
- children: [
686
- !settingsProvider && /*#__PURE__*/ _jsx("option", {
687
- value: "",
688
- children: "Select provider in account settings"
689
- }),
690
- settingsProvider && aiModelConfig.providers[settingsProvider].map((model)=>/*#__PURE__*/ _jsx("option", {
691
- value: model.value,
692
- children: model.label
693
- }, model.value))
694
- ]
695
- })
696
- ]
697
- })
698
- ]
699
- })
700
- }),
701
- /*#__PURE__*/ _jsxs("button", {
702
- className: styles.chatButton,
703
- disabled: !prompt.trim() || !settingsProvider || !selectedModel || isLoading,
704
- onClick: ()=>void handleSubmit(),
705
- type: "button",
706
- children: [
707
- /*#__PURE__*/ _jsx(Send, {
708
- width: 14,
709
- height: 14
710
- }),
711
- isLoading ? "Sending..." : "Send"
712
- ]
713
- })
714
- ]
715
- }),
716
- /*#__PURE__*/ _jsx(ActionToast, {
717
- apiRoute: config.routes.api,
718
- appliedProposalIndexes: appliedProposalIndexes,
719
- description: response,
720
- error: error,
721
- getViewURL: getProposalViewURL,
722
- isApplying: isApplying,
723
- onDismiss: ()=>{
724
- setAppliedProposalIndexes([]);
725
- setError("");
726
- setProposals([]);
727
- setResponse("");
728
- setTokenUsage(null);
729
- clearInput();
730
- },
731
- onDismissError: ()=>{
732
- setError("");
733
- },
734
- onApply: (proposal, _index)=>void handleApplyProposal(proposal),
735
- proposals: proposals,
736
- tokenUsage: tokenUsage
737
- })
738
- ]
739
- }),
740
- /*#__PURE__*/ _jsx(RecentChangesList, {
741
- allChangesURL: allChangesURL,
742
- changes: appliedChanges
743
- })
744
- ]
745
- });
746
- };
747
- export default AIInput;