/** * app.js — 公共逻辑:路由、公共组件、API 封装 */ // ===== 全局常量(供 rag.js / settings.js 共享) ===== const PROMPT_DEFAULTS = { system_prompt: '你是法律助手,根据提供的法规条文回答问题,必须引用法规名和条号。', template_simple: '你是一位耐心的法律科普助手。请根据以下法规条文,用通俗易懂的语言回答用户问题。\n要求:\n1. 避免法律术语,用日常语言解释\n2. 必须引用依据的法规名和条号,格式如"根据《XX法》第X条"\n3. 如果条文不足以回答,明确说明"根据现有法规无法完全回答"\n4. 不要编造法规\n\n【相关法规条文】\n{context}\n\n【用户问题】\n{question}', template_professional: '你是一位专业的法律分析助手。请根据以下法规条文,对用户问题进行专业分析。\n要求:\n1. 使用规范法律术语\n2. 必须引用依据的法规名和条号,格式如"依据《XX法》第X条"\n3. 分析条文的适用条件、法律后果\n4. 如有多条相关,对比分析\n5. 指出条文的适用边界和可能的争议点\n6. 不要编造法规\n\n【相关法规条文】\n{context}\n\n【用户问题】\n{question}', template_compare: '你是一位法规研究助手。请根据以下法规条文,对比分析不同法规对同一问题的规定。\n要求:\n1. 列出每条相关法规的具体规定\n2. 对比规定的异同\n3. 标注法规名、条号、发布日期\n4. 指出适用范围差异(如全国性 vs 地方性)\n5. 不要编造法规\n\n【相关法规条文】\n{context}\n\n【用户问题】\n{question}', }; const TEMPLATES = [ { value: 'simple', label: '通俗解释' }, { value: 'professional', label: '专业分析' }, { value: 'compare', label: '对比条文' }, ]; // ===== 路由 ===== const PAGES = ['rag', 'search', 'browse', 'history', 'settings']; let currentPage = 'rag'; /** 渲染指定页面(不修改 URL) */ function renderPage(page) { if (!PAGES.includes(page)) page = 'search'; currentPage = page; // 更新导航激活态 document.querySelectorAll('.nav-item').forEach(btn => { if (btn.dataset.page === page) { btn.classList.remove('nav-inactive'); btn.classList.add('nav-active'); } else { btn.classList.remove('nav-active'); btn.classList.add('nav-inactive'); } }); // 渲染页面 const content = document.getElementById('content'); if (page === 'search') content.innerHTML = SearchPage.render(); if (page === 'rag') content.innerHTML = RagPage.render(); if (page === 'browse') content.innerHTML = BrowsePage.render(); if (page === 'history') content.innerHTML = HistoryPage.render(); if (page === 'settings') content.innerHTML = SettingsPage.render(); // 页面初始化 if (page === 'search') SearchPage.init(); if (page === 'rag') RagPage.init(); if (page === 'browse') BrowsePage.init(); if (page === 'history') HistoryPage.init(); if (page === 'settings') SettingsPage.init(); // 关闭移动端菜单 document.getElementById('sidebar').classList.remove('open'); } /** 导航到指定页面(修改 URL + 渲染) */ function navigate(page) { if (!PAGES.includes(page)) return; if (page === currentPage) return; // 更新 URL(不触发 popstate) history.pushState({ page }, '', `/${page}`); renderPage(page); } /** 从 URL 路径解析当前页面 */ function pageFromPath() { const path = window.location.pathname.replace(/^\//, '').replace(/\/$/, ''); return PAGES.includes(path) ? path : 'rag'; } // 监听浏览器前进/后退 window.addEventListener('popstate', (e) => { const page = (e.state && e.state.page) || pageFromPath(); renderPage(page); }); // ===== 公共组件 ===== const UI = { /** 加载中 spinner */ spinner(size = 16) { return `
`; }, /** 空状态 */ emptyState(title, desc, icon = '📋') { return `
${icon}
${title}
${desc}
`; }, /** 错误状态 */ errorState(msg, onRetry) { const retryBtn = onRetry ? `` : ''; return `
⚠️
${msg}
${retryBtn}
`; }, /** 类别标签 */ categoryBadge(category) { const colors = { '法律': 'bg-blue-50 text-blue-700', '行政法规': 'bg-purple-50 text-purple-700', '监察法规': 'bg-amber-50 text-amber-700', '司法解释': 'bg-teal-50 text-teal-700', '地方性法规': 'bg-emerald-50 text-emerald-700', }; const cls = colors[category] || 'bg-gray-100 text-gray-600'; return `${category}`; }, /** 分页 */ pagination(page, pageSize, total, onChange) { const totalPages = Math.ceil(total / pageSize); if (totalPages <= 1) return ''; const prev = page > 1 ? `` : ''; const next = page < totalPages ? `` : ''; return `
共 ${total} 条,第 ${page}/${totalPages} 页
${prev}${next}
`; }, /** 条文卡片 */ clauseCard(item, highlight = false) { const badge = UI.categoryBadge(item.category); const province = item.province ? `[${item.province}${item.city ? ' · ' + item.city : ''}]` : ''; const chapter = item.chapter ? `${item.chapter}` : ''; const clauseNo = item.clause_no ? `${item.clause_no}` : ''; const score = item.score !== undefined ? `相似度 ${item.score.toFixed(4)}` : ''; const cls = highlight ? 'clause-highlight' : 'border border-gray-200 bg-white'; return `
${item.law_name} ${badge} ${clauseNo} ${chapter} ${province} ${score}
${item.content.substring(0, 200)}${item.content.length > 200 ? '...' : ''}
${item.law_id ? `` : ''}
`; }, /** 跳转查看原文 */ viewLaw(lawId) { navigate('browse'); setTimeout(() => BrowsePage.viewLaw(lawId), 100); }, }; // ===== API 封装 ===== const API = { async get(url) { const resp = await fetch(url); if (!resp.ok) { const err = await resp.json().catch(() => ({ detail: resp.statusText })); throw new Error(err.detail || `HTTP ${resp.status}`); } return resp.json(); }, async post(url, body) { const resp = await fetch(url, { method: 'POST', headers: { 'Content-Type': 'application/json' }, body: JSON.stringify(body), }); if (!resp.ok) { const err = await resp.json().catch(() => ({ detail: resp.statusText })); throw new Error(err.detail || `HTTP ${resp.status}`); } return resp; }, async put(url, body) { const resp = await fetch(url, { method: 'PUT', headers: { 'Content-Type': 'application/json' }, body: JSON.stringify(body), }); if (!resp.ok) { const err = await resp.json().catch(() => ({ detail: resp.statusText })); throw new Error(err.detail || `HTTP ${resp.status}`); } return resp.json(); }, async delete(url) { const resp = await fetch(url, { method: 'DELETE' }); if (!resp.ok) { const err = await resp.json().catch(() => ({ detail: resp.statusText })); throw new Error(err.detail || `HTTP ${resp.status}`); } return resp.json(); }, /** 检索 */ search(params) { const qs = new URLSearchParams(params).toString(); return API.get(`/api/search?${qs}`); }, /** 法规列表 */ listLaws(params) { const qs = new URLSearchParams(params).toString(); return API.get(`/api/laws?${qs}`); }, /** 法规详情 */ getLaw(lawId) { return API.get(`/api/law/${lawId}`); }, /** 统计 */ stats() { return API.get('/api/stats'); }, }; // ===== 初始化 ===== document.addEventListener('DOMContentLoaded', () => { // 导航绑定 document.querySelectorAll('.nav-item').forEach(btn => { btn.addEventListener('click', () => navigate(btn.dataset.page)); }); // 移动端菜单 document.getElementById('mobile-menu-btn').addEventListener('click', () => { document.getElementById('sidebar').classList.toggle('open'); }); // 加载统计 loadStats(); // 从 URL 决定初始页 const initialPage = pageFromPath(); // 替换当前历史记录,确保有 state history.replaceState({ page: initialPage }, '', `/${initialPage === 'rag' ? '' : initialPage}`); renderPage(initialPage); }); async function loadStats() { try { const resp = await API.stats(); const s = resp.data; if (s && s.ready) { document.getElementById('status-dot').className = 'w-2 h-2 rounded-full bg-emerald-500'; document.getElementById('status-text').textContent = '就绪'; document.getElementById('stat-clauses').textContent = s.total_clauses.toLocaleString(); document.getElementById('stat-laws').textContent = s.total_laws.toLocaleString(); } else { document.getElementById('status-dot').className = 'w-2 h-2 rounded-full bg-amber-400'; document.getElementById('status-text').textContent = '索引未就绪'; } } catch (e) { document.getElementById('status-dot').className = 'w-2 h-2 rounded-full bg-rose-500'; document.getElementById('status-text').textContent = '服务不可用'; } }