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,406 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* @generated-by AI: edenxpzhang
|
|
3
|
+
* @generated-date 2026-05-13
|
|
4
|
+
*/
|
|
5
|
+
|
|
6
|
+
import { MessagePlugin, Message, RenderContext, ToolCall } from '../types/index.js';
|
|
7
|
+
import { detectMermaid } from '../parser/mermaid.js';
|
|
8
|
+
import { detectLatex, renderLatex } from '../parser/latex.js';
|
|
9
|
+
import { parseMarkdown } from '../parser/markdown.js';
|
|
10
|
+
|
|
11
|
+
/**
|
|
12
|
+
* 代码块插件
|
|
13
|
+
* 支持代码高亮、复制按钮、语言标签
|
|
14
|
+
*/
|
|
15
|
+
export class CodeBlockPlugin implements MessagePlugin {
|
|
16
|
+
type = 'code-block';
|
|
17
|
+
priority = 10;
|
|
18
|
+
|
|
19
|
+
/**
|
|
20
|
+
* 判断是否匹配代码块
|
|
21
|
+
*/
|
|
22
|
+
match(content: string): boolean {
|
|
23
|
+
if (!content || typeof content !== 'string') {
|
|
24
|
+
return false;
|
|
25
|
+
}
|
|
26
|
+
|
|
27
|
+
// 检测 ``` 代码块
|
|
28
|
+
const codeBlockRegex = /```[\s\S]*?```/;
|
|
29
|
+
return codeBlockRegex.test(content);
|
|
30
|
+
}
|
|
31
|
+
|
|
32
|
+
/**
|
|
33
|
+
* 渲染代码块
|
|
34
|
+
*/
|
|
35
|
+
render(content: string, context: RenderContext): HTMLElement | string {
|
|
36
|
+
if (!content) {
|
|
37
|
+
return '';
|
|
38
|
+
}
|
|
39
|
+
|
|
40
|
+
// 使用 marked 解析 Markdown(它会自动处理代码块高亮)
|
|
41
|
+
const html = parseMarkdown(content);
|
|
42
|
+
|
|
43
|
+
// 创建容器
|
|
44
|
+
const container = document.createElement('div');
|
|
45
|
+
container.className = 'plugin-code-block';
|
|
46
|
+
container.innerHTML = html;
|
|
47
|
+
|
|
48
|
+
// 为代码块添加复制按钮和语言标签
|
|
49
|
+
this.enhanceCodeBlocks(container);
|
|
50
|
+
|
|
51
|
+
return container;
|
|
52
|
+
}
|
|
53
|
+
|
|
54
|
+
/**
|
|
55
|
+
* 增强代码块(添加复制按钮等)
|
|
56
|
+
*/
|
|
57
|
+
private enhanceCodeBlocks(container: HTMLElement): void {
|
|
58
|
+
const codeBlocks = container.querySelectorAll('pre code');
|
|
59
|
+
|
|
60
|
+
codeBlocks.forEach((codeEl) => {
|
|
61
|
+
const pre = codeEl.closest('pre');
|
|
62
|
+
if (!pre) return;
|
|
63
|
+
|
|
64
|
+
// 创建包装器
|
|
65
|
+
const wrapper = document.createElement('div');
|
|
66
|
+
wrapper.className = 'code-block-wrapper';
|
|
67
|
+
|
|
68
|
+
// 获取语言
|
|
69
|
+
const classes = codeEl.className;
|
|
70
|
+
const langMatch = classes.match(/language-(\S+)/);
|
|
71
|
+
const language = langMatch ? langMatch[1] : 'plaintext';
|
|
72
|
+
|
|
73
|
+
// 创建头部
|
|
74
|
+
const header = document.createElement('div');
|
|
75
|
+
header.className = 'code-block-header';
|
|
76
|
+
|
|
77
|
+
const langLabel = document.createElement('span');
|
|
78
|
+
langLabel.className = 'code-block-lang';
|
|
79
|
+
langLabel.textContent = language;
|
|
80
|
+
|
|
81
|
+
const copyBtn = document.createElement('button');
|
|
82
|
+
copyBtn.className = 'code-block-copy-btn';
|
|
83
|
+
copyBtn.textContent = '复制';
|
|
84
|
+
copyBtn.addEventListener('click', () => {
|
|
85
|
+
const code = codeEl.textContent || '';
|
|
86
|
+
navigator.clipboard.writeText(code).then(() => {
|
|
87
|
+
copyBtn.textContent = '已复制';
|
|
88
|
+
setTimeout(() => {
|
|
89
|
+
copyBtn.textContent = '复制';
|
|
90
|
+
}, 2000);
|
|
91
|
+
});
|
|
92
|
+
});
|
|
93
|
+
|
|
94
|
+
header.appendChild(langLabel);
|
|
95
|
+
header.appendChild(copyBtn);
|
|
96
|
+
|
|
97
|
+
// 重新组织 DOM
|
|
98
|
+
pre.parentNode?.insertBefore(wrapper, pre);
|
|
99
|
+
wrapper.appendChild(header);
|
|
100
|
+
wrapper.appendChild(pre);
|
|
101
|
+
});
|
|
102
|
+
}
|
|
103
|
+
}
|
|
104
|
+
|
|
105
|
+
/**
|
|
106
|
+
* Mermaid 图表插件
|
|
107
|
+
* 检测并渲染 Mermaid 图表
|
|
108
|
+
*/
|
|
109
|
+
export class MermaidPlugin implements MessagePlugin {
|
|
110
|
+
type = 'mermaid';
|
|
111
|
+
priority = 20;
|
|
112
|
+
|
|
113
|
+
/**
|
|
114
|
+
* 判断是否包含 Mermaid 图表
|
|
115
|
+
*/
|
|
116
|
+
match(content: string): boolean {
|
|
117
|
+
if (!content || typeof content !== 'string') {
|
|
118
|
+
return false;
|
|
119
|
+
}
|
|
120
|
+
|
|
121
|
+
// 检测 ```mermaid 代码块
|
|
122
|
+
const mermaidBlockRegex = /```mermaid[\s\S]*?```/i;
|
|
123
|
+
if (mermaidBlockRegex.test(content)) {
|
|
124
|
+
return true;
|
|
125
|
+
}
|
|
126
|
+
|
|
127
|
+
// 或使用专门的 mermaid 检测函数
|
|
128
|
+
return detectMermaid(content);
|
|
129
|
+
}
|
|
130
|
+
|
|
131
|
+
/**
|
|
132
|
+
* 渲染 Mermaid 图表
|
|
133
|
+
*/
|
|
134
|
+
render(content: string, context: RenderContext): HTMLElement | string {
|
|
135
|
+
if (!content) {
|
|
136
|
+
return '';
|
|
137
|
+
}
|
|
138
|
+
|
|
139
|
+
const container = document.createElement('div');
|
|
140
|
+
container.className = 'plugin-mermaid';
|
|
141
|
+
|
|
142
|
+
// 提取所有 mermaid 代码块
|
|
143
|
+
const mermaidRegex = /```mermaid\s*([\s\S]*?)```/gi;
|
|
144
|
+
let match: RegExpExecArray | null;
|
|
145
|
+
let remainingContent = content;
|
|
146
|
+
|
|
147
|
+
const parts: Array<{ type: 'text' | 'mermaid'; content: string }> = [];
|
|
148
|
+
|
|
149
|
+
while ((match = mermaidRegex.exec(content)) !== null) {
|
|
150
|
+
const mermaidCode = match[1]?.trim() || '';
|
|
151
|
+
|
|
152
|
+
// 添加 mermaid 之前的文本
|
|
153
|
+
const textBefore = content.substring(0, match.index);
|
|
154
|
+
if (textBefore) {
|
|
155
|
+
parts.push({ type: 'text', content: textBefore });
|
|
156
|
+
}
|
|
157
|
+
|
|
158
|
+
// 添加 mermaid 代码
|
|
159
|
+
parts.push({ type: 'mermaid', content: mermaidCode });
|
|
160
|
+
|
|
161
|
+
// 更新剩余内容
|
|
162
|
+
remainingContent = content.substring(match.index + match[0].length);
|
|
163
|
+
}
|
|
164
|
+
|
|
165
|
+
// 添加剩余文本
|
|
166
|
+
if (remainingContent) {
|
|
167
|
+
parts.push({ type: 'text', content: remainingContent });
|
|
168
|
+
}
|
|
169
|
+
|
|
170
|
+
// 渲染各部分
|
|
171
|
+
parts.forEach(part => {
|
|
172
|
+
if (part.type === 'text') {
|
|
173
|
+
const textHtml = parseMarkdown(part.content);
|
|
174
|
+
const textDiv = document.createElement('div');
|
|
175
|
+
textDiv.innerHTML = textHtml;
|
|
176
|
+
container.appendChild(textDiv);
|
|
177
|
+
} else if (part.type === 'mermaid') {
|
|
178
|
+
const mermaidContainer = document.createElement('div');
|
|
179
|
+
mermaidContainer.className = 'mermaid-render-area';
|
|
180
|
+
container.appendChild(mermaidContainer);
|
|
181
|
+
|
|
182
|
+
// 动态导入并渲染 mermaid
|
|
183
|
+
this.renderMermaidDiagram(part.content, mermaidContainer);
|
|
184
|
+
}
|
|
185
|
+
});
|
|
186
|
+
|
|
187
|
+
return container;
|
|
188
|
+
}
|
|
189
|
+
|
|
190
|
+
/**
|
|
191
|
+
* 渲染 Mermaid 图表
|
|
192
|
+
*/
|
|
193
|
+
private renderMermaidDiagram(code: string, container: HTMLElement): void {
|
|
194
|
+
import('mermaid')
|
|
195
|
+
.then((mermaid) => {
|
|
196
|
+
mermaid.default.initialize({
|
|
197
|
+
startOnLoad: false,
|
|
198
|
+
theme: 'default',
|
|
199
|
+
securityLevel: 'loose'
|
|
200
|
+
});
|
|
201
|
+
|
|
202
|
+
const id = `mermaid-${Date.now()}-${Math.random().toString(36).substr(2, 9)}`;
|
|
203
|
+
container.id = id;
|
|
204
|
+
container.textContent = code;
|
|
205
|
+
|
|
206
|
+
mermaid.default.run({ querySelector: `#${id}` });
|
|
207
|
+
})
|
|
208
|
+
.catch((err) => {
|
|
209
|
+
console.error('Failed to load mermaid:', err);
|
|
210
|
+
container.innerHTML = `<pre style="color:red;">Mermaid 渲染失败</pre>`;
|
|
211
|
+
});
|
|
212
|
+
}
|
|
213
|
+
}
|
|
214
|
+
|
|
215
|
+
/**
|
|
216
|
+
* Latex 公式插件
|
|
217
|
+
* 检测并渲染 Latex 公式
|
|
218
|
+
*/
|
|
219
|
+
export class LatexPlugin implements MessagePlugin {
|
|
220
|
+
type = 'latex';
|
|
221
|
+
priority = 15;
|
|
222
|
+
|
|
223
|
+
/**
|
|
224
|
+
* 判断是否包含 Latex 公式
|
|
225
|
+
*/
|
|
226
|
+
match(content: string): boolean {
|
|
227
|
+
if (!content || typeof content !== 'string') {
|
|
228
|
+
return false;
|
|
229
|
+
}
|
|
230
|
+
|
|
231
|
+
return detectLatex(content).inline.length > 0 || detectLatex(content).block.length > 0;
|
|
232
|
+
}
|
|
233
|
+
|
|
234
|
+
/**
|
|
235
|
+
* 渲染 Latex 公式
|
|
236
|
+
*/
|
|
237
|
+
render(content: string, context: RenderContext): HTMLElement | string {
|
|
238
|
+
if (!content) {
|
|
239
|
+
return '';
|
|
240
|
+
}
|
|
241
|
+
|
|
242
|
+
// 使用 KaTeX 或 MathJax 渲染
|
|
243
|
+
const container = document.createElement('div');
|
|
244
|
+
container.className = 'plugin-latex';
|
|
245
|
+
|
|
246
|
+
// 先解析 Markdown
|
|
247
|
+
let html = parseMarkdown(content);
|
|
248
|
+
|
|
249
|
+
// 替换 Latex 占位符为渲染后的公式
|
|
250
|
+
// 这里需要外部库(KaTeX/MathJax)支持
|
|
251
|
+
// 返回带有 class 标记的 HTML,供外部渲染
|
|
252
|
+
container.innerHTML = renderLatex(html);
|
|
253
|
+
|
|
254
|
+
// 尝试使用 KaTeX 渲染
|
|
255
|
+
this.renderWithKaTeX(container);
|
|
256
|
+
|
|
257
|
+
return container;
|
|
258
|
+
}
|
|
259
|
+
|
|
260
|
+
/**
|
|
261
|
+
* 使用 KaTeX 渲染
|
|
262
|
+
*/
|
|
263
|
+
private renderWithKaTeX(container: HTMLElement): void {
|
|
264
|
+
import('katex')
|
|
265
|
+
.then((katex) => {
|
|
266
|
+
// 渲染块级公式
|
|
267
|
+
const blockFormulas = container.querySelectorAll('.latex-block');
|
|
268
|
+
blockFormulas.forEach(el => {
|
|
269
|
+
if (!(el instanceof HTMLElement)) return;
|
|
270
|
+
const formula = el.textContent || '';
|
|
271
|
+
try {
|
|
272
|
+
katex.default.render(formula.replace(/^\$|\$$/g, ''), el, {
|
|
273
|
+
displayMode: true,
|
|
274
|
+
throwOnError: false
|
|
275
|
+
});
|
|
276
|
+
} catch (err) {
|
|
277
|
+
console.error('KaTeX block render error:', err);
|
|
278
|
+
}
|
|
279
|
+
});
|
|
280
|
+
|
|
281
|
+
// 渲染行内公式
|
|
282
|
+
const inlineFormulas = container.querySelectorAll('.latex-inline');
|
|
283
|
+
inlineFormulas.forEach(el => {
|
|
284
|
+
if (!(el instanceof HTMLElement)) return;
|
|
285
|
+
const formula = el.textContent || '';
|
|
286
|
+
try {
|
|
287
|
+
katex.default.render(formula.replace(/^\$|\$$/g, ''), el, {
|
|
288
|
+
displayMode: false,
|
|
289
|
+
throwOnError: false
|
|
290
|
+
});
|
|
291
|
+
} catch (err) {
|
|
292
|
+
console.error('KaTeX inline render error:', err);
|
|
293
|
+
}
|
|
294
|
+
});
|
|
295
|
+
})
|
|
296
|
+
.catch((err) => {
|
|
297
|
+
console.warn('KaTeX not loaded:', err);
|
|
298
|
+
});
|
|
299
|
+
}
|
|
300
|
+
}
|
|
301
|
+
|
|
302
|
+
/**
|
|
303
|
+
* 工具调用插件
|
|
304
|
+
* 渲染工具调用状态和结果
|
|
305
|
+
*/
|
|
306
|
+
export class ToolCallPlugin implements MessagePlugin {
|
|
307
|
+
type = 'tool-call';
|
|
308
|
+
priority = 5;
|
|
309
|
+
|
|
310
|
+
/**
|
|
311
|
+
* 判断是否包含工具调用
|
|
312
|
+
*/
|
|
313
|
+
match(content: string): boolean {
|
|
314
|
+
// 这个插件主要通过 ToolCall 数据触发,而非内容匹配
|
|
315
|
+
return false;
|
|
316
|
+
}
|
|
317
|
+
|
|
318
|
+
/**
|
|
319
|
+
* 渲染工具调用
|
|
320
|
+
*/
|
|
321
|
+
render(content: string, context: RenderContext): HTMLElement | string {
|
|
322
|
+
const container = document.createElement('div');
|
|
323
|
+
container.className = 'plugin-tool-call';
|
|
324
|
+
|
|
325
|
+
const message = context.message;
|
|
326
|
+
if (!message || !message.toolCalls || message.toolCalls.length === 0) {
|
|
327
|
+
return '';
|
|
328
|
+
}
|
|
329
|
+
|
|
330
|
+
// 渲染每个工具调用
|
|
331
|
+
message.toolCalls.forEach(toolCall => {
|
|
332
|
+
const toolElement = this.renderToolCall(toolCall);
|
|
333
|
+
container.appendChild(toolElement);
|
|
334
|
+
});
|
|
335
|
+
|
|
336
|
+
return container;
|
|
337
|
+
}
|
|
338
|
+
|
|
339
|
+
/**
|
|
340
|
+
* 渲染单个工具调用
|
|
341
|
+
*/
|
|
342
|
+
private renderToolCall(toolCall: ToolCall): HTMLElement {
|
|
343
|
+
const wrapper = document.createElement('div');
|
|
344
|
+
wrapper.className = `tool-call tool-call--${toolCall.status}`;
|
|
345
|
+
|
|
346
|
+
// 头部:工具名称和状态
|
|
347
|
+
const header = document.createElement('div');
|
|
348
|
+
header.className = 'tool-call__header';
|
|
349
|
+
|
|
350
|
+
const name = document.createElement('span');
|
|
351
|
+
name.className = 'tool-call__name';
|
|
352
|
+
name.textContent = toolCall.name;
|
|
353
|
+
|
|
354
|
+
const status = document.createElement('span');
|
|
355
|
+
status.className = 'tool-call__status';
|
|
356
|
+
status.textContent = this.getStatusText(toolCall.status);
|
|
357
|
+
|
|
358
|
+
header.appendChild(name);
|
|
359
|
+
header.appendChild(status);
|
|
360
|
+
|
|
361
|
+
// 参数
|
|
362
|
+
const args = document.createElement('pre');
|
|
363
|
+
args.className = 'tool-call__args';
|
|
364
|
+
args.textContent = JSON.stringify(toolCall.arguments, null, 2);
|
|
365
|
+
|
|
366
|
+
wrapper.appendChild(header);
|
|
367
|
+
wrapper.appendChild(args);
|
|
368
|
+
|
|
369
|
+
// 结果(如果有)
|
|
370
|
+
if (toolCall.result !== undefined) {
|
|
371
|
+
const result = document.createElement('pre');
|
|
372
|
+
result.className = 'tool-call__result';
|
|
373
|
+
result.textContent = typeof toolCall.result === 'string'
|
|
374
|
+
? toolCall.result
|
|
375
|
+
: JSON.stringify(toolCall.result, null, 2);
|
|
376
|
+
wrapper.appendChild(result);
|
|
377
|
+
}
|
|
378
|
+
|
|
379
|
+
return wrapper;
|
|
380
|
+
}
|
|
381
|
+
|
|
382
|
+
/**
|
|
383
|
+
* 获取状态文本
|
|
384
|
+
*/
|
|
385
|
+
private getStatusText(status: ToolCall['status']): string {
|
|
386
|
+
const statusMap: Record<ToolCall['status'], string> = {
|
|
387
|
+
pending: '等待中',
|
|
388
|
+
executing: '执行中',
|
|
389
|
+
completed: '已完成',
|
|
390
|
+
failed: '失败'
|
|
391
|
+
};
|
|
392
|
+
|
|
393
|
+
return statusMap[status] || status;
|
|
394
|
+
}
|
|
395
|
+
}
|
|
396
|
+
|
|
397
|
+
/**
|
|
398
|
+
* 注册所有内置插件到插件管理器
|
|
399
|
+
* @param manager 插件管理器实例
|
|
400
|
+
*/
|
|
401
|
+
export function registerBuiltinPlugins(manager: import('./PluginManager.js').PluginManager): void {
|
|
402
|
+
manager.register(new CodeBlockPlugin());
|
|
403
|
+
manager.register(new MermaidPlugin());
|
|
404
|
+
manager.register(new LatexPlugin());
|
|
405
|
+
manager.register(new ToolCallPlugin());
|
|
406
|
+
}
|
|
@@ -0,0 +1,163 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* @generated-by AI: edenxpzhang
|
|
3
|
+
* @generated-date 2026-05-13
|
|
4
|
+
*/
|
|
5
|
+
|
|
6
|
+
import { BaseStore } from './base.js';
|
|
7
|
+
import { ChatState, Message, MessageRole, ChatConfig } from '../types/index.js';
|
|
8
|
+
|
|
9
|
+
// 生成唯一 ID
|
|
10
|
+
function generateId(): string {
|
|
11
|
+
return `msg_${Date.now()}_${Math.random().toString(36).substr(2, 9)}`;
|
|
12
|
+
}
|
|
13
|
+
|
|
14
|
+
// 初始状态
|
|
15
|
+
const initialState: ChatState = {
|
|
16
|
+
messages: [],
|
|
17
|
+
isLoading: false,
|
|
18
|
+
isStreaming: false,
|
|
19
|
+
error: null,
|
|
20
|
+
sessionId: generateId()
|
|
21
|
+
};
|
|
22
|
+
|
|
23
|
+
export class ChatStore extends BaseStore<ChatState> {
|
|
24
|
+
private config: ChatConfig = {};
|
|
25
|
+
|
|
26
|
+
constructor() {
|
|
27
|
+
super(initialState);
|
|
28
|
+
}
|
|
29
|
+
|
|
30
|
+
// 设置配置
|
|
31
|
+
setConfig(config: ChatConfig): void {
|
|
32
|
+
this.config = { ...this.config, ...config };
|
|
33
|
+
}
|
|
34
|
+
|
|
35
|
+
// 获取配置
|
|
36
|
+
getConfig(): ChatConfig {
|
|
37
|
+
return this.config;
|
|
38
|
+
}
|
|
39
|
+
|
|
40
|
+
// 添加消息
|
|
41
|
+
addMessage(role: MessageRole, content: string, metadata?: Record<string, unknown>): Message {
|
|
42
|
+
const message: Message = {
|
|
43
|
+
id: generateId(),
|
|
44
|
+
role,
|
|
45
|
+
content,
|
|
46
|
+
timestamp: Date.now(),
|
|
47
|
+
metadata
|
|
48
|
+
};
|
|
49
|
+
|
|
50
|
+
const messages = [...this.state.messages, message];
|
|
51
|
+
|
|
52
|
+
// 限制历史消息数量
|
|
53
|
+
const maxHistory = this.config.maxHistory || 50;
|
|
54
|
+
const trimmedMessages = messages.length > maxHistory
|
|
55
|
+
? messages.slice(-maxHistory)
|
|
56
|
+
: messages;
|
|
57
|
+
|
|
58
|
+
this.setState({
|
|
59
|
+
messages: trimmedMessages,
|
|
60
|
+
error: null
|
|
61
|
+
});
|
|
62
|
+
|
|
63
|
+
return message;
|
|
64
|
+
}
|
|
65
|
+
|
|
66
|
+
// 更新消息(用于流式输出)
|
|
67
|
+
updateMessage(id: string, content: string): void {
|
|
68
|
+
const messages = this.state.messages.map(msg =>
|
|
69
|
+
msg.id === id ? { ...msg, content } : msg
|
|
70
|
+
);
|
|
71
|
+
this.setState({ messages });
|
|
72
|
+
}
|
|
73
|
+
|
|
74
|
+
// 设置工具调用
|
|
75
|
+
setToolCalls(messageId: string, toolCalls: Message['toolCalls']): void {
|
|
76
|
+
const messages = this.state.messages.map(msg =>
|
|
77
|
+
msg.id === messageId ? { ...msg, toolCalls } : msg
|
|
78
|
+
);
|
|
79
|
+
this.setState({ messages });
|
|
80
|
+
}
|
|
81
|
+
|
|
82
|
+
// 更新工具调用结果
|
|
83
|
+
updateToolCallResult(messageId: string, toolCallId: string, result: unknown): void {
|
|
84
|
+
const messages = this.state.messages.map(msg => {
|
|
85
|
+
if (msg.id === messageId && msg.toolCalls) {
|
|
86
|
+
const toolCalls = msg.toolCalls.map(tc =>
|
|
87
|
+
tc.id === toolCallId ? { ...tc, result, status: 'completed' as const } : tc
|
|
88
|
+
);
|
|
89
|
+
return { ...msg, toolCalls };
|
|
90
|
+
}
|
|
91
|
+
return msg;
|
|
92
|
+
});
|
|
93
|
+
this.setState({ messages });
|
|
94
|
+
}
|
|
95
|
+
|
|
96
|
+
// 设置加载状态
|
|
97
|
+
setLoading(isLoading: boolean): void {
|
|
98
|
+
this.setState({ isLoading });
|
|
99
|
+
}
|
|
100
|
+
|
|
101
|
+
// 设置流式输出状态
|
|
102
|
+
setStreaming(isStreaming: boolean): void {
|
|
103
|
+
this.setState({ isStreaming });
|
|
104
|
+
}
|
|
105
|
+
|
|
106
|
+
// 设置错误
|
|
107
|
+
setError(error: Error | null): void {
|
|
108
|
+
this.setState({ error, isLoading: false, isStreaming: false });
|
|
109
|
+
}
|
|
110
|
+
|
|
111
|
+
// 清空消息
|
|
112
|
+
clearMessages(): void {
|
|
113
|
+
this.setState({ messages: [], error: null });
|
|
114
|
+
}
|
|
115
|
+
|
|
116
|
+
// 删除消息
|
|
117
|
+
deleteMessage(id: string): void {
|
|
118
|
+
const messages = this.state.messages.filter(msg => msg.id !== id);
|
|
119
|
+
this.setState({ messages });
|
|
120
|
+
}
|
|
121
|
+
|
|
122
|
+
// 获取消息历史(用于 API 调用)
|
|
123
|
+
getMessageHistory(): Array<{ role: string; content: string }> {
|
|
124
|
+
return this.state.messages.map(msg => ({
|
|
125
|
+
role: msg.role,
|
|
126
|
+
content: msg.content
|
|
127
|
+
}));
|
|
128
|
+
}
|
|
129
|
+
|
|
130
|
+
// 重置
|
|
131
|
+
reset(): void {
|
|
132
|
+
this.setState(initialState);
|
|
133
|
+
this.config = {};
|
|
134
|
+
}
|
|
135
|
+
|
|
136
|
+
// 导出会话
|
|
137
|
+
exportSession(): string {
|
|
138
|
+
return JSON.stringify({
|
|
139
|
+
sessionId: this.state.sessionId,
|
|
140
|
+
messages: this.state.messages,
|
|
141
|
+
config: this.config,
|
|
142
|
+
exportTime: Date.now()
|
|
143
|
+
});
|
|
144
|
+
}
|
|
145
|
+
|
|
146
|
+
// 导入会话
|
|
147
|
+
importSession(data: string): void {
|
|
148
|
+
try {
|
|
149
|
+
const session = JSON.parse(data);
|
|
150
|
+
this.setState({
|
|
151
|
+
sessionId: session.sessionId || generateId(),
|
|
152
|
+
messages: session.messages || [],
|
|
153
|
+
error: null
|
|
154
|
+
});
|
|
155
|
+
if (session.config) {
|
|
156
|
+
this.config = session.config;
|
|
157
|
+
}
|
|
158
|
+
} catch (error) {
|
|
159
|
+
console.error('Failed to import session:', error);
|
|
160
|
+
this.setError(error as Error);
|
|
161
|
+
}
|
|
162
|
+
}
|
|
163
|
+
}
|
|
@@ -0,0 +1,136 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* @generated-by AI: edenxpzhang
|
|
3
|
+
* @generated-date 2026-05-13
|
|
4
|
+
*/
|
|
5
|
+
|
|
6
|
+
import { BaseStore } from './base.js';
|
|
7
|
+
import { ModelConfig } from '../types/index.js';
|
|
8
|
+
|
|
9
|
+
// 初始状态
|
|
10
|
+
const initialConfig: ModelConfig = {
|
|
11
|
+
provider: 'openai',
|
|
12
|
+
model: 'gpt-3.5-turbo',
|
|
13
|
+
params: {
|
|
14
|
+
temperature: 0.7,
|
|
15
|
+
topP: 1,
|
|
16
|
+
maxTokens: 2048
|
|
17
|
+
}
|
|
18
|
+
};
|
|
19
|
+
|
|
20
|
+
export class ModelConfigStore extends BaseStore<ModelConfig> {
|
|
21
|
+
constructor() {
|
|
22
|
+
super({ ...initialConfig });
|
|
23
|
+
}
|
|
24
|
+
|
|
25
|
+
// 设置提供商
|
|
26
|
+
setProvider(provider: ModelConfig['provider']): void {
|
|
27
|
+
this.setState({ provider });
|
|
28
|
+
// 根据提供商自动设置默认端点
|
|
29
|
+
this.setDefaultEndpoint(provider);
|
|
30
|
+
}
|
|
31
|
+
|
|
32
|
+
// 设置模型
|
|
33
|
+
setModel(model: string): void {
|
|
34
|
+
this.setState({ model });
|
|
35
|
+
}
|
|
36
|
+
|
|
37
|
+
// 设置 API 端点
|
|
38
|
+
setApiEndpoint(endpoint: string): void {
|
|
39
|
+
this.setState({ apiEndpoint: endpoint });
|
|
40
|
+
}
|
|
41
|
+
|
|
42
|
+
// 设置 API Key
|
|
43
|
+
setApiKey(apiKey: string): void {
|
|
44
|
+
this.setState({ apiKey });
|
|
45
|
+
}
|
|
46
|
+
|
|
47
|
+
// 设置请求头
|
|
48
|
+
setHeaders(headers: Record<string, string>): void {
|
|
49
|
+
this.setState({ headers: { ...this.state.headers, ...headers } });
|
|
50
|
+
}
|
|
51
|
+
|
|
52
|
+
// 设置参数
|
|
53
|
+
setParams(params: ModelConfig['params']): void {
|
|
54
|
+
this.setState({
|
|
55
|
+
params: { ...this.state.params, ...params }
|
|
56
|
+
});
|
|
57
|
+
}
|
|
58
|
+
|
|
59
|
+
// 根据提供商设置默认端点
|
|
60
|
+
private setDefaultEndpoint(provider: ModelConfig['provider']): void {
|
|
61
|
+
const endpoints: Record<string, string> = {
|
|
62
|
+
openai: 'https://api.openai.com/v1/chat/completions',
|
|
63
|
+
ernie: 'https://aip.baidubce.com/rpc/2.0/ai_custom/v1/wenxinworkshop/chat',
|
|
64
|
+
qwen: 'https://dashscope.aliyuncs.com/api/v1/services/aigc/text-generation/generation',
|
|
65
|
+
spark: 'https://spark-api.xf-yun.com/v1/chat/completions',
|
|
66
|
+
hunyuan: 'https://hunyuan.tencentcloudapi.com/'
|
|
67
|
+
};
|
|
68
|
+
|
|
69
|
+
if (provider !== 'custom' && endpoints[provider]) {
|
|
70
|
+
this.setState({ apiEndpoint: endpoints[provider] });
|
|
71
|
+
}
|
|
72
|
+
}
|
|
73
|
+
|
|
74
|
+
// 获取请求头(根据提供商)
|
|
75
|
+
getRequestHeaders(): Record<string, string> {
|
|
76
|
+
const headers: Record<string, string> = {
|
|
77
|
+
'Content-Type': 'application/json',
|
|
78
|
+
...this.state.headers
|
|
79
|
+
};
|
|
80
|
+
|
|
81
|
+
// 根据提供商添加认证头
|
|
82
|
+
switch (this.state.provider) {
|
|
83
|
+
case 'openai':
|
|
84
|
+
if (this.state.apiKey) {
|
|
85
|
+
headers['Authorization'] = `Bearer ${this.state.apiKey}`;
|
|
86
|
+
}
|
|
87
|
+
break;
|
|
88
|
+
case 'qwen':
|
|
89
|
+
if (this.state.apiKey) {
|
|
90
|
+
headers['Authorization'] = `Bearer ${this.state.apiKey}`;
|
|
91
|
+
}
|
|
92
|
+
break;
|
|
93
|
+
// 其他提供商的认证方式
|
|
94
|
+
}
|
|
95
|
+
|
|
96
|
+
return headers;
|
|
97
|
+
}
|
|
98
|
+
|
|
99
|
+
// 验证配置
|
|
100
|
+
validate(): { valid: boolean; errors: string[] } {
|
|
101
|
+
const errors: string[] = [];
|
|
102
|
+
|
|
103
|
+
if (!this.state.model) {
|
|
104
|
+
errors.push('Model is required');
|
|
105
|
+
}
|
|
106
|
+
|
|
107
|
+
if (this.state.provider !== 'custom' && !this.state.apiEndpoint) {
|
|
108
|
+
errors.push('API endpoint is required');
|
|
109
|
+
}
|
|
110
|
+
|
|
111
|
+
return {
|
|
112
|
+
valid: errors.length === 0,
|
|
113
|
+
errors
|
|
114
|
+
};
|
|
115
|
+
}
|
|
116
|
+
|
|
117
|
+
// 重置
|
|
118
|
+
reset(): void {
|
|
119
|
+
this.setState({ ...initialConfig });
|
|
120
|
+
}
|
|
121
|
+
|
|
122
|
+
// 导出配置
|
|
123
|
+
exportConfig(): string {
|
|
124
|
+
return JSON.stringify(this.state);
|
|
125
|
+
}
|
|
126
|
+
|
|
127
|
+
// 导入配置
|
|
128
|
+
importConfig(data: string): void {
|
|
129
|
+
try {
|
|
130
|
+
const config = JSON.parse(data);
|
|
131
|
+
this.setState(config);
|
|
132
|
+
} catch (error) {
|
|
133
|
+
console.error('Failed to import config:', error);
|
|
134
|
+
}
|
|
135
|
+
}
|
|
136
|
+
}
|