/**
* 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 `
智能问答
${templates.map(t => `
`).join('')}
${this.state.question.length}/1000
${UI.emptyState('提问开始对话', '系统将检索相关法规条文并由 AI 生成带引用的答案')}
`;
},
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 = `${UI.spinner(20)}检索法规并生成答案中...
`;
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
? `
Thinking 思考过程
${this.state.thinking.length} 字
${this.state.streaming && !this.state.answer ? '思考中...' : ''}
`
: '';
const answerHtml = this.state.answer
? `${marked.parse(this.state.answer)}
`
: (this.state.streaming && this.state.thinking ? '等待生成答案...
' : '');
const citationsHtml = this.state.citations.length
? `引用条文(${this.state.citations.length})
${this.state.citations.map(c => UI.clauseCard(c)).join('')}
`
: '';
el.innerHTML = `
AI 回答
${thinkingHtml}
${answerHtml}
${this.state.streaming ? `
${UI.spinner(14)} 生成中...
` : ''}
${citationsHtml}
`;
},
_escapeHtml(text) {
const div = document.createElement('div');
div.textContent = text;
return div.innerHTML;
},
};