dsh-task-manager: Add a floating task panel to DSH chat

Foreword

When running multi-step tasks in Harness, there are three common sources of chaos: tasks scattered throughout the conversation, making them hard to find later; unclear whether a task was proposed by the model or manually copied, making it hard to distinguish; and automatically checking off tasks when the model says “completed” without anyone actually confirming. dsh-task-manager addresses these three issues: collects tasks into a floating panel, labels the source of each annotation, and requires you to make the final decision when the model proposes completion.

What is this

dsh-task-manager is maintained by caipanjin, a plugin for DeepSeek Harness (DSH), published on npm, current version 1.1.5. It provides a floating task panel in the bottom right corner, placing model-suggested tasks and manually added tasks in the same place, and additionally handles three things: double confirmation is required to click “Start” before sending the task into the conversation; when the model reports completion, it only proposes without automatically finalizing; and the panel position is remembered by the browser. The README describes it as “a dependency-free small plugin.” The license is MIT.

Core Features

  1. Two task sources: Tasks written by the model via the task_write tool are labeled “Model Suggested,” and annotations added in the panel input box are “Manually Added.” Each has a source badge.

  2. Select text to create tasks: Select a paragraph of text in the conversation, and a “+ Add as Task” button floats nearby. When the selected content is a rendered list (-, 1., 2. etc.), the button changes to “+ Add N tasks,” creating a task for each list item. Since list markers are drawn by CSS ::marker and are not included in the selected text, the plugin reads the DOM <li> structure to identify items, rather than regex matching the text.

  3. Floating panel: Located above the shell overlay, it expands from a compact capsule, is draggable to any position, and its position is saved by the browser.

  4. Status dot and status text: Each row displays a status dot and the text “To Do / In Progress / Completed.” Colors are auxiliary (Gray for To Do, Amber for In Progress, Green for Completed).

  5. Explicit completion, confirmation to finalize: Each row has only one action button that changes with the status (“Mark In Progress” → “✓ Complete” → “Reopen”). When the model judges a task complete, it only proposes; a banner appears on the panel saying “Confirm All / Ignore,” and it will not truly finalize until you confirm.

  6. Double confirmation to start: Clicking “Start” on a row first pops up a confirmation bar; clicking “Send” actually sends “Start executing this task: ” to the dialogue box, and the panel then automatically collapses.</p> </li> <li> <p><strong>Delete and Clear</strong>: Both model tasks and user tasks have a “✕,” and there is a “Clear Completed” batch removal (excluding pending).</p> </li> <li> <p><strong>Theme and Language</strong>: Only uses Harness real theme tokens, automatically following the application’s light/dark mode; provides Simplified Chinese and English.</p> </li> </ol> <h1 id="installation-and-enablement">Installation and Enablement<a class="headerlink" href="#installation-and-enablement" title="永久链接">¶</a></h1> <p>It has been published to npm, and the installation command is:</p> <div class="highlight"><pre><span></span><code>dsh<span class="w"> </span>plugin<span class="w"> </span>--profile<span class="w"> </span>web<span class="w"> </span>add<span class="w"> </span>@caipj/dsh-task-manager </code></pre></div> <p>You can also install directly from source:</p> <div class="highlight"><pre><span></span><code>dsh<span class="w"> </span>plugin<span class="w"> </span>--profile<span class="w"> </span>web<span class="w"> </span>add<span class="w"> </span>github:caipanjin/dsh-task-manager </code></pre></div> <p>After installation, restart the Harness host once to let the host-side tool <code>task_write</code> register, and refresh the window. The task capsule will appear in the bottom right corner; click to expand, and it can be dragged to move.</p> <h1 id="typical-usage">Typical Usage<a class="headerlink" href="#typical-usage" title="永久链接">¶</a></h1> <p>Walk through the steps below as described in the README.</p> <ol> <li> <p><strong>Add task</strong>: Expand the capsule, type in the input box and click “Add.” The task stays only in the list and is not copied to the chat box.</p> </li> <li> <p><strong>Add from conversation text</strong>: Select text and click the floating “+ Add as Task”; if a list is selected, click “+ Add N tasks.”</p> </li> <li> <p><strong>Model tasks</strong>: When the model calls <code>task_write</code>, the task automatically appears and is labeled “Model Suggested.”</p> </li> <li> <p><strong>Switch status</strong>: Click “Mark In Progress” on To Do, “✓ Complete” on In Progress, and “Reopen” on Completed.</p> </li> <li> <p><strong>Start task</strong>: Click “Start” on a row, click “Send” after the confirmation bar appears, and the prompt is sent to the conversation.</p> </li> <li> <p><strong>Completion confirmation</strong>: After the model reports completion, a confirmation banner appears. You can “Confirm All” or “Confirm” row by row. When the model reports, in addition to the task ID, you can also write the title directly. The title is matched by normalization (ignoring case, full/half-width, spaces, punctuation), and the first unique match takes effect; unique prefixes are also accepted. If multiple matches are found, an error is reported listing the candidates.</p> </li> <li> <p><strong>Delete / Clear</strong>: The “✕” in the row deletes a single item, and “Clear Completed” removes them in batch.</p> </li> </ol> <h1 id="how-it-works-and-compatibility">How it Works and Compatibility<a class="headerlink" href="#how-it-works-and-compatibility" title="永久链接">¶</a></h1> <p>The host-side (<code>index.js</code>) registers a tool <code>task_write</code> facing the model. Sub-commands include <code>add</code>, <code>set_status</code>, <code>propose_done</code>, <code>remove</code>, <code>replace</code>, <code>clear</code>, and <code>list</code>, and there is a session projection <code>taskManager</code>. The rendering result of the tool embeds a machine tag <code>TASK_STATE::<json></code>, carrying the complete task list; the projector folds out these tags from the existing <code>tool/result</code> events, without introducing custom session event types.</p> <p>The client-side (<code>client.js</code>) registers two widgets to the <code>shell.overlay</code> slot: the task card and the floating layer button that appears when text is selected. The client reads model tasks from the projection. User tasks, local state modifications to model tasks (hide / complete), and confirmation status are stored in <code>localStorage</code>—since installed plugins cannot write to the host projection, this state is designed to be on the client side. “Start” calls <code>conversation.send(text)</code> for the current session.</p> <p>A fix worth noting in version 1.1.5: When the model uses <code>replace</code> to rewrite the task list, previously every task would get a new ID, causing local hide, status modifications, and completion confirmations recorded by ID to be lost. Now <code>replace</code> reuses the IDs of existing tasks based on the title, and only truly new tasks generate a new ID; title matching also uses normalization.</p> <p>The platform is <strong>web</strong> (Harness Desktop / Web UI). It requires Harness to expose the <code>shell.overlay</code> slot, <code>sessionProjections</code> service, and the <code>conversation.send</code> / <code>sessions.scope</code> client services. The plugin only uses publicly stable theme tokens (<code>--dsw-alias-*</code>).</p> <p>Regarding permissions: No network requests, no external services, no telemetry; reads tasks from the session projection, and only writes to its own <code>localStorage</code> keys <code>@caipj/dsh-task-manager:*</code>; adds a new model tool <code>task_write</code> without touching anything else.</p> <h1 id="suitable-scenarios-and-notes">Suitable Scenarios and Notes<a class="headerlink" href="#suitable-scenarios-and-notes" title="永久链接">¶</a></h1> <p>Suitable for developers who want to maintain multi-step tasks in the Harness chat but do not want to manually copy and paste points into the conversation.</p> <p>Three things should be confirmed before use:</p> <ol> <li>Does the Harness version expose the <code>shell.overlay</code> slot, <code>sessionProjections</code> service, and related client services? If not met, the panel will not appear.</li> <li>This plugin runs with the current dsh process permissions. You should read the source code and license (MIT) before installing to confirm trust.</li> <li>Known limitations: Completion/deletion of model tasks is a local override, saved to <code>localStorage</code> per session, and does not write back to the session log; the local record takes precedence when reopening the session. User tasks only exist locally in the browser/session and do not enter the model context until “Start” is clicked. Creating tasks from selected text relies on DOM structure to identify list items. When text comes from code blocks or plain text (where markers are real characters), it will fall back to splitting by <code>-</code> / <code>1.</code> in the text.</li> </ol> <h1 id="conclusion">Conclusion<a class="headerlink" href="#conclusion" title="永久链接">¶</a></h1> <p><code>dsh-task-manager</code> does something very specific: it adds a task panel to Harness chat with source labels and confirmation requirements, making the execution order and completion status of multi-step tasks at least verifiable. The directory page is at https://www.skillhub.cn/plugins/caipanjin/dsh-task-manager , and the source code is at https://github.com/caipanjin/dsh-task-manager .</p></div> </div> <!-- 文末优惠券推荐(一行,数量随宽度自适应;每张卡片含打开领券页 / 复制淘口令) --> <section class="article-coupon-row" id="articleCouponRow" style="display: none;" aria-label="淘宝优惠券推荐"> <div class="article-coupon-header"> <h4><i class="fas fa-ticket-alt me-2" aria-hidden="true"></i>精选优惠券</h4> <a class="article-coupon-more-btn" href="/coupon/" target="_blank" rel="noopener noreferrer"> 查看更多优惠券 <i class="fas fa-angle-right"></i> </a> </div> <div class="article-coupon-grid" id="articleCouponGrid"></div> <p class="article-coupon-empty" id="articleCouponEmpty">暂无优惠券推荐</p> </section> <!-- 上一篇/下一篇导航 --> <div class="article-navigation"> <div> <a href="/article/1793991344017?lang=en" class="btn btn-outline-secondary"> <i class="fas fa-arrow-left"></i> Previous: dsh-plugin-bulkpublish: Enable DSH Agent to manage posting and engagement on 15 social media platforms. </a> </div> <div> <a href="/article/1793991644082?lang=en" class="btn btn-outline-secondary"> Next: dsh-sooyo: Combine multiple information sources into a bento homepage <i class="fas fa-arrow-right"></i> </a> </div> </div> <!-- 相关文章 --> <div class="related-articles"> <h4>Related Articles</h4> <div class="list-group"> <a href="/article/1743835538155?lang=en" class="list-group-item list-group-item-action"> <span class="text-truncate d-inline-block" style="max-width: 70%;">Quickly Deploy a DeepSeek-R1 Service from Scratch</span> <small class="text-muted float-end">2025-04-05</small> </a> <a href="/article/1786930432799?lang=en" class="list-group-item list-group-item-action"> <span class="text-truncate d-inline-block" style="max-width: 70%;">Configure Two-stage Agent Preset for DeepSeek Harness Using dsh-anchored-standard</span> <small class="text-muted float-end">2026-08-17</small> </a> <a href="/article/1786929164159?lang=en" class="list-group-item list-group-item-action"> <span class="text-truncate d-inline-block" style="max-width: 70%;">Using dsh-web-ui to turn DeepSeek Harness web interface into a workbench</span> <small class="text-muted float-end">2026-08-17</small> </a> <a href="/article/1786929385565?lang=en" class="list-group-item list-group-item-action"> <span class="text-truncate d-inline-block" style="max-width: 70%;">Add Vision Capabilities to the Plain-text Models of DeepSeek Harness with ModLens</span> <small class="text-muted float-end">2026-08-17</small> </a> <a href="/article/1786929634637?lang=en" class="list-group-item list-group-item-action"> <span class="text-truncate d-inline-block" style="max-width: 70%;">Add Full-Screen Terminal Interface to DeepSeek Harness with dsh-TUI</span> <small class="text-muted float-end">2026-08-17</small> </a> </div> </div> </article> <!-- 羽毛球小程序推荐按钮(鼠标悬停显示二维码) --> <div class="badminton-miniapp-button" id="badmintonMiniappButton"> <img class="badminton-miniapp-icon" src="https://badminton.yeyupiaoling.cn/web/logo.jpg" alt="羽毛球分组比赛记分"> </div> <!-- 羽毛球小程序二维码弹窗(悬停显示) --> <div class="badminton-miniapp-dialog" id="badmintonMiniappDialog"> <div class="badminton-miniapp-body"> <img class="badminton-miniapp-qrcode" src="https://badminton.yeyupiaoling.cn/web/badminton_wechat.jpg" alt="小程序二维码"> <p class="badminton-miniapp-tip">欢迎使用《羽毛球分组比赛记分》微信小程序</p> </div> </div> <!-- AI对话按钮和对话框 --> <div class="ai-chat-button" onclick="toggleChat()"> <i class="fas fa-robot"></i> </div> <!-- AI对话按钮提示气泡 --> <div class="ai-chat-tooltip" id="chatTooltip"></div> <div class="ai-chat-dialog" id="chatDialog"> <div class="ai-chat-header"> <span>Xiaoye</span> <button class="btn btn-sm btn-light" onclick="toggleChat()">×</button> </div> <div class="ai-chat-messages" id="chatMessages"></div> <div class="ai-chat-input"> <div class="input-group"> <input type="text" class="form-control" id="chatInput" placeholder="Enter your question..."> <button class="btn btn-primary" onclick="sendMessage()">Send</button> </div> </div> </div> <!-- 隐藏的文章ID和语言变量 --> <input type="hidden" id="article_id" value="1793991454296"> <input type="hidden" id="current_lang" value="en"> </article> </div> </main> <!-- 左侧淘宝优惠券浮动弹窗 --> <!-- 左侧淘宝优惠券浮动弹窗 --> <div class="coupon-float is-hidden" id="couponFloat" aria-label="淘宝优惠券推荐"> <div class="coupon-float-panel"> <div class="coupon-float-head"> <span><i class="fas fa-ticket-alt me-1" aria-hidden="true"></i>优惠券</span> <button type="button" class="coupon-float-toggle" data-action="collapse" title="收起" aria-label="收起优惠券弹窗"> <i class="fas fa-chevron-left"></i> </button> </div> <a class="coupon-float-body" href="/coupon/" title="查看优惠券详情" target="_blank" rel="noopener noreferrer"> <div class="coupon-float-cover"> <img src="" alt="优惠券商品" loading="lazy" decoding="async"> <span class="coupon-float-badge"></span> </div> <div class="coupon-float-title"></div> <div class="coupon-float-price"> <span class="coupon-float-final"><small>¥</small>0</span> <span class="coupon-float-label">优惠券</span> </div> </a> <div class="coupon-float-actions"> <a class="coupon-float-btn coupon-float-btn-primary" id="couponFloatOpenBtn" href="#" target="_blank" rel="noopener sponsored nofollow"> 打开领券页 </a> <button type="button" class="coupon-float-btn coupon-float-btn-outline" id="couponFloatTpwdBtn"> 复制淘口令 </button> </div> <div class="coupon-float-foot"> <a class="coupon-float-more" href="/coupon/" target="_blank" rel="noopener noreferrer">查看更多优惠券 →</a> </div> </div> <button type="button" class="coupon-float-tab" data-action="expand" title="展开优惠券" aria-label="展开优惠券弹窗"> <i class="fas fa-ticket-alt"></i> 优惠券 </button> <div class="coupon-float-toast" id="couponFloatToast" style="display: none;" role="status"></div> </div> <!-- 页脚 --> <footer class="mt-5 py-4" style="background-color: var(--footer-bg);"> <div class="container"> <div class="row"> <div class="col-12 text-center"> <div class="footer-friend-links mb-3"> <div class="footer-friend-links-title text-muted">Friend Links</div> <nav class="footer-friend-links-list" aria-label="Friend Links"> <a href="https://tools.yeyupiaoling.cn/" target="_blank" rel="noopener noreferrer">Xiaoye Tools</a> <a href="https://image-tools.yeyupiaoling.cn/" target="_blank" rel="noopener noreferrer">Image Toolbox</a> <a href="https://audio-tools.yeyupiaoling.cn/" target="_blank" rel="noopener noreferrer">Audio/Video Toolbox</a> <a href="https://dsh.yeyupiaoling.cn/" target="_blank" rel="noopener noreferrer">DeepSeek Harness Plugins</a> <a href="https://skills.yeyupiaoling.cn/" target="_blank" rel="noopener noreferrer">Agent Skills</a> <a href="https://www.yeyupiaoling.cn/" target="_blank" rel="noopener noreferrer">Desktop Site</a> <a href="https://badminton.yeyupiaoling.cn/web" target="_blank" rel="noopener noreferrer">Badminton Scoring</a> </nav> </div> <p class="text-muted mb-0"> © 2025 Yeyupiaoling | <a href="/auth/login" class="text-decoration-none text-muted"> <i class="fas fa-sign-in-alt"></i> Admin Login </a> | Total Articles: 37203 | <a href="https://beian.miit.gov.cn/" target="_blank" class="text-decoration-none text-muted">粤ICP备17019586号</a> </p> </div> </div> </div> </footer> <!-- 百度统计 --> <script> var _hmt = _hmt || []; (function() { var hm = document.createElement("script"); hm.src = "https://hm.baidu.com/hm.js?2040d9bf2243a546711228193e400ffa"; var s = document.getElementsByTagName("script")[0]; s.parentNode.insertBefore(hm, s); })(); </script> <!-- Bootstrap 5 JS Bundle with Popper --> <script src="https://s4.zstatic.net/ajax/libs/bootstrap/5.3.3/js/bootstrap.bundle.min.js"></script> <!-- jQuery --> <script src="https://s4.zstatic.net/ajax/libs/jquery/3.7.1/jquery.min.js"></script> <!-- 自定义JS --> <script src="/static/js/main.js"></script> <script src="/static/js/coupon_pref.js"></script> <script src="/static/js/coupon-widget.js"></script> <!-- 主题切换脚本 --> <script> (function() { var themeToggle = document.getElementById('themeToggle'); var themeIcon = document.getElementById('themeIcon'); function updateIcon(theme) { if (theme === 'dark') { themeIcon.classList.remove('fa-sun'); themeIcon.classList.add('fa-moon'); themeToggle.title = 'Dark Mode'; } else { themeIcon.classList.remove('fa-moon'); themeIcon.classList.add('fa-sun'); themeToggle.title = 'Light Mode'; } } // 初始化图标 var currentTheme = localStorage.getItem('theme') || 'light'; updateIcon(currentTheme); // 切换主题 themeToggle.addEventListener('click', function() { var currentTheme = document.documentElement.getAttribute('data-theme'); var newTheme = currentTheme === 'dark' ? 'light' : 'dark'; document.documentElement.setAttribute('data-theme', newTheme); localStorage.setItem('theme', newTheme); updateIcon(newTheme); }); })(); </script> <script src="https://s4.zstatic.net/ajax/libs/highlight.js/11.11.1/highlight.min.js"></script> <!-- KaTeX库 - 用于渲染LaTeX公式 --> <script src="https://s4.zstatic.net/ajax/libs/KaTeX/0.16.9/katex.min.js"></script> <!-- KaTeX自动渲染扩展 - 自动识别并渲染文档中的公式 --> <script src="https://s4.zstatic.net/ajax/libs/KaTeX/0.16.9/contrib/auto-render.min.js"></script> <!-- 添加marked.js库用于解析Markdown --> <script src="https://s4.zstatic.net/ajax/libs/marked/15.0.7/marked.min.js"></script> <script> // 翻译文本 const translations = { aiTooltip: "Feel free to ask me anything", aiError: "Sorry, an error occurred. Please try again later.", aiWelcome: "Hello! I'm Xiaoye, your AI assistant. I can help you answer any questions about this article or other programming-related questions. What would you like to know?" }; document.addEventListener('DOMContentLoaded', (event) => { // 代码高亮 document.querySelectorAll('pre code').forEach((el) => { hljs.highlightElement(el); }); // 为代码块添加复制按钮 document.querySelectorAll('.article-content pre').forEach((pre) => { const copyBtn = document.createElement('button'); copyBtn.className = 'code-copy-btn'; copyBtn.innerHTML = '<i class="far fa-copy"></i> 复制'; copyBtn.addEventListener('click', async () => { const code = pre.querySelector('code'); const text = code ? code.textContent : pre.textContent; try { await navigator.clipboard.writeText(text); copyBtn.innerHTML = '<i class="fas fa-check"></i> 已复制'; copyBtn.classList.add('copied'); setTimeout(() => { copyBtn.innerHTML = '<i class="far fa-copy"></i> 复制'; copyBtn.classList.remove('copied'); }, 2000); } catch (err) { console.error('复制失败:', err); copyBtn.innerHTML = '<i class="fas fa-times"></i> 失败'; setTimeout(() => { copyBtn.innerHTML = '<i class="far fa-copy"></i> 复制'; }, 2000); } }); pre.appendChild(copyBtn); }); // KaTeX数学公式渲染 renderMathInElement(document.body, { delimiters: [ {left: '$$', right: '$$', display: true}, {left: '$', right: '$', display: false}, {left: '\\[', right: '\\]', display: true}, {left: '\\(', right: '\\)', display: false} ], throwOnError: false }); }); let messages = []; const chatDialog = document.getElementById('chatDialog'); const chatMessages = document.getElementById('chatMessages'); const chatInput = document.getElementById('chatInput'); const chatTooltip = document.getElementById('chatTooltip'); // 自动打开/关闭对话框的定时器 let autoOpenTimer = null; let autoCloseTimer = null; let userInteracted = false; // 跟踪用户是否有操作 let isAutoOpened = false; // 标记对话框是否是自动打开的 // 检测是否为移动端 function isMobileDevice() { return window.innerWidth <= 768 || /Android|webOS|iPhone|iPad|iPod|BlackBerry|IEMobile|Opera Mini/i.test(navigator.userAgent); } // 配置Marked选项 marked.setOptions({ breaks: true, // 将换行符转换为<br> gfm: true // 启用GitHub风格的Markdown }); // 打字效果函数 function typeWriter(element, text, i, speed) { if (i < text.length) { element.textContent += text.charAt(i); i++; setTimeout(() => typeWriter(element, text, i, speed), speed); } else { // 打字效果结束后,延迟一段时间再消失 setTimeout(() => { element.style.animation = 'fadeOut 1s forwards'; setTimeout(() => { element.style.display = 'none'; }, 1000); }, 3000); } } // 页面加载后启动打字效果 window.addEventListener('DOMContentLoaded', () => { setTimeout(() => { typeWriter(chatTooltip, translations.aiTooltip, 0, 100); }, 1000); // 10秒后自动打开AI对话框(仅在非移动端) if (!isMobileDevice()) { autoOpenTimer = setTimeout(() => { if (!userInteracted && messages.length === 0 && chatDialog.style.display !== 'flex') { isAutoOpened = true; toggleChat(true); // true表示自动打开 } }, 10000); } }); // 监听用户操作(用于阻止自动打开) function markUserInteraction() { userInteracted = true; // 清除自动打开定时器 if (autoOpenTimer) { clearTimeout(autoOpenTimer); autoOpenTimer = null; } } // 标记对话框内的用户操作(用于阻止自动关闭) function markDialogInteraction() { // 如果对话框是自动打开的,用户开始操作后就不再自动关闭 if (isAutoOpened) { isAutoOpened = false; } // 清除自动关闭定时器 if (autoCloseTimer) { clearTimeout(autoCloseTimer); autoCloseTimer = null; } } // 监听页面上的用户操作(用于阻止自动打开) document.addEventListener('click', markUserInteraction); document.addEventListener('keydown', markUserInteraction); document.addEventListener('scroll', markUserInteraction); // 监听对话框内的用户操作(用于阻止自动关闭) // 只在用户输入内容时阻止自动关闭,focus不算操作 chatInput.addEventListener('input', markDialogInteraction); // 检查是否需要自动关闭对话框 function checkAutoClose() { // 清除之前的自动关闭定时器 if (autoCloseTimer) { clearTimeout(autoCloseTimer); autoCloseTimer = null; } // 如果对话框是自动打开的,且对话为空,10秒后自动关闭 // isAutoOpened会在用户在对话框中操作时被设置为false if (isAutoOpened && messages.length === 0) { autoCloseTimer = setTimeout(() => { // 再次检查条件,确保仍然满足自动关闭的条件 if (chatDialog.style.display === 'flex' && isAutoOpened && messages.length === 0) { isAutoOpened = false; toggleChat(); } }, 10000); } } function toggleChat(isAutoOpen = false) { const isOpening = chatDialog.style.display !== 'flex'; // 如果是手动打开(不是自动打开),清除自动打开标记 if (isOpening && !isAutoOpen) { isAutoOpened = false; // 清除自动打开定时器(如果还在等待) if (autoOpenTimer) { clearTimeout(autoOpenTimer); autoOpenTimer = null; } } chatDialog.style.display = isOpening ? 'flex' : 'none'; // 当打开对话框时隐藏提示 if (isOpening) { chatTooltip.style.display = 'none'; // 在移动设备上禁用滚动 if (window.innerWidth <= 768) { document.body.style.overflow = 'hidden'; } // 如果消息列表为空,显示欢迎消息(每次打开对话框时检查) if (messages.length === 0 && chatMessages.children.length === 0) { addMessage(translations.aiWelcome, false); } // 如果是自动打开,设置自动关闭检查 if (isAutoOpen) { checkAutoClose(); } } else { // 关闭对话框时恢复滚动 document.body.style.overflow = ''; // 清除自动关闭定时器 if (autoCloseTimer) { clearTimeout(autoCloseTimer); autoCloseTimer = null; } // 重置自动打开标记 isAutoOpened = false; } } function addMessage(content, isUser) { const messageDiv = document.createElement('div'); messageDiv.className = `message ${isUser ? 'user-message' : 'ai-message'}`; // 用户消息使用文本显示,AI消息解析Markdown if (isUser) { messageDiv.textContent = content; } else { // 解析Markdown并设置为HTML messageDiv.innerHTML = marked.parse(content); } chatMessages.appendChild(messageDiv); chatMessages.scrollTop = chatMessages.scrollHeight; } // 羽毛球小程序按钮和弹窗的DOM元素引用 const badmintonButton = document.getElementById('badmintonMiniappButton'); const badmintonDialog = document.getElementById('badmintonMiniappDialog'); // 鼠标悬停按钮时显示二维码弹窗 if (badmintonButton && badmintonDialog) { // 鼠标进入按钮或弹窗时显示弹窗 const showBadmintonDialog = () => { badmintonDialog.style.display = 'flex'; }; // 鼠标离开按钮和弹窗时隐藏弹窗 const hideBadmintonDialog = () => { badmintonDialog.style.display = 'none'; }; // 监听鼠标进入和离开事件 badmintonButton.addEventListener('mouseenter', showBadmintonDialog); badmintonButton.addEventListener('mouseleave', () => { // 延迟隐藏,给用户时间移动到弹窗上 setTimeout(() => { if (!badmintonDialog.matches(':hover')) { hideBadmintonDialog(); } }, 100); }); badmintonDialog.addEventListener('mouseenter', showBadmintonDialog); badmintonDialog.addEventListener('mouseleave', hideBadmintonDialog); } async function sendMessage() { const content = chatInput.value.trim(); if (!content) return; // 标记对话框内的用户交互(阻止自动关闭) markDialogInteraction(); chatInput.value = ''; addMessage(content, true); messages.push({ role: 'user', content }); // 获取文章ID和语言类型 const articleId = document.getElementById('article_id').value; const currentLang = document.getElementById('current_lang').value; try { const response = await fetch('/chat_stream', { method: 'POST', headers: { 'Content-Type': 'application/json', }, body: JSON.stringify({ messages: messages, article_id: articleId, // 文章ID lang: currentLang // 语言类型 }), }); let allContent = ''; addMessage('', false); // 添加一个空的AI消息容器 messages.push({ role: 'assistant', content: allContent }); const reader = response.body.getReader(); const decoder = new TextDecoder(); while (true) { const { value, done } = await reader.read(); if (done) break; const chunk = decoder.decode(value); const lines = chunk.split('\n'); for (const line of lines) { if (line.startsWith('chat_data:')) { try { const data = JSON.parse(line.slice(10)); if (data.content) { allContent += data.content; messages[messages.length - 1].content = allContent; // 更新最后一条AI消息的内容,并解析Markdown const aiMessages = document.querySelectorAll('.ai-message'); aiMessages[aiMessages.length - 1].innerHTML = marked.parse(allContent); chatMessages.scrollTop = chatMessages.scrollHeight; } } catch (e) { console.error('Error parsing JSON:', e); } } } } } catch (error) { console.error('Error:', error); addMessage(translations.aiError, false); } } // 按Enter键发送消息 chatInput.addEventListener('keypress', (e) => { if (e.key === 'Enter') { sendMessage(); } }); </script> </body> </html>