ai-chat-ui-kit 0.1.0
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.
- package/.eslintrc.cjs +74 -0
- package/.github/actions/screenshot/action.yml +35 -0
- package/.github/workflows/pages.yml +46 -0
- package/README.md +285 -0
- package/docs/README.md +176 -0
- package/docs/api/components.md +344 -0
- package/docs/api/core.md +349 -0
- package/docs/chat-style-1-minimal.html +78 -0
- package/docs/chat-style-2-neon.html +74 -0
- package/docs/chat-style-3-glass.html +73 -0
- package/docs/chat-style-4-terminal.html +84 -0
- package/docs/chat-style-5-gradient.html +69 -0
- package/docs/chat-style-6-corporate.html +116 -0
- package/docs/examples/basic-chat.md +291 -0
- package/docs/examples/custom-plugins.md +431 -0
- package/docs/examples/multi-model.md +466 -0
- package/docs/guide/api-adapters.md +431 -0
- package/docs/guide/getting-started.md +244 -0
- package/docs/guide/headless-mode.md +508 -0
- package/docs/guide/plugins.md +416 -0
- package/docs/guide/themes.md +327 -0
- package/docs/index.html +256 -0
- package/docs/theme-preview-1-minimal.html +74 -0
- package/docs/theme-preview-2-neon.html +73 -0
- package/docs/theme-preview-3-glass.html +77 -0
- package/docs/theme-preview-4-terminal.html +86 -0
- package/docs/theme-preview-5-gradient.html +79 -0
- package/docs/theme-preview-6-corporate.html +71 -0
- package/examples/index.html +414 -0
- package/examples/react-app/App.tsx +131 -0
- package/examples/react-app/index.html +12 -0
- package/examples/react-app/main.tsx +15 -0
- package/examples/react-app/package.json +24 -0
- package/examples/vue-app/index.html +12 -0
- package/examples/vue-app/package.json +22 -0
- package/examples/vue-app/src/App.vue +145 -0
- package/examples/vue-app/src/main.ts +9 -0
- package/package.json +44 -0
- package/packages/components/package.json +25 -0
- package/packages/components/src/chat/chat.css +80 -0
- package/packages/components/src/chat/chat.ts +236 -0
- package/packages/components/src/index.ts +36 -0
- package/packages/components/src/input/input.css +52 -0
- package/packages/components/src/input/input.ts +116 -0
- package/packages/components/src/markdown/markdown.css +118 -0
- package/packages/components/src/markdown/markdown.ts +229 -0
- package/packages/components/src/message/message.css +56 -0
- package/packages/components/src/message/message.ts +72 -0
- package/packages/components/src/styles/global.css +43 -0
- package/packages/components/src/tool-call/tool-call.css +98 -0
- package/packages/components/src/tool-call/tool-call.ts +171 -0
- package/packages/components/src/types.ts +55 -0
- package/packages/components/src/utils/helpers.ts +128 -0
- package/packages/components/tsconfig.json +25 -0
- package/packages/components/tsup.config.ts +18 -0
- package/packages/core/package.json +47 -0
- package/packages/core/pnpm-lock.yaml +2032 -0
- package/packages/core/pnpm-workspace.yaml +2 -0
- package/packages/core/src/api/adapters.ts +717 -0
- package/packages/core/src/api/base.ts +210 -0
- package/packages/core/src/api/index.ts +54 -0
- package/packages/core/src/index.ts +93 -0
- package/packages/core/src/parser/latex.ts +274 -0
- package/packages/core/src/parser/markdown.test.ts +58 -0
- package/packages/core/src/parser/markdown.ts +206 -0
- package/packages/core/src/parser/mermaid.ts +276 -0
- package/packages/core/src/plugins/PluginManager.ts +232 -0
- package/packages/core/src/plugins/builtin.ts +406 -0
- package/packages/core/src/store/ChatStore.ts +163 -0
- package/packages/core/src/store/ModelConfigStore.ts +136 -0
- package/packages/core/src/store/ToolCallStore.ts +164 -0
- package/packages/core/src/store/base.ts +75 -0
- package/packages/core/src/types/index.ts +133 -0
- package/packages/core/tsup.config.ts +18 -0
- package/packages/themes/package.json +33 -0
- package/packages/themes/src/corporate/index.ts +52 -0
- package/packages/themes/src/corporate/theme.css +228 -0
- package/packages/themes/src/glass/index.ts +52 -0
- package/packages/themes/src/glass/theme.css +237 -0
- package/packages/themes/src/gradient/index.ts +53 -0
- package/packages/themes/src/gradient/theme.css +218 -0
- package/packages/themes/src/index.ts +13 -0
- package/packages/themes/src/minimal/index.ts +52 -0
- package/packages/themes/src/minimal/theme.css +198 -0
- package/packages/themes/src/neon/index.ts +52 -0
- package/packages/themes/src/neon/theme.css +233 -0
- package/packages/themes/src/terminal/index.ts +52 -0
- package/packages/themes/src/terminal/theme.css +235 -0
- package/packages/themes/src/types.ts +10 -0
- package/packages/themes/src/vite-env.d.ts +9 -0
- package/packages/themes/tsup.config.ts +21 -0
- package/pnpm-workspace.yaml +4 -0
- package/tsconfig.json +27 -0
- package/vite.config.ts +25 -0
- package/vitest.config.ts +28 -0
|
@@ -0,0 +1,206 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* @generated-by AI: edenxpzhang
|
|
3
|
+
* @generated-date 2026-05-13
|
|
4
|
+
*/
|
|
5
|
+
|
|
6
|
+
import { Marked } from 'marked';
|
|
7
|
+
import { markedHighlight } from 'marked-highlight';
|
|
8
|
+
import hljs from 'highlight.js';
|
|
9
|
+
|
|
10
|
+
// 声明全局 __copyCode 方法
|
|
11
|
+
declare global {
|
|
12
|
+
interface Window {
|
|
13
|
+
__copyCode(button: HTMLElement): void;
|
|
14
|
+
}
|
|
15
|
+
}
|
|
16
|
+
|
|
17
|
+
// 创建 marked 实例
|
|
18
|
+
const marked = new Marked(
|
|
19
|
+
markedHighlight({
|
|
20
|
+
langPrefix: 'hljs language-',
|
|
21
|
+
highlight(code, lang) {
|
|
22
|
+
if (lang && hljs.getLanguage(lang)) {
|
|
23
|
+
return hljs.highlight(code, { language: lang }).value;
|
|
24
|
+
}
|
|
25
|
+
return hljs.highlightAuto(code).value;
|
|
26
|
+
}
|
|
27
|
+
})
|
|
28
|
+
);
|
|
29
|
+
|
|
30
|
+
// 配置 marked
|
|
31
|
+
marked.setOptions({
|
|
32
|
+
breaks: true,
|
|
33
|
+
gfm: true
|
|
34
|
+
});
|
|
35
|
+
|
|
36
|
+
/**
|
|
37
|
+
* 自定义 renderer,扩展代码块渲染
|
|
38
|
+
*/
|
|
39
|
+
function createRenderer() {
|
|
40
|
+
const renderer = new marked.Renderer();
|
|
41
|
+
|
|
42
|
+
// 自定义代码块渲染
|
|
43
|
+
renderer.code = function(code: string, infostring: string | undefined, escaped: boolean) {
|
|
44
|
+
const language = infostring || '';
|
|
45
|
+
const highlighted = language && hljs.getLanguage(language)
|
|
46
|
+
? hljs.highlight(code, { language }).value
|
|
47
|
+
: hljs.highlightAuto(code).value;
|
|
48
|
+
|
|
49
|
+
return `
|
|
50
|
+
<div class="markdown-code-block" data-language="${language}">
|
|
51
|
+
<div class="code-block-header">
|
|
52
|
+
<span class="code-language">${language || 'plaintext'}</span>
|
|
53
|
+
<button class="code-copy-btn" onclick="window.__copyCode(this)">复制</button>
|
|
54
|
+
</div>
|
|
55
|
+
<pre><code class="hljs language-${language}">${highlighted}</code></pre>
|
|
56
|
+
</div>
|
|
57
|
+
`;
|
|
58
|
+
};
|
|
59
|
+
|
|
60
|
+
// 自定义表格渲染
|
|
61
|
+
renderer.table = function(header: string, body: string) {
|
|
62
|
+
return `
|
|
63
|
+
<div class="markdown-table-wrapper">
|
|
64
|
+
<table>
|
|
65
|
+
<thead>${header}</thead>
|
|
66
|
+
<tbody>${body}</tbody>
|
|
67
|
+
</table>
|
|
68
|
+
</div>
|
|
69
|
+
`;
|
|
70
|
+
};
|
|
71
|
+
|
|
72
|
+
return renderer;
|
|
73
|
+
}
|
|
74
|
+
|
|
75
|
+
// 设置自定义 renderer
|
|
76
|
+
marked.use({ renderer: createRenderer() });
|
|
77
|
+
|
|
78
|
+
/**
|
|
79
|
+
* 解析 Markdown 为 HTML
|
|
80
|
+
* @param content Markdown 字符串
|
|
81
|
+
* @returns 解析后的 HTML 字符串
|
|
82
|
+
*/
|
|
83
|
+
export function parseMarkdown(content: string): string {
|
|
84
|
+
if (!content) {
|
|
85
|
+
return '';
|
|
86
|
+
}
|
|
87
|
+
|
|
88
|
+
try {
|
|
89
|
+
return marked.parse(content) as string;
|
|
90
|
+
} catch (error) {
|
|
91
|
+
console.error('Markdown parse error:', error);
|
|
92
|
+
// 降级:转义 HTML 并返回纯文本
|
|
93
|
+
return escapeHtml(content);
|
|
94
|
+
}
|
|
95
|
+
}
|
|
96
|
+
|
|
97
|
+
/**
|
|
98
|
+
* 检测内容是否包含 Markdown 语法
|
|
99
|
+
* @param content 待检测内容
|
|
100
|
+
* @returns 是否包含 Markdown
|
|
101
|
+
*/
|
|
102
|
+
export function detectMarkdown(content: string): boolean {
|
|
103
|
+
if (!content) {
|
|
104
|
+
return false;
|
|
105
|
+
}
|
|
106
|
+
|
|
107
|
+
// 常见的 Markdown 语法正则
|
|
108
|
+
const markdownPatterns = [
|
|
109
|
+
/^#{1,6}\s+.+/m, // 标题
|
|
110
|
+
/`{3}[\s\S]*?`{3}/, // 代码块
|
|
111
|
+
/`[^`]+`/, // 行内代码
|
|
112
|
+
/\*\*[^*]+\*\*/, // 粗体
|
|
113
|
+
/\*[^*]+\*/, // 斜体
|
|
114
|
+
/\[.+?\]\(.+?\)/, // 链接
|
|
115
|
+
/!\[.+?\]\(.+?\)/, // 图片
|
|
116
|
+
/^\s*[-*+]\s+.+/m, // 无序列表
|
|
117
|
+
/^\s*\d+\.\s+.+/m, // 有序列表
|
|
118
|
+
/^\s*>.+/m, // 引用
|
|
119
|
+
/\|(.+)\|/, // 表格
|
|
120
|
+
/^---$/m // 分隔线
|
|
121
|
+
];
|
|
122
|
+
|
|
123
|
+
return markdownPatterns.some(pattern => pattern.test(content));
|
|
124
|
+
}
|
|
125
|
+
|
|
126
|
+
/**
|
|
127
|
+
* 转义 HTML 特殊字符
|
|
128
|
+
*/
|
|
129
|
+
function escapeHtml(text: string): string {
|
|
130
|
+
const map: Record<string, string> = {
|
|
131
|
+
'&': '&',
|
|
132
|
+
'<': '<',
|
|
133
|
+
'>': '>',
|
|
134
|
+
'"': '"',
|
|
135
|
+
"'": '''
|
|
136
|
+
};
|
|
137
|
+
|
|
138
|
+
return text.replace(/[&<>"']/g, char => map[char]);
|
|
139
|
+
}
|
|
140
|
+
|
|
141
|
+
/**
|
|
142
|
+
* 注册全局代码复制函数
|
|
143
|
+
* 需要在浏览器环境中调用
|
|
144
|
+
*/
|
|
145
|
+
export function registerCodeCopyHandler(): void {
|
|
146
|
+
if (typeof window !== 'undefined') {
|
|
147
|
+
window.__copyCode = function(button: HTMLElement) {
|
|
148
|
+
const codeBlock = button.closest('.markdown-code-block');
|
|
149
|
+
if (!codeBlock) return;
|
|
150
|
+
|
|
151
|
+
const code = codeBlock.querySelector('code');
|
|
152
|
+
if (!code) return;
|
|
153
|
+
|
|
154
|
+
const text = code.textContent || '';
|
|
155
|
+
|
|
156
|
+
navigator.clipboard.writeText(text).then(() => {
|
|
157
|
+
const originalText = button.textContent;
|
|
158
|
+
button.textContent = '已复制';
|
|
159
|
+
button.classList.add('copied');
|
|
160
|
+
|
|
161
|
+
setTimeout(() => {
|
|
162
|
+
button.textContent = originalText;
|
|
163
|
+
button.classList.remove('copied');
|
|
164
|
+
}, 2000);
|
|
165
|
+
}).catch(err => {
|
|
166
|
+
console.error('Copy failed:', err);
|
|
167
|
+
});
|
|
168
|
+
};
|
|
169
|
+
}
|
|
170
|
+
}
|
|
171
|
+
|
|
172
|
+
/**
|
|
173
|
+
* 渲染 Markdown 到指定容器
|
|
174
|
+
* @param content Markdown 内容
|
|
175
|
+
* @param container 目标容器元素
|
|
176
|
+
* @returns 渲染后的 HTML 字符串
|
|
177
|
+
*/
|
|
178
|
+
export function renderMarkdownToContainer(content: string, container: HTMLElement): string {
|
|
179
|
+
const html = parseMarkdown(content);
|
|
180
|
+
container.innerHTML = html;
|
|
181
|
+
|
|
182
|
+
// 为代码块添加复制功能
|
|
183
|
+
container.querySelectorAll('.code-copy-btn').forEach(btn => {
|
|
184
|
+
btn.addEventListener('click', function(this: HTMLButtonElement) {
|
|
185
|
+
const codeBlock = this.closest('.markdown-code-block');
|
|
186
|
+
if (!codeBlock) return;
|
|
187
|
+
|
|
188
|
+
const code = codeBlock.querySelector('code');
|
|
189
|
+
if (!code) return;
|
|
190
|
+
|
|
191
|
+
const text = code.textContent || '';
|
|
192
|
+
navigator.clipboard.writeText(text).then(() => {
|
|
193
|
+
const originalText = this.textContent;
|
|
194
|
+
this.textContent = '已复制';
|
|
195
|
+
this.classList.add('copied');
|
|
196
|
+
|
|
197
|
+
setTimeout(() => {
|
|
198
|
+
this.textContent = originalText;
|
|
199
|
+
this.classList.remove('copied');
|
|
200
|
+
}, 2000);
|
|
201
|
+
});
|
|
202
|
+
});
|
|
203
|
+
});
|
|
204
|
+
|
|
205
|
+
return html;
|
|
206
|
+
}
|
|
@@ -0,0 +1,276 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* @generated-by AI: edenxpzhang
|
|
3
|
+
* @generated-date 2026-05-13
|
|
4
|
+
*/
|
|
5
|
+
|
|
6
|
+
/**
|
|
7
|
+
* Mermaid 图表类型
|
|
8
|
+
*/
|
|
9
|
+
export type MermaidChartType =
|
|
10
|
+
| 'graph' // 流程图
|
|
11
|
+
| 'flowchart' // 流图(新语法)
|
|
12
|
+
| 'sequence' // 时序图
|
|
13
|
+
| 'class' // 类图
|
|
14
|
+
| 'state' // 状态图
|
|
15
|
+
| 'er' // ER 图
|
|
16
|
+
| 'journey' // 用户旅程图
|
|
17
|
+
| 'gantt' // 甘特图
|
|
18
|
+
| 'pie' // 饼图
|
|
19
|
+
| 'gitgraph' // Git 图
|
|
20
|
+
| 'mindmap' // 思维导图
|
|
21
|
+
| 'timeline' // 时间线
|
|
22
|
+
| 'unknown'; // 未知类型
|
|
23
|
+
|
|
24
|
+
/**
|
|
25
|
+
* Mermaid 检测结果的接口
|
|
26
|
+
*/
|
|
27
|
+
export interface MermaidDetectionResult {
|
|
28
|
+
isMermaid: boolean;
|
|
29
|
+
chartType: MermaidChartType;
|
|
30
|
+
code: string;
|
|
31
|
+
}
|
|
32
|
+
|
|
33
|
+
/**
|
|
34
|
+
* 检测代码块是否为 Mermaid 图表
|
|
35
|
+
* @param code 待检测的代码字符串
|
|
36
|
+
* @returns 是否为 Mermaid 图表
|
|
37
|
+
*/
|
|
38
|
+
export function detectMermaid(code: string): boolean {
|
|
39
|
+
if (!code || typeof code !== 'string') {
|
|
40
|
+
return false;
|
|
41
|
+
}
|
|
42
|
+
|
|
43
|
+
const trimmed = code.trim();
|
|
44
|
+
|
|
45
|
+
// Mermaid 代码块通常以这些关键字开头
|
|
46
|
+
const mermaidKeywords = [
|
|
47
|
+
'graph', // 流程图(LR, RL, TB, BT 方向)
|
|
48
|
+
'flowchart', // 新流程图语法
|
|
49
|
+
'sequenceDiagram', // 时序图
|
|
50
|
+
'classDiagram', // 类图
|
|
51
|
+
'stateDiagram', // 状态图
|
|
52
|
+
'erDiagram', // ER 图
|
|
53
|
+
'journey', // 用户旅程图
|
|
54
|
+
'gantt', // 甘特图
|
|
55
|
+
'pie', // 饼图
|
|
56
|
+
'gitGraph', // Git 图
|
|
57
|
+
'mindmap', // 思维导图
|
|
58
|
+
'timeline', // 时间线
|
|
59
|
+
'quadrantChart' // 象限图
|
|
60
|
+
];
|
|
61
|
+
|
|
62
|
+
// 检查是否以 Mermaid 关键字开头
|
|
63
|
+
for (const keyword of mermaidKeywords) {
|
|
64
|
+
if (trimmed.startsWith(keyword)) {
|
|
65
|
+
return true;
|
|
66
|
+
}
|
|
67
|
+
}
|
|
68
|
+
|
|
69
|
+
// 检查 graph 方向简写(如 graph TD, graph LR)
|
|
70
|
+
const graphDirectionRegex = /^(graph|flowchart)\s+(TB|TD|BT|RL|LR)/i;
|
|
71
|
+
if (graphDirectionRegex.test(trimmed)) {
|
|
72
|
+
return true;
|
|
73
|
+
}
|
|
74
|
+
|
|
75
|
+
return false;
|
|
76
|
+
}
|
|
77
|
+
|
|
78
|
+
/**
|
|
79
|
+
* 检测 Mermaid 图表类型
|
|
80
|
+
* @param code Mermaid 代码
|
|
81
|
+
* @returns 图表类型
|
|
82
|
+
*/
|
|
83
|
+
export function detectMermaidChartType(code: string): MermaidChartType {
|
|
84
|
+
if (!code || typeof code !== 'string') {
|
|
85
|
+
return 'unknown';
|
|
86
|
+
}
|
|
87
|
+
|
|
88
|
+
const trimmed = code.trim();
|
|
89
|
+
|
|
90
|
+
if (/^(graph|flowchart)/i.test(trimmed)) {
|
|
91
|
+
return 'flowchart';
|
|
92
|
+
}
|
|
93
|
+
|
|
94
|
+
if (/^sequenceDiagram/i.test(trimmed)) {
|
|
95
|
+
return 'sequence';
|
|
96
|
+
}
|
|
97
|
+
|
|
98
|
+
if (/^classDiagram/i.test(trimmed)) {
|
|
99
|
+
return 'class';
|
|
100
|
+
}
|
|
101
|
+
|
|
102
|
+
if (/^stateDiagram/i.test(trimmed)) {
|
|
103
|
+
return 'state';
|
|
104
|
+
}
|
|
105
|
+
|
|
106
|
+
if (/^erDiagram/i.test(trimmed)) {
|
|
107
|
+
return 'er';
|
|
108
|
+
}
|
|
109
|
+
|
|
110
|
+
if (/^journey/i.test(trimmed)) {
|
|
111
|
+
return 'journey';
|
|
112
|
+
}
|
|
113
|
+
|
|
114
|
+
if (/^gantt/i.test(trimmed)) {
|
|
115
|
+
return 'gantt';
|
|
116
|
+
}
|
|
117
|
+
|
|
118
|
+
if (/^pie/i.test(trimmed)) {
|
|
119
|
+
return 'pie';
|
|
120
|
+
}
|
|
121
|
+
|
|
122
|
+
if (/^gitGraph/i.test(trimmed)) {
|
|
123
|
+
return 'gitgraph';
|
|
124
|
+
}
|
|
125
|
+
|
|
126
|
+
if (/^mindmap/i.test(trimmed)) {
|
|
127
|
+
return 'mindmap';
|
|
128
|
+
}
|
|
129
|
+
|
|
130
|
+
if (/^timeline/i.test(trimmed)) {
|
|
131
|
+
return 'timeline';
|
|
132
|
+
}
|
|
133
|
+
|
|
134
|
+
return 'unknown';
|
|
135
|
+
}
|
|
136
|
+
|
|
137
|
+
/**
|
|
138
|
+
* 从 Markdown 内容中提取 Mermaid 代码块
|
|
139
|
+
* @param content Markdown 内容
|
|
140
|
+
* @returns 提取的 Mermaid 代码数组
|
|
141
|
+
*/
|
|
142
|
+
export function extractMermaidFromMarkdown(content: string): string[] {
|
|
143
|
+
if (!content) {
|
|
144
|
+
return [];
|
|
145
|
+
}
|
|
146
|
+
|
|
147
|
+
const mermaidBlocks: string[] = [];
|
|
148
|
+
|
|
149
|
+
// 匹配 ```mermaid ... ``` 代码块
|
|
150
|
+
const mermaidRegex = /```mermaid\s*([\s\S]*?)```/gi;
|
|
151
|
+
let match: RegExpExecArray | null;
|
|
152
|
+
|
|
153
|
+
while ((match = mermaidRegex.exec(content)) !== null) {
|
|
154
|
+
if (match[1]) {
|
|
155
|
+
mermaidBlocks.push(match[1].trim());
|
|
156
|
+
}
|
|
157
|
+
}
|
|
158
|
+
|
|
159
|
+
return mermaidBlocks;
|
|
160
|
+
}
|
|
161
|
+
|
|
162
|
+
/**
|
|
163
|
+
* 渲染 Mermaid 图表到指定容器
|
|
164
|
+
* @param code Mermaid 代码
|
|
165
|
+
* @param container 目标容器元素
|
|
166
|
+
* @param options 可选配置
|
|
167
|
+
*/
|
|
168
|
+
export function renderMermaid(
|
|
169
|
+
code: string,
|
|
170
|
+
container: HTMLElement,
|
|
171
|
+
options?: {
|
|
172
|
+
theme?: 'default' | 'dark' | 'forest' | 'neutral';
|
|
173
|
+
backgroundColor?: string;
|
|
174
|
+
scale?: number;
|
|
175
|
+
}
|
|
176
|
+
): void {
|
|
177
|
+
if (!code || !container) {
|
|
178
|
+
console.error('renderMermaid: code or container is missing');
|
|
179
|
+
return;
|
|
180
|
+
}
|
|
181
|
+
|
|
182
|
+
// 检查 mermaid 是否可用
|
|
183
|
+
if (typeof window === 'undefined') {
|
|
184
|
+
console.error('renderMermaid: only works in browser environment');
|
|
185
|
+
return;
|
|
186
|
+
}
|
|
187
|
+
|
|
188
|
+
// 动态导入 mermaid(避免 SSR 问题)
|
|
189
|
+
import('mermaid')
|
|
190
|
+
.then((mermaid) => {
|
|
191
|
+
// 初始化 mermaid
|
|
192
|
+
mermaid.default.initialize({
|
|
193
|
+
startOnLoad: false,
|
|
194
|
+
theme: options?.theme || 'default',
|
|
195
|
+
securityLevel: 'loose',
|
|
196
|
+
flowchart: { useMaxWidth: true, htmlLabels: true },
|
|
197
|
+
...options
|
|
198
|
+
});
|
|
199
|
+
|
|
200
|
+
// 生成唯一 ID
|
|
201
|
+
const id = `mermaid-${Date.now()}-${Math.random().toString(36).substr(2, 9)}`;
|
|
202
|
+
|
|
203
|
+
// 创建渲染容器
|
|
204
|
+
const wrapper = document.createElement('div');
|
|
205
|
+
wrapper.className = 'mermaid-chart-wrapper';
|
|
206
|
+
wrapper.style.overflow = 'auto';
|
|
207
|
+
|
|
208
|
+
const diagramDiv = document.createElement('div');
|
|
209
|
+
diagramDiv.className = 'mermaid-chart';
|
|
210
|
+
diagramDiv.id = id;
|
|
211
|
+
|
|
212
|
+
// 设置 mermaid 代码
|
|
213
|
+
diagramDiv.textContent = code;
|
|
214
|
+
|
|
215
|
+
wrapper.appendChild(diagramDiv);
|
|
216
|
+
container.appendChild(wrapper);
|
|
217
|
+
|
|
218
|
+
// 渲染
|
|
219
|
+
mermaid.default.run({
|
|
220
|
+
querySelector: `#${id}`
|
|
221
|
+
}).catch((err: Error) => {
|
|
222
|
+
console.error('Mermaid render error:', err);
|
|
223
|
+
diagramDiv.innerHTML = `<pre style="color:red;">Mermaid 渲染失败: ${err.message}</pre>`;
|
|
224
|
+
});
|
|
225
|
+
})
|
|
226
|
+
.catch((err) => {
|
|
227
|
+
console.error('Failed to load mermaid:', err);
|
|
228
|
+
container.innerHTML = `<pre style="color:red;">无法加载 Mermaid 库</pre>`;
|
|
229
|
+
});
|
|
230
|
+
}
|
|
231
|
+
|
|
232
|
+
/**
|
|
233
|
+
* 批量渲染容器中的所有 Mermaid 代码块
|
|
234
|
+
* @param container 容器元素
|
|
235
|
+
*/
|
|
236
|
+
export function renderAllMermaidInContainer(container: HTMLElement): void {
|
|
237
|
+
if (!container) {
|
|
238
|
+
return;
|
|
239
|
+
}
|
|
240
|
+
|
|
241
|
+
// 查找所有 class 包含 language-mermaid 的 code 元素
|
|
242
|
+
const mermaidElements = container.querySelectorAll('code.language-mermaid, code.language-flow');
|
|
243
|
+
|
|
244
|
+
mermaidElements.forEach((el) => {
|
|
245
|
+
const code = el.textContent || '';
|
|
246
|
+
const parent = el.closest('pre');
|
|
247
|
+
|
|
248
|
+
if (parent) {
|
|
249
|
+
const wrapper = document.createElement('div');
|
|
250
|
+
wrapper.className = 'mermaid-chart-wrapper';
|
|
251
|
+
|
|
252
|
+
parent.parentNode?.replaceChild(wrapper, parent);
|
|
253
|
+
renderMermaid(code, wrapper);
|
|
254
|
+
}
|
|
255
|
+
});
|
|
256
|
+
}
|
|
257
|
+
|
|
258
|
+
/**
|
|
259
|
+
* 验证 Mermaid 代码语法
|
|
260
|
+
* @param code Mermaid 代码
|
|
261
|
+
* @returns 是否有效
|
|
262
|
+
*/
|
|
263
|
+
export async function validateMermaid(code: string): Promise<boolean> {
|
|
264
|
+
if (!code) {
|
|
265
|
+
return false;
|
|
266
|
+
}
|
|
267
|
+
|
|
268
|
+
try {
|
|
269
|
+
const mermaid = await import('mermaid');
|
|
270
|
+
await mermaid.default.parse(code);
|
|
271
|
+
return true;
|
|
272
|
+
} catch (error) {
|
|
273
|
+
console.error('Mermaid validation error:', error);
|
|
274
|
+
return false;
|
|
275
|
+
}
|
|
276
|
+
}
|
|
@@ -0,0 +1,232 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* @generated-by AI: edenxpzhang
|
|
3
|
+
* @generated-date 2026-05-13
|
|
4
|
+
*/
|
|
5
|
+
|
|
6
|
+
import { MessagePlugin, Message, RenderContext } from '../types/index.js';
|
|
7
|
+
|
|
8
|
+
/**
|
|
9
|
+
* 插件管理器
|
|
10
|
+
* 负责管理所有消息插件,包括注册、注销、匹配和渲染
|
|
11
|
+
*/
|
|
12
|
+
export class PluginManager {
|
|
13
|
+
private plugins: Map<string, MessagePlugin> = new Map();
|
|
14
|
+
private renderOrder: string[] = []; // 渲染顺序(按优先级)
|
|
15
|
+
|
|
16
|
+
/**
|
|
17
|
+
* 注册插件
|
|
18
|
+
* @param plugin 插件实例
|
|
19
|
+
*/
|
|
20
|
+
register(plugin: MessagePlugin): void {
|
|
21
|
+
if (!plugin || !plugin.type) {
|
|
22
|
+
console.error('PluginManager: Cannot register invalid plugin', plugin);
|
|
23
|
+
return;
|
|
24
|
+
}
|
|
25
|
+
|
|
26
|
+
if (this.plugins.has(plugin.type)) {
|
|
27
|
+
console.warn(`PluginManager: Plugin "${plugin.type}" is already registered, overwriting`);
|
|
28
|
+
}
|
|
29
|
+
|
|
30
|
+
this.plugins.set(plugin.type, plugin);
|
|
31
|
+
this.updateRenderOrder();
|
|
32
|
+
}
|
|
33
|
+
|
|
34
|
+
/**
|
|
35
|
+
* 注销插件
|
|
36
|
+
* @param type 插件类型标识
|
|
37
|
+
*/
|
|
38
|
+
unregister(type: string): void {
|
|
39
|
+
if (this.plugins.has(type)) {
|
|
40
|
+
this.plugins.delete(type);
|
|
41
|
+
this.updateRenderOrder();
|
|
42
|
+
}
|
|
43
|
+
}
|
|
44
|
+
|
|
45
|
+
/**
|
|
46
|
+
* 获取指定类型的插件
|
|
47
|
+
* @param type 插件类型标识
|
|
48
|
+
* @returns 插件实例或 undefined
|
|
49
|
+
*/
|
|
50
|
+
getPlugin(type: string): MessagePlugin | undefined {
|
|
51
|
+
return this.plugins.get(type);
|
|
52
|
+
}
|
|
53
|
+
|
|
54
|
+
/**
|
|
55
|
+
* 获取所有已注册的插件
|
|
56
|
+
* @returns 插件数组
|
|
57
|
+
*/
|
|
58
|
+
getAllPlugins(): MessagePlugin[] {
|
|
59
|
+
return Array.from(this.plugins.values());
|
|
60
|
+
}
|
|
61
|
+
|
|
62
|
+
/**
|
|
63
|
+
* 根据内容匹配最合适的插件(按优先级排序)
|
|
64
|
+
* @param content 消息内容
|
|
65
|
+
* @returns 匹配的插件或 null
|
|
66
|
+
*/
|
|
67
|
+
matchPlugin(content: string): MessagePlugin | null {
|
|
68
|
+
if (!content || typeof content !== 'string') {
|
|
69
|
+
return null;
|
|
70
|
+
}
|
|
71
|
+
|
|
72
|
+
const matchedPlugins: Array<{ plugin: MessagePlugin; priority: number }> = [];
|
|
73
|
+
|
|
74
|
+
for (const plugin of this.plugins.values()) {
|
|
75
|
+
if (plugin.match && typeof plugin.match === 'function') {
|
|
76
|
+
try {
|
|
77
|
+
const isMatch = plugin.match(content);
|
|
78
|
+
if (isMatch) {
|
|
79
|
+
matchedPlugins.push({
|
|
80
|
+
plugin,
|
|
81
|
+
priority: plugin.priority || 0
|
|
82
|
+
});
|
|
83
|
+
}
|
|
84
|
+
} catch (error) {
|
|
85
|
+
console.error(`PluginManager: Error in plugin "${plugin.type}" match function:`, error);
|
|
86
|
+
}
|
|
87
|
+
}
|
|
88
|
+
}
|
|
89
|
+
|
|
90
|
+
// 按优先级排序(数字越大优先级越高)
|
|
91
|
+
matchedPlugins.sort((a, b) => b.priority - a.priority);
|
|
92
|
+
|
|
93
|
+
return matchedPlugins.length > 0 ? matchedPlugins[0].plugin : null;
|
|
94
|
+
}
|
|
95
|
+
|
|
96
|
+
/**
|
|
97
|
+
* 匹配所有符合条件的插件(按优先级排序)
|
|
98
|
+
* @param content 消息内容
|
|
99
|
+
* @returns 所有匹配的插件数组
|
|
100
|
+
*/
|
|
101
|
+
matchAllPlugins(content: string): MessagePlugin[] {
|
|
102
|
+
if (!content || typeof content !== 'string') {
|
|
103
|
+
return [];
|
|
104
|
+
}
|
|
105
|
+
|
|
106
|
+
const matchedPlugins: Array<{ plugin: MessagePlugin; priority: number }> = [];
|
|
107
|
+
|
|
108
|
+
for (const plugin of this.plugins.values()) {
|
|
109
|
+
if (plugin.match && typeof plugin.match === 'function') {
|
|
110
|
+
try {
|
|
111
|
+
const isMatch = plugin.match(content);
|
|
112
|
+
if (isMatch) {
|
|
113
|
+
matchedPlugins.push({
|
|
114
|
+
plugin,
|
|
115
|
+
priority: plugin.priority || 0
|
|
116
|
+
});
|
|
117
|
+
}
|
|
118
|
+
} catch (error) {
|
|
119
|
+
console.error(`PluginManager: Error in plugin "${plugin.type}" match function:`, error);
|
|
120
|
+
}
|
|
121
|
+
}
|
|
122
|
+
}
|
|
123
|
+
|
|
124
|
+
// 按优先级排序
|
|
125
|
+
matchedPlugins.sort((a, b) => b.priority - a.priority);
|
|
126
|
+
|
|
127
|
+
return matchedPlugins.map(item => item.plugin);
|
|
128
|
+
}
|
|
129
|
+
|
|
130
|
+
/**
|
|
131
|
+
* 使用匹配的插件渲染内容
|
|
132
|
+
* @param content 消息内容
|
|
133
|
+
* @param context 渲染上下文
|
|
134
|
+
* @returns 渲染结果(HTMLElement 或 HTML 字符串)
|
|
135
|
+
*/
|
|
136
|
+
renderWithPlugin(content: string, context: RenderContext): HTMLElement | string {
|
|
137
|
+
const plugin = this.matchPlugin(content);
|
|
138
|
+
|
|
139
|
+
if (plugin && plugin.render) {
|
|
140
|
+
try {
|
|
141
|
+
return plugin.render(content, context);
|
|
142
|
+
} catch (error) {
|
|
143
|
+
console.error(`PluginManager: Error in plugin "${plugin.type}" render function:`, error);
|
|
144
|
+
return content; // 降级返回原始内容
|
|
145
|
+
}
|
|
146
|
+
}
|
|
147
|
+
|
|
148
|
+
// 没有匹配的插件,返回原始内容
|
|
149
|
+
return content;
|
|
150
|
+
}
|
|
151
|
+
|
|
152
|
+
/**
|
|
153
|
+
* 渲染所有匹配的插件(用于一个消息需要多个插件处理的情况)
|
|
154
|
+
* @param content 消息内容
|
|
155
|
+
* @param context 渲染上下文
|
|
156
|
+
* @returns 渲染结果数组
|
|
157
|
+
*/
|
|
158
|
+
renderWithAllPlugins(content: string, context: RenderContext): Array<{ plugin: MessagePlugin; result: HTMLElement | string }> {
|
|
159
|
+
const plugins = this.matchAllPlugins(content);
|
|
160
|
+
const results: Array<{ plugin: MessagePlugin; result: HTMLElement | string }> = [];
|
|
161
|
+
|
|
162
|
+
for (const plugin of plugins) {
|
|
163
|
+
if (plugin.render) {
|
|
164
|
+
try {
|
|
165
|
+
const result = plugin.render(content, context);
|
|
166
|
+
results.push({ plugin, result });
|
|
167
|
+
} catch (error) {
|
|
168
|
+
console.error(`PluginManager: Error in plugin "${plugin.type}" render function:`, error);
|
|
169
|
+
}
|
|
170
|
+
}
|
|
171
|
+
}
|
|
172
|
+
|
|
173
|
+
return results;
|
|
174
|
+
}
|
|
175
|
+
|
|
176
|
+
/**
|
|
177
|
+
* 清除所有插件
|
|
178
|
+
*/
|
|
179
|
+
clear(): void {
|
|
180
|
+
this.plugins.clear();
|
|
181
|
+
this.renderOrder = [];
|
|
182
|
+
}
|
|
183
|
+
|
|
184
|
+
/**
|
|
185
|
+
* 获取已注册插件的数量
|
|
186
|
+
*/
|
|
187
|
+
get size(): number {
|
|
188
|
+
return this.plugins.size;
|
|
189
|
+
}
|
|
190
|
+
|
|
191
|
+
/**
|
|
192
|
+
* 检查是否注册了指定类型的插件
|
|
193
|
+
* @param type 插件类型标识
|
|
194
|
+
*/
|
|
195
|
+
hasPlugin(type: string): boolean {
|
|
196
|
+
return this.plugins.has(type);
|
|
197
|
+
}
|
|
198
|
+
|
|
199
|
+
/**
|
|
200
|
+
* 更新渲染顺序(按优先级排序)
|
|
201
|
+
*/
|
|
202
|
+
private updateRenderOrder(): void {
|
|
203
|
+
const sorted = Array.from(this.plugins.entries())
|
|
204
|
+
.sort((a, b) => {
|
|
205
|
+
const priorityA = a[1].priority || 0;
|
|
206
|
+
const priorityB = b[1].priority || 0;
|
|
207
|
+
return priorityB - priorityA; // 降序
|
|
208
|
+
})
|
|
209
|
+
.map(([type]) => type);
|
|
210
|
+
|
|
211
|
+
this.renderOrder = sorted;
|
|
212
|
+
}
|
|
213
|
+
}
|
|
214
|
+
|
|
215
|
+
/**
|
|
216
|
+
* 创建插件管理器的单例
|
|
217
|
+
*/
|
|
218
|
+
let defaultPluginManager: PluginManager | null = null;
|
|
219
|
+
|
|
220
|
+
export function getDefaultPluginManager(): PluginManager {
|
|
221
|
+
if (!defaultPluginManager) {
|
|
222
|
+
defaultPluginManager = new PluginManager();
|
|
223
|
+
}
|
|
224
|
+
return defaultPluginManager;
|
|
225
|
+
}
|
|
226
|
+
|
|
227
|
+
export function resetDefaultPluginManager(): void {
|
|
228
|
+
if (defaultPluginManager) {
|
|
229
|
+
defaultPluginManager.clear();
|
|
230
|
+
}
|
|
231
|
+
defaultPluginManager = new PluginManager();
|
|
232
|
+
}
|