@mvriu5/payload-ai 0.5.0 → 0.5.8
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.
- package/README.md +52 -5
- package/dist/ai/providerOptions.d.ts +1 -14
- package/dist/ai/providerOptions.js +0 -23
- package/dist/ai/providerRuntime.d.ts +3 -2
- package/dist/ai/providerRuntime.js +6 -15
- package/dist/components/AIActionProposalList.d.ts +1 -1
- package/dist/components/AIApiKeyField.js +0 -8
- package/dist/components/AIInput.d.ts +1 -1
- package/dist/components/AIInput.js +32 -14
- package/dist/components/AIInput.module.css +165 -148
- package/dist/components/CollectionMentionPopover.d.ts +1 -1
- package/dist/components/Icons.d.ts +5 -0
- package/dist/components/Icons.js +179 -0
- package/dist/components/hooks/useAISettings.d.ts +1 -1
- package/dist/endpoints/aiApplyActionEndpointHandler.d.ts +2 -1
- package/dist/endpoints/aiApplyActionEndpointHandler.js +9 -4
- package/dist/endpoints/aiChatEndpointHandler.d.ts +5 -1
- package/dist/endpoints/aiChatEndpointHandler.js +26 -2
- package/dist/endpoints/aiMentionSuggestionsEndpointHandler.d.ts +2 -1
- package/dist/endpoints/aiMentionSuggestionsEndpointHandler.js +7 -7
- package/dist/index.d.ts +3 -1
- package/dist/index.js +29 -18
- package/dist/payload/collectionPermissions.d.ts +22 -0
- package/dist/payload/collectionPermissions.js +68 -0
- package/dist/payload/schemaContext.d.ts +2 -1
- package/dist/payload/schemaContext.js +6 -3
- package/package.json +113 -122
package/README.md
CHANGED
|
@@ -5,7 +5,7 @@ AI assistant plugin for Payload CMS. It adds an admin dashboard assistant that c
|
|
|
5
5
|
## Install
|
|
6
6
|
|
|
7
7
|
```bash
|
|
8
|
-
|
|
8
|
+
npm add @mvriu5/payload-ai
|
|
9
9
|
```
|
|
10
10
|
|
|
11
11
|
## Usage
|
|
@@ -30,7 +30,7 @@ The plugin adds two fields to the configured Payload admin user collection:
|
|
|
30
30
|
- `aiProvider`
|
|
31
31
|
- `aiApiKey`
|
|
32
32
|
|
|
33
|
-
Users can select their provider and store their own API key in account settings.
|
|
33
|
+
Users can select their provider and optionally store their own API key in account settings. If no account-level key is set, the chat endpoint uses provider environment variables.
|
|
34
34
|
|
|
35
35
|
## Options
|
|
36
36
|
|
|
@@ -38,8 +38,14 @@ Users can select their provider and store their own API key in account settings.
|
|
|
38
38
|
import type { PayloadAiPluginOptions } from "payload-ai-plugin";
|
|
39
39
|
|
|
40
40
|
const options: PayloadAiPluginOptions = {
|
|
41
|
+
allowUserApiKeys: false,
|
|
41
42
|
collections: {
|
|
42
|
-
posts:
|
|
43
|
+
posts: {
|
|
44
|
+
read: true,
|
|
45
|
+
create: true,
|
|
46
|
+
update: true,
|
|
47
|
+
delete: false,
|
|
48
|
+
},
|
|
43
49
|
},
|
|
44
50
|
models: {
|
|
45
51
|
defaults: {
|
|
@@ -59,22 +65,63 @@ const options: PayloadAiPluginOptions = {
|
|
|
59
65
|
|
|
60
66
|
Restricts AI read and write proposals to enabled collection slugs. If omitted, all non-internal Payload collections are available.
|
|
61
67
|
|
|
68
|
+
Use `true` to enable all AI actions for a collection:
|
|
69
|
+
|
|
70
|
+
```ts
|
|
71
|
+
payloadAiPlugin({
|
|
72
|
+
collections: {
|
|
73
|
+
posts: true,
|
|
74
|
+
},
|
|
75
|
+
})
|
|
76
|
+
```
|
|
77
|
+
|
|
78
|
+
Use granular permissions to control each action:
|
|
79
|
+
|
|
80
|
+
```ts
|
|
81
|
+
payloadAiPlugin({
|
|
82
|
+
collections: {
|
|
83
|
+
posts: {
|
|
84
|
+
read: true,
|
|
85
|
+
create: true,
|
|
86
|
+
update: true,
|
|
87
|
+
delete: false,
|
|
88
|
+
},
|
|
89
|
+
},
|
|
90
|
+
})
|
|
91
|
+
```
|
|
92
|
+
|
|
93
|
+
`read` controls schema/context access, document search, and mentions. `create`, `update`, and `delete` control AI action proposals and server-side apply permissions.
|
|
94
|
+
|
|
62
95
|
### `models`
|
|
63
96
|
|
|
64
97
|
Overrides the model list shown in the admin UI and the default model per provider.
|
|
65
98
|
|
|
99
|
+
### `allowUserApiKeys`
|
|
100
|
+
|
|
101
|
+
Controls whether the plugin adds an `aiApiKey` field to the admin user collection.
|
|
102
|
+
|
|
103
|
+
```ts
|
|
104
|
+
payloadAiPlugin({
|
|
105
|
+
allowUserApiKeys: false,
|
|
106
|
+
})
|
|
107
|
+
```
|
|
108
|
+
|
|
109
|
+
When disabled, users can still select an AI provider, but API keys must come from environment variables.
|
|
110
|
+
|
|
66
111
|
### `disabled`
|
|
67
112
|
|
|
68
113
|
Disables endpoint and UI registration while keeping the plugin call in your config.
|
|
69
114
|
|
|
70
115
|
## Provider Environment Variables
|
|
71
116
|
|
|
72
|
-
|
|
117
|
+
API key priority is:
|
|
118
|
+
|
|
119
|
+
1. account-level API key, unless `allowUserApiKeys: false`
|
|
120
|
+
2. provider environment variables
|
|
73
121
|
|
|
74
122
|
- `OPENAI_API_KEY`, `OPENAI_MODEL`
|
|
75
123
|
- `ANTHROPIC_API_KEY`, `ANTHROPIC_MODEL`
|
|
76
124
|
- `GOOGLE_GENERATIVE_AI_API_KEY`, `GOOGLE_GENERATIVE_AI_MODEL`
|
|
77
|
-
- `GROQ_API_KEY`, `GROQ_MODEL`
|
|
78
125
|
- `MISTRAL_API_KEY`, `MISTRAL_MODEL`
|
|
79
126
|
|
|
80
127
|
`PAYLOAD_SECRET` is required for signing AI action proposals.
|
|
@@ -26,19 +26,6 @@ declare const aiProviderModels: {
|
|
|
26
26
|
readonly label: "Gemini 2.5 Pro";
|
|
27
27
|
readonly value: "gemini-2.5-pro";
|
|
28
28
|
}];
|
|
29
|
-
readonly groq: readonly [{
|
|
30
|
-
readonly label: "Llama 3.3 70B Versatile";
|
|
31
|
-
readonly value: "llama-3.3-70b-versatile";
|
|
32
|
-
}, {
|
|
33
|
-
readonly label: "Llama 3.1 8B Instant";
|
|
34
|
-
readonly value: "llama-3.1-8b-instant";
|
|
35
|
-
}, {
|
|
36
|
-
readonly label: "GPT OSS 120B";
|
|
37
|
-
readonly value: "openai/gpt-oss-120b";
|
|
38
|
-
}, {
|
|
39
|
-
readonly label: "GPT OSS 20B";
|
|
40
|
-
readonly value: "openai/gpt-oss-20b";
|
|
41
|
-
}];
|
|
42
29
|
readonly mistral: readonly [{
|
|
43
30
|
readonly label: "Mistral Small";
|
|
44
31
|
readonly value: "mistral-small-latest";
|
|
@@ -78,7 +65,7 @@ export type AIModelConfig = {
|
|
|
78
65
|
providers?: Partial<Record<AIProvider, AIProviderModelOption[]>>;
|
|
79
66
|
};
|
|
80
67
|
export declare const getResolvedAIModelConfig: (modelConfig?: AIModelConfig) => {
|
|
81
|
-
defaults: Record<"claude" | "google" | "
|
|
68
|
+
defaults: Record<"claude" | "google" | "mistral" | "openai", string>;
|
|
82
69
|
providers: AIProviderModels;
|
|
83
70
|
};
|
|
84
71
|
export declare const isAIProvider: (provider: string) => provider is AIProvider;
|
|
@@ -31,24 +31,6 @@ const aiProviderModels = {
|
|
|
31
31
|
value: "gemini-2.5-pro"
|
|
32
32
|
}
|
|
33
33
|
],
|
|
34
|
-
groq: [
|
|
35
|
-
{
|
|
36
|
-
label: "Llama 3.3 70B Versatile",
|
|
37
|
-
value: "llama-3.3-70b-versatile"
|
|
38
|
-
},
|
|
39
|
-
{
|
|
40
|
-
label: "Llama 3.1 8B Instant",
|
|
41
|
-
value: "llama-3.1-8b-instant"
|
|
42
|
-
},
|
|
43
|
-
{
|
|
44
|
-
label: "GPT OSS 120B",
|
|
45
|
-
value: "openai/gpt-oss-120b"
|
|
46
|
-
},
|
|
47
|
-
{
|
|
48
|
-
label: "GPT OSS 20B",
|
|
49
|
-
value: "openai/gpt-oss-20b"
|
|
50
|
-
}
|
|
51
|
-
],
|
|
52
34
|
mistral: [
|
|
53
35
|
{
|
|
54
36
|
label: "Mistral Small",
|
|
@@ -95,10 +77,6 @@ export const aiProviders = [
|
|
|
95
77
|
label: "Google Gemini",
|
|
96
78
|
value: "google"
|
|
97
79
|
},
|
|
98
|
-
{
|
|
99
|
-
label: "Groq",
|
|
100
|
-
value: "groq"
|
|
101
|
-
},
|
|
102
80
|
{
|
|
103
81
|
label: "Mistral",
|
|
104
82
|
value: "mistral"
|
|
@@ -111,7 +89,6 @@ export const aiProviders = [
|
|
|
111
89
|
export const defaultAIModels = {
|
|
112
90
|
claude: aiProviderModels.claude[0].value,
|
|
113
91
|
google: aiProviderModels.google[0].value,
|
|
114
|
-
groq: aiProviderModels.groq[0].value,
|
|
115
92
|
mistral: aiProviderModels.mistral[0].value,
|
|
116
93
|
openai: aiProviderModels.openai[0].value
|
|
117
94
|
};
|
|
@@ -1,7 +1,8 @@
|
|
|
1
1
|
import type { LanguageModel } from "ai";
|
|
2
|
-
import { type AIProvider } from "./providerOptions.js";
|
|
2
|
+
import { type AIProvider, type AIModelConfig } from "./providerOptions.js";
|
|
3
3
|
type ProviderConfig = {
|
|
4
4
|
apiKey?: string | null;
|
|
5
|
+
defaultModels?: AIModelConfig["defaults"];
|
|
5
6
|
model?: string | null;
|
|
6
7
|
provider: AIProvider;
|
|
7
8
|
};
|
|
@@ -10,7 +11,7 @@ type ModelConfig = {
|
|
|
10
11
|
model: string;
|
|
11
12
|
provider: AIProvider;
|
|
12
13
|
};
|
|
13
|
-
export declare const getProviderConfig: ({ apiKey, model, provider, }: ProviderConfig) => {
|
|
14
|
+
export declare const getProviderConfig: ({ apiKey, defaultModels, model, provider, }: ProviderConfig) => {
|
|
14
15
|
apiKey: string | undefined;
|
|
15
16
|
modelID: string;
|
|
16
17
|
};
|
|
@@ -1,37 +1,31 @@
|
|
|
1
1
|
import { createAnthropic } from "@ai-sdk/anthropic";
|
|
2
2
|
import { createGoogleGenerativeAI } from "@ai-sdk/google";
|
|
3
|
-
import { createGroq } from "@ai-sdk/groq";
|
|
4
3
|
import { createMistral } from "@ai-sdk/mistral";
|
|
5
4
|
import { createOpenAI } from "@ai-sdk/openai";
|
|
6
5
|
import { defaultAIModels } from "./providerOptions.js";
|
|
7
|
-
export const getProviderConfig = ({ apiKey, model, provider })=>{
|
|
6
|
+
export const getProviderConfig = ({ apiKey, defaultModels, model, provider })=>{
|
|
7
|
+
const defaultModel = defaultModels?.[provider] || defaultAIModels[provider];
|
|
8
8
|
if (provider === "claude") {
|
|
9
9
|
return {
|
|
10
10
|
apiKey: apiKey || process.env.ANTHROPIC_API_KEY,
|
|
11
|
-
modelID: model || process.env.ANTHROPIC_MODEL ||
|
|
11
|
+
modelID: model || process.env.ANTHROPIC_MODEL || defaultModel
|
|
12
12
|
};
|
|
13
13
|
}
|
|
14
14
|
if (provider === "google") {
|
|
15
15
|
return {
|
|
16
16
|
apiKey: apiKey || process.env.GOOGLE_GENERATIVE_AI_API_KEY,
|
|
17
|
-
modelID: model || process.env.GOOGLE_GENERATIVE_AI_MODEL ||
|
|
18
|
-
};
|
|
19
|
-
}
|
|
20
|
-
if (provider === "groq") {
|
|
21
|
-
return {
|
|
22
|
-
apiKey: apiKey || process.env.GROQ_API_KEY,
|
|
23
|
-
modelID: model || process.env.GROQ_MODEL || defaultAIModels.groq
|
|
17
|
+
modelID: model || process.env.GOOGLE_GENERATIVE_AI_MODEL || defaultModel
|
|
24
18
|
};
|
|
25
19
|
}
|
|
26
20
|
if (provider === "mistral") {
|
|
27
21
|
return {
|
|
28
22
|
apiKey: apiKey || process.env.MISTRAL_API_KEY,
|
|
29
|
-
modelID: model || process.env.MISTRAL_MODEL ||
|
|
23
|
+
modelID: model || process.env.MISTRAL_MODEL || defaultModel
|
|
30
24
|
};
|
|
31
25
|
}
|
|
32
26
|
return {
|
|
33
27
|
apiKey: apiKey || process.env.OPENAI_API_KEY,
|
|
34
|
-
modelID: model || process.env.OPENAI_MODEL ||
|
|
28
|
+
modelID: model || process.env.OPENAI_MODEL || defaultModel
|
|
35
29
|
};
|
|
36
30
|
};
|
|
37
31
|
export const getModel = ({ apiKey, model, provider })=>{
|
|
@@ -41,9 +35,6 @@ export const getModel = ({ apiKey, model, provider })=>{
|
|
|
41
35
|
if (provider === "google") return createGoogleGenerativeAI({
|
|
42
36
|
apiKey
|
|
43
37
|
})(model);
|
|
44
|
-
if (provider === "groq") return createGroq({
|
|
45
|
-
apiKey
|
|
46
|
-
})(model);
|
|
47
38
|
if (provider === "mistral") return createMistral({
|
|
48
39
|
apiKey
|
|
49
40
|
})(model);
|
|
@@ -21,5 +21,5 @@ type AIActionProposalListProps = {
|
|
|
21
21
|
onApply: (proposal: AIActionProposal, index: number) => void;
|
|
22
22
|
proposals: AIActionProposal[];
|
|
23
23
|
};
|
|
24
|
-
export declare const AIActionProposalList: ({ appliedProposalIndexes, description, error, getViewURL, isApplying, onDismiss, onDismissError, onApply, proposals, }: AIActionProposalListProps) => import("react
|
|
24
|
+
export declare const AIActionProposalList: ({ appliedProposalIndexes, description, error, getViewURL, isApplying, onDismiss, onDismissError, onApply, proposals, }: AIActionProposalListProps) => import("react").JSX.Element | null;
|
|
25
25
|
export {};
|
|
@@ -1,13 +1,5 @@
|
|
|
1
1
|
"use client";
|
|
2
2
|
import { useField } from "@payloadcms/ui";
|
|
3
|
-
const getLabel = (label, fallback)=>{
|
|
4
|
-
if (typeof label === "string") return label;
|
|
5
|
-
if (label && typeof label === "object") {
|
|
6
|
-
const firstLabel = Object.values(label)[0];
|
|
7
|
-
if (typeof firstLabel === "string") return firstLabel;
|
|
8
|
-
}
|
|
9
|
-
return fallback;
|
|
10
|
-
};
|
|
11
3
|
const getFieldClassName = ({ className, isReadOnly, showError })=>[
|
|
12
4
|
"field-type",
|
|
13
5
|
"password",
|
|
@@ -1 +1 @@
|
|
|
1
|
-
export declare const AIInput: () => import("react
|
|
1
|
+
export declare const AIInput: () => import("react").JSX.Element;
|
|
@@ -7,6 +7,7 @@ import { getSerializableLabel, isInternalCollection } from "../payload/shared.js
|
|
|
7
7
|
import { AIActionProposalList } from "./AIActionProposalList.js";
|
|
8
8
|
import styles from "./AIInput.module.css";
|
|
9
9
|
import { CollectionMentionPopover } from "./CollectionMentionPopover.js";
|
|
10
|
+
import { ClaudeIcon, GoogleGeminiIcon, MistralAiIcon, OpenaiIcon } from "./Icons.js";
|
|
10
11
|
import { useAISettings } from "./hooks/useAISettings.js";
|
|
11
12
|
import { useDocumentMentionSuggestions } from "./hooks/useDocumentMentionSuggestions.js";
|
|
12
13
|
const collectBlockOptions = ({ fields, parent })=>{
|
|
@@ -35,6 +36,24 @@ const collectBlockOptions = ({ fields, parent })=>{
|
|
|
35
36
|
}
|
|
36
37
|
return options;
|
|
37
38
|
};
|
|
39
|
+
const getProviderIcon = (provider)=>{
|
|
40
|
+
const iconProps = {
|
|
41
|
+
"aria-hidden": true,
|
|
42
|
+
className: styles.selectProviderIcon
|
|
43
|
+
};
|
|
44
|
+
switch(provider){
|
|
45
|
+
case "claude":
|
|
46
|
+
return /*#__PURE__*/ React.createElement(ClaudeIcon, iconProps);
|
|
47
|
+
case "google":
|
|
48
|
+
return /*#__PURE__*/ React.createElement(GoogleGeminiIcon, iconProps);
|
|
49
|
+
case "mistral":
|
|
50
|
+
return /*#__PURE__*/ React.createElement(MistralAiIcon, iconProps);
|
|
51
|
+
case "openai":
|
|
52
|
+
return /*#__PURE__*/ React.createElement(OpenaiIcon, iconProps);
|
|
53
|
+
default:
|
|
54
|
+
return null;
|
|
55
|
+
}
|
|
56
|
+
};
|
|
38
57
|
export const AIInput = ()=>{
|
|
39
58
|
const { config } = useConfig();
|
|
40
59
|
const editorRef = useRef(null);
|
|
@@ -58,7 +77,12 @@ export const AIInput = ()=>{
|
|
|
58
77
|
const [proposals, setProposals] = useState([]);
|
|
59
78
|
const [isLoading, setIsLoading] = useState(false);
|
|
60
79
|
const [isApplying, setIsApplying] = useState(false);
|
|
61
|
-
const
|
|
80
|
+
const enabledCollectionSlugs = config.admin?.custom?.payloadAiPlugin?.collectionSlugs;
|
|
81
|
+
const enabledCollectionSlugSet = useMemo(()=>enabledCollectionSlugs ? new Set(enabledCollectionSlugs) : null, [
|
|
82
|
+
enabledCollectionSlugs
|
|
83
|
+
]);
|
|
84
|
+
const isCollectionMentionEnabled = (slug)=>!enabledCollectionSlugSet || enabledCollectionSlugSet.has(slug);
|
|
85
|
+
const collections = config.collections.filter((collection)=>!isInternalCollection(collection.slug)).filter((collection)=>isCollectionMentionEnabled(collection.slug)).map((collection)=>({
|
|
62
86
|
label: getSerializableLabel(collection.labels?.singular, collection.slug),
|
|
63
87
|
slug: collection.slug,
|
|
64
88
|
type: "collection"
|
|
@@ -69,7 +93,7 @@ export const AIInput = ()=>{
|
|
|
69
93
|
type: "global"
|
|
70
94
|
})) || [];
|
|
71
95
|
const blocks = [
|
|
72
|
-
...config.collections.flatMap((collection)=>collectBlockOptions({
|
|
96
|
+
...config.collections.filter((collection)=>isCollectionMentionEnabled(collection.slug)).flatMap((collection)=>collectBlockOptions({
|
|
73
97
|
fields: collection.fields,
|
|
74
98
|
parent: collection.slug
|
|
75
99
|
})),
|
|
@@ -106,14 +130,6 @@ export const AIInput = ()=>{
|
|
|
106
130
|
clonedRange.setEnd(range.endContainer, range.endOffset);
|
|
107
131
|
return clonedRange.toString().length;
|
|
108
132
|
};
|
|
109
|
-
const moveCaretToEnd = (element)=>{
|
|
110
|
-
const selection = window.getSelection();
|
|
111
|
-
const range = document.createRange();
|
|
112
|
-
range.selectNodeContents(element);
|
|
113
|
-
range.collapse(false);
|
|
114
|
-
selection?.removeAllRanges();
|
|
115
|
-
selection?.addRange(range);
|
|
116
|
-
};
|
|
117
133
|
const getTextNodeAtOffset = (element, offset)=>{
|
|
118
134
|
const walker = document.createTreeWalker(element, NodeFilter.SHOW_TEXT);
|
|
119
135
|
let currentOffset = 0;
|
|
@@ -349,17 +365,19 @@ export const AIInput = ()=>{
|
|
|
349
365
|
className: styles.setting
|
|
350
366
|
}, /*#__PURE__*/ React.createElement("span", {
|
|
351
367
|
className: styles.settingLabel
|
|
352
|
-
}, "Model"), /*#__PURE__*/ React.createElement("
|
|
368
|
+
}, "Model"), /*#__PURE__*/ React.createElement("div", {
|
|
369
|
+
className: styles.selectWrapper
|
|
370
|
+
}, getProviderIcon(settingsProvider), /*#__PURE__*/ React.createElement("select", {
|
|
353
371
|
className: styles.select,
|
|
354
372
|
disabled: !settingsProvider,
|
|
355
373
|
onChange: (event)=>setSelectedModel(event.target.value),
|
|
356
374
|
value: selectedModel
|
|
357
|
-
}, !settingsProvider
|
|
375
|
+
}, !settingsProvider && /*#__PURE__*/ React.createElement("option", {
|
|
358
376
|
value: ""
|
|
359
|
-
}, "Select provider in account settings")
|
|
377
|
+
}, "Select provider in account settings"), settingsProvider && aiModelConfig.providers[settingsProvider].map((model)=>/*#__PURE__*/ React.createElement("option", {
|
|
360
378
|
key: model.value,
|
|
361
379
|
value: model.value
|
|
362
|
-
}, model.label))
|
|
380
|
+
}, model.label)))))), /*#__PURE__*/ React.createElement("button", {
|
|
363
381
|
className: styles.chatButton,
|
|
364
382
|
disabled: !prompt.trim() || !settingsProvider || !selectedModel || isLoading,
|
|
365
383
|
onClick: ()=>void handleSubmit(),
|