返回 presentation-ai
slash-node.tsx
根目录 / src / components / plate / ui / slash-node.tsx
1 "use client";
2
3 import { AIChatPlugin } from "@platejs/ai/react";
4 import {
5 AlertCircleIcon,
6 CalendarIcon,
7 CheckCircleIcon,
8 ChevronRightIcon,
9 Code2,
10 Columns3Icon,
11 FileTextIcon,
12 Heading1Icon,
13 Heading2Icon,
14 Heading3Icon,
15 HelpCircleIcon,
16 InfoIcon,
17 LightbulbIcon,
18 ListIcon,
19 ListOrdered,
20 PilcrowIcon,
21 Quote,
22 RadicalIcon,
23 SparklesIcon,
24 Square,
25 Table,
26 TableOfContentsIcon,
27 XCircleIcon,
28 } from "lucide-react";
29 import { KEYS, type TComboboxInputElement, type TElement } from "platejs";
30 import {
31 PlateElement,
32 type PlateEditor,
33 type PlateElementProps,
34 } from "platejs/react";
35 import type * as React from "react";
36
37 import { PRESENTATION_TITLE_ELEMENT } from "@/components/notebook/presentation/editor/lib";
38 import {
39 insertBlock,
40 insertInlineElement,
41 } from "@/components/plate/utils/transforms";
42 import { type CalloutVariant } from "./callout-variants";
43 import {
44 InlineCombobox,
45 InlineComboboxContent,
46 InlineComboboxEmpty,
47 InlineComboboxGroup,
48 InlineComboboxGroupLabel,
49 InlineComboboxInput,
50 InlineComboboxItem,
51 } from "./inline-combobox";
52
53 type Group = {
54 group: string;
55 items: Item[];
56 };
57
58 interface Item {
59 icon: React.ReactNode;
60 value: string;
61 onSelect: (editor: PlateEditor, value: string) => void;
62 className?: string;
63 focusEditor?: boolean;
64 keywords?: string[];
65 label?: string;
66 nodeType?: string;
67 props?: Partial<TElement>;
68 }
69
70 type CalloutSlashItem = Omit<Item, "onSelect">;
71
72 const calloutItems: Item[] = (
73 [
74 {
75 icon: <FileTextIcon />,
76 keywords: ["callout", "note"],
77 label: "Note box",
78 props: { variant: "note" satisfies CalloutVariant },
79 value: "callout-note",
80 },
81 {
82 icon: <InfoIcon />,
83 keywords: ["callout", "info"],
84 label: "Info box",
85 props: { variant: "info" satisfies CalloutVariant },
86 value: "callout-info",
87 },
88 {
89 icon: <AlertCircleIcon />,
90 keywords: ["callout", "warning", "alert"],
91 label: "Warning box",
92 props: { variant: "warning" satisfies CalloutVariant },
93 value: "callout-warning",
94 },
95 {
96 icon: <XCircleIcon />,
97 keywords: ["callout", "caution", "danger"],
98 label: "Caution box",
99 props: { variant: "caution" satisfies CalloutVariant },
100 value: "callout-caution",
101 },
102 {
103 icon: <CheckCircleIcon />,
104 keywords: ["callout", "success", "done"],
105 label: "Success box",
106 props: { variant: "success" satisfies CalloutVariant },
107 value: "callout-success",
108 },
109 {
110 icon: <HelpCircleIcon />,
111 keywords: ["callout", "question", "help"],
112 label: "Question box",
113 props: { variant: "question" satisfies CalloutVariant },
114 value: "callout-question",
115 },
116 ] satisfies CalloutSlashItem[]
117 ).map((item) => ({
118 ...item,
119 nodeType: KEYS.callout,
120 onSelect: (editor) => {
121 insertBlock(editor, KEYS.callout, {
122 props: item.props,
123 });
124 },
125 }));
126
127 const groups: Group[] = [
128 {
129 group: "AI",
130 items: [
131 {
132 focusEditor: false,
133 icon: <SparklesIcon />,
134 value: "AI",
135 onSelect: (editor) => {
136 editor.getApi(AIChatPlugin).aiChat.show();
137 },
138 },
139 ],
140 },
141 {
142 group: "Basic blocks",
143 items: [
144 {
145 icon: <Heading1Icon />,
146 keywords: ["title", "!"],
147 label: "Title",
148 props: { variant: "title" },
149 value: PRESENTATION_TITLE_ELEMENT,
150 },
151 {
152 icon: <Heading2Icon />,
153 keywords: ["display", "!!"],
154 label: "Display",
155 props: { variant: "display" },
156 value: "presentation-title-display",
157 nodeType: PRESENTATION_TITLE_ELEMENT,
158 },
159 {
160 icon: <Heading3Icon />,
161 keywords: ["humongous", "!!!"],
162 label: "Humongous",
163 props: { variant: "humongous" },
164 value: "presentation-title-humongous",
165 nodeType: PRESENTATION_TITLE_ELEMENT,
166 },
167 {
168 icon: <PilcrowIcon />,
169 keywords: ["paragraph"],
170 label: "Text",
171 value: KEYS.p,
172 },
173 {
174 icon: <Heading1Icon />,
175 keywords: ["title", "h1"],
176 label: "Heading 1",
177 value: KEYS.h1,
178 },
179 {
180 icon: <Heading2Icon />,
181 keywords: ["subtitle", "h2"],
182 label: "Heading 2",
183 value: KEYS.h2,
184 },
185 {
186 icon: <Heading3Icon />,
187 keywords: ["subtitle", "h3"],
188 label: "Heading 3",
189 value: KEYS.h3,
190 },
191 {
192 icon: <ListIcon />,
193 keywords: ["unordered", "ul", "-"],
194 label: "Bulleted list",
195 value: KEYS.ul,
196 },
197 {
198 icon: <ListOrdered />,
199 keywords: ["ordered", "ol", "1"],
200 label: "Numbered list",
201 value: KEYS.ol,
202 },
203 {
204 icon: <Square />,
205 keywords: ["checklist", "task", "checkbox", "[]"],
206 label: "To-do list",
207 value: KEYS.listTodo,
208 },
209 {
210 icon: <ChevronRightIcon />,
211 keywords: ["collapsible", "expandable"],
212 label: "Toggle",
213 value: KEYS.toggle,
214 },
215 {
216 icon: <Code2 />,
217 keywords: ["```"],
218 label: "Code Block",
219 value: KEYS.codeBlock,
220 },
221 {
222 icon: <Table />,
223 label: "Table",
224 value: KEYS.table,
225 },
226 {
227 icon: <Quote />,
228 keywords: ["citation", "blockquote", "quote", ">"],
229 label: "Blockquote",
230 value: KEYS.blockquote,
231 },
232 {
233 description: "Insert a highlighted block.",
234 icon: <LightbulbIcon />,
235 keywords: ["note"],
236 label: "Callout",
237 value: KEYS.callout,
238 },
239 ].map((item) => ({
240 ...item,
241 onSelect: (editor) => {
242 insertBlock(editor, item.nodeType ?? item.value, {
243 props: item.props,
244 });
245 },
246 })),
247 },
248 {
249 group: "Callout boxes",
250 items: calloutItems,
251 },
252 {
253 group: "Advanced blocks",
254 items: [
255 {
256 icon: <TableOfContentsIcon />,
257 keywords: ["toc"],
258 label: "Table of contents",
259 value: KEYS.toc,
260 },
261 {
262 icon: <Columns3Icon />,
263 label: "3 columns",
264 value: "action_three_columns",
265 },
266 {
267 focusEditor: false,
268 icon: <RadicalIcon />,
269 label: "Equation",
270 value: KEYS.equation,
271 },
272 ].map((item) => ({
273 ...item,
274 onSelect: (editor, value) => {
275 insertBlock(editor, value);
276 },
277 })),
278 },
279 {
280 group: "Inline",
281 items: [
282 {
283 focusEditor: true,
284 icon: <CalendarIcon />,
285 keywords: ["time"],
286 label: "Date",
287 value: KEYS.date,
288 },
289 {
290 focusEditor: false,
291 icon: <RadicalIcon />,
292 label: "Inline Equation",
293 value: KEYS.inlineEquation,
294 },
295 ].map((item) => ({
296 ...item,
297 onSelect: (editor, value) => {
298 insertInlineElement(editor, value);
299 },
300 })),
301 },
302 ];
303
304 export function SlashInputElement(
305 props: PlateElementProps<TComboboxInputElement>,
306 ) {
307 const { editor, element } = props;
308
309 return (
310 <PlateElement {...props} as="span" data-slate-value={element.value}>
311 <InlineCombobox element={element} trigger="/">
312 <InlineComboboxInput />
313
314 <InlineComboboxContent>
315 <InlineComboboxEmpty>No results</InlineComboboxEmpty>
316
317 {groups.map(({ group, items }) => (
318 <InlineComboboxGroup key={group}>
319 <InlineComboboxGroupLabel>{group}</InlineComboboxGroupLabel>
320
321 {items.map(
322 ({ focusEditor, icon, keywords, label, value, onSelect }) => (
323 <InlineComboboxItem
324 key={value}
325 value={value}
326 onClick={() => onSelect(editor, value)}
327 label={label}
328 focusEditor={focusEditor}
329 group={group}
330 keywords={keywords}
331 >
332 <div className="mr-2 text-muted-foreground">{icon}</div>
333 {label ?? value}
334 </InlineComboboxItem>
335 ),
336 )}
337 </InlineComboboxGroup>
338 ))}
339 </InlineComboboxContent>
340 </InlineCombobox>
341
342 {props.children}
343 </PlateElement>
344 );
345 }
346
346 lines Plain Text