@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.
Files changed (41) hide show
  1. package/README.md +102 -13
  2. package/dist/ai/providerOptions.d.ts +24 -1
  3. package/dist/ai/providerOptions.js +81 -0
  4. package/dist/ai/providerRuntime.d.ts +2 -1
  5. package/dist/ai/providerRuntime.js +5 -2
  6. package/dist/ai/tokenUsage.d.ts +38 -0
  7. package/dist/ai/tokenUsage.js +106 -0
  8. package/dist/components/Icons.d.ts +1 -0
  9. package/dist/components/Icons.js +15 -0
  10. package/dist/components/action-toast/ActionToast.d.ts +5 -1
  11. package/dist/components/action-toast/ActionToast.js +57 -21
  12. package/dist/components/ai-input/AIInput.d.ts +4 -1
  13. package/dist/components/ai-input/AIInput.js +196 -51
  14. package/dist/components/ai-input/AIInput.module.css +89 -2
  15. package/dist/components/audit-log-list/AuditLogList.js +9 -2
  16. package/dist/components/dashboard/Dashboard.js +3 -1
  17. package/dist/components/generate-field/GenerateField.d.ts +11 -0
  18. package/dist/components/generate-field/GenerateField.js +109 -0
  19. package/dist/components/generate-field/GenerateField.module.css +39 -0
  20. package/dist/components/generate-field/richText.d.ts +29 -0
  21. package/dist/components/generate-field/richText.js +30 -0
  22. package/dist/components/hooks/useAIChatStream.d.ts +12 -1
  23. package/dist/components/hooks/useAIChatStream.js +13 -4
  24. package/dist/components/hooks/useAISettings.d.ts +6 -4
  25. package/dist/components/hooks/useAISettings.js +62 -22
  26. package/dist/components/hooks/usePluginConfig.d.ts +8 -20
  27. package/dist/components/hooks/usePluginConfig.js +9 -2
  28. package/dist/components/text-shimmer/TextShimmer.d.ts +9 -0
  29. package/dist/components/text-shimmer/TextShimmer.js +34 -0
  30. package/dist/components/text-shimmer/TextShimmer.module.css +19 -0
  31. package/dist/exports/client.d.ts +4 -0
  32. package/dist/exports/client.js +4 -0
  33. package/dist/handlers/chatHandler.d.ts +4 -1
  34. package/dist/handlers/chatHandler.js +198 -17
  35. package/dist/handlers/generateFieldHandler.d.ts +14 -0
  36. package/dist/handlers/generateFieldHandler.js +144 -0
  37. package/dist/index.d.ts +8 -1
  38. package/dist/index.js +141 -5
  39. package/dist/payload/textFieldGeneration.d.ts +26 -0
  40. package/dist/payload/textFieldGeneration.js +99 -0
  41. 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 { config } = useConfig();
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-labelabel": "View all",
53
+ "aria-label": "View all",
47
54
  margin: false,
48
55
  buttonStyle: "tab",
49
56
  size: "small",
@@ -7,7 +7,9 @@ const Dashboard = ()=>{
7
7
  return /*#__PURE__*/ _jsxs("div", {
8
8
  className: styles.dashboard,
9
9
  children: [
10
- /*#__PURE__*/ _jsx(AIInput, {}),
10
+ /*#__PURE__*/ _jsx(AIInput, {
11
+ isDashboard: true
12
+ }),
11
13
  /*#__PURE__*/ _jsx(AuditLogList, {})
12
14
  ]
13
15
  });
@@ -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 declare const useAIChatStream: ({ apiRoute, clearInput, mentionsRef, prompt, selectedModel, }: {
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 { type AIProvider } from "../../ai/providerOptions.js";
1
+ import type { AIProviderProfile } from "../../ai/providerOptions.js";
2
2
  interface UseAISettingsOptions {
3
3
  adminUserSlug?: string;
4
4
  apiRoute: string;
5
- defaultModels: Record<AIProvider, string>;
5
+ managedProviders: boolean;
6
+ providerProfiles: AIProviderProfile[];
6
7
  }
7
- export declare const useAISettings: ({ adminUserSlug, apiRoute, defaultModels }: UseAISettingsOptions) => {
8
+ export declare const useAISettings: ({ adminUserSlug, apiRoute, managedProviders, providerProfiles }: UseAISettingsOptions) => {
8
9
  selectedModel: string;
9
10
  setSelectedModel: (model: string) => void;
10
- settingsProvider: "claude" | "google" | "mistral" | "openai" | "openrouter" | null;
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
- const provider = result.user?.aiProvider;
25
- return provider && isAIProvider(provider) ? provider : null;
37
+ return result.user?.aiProvider || null;
26
38
  };
27
- export const useAISettings = ({ adminUserSlug, apiRoute, defaultModels })=>{
28
- const [fetchedProvider, setFetchedProvider] = useState(null);
29
- const [selectedModel, setSelectedModel] = useState("");
30
- const setStoredSelectedModel = (model)=>{
31
- setSelectedModel(model);
32
- if (fetchedProvider && model) {
33
- storeModel(fetchedProvider, model);
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 (!adminUserSlug) return;
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
- if (!provider) {
47
- setFetchedProvider(null);
48
- setSelectedModel("");
83
+ const profile = providerProfiles.find((candidate)=>candidate.id === provider);
84
+ if (!profile) {
85
+ setSettingsProvider(null);
86
+ setSelectedModelState("");
49
87
  return;
50
88
  }
51
- setFetchedProvider(provider);
52
- setSelectedModel(getStoredModel(provider) || defaultModels[provider]);
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
- setFetchedProvider(null);
56
- setSelectedModel("");
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
- defaultModels
103
+ managedProviders,
104
+ providerProfiles
65
105
  ]);
66
- const settingsProvider = adminUserSlug ? fetchedProvider : null;
67
106
  return {
68
- selectedModel: adminUserSlug ? selectedModel : "",
69
- setSelectedModel: setStoredSelectedModel,
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
- label: string;
26
- value: string;
27
- }[];
28
- google: {
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 {};