/** * BRAIN Alpha 模拟器 - 前端 JavaScript * 处理模拟器的用户界面,包括参数输入和日志监控 */ let currentLogFile = null; let logPollingInterval = null; let isSimulationRunning = false; let simulationAbortController = null; let userSelectedLogFile = false; // 跟踪用户是否手动选择了日志文件 // 当 DOM 加载完成时初始化页面 document.addEventListener('DOMContentLoaded', function() { refreshLogFiles(); setupFormValidation(); loadSimulatorDefaults(); }); /** * 设置表单验证和变更处理程序 */ function setupFormValidation() { const startPosition = document.getElementById('startPosition'); const randomShuffle = document.getElementById('randomShuffle'); const jsonFile = document.getElementById('jsonFile'); // 当文件可能被覆盖时显示警告 function checkOverwriteWarning() { const warning = document.getElementById('overwriteWarning'); const showWarning = parseInt(startPosition.value) > 0 || randomShuffle.checked; warning.style.display = showWarning ? 'block' : 'none'; } startPosition.addEventListener('input', checkOverwriteWarning); randomShuffle.addEventListener('change', checkOverwriteWarning); // 处理 JSON 文件选择 jsonFile.addEventListener('change', function(e) { const file = e.target.files[0]; const info = document.getElementById('jsonFileInfo'); if (file) { info.innerHTML = ` 已选择: ${file.name}
大小: ${(file.size / 1024).toFixed(1)} KB
修改时间: ${new Date(file.lastModified).toLocaleString()} `; info.style.display = 'block'; // 尝试读取并验证 JSON const reader = new FileReader(); reader.onload = function(e) { try { const data = JSON.parse(e.target.result); if (Array.isArray(data)) { const maxStart = Math.max(0, data.length - 1); startPosition.max = maxStart; info.innerHTML += `
表达式数量: 找到 ${data.length} 个`; } else { info.innerHTML += '
⚠️ 警告: 不是数组格式'; } } catch (err) { info.innerHTML += '
❌ JSON 格式无效'; } }; reader.readAsText(file); } else { info.style.display = 'none'; } }); } /** * 从 localStorage 加载默认值(如果可用) */ function loadSimulatorDefaults() { const username = localStorage.getItem('simulator_username'); if (username) { document.getElementById('username').value = username; } const concurrentCount = localStorage.getItem('simulator_concurrent'); if (concurrentCount) { document.getElementById('concurrentCount').value = concurrentCount; } } /** * 将当前表单值保存到 localStorage */ function saveSimulatorDefaults() { localStorage.setItem('simulator_username', document.getElementById('username').value); localStorage.setItem('simulator_concurrent', document.getElementById('concurrentCount').value); } /** * 切换密码可见性 */ function togglePassword() { const passwordInput = document.getElementById('password'); const isPassword = passwordInput.type === 'password'; passwordInput.type = isPassword ? 'text' : 'password'; const toggleBtn = document.querySelector('.password-toggle'); toggleBtn.textContent = isPassword ? '🙈' : '👁️'; } /** * 切换多模拟选项 */ function toggleMultiSimOptions() { const checkbox = document.getElementById('useMultiSim'); const options = document.getElementById('multiSimOptions'); options.style.display = checkbox.checked ? 'block' : 'none'; } /** * 刷新可用的日志文件 */ async function refreshLogFiles() { try { const response = await fetch('/api/simulator/logs'); const data = await response.json(); const selector = document.getElementById('logSelector'); selector.innerHTML = ''; if (data.logs && data.logs.length > 0) { data.logs.forEach(log => { const option = document.createElement('option'); option.value = log.filename; option.textContent = `${log.filename} (${log.size}, ${log.modified})`; selector.appendChild(option); }); // 只有当用户没有手动选择时才自动选择最新的日志文件 if (data.latest && !userSelectedLogFile) { selector.value = data.latest; currentLogFile = data.latest; // 更新 UI 显示自动监控 document.getElementById('currentLogFile').innerHTML = ` 🔄 自动监控: ${data.latest}
当出现新日志文件时,将自动选择最新的。 `; loadSelectedLog(); // 确保对自动选择的文件也启用轮询 ensureLogPollingActive(); } } } catch (error) { console.error('刷新日志文件时出错:', error); updateStatus('加载日志文件时出错', 'error'); } // 确保刷新后轮询继续 ensureLogPollingActive(); } /** * 加载选定的日志文件内容 */ async function loadSelectedLog() { const selector = document.getElementById('logSelector'); const selectedLog = selector.value; if (!selectedLog) { // 如果用户取消选择,则重置 userSelectedLogFile = false; currentLogFile = null; document.getElementById('currentLogFile').innerHTML = ` 🔄 自动模式已启用: 将在可用时监控最新日志
系统将自动选择并监控最新的日志文件。 `; // 尝试再次自动选择最新的 refreshLogFiles(); return; } // 标记用户已手动选择日志文件 userSelectedLogFile = true; currentLogFile = selectedLog; // 如果尚未运行,则开始轮询以监控选定的文件 ensureLogPollingActive(); try { const response = await fetch(`/api/simulator/logs/${encodeURIComponent(selectedLog)}`); const data = await response.json(); if (data.content !== undefined) { const logViewer = document.getElementById('logViewer'); // 使用 innerHTML 正确处理中文字符编码 const content = data.content || '日志文件为空。'; logViewer.textContent = content; logViewer.scrollTop = logViewer.scrollHeight; // 仅当用户手动选择时(非自动选择)更新状态 if (userSelectedLogFile) { document.getElementById('currentLogFile').innerHTML = ` 📌 手动选择: ${selectedLog}
已禁用自动切换到最新日志。选择"选择日志文件..."以重新启用。 `; } } } catch (error) { console.error('加载日志文件时出错:', error); updateStatus('加载日志内容时出错', 'error'); } } /** * 测试连接到 BRAIN API */ async function testConnection() { const username = document.getElementById('username').value; const password = document.getElementById('password').value; if (!username || !password) { updateStatus('请先输入用户名和密码', 'error'); return; } const testBtn = document.getElementById('testBtn'); testBtn.disabled = true; testBtn.textContent = '🔄 测试中...'; updateStatus('正在测试 BRAIN API 连接...', 'running'); try { const response = await fetch('/api/simulator/test-connection', { method: 'POST', headers: { 'Content-Type': 'application/json' }, body: JSON.stringify({ username: username, password: password }) }); const data = await response.json(); if (data.success) { updateStatus('✅ 连接成功!准备运行模拟。', 'success'); saveSimulatorDefaults(); } else { updateStatus(`❌ 连接失败: ${data.error}`, 'error'); } } catch (error) { updateStatus(`❌ 连接错误: ${error.message}`, 'error'); } finally { testBtn.disabled = false; testBtn.textContent = '🔗 测试连接'; } } /** * 使用用户参数运行模拟器 */ async function runSimulator() { if (isSimulationRunning) { updateStatus('模拟已在运行中', 'error'); return; } // 验证表单 const form = document.getElementById('simulatorForm'); if (!form.checkValidity()) { form.reportValidity(); return; } const jsonFile = document.getElementById('jsonFile').files[0]; if (!jsonFile) { updateStatus('请选择 JSON 文件', 'error'); return; } // 准备表单数据 const formData = new FormData(); formData.append('jsonFile', jsonFile); formData.append('username', document.getElementById('username').value); formData.append('password', document.getElementById('password').value); formData.append('startPosition', document.getElementById('startPosition').value); formData.append('concurrentCount', document.getElementById('concurrentCount').value); formData.append('randomShuffle', document.getElementById('randomShuffle').checked); formData.append('useMultiSim', document.getElementById('useMultiSim').checked); formData.append('alphaCountPerSlot', document.getElementById('alphaCountPerSlot').value); // UI 更新 isSimulationRunning = true; const runBtn = document.getElementById('runSimulator'); const stopBtn = document.getElementById('stopBtn'); runBtn.disabled = true; runBtn.textContent = '🔄 运行中...'; stopBtn.style.display = 'inline-block'; updateStatus('正在启动模拟...', 'running'); showProgress(true); // 创建中止控制器用于停止模拟 simulationAbortController = new AbortController(); try { saveSimulatorDefaults(); const response = await fetch('/api/simulator/run', { method: 'POST', body: formData, signal: simulationAbortController.signal }); const data = await response.json(); if (data.success) { updateStatus('✅ 模拟器已在新的终端窗口中启动!请查看终端窗口了解进度。', 'success'); // 显示启动信息 if (data.parameters) { showLaunchInfo(data.parameters); } // 由于模拟正在运行,开始更频繁地监控日志文件 startLogPolling(); // 刷新日志文件以获取最新的模拟日志 setTimeout(() => { refreshLogFiles(); }, 3000); } else { updateStatus(`❌ 启动模拟器失败: ${data.error}`, 'error'); } } catch (error) { if (error.name === 'AbortError') { updateStatus('⏹️ 模拟已被用户停止', 'idle'); } else { updateStatus(`❌ 模拟错误: ${error.message}`, 'error'); } } finally { isSimulationRunning = false; runBtn.disabled = false; runBtn.textContent = '🚀 开始模拟'; stopBtn.style.display = 'none'; simulationAbortController = null; showProgress(false); } } /** * 停止正在运行的模拟 */ async function stopSimulation() { if (simulationAbortController) { simulationAbortController.abort(); } try { await fetch('/api/simulator/stop', { method: 'POST' }); } catch (error) { console.error('停止模拟时出错:', error); } updateStatus('正在停止模拟...', 'idle'); } /** * 确保日志轮询在需要监控日志文件时处于活动状态 */ function ensureLogPollingActive() { if (currentLogFile && !logPollingInterval) { console.log('开始对文件进行日志轮询:', currentLogFile); startLogPolling(); // 添加轮询活动状态的视觉指示器 const currentLogFileDiv = document.getElementById('currentLogFile'); if (userSelectedLogFile) { currentLogFileDiv.innerHTML = ` 📌 手动选择: ${currentLogFile}
已禁用自动切换到最新日志。选择"选择日志文件..."以重新启用。 `; } else { currentLogFileDiv.innerHTML = ` 🔄 自动监控: ${currentLogFile}
当出现新日志文件时,将自动选择最新的。 `; } } else if (currentLogFile && logPollingInterval) { console.log('日志轮询已对以下文件处于活动状态:', currentLogFile); } } /** * 开始轮询日志更新 */ function startLogPolling() { if (logPollingInterval) { clearInterval(logPollingInterval); } // 当模拟在终端中运行时,开始更频繁的轮询 logPollingInterval = setInterval(async () => { try { // 仅当用户未手动选择文件时刷新日志文件列表 // 这允许系统检测新日志文件,但不会干扰用户的选择 if (!userSelectedLogFile) { await refreshLogFiles(); } // 始终刷新当前监控的日志文件内容 if (currentLogFile) { console.log('轮询日志文件:', currentLogFile, '用户选择:', userSelectedLogFile); const response = await fetch(`/api/simulator/logs/${encodeURIComponent(currentLogFile)}`); const data = await response.json(); if (data.content !== undefined) { const logViewer = document.getElementById('logViewer'); logViewer.textContent = data.content; logViewer.scrollTop = logViewer.scrollHeight; } } } catch (error) { console.error('轮询日志时出错:', error); } }, 3000); // 在终端中运行时每 3 秒轮询一次 // 15 分钟后自动停止轮询,防止服务器负载过高 setTimeout(() => { if (logPollingInterval) { clearInterval(logPollingInterval); logPollingInterval = null; console.log('15 分钟后自动停止日志轮询'); } }, 900000); // 15 分钟 } /** * 停止日志轮询 */ function stopLogPolling() { if (logPollingInterval) { clearInterval(logPollingInterval); logPollingInterval = null; } } /** * 更新状态指示器 */ function updateStatus(message, type = 'idle') { const statusEl = document.getElementById('simulatorStatus'); statusEl.textContent = message; statusEl.className = `status-indicator status-${type}`; } /** * 显示/隐藏进度条 */ function showProgress(show) { const progressDiv = document.getElementById('simulationProgress'); progressDiv.style.display = show ? 'block' : 'none'; if (!show) { updateProgress(0, 0); } } /** * 更新进度条 */ function updateProgress(current, total) { const progressText = document.getElementById('progressText'); const progressBar = document.getElementById('progressBar'); progressText.textContent = `${current}/${total}`; if (total > 0) { const percentage = (current / total) * 100; progressBar.style.width = `${percentage}%`; } else { progressBar.style.width = '0%'; } } /** * 当模拟器在终端中启动时显示启动信息 */ function showLaunchInfo(parameters) { const resultsPanel = document.getElementById('resultsPanel'); const resultsDiv = document.getElementById('simulationResults'); let html = '
'; html += '

🚀 模拟器启动成功

'; html += '

模拟器正在单独的终端窗口中运行。您可以在那里监控进度。

'; html += '
'; html += '
📋 配置摘要:
'; html += `

总表达式数: ${parameters.expressions_count}

`; html += `

并发模拟数: ${parameters.concurrent_count}

`; if (parameters.use_multi_sim) { html += `

多模拟模式: 是 (每个插槽 ${parameters.alpha_count_per_slot} 个 alpha)

`; html += `

预计总 Alpha 数: ${parameters.expressions_count * parameters.alpha_count_per_slot}

`; } else { html += `

多模拟模式:

`; } html += '
'; html += '
'; html += '

💡 监控提示:

'; html += ''; html += '
'; html += '
'; resultsDiv.innerHTML = html; resultsPanel.style.display = 'block'; // 滚动到结果区域 resultsPanel.scrollIntoView({ behavior: 'smooth' }); } /** * 显示模拟结果 */ function showResults(results) { const resultsPanel = document.getElementById('resultsPanel'); const resultsDiv = document.getElementById('simulationResults'); let html = '
'; html += `

总模拟数: ${results.total || 0}

`; html += `

成功: ${results.successful || 0}

`; html += `

失败: ${results.failed || 0}

`; // 如果适用,添加多模拟信息 if (results.use_multi_sim && results.alpha_count_per_slot) { html += `
`; html += `📌 多模拟模式:
`; html += `每个模拟插槽包含 ${results.alpha_count_per_slot} 个 alpha。
`; html += `处理的单个 alpha 总数: ${results.successful * results.alpha_count_per_slot}`; html += `
`; } html += '
'; if (results.alphaIds && results.alphaIds.length > 0) { html += '

生成的 Alpha ID:

'; html += '
'; results.alphaIds.forEach((alphaId, index) => { html += `
${index + 1}. ${alphaId}
`; }); html += '
'; // 为 Alpha ID 添加复制按钮 html += '
'; html += ''; html += '
'; } resultsDiv.innerHTML = html; resultsPanel.style.display = 'block'; // 存储结果以供复制 window.lastSimulationResults = results; // 滚动到结果区域 resultsPanel.scrollIntoView({ behavior: 'smooth' }); } /** * 将所有 Alpha ID 复制到剪贴板 */ function copyAlphaIds() { if (window.lastSimulationResults && window.lastSimulationResults.alphaIds) { const alphaIds = window.lastSimulationResults.alphaIds.join('\n'); navigator.clipboard.writeText(alphaIds).then(() => { updateStatus('✅ Alpha ID 已复制到剪贴板!', 'success'); }).catch(err => { console.error('复制失败: ', err); updateStatus('❌ 复制 Alpha ID 失败', 'error'); }); } } /** * 处理页面卸载 - 清理轮询 */ window.addEventListener('beforeunload', function() { stopLogPolling(); if (simulationAbortController) { simulationAbortController.abort(); } });