let websocket = null, isConnected = false, conversationId = null; // 对话区独立缓冲 let convEl = null, convContent = ''; // 评判区独立缓冲 let judgeEl = null, judgeContent = ''; let currentMessages = []; // [{role, content, model?}] let latestJudgeMarkdown = ''; // 最近一次评判Markdown let hasAnyContent = false; // 是否已有模型输出 let exportedOnce = false; // 是否已导出任意一次 let currentSpeakingModel = ''; let isExporting = false; // 是否正在导出,避免触发离开页面提示 const startBtn = document.getElementById('startBtn'), stopBtn = document.getElementById('stopBtn'), judgeBtn = document.getElementById('judgeBtn'), summaryBtn = document.getElementById('summaryBtn'); const outputDiv = document.getElementById('output'), judgeSection = document.getElementById('judge-section'), judgeOutputDiv = document.getElementById('judge-output'); const exportAllBtn = document.getElementById('exportAllBtn'); const exportFormatSel = document.getElementById('exportFormat'); const proModelLabel = document.querySelector('label[for="proModel"]'); const conModelLabel = document.querySelector('label[for="conModel"]'); const topicLabel = document.querySelector('label[for="topic"]'); const initialPromptTextarea = document.getElementById('initialPrompt'); document.querySelectorAll('input[name="mode"]').forEach(radio => { radio.addEventListener('change', function() { updateLabels(this.value); }); }); function updateLabels(mode) { const judgeSectionTitle = document.getElementById('judgeSectionTitle'); if (mode === 'debate') { proModelLabel.textContent = 'AI 1 (正方)'; conModelLabel.textContent = 'AI 2 (反方)'; topicLabel.textContent = '对话任务/话题'; judgeBtn.textContent = '评判双方辩论表现'; judgeBtn.style.display = 'inline-block'; summaryBtn.style.display = 'none'; judgeSectionTitle.textContent = '评判区'; judgeSection.style.display = 'block'; initialPromptTextarea.placeholder = "默认提示示例:'你将作为正方,就[话题]进行辩论...'。你可以在此输入额外指示(默认追加),或选择覆盖默认提示。"; } else { proModelLabel.textContent = 'AI 1'; conModelLabel.textContent = 'AI 2'; topicLabel.textContent = '协作任务'; judgeBtn.style.display = 'none'; summaryBtn.textContent = '总结对话'; summaryBtn.style.display = 'inline-block'; judgeSectionTitle.textContent = '总结对话'; judgeSection.style.display = 'block'; initialPromptTextarea.placeholder = "默认提示示例:'你将作为AI 1,与AI 2一同协作...'。你可以在此输入额外指示(默认追加),或选择覆盖默认提示。"; } } function handleWebSocketMessage(data) { let shouldScroll = Math.abs(outputDiv.scrollHeight - outputDiv.clientHeight - outputDiv.scrollTop) < 10; switch (data.type) { case 'conversation_started': outputDiv.innerHTML = ''; judgeOutputDiv.innerHTML = ''; judgeBtn.disabled = true; // 对话进行中不允许评判 summaryBtn.disabled = true; // 对话进行中不允许总结 judgeBtn.textContent = '评判双方辩论表现'; summaryBtn.textContent = '总结对话'; currentMessages = []; latestJudgeMarkdown = ''; hasAnyContent = false; exportedOnce = false; exportAllBtn.disabled = true; // 清空缓冲 convEl = null; convContent = ''; judgeEl = null; judgeContent = ''; // 使用结构化块,避免在窄屏下被分隔压缩导致竖向排版 const debateInfo = document.createElement('div'); debateInfo.className = 'debate-meta'; const topicRow = document.createElement('div'); topicRow.className = 'meta'; const topicLabel = document.createElement('strong'); topicLabel.textContent = '话题:'; const topicSpan = document.createElement('span'); topicSpan.className = 'meta-topic'; topicSpan.textContent = data.topic; topicRow.appendChild(topicLabel); topicRow.appendChild(document.createTextNode(' ')); topicRow.appendChild(topicSpan); const proRow = document.createElement('div'); proRow.className = 'meta'; const proLabel = document.createElement('strong'); proLabel.textContent = (data.mode === 'debate') ? '正方:' : 'AI 1:'; proRow.appendChild(proLabel); proRow.appendChild(document.createTextNode(' ' + data.pro_model)); const conRow = document.createElement('div'); conRow.className = 'meta'; const conLabel = document.createElement('strong'); conLabel.textContent = (data.mode === 'debate') ? '反方:' : 'AI 2:'; conRow.appendChild(conLabel); conRow.appendChild(document.createTextNode(' ' + data.con_model)); debateInfo.appendChild(topicRow); debateInfo.appendChild(proRow); debateInfo.appendChild(conRow); outputDiv.appendChild(debateInfo); if(data.debate_id) conversationId = data.debate_id; break; case 'round_info': const separator = document.createElement('div'); separator.className = 'round-separator'; separator.textContent = data.message.trim(); outputDiv.appendChild(separator); break; case 'model_speaking': convContent = ''; // 重置当前消息内容 const messageDiv = document.createElement('div'); // AI 1 和 正方 使用 'pro' 样式, AI 2 和 反方 使用 'con' 样式 const roleClass = (data.role === '正方' || data.role === 'AI 1') ? 'pro' : 'con'; messageDiv.className = `message ${roleClass}`; messageDiv.innerHTML = `
${data.model.substring(0, 1).toUpperCase()}
${data.model} (${data.role})
正在思考...
`; outputDiv.appendChild(messageDiv); convEl = messageDiv.querySelector('.text'); // 暂存当前说话模型,便于记录 currentSpeakingModel = data.model; break; case 'stream_content': if (convEl) { convContent += data.content; convEl.innerHTML = DOMPurify.sanitize(marked.parse(convContent)); } break; case 'stream_end': convEl = null; // 结束一段发言,存入messages(记录模型名) currentMessages.push({ role: 'assistant', model: currentSpeakingModel || '', content: convContent }); hasAnyContent = hasAnyContent || !!convContent; break; case 'conversation_stopped': const endMsg = document.createElement('div'); endMsg.className = 'round-separator'; endMsg.textContent = data.message; outputDiv.appendChild(endMsg); startBtn.disabled = false; stopBtn.disabled = true; const currentMode = document.querySelector('input[name="mode"]:checked').value; if (currentMode === 'debate') { judgeBtn.disabled = false; // 停止后允许评判 judgeBtn.textContent = '评判双方辩论表现'; } else { summaryBtn.disabled = false; // 停止后允许总结 summaryBtn.textContent = '总结对话'; } exportAllBtn.disabled = false; // 可导出 break; case 'conversation_ended': const endedMsg = document.createElement('div'); endedMsg.className = 'round-separator'; endedMsg.textContent = data.message || '=== 对话结束 ==='; outputDiv.appendChild(endedMsg); startBtn.disabled = false; stopBtn.disabled = true; const endMode = document.querySelector('input[name="mode"]:checked').value; if (endMode === 'debate') { judgeBtn.disabled = false; // 结束后允许评判 judgeBtn.textContent = '评判双方辩论表现'; } else { summaryBtn.disabled = false; // 结束后允许总结 summaryBtn.textContent = '总结对话'; } exportAllBtn.disabled = false; // 可导出 break; case 'judge_started': judgeOutputDiv.innerHTML = ''; const judgeHeader = document.createElement('div'); judgeHeader.className = 'message'; judgeHeader.innerHTML = `
评判员 (${data.model})
正在分析辩论...
`; judgeOutputDiv.appendChild(judgeHeader); judgeEl = judgeHeader.querySelector('.text'); judgeContent = ''; break; case 'judge_stream_content': if (judgeEl) { const shouldScrollJudge = Math.abs(judgeOutputDiv.scrollHeight - judgeOutputDiv.clientHeight - judgeOutputDiv.scrollTop) < 10; judgeContent += data.content; judgeEl.innerHTML = DOMPurify.sanitize(marked.parse(judgeContent)); if (shouldScrollJudge) { judgeOutputDiv.scrollTop = judgeOutputDiv.scrollHeight; } } break; case 'judge_stream_end': judgeEl = null; judgeBtn.disabled = false; // 始终可点 judgeBtn.textContent = '重新评判'; latestJudgeMarkdown = judgeContent || ''; exportAllBtn.disabled = false; hasAnyContent = hasAnyContent || !!latestJudgeMarkdown; break; case 'summary_started': judgeOutputDiv.innerHTML = ''; const summaryHeader = document.createElement('div'); summaryHeader.className = 'message'; summaryHeader.innerHTML = `
总结员 (${data.model})
正在总结对话...
`; judgeOutputDiv.appendChild(summaryHeader); judgeEl = summaryHeader.querySelector('.text'); judgeContent = ''; break; case 'summary_stream_content': if (judgeEl) { const shouldScrollJudge = Math.abs(judgeOutputDiv.scrollHeight - judgeOutputDiv.clientHeight - judgeOutputDiv.scrollTop) < 10; judgeContent += data.content; judgeEl.innerHTML = DOMPurify.sanitize(marked.parse(judgeContent)); if (shouldScrollJudge) { judgeOutputDiv.scrollTop = judgeOutputDiv.scrollHeight; } } break; case 'summary_stream_end': judgeEl = null; summaryBtn.disabled = false; // 始终可点 summaryBtn.textContent = '重新总结'; latestJudgeMarkdown = judgeContent || ''; exportAllBtn.disabled = false; hasAnyContent = hasAnyContent || !!latestJudgeMarkdown; break; case 'error': outputDiv.innerHTML += `
错误: ${data.message}
`; break; } if(shouldScroll) { outputDiv.scrollTop = outputDiv.scrollHeight; } } function connect() { const protocol = window.location.protocol === 'https:' ? 'wss:' : 'ws:'; const wsUrl = `${protocol}//${window.location.host}/ws`; websocket = new WebSocket(wsUrl); websocket.onopen = () => { isConnected = true; startBtn.disabled = false; }; websocket.onmessage = (event) => handleWebSocketMessage(JSON.parse(event.data)); websocket.onclose = () => { isConnected = false; startBtn.disabled = true; stopBtn.disabled = true; judgeBtn.disabled = true; summaryBtn.disabled = true; }; websocket.onerror = (error) => { console.error('WebSocket Error:', error); }; } window.addEventListener('load', connect); startBtn.addEventListener('click', () => { if (!websocket) return; const message = { action: "start_conversation", mode: document.querySelector('input[name="mode"]:checked').value, topic: document.getElementById('topic').value, rounds: parseInt(document.getElementById('rounds').value), pro_model: document.getElementById('proModel').value, con_model: document.getElementById('conModel').value, initial_prompt: document.getElementById('initialPrompt').value, initial_prompt_mode: document.querySelector('input[name="promptMode"]:checked').value }; websocket.send(JSON.stringify(message)); startBtn.disabled = true; stopBtn.disabled = false; }); stopBtn.addEventListener('click', () => { if (!websocket) return; websocket.send(JSON.stringify({ action: "stop_conversation" })); }); judgeBtn.addEventListener('click', () => { if (!websocket || !conversationId) { alert("无法找到有效的对话ID来进行评判。"); return; } const message = { action: "judge_debate", debate_id: conversationId, judge_model: document.getElementById('judgeModel').value }; websocket.send(JSON.stringify(message)); judgeBtn.disabled = false; // 不禁用 judgeBtn.textContent = '正在评判...'; }); summaryBtn.addEventListener('click', () => { if (!websocket || !conversationId) { alert("无法找到有效的对话ID来进行总结。"); return; } const message = { action: "summarize_collaboration", debate_id: conversationId, summary_model: document.getElementById('judgeModel').value }; websocket.send(JSON.stringify(message)); summaryBtn.disabled = false; // 不禁用 summaryBtn.textContent = '正在总结...'; }); function downloadBlob(content, filename, mime) { try { const blob = new Blob([content], { type: mime }); const file = new File([blob], filename, { type: mime }); // 优先使用 Web Share API(移动端体验更好) if (navigator.canShare && navigator.canShare({ files: [file] })) { navigator.share({ files: [file], title: filename }).catch(() => { fallbackDownload(blob, filename); }); return; } // 常规下载回退 fallbackDownload(blob, filename); } catch (e) { console.error('downloadBlob error:', e); try { // 最后兜底:直接打开新标签提示用户手动保存 const dataUrl = 'data:' + (mime || 'text/plain') + ';charset=utf-8,' + encodeURIComponent(String(content)); window.open(dataUrl, '_blank'); } catch (e2) {} } } function fallbackDownload(blob, filename) { const url = URL.createObjectURL(blob); const a = document.createElement('a'); a.href = url; a.download = filename; a.rel = 'noopener'; a.target = '_blank'; // 兼容部分移动端需要新标签 a.style.display = 'none'; document.body.appendChild(a); a.click(); document.body.removeChild(a); // 某些 iOS Safari 不支持 download 属性,尝试直接打开 setTimeout(() => { URL.revokeObjectURL(url); }, 1000); } // 导出:合并对话与评判 exportAllBtn?.addEventListener('click', () => { if (!conversationId || currentMessages.length === 0) { alert('暂无可导出的记录'); return; } const format = exportFormatSel?.value || 'md'; const url = `/api/export?debate_id=${encodeURIComponent(conversationId)}&format=${encodeURIComponent(format)}`; // 直接跳转下载,移动端兼容性更好;并避免 beforeunload 弹窗 exportedOnce = true; isExporting = true; setTimeout(() => { window.location.href = url; // 下载触发后稍后清除导出标记 setTimeout(() => { isExporting = false; }, 1500); }, 0); }); // 刷新/关闭前提醒保存(仅当有内容且未导出) let hasWarnedOnce = false; window.addEventListener('beforeunload', (e) => { if (isExporting) { // 正在导出时,不提示离开 return; } const needWarn = hasAnyContent && !exportedOnce; if (needWarn) { e.preventDefault(); e.returnValue = '建议先点击“导出记录”保存到本地,确定要离开吗?'; return '建议先点击“导出记录”保存到本地,确定要离开吗?'; } else if (!hasWarnedOnce && hasAnyContent) { hasWarnedOnce = true; setTimeout(() => alert('温馨提示:如需留存,请使用“导出记录”按钮保存到本地。'), 0); } }); // Initial label setup updateLabels(document.querySelector('input[name="mode"]:checked').value);