641e33b834
- 语义检索(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>
169 lines
7.6 KiB
JavaScript
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}`;
|
|
},
|
|
};
|