Files
law-kb/app/static/js/search.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

169 lines
7.6 KiB
JavaScript

/**
* search.js — 语义检索页
*/
const SearchPage = {
state: { query: '', mode: 'semantic', category: '', province: '', city: '', results: [], total: 0, page: 1, pageSize: 20, loading: false, error: null },
render() {
const isKeyword = this.state.mode === 'keyword';
return `
<div class="p-4 md:p-6 max-w-5xl mx-auto">
<h2 class="text-lg font-semibold mb-4">法规检索</h2>
<!-- 筛选栏 -->
<div class="bg-white rounded-xl border border-gray-200 p-4 mb-4">
<!-- 模式切换 -->
<div class="flex gap-2 mb-3">
<button onclick="SearchPage.setMode('semantic')"
class="px-3 py-1.5 text-xs rounded-lg ${!isKeyword ? 'bg-gray-900 text-white' : 'bg-gray-100 text-gray-600 hover:bg-gray-200'}">
语义检索
</button>
<button onclick="SearchPage.setMode('keyword')"
class="px-3 py-1.5 text-xs rounded-lg ${isKeyword ? 'bg-gray-900 text-white' : 'bg-gray-100 text-gray-600 hover:bg-gray-200'}">
精确查找
</button>
<span class="text-xs text-gray-400 self-center ml-1">
${isKeyword ? '按法规名+条号精确匹配,如"郑州市劳动用工条例 第三十二条"' : '自然语言语义搜索,如"加班工资计算基数"'}
</span>
</div>
<div class="flex gap-3 mb-3">
<input id="search-input" type="text"
placeholder="${isKeyword ? '输入法规名和条号,如:郑州市劳动用工条例 第三十二条' : '输入查询,如:个人信息保护、竞业协议补偿金...'}"
class="flex-1 rounded-lg border border-gray-200 px-3 py-2 text-sm focus:outline-none focus:ring-2 focus:ring-gray-900"
value="${this.state.query}" maxlength="1000"
onkeydown="if(event.key==='Enter')SearchPage.doSearch()">
<button onclick="SearchPage.doSearch()" class="px-4 py-2 bg-gray-900 text-white text-sm rounded-lg hover:bg-gray-800 flex items-center gap-2">
<svg width="16" height="16" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2"><circle cx="11" cy="11" r="8"/><path d="m21 21-4.3-4.3"/></svg>
搜索
</button>
</div>
<div class="flex gap-3 flex-wrap text-sm">
<select id="filter-category" class="rounded-lg border border-gray-200 px-3 py-1.5 text-sm" onchange="SearchPage.onFilterChange()">
<option value="">全部类别</option>
<option value="法律">法律</option>
<option value="行政法规">行政法规</option>
<option value="监察法规">监察法规</option>
<option value="司法解释">司法解释</option>
<option value="地方性法规">地方性法规</option>
</select>
<select id="filter-province" class="rounded-lg border border-gray-200 px-3 py-1.5 text-sm" onchange="SearchPage.onFilterChange()">
<option value="">全部省份</option>
</select>
<span class="text-xs text-gray-400 self-center">${this.state.query.length}/1000</span>
</div>
</div>
<!-- 结果区 -->
<div id="search-results">
${UI.emptyState('输入查询开始检索', isKeyword ? '按法规名+条号精确匹配' : '支持自然语言查询,如"个人信息跨境传输的规定"')}
</div>
</div>`;
},
setMode(mode) {
this.state.mode = mode;
this.state.results = [];
this.state.total = 0;
const content = document.getElementById('content');
content.innerHTML = this.render();
this.init();
},
init() {
// 加载省份列表(从统计或固定列表)
this.loadProvinces();
// 恢复筛选状态
if (this.state.category) document.getElementById('filter-category').value = this.state.category;
if (this.state.province) document.getElementById('filter-province').value = this.state.province;
},
provinces: ['北京市','天津市','河北省','山西省','内蒙古自治区','辽宁省','吉林省','黑龙江省','上海市','江苏省','浙江省','安徽省','福建省','江西省','山东省','河南省','湖北省','湖南省','广东省','广西壮族自治区','海南省','重庆市','四川省','贵州省','云南省','西藏自治区','陕西省','甘肃省','青海省','宁夏回族自治区','新疆维吾尔自治区'],
loadProvinces() {
const sel = document.getElementById('filter-province');
if (!sel) return;
this.provinces.forEach(p => {
const opt = document.createElement('option');
opt.value = p; opt.textContent = p;
sel.appendChild(opt);
});
},
onFilterChange() {
this.state.category = document.getElementById('filter-category').value;
this.state.province = document.getElementById('filter-province').value;
if (this.state.query) this.doSearch();
},
async doSearch(page) {
const input = document.getElementById('search-input');
if (input) this.state.query = input.value.trim();
if (!this.state.query) return;
// 自动判定模式:包含"第X条"且看起来像法规名+条号时,自动用精确查找
const hasClauseNo = /第[一二三四五六七八九十百千零\d]+条/.test(this.state.query);
const autoMode = hasClauseNo ? 'keyword' : this.state.mode;
this.state.page = page || 1;
this.state.loading = true;
this.state.error = null;
this.renderResults();
try {
const params = {
query: this.state.query,
mode: autoMode,
top_k: 20,
page: this.state.page,
page_size: this.state.pageSize,
};
if (this.state.category) params.category = this.state.category;
if (this.state.province) params.province = this.state.province;
const resp = await API.search(params);
this.state.results = resp.data.results || [];
this.state.total = resp.data.total || 0;
this.state.loading = false;
this.renderResults();
} catch (e) {
this.state.loading = false;
this.state.error = e.message;
this.renderResults();
}
},
renderResults() {
const el = document.getElementById('search-results');
if (!el) return;
if (this.state.loading) {
el.innerHTML = `<div class="flex items-center justify-center py-16 gap-3 text-gray-400">${UI.spinner(20)}<span class="text-sm">检索中...</span></div>`;
return;
}
if (this.state.error) {
el.innerHTML = UI.errorState(this.state.error, 'SearchPage.doSearch()');
return;
}
if (!this.state.results.length && this.state.query) {
el.innerHTML = UI.emptyState('无检索结果', '试试调整查询词或筛选条件');
return;
}
if (!this.state.results.length) {
el.innerHTML = UI.emptyState('输入查询开始检索', '支持自然语言查询');
return;
}
const cards = this.state.results.map(r => UI.clauseCard(r)).join('');
const pager = UI.pagination(this.state.page, this.state.pageSize, this.state.total, 'SearchPage.doSearch');
// 显示当前检索模式
const hasClauseNo = /第[一二三四五六七八九十百千零\d]+条/.test(this.state.query);
const autoMode = hasClauseNo ? 'keyword' : this.state.mode;
const modeLabel = autoMode === 'keyword' ? '精确查找' : '语义检索';
const autoHint = hasClauseNo && this.state.mode === 'semantic' ? ' <span class="text-xs text-amber-500">(检测到条号,自动切换精确查找)</span>' : '';
el.innerHTML = `<div class="mb-3 text-sm text-gray-500">找到 ${this.state.total} 条结果 <span class="text-xs text-gray-400">[${modeLabel}]</span>${autoHint}</div>${cards}${pager}`;
},
};