cc-solver / static /solver.html
VeryOnly123's picture
update
5f229e4
Raw History Blame Contribute Delete
34.9 kB
<!DOCTYPE html>
<html lang="zh-CN">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>CC Solver</title>
<script src="https://cdn.jsdelivr.net/npm/marked/marked.min.js"></script>
<link rel="stylesheet" href="https://cdn.jsdelivr.net/npm/katex@0.16.9/dist/katex.min.css">
<script src="https://cdn.jsdelivr.net/npm/katex@0.16.9/dist/katex.min.js"></script>
<script src="https://cdn.jsdelivr.net/npm/katex@0.16.9/dist/contrib/auto-render.min.js"></script>
<style>
* { box-sizing: border-box; margin: 0; padding: 0; }
body { font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', sans-serif; background: #f0f2f5; color: #333; min-height: 100vh; }
.container { max-width: 960px; margin: 0 auto; padding: 20px; }
h1 { text-align: center; padding: 20px 0 6px; color: #1a1a2e; font-size: 1.8em; }
.subtitle { text-align: center; color: #666; margin-bottom: 24px; font-size: 0.9em; }
.card { background: #fff; border-radius: 12px; box-shadow: 0 2px 8px rgba(0,0,0,0.08); padding: 24px; margin-bottom: 20px; }
.card-title { font-size: 0.95em; font-weight: 700; color: #1a1a2e; margin-bottom: 12px; }
/* Input area */
.input-mode-toggle { display: flex; gap: 0; margin-bottom: 12px; background: #f0f0f0; border-radius: 8px; overflow: hidden; }
.input-mode-toggle button { flex: 1; padding: 8px 16px; border: none; cursor: pointer; font-size: 13px; font-weight: 600; background: #f0f0f0; color: #666; transition: all 0.15s; }
.input-mode-toggle button.active { background: #4361ee; color: #fff; }
textarea { width: 100%; min-height: 150px; border: 1px solid #ddd; border-radius: 8px; padding: 12px; font-size: 14px; font-family: inherit; resize: vertical; line-height: 1.6; }
textarea:focus { outline: none; border-color: #4361ee; box-shadow: 0 0 0 3px rgba(67,97,238,0.15); }
.file-drop { border: 2px dashed #ccc; border-radius: 8px; padding: 32px 16px; text-align: center; color: #999; cursor: pointer; transition: border-color 0.2s, background 0.2s; }
.file-drop:hover, .file-drop.dragover { border-color: #4361ee; background: #f5f7ff; }
.file-drop input[type="file"] { display: none; }
.file-list { margin-top: 8px; font-size: 13px; }
.file-list .file-item { display: flex; justify-content: space-between; padding: 4px 0; }
.file-list .file-item .remove { color: #e74c3c; cursor: pointer; font-weight: 700; }
.btn-row { display: flex; gap: 12px; margin-top: 12px; }
.btn { padding: 10px 28px; border: none; border-radius: 8px; font-size: 15px; cursor: pointer; font-weight: 600; transition: all 0.15s; }
.btn-primary { background: #4361ee; color: #fff; }
.btn-primary:hover:not(:disabled) { background: #3a56d4; }
.btn-primary:disabled { background: #a0b0f0; cursor: not-allowed; }
.btn-secondary { background: #e8e8e8; color: #333; }
.btn-secondary:hover { background: #d8d8d8; }
.btn-download { background: #2ecc71; color: #fff; padding: 6px 14px; font-size: 12px; border-radius: 6px; text-decoration: none; }
.btn-download:hover { background: #27ae60; }
/* Config */
.config-row { display: flex; gap: 8px; margin-bottom: 8px; align-items: center; }
.config-row label { font-size: 13px; color: #666; min-width: 90px; }
.config-row input { flex: 1; padding: 6px 10px; border: 1px solid #ddd; border-radius: 6px; font-size: 13px; font-family: monospace; }
.config-row button { padding: 6px 14px; border: none; border-radius: 6px; background: #4361ee; color: #fff; font-size: 12px; cursor: pointer; white-space: nowrap; }
/* Task list */
.task-list { max-height: 400px; overflow-y: auto; }
.task-item { display: flex; justify-content: space-between; align-items: center; padding: 10px 0; border-bottom: 1px solid #f0f0f0; gap: 8px; }
.task-item:last-child { border-bottom: none; }
.task-name { font-weight: 600; font-size: 14px; }
.task-preview { font-size: 12px; color: #888; max-width: 300px; overflow: hidden; text-overflow: ellipsis; white-space: nowrap; }
.task-meta { display: flex; align-items: center; gap: 8px; }
.task-delete { cursor: pointer; font-size: 14px; color: #ccc; font-weight: 700; transition: color 0.15s; }
.task-delete:hover { color: #e74c3c; }
.badge { font-size: 11px; padding: 2px 8px; border-radius: 10px; font-weight: 600; }
.badge-pending { background: #fff3cd; color: #856404; }
.badge-running { background: #cce5ff; color: #004085; }
.badge-complete { background: #d4edda; color: #155724; }
.badge-error { background: #f8d7da; color: #721c24; }
/* Modal */
.modal-overlay { display: none; position: fixed; inset: 0; background: rgba(0,0,0,0.4); z-index: 100; align-items: center; justify-content: center; }
.modal-overlay.visible { display: flex; }
.modal { background: #fff; border-radius: 12px; max-width: 800px; width: 90%; max-height: 80vh; overflow-y: auto; padding: 24px; }
.modal h2 { font-size: 1.1em; margin-bottom: 12px; }
.modal .result-content { line-height: 1.7; }
.modal .result-content table { border-collapse: collapse; width: 100%; margin: 12px 0; }
.modal .result-content th, .modal .result-content td { border: 1px solid #ddd; padding: 8px 12px; text-align: left; }
.modal .result-content th { background: #f5f5f5; }
.modal .result-content pre { background: #f8f8f8; padding: 12px; border-radius: 6px; overflow-x: auto; font-size: 13px; }
.modal .result-content code { background: #f0f0f0; padding: 1px 4px; border-radius: 3px; font-size: 13px; }
.modal .result-content pre code { background: none; padding: 0; }
.modal .result-content blockquote { border-left: 3px solid #4361ee; padding-left: 12px; color: #555; margin: 8px 0; }
.modal .log-console { background: #1e1e2e; color: #cdd6f4; border-radius: 8px; padding: 12px; max-height: 200px; overflow-y: auto; font-family: monospace; font-size: 12px; line-height: 1.5; white-space: pre-wrap; word-break: break-all; }
.error-msg { background: #f8d7da; color: #721c24; padding: 10px; border-radius: 8px; margin-top: 8px; display: none; font-size: 13px; }
.error-msg.visible { display: block; }
/* Per-task progress */
.task-files { display: flex; gap: 4px; margin-top: 4px; }
.task-file-step { font-size: 10px; padding: 1px 6px; border-radius: 4px; background: #e8e8e8; color: #999; }
.task-file-step.done { background: #d4edda; color: #155724; font-weight: 600; }
.task-file-step.current { background: #cce5ff; color: #004085; font-weight: 600; animation: pulse 1.5s infinite; }
/* Workspace file tabs */
.ws-tab:hover { background: #e0e0e0 !important; }
/* Math formulas */
.math-display { overflow-x: auto; margin: 12px 0; padding: 8px 0; text-align: center; }
</style>
</head>
<body>
<div class="container">
<h1>CC Solver</h1>
<p class="subtitle">Multi-agent physics problem solver</p>
<!-- Config -->
<div class="card">
<div class="card-title">Solver Config</div>
<div class="config-row">
<label>Repo URL</label>
<input type="text" id="solver-url" placeholder="https://github.com/Usami-Mira/CC_Integrater/raw/refs/heads/main/">
<button onclick="updateConfig()">Update</button>
</div>
<div class="config-row">
<label>Token</label>
<input type="password" id="solver-token" placeholder="Enter password">
</div>
<p style="font-size:12px;color:#888;margin-top:8px;">Model 和 timeout 由仓库 config.json 控制。</p>
</div>
<!-- Input -->
<div class="card">
<div class="card-title">New Task</div>
<div class="input-mode-toggle">
<button id="mode-text" class="active" onclick="setInputMode('text')">Text Input</button>
<button id="mode-file" onclick="setInputMode('file')">Upload Files</button>
</div>
<!-- Config override -->
<details style="margin-bottom:12px;">
<summary style="cursor:pointer;font-size:13px;font-weight:600;color:#666;">Custom Config (config.json)</summary>
<textarea id="config-input" placeholder='Loading config...' style="min-height:80px;margin-top:8px;font-family:monospace;font-size:13px;"></textarea>
</details>
<!-- Private mode -->
<div style="display:flex;align-items:center;gap:8px;margin-bottom:12px;">
<label style="font-size:13px;font-weight:600;color:#666;cursor:pointer;display:flex;align-items:center;gap:6px;">
<input type="checkbox" id="private-mode" style="width:16px;height:16px;cursor:pointer;">
Private Mode
</label>
<span id="secret-display" style="font-size:12px;color:#aaa;font-family:monospace;display:none;"></span>
</div>
<div id="input-text-area">
<textarea id="problem-input" placeholder="Paste your physics problem here..."></textarea>
<div class="btn-row">
<button class="btn btn-primary" id="submit-btn" onclick="submitText()">Solve</button>
</div>
</div>
<div id="input-file-area" style="display:none;">
<div class="file-drop" id="file-drop" onclick="document.getElementById('file-input').click()">
<p>Click or drag <strong>.md</strong> files here</p>
<input type="file" id="file-input" accept=".md" multiple>
</div>
<div class="file-list" id="file-list"></div>
<div class="btn-row">
<button class="btn btn-primary" id="submit-files-btn" onclick="submitFiles()">Solve All</button>
</div>
</div>
<div class="error-msg" id="error-msg"></div>
</div>
<!-- Tasks -->
<div class="card">
<div style="display:flex;justify-content:space-between;align-items:center;margin-bottom:12px;">
<div class="card-title" style="margin-bottom:0;">Tasks</div>
<div>
<a class="btn-download" id="cleanup-btn" style="display:none;margin-right:8px;cursor:pointer;" onclick="cleanupTasks()">Clear Done</a>
<a class="btn-download" id="download-all-btn" style="display:none;" onclick="downloadAll()">Download All</a>
</div>
</div>
<div class="task-list" id="task-list">
<p style="color:#999;font-size:13px;">No tasks yet.</p>
</div>
</div>
</div>
<!-- Task detail modal -->
<div class="modal-overlay" id="modal-overlay" onclick="closeModal(event)">
<div class="modal" id="modal-content">
<div style="display:flex;justify-content:space-between;align-items:center;margin-bottom:12px;">
<h2 id="modal-title" style="margin-bottom:0;">Task Detail</h2>
<div>
<button id="modal-refresh" style="padding:6px 10px;border:1px solid #ddd;border-radius:6px;background:#fff;cursor:pointer;font-size:12px;margin-right:8px;" title="Refresh" onclick="refreshModalContent()">Refresh</button>
<a class="btn-download" id="modal-download" style="display:none;margin-right:8px;" onclick="">Download</a>
<button class="btn btn-secondary" onclick="closeModalDirect()" style="padding:6px 14px;font-size:12px;">Close</button>
</div>
</div>
<div id="modal-body"></div>
</div>
</div>
<script>
let inputMode = 'text';
let pendingFiles = [];
let pollTimers = {};
// Configure marked to preserve math delimiters
marked.setOptions({
breaks: true,
gfm: true,
});
function renderMarkdownWithMath(md) {
// Protect math delimiters from marked escaping
const mathBlocks = [];
const protectedMd = md
.replace(/\$\$([\s\S]+?)\$\$/g, (m) => {
mathBlocks.push({ type: 'block', content: m.slice(2, -2) });
return `%%MATH_BLOCK_${mathBlocks.length - 1}%%`;
})
.replace(/\$([^\$\n]+?)\$/g, (m) => {
mathBlocks.push({ type: 'inline', content: m.slice(1, -1) });
return `%%MATH_BLOCK_${mathBlocks.length - 1}%%`;
});
let html = marked.parse(protectedMd);
// Restore math blocks
html = html.replace(/%%MATH_BLOCK_(\d+)%%/g, (_, idx) => {
const block = mathBlocks[parseInt(idx)];
try {
if (block.type === 'block') {
return `<div class="math-display">${katex.renderToString(block.content.trim(), { displayMode: true, throwOnError: false })}</div>`;
}
return katex.renderToString(block.content, { displayMode: false, throwOnError: false });
} catch (e) {
return block.type === 'block' ? `$$${block.content}$$` : `$${block.content}$`;
}
});
return html;
}
function getToken() {
const val = document.getElementById('solver-token').value.trim();
return val;
}
function getConfigOverride() {
const val = document.getElementById('config-input').value.trim();
if (!val) return null;
try { return JSON.parse(val); }
catch (e) { showError('Invalid config JSON: ' + e.message); return null; }
}
// Secret key for private mode
function getSecretKey() {
let key = localStorage.getItem('secret_key');
if (!key) {
key = generateKey();
localStorage.setItem('secret_key', key);
}
return key;
}
function generateKey() {
const chars = 'ABCDEFGHJKLMNPQRSTUVWXYZabcdefghijkmnopqrstuvwxyz23456789';
let result = '';
const buf = new Uint8Array(16);
crypto.getRandomValues(buf);
for (let i = 0; i < 16; i++) {
result += chars[buf[i] % chars.length];
}
return result;
}
function api(url, opts = {}) {
const token = getToken();
const sk = getSecretKey();
const separator = url.includes('?') ? '&' : '?';
let sep = '';
if (token) sep += `token=${encodeURIComponent(token)}`;
if (sk) {
sep += `${sep ? '&' : ''}key=${encodeURIComponent(sk)}`;
}
return fetch(url + (sep ? separator + sep : ''), opts);
}
function setInputMode(mode) {
inputMode = mode;
document.getElementById('mode-text').classList.toggle('active', mode === 'text');
document.getElementById('mode-file').classList.toggle('active', mode === 'file');
document.getElementById('input-text-area').style.display = mode === 'text' ? 'block' : 'none';
document.getElementById('input-file-area').style.display = mode === 'file' ? 'block' : 'none';
}
// File handling
const fileInput = document.getElementById('file-input');
const fileDrop = document.getElementById('file-drop');
fileInput.addEventListener('change', (e) => addFiles(e.target.files));
fileDrop.addEventListener('dragover', (e) => { e.preventDefault(); fileDrop.classList.add('dragover'); });
fileDrop.addEventListener('dragleave', () => fileDrop.classList.remove('dragover'));
fileDrop.addEventListener('drop', (e) => { e.preventDefault(); fileDrop.classList.remove('dragover'); addFiles(e.dataTransfer.files); });
function addFiles(files) {
for (const f of files) {
if (f.name.endsWith('.md') && !pendingFiles.find(x => x.name === f.name)) {
pendingFiles.push(f);
}
}
renderFileList();
}
function removeFile(idx) {
pendingFiles.splice(idx, 1);
renderFileList();
}
function renderFileList() {
const el = document.getElementById('file-list');
if (pendingFiles.length === 0) { el.innerHTML = ''; return; }
el.innerHTML = pendingFiles.map((f, i) =>
`<div class="file-item"><span>${f.name} (${(f.size/1024).toFixed(1)} KB)</span><span class="remove" onclick="removeFile(${i})">×</span></div>`
).join('');
}
function showError(msg) {
const el = document.getElementById('error-msg');
el.textContent = msg;
el.classList.add('visible');
setTimeout(() => el.classList.remove('visible'), 5000);
}
function hideError() { document.getElementById('error-msg').classList.remove('visible'); }
// Submit text problem
async function submitText() {
const text = document.getElementById('problem-input').value.trim();
if (!text) return;
hideError();
try {
const body = { problem: text };
const cfg = getConfigOverride();
if (cfg) body.config = cfg;
if (document.getElementById('private-mode').checked) {
body.secret_key = getSecretKey();
}
const resp = await api('/api/submit', {
method: 'POST',
headers: {'Content-Type': 'application/json'},
body: JSON.stringify(body)
});
const data = await resp.json();
if (data.error) { showError(data.error); return; }
document.getElementById('problem-input').value = '';
loadTasks();
} catch (e) { showError('Network error: ' + e.message); }
}
// Submit files
async function submitFiles() {
if (pendingFiles.length === 0) { showError('No files selected'); return; }
hideError();
const fd = new FormData();
pendingFiles.forEach(f => fd.append('files', f));
const cfg = getConfigOverride();
if (cfg) fd.append('config', JSON.stringify(cfg));
if (document.getElementById('private-mode').checked) {
fd.append('secret_key', getSecretKey());
}
try {
const token = getToken();
const resp = await api('/api/submit-file', { method: 'POST', body: fd });
const data = await resp.json();
if (data.error) { showError(data.error); return; }
pendingFiles = [];
renderFileList();
fileInput.value = '';
loadTasks();
} catch (e) { showError('Network error: ' + e.message); }
}
const DEFAULT_URL = 'https://github.com/Usami-Mira/CC_Integrater/raw/refs/heads/main/';
// Load config
const savedUrl = localStorage.getItem('solver_url');
document.getElementById('solver-url').value = savedUrl || DEFAULT_URL;
api('/api/config')
.then(r => r.json())
.then(data => {
if (data.solver_url) {
document.getElementById('solver-url').value = data.solver_url;
localStorage.setItem('solver_url', data.solver_url);
}
if (data.default_config && Object.keys(data.default_config).length > 0) {
document.getElementById('config-input').value = JSON.stringify(data.default_config, null, 2);
}
})
.catch(() => {});
// Load saved token
const savedToken = localStorage.getItem('solver_token');
if (savedToken) {
document.getElementById('solver-token').value = savedToken;
}
document.getElementById('solver-token').addEventListener('change', (e) => {
localStorage.setItem('solver_token', e.target.value);
});
async function updateConfig() {
const url = document.getElementById('solver-url').value.trim();
if (!url) return;
localStorage.setItem('solver_url', url);
const btn = document.querySelector('.config-row button');
btn.textContent = 'Fetching...';
btn.disabled = true;
try {
const resp = await api('/api/config', {
method: 'POST',
headers: {'Content-Type': 'application/json'},
body: JSON.stringify({solver_url: url})
});
const data = await resp.json();
if (data.status === 'ok') {
alert('Solver files updated successfully!');
} else {
alert('Update failed: ' + (data.error || 'unknown'));
}
} catch (e) { alert('Network error'); }
btn.textContent = 'Update';
btn.disabled = false;
}
// Task list
function renderTaskProgress(filesDone, status) {
const steps = (filesDone || []).map(f => {
const step = typeof f === 'string' ? f : f.step;
const st = typeof f === 'string' ? 'history' : f.status;
const cls = st === 'current' ? 'current' : 'done';
return `<span class="task-file-step ${cls}">${step}</span>`;
});
if (status === 'running' || status === 'pending') {
steps.push(`<span class="task-file-step current">running…</span>`);
}
return steps.join('');
}
async function loadTasks() {
try {
const resp = await api('/api/tasks');
const data = await resp.json();
const el = document.getElementById('task-list');
const downloadAll = document.getElementById('download-all-btn');
if (!data.tasks || data.tasks.length === 0) {
el.innerHTML = '<p style="color:#999;font-size:13px;">No tasks yet.</p>';
downloadAll.style.display = 'none';
document.getElementById('cleanup-btn').style.display = 'none';
return;
}
const hasComplete = data.tasks.some(t => t.status === 'complete');
downloadAll.style.display = hasComplete ? 'inline-block' : 'none';
const hasCleanup = data.tasks.some(t => t.status === 'complete' || t.status === 'error');
document.getElementById('cleanup-btn').style.display = hasCleanup ? 'inline-block' : 'none';
const token = getToken();
const tokenSep = token ? `?token=${encodeURIComponent(token)}&` : '?';
el.innerHTML = data.tasks.map(t => {
const badgeClass = 'badge-' + t.status;
const progressHtml = renderTaskProgress(t.files_done || [], t.status);
const lockIcon = t.is_private ? '<span style="font-size:12px;color:#e67e22;" title="Private task">🔒</span>' : '';
const deleteBtn = t.status !== 'running'
? `<span class="task-delete" onclick="event.stopPropagation();deleteTask('${t.task_id}')" title="Delete">✕</span>`
: '';
const sk = getSecretKey();
const downloadHref = `/api/download/${t.task_id}?key=${encodeURIComponent(sk)}` + (token ? `&token=${encodeURIComponent(token)}` : '');
return `<div class="task-item" onclick="openTask('${t.task_id}')">
<div style="flex:1;min-width:0;">
<div class="task-name">${t.name || t.task_id} ${lockIcon}</div>
<div style="font-size:11px;color:#aaa;font-family:monospace;">${t.task_id}</div>
<div class="task-preview">${t.problem_preview || ''}</div>
<div class="task-files">${progressHtml}</div>
</div>
<div class="task-meta">
<span class="badge ${badgeClass}">${t.status}</span>
${t.status === 'complete' ? `<a class="btn-download" href="${downloadHref}" onclick="event.stopPropagation()">↓ Download</a>` : ''}
${deleteBtn}
</div>
</div>`;
}).join('');
} catch (e) {}
}
// Task detail modal
async function openTask(taskId) {
const overlay = document.getElementById('modal-overlay');
const body = document.getElementById('modal-body');
const title = document.getElementById('modal-title');
const downloadBtn = document.getElementById('modal-download');
title.textContent = 'Loading...';
downloadBtn.style.display = 'none';
body.innerHTML = '<p style="color:#888;">Loading task info...</p>';
overlay.classList.add('visible');
try {
const resp = await api('/api/status/' + taskId);
if (!resp.ok) {
const errData = await resp.json().catch(() => null);
body.innerHTML = `<p style="color:#888;">Failed to load task: HTTP ${resp.status}${errData?.error ? ' — ' + errData.error : ''}</p>`;
return;
}
const t = await resp.json();
if (!t || !t.status) {
body.innerHTML = `<p style="color:#888;">Invalid response from server</p>`;
return;
}
title.textContent = t.name || taskId;
const progressHtml = renderTaskProgress(t.files_done || [], t.status);
if (t.status === 'complete') {
const resResp = await api('/api/result/' + taskId);
const resData = await resResp.json();
const logResp = await api('/api/logs/' + taskId + '?lines=100');
const logData = await logResp.json();
const logHtml = (logData.logs || []).length > 0
? `<details style="margin-top:16px;"><summary style="cursor:pointer;font-size:13px;font-weight:600;color:#666;">Logs</summary><div class="log-console">${(logData.logs || []).map(l => escapeHtml(l)).join('\n')}</div></details>`
: '';
if (resData.content) {
body.innerHTML = `<div class="task-files" style="margin-bottom:12px;">${progressHtml}</div><div class="result-content">${renderMarkdownWithMath(resData.content)}</div>${logHtml}`;
} else {
body.innerHTML = `<div class="task-files" style="margin-bottom:12px;">${progressHtml}</div>${logHtml}<p style="color:#888;">Result file not found. <a href="/api/download/${taskId}">Download workspace</a> instead.</p>`;
}
const tkn = getToken();
downloadBtn.href = `/api/download/${taskId}`;
downloadBtn.style.display = 'inline-block';
} else if (t.status === 'error') {
const logResp = await api('/api/logs/' + taskId + '?lines=100');
const logData = await logResp.json();
body.innerHTML = `<div class="task-files" style="margin-bottom:12px;">${progressHtml}</div><div class="error-msg visible">${t.error || 'Task failed'}</div><div class="log-console" style="margin-top:12px;">${(logData.logs || []).map(l => escapeHtml(l)).join('\n')}</div>`;
} else {
// Running/pending: show progress + logs + workspace files
const logResp = await api('/api/logs/' + taskId + '?lines=50');
const logData = await logResp.json();
const wsFiles = await loadWorkspaceFiles(taskId);
body.innerHTML = `<div class="task-files" style="margin-bottom:12px;">${progressHtml}</div>${renderWorkspaceFiles(taskId, wsFiles)}<div class="log-console" style="margin-top:12px;">${(logData.logs || []).map(l => escapeHtml(l)).join('\n')}</div>`;
// Auto-load first file
if (wsFiles.length > 0) {
showWorkspaceFile(taskId, wsFiles[0], 'ws-tabs-' + taskId, 'ws-content-' + taskId);
}
}
// Start polling if running
if (t.status === 'running' || t.status === 'pending') {
startTaskPoll(taskId);
}
} catch (e) {
body.innerHTML = `<p style="color:#888;">Failed to load task: ${e.message}</p>`;
console.error('openTask error:', e);
}
}
function startTaskPoll(taskId) {
if (pollTimers[taskId]) clearInterval(pollTimers[taskId]);
pollTimers[taskId] = setInterval(async () => {
try {
const resp = await api('/api/status/' + taskId);
const t = await resp.json();
if (t.status === 'complete') {
clearInterval(pollTimers[taskId]);
delete pollTimers[taskId];
openTask(taskId);
loadTasks();
} else if (t.status === 'error') {
clearInterval(pollTimers[taskId]);
delete pollTimers[taskId];
loadTasks();
} else {
// Running/pending: only refresh task list, leave modal alone
loadTasks();
}
} catch (e) {}
}, 10000);
}
// Workspace files for running tasks
async function loadWorkspaceFiles(taskId) {
try {
const resp = await api('/api/workspace/' + taskId + '/files');
const data = await resp.json();
return data.files || [];
} catch (e) { return []; }
}
async function loadWorkspaceFile(taskId, filename) {
try {
const resp = await api('/api/workspace/' + taskId + '/' + filename);
const data = await resp.json();
return data.content || null;
} catch (e) { return null; }
}
function renderWorkspaceFiles(taskId, files) {
if (!files || files.length === 0) return '';
const tabsId = 'ws-tabs-' + taskId;
const contentId = 'ws-content-' + taskId;
const firstFile = files[0];
const tabs = files.map(f =>
`<button class="ws-tab" data-task="${taskId}" data-file="${f}" onclick="showWorkspaceFile('${taskId}','${f}','${tabsId}','${contentId}')" style="padding:4px 10px;border:1px solid #ddd;border-radius:4px 4px 0 0;background:${f === firstFile ? '#4361ee' : '#f0f0f0'};color:${f === firstFile ? '#fff' : '#666'};border-color:${f === firstFile ? '#4361ee' : '#ddd'};cursor:pointer;font-size:12px;font-family:monospace;">${f}</button>`
).join('');
return `<div id="${tabsId}" style="display:flex;gap:4px;margin-bottom:8px;flex-wrap:wrap;">${tabs}</div><div id="${contentId}" style="background:#1e1e2e;color:#cdd6f4;border-radius:8px;padding:12px;max-height:300px;overflow-y:auto;font-family:monospace;font-size:12px;line-height:1.5;white-space:pre-wrap;word-break:break-all;">Loading...</div>`;
}
async function showWorkspaceFile(taskId, filename, tabsId, contentId) {
const content = await loadWorkspaceFile(taskId, filename);
const el = document.getElementById(contentId);
if (el) el.textContent = content || 'File not found';
// Highlight active tab
document.querySelectorAll(`#${tabsId} .ws-tab`).forEach(btn => {
btn.style.background = btn.dataset.file === filename ? '#4361ee' : '#f0f0f0';
btn.style.color = btn.dataset.file === filename ? '#fff' : '#666';
btn.style.borderColor = btn.dataset.file === filename ? '#4361ee' : '#ddd';
});
}
// Delete task
async function deleteTask(taskId) {
if (!confirm('Delete this task? This cannot be undone.')) return;
try {
const resp = await api(`/api/task/${taskId}`, { method: 'DELETE' });
const data = await resp.json();
if (data.error) { showError(data.error); return; }
loadTasks();
} catch (e) { showError('Network error: ' + e.message); }
}
// Cleanup completed/errored tasks
async function cleanupTasks() {
if (!confirm('Clear all completed and errored tasks? This cannot be undone.')) return;
try {
const resp = await api('/api/tasks/cleanup', { method: 'POST' });
const data = await resp.json();
loadTasks();
} catch (e) { showError('Network error: ' + e.message); }
}
function closeModal(e) {
if (e.target === document.getElementById('modal-overlay')) {
closeModalDirect();
}
}
function closeModalDirect() {
document.getElementById('modal-overlay').classList.remove('visible');
Object.values(pollTimers).forEach(t => clearInterval(t));
pollTimers = {};
}
async function refreshModalContent() {
const taskId = document.getElementById('modal-download').href?.split('/').pop();
if (!taskId) return;
// Remember which file user was viewing
const activeTab = document.querySelector('.ws-tab[style*="#4361ee"]');
const currentFile = activeTab ? activeTab.dataset.file : null;
const resp = await api('/api/status/' + taskId);
if (!resp.ok) return;
const t = await resp.json();
document.getElementById('modal-title').textContent = t.name || taskId;
const progressHtml = renderTaskProgress(t.files_done || [], t.status);
if (t.status === 'complete') {
const resResp = await api('/api/result/' + taskId);
const resData = await resResp.json();
const logResp = await api('/api/logs/' + taskId + '?lines=100');
const logData = await logResp.json();
const logHtml = (logData.logs || []).length > 0
? `<details style="margin-top:16px;"><summary style="cursor:pointer;font-size:13px;font-weight:600;color:#666;">Logs</summary><div class="log-console">${(logData.logs || []).map(l => escapeHtml(l)).join('\n')}</div></details>`
: '';
if (resData.content) {
document.getElementById('modal-body').innerHTML = `<div class="task-files" style="margin-bottom:12px;">${progressHtml}</div><div class="result-content">${renderMarkdownWithMath(resData.content)}</div>${logHtml}`;
} else {
document.getElementById('modal-body').innerHTML = `<div class="task-files" style="margin-bottom:12px;">${progressHtml}</div>${logHtml}<p style="color:#888;">Result file not found. <a href="/api/download/${taskId}">Download workspace</a> instead.</p>`;
}
const sk = getSecretKey();
const token = getToken();
document.getElementById('modal-download').href = `/api/download/${taskId}?key=${encodeURIComponent(sk)}${token ? '&token=' + encodeURIComponent(token) : ''}`;
document.getElementById('modal-download').style.display = 'inline-block';
} else if (t.status === 'error') {
const logResp = await api('/api/logs/' + taskId + '?lines=100');
const logData = await logResp.json();
document.getElementById('modal-body').innerHTML = `<div class="task-files" style="margin-bottom:12px;">${progressHtml}</div><div class="error-msg visible">${t.error || 'Task failed'}</div><div class="log-console" style="margin-top:12px;">${(logData.logs || []).map(l => escapeHtml(l)).join('\n')}</div>`;
} else {
const logResp = await api('/api/logs/' + taskId + '?lines=50');
const logData = await logResp.json();
const wsFiles = await loadWorkspaceFiles(taskId);
document.getElementById('modal-body').innerHTML = `<div class="task-files" style="margin-bottom:12px;">${progressHtml}</div>${renderWorkspaceFiles(taskId, wsFiles)}<div class="log-console" style="margin-top:12px;">${(logData.logs || []).map(l => escapeHtml(l)).join('\n')}</div>`;
// Restore previously viewed file
const fileToLoad = wsFiles.includes(currentFile) ? currentFile : wsFiles[0];
if (fileToLoad) {
showWorkspaceFile(taskId, fileToLoad, 'ws-tabs-' + taskId, 'ws-content-' + taskId);
}
}
}
function downloadAll() {
const token = getToken();
const sk = getSecretKey();
const sep = token ? '?' : '?';
window.location.href = `/api/download-all?${sep}key=${encodeURIComponent(sk)}${token ? '&token=' + encodeURIComponent(token) : ''}`;
}
function escapeHtml(s) {
return s.replace(/&/g,'&amp;').replace(/</g,'&lt;').replace(/>/g,'&gt;');
}
// Private mode UI
const privateModeCheckbox = document.getElementById('private-mode');
const secretDisplay = document.getElementById('secret-display');
if (privateModeCheckbox) {
privateModeCheckbox.addEventListener('change', () => {
secretDisplay.style.display = privateModeCheckbox.checked ? 'inline' : 'none';
secretDisplay.textContent = privateModeCheckbox.checked ? `Key: ${getSecretKey()}` : '';
});
}
// Initial load
loadTasks();
setInterval(loadTasks, 10000);
</script>
</body>
</html>