@mvriu5/payload-ai 0.5.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.
- package/README.md +99 -0
- package/dist/ai/proposals.d.ts +12 -0
- package/dist/ai/proposals.js +50 -0
- package/dist/ai/providerOptions.d.ts +85 -0
- package/dist/ai/providerOptions.js +141 -0
- package/dist/ai/providerRuntime.d.ts +18 -0
- package/dist/ai/providerRuntime.js +53 -0
- package/dist/ai/sensitiveData.d.ts +2 -0
- package/dist/ai/sensitiveData.js +30 -0
- package/dist/components/AIActionProposalList.d.ts +25 -0
- package/dist/components/AIActionProposalList.js +94 -0
- package/dist/components/AIActionProposalList.module.css +175 -0
- package/dist/components/AIApiKeyField.d.ts +2 -0
- package/dist/components/AIApiKeyField.js +55 -0
- package/dist/components/AIInput.d.ts +1 -0
- package/dist/components/AIInput.js +386 -0
- package/dist/components/AIInput.module.css +237 -0
- package/dist/components/CollectionMentionPopover.d.ts +16 -0
- package/dist/components/CollectionMentionPopover.js +77 -0
- package/dist/components/CollectionMentionPopover.module.css +67 -0
- package/dist/components/hooks/useAISettings.d.ts +10 -0
- package/dist/components/hooks/useAISettings.js +56 -0
- package/dist/components/hooks/useDocumentMentionSuggestions.d.ts +16 -0
- package/dist/components/hooks/useDocumentMentionSuggestions.js +53 -0
- package/dist/components/hooks/utils.d.ts +1 -0
- package/dist/components/hooks/utils.js +3 -0
- package/dist/endpoints/aiApplyActionEndpointHandler.d.ts +6 -0
- package/dist/endpoints/aiApplyActionEndpointHandler.js +164 -0
- package/dist/endpoints/aiChatEndpointHandler.d.ts +31 -0
- package/dist/endpoints/aiChatEndpointHandler.js +297 -0
- package/dist/endpoints/aiMentionSuggestionsEndpointHandler.d.ts +6 -0
- package/dist/endpoints/aiMentionSuggestionsEndpointHandler.js +63 -0
- package/dist/exports/client.d.ts +2 -0
- package/dist/exports/client.js +2 -0
- package/dist/index.d.ts +9 -0
- package/dist/index.js +75 -0
- package/dist/payload/normalizeData.d.ts +29 -0
- package/dist/payload/normalizeData.js +192 -0
- package/dist/payload/schemaContext.d.ts +52 -0
- package/dist/payload/schemaContext.js +162 -0
- package/dist/payload/shared.d.ts +2 -0
- package/dist/payload/shared.js +11 -0
- package/package.json +126 -0
package/README.md
ADDED
|
@@ -0,0 +1,99 @@
|
|
|
1
|
+
# payload-ai-plugin
|
|
2
|
+
|
|
3
|
+
AI assistant plugin for Payload CMS. It adds an admin dashboard assistant that can read CMS context, use mentions, and create signed action proposals for create, update, delete, and global updates.
|
|
4
|
+
|
|
5
|
+
## Install
|
|
6
|
+
|
|
7
|
+
```bash
|
|
8
|
+
pnpm add payload-ai-plugin
|
|
9
|
+
```
|
|
10
|
+
|
|
11
|
+
## Usage
|
|
12
|
+
|
|
13
|
+
```ts
|
|
14
|
+
import { buildConfig } from "payload";
|
|
15
|
+
import { payloadAiPlugin } from "payload-ai-plugin";
|
|
16
|
+
|
|
17
|
+
export default buildConfig({
|
|
18
|
+
plugins: [
|
|
19
|
+
payloadAiPlugin({
|
|
20
|
+
collections: {
|
|
21
|
+
posts: true,
|
|
22
|
+
},
|
|
23
|
+
}),
|
|
24
|
+
],
|
|
25
|
+
});
|
|
26
|
+
```
|
|
27
|
+
|
|
28
|
+
The plugin adds two fields to the configured Payload admin user collection:
|
|
29
|
+
|
|
30
|
+
- `aiProvider`
|
|
31
|
+
- `aiApiKey`
|
|
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.
|
|
34
|
+
|
|
35
|
+
## Options
|
|
36
|
+
|
|
37
|
+
```ts
|
|
38
|
+
import type { PayloadAiPluginOptions } from "payload-ai-plugin";
|
|
39
|
+
|
|
40
|
+
const options: PayloadAiPluginOptions = {
|
|
41
|
+
collections: {
|
|
42
|
+
posts: true,
|
|
43
|
+
},
|
|
44
|
+
models: {
|
|
45
|
+
defaults: {
|
|
46
|
+
openai: "gpt-4.1-mini",
|
|
47
|
+
},
|
|
48
|
+
providers: {
|
|
49
|
+
openai: [
|
|
50
|
+
{ label: "GPT-4.1 Mini", value: "gpt-4.1-mini" },
|
|
51
|
+
{ label: "GPT-4.1", value: "gpt-4.1" },
|
|
52
|
+
],
|
|
53
|
+
},
|
|
54
|
+
},
|
|
55
|
+
};
|
|
56
|
+
```
|
|
57
|
+
|
|
58
|
+
### `collections`
|
|
59
|
+
|
|
60
|
+
Restricts AI read and write proposals to enabled collection slugs. If omitted, all non-internal Payload collections are available.
|
|
61
|
+
|
|
62
|
+
### `models`
|
|
63
|
+
|
|
64
|
+
Overrides the model list shown in the admin UI and the default model per provider.
|
|
65
|
+
|
|
66
|
+
### `disabled`
|
|
67
|
+
|
|
68
|
+
Disables endpoint and UI registration while keeping the plugin call in your config.
|
|
69
|
+
|
|
70
|
+
## Provider Environment Variables
|
|
71
|
+
|
|
72
|
+
Account-level API keys take priority. If a user has no key configured, the server falls back to provider environment variables:
|
|
73
|
+
|
|
74
|
+
- `OPENAI_API_KEY`, `OPENAI_MODEL`
|
|
75
|
+
- `ANTHROPIC_API_KEY`, `ANTHROPIC_MODEL`
|
|
76
|
+
- `GOOGLE_GENERATIVE_AI_API_KEY`, `GOOGLE_GENERATIVE_AI_MODEL`
|
|
77
|
+
- `GROQ_API_KEY`, `GROQ_MODEL`
|
|
78
|
+
- `MISTRAL_API_KEY`, `MISTRAL_MODEL`
|
|
79
|
+
|
|
80
|
+
`PAYLOAD_SECRET` is required for signing AI action proposals.
|
|
81
|
+
|
|
82
|
+
## Security
|
|
83
|
+
|
|
84
|
+
AI write operations are proposal-based. The chat endpoint signs every proposal with an HMAC signature and a short TTL. The apply endpoint verifies the signature, validates the target collection/global, enforces Payload access control with `overrideAccess: false`, and rejects proposals containing sensitive API-key-like fields.
|
|
85
|
+
|
|
86
|
+
The apply endpoint returns only minimal status/doc references and does not return normalized data, proposal payloads, API keys, or raw error details to the client.
|
|
87
|
+
|
|
88
|
+
## Exports
|
|
89
|
+
|
|
90
|
+
```ts
|
|
91
|
+
import { payloadAiPlugin } from "payload-ai-plugin";
|
|
92
|
+
import type { PayloadAiPluginOptions } from "payload-ai-plugin";
|
|
93
|
+
```
|
|
94
|
+
|
|
95
|
+
Client components are exported through:
|
|
96
|
+
|
|
97
|
+
```ts
|
|
98
|
+
import { AIInput, AIApiKeyField } from "payload-ai-plugin/client";
|
|
99
|
+
```
|
|
@@ -0,0 +1,12 @@
|
|
|
1
|
+
export type AIActionSignature = {
|
|
2
|
+
expiresAt: string;
|
|
3
|
+
value: string;
|
|
4
|
+
};
|
|
5
|
+
type SignableProposal = Record<string, unknown> & {
|
|
6
|
+
_aiSignature?: AIActionSignature;
|
|
7
|
+
};
|
|
8
|
+
export declare const signAIActionProposal: <Proposal extends SignableProposal>(proposal: Proposal) => Proposal & {
|
|
9
|
+
_aiSignature: AIActionSignature;
|
|
10
|
+
};
|
|
11
|
+
export declare const verifyAIActionProposal: (proposal: SignableProposal) => boolean;
|
|
12
|
+
export {};
|
|
@@ -0,0 +1,50 @@
|
|
|
1
|
+
import { createHmac, timingSafeEqual } from "node:crypto";
|
|
2
|
+
const signatureTTL = 10 * 60 * 1000;
|
|
3
|
+
const stableStringify = (value)=>{
|
|
4
|
+
if (Array.isArray(value)) {
|
|
5
|
+
return `[${value.map(stableStringify).join(",")}]`;
|
|
6
|
+
}
|
|
7
|
+
if (value && typeof value === "object") {
|
|
8
|
+
const entries = Object.entries(value).filter(([key])=>key !== "_aiSignature").sort(([a], [b])=>a.localeCompare(b));
|
|
9
|
+
return `{${entries.map(([key, entryValue])=>`${JSON.stringify(key)}:${stableStringify(entryValue)}`).join(",")}}`;
|
|
10
|
+
}
|
|
11
|
+
return JSON.stringify(value) ?? "undefined";
|
|
12
|
+
};
|
|
13
|
+
const getSigningSecret = ()=>{
|
|
14
|
+
const secret = process.env.PAYLOAD_SECRET;
|
|
15
|
+
if (!secret) {
|
|
16
|
+
throw new Error("PAYLOAD_SECRET is required to sign AI proposals.");
|
|
17
|
+
}
|
|
18
|
+
return secret;
|
|
19
|
+
};
|
|
20
|
+
const getSignaturePayload = (proposal, expiresAt)=>{
|
|
21
|
+
return stableStringify({
|
|
22
|
+
expiresAt,
|
|
23
|
+
proposal
|
|
24
|
+
});
|
|
25
|
+
};
|
|
26
|
+
const signPayload = (payload)=>{
|
|
27
|
+
return createHmac("sha256", getSigningSecret()).update(payload).digest("hex");
|
|
28
|
+
};
|
|
29
|
+
export const signAIActionProposal = (proposal)=>{
|
|
30
|
+
const expiresAt = new Date(Date.now() + signatureTTL).toISOString();
|
|
31
|
+
const value = signPayload(getSignaturePayload(proposal, expiresAt));
|
|
32
|
+
return {
|
|
33
|
+
...proposal,
|
|
34
|
+
_aiSignature: {
|
|
35
|
+
expiresAt,
|
|
36
|
+
value
|
|
37
|
+
}
|
|
38
|
+
};
|
|
39
|
+
};
|
|
40
|
+
export const verifyAIActionProposal = (proposal)=>{
|
|
41
|
+
const signature = proposal._aiSignature;
|
|
42
|
+
if (!signature?.expiresAt || !signature.value) return false;
|
|
43
|
+
if (Number.isNaN(new Date(signature.expiresAt).getTime())) return false;
|
|
44
|
+
if (new Date(signature.expiresAt).getTime() < Date.now()) return false;
|
|
45
|
+
const expected = signPayload(getSignaturePayload(proposal, signature.expiresAt));
|
|
46
|
+
const expectedBuffer = Buffer.from(expected, "hex");
|
|
47
|
+
const actualBuffer = Buffer.from(signature.value, "hex");
|
|
48
|
+
if (expectedBuffer.length !== actualBuffer.length) return false;
|
|
49
|
+
return timingSafeEqual(expectedBuffer, actualBuffer);
|
|
50
|
+
};
|
|
@@ -0,0 +1,85 @@
|
|
|
1
|
+
type AIProviderModelOption = {
|
|
2
|
+
label: string;
|
|
3
|
+
value: string;
|
|
4
|
+
};
|
|
5
|
+
declare const aiProviderModels: {
|
|
6
|
+
readonly claude: readonly [{
|
|
7
|
+
readonly label: "Claude 3 Haiku";
|
|
8
|
+
readonly value: "claude-3-haiku-20240307";
|
|
9
|
+
}, {
|
|
10
|
+
readonly label: "Claude Sonnet 4.5";
|
|
11
|
+
readonly value: "claude-sonnet-4-5";
|
|
12
|
+
}, {
|
|
13
|
+
readonly label: "Claude Sonnet 4";
|
|
14
|
+
readonly value: "claude-sonnet-4-0";
|
|
15
|
+
}];
|
|
16
|
+
readonly google: readonly [{
|
|
17
|
+
readonly label: "Gemini 2.0 Flash";
|
|
18
|
+
readonly value: "gemini-2.0-flash";
|
|
19
|
+
}, {
|
|
20
|
+
readonly label: "Gemini 2.5 Flash";
|
|
21
|
+
readonly value: "gemini-2.5-flash";
|
|
22
|
+
}, {
|
|
23
|
+
readonly label: "Gemini 2.5 Flash Lite";
|
|
24
|
+
readonly value: "gemini-2.5-flash-lite";
|
|
25
|
+
}, {
|
|
26
|
+
readonly label: "Gemini 2.5 Pro";
|
|
27
|
+
readonly value: "gemini-2.5-pro";
|
|
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
|
+
readonly mistral: readonly [{
|
|
43
|
+
readonly label: "Mistral Small";
|
|
44
|
+
readonly value: "mistral-small-latest";
|
|
45
|
+
}, {
|
|
46
|
+
readonly label: "Mistral Medium";
|
|
47
|
+
readonly value: "mistral-medium-latest";
|
|
48
|
+
}, {
|
|
49
|
+
readonly label: "Mistral Large";
|
|
50
|
+
readonly value: "mistral-large-latest";
|
|
51
|
+
}, {
|
|
52
|
+
readonly label: "Ministral 8B";
|
|
53
|
+
readonly value: "ministral-8b-latest";
|
|
54
|
+
}];
|
|
55
|
+
readonly openai: readonly [{
|
|
56
|
+
readonly label: "GPT-4.1 Mini";
|
|
57
|
+
readonly value: "gpt-4.1-mini";
|
|
58
|
+
}, {
|
|
59
|
+
readonly label: "GPT-4.1 Nano";
|
|
60
|
+
readonly value: "gpt-4.1-nano";
|
|
61
|
+
}, {
|
|
62
|
+
readonly label: "GPT-4.1";
|
|
63
|
+
readonly value: "gpt-4.1";
|
|
64
|
+
}, {
|
|
65
|
+
readonly label: "GPT-4o Mini";
|
|
66
|
+
readonly value: "gpt-4o-mini";
|
|
67
|
+
}];
|
|
68
|
+
};
|
|
69
|
+
export type AIProvider = keyof typeof aiProviderModels;
|
|
70
|
+
type AIProviderModels = Record<AIProvider, AIProviderModelOption[]>;
|
|
71
|
+
export declare const aiProviders: {
|
|
72
|
+
label: string;
|
|
73
|
+
value: AIProvider;
|
|
74
|
+
}[];
|
|
75
|
+
export declare const defaultAIModels: Record<AIProvider, string>;
|
|
76
|
+
export type AIModelConfig = {
|
|
77
|
+
defaults?: Partial<Record<AIProvider, string>>;
|
|
78
|
+
providers?: Partial<Record<AIProvider, AIProviderModelOption[]>>;
|
|
79
|
+
};
|
|
80
|
+
export declare const getResolvedAIModelConfig: (modelConfig?: AIModelConfig) => {
|
|
81
|
+
defaults: Record<"claude" | "google" | "groq" | "mistral" | "openai", string>;
|
|
82
|
+
providers: AIProviderModels;
|
|
83
|
+
};
|
|
84
|
+
export declare const isAIProvider: (provider: string) => provider is AIProvider;
|
|
85
|
+
export {};
|
|
@@ -0,0 +1,141 @@
|
|
|
1
|
+
const aiProviderModels = {
|
|
2
|
+
claude: [
|
|
3
|
+
{
|
|
4
|
+
label: "Claude 3 Haiku",
|
|
5
|
+
value: "claude-3-haiku-20240307"
|
|
6
|
+
},
|
|
7
|
+
{
|
|
8
|
+
label: "Claude Sonnet 4.5",
|
|
9
|
+
value: "claude-sonnet-4-5"
|
|
10
|
+
},
|
|
11
|
+
{
|
|
12
|
+
label: "Claude Sonnet 4",
|
|
13
|
+
value: "claude-sonnet-4-0"
|
|
14
|
+
}
|
|
15
|
+
],
|
|
16
|
+
google: [
|
|
17
|
+
{
|
|
18
|
+
label: "Gemini 2.0 Flash",
|
|
19
|
+
value: "gemini-2.0-flash"
|
|
20
|
+
},
|
|
21
|
+
{
|
|
22
|
+
label: "Gemini 2.5 Flash",
|
|
23
|
+
value: "gemini-2.5-flash"
|
|
24
|
+
},
|
|
25
|
+
{
|
|
26
|
+
label: "Gemini 2.5 Flash Lite",
|
|
27
|
+
value: "gemini-2.5-flash-lite"
|
|
28
|
+
},
|
|
29
|
+
{
|
|
30
|
+
label: "Gemini 2.5 Pro",
|
|
31
|
+
value: "gemini-2.5-pro"
|
|
32
|
+
}
|
|
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
|
+
mistral: [
|
|
53
|
+
{
|
|
54
|
+
label: "Mistral Small",
|
|
55
|
+
value: "mistral-small-latest"
|
|
56
|
+
},
|
|
57
|
+
{
|
|
58
|
+
label: "Mistral Medium",
|
|
59
|
+
value: "mistral-medium-latest"
|
|
60
|
+
},
|
|
61
|
+
{
|
|
62
|
+
label: "Mistral Large",
|
|
63
|
+
value: "mistral-large-latest"
|
|
64
|
+
},
|
|
65
|
+
{
|
|
66
|
+
label: "Ministral 8B",
|
|
67
|
+
value: "ministral-8b-latest"
|
|
68
|
+
}
|
|
69
|
+
],
|
|
70
|
+
openai: [
|
|
71
|
+
{
|
|
72
|
+
label: "GPT-4.1 Mini",
|
|
73
|
+
value: "gpt-4.1-mini"
|
|
74
|
+
},
|
|
75
|
+
{
|
|
76
|
+
label: "GPT-4.1 Nano",
|
|
77
|
+
value: "gpt-4.1-nano"
|
|
78
|
+
},
|
|
79
|
+
{
|
|
80
|
+
label: "GPT-4.1",
|
|
81
|
+
value: "gpt-4.1"
|
|
82
|
+
},
|
|
83
|
+
{
|
|
84
|
+
label: "GPT-4o Mini",
|
|
85
|
+
value: "gpt-4o-mini"
|
|
86
|
+
}
|
|
87
|
+
]
|
|
88
|
+
};
|
|
89
|
+
export const aiProviders = [
|
|
90
|
+
{
|
|
91
|
+
label: "Claude",
|
|
92
|
+
value: "claude"
|
|
93
|
+
},
|
|
94
|
+
{
|
|
95
|
+
label: "Google Gemini",
|
|
96
|
+
value: "google"
|
|
97
|
+
},
|
|
98
|
+
{
|
|
99
|
+
label: "Groq",
|
|
100
|
+
value: "groq"
|
|
101
|
+
},
|
|
102
|
+
{
|
|
103
|
+
label: "Mistral",
|
|
104
|
+
value: "mistral"
|
|
105
|
+
},
|
|
106
|
+
{
|
|
107
|
+
label: "OpenAI",
|
|
108
|
+
value: "openai"
|
|
109
|
+
}
|
|
110
|
+
];
|
|
111
|
+
export const defaultAIModels = {
|
|
112
|
+
claude: aiProviderModels.claude[0].value,
|
|
113
|
+
google: aiProviderModels.google[0].value,
|
|
114
|
+
groq: aiProviderModels.groq[0].value,
|
|
115
|
+
mistral: aiProviderModels.mistral[0].value,
|
|
116
|
+
openai: aiProviderModels.openai[0].value
|
|
117
|
+
};
|
|
118
|
+
export const getResolvedAIModelConfig = (modelConfig)=>{
|
|
119
|
+
const providers = Object.fromEntries(Object.entries(aiProviderModels).map(([provider, models])=>[
|
|
120
|
+
provider,
|
|
121
|
+
modelConfig?.providers?.[provider] || [
|
|
122
|
+
...models
|
|
123
|
+
]
|
|
124
|
+
]));
|
|
125
|
+
const defaults = Object.fromEntries(Object.entries(defaultAIModels).map(([provider, defaultModel])=>{
|
|
126
|
+
const providerKey = provider;
|
|
127
|
+
const configuredDefault = modelConfig?.defaults?.[providerKey];
|
|
128
|
+
const providerModels = providers[providerKey];
|
|
129
|
+
return [
|
|
130
|
+
provider,
|
|
131
|
+
configuredDefault || providerModels[0]?.value || defaultModel
|
|
132
|
+
];
|
|
133
|
+
}));
|
|
134
|
+
return {
|
|
135
|
+
defaults,
|
|
136
|
+
providers
|
|
137
|
+
};
|
|
138
|
+
};
|
|
139
|
+
export const isAIProvider = (provider)=>{
|
|
140
|
+
return provider in aiProviderModels;
|
|
141
|
+
};
|
|
@@ -0,0 +1,18 @@
|
|
|
1
|
+
import type { LanguageModel } from "ai";
|
|
2
|
+
import { type AIProvider } from "./providerOptions.js";
|
|
3
|
+
type ProviderConfig = {
|
|
4
|
+
apiKey?: string | null;
|
|
5
|
+
model?: string | null;
|
|
6
|
+
provider: AIProvider;
|
|
7
|
+
};
|
|
8
|
+
type ModelConfig = {
|
|
9
|
+
apiKey: string;
|
|
10
|
+
model: string;
|
|
11
|
+
provider: AIProvider;
|
|
12
|
+
};
|
|
13
|
+
export declare const getProviderConfig: ({ apiKey, model, provider, }: ProviderConfig) => {
|
|
14
|
+
apiKey: string | undefined;
|
|
15
|
+
modelID: string;
|
|
16
|
+
};
|
|
17
|
+
export declare const getModel: ({ apiKey, model, provider }: ModelConfig) => LanguageModel;
|
|
18
|
+
export {};
|
|
@@ -0,0 +1,53 @@
|
|
|
1
|
+
import { createAnthropic } from "@ai-sdk/anthropic";
|
|
2
|
+
import { createGoogleGenerativeAI } from "@ai-sdk/google";
|
|
3
|
+
import { createGroq } from "@ai-sdk/groq";
|
|
4
|
+
import { createMistral } from "@ai-sdk/mistral";
|
|
5
|
+
import { createOpenAI } from "@ai-sdk/openai";
|
|
6
|
+
import { defaultAIModels } from "./providerOptions.js";
|
|
7
|
+
export const getProviderConfig = ({ apiKey, model, provider })=>{
|
|
8
|
+
if (provider === "claude") {
|
|
9
|
+
return {
|
|
10
|
+
apiKey: apiKey || process.env.ANTHROPIC_API_KEY,
|
|
11
|
+
modelID: model || process.env.ANTHROPIC_MODEL || defaultAIModels.claude
|
|
12
|
+
};
|
|
13
|
+
}
|
|
14
|
+
if (provider === "google") {
|
|
15
|
+
return {
|
|
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
|
|
24
|
+
};
|
|
25
|
+
}
|
|
26
|
+
if (provider === "mistral") {
|
|
27
|
+
return {
|
|
28
|
+
apiKey: apiKey || process.env.MISTRAL_API_KEY,
|
|
29
|
+
modelID: model || process.env.MISTRAL_MODEL || defaultAIModels.mistral
|
|
30
|
+
};
|
|
31
|
+
}
|
|
32
|
+
return {
|
|
33
|
+
apiKey: apiKey || process.env.OPENAI_API_KEY,
|
|
34
|
+
modelID: model || process.env.OPENAI_MODEL || defaultAIModels.openai
|
|
35
|
+
};
|
|
36
|
+
};
|
|
37
|
+
export const getModel = ({ apiKey, model, provider })=>{
|
|
38
|
+
if (provider === "claude") return createAnthropic({
|
|
39
|
+
apiKey
|
|
40
|
+
})(model);
|
|
41
|
+
if (provider === "google") return createGoogleGenerativeAI({
|
|
42
|
+
apiKey
|
|
43
|
+
})(model);
|
|
44
|
+
if (provider === "groq") return createGroq({
|
|
45
|
+
apiKey
|
|
46
|
+
})(model);
|
|
47
|
+
if (provider === "mistral") return createMistral({
|
|
48
|
+
apiKey
|
|
49
|
+
})(model);
|
|
50
|
+
return createOpenAI({
|
|
51
|
+
apiKey
|
|
52
|
+
})(model);
|
|
53
|
+
};
|
|
@@ -0,0 +1,30 @@
|
|
|
1
|
+
const sensitiveKeyPatterns = [
|
|
2
|
+
/^apiKey$/i,
|
|
3
|
+
/^api_key$/i,
|
|
4
|
+
/^aiApiKey$/i,
|
|
5
|
+
/^authorization$/i,
|
|
6
|
+
/^accessToken$/i,
|
|
7
|
+
/^refreshToken$/i,
|
|
8
|
+
/^secret$/i
|
|
9
|
+
];
|
|
10
|
+
const isRecord = (value)=>{
|
|
11
|
+
return Boolean(value) && typeof value === "object" && !Array.isArray(value);
|
|
12
|
+
};
|
|
13
|
+
const isSensitiveKey = (key)=>{
|
|
14
|
+
return sensitiveKeyPatterns.some((pattern)=>pattern.test(key));
|
|
15
|
+
};
|
|
16
|
+
export const containsSensitiveData = (value)=>{
|
|
17
|
+
if (Array.isArray(value)) return value.some(containsSensitiveData);
|
|
18
|
+
if (!isRecord(value)) return false;
|
|
19
|
+
return Object.entries(value).some(([key, entryValue])=>{
|
|
20
|
+
return isSensitiveKey(key) || containsSensitiveData(entryValue);
|
|
21
|
+
});
|
|
22
|
+
};
|
|
23
|
+
export const redactSensitiveData = (value)=>{
|
|
24
|
+
if (Array.isArray(value)) return value.map(redactSensitiveData);
|
|
25
|
+
if (!isRecord(value)) return value;
|
|
26
|
+
return Object.fromEntries(Object.entries(value).map(([key, entryValue])=>[
|
|
27
|
+
key,
|
|
28
|
+
isSensitiveKey(key) ? "[redacted]" : redactSensitiveData(entryValue)
|
|
29
|
+
]));
|
|
30
|
+
};
|
|
@@ -0,0 +1,25 @@
|
|
|
1
|
+
export type AIActionProposal = {
|
|
2
|
+
_aiSignature?: {
|
|
3
|
+
expiresAt: string;
|
|
4
|
+
value: string;
|
|
5
|
+
};
|
|
6
|
+
action: "create" | "delete" | "update" | "updateGlobal";
|
|
7
|
+
collection?: string;
|
|
8
|
+
data?: Record<string, unknown>;
|
|
9
|
+
id?: string;
|
|
10
|
+
label: string;
|
|
11
|
+
slug?: string;
|
|
12
|
+
};
|
|
13
|
+
type AIActionProposalListProps = {
|
|
14
|
+
appliedProposalIndexes: number[];
|
|
15
|
+
description?: string;
|
|
16
|
+
error?: string;
|
|
17
|
+
getViewURL?: (proposal: AIActionProposal) => string | null;
|
|
18
|
+
isApplying: boolean;
|
|
19
|
+
onDismiss?: () => void;
|
|
20
|
+
onDismissError?: () => void;
|
|
21
|
+
onApply: (proposal: AIActionProposal, index: number) => void;
|
|
22
|
+
proposals: AIActionProposal[];
|
|
23
|
+
};
|
|
24
|
+
export declare const AIActionProposalList: ({ appliedProposalIndexes, description, error, getViewURL, isApplying, onDismiss, onDismissError, onApply, proposals, }: AIActionProposalListProps) => import("react/jsx-runtime").JSX.Element | null;
|
|
25
|
+
export {};
|
|
@@ -0,0 +1,94 @@
|
|
|
1
|
+
import styles from "./AIActionProposalList.module.css";
|
|
2
|
+
import { redactSensitiveData } from "../ai/sensitiveData.js";
|
|
3
|
+
const maxDescriptionLength = 220;
|
|
4
|
+
const getDescriptionPreview = (description)=>{
|
|
5
|
+
if (description.length <= maxDescriptionLength) return description;
|
|
6
|
+
return `${description.slice(0, maxDescriptionLength).trim()}...`;
|
|
7
|
+
};
|
|
8
|
+
const getSafeProposalDetails = (proposal)=>{
|
|
9
|
+
const redactedProposal = redactSensitiveData(proposal);
|
|
10
|
+
if (redactedProposal._aiSignature) {
|
|
11
|
+
redactedProposal._aiSignature = {
|
|
12
|
+
expiresAt: redactedProposal._aiSignature.expiresAt,
|
|
13
|
+
value: "[redacted]"
|
|
14
|
+
};
|
|
15
|
+
}
|
|
16
|
+
return redactedProposal;
|
|
17
|
+
};
|
|
18
|
+
export const AIActionProposalList = ({ appliedProposalIndexes, description, error, getViewURL, isApplying, onDismiss, onDismissError, onApply, proposals })=>{
|
|
19
|
+
if (proposals.length === 0 && !error && !description) return null;
|
|
20
|
+
const descriptionPreview = description ? getDescriptionPreview(description) : "";
|
|
21
|
+
const isDescriptionTruncated = Boolean(description) && descriptionPreview !== description;
|
|
22
|
+
return /*#__PURE__*/ React.createElement("div", {
|
|
23
|
+
className: styles.list
|
|
24
|
+
}, error && /*#__PURE__*/ React.createElement("div", {
|
|
25
|
+
className: `${styles.item} ${styles.errorItem}`
|
|
26
|
+
}, /*#__PURE__*/ React.createElement("div", null, /*#__PURE__*/ React.createElement("div", {
|
|
27
|
+
className: styles.label
|
|
28
|
+
}, "AI request failed"), /*#__PURE__*/ React.createElement("div", {
|
|
29
|
+
className: styles.description
|
|
30
|
+
}, error)), onDismissError && /*#__PURE__*/ React.createElement("button", {
|
|
31
|
+
className: styles.button,
|
|
32
|
+
onClick: onDismissError,
|
|
33
|
+
type: "button"
|
|
34
|
+
}, "Dismiss")), !error && proposals.length === 0 && description && /*#__PURE__*/ React.createElement("div", {
|
|
35
|
+
className: styles.item
|
|
36
|
+
}, /*#__PURE__*/ React.createElement("div", null, /*#__PURE__*/ React.createElement("div", {
|
|
37
|
+
className: styles.label
|
|
38
|
+
}, "AI response"), /*#__PURE__*/ React.createElement("div", {
|
|
39
|
+
className: styles.description
|
|
40
|
+
}, descriptionPreview), isDescriptionTruncated ? /*#__PURE__*/ React.createElement("details", {
|
|
41
|
+
className: styles.details
|
|
42
|
+
}, /*#__PURE__*/ React.createElement("summary", {
|
|
43
|
+
className: styles.summary
|
|
44
|
+
}, "Full response"), /*#__PURE__*/ React.createElement("pre", {
|
|
45
|
+
className: styles.proposalDetails
|
|
46
|
+
}, description)) : null)), proposals.map((proposal, index)=>{
|
|
47
|
+
const isApplied = appliedProposalIndexes.includes(index);
|
|
48
|
+
const viewURL = getViewURL?.(proposal);
|
|
49
|
+
return /*#__PURE__*/ React.createElement("div", {
|
|
50
|
+
className: styles.item,
|
|
51
|
+
key: `${proposal.action}-${index}`
|
|
52
|
+
}, /*#__PURE__*/ React.createElement("div", {
|
|
53
|
+
className: styles.content
|
|
54
|
+
}, /*#__PURE__*/ React.createElement("div", {
|
|
55
|
+
className: styles.label
|
|
56
|
+
}, proposal.label), /*#__PURE__*/ React.createElement("div", {
|
|
57
|
+
className: styles.meta
|
|
58
|
+
}, proposal.action, " in ", proposal.collection || proposal.slug, proposal.id ? ` #${proposal.id}` : ""), description && /*#__PURE__*/ React.createElement("div", {
|
|
59
|
+
className: styles.description
|
|
60
|
+
}, descriptionPreview), description && isDescriptionTruncated && /*#__PURE__*/ React.createElement("details", {
|
|
61
|
+
className: styles.details
|
|
62
|
+
}, /*#__PURE__*/ React.createElement("summary", {
|
|
63
|
+
className: styles.summary
|
|
64
|
+
}, "Full response"), /*#__PURE__*/ React.createElement("pre", {
|
|
65
|
+
className: styles.proposalDetails
|
|
66
|
+
}, description)), /*#__PURE__*/ React.createElement("details", {
|
|
67
|
+
className: styles.details
|
|
68
|
+
}, /*#__PURE__*/ React.createElement("summary", {
|
|
69
|
+
className: styles.summary
|
|
70
|
+
}, "Details"), /*#__PURE__*/ React.createElement("pre", {
|
|
71
|
+
className: styles.proposalDetails
|
|
72
|
+
}, JSON.stringify(getSafeProposalDetails(proposal), null, 2)))), /*#__PURE__*/ React.createElement("div", {
|
|
73
|
+
className: styles.footer
|
|
74
|
+
}, /*#__PURE__*/ React.createElement("div", {
|
|
75
|
+
className: styles.viewAction
|
|
76
|
+
}, viewURL && /*#__PURE__*/ React.createElement("a", {
|
|
77
|
+
className: styles.secondaryButton,
|
|
78
|
+
href: viewURL,
|
|
79
|
+
rel: "noreferrer noopener",
|
|
80
|
+
target: "_blank"
|
|
81
|
+
}, "View")), /*#__PURE__*/ React.createElement("div", {
|
|
82
|
+
className: styles.actions
|
|
83
|
+
}, onDismiss && /*#__PURE__*/ React.createElement("button", {
|
|
84
|
+
className: styles.secondaryButton,
|
|
85
|
+
onClick: onDismiss,
|
|
86
|
+
type: "button"
|
|
87
|
+
}, "Dismiss"), /*#__PURE__*/ React.createElement("button", {
|
|
88
|
+
className: styles.button,
|
|
89
|
+
disabled: isApplying || isApplied,
|
|
90
|
+
onClick: ()=>onApply(proposal, index),
|
|
91
|
+
type: "button"
|
|
92
|
+
}, isApplied ? "Applied" : "Apply"))));
|
|
93
|
+
}));
|
|
94
|
+
};
|