/**
* 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 `
`;
},
/** 错误状态 */
errorState(msg, onRetry) {
const retryBtn = onRetry ? `` : '';
return `
`;
},
/** 类别标签 */
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 = '服务不可用';
}
}