返回 presentation-ai
useAntvInfographicRendering.ts
根目录 / src / hooks / presentation / infographic / useAntvInfographicRendering.ts
1 "use client";
2
3 import { type Infographic, type InfographicOptions } from "@antv/infographic";
4 import {
5 useEffect,
6 type Dispatch,
7 type MutableRefObject,
8 type RefObject,
9 type SetStateAction,
10 } from "react";
11
12 import { type TAntvInfographicElement } from "@/components/notebook/presentation/editor/plugins/antv-infographic-plugin";
13 import { enforceInfographicCardBackground } from "@/components/notebook/presentation/editor/utils/infographic-card-background";
14 import {
15 applyInitialInfographicTitleLayout,
16 hasInfographicTitleLayoutAttributes,
17 } from "@/components/notebook/presentation/editor/utils/infographic-title-layout";
18 import {
19 applyColorModeToData,
20 applyThemeToData,
21 applyThemeToSyntax,
22 type InfographicPaletteThemeColors,
23 } from "@/components/notebook/presentation/editor/utils/infographic-utils";
24 import { loadInfographicFonts } from "./infographic-font-loader";
25
26 type RenderingParams = {
27 infographicRef: RefObject<Infographic | null>;
28 containerRef: RefObject<HTMLDivElement | null>;
29 element: TAntvInfographicElement;
30 elementRef: RefObject<TAntvInfographicElement>;
31 themedSyntax: string;
32 isDark: boolean;
33 themeColors?: InfographicPaletteThemeColors | null;
34 renderThemeMode?: "presentation" | "app";
35 skipNextRenderRef?: MutableRefObject<boolean>;
36 syntax: string;
37 setHasError: Dispatch<SetStateAction<boolean>>;
38 };
39
40 type RenderPayload = string | Partial<InfographicOptions>;
41
42 type RenderWithLayoutParams = {
43 containerRef: RefObject<HTMLDivElement | null>;
44 infographicRef: RefObject<Infographic | null>;
45 instance: Infographic;
46 isCancelled: () => boolean;
47 shouldApplyInitialTitleLayout: boolean;
48 payload: RenderPayload;
49 };
50
51 function refreshInfographicLayout({
52 container,
53 instance,
54 shouldApplyInitialTitleLayout,
55 }: {
56 container: HTMLDivElement | null;
57 instance: Infographic;
58 shouldApplyInitialTitleLayout: boolean;
59 }): void {
60 if (!container) return;
61
62 if (shouldApplyInitialTitleLayout) {
63 applyInitialInfographicTitleLayout(container, instance);
64 }
65 enforceInfographicCardBackground(container);
66 }
67
68 function renderWithLayoutRefresh({
69 containerRef,
70 infographicRef,
71 instance,
72 isCancelled,
73 shouldApplyInitialTitleLayout,
74 payload,
75 }: RenderWithLayoutParams): number {
76 const shouldRerenderAfterFontsLoad = loadInfographicFonts(payload);
77
78 instance.render(payload);
79 const frameId = window.requestAnimationFrame(() => {
80 refreshInfographicLayout({
81 container: containerRef.current,
82 instance,
83 shouldApplyInitialTitleLayout,
84 });
85 });
86
87 if (shouldRerenderAfterFontsLoad) {
88 void document.fonts.ready.then(() => {
89 if (isCancelled() || infographicRef.current !== instance) return;
90
91 instance.render(payload);
92 refreshInfographicLayout({
93 container: containerRef.current,
94 instance,
95 shouldApplyInitialTitleLayout,
96 });
97 });
98 }
99
100 return frameId;
101 }
102
103 function getSavedDataPayload({
104 elementRef,
105 isDark,
106 renderThemeMode,
107 themedSyntax,
108 themeColors,
109 }: Pick<
110 RenderingParams,
111 "elementRef" | "isDark" | "renderThemeMode" | "themedSyntax" | "themeColors"
112 >): RenderPayload {
113 const data = elementRef.current.data;
114 if (!data || Object.keys(data).length === 0) return themedSyntax;
115
116 if (renderThemeMode === "presentation") {
117 return applyThemeToData(data, isDark, themeColors);
118 }
119
120 return applyColorModeToData(data, isDark);
121 }
122
123 export function useAntvInfographicRendering({
124 infographicRef,
125 containerRef,
126 element,
127 elementRef,
128 themedSyntax,
129 isDark,
130 themeColors,
131 renderThemeMode = "presentation",
132 skipNextRenderRef,
133 syntax,
134 setHasError,
135 }: RenderingParams) {
136 useEffect(() => {
137 const instance = infographicRef.current;
138 if (!instance || element.isLoading) return;
139
140 if (skipNextRenderRef?.current) {
141 skipNextRenderRef.current = false;
142 return;
143 }
144
145 let cancelled = false;
146 let frameId: number | null = null;
147
148 const payload = getSavedDataPayload({
149 elementRef,
150 isDark,
151 renderThemeMode,
152 themedSyntax,
153 themeColors,
154 });
155 const shouldApplyInitialTitleLayout = !hasInfographicTitleLayoutAttributes(
156 elementRef.current.data,
157 );
158
159 if (!payload || (typeof payload === "string" && !payload.trim())) return;
160
161 try {
162 frameId = renderWithLayoutRefresh({
163 containerRef,
164 infographicRef,
165 instance,
166 isCancelled: () => cancelled,
167 shouldApplyInitialTitleLayout,
168 payload,
169 });
170 setHasError(false);
171 } catch (err) {
172 console.error("Failed to render infographic:", err);
173 setHasError(true);
174 }
175
176 return () => {
177 cancelled = true;
178 if (frameId !== null) {
179 window.cancelAnimationFrame(frameId);
180 }
181 };
182 }, [
183 themedSyntax,
184 element.isLoading,
185 elementRef,
186 infographicRef,
187 containerRef,
188 setHasError,
189 isDark,
190 themeColors,
191 renderThemeMode,
192 skipNextRenderRef,
193 ]);
194
195 // This hooks is for streaming rendering
196 useEffect(() => {
197 const instance = infographicRef.current;
198 if (!instance || element.isLoading || element.data || !syntax) return;
199 let cancelled = false;
200 let frameId: number | null = null;
201
202 try {
203 const payload =
204 renderThemeMode === "presentation"
205 ? applyThemeToSyntax(syntax, isDark, themeColors)
206 : themedSyntax;
207 frameId = renderWithLayoutRefresh({
208 containerRef,
209 infographicRef,
210 instance,
211 isCancelled: () => cancelled,
212 shouldApplyInitialTitleLayout: true,
213 payload,
214 });
215 } catch {
216 // Ignore parse errors during streaming
217 }
218
219 return () => {
220 cancelled = true;
221 if (frameId !== null) {
222 window.cancelAnimationFrame(frameId);
223 }
224 };
225 }, [
226 syntax,
227 element.isLoading,
228 element.data,
229 isDark,
230 themeColors,
231 themedSyntax,
232 renderThemeMode,
233 infographicRef,
234 containerRef,
235 ]);
236 }
237
237 lines TYPESCRIPT