@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
@@ -0,0 +1,237 @@
1
+ .chat {
2
+ border: 1px solid var(--theme-elevation-150);
3
+ border-radius: 8px;
4
+ margin-bottom: 24px;
5
+ max-width: 920px;
6
+ padding: 20px;
7
+ }
8
+
9
+ .chatHeader {
10
+ align-items: flex-start;
11
+ display: flex;
12
+ justify-content: space-between;
13
+ margin-bottom: 16px;
14
+ }
15
+
16
+ .chatTitle {
17
+ font-size: 18px;
18
+ line-height: 1.3;
19
+ margin: 0;
20
+ }
21
+
22
+ .chatDescription {
23
+ color: var(--theme-elevation-600);
24
+ font-size: 13px;
25
+ line-height: 1.4;
26
+ margin: 4px 0 0;
27
+ }
28
+
29
+ .settings {
30
+ align-items: flex-end;
31
+ display: flex;
32
+ gap: 10px;
33
+ }
34
+
35
+ .setting {
36
+ display: flex;
37
+ flex-direction: column;
38
+ gap: 4px;
39
+ }
40
+
41
+ .settingLabel {
42
+ color: var(--theme-elevation-600);
43
+ font-size: 11px;
44
+ line-height: 1.2;
45
+ }
46
+
47
+ .select {
48
+ background: var(--theme-input-bg);
49
+ border: 1px solid var(--theme-elevation-150);
50
+ border-radius: 4px;
51
+ color: var(--theme-text);
52
+ font: inherit;
53
+ min-height: 34px;
54
+ min-width: 160px;
55
+ padding: 0 28px 0 10px;
56
+ }
57
+
58
+ .select:focus {
59
+ border-color: var(--theme-success-500);
60
+ outline: none;
61
+ }
62
+
63
+ .chatInputRow {
64
+ position: relative;
65
+ width: 100%;
66
+ }
67
+
68
+ .chatActionsRow {
69
+ align-items: flex-end;
70
+ display: flex;
71
+ gap: 12px;
72
+ justify-content: space-between;
73
+ margin-top: 12px;
74
+ }
75
+
76
+ .chatInput {
77
+ background: transparent;
78
+ border: 0;
79
+ color: var(--theme-text);
80
+ font: inherit;
81
+ min-height: 84px;
82
+ padding: 0;
83
+ white-space: pre-wrap;
84
+ width: 100%;
85
+ }
86
+
87
+ .chatInput:focus {
88
+ outline: none;
89
+ }
90
+
91
+ .chatInput:empty::before {
92
+ color: var(--theme-elevation-400);
93
+ content: attr(data-placeholder);
94
+ }
95
+
96
+ .chatInputSurface {
97
+ background: var(--theme-input-bg);
98
+ border: 1px solid var(--theme-elevation-150);
99
+ border-radius: 4px;
100
+ padding: 12px;
101
+ width: 100%;
102
+ }
103
+
104
+ .chatInputSurface:focus-within {
105
+ border-color: var(--theme-success-500);
106
+ }
107
+
108
+ .inlineBadge {
109
+ margin: 0 2px;
110
+ vertical-align: middle;
111
+ }
112
+
113
+ .badge {
114
+ align-items: center;
115
+ background: var(--theme-elevation-100);
116
+ border: 1px solid var(--theme-elevation-150);
117
+ border-radius: 999px;
118
+ color: var(--theme-text);
119
+ display: inline-flex;
120
+ font-size: 11px;
121
+ gap: 3px;
122
+ line-height: 1;
123
+ min-height: 20px;
124
+ padding: 0 8px;
125
+ position: relative;
126
+ top: -2px;
127
+ white-space: nowrap;
128
+ }
129
+
130
+ .prefix {
131
+ color: var(--theme-elevation-500);
132
+ }
133
+
134
+ .name {
135
+ color: var(--theme-text);
136
+ }
137
+
138
+ .collection {
139
+ background: color-mix(in srgb, #2f7d68 12%, var(--theme-bg));
140
+ border-color: color-mix(in srgb, #2f7d68 28%, var(--theme-elevation-150));
141
+ }
142
+
143
+ .global {
144
+ background: color-mix(in srgb, #8b6f24 12%, var(--theme-bg));
145
+ border-color: color-mix(in srgb, #8b6f24 28%, var(--theme-elevation-150));
146
+ }
147
+
148
+ .block {
149
+ background: color-mix(in srgb, #5a6fa8 12%, var(--theme-bg));
150
+ border-color: color-mix(in srgb, #5a6fa8 28%, var(--theme-elevation-150));
151
+ }
152
+
153
+ .doc {
154
+ background: color-mix(in srgb, #7a5c9e 12%, var(--theme-bg));
155
+ border-color: color-mix(in srgb, #7a5c9e 28%, var(--theme-elevation-150));
156
+ }
157
+
158
+ .chatButton {
159
+ background: var(--theme-text);
160
+ border: 0;
161
+ border-radius: 4px;
162
+ color: var(--theme-bg);
163
+ cursor: pointer;
164
+ font: inherit;
165
+ min-height: 40px;
166
+ padding: 0 18px;
167
+ }
168
+
169
+ .chatButton:disabled {
170
+ cursor: not-allowed;
171
+ opacity: 0.45;
172
+ }
173
+
174
+ .chatError {
175
+ color: var(--theme-error-500);
176
+ font-size: 13px;
177
+ line-height: 1.4;
178
+ margin-top: 12px;
179
+ }
180
+
181
+ .debugInfo {
182
+ background: var(--theme-elevation-50);
183
+ border: 1px solid var(--theme-elevation-150);
184
+ border-radius: 4px;
185
+ color: var(--theme-elevation-800);
186
+ font-size: 12px;
187
+ line-height: 1.5;
188
+ margin: 12px 0 0;
189
+ max-height: 260px;
190
+ overflow: auto;
191
+ padding: 12px;
192
+ white-space: pre-wrap;
193
+ }
194
+
195
+ .chatResponse {
196
+ background: var(--theme-elevation-50);
197
+ border: 1px solid var(--theme-elevation-100);
198
+ border-radius: 4px;
199
+ line-height: 1.5;
200
+ margin-top: 16px;
201
+ padding: 12px;
202
+ white-space: pre-wrap;
203
+ }
204
+
205
+ @media (max-width: 768px) {
206
+ .chatHeader {
207
+ flex-direction: column;
208
+ gap: 12px;
209
+ }
210
+
211
+ .chatInputRow {
212
+ width: 100%;
213
+ }
214
+
215
+ .chatActionsRow {
216
+ align-items: stretch;
217
+ flex-direction: column;
218
+ }
219
+
220
+ .settings {
221
+ align-items: stretch;
222
+ width: 100%;
223
+ }
224
+
225
+ .setting {
226
+ flex: 1;
227
+ }
228
+
229
+ .select {
230
+ min-width: 0;
231
+ width: 100%;
232
+ }
233
+
234
+ .chatButton {
235
+ align-self: stretch;
236
+ }
237
+ }
@@ -0,0 +1,16 @@
1
+ import type { RefObject } from "react";
2
+ export type CollectionMentionOption = {
3
+ collection?: string;
4
+ id?: string;
5
+ label: string;
6
+ parent?: string;
7
+ slug: string;
8
+ type: "block" | "collection" | "doc" | "global";
9
+ };
10
+ type CollectionMentionPopoverProps = {
11
+ containerRef?: RefObject<HTMLDivElement | null>;
12
+ suggestions: CollectionMentionOption[];
13
+ onSelect: (suggestion: CollectionMentionOption) => void;
14
+ };
15
+ export declare const CollectionMentionPopover: ({ containerRef, onSelect, suggestions }: CollectionMentionPopoverProps) => import("react/jsx-runtime").JSX.Element | null;
16
+ export {};
@@ -0,0 +1,77 @@
1
+ import styles from "./CollectionMentionPopover.module.css";
2
+ const suggestionGroups = [
3
+ {
4
+ label: "Collections",
5
+ type: "collection"
6
+ },
7
+ {
8
+ label: "Collection items",
9
+ type: "doc"
10
+ },
11
+ {
12
+ label: "Globals",
13
+ type: "global"
14
+ },
15
+ {
16
+ label: "Blocks",
17
+ type: "block"
18
+ }
19
+ ];
20
+ const getSuggestionTitle = (suggestion)=>{
21
+ if (suggestion.type === "collection") return `@${suggestion.slug}`;
22
+ if (suggestion.type === "global") return `@${suggestion.slug}`;
23
+ if (suggestion.type === "block") return `@${suggestion.slug}`;
24
+ return suggestion.label;
25
+ };
26
+ const getSuggestionLabel = (suggestion)=>{
27
+ if (suggestion.type === "collection") return suggestion.label;
28
+ if (suggestion.type === "global") return "global";
29
+ if (suggestion.type === "block") return `${suggestion.parent} block`;
30
+ return `${suggestion.collection} item`;
31
+ };
32
+ export const CollectionMentionPopover = ({ containerRef, onSelect, suggestions })=>{
33
+ if (suggestions.length === 0) return null;
34
+ const onKeyDown = (event, suggestion)=>{
35
+ if (event.key === "Enter" || event.key === " ") {
36
+ event.preventDefault();
37
+ onSelect(suggestion);
38
+ return;
39
+ }
40
+ if (event.key !== "ArrowDown" && event.key !== "ArrowUp") return;
41
+ const popover = event.currentTarget.closest(`.${styles.popover}`);
42
+ if (!popover) return;
43
+ const buttons = Array.from(popover.querySelectorAll("button"));
44
+ const currentIndex = buttons.indexOf(event.currentTarget);
45
+ if (currentIndex === -1) return;
46
+ event.preventDefault();
47
+ const nextIndex = event.key === "ArrowDown" ? Math.min(currentIndex + 1, buttons.length - 1) : Math.max(currentIndex - 1, 0);
48
+ buttons[nextIndex]?.focus();
49
+ };
50
+ const onMouseDown = (event, suggestion)=>{
51
+ event.preventDefault();
52
+ onSelect(suggestion);
53
+ };
54
+ return /*#__PURE__*/ React.createElement("div", {
55
+ className: styles.popover,
56
+ ref: containerRef
57
+ }, suggestionGroups.map((group)=>{
58
+ const groupSuggestions = suggestions.filter((s)=>s.type === group.type);
59
+ if (groupSuggestions.length === 0) return null;
60
+ return /*#__PURE__*/ React.createElement("div", {
61
+ className: styles.group,
62
+ key: group.type
63
+ }, /*#__PURE__*/ React.createElement("div", {
64
+ className: styles.groupLabel
65
+ }, group.label), groupSuggestions.map((suggestion)=>/*#__PURE__*/ React.createElement("button", {
66
+ className: styles.option,
67
+ key: `${suggestion.type}-${suggestion.slug}-${suggestion.parent || ""}-${suggestion.collection || ""}-${suggestion.id || ""}`,
68
+ onMouseDown: (e)=>onMouseDown(e, suggestion),
69
+ onKeyDown: (e)=>onKeyDown(e, suggestion),
70
+ type: "button"
71
+ }, /*#__PURE__*/ React.createElement("span", {
72
+ className: styles.slug
73
+ }, getSuggestionTitle(suggestion)), /*#__PURE__*/ React.createElement("span", {
74
+ className: styles.label
75
+ }, getSuggestionLabel(suggestion)))));
76
+ }));
77
+ };
@@ -0,0 +1,67 @@
1
+ .popover {
2
+ background: var(--theme-bg);
3
+ border: 1px solid var(--theme-elevation-150);
4
+ border-radius: 6px;
5
+ box-shadow: 0 8px 24px rgb(0 0 0 / 12%);
6
+ left: 0;
7
+ max-height: 220px;
8
+ max-width: calc(100% - 96px);
9
+ min-width: 260px;
10
+ overflow: auto;
11
+ padding: 6px;
12
+ position: absolute;
13
+ top: calc(100% + 6px);
14
+ z-index: 10;
15
+ }
16
+
17
+ .group + .group {
18
+ margin-top: 6px;
19
+ }
20
+
21
+ .groupLabel {
22
+ color: var(--theme-elevation-500);
23
+ font-size: 10px;
24
+ font-weight: 600;
25
+ line-height: 1.2;
26
+ padding: 6px 10px 4px;
27
+ text-transform: uppercase;
28
+ }
29
+
30
+ .option {
31
+ align-items: center;
32
+ background: transparent;
33
+ border: 0;
34
+ border-radius: 4px;
35
+ color: var(--theme-text);
36
+ cursor: pointer;
37
+ display: flex;
38
+ font: inherit;
39
+ gap: 10px;
40
+ justify-content: flex-start;
41
+ padding: 8px 10px;
42
+ text-align: left;
43
+ width: 100%;
44
+ }
45
+
46
+ .option:hover,
47
+ .option:focus {
48
+ background: var(--theme-elevation-50);
49
+ outline: none;
50
+ }
51
+
52
+ .slug {
53
+ font-weight: 600;
54
+ }
55
+
56
+ .label {
57
+ color: var(--theme-elevation-600);
58
+ font-size: 12px;
59
+ }
60
+
61
+ @media (max-width: 768px) {
62
+ .popover {
63
+ max-width: 100%;
64
+ min-width: 0;
65
+ width: 100%;
66
+ }
67
+ }
@@ -0,0 +1,10 @@
1
+ import { type AIProvider } from "../../ai/providerOptions.js";
2
+ export declare const useAISettings: ({ adminUserSlug, apiRoute, defaultModels, }: {
3
+ adminUserSlug?: string;
4
+ apiRoute: string;
5
+ defaultModels: Record<AIProvider, string>;
6
+ }) => {
7
+ selectedModel: string;
8
+ setSelectedModel: import("react").Dispatch<import("react").SetStateAction<string>>;
9
+ settingsProvider: "claude" | "google" | "groq" | "mistral" | "openai" | null;
10
+ };
@@ -0,0 +1,56 @@
1
+ "use client";
2
+ import { formatAdminURL } from "payload/shared";
3
+ import { useEffect, useState } from "react";
4
+ import { isAIProvider } from "../../ai/providerOptions.js";
5
+ import { isAbortError } from "./utils.js";
6
+ export const useAISettings = ({ adminUserSlug, apiRoute, defaultModels })=>{
7
+ const [settingsProvider, setSettingsProvider] = useState(null);
8
+ const [selectedModel, setSelectedModel] = useState("");
9
+ useEffect(()=>{
10
+ if (!adminUserSlug) {
11
+ setSettingsProvider(null);
12
+ setSelectedModel("");
13
+ return;
14
+ }
15
+ const abortController = new AbortController();
16
+ const fetchCurrentUser = async ()=>{
17
+ try {
18
+ const res = await fetch(formatAdminURL({
19
+ apiRoute,
20
+ path: `/${adminUserSlug}/me`
21
+ }), {
22
+ signal: abortController.signal
23
+ });
24
+ if (!res.ok) {
25
+ setSettingsProvider(null);
26
+ setSelectedModel("");
27
+ return;
28
+ }
29
+ const result = await res.json();
30
+ const provider = result.user?.aiProvider;
31
+ if (!provider || !isAIProvider(provider)) {
32
+ setSettingsProvider(null);
33
+ setSelectedModel("");
34
+ return;
35
+ }
36
+ setSettingsProvider(provider);
37
+ setSelectedModel(defaultModels[provider]);
38
+ } catch (err) {
39
+ if (isAbortError(err)) return;
40
+ setSettingsProvider(null);
41
+ setSelectedModel("");
42
+ }
43
+ };
44
+ void fetchCurrentUser();
45
+ return ()=>abortController.abort();
46
+ }, [
47
+ adminUserSlug,
48
+ apiRoute,
49
+ defaultModels
50
+ ]);
51
+ return {
52
+ selectedModel,
53
+ setSelectedModel,
54
+ settingsProvider
55
+ };
56
+ };
@@ -0,0 +1,16 @@
1
+ import type { CollectionMentionOption } from "../CollectionMentionPopover.js";
2
+ type MentionRange = {
3
+ end: number;
4
+ start: number;
5
+ };
6
+ interface DocumentMentionSuggestions {
7
+ apiRoute: string;
8
+ documentSuggestionCollection?: null | string;
9
+ mentionQuery: string;
10
+ mentionRange: MentionRange | null;
11
+ }
12
+ export declare const useDocumentMentionSuggestions: ({ apiRoute, documentSuggestionCollection, mentionQuery, mentionRange, }: DocumentMentionSuggestions) => {
13
+ documentSuggestions: CollectionMentionOption[];
14
+ resetDocumentSuggestions: () => void;
15
+ };
16
+ export {};
@@ -0,0 +1,53 @@
1
+ "use client";
2
+ import { formatAdminURL } from "payload/shared";
3
+ import { useEffect, useState } from "react";
4
+ import { isAbortError } from "./utils.js";
5
+ export const useDocumentMentionSuggestions = ({ apiRoute, documentSuggestionCollection, mentionQuery, mentionRange })=>{
6
+ const [documentSuggestions, setDocumentSuggestions] = useState([]);
7
+ useEffect(()=>{
8
+ const trimmedQuery = mentionQuery.trim();
9
+ if (!mentionRange || !trimmedQuery && !documentSuggestionCollection) {
10
+ setDocumentSuggestions([]);
11
+ return;
12
+ }
13
+ const abortController = new AbortController();
14
+ const fetchDocumentSuggestions = async ()=>{
15
+ try {
16
+ const res = await fetch(formatAdminURL({
17
+ apiRoute,
18
+ path: "/ai-mention-suggestions"
19
+ }), {
20
+ body: JSON.stringify({
21
+ collectionSlug: documentSuggestionCollection,
22
+ query: documentSuggestionCollection ? "" : trimmedQuery
23
+ }),
24
+ headers: {
25
+ "Content-Type": "application/json"
26
+ },
27
+ method: "POST",
28
+ signal: abortController.signal
29
+ });
30
+ if (!res.ok) {
31
+ setDocumentSuggestions([]);
32
+ return;
33
+ }
34
+ const result = await res.json();
35
+ setDocumentSuggestions(result.suggestions || []);
36
+ } catch (err) {
37
+ if (isAbortError(err)) return;
38
+ setDocumentSuggestions([]);
39
+ }
40
+ };
41
+ void fetchDocumentSuggestions();
42
+ return ()=>abortController.abort();
43
+ }, [
44
+ apiRoute,
45
+ documentSuggestionCollection,
46
+ mentionQuery,
47
+ mentionRange
48
+ ]);
49
+ return {
50
+ documentSuggestions,
51
+ resetDocumentSuggestions: ()=>setDocumentSuggestions([])
52
+ };
53
+ };
@@ -0,0 +1 @@
1
+ export declare const isAbortError: (err: unknown) => boolean;
@@ -0,0 +1,3 @@
1
+ export const isAbortError = (err)=>{
2
+ return err instanceof DOMException && err.name === "AbortError";
3
+ };
@@ -0,0 +1,6 @@
1
+ import type { PayloadHandler } from "payload";
2
+ type AIApplyActionEndpointOptions = {
3
+ collections?: string[];
4
+ };
5
+ export declare const createAIApplyActionEndpointHandler: (options?: AIApplyActionEndpointOptions) => PayloadHandler;
6
+ export {};