@alicloud/appflow-chat 0.0.1-beta.2 → 0.0.1-beta.4

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.
@@ -0,0 +1,265 @@
1
+ import React, { useEffect, useRef, useState } from 'react';
2
+ import loadMermaidScript from '@/utils/loadMermaid';
3
+
4
+ interface IProps {
5
+ code: string;
6
+ }
7
+
8
+ // 生成唯一ID
9
+ let mermaidIdCounter = 0;
10
+ const generateMermaidId = () => `mermaid-${Date.now()}-${mermaidIdCounter++}`;
11
+
12
+ // 检查 mermaid 代码是否可能完整
13
+ const isMermaidCodeComplete = (code: string): boolean => {
14
+ if (!code || code.trim().length < 10) return false;
15
+
16
+ const trimmed = code.trim();
17
+
18
+ // 检查是否有基本的图表类型声明
19
+ const graphTypes = [
20
+ 'graph', 'flowchart', 'sequenceDiagram', 'classDiagram',
21
+ 'stateDiagram', 'erDiagram', 'journey', 'gantt', 'pie',
22
+ 'quadrantChart', 'requirementDiagram', 'gitGraph', 'mindmap', 'timeline'
23
+ ];
24
+
25
+ const hasGraphType = graphTypes.some(type =>
26
+ trimmed.startsWith(type) || trimmed.match(new RegExp(`^${type}\\s`, 'm'))
27
+ );
28
+
29
+ if (!hasGraphType) return false;
30
+
31
+ // 检查是否有至少一个完整的节点或连接定义
32
+ // 对于 flowchart/graph: A --> B 或 A[text]
33
+ // 对于 sequenceDiagram: participant A 或 A->>B
34
+ const hasContent = /(\w+\s*-->|\w+\s*---|\w+\s*-\.->|\w+\s*==>|\w+\s*\[.*?\]|\w+\s*\(.*?\)|participant\s+\w+|\w+\s*->>)/m.test(trimmed);
35
+
36
+ return hasContent;
37
+ };
38
+
39
+ export const Mermaid: React.FC<IProps> = ({ code }) => {
40
+ const containerRef = useRef<HTMLDivElement>(null);
41
+ const [svg, setSvg] = useState<string>('');
42
+ const [error, setError] = useState<string>('');
43
+ const [isLoading, setIsLoading] = useState(true);
44
+ const [isMermaidLoaded, setIsMermaidLoaded] = useState(false);
45
+ const [isStreaming, setIsStreaming] = useState(true);
46
+ const lastCodeRef = useRef<string>('');
47
+ const renderTimeoutRef = useRef<NodeJS.Timeout | null>(null);
48
+ const stableTimeoutRef = useRef<NodeJS.Timeout | null>(null);
49
+
50
+ // 加载mermaid脚本
51
+ useEffect(() => {
52
+ const initMermaid = async () => {
53
+ try {
54
+ await loadMermaidScript();
55
+ setIsMermaidLoaded(true);
56
+ } catch (error) {
57
+ console.error('mermaid加载失败:', error);
58
+ setError('Mermaid 库加载失败');
59
+ setIsLoading(false);
60
+ }
61
+ };
62
+
63
+ initMermaid();
64
+
65
+ return () => {
66
+ if (renderTimeoutRef.current) {
67
+ clearTimeout(renderTimeoutRef.current);
68
+ }
69
+ if (stableTimeoutRef.current) {
70
+ clearTimeout(stableTimeoutRef.current);
71
+ }
72
+ };
73
+ }, []);
74
+
75
+ // 渲染mermaid图表(带防抖)
76
+ useEffect(() => {
77
+ if (!code || !isMermaidLoaded) return;
78
+
79
+ const mermaid = (window as any).mermaid;
80
+ if (!mermaid) {
81
+ setError('Mermaid 未正确加载');
82
+ setIsLoading(false);
83
+ return;
84
+ }
85
+
86
+ // 检查代码是否变化
87
+ const codeChanged = code !== lastCodeRef.current;
88
+ lastCodeRef.current = code;
89
+
90
+ // 如果代码变化了,说明还在流式输出
91
+ if (codeChanged) {
92
+ setIsStreaming(true);
93
+
94
+ // 清除之前的稳定检测定时器
95
+ if (stableTimeoutRef.current) {
96
+ clearTimeout(stableTimeoutRef.current);
97
+ }
98
+
99
+ // 设置新的稳定检测定时器(500ms 没有变化认为稳定)
100
+ stableTimeoutRef.current = setTimeout(() => {
101
+ setIsStreaming(false);
102
+ }, 500);
103
+ }
104
+
105
+ // 检查代码是否可能完整
106
+ if (!isMermaidCodeComplete(code)) {
107
+ // 代码不完整,显示加载状态
108
+ setIsLoading(true);
109
+ setError('');
110
+ return;
111
+ }
112
+
113
+ // 清除之前的渲染定时器
114
+ if (renderTimeoutRef.current) {
115
+ clearTimeout(renderTimeoutRef.current);
116
+ }
117
+
118
+ // 使用防抖延迟渲染(300ms)
119
+ renderTimeoutRef.current = setTimeout(async () => {
120
+ setIsLoading(true);
121
+ setError('');
122
+
123
+ try {
124
+ const id = generateMermaidId();
125
+ const { svg } = await mermaid.render(id, code.trim());
126
+ setSvg(svg);
127
+ setError('');
128
+
129
+ // 清理mermaid.render()创建的临时DOM元素
130
+ const tempDiv = document.getElementById(id);
131
+ if (tempDiv) {
132
+ tempDiv.remove();
133
+ }
134
+ } catch (err: any) {
135
+ // 清理可能创建的临时DOM元素
136
+ const tempDivs = document.querySelectorAll('[id^="dmermaid-"]');
137
+ tempDivs.forEach(div => div.remove());
138
+
139
+ // 如果还在流式输出中,不显示错误,继续显示加载状态
140
+ if (isStreaming) {
141
+ console.log('Mermaid 流式输出中,暂不显示错误');
142
+ // 保持加载状态,不设置错误
143
+ } else {
144
+ // 流式输出结束后,如果仍然有错误,才显示错误
145
+ console.error('Mermaid 渲染错误:', err);
146
+ setError(err?.message || 'Mermaid 图表渲染失败');
147
+ }
148
+ } finally {
149
+ setIsLoading(false);
150
+ }
151
+ }, 300);
152
+
153
+ return () => {
154
+ if (renderTimeoutRef.current) {
155
+ clearTimeout(renderTimeoutRef.current);
156
+ }
157
+ };
158
+ }, [code, isMermaidLoaded, isStreaming]);
159
+
160
+ // 加载状态
161
+ if (isLoading) {
162
+ return (
163
+ <div
164
+ style={{
165
+ width: '100%',
166
+ minHeight: '100px',
167
+ padding: '20px',
168
+ display: 'flex',
169
+ alignItems: 'center',
170
+ justifyContent: 'center',
171
+ border: '1px dashed #d9d9d9',
172
+ borderRadius: '6px',
173
+ color: '#666',
174
+ backgroundColor: '#fafafa',
175
+ marginTop: '10px',
176
+ marginBottom: '10px',
177
+ }}
178
+ >
179
+ 图表加载中...
180
+ </div>
181
+ );
182
+ }
183
+
184
+ // 错误状态(只在非流式输出时显示)
185
+ if (error && !isStreaming) {
186
+ return (
187
+ <div
188
+ style={{
189
+ width: '100%',
190
+ padding: '20px',
191
+ border: '1px solid #ffccc7',
192
+ borderRadius: '6px',
193
+ color: '#cf1322',
194
+ backgroundColor: '#fff2f0',
195
+ marginTop: '10px',
196
+ marginBottom: '10px',
197
+ }}
198
+ >
199
+ <div
200
+ style={{
201
+ fontWeight: 'bold',
202
+ marginBottom: '8px'
203
+ }}
204
+ >
205
+ Mermaid 图表渲染失败
206
+ </div>
207
+ <div
208
+ style={{
209
+ fontSize: '12px',
210
+ color: '#666',
211
+ wordBreak: 'break-word',
212
+ overflowWrap: 'break-word',
213
+ whiteSpace: 'pre-wrap'
214
+ }}
215
+ >
216
+ {error}
217
+ </div>
218
+ </div>
219
+ );
220
+ }
221
+
222
+ // 如果有错误但还在流式输出,显示加载状态
223
+ if (error && isStreaming) {
224
+ return (
225
+ <div
226
+ style={{
227
+ width: '100%',
228
+ minHeight: '100px',
229
+ padding: '20px',
230
+ display: 'flex',
231
+ alignItems: 'center',
232
+ justifyContent: 'center',
233
+ border: '1px dashed #d9d9d9',
234
+ borderRadius: '6px',
235
+ color: '#666',
236
+ backgroundColor: '#fafafa',
237
+ marginTop: '10px',
238
+ marginBottom: '10px',
239
+ }}
240
+ >
241
+ 图表加载中...
242
+ </div>
243
+ );
244
+ }
245
+
246
+ // 成功渲染
247
+ return (
248
+ <div
249
+ ref={containerRef}
250
+ style={{
251
+ width: '100%',
252
+ overflow: 'auto',
253
+ marginTop: '10px',
254
+ marginBottom: '10px',
255
+ padding: '10px',
256
+ backgroundColor: '#fff',
257
+ borderRadius: '6px',
258
+ border: '1px solid #e8e8e8',
259
+ }}
260
+ dangerouslySetInnerHTML={{ __html: svg }}
261
+ />
262
+ );
263
+ };
264
+
265
+ export default Mermaid;
@@ -13,6 +13,7 @@ import Chart from './components/Chart';
13
13
  import { convertTableDataToMarkdown } from './utils/dataProcessor';
14
14
  import Loading from './components/Loading';
15
15
  import Error from './components/Error';
16
+ import Mermaid from './components/Mermaid';
16
17
 
17
18
  export interface MarkdownViewProps {
18
19
  /** Markdown 内容 */
@@ -135,6 +136,12 @@ export const MarkdownView: React.FC<MarkdownViewProps> = ({
135
136
  }
136
137
  }
137
138
 
139
+ // mermaid图表信息块处理
140
+ if (finalLang === 'language-mermaid') {
141
+ const codeContent = String(props.children).replace(/\n$/, '');
142
+ return <Mermaid code={codeContent} />;
143
+ }
144
+
138
145
  return <ASyntaxHighLight>{props.children}</ASyntaxHighLight>;
139
146
  },
140
147
  p: (paragraph: any) => {
@@ -0,0 +1,40 @@
1
+ export const loadMermaidScript = (): Promise<void> => {
2
+ return new Promise((resolve, reject) => {
3
+ // 检查mermaid是否已经加载
4
+ if (typeof window !== 'undefined' && (window as any).mermaid) {
5
+ resolve();
6
+ return;
7
+ }
8
+
9
+ // 使用cdnjs (Cloudflare)CDN
10
+ const mermaidScript = 'https://cdnjs.cloudflare.com/ajax/libs/mermaid/10.9.0/mermaid.min.js';
11
+
12
+ addExternalScript(mermaidScript)
13
+ .then(() => {
14
+ // 初始化 mermaid 配置
15
+ (window as any).mermaid.initialize({
16
+ startOnLoad: false,
17
+ theme: 'default',
18
+ securityLevel: 'loose',
19
+ fontFamily: 'inherit',
20
+ });
21
+ resolve();
22
+ })
23
+ .catch((error) => {
24
+ console.error('mermaid加载失败:', error);
25
+ reject(error);
26
+ });
27
+ });
28
+ };
29
+
30
+ export function addExternalScript(src: string) {
31
+ return new Promise<void>((resolve, reject) => {
32
+ const script = document.createElement('script');
33
+ script.src = src;
34
+ script.onload = () => resolve();
35
+ script.onerror = () => reject();
36
+ document.body.appendChild(script);
37
+ });
38
+ }
39
+
40
+ export default loadMermaidScript;