const state = { userId: null, files: [], sessions: [], currentSessionId: null, currentTaskId: null, pollTimer: null, }; function ensureUserId() { const key = "vibe_data_ana_user_id"; let userId = localStorage.getItem(key); if (!userId) { userId = `guest_${crypto.randomUUID()}`; localStorage.setItem(key, userId); } state.userId = userId; document.getElementById("user-id").textContent = userId; } async function api(path, options = {}) { const response = await fetch(path, options); const text = await response.text(); let data = {}; try { data = text ? JSON.parse(text) : {}; } catch { data = { raw: text }; } if (!response.ok) { throw new Error(data.detail || data.error || response.statusText); } return data; } function setText(id, value) { document.getElementById(id).textContent = value || ""; } function renderFiles() { const fileList = document.getElementById("file-list"); const picker = document.getElementById("session-file-picker"); fileList.innerHTML = ""; picker.innerHTML = ""; if (!state.files.length) { fileList.innerHTML = '
还没有上传文件。
'; picker.innerHTML = '
先上传文件后才能创建会话。
'; return; } state.files.forEach((file) => { const item = document.createElement("div"); item.className = "file-item"; item.innerHTML = `${file.original_name}
${file.id}
`; fileList.appendChild(item); const label = document.createElement("label"); label.className = "checkbox-item"; label.innerHTML = ` ${file.original_name} `; picker.appendChild(label); }); } function statusBadge(status) { return `${status}`; } function renderSessions() { const container = document.getElementById("session-list"); container.innerHTML = ""; if (!state.sessions.length) { container.innerHTML = '
暂无会话。
'; return; } state.sessions.forEach((session) => { const card = document.createElement("button"); card.type = "button"; card.className = `session-card ${session.id === state.currentSessionId ? "active" : ""}`; card.innerHTML = `
${session.title}
${session.id}
${statusBadge(session.status)}
`; card.onclick = () => loadSessionDetail(session.id); container.appendChild(card); }); } function renderTasks(tasks) { const container = document.getElementById("task-list"); container.innerHTML = ""; if (!tasks.length) { container.innerHTML = '
当前会话还没有专题任务。
'; return; } tasks.forEach((task) => { const card = document.createElement("button"); card.type = "button"; card.className = `task-card ${task.id === state.currentTaskId ? "active" : ""}`; card.innerHTML = `
${task.query}
${task.created_at}
${statusBadge(task.status)}
`; card.onclick = () => loadTaskReport(task.id); container.appendChild(card); }); } async function refreshFiles() { const data = await api(`/files?user_id=${encodeURIComponent(state.userId)}`); state.files = data.files || []; renderFiles(); } async function refreshSessions(selectSessionId = null) { const data = await api(`/sessions?user_id=${encodeURIComponent(state.userId)}`); state.sessions = data.sessions || []; renderSessions(); if (selectSessionId) { await loadSessionDetail(selectSessionId); } else if (state.currentSessionId) { const exists = state.sessions.some((session) => session.id === state.currentSessionId); if (exists) { await loadSessionDetail(state.currentSessionId, false); } } } async function loadSessionDetail(sessionId, renderReport = true) { const data = await api(`/sessions/${sessionId}?user_id=${encodeURIComponent(state.userId)}`); state.currentSessionId = sessionId; document.getElementById("detail-title").textContent = data.session.title; document.getElementById("detail-meta").textContent = `${data.session.id} · ${data.session.status}`; renderSessions(); renderTasks(data.tasks || []); const latestDoneTask = (data.tasks || []).slice().reverse().find((task) => task.status === "succeeded"); if (renderReport && latestDoneTask) { await loadTaskReport(latestDoneTask.id); } else if (!latestDoneTask) { setText("report-title", "暂无已完成专题"); setText("report-content", "当前会话还没有可展示的报告。"); document.getElementById("artifact-gallery").innerHTML = ""; } } async function loadTaskReport(taskId) { state.currentTaskId = taskId; renderSessions(); const taskData = await api(`/tasks/${taskId}?user_id=${encodeURIComponent(state.userId)}`); setText("report-title", taskData.task.query); if (taskData.task.status !== "succeeded") { setText("report-content", `当前任务状态为 ${taskData.task.status}。\n错误信息:${taskData.task.error_message || "暂无"}`); document.getElementById("artifact-gallery").innerHTML = ""; return; } const reportData = await api(`/tasks/${taskId}/report/content?user_id=${encodeURIComponent(state.userId)}`); setText("report-content", reportData.content || ""); const artifactData = await api(`/tasks/${taskId}/artifacts?user_id=${encodeURIComponent(state.userId)}`); renderArtifacts(artifactData.artifacts || []); } function renderArtifacts(artifacts) { const gallery = document.getElementById("artifact-gallery"); gallery.innerHTML = ""; const images = artifacts.filter((item) => item.is_image); if (!images.length) { gallery.innerHTML = '
当前任务没有图片产物。
'; return; } images.forEach((artifact) => { const card = document.createElement("div"); card.className = "artifact-card"; card.innerHTML = ` ${artifact.name}
${artifact.name}
`; gallery.appendChild(card); }); } async function handleUpload(event) { event.preventDefault(); const input = document.getElementById("upload-input"); if (!input.files.length) { setText("upload-status", "请选择至少一个文件。"); return; } const formData = new FormData(); formData.append("user_id", state.userId); Array.from(input.files).forEach((file) => formData.append("files", file)); setText("upload-status", "上传中..."); await api("/files/upload", { method: "POST", body: formData }); setText("upload-status", "文件上传完成。"); input.value = ""; await refreshFiles(); } async function handleCreateSession(event) { event.preventDefault(); const checked = Array.from(document.querySelectorAll("#session-file-picker input:checked")); const fileIds = checked.map((item) => item.value); if (!fileIds.length) { setText("session-status", "请至少选择一个文件。"); return; } const payload = { user_id: state.userId, title: document.getElementById("session-title").value.trim(), query: document.getElementById("session-query").value.trim(), file_ids: fileIds, }; setText("session-status", "会话创建中..."); const data = await api("/sessions", { method: "POST", headers: { "Content-Type": "application/json" }, body: JSON.stringify(payload), }); setText("session-status", "会话已创建,正在执行首个专题。"); document.getElementById("session-form").reset(); await refreshSessions(data.session.id); } async function handleFollowup() { if (!state.currentSessionId) { setText("detail-meta", "请先选择一个会话。"); return; } const query = document.getElementById("followup-query").value.trim(); if (!query) { return; } await api(`/sessions/${state.currentSessionId}/topics`, { method: "POST", headers: { "Content-Type": "application/json" }, body: JSON.stringify({ user_id: state.userId, query }), }); document.getElementById("followup-query").value = ""; await refreshSessions(state.currentSessionId); } async function handleCloseSession() { if (!state.currentSessionId) { return; } await api(`/sessions/${state.currentSessionId}/close?user_id=${encodeURIComponent(state.userId)}`, { method: "POST", }); await refreshSessions(state.currentSessionId); } function startPolling() { if (state.pollTimer) { clearInterval(state.pollTimer); } state.pollTimer = setInterval(() => { refreshSessions().catch((error) => console.error(error)); }, 8000); } async function bootstrap() { ensureUserId(); document.getElementById("upload-form").addEventListener("submit", (event) => { handleUpload(event).catch((error) => setText("upload-status", error.message)); }); document.getElementById("session-form").addEventListener("submit", (event) => { handleCreateSession(event).catch((error) => setText("session-status", error.message)); }); document.getElementById("submit-followup").onclick = () => { handleFollowup().catch((error) => setText("detail-meta", error.message)); }; document.getElementById("close-session").onclick = () => { handleCloseSession().catch((error) => setText("detail-meta", error.message)); }; document.getElementById("refresh-sessions").onclick = () => { refreshSessions().catch((error) => console.error(error)); }; await refreshFiles(); await refreshSessions(); startPolling(); } bootstrap().catch((error) => { console.error(error); setText("detail-meta", error.message); });