@mvriu5/payload-ai 1.2.0 → 1.3.2
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 +42 -0
- package/dist/components/Icons.d.ts +0 -8
- package/dist/components/Icons.js +4 -216
- package/dist/components/{ActionToast.d.ts → action-toast/ActionToast.d.ts} +1 -2
- package/dist/components/{ActionToast.js → action-toast/ActionToast.js} +25 -34
- package/dist/components/{ActionToast.module.css → action-toast/ActionToast.module.css} +0 -79
- package/dist/components/ai-input/AIInput.js +433 -0
- package/dist/components/{AIInput.module.css → ai-input/AIInput.module.css} +111 -43
- package/dist/components/ai-input/badge.d.ts +14 -0
- package/dist/components/ai-input/badge.js +85 -0
- package/dist/components/{AuditLogList.d.ts → audit-log-list/AuditLogList.d.ts} +4 -8
- package/dist/components/{AuditLogList.js → audit-log-list/AuditLogList.js} +49 -21
- package/dist/components/{AuditLogList.module.css → audit-log-list/AuditLogList.module.css} +11 -65
- package/dist/components/dashboard/Dashboard.d.ts +2 -0
- package/dist/components/dashboard/Dashboard.js +15 -0
- package/dist/components/dashboard/Dashboard.module.css +13 -0
- package/dist/components/{DiffDialog.d.ts → diff-dialog/DiffDialog.d.ts} +2 -2
- package/dist/components/{DiffDialog.js → diff-dialog/DiffDialog.js} +200 -189
- package/dist/components/{DiffDialog.module.css → diff-dialog/DiffDialog.module.css} +17 -35
- package/dist/components/hooks/useAIChatStream.d.ts +39 -0
- package/dist/components/hooks/useAIChatStream.js +217 -0
- package/dist/components/hooks/useAISettings.js +26 -25
- package/dist/components/hooks/useAuditLog.d.ts +11 -0
- package/dist/components/hooks/useAuditLog.js +41 -0
- package/dist/components/hooks/useDocumentMentionSuggestions.d.ts +1 -1
- package/dist/components/hooks/useDocumentMentionSuggestions.js +31 -27
- package/dist/components/hooks/useMentions.d.ts +58 -0
- package/dist/components/hooks/useMentions.js +276 -0
- package/dist/components/hooks/usePluginConfig.d.ts +52 -0
- package/dist/components/hooks/usePluginConfig.js +20 -0
- package/dist/components/{MentionPopover.d.ts → mention-popover/MentionPopover.d.ts} +2 -4
- package/dist/components/{MentionPopover.js → mention-popover/MentionPopover.js} +1 -8
- package/dist/components/{MentionPopover.module.css → mention-popover/MentionPopover.module.css} +1 -1
- package/dist/exports/client.d.ts +2 -2
- package/dist/exports/client.js +2 -2
- package/dist/handlers/applyActionHandler.js +27 -7
- package/dist/handlers/chatHandler.js +302 -58
- package/dist/handlers/mediaUploadHandler.d.ts +7 -0
- package/dist/handlers/mediaUploadHandler.js +99 -0
- package/dist/handlers/mentionSuggestionHandler.js +16 -14
- package/dist/handlers/proposalDiffHandler.js +41 -29
- package/dist/index.d.ts +6 -0
- package/dist/index.js +39 -7
- package/dist/payload/collectionPermissions.js +3 -1
- package/dist/payload/normalizeData.d.ts +0 -6
- package/dist/payload/normalizeData.js +25 -13
- package/dist/payload/proposalData.js +4 -1
- package/dist/payload/schemaContext.js +98 -43
- package/package.json +6 -6
- package/dist/components/AIInput.js +0 -896
- /package/dist/components/{AIInput.d.ts → ai-input/AIInput.d.ts} +0 -0
|
@@ -0,0 +1,217 @@
|
|
|
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, mentionsRef, prompt, selectedModel })=>{
|
|
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
|
+
setIsLoading(true);
|
|
80
|
+
resetChatState();
|
|
81
|
+
try {
|
|
82
|
+
const res = await fetch(formatAdminURL({
|
|
83
|
+
apiRoute,
|
|
84
|
+
path: "/ai-chat"
|
|
85
|
+
}), {
|
|
86
|
+
body: JSON.stringify({
|
|
87
|
+
...attachments.length > 0 ? {
|
|
88
|
+
attachments
|
|
89
|
+
} : {},
|
|
90
|
+
mentions: mentionsRef.current,
|
|
91
|
+
model: selectedModel,
|
|
92
|
+
prompt: trimmedPrompt
|
|
93
|
+
}),
|
|
94
|
+
headers: {
|
|
95
|
+
"Content-Type": "application/json"
|
|
96
|
+
},
|
|
97
|
+
method: "POST"
|
|
98
|
+
});
|
|
99
|
+
if (!res.ok) {
|
|
100
|
+
const result = await res.json().catch(()=>null);
|
|
101
|
+
throw new Error(result?.error || "AI request failed");
|
|
102
|
+
}
|
|
103
|
+
if (!res.body) {
|
|
104
|
+
throw new Error("AI response stream is unavailable");
|
|
105
|
+
}
|
|
106
|
+
const reader = res.body.getReader();
|
|
107
|
+
const decoder = new TextDecoder();
|
|
108
|
+
let buffer = "";
|
|
109
|
+
let finalDebugInfo = null;
|
|
110
|
+
let receivedProposals = [];
|
|
111
|
+
let receivedText = "";
|
|
112
|
+
let receivedVisibleText = "";
|
|
113
|
+
const handleEvent = (event)=>{
|
|
114
|
+
if (event.event === "text") {
|
|
115
|
+
if (!event.data.delta) return;
|
|
116
|
+
const nextDelta = event.data.delta.replace(/\*\*/g, "");
|
|
117
|
+
receivedText += nextDelta;
|
|
118
|
+
receivedVisibleText += nextDelta.replace(/\s+/g, "");
|
|
119
|
+
setResponse((current)=>current + nextDelta);
|
|
120
|
+
return;
|
|
121
|
+
}
|
|
122
|
+
if (event.event === "proposals") {
|
|
123
|
+
const incoming = event.data.proposals || [];
|
|
124
|
+
const grouped = new Map();
|
|
125
|
+
[
|
|
126
|
+
...receivedProposals,
|
|
127
|
+
...incoming
|
|
128
|
+
].forEach((p)=>{
|
|
129
|
+
const key = [
|
|
130
|
+
p.action,
|
|
131
|
+
p.collection ?? "",
|
|
132
|
+
p.slug ?? "",
|
|
133
|
+
p.id ?? "",
|
|
134
|
+
p.label
|
|
135
|
+
].join("|");
|
|
136
|
+
if (!grouped.has(key)) {
|
|
137
|
+
grouped.set(key, p);
|
|
138
|
+
}
|
|
139
|
+
});
|
|
140
|
+
receivedProposals = Array.from(grouped.values());
|
|
141
|
+
setProposals(receivedProposals);
|
|
142
|
+
setTokenUsage(event.data.usage ?? null);
|
|
143
|
+
return;
|
|
144
|
+
}
|
|
145
|
+
if (event.event === "debug") {
|
|
146
|
+
finalDebugInfo = event.data;
|
|
147
|
+
if ((event.data.proposalCount || 0) === 0 && !receivedVisibleText) {
|
|
148
|
+
if (event.data.reason === "tool_validation_failed") {
|
|
149
|
+
setResponse(getChatDebugMessage(event.data));
|
|
150
|
+
window.setTimeout(()=>setResponse(""), responseOnlyToastCooldownMs);
|
|
151
|
+
} else {
|
|
152
|
+
setResponse("No action needed");
|
|
153
|
+
}
|
|
154
|
+
}
|
|
155
|
+
return;
|
|
156
|
+
}
|
|
157
|
+
if (event.event === "error") {
|
|
158
|
+
throw new Error(event.data.error || "AI request failed");
|
|
159
|
+
}
|
|
160
|
+
};
|
|
161
|
+
while(true){
|
|
162
|
+
const { done, value } = await reader.read();
|
|
163
|
+
if (done) break;
|
|
164
|
+
buffer += decoder.decode(value, {
|
|
165
|
+
stream: true
|
|
166
|
+
});
|
|
167
|
+
const chunks = buffer.split("\n\n");
|
|
168
|
+
buffer = chunks.pop() || "";
|
|
169
|
+
for (const chunk of chunks){
|
|
170
|
+
const event = parseSSEEvent(chunk);
|
|
171
|
+
if (event) handleEvent(event);
|
|
172
|
+
}
|
|
173
|
+
}
|
|
174
|
+
const finalEvent = buffer.trim() ? parseSSEEvent(buffer.trim()) : null;
|
|
175
|
+
if (finalEvent) handleEvent(finalEvent);
|
|
176
|
+
if (receivedProposals.length === 0) {
|
|
177
|
+
if (finalDebugInfo) {
|
|
178
|
+
const debugMessage = getChatDebugMessage(finalDebugInfo);
|
|
179
|
+
const isMeaningfulVisibleText = receivedVisibleText.length >= 12;
|
|
180
|
+
const trimmedReceivedText = receivedText.trim();
|
|
181
|
+
setResponse((current)=>!isMeaningfulVisibleText || trimmedReceivedText.length < 12 ? debugMessage : current.trim() || debugMessage);
|
|
182
|
+
} else {
|
|
183
|
+
setResponse("No action needed");
|
|
184
|
+
}
|
|
185
|
+
clearInput();
|
|
186
|
+
}
|
|
187
|
+
} catch (err) {
|
|
188
|
+
setProposals([]);
|
|
189
|
+
setResponse("");
|
|
190
|
+
setTokenUsage(null);
|
|
191
|
+
setError(err instanceof Error ? err.message : "AI request failed");
|
|
192
|
+
} finally{
|
|
193
|
+
setIsLoading(false);
|
|
194
|
+
}
|
|
195
|
+
}, [
|
|
196
|
+
apiRoute,
|
|
197
|
+
clearInput,
|
|
198
|
+
mentionsRef,
|
|
199
|
+
prompt,
|
|
200
|
+
resetChatState,
|
|
201
|
+
selectedModel
|
|
202
|
+
]);
|
|
203
|
+
return {
|
|
204
|
+
dismissChat,
|
|
205
|
+
error,
|
|
206
|
+
isLoading,
|
|
207
|
+
proposals,
|
|
208
|
+
resetChatState,
|
|
209
|
+
response,
|
|
210
|
+
setError,
|
|
211
|
+
setProposals,
|
|
212
|
+
setResponse,
|
|
213
|
+
setTokenUsage,
|
|
214
|
+
submit,
|
|
215
|
+
tokenUsage
|
|
216
|
+
};
|
|
217
|
+
};
|
|
@@ -12,59 +12,60 @@ const storeModel = (provider, model)=>{
|
|
|
12
12
|
if (typeof window === "undefined") return;
|
|
13
13
|
window.localStorage.setItem(getStoredModelKey(provider), model);
|
|
14
14
|
};
|
|
15
|
+
const fetchCurrentUserProvider = async ({ adminUserSlug, apiRoute, signal })=>{
|
|
16
|
+
const res = await fetch(formatAdminURL({
|
|
17
|
+
apiRoute,
|
|
18
|
+
path: `/${adminUserSlug}/me`
|
|
19
|
+
}), {
|
|
20
|
+
signal
|
|
21
|
+
});
|
|
22
|
+
if (!res.ok) return null;
|
|
23
|
+
const result = await res.json();
|
|
24
|
+
const provider = result.user?.aiProvider;
|
|
25
|
+
return provider && isAIProvider(provider) ? provider : null;
|
|
26
|
+
};
|
|
15
27
|
export const useAISettings = ({ adminUserSlug, apiRoute, defaultModels })=>{
|
|
16
|
-
const [
|
|
28
|
+
const [fetchedProvider, setFetchedProvider] = useState(null);
|
|
17
29
|
const [selectedModel, setSelectedModel] = useState("");
|
|
18
30
|
const setStoredSelectedModel = (model)=>{
|
|
19
31
|
setSelectedModel(model);
|
|
20
|
-
if (
|
|
21
|
-
storeModel(
|
|
32
|
+
if (fetchedProvider && model) {
|
|
33
|
+
storeModel(fetchedProvider, model);
|
|
22
34
|
}
|
|
23
35
|
};
|
|
24
36
|
useEffect(()=>{
|
|
25
|
-
if (!adminUserSlug)
|
|
26
|
-
setSettingsProvider(null);
|
|
27
|
-
setSelectedModel("");
|
|
28
|
-
return;
|
|
29
|
-
}
|
|
37
|
+
if (!adminUserSlug) return;
|
|
30
38
|
const abortController = new AbortController();
|
|
31
|
-
const
|
|
39
|
+
const loadCurrentUserProvider = async ()=>{
|
|
32
40
|
try {
|
|
33
|
-
const
|
|
41
|
+
const provider = await fetchCurrentUserProvider({
|
|
42
|
+
adminUserSlug,
|
|
34
43
|
apiRoute,
|
|
35
|
-
path: `/${adminUserSlug}/me`
|
|
36
|
-
}), {
|
|
37
44
|
signal: abortController.signal
|
|
38
45
|
});
|
|
39
|
-
if (!
|
|
40
|
-
|
|
41
|
-
setSelectedModel("");
|
|
42
|
-
return;
|
|
43
|
-
}
|
|
44
|
-
const result = await res.json();
|
|
45
|
-
const provider = result.user?.aiProvider;
|
|
46
|
-
if (!provider || !isAIProvider(provider)) {
|
|
47
|
-
setSettingsProvider(null);
|
|
46
|
+
if (!provider) {
|
|
47
|
+
setFetchedProvider(null);
|
|
48
48
|
setSelectedModel("");
|
|
49
49
|
return;
|
|
50
50
|
}
|
|
51
|
-
|
|
51
|
+
setFetchedProvider(provider);
|
|
52
52
|
setSelectedModel(getStoredModel(provider) || defaultModels[provider]);
|
|
53
53
|
} catch (err) {
|
|
54
54
|
if (isAbortError(err)) return;
|
|
55
|
-
|
|
55
|
+
setFetchedProvider(null);
|
|
56
56
|
setSelectedModel("");
|
|
57
57
|
}
|
|
58
58
|
};
|
|
59
|
-
void
|
|
59
|
+
void loadCurrentUserProvider();
|
|
60
60
|
return ()=>abortController.abort();
|
|
61
61
|
}, [
|
|
62
62
|
adminUserSlug,
|
|
63
63
|
apiRoute,
|
|
64
64
|
defaultModels
|
|
65
65
|
]);
|
|
66
|
+
const settingsProvider = adminUserSlug ? fetchedProvider : null;
|
|
66
67
|
return {
|
|
67
|
-
selectedModel,
|
|
68
|
+
selectedModel: adminUserSlug ? selectedModel : "",
|
|
68
69
|
setSelectedModel: setStoredSelectedModel,
|
|
69
70
|
settingsProvider
|
|
70
71
|
};
|
|
@@ -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
|
+
};
|
|
@@ -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
|
-
|
|
9
|
-
if (!mentionRange || !trimmedQuery && !documentSuggestionCollection) {
|
|
10
|
-
setDocumentSuggestions([]);
|
|
11
|
-
return;
|
|
12
|
-
}
|
|
29
|
+
if (!shouldLoadSuggestions) return;
|
|
13
30
|
const abortController = new AbortController();
|
|
14
|
-
const
|
|
31
|
+
const loadDocumentSuggestions = async ()=>{
|
|
15
32
|
try {
|
|
16
|
-
const
|
|
33
|
+
const suggestions = await fetchDocumentMentionSuggestions({
|
|
17
34
|
apiRoute,
|
|
18
|
-
|
|
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
|
-
|
|
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
|
|
45
|
+
void loadDocumentSuggestions();
|
|
42
46
|
return ()=>abortController.abort();
|
|
43
47
|
}, [
|
|
44
48
|
apiRoute,
|
|
45
49
|
documentSuggestionCollection,
|
|
46
|
-
|
|
47
|
-
|
|
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 {};
|