@mvriu5/payload-ai 1.3.2 → 1.4.0

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 (32) hide show
  1. package/README.md +76 -13
  2. package/dist/ai/providerOptions.d.ts +24 -1
  3. package/dist/ai/providerOptions.js +81 -0
  4. package/dist/ai/providerRuntime.d.ts +2 -1
  5. package/dist/ai/providerRuntime.js +5 -2
  6. package/dist/ai/tokenUsage.d.ts +38 -0
  7. package/dist/ai/tokenUsage.js +106 -0
  8. package/dist/components/Icons.d.ts +1 -0
  9. package/dist/components/Icons.js +15 -0
  10. package/dist/components/action-toast/ActionToast.d.ts +5 -1
  11. package/dist/components/action-toast/ActionToast.js +57 -21
  12. package/dist/components/ai-input/AIInput.d.ts +4 -1
  13. package/dist/components/ai-input/AIInput.js +165 -50
  14. package/dist/components/ai-input/AIInput.module.css +40 -2
  15. package/dist/components/audit-log-list/AuditLogList.js +9 -2
  16. package/dist/components/dashboard/Dashboard.js +3 -1
  17. package/dist/components/hooks/useAIChatStream.d.ts +12 -1
  18. package/dist/components/hooks/useAIChatStream.js +13 -4
  19. package/dist/components/hooks/useAISettings.d.ts +6 -4
  20. package/dist/components/hooks/useAISettings.js +62 -22
  21. package/dist/components/hooks/usePluginConfig.d.ts +8 -20
  22. package/dist/components/hooks/usePluginConfig.js +9 -2
  23. package/dist/components/text-shimmer/TextShimmer.d.ts +9 -0
  24. package/dist/components/text-shimmer/TextShimmer.js +34 -0
  25. package/dist/components/text-shimmer/TextShimmer.module.css +19 -0
  26. package/dist/exports/client.d.ts +2 -0
  27. package/dist/exports/client.js +2 -0
  28. package/dist/handlers/chatHandler.d.ts +4 -1
  29. package/dist/handlers/chatHandler.js +198 -17
  30. package/dist/index.d.ts +6 -1
  31. package/dist/index.js +92 -5
  32. package/package.json +18 -16
@@ -4,7 +4,28 @@ import { formatAdminURL } from "payload/shared";
4
4
  import { useState } from "react";
5
5
  import { redactSensitiveData } from "../../ai/sensitiveData.js";
6
6
  import { DiffDialog } from "../diff-dialog/DiffDialog.js";
7
+ import { TextShimmer } from "../text-shimmer/TextShimmer.js";
7
8
  import styles from "./ActionToast.module.css";
9
+ const ACTION_TOAST_TEXT = {
10
+ aiRequestFailed: "AI request failed",
11
+ aiResponse: "AI response",
12
+ applyProposal: "Apply proposal",
13
+ details: "Details",
14
+ diffReviewFailed: "Diff review failed",
15
+ dismiss: "Dismiss",
16
+ dismissProposals: "Dismiss proposals",
17
+ fullResponse: "Full response",
18
+ goToSource: "Go to source",
19
+ loading: "Loading",
20
+ proposalDiffError: "Could not load proposal diff.",
21
+ redacted: "[redacted]",
22
+ review: "Review",
23
+ reviewProposal: "Review proposal",
24
+ waitingForResponse: "Please wait, until the Response is received"
25
+ };
26
+ const PROPOSAL_DIFF_ENDPOINT = "/ai-proposal-diff";
27
+ const JSON_CONTENT_TYPE = "application/json";
28
+ const POST_METHOD = "POST";
8
29
  const maxDescriptionLength = 220;
9
30
  const getDescriptionPreview = (description)=>{
10
31
  if (description.length <= maxDescriptionLength) return description;
@@ -15,16 +36,16 @@ const getSafeProposalDetails = (proposal)=>{
15
36
  if (redactedProposal._aiSignature) {
16
37
  redactedProposal._aiSignature = {
17
38
  expiresAt: redactedProposal._aiSignature.expiresAt,
18
- value: "[redacted]"
39
+ value: ACTION_TOAST_TEXT.redacted
19
40
  };
20
41
  }
21
42
  return redactedProposal;
22
43
  };
23
- export const ActionToast = ({ apiRoute, description, error, getViewURL, isApplying, onDismiss, onDismissError, onApply, prompt, proposals, tokenUsage })=>{
44
+ export const ActionToast = ({ apiRoute, description, error, getViewURL, isApplying, isLoading, onDismiss, onDismissError, onApply, prompt, proposals, tokenUsage })=>{
24
45
  const [activeDiff, setActiveDiff] = useState(null);
25
46
  const [diffError, setDiffError] = useState("");
26
47
  const [loadingDiffIndex, setLoadingDiffIndex] = useState(null);
27
- if (proposals.length === 0 && !error && !description) return null;
48
+ if (proposals.length === 0 && !error && !description && !isLoading) return null;
28
49
  const descriptionPreview = description ? getDescriptionPreview(description) : "";
29
50
  const isDescriptionTruncated = Boolean(description) && descriptionPreview !== description;
30
51
  const openDiff = async (proposal, index)=>{
@@ -33,20 +54,20 @@ export const ActionToast = ({ apiRoute, description, error, getViewURL, isApplyi
33
54
  try {
34
55
  const res = await fetch(formatAdminURL({
35
56
  apiRoute,
36
- path: "/ai-proposal-diff"
57
+ path: PROPOSAL_DIFF_ENDPOINT
37
58
  }), {
38
59
  body: JSON.stringify({
39
60
  proposal,
40
61
  prompt
41
62
  }),
42
63
  headers: {
43
- "Content-Type": "application/json"
64
+ "Content-Type": JSON_CONTENT_TYPE
44
65
  },
45
- method: "POST"
66
+ method: POST_METHOD
46
67
  });
47
68
  const result = await res.json().catch(()=>null);
48
69
  if (!res.ok || !result) {
49
- throw new Error(result?.error || "Could not load proposal diff.");
70
+ throw new Error(result?.error || ACTION_TOAST_TEXT.proposalDiffError);
50
71
  }
51
72
  setActiveDiff({
52
73
  change: null,
@@ -57,7 +78,7 @@ export const ActionToast = ({ apiRoute, description, error, getViewURL, isApplyi
57
78
  proposal
58
79
  });
59
80
  } catch (err) {
60
- setDiffError(err instanceof Error ? err.message : "Could not load proposal diff.");
81
+ setDiffError(err instanceof Error ? err.message : ACTION_TOAST_TEXT.proposalDiffError);
61
82
  } finally{
62
83
  setLoadingDiffIndex(null);
63
84
  }
@@ -65,6 +86,21 @@ export const ActionToast = ({ apiRoute, description, error, getViewURL, isApplyi
65
86
  return /*#__PURE__*/ _jsxs("div", {
66
87
  className: styles.list,
67
88
  children: [
89
+ proposals.length === 0 && isLoading && /*#__PURE__*/ _jsxs("div", {
90
+ className: styles.item,
91
+ children: [
92
+ /*#__PURE__*/ _jsx("div", {
93
+ className: styles.label,
94
+ children: ACTION_TOAST_TEXT.aiResponse
95
+ }),
96
+ /*#__PURE__*/ _jsx("div", {
97
+ className: styles.description,
98
+ children: /*#__PURE__*/ _jsx(TextShimmer, {
99
+ children: ACTION_TOAST_TEXT.waitingForResponse
100
+ })
101
+ })
102
+ ]
103
+ }),
68
104
  error && /*#__PURE__*/ _jsxs("div", {
69
105
  className: `${styles.item} ${styles.errorItem}`,
70
106
  children: [
@@ -72,7 +108,7 @@ export const ActionToast = ({ apiRoute, description, error, getViewURL, isApplyi
72
108
  children: [
73
109
  /*#__PURE__*/ _jsx("div", {
74
110
  className: styles.label,
75
- children: "AI request failed"
111
+ children: ACTION_TOAST_TEXT.aiRequestFailed
76
112
  }),
77
113
  /*#__PURE__*/ _jsx("div", {
78
114
  className: styles.description,
@@ -84,7 +120,7 @@ export const ActionToast = ({ apiRoute, description, error, getViewURL, isApplyi
84
120
  className: styles.button,
85
121
  onClick: onDismissError,
86
122
  type: "button",
87
- children: "Dismiss"
123
+ children: ACTION_TOAST_TEXT.dismiss
88
124
  })
89
125
  ]
90
126
  }),
@@ -94,7 +130,7 @@ export const ActionToast = ({ apiRoute, description, error, getViewURL, isApplyi
94
130
  children: [
95
131
  /*#__PURE__*/ _jsx("div", {
96
132
  className: styles.label,
97
- children: "AI response"
133
+ children: ACTION_TOAST_TEXT.aiResponse
98
134
  }),
99
135
  /*#__PURE__*/ _jsx("div", {
100
136
  className: styles.description,
@@ -105,7 +141,7 @@ export const ActionToast = ({ apiRoute, description, error, getViewURL, isApplyi
105
141
  children: [
106
142
  /*#__PURE__*/ _jsx("summary", {
107
143
  className: styles.summary,
108
- children: "Full response"
144
+ children: ACTION_TOAST_TEXT.fullResponse
109
145
  }),
110
146
  /*#__PURE__*/ _jsx("pre", {
111
147
  className: styles.proposalDetails,
@@ -146,7 +182,7 @@ export const ActionToast = ({ apiRoute, description, error, getViewURL, isApplyi
146
182
  children: [
147
183
  /*#__PURE__*/ _jsx("summary", {
148
184
  className: styles.summary,
149
- children: "Full response"
185
+ children: ACTION_TOAST_TEXT.fullResponse
150
186
  }),
151
187
  /*#__PURE__*/ _jsx("pre", {
152
188
  className: styles.proposalDetails,
@@ -159,7 +195,7 @@ export const ActionToast = ({ apiRoute, description, error, getViewURL, isApplyi
159
195
  children: [
160
196
  /*#__PURE__*/ _jsx("summary", {
161
197
  className: styles.summary,
162
- children: "Details"
198
+ children: ACTION_TOAST_TEXT.details
163
199
  }),
164
200
  /*#__PURE__*/ _jsx("pre", {
165
201
  className: styles.proposalDetails,
@@ -176,14 +212,14 @@ export const ActionToast = ({ apiRoute, description, error, getViewURL, isApplyi
176
212
  className: styles.viewAction,
177
213
  children: [
178
214
  /*#__PURE__*/ _jsxs(Button, {
179
- "aria-label": `Review proposal: ${proposal.label}`,
215
+ "aria-label": `${ACTION_TOAST_TEXT.reviewProposal}: ${proposal.label}`,
180
216
  margin: false,
181
217
  buttonStyle: "subtle",
182
218
  disabled: loadingDiffIndex === index,
183
219
  onClick: ()=>void openDiff(proposal, index),
184
220
  children: [
185
221
  /*#__PURE__*/ _jsx(SwapIcon, {}),
186
- loadingDiffIndex === index ? "Loading" : "Review"
222
+ loadingDiffIndex === index ? ACTION_TOAST_TEXT.loading : ACTION_TOAST_TEXT.review
187
223
  ]
188
224
  }),
189
225
  viewURL && /*#__PURE__*/ _jsx(Button, {
@@ -192,7 +228,7 @@ export const ActionToast = ({ apiRoute, description, error, getViewURL, isApplyi
192
228
  url: viewURL,
193
229
  newTab: true,
194
230
  margin: false,
195
- children: "Go to source"
231
+ children: ACTION_TOAST_TEXT.goToSource
196
232
  })
197
233
  ]
198
234
  }),
@@ -201,13 +237,13 @@ export const ActionToast = ({ apiRoute, description, error, getViewURL, isApplyi
201
237
  children: [
202
238
  onDismiss && /*#__PURE__*/ _jsx(Button, {
203
239
  icon: "x",
204
- "aria-label": "Dismiss proposals",
240
+ "aria-label": ACTION_TOAST_TEXT.dismissProposals,
205
241
  buttonStyle: "subtle",
206
242
  margin: false,
207
243
  onClick: onDismiss
208
244
  }),
209
245
  /*#__PURE__*/ _jsx(Button, {
210
- "aria-label": `Apply proposal: ${proposal.label}`,
246
+ "aria-label": `${ACTION_TOAST_TEXT.applyProposal}: ${proposal.label}`,
211
247
  margin: false,
212
248
  buttonStyle: "primary",
213
249
  disabled: isApplying,
@@ -228,7 +264,7 @@ export const ActionToast = ({ apiRoute, description, error, getViewURL, isApplyi
228
264
  children: [
229
265
  /*#__PURE__*/ _jsx("div", {
230
266
  className: styles.label,
231
- children: "Diff review failed"
267
+ children: ACTION_TOAST_TEXT.diffReviewFailed
232
268
  }),
233
269
  /*#__PURE__*/ _jsx("div", {
234
270
  className: styles.description,
@@ -240,7 +276,7 @@ export const ActionToast = ({ apiRoute, description, error, getViewURL, isApplyi
240
276
  className: styles.button,
241
277
  onClick: ()=>setDiffError(""),
242
278
  type: "button",
243
- children: "Dismiss"
279
+ children: ACTION_TOAST_TEXT.dismiss
244
280
  })
245
281
  ]
246
282
  }),
@@ -1,2 +1,5 @@
1
- declare const AIInput: () => import("react").JSX.Element;
1
+ type AIInputProps = {
2
+ isDashboard?: boolean;
3
+ };
4
+ declare const AIInput: ({ isDashboard }: AIInputProps) => import("react").JSX.Element;
2
5
  export default AIInput;
@@ -1,15 +1,16 @@
1
1
  "use client";
2
2
  import { jsx as _jsx, jsxs as _jsxs, Fragment as _Fragment } from "react/jsx-runtime";
3
- import { Button, PlusIcon, useConfig } from "@payloadcms/ui";
3
+ import { Button, useConfig, useDocumentForm, useDocumentInfo, useLocale } from "@payloadcms/ui";
4
4
  import { formatAdminURL } from "payload/shared";
5
5
  import { useEffect, useRef, useState } from "react";
6
+ import { mergeData } from "../../payload/shared.js";
6
7
  import { ActionToast } from "../action-toast/ActionToast.js";
7
8
  import { useAIChatStream } from "../hooks/useAIChatStream.js";
8
9
  import { useAISettings } from "../hooks/useAISettings.js";
9
10
  import { useAuditLog } from "../hooks/useAuditLog.js";
10
11
  import { getTextBeforeCaret, useMentions } from "../hooks/useMentions.js";
11
12
  import { usePluginConfig } from "../hooks/usePluginConfig.js";
12
- import { ClaudeIcon, GoogleGeminiIcon, MistralAiIcon, OpenaiIcon, OpenrouterIcon } from "../Icons.js";
13
+ import { ClaudeIcon, GoogleGeminiIcon, MistralAiIcon, OpenaiIcon, OpenrouterIcon, PaperclipIcon } from "../Icons.js";
13
14
  import { MentionPopover } from "../mention-popover/MentionPopover.js";
14
15
  import styles from "./AIInput.module.css";
15
16
  const getProviderIcon = (provider)=>{
@@ -43,7 +44,32 @@ const getProviderIcon = (provider)=>{
43
44
  }
44
45
  };
45
46
  const getFileSignature = (file)=>`${file.name}:${file.size}:${file.lastModified}:${file.type}`;
46
- const AIInput = ()=>{
47
+ const getModelSelectionValue = (provider, model)=>JSON.stringify([
48
+ provider,
49
+ model
50
+ ]);
51
+ const fallbackConfig = {
52
+ admin: {},
53
+ collections: [],
54
+ globals: [],
55
+ routes: {
56
+ admin: "/admin",
57
+ api: "/api"
58
+ }
59
+ };
60
+ const parseModelSelectionValue = (value)=>{
61
+ try {
62
+ const selection = JSON.parse(value);
63
+ if (!Array.isArray(selection) || selection.length !== 2 || selection.some((part)=>typeof part !== "string")) return null;
64
+ return {
65
+ model: selection[1],
66
+ provider: selection[0]
67
+ };
68
+ } catch {
69
+ return null;
70
+ }
71
+ };
72
+ const AIInputCore = ({ applyProposalLocally, documentScope, isDashboard })=>{
47
73
  const editorRef = useRef(null);
48
74
  const fileInputRef = useRef(null);
49
75
  const [prompt, setPrompt] = useState("");
@@ -54,19 +80,24 @@ const AIInput = ()=>{
54
80
  const selectedFilesRef = useRef([]);
55
81
  const mediaAttachmentsRef = useRef([]);
56
82
  const uploadedFileSignaturesRef = useRef(new Set());
57
- const { config } = useConfig();
58
- const { aiModelConfig, isCollectionMentionEnabled, locales, defaultLocale, media } = usePluginConfig(config);
83
+ const payloadConfigContext = useConfig();
84
+ const config = payloadConfigContext?.config ?? fallbackConfig;
85
+ const hasPayloadConfigContext = Boolean(payloadConfigContext?.config);
86
+ const { isCollectionMentionEnabled, locales, defaultLocale, managedProviders, media, providerProfiles } = usePluginConfig(config);
59
87
  const acceptedMimeTypes = media?.acceptedMimeTypes?.join(",");
60
88
  const mediaEnabled = Boolean(media?.enabled);
61
89
  const { loadRecentChanges, prependChange } = useAuditLog({
62
90
  adminRoute: config.routes.admin,
63
91
  apiRoute: config.routes.api
64
92
  });
65
- const { selectedModel, setSelectedModel, settingsProvider } = useAISettings({
93
+ const { selectedModel, setSelectedProviderModel, settingsProvider } = useAISettings({
66
94
  adminUserSlug: config.admin?.user,
67
95
  apiRoute: config.routes.api,
68
- defaultModels: aiModelConfig.defaults
96
+ managedProviders,
97
+ providerProfiles
69
98
  });
99
+ const selectedProviderProfile = providerProfiles.find((profile)=>profile.id === settingsProvider);
100
+ const selectableProviderProfiles = managedProviders ? providerProfiles : providerProfiles.filter((profile)=>profile.id === settingsProvider);
70
101
  const { clearMentions, insertMention, mentionPopoverPosition, mentionRange, mentionSuggestions, mentionsRef, updateMentionState } = useMentions({
71
102
  apiRoute: config.routes.api,
72
103
  config,
@@ -118,12 +149,14 @@ const AIInput = ()=>{
118
149
  const removeMediaAttachment = (attachmentIndex)=>{
119
150
  updateMediaAttachments((currentAttachments)=>currentAttachments.filter((_, index)=>index !== attachmentIndex));
120
151
  };
121
- const { dismissChat, error, isLoading, proposals, resetChatState, response, setError, setProposals, setResponse, submit, tokenUsage } = useAIChatStream({
152
+ const { dismissChat, error, isLoading, setIsLoading, proposals, resetChatState, response, setError, setProposals, setResponse, submit, tokenUsage } = useAIChatStream({
122
153
  apiRoute: config.routes.api,
123
154
  clearInput,
155
+ documentScope,
124
156
  mentionsRef,
125
157
  prompt,
126
- selectedModel
158
+ selectedModel,
159
+ selectedProvider: settingsProvider
127
160
  });
128
161
  const uploadSelectedFiles = async ()=>{
129
162
  const filesToUpload = selectedFilesRef.current;
@@ -153,6 +186,7 @@ const AIInput = ()=>{
153
186
  };
154
187
  const handleSubmit = async ()=>{
155
188
  if (isUploadingMedia) return;
189
+ setIsLoading(true);
156
190
  setError("");
157
191
  setIsUploadingMedia(true);
158
192
  try {
@@ -201,6 +235,12 @@ const AIInput = ()=>{
201
235
  setIsApplying(true);
202
236
  setError("");
203
237
  try {
238
+ if (applyProposalLocally) {
239
+ await applyProposalLocally(proposal);
240
+ resetChatState();
241
+ clearInput();
242
+ return;
243
+ }
204
244
  const res = await fetch(formatAdminURL({
205
245
  apiRoute: config.routes.api,
206
246
  path: "/ai-apply-action"
@@ -235,8 +275,33 @@ const AIInput = ()=>{
235
275
  setIsApplying(false);
236
276
  }
237
277
  };
278
+ if (!hasPayloadConfigContext) {
279
+ return /*#__PURE__*/ _jsx("div", {
280
+ className: styles.chatLayout,
281
+ children: /*#__PURE__*/ _jsxs("div", {
282
+ className: styles.chat,
283
+ children: [
284
+ /*#__PURE__*/ _jsx("div", {
285
+ className: styles.chatHeader,
286
+ children: /*#__PURE__*/ _jsx("h2", {
287
+ className: styles.chatTitle,
288
+ children: "AI Assistant"
289
+ })
290
+ }),
291
+ /*#__PURE__*/ _jsx("div", {
292
+ className: styles.chatError,
293
+ children: "Payload config context is unavailable. Make sure the app and this plugin resolve the same @payloadcms/ui version."
294
+ })
295
+ ]
296
+ })
297
+ });
298
+ }
238
299
  return /*#__PURE__*/ _jsx("div", {
239
300
  className: styles.chatLayout,
301
+ style: {
302
+ marginBottom: isDashboard ? "0" : "20px",
303
+ height: isDashboard ? "332px" : "220px"
304
+ },
240
305
  children: /*#__PURE__*/ _jsxs("div", {
241
306
  className: styles.chat,
242
307
  children: [
@@ -325,10 +390,37 @@ const AIInput = ()=>{
325
390
  ]
326
391
  }, `${attachment.collection}-${attachment.id}`))
327
392
  ]
393
+ }),
394
+ mediaEnabled && /*#__PURE__*/ _jsxs(_Fragment, {
395
+ children: [
396
+ /*#__PURE__*/ _jsx("input", {
397
+ ref: fileInputRef,
398
+ type: "file",
399
+ accept: acceptedMimeTypes,
400
+ className: styles.fileInput,
401
+ multiple: true,
402
+ onChange: handleSelectFile
403
+ }),
404
+ /*#__PURE__*/ _jsx("button", {
405
+ type: "button",
406
+ className: styles.mediaButton,
407
+ "aria-label": "Attach media",
408
+ title: "Attach media",
409
+ disabled: isLoading || isUploadingMedia || proposals.length > 0,
410
+ onClick: (event)=>{
411
+ event.stopPropagation();
412
+ fileInputRef.current?.click();
413
+ },
414
+ children: /*#__PURE__*/ _jsx(PaperclipIcon, {
415
+ width: 16,
416
+ height: 16
417
+ })
418
+ })
419
+ ]
328
420
  })
329
421
  ]
330
422
  }),
331
- mentionRange && /*#__PURE__*/ _jsx(MentionPopover, {
423
+ mentionRange && isDashboard && /*#__PURE__*/ _jsx(MentionPopover, {
332
424
  onSelect: insertMention,
333
425
  style: mentionPopoverPosition ? {
334
426
  left: `${mentionPopoverPosition.left}px`,
@@ -353,24 +445,30 @@ const AIInput = ()=>{
353
445
  /*#__PURE__*/ _jsxs("div", {
354
446
  className: styles.selectWrapper,
355
447
  children: [
356
- settingsProvider && getProviderIcon(settingsProvider),
448
+ selectedProviderProfile && getProviderIcon(selectedProviderProfile.provider),
357
449
  /*#__PURE__*/ _jsxs("select", {
358
450
  className: styles.select,
359
451
  style: {
360
- paddingLeft: settingsProvider ? "34px" : "12px"
452
+ paddingLeft: selectedProviderProfile ? "34px" : "12px"
361
453
  },
362
454
  disabled: !settingsProvider,
363
- onChange: (event)=>setSelectedModel(event.target.value),
364
- value: selectedModel,
455
+ onChange: (event)=>{
456
+ const selection = parseModelSelectionValue(event.target.value);
457
+ if (selection) setSelectedProviderModel(selection.provider, selection.model);
458
+ },
459
+ value: settingsProvider && selectedModel ? getModelSelectionValue(settingsProvider, selectedModel) : "",
365
460
  children: [
366
461
  !settingsProvider && /*#__PURE__*/ _jsx("option", {
367
462
  value: "",
368
463
  children: "No provider selected"
369
464
  }),
370
- settingsProvider && aiModelConfig.providers[settingsProvider].map((model)=>/*#__PURE__*/ _jsx("option", {
371
- value: model.value,
372
- children: model.label
373
- }, model.value))
465
+ selectableProviderProfiles.map((profile)=>/*#__PURE__*/ _jsx("optgroup", {
466
+ label: profile.label,
467
+ children: profile.models.map((model)=>/*#__PURE__*/ _jsx("option", {
468
+ value: getModelSelectionValue(profile.id, model.value),
469
+ children: model.label
470
+ }, `${profile.id}-${model.value}`))
471
+ }, profile.id))
374
472
  ]
375
473
  })
376
474
  ]
@@ -378,47 +476,26 @@ const AIInput = ()=>{
378
476
  ]
379
477
  })
380
478
  }),
381
- /*#__PURE__*/ _jsxs("div", {
479
+ /*#__PURE__*/ _jsx("div", {
382
480
  className: styles.actions,
383
- children: [
384
- mediaEnabled && /*#__PURE__*/ _jsxs(_Fragment, {
385
- children: [
386
- /*#__PURE__*/ _jsx("input", {
387
- ref: fileInputRef,
388
- type: "file",
389
- accept: acceptedMimeTypes,
390
- className: styles.fileInput,
391
- multiple: true,
392
- onChange: handleSelectFile
393
- }),
394
- /*#__PURE__*/ _jsx(Button, {
395
- buttonStyle: "tab",
396
- "aria-label": "Attach media",
397
- margin: false,
398
- disabled: isLoading || isUploadingMedia || proposals.length > 0,
399
- onClick: ()=>fileInputRef.current?.click(),
400
- children: /*#__PURE__*/ _jsx(PlusIcon, {})
401
- })
402
- ]
403
- }),
404
- /*#__PURE__*/ _jsx(Button, {
405
- buttonStyle: "primary",
406
- "aria-label": "Send",
407
- margin: false,
408
- disabled: !prompt.trim() || !settingsProvider || !selectedModel || isLoading || isUploadingMedia || Boolean(error) || Boolean(response) || proposals.length > 0,
409
- onClick: ()=>void handleSubmit(),
410
- children: isUploadingMedia || isLoading ? "Sending..." : "Send"
411
- })
412
- ]
481
+ children: /*#__PURE__*/ _jsx(Button, {
482
+ buttonStyle: "primary",
483
+ "aria-label": "Send",
484
+ margin: false,
485
+ disabled: !prompt.trim() || !settingsProvider || !selectedModel || isLoading || isUploadingMedia || Boolean(error) || Boolean(response) || proposals.length > 0,
486
+ onClick: ()=>void handleSubmit(),
487
+ children: isUploadingMedia || isLoading ? "Sending..." : "Send"
488
+ })
413
489
  })
414
490
  ]
415
491
  }),
416
- (proposals.length > 0 || response) && /*#__PURE__*/ _jsx(ActionToast, {
492
+ (proposals.length > 0 || response || isLoading) && /*#__PURE__*/ _jsx(ActionToast, {
417
493
  apiRoute: config.routes.api,
418
494
  description: response,
419
495
  error: error,
420
496
  getViewURL: getProposalViewURL,
421
497
  isApplying: isApplying,
498
+ isLoading: isLoading,
422
499
  onDismiss: ()=>dismissChat(),
423
500
  onDismissError: ()=>setError(""),
424
501
  onApply: (proposal, _index)=>void handleApplyProposal(proposal),
@@ -430,4 +507,42 @@ const AIInput = ()=>{
430
507
  })
431
508
  });
432
509
  };
510
+ const DocumentAIInput = ()=>{
511
+ const documentInfo = useDocumentInfo();
512
+ const documentForm = useDocumentForm();
513
+ const locale = useLocale();
514
+ const documentScope = documentInfo.collectionSlug ? {
515
+ collection: documentInfo.collectionSlug,
516
+ ...documentInfo.id === undefined ? {} : {
517
+ id: String(documentInfo.id)
518
+ },
519
+ type: "collection"
520
+ } : documentInfo.globalSlug ? {
521
+ slug: documentInfo.globalSlug,
522
+ type: "global"
523
+ } : undefined;
524
+ const applyProposalLocally = async (proposal)=>{
525
+ if (proposal.action === "delete") {
526
+ throw new Error("Delete proposals cannot be applied without saving.");
527
+ }
528
+ const proposalData = proposal.localizedData ? proposal.localizedData[proposal.locale || locale.code] : proposal.data;
529
+ if (!proposalData) {
530
+ throw new Error(`This proposal has no changes for the active locale "${locale.code}".`);
531
+ }
532
+ const currentData = documentForm.getData();
533
+ await documentForm.reset(mergeData(currentData, proposalData));
534
+ documentForm.setModified(true);
535
+ };
536
+ return /*#__PURE__*/ _jsx(AIInputCore, {
537
+ applyProposalLocally: applyProposalLocally,
538
+ documentScope: documentScope,
539
+ isDashboard: false
540
+ });
541
+ };
542
+ const AIInput = ({ isDashboard = false })=>{
543
+ if (isDashboard) return /*#__PURE__*/ _jsx(AIInputCore, {
544
+ isDashboard: true
545
+ });
546
+ return /*#__PURE__*/ _jsx(DocumentAIInput, {});
547
+ };
433
548
  export default AIInput;
@@ -3,7 +3,6 @@
3
3
  max-width: none;
4
4
  display: flex;
5
5
  flex-direction: column;
6
- height: 332px;
7
6
  overflow: hidden;
8
7
  }
9
8
 
@@ -108,9 +107,47 @@
108
107
  display: none;
109
108
  }
110
109
 
110
+ .mediaButton {
111
+ align-items: center;
112
+ background: transparent;
113
+ border: 0;
114
+ bottom: 8px;
115
+ color: var(--theme-elevation-600);
116
+ cursor: pointer;
117
+ display: inline-flex;
118
+ height: 28px;
119
+ justify-content: center;
120
+ padding: 0;
121
+ position: absolute;
122
+ right: 8px;
123
+ width: 28px;
124
+ z-index: 1;
125
+ }
126
+
127
+ .mediaButton:hover,
128
+ .mediaButton:focus-visible {
129
+ color: var(--theme-text);
130
+ }
131
+
132
+ .mediaButton:focus-visible {
133
+ outline: 1px solid currentColor;
134
+ outline-offset: 1px;
135
+ }
136
+
137
+ .mediaButton:disabled {
138
+ cursor: not-allowed;
139
+ opacity: 0.45;
140
+ }
141
+
142
+ .mediaButton svg {
143
+ height: 16px;
144
+ width: 16px;
145
+ }
146
+
111
147
  .chatInput {
112
148
  background: transparent;
113
149
  border: 0;
150
+ box-sizing: border-box;
114
151
  color: var(--theme-text);
115
152
  font: inherit;
116
153
  flex: 1 1 0;
@@ -118,7 +155,7 @@
118
155
  min-height: 0;
119
156
  overflow-y: auto;
120
157
  overscroll-behavior: contain;
121
- padding: 0;
158
+ padding: 0 32px 32px 0;
122
159
  white-space: pre-wrap;
123
160
  word-break: break-word;
124
161
  width: 100%;
@@ -161,6 +198,7 @@
161
198
  flex-wrap: wrap;
162
199
  gap: 6px;
163
200
  min-height: 22px;
201
+ padding-right: 32px;
164
202
  }
165
203
 
166
204
  .attachmentPill {
@@ -12,8 +12,15 @@ const getChangeProposal = (change)=>({
12
12
  label: change.title,
13
13
  slug: change.slug || undefined
14
14
  });
15
+ const fallbackConfig = {
16
+ routes: {
17
+ admin: "/admin",
18
+ api: "/api"
19
+ }
20
+ };
15
21
  const AuditLogList = ()=>{
16
- const { config } = useConfig();
22
+ const payloadConfigContext = useConfig();
23
+ const config = payloadConfigContext?.config ?? fallbackConfig;
17
24
  const { loadRecentChanges, allChangesURL, appliedChanges: changes } = useAuditLog({
18
25
  adminRoute: config.routes.admin,
19
26
  apiRoute: config.routes.api
@@ -43,7 +50,7 @@ const AuditLogList = ()=>{
43
50
  allChangesURL && /*#__PURE__*/ _jsx(Button, {
44
51
  url: allChangesURL,
45
52
  el: "anchor",
46
- "aria-labelabel": "View all",
53
+ "aria-label": "View all",
47
54
  margin: false,
48
55
  buttonStyle: "tab",
49
56
  size: "small",
@@ -7,7 +7,9 @@ const Dashboard = ()=>{
7
7
  return /*#__PURE__*/ _jsxs("div", {
8
8
  className: styles.dashboard,
9
9
  children: [
10
- /*#__PURE__*/ _jsx(AIInput, {}),
10
+ /*#__PURE__*/ _jsx(AIInput, {
11
+ isDashboard: true
12
+ }),
11
13
  /*#__PURE__*/ _jsx(AuditLogList, {})
12
14
  ]
13
15
  });