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>
259 lines
12 KiB
JavaScript
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;
|
|
},
|
|
};
|