@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 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
- pnpm add payload-ai-plugin
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. The key is used server-side when the chat endpoint calls the selected model.
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: true,
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
- Account-level API keys take priority. If a user has no key configured, the server falls back to provider environment variables:
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" | "groq" | "mistral" | "openai", string>;
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 || defaultAIModels.claude
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 || defaultAIModels.google
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 || defaultAIModels.mistral
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 || defaultAIModels.openai
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/jsx-runtime").JSX.Element | null;
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/jsx-runtime").JSX.Element;
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 collections = config.collections.filter((collection)=>!isInternalCollection(collection.slug)).map((collection)=>({
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("select", {
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 ? /*#__PURE__*/ React.createElement("option", {
375
+ }, !settingsProvider && /*#__PURE__*/ React.createElement("option", {
358
376
  value: ""
359
- }, "Select provider in account settings") : null, settingsProvider ? aiModelConfig.providers[settingsProvider].map((model)=>/*#__PURE__*/ React.createElement("option", {
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)) : null))), /*#__PURE__*/ React.createElement("button", {
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(),