Files
law-kb/app/static/js/app.js
T
freedak 641e33b834 feat: QYLAW 法律法规知识库
- 语义检索(FAISS + embedding)+ 精确查找(法规名+条号)
- RAG 问答(SSE 流式,支持 thinking 折叠显示)
- 法规浏览(原文阅读)
- 历史记录(检索+对话持久化到 SQLite)
- 设置页(系统提示词/模板/LLM 参数可配置)
- 检索质量评估脚本

Generated with [Devin](https://devin.ai)

Co-Authored-By: Devin <158243242+devin-ai-integration[bot]@users.noreply.github.com>
2026-08-07 14:55:25 +08:00

265 lines
10 KiB
JavaScript

/**
* 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 `<div class="spinner" style="width:${size}px;height:${size}px"></div>`;
},
/** 空状态 */
emptyState(title, desc, icon = '📋') {
return `
<div class="flex flex-col items-center justify-center py-16 text-gray-400">
<div class="text-4xl mb-3">${icon}</div>
<div class="text-sm font-medium text-gray-600">${title}</div>
<div class="text-xs mt-1">${desc}</div>
</div>`;
},
/** 错误状态 */
errorState(msg, onRetry) {
const retryBtn = onRetry ? `<button onclick="${onRetry}" class="mt-3 px-3 py-1.5 text-xs bg-rose-50 text-rose-600 rounded-lg hover:bg-rose-100">重试</button>` : '';
return `
<div class="flex flex-col items-center justify-center py-16 text-rose-500">
<div class="text-3xl mb-2">⚠️</div>
<div class="text-sm font-medium">${msg}</div>
${retryBtn}
</div>`;
},
/** 类别标签 */
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 `<span class="inline-block px-2 py-0.5 text-xs rounded ${cls}">${category}</span>`;
},
/** 分页 */
pagination(page, pageSize, total, onChange) {
const totalPages = Math.ceil(total / pageSize);
if (totalPages <= 1) return '';
const prev = page > 1 ? `<button onclick="${onChange}(${page-1})" class="px-3 py-1.5 text-sm rounded-lg border border-gray-200 hover:bg-gray-50">上一页</button>` : '';
const next = page < totalPages ? `<button onclick="${onChange}(${page+1})" class="px-3 py-1.5 text-sm rounded-lg border border-gray-200 hover:bg-gray-50">下一页</button>` : '';
return `
<div class="flex items-center justify-between mt-4">
<div class="text-xs text-gray-500">共 ${total} 条,第 ${page}/${totalPages} 页</div>
<div class="flex gap-2">${prev}${next}</div>
</div>`;
},
/** 条文卡片 */
clauseCard(item, highlight = false) {
const badge = UI.categoryBadge(item.category);
const province = item.province ? `<span class="text-xs text-gray-400">[${item.province}${item.city ? ' · ' + item.city : ''}]</span>` : '';
const chapter = item.chapter ? `<span class="text-xs text-gray-400">${item.chapter}</span>` : '';
const clauseNo = item.clause_no ? `<span class="text-sm font-semibold text-gray-700">${item.clause_no}</span>` : '';
const score = item.score !== undefined ? `<span class="text-xs mono text-gray-400">相似度 ${item.score.toFixed(4)}</span>` : '';
const cls = highlight ? 'clause-highlight' : 'border border-gray-200 bg-white';
return `
<div class="${cls} rounded-lg p-3 mb-2">
<div class="flex items-center gap-2 mb-1.5 flex-wrap">
<span class="text-sm font-medium text-gray-900">${item.law_name}</span>
${badge}
${clauseNo}
${chapter}
${province}
${score}
</div>
<div class="text-sm text-gray-600 leading-relaxed">${item.content.substring(0, 200)}${item.content.length > 200 ? '...' : ''}</div>
${item.law_id ? `<button onclick="UI.viewLaw(${item.law_id})" class="mt-2 text-xs text-gray-500 hover:text-gray-900 underline">查看原文</button>` : ''}
</div>`;
},
/** 跳转查看原文 */
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 = '服务不可用';
}
}