@mvriu5/payload-ai 1.3.2 → 1.4.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 (32) hide show
  1. package/README.md +76 -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 +165 -50
  14. package/dist/components/ai-input/AIInput.module.css +40 -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/hooks/useAIChatStream.d.ts +12 -1
  18. package/dist/components/hooks/useAIChatStream.js +13 -4
  19. package/dist/components/hooks/useAISettings.d.ts +6 -4
  20. package/dist/components/hooks/useAISettings.js +62 -22
  21. package/dist/components/hooks/usePluginConfig.d.ts +8 -20
  22. package/dist/components/hooks/usePluginConfig.js +9 -2
  23. package/dist/components/text-shimmer/TextShimmer.d.ts +9 -0
  24. package/dist/components/text-shimmer/TextShimmer.js +34 -0
  25. package/dist/components/text-shimmer/TextShimmer.module.css +19 -0
  26. package/dist/exports/client.d.ts +2 -0
  27. package/dist/exports/client.js +2 -0
  28. package/dist/handlers/chatHandler.d.ts +4 -1
  29. package/dist/handlers/chatHandler.js +198 -17
  30. package/dist/index.d.ts +6 -1
  31. package/dist/index.js +92 -5
  32. package/package.json +18 -16
@@ -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 {};
@@ -1,10 +1,15 @@
1
1
  import { useMemo } from "react";
2
- import { getResolvedAIModelConfig } from "../../ai/providerOptions.js";
2
+ import { getLegacyAIProviderProfiles, getResolvedAIModelConfig } from "../../ai/providerOptions.js";
3
3
  export const usePluginConfig = (config)=>{
4
4
  const pluginConfig = config.admin?.custom?.payloadAiPlugin;
5
5
  const aiModelConfig = useMemo(()=>getResolvedAIModelConfig(pluginConfig?.models), [
6
6
  pluginConfig?.models
7
7
  ]);
8
+ const providerProfiles = useMemo(()=>pluginConfig?.managedProviders ? pluginConfig.providers || [] : getLegacyAIProviderProfiles(pluginConfig?.models), [
9
+ pluginConfig?.managedProviders,
10
+ pluginConfig?.models,
11
+ pluginConfig?.providers
12
+ ]);
8
13
  const enabledCollectionSlugSet = useMemo(()=>pluginConfig?.collectionSlugs ? new Set(pluginConfig.collectionSlugs) : null, [
9
14
  pluginConfig?.collectionSlugs
10
15
  ]);
@@ -15,6 +20,8 @@ export const usePluginConfig = (config)=>{
15
20
  enabledCollectionSlugSet,
16
21
  isCollectionMentionEnabled: (slug)=>!enabledCollectionSlugSet || enabledCollectionSlugSet.has(slug),
17
22
  locales: localization?.locales ?? [],
18
- media: pluginConfig?.media
23
+ managedProviders: Boolean(pluginConfig?.managedProviders),
24
+ media: pluginConfig?.media,
25
+ providerProfiles
19
26
  };
20
27
  };
@@ -0,0 +1,9 @@
1
+ import React from "react";
2
+ export type TextShimmerProps = {
3
+ children: string;
4
+ as?: React.ElementType;
5
+ className?: string;
6
+ duration?: number;
7
+ spread?: number;
8
+ };
9
+ export declare function TextShimmer({ children, as: Component, className, duration, spread }: TextShimmerProps): React.JSX.Element;
@@ -0,0 +1,34 @@
1
+ "use client";
2
+ import { jsx as _jsx } from "react/jsx-runtime";
3
+ import React, { useMemo } from "react";
4
+ import styles from "./TextShimmer.module.css";
5
+ export function TextShimmer({ children, as: Component = "p", className, duration = 1, spread = 2 }) {
6
+ const dynamicSpread = useMemo(()=>{
7
+ return children.length * spread;
8
+ }, [
9
+ children,
10
+ spread
11
+ ]);
12
+ return /*#__PURE__*/ _jsx(Component, {
13
+ className: `${className ?? ""} ${styles.textShimmer}`,
14
+ style: {
15
+ position: "relative",
16
+ display: "inline-block",
17
+ backgroundImage: `
18
+ linear-gradient(
19
+ 90deg,
20
+ transparent calc(50% - ${dynamicSpread}px),
21
+ var(--shimmer-color, #000),
22
+ transparent calc(50% + ${dynamicSpread}px)
23
+ ),
24
+ linear-gradient(var(--base-color, #a1a1aa), var(--base-color, #a1a1aa))
25
+ `,
26
+ backgroundSize: "250% 100%, auto",
27
+ backgroundRepeat: "no-repeat",
28
+ backgroundClip: "text",
29
+ WebkitBackgroundClip: "text",
30
+ color: "transparent"
31
+ },
32
+ children: children
33
+ });
34
+ }
@@ -0,0 +1,19 @@
1
+ .textShimmer {
2
+ animation: textShimmer 2s linear infinite;
3
+ }
4
+
5
+ @keyframes textShimmer {
6
+ 0% {
7
+ background-position: 250% 0;
8
+ }
9
+ 100% {
10
+ background-position: -250% 0;
11
+ }
12
+ }
13
+
14
+ @media (prefers-color-scheme: dark) {
15
+ :root {
16
+ --base-color: #71717a;
17
+ --shimmer-color: #ffffff;
18
+ }
19
+ }
@@ -1,4 +1,6 @@
1
1
  import Dashboard from "../components/dashboard/Dashboard.js";
2
2
  import APIKeyField from "../components/APIKeyField.js";
3
+ import AIInput from "../components/ai-input/AIInput.js";
3
4
  export { Dashboard };
4
5
  export { APIKeyField as AIApiKeyField };
6
+ export { AIInput };
@@ -1,4 +1,6 @@
1
1
  import Dashboard from "../components/dashboard/Dashboard.js";
2
2
  import APIKeyField from "../components/APIKeyField.js";
3
+ import AIInput from "../components/ai-input/AIInput.js";
3
4
  export { Dashboard };
4
5
  export { APIKeyField as AIApiKeyField };
6
+ export { AIInput };
@@ -1,6 +1,7 @@
1
1
  import type { PayloadHandler } from "payload";
2
2
  import { type AIActionSignature } from "../ai/proposalSigning.js";
3
- import { type AIModelConfig } from "../ai/providerOptions.js";
3
+ import { type AIModelConfig, type ResolvedAIProviderConfig } from "../ai/providerOptions.js";
4
+ import { type ResolvedMaxTokenUsageOptions } from "../ai/tokenUsage.js";
4
5
  import { type ResolvedCollectionPermissionMap } from "../payload/collectionPermissions.js";
5
6
  type LocalizedDataInput = Record<string, Record<string, unknown>>;
6
7
  type ProposalWritePayload = {
@@ -36,7 +37,9 @@ type ChatOptions = {
36
37
  allowUserApiKeys?: boolean;
37
38
  collections?: ResolvedCollectionPermissionMap;
38
39
  maxOutputTokens?: number;
40
+ maxTokenUsage?: ResolvedMaxTokenUsageOptions;
39
41
  models?: AIModelConfig;
42
+ providers?: ResolvedAIProviderConfig[];
40
43
  };
41
44
  export declare const createChatHandler: (options?: ChatOptions) => PayloadHandler;
42
45
  export {};