/** * RAG-cut Demo Frontend */ const API_BASE = (() => { const meta = document.querySelector('meta[name="api-base"]')?.content?.trim(); if (meta) return meta.replace(/\/$/, ""); if (window.location.port && window.location.port !== "8000") { return "http://127.0.0.1:8000"; } return ""; })(); function apiUrl(path) { return `${API_BASE}${path}`; } const $ = (sel) => document.querySelector(sel); const $$ = (sel) => document.querySelectorAll(sel); const state = { file: null, result: null, selectedIndex: 0, objectUrl: null, history: [], activeHistoryId: null, }; const els = { dropzone: $("#dropzone"), fileInput: $("#fileInput"), fileName: $("#fileName"), configForm: $("#configForm"), submitBtn: $("#submitBtn"), splitMode: $("#splitMode"), modeHint: $("#modeHint"), modeOptionsDefault: $("#modeOptionsDefault"), modeOptionsDelimiter: $("#modeOptionsDelimiter"), modeOptionsParentChild: $("#modeOptionsParentChild"), modeOptionsByRow: $("#modeOptionsByRow"), maxChunkSize: $("#maxChunkSize"), overlap: $("#overlap"), delimiter: $("#delimiter"), delimiterMaxSize: $("#delimiterMaxSize"), delimiterOverlap: $("#delimiterOverlap"), parentDelimiter: $("#parentDelimiter"), childDelimiter: $("#childDelimiter"), parentMaxSize: $("#parentMaxSize"), childMaxSize: $("#childMaxSize"), parentChildOverlap: $("#parentChildOverlap"), headerRowStart: $("#headerRowStart"), headerRowEnd: $("#headerRowEnd"), startRow: $("#startRow"), rowsPerChunk: $("#rowsPerChunk"), autoPolicy: $("#autoPolicy"), policyKicker: $("#policyKicker"), policyTitle: $("#policyTitle"), autoPolicyGrid: $("#autoPolicyGrid"), statusBadge: $("#statusBadge"), emptyState: $("#emptyState"), dualPane: $("#workspacePane"), previewPanel: $("#previewPanel"), apiPanel: $("#apiPanel"), recallPanel: $("#recallPanel"), recallForm: $("#recallForm"), recallQuery: $("#recallQuery"), recallTopK: $("#recallTopK"), recallSubmit: $("#recallSubmit"), recallHint: $("#recallHint"), recallResults: $("#recallResults"), statsSection: $("#statsSection"), historyList: $("#historyList"), historyHint: $("#historyHint"), historyRefreshBtn: $("#historyRefreshBtn"), originalPreview: $("#originalPreview"), chunkList: $("#chunkList"), chunkPreview: $("#chunkPreview"), originalTag: $("#originalTag"), chunkCountTag: $("#chunkCountTag"), selectedChunkTag: $("#selectedChunkTag"), statBlocks: $("#statBlocks"), statChunks: $("#statChunks"), statMode: $("#statMode"), statDocId: $("#statDocId"), apiRequest: $("#apiRequest"), apiResponse: $("#apiResponse"), }; function setStatus(type, text) { els.statusBadge.innerHTML = `${text}`; } function setFile(file) { if (state.objectUrl) { URL.revokeObjectURL(state.objectUrl); state.objectUrl = null; } state.file = file; state.result = null; state.activeHistoryId = null; els.fileName.textContent = file ? file.name : ""; els.submitBtn.disabled = !file; els.submitBtn.title = file ? "开始切分已选文件" : "请先上传文档"; highlightHistoryActive(); if (file) setStatus("idle", "已选择文件"); else setStatus("idle", "等待上传"); } function formatSavedAt(iso) { if (!iso) return ""; try { const d = new Date(iso); if (Number.isNaN(d.getTime())) return ""; return d.toLocaleString("zh-CN", { hour12: false }); } catch { return ""; } } function highlightHistoryActive() { if (!els.historyList) return; els.historyList.querySelectorAll(".history-item").forEach((row) => { const active = row.dataset.docId === state.activeHistoryId; row.classList.toggle("active", active); row.setAttribute("aria-selected", active ? "true" : "false"); }); } function clearWorkspace() { state.result = null; state.selectedIndex = 0; state.activeHistoryId = null; if (state.objectUrl) { URL.revokeObjectURL(state.objectUrl); state.objectUrl = null; } els.emptyState?.classList.remove("hidden"); els.dualPane?.classList.add("hidden"); els.apiPanel?.classList.add("hidden"); els.recallPanel?.classList.add("hidden"); els.statsSection?.classList.add("hidden"); if (els.originalPreview) els.originalPreview.innerHTML = ""; if (els.chunkList) els.chunkList.innerHTML = ""; if (els.chunkPreview) els.chunkPreview.innerHTML = ""; if (els.apiRequest) els.apiRequest.textContent = ""; if (els.apiResponse) els.apiResponse.textContent = ""; if (els.recallResults) els.recallResults.innerHTML = ""; highlightHistoryActive(); setStatus("idle", "等待上传"); } function renderHistoryList(items) { state.history = items || []; if (!els.historyList) return; if (!state.history.length) { els.historyHint.textContent = "暂无历史记录,上传并切分后会出现在这里"; els.historyList.innerHTML = ""; return; } els.historyHint.textContent = `共 ${state.history.length} 条 · 点击加载`; els.historyList.innerHTML = state.history.map((item) => { const when = formatSavedAt(item.saved_at); const orig = item.has_original ? "" : " · 无原文"; const active = item.doc_id === state.activeHistoryId; return `
历史记录中未找到原文文件(可能已被清理)。仍可查看右侧切片结果。
'; return; } els.originalPreview.innerHTML = '原文加载中…
'; try { const res = await fetch(apiUrl(`/api/results/${docId}/original`)); if (!res.ok) { const data = await res.json().catch(() => ({})); throw new Error(data.detail || `HTTP ${res.status}`); } const blob = await res.blob(); const name = filename || "document"; const file = new File([blob], name, { type: blob.type || "application/octet-stream" }); state.file = file; els.fileName.textContent = name; els.submitBtn.disabled = false; await renderOriginalPreview(file, docId); } catch (err) { els.originalTag.textContent = filename || docId; els.originalPreview.innerHTML = `原文预览失败:${escapeHtml(err.message || String(err))}。仍可查看切片结果。
`; } } async function loadHistoryResult(docId) { if (!docId) return; setStatus("loading", "加载历史…"); els.apiRequest.textContent = JSON.stringify( { method: "GET", url: apiUrl(`/api/results/${docId}`) }, null, 2, ); try { const res = await fetch(apiUrl(`/api/results/${docId}`)); const data = await res.json(); if (!res.ok) throw new Error(data.detail || `HTTP ${res.status}`); state.activeHistoryId = docId; highlightHistoryActive(); els.apiResponse.textContent = JSON.stringify( { doc_id: data.doc_id, filename: data.filename, chunk_count: data.chunk_count, split_mode: data.split_mode }, null, 2, ); showResult(data); setStatus("success", `已加载历史 · ${data.chunk_count} 片`); await renderOriginalFromHistory(docId, data.filename, data.has_original); } catch (err) { setStatus("error", "加载失败"); els.apiResponse.textContent = JSON.stringify({ error: err.message || String(err) }, null, 2); alert(`加载历史失败:${err.message || err}`); } } function buildFormData() { const fd = new FormData(); fd.append("file", state.file); const fields = collectSplitFields(); Object.entries(fields).forEach(([key, value]) => { if (value != null && value !== "") fd.append(key, String(value)); }); return fd; } function unescapeDelimiter(raw) { return String(raw || "") .replace(/\\n/g, "\n") .replace(/\\t/g, "\t") .replace(/\\r/g, "\r"); } function collectSplitFields() { const mode = els.splitMode?.value || "default"; if (mode === "default") { return { mode: "default", max_chunk_size: Number(els.maxChunkSize?.value || 2200), overlap: Number(els.overlap?.value || 0), }; } if (mode === "delimiter") { const delimiter = unescapeDelimiter(els.delimiter?.value?.trim()); return { mode: "delimiter", delimiter: delimiter || "###", max_chunk_size: Number(els.delimiterMaxSize?.value || 1500), overlap: Number(els.delimiterOverlap?.value || 0), }; } if (mode === "parent_child") { const parentDelimiter = unescapeDelimiter(els.parentDelimiter?.value?.trim()); const childDelimiter = unescapeDelimiter(els.childDelimiter?.value?.trim()); return { mode: "parent_child", parent_delimiter: parentDelimiter || "##", child_delimiter: childDelimiter || "", max_chunk_size: Number(els.parentMaxSize?.value || 2000), child_max_size: Number(els.childMaxSize?.value || 512), overlap: Number(els.parentChildOverlap?.value || 0), }; } if (mode === "by_row") { return { mode: "by_row", header_row_start: Number(els.headerRowStart?.value || 1), header_row_end: Number(els.headerRowEnd?.value || 1), start_row: Number(els.startRow?.value || 2), rows_per_chunk: Number(els.rowsPerChunk?.value || 10), max_chunk_size: 2400, overlap: 0, }; } return {}; } function buildRequestPreview() { const fields = { file: state.file?.name, ...collectSplitFields(), }; return { method: "POST", url: apiUrl("/api/chunk"), contentType: "multipart/form-data", fields, }; } const MODE_HINTS = { default: "按标题/结构切分,适合 PDF、Word、PPT 等", delimiter: "按自定义标识符切开,适合 Markdown/带固定分隔符的文本", parent_child: "先按父标识符切父片,再按子标识符切子片;检索用子片、父片作上下文", by_row: "按表格行切分,适合 xlsx / xls / csv", }; function syncModeOptions() { const mode = els.splitMode?.value || "default"; els.modeOptionsDefault?.classList.toggle("hidden", mode !== "default"); els.modeOptionsDelimiter?.classList.toggle("hidden", mode !== "delimiter"); els.modeOptionsParentChild?.classList.toggle("hidden", mode !== "parent_child"); els.modeOptionsByRow?.classList.toggle("hidden", mode !== "by_row"); if (els.modeHint) els.modeHint.textContent = MODE_HINTS[mode] || MODE_HINTS.default; // CSS-hidden fields still join HTML5 constraint validation and can silently // block