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>
This commit is contained in:
@@ -0,0 +1,207 @@
|
||||
/**
|
||||
* rag.js — 智能问答页(RAG + SSE 流式)
|
||||
*/
|
||||
|
||||
const RagPage = {
|
||||
state: {
|
||||
question: '', template: 'simple', answer: '', citations: [], streaming: false, error: null,
|
||||
thinking: '', // thinking 内容
|
||||
thinkingEnabled: true, // thinking 开关(从 API 加载)
|
||||
},
|
||||
|
||||
render() {
|
||||
const templates = [
|
||||
{ value: 'simple', label: '通俗解释' },
|
||||
{ value: 'professional', label: '专业分析' },
|
||||
{ value: 'compare', label: '对比条文' },
|
||||
];
|
||||
return `
|
||||
<div class="p-4 md:p-6 max-w-4xl 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">
|
||||
<textarea id="rag-input" rows="3" placeholder="输入你的问题,如:个人信息保护法对敏感个人信息有什么规定?"
|
||||
class="w-full rounded-lg border border-gray-200 px-3 py-2 text-sm focus:outline-none focus:ring-2 focus:ring-gray-900 resize-none"
|
||||
maxlength="1000" oninput="RagPage.onInput()">${this.state.question}</textarea>
|
||||
<div class="flex items-center justify-between mt-3">
|
||||
<div class="flex gap-2">
|
||||
${templates.map(t => `
|
||||
<button onclick="RagPage.setTemplate('${t.value}')"
|
||||
class="px-3 py-1.5 text-xs rounded-lg ${this.state.template === t.value ? 'bg-gray-900 text-white' : 'bg-gray-100 text-gray-600 hover:bg-gray-200'}">
|
||||
${t.label}
|
||||
</button>`).join('')}
|
||||
</div>
|
||||
<div class="flex items-center gap-3">
|
||||
<span class="text-xs text-gray-400">${this.state.question.length}/1000</span>
|
||||
<button id="rag-submit" onclick="RagPage.submit()" class="px-4 py-2 bg-gray-900 text-white text-sm rounded-lg hover:bg-gray-800 disabled:opacity-50">
|
||||
提问
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- 答案区 -->
|
||||
<div id="rag-answer">
|
||||
${UI.emptyState('提问开始对话', '系统将检索相关法规条文并由 AI 生成带引用的答案')}
|
||||
</div>
|
||||
</div>`;
|
||||
},
|
||||
|
||||
init() {
|
||||
// 加载 LLM 参数(用于 thinking 状态显示)
|
||||
this.loadLLMParams();
|
||||
},
|
||||
|
||||
async loadLLMParams() {
|
||||
try {
|
||||
const resp = await API.get('/api/prompts');
|
||||
this.state.thinkingEnabled = resp.data.llm_params.thinking_enabled;
|
||||
} catch (e) {
|
||||
console.error('加载 LLM 参数失败:', e);
|
||||
}
|
||||
},
|
||||
|
||||
onInput() {
|
||||
const input = document.getElementById('rag-input');
|
||||
if (input) this.state.question = input.value;
|
||||
},
|
||||
|
||||
setTemplate(t) {
|
||||
this.state.template = t;
|
||||
const content = document.getElementById('content');
|
||||
content.innerHTML = this.render();
|
||||
},
|
||||
|
||||
async submit() {
|
||||
const input = document.getElementById('rag-input');
|
||||
if (input) this.state.question = input.value.trim();
|
||||
if (!this.state.question || this.state.streaming) return;
|
||||
|
||||
this.state.answer = '';
|
||||
this.state.thinking = '';
|
||||
this.state.citations = [];
|
||||
this.state.streaming = true;
|
||||
this.state.error = null;
|
||||
this.renderAnswer();
|
||||
|
||||
try {
|
||||
const resp = await fetch('/api/rag', {
|
||||
method: 'POST',
|
||||
headers: { 'Content-Type': 'application/json' },
|
||||
body: JSON.stringify({
|
||||
question: this.state.question,
|
||||
template: this.state.template,
|
||||
}),
|
||||
});
|
||||
|
||||
if (!resp.ok) {
|
||||
const err = await resp.json().catch(() => ({ detail: resp.statusText }));
|
||||
throw new Error(err.detail || `HTTP ${resp.status}`);
|
||||
}
|
||||
|
||||
// SSE 流式读取
|
||||
const reader = resp.body.getReader();
|
||||
const decoder = new TextDecoder();
|
||||
let buffer = '';
|
||||
|
||||
while (true) {
|
||||
const { done, value } = await reader.read();
|
||||
if (done) break;
|
||||
buffer += decoder.decode(value, { stream: true });
|
||||
const lines = buffer.split('\n');
|
||||
buffer = lines.pop(); // 保留不完整的行
|
||||
|
||||
for (const line of lines) {
|
||||
if (!line.startsWith('data: ')) continue;
|
||||
try {
|
||||
const evt = JSON.parse(line.slice(6));
|
||||
if (evt.type === 'answer') {
|
||||
this.state.answer += evt.content;
|
||||
this.renderAnswer();
|
||||
} else if (evt.type === 'thinking') {
|
||||
this.state.thinking += evt.content;
|
||||
this.renderAnswer();
|
||||
} else if (evt.type === 'params') {
|
||||
this.state.thinkingEnabled = evt.thinking_enabled;
|
||||
} else if (evt.type === 'citations') {
|
||||
this.state.citations = evt.clauses || [];
|
||||
this.renderAnswer();
|
||||
} else if (evt.type === 'no_result') {
|
||||
this.state.error = evt.message;
|
||||
this.renderAnswer();
|
||||
} else if (evt.type === 'error') {
|
||||
this.state.error = evt.message;
|
||||
this.renderAnswer();
|
||||
} else if (evt.type === 'done') {
|
||||
// 完成
|
||||
}
|
||||
} catch (e) { /* 忽略解析错误 */ }
|
||||
}
|
||||
}
|
||||
this.state.streaming = false;
|
||||
this.renderAnswer();
|
||||
} catch (e) {
|
||||
this.state.streaming = false;
|
||||
this.state.error = e.message;
|
||||
this.renderAnswer();
|
||||
}
|
||||
},
|
||||
|
||||
renderAnswer() {
|
||||
const el = document.getElementById('rag-answer');
|
||||
if (!el) return;
|
||||
|
||||
if (this.state.error) {
|
||||
el.innerHTML = UI.errorState(this.state.error, 'RagPage.submit()');
|
||||
return;
|
||||
}
|
||||
|
||||
if (!this.state.answer && !this.state.citations.length && this.state.streaming) {
|
||||
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.answer && !this.state.thinking && !this.state.citations.length) {
|
||||
el.innerHTML = UI.emptyState('提问开始对话', '系统将检索相关法规条文并由 AI 生成带引用的答案');
|
||||
return;
|
||||
}
|
||||
|
||||
// thinking 折叠区(仅 thinking 开启且有内容时显示)
|
||||
const thinkingHtml = this.state.thinking
|
||||
? `<details class="mb-4 group">
|
||||
<summary class="cursor-pointer flex items-center gap-2 text-xs text-gray-400 hover:text-gray-600 py-2">
|
||||
<svg class="w-3 h-3 transition-transform group-open:rotate-90" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2"><polyline points="9 18 15 12 9 6"/></svg>
|
||||
<span>Thinking 思考过程</span>
|
||||
<span class="text-gray-300">${this.state.thinking.length} 字</span>
|
||||
${this.state.streaming && !this.state.answer ? '<span class="text-emerald-500">思考中...</span>' : ''}
|
||||
</summary>
|
||||
<div class="mt-2 p-3 bg-gray-50 rounded-lg border border-gray-100 max-h-96 overflow-y-auto scroll-thin">
|
||||
<pre class="text-xs text-gray-500 whitespace-pre-wrap font-mono leading-relaxed">${this._escapeHtml(this.state.thinking)}</pre>
|
||||
</div>
|
||||
</details>`
|
||||
: '';
|
||||
|
||||
const answerHtml = this.state.answer
|
||||
? `<div class="md-content text-sm leading-relaxed ${this.state.streaming ? 'stream-cursor' : ''}">${marked.parse(this.state.answer)}</div>`
|
||||
: (this.state.streaming && this.state.thinking ? '<div class="text-xs text-gray-400 py-2">等待生成答案...</div>' : '');
|
||||
const citationsHtml = this.state.citations.length
|
||||
? `<div class="mt-6 pt-4 border-t border-gray-200"><div class="text-xs font-semibold text-gray-500 mb-3">引用条文(${this.state.citations.length})</div>${this.state.citations.map(c => UI.clauseCard(c)).join('')}</div>`
|
||||
: '';
|
||||
|
||||
el.innerHTML = `
|
||||
<div class="bg-white rounded-xl border border-gray-200 p-4">
|
||||
<div class="text-xs font-semibold text-gray-500 mb-3">AI 回答</div>
|
||||
${thinkingHtml}
|
||||
${answerHtml}
|
||||
${this.state.streaming ? `<div class="flex items-center gap-2 mt-3 text-xs text-gray-400">${UI.spinner(14)} 生成中...</div>` : ''}
|
||||
${citationsHtml}
|
||||
</div>`;
|
||||
},
|
||||
|
||||
_escapeHtml(text) {
|
||||
const div = document.createElement('div');
|
||||
div.textContent = text;
|
||||
return div.innerHTML;
|
||||
},
|
||||
};
|
||||
Reference in New Issue
Block a user