astrbot_help / static /index.html
qa1145's picture
Upload 28 files
d347708 verified
<!DOCTYPE html>
<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 !important;
padding: 0 !important;
border-top: none !important;
}
.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>