/**
* AI Chat Stream Demo - 插件懒加载与启用/禁用管理
*
* ## 🚀 快速开始
*
* ### 1. 引入 Cherry Markdown
* ```html
* <script src="path/to/cherry-markdown.stream.js"></script>
* ```
*
* ### 2. 创建 HTML 结构
* ```html
* <!-- 插件开关 -->
* <input type="checkbox"
* id="plugin-echarts"
* class="j-plugin-checkbox"
* data-plugin="echarts">
* <label for="plugin-echarts">
* ECharts <span class="status-dot j-status-dot-echarts"></span>
* </label>
*
* <!-- 消息容器 -->
* <div class="dialog j-dialog"></div>
*
* <!-- 消息模板 -->
* <div class="one-msg j-one-msg" style="display:none">
* <div class="chat-one-msg"></div>
* </div>
* ```
*
* ### 3. 初始化场景
* ```javascript
* import { aiChatStreamScenario } from './ai-chat-stream-demo.js';
* aiChatStreamScenario();
* ```
*
* ## 📋 核心功能
*
* - ✅ **插件懒加载** - 勾选时才加载,节省初始加载时间
* - ✅ **插件动态切换** - 启用/禁用无需刷新页面
* - ✅ **流式渲染** - 逐字打印效果,适合 AI 对话场景
* - ✅ **多实例支持** - 每条消息独立的 Cherry 实例
*
* ## 🎨 支持的插件
*
* | 插件 | 功能 | 互斥 | 配置路径 |
* |------|------|------|----------|
* | Mermaid | 流程图/时序图 | 无 | codeBlock.customRenderer.mermaid |
* | KaTeX | 数学公式(快) | MathJax | mathBlock.engine='katex' |
* | MathJax | 数学公式(全) | KaTeX | mathBlock.engine='MathJax' |
* | ECharts | 表格图表 | 无 | table.enableChart=true |
*
* ## 🔧 关键机制
*
* ### 数学公式(KaTeX / MathJax)
* - Cherry 默认 `mathBlock.engine='MathJax'`,必须显式覆盖为空来禁用
* - `initMath()` 在 engine/src 都为空时跳过加载
*
* ### Mermaid
* - 不使用 `Cherry.usePlugin()`(会永久污染全局配置)
* - 通过 `codeBlock.customRenderer.mermaid` 注入实例
* - 使用 `wrapperRender` 阻止 MutationObserver 自动渲染
*
* ### ECharts 表格图表
* - 通过 `table.enableChart` + `chartRenderEngine` + `externals` 配置
* - ⚠️ `enableChart` 必须是布尔值 `true`,不能是对象
* - 需要在 `externals` 中注入 `echarts` 实例
*
* ### 流式打印
* - 每条消息创建独立的 Cherry 实例
* - 通过 `setMarkdown(msg.substring(0, index))` 逐字更新
* - 支持暂停/继续功能
*
* ## 💡 最佳实践
*
* 1. **插件按需加载**:只勾选需要的插件,减少初始加载时间
* 2. **数学引擎二选一**:KaTeX 更快,MathJax 功能更全
* 3. **流式适配**:开启后打印速度更快(30ms),适合快速演示
* 4. **自定义内容**:可在文本框输入自己的 Markdown 内容测试
*/
// ============================================================================
// 插件 CDN 配置
// ============================================================================
/**
* 插件 CDN 配置
*
* 每个插件包含:
* - src: 主库的 CDN 地址
* - css: 样式文件(可选)
* - pluginSrc: Cherry 适配插件的路径(可选)
*/
const PLUGIN_CDN = {
mermaid: {
src: 'https://cdn.jsdelivr.net/npm/mermaid@11.6.0/dist/mermaid.min.js',
pluginSrc: '../packages/cherry-markdown/dist/addons/cherry-code-block-mermaid-plugin.js',
},
katex: {
src: 'https://cdn.jsdelivr.net/npm/katex@0.16.9/dist/katex.min.js',
css: 'https://cdn.jsdelivr.net/npm/katex@0.16.9/dist/katex.min.css',
},
mathjax: {
src: 'https://cdn.jsdelivr.net/npm/mathjax@3/es5/tex-svg.js',
},
echarts: {
src: 'https://cdn.jsdelivr.net/npm/echarts@5.5.1/dist/echarts.min.js',
pluginSrc: '../packages/cherry-markdown/dist/addons/advance/cherry-table-echarts-plugin.js',
},
};
/**
* 插件运行时状态
* 用于跟踪每个插件的加载状态,避免重复加载
*/
const pluginState = {
mermaid: { loaded: false, loading: false },
katex: { loaded: false, loading: false },
mathjax: { loaded: false, loading: false },
echarts: { loaded: false, loading: false },
};
/**
* KaTeX ↔ MathJax 互斥映射
* 两个数学引擎不能同时使用,勾选其中一个会自动取消另一个
*/
const MUTUAL_EXCLUSION = { katex: 'mathjax', mathjax: 'katex' };
// ============================================================================
// 示例消息
// ============================================================================
const msgList = [
{
title: '概述:流式渲染配置',
content:
'### 概述\n\n#### 1. 引入 Stream 版本\n```html\n<script src="path/to/cherry-markdown.stream.js"></script>\n```\n\n#### 2. 启用流式渲染能力\n```javascript\nconst cherry = new Cherry({\n editor: {\n height: "auto",\n defaultModel: "previewOnly", // 纯预览模式\n },\n engine: {\n global: {\n flowSessionContext: true, // 开启流式渲染\n flowSessionCursor: "default",\n },\n },\n});\n```\n\n#### 3. 流式更新内容\n```javascript\n// 逐字更新内容\ncherry.setMarkdown(text.substring(0, index));\n```\n',
},
{
title: '数学公式',
content:
'### 数学公式示例\n\n#### 行内公式\n质能方程:$E = mc^2$\n\n#### 块级公式\n高斯公式:\n$$\\oint_S \\vec{F} \\cdot d\\vec{A} = \\int_V (\\nabla \\cdot \\vec{F}) dV$$\n\n二次方程根:\n$$x = \\frac{-b \\pm \\sqrt{b^2 - 4ac}}{2a}$$\n',
},
{
title: 'Mermaid 流程图',
content:
'### Mermaid 流程图示例\n\n```mermaid\ngraph TD\n A[开始] --> B{是否加载插件?}\n B -->|是| C[懒加载插件]\n B -->|否| D[使用默认渲染]\n C --> E[渲染内容]\n D --> E\n E --> F[结束]\n```\n\n#### 时序图\n\n```mermaid\nsequenceDiagram\n participant 用户\n participant Cherry\n participant 插件\n 用户->>Cherry: setMarkdown()\n Cherry->>插件: 检查是否需要渲染\n 插件-->>Cherry: 返回渲染结果\n Cherry-->>用户: 显示内容\n```\n',
},
{
title: '表格图表(ECharts)',
content:
'## 表格图表示例\n\n### 折线图\n| :line:{"title": "折线图"} | Header1 | Header2 | Header3 | Header4 |\n| ------ | ------ | ------ | ------ | ------ |\n| Sample1 | 11 | 11 | 4 | 33 |\n| Sample2 | 112 | 111 | 22 | 222 |\n| Sample3 | 333 | 142 | 311 | 11 |\n\n### 柱状图\n| :bar:{"title": "柱状图"} | Header1 | Header2 | Header3 | Header4 |\n| ------ | ------ | ------ | ------ | ------ |\n| Sample1 | 11 | 11 | 4 | 33 |\n| Sample2 | 112 | 111 | 22 | 222 |\n| Sample3 | 333 | 142 | 311 | 11 |\n\n### 热力图\n| :heatmap:{"title": "热力图"} | 周一 | 周二 | 周三 | 周四 | 周五 |\n| ------ | ------ | ------ | ------ | ------ | ------ |\n| 上午 | 10 | 20 | 30 | 40 | 50 |\n| 下午 | 15 | 25 | 35 | 45 | 55 |\n| 晚上 | 5 | 15 | 25 | 35 | 45 |\n\n### 饼图\n| :pie:{"title": "饼图"} | 数值 |\n| ------ | ------ |\n| 苹果 | 40 |\n| 香蕉 | 30 |\n| 橙子 | 20 |\n| 葡萄 | 10 |\n\n### 雷达图\n| :radar:{"title": "雷达图"} | 技能1 | 技能2 | 技能3 | 技能4 | 技能5 |\n| ------ | ------ | ------ | ------ | ------ | ------ |\n| 用户A | 90 | 85 | 75 | 80 | 88 |\n| 用户B | 75 | 90 | 88 | 85 | 78 |\n| 用户C | 85 | 78 | 90 | 88 | 85 |\n\n### 散点图\n| :scatter:{"title": "数据散点图"} | 横坐标 | 纵坐标 | 大小 | 系列 |\n| ------ | ------ | ------ | ------ | ------ |\n| A1 | 10 | 20 | 5 | 系列一 |\n| A2 | 15 | 25 | 10 | 系列一 |\n| A3 | 18 | 22 | 8 | 系列一 |\n| A4 | 22 | 28 | 12 | 系列一 |\n| A5 | 25 | 35 | 15 | 系列一 |\n| B1 | 12 | 18 | 8 | 系列二 |\n| B2 | 20 | 30 | 12 | 系列二 |\n| B3 | 28 | 25 | 10 | 系列二 |\n| B4 | 35 | 38 | 14 | 系列二 |\n| B5 | 40 | 45 | 16 | 系列二 |\n\n### 桑基图\n| :sankey:{"title": "能源流向图"} | 目标 | 数值 |\n| ------ | ------ | ------ |\n| 煤炭 | 发电 | 300 |\n| 天然气 | 发电 | 200 |\n| 石油 | 交通 | 250 |\n| 水力 | 发电 | 150 |\n| 发电 | 工业 | 400 |\n| 发电 | 居民 | 250 |\n| 交通 | 货运 | 150 |\n| 交通 | 客运 | 100 |\n\n### 地图\n| :map:{"title": "中国地图"} | 数值 |\n| :-: | :-: |\n| 北京 | 100 |\n| 上海 | 200 |\n| 广东 | 300 |\n| 四川 | 150 |\n| 江苏 | 250 |\n| 浙江 | 180 |\n',
},
];
// ============================================================================
// 工具函数
// ============================================================================
/**
* Promise 超时包装器
* 给任意 Promise 添加超时保护,超时后自动 reject
* @param {Promise} promise - 原始 Promise
* @param {number} ms - 超时毫秒数
* @param {string} message - 超时错误信息
* @returns {Promise}
*/
function withTimeout(promise, ms, message = '操作超时') {
let timer;
const timeout = new Promise((_, reject) => {
timer = setTimeout(() => reject(new Error(message)), ms);
});
return Promise.race([promise, timeout]).finally(() => clearTimeout(timer));
}
/**
* 轻量 Toast 提示
* @param {string} message - 提示文本
* @param {'error'|'success'|'info'} type - 提示类型
* @param {number} duration - 显示时长(毫秒),默认 3000
*/
function showToast(message, type = 'info', duration = 3000) {
const container = document.querySelector('.j-toast-container');
if (!container) return;
const toast = document.createElement('div');
toast.className = `toast toast-${type}`;
toast.textContent = message;
container.appendChild(toast);
// 触发淡入(需等下一帧让浏览器应用初始样式)
requestAnimationFrame(() => { toast.classList.add('toast-visible'); });
setTimeout(() => {
toast.classList.remove('toast-visible');
toast.addEventListener('transitionend', () => toast.remove(), { once: true });
// 兜底:如果 transitionend 没触发,400ms 后强制移除
setTimeout(() => toast.remove(), 400);
}, duration);
}
/**
* 动态加载 JavaScript 脚本
* @param {string} src - 脚本 URL
* @param {string} id - 脚本元素 ID(用于避免重复加载)
* @param {Object} options - 配置选项
* @param {boolean} options.module - 是否为 ES module
* @returns {Promise<void>}
*/
function loadScript(src, id, { module = false } = {}) {
return new Promise((resolve, reject) => {
if (document.getElementById(id)) {
resolve();
return;
}
const script = document.createElement('script');
script.id = id;
script.src = src;
if (module) script.type = 'module';
script.onload = resolve; // 浏览器保证 onload 时脚本已执行,无需额外延迟
script.onerror = () => reject(new Error(`脚本加载失败: ${src}`));
document.head.appendChild(script);
});
}
/**
* 动态加载 CSS 样式表
* @param {string} href - 样式表 URL
* @param {string} id - link 元素 ID
* @returns {Promise<void>}
*/
function loadCSS(href, id) {
return new Promise((resolve, reject) => {
if (document.getElementById(id)) {
resolve();
return;
}
const link = document.createElement('link');
link.id = id;
link.rel = 'stylesheet';
link.href = href;
link.onload = resolve;
link.onerror = () => reject(new Error(`样式加载失败: ${href}`));
document.head.appendChild(link);
});
}
/**
* 更新插件状态指示器(小圆点)
* @param {string} plugin - 插件名称
* @param {string} status - 状态: '' | 'loading' | 'loaded'
*/
function updatePluginStatus(plugin, status) {
const dot = document.querySelector(`.j-status-dot-${plugin}`);
if (dot) {
dot.className = `status-dot j-plugin-status j-status-dot-${plugin}${status ? ` ${status}` : ''}`;
}
}
/**
* 检查插件复选框是否被勾选
* @param {string} plugin - 插件名称
* @returns {boolean}
*/
function isChecked(plugin) {
return document.getElementById(`plugin-${plugin}`)?.checked ?? false;
}
// ============================================================================
// 插件加载 / 卸载
// ============================================================================
/**
* 懒加载插件脚本到 DOM。
* 不调用 Cherry.usePlugin(),避免污染全局默认配置(静态属性,注册后无法撤销)。
*/
async function loadPlugin(plugin) {
const state = pluginState[plugin];
const cdn = PLUGIN_CDN[plugin];
if (state.loaded || state.loading) return;
state.loading = true;
updatePluginStatus(plugin, 'loading');
try {
// CDN 资源加载超时 15s(大文件如 ECharts ~3MB 需要足够时间)
if (cdn.css) await withTimeout(loadCSS(cdn.css, `${plugin}-css`), 15000, `${plugin} 样式加载超时`);
await withTimeout(loadScript(cdn.src, `${plugin}-js`), 15000, `${plugin} 脚本加载超时`);
if (plugin === 'mermaid' && window.mermaid) {
window.mermaid.initialize({ startOnLoad: false });
}
if (cdn.pluginSrc) {
await withTimeout(
loadScript(cdn.pluginSrc, `${plugin}-plugin-js`, { module: true }),
15000,
`${plugin} 适配插件加载超时`,
);
// 等待 module script 执行完成(5s 超时保护)
await withTimeout(
new Promise((resolve) => {
const check = () =>
(plugin === 'mermaid' && window.CherryCodeBlockMermaidPlugin) ||
(plugin === 'echarts' && window.CherryTableEchartsPlugin)
? resolve()
: setTimeout(check, 50);
check();
}),
5000,
`${plugin} 插件初始化超时`,
);
}
state.loaded = true;
state.loading = false;
updatePluginStatus(plugin, 'loaded');
showToast(`${plugin} 加载成功`, 'success', 2000);
} catch (e) {
state.loading = false;
updatePluginStatus(plugin, '');
// 加载失败时取消勾选
const cb = document.getElementById(`plugin-${plugin}`);
if (cb) cb.checked = false;
showToast(`${plugin} 加载失败: ${e.message}`, 'error');
console.error(`[Plugin] ${plugin} 加载失败:`, e);
}
}
/**
* 卸载插件。
* - mermaid:不删除脚本(MutationObserver 不可撤销),仅标记 loaded=false
* - katex/mathjax/echarts:删除 JS 标签 + 清理 window 引用
* - CSS 保留不删除,避免已渲染消息的公式/图表样式错乱
*/
function unloadPlugin(plugin) {
const state = pluginState[plugin];
state.loaded = false;
state.loading = false;
if (plugin === 'mermaid') {
window.mermaid?.initialize({ startOnLoad: false });
} else {
document.getElementById(`${plugin}-js`)?.remove();
// 注意:不删除 CSS,保留已渲染内容的样式
if (plugin === 'katex') delete window.katex;
if (plugin === 'mathjax') delete window.MathJax;
if (plugin === 'echarts') {
delete window.echarts;
delete window.CherryTableEchartsPlugin;
}
}
updatePluginStatus(plugin, '');
}
// ============================================================================
// Cherry 配置生成
// ============================================================================
/**
* 根据 UI 状态动态生成 Cherry 配置
*
* 这是整个 demo 的核心函数,负责:
* 1. 检查插件勾选状态和加载状态
* 2. 生成对应的 Cherry 配置对象
* 3. 配置数学引擎、Mermaid、ECharts 等插件
*
* @returns {Object} Cherry Markdown 配置对象
*
* @example
* // 在创建 Cherry 实例时调用
* const config = getCherryConfig();
* config.el = document.getElementById('container');
* const cherry = new Cherry(config);
*/
function getCherryConfig() {
// ---- 数学引擎配置 ----
// KaTeX 和 MathJax 互斥,只能二选一
const mathConfig =
isChecked('katex') && pluginState.katex.loaded
? { engine: 'katex', src: PLUGIN_CDN.katex.src, css: PLUGIN_CDN.katex.css || '' }
: isChecked('mathjax') && pluginState.mathjax.loaded
? { engine: 'MathJax', src: PLUGIN_CDN.mathjax.src, css: '' }
: { engine: '', src: '', css: '' }; // 都未勾选时禁用数学公式
// ---- Mermaid 流程图配置 ----
const mermaidReady =
isChecked('mermaid') && pluginState.mermaid.loaded && window.CherryCodeBlockMermaidPlugin && window.mermaid;
const codeBlockCfg = {
selfClosing: false,
mermaid: { showSourceToolbar: true },
// 通过 customRenderer 注入 Mermaid 插件(不使用 usePlugin)
customRenderer: mermaidReady
? {
mermaid: new window.CherryCodeBlockMermaidPlugin({
mermaid: window.mermaid,
mermaidAPI: window.mermaid,
}),
}
: undefined,
// 阻止 mermaid MutationObserver 自动渲染(即使未启用也设置)
wrapperRender: (language, _code, innerHTML) =>
language === 'mermaid' ? innerHTML.replace(/language-mermaid/g, 'language-mermaid-disabled') : innerHTML,
};
// ---- ECharts 表格图表配置 ----
const echartsReady = isChecked('echarts') && pluginState.echarts.loaded && !!window.echarts;
const tableConfig = echartsReady && window.CherryTableEchartsPlugin
? {
enableChart: true, // 必须是布尔值 true,不能是对象
chartRenderEngine: window.CherryTableEchartsPlugin,
externals: ['echarts'], // 声明需要注入的外部依赖
selfClosing: false,
}
: {
enableChart: false,
selfClosing: false,
};
// ---- 组装完整的 Cherry 配置 ----
return {
editor: {
height: 'auto',
defaultModel: 'previewOnly', // 纯预览模式(流式场景必需)
},
engine: {
global: {
flowSessionContext: document.querySelector('.j-status-input').checked, // 流式渲染开关
flowSessionCursor: 'default',
},
syntax: {
codeBlock: codeBlockCfg,
table: tableConfig,
inlineCode: { selfClosing: false },
header: { anchorStyle: 'none', selfClosing: false },
fontEmphasis: { selfClosing: false },
link: { selfClosing: false },
image: { selfClosing: false },
mathBlock: { selfClosing: false, ...mathConfig },
inlineMath: { selfClosing: false, engine: mathConfig.engine, src: '' },
},
},
externals: {
echarts: window.echarts, // 全局注入 echarts 实例
},
previewer: { enablePreviewerBubble: true },
};
}
// ============================================================================
// 场景初始化 - 流式打印主逻辑
// ============================================================================
/**
* 初始化 AI Chat Stream 场景
*
* 这是整个 demo 的入口函数,负责:
* 1. 渲染示例消息按钮
* 2. 绑定插件开关事件
* 3. 实现流式打印功能
* 4. 处理用户交互
*
* @example
* // 在 HTML 文件中调用
* <script type="module">
* import { aiChatStreamScenario } from './ai-chat-stream-demo.js';
* aiChatStreamScenario();
* </script>
*/
export function aiChatStreamScenario() {
// 获取 DOM 元素
const dialog = document.querySelector('.j-dialog');
const msgTemplate = document.querySelector('.j-one-msg');
const msgPickerList = document.querySelector('.j-msg-picker-list');
const pauseBtn = document.querySelector('.j-pause-button');
const customTextarea = document.querySelector('.j-custom-textarea');
const customButton = document.querySelector('.j-custom-button');
// 流式打印状态
let currentCherry = null; // 当前 Cherry 实例
let printing = false; // 是否正在打印
let paused = false; // 是否暂停
let currentWordIndex = 0; // 当前打印到的字符索引
let interval = 30; // 打印间隔(毫秒)
let rafId = null; // requestAnimationFrame ID
// Cherry 实例管理(防止内存泄漏)
const MAX_INSTANCES = 5;
const cherryInstances = [];
/**
* 销毁所有 Cherry 实例
*/
function destroyAllInstances() {
while (cherryInstances.length > 0) {
const instance = cherryInstances.shift();
try { instance.destroy(); } catch (e) { /* noop */ }
}
}
/**
* 注册 Cherry 实例,超过上限时销毁最早的实例
* @param {Object} cherry - Cherry 实例
*/
function registerInstance(cherry) {
cherryInstances.push(cherry);
while (cherryInstances.length > MAX_INSTANCES) {
const oldest = cherryInstances.shift();
try { oldest.destroy(); } catch (e) { /* noop */ }
}
}
/**
* 打印期间禁用/恢复交互控件
* @param {boolean} disabled - 是否禁用
*/
function setControlsDisabled(disabled) {
document.querySelectorAll('.j-msg-pick-btn, .j-custom-button').forEach((el) => {
el.disabled = disabled;
});
customTextarea.disabled = disabled;
}
/**
* 更新暂停按钮状态
* @param {boolean} isPrinting - 当前是否正在打印
*/
function updatePauseButton(isPrinting) {
pauseBtn.disabled = !isPrinting;
if (!isPrinting) {
paused = false;
pauseBtn.innerText = '暂停流式';
}
}
// 渲染消息选择按钮
msgList.forEach((item, index) => {
const btn = document.createElement('button');
btn.type = 'button';
btn.className = 'button secondary j-msg-pick-btn';
btn.dataset.index = index;
btn.textContent = item.title;
msgPickerList.appendChild(btn);
});
/**
* 确保所有已勾选的插件加载完成
* 在开始打印前调用,避免渲染时插件未就绪
*/
async function ensureCheckedPluginsLoaded() {
const checkboxes = document.querySelectorAll('.j-plugin-checkbox:checked');
for (const cb of checkboxes) {
await loadPlugin(cb.dataset.plugin);
}
}
/**
* 开始流式打印
* 创建新的 Cherry 实例并使用 rAF + 批量字符推进
* @param {string} msg - 要打印的 Markdown 内容
*/
function beginPrint(msg) {
// 取消之前的 rAF(若有残留)
if (rafId) { cancelAnimationFrame(rafId); rafId = null; }
printing = true;
paused = false;
currentWordIndex = 0;
setControlsDisabled(true);
updatePauseButton(true);
// 克隆消息模板
const msgEl = msgTemplate.cloneNode(true);
msgEl.classList.remove('j-one-msg');
// 创建 Cherry 实例
const config = getCherryConfig();
config.el = msgEl.querySelector('.chat-one-msg');
currentCherry = new Cherry(config);
registerInstance(currentCherry);
dialog.appendChild(msgEl);
// rAF + 基于时间的批量字符推进
let lastTime = performance.now();
function step(now) {
if (!printing) return;
if (paused) {
lastTime = now; // 暂停时重置时间基准,避免恢复后一次性跳过大量字符
rafId = requestAnimationFrame(step);
return;
}
// 根据经过时间计算本帧应推进的字符数
const elapsed = now - lastTime;
const charsToAdvance = Math.max(1, Math.floor(elapsed / interval));
lastTime = now;
currentWordIndex = Math.min(currentWordIndex + charsToAdvance, msg.length);
currentCherry.setMarkdown(msg.substring(0, currentWordIndex));
if (currentWordIndex < msg.length) {
rafId = requestAnimationFrame(step);
} else {
// 打印完成
rafId = null;
printing = false;
setControlsDisabled(false);
updatePauseButton(false);
}
}
rafId = requestAnimationFrame(step);
}
// ========================================================================
// 事件绑定
// ========================================================================
// 消息选择按钮点击事件
msgPickerList.addEventListener('click', async (e) => {
const btn = e.target.closest('.j-msg-pick-btn');
if (!btn || printing) return;
await ensureCheckedPluginsLoaded(); // 先加载插件
beginPrint(msgList[Number(btn.dataset.index)].content); // 开始打印
});
// 插件复选框切换事件(加载/卸载 + 互斥处理)
document.querySelectorAll('.j-plugin-checkbox').forEach((checkbox) => {
checkbox.addEventListener('change', async function () {
const plugin = this.dataset.plugin;
if (this.checked) {
// 加载期间禁用所有插件复选框,防止重复点击
document.querySelectorAll('.j-plugin-checkbox').forEach((cb) => { cb.disabled = true; });
// KaTeX ↔ MathJax 互斥:勾选其中一个会自动取消另一个
const other = MUTUAL_EXCLUSION[plugin];
if (other) {
const otherCb = document.getElementById(`plugin-${other}`);
if (otherCb?.checked) {
otherCb.checked = false;
unloadPlugin(other);
}
}
await loadPlugin(plugin); // 懒加载插件
// 加载完成,恢复所有复选框可用
document.querySelectorAll('.j-plugin-checkbox').forEach((cb) => { cb.disabled = false; });
} else {
unloadPlugin(plugin); // 卸载插件
}
});
});
// 流式适配开关(影响打印速度)
document.querySelector('.j-status-input').addEventListener('change', function () {
interval = this.checked ? 30 : 50; // 开启时快速打印(30ms),关闭时慢速(50ms)
destroyAllInstances(); // 销毁所有 Cherry 实例
dialog.innerHTML = ''; // 清空消息列表
});
// 暂停/继续按钮
pauseBtn.addEventListener('click', () => {
if (!printing) return; // 非打印状态不响应
paused = !paused;
pauseBtn.innerText = paused ? '继续流式' : '暂停流式';
});
// 自定义内容打印
customButton.addEventListener('click', async () => {
if (printing) return;
const content = customTextarea.value.trim();
if (!content) {
showToast('请输入要流式打印的内容', 'info');
return;
}
await ensureCheckedPluginsLoaded();
beginPrint(content);
});
}