Spaces:
Sleeping
Sleeping
| <html lang="zh-CN"> | |
| <head> | |
| <meta charset="UTF-8"> | |
| <meta name="viewport" content="width=device-width, initial-scale=1.0"> | |
| <title>astrbot-问题帮助助手</title> | |
| <link rel="stylesheet" href="https://cdnjs.cloudflare.com/ajax/libs/font-awesome/6.4.0/css/all.min.css"> | |
| <style> | |
| :root { | |
| --primary: #4361ee; | |
| --primary-light: #4895ef; | |
| --secondary: #3f37c9; | |
| --accent: #f72585; | |
| --light: #ffffff; | |
| --light-gray: #f8f9fa; | |
| --gray: #e9ecef; | |
| --dark-gray: #6c757d; | |
| --dark: #212529; | |
| --shadow: 0 4px 12px rgba(0, 0, 0, 0.08); | |
| --shadow-hover: 0 8px 24px rgba(0, 0, 0, 0.12); | |
| } | |
| * { | |
| margin: 0; | |
| padding: 0; | |
| box-sizing: border-box; | |
| font-family: 'Inter', 'Segoe UI', 'Microsoft YaHei', sans-serif; | |
| } | |
| body { | |
| background: linear-gradient(135deg, #f5f7fa 0%, #e4edf5 100%); | |
| color: var(--dark); | |
| min-height: 100vh; | |
| display: flex; | |
| align-items: center; | |
| justify-content: center; | |
| padding: 20px; | |
| } | |
| .container { | |
| max-width: 900px; | |
| width: 100%; | |
| } | |
| .chat-card { | |
| background: var(--light); | |
| border-radius: 20px; | |
| box-shadow: var(--shadow); | |
| overflow: hidden; | |
| height: 85vh; | |
| display: flex; | |
| flex-direction: column; | |
| } | |
| .chat-header { | |
| padding: 20px; | |
| border-bottom: 1px solid var(--gray); | |
| display: flex; | |
| align-items: center; | |
| justify-content: space-between; | |
| background: var(--light); | |
| } | |
| .header-title { | |
| font-size: 1.5rem; | |
| font-weight: 600; | |
| color: var(--primary); | |
| letter-spacing: -0.5px; | |
| } | |
| .header-actions { | |
| display: flex; | |
| gap: 10px; | |
| } | |
| .clear-btn { | |
| background: transparent; | |
| border: 1px solid var(--gray); | |
| color: var(--dark-gray); | |
| border-radius: 8px; | |
| padding: 8px 12px; | |
| cursor: pointer; | |
| font-size: 0.85rem; | |
| transition: all 0.3s; | |
| } | |
| .clear-btn:hover { | |
| background: #ffebee; | |
| border-color: #ef5350; | |
| color: #c62828; | |
| } | |
| .chat-messages { | |
| flex: 1; | |
| padding: 20px; | |
| overflow-y: auto; | |
| display: flex; | |
| flex-direction: column; | |
| gap: 15px; | |
| background: var(--light-gray); | |
| } | |
| .message { | |
| max-width: 80%; | |
| padding: 12px 18px; | |
| border-radius: 18px; | |
| position: relative; | |
| animation: messageAppear 0.3s ease; | |
| } | |
| @keyframes messageAppear { | |
| from { opacity: 0; transform: translateY(10px); } | |
| to { opacity: 1; transform: translateY(0); } | |
| } | |
| .user-message { | |
| align-self: flex-end; | |
| background: var(--primary); | |
| color: white; | |
| border-bottom-right-radius: 5px; | |
| } | |
| .bot-message { | |
| align-self: flex-start; | |
| background: var(--light); | |
| color: var(--dark); | |
| border-bottom-left-radius: 5px; | |
| box-shadow: 0 2px 4px rgba(0, 0, 0, 0.05); | |
| } | |
| .chat-input-container { | |
| padding: 20px; | |
| border-top: 1px solid var(--gray); | |
| display: flex; | |
| flex-direction: column; | |
| gap: 15px; | |
| background: var(--light); | |
| } | |
| .input-row { | |
| display: flex; | |
| gap: 10px; | |
| } | |
| .chat-input { | |
| flex: 1; | |
| padding: 15px; | |
| background: var(--light-gray); | |
| border: 1px solid var(--gray); | |
| border-radius: 12px; | |
| font-size: 1rem; | |
| color: var(--dark); | |
| outline: none; | |
| transition: all 0.3s; | |
| } | |
| .chat-input:focus { | |
| border-color: var(--primary); | |
| box-shadow: 0 0 0 2px rgba(67, 97, 238, 0.1); | |
| } | |
| .chat-input::placeholder { | |
| color: var(--dark-gray); | |
| } | |
| .send-button { | |
| background: var(--primary); | |
| color: white; | |
| border: none; | |
| border-radius: 12px; | |
| padding: 0 25px; | |
| cursor: pointer; | |
| font-weight: 600; | |
| transition: all 0.3s; | |
| display: flex; | |
| align-items: center; | |
| gap: 8px; | |
| } | |
| .send-button:hover { | |
| background: var(--secondary); | |
| transform: translateY(-2px); | |
| } | |
| .send-button:disabled { | |
| opacity: 0.6; | |
| cursor: not-allowed; | |
| transform: none; | |
| } | |
| .typing-indicator { | |
| display: flex; | |
| align-items: center; | |
| gap: 5px; | |
| padding: 15px; | |
| background: var(--light); | |
| border-radius: 18px; | |
| align-self: flex-start; | |
| width: fit-content; | |
| margin-bottom: 10px; | |
| box-shadow: 0 2px 4px rgba(0, 0, 0, 0.05); | |
| } | |
| .typing-dot { | |
| width: 8px; | |
| height: 8px; | |
| background: var(--dark-gray); | |
| border-radius: 50%; | |
| animation: typing 1.4s infinite ease-in-out; | |
| } | |
| .typing-dot:nth-child(1) { animation-delay: -0.32s; } | |
| .typing-dot:nth-child(2) { animation-delay: -0.16s; } | |
| @keyframes typing { | |
| 0%, 80%, 100% { transform: scale(0.8); opacity: 0.5; } | |
| 40% { transform: scale(1); opacity: 1; } | |
| } | |
| /* 步骤容器样式 */ | |
| .step-container { | |
| margin: 8px 0; | |
| background: rgba(67, 97, 238, 0.05); | |
| border: 1px solid rgba(67, 97, 238, 0.2); | |
| border-radius: 8px; | |
| overflow: hidden; | |
| } | |
| .step-header { | |
| padding: 8px 12px; | |
| background: rgba(67, 97, 238, 0.1); | |
| cursor: pointer; | |
| display: flex; | |
| align-items: center; | |
| justify-content: space-between; | |
| font-weight: 600; | |
| color: var(--primary); | |
| transition: background 0.3s; | |
| font-size: 0.9rem; | |
| } | |
| .step-header:hover { | |
| background: rgba(67, 97, 238, 0.15); | |
| } | |
| .step-toggle { | |
| font-size: 0.7rem; | |
| transition: transform 0.3s; | |
| } | |
| .step-toggle.expanded { | |
| transform: rotate(180deg); | |
| } | |
| .step-content { | |
| max-height: 300px; | |
| overflow: auto; | |
| transition: max-height 0.3s ease, padding 0.3s ease; | |
| padding: 12px; | |
| background: var(--light); | |
| border-top: 1px solid rgba(67, 97, 238, 0.1); | |
| font-size: 0.9rem; | |
| line-height: 1.4; | |
| } | |
| .step-content.collapsed { | |
| max-height: 0 ; | |
| padding: 0 ; | |
| border-top: none ; | |
| } | |
| .step-item { | |
| margin-bottom: 10px; | |
| padding: 8px; | |
| background: var(--light-gray); | |
| border-radius: 6px; | |
| } | |
| .step-item:last-child { | |
| margin-bottom: 0; | |
| } | |
| .step-thought { | |
| color: var(--primary); | |
| font-weight: 500; | |
| margin-bottom: 5px; | |
| } | |
| .step-action { | |
| color: var(--secondary); | |
| font-family: monospace; | |
| font-size: 0.85rem; | |
| margin-bottom: 5px; | |
| } | |
| .step-observation { | |
| color: var(--dark-gray); | |
| font-size: 0.85rem; | |
| white-space: pre-wrap; | |
| word-break: break-word; | |
| } | |
| .step-observation.success { | |
| color: #2e7d32; | |
| } | |
| .step-observation.error { | |
| color: #c62828; | |
| } | |
| /* 最终答案样式 */ | |
| .final-answer { | |
| margin-top: 10px; | |
| padding: 12px; | |
| border-radius: 8px; | |
| } | |
| .final-answer-title { | |
| font-weight: 600; | |
| margin-bottom: 8px; | |
| } | |
| .final-answer-content { | |
| line-height: 1.5; | |
| } | |
| @media (max-width: 768px) { | |
| .chat-card { | |
| height: 100vh; | |
| border-radius: 0; | |
| } | |
| .message { | |
| max-width: 90%; | |
| } | |
| } | |
| .disclaimer { | |
| font-size: 0.75rem; | |
| color: var(--dark-gray); | |
| text-align: center; | |
| margin-top: 8px; | |
| } | |
| .page-footer { | |
| font-size: 0.75rem; | |
| color: var(--dark-gray); | |
| text-align: center; | |
| margin-top: 12px; | |
| } | |
| /* 弹窗样式 */ | |
| .modal-overlay { | |
| position: fixed; | |
| top: 0; | |
| left: 0; | |
| right: 0; | |
| bottom: 0; | |
| background: rgba(0, 0, 0, 0.5); | |
| display: flex; | |
| align-items: center; | |
| justify-content: center; | |
| z-index: 9999; | |
| opacity: 0; | |
| visibility: hidden; | |
| transition: all 0.3s ease; | |
| } | |
| .modal-overlay.active { | |
| opacity: 1; | |
| visibility: visible; | |
| } | |
| .modal-overlay.fade-out { | |
| opacity: 0; | |
| visibility: hidden; | |
| } | |
| .modal-container { | |
| background: var(--light); | |
| border-radius: 16px; | |
| box-shadow: 0 20px 60px rgba(0, 0, 0, 0.3); | |
| max-width: 500px; | |
| width: 90%; | |
| max-height: 80vh; | |
| overflow: auto; | |
| transform: scale(0.9) translateY(20px); | |
| transition: all 0.3s cubic-bezier(0.68, -0.55, 0.265, 1.55); | |
| } | |
| .modal-overlay.active .modal-container { | |
| transform: scale(1) translateY(0); | |
| } | |
| .modal-header { | |
| padding: 20px 24px; | |
| border-bottom: 1px solid var(--gray); | |
| display: flex; | |
| align-items: center; | |
| gap: 12px; | |
| } | |
| .modal-header-icon { | |
| font-size: 1.5rem; | |
| color: var(--primary); | |
| } | |
| .modal-header-title { | |
| font-size: 1.25rem; | |
| font-weight: 600; | |
| color: var(--dark); | |
| flex: 1; | |
| } | |
| .modal-close-button { | |
| background: transparent; | |
| border: none; | |
| color: var(--dark-gray); | |
| font-size: 1.5rem; | |
| cursor: pointer; | |
| padding: 4px 8px; | |
| border-radius: 4px; | |
| transition: all 0.2s; | |
| line-height: 1; | |
| } | |
| .modal-close-button:hover { | |
| background: var(--light-gray); | |
| color: var(--dark); | |
| } | |
| .modal-content { | |
| padding: 24px; | |
| line-height: 1.6; | |
| color: var(--dark); | |
| } | |
| .modal-content p { | |
| margin-bottom: 12px; | |
| } | |
| .modal-content p:last-child { | |
| margin-bottom: 0; | |
| } | |
| .modal-footer { | |
| padding: 16px 24px; | |
| border-top: 1px solid var(--gray); | |
| display: flex; | |
| gap: 12px; | |
| justify-content: flex-end; | |
| flex-wrap: wrap; | |
| } | |
| .modal-button { | |
| padding: 10px 20px; | |
| border-radius: 8px; | |
| font-size: 0.95rem; | |
| font-weight: 500; | |
| cursor: pointer; | |
| transition: all 0.2s; | |
| border: none; | |
| outline: none; | |
| } | |
| .modal-button-primary { | |
| background: var(--primary); | |
| color: white; | |
| } | |
| .modal-button-primary:hover { | |
| background: var(--secondary); | |
| transform: translateY(-1px); | |
| } | |
| .modal-button-secondary { | |
| background: var(--light-gray); | |
| color: var(--dark); | |
| border: 1px solid var(--gray); | |
| } | |
| .modal-button-secondary:hover { | |
| background: var(--gray); | |
| } | |
| .modal-button-link { | |
| background: transparent; | |
| color: var(--primary); | |
| padding: 10px 16px; | |
| } | |
| .modal-button-link:hover { | |
| background: rgba(67, 97, 238, 0.1); | |
| } | |
| @media (max-width: 600px) { | |
| .modal-container { | |
| width: 95%; | |
| max-height: 90vh; | |
| } | |
| .modal-footer { | |
| flex-direction: column; | |
| } | |
| .modal-button { | |
| width: 100%; | |
| } | |
| } | |
| </style> | |
| </head> | |
| <body> | |
| <div class="container"> | |
| <div class="chat-card"> | |
| <div class="chat-header"> | |
| <div class="header-title">astrbot-问题帮助助手</div> | |
| <div class="header-actions"> | |
| <button class="clear-btn" id="clearBtn" title="开始新对话"> | |
| <i class="fas fa-plus"></i> 新对话 | |
| </button> | |
| </div> | |
| </div> | |
| <div class="chat-messages" id="chatMessages"> | |
| <div class="message bot-message"> | |
| 您好!我是astrbot帮助助手,一个智能帮助助手。<br><br> | |
| 💡 我可以帮您:<br> | |
| • 了解astrbot<br> | |
| • 协助安装<br> | |
| • 解决遇到的问题<br> | |
| 请直接输入您的问题! | |
| </div> | |
| </div> | |
| <div class="chat-input-container"> | |
| <div class="input-row"> | |
| <input type="text" class="chat-input" id="questionInput" placeholder="输入关于代码的问题..."> | |
| <button class="send-button" id="sendButton"> | |
| <i class="fas fa-paper-plane"></i> | |
| </button> | |
| </div> | |
| </div> | |
| </div> | |
| <div class="page-footer"> | |
| ⚠️ AI 回复仅供参考,请自行鉴别准确性 | |
| </div> | |
| </div> | |
| <!-- 弹窗容器 --> | |
| <div id="modalOverlay" class="modal-overlay"> | |
| <div class="modal-container" id="modalContainer"> | |
| <div class="modal-header"> | |
| <i id="modalHeaderIcon" class="modal-header-icon"></i> | |
| <h3 id="modalHeaderTitle" class="modal-header-title"></h3> | |
| <button id="modalCloseButton" class="modal-close-button" aria-label="关闭"> | |
| <i class="fas fa-times"></i> | |
| </button> | |
| </div> | |
| <div id="modalContent" class="modal-content"></div> | |
| <div id="modalFooter" class="modal-footer"></div> | |
| </div> | |
| </div> | |
| <script> | |
| // 会话管理 | |
| const SESSION_KEY = 'astrbot_session_id'; | |
| function getSessionId() { | |
| let sessionId = localStorage.getItem(SESSION_KEY); | |
| if (!sessionId) { | |
| sessionId = generateSessionId(); | |
| localStorage.setItem(SESSION_KEY, sessionId); | |
| } | |
| return sessionId; | |
| } | |
| function generateSessionId() { | |
| return 'session_' + Date.now() + '_' + Math.random().toString(36).substr(2, 9); | |
| } | |
| function clearSession() { | |
| localStorage.removeItem(SESSION_KEY); | |
| } | |
| function setSessionId(sessionId) { | |
| localStorage.setItem(SESSION_KEY, sessionId); | |
| } | |
| // 消息历史记录 | |
| let messageHistory = []; | |
| const MAX_HISTORY_LENGTH = 20; | |
| // 防止重复发送的标志 | |
| let isSending = false; | |
| // 获取DOM元素 | |
| const chatMessages = document.getElementById('chatMessages'); | |
| const questionInput = document.getElementById('questionInput'); | |
| const sendButton = document.getElementById('sendButton'); | |
| const clearBtn = document.getElementById('clearBtn'); | |
| // 新建会话 | |
| clearBtn.addEventListener('click', async function() { | |
| if (!confirm('确定要开始新对话吗?当前对话将被清除。')) { | |
| return; | |
| } | |
| try { | |
| const sessionId = getSessionId(); | |
| await fetch('/api/session/clear', { | |
| method: 'POST', | |
| headers: { 'Content-Type': 'application/json' }, | |
| body: JSON.stringify({ session_id: sessionId }) | |
| }); | |
| } catch (e) { | |
| console.error('清除会话失败:', e); | |
| } | |
| // 生成新的会话 ID | |
| const newSessionId = generateSessionId(); | |
| setSessionId(newSessionId); | |
| // 清除聊天记录 | |
| chatMessages.innerHTML = ` | |
| <div class="message bot-message"> | |
| 您好!我是astrbot帮助助手。(非官方)<br><br> | |
| 💡 我可以帮您:<br> | |
| • 了解astrbot<br> | |
| • 协助安装<br> | |
| • 解决遇到的问题<br> | |
| 请直接输入您的问题! | |
| </div> | |
| `; | |
| messageHistory = []; | |
| }); | |
| // 发送消息 | |
| async function sendMessage() { | |
| // 防止重复发送 | |
| if (isSending) { | |
| return; | |
| } | |
| const message = questionInput.value.trim(); | |
| if (!message) { | |
| return; | |
| } | |
| // 设置发送标志 | |
| isSending = true; | |
| sendButton.disabled = true; | |
| sendButton.style.opacity = '0.6'; | |
| // 添加用户消息 | |
| addMessage(message, 'user'); | |
| messageHistory.push({ role: 'user', content: message }); | |
| // 清空输入 | |
| questionInput.value = ''; | |
| // 显示机器人正在输入 | |
| showTypingIndicator(); | |
| try { | |
| console.log('发送请求到 /api/ask...'); | |
| // 获取会话 ID | |
| const sessionId = getSessionId(); | |
| // 调用 Read Agent API(流式) | |
| const response = await fetch('/api/ask', { | |
| method: 'POST', | |
| headers: { | |
| 'Content-Type': 'application/json' | |
| }, | |
| body: JSON.stringify({ | |
| question: message, | |
| stream: true, | |
| session_id: sessionId | |
| }) | |
| }); | |
| console.log('响应状态:', response.status); | |
| if (!response.ok) { | |
| const errorText = await response.text(); | |
| console.error('API 错误:', response.status, errorText); | |
| throw new Error(`API请求失败: ${response.status} - ${errorText}`); | |
| } | |
| console.log('开始读取流式响应...'); | |
| console.log('response.body:', response.body); | |
| if (!response.body) { | |
| console.error('response.body is null!'); | |
| throw new Error('响应体为空'); | |
| } | |
| // 创建消息容器(但不移除 loading,等收到第一个数据再移除) | |
| const messageDiv = document.createElement('div'); | |
| messageDiv.classList.add('message', 'bot-message'); | |
| chatMessages.appendChild(messageDiv); | |
| // 创建步骤容器 | |
| const stepContainer = document.createElement('div'); | |
| stepContainer.className = 'step-container'; | |
| stepContainer.style.display = 'none'; | |
| const stepHeader = document.createElement('div'); | |
| stepHeader.className = 'step-header'; | |
| const stepTitle = document.createElement('span'); | |
| stepTitle.textContent = '分析过程'; | |
| const stepToggle = document.createElement('span'); | |
| stepToggle.className = 'step-toggle'; | |
| stepToggle.textContent = '▼'; | |
| const stepContent = document.createElement('div'); | |
| stepContent.className = 'step-content'; | |
| stepHeader.appendChild(stepTitle); | |
| stepHeader.appendChild(stepToggle); | |
| stepContainer.appendChild(stepHeader); | |
| stepContainer.appendChild(stepContent); | |
| // 添加点击事件 | |
| stepHeader.onclick = function() { | |
| const isCollapsing = stepContent.classList.contains('collapsed'); | |
| stepContent.classList.toggle('collapsed'); | |
| stepToggle.classList.toggle('expanded'); | |
| // 展开时滚动到底部 | |
| if (isCollapsing) { | |
| requestAnimationFrame(() => { | |
| stepContent.scrollTop = stepContent.scrollHeight; | |
| }); | |
| } | |
| }; | |
| messageDiv.appendChild(stepContainer); | |
| // 创建最终答案容器(打字机效果) | |
| const finalAnswerDiv = document.createElement('div'); | |
| finalAnswerDiv.className = 'final-answer'; | |
| finalAnswerDiv.style.display = 'none'; | |
| const answerTitle = document.createElement('div'); | |
| answerTitle.className = 'final-answer-title'; | |
| answerTitle.textContent = ''; | |
| answerTitle.style.display = 'none'; | |
| const answerContent = document.createElement('div'); | |
| answerContent.className = 'final-answer-content'; | |
| finalAnswerDiv.appendChild(answerTitle); | |
| finalAnswerDiv.appendChild(answerContent); | |
| messageDiv.appendChild(finalAnswerDiv); | |
| let currentThoughtDiv = null; | |
| let currentThoughtText = ''; | |
| let currentStepItem = null; | |
| let finalAnswer = ''; | |
| let hasSteps = false; | |
| let isThinking = false; | |
| let isAnswering = false; | |
| let hasReceivedData = false; // 标记是否收到数据 | |
| // 处理流式响应 | |
| const reader = response.body.getReader(); | |
| const decoder = new TextDecoder(); | |
| console.log('开始读取流式响应...'); | |
| while (true) { | |
| const { done, value } = await reader.read(); | |
| if (done) { | |
| console.log('流式响应结束'); | |
| break; | |
| } | |
| const chunk = decoder.decode(value); | |
| console.log('收到数据:', chunk); | |
| const lines = chunk.split('\n'); | |
| for (const line of lines) { | |
| if (line.startsWith('data: ')) { | |
| const data = line.slice(6); | |
| if (data === '[DONE]') { | |
| continue; | |
| } | |
| try { | |
| const dataObj = JSON.parse(data); | |
| if (dataObj.type === 'session_id') { | |
| // 更新本地会话 ID | |
| if (dataObj.session_id) { | |
| setSessionId(dataObj.session_id); | |
| } | |
| continue; | |
| } else if (dataObj.type === 'start') { | |
| // 开始 | |
| continue; | |
| } else if (dataObj.type === 'question') { | |
| // 问题 | |
| continue; | |
| } else if (dataObj.type === 'chunk') { | |
| // 收到第一个数据时移除 loading | |
| if (!hasReceivedData) { | |
| hasReceivedData = true; | |
| removeTypingIndicator(); | |
| } | |
| // 打字机效果 - 逐字输出 | |
| if (dataObj.stream_type === 'thought') { | |
| // 思考输出 | |
| if (!isThinking) { | |
| isThinking = true; | |
| hasSteps = true; | |
| stepContainer.style.display = 'block'; | |
| // 确保展开状态 | |
| stepContent.classList.remove('collapsed'); | |
| stepToggle.classList.add('expanded'); | |
| currentStepItem = document.createElement('div'); | |
| currentStepItem.className = 'step-item'; | |
| stepContent.appendChild(currentStepItem); | |
| requestAnimationFrame(() => { | |
| console.log('滚动前 - scrollHeight:', stepContent.scrollHeight, 'scrollTop:', stepContent.scrollTop, 'clientHeight:', stepContent.clientHeight, 'collapsed:', stepContent.classList.contains('collapsed')); | |
| stepContent.scrollTop = stepContent.scrollHeight; | |
| console.log('滚动后 - scrollHeight:', stepContent.scrollHeight, 'scrollTop:', stepContent.scrollTop); | |
| }); | |
| currentThoughtDiv = document.createElement('div'); | |
| currentThoughtDiv.className = 'step-thought'; | |
| currentThoughtDiv.textContent = '💭 '; | |
| currentStepItem.appendChild(currentThoughtDiv); | |
| chatMessages.scrollTop = chatMessages.scrollHeight; | |
| } | |
| currentThoughtText += dataObj.content; | |
| currentThoughtDiv.textContent = '💭 ' + currentThoughtText; | |
| chatMessages.scrollTop = chatMessages.scrollHeight; | |
| requestAnimationFrame(() => { | |
| stepContent.scrollTop = stepContent.scrollHeight; | |
| }); | |
| } else if (dataObj.stream_type === 'answer') { | |
| // 最终答案输出 | |
| if (!isAnswering) { | |
| isAnswering = true; | |
| finalAnswerDiv.style.display = 'block'; | |
| } | |
| finalAnswer += dataObj.content; | |
| answerContent.innerHTML = renderMarkdown(finalAnswer); | |
| chatMessages.scrollTop = chatMessages.scrollHeight; | |
| } | |
| } else if (dataObj.type === 'step_thought_done') { | |
| // 思考完成,准备行动 | |
| isThinking = false; | |
| currentThoughtText = ''; | |
| currentThoughtDiv = null; | |
| if (dataObj.has_action) { | |
| const actionDiv = document.createElement('div'); | |
| actionDiv.className = 'step-action'; | |
| actionDiv.textContent = '🔧 正在执行工具...'; | |
| currentStepItem.appendChild(actionDiv); | |
| requestAnimationFrame(() => { | |
| stepContent.scrollTop = stepContent.scrollHeight; | |
| }); | |
| } | |
| chatMessages.scrollTop = chatMessages.scrollHeight; | |
| } else if (dataObj.type === 'step') { | |
| // 步骤完成(工具调用结果) | |
| hasSteps = true; | |
| stepContainer.style.display = 'block'; | |
| const stepItem = document.createElement('div'); | |
| stepItem.className = 'step-item'; | |
| if (dataObj.thought) { | |
| const thoughtDiv = document.createElement('div'); | |
| thoughtDiv.className = 'step-thought'; | |
| thoughtDiv.textContent = `💭 ${dataObj.thought}`; | |
| stepItem.appendChild(thoughtDiv); | |
| } | |
| if (dataObj.action) { | |
| const actionDiv = document.createElement('div'); | |
| actionDiv.className = 'step-action'; | |
| actionDiv.textContent = `🔧 ${dataObj.action}`; | |
| stepItem.appendChild(actionDiv); | |
| } | |
| if (dataObj.observation) { | |
| const obsDiv = document.createElement('div'); | |
| obsDiv.className = 'step-observation'; | |
| if (dataObj.observation.success) { | |
| obsDiv.classList.add('success'); | |
| obsDiv.textContent = `✅ ${JSON.stringify(dataObj.observation.result, null, 2)}`; | |
| } else { | |
| obsDiv.classList.add('error'); | |
| obsDiv.textContent = `❌ ${dataObj.observation.error}`; | |
| } | |
| stepItem.appendChild(obsDiv); | |
| } | |
| if (dataObj.final_answer) { | |
| finalAnswer = dataObj.final_answer; | |
| isAnswering = true; | |
| finalAnswerDiv.style.display = 'block'; | |
| answerContent.innerHTML = renderMarkdown(finalAnswer); | |
| } | |
| stepContent.appendChild(stepItem); | |
| // 确保展开状态再滚动 | |
| stepContent.classList.remove('collapsed'); | |
| stepToggle.classList.add('expanded'); | |
| requestAnimationFrame(() => { | |
| stepContent.scrollTop = stepContent.scrollHeight; | |
| }); | |
| chatMessages.scrollTop = chatMessages.scrollHeight; | |
| } else if (dataObj.type === 'done') { | |
| // 完成,自动折叠分析过程 | |
| if (hasSteps) { | |
| stepContent.classList.add('collapsed'); | |
| stepToggle.classList.remove('expanded'); | |
| } | |
| chatMessages.scrollTop = chatMessages.scrollHeight; | |
| } else if (dataObj.type === 'error') { | |
| throw new Error(dataObj.error); | |
| } | |
| } catch (e) { | |
| // 忽略解析错误 | |
| } | |
| } | |
| } | |
| } | |
| // 如果没有步骤,隐藏步骤容器 | |
| if (!hasSteps) { | |
| stepContainer.style.display = 'none'; | |
| } else if (finalAnswer) { | |
| // 有步骤且有最终答案时,折叠分析过程 | |
| stepContent.classList.add('collapsed'); | |
| stepToggle.classList.remove('expanded'); | |
| } | |
| // 如果没有最终答案,但有内容 | |
| if (finalAnswer && finalAnswerDiv.style.display === 'none') { | |
| finalAnswerDiv.style.display = 'block'; | |
| } | |
| // 添加到历史记录 | |
| messageHistory.push({ role: 'assistant', content: finalAnswer }); | |
| // 限制历史记录长度 | |
| if (messageHistory.length > MAX_HISTORY_LENGTH) { | |
| messageHistory = messageHistory.slice(-MAX_HISTORY_LENGTH); | |
| } | |
| } catch (error) { | |
| console.error('Error:', error); | |
| removeTypingIndicator(); | |
| addMessage('抱歉,获取帮助时出现错误。请检查配置后重试。', 'bot', true); | |
| } finally { | |
| isSending = false; | |
| sendButton.disabled = false; | |
| sendButton.style.opacity = '1'; | |
| } | |
| } | |
| // 简单的Markdown渲染函数 | |
| function renderMarkdown(text) { | |
| // 处理代码块 | |
| text = text.replace(/```(\w+)?\n([\s\S]*?)```/g, '<pre><code>$2</code></pre>'); | |
| text = text.replace(/`([^`]+)`/g, '<code>$1</code>'); | |
| // 处理标题 | |
| text = text.replace(/^### (.*$)/gim, '<h3>$1</h3>'); | |
| text = text.replace(/^## (.*$)/gim, '<h2>$1</h2>'); | |
| text = text.replace(/^# (.*$)/gim, '<h1>$1</h1>'); | |
| // 处理粗体和斜体 | |
| text = text.replace(/\*\*(.+?)\*\*/g, '<strong>$1</strong>'); | |
| text = text.replace(/\*(.+?)\*/g, '<em>$1</em>'); | |
| // 处理链接 | |
| text = text.replace(/\[([^\]]+)\]\(([^)]+)\)/g, '<a href="$2" target="_blank">$1</a>'); | |
| // 处理列表 | |
| text = text.replace(/^\* (.+)/gim, '<li>$1</li>'); | |
| text = text.replace(/^\- (.+)/gim, '<li>$1</li>'); | |
| text = text.replace(/^(\d+)\. (.+)/gim, '<li>$2</li>'); | |
| // 处理换行 | |
| text = text.replace(/\n/g, '<br>'); | |
| return text; | |
| } | |
| // 添加消息到聊天区域 | |
| function addMessage(text, sender, isError = false) { | |
| const messageDiv = document.createElement('div'); | |
| messageDiv.classList.add('message'); | |
| if (isError) { | |
| messageDiv.style.background = '#ffebee'; | |
| messageDiv.style.color = '#c62828'; | |
| messageDiv.style.border = '1px solid #ef5350'; | |
| } else { | |
| messageDiv.classList.add(sender === 'user' ? 'user-message' : 'bot-message'); | |
| } | |
| let formattedText; | |
| if (sender === 'bot' && !isError) { | |
| formattedText = renderMarkdown(text); | |
| } else { | |
| formattedText = text.replace(/\n/g, '<br>'); | |
| } | |
| messageDiv.innerHTML = formattedText; | |
| chatMessages.appendChild(messageDiv); | |
| chatMessages.scrollTop = chatMessages.scrollHeight; | |
| } | |
| // 显示正在输入指示器 | |
| function showTypingIndicator() { | |
| const typingDiv = document.createElement('div'); | |
| typingDiv.classList.add('typing-indicator'); | |
| typingDiv.id = 'typingIndicator'; | |
| typingDiv.innerHTML = ` | |
| <div class="typing-dot"></div> | |
| <div class="typing-dot"></div> | |
| <div class="typing-dot"></div> | |
| `; | |
| chatMessages.appendChild(typingDiv); | |
| chatMessages.scrollTop = chatMessages.scrollHeight; | |
| } | |
| // 移除正在输入指示器 | |
| function removeTypingIndicator() { | |
| const typingIndicator = document.getElementById('typingIndicator'); | |
| if (typingIndicator) { | |
| typingIndicator.remove(); | |
| } | |
| } | |
| // 事件监听 | |
| sendButton.addEventListener('click', sendMessage); | |
| questionInput.addEventListener('keypress', function(e) { | |
| if (e.key === 'Enter') { | |
| sendMessage(); | |
| } | |
| }); | |
| // ===== 弹窗管理 ===== | |
| const ModalManager = { | |
| config: null, | |
| overlay: null, | |
| container: null, | |
| storageKey: null, | |
| async init() { | |
| this.overlay = document.getElementById('modalOverlay'); | |
| this.container = document.getElementById('modalContainer'); | |
| if (!this.overlay || !this.container) { | |
| console.warn('弹窗元素未找到'); | |
| return; | |
| } | |
| await this.loadConfig(); | |
| if (this.shouldShow()) { | |
| const delay = this.config.display?.delay || 500; | |
| setTimeout(() => this.show(), delay); | |
| } | |
| this.bindEvents(); | |
| }, | |
| async loadConfig() { | |
| try { | |
| const response = await fetch('/api/popup/config'); | |
| const data = await response.json(); | |
| this.config = data; | |
| this.storageKey = data.storage?.key || 'popup_dismissed'; | |
| console.log('弹窗配置加载成功:', this.config); | |
| } catch (error) { | |
| console.error('加载弹窗配置失败:', error); | |
| this.config = { enabled: false }; | |
| } | |
| }, | |
| shouldShow() { | |
| if (!this.config || !this.config.enabled) { | |
| return false; | |
| } | |
| // 检查用户是否已选择"不再显示" | |
| const dismissed = this.getStorageValue(); | |
| if (dismissed) { | |
| return false; | |
| } | |
| // 检查显示规则 | |
| const showRules = this.config.showRules || {}; | |
| const maxShows = showRules.maxShows || 0; | |
| const frequency = showRules.frequency || 'once'; | |
| if (maxShows > 0) { | |
| const showCount = this.getShowCount(); | |
| if (showCount >= maxShows) { | |
| return false; | |
| } | |
| } | |
| if (frequency === 'daily') { | |
| const lastShowDate = this.getLastShowDate(); | |
| const today = new Date().toDateString(); | |
| if (lastShowDate === today) { | |
| return false; | |
| } | |
| } | |
| return true; | |
| }, | |
| show() { | |
| if (!this.config || !this.config.enabled) { | |
| return; | |
| } | |
| this.renderContent(); | |
| this.overlay.classList.add('active'); | |
| this.incrementShowCount(); | |
| this.setLastShowDate(); | |
| console.log('弹窗已显示'); | |
| }, | |
| hide() { | |
| this.overlay.classList.remove('active'); | |
| this.overlay.classList.add('fade-out'); | |
| setTimeout(() => { | |
| this.overlay.classList.remove('fade-out'); | |
| }, 300); | |
| console.log('弹窗已隐藏'); | |
| }, | |
| dismiss() { | |
| this.setStorageValue(true); | |
| this.hide(); | |
| console.log('用户选择不再显示弹窗'); | |
| }, | |
| renderContent() { | |
| const content = this.config.content || {}; | |
| const display = this.config.display || {}; | |
| document.getElementById('modalHeaderTitle').textContent = content.title || '提示'; | |
| const icon = document.getElementById('modalHeaderIcon'); | |
| icon.className = 'modal-header-icon ' + (content.icon || 'fas fa-info-circle'); | |
| const contentDiv = document.getElementById('modalContent'); | |
| contentDiv.innerHTML = content.html || ''; | |
| this.renderButtons(); | |
| const closeButton = document.getElementById('modalCloseButton'); | |
| closeButton.style.display = display.showCloseButton !== false ? 'block' : 'none'; | |
| }, | |
| renderButtons() { | |
| const footer = document.getElementById('modalFooter'); | |
| footer.innerHTML = ''; | |
| const buttons = this.config.buttons || []; | |
| buttons.forEach(button => { | |
| const btn = document.createElement('button'); | |
| btn.textContent = button.text || '确定'; | |
| const buttonClass = 'modal-button'; | |
| switch (button.type) { | |
| case 'primary': | |
| btn.className = buttonClass + ' modal-button-primary'; | |
| break; | |
| case 'secondary': | |
| btn.className = buttonClass + ' modal-button-secondary'; | |
| break; | |
| case 'link': | |
| btn.className = buttonClass + ' modal-button-link'; | |
| break; | |
| default: | |
| btn.className = buttonClass + ' modal-button-secondary'; | |
| } | |
| btn.addEventListener('click', () => this.handleButtonClick(button)); | |
| footer.appendChild(btn); | |
| }); | |
| }, | |
| handleButtonClick(button) { | |
| switch (button.action) { | |
| case 'close': | |
| this.hide(); | |
| break; | |
| case 'dismiss': | |
| this.dismiss(); | |
| break; | |
| case 'url': | |
| if (button.url) { | |
| window.open(button.url, '_blank'); | |
| this.hide(); | |
| } | |
| break; | |
| default: | |
| this.hide(); | |
| } | |
| }, | |
| bindEvents() { | |
| const display = this.config.display || {}; | |
| const closeButton = document.getElementById('modalCloseButton'); | |
| if (closeButton) { | |
| closeButton.addEventListener('click', () => this.hide()); | |
| } | |
| if (display.closeOnOutsideClick !== false) { | |
| this.overlay.addEventListener('click', (e) => { | |
| if (e.target === this.overlay) { | |
| this.hide(); | |
| } | |
| }); | |
| } | |
| if (display.closeOnEscapeKey !== false) { | |
| document.addEventListener('keydown', (e) => { | |
| if (e.key === 'Escape' && this.overlay.classList.contains('active')) { | |
| this.hide(); | |
| } | |
| }); | |
| } | |
| const autoCloseAfter = display.autoCloseAfter || 0; | |
| if (autoCloseAfter > 0) { | |
| setTimeout(() => { | |
| if (this.overlay.classList.contains('active')) { | |
| this.hide(); | |
| } | |
| }, autoCloseAfter); | |
| } | |
| }, | |
| getStorageValue() { | |
| const storageType = this.config.storage?.type || 'localStorage'; | |
| const key = this.storageKey; | |
| if (storageType === 'cookie') { | |
| return this.getCookie(key); | |
| } else { | |
| return localStorage.getItem(key) === 'true'; | |
| } | |
| }, | |
| setStorageValue(value) { | |
| const storageType = this.config.storage?.type || 'localStorage'; | |
| const key = this.storageKey; | |
| const expiresIn = this.config.storage?.expiresIn || 0; | |
| if (storageType === 'cookie') { | |
| let expires = ''; | |
| if (expiresIn > 0) { | |
| const date = new Date(); | |
| date.setTime(date.getTime() + expiresIn); | |
| expires = '; expires=' + date.toUTCString(); | |
| } | |
| document.cookie = key + '=' + value + expires + '; path=/'; | |
| } else { | |
| localStorage.setItem(key, String(value)); | |
| } | |
| }, | |
| getCookie(name) { | |
| const value = '; ' + document.cookie; | |
| const parts = value.split('; ' + name + '='); | |
| if (parts.length === 2) { | |
| return parts.pop().split(';').shift(); | |
| } | |
| return null; | |
| }, | |
| getShowCount() { | |
| return parseInt(localStorage.getItem(this.storageKey + '_count') || '0'); | |
| }, | |
| incrementShowCount() { | |
| const count = this.getShowCount() + 1; | |
| localStorage.setItem(this.storageKey + '_count', String(count)); | |
| }, | |
| getLastShowDate() { | |
| return localStorage.getItem(this.storageKey + '_last_date') || ''; | |
| }, | |
| setLastShowDate() { | |
| localStorage.setItem(this.storageKey + '_last_date', new Date().toDateString()); | |
| }, | |
| async reload() { | |
| await this.loadConfig(); | |
| return this.config; | |
| }, | |
| reset() { | |
| localStorage.removeItem(this.storageKey); | |
| localStorage.removeItem(this.storageKey + '_count'); | |
| localStorage.removeItem(this.storageKey + '_last_date'); | |
| console.log('弹窗设置已重置'); | |
| } | |
| }; | |
| // 页面加载完成后初始化弹窗 | |
| document.addEventListener('DOMContentLoaded', () => { | |
| ModalManager.init(); | |
| }); | |
| // 暴露到全局,方便调试 | |
| window.ModalManager = ModalManager; | |
| </script> | |
| </body> | |
| </html> |