Files
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

259 lines
12 KiB
JavaScript

/**
* 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;
},
};