const storageKey = "arbitration_cases"; const state = { cases: [], activeCaseId: null, }; const caseForm = document.getElementById("caseForm"); const caseIdInput = document.getElementById("caseId"); const caseTitleInput = document.getElementById("caseTitle"); const caseDescInput = document.getElementById("caseDesc"); const caseFormReset = document.getElementById("caseFormReset"); const casesTable = document.getElementById("casesTable"); const caseTimeline = document.getElementById("caseTimeline"); const arbitrationForm = document.getElementById("arbitrationForm"); const arbitrationCaseSelect = document.getElementById("arbitrationCaseSelect"); const newCaseIdInput = document.getElementById("newCaseId"); const newCaseTitleInput = document.getElementById("newCaseTitle"); const applicationResult = document.getElementById("applicationResult"); const transcriptResult = document.getElementById("transcriptResult"); const finalDecision = document.getElementById("finalDecision"); const confirmMaterials = document.getElementById("confirmMaterials"); const confirmDecision = document.getElementById("confirmDecision"); const arbitrationStatus = document.getElementById("arbitrationStatus"); const applicationFiles = document.getElementById("applicationFiles"); const transcriptFiles = document.getElementById("transcriptFiles"); const evidenceFiles = document.getElementById("evidenceFiles"); const lawResults = document.getElementById("lawResults"); const judgementReasoning = document.getElementById("judgementReasoning"); const disputeFindings = document.getElementById("disputeFindings"); const apiStatus = document.getElementById("apiStatus"); const workflowStatus = document.getElementById("workflowStatus"); const toolApplicationRun = document.getElementById("toolApplicationRun"); const toolTranscriptRun = document.getElementById("toolTranscriptRun"); const toolLawRun = document.getElementById("toolLawRun"); const toolApplicationInput = document.getElementById("toolApplicationInput"); const toolTranscriptInput = document.getElementById("toolTranscriptInput"); const toolLawInput = document.getElementById("toolLawInput"); const toolApplicationOutput = document.getElementById("toolApplicationOutput"); const toolTranscriptOutput = document.getElementById("toolTranscriptOutput"); const toolLawOutput = document.getElementById("toolLawOutput"); const toolApplicationFiles = document.getElementById("toolApplicationFiles"); const toolTranscriptFiles = document.getElementById("toolTranscriptFiles"); const toolLawFiles = document.getElementById("toolLawFiles"); const previewModal = document.getElementById("previewModal"); const modalBackdrop = document.getElementById("modalBackdrop"); const modalClose = document.getElementById("modalClose"); const modalFilePreview = document.getElementById("modalFilePreview"); const modalResultText = document.getElementById("modalResultText"); const toast = document.getElementById("toast"); const arbitrationSteps = document.querySelectorAll(".stepper .step"); let modalTarget = null; function loadCases() { return callBackend("/api/cases", null, false, "GET") .then((result) => { state.cases = result.cases || []; }) .catch(() => { state.cases = []; }); } function saveCases() { return; } function getApiBase() { if (window.location.origin === "null") { return "http://127.0.0.1:8000"; } return window.location.origin; } function upsertCase(caseData) { const payload = { case_id: caseData.caseId, title: caseData.title, description: caseData.description || "", status: caseData.status || "草稿", stage: caseData.stage || "案件管理", }; return callBackend("/api/cases", payload, false, "POST") .then(() => loadCases()) .finally(() => { renderCases(); renderCaseSelect(); }); } function deleteCase(caseId) { const payload = { case_id: caseId }; return callBackend("/api/cases", payload, false, "DELETE") .then(() => loadCases()) .finally(() => { renderCases(); renderCaseSelect(); }); } function getCaseId(item) { return item?.caseId || item?.case_id || ""; } function updateCaseStatus(caseId, status, stage) { const target = state.cases.find((item) => getCaseId(item) === caseId); if (target) { target.status = status; target.stage = stage || target.stage; target.updatedAt = new Date().toLocaleString(); const payload = { case_id: getCaseId(target), title: target.title, description: target.description || "", status: target.status, stage: target.stage || "", }; callBackend("/api/cases", payload, false, "PUT") .then(() => loadCases()) .finally(() => { renderCases(); updateWorkflowStatus(target.stage); }); } } function renderCases() { casesTable.innerHTML = ""; state.cases.forEach((item) => { const row = document.createElement("tr"); const updated = item.updated_at ? new Date(item.updated_at).toLocaleString() : item.updatedAt || "-"; const caseId = getCaseId(item); row.innerHTML = ` ${caseId} ${item.title} ${item.status || "草稿"} ${updated} `; casesTable.appendChild(row); }); caseTimeline.innerHTML = ""; state.cases.forEach((item) => { const node = document.createElement("div"); node.className = "timeline-item"; const caseId = getCaseId(item); node.textContent = `${caseId} - ${item.stage || "未开始"} - ${item.status || "草稿"}`; caseTimeline.appendChild(node); }); } function renderCaseSelect() { arbitrationCaseSelect.innerHTML = ``; state.cases.forEach((item) => { const option = document.createElement("option"); const caseId = getCaseId(item); option.value = caseId; option.textContent = `${caseId} - ${item.title}`; arbitrationCaseSelect.appendChild(option); }); } function resetCaseForm() { caseIdInput.value = ""; caseTitleInput.value = ""; caseDescInput.value = ""; } caseForm.addEventListener("submit", async (event) => { event.preventDefault(); const caseId = caseIdInput.value.trim(); const title = caseTitleInput.value.trim(); const description = caseDescInput.value.trim(); if (!caseId || !title) { return; } const submitButton = caseForm.querySelector("button[type='submit']"); setLoading(submitButton, true); showToast("保存中"); try { await upsertCase({ caseId, title, description, status: "草稿", stage: "案件管理", updatedAt: new Date().toLocaleString(), }); showToast("保存成功"); } catch (error) { showToast("保存失败,请检查服务状态"); } finally { setLoading(submitButton, false); resetCaseForm(); } }); caseFormReset.addEventListener("click", () => { resetCaseForm(); }); function switchTab(tabName) { document.querySelectorAll(".nav-item").forEach((btn) => btn.classList.remove("active")); document.querySelectorAll(".tab").forEach((tab) => tab.classList.remove("active")); const targetButton = document.querySelector(`.nav-item[data-tab="${tabName}"]`); const targetTab = document.getElementById(`tab-${tabName}`); if (targetButton) { targetButton.classList.add("active"); } if (targetTab) { targetTab.classList.add("active"); } } async function loadArbitrationCase(caseId) { if (!caseId) { return; } updateArbitrationStatus("正在加载案件数据..."); try { const result = await callBackend(`/api/arbitration/case?case_id=${encodeURIComponent(caseId)}`, null, false, "GET"); const data = result?.case || {}; if (newCaseTitleInput) { newCaseTitleInput.value = data.title || ""; } if (applicationResult) { applicationResult.value = data.application_result || data.application_text || ""; } if (transcriptResult) { transcriptResult.value = data.transcript_result || data.transcript_text || ""; } renderFileList(applicationFiles, data.application_files || [], applicationResult); renderFileList(transcriptFiles, data.transcript_files || [], transcriptResult); renderEvidenceList(evidenceFiles, data.evidence_files || {}); if (lawResults) { lawResults.value = formatLawResults(data.law_results); } if (judgementReasoning) { judgementReasoning.value = data.final_judgement?.reasoning || ""; } if (disputeFindings) { disputeFindings.value = formatDisputeFindings(data.final_judgement?.dispute_point_findings); } if (finalDecision) { finalDecision.value = data.final_judgement?.final_decision || data.final_decision || ""; } updateArbitrationStatus("案件数据已载入"); showToast("已加载案件数据"); } catch (error) { updateArbitrationStatus(`案件数据加载失败:${error.message || "请检查服务状态"}`); showToast("案件数据加载失败"); } } casesTable.addEventListener("click", (event) => { const target = event.target; if (!(target instanceof HTMLButtonElement)) { return; } const caseId = target.dataset.id; if (!caseId) { return; } if (target.dataset.action === "edit") { switchTab("arbitration"); arbitrationCaseSelect.value = caseId; newCaseIdInput.value = ""; newCaseTitleInput.value = ""; loadArbitrationCase(caseId); } if (target.dataset.action === "delete") { deleteCase(caseId); } }); function getCurrentCaseId() { const selected = arbitrationCaseSelect.value; if (selected) { return selected; } const newCaseId = newCaseIdInput.value.trim(); if (newCaseId) { return newCaseId; } return ""; } function ensureCaseForArbitration() { let caseId = arbitrationCaseSelect.value; if (!caseId) { caseId = newCaseIdInput.value.trim(); const title = newCaseTitleInput.value.trim() || "未命名案件"; if (caseId) { upsertCase({ caseId, title, status: "草稿", stage: "材料提交", updatedAt: new Date().toLocaleString(), }); arbitrationCaseSelect.value = caseId; } } return caseId; } function updateArbitrationStatus(message) { arbitrationStatus.textContent = message || ""; } function updateStepper(activeIndex) { if (!arbitrationSteps || arbitrationSteps.length === 0) { return; } arbitrationSteps.forEach((step, index) => { step.classList.toggle("active", index === activeIndex); step.classList.toggle("done", index < activeIndex); }); } function showToast(message) { if (!toast) { return; } toast.textContent = message; toast.classList.add("show"); setTimeout(() => { toast.classList.remove("show"); }, 1800); } function setLoading(button, isLoading) { if (!button) { return; } const loadingText = button.getAttribute("data-loading-text") || "处理中..."; if (isLoading) { button.dataset.originalText = button.textContent || ""; button.textContent = loadingText; button.classList.add("loading"); button.disabled = true; } else { button.textContent = button.dataset.originalText || button.textContent; button.classList.remove("loading"); button.disabled = false; } } function openPreviewModal(file, targetTextarea) { if (!previewModal) { return; } modalTarget = targetTextarea || null; if (modalResultText) { modalResultText.value = modalTarget ? modalTarget.value : ""; } if (modalFilePreview) { modalFilePreview.src = file.url || ""; } previewModal.classList.add("open"); } function closePreviewModal() { if (!previewModal) { return; } previewModal.classList.remove("open"); if (modalFilePreview) { modalFilePreview.src = ""; } modalTarget = null; } if (modalClose) { modalClose.addEventListener("click", closePreviewModal); } if (modalBackdrop) { modalBackdrop.addEventListener("click", closePreviewModal); } if (modalResultText) { modalResultText.addEventListener("input", () => { if (modalTarget) { modalTarget.value = modalResultText.value; } }); } function renderFileList(container, files, targetTextarea) { if (!container) { return; } container.innerHTML = ""; if (!files || files.length === 0) { return; } files.forEach((item) => { const row = document.createElement("div"); row.className = "file-item"; const name = document.createElement("div"); name.textContent = item.name || "文件"; const actions = document.createElement("div"); const viewBtn = document.createElement("button"); viewBtn.textContent = "预览对照"; viewBtn.addEventListener("click", () => { openPreviewModal(item, targetTextarea); }); const link = document.createElement("a"); link.textContent = "下载"; link.href = item.url || "#"; link.target = "_blank"; actions.appendChild(viewBtn); actions.appendChild(link); row.appendChild(name); row.appendChild(actions); container.appendChild(row); }); } function renderEvidenceFileList(container, files) { if (!container) { return; } container.innerHTML = ""; if (!Array.isArray(files) || files.length === 0) { return; } files.forEach((item) => { const row = document.createElement("div"); row.className = "file-item"; const name = document.createElement("div"); name.textContent = item.name || "文件"; const actions = document.createElement("div"); const link = document.createElement("a"); link.textContent = "下载"; link.href = item.url || "#"; link.target = "_blank"; actions.appendChild(link); row.appendChild(name); row.appendChild(actions); container.appendChild(row); }); } function renderEvidenceList(container, evidenceMap) { if (!container) { return; } container.innerHTML = ""; if (!evidenceMap || typeof evidenceMap !== "object") { return; } Object.keys(evidenceMap).forEach((category) => { const group = document.createElement("div"); group.className = "file-group"; const title = document.createElement("div"); title.className = "file-group-title"; title.textContent = category; const list = document.createElement("div"); list.className = "file-group-list"; group.appendChild(title); group.appendChild(list); container.appendChild(group); const files = Array.isArray(evidenceMap[category]) ? evidenceMap[category] : []; renderEvidenceFileList(list, files); }); } function formatLawResults(lawResultsMap) { if (!lawResultsMap) { return ""; } let data = lawResultsMap; if (typeof data === "string") { try { data = JSON.parse(data); } catch (error) { return data; } } const lines = []; if (Array.isArray(data)) { data.forEach((item, index) => { const lawId = item?.law_id || "未知"; const content = item?.content || ""; lines.push(`${index + 1}. ${lawId} ${content}`); }); return lines.join("\n").trim(); } if (typeof data !== "object") { return ""; } Object.keys(data).forEach((point) => { lines.push(`争议焦点:${point}`); const items = Array.isArray(data[point]) ? data[point] : []; items.forEach((item, index) => { const lawId = item?.law_id || "未知"; const content = item?.content || ""; lines.push(`${index + 1}. ${lawId} ${content}`); }); lines.push(""); }); return lines.join("\n").trim(); } function formatDisputeFindings(findings) { if (!Array.isArray(findings)) { return ""; } return findings .map((item, index) => { const point = item?.dispute_point || "未命名争议焦点"; const finding = item?.finding || ""; const evidence = Array.isArray(item?.evidence_used) ? item.evidence_used.join("、") : ""; const laws = Array.isArray(item?.law_applied) ? item.law_applied.join("、") : ""; return `${index + 1}. ${point}\n结论:${finding}\n证据:${evidence}\n法律:${laws}`; }) .join("\n\n"); } async function callBackend(path, payload, isFormData, method = "POST") { const url = `${getApiBase()}${path}`; const options = { method, headers: {}, body: null, }; if (method === "GET") { options.body = null; } else if (isFormData) { options.body = payload; } else { options.headers["Content-Type"] = "application/json"; options.body = JSON.stringify(payload); } const response = await fetch(url, options); if (!response.ok) { let message = "后端请求失败"; try { const data = await response.json(); message = data.detail || data.message || message; } catch (error) { try { const text = await response.text(); if (text) { message = text; } } catch (readError) { message = "后端请求失败"; } } throw new Error(message); } return response.json(); } arbitrationForm.addEventListener("submit", async (event) => { event.preventDefault(); const caseId = ensureCaseForArbitration(); if (!caseId) { updateArbitrationStatus("请先选择或输入案件编号"); return; } const submitButton = arbitrationForm.querySelector("button[type='submit']"); setLoading(submitButton, true); updateCaseStatus(caseId, "草稿", "材料提交"); updateArbitrationStatus("材料提交中..."); updateStepper(0); const formData = new FormData(arbitrationForm); formData.append("case_id", caseId); formData.append("case_title", newCaseTitleInput.value.trim()); try { const result = await callBackend("/api/arbitration/submit", formData, true); if (result) { applicationResult.value = result.application_result || ""; transcriptResult.value = result.transcript_result || ""; renderFileList(applicationFiles, result.application_files || [], applicationResult); renderFileList(transcriptFiles, result.transcript_files || [], transcriptResult); renderEvidenceList(evidenceFiles, result.evidence_files || {}); } else { applicationResult.value = "申请书处理结果已生成,可编辑调整。"; transcriptResult.value = "庭审笔录处理结果已生成,可编辑调整。"; } if (!applicationResult.value && !transcriptResult.value) { updateArbitrationStatus("未识别到材料内容,请确认已上传申请书/庭审笔录"); showToast("未识别到材料内容"); } else { updateArbitrationStatus("材料已处理,可修改后确认"); showToast("材料处理完成"); } updateStepper(1); } catch (error) { applicationResult.value = "申请书处理结果已生成,可编辑调整。"; transcriptResult.value = "庭审笔录处理结果已生成,可编辑调整。"; updateArbitrationStatus(`材料处理失败:${error.message || "请检查服务状态"}`); showToast("材料处理失败"); } finally { setLoading(submitButton, false); } }); confirmMaterials.addEventListener("click", async () => { const caseId = getCurrentCaseId(); if (!caseId) { updateArbitrationStatus("请先提交材料"); return; } setLoading(confirmMaterials, true); updateCaseStatus(caseId, "草稿", "裁决中"); updateArbitrationStatus("正在生成裁决..."); updateStepper(1); const payload = { case_id: caseId, application_result: applicationResult.value.trim(), transcript_result: transcriptResult.value.trim(), }; try { const result = await callBackend("/api/arbitration/judgement", payload, false); if (result) { finalDecision.value = result.final_decision || ""; if (lawResults) { lawResults.value = formatLawResults(result.law_results); } if (judgementReasoning) { judgementReasoning.value = result.final_judgement?.reasoning || ""; } if (disputeFindings) { disputeFindings.value = formatDisputeFindings(result.final_judgement?.dispute_point_findings); } } else { finalDecision.value = "裁决结果已生成,可编辑调整。"; } updateArbitrationStatus("裁决已生成,可修改后确认"); updateStepper(2); showToast("裁决生成完成"); } catch (error) { finalDecision.value = "裁决结果已生成,可编辑调整。"; updateArbitrationStatus(`裁决生成失败:${error.message || "请检查服务状态"}`); showToast("裁决生成失败"); } finally { setLoading(confirmMaterials, false); } }); confirmDecision.addEventListener("click", async () => { const caseId = getCurrentCaseId(); if (!caseId) { updateArbitrationStatus("请先完成裁决"); return; } setLoading(confirmDecision, true); const payload = { case_id: caseId, final_decision: finalDecision.value.trim(), }; try { const result = await callBackend("/api/arbitration/confirm", payload, false); updateArbitrationStatus("裁决已入库"); updateStepper(2); showToast("裁决已入库"); } catch (error) { updateArbitrationStatus(`入库失败:${error.message || "请检查服务状态"}`); showToast("裁决入库失败"); } updateCaseStatus(caseId, "已完成", "裁决完成"); setLoading(confirmDecision, false); }); function runTool(input, output, path) { return async () => { if (!input.files || input.files.length === 0) { output.value = "请选择文件"; showToast("请选择文件"); return; } const button = input.closest(".card")?.querySelector("button"); setLoading(button, true); output.value = "处理中..."; showToast("开始处理"); const payload = new FormData(); Array.from(input.files).forEach((file) => payload.append("files", file)); try { const result = await callBackend(path, payload, true); output.value = JSON.stringify(result, null, 2); if (path.includes("/application")) { renderFileList(toolApplicationFiles, result.files || [], output); } if (path.includes("/transcript")) { renderFileList(toolTranscriptFiles, result.files || [], output); } showToast("处理完成"); } catch (error) { output.value = `处理失败:${error.message || "请检查服务状态"}`; showToast("处理失败"); } finally { setLoading(button, false); } }; } toolApplicationRun.addEventListener("click", runTool(toolApplicationInput, toolApplicationOutput, "/api/tools/application")); toolTranscriptRun.addEventListener("click", runTool(toolTranscriptInput, toolTranscriptOutput, "/api/tools/transcript")); toolLawRun.addEventListener("click", async () => { const query = toolLawInput?.value?.trim() || ""; if (!query) { toolLawOutput.value = "请输入法律问题"; showToast("请输入法律问题"); return; } setLoading(toolLawRun, true); toolLawOutput.value = "检索中..."; showToast("开始检索"); try { const result = await callBackend("/api/tools/law", { query }, false); toolLawOutput.value = JSON.stringify(result.result || [], null, 2); showToast("检索完成"); } catch (error) { toolLawOutput.value = `检索失败:${error.message || "请检查服务状态"}`; showToast("检索失败"); } finally { setLoading(toolLawRun, false); } }); document.querySelectorAll(".nav-item").forEach((item) => { item.addEventListener("click", () => { document.querySelectorAll(".nav-item").forEach((btn) => btn.classList.remove("active")); document.querySelectorAll(".tab").forEach((tab) => tab.classList.remove("active")); item.classList.add("active"); const target = item.dataset.tab; document.getElementById(`tab-${target}`).classList.add("active"); }); }); function updateWorkflowStatus(stage) { if (!workflowStatus) { return; } workflowStatus.textContent = stage || "待提交材料"; } function initApiStatus() { if (apiStatus) { apiStatus.textContent = getApiBase(); } } loadCases().finally(() => { renderCases(); renderCaseSelect(); initApiStatus(); updateWorkflowStatus(); updateStepper(0); });