UNPKG

mcp-interactive-feedback-server

Version:

一个简洁高效的MCP服务器,支持AI与用户的实时交互问答

1,263 lines (1,067 loc) 44.2 kB
<!DOCTYPE html> <html lang="zh-CN"> <head> <meta charset="UTF-8"> <meta name="viewport" content="width=device-width, initial-scale=1.0, maximum-scale=1.0, user-scalable=no"> <title>AI 交互问答界面</title> <!-- Bootstrap 5 CSS --> <link href="libs/bootstrap.min.css" rel="stylesheet"> <script src="libs/marked.min.js"></script> <link rel="stylesheet" href="libs/github.min.css"> <script src="libs/highlight.min.js"></script> <style> html, body { height: 100%; overflow: hidden; padding: 0; margin: 0; box-sizing: border-box; } body { font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, sans-serif; background: #f5f5f5; } .app-container { height: 100%; display: flex; flex-direction: column; overflow: hidden; box-sizing: border-box; } .header { background: linear-gradient(135deg, #667eea 0%, #764ba2 100%); color: white; padding: 12px 0; box-shadow: 0 2px 10px rgba(0, 0, 0, 0.1); flex-shrink: 0; z-index: 1030; } .header h1 { font-size: 18px; margin: 0; font-weight: 600; } .header .btn { background: rgba(255, 255, 255, 0.2); color: white; border: none; font-size: 12px; padding: 6px 12px; border-radius: 20px; transition: all 0.3s ease; } .header .btn:hover { background: rgba(255, 255, 255, 0.3); color: white; transform: translateY(-1px); } .connection-status { background: rgba(255, 255, 255, 0.1); padding: 6px 12px; border-radius: 15px; font-size: 11px; display: inline-flex; align-items: center; gap: 6px; } .status-indicator { width: 8px; height: 8px; border-radius: 50%; background: #4CAF50; animation: pulse 2s infinite; } .status-indicator.disconnected { background: #f44336; animation: none; } @keyframes pulse { 0% { opacity: 1; } 50% { opacity: 0.5; } 100% { opacity: 1; } } .chat-container { flex: 1; display: flex; flex-direction: column; overflow: hidden; background: #f5f5f5; } .chat-messages { flex: 1; overflow-y: auto; padding: 20px; background: linear-gradient(to bottom, #e3f2fd 0%, #f5f5f5 100%); } .message-group { margin-bottom: 20px; } .message-bubble { max-width: 70%; padding: 12px 16px; border-radius: 18px; margin-bottom: 8px; position: relative; word-wrap: break-word; box-shadow: 0 2px 8px rgba(0, 0, 0, 0.1); animation: fadeInUp 0.3s ease; } .message-copy-btn { position: absolute; top: 8px; right: 8px; background: rgba(255, 255, 255, 0.9); border: 1px solid #ddd; border-radius: 4px; padding: 4px 8px; font-size: 11px; cursor: pointer; opacity: 0; transition: opacity 0.2s ease; z-index: 20; color: #666; min-width: 40px; text-align: center; } .message-copy-btn:hover { background: rgba(255, 255, 255, 1); border-color: #bbb; color: #333; } .message-bubble:hover .message-copy-btn { opacity: 1; } .message-copy-btn.copied { background: #d4edda; border-color: #c3e6cb; color: #155724; } .message-user .message-copy-btn { background: rgba(255, 255, 255, 0.2); border-color: rgba(255, 255, 255, 0.3); color: rgba(255, 255, 255, 0.8); } .message-user .message-copy-btn:hover { background: rgba(255, 255, 255, 0.3); border-color: rgba(255, 255, 255, 0.4); color: rgba(255, 255, 255, 1); } @keyframes fadeInUp { from { opacity: 0; transform: translateY(10px); } to { opacity: 1; transform: translateY(0); } } .message-ai { background: white; border: 1px solid #e1e5e9; margin-right: auto; border-bottom-left-radius: 6px; } .message-ai::before { content: ''; position: absolute; left: -8px; bottom: 8px; width: 0; height: 0; border-style: solid; border-width: 0 8px 8px 0; border-color: transparent white transparent transparent; } .message-user { background: linear-gradient(135deg, #667eea 0%, #764ba2 100%); color: white; margin-left: auto; border-bottom-right-radius: 6px; } .message-user::after { content: ''; position: absolute; right: -8px; bottom: 8px; width: 0; height: 0; border-style: solid; border-width: 0 0 8px 8px; border-color: transparent transparent #764ba2 transparent; } .message-pending { background: #fff3cd; border: 1px solid #ffeaa7; margin-right: auto; border-bottom-left-radius: 6px; position: relative; } .message-pending::before { content: ''; position: absolute; left: -8px; bottom: 8px; width: 0; height: 0; border-style: solid; border-width: 0 8px 8px 0; border-color: transparent #fff3cd transparent transparent; } .message-pending .pending-indicator { display: inline-flex; align-items: center; gap: 8px; color: #856404; font-style: italic; } .typing-indicator { display: inline-flex; gap: 4px; } .typing-dot { width: 6px; height: 6px; border-radius: 50%; background: #856404; 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; } } .message-avatar { width: 32px; height: 32px; border-radius: 50%; margin-bottom: 8px; display: flex; align-items: center; justify-content: center; font-size: 16px; font-weight: bold; } .avatar-ai { background: linear-gradient(135deg, #667eea 0%, #764ba2 100%); color: white; } .avatar-user { background: linear-gradient(135deg, #4CAF50 0%, #45a049 100%); color: white; } .message-time { font-size: 11px; color: #999; text-align: center; margin: 8px 0; } .message-content { line-height: 1.5; } .message-ai .message-content { color: #333; } .message-user .message-content { color: white; } .input-container { background: white; border-top: 1px solid #e1e5e9; padding: 16px 20px; flex-shrink: 0; box-shadow: 0 -2px 10px rgba(0, 0, 0, 0.1); } .input-wrapper { display: flex; align-items: flex-end; gap: 12px; max-width: 100%; } .input-field { flex: 1; min-height: 40px; max-height: 120px; border: 1px solid #e1e5e9; border-radius: 20px; padding: 10px 16px; resize: none; font-size: 14px; line-height: 1.4; transition: border-color 0.3s ease; overflow-y: auto; } .input-field:focus { outline: none; border-color: #667eea; box-shadow: 0 0 0 3px rgba(102, 126, 234, 0.1); } .send-button { width: 40px; height: 40px; border-radius: 50%; border: none; background: linear-gradient(135deg, #667eea 0%, #764ba2 100%); color: white; display: flex; align-items: center; justify-content: center; font-size: 16px; cursor: pointer; transition: all 0.3s ease; flex-shrink: 0; } .send-button:hover { transform: scale(1.05); box-shadow: 0 4px 12px rgba(102, 126, 234, 0.3); } .send-button:disabled { background: #ccc; cursor: not-allowed; transform: none; box-shadow: none; } .empty-state { display: flex; flex-direction: column; align-items: center; justify-content: center; height: 100%; color: #666; text-align: center; padding: 40px; } .empty-state-icon { font-size: 48px; margin-bottom: 16px; opacity: 0.5; } .empty-state-title { font-size: 18px; font-weight: 600; margin-bottom: 8px; } .empty-state-subtitle { font-size: 14px; opacity: 0.7; } /* Markdown content styles */ .markdown-content { font-size: 14px; line-height: 1.6; word-wrap: break-word; } .markdown-content h1, .markdown-content h2, .markdown-content h3, .markdown-content h4, .markdown-content h5, .markdown-content h6 { margin: 12px 0 8px 0; font-weight: 600; } .message-ai .markdown-content h1, .message-ai .markdown-content h2, .message-ai .markdown-content h3, .message-ai .markdown-content h4, .message-ai .markdown-content h5, .message-ai .markdown-content h6 { color: #2c3e50; } .message-user .markdown-content h1, .message-user .markdown-content h2, .message-user .markdown-content h3, .message-user .markdown-content h4, .message-user .markdown-content h5, .message-user .markdown-content h6 { color: rgba(255, 255, 255, 0.9); } .markdown-content h1 { font-size: 18px; } .markdown-content h2 { font-size: 16px; } .markdown-content h3 { font-size: 15px; } .markdown-content h4 { font-size: 14px; } .markdown-content p { margin: 6px 0; } .markdown-content ul, .markdown-content ol { margin: 6px 0; padding-left: 16px; } .markdown-content li { margin: 2px 0; } .markdown-content blockquote { border-left: 3px solid #667eea; margin: 8px 0; padding: 6px 12px; background: rgba(102, 126, 234, 0.1); border-radius: 4px; font-style: italic; } .message-user .markdown-content blockquote { border-left-color: rgba(255, 255, 255, 0.5); background: rgba(255, 255, 255, 0.1); } .markdown-content code { background: rgba(0, 0, 0, 0.1); padding: 2px 4px; border-radius: 3px; font-family: 'Monaco', 'Menlo', 'Ubuntu Mono', monospace; font-size: 12px; } .message-user .markdown-content code { background: rgba(255, 255, 255, 0.2); color: rgba(255, 255, 255, 0.9); } .markdown-content pre { background: rgba(0, 0, 0, 0.05); border: 1px solid rgba(0, 0, 0, 0.1); border-radius: 6px; padding: 8px; margin: 8px 0; overflow-x: auto; position: relative; } .message-user .markdown-content pre { background: rgba(255, 255, 255, 0.1); border-color: rgba(255, 255, 255, 0.2); } .markdown-content pre code { background: none; padding: 0; font-size: 12px; } .copy-button { position: absolute; top: 6px; right: 6px; background: rgba(255, 255, 255, 0.9); border: 1px solid #ddd; border-radius: 4px; padding: 2px 6px; font-size: 11px; cursor: pointer; opacity: 0; transition: opacity 0.2s ease; z-index: 10; color: #666; } .copy-button:hover { background: rgba(255, 255, 255, 1); border-color: #bbb; color: #333; } .markdown-content pre:hover .copy-button { opacity: 1; } .copy-button.copied { background: #d4edda; border-color: #c3e6cb; color: #155724; } .message-user .copy-button { background: rgba(255, 255, 255, 0.2); border-color: rgba(255, 255, 255, 0.3); color: rgba(255, 255, 255, 0.8); } .message-user .copy-button:hover { background: rgba(255, 255, 255, 0.3); border-color: rgba(255, 255, 255, 0.4); color: rgba(255, 255, 255, 1); } .markdown-content table { border-collapse: collapse; width: 100%; margin: 8px 0; font-size: 13px; } .markdown-content th, .markdown-content td { border: 1px solid rgba(0, 0, 0, 0.1); padding: 4px 8px; text-align: left; } .markdown-content th { background: rgba(0, 0, 0, 0.05); font-weight: 600; } .message-user .markdown-content th { background: rgba(255, 255, 255, 0.1); } .message-user .markdown-content th, .message-user .markdown-content td { border-color: rgba(255, 255, 255, 0.2); } .markdown-content a { color: #667eea; text-decoration: none; } .message-user .markdown-content a { color: rgba(255, 255, 255, 0.9); } .markdown-content a:hover { text-decoration: underline; } .markdown-content hr { border: none; border-top: 1px solid rgba(0, 0, 0, 0.1); margin: 12px 0; } .message-user .markdown-content hr { border-top-color: rgba(255, 255, 255, 0.2); } /* Mobile responsive */ @media (max-width: 768px) { .header h1 { font-size: 16px; } .header .btn { font-size: 11px; padding: 4px 8px; } .connection-status { font-size: 10px; padding: 4px 8px; } .chat-messages { padding: 12px; } .message-bubble { max-width: 85%; padding: 10px 14px; font-size: 14px; } .input-container { padding: 12px; } .input-field { font-size: 16px; /* Prevent zoom on iOS */ } .markdown-content { font-size: 13px; } .markdown-content h1 { font-size: 16px; } .markdown-content h2 { font-size: 15px; } .markdown-content h3 { font-size: 14px; } .markdown-content h4 { font-size: 13px; } /* 手机端消息复制按钮调整 */ .message-copy-btn { opacity: 1; /* 手机端始终显示 */ font-size: 10px; padding: 3px 6px; top: 6px; right: 6px; min-width: 35px; } /* 手机端代码复制按钮调整 */ .copy-button { opacity: 1; /* 手机端始终显示 */ font-size: 10px; padding: 2px 5px; top: 4px; right: 4px; } } /* Scrollbar styling */ .chat-messages::-webkit-scrollbar { width: 6px; } .chat-messages::-webkit-scrollbar-track { background: transparent; } .chat-messages::-webkit-scrollbar-thumb { background: rgba(0, 0, 0, 0.2); border-radius: 3px; } .chat-messages::-webkit-scrollbar-thumb:hover { background: rgba(0, 0, 0, 0.3); } </style> </head> <body> <div class="app-container"> <div class="header"> <div class="container-fluid"> <div class="row align-items-center"> <div class="col"> <h1>🤖 AI 智能助手</h1> </div> <div class="col-auto"> <div class="d-flex align-items-center gap-2"> <button class="btn btn-sm" id="audioPermissionBtn" onclick="app.requestAudioPermission()" title="声音通知权限"> 🔇 </button> <button class="btn btn-sm" onclick="app.clearHistory()"> 🗑️ 清空 </button> <button class="btn btn-sm btn-danger" onclick="app.stopServer()" title="停止服务器"> 🛑 停止 </button> <div class="connection-status" id="connectionStatus"> <div class="status-indicator" id="statusIndicator"></div> <span id="statusText">连接中...</span> </div> </div> </div> </div> </div> </div> <div class="chat-container"> <div class="chat-messages" id="chatMessages"> <div class="empty-state" id="emptyState"> <div class="empty-state-icon">💬</div> <div class="empty-state-title">欢迎使用 AI 智能助手</div> <div class="empty-state-subtitle">开始对话,我将为您提供帮助</div> </div> </div> <div class="input-container"> <form id="messageForm"> <div class="input-wrapper"> <textarea id="messageInput" class="input-field" placeholder="输入消息..." rows="1" required ></textarea> <button type="submit" class="send-button" id="sendButton"></button> </div> </form> </div> </div> </div> <!-- Bootstrap 5 JS --> <script src="libs/bootstrap.bundle.min.js"></script> <script> marked.setOptions({ highlight: function(code, lang) { if (lang && hljs.getLanguage(lang)) { try { return hljs.highlight(code, { language: lang }).value; } catch (err) {} } return hljs.highlightAuto(code).value; }, breaks: true, gfm: true }); class ChatApp { constructor() { this.ws = null; this.history = []; this.reconnectAttempts = 0; this.maxReconnectAttempts = 10; this.baseReconnectDelay = 1000; this.maxReconnectDelay = 30000; // 音频相关 this.audioContext = null; this.audioPermissionGranted = false; this.connectWebSocket(); this.setupEventListeners(); this.setupAutoResize(); this.initAudio(); } connectWebSocket() { try { const protocol = window.location.protocol === 'https:' ? 'wss:' : 'ws:'; const wsUrl = `${protocol}//${window.location.host}`; this.ws = new WebSocket(wsUrl); this.ws.onopen = () => { console.log('WebSocket连接已建立'); this.reconnectAttempts = 0; this.updateConnectionStatus(true); }; this.ws.onmessage = (event) => { try { const data = JSON.parse(event.data); console.log('收到消息:', data); this.handleMessage(data); } catch (error) { console.error('解析消息失败:', error); } }; this.ws.onclose = () => { console.log('WebSocket连接已关闭'); this.updateConnectionStatus(false); if (this.reconnectAttempts < this.maxReconnectAttempts) { const delay = Math.min( this.baseReconnectDelay * Math.pow(2, this.reconnectAttempts), this.maxReconnectDelay ); this.reconnectAttempts++; setTimeout(() => { this.connectWebSocket(); }, delay); } }; this.ws.onerror = (error) => { console.error('WebSocket错误:', error); this.updateConnectionStatus(false); }; } catch (error) { console.error('创建WebSocket连接失败:', error); this.updateConnectionStatus(false); } } setupEventListeners() { document.getElementById('messageForm').addEventListener('submit', (e) => { e.preventDefault(); this.sendMessage(); }); document.getElementById('messageInput').addEventListener('keydown', (e) => { if (e.key === 'Enter' && !e.ctrlKey && !e.shiftKey) { e.preventDefault(); this.sendMessage(); } }); } setupAutoResize() { const messageInput = document.getElementById('messageInput'); messageInput.addEventListener('input', () => { messageInput.style.height = 'auto'; messageInput.style.height = Math.min(messageInput.scrollHeight, 120) + 'px'; }); } handleMessage(data) { switch (data.type) { case 'history': console.log('收到历史记录:', data.history.length, '条'); this.loadHistory(data.history); break; case 'ai_message': console.log('收到AI消息:', data.message); this.loadHistory(data.history); this.scrollToBottom(); // 播放提示音 this.playNotificationSound(); break; case 'history_updated': console.log('历史记录已更新'); this.loadHistory(data.history); this.scrollToBottom(); break; case 'history_cleared': console.log('历史记录已清理'); this.history = []; this.renderMessages(); break; case 'error': alert('错误: ' + data.message); break; case 'server_stopping': console.log('服务器正在关闭:', data.message); this.updateConnectionStatus(false); setTimeout(() => { alert('服务器已关闭。请刷新页面或重新启动服务器。'); }, 2000); break; default: console.log('未知消息类型:', data.type); } } loadHistory(historyData) { this.history = historyData; this.renderMessages(); } renderMessages() { const chatMessages = document.getElementById('chatMessages'); const emptyState = document.getElementById('emptyState'); // 先清除现有消息,无论历史记录是否为空 const existingMessages = chatMessages.querySelectorAll('.message-group, .message-bubble'); existingMessages.forEach(msg => msg.remove()); if (this.history.length === 0) { emptyState.style.display = 'flex'; return; } emptyState.style.display = 'none'; this.history.forEach((item, index) => { if (item.type === 'ai') { this.addAIMessage(item.message, item.timestamp); } else if (item.type === 'user') { this.addUserMessage(item.message, item.timestamp); } }); this.scrollToBottom(); } addAIMessage(message, timestamp) { const chatMessages = document.getElementById('chatMessages'); const messageGroup = document.createElement('div'); messageGroup.className = 'message-group'; if (timestamp) { const timeDiv = document.createElement('div'); timeDiv.className = 'message-time'; timeDiv.textContent = new Date(timestamp).toLocaleString(); messageGroup.appendChild(timeDiv); } const messageDiv = document.createElement('div'); messageDiv.className = 'message-bubble message-ai'; const avatarDiv = document.createElement('div'); avatarDiv.className = 'message-avatar avatar-ai'; avatarDiv.textContent = '🤖'; const contentDiv = document.createElement('div'); contentDiv.className = 'message-content markdown-content'; contentDiv.innerHTML = marked.parse(message); // 添加消息复制按钮 const copyBtn = document.createElement('button'); copyBtn.className = 'message-copy-btn'; copyBtn.textContent = '复制'; copyBtn.title = '复制消息'; copyBtn.addEventListener('click', () => this.copyMessage(message, copyBtn)); messageDiv.appendChild(contentDiv); messageDiv.appendChild(copyBtn); messageGroup.appendChild(messageDiv); chatMessages.appendChild(messageGroup); // Highlight code blocks messageDiv.querySelectorAll('pre code').forEach((block) => { hljs.highlightElement(block); }); // Add copy buttons to code blocks this.addCopyButtons(messageDiv); } addUserMessage(message, timestamp) { const chatMessages = document.getElementById('chatMessages'); const messageGroup = document.createElement('div'); messageGroup.className = 'message-group'; const messageDiv = document.createElement('div'); messageDiv.className = 'message-bubble message-user'; const contentDiv = document.createElement('div'); contentDiv.className = 'message-content markdown-content'; contentDiv.innerHTML = marked.parse(message); // 添加消息复制按钮 const copyBtn = document.createElement('button'); copyBtn.className = 'message-copy-btn'; copyBtn.textContent = '复制'; copyBtn.title = '复制消息'; copyBtn.addEventListener('click', () => this.copyMessage(message, copyBtn)); messageDiv.appendChild(contentDiv); messageDiv.appendChild(copyBtn); messageGroup.appendChild(messageDiv); if (timestamp) { const timeDiv = document.createElement('div'); timeDiv.className = 'message-time'; timeDiv.textContent = new Date(timestamp).toLocaleString(); messageGroup.appendChild(timeDiv); } chatMessages.appendChild(messageGroup); // Highlight code blocks messageDiv.querySelectorAll('pre code').forEach((block) => { hljs.highlightElement(block); }); // Add copy buttons to code blocks this.addCopyButtons(messageDiv); } sendMessage() { const messageInput = document.getElementById('messageInput'); const message = messageInput.value.trim(); if (!message) return; if (this.ws && this.ws.readyState === WebSocket.OPEN) { // 只发送消息到服务器,不在前端立即显示 // 等待服务器的history_updated消息来统一更新界面 this.ws.send(JSON.stringify({ type: 'user_message', message: message })); console.log('发送用户消息:', message); } messageInput.value = ''; messageInput.style.height = 'auto'; // 移除立即滚动,等历史更新后再滚动 } scrollToBottom() { const chatMessages = document.getElementById('chatMessages'); setTimeout(() => { chatMessages.scrollTop = chatMessages.scrollHeight; }, 100); } updateConnectionStatus(connected) { const indicator = document.getElementById('statusIndicator'); const statusText = document.getElementById('statusText'); if (indicator && statusText) { if (connected) { statusText.textContent = '已连接'; indicator.className = 'status-indicator'; } else { statusText.textContent = '已断开'; indicator.className = 'status-indicator disconnected'; } } } clearHistory() { if (confirm('确定要清空所有聊天记录吗?')) { // 只发送清空请求给服务器,不在本地立即清空 // 等待服务器的 history_cleared 消息来更新界面 if (this.ws && this.ws.readyState === WebSocket.OPEN) { this.ws.send(JSON.stringify({ type: 'clear_history' })); } } } stopServer() { if (confirm('确定要停止服务器吗?这将关闭整个应用程序。')) { if (this.ws && this.ws.readyState === WebSocket.OPEN) { this.ws.send(JSON.stringify({ type: 'stop_server' })); } this.updateConnectionStatus(false); } } initAudio() { // 检查音频权限状态 this.checkAudioPermission(); } async checkAudioPermission() { try { // 检查浏览器是否支持Web Audio API if (!window.AudioContext && !window.webkitAudioContext) { console.log('浏览器不支持Web Audio API'); this.updateAudioButton(false, '不支持'); return; } // 尝试创建AudioContext来检查权限 const AudioContextClass = window.AudioContext || window.webkitAudioContext; // 检查是否已经有权限 if (this.audioContext && this.audioContext.state === 'running') { this.audioPermissionGranted = true; this.updateAudioButton(true, '已授权'); return; } // 更新按钮状态为未授权 this.updateAudioButton(false, '需要授权'); } catch (error) { console.error('检查音频权限失败:', error); this.updateAudioButton(false, '检查失败'); } } async requestAudioPermission() { try { const AudioContextClass = window.AudioContext || window.webkitAudioContext; if (!this.audioContext) { this.audioContext = new AudioContextClass(); } // 如果AudioContext处于suspended状态,需要用户交互来激活 if (this.audioContext.state === 'suspended') { await this.audioContext.resume(); } // 播放一个测试音来确认权限 await this.playNotificationSound(); this.audioPermissionGranted = true; this.updateAudioButton(true, '已授权'); console.log('音频权限已获取'); } catch (error) { console.error('请求音频权限失败:', error); this.updateAudioButton(false, '授权失败'); alert('音频权限获取失败,请检查浏览器设置'); } } updateAudioButton(hasPermission, tooltip) { const btn = document.getElementById('audioPermissionBtn'); if (btn) { if (hasPermission) { btn.innerHTML = '🔊'; btn.className = 'btn btn-sm btn-success'; btn.title = `声音通知: ${tooltip}`; } else { btn.innerHTML = '🔇'; btn.className = 'btn btn-sm btn-warning'; btn.title = `声音通知: ${tooltip}`; } } } addCopyButtons(messageElement) { const codeBlocks = messageElement.querySelectorAll('pre'); codeBlocks.forEach(pre => { // 检查是否已经有复制按钮 if (pre.querySelector('.copy-button')) { return; } const copyButton = document.createElement('button'); copyButton.className = 'copy-button'; copyButton.textContent = '复制'; copyButton.title = '复制代码'; copyButton.addEventListener('click', async () => { const codeElement = pre.querySelector('code'); const code = codeElement ? codeElement.textContent : pre.textContent; try { await navigator.clipboard.writeText(code); copyButton.textContent = '已复制'; copyButton.classList.add('copied'); setTimeout(() => { copyButton.textContent = '复制'; copyButton.classList.remove('copied'); }, 2000); } catch (err) { console.error('复制失败:', err); // 降级方案:使用传统方法复制 this.fallbackCopyText(code); copyButton.textContent = '已复制'; copyButton.classList.add('copied'); setTimeout(() => { copyButton.textContent = '复制'; copyButton.classList.remove('copied'); }, 2000); } }); pre.appendChild(copyButton); }); } fallbackCopyText(text) { const textArea = document.createElement('textarea'); textArea.value = text; textArea.style.position = 'fixed'; textArea.style.left = '-999999px'; textArea.style.top = '-999999px'; document.body.appendChild(textArea); textArea.focus(); textArea.select(); try { document.execCommand('copy'); } catch (err) { console.error('降级复制也失败了:', err); } document.body.removeChild(textArea); } async copyMessage(message, button) { try { await navigator.clipboard.writeText(message); button.textContent = '已复制'; button.classList.add('copied'); setTimeout(() => { button.textContent = '复制'; button.classList.remove('copied'); }, 2000); } catch (err) { console.error('复制消息失败:', err); // 降级方案:使用传统方法复制 this.fallbackCopyText(message); button.textContent = '已复制'; button.classList.add('copied'); setTimeout(() => { button.textContent = '复制'; button.classList.remove('copied'); }, 2000); } } async playNotificationSound() { if (!this.audioContext || !this.audioPermissionGranted) { console.log('音频未初始化或无权限'); return; } try { // 创建一个1秒左右的提示音 const oscillator = this.audioContext.createOscillator(); const gainNode = this.audioContext.createGain(); // 连接音频节点 oscillator.connect(gainNode); gainNode.connect(this.audioContext.destination); // 设置音频参数 - 创建一个愉快的提示音序列 oscillator.type = 'sine'; const currentTime = this.audioContext.currentTime; // 音符序列:C5 -> E5 -> G5 -> C6 (大约1秒) oscillator.frequency.setValueAtTime(523, currentTime); // C5 oscillator.frequency.setValueAtTime(659, currentTime + 0.25); // E5 oscillator.frequency.setValueAtTime(784, currentTime + 0.5); // G5 oscillator.frequency.setValueAtTime(1047, currentTime + 0.75); // C6 // 设置音量包络 - 1秒总时长 gainNode.gain.setValueAtTime(0, currentTime); gainNode.gain.linearRampToValueAtTime(0.2, currentTime + 0.1); gainNode.gain.setValueAtTime(0.2, currentTime + 0.8); gainNode.gain.linearRampToValueAtTime(0, currentTime + 1.0); // 播放声音 - 1秒时长 oscillator.start(currentTime); oscillator.stop(currentTime + 1.0); } catch (error) { console.error('播放提示音失败:', error); } } } // 初始化应用 const app = new ChatApp(); </script> </body> </html>