@mvriu5/payload-ai 1.3.2 → 1.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 +102 -13
- package/dist/ai/providerOptions.d.ts +24 -1
- package/dist/ai/providerOptions.js +81 -0
- package/dist/ai/providerRuntime.d.ts +2 -1
- package/dist/ai/providerRuntime.js +5 -2
- package/dist/ai/tokenUsage.d.ts +38 -0
- package/dist/ai/tokenUsage.js +106 -0
- package/dist/components/Icons.d.ts +1 -0
- package/dist/components/Icons.js +15 -0
- package/dist/components/action-toast/ActionToast.d.ts +5 -1
- package/dist/components/action-toast/ActionToast.js +57 -21
- package/dist/components/ai-input/AIInput.d.ts +4 -1
- package/dist/components/ai-input/AIInput.js +196 -51
- package/dist/components/ai-input/AIInput.module.css +89 -2
- package/dist/components/audit-log-list/AuditLogList.js +9 -2
- package/dist/components/dashboard/Dashboard.js +3 -1
- package/dist/components/generate-field/GenerateField.d.ts +11 -0
- package/dist/components/generate-field/GenerateField.js +109 -0
- package/dist/components/generate-field/GenerateField.module.css +39 -0
- package/dist/components/generate-field/richText.d.ts +29 -0
- package/dist/components/generate-field/richText.js +30 -0
- package/dist/components/hooks/useAIChatStream.d.ts +12 -1
- package/dist/components/hooks/useAIChatStream.js +13 -4
- package/dist/components/hooks/useAISettings.d.ts +6 -4
- package/dist/components/hooks/useAISettings.js +62 -22
- package/dist/components/hooks/usePluginConfig.d.ts +8 -20
- package/dist/components/hooks/usePluginConfig.js +9 -2
- package/dist/components/text-shimmer/TextShimmer.d.ts +9 -0
- package/dist/components/text-shimmer/TextShimmer.js +34 -0
- package/dist/components/text-shimmer/TextShimmer.module.css +19 -0
- package/dist/exports/client.d.ts +4 -0
- package/dist/exports/client.js +4 -0
- package/dist/handlers/chatHandler.d.ts +4 -1
- package/dist/handlers/chatHandler.js +198 -17
- package/dist/handlers/generateFieldHandler.d.ts +14 -0
- package/dist/handlers/generateFieldHandler.js +144 -0
- package/dist/index.d.ts +8 -1
- package/dist/index.js +141 -5
- package/dist/payload/textFieldGeneration.d.ts +26 -0
- package/dist/payload/textFieldGeneration.js +99 -0
- package/package.json +18 -16
|
@@ -1,9 +1,57 @@
|
|
|
1
|
+
.collapsible {
|
|
2
|
+
border: 1px solid var(--theme-elevation-150);
|
|
3
|
+
border-radius: 8px;
|
|
4
|
+
margin-bottom: 20px;
|
|
5
|
+
overflow: hidden;
|
|
6
|
+
width: 100%;
|
|
7
|
+
}
|
|
8
|
+
|
|
9
|
+
.collapsibleToggle {
|
|
10
|
+
align-items: center;
|
|
11
|
+
background: transparent;
|
|
12
|
+
border: 0;
|
|
13
|
+
color: var(--theme-text);
|
|
14
|
+
cursor: pointer;
|
|
15
|
+
display: flex;
|
|
16
|
+
font: inherit;
|
|
17
|
+
font-weight: 600;
|
|
18
|
+
justify-content: space-between;
|
|
19
|
+
min-height: 44px;
|
|
20
|
+
padding: 0 20px;
|
|
21
|
+
text-align: left;
|
|
22
|
+
width: 100%;
|
|
23
|
+
}
|
|
24
|
+
|
|
25
|
+
.collapsibleToggle:hover {
|
|
26
|
+
color: var(--theme-elevation-800);
|
|
27
|
+
}
|
|
28
|
+
|
|
29
|
+
.collapsibleToggle:focus-visible {
|
|
30
|
+
outline: 2px solid var(--theme-success-500);
|
|
31
|
+
outline-offset: 2px;
|
|
32
|
+
}
|
|
33
|
+
|
|
34
|
+
.collapsibleToggle .collapsibleChevron {
|
|
35
|
+
flex: 0 0 24px;
|
|
36
|
+
height: 24px;
|
|
37
|
+
width: 24px;
|
|
38
|
+
}
|
|
39
|
+
|
|
40
|
+
.collapsibleContent {
|
|
41
|
+
padding: 0;
|
|
42
|
+
}
|
|
43
|
+
|
|
44
|
+
.collapsibleContent .chat {
|
|
45
|
+
border: 0;
|
|
46
|
+
border-radius: 0;
|
|
47
|
+
padding-top: 0;
|
|
48
|
+
}
|
|
49
|
+
|
|
1
50
|
.chatLayout {
|
|
2
51
|
width: 100%;
|
|
3
52
|
max-width: none;
|
|
4
53
|
display: flex;
|
|
5
54
|
flex-direction: column;
|
|
6
|
-
height: 332px;
|
|
7
55
|
overflow: hidden;
|
|
8
56
|
}
|
|
9
57
|
|
|
@@ -108,9 +156,47 @@
|
|
|
108
156
|
display: none;
|
|
109
157
|
}
|
|
110
158
|
|
|
159
|
+
.mediaButton {
|
|
160
|
+
align-items: center;
|
|
161
|
+
background: transparent;
|
|
162
|
+
border: 0;
|
|
163
|
+
bottom: 8px;
|
|
164
|
+
color: var(--theme-elevation-600);
|
|
165
|
+
cursor: pointer;
|
|
166
|
+
display: inline-flex;
|
|
167
|
+
height: 28px;
|
|
168
|
+
justify-content: center;
|
|
169
|
+
padding: 0;
|
|
170
|
+
position: absolute;
|
|
171
|
+
right: 8px;
|
|
172
|
+
width: 28px;
|
|
173
|
+
z-index: 1;
|
|
174
|
+
}
|
|
175
|
+
|
|
176
|
+
.mediaButton:hover,
|
|
177
|
+
.mediaButton:focus-visible {
|
|
178
|
+
color: var(--theme-text);
|
|
179
|
+
}
|
|
180
|
+
|
|
181
|
+
.mediaButton:focus-visible {
|
|
182
|
+
outline: 1px solid currentColor;
|
|
183
|
+
outline-offset: 1px;
|
|
184
|
+
}
|
|
185
|
+
|
|
186
|
+
.mediaButton:disabled {
|
|
187
|
+
cursor: not-allowed;
|
|
188
|
+
opacity: 0.45;
|
|
189
|
+
}
|
|
190
|
+
|
|
191
|
+
.mediaButton svg {
|
|
192
|
+
height: 16px;
|
|
193
|
+
width: 16px;
|
|
194
|
+
}
|
|
195
|
+
|
|
111
196
|
.chatInput {
|
|
112
197
|
background: transparent;
|
|
113
198
|
border: 0;
|
|
199
|
+
box-sizing: border-box;
|
|
114
200
|
color: var(--theme-text);
|
|
115
201
|
font: inherit;
|
|
116
202
|
flex: 1 1 0;
|
|
@@ -118,7 +204,7 @@
|
|
|
118
204
|
min-height: 0;
|
|
119
205
|
overflow-y: auto;
|
|
120
206
|
overscroll-behavior: contain;
|
|
121
|
-
padding: 0;
|
|
207
|
+
padding: 0 32px 32px 0;
|
|
122
208
|
white-space: pre-wrap;
|
|
123
209
|
word-break: break-word;
|
|
124
210
|
width: 100%;
|
|
@@ -161,6 +247,7 @@
|
|
|
161
247
|
flex-wrap: wrap;
|
|
162
248
|
gap: 6px;
|
|
163
249
|
min-height: 22px;
|
|
250
|
+
padding-right: 32px;
|
|
164
251
|
}
|
|
165
252
|
|
|
166
253
|
.attachmentPill {
|
|
@@ -12,8 +12,15 @@ const getChangeProposal = (change)=>({
|
|
|
12
12
|
label: change.title,
|
|
13
13
|
slug: change.slug || undefined
|
|
14
14
|
});
|
|
15
|
+
const fallbackConfig = {
|
|
16
|
+
routes: {
|
|
17
|
+
admin: "/admin",
|
|
18
|
+
api: "/api"
|
|
19
|
+
}
|
|
20
|
+
};
|
|
15
21
|
const AuditLogList = ()=>{
|
|
16
|
-
const
|
|
22
|
+
const payloadConfigContext = useConfig();
|
|
23
|
+
const config = payloadConfigContext?.config ?? fallbackConfig;
|
|
17
24
|
const { loadRecentChanges, allChangesURL, appliedChanges: changes } = useAuditLog({
|
|
18
25
|
adminRoute: config.routes.admin,
|
|
19
26
|
apiRoute: config.routes.api
|
|
@@ -43,7 +50,7 @@ const AuditLogList = ()=>{
|
|
|
43
50
|
allChangesURL && /*#__PURE__*/ _jsx(Button, {
|
|
44
51
|
url: allChangesURL,
|
|
45
52
|
el: "anchor",
|
|
46
|
-
"aria-
|
|
53
|
+
"aria-label": "View all",
|
|
47
54
|
margin: false,
|
|
48
55
|
buttonStyle: "tab",
|
|
49
56
|
size: "small",
|
|
@@ -0,0 +1,11 @@
|
|
|
1
|
+
type GenerateFieldProps = {
|
|
2
|
+
field?: {
|
|
3
|
+
hasMany?: boolean;
|
|
4
|
+
};
|
|
5
|
+
generationFieldKey: string;
|
|
6
|
+
generationFieldType: "json" | "richText" | "text" | "textarea";
|
|
7
|
+
path: string;
|
|
8
|
+
readOnly?: boolean;
|
|
9
|
+
};
|
|
10
|
+
declare const GenerateField: ({ field, generationFieldKey, generationFieldType, path, readOnly }: GenerateFieldProps) => import("react").JSX.Element | null;
|
|
11
|
+
export default GenerateField;
|
|
@@ -0,0 +1,109 @@
|
|
|
1
|
+
"use client";
|
|
2
|
+
import { jsx as _jsx, jsxs as _jsxs } from "react/jsx-runtime";
|
|
3
|
+
import { useConfig, useDocumentForm, useDocumentInfo, useField, useLocale } from "@payloadcms/ui";
|
|
4
|
+
import { formatAdminURL } from "payload/shared";
|
|
5
|
+
import { useState } from "react";
|
|
6
|
+
import { createGeneratedRichTextValue } from "./richText.js";
|
|
7
|
+
import styles from "./GenerateField.module.css";
|
|
8
|
+
const pendingRequests = new Map();
|
|
9
|
+
const requestGeneratedValue = ({ apiRoute, body, cacheKey })=>{
|
|
10
|
+
const pending = pendingRequests.get(cacheKey);
|
|
11
|
+
if (pending) return pending;
|
|
12
|
+
const request = fetch(formatAdminURL({
|
|
13
|
+
apiRoute,
|
|
14
|
+
path: "/ai-generate-field"
|
|
15
|
+
}), {
|
|
16
|
+
body: JSON.stringify(body),
|
|
17
|
+
headers: {
|
|
18
|
+
"Content-Type": "application/json"
|
|
19
|
+
},
|
|
20
|
+
method: "POST"
|
|
21
|
+
}).then(async (response)=>{
|
|
22
|
+
const result = await response.json().catch(()=>null);
|
|
23
|
+
if (!response.ok || result?.value === undefined) throw new Error(result?.error || "Could not generate field content.");
|
|
24
|
+
return result;
|
|
25
|
+
}).finally(()=>pendingRequests.delete(cacheKey));
|
|
26
|
+
pendingRequests.set(cacheKey, request);
|
|
27
|
+
return request;
|
|
28
|
+
};
|
|
29
|
+
const GenerateField = ({ field, generationFieldKey, generationFieldType, path, readOnly })=>{
|
|
30
|
+
const configContext = useConfig();
|
|
31
|
+
const documentForm = useDocumentForm();
|
|
32
|
+
const documentInfo = useDocumentInfo();
|
|
33
|
+
const locale = useLocale();
|
|
34
|
+
const { disabled, setValue } = useField({
|
|
35
|
+
path
|
|
36
|
+
});
|
|
37
|
+
const [error, setError] = useState("");
|
|
38
|
+
const [isGenerating, setIsGenerating] = useState(false);
|
|
39
|
+
const scope = documentInfo.collectionSlug ? {
|
|
40
|
+
slug: documentInfo.collectionSlug,
|
|
41
|
+
type: "collection"
|
|
42
|
+
} : documentInfo.globalSlug ? {
|
|
43
|
+
slug: documentInfo.globalSlug,
|
|
44
|
+
type: "global"
|
|
45
|
+
} : null;
|
|
46
|
+
const generate = async ()=>{
|
|
47
|
+
const apiRoute = configContext?.config?.routes?.api;
|
|
48
|
+
if (!apiRoute || !scope || isGenerating) return;
|
|
49
|
+
setError("");
|
|
50
|
+
setIsGenerating(true);
|
|
51
|
+
try {
|
|
52
|
+
const context = documentForm.getData();
|
|
53
|
+
const cacheKey = JSON.stringify([
|
|
54
|
+
scope.type,
|
|
55
|
+
scope.slug,
|
|
56
|
+
generationFieldKey,
|
|
57
|
+
locale.code,
|
|
58
|
+
context
|
|
59
|
+
]);
|
|
60
|
+
const result = await requestGeneratedValue({
|
|
61
|
+
apiRoute,
|
|
62
|
+
body: {
|
|
63
|
+
context,
|
|
64
|
+
fieldKey: generationFieldKey,
|
|
65
|
+
locale: locale.code,
|
|
66
|
+
scope
|
|
67
|
+
},
|
|
68
|
+
cacheKey
|
|
69
|
+
});
|
|
70
|
+
if (generationFieldType === "richText") {
|
|
71
|
+
const value = createGeneratedRichTextValue(result.value);
|
|
72
|
+
documentForm.dispatchFields({
|
|
73
|
+
initialValue: value,
|
|
74
|
+
path,
|
|
75
|
+
type: "UPDATE",
|
|
76
|
+
value
|
|
77
|
+
});
|
|
78
|
+
} else {
|
|
79
|
+
setValue(field?.hasMany ? [
|
|
80
|
+
result.value
|
|
81
|
+
] : result.value);
|
|
82
|
+
}
|
|
83
|
+
documentForm.setModified(true);
|
|
84
|
+
} catch (err) {
|
|
85
|
+
setError(err instanceof Error ? err.message : "Could not generate field content.");
|
|
86
|
+
} finally{
|
|
87
|
+
setIsGenerating(false);
|
|
88
|
+
}
|
|
89
|
+
};
|
|
90
|
+
if (!scope) return null;
|
|
91
|
+
return /*#__PURE__*/ _jsxs("div", {
|
|
92
|
+
className: styles.generateField,
|
|
93
|
+
children: [
|
|
94
|
+
/*#__PURE__*/ _jsx("button", {
|
|
95
|
+
className: styles.generateButton,
|
|
96
|
+
disabled: Boolean(readOnly || disabled || isGenerating),
|
|
97
|
+
onClick: ()=>void generate(),
|
|
98
|
+
type: "button",
|
|
99
|
+
children: isGenerating ? "Generating..." : "Generate"
|
|
100
|
+
}),
|
|
101
|
+
error && /*#__PURE__*/ _jsx("span", {
|
|
102
|
+
className: styles.generateError,
|
|
103
|
+
role: "alert",
|
|
104
|
+
children: error
|
|
105
|
+
})
|
|
106
|
+
]
|
|
107
|
+
});
|
|
108
|
+
};
|
|
109
|
+
export default GenerateField;
|
|
@@ -0,0 +1,39 @@
|
|
|
1
|
+
.generateField {
|
|
2
|
+
align-items: center;
|
|
3
|
+
display: flex;
|
|
4
|
+
gap: 8px;
|
|
5
|
+
justify-content: flex-end;
|
|
6
|
+
margin-top: 6px;
|
|
7
|
+
}
|
|
8
|
+
|
|
9
|
+
.generateButton {
|
|
10
|
+
background: transparent;
|
|
11
|
+
border: 0;
|
|
12
|
+
color: var(--theme-elevation-600);
|
|
13
|
+
cursor: pointer;
|
|
14
|
+
font: inherit;
|
|
15
|
+
font-size: 12px;
|
|
16
|
+
line-height: 1.2;
|
|
17
|
+
padding: 2px 0;
|
|
18
|
+
}
|
|
19
|
+
|
|
20
|
+
.generateButton:hover,
|
|
21
|
+
.generateButton:focus-visible {
|
|
22
|
+
color: var(--theme-text);
|
|
23
|
+
}
|
|
24
|
+
|
|
25
|
+
.generateButton:focus-visible {
|
|
26
|
+
outline: 1px solid currentColor;
|
|
27
|
+
outline-offset: 2px;
|
|
28
|
+
}
|
|
29
|
+
|
|
30
|
+
.generateButton:disabled {
|
|
31
|
+
cursor: not-allowed;
|
|
32
|
+
opacity: 0.45;
|
|
33
|
+
}
|
|
34
|
+
|
|
35
|
+
.generateError {
|
|
36
|
+
color: var(--theme-error-500);
|
|
37
|
+
font-size: 11px;
|
|
38
|
+
line-height: 1.2;
|
|
39
|
+
}
|
|
@@ -0,0 +1,29 @@
|
|
|
1
|
+
type LexicalTextNode = {
|
|
2
|
+
detail: number;
|
|
3
|
+
format: number;
|
|
4
|
+
mode: "normal";
|
|
5
|
+
style: string;
|
|
6
|
+
text: string;
|
|
7
|
+
type: "text";
|
|
8
|
+
version: 1;
|
|
9
|
+
};
|
|
10
|
+
type LexicalParagraphNode = {
|
|
11
|
+
children: LexicalTextNode[];
|
|
12
|
+
direction: "ltr";
|
|
13
|
+
format: "";
|
|
14
|
+
indent: number;
|
|
15
|
+
type: "paragraph";
|
|
16
|
+
version: 1;
|
|
17
|
+
};
|
|
18
|
+
export type GeneratedRichTextValue = {
|
|
19
|
+
root: {
|
|
20
|
+
children: LexicalParagraphNode[];
|
|
21
|
+
direction: "ltr";
|
|
22
|
+
format: "";
|
|
23
|
+
indent: number;
|
|
24
|
+
type: "root";
|
|
25
|
+
version: 1;
|
|
26
|
+
};
|
|
27
|
+
};
|
|
28
|
+
export declare const createGeneratedRichTextValue: (value: string) => GeneratedRichTextValue;
|
|
29
|
+
export {};
|
|
@@ -0,0 +1,30 @@
|
|
|
1
|
+
export const createGeneratedRichTextValue = (value)=>{
|
|
2
|
+
const paragraphs = value.split(/\n\s*\n/).map((paragraph)=>paragraph.replace(/\s*\n\s*/g, " ").trim()).filter(Boolean);
|
|
3
|
+
return {
|
|
4
|
+
root: {
|
|
5
|
+
children: paragraphs.map((text)=>({
|
|
6
|
+
children: [
|
|
7
|
+
{
|
|
8
|
+
detail: 0,
|
|
9
|
+
format: 0,
|
|
10
|
+
mode: "normal",
|
|
11
|
+
style: "",
|
|
12
|
+
text,
|
|
13
|
+
type: "text",
|
|
14
|
+
version: 1
|
|
15
|
+
}
|
|
16
|
+
],
|
|
17
|
+
direction: "ltr",
|
|
18
|
+
format: "",
|
|
19
|
+
indent: 0,
|
|
20
|
+
type: "paragraph",
|
|
21
|
+
version: 1
|
|
22
|
+
})),
|
|
23
|
+
direction: "ltr",
|
|
24
|
+
format: "",
|
|
25
|
+
indent: 0,
|
|
26
|
+
type: "root",
|
|
27
|
+
version: 1
|
|
28
|
+
}
|
|
29
|
+
};
|
|
30
|
+
};
|
|
@@ -15,16 +15,27 @@ export type MediaAttachment = {
|
|
|
15
15
|
type: "media";
|
|
16
16
|
url?: string;
|
|
17
17
|
};
|
|
18
|
-
export
|
|
18
|
+
export type DocumentScope = {
|
|
19
|
+
collection: string;
|
|
20
|
+
id?: string;
|
|
21
|
+
type: "collection";
|
|
22
|
+
} | {
|
|
23
|
+
slug: string;
|
|
24
|
+
type: "global";
|
|
25
|
+
};
|
|
26
|
+
export declare const useAIChatStream: ({ apiRoute, clearInput, documentScope, mentionsRef, prompt, selectedModel, selectedProvider, }: {
|
|
19
27
|
apiRoute: string;
|
|
20
28
|
clearInput: () => void;
|
|
29
|
+
documentScope?: DocumentScope;
|
|
21
30
|
mentionsRef: RefObject<Mention[]>;
|
|
22
31
|
prompt: string;
|
|
23
32
|
selectedModel: string;
|
|
33
|
+
selectedProvider: string | null;
|
|
24
34
|
}) => {
|
|
25
35
|
dismissChat: () => void;
|
|
26
36
|
error: string;
|
|
27
37
|
isLoading: boolean;
|
|
38
|
+
setIsLoading: import("react").Dispatch<import("react").SetStateAction<boolean>>;
|
|
28
39
|
proposals: ActionProposal[];
|
|
29
40
|
resetChatState: () => void;
|
|
30
41
|
response: string;
|
|
@@ -54,7 +54,7 @@ const getChatDebugMessage = (debugInfo)=>{
|
|
|
54
54
|
}
|
|
55
55
|
return getDebugReasonLabel(debugInfo.reason);
|
|
56
56
|
};
|
|
57
|
-
export const useAIChatStream = ({ apiRoute, clearInput, mentionsRef, prompt, selectedModel })=>{
|
|
57
|
+
export const useAIChatStream = ({ apiRoute, clearInput, documentScope, mentionsRef, prompt, selectedModel, selectedProvider })=>{
|
|
58
58
|
const [response, setResponse] = useState("");
|
|
59
59
|
const [tokenUsage, setTokenUsage] = useState(null);
|
|
60
60
|
const [error, setError] = useState("");
|
|
@@ -76,7 +76,6 @@ export const useAIChatStream = ({ apiRoute, clearInput, mentionsRef, prompt, sel
|
|
|
76
76
|
const submit = useCallback(async ({ attachments = [] } = {})=>{
|
|
77
77
|
const trimmedPrompt = prompt.trim();
|
|
78
78
|
if (!trimmedPrompt) return;
|
|
79
|
-
setIsLoading(true);
|
|
80
79
|
resetChatState();
|
|
81
80
|
try {
|
|
82
81
|
const res = await fetch(formatAdminURL({
|
|
@@ -87,15 +86,22 @@ export const useAIChatStream = ({ apiRoute, clearInput, mentionsRef, prompt, sel
|
|
|
87
86
|
...attachments.length > 0 ? {
|
|
88
87
|
attachments
|
|
89
88
|
} : {},
|
|
89
|
+
...documentScope ? {
|
|
90
|
+
documentScope
|
|
91
|
+
} : {},
|
|
90
92
|
mentions: mentionsRef.current,
|
|
91
93
|
model: selectedModel,
|
|
92
|
-
prompt: trimmedPrompt
|
|
94
|
+
prompt: trimmedPrompt,
|
|
95
|
+
...selectedProvider ? {
|
|
96
|
+
provider: selectedProvider
|
|
97
|
+
} : {}
|
|
93
98
|
}),
|
|
94
99
|
headers: {
|
|
95
100
|
"Content-Type": "application/json"
|
|
96
101
|
},
|
|
97
102
|
method: "POST"
|
|
98
103
|
});
|
|
104
|
+
setIsLoading(false);
|
|
99
105
|
if (!res.ok) {
|
|
100
106
|
const result = await res.json().catch(()=>null);
|
|
101
107
|
throw new Error(result?.error || "AI request failed");
|
|
@@ -195,15 +201,18 @@ export const useAIChatStream = ({ apiRoute, clearInput, mentionsRef, prompt, sel
|
|
|
195
201
|
}, [
|
|
196
202
|
apiRoute,
|
|
197
203
|
clearInput,
|
|
204
|
+
documentScope,
|
|
198
205
|
mentionsRef,
|
|
199
206
|
prompt,
|
|
200
207
|
resetChatState,
|
|
201
|
-
selectedModel
|
|
208
|
+
selectedModel,
|
|
209
|
+
selectedProvider
|
|
202
210
|
]);
|
|
203
211
|
return {
|
|
204
212
|
dismissChat,
|
|
205
213
|
error,
|
|
206
214
|
isLoading,
|
|
215
|
+
setIsLoading,
|
|
207
216
|
proposals,
|
|
208
217
|
resetChatState,
|
|
209
218
|
response,
|
|
@@ -1,12 +1,14 @@
|
|
|
1
|
-
import {
|
|
1
|
+
import type { AIProviderProfile } from "../../ai/providerOptions.js";
|
|
2
2
|
interface UseAISettingsOptions {
|
|
3
3
|
adminUserSlug?: string;
|
|
4
4
|
apiRoute: string;
|
|
5
|
-
|
|
5
|
+
managedProviders: boolean;
|
|
6
|
+
providerProfiles: AIProviderProfile[];
|
|
6
7
|
}
|
|
7
|
-
export declare const useAISettings: ({ adminUserSlug, apiRoute,
|
|
8
|
+
export declare const useAISettings: ({ adminUserSlug, apiRoute, managedProviders, providerProfiles }: UseAISettingsOptions) => {
|
|
8
9
|
selectedModel: string;
|
|
9
10
|
setSelectedModel: (model: string) => void;
|
|
10
|
-
|
|
11
|
+
setSelectedProviderModel: (provider: string, model: string) => void;
|
|
12
|
+
settingsProvider: string | null;
|
|
11
13
|
};
|
|
12
14
|
export {};
|
|
@@ -1,8 +1,8 @@
|
|
|
1
1
|
"use client";
|
|
2
2
|
import { formatAdminURL } from "payload/shared";
|
|
3
3
|
import { useEffect, useState } from "react";
|
|
4
|
-
import { isAIProvider } from "../../ai/providerOptions.js";
|
|
5
4
|
import { isAbortError } from "../../payload/shared.js";
|
|
5
|
+
const managedSelectionKey = "payload-ai:selected-managed-model";
|
|
6
6
|
const getStoredModelKey = (provider)=>`payload-ai:selected-model:${provider}`;
|
|
7
7
|
const getStoredModel = (provider)=>{
|
|
8
8
|
if (typeof window === "undefined") return null;
|
|
@@ -12,6 +12,19 @@ const storeModel = (provider, model)=>{
|
|
|
12
12
|
if (typeof window === "undefined") return;
|
|
13
13
|
window.localStorage.setItem(getStoredModelKey(provider), model);
|
|
14
14
|
};
|
|
15
|
+
const getStoredManagedSelection = ()=>{
|
|
16
|
+
if (typeof window === "undefined") return null;
|
|
17
|
+
try {
|
|
18
|
+
const value = JSON.parse(window.localStorage.getItem(managedSelectionKey) || "null");
|
|
19
|
+
return value && typeof value.provider === "string" && typeof value.model === "string" ? value : null;
|
|
20
|
+
} catch {
|
|
21
|
+
return null;
|
|
22
|
+
}
|
|
23
|
+
};
|
|
24
|
+
const storeManagedSelection = (selection)=>{
|
|
25
|
+
if (typeof window === "undefined") return;
|
|
26
|
+
window.localStorage.setItem(managedSelectionKey, JSON.stringify(selection));
|
|
27
|
+
};
|
|
15
28
|
const fetchCurrentUserProvider = async ({ adminUserSlug, apiRoute, signal })=>{
|
|
16
29
|
const res = await fetch(formatAdminURL({
|
|
17
30
|
apiRoute,
|
|
@@ -21,20 +34,44 @@ const fetchCurrentUserProvider = async ({ adminUserSlug, apiRoute, signal })=>{
|
|
|
21
34
|
});
|
|
22
35
|
if (!res.ok) return null;
|
|
23
36
|
const result = await res.json();
|
|
24
|
-
|
|
25
|
-
return provider && isAIProvider(provider) ? provider : null;
|
|
37
|
+
return result.user?.aiProvider || null;
|
|
26
38
|
};
|
|
27
|
-
|
|
28
|
-
|
|
29
|
-
const [
|
|
30
|
-
const
|
|
31
|
-
|
|
32
|
-
|
|
33
|
-
|
|
39
|
+
const hasModel = (profile, model)=>profile.models.some((option)=>option.value === model);
|
|
40
|
+
export const useAISettings = ({ adminUserSlug, apiRoute, managedProviders, providerProfiles })=>{
|
|
41
|
+
const [settingsProvider, setSettingsProvider] = useState(null);
|
|
42
|
+
const [selectedModel, setSelectedModelState] = useState("");
|
|
43
|
+
const setSelectedProviderModel = (provider, model)=>{
|
|
44
|
+
const profile = providerProfiles.find((candidate)=>candidate.id === provider);
|
|
45
|
+
if (!profile || !hasModel(profile, model)) return;
|
|
46
|
+
setSettingsProvider(provider);
|
|
47
|
+
setSelectedModelState(model);
|
|
48
|
+
if (managedProviders) {
|
|
49
|
+
storeManagedSelection({
|
|
50
|
+
model,
|
|
51
|
+
provider
|
|
52
|
+
});
|
|
53
|
+
} else {
|
|
54
|
+
storeModel(provider, model);
|
|
34
55
|
}
|
|
35
56
|
};
|
|
57
|
+
const setSelectedModel = (model)=>{
|
|
58
|
+
if (settingsProvider) setSelectedProviderModel(settingsProvider, model);
|
|
59
|
+
};
|
|
36
60
|
useEffect(()=>{
|
|
37
|
-
if (
|
|
61
|
+
if (managedProviders) {
|
|
62
|
+
const storedSelection = getStoredManagedSelection();
|
|
63
|
+
const storedProfile = storedSelection ? providerProfiles.find((profile)=>profile.id === storedSelection.provider && hasModel(profile, storedSelection.model)) : null;
|
|
64
|
+
const profile = storedProfile || providerProfiles[0];
|
|
65
|
+
const model = storedProfile && storedSelection ? storedSelection.model : profile?.defaultModel;
|
|
66
|
+
setSettingsProvider(profile?.id || null);
|
|
67
|
+
setSelectedModelState(model || "");
|
|
68
|
+
return;
|
|
69
|
+
}
|
|
70
|
+
if (!adminUserSlug) {
|
|
71
|
+
setSettingsProvider(null);
|
|
72
|
+
setSelectedModelState("");
|
|
73
|
+
return;
|
|
74
|
+
}
|
|
38
75
|
const abortController = new AbortController();
|
|
39
76
|
const loadCurrentUserProvider = async ()=>{
|
|
40
77
|
try {
|
|
@@ -43,17 +80,19 @@ export const useAISettings = ({ adminUserSlug, apiRoute, defaultModels })=>{
|
|
|
43
80
|
apiRoute,
|
|
44
81
|
signal: abortController.signal
|
|
45
82
|
});
|
|
46
|
-
|
|
47
|
-
|
|
48
|
-
|
|
83
|
+
const profile = providerProfiles.find((candidate)=>candidate.id === provider);
|
|
84
|
+
if (!profile) {
|
|
85
|
+
setSettingsProvider(null);
|
|
86
|
+
setSelectedModelState("");
|
|
49
87
|
return;
|
|
50
88
|
}
|
|
51
|
-
|
|
52
|
-
|
|
89
|
+
const storedModel = getStoredModel(profile.id);
|
|
90
|
+
setSettingsProvider(profile.id);
|
|
91
|
+
setSelectedModelState(storedModel && hasModel(profile, storedModel) ? storedModel : profile.defaultModel);
|
|
53
92
|
} catch (err) {
|
|
54
93
|
if (isAbortError(err)) return;
|
|
55
|
-
|
|
56
|
-
|
|
94
|
+
setSettingsProvider(null);
|
|
95
|
+
setSelectedModelState("");
|
|
57
96
|
}
|
|
58
97
|
};
|
|
59
98
|
void loadCurrentUserProvider();
|
|
@@ -61,12 +100,13 @@ export const useAISettings = ({ adminUserSlug, apiRoute, defaultModels })=>{
|
|
|
61
100
|
}, [
|
|
62
101
|
adminUserSlug,
|
|
63
102
|
apiRoute,
|
|
64
|
-
|
|
103
|
+
managedProviders,
|
|
104
|
+
providerProfiles
|
|
65
105
|
]);
|
|
66
|
-
const settingsProvider = adminUserSlug ? fetchedProvider : null;
|
|
67
106
|
return {
|
|
68
|
-
selectedModel
|
|
69
|
-
setSelectedModel
|
|
107
|
+
selectedModel,
|
|
108
|
+
setSelectedModel,
|
|
109
|
+
setSelectedProviderModel,
|
|
70
110
|
settingsProvider
|
|
71
111
|
};
|
|
72
112
|
};
|
|
@@ -1,3 +1,4 @@
|
|
|
1
|
+
import { type AIProviderProfile } from "../../ai/providerOptions.js";
|
|
1
2
|
type MediaConfig = {
|
|
2
3
|
acceptedMimeTypes?: string[];
|
|
3
4
|
collectionSlug: string;
|
|
@@ -21,32 +22,19 @@ export declare const usePluginConfig: (config: {
|
|
|
21
22
|
aiModelConfig: {
|
|
22
23
|
defaults: Record<"claude" | "google" | "mistral" | "openai" | "openrouter", string>;
|
|
23
24
|
providers: {
|
|
24
|
-
claude:
|
|
25
|
-
|
|
26
|
-
|
|
27
|
-
|
|
28
|
-
|
|
29
|
-
label: string;
|
|
30
|
-
value: string;
|
|
31
|
-
}[];
|
|
32
|
-
mistral: {
|
|
33
|
-
label: string;
|
|
34
|
-
value: string;
|
|
35
|
-
}[];
|
|
36
|
-
openai: {
|
|
37
|
-
label: string;
|
|
38
|
-
value: string;
|
|
39
|
-
}[];
|
|
40
|
-
openrouter: {
|
|
41
|
-
label: string;
|
|
42
|
-
value: string;
|
|
43
|
-
}[];
|
|
25
|
+
claude: import("../../ai/providerOptions.js").AIProviderModelOption[];
|
|
26
|
+
google: import("../../ai/providerOptions.js").AIProviderModelOption[];
|
|
27
|
+
mistral: import("../../ai/providerOptions.js").AIProviderModelOption[];
|
|
28
|
+
openai: import("../../ai/providerOptions.js").AIProviderModelOption[];
|
|
29
|
+
openrouter: import("../../ai/providerOptions.js").AIProviderModelOption[];
|
|
44
30
|
};
|
|
45
31
|
};
|
|
46
32
|
defaultLocale: string | undefined;
|
|
47
33
|
enabledCollectionSlugSet: Set<string> | null;
|
|
48
34
|
isCollectionMentionEnabled: (slug: string) => boolean;
|
|
49
35
|
locales: LocaleConfig[];
|
|
36
|
+
managedProviders: boolean;
|
|
50
37
|
media: MediaConfig | undefined;
|
|
38
|
+
providerProfiles: AIProviderProfile[];
|
|
51
39
|
};
|
|
52
40
|
export {};
|