返回 presentation-ai
suggestion-node.tsx
根目录 / src / components / plate / ui / suggestion-node.tsx
1 "use client";
2
3 import {
4 BaseSuggestionPlugin,
5 type BaseSuggestionConfig,
6 } from "@platejs/suggestion";
7 import { cva } from "class-variance-authority";
8 import { CornerDownLeftIcon } from "lucide-react";
9 import {
10 type ExtendConfig,
11 type Path,
12 type TSuggestionData,
13 type TSuggestionText,
14 } from "platejs";
15 import {
16 PlateLeaf,
17 useEditorPlugin,
18 usePluginOption,
19 type PlateLeafProps,
20 type PlatePlugin,
21 type RenderNodeWrapper,
22 } from "platejs/react";
23 import * as React from "react";
24
25 import { type SuggestionConfig } from "@/components/plate/plugins/suggestion-kit";
26 import { cn } from "@/lib/utils";
27
28 // Use BaseSuggestionPlugin cast to SuggestionConfig to avoid circular dependency
29 // with suggestion-kit.tsx. Plugins are resolved by key at runtime, so this is safe.
30 const suggestionPlugin =
31 BaseSuggestionPlugin as unknown as PlatePlugin<SuggestionConfig>;
32
33 type SuggestionRenderConfig = ExtendConfig<
34 BaseSuggestionConfig,
35 Partial<{
36 activeId: string | null;
37 hoverId: string | null;
38 uniquePathMap: Map<string, Path>;
39 }>
40 >;
41
42 const suggestionVariants = cva(
43 cn(
44 "bg-emerald-100 text-emerald-700 no-underline transition-colors duration-200",
45 ),
46 {
47 defaultVariants: {
48 insertActive: false,
49 remove: false,
50 removeActive: false,
51 },
52 variants: {
53 insertActive: {
54 false: "",
55 true: "bg-emerald-200/80",
56 },
57 remove: {
58 false: "",
59 true: "bg-red-100 text-red-700",
60 },
61 removeActive: {
62 false: "",
63 true: "bg-red-200/80 no-underline",
64 },
65 },
66 },
67 );
68
69 export function SuggestionLeaf(props: PlateLeafProps<TSuggestionText>) {
70 const { api, setOption } = useEditorPlugin(suggestionPlugin);
71 const leaf = props.leaf;
72
73 const leafId: string = api.suggestion.nodeId(leaf) ?? "";
74 const activeSuggestionId = usePluginOption(suggestionPlugin, "activeId");
75 const hoverSuggestionId = usePluginOption(suggestionPlugin, "hoverId");
76 const dataList = api.suggestion.dataList(leaf);
77
78 const hasRemove = dataList.some((data) => data.type === "remove");
79 const hasActive = dataList.some((data) => data.id === activeSuggestionId);
80 const hasHover = dataList.some((data) => data.id === hoverSuggestionId);
81
82 const diffOperation = { type: hasRemove ? "delete" : "insert" } as const;
83
84 const Component = ({ delete: "del", insert: "ins", update: "span" } as const)[
85 diffOperation.type
86 ];
87
88 return (
89 <PlateLeaf
90 {...props}
91 as={Component}
92 className={cn(
93 suggestionVariants({
94 insertActive: hasActive || hasHover,
95 remove: hasRemove,
96 removeActive: (hasActive || hasHover) && hasRemove,
97 }),
98 )}
99 attributes={{
100 ...props.attributes,
101 onMouseEnter: () => setOption("hoverId", leafId),
102 onMouseLeave: () => setOption("hoverId", null),
103 }}
104 >
105 {props.children}
106 </PlateLeaf>
107 );
108 }
109 export const SuggestionLineBreak: RenderNodeWrapper<SuggestionRenderConfig> = ({
110 api,
111 element,
112 }) => {
113 if (!api.suggestion.isBlockSuggestion(element)) return;
114
115 const suggestionData = element.suggestion as TSuggestionData;
116
117 return function Component({ children }) {
118 return (
119 <SuggestionLineBreakContent suggestionData={suggestionData}>
120 {children}
121 </SuggestionLineBreakContent>
122 );
123 };
124 };
125
126 function SuggestionLineBreakContent({
127 children,
128 suggestionData,
129 }: {
130 children: React.ReactNode;
131 suggestionData: TSuggestionData;
132 }) {
133 const { isLineBreak, type } = suggestionData;
134 const isRemove = type === "remove";
135 const isInsert = type === "insert";
136
137 const activeSuggestionId = usePluginOption(suggestionPlugin, "activeId");
138 const hoverSuggestionId = usePluginOption(suggestionPlugin, "hoverId");
139
140 const isActive = activeSuggestionId === suggestionData.id;
141 const isHover = hoverSuggestionId === suggestionData.id;
142
143 const spanRef = React.useRef<HTMLSpanElement>(null);
144 const { setOption } = useEditorPlugin(suggestionPlugin);
145
146 return (
147 <>
148 {isLineBreak ? (
149 <>
150 {children}
151 <span
152 ref={spanRef}
153 className={cn(
154 "absolute text-justify",
155 suggestionVariants({
156 insertActive: isInsert && (isActive || isHover),
157 remove: isRemove,
158 removeActive: (isActive || isHover) && isRemove,
159 }),
160 )}
161 style={{
162 bottom: 3.5,
163 height: 21,
164 }}
165 contentEditable={false}
166 >
167 <CornerDownLeftIcon className="mt-0.5 size-4" />
168 </span>
169 </>
170 ) : (
171 <div
172 className={cn(
173 suggestionVariants({
174 insertActive: isInsert && (isActive || isHover),
175 remove: isRemove,
176 removeActive: (isActive || isHover) && isRemove,
177 }),
178 )}
179 onMouseEnter={() => setOption("hoverId", suggestionData.id)}
180 onMouseLeave={() => setOption("hoverId", null)}
181 data-block-suggestion="true"
182 >
183 {children}
184 </div>
185 )}
186 </>
187 );
188 }
189
189 lines Plain Text