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,258 @@
|
||||
/**
|
||||
* settings.js — 设置页(系统提示词 + 模板正文 + LLM 参数)
|
||||
* 所有配置持久化到 SQLite,通过 /api/prompts 读写
|
||||
*/
|
||||
|
||||
const SettingsPage = {
|
||||
state: {
|
||||
template: 'simple',
|
||||
configs: null, // 从 API 加载的配置(含自定义值)
|
||||
llmParams: null, // LLM 参数
|
||||
systemPrompt: '', // 系统提示词(编辑框值,空=使用默认)
|
||||
templateText: '', // 模板正文(编辑框值,空=使用默认)
|
||||
saveStatus: '', // 保存状态提示
|
||||
loading: true,
|
||||
error: null,
|
||||
},
|
||||
|
||||
render() {
|
||||
if (this.state.loading) {
|
||||
return `<div class="p-4 md:p-6 max-w-3xl mx-auto">
|
||||
<h2 class="text-lg font-semibold mb-4">设置</h2>
|
||||
<div class="flex items-center justify-center py-16 gap-3 text-gray-400">${UI.spinner(20)}<span class="text-sm">加载配置中...</span></div>
|
||||
</div>`;
|
||||
}
|
||||
if (this.state.error) {
|
||||
return `<div class="p-4 md:p-6 max-w-3xl mx-auto">
|
||||
<h2 class="text-lg font-semibold mb-4">设置</h2>
|
||||
${UI.errorState(this.state.error, 'SettingsPage.init()')}
|
||||
</div>`;
|
||||
}
|
||||
|
||||
const tpl = TEMPLATES.find(t => t.value === this.state.template);
|
||||
const llm = this.state.llmParams || {};
|
||||
|
||||
return `
|
||||
<div class="p-4 md:p-6 max-w-3xl mx-auto">
|
||||
<h2 class="text-lg font-semibold mb-1">设置</h2>
|
||||
<p class="text-xs text-gray-400 mb-6">配置系统提示词、问答模板和 LLM 参数,所有设置持久化到数据库。</p>
|
||||
|
||||
<!-- 系统提示词 -->
|
||||
<div class="bg-white rounded-xl border border-gray-200 p-4 mb-4">
|
||||
<div class="flex items-center justify-between mb-2">
|
||||
<label class="text-sm font-semibold text-gray-700">系统提示词(System Prompt)</label>
|
||||
<button onclick="SettingsPage.resetSystemPrompt()" class="text-xs text-gray-400 hover:text-gray-600">恢复默认</button>
|
||||
</div>
|
||||
<textarea id="settings-system-prompt" rows="2"
|
||||
class="w-full rounded-lg border border-gray-200 px-3 py-2 text-xs mono focus:outline-none focus:ring-2 focus:ring-gray-900 resize-none"
|
||||
placeholder="留空使用默认:你是法律助手,根据提供的法规条文回答问题..."
|
||||
oninput="SettingsPage.saveSystemPrompt(this.value)">${this._escapeHtml(this.state.systemPrompt)}</textarea>
|
||||
<p class="text-xs text-gray-400 mt-1">定义 AI 的角色和基本行为。留空使用默认值。</p>
|
||||
</div>
|
||||
|
||||
<!-- 模板选择 + 正文 -->
|
||||
<div class="bg-white rounded-xl border border-gray-200 p-4 mb-4">
|
||||
<div class="flex items-center justify-between mb-3">
|
||||
<label class="text-sm font-semibold text-gray-700">问答模板</label>
|
||||
<button onclick="SettingsPage.resetTemplateText()" class="text-xs text-gray-400 hover:text-gray-600">恢复默认</button>
|
||||
</div>
|
||||
<!-- 模板切换 -->
|
||||
<div class="flex gap-2 mb-3">
|
||||
${TEMPLATES.map(t => `
|
||||
<button onclick="SettingsPage.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>
|
||||
<textarea id="settings-template-text" rows="8"
|
||||
class="w-full rounded-lg border border-gray-200 px-3 py-2 text-xs mono focus:outline-none focus:ring-2 focus:ring-gray-900 resize-none"
|
||||
placeholder="留空使用当前模板的预设正文。自定义时必须包含 {context} 和 {question} 占位符。"
|
||||
oninput="SettingsPage.saveTemplateText(this.value)">${this._escapeHtml(this.state.templateText)}</textarea>
|
||||
<p class="text-xs text-gray-400 mt-1">自定义模板正文,必须包含 <code class="bg-gray-100 px-1 rounded">{context}</code> 和 <code class="bg-gray-100 px-1 rounded">{question}</code> 占位符。留空使用预设模板。</p>
|
||||
|
||||
<!-- 预设模板参考 -->
|
||||
<details class="text-xs text-gray-400 mt-3">
|
||||
<summary class="cursor-pointer hover:text-gray-600">查看预设模板参考(当前: ${tpl?.label || ''})</summary>
|
||||
<pre class="mt-2 p-3 bg-gray-50 rounded-lg text-xs overflow-x-auto scroll-thin whitespace-pre-wrap">${this._escapeHtml(PROMPT_DEFAULTS['template_' + this.state.template] || '')}</pre>
|
||||
</details>
|
||||
</div>
|
||||
|
||||
<!-- LLM 参数 -->
|
||||
<div class="bg-white rounded-xl border border-gray-200 p-4 mb-4">
|
||||
<div class="text-sm font-semibold text-gray-700 mb-3">LLM 参数</div>
|
||||
<div class="grid grid-cols-2 gap-4">
|
||||
<!-- thinking 开关 -->
|
||||
<div class="flex items-center justify-between bg-gray-50 rounded-lg px-3 py-2">
|
||||
<label class="text-xs text-gray-600">Thinking 思考</label>
|
||||
<label class="relative inline-flex items-center cursor-pointer">
|
||||
<input type="checkbox" id="settings-thinking-toggle" class="sr-only peer"
|
||||
${llm.thinking_enabled ? 'checked' : ''}
|
||||
onchange="SettingsPage.saveLLMParam('llm_thinking_enabled', this.checked ? 'true' : 'false')">
|
||||
<div class="w-9 h-5 bg-gray-200 peer-focus:outline-none peer-focus:ring-2 peer-focus:ring-gray-900 rounded-full peer peer-checked:after:translate-x-full peer-checked:after:border-white after:content-[''] after:absolute after:top-[2px] after:left-[2px] after:bg-white after:border-gray-300 after:border after:rounded-full after:h-4 after:w-4 after:transition-all peer-checked:bg-gray-900"></div>
|
||||
</label>
|
||||
</div>
|
||||
<!-- temperature -->
|
||||
<div class="bg-gray-50 rounded-lg px-3 py-2">
|
||||
<div class="flex items-center justify-between mb-1">
|
||||
<label class="text-xs text-gray-600">Temperature</label>
|
||||
<span id="settings-temperature-val" class="text-xs text-gray-900 font-mono">${llm.temperature ?? 0.3}</span>
|
||||
</div>
|
||||
<input type="range" id="settings-temperature" min="0" max="1" step="0.1"
|
||||
value="${llm.temperature ?? 0.3}"
|
||||
class="w-full h-1 bg-gray-200 rounded-lg appearance-none cursor-pointer accent-gray-900"
|
||||
oninput="document.getElementById('settings-temperature-val').textContent=this.value"
|
||||
onchange="SettingsPage.saveLLMParam('llm_temperature', this.value)">
|
||||
</div>
|
||||
<!-- max_tokens -->
|
||||
<div class="bg-gray-50 rounded-lg px-3 py-2">
|
||||
<label class="text-xs text-gray-600 block mb-1">Max Tokens(含 thinking)</label>
|
||||
<input type="number" id="settings-max-tokens" min="256" max="32768" step="256"
|
||||
value="${llm.max_tokens ?? 4096}"
|
||||
class="w-full rounded border border-gray-200 px-2 py-1 text-xs focus:outline-none focus:ring-1 focus:ring-gray-900"
|
||||
onchange="SettingsPage.saveLLMParam('llm_max_tokens', this.value)">
|
||||
</div>
|
||||
<!-- thinking_budget -->
|
||||
<div class="bg-gray-50 rounded-lg px-3 py-2">
|
||||
<label class="text-xs text-gray-600 block mb-1">Thinking 预算(token)</label>
|
||||
<input type="number" id="settings-thinking-budget" min="0" max="16384" step="256"
|
||||
value="${llm.thinking_budget ?? 2048}"
|
||||
class="w-full rounded border border-gray-200 px-2 py-1 text-xs focus:outline-none focus:ring-1 focus:ring-gray-900"
|
||||
onchange="SettingsPage.saveLLMParam('llm_thinking_budget', this.value)">
|
||||
</div>
|
||||
</div>
|
||||
<p class="text-xs text-gray-400 mt-2">Thinking 开启时,AI 会先思考再回答(可折叠查看)。Max Tokens 是总 token 上限(含 thinking)。</p>
|
||||
</div>
|
||||
|
||||
<!-- 保存状态 -->
|
||||
<div id="settings-save-status" class="text-xs text-emerald-600 transition-opacity text-center" style="opacity:0"></div>
|
||||
</div>
|
||||
`;
|
||||
},
|
||||
|
||||
async init() {
|
||||
await this.loadConfigs();
|
||||
},
|
||||
|
||||
async loadConfigs() {
|
||||
this.state.loading = true;
|
||||
const content = document.getElementById('content');
|
||||
if (content) content.innerHTML = this.render();
|
||||
|
||||
try {
|
||||
const resp = await API.get('/api/prompts');
|
||||
this.state.configs = resp.data.configs;
|
||||
this.state.llmParams = resp.data.llm_params;
|
||||
// 系统提示词:直接显示数据库值(空则用默认值填充)
|
||||
this.state.systemPrompt = this.state.configs.system_prompt || PROMPT_DEFAULTS.system_prompt;
|
||||
// 当前模板正文:直接显示数据库值(空则用默认值填充)
|
||||
this._loadTemplateText();
|
||||
this.state.loading = false;
|
||||
this.state.error = null;
|
||||
if (content) content.innerHTML = this.render();
|
||||
} catch (e) {
|
||||
this.state.loading = false;
|
||||
this.state.error = e.message;
|
||||
if (content) content.innerHTML = this.render();
|
||||
}
|
||||
},
|
||||
|
||||
_loadTemplateText() {
|
||||
const tplKey = `template_${this.state.template}`;
|
||||
this.state.templateText = this.state.configs[tplKey] || PROMPT_DEFAULTS[tplKey];
|
||||
},
|
||||
|
||||
setTemplate(t) {
|
||||
this.state.template = t;
|
||||
if (this.state.configs) this._loadTemplateText();
|
||||
const content = document.getElementById('content');
|
||||
content.innerHTML = this.render();
|
||||
},
|
||||
|
||||
// ===== 系统提示词 =====
|
||||
async saveSystemPrompt(val) {
|
||||
this.state.systemPrompt = val;
|
||||
this._showSaveStatus('保存中...');
|
||||
try {
|
||||
await API.put('/api/prompts', { system_prompt: val });
|
||||
this._showSaveStatus('已保存');
|
||||
} catch (e) {
|
||||
this._showSaveStatus('保存失败');
|
||||
}
|
||||
},
|
||||
|
||||
async resetSystemPrompt() {
|
||||
this.state.systemPrompt = '';
|
||||
this._showSaveStatus('重置中...');
|
||||
try {
|
||||
await API.put('/api/prompts', { system_prompt: '' });
|
||||
const el = document.getElementById('settings-system-prompt');
|
||||
if (el) el.value = '';
|
||||
this._showSaveStatus('已恢复默认');
|
||||
} catch (e) {
|
||||
this._showSaveStatus('重置失败');
|
||||
}
|
||||
},
|
||||
|
||||
// ===== 模板正文 =====
|
||||
async saveTemplateText(val) {
|
||||
this.state.templateText = val;
|
||||
this._showSaveStatus('保存中...');
|
||||
try {
|
||||
const body = {};
|
||||
body[`template_${this.state.template}`] = val;
|
||||
await API.put('/api/prompts', body);
|
||||
this._showSaveStatus('已保存');
|
||||
} catch (e) {
|
||||
this._showSaveStatus('保存失败');
|
||||
}
|
||||
},
|
||||
|
||||
async resetTemplateText() {
|
||||
this.state.templateText = '';
|
||||
this._showSaveStatus('重置中...');
|
||||
try {
|
||||
const body = {};
|
||||
body[`template_${this.state.template}`] = '';
|
||||
await API.put('/api/prompts', body);
|
||||
const el = document.getElementById('settings-template-text');
|
||||
if (el) el.value = '';
|
||||
this._showSaveStatus('已恢复默认');
|
||||
} catch (e) {
|
||||
this._showSaveStatus('重置失败');
|
||||
}
|
||||
},
|
||||
|
||||
// ===== LLM 参数 =====
|
||||
async saveLLMParam(key, value) {
|
||||
this._showSaveStatus('保存中...');
|
||||
try {
|
||||
const body = {};
|
||||
body[key] = String(value);
|
||||
await API.put('/api/prompts', body);
|
||||
this._showSaveStatus('已保存');
|
||||
// 重新加载参数
|
||||
const resp = await API.get('/api/prompts');
|
||||
this.state.llmParams = resp.data.llm_params;
|
||||
} catch (e) {
|
||||
this._showSaveStatus('保存失败');
|
||||
}
|
||||
},
|
||||
|
||||
// ===== 工具方法 =====
|
||||
_showSaveStatus(msg) {
|
||||
this.state.saveStatus = msg;
|
||||
const el = document.getElementById('settings-save-status');
|
||||
if (el) {
|
||||
el.textContent = msg;
|
||||
el.style.opacity = '1';
|
||||
setTimeout(() => { el.style.opacity = '0'; }, 2000);
|
||||
}
|
||||
},
|
||||
|
||||
_escapeHtml(text) {
|
||||
if (!text) return '';
|
||||
const div = document.createElement('div');
|
||||
div.textContent = text;
|
||||
return div.innerHTML;
|
||||
},
|
||||
};
|
||||
Reference in New Issue
Block a user