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);
});