@mvriu5/payload-ai 1.2.0 → 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 (63) hide show
  1. package/README.md +118 -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 -8
  9. package/dist/components/Icons.js +10 -207
  10. package/dist/components/{ActionToast.d.ts → action-toast/ActionToast.d.ts} +5 -2
  11. package/dist/components/{ActionToast.js → action-toast/ActionToast.js} +81 -54
  12. package/dist/components/{ActionToast.module.css → action-toast/ActionToast.module.css} +0 -79
  13. package/dist/components/ai-input/AIInput.d.ts +5 -0
  14. package/dist/components/ai-input/AIInput.js +548 -0
  15. package/dist/components/{AIInput.module.css → ai-input/AIInput.module.css} +150 -44
  16. package/dist/components/ai-input/badge.d.ts +14 -0
  17. package/dist/components/ai-input/badge.js +85 -0
  18. package/dist/components/{AuditLogList.d.ts → audit-log-list/AuditLogList.d.ts} +4 -8
  19. package/dist/components/{AuditLogList.js → audit-log-list/AuditLogList.js} +56 -21
  20. package/dist/components/{AuditLogList.module.css → audit-log-list/AuditLogList.module.css} +11 -65
  21. package/dist/components/dashboard/Dashboard.d.ts +2 -0
  22. package/dist/components/dashboard/Dashboard.js +17 -0
  23. package/dist/components/dashboard/Dashboard.module.css +13 -0
  24. package/dist/components/{DiffDialog.d.ts → diff-dialog/DiffDialog.d.ts} +2 -2
  25. package/dist/components/{DiffDialog.js → diff-dialog/DiffDialog.js} +200 -189
  26. package/dist/components/{DiffDialog.module.css → diff-dialog/DiffDialog.module.css} +17 -35
  27. package/dist/components/hooks/useAIChatStream.d.ts +50 -0
  28. package/dist/components/hooks/useAIChatStream.js +226 -0
  29. package/dist/components/hooks/useAISettings.d.ts +6 -4
  30. package/dist/components/hooks/useAISettings.js +68 -27
  31. package/dist/components/hooks/useAuditLog.d.ts +11 -0
  32. package/dist/components/hooks/useAuditLog.js +41 -0
  33. package/dist/components/hooks/useDocumentMentionSuggestions.d.ts +1 -1
  34. package/dist/components/hooks/useDocumentMentionSuggestions.js +31 -27
  35. package/dist/components/hooks/useMentions.d.ts +58 -0
  36. package/dist/components/hooks/useMentions.js +276 -0
  37. package/dist/components/hooks/usePluginConfig.d.ts +40 -0
  38. package/dist/components/hooks/usePluginConfig.js +27 -0
  39. package/dist/components/{MentionPopover.d.ts → mention-popover/MentionPopover.d.ts} +2 -4
  40. package/dist/components/{MentionPopover.js → mention-popover/MentionPopover.js} +1 -8
  41. package/dist/components/{MentionPopover.module.css → mention-popover/MentionPopover.module.css} +1 -1
  42. package/dist/components/text-shimmer/TextShimmer.d.ts +9 -0
  43. package/dist/components/text-shimmer/TextShimmer.js +34 -0
  44. package/dist/components/text-shimmer/TextShimmer.module.css +19 -0
  45. package/dist/exports/client.d.ts +4 -2
  46. package/dist/exports/client.js +4 -2
  47. package/dist/handlers/applyActionHandler.js +27 -7
  48. package/dist/handlers/chatHandler.d.ts +4 -1
  49. package/dist/handlers/chatHandler.js +499 -74
  50. package/dist/handlers/mediaUploadHandler.d.ts +7 -0
  51. package/dist/handlers/mediaUploadHandler.js +99 -0
  52. package/dist/handlers/mentionSuggestionHandler.js +16 -14
  53. package/dist/handlers/proposalDiffHandler.js +41 -29
  54. package/dist/index.d.ts +12 -1
  55. package/dist/index.js +131 -12
  56. package/dist/payload/collectionPermissions.js +3 -1
  57. package/dist/payload/normalizeData.d.ts +0 -6
  58. package/dist/payload/normalizeData.js +25 -13
  59. package/dist/payload/proposalData.js +4 -1
  60. package/dist/payload/schemaContext.js +98 -43
  61. package/package.json +22 -20
  62. package/dist/components/AIInput.d.ts +0 -2
  63. package/dist/components/AIInput.js +0 -896
@@ -0,0 +1,226 @@
1
+ import { formatAdminURL } from "payload/shared";
2
+ import { useCallback, useState } from "react";
3
+ const responseOnlyToastCooldownMs = 10000;
4
+ const parseSSEEvent = (chunk)=>{
5
+ const lines = chunk.split("\n");
6
+ let eventName = "";
7
+ const dataLines = [];
8
+ for (const line of lines){
9
+ if (line.startsWith("event:")) {
10
+ eventName = line.slice(6).trim();
11
+ continue;
12
+ }
13
+ if (line.startsWith("data:")) {
14
+ dataLines.push(line.slice(5).trim());
15
+ }
16
+ }
17
+ if (!eventName) return null;
18
+ try {
19
+ const data = JSON.parse(dataLines.join("\n"));
20
+ if (![
21
+ "text",
22
+ "proposals",
23
+ "error",
24
+ "done",
25
+ "debug"
26
+ ].includes(eventName)) {
27
+ return null;
28
+ }
29
+ return {
30
+ data,
31
+ event: eventName
32
+ };
33
+ } catch {
34
+ return null;
35
+ }
36
+ };
37
+ const getDebugReasonLabel = (reason)=>{
38
+ switch(reason){
39
+ case "model_did_not_call_tool":
40
+ return "Model did not create a proposal tool call.";
41
+ case "proposal_created":
42
+ return "Proposal created.";
43
+ case "tool_validation_failed":
44
+ return "Tool validation failed before a proposal could be created.";
45
+ case "write_intent_without_tool_call":
46
+ return "The selected model did not produce the required proposal tool call for this content change.";
47
+ default:
48
+ return "Unknown";
49
+ }
50
+ };
51
+ const getChatDebugMessage = (debugInfo)=>{
52
+ if (debugInfo.toolFailures?.length) {
53
+ return debugInfo.toolFailures[0]?.message || getDebugReasonLabel(debugInfo.reason);
54
+ }
55
+ return getDebugReasonLabel(debugInfo.reason);
56
+ };
57
+ export const useAIChatStream = ({ apiRoute, clearInput, documentScope, mentionsRef, prompt, selectedModel, selectedProvider })=>{
58
+ const [response, setResponse] = useState("");
59
+ const [tokenUsage, setTokenUsage] = useState(null);
60
+ const [error, setError] = useState("");
61
+ const [proposals, setProposals] = useState([]);
62
+ const [isLoading, setIsLoading] = useState(false);
63
+ const resetChatState = useCallback(()=>{
64
+ setError("");
65
+ setProposals([]);
66
+ setResponse("");
67
+ setTokenUsage(null);
68
+ }, []);
69
+ const dismissChat = useCallback(()=>{
70
+ resetChatState();
71
+ clearInput();
72
+ }, [
73
+ clearInput,
74
+ resetChatState
75
+ ]);
76
+ const submit = useCallback(async ({ attachments = [] } = {})=>{
77
+ const trimmedPrompt = prompt.trim();
78
+ if (!trimmedPrompt) return;
79
+ resetChatState();
80
+ try {
81
+ const res = await fetch(formatAdminURL({
82
+ apiRoute,
83
+ path: "/ai-chat"
84
+ }), {
85
+ body: JSON.stringify({
86
+ ...attachments.length > 0 ? {
87
+ attachments
88
+ } : {},
89
+ ...documentScope ? {
90
+ documentScope
91
+ } : {},
92
+ mentions: mentionsRef.current,
93
+ model: selectedModel,
94
+ prompt: trimmedPrompt,
95
+ ...selectedProvider ? {
96
+ provider: selectedProvider
97
+ } : {}
98
+ }),
99
+ headers: {
100
+ "Content-Type": "application/json"
101
+ },
102
+ method: "POST"
103
+ });
104
+ setIsLoading(false);
105
+ if (!res.ok) {
106
+ const result = await res.json().catch(()=>null);
107
+ throw new Error(result?.error || "AI request failed");
108
+ }
109
+ if (!res.body) {
110
+ throw new Error("AI response stream is unavailable");
111
+ }
112
+ const reader = res.body.getReader();
113
+ const decoder = new TextDecoder();
114
+ let buffer = "";
115
+ let finalDebugInfo = null;
116
+ let receivedProposals = [];
117
+ let receivedText = "";
118
+ let receivedVisibleText = "";
119
+ const handleEvent = (event)=>{
120
+ if (event.event === "text") {
121
+ if (!event.data.delta) return;
122
+ const nextDelta = event.data.delta.replace(/\*\*/g, "");
123
+ receivedText += nextDelta;
124
+ receivedVisibleText += nextDelta.replace(/\s+/g, "");
125
+ setResponse((current)=>current + nextDelta);
126
+ return;
127
+ }
128
+ if (event.event === "proposals") {
129
+ const incoming = event.data.proposals || [];
130
+ const grouped = new Map();
131
+ [
132
+ ...receivedProposals,
133
+ ...incoming
134
+ ].forEach((p)=>{
135
+ const key = [
136
+ p.action,
137
+ p.collection ?? "",
138
+ p.slug ?? "",
139
+ p.id ?? "",
140
+ p.label
141
+ ].join("|");
142
+ if (!grouped.has(key)) {
143
+ grouped.set(key, p);
144
+ }
145
+ });
146
+ receivedProposals = Array.from(grouped.values());
147
+ setProposals(receivedProposals);
148
+ setTokenUsage(event.data.usage ?? null);
149
+ return;
150
+ }
151
+ if (event.event === "debug") {
152
+ finalDebugInfo = event.data;
153
+ if ((event.data.proposalCount || 0) === 0 && !receivedVisibleText) {
154
+ if (event.data.reason === "tool_validation_failed") {
155
+ setResponse(getChatDebugMessage(event.data));
156
+ window.setTimeout(()=>setResponse(""), responseOnlyToastCooldownMs);
157
+ } else {
158
+ setResponse("No action needed");
159
+ }
160
+ }
161
+ return;
162
+ }
163
+ if (event.event === "error") {
164
+ throw new Error(event.data.error || "AI request failed");
165
+ }
166
+ };
167
+ while(true){
168
+ const { done, value } = await reader.read();
169
+ if (done) break;
170
+ buffer += decoder.decode(value, {
171
+ stream: true
172
+ });
173
+ const chunks = buffer.split("\n\n");
174
+ buffer = chunks.pop() || "";
175
+ for (const chunk of chunks){
176
+ const event = parseSSEEvent(chunk);
177
+ if (event) handleEvent(event);
178
+ }
179
+ }
180
+ const finalEvent = buffer.trim() ? parseSSEEvent(buffer.trim()) : null;
181
+ if (finalEvent) handleEvent(finalEvent);
182
+ if (receivedProposals.length === 0) {
183
+ if (finalDebugInfo) {
184
+ const debugMessage = getChatDebugMessage(finalDebugInfo);
185
+ const isMeaningfulVisibleText = receivedVisibleText.length >= 12;
186
+ const trimmedReceivedText = receivedText.trim();
187
+ setResponse((current)=>!isMeaningfulVisibleText || trimmedReceivedText.length < 12 ? debugMessage : current.trim() || debugMessage);
188
+ } else {
189
+ setResponse("No action needed");
190
+ }
191
+ clearInput();
192
+ }
193
+ } catch (err) {
194
+ setProposals([]);
195
+ setResponse("");
196
+ setTokenUsage(null);
197
+ setError(err instanceof Error ? err.message : "AI request failed");
198
+ } finally{
199
+ setIsLoading(false);
200
+ }
201
+ }, [
202
+ apiRoute,
203
+ clearInput,
204
+ documentScope,
205
+ mentionsRef,
206
+ prompt,
207
+ resetChatState,
208
+ selectedModel,
209
+ selectedProvider
210
+ ]);
211
+ return {
212
+ dismissChat,
213
+ error,
214
+ isLoading,
215
+ setIsLoading,
216
+ proposals,
217
+ resetChatState,
218
+ response,
219
+ setError,
220
+ setProposals,
221
+ setResponse,
222
+ setTokenUsage,
223
+ submit,
224
+ tokenUsage
225
+ };
226
+ };
@@ -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,60 +12,101 @@ const storeModel = (provider, model)=>{
12
12
  if (typeof window === "undefined") return;
13
13
  window.localStorage.setItem(getStoredModelKey(provider), model);
14
14
  };
15
- export const useAISettings = ({ adminUserSlug, apiRoute, defaultModels })=>{
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
+ };
28
+ const fetchCurrentUserProvider = async ({ adminUserSlug, apiRoute, signal })=>{
29
+ const res = await fetch(formatAdminURL({
30
+ apiRoute,
31
+ path: `/${adminUserSlug}/me`
32
+ }), {
33
+ signal
34
+ });
35
+ if (!res.ok) return null;
36
+ const result = await res.json();
37
+ return result.user?.aiProvider || null;
38
+ };
39
+ const hasModel = (profile, model)=>profile.models.some((option)=>option.value === model);
40
+ export const useAISettings = ({ adminUserSlug, apiRoute, managedProviders, providerProfiles })=>{
16
41
  const [settingsProvider, setSettingsProvider] = useState(null);
17
- const [selectedModel, setSelectedModel] = useState("");
18
- const setStoredSelectedModel = (model)=>{
19
- setSelectedModel(model);
20
- if (settingsProvider && model) {
21
- storeModel(settingsProvider, model);
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);
22
55
  }
23
56
  };
57
+ const setSelectedModel = (model)=>{
58
+ if (settingsProvider) setSelectedProviderModel(settingsProvider, model);
59
+ };
24
60
  useEffect(()=>{
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
+ }
25
70
  if (!adminUserSlug) {
26
71
  setSettingsProvider(null);
27
- setSelectedModel("");
72
+ setSelectedModelState("");
28
73
  return;
29
74
  }
30
75
  const abortController = new AbortController();
31
- const fetchCurrentUser = async ()=>{
76
+ const loadCurrentUserProvider = async ()=>{
32
77
  try {
33
- const res = await fetch(formatAdminURL({
78
+ const provider = await fetchCurrentUserProvider({
79
+ adminUserSlug,
34
80
  apiRoute,
35
- path: `/${adminUserSlug}/me`
36
- }), {
37
81
  signal: abortController.signal
38
82
  });
39
- if (!res.ok) {
40
- setSettingsProvider(null);
41
- setSelectedModel("");
42
- return;
43
- }
44
- const result = await res.json();
45
- const provider = result.user?.aiProvider;
46
- if (!provider || !isAIProvider(provider)) {
83
+ const profile = providerProfiles.find((candidate)=>candidate.id === provider);
84
+ if (!profile) {
47
85
  setSettingsProvider(null);
48
- setSelectedModel("");
86
+ setSelectedModelState("");
49
87
  return;
50
88
  }
51
- setSettingsProvider(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
94
  setSettingsProvider(null);
56
- setSelectedModel("");
95
+ setSelectedModelState("");
57
96
  }
58
97
  };
59
- void fetchCurrentUser();
98
+ void loadCurrentUserProvider();
60
99
  return ()=>abortController.abort();
61
100
  }, [
62
101
  adminUserSlug,
63
102
  apiRoute,
64
- defaultModels
103
+ managedProviders,
104
+ providerProfiles
65
105
  ]);
66
106
  return {
67
107
  selectedModel,
68
- setSelectedModel: setStoredSelectedModel,
108
+ setSelectedModel,
109
+ setSelectedProviderModel,
69
110
  settingsProvider
70
111
  };
71
112
  };
@@ -0,0 +1,11 @@
1
+ import type { AppliedChange } from "../audit-log-list/AuditLogList.js";
2
+ export declare const useAuditLog: ({ adminRoute, apiRoute }: {
3
+ adminRoute?: string;
4
+ apiRoute: string;
5
+ }) => {
6
+ allChangesURL: string;
7
+ appliedChanges: AppliedChange[];
8
+ loadRecentChanges: () => Promise<void>;
9
+ prependChange: (change: AppliedChange) => void;
10
+ setAppliedChanges: import("react").Dispatch<import("react").SetStateAction<AppliedChange[]>>;
11
+ };
@@ -0,0 +1,41 @@
1
+ import { useCallback, useEffect, useMemo, useState } from "react";
2
+ import { formatAdminURL } from "payload/shared";
3
+ export const useAuditLog = ({ adminRoute, apiRoute })=>{
4
+ const [appliedChanges, setAppliedChanges] = useState([]);
5
+ const recentChangesEndpoint = useMemo(()=>formatAdminURL({
6
+ apiRoute,
7
+ path: "/ai-audit-log"
8
+ }), [
9
+ apiRoute
10
+ ]);
11
+ const allChangesURL = useMemo(()=>`${adminRoute || "/admin"}/collections/$payload-ai-auditlog`, [
12
+ adminRoute
13
+ ]);
14
+ const loadRecentChanges = useCallback(async ()=>{
15
+ const res = await fetch(recentChangesEndpoint);
16
+ const result = await res.json().catch(()=>null);
17
+ if (res.ok && result?.changes) {
18
+ setAppliedChanges(result.changes.slice(0, 8));
19
+ }
20
+ }, [
21
+ recentChangesEndpoint
22
+ ]);
23
+ useEffect(()=>{
24
+ void loadRecentChanges().catch(()=>undefined);
25
+ }, [
26
+ loadRecentChanges
27
+ ]);
28
+ const prependChange = useCallback((change)=>{
29
+ setAppliedChanges((current)=>[
30
+ change,
31
+ ...current
32
+ ].slice(0, 8));
33
+ }, []);
34
+ return {
35
+ allChangesURL,
36
+ appliedChanges,
37
+ loadRecentChanges,
38
+ prependChange,
39
+ setAppliedChanges
40
+ };
41
+ };
@@ -1,4 +1,4 @@
1
- import type { MentionOption } from "../MentionPopover.js";
1
+ import type { MentionOption } from "../mention-popover/MentionPopover.js";
2
2
  type MentionRange = {
3
3
  end: number;
4
4
  start: number;
@@ -2,52 +2,56 @@
2
2
  import { formatAdminURL } from "payload/shared";
3
3
  import { useEffect, useState } from "react";
4
4
  import { isAbortError } from "../../payload/shared.js";
5
+ const fetchDocumentMentionSuggestions = async ({ apiRoute, collectionSlug, query, signal })=>{
6
+ const res = await fetch(formatAdminURL({
7
+ apiRoute,
8
+ path: "/ai-mention-suggestion"
9
+ }), {
10
+ body: JSON.stringify({
11
+ collectionSlug,
12
+ query
13
+ }),
14
+ headers: {
15
+ "Content-Type": "application/json"
16
+ },
17
+ method: "POST",
18
+ signal
19
+ });
20
+ if (!res.ok) return [];
21
+ const result = await res.json();
22
+ return result.suggestions || [];
23
+ };
5
24
  export const useDocumentMentionSuggestions = ({ apiRoute, documentSuggestionCollection, mentionQuery, mentionRange })=>{
6
25
  const [documentSuggestions, setDocumentSuggestions] = useState([]);
26
+ const trimmedQuery = mentionQuery.trim();
27
+ const shouldLoadSuggestions = Boolean(mentionRange && (trimmedQuery || documentSuggestionCollection));
7
28
  useEffect(()=>{
8
- const trimmedQuery = mentionQuery.trim();
9
- if (!mentionRange || !trimmedQuery && !documentSuggestionCollection) {
10
- setDocumentSuggestions([]);
11
- return;
12
- }
29
+ if (!shouldLoadSuggestions) return;
13
30
  const abortController = new AbortController();
14
- const fetchDocumentSuggestions = async ()=>{
31
+ const loadDocumentSuggestions = async ()=>{
15
32
  try {
16
- const res = await fetch(formatAdminURL({
33
+ const suggestions = await fetchDocumentMentionSuggestions({
17
34
  apiRoute,
18
- path: "/ai-mention-suggestion"
19
- }), {
20
- body: JSON.stringify({
21
- collectionSlug: documentSuggestionCollection,
22
- query: documentSuggestionCollection ? "" : trimmedQuery
23
- }),
24
- headers: {
25
- "Content-Type": "application/json"
26
- },
27
- method: "POST",
35
+ collectionSlug: documentSuggestionCollection,
36
+ query: documentSuggestionCollection ? "" : trimmedQuery,
28
37
  signal: abortController.signal
29
38
  });
30
- if (!res.ok) {
31
- setDocumentSuggestions([]);
32
- return;
33
- }
34
- const result = await res.json();
35
- setDocumentSuggestions(result.suggestions || []);
39
+ setDocumentSuggestions(suggestions);
36
40
  } catch (err) {
37
41
  if (isAbortError(err)) return;
38
42
  setDocumentSuggestions([]);
39
43
  }
40
44
  };
41
- void fetchDocumentSuggestions();
45
+ void loadDocumentSuggestions();
42
46
  return ()=>abortController.abort();
43
47
  }, [
44
48
  apiRoute,
45
49
  documentSuggestionCollection,
46
- mentionQuery,
47
- mentionRange
50
+ shouldLoadSuggestions,
51
+ trimmedQuery
48
52
  ]);
49
53
  return {
50
- documentSuggestions,
54
+ documentSuggestions: shouldLoadSuggestions ? documentSuggestions : [],
51
55
  resetDocumentSuggestions: ()=>setDocumentSuggestions([])
52
56
  };
53
57
  };
@@ -0,0 +1,58 @@
1
+ import { type RefObject } from "react";
2
+ import type { MentionOption } from "../mention-popover/MentionPopover.js";
3
+ export type Mention = {
4
+ collection?: string;
5
+ id?: string;
6
+ isDefault?: boolean;
7
+ label: string;
8
+ parent?: string;
9
+ slug: string;
10
+ type: "collection" | "doc" | "global" | "locale";
11
+ };
12
+ type MentionRange = {
13
+ end: number;
14
+ start: number;
15
+ };
16
+ type LocaleConfig = string | {
17
+ code?: string;
18
+ label?: unknown;
19
+ };
20
+ type UseMentionPopoverArgs = {
21
+ apiRoute: string;
22
+ config: {
23
+ collections: Array<{
24
+ fields: unknown[];
25
+ labels?: {
26
+ singular?: unknown;
27
+ };
28
+ slug: string;
29
+ }>;
30
+ globals?: Array<{
31
+ fields: unknown[];
32
+ label?: unknown;
33
+ slug: string;
34
+ }>;
35
+ };
36
+ defaultLocale?: string;
37
+ editorRef: RefObject<HTMLDivElement | null>;
38
+ isCollectionMentionEnabled: (slug: string) => boolean;
39
+ locales: LocaleConfig[];
40
+ setPrompt: (value: string) => void;
41
+ styles: Record<string, string>;
42
+ };
43
+ export declare const getTextBeforeCaret: (element: HTMLElement) => string;
44
+ export declare const useMentions: ({ apiRoute, config, defaultLocale, editorRef, isCollectionMentionEnabled, locales, setPrompt, styles }: UseMentionPopoverArgs) => {
45
+ clearMentions: () => void;
46
+ getTextBeforeCaret: (element: HTMLElement) => string;
47
+ insertMention: (suggestion: MentionOption) => void;
48
+ mentionPopoverPosition: {
49
+ left: number;
50
+ top: number;
51
+ } | null;
52
+ mentionQuery: string;
53
+ mentionRange: MentionRange | null;
54
+ mentionSuggestions: MentionOption[];
55
+ mentionsRef: RefObject<Mention[]>;
56
+ updateMentionState: (valueBeforeCaret: string) => void;
57
+ };
58
+ export {};