visualknowledge 0.1.0 → 0.1.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.
Files changed (36) hide show
  1. package/frontend/index.html +41 -0
  2. package/frontend/src/App.jsx +59 -0
  3. package/frontend/src/components/AssistantMessage.jsx +58 -0
  4. package/frontend/src/components/ChatArea.jsx +20 -0
  5. package/frontend/src/components/InputArea.jsx +68 -0
  6. package/frontend/src/components/Message.jsx +16 -0
  7. package/frontend/src/components/MessageList.jsx +23 -0
  8. package/frontend/src/components/TextPart.jsx +45 -0
  9. package/frontend/src/components/TopBar.jsx +27 -0
  10. package/frontend/src/components/TypingIndicator.jsx +13 -0
  11. package/frontend/src/components/UserMessage.jsx +14 -0
  12. package/frontend/src/components/WelcomeScreen.jsx +15 -0
  13. package/frontend/src/components/widgets/FullscreenOverlay.jsx +44 -0
  14. package/frontend/src/components/widgets/HtmlWidget.jsx +89 -0
  15. package/frontend/src/components/widgets/MermaidWidget.jsx +61 -0
  16. package/frontend/src/components/widgets/SvgWidget.jsx +59 -0
  17. package/frontend/src/components/widgets/WidgetContainer.jsx +32 -0
  18. package/frontend/src/context/AppContext.jsx +151 -0
  19. package/frontend/src/hooks/useChat.js +160 -0
  20. package/frontend/src/hooks/useModels.js +52 -0
  21. package/frontend/src/hooks/useTheme.js +31 -0
  22. package/frontend/src/lib/markdownRenderer.js +80 -0
  23. package/frontend/src/lib/mermaidRenderer.js +99 -0
  24. package/frontend/src/lib/streamProcessor.js +205 -0
  25. package/frontend/src/main.js +16 -0
  26. package/frontend/src/utils/escape.js +10 -0
  27. package/frontend/styles/base.css +7 -0
  28. package/frontend/styles/chat-area.css +24 -0
  29. package/frontend/styles/input-area.css +37 -0
  30. package/frontend/styles/markdown.css +50 -0
  31. package/frontend/styles/message.css +24 -0
  32. package/frontend/styles/top-bar.css +32 -0
  33. package/frontend/styles/variables.css +74 -0
  34. package/frontend/styles/widget.css +62 -0
  35. package/package.json +29 -1
  36. package/server.py +11 -1
@@ -0,0 +1,32 @@
1
+ /**
2
+ * WidgetContainer - 通用 Widget 外壳
3
+ *
4
+ * 渲染 header(badge + typeLabel + zoom 按钮)+ body(children)
5
+ */
6
+
7
+ import { html } from 'htm/react';
8
+
9
+ export function WidgetContainer({ badge, typeLabel, status = 'done', children, onZoom }) {
10
+ const uid = `wc_${Date.now()}_${Math.random().toString(36).substr(2, 6)}`;
11
+
12
+ return html`
13
+ <div class="widget-container">
14
+ <div class="widget-header">
15
+ <div class="left">
16
+ <span class="badge">${badge}</span>
17
+ <span class="widget-type">${typeLabel}</span>
18
+ </div>
19
+ ${onZoom ? html`
20
+ <button class="btn" id="zoom-${uid}" onClick=${onZoom}>⛶ 放大</button>
21
+ ` : ''}
22
+ </div>
23
+ <div class="widget-body">
24
+ ${status === 'loading' ? html`
25
+ <div class="widget-loading">
26
+ <div class="spinner"></div> 渲染中...
27
+ </div>
28
+ ` : children}
29
+ </div>
30
+ </div>
31
+ `;
32
+ }
@@ -0,0 +1,151 @@
1
+ /**
2
+ * AppContext - 全局状态管理
3
+ *
4
+ * 基于 React Context + useReducer 的单一数据源。
5
+ * 管理对话消息、流式状态、主题、模型选择、全屏 Widget。
6
+ */
7
+
8
+ import { createContext, useContext, useReducer } from 'react';
9
+
10
+ // ====== Action Types ======
11
+
12
+ export const ActionTypes = {
13
+ ADD_USER_MESSAGE: 'ADD_USER_MESSAGE',
14
+ START_ASSISTANT_MESSAGE: 'START_ASSISTANT_MESSAGE',
15
+ APPEND_TO_SEGMENT: 'APPEND_TO_SEGMENT',
16
+ FINALIZE_SEGMENTS: 'FINALIZE_SEGMENTS',
17
+ SET_STREAMING_ERROR: 'SET_STREAMING_ERROR',
18
+ SET_MODELS: 'SET_MODELS',
19
+ SET_THEME: 'SET_THEME',
20
+ SET_FULLSCREEN_WIDGET: 'SET_FULLSCREEN_WIDGET',
21
+ };
22
+
23
+ // ====== Initial State ======
24
+
25
+ const initialState = {
26
+ messages: [],
27
+ isStreaming: false,
28
+ currentModel: '',
29
+ availableModels: [],
30
+ theme: (() => {
31
+ try { return localStorage.getItem('claude-chat-theme') || 'dark'; }
32
+ catch { return 'dark'; }
33
+ })(),
34
+ fullscreenWidget: null,
35
+ };
36
+
37
+ // ====== Reducer ======
38
+
39
+ function appReducer(state, action) {
40
+ switch (action.type) {
41
+
42
+ case ActionTypes.ADD_USER_MESSAGE:
43
+ return {
44
+ ...state,
45
+ messages: [...state.messages, {
46
+ id: `msg_${Date.now()}_${Math.random().toString(36).substr(2, 6)}`,
47
+ role: 'user',
48
+ content: action.content,
49
+ segments: [{ id: `seg_txt_user_${Date.now()}`, type: 'text', content: action.content, renderedContent: null, renderStatus: 'done', renderError: null }],
50
+ status: 'complete',
51
+ error: null,
52
+ timestamp: Date.now(),
53
+ }],
54
+ };
55
+
56
+ case ActionTypes.START_ASSISTANT_MESSAGE:
57
+ return {
58
+ ...state,
59
+ isStreaming: true,
60
+ messages: [...state.messages, {
61
+ id: action.messageId,
62
+ role: 'assistant',
63
+ content: '',
64
+ segments: [],
65
+ status: 'streaming',
66
+ error: null,
67
+ timestamp: Date.now(),
68
+ }],
69
+ };
70
+
71
+ case ActionTypes.FINALIZE_SEGMENTS:
72
+ return {
73
+ ...state,
74
+ messages: state.messages.map(msg =>
75
+ msg.id === action.messageId
76
+ ? { ...msg, segments: action.segments, content: _extractTextFromSegments(action.segments) }
77
+ : msg
78
+ ),
79
+ };
80
+
81
+ case ActionTypes.SET_STREAMING_ERROR:
82
+ return {
83
+ ...state,
84
+ isStreaming: false,
85
+ messages: state.messages.map(msg =>
86
+ msg.id === action.messageId
87
+ ? { ...msg, status: 'error', error: action.error }
88
+ : msg
89
+ ),
90
+ };
91
+
92
+ // 流结束,标记消息完成
93
+ case 'COMPLETE_STREAM':
94
+ return {
95
+ ...state,
96
+ isStreaming: false,
97
+ messages: state.messages.map(msg =>
98
+ msg.id === action.messageId && msg.status === 'streaming'
99
+ ? { ...msg, status: 'complete' }
100
+ : msg
101
+ ),
102
+ };
103
+
104
+ case ActionTypes.SET_MODELS:
105
+ return {
106
+ ...state,
107
+ availableModels: action.models,
108
+ currentModel: action.current || state.currentModel,
109
+ };
110
+
111
+ case ActionTypes.SET_THEME:
112
+ return { ...state, theme: action.theme };
113
+
114
+ case ActionTypes.SET_FULLSCREEN_WIDGET:
115
+ return { ...state, fullscreenWidget: action.widget };
116
+
117
+ default:
118
+ return state;
119
+ }
120
+ }
121
+
122
+ // 从 segments 中提取纯文本(用于 conversationHistory)
123
+ function _extractTextFromSegments(segments) {
124
+ return segments
125
+ .filter(s => s.type === 'text')
126
+ .map(s => s.content)
127
+ .join('');
128
+ }
129
+
130
+ // ====== Context ======
131
+
132
+ export const AppContext = createContext(null);
133
+
134
+ export function AppProvider({ children }) {
135
+ const [state, dispatch] = useReducer(appReducer, initialState);
136
+
137
+ return (
138
+ AppContext.Provider value={{ state, dispatch }}>
139
+ {children}
140
+ </AppContext.Provider>
141
+ );
142
+ }
143
+
144
+ /**
145
+ * 便捷 Hook:获取全局 state 和 dispatch
146
+ */
147
+ export function useAppState() {
148
+ const ctx = useContext(AppContext);
149
+ if (!ctx) throw new Error('useAppState must be used within AppProvider');
150
+ return ctx;
151
+ }
@@ -0,0 +1,160 @@
1
+ /**
2
+ * useChat - 核心对话 Hook
3
+ *
4
+ * 封装 SSE 流式连接和 StreamProcessor 协调逻辑。
5
+ * 管理消息发送、流式接收、状态更新全生命周期。
6
+ *
7
+ * 返回: { messages, isStreaming, sendMessage }
8
+ */
9
+
10
+ import { useState, useCallback, useRef } from 'react';
11
+ import { useAppState, ActionTypes } from '../context/AppContext.jsx';
12
+ import { StreamProcessor } from '../lib/streamProcessor.js';
13
+
14
+ export function useChat() {
15
+ const { state, dispatch } = useAppState();
16
+ const processorRef = useRef(null);
17
+ const [localMessages, setLocalMessages] = useState([]);
18
+ const [localStreaming, setLocalStreaming] = useState(false);
19
+
20
+ /**
21
+ * 发送消息并启动流式接收
22
+ */
23
+ const sendMessage = useCallback(async (text) => {
24
+ if (!text.trim() || localStreaming) return;
25
+
26
+ // 1. 添加用户消息
27
+ const userMsg = {
28
+ id: `msg_${Date.now()}_${Math.random().toString(36).substr(2, 6)}`,
29
+ role: 'user',
30
+ content: text,
31
+ segments: [{ id: `seg_txt_user_${Date.now()}`, type: 'text', content: text, renderedContent: null, renderStatus: 'done', renderError: null }],
32
+ status: 'complete',
33
+ error: null,
34
+ timestamp: Date.now(),
35
+ };
36
+ dispatch({ type: ActionTypes.ADD_USER_MESSAGE, content: text });
37
+ setLocalMessages(prev => [...prev, userMsg]);
38
+
39
+ // 2. 创建 AI 消息占位
40
+ const assistantId = `msg_ast_${Date.now()}`;
41
+ dispatch({ type: ActionTypes.START_ASSISTANT_MESSAGE, messageId: assistantId });
42
+ setLocalStreaming(true);
43
+
44
+ // 3. 初始化 StreamProcessor
45
+ const processor = new StreamProcessor();
46
+ processorRef.current = processor;
47
+
48
+ // 4. 节流:限制 segments 更新频率(每 100ms 最多一次)
49
+ let lastUpdate = 0;
50
+ const THROTTLE_MS = 100;
51
+
52
+ const scheduleUpdate = () => {
53
+ const now = Date.now();
54
+ if (now - lastUpdate < THROTTLE_MS) return;
55
+ lastUpdate = now;
56
+ const segments = processor.getSegments();
57
+ dispatch({
58
+ type: ActionTypes.FINALIZE_SEGMENTS,
59
+ messageId: assistantId,
60
+ segments: JSON.parse(JSON.stringify(segments)), // deep clone for React re-render
61
+ });
62
+ };
63
+
64
+ try {
65
+ // 5. 发起 SSE 请求
66
+ const conversationHistory = [...localMessages, userMsg].map(m => ({
67
+ role: m.role,
68
+ content: m.content,
69
+ }));
70
+
71
+ const response = await fetch('/api/chat', {
72
+ method: 'POST',
73
+ headers: { 'Content-Type': 'application/json' },
74
+ body: JSON.stringify({
75
+ messages: conversationHistory,
76
+ model: state.currentModel || undefined,
77
+ }),
78
+ });
79
+
80
+ if (!response.ok) throw new Error(`HTTP ${response.status}`);
81
+
82
+ const reader = response.body.getReader();
83
+ const decoder = new TextDecoder();
84
+ let buffer = '';
85
+
86
+ // 6. 读取 SSE 流
87
+ while (true) {
88
+ const { done, value } = await reader.read();
89
+ if (done) break;
90
+
91
+ buffer += decoder.decode(value, { stream: true });
92
+ const lines = buffer.split('\n');
93
+ buffer = lines.pop() || '';
94
+
95
+ for (const line of lines) {
96
+ if (!line.startsWith('data: ')) continue;
97
+ const data = line.slice(6);
98
+ if (data === '[DONE]') continue;
99
+
100
+ let event;
101
+ try { event = JSON.parse(data); } catch { continue; }
102
+
103
+ if (event.type === 'text') {
104
+ processor.feed(event.content);
105
+ scheduleUpdate();
106
+ } else if (event.type === 'error') {
107
+ dispatch({
108
+ type: ActionTypes.SET_STREAMING_ERROR,
109
+ messageId: assistantId,
110
+ error: event.message || 'Unknown error',
111
+ });
112
+ setLocalStreaming(false);
113
+ return;
114
+ }
115
+ }
116
+ }
117
+
118
+ // 7. 流结束,finalize
119
+ const finalSegments = processor.finalize();
120
+ dispatch({
121
+ type: ActionTypes.FINALIZE_SEGMENTS,
122
+ messageId: assistantId,
123
+ segments: finalSegments,
124
+ });
125
+ dispatch({ type: 'COMPLETE_STREAM', messageId: assistantId });
126
+
127
+ // 更新本地 messages 列表
128
+ const finalText = finalSegments
129
+ .filter(s => s.type === 'text')
130
+ .map(s => s.content)
131
+ .join('');
132
+ setLocalMessages(prev => [...prev, {
133
+ id: assistantId,
134
+ role: 'assistant',
135
+ content: finalText,
136
+ segments: finalSegments,
137
+ status: 'complete',
138
+ error: null,
139
+ timestamp: Date.now(),
140
+ }]);
141
+
142
+ } catch (err) {
143
+ console.error('Chat error:', err);
144
+ dispatch({
145
+ type: ActionTypes.SET_STREAMING_ERROR,
146
+ messageId: assistantId,
147
+ error: err.message || String(err),
148
+ });
149
+ }
150
+
151
+ setLocalStreaming(false);
152
+ processorRef.current = null;
153
+ }, [localStreaming, state.currentModel, localMessages, dispatch]);
154
+
155
+ return {
156
+ messages: localMessages,
157
+ isStreaming: localStreaming,
158
+ sendMessage,
159
+ };
160
+ }
@@ -0,0 +1,52 @@
1
+ /**
2
+ * useModels - 模型加载 Hook
3
+ *
4
+ * useEffect 中 fetch GET /api/models,
5
+ * 解析响应后 dispatch SET_MODELS。
6
+ */
7
+
8
+ import { useState, useEffect, useCallback } from 'react';
9
+ import { useAppState, ActionTypes } from '../context/AppContext.jsx';
10
+
11
+ export function useModels() {
12
+ const { state, dispatch } = useAppState();
13
+ const [isLoading, setIsLoading] = useState(true);
14
+
15
+ useEffect(() => {
16
+ let cancelled = false;
17
+
18
+ async function load() {
19
+ try {
20
+ const res = await fetch('/api/models');
21
+ if (!res.ok) throw new Error(`HTTP ${res.status}`);
22
+ const data = await res.json();
23
+ if (!cancelled) {
24
+ const models = [...new Set(data.available || [data.current])];
25
+ dispatch({
26
+ type: ActionTypes.SET_MODELS,
27
+ models,
28
+ current: data.current,
29
+ });
30
+ }
31
+ } catch (e) {
32
+ console.error('Failed to load models:', e);
33
+ } finally {
34
+ if (!cancelled) setIsLoading(false);
35
+ }
36
+ }
37
+
38
+ load();
39
+ return () => { cancelled = true; };
40
+ }, [dispatch]);
41
+
42
+ const setModel = useCallback((model) => {
43
+ dispatch({ type: ActionTypes.SET_MODELS, models: state.availableModels, current: model });
44
+ }, [state.availableModels, dispatch]);
45
+
46
+ return {
47
+ currentModel: state.currentModel,
48
+ models: state.availableModels,
49
+ setModel,
50
+ isLoading,
51
+ };
52
+ }
@@ -0,0 +1,31 @@
1
+ /**
2
+ * useTheme - 主题切换 Hook
3
+ *
4
+ * 读取/写入 localStorage('claude-chat-theme'),
5
+ * 切换时更新 document.documentElement.dataset.theme。
6
+ */
7
+
8
+ import { useState, useCallback } from 'react';
9
+
10
+ const STORAGE_KEY = 'claude-chat-theme';
11
+
12
+ export function useTheme() {
13
+ const [theme, setThemeState] = useState(() => {
14
+ try { return localStorage.getItem(STORAGE_KEY) || 'dark'; }
15
+ catch { return 'dark'; }
16
+ });
17
+
18
+ const toggleTheme = useCallback(() => {
19
+ const next = theme === 'light' ? 'dark' : 'light';
20
+ setThemeState(next);
21
+ document.documentElement.setAttribute('data-theme', next);
22
+ try { localStorage.setItem(STORAGE_KEY, next); } catch {}
23
+ }, [theme]);
24
+
25
+ // 初始化:设置 DOM 主题
26
+ useState(() => {
27
+ document.documentElement.setAttribute('data-theme', theme);
28
+ });
29
+
30
+ return { theme, toggleTheme };
31
+ }
@@ -0,0 +1,80 @@
1
+ /**
2
+ * Markdown + KaTeX Renderer
3
+ *
4
+ * 封装 marked.parse 和 katex 渲染逻辑。
5
+ * 依赖全局变量: marked, katex(通过 CDN <script> 加载)
6
+ */
7
+
8
+ /**
9
+ * 将原始文本渲染为 HTML
10
+ * 处理:[VISUALIZE:] 标签移除、代码块过滤、marked 解析、KaTeX 公式渲染
11
+ *
12
+ * @param {string} text - 原始 Markdown 文本
13
+ * @returns {string} 渲染后的 HTML 字符串
14
+ */
15
+ export function renderMarkdown(text) {
16
+ let cleaned = text;
17
+
18
+ // 移除可视化标签
19
+ cleaned = cleaned.replace(/^\[VISUALIZE:\s*(yes|no)\]\s*\n?/m, '');
20
+
21
+ // 移除可视化代码块(由独立 Widget 渲染器处理)
22
+ cleaned = cleaned.replace(/```mermaid\s*\n[\s\S]*?```/g, '');
23
+ cleaned = cleaned.replace(/```mermaid[\s\S]*$/g, '');
24
+ cleaned = cleaned.replace(/```html\s*\n[\s\S]*?```/g, '');
25
+ cleaned = cleaned.replace(/```html[\s\S]*$/g, '');
26
+ cleaned = cleaned.replace(/```svg\s*\n[\s\S]*?```/g, '');
27
+ cleaned = cleaned.replace(/```svg[\s\S]*$/g, '');
28
+
29
+ // 移除 visualization placeholder 注释
30
+ const phRegex = new RegExp('<' + '!-- visualization placeholder -->\\s*', 'g');
31
+ cleaned = cleaned.replace(phRegex, '');
32
+
33
+ try {
34
+ const html = marked.parse(cleaned);
35
+ return renderMath(html);
36
+ } catch (e) {
37
+ // fallback: 转义后返回纯文本
38
+ if (typeof escapeHtml !== 'undefined') {
39
+ return escapeHtml(cleaned);
40
+ }
41
+ return cleaned
42
+ .replace(/&/g, '&amp;')
43
+ .replace(/</g, '&lt;')
44
+ .replace(/>/g, '&gt;');
45
+ }
46
+ }
47
+
48
+ /**
49
+ * 在 HTML 中渲染 KaTeX 数学公式
50
+ *
51
+ * @param {string} html - 包含 $...$ / $$...$$ 标记的 HTML
52
+ * @returns {string} 渲染后的 HTML
53
+ */
54
+ export function renderMath(html) {
55
+ if (typeof katex === 'undefined') return html;
56
+
57
+ try {
58
+ // 块级公式 $$...$$
59
+ let result = html.replace(/\$\$([\s\S]+?)\$\$/g, (_, tex) => {
60
+ try {
61
+ return katex.renderToString(tex.trim(), { displayMode: true, throwOnError: false });
62
+ } catch {
63
+ return `$$${tex}$$`;
64
+ }
65
+ });
66
+
67
+ // 行内公式 $...$
68
+ result = result.replace(/\$([^\$\n]+?)\$/g, (_, tex) => {
69
+ try {
70
+ return katex.renderToString(tex.trim(), { displayMode: false, throwOnError: false });
71
+ } catch {
72
+ return `$${tex}$`;
73
+ }
74
+ });
75
+
76
+ return result;
77
+ } catch (e) {
78
+ return html;
79
+ }
80
+ }
@@ -0,0 +1,99 @@
1
+ /**
2
+ * Mermaid Renderer
3
+ *
4
+ * 封装 Mermaid 初始化和图表渲染逻辑。
5
+ * 依赖 ESM 模块: mermaid(通过 importmap 加载)
6
+ */
7
+
8
+ const darkConfig = {
9
+ startOnLoad: false,
10
+ theme: 'dark',
11
+ themeVariables: {
12
+ primaryColor: '#78350f',
13
+ primaryTextColor: '#fafaf9',
14
+ primaryBorderColor: '#d97706',
15
+ lineColor: '#60a5fa',
16
+ secondaryColor: '#292524',
17
+ tertiaryColor: '#1c1917',
18
+ fontFamily: '-apple-system,BlinkMacSystemFont,Segoe UI,Roboto,sans-serif',
19
+ fontSize: '14px',
20
+ nodeTextColor: '#fafaf9',
21
+ },
22
+ flowchart: { htmlLabels: true, curve: 'basis' },
23
+ sequence: { mirrorActors: false },
24
+ mindmap: { padding: 20 },
25
+ };
26
+
27
+ const lightConfig = {
28
+ startOnLoad: false,
29
+ theme: 'base',
30
+ themeVariables: {
31
+ primaryColor: '#fef3c7',
32
+ primaryTextColor: '#1c1917',
33
+ primaryBorderColor: '#d97706',
34
+ lineColor: '#2563eb',
35
+ secondaryColor: '#f5f5f4',
36
+ tertiaryColor: '#fafaf9',
37
+ fontFamily: '-apple-system,BlinkMacSystemFont,Segoe UI,Roboto,sans-serif',
38
+ fontSize: '14px',
39
+ nodeTextColor: '#1c1917',
40
+ },
41
+ flowchart: { htmlLabels: true, curve: 'basis' },
42
+ sequence: { mirrorActors: false },
43
+ mindmap: { padding: 20 },
44
+ };
45
+
46
+ let _mermaidModule = null;
47
+
48
+ /**
49
+ * 动态加载 Mermaid ESM 模块(懒加载)
50
+ * @returns {Promise<object>} Mermaid 模块
51
+ */
52
+ async function _getMermaid() {
53
+ if (_mermaidModule) return _mermaidModule;
54
+ _mermaidModule = await import('https://cdn.jsdelivr.net/npm/mermaid@10/dist/mermaid.esm.min.mjs');
55
+ return _mermaidModule;
56
+ }
57
+
58
+ /**
59
+ * 根据 Mermaid 代码首行判断图表类型
60
+ * @param {string} code - Mermaid 源代码
61
+ * @returns {string} 中文类型名称
62
+ */
63
+ export function getChartType(code) {
64
+ const first = code.trim().split('\n')[0].trim().toLowerCase();
65
+ const map = {
66
+ graph: '流程图', flowchart: '流程图',
67
+ sequence: '序列图', class: '类图', state: '状态图',
68
+ er: 'ER图', gantt: '甘特图', pie: '饼图',
69
+ journey: '用户旅程图', mindmap: '思维导图', timeline: '时间线',
70
+ gitgraph: 'Git图', sankey: '桑基图', xychart: 'XY图表',
71
+ block: '块图',
72
+ };
73
+ for (const [key, label] of Object.entries(map)) {
74
+ if (first.startsWith(key)) return label;
75
+ }
76
+ return '图表';
77
+ }
78
+
79
+ /**
80
+ * 渲染 Mermaid 图表为 SVG
81
+ *
82
+ * @param {string} code - Mermaid 源代码
83
+ * @param {boolean} isLight - 是否浅色主题
84
+ * @returns {Promise<{svg: string|null, error: string|null}>}
85
+ */
86
+ export async function renderMermaid(code, isLight = false) {
87
+ const id = 'm_' + Math.random().toString(36).substr(2, 9);
88
+
89
+ try {
90
+ const mermaid = await _getMermaid();
91
+
92
+ // 每次渲染前用正确的主题重新初始化
93
+ mermaid.initialize(isLight ? lightConfig : darkConfig);
94
+ const { svg } = await mermaid.render(id, code);
95
+ return { svg, error: null };
96
+ } catch (e) {
97
+ return { svg: null, error: e.message || String(e) };
98
+ }
99
+ }