@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.
Files changed (43) hide show
  1. package/README.md +99 -0
  2. package/dist/ai/proposals.d.ts +12 -0
  3. package/dist/ai/proposals.js +50 -0
  4. package/dist/ai/providerOptions.d.ts +85 -0
  5. package/dist/ai/providerOptions.js +141 -0
  6. package/dist/ai/providerRuntime.d.ts +18 -0
  7. package/dist/ai/providerRuntime.js +53 -0
  8. package/dist/ai/sensitiveData.d.ts +2 -0
  9. package/dist/ai/sensitiveData.js +30 -0
  10. package/dist/components/AIActionProposalList.d.ts +25 -0
  11. package/dist/components/AIActionProposalList.js +94 -0
  12. package/dist/components/AIActionProposalList.module.css +175 -0
  13. package/dist/components/AIApiKeyField.d.ts +2 -0
  14. package/dist/components/AIApiKeyField.js +55 -0
  15. package/dist/components/AIInput.d.ts +1 -0
  16. package/dist/components/AIInput.js +386 -0
  17. package/dist/components/AIInput.module.css +237 -0
  18. package/dist/components/CollectionMentionPopover.d.ts +16 -0
  19. package/dist/components/CollectionMentionPopover.js +77 -0
  20. package/dist/components/CollectionMentionPopover.module.css +67 -0
  21. package/dist/components/hooks/useAISettings.d.ts +10 -0
  22. package/dist/components/hooks/useAISettings.js +56 -0
  23. package/dist/components/hooks/useDocumentMentionSuggestions.d.ts +16 -0
  24. package/dist/components/hooks/useDocumentMentionSuggestions.js +53 -0
  25. package/dist/components/hooks/utils.d.ts +1 -0
  26. package/dist/components/hooks/utils.js +3 -0
  27. package/dist/endpoints/aiApplyActionEndpointHandler.d.ts +6 -0
  28. package/dist/endpoints/aiApplyActionEndpointHandler.js +164 -0
  29. package/dist/endpoints/aiChatEndpointHandler.d.ts +31 -0
  30. package/dist/endpoints/aiChatEndpointHandler.js +297 -0
  31. package/dist/endpoints/aiMentionSuggestionsEndpointHandler.d.ts +6 -0
  32. package/dist/endpoints/aiMentionSuggestionsEndpointHandler.js +63 -0
  33. package/dist/exports/client.d.ts +2 -0
  34. package/dist/exports/client.js +2 -0
  35. package/dist/index.d.ts +9 -0
  36. package/dist/index.js +75 -0
  37. package/dist/payload/normalizeData.d.ts +29 -0
  38. package/dist/payload/normalizeData.js +192 -0
  39. package/dist/payload/schemaContext.d.ts +52 -0
  40. package/dist/payload/schemaContext.js +162 -0
  41. package/dist/payload/shared.d.ts +2 -0
  42. package/dist/payload/shared.js +11 -0
  43. 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,2 @@
1
+ export declare const containsSensitiveData: (value: unknown) => boolean;
2
+ export declare const redactSensitiveData: (value: unknown) => unknown;
@@ -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
+ };