Skip to content

Commit 741d415

Browse files
XiNGRZartisnanbingxyz
authored andcommitted
✨: Chat messages support ECharts
1 parent 1487961 commit 741d415

File tree

6 files changed

+170
-142
lines changed

6 files changed

+170
-142
lines changed

package-lock.json

Lines changed: 17 additions & 2 deletions
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

public/locales/en/translation.json

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -271,5 +271,6 @@
271271
"Upgrade.Install": "Install",
272272
"Upgrade.QuitAndInstall": "Quit and install new version.",
273273
"Folder.Settings.Description": "Folder settings are automatically applied to all conversations inside. Changes to folder settings will overwrite individual conversation settings.",
274-
"Common.NumberOfContextMessages": "Number of Context Messages"
274+
"Common.NumberOfContextMessages": "Number of Context Messages",
275+
"Message.Error.EChartsRenderFailed": "The echats render failed."
275276
}

public/locales/zh/translation.json

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -264,5 +264,6 @@
264264
"Upgrade.Install": "安装",
265265
"Upgrade.QuitAndInstall": "退出并安装新版本",
266266
"Folder.Settings.Description": "对话默认继承文件夹设置。修改文件夹设置将覆盖其内部对话的相关配置。",
267-
"Common.NumberOfContextMessages": "上下文消息数量"
267+
"Common.NumberOfContextMessages": "上下文消息数量",
268+
"Message.Error.EChartsRenderFailed": "Echats 图片渲染失败"
268269
}

src/hooks/useECharts.ts

Lines changed: 59 additions & 65 deletions
Original file line numberDiff line numberDiff line change
@@ -3,73 +3,67 @@ import useAppearanceStore from 'stores/useAppearanceStore';
33
import * as echarts from 'echarts';
44
import { IChatMessage } from 'intellichat/types';
55
import { useMemo, useRef } from 'react';
6+
import { useTranslation } from 'react-i18next';
67

7-
export default function useECharts({ message }: { message: IChatMessage }) {
8-
const theme = useAppearanceStore((state) => state.theme);
9-
const messageId = useMemo(() => message.id, [message]);
10-
const containersRef = useRef([]);
11-
12-
const disposeECharts = () => {
13-
const chartInstances = containersRef.current;
14-
if (chartInstances.length < 1) return;
15-
chartInstances.forEach(({ cleanup }: { cleanup: Function }) => {
16-
cleanup();
17-
});
18-
chartInstances.length = 0;
19-
};
20-
21-
const initECharts = () => {
22-
23-
// 清理已有实例
24-
disposeECharts();
25-
const chartInstances = containersRef.current;
26-
27-
// 查找所有图表容器
28-
const containers = document.querySelectorAll(`#${messageId} .echarts-container`);
29-
30-
containers.forEach(container => {
31-
const chartId = container.id;
32-
const encodedConfig = container.getAttribute('data-echarts-config');
8+
const parseOption = (optStr: string) => {
9+
try {
10+
const match = optStr.match(/\{(.+)\}/s);
11+
if (!match) return {};
12+
return new Function(`return {${match[1]}}`)();
13+
} catch (error) {
14+
throw new Error('Invalid ECharts option format');
15+
}
16+
};
3317

34-
if (!encodedConfig) return;
35-
36-
try {
37-
// 解码并解析配置
38-
const config = decodeURIComponent(encodedConfig);
39-
const option = new Function(`return ${config}`)();
40-
41-
// 初始化图表
42-
// 使用类型断言将 container 转换为 HTMLDivElement 类型,以解决类型不匹配问题
43-
console.debug("echart container:",container);
44-
const chart = echarts.init(container as HTMLDivElement, theme);
45-
chart.setOption(option);
46-
47-
// 响应式调整
48-
const resizeHandler = () => chart.resize();
49-
window.addEventListener('resize', resizeHandler);
18+
export default function useECharts({ message }: { message: IChatMessage }) {
19+
const { t } = useTranslation();
20+
const theme = useAppearanceStore((state) => state.theme);
21+
const messageId = useMemo(() => message.id, [message]);
22+
const containersRef = useRef<{ [key: string]: echarts.EChartsType }>({});
5023

51-
// 保存实例
52-
chartInstances.push({
53-
chartId,
54-
instance: chart,
55-
cleanup: () => {
56-
window.removeEventListener('resize', resizeHandler);
57-
chart.dispose();
58-
}
59-
});
60-
} catch (error: any) {
61-
console.error('ECharts初始化错误:', error);
62-
container.innerHTML = `
63-
<div style="color:red;padding:20px;">
64-
图表渲染错误: ${error.message}
65-
</div>
66-
`;
67-
}
68-
});
69-
};
24+
const disposeECharts = () => {
25+
const chartInstances = Object.values(containersRef.current);
26+
chartInstances.forEach(({ cleanup }: { cleanup: Function }) => {
27+
cleanup();
28+
});
29+
containersRef.current = {};
30+
};
7031

71-
return {
72-
disposeECharts,
73-
initECharts,
32+
const initECharts = (prefix: string, chartId: string) => {
33+
if (containersRef.current[`${prefix}-${chartId}`]) return; // already initialized
34+
const chartInstances = containersRef.current;
35+
const container = document.querySelector(
36+
`#${messageId} .echarts-container#${chartId}`,
37+
) as HTMLDivElement;
38+
if (!container) return;
39+
const encodedConfig = container.getAttribute('data-echarts-config');
40+
if (!encodedConfig) return;
41+
try {
42+
let config = decodeURIComponent(encodedConfig);
43+
const option = parseOption(config);
44+
const chart = echarts.init(container, theme);
45+
chart.setOption(option);
46+
const resizeHandler = () => chart.resize();
47+
window.addEventListener('resize', resizeHandler);
48+
chartInstances[`${prefix}-${chartId}`] = {
49+
chartId,
50+
instance: chart,
51+
cleanup: () => {
52+
window.removeEventListener('resize', resizeHandler);
53+
chart.dispose();
54+
},
55+
};
56+
} catch (error: any) {
57+
container.innerHTML = `
58+
<div class="text-gray-400">
59+
${t('Message.Error.EChartsRenderFailed')}
60+
</div>
61+
`;
7462
}
75-
}
63+
};
64+
65+
return {
66+
disposeECharts,
67+
initECharts,
68+
};
69+
}

src/libs/markdownit-plugins/markdownItEChartsPlugin.ts

Lines changed: 48 additions & 34 deletions
Original file line numberDiff line numberDiff line change
@@ -5,40 +5,54 @@ import MarkdownIt from 'markdown-it';
55
//@ts-ignore
66
import Token from 'markdown-it/lib/token.mjs';
77

8-
9-
108
export default function markdownItEChartsPlugin(md: MarkdownIt) {
11-
window.echarts = echarts; // 将 echarts 暴露到全局作用域中,以便在其他地方使用
12-
13-
// 默认渲染函数
14-
const defaultFence = md.renderer.rules.fence || function (tokens: Token[], idx: number, options: MarkdownIt.Options, env: any, self: MarkdownIt.Renderer) {
15-
return self.renderToken(tokens, idx, options);
16-
};
17-
18-
// 覆盖fence渲染规则
19-
md.renderer.rules.fence = (tokens, idx, options, env, self) => {
20-
const token = tokens[idx];
21-
const info = token.info.trim();
22-
23-
// 只处理echarts代码块
24-
if (info !== 'echarts') {
25-
return defaultFence(tokens, idx, options, env, self);
26-
}
27-
28-
// 生成唯一ID
29-
const chartId = 'echart-' + Math.random().toString(36).slice(2, 11);
30-
const code = token.content.trim();
31-
32-
// 返回带有数据属性的容器
33-
return `
34-
<div class="echarts-container"
35-
id="${chartId}"
36-
data-echarts-config="${encodeURIComponent(code)}"
37-
style="width: 400px;height:400px;">
38-
39-
</div>
40-
`;
9+
window.echarts = echarts; // 将 echarts 暴露到全局作用域中,以便在其他地方使用
10+
11+
//
12+
const defaultFence =
13+
md.renderer.rules.fence ||
14+
function (
15+
tokens: Token[],
16+
idx: number,
17+
options: MarkdownIt.Options,
18+
env: any,
19+
self: MarkdownIt.Renderer,
20+
) {
21+
return self.renderToken(tokens, idx, options);
4122
};
4223

43-
44-
}
24+
// 覆盖fence渲染规则
25+
interface EChartsToken extends Token {
26+
info: string;
27+
content: string;
28+
}
29+
30+
md.renderer.rules.fence = (
31+
tokens: Token[],
32+
idx: number,
33+
options: MarkdownIt.Options,
34+
env: any,
35+
self: MarkdownIt.Renderer,
36+
): string => {
37+
const token = tokens[idx] as EChartsToken;
38+
const info = token.info.trim();
39+
40+
// only process code blocks with info 'echarts'
41+
if (info !== 'echarts') {
42+
return defaultFence(tokens, idx, options, env, self);
43+
}
44+
45+
// generate a unique id for the chart container
46+
const chartId: string = 'echart-' + idx;
47+
const code: string = token.content.trim();
48+
49+
return `
50+
<div class="echarts-container"
51+
id="${chartId}"
52+
data-echarts-config="${encodeURIComponent(code)}"
53+
style="width:100%;height:400px;">
54+
<pre><code>${code}</code></pre>
55+
</div>
56+
`;
57+
};
58+
}

0 commit comments

Comments
 (0)