refactor: remove Agent preset system, fix run_command enable bug
- Delete preset-manager.ts and preset-bar.ts - Clean up all preset references from main.ts, types.d.ts, state.ts - Remove preset modal HTML and CSS, rename temp-slider/modal-actions classes - Fix run_command tool not being enabled (missing setToolEnabled call)
This commit is contained in:
@@ -1,6 +1,6 @@
|
||||
/**
|
||||
* MemoryPanel - Agent 记忆管理面板
|
||||
* 替代 PresetBar,提供记忆的查看、搜索、编辑、删除功能
|
||||
* 提供记忆的查看、搜索、编辑、删除功能
|
||||
*/
|
||||
|
||||
import { state, KEYS } from '../state/state.js';
|
||||
|
||||
@@ -1,187 +0,0 @@
|
||||
/**
|
||||
* PresetBar - 预设栏组件
|
||||
*/
|
||||
|
||||
import { state, KEYS } from '../state/state.js';
|
||||
import {
|
||||
getPresets, getActivePresetId, activatePreset,
|
||||
createPreset, updatePreset, deletePreset
|
||||
} from '../services/preset-manager.js';
|
||||
import { showToast } from './toast.js';
|
||||
import type { Preset } from '../types.js';
|
||||
|
||||
let presetBarEl: HTMLElement;
|
||||
let presetListEl: HTMLElement;
|
||||
let editingPresetId: string | null = null;
|
||||
|
||||
function escapeHtml(str: string): string {
|
||||
const map: Record<string, string> = { '&': '&', '<': '<', '>': '>', '"': '"', "'": ''' };
|
||||
return String(str).replace(/[&<>"']/g, c => map[c]);
|
||||
}
|
||||
|
||||
export function initPresetBar(): void {
|
||||
const modelBar = document.querySelector('.model-bar');
|
||||
if (!modelBar) return;
|
||||
|
||||
const bar = document.createElement('div');
|
||||
bar.className = 'preset-bar';
|
||||
bar.innerHTML = `
|
||||
<div class="preset-list" id="presetList"></div>
|
||||
<button class="preset-add-btn" id="btnAddPreset" title="管理预设">
|
||||
<svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2">
|
||||
<line x1="12" y1="5" x2="12" y2="19"/><line x1="5" y1="12" x2="19" y2="12"/>
|
||||
</svg>
|
||||
</button>
|
||||
`;
|
||||
modelBar.after(bar);
|
||||
|
||||
presetBarEl = bar;
|
||||
presetListEl = bar.querySelector('#presetList')!;
|
||||
|
||||
bar.querySelector('#btnAddPreset')!.addEventListener('click', () => openPresetModal());
|
||||
|
||||
state.on('activePresetId', renderPresetBar);
|
||||
state.on('presets', renderPresetBar);
|
||||
|
||||
renderPresetBar();
|
||||
}
|
||||
|
||||
function renderPresetBar(): void {
|
||||
if (!presetListEl) return;
|
||||
|
||||
const presets = getPresets();
|
||||
const activeId = getActivePresetId();
|
||||
|
||||
presetListEl.innerHTML = '';
|
||||
|
||||
for (const preset of presets) {
|
||||
const chip = document.createElement('button');
|
||||
chip.className = 'preset-chip' + (preset.id === activeId ? ' active' : '');
|
||||
chip.title = preset.systemPrompt ? preset.systemPrompt.slice(0, 100) : '无系统提示词';
|
||||
chip.innerHTML = `<span class="preset-chip-icon">${preset.icon}</span><span class="preset-chip-name">${escapeHtml(preset.name)}</span>`;
|
||||
|
||||
chip.addEventListener('click', () => handlePresetClick(preset.id));
|
||||
chip.addEventListener('contextmenu', (e) => {
|
||||
if (!preset.builtIn) {
|
||||
e.preventDefault();
|
||||
editingPresetId = preset.id;
|
||||
openPresetModal(preset);
|
||||
}
|
||||
});
|
||||
|
||||
presetListEl.appendChild(chip);
|
||||
}
|
||||
}
|
||||
|
||||
async function handlePresetClick(presetId: string): Promise<void> {
|
||||
const currentId = getActivePresetId();
|
||||
if (presetId === currentId) return;
|
||||
|
||||
await activatePreset(presetId);
|
||||
|
||||
const preset = getPresets().find(p => p.id === presetId);
|
||||
if (preset) {
|
||||
showToast(`已切换到 ${preset.icon} ${preset.name}`, 'success', 2000);
|
||||
}
|
||||
}
|
||||
|
||||
function openPresetModal(editPreset?: Preset): void {
|
||||
const modal = document.querySelector('#presetModal') as HTMLElement;
|
||||
const titleEl = modal.querySelector('#presetModalTitle')!;
|
||||
const nameInput = modal.querySelector('#inputPresetName') as HTMLInputElement;
|
||||
const iconInput = modal.querySelector('#inputPresetIcon') as HTMLInputElement;
|
||||
const spInput = modal.querySelector('#inputPresetSystemPrompt') as HTMLTextAreaElement;
|
||||
const tempInput = modal.querySelector('#inputPresetTemp') as HTMLInputElement;
|
||||
const tempVal = modal.querySelector('#presetTempValue')!;
|
||||
const ctxInput = modal.querySelector('#inputPresetNumCtx') as HTMLInputElement;
|
||||
const thinkCheck = modal.querySelector('#togglePresetThink') as HTMLInputElement;
|
||||
const btnSave = modal.querySelector('#btnSavePreset') as HTMLButtonElement;
|
||||
const btnDelete = modal.querySelector('#btnDeletePreset') as HTMLElement;
|
||||
|
||||
if (editPreset && editPreset.id) {
|
||||
editingPresetId = editPreset.id;
|
||||
titleEl.textContent = '✏️ 编辑预设';
|
||||
nameInput.value = editPreset.name;
|
||||
iconInput.value = editPreset.icon;
|
||||
spInput.value = editPreset.systemPrompt;
|
||||
tempInput.value = String(editPreset.temperature ?? 0.7);
|
||||
tempVal.textContent = (editPreset.temperature ?? 0.7).toFixed(1);
|
||||
ctxInput.value = String(editPreset.numCtx || 24576);
|
||||
thinkCheck.checked = editPreset.think ?? false;
|
||||
btnDelete.style.display = editPreset.builtIn ? 'none' : '';
|
||||
btnSave.textContent = '保存';
|
||||
} else {
|
||||
editingPresetId = null;
|
||||
titleEl.textContent = '✨ 新建预设';
|
||||
nameInput.value = '';
|
||||
iconInput.value = '🤖';
|
||||
spInput.value = '';
|
||||
tempInput.value = '0.7';
|
||||
tempVal.textContent = '0.7';
|
||||
ctxInput.value = '24576';
|
||||
thinkCheck.checked = false;
|
||||
btnDelete.style.display = 'none';
|
||||
btnSave.textContent = '创建';
|
||||
}
|
||||
|
||||
modal.style.display = '';
|
||||
nameInput.focus();
|
||||
|
||||
tempInput.oninput = () => {
|
||||
tempVal.textContent = parseFloat(tempInput.value).toFixed(1);
|
||||
};
|
||||
}
|
||||
|
||||
export function initPresetModal(): void {
|
||||
const modal = document.querySelector('#presetModal') as HTMLElement;
|
||||
if (!modal) return;
|
||||
|
||||
modal.querySelector('#btnClosePresetModal')!.addEventListener('click', () => {
|
||||
modal.style.display = 'none';
|
||||
});
|
||||
modal.addEventListener('click', (e) => {
|
||||
if (e.target === modal) modal.style.display = 'none';
|
||||
});
|
||||
|
||||
modal.querySelector('#btnSavePreset')!.addEventListener('click', async () => {
|
||||
const name = (modal.querySelector('#inputPresetName') as HTMLInputElement).value.trim();
|
||||
if (!name) { showToast('请输入预设名称', 'warning'); return; }
|
||||
|
||||
const data = {
|
||||
name,
|
||||
icon: (modal.querySelector('#inputPresetIcon') as HTMLInputElement).value.trim() || '🤖',
|
||||
systemPrompt: (modal.querySelector('#inputPresetSystemPrompt') as HTMLTextAreaElement).value.trim(),
|
||||
temperature: parseFloat((modal.querySelector('#inputPresetTemp') as HTMLInputElement).value) || 0.7,
|
||||
numCtx: parseInt((modal.querySelector('#inputPresetNumCtx') as HTMLInputElement).value) || 24576,
|
||||
think: (modal.querySelector('#togglePresetThink') as HTMLInputElement).checked
|
||||
};
|
||||
|
||||
if (editingPresetId) {
|
||||
await updatePreset(editingPresetId, data);
|
||||
showToast('预设已更新', 'success', 2000);
|
||||
if (editingPresetId === getActivePresetId()) {
|
||||
await activatePreset(editingPresetId);
|
||||
}
|
||||
} else {
|
||||
const created = await createPreset(data);
|
||||
await activatePreset(created.id);
|
||||
showToast(`已创建并激活 ${data.icon} ${data.name}`, 'success', 2000);
|
||||
}
|
||||
|
||||
modal.style.display = 'none';
|
||||
editingPresetId = null;
|
||||
});
|
||||
|
||||
modal.querySelector('#btnDeletePreset')!.addEventListener('click', async () => {
|
||||
if (!editingPresetId) return;
|
||||
const preset = getPresets().find(p => p.id === editingPresetId);
|
||||
if (!preset) return;
|
||||
|
||||
if (confirm(`确定删除预设 "${preset.icon} ${preset.name}" ?`)) {
|
||||
await deletePreset(editingPresetId);
|
||||
showToast('预设已删除', 'info', 2000);
|
||||
modal.style.display = 'none';
|
||||
editingPresetId = null;
|
||||
}
|
||||
});
|
||||
}
|
||||
@@ -6,6 +6,7 @@ import { state, KEYS } from '../state/state.js';
|
||||
import { debounce } from '../utils/utils.js';
|
||||
import { encryptData, decryptData, isMetonaFile } from '../services/crypto.js';
|
||||
import { setMemoryEnabled } from '../services/memory-manager.js';
|
||||
import { setToolEnabled } from '../services/tool-registry.js';
|
||||
import { updateConnectionInfo, updateRunningModels } from './header.js';
|
||||
import { loadModels } from './model-bar.js';
|
||||
import { showToast } from './toast.js';
|
||||
@@ -122,6 +123,7 @@ export function initSettingsModal(): void {
|
||||
const db = state.get<ChatDB | null>(KEYS.DB);
|
||||
state.set('runCommandEnabled', toggleRunCommand.checked);
|
||||
if (db) await db.saveSetting('runCommandEnabled', toggleRunCommand.checked);
|
||||
setToolEnabled('run_command', toggleRunCommand.checked);
|
||||
if (toggleRunCommand.checked) {
|
||||
showToast('⚠️ 命令执行已开启,请谨慎使用', 'warning');
|
||||
}
|
||||
|
||||
+3
-53
@@ -190,7 +190,7 @@
|
||||
</div>
|
||||
<p class="text-muted" style="margin-top:4px;font-size:11px;">常见值:2048 / 4096 / 8192 / 32768。值越大上下文越长,显存占用越高。</p>
|
||||
<label class="setting-label" style="margin-top:12px;">温度(Temperature):<span id="tempValue">0.7</span></label>
|
||||
<input type="range" id="inputTemperature" class="preset-temp-slider" min="0" max="2" step="0.1" value="0.7">
|
||||
<input type="range" id="inputTemperature" class="temp-slider" min="0" max="2" step="0.1" value="0.7">
|
||||
<p class="text-muted" style="margin-top:4px;font-size:11px;">低值更精确稳定,高值更有创意多样(0 = 确定性,2 = 最大随机)</p>
|
||||
</div>
|
||||
<div class="setting-group">
|
||||
@@ -356,57 +356,7 @@
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- ═══════════════ 预设管理模态框 ═══════════════ -->
|
||||
<div class="modal-overlay" id="presetModal" style="display:none;">
|
||||
<div class="modal">
|
||||
<div class="modal-header">
|
||||
<h3 id="presetModalTitle">✨ 新建预设</h3>
|
||||
<button class="icon-btn" id="btnClosePresetModal">
|
||||
<svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2">
|
||||
<line x1="18" y1="6" x2="6" y2="18"/><line x1="6" y1="6" x2="18" y2="18"/>
|
||||
</svg>
|
||||
</button>
|
||||
</div>
|
||||
<div class="modal-body">
|
||||
<div class="setting-group">
|
||||
<label class="setting-label">名称</label>
|
||||
<div class="preset-name-row">
|
||||
<input class="setting-input preset-icon-input" id="inputPresetIcon" type="text" maxlength="4" placeholder="🤖">
|
||||
<input class="setting-input" id="inputPresetName" type="text" placeholder="例如:翻译官" style="flex:1;">
|
||||
</div>
|
||||
</div>
|
||||
<div class="setting-group">
|
||||
<label class="setting-label">系统提示词(System Prompt)</label>
|
||||
<textarea class="setting-input" id="inputPresetSystemPrompt" rows="6" placeholder="定义这个角色的行为、能力和回答风格..." style="resize:vertical;min-height:120px;"></textarea>
|
||||
</div>
|
||||
<div class="setting-group">
|
||||
<label class="setting-label">温度(Temperature):<span id="presetTempValue">0.7</span></label>
|
||||
<input type="range" id="inputPresetTemp" class="preset-temp-slider" min="0" max="2" step="0.1" value="0.7">
|
||||
</div>
|
||||
<div class="setting-group">
|
||||
<label class="setting-label">上下文长度(num_ctx)</label>
|
||||
<div style="display:flex;gap:8px;align-items:center;">
|
||||
<input class="setting-input" id="inputPresetNumCtx" type="number" min="512" max="131072" step="512" value="24576" style="margin-bottom:0;">
|
||||
<span class="text-muted" style="white-space:nowrap;">tokens</span>
|
||||
</div>
|
||||
</div>
|
||||
<div class="setting-group">
|
||||
<label class="setting-label">Think 深度推理</label>
|
||||
<label class="toggle-label" style="display:inline-flex;align-items:center;gap:8px;">
|
||||
<input type="checkbox" id="togglePresetThink">
|
||||
<span class="toggle-slider"></span>
|
||||
<span class="text-muted" style="font-size:12px;">需要模型支持</span>
|
||||
</label>
|
||||
</div>
|
||||
<div class="preset-modal-actions">
|
||||
<button class="btn btn-danger" id="btnDeletePreset" style="display:none;">🗑️ 删除</button>
|
||||
<div style="flex:1;"></div>
|
||||
<button class="btn btn-outline" id="btnClosePresetModal2" onclick="document.querySelector('#presetModal').style.display='none';">取消</button>
|
||||
<button class="btn btn-primary" id="btnSavePreset">创建</button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
|
||||
<!-- ═══════════════ 图片预览 Lightbox ═══════════════ -->
|
||||
<div class="lightbox-overlay" id="lightbox" style="display:none;">
|
||||
@@ -423,7 +373,7 @@
|
||||
<div class="modal-body" id="toolConfirmBody">
|
||||
<p>AI 请求执行一个需要确认的操作。</p>
|
||||
</div>
|
||||
<div class="preset-modal-actions" style="padding:12px 20px 16px;">
|
||||
<div class="modal-actions" style="padding:12px 20px 16px;">
|
||||
<div style="flex:1;"></div>
|
||||
<button class="btn btn-outline" id="toolCancelBtn">❌ 取消</button>
|
||||
<button class="btn btn-primary" id="toolConfirmBtn">✅ 确认执行</button>
|
||||
|
||||
@@ -20,10 +20,9 @@ import { initInputArea } from './components/input-area.js';
|
||||
import { initSettingsModal, closeSettingsModal } from './components/settings-modal.js';
|
||||
import { initHistoryModal, closeHistoryModal } from './components/history-modal.js';
|
||||
import { initKBModal } from './components/kb-modal.js';
|
||||
import { initPresetBar, initPresetModal } from './components/preset-bar.js';
|
||||
import { initPresetManager } from './services/preset-manager.js';
|
||||
import { initVectorStore } from './services/rag.js';
|
||||
import { initMemoryManager } from './services/memory-manager.js';
|
||||
import { setToolEnabled } from './services/tool-registry.js';
|
||||
import { initToolConfirmModal } from './components/tool-confirm-modal.js';
|
||||
import { initMemoryPanel } from './components/memory-panel.js';
|
||||
import type { ChatSession } from './types.js';
|
||||
@@ -155,8 +154,6 @@ async function init(): Promise<void> {
|
||||
initSettingsModal();
|
||||
initHistoryModal();
|
||||
initKBModal();
|
||||
initPresetBar();
|
||||
initPresetModal();
|
||||
initHelpModal();
|
||||
initToolConfirmModal();
|
||||
initMemoryPanel();
|
||||
@@ -167,7 +164,6 @@ async function init(): Promise<void> {
|
||||
await checkConnection();
|
||||
await loadModels();
|
||||
await initVectorStore();
|
||||
await initPresetManager();
|
||||
await initMemoryManager();
|
||||
|
||||
const savedModel = await db.getSetting('selectedModel', '');
|
||||
@@ -199,6 +195,7 @@ async function init(): Promise<void> {
|
||||
const runCommandEnabled = await db.getSetting('runCommandEnabled', false);
|
||||
state.set('toolCallingEnabled', toolCallingEnabled);
|
||||
state.set('runCommandEnabled', runCommandEnabled);
|
||||
if (runCommandEnabled) setToolEnabled('run_command', true);
|
||||
const toggleTC = document.querySelector<HTMLInputElement>('#toggleToolCalling');
|
||||
if (toggleTC) toggleTC.checked = toolCallingEnabled;
|
||||
const toggleRC = document.querySelector<HTMLInputElement>('#toggleRunCommand');
|
||||
@@ -227,8 +224,6 @@ async function init(): Promise<void> {
|
||||
initInputArea();
|
||||
initSettingsModal();
|
||||
initHistoryModal();
|
||||
initPresetBar();
|
||||
initPresetModal();
|
||||
setupDesktopIntegration();
|
||||
checkConnection().then(loadModels);
|
||||
}
|
||||
|
||||
@@ -1,179 +0,0 @@
|
||||
/**
|
||||
* PresetManager - Agent 预设管理
|
||||
*/
|
||||
|
||||
import { state, KEYS } from '../state/state.js';
|
||||
import { generateId } from '../utils/utils.js';
|
||||
import type { Preset, ChatDB } from '../types.js';
|
||||
|
||||
const PRESETS_KEY = 'agentPresets';
|
||||
const ACTIVE_PRESET_KEY = 'activePresetId';
|
||||
|
||||
const BUILT_IN_PRESETS: Preset[] = [
|
||||
{ id: '_default', name: '默认', icon: '💬', systemPrompt: '', temperature: 0.7, numCtx: 24576, think: false, builtIn: true, createdAt: 0, updatedAt: 0 },
|
||||
{ id: '_translator', name: '翻译官', icon: '🌐', systemPrompt: '你是一位专业翻译官。你的任务是将用户输入的内容准确翻译为目标语言。\n\n规则:\n1. 自动识别源语言,翻译为用户指定的目标语言(如未指定则翻译为中文)\n2. 保持原文的格式、语气和风格\n3. 专有名词保留原文并给出通用译名\n4. 对于有多种含义的词语,结合上下文选择最合适的翻译\n5. 直接输出翻译结果,不需要额外解释(除非用户要求)', temperature: 0.3, numCtx: 16384, think: false, builtIn: true, createdAt: 0, updatedAt: 0 },
|
||||
{ id: '_code_review', name: '代码审查', icon: '🔍', systemPrompt: '你是一位资深代码审查专家。对用户提交的代码进行全面审查。\n\n审查维度:\n1. **Bug 与逻辑错误** — 潜在的运行时错误、边界条件、空指针等\n2. **安全性** — SQL注入、XSS、敏感信息泄露等安全隐患\n3. **性能** — 时间/空间复杂度、不必要的计算、内存泄漏\n4. **可读性** — 命名规范、代码结构、注释质量\n5. **最佳实践** — 设计模式、语言惯用法、框架约定\n\n输出格式:\n- 按严重程度排列问题(严重 → 轻微)\n- 每个问题给出:位置、问题描述、修复建议、改进后的代码\n- 最后给出整体评价和改进建议', temperature: 0.2, numCtx: 32768, think: true, builtIn: true, createdAt: 0, updatedAt: 0 },
|
||||
{ id: '_writer', name: '写作助手', icon: '✍️', systemPrompt: '你是一位才华横溢的写作助手。擅长多种文体和写作风格。\n\n能力范围:\n1. 文章撰写 — 议论文、说明文、散文、故事\n2. 文案创作 — 广告文案、产品描述、社交媒体文案\n3. 内容润色 — 修改语法、优化措辞、提升表达力\n4. 风格调整 — 正式/口语/文艺/简洁等多种风格\n5. 创意写作 — 小说、诗歌、剧本、对话\n\n工作方式:\n- 仔细理解用户的写作需求(读者、用途、风格偏好)\n- 如需信息不全,主动询问确认\n- 提供多种版本供选择\n- 解释重要的写作决策', temperature: 0.9, numCtx: 24576, think: false, builtIn: true, createdAt: 0, updatedAt: 0 },
|
||||
{ id: '_analyst', name: '数据分析师', icon: '📊', systemPrompt: '你是一位专业的数据分析师。擅长从数据中提取洞察并给出可执行的建议。\n\n分析方法:\n1. 数据概览 — 数据量、字段类型、缺失值、异常值\n2. 统计分析 — 描述性统计、相关性分析、趋势分析\n3. 可视化建议 — 推荐合适的图表类型和展示方式\n4. 洞察提取 — 关键发现、模式识别、因果推断\n5. 行动建议 — 基于数据的具体可执行建议\n\n输出格式:\n- 先给出核心结论(Executive Summary)\n- 再展开详细分析过程\n- 使用表格和列表组织数据\n- 给出代码(Python/SQL)时附带注释', temperature: 0.4, numCtx: 32768, think: true, builtIn: true, createdAt: 0, updatedAt: 0 },
|
||||
{ id: '_tutor', name: '学习导师', icon: '🎓', systemPrompt: '你是一位耐心且善于启发的学习导师。用苏格拉底式教学法帮助用户深入理解知识。\n\n教学原则:\n1. **先问后答** — 通过引导性问题帮助用户自己找到答案\n2. **由浅入深** — 从基础概念开始,逐步构建知识体系\n3. **类比教学** — 用生活中的类比解释抽象概念\n4. **实践导向** — 每个知识点配一个动手练习\n5. **鼓励探索** — 提供延伸阅读方向和思考题', temperature: 0.6, numCtx: 24576, think: true, builtIn: true, createdAt: 0, updatedAt: 0 }
|
||||
];
|
||||
|
||||
let presets: Preset[] = [];
|
||||
let activePresetId: string | null = null;
|
||||
|
||||
export async function initPresetManager(): Promise<void> {
|
||||
const db = state.get<ChatDB | null>(KEYS.DB);
|
||||
|
||||
let userPresets: Preset[] = [];
|
||||
if (db) {
|
||||
userPresets = await db.getSetting(PRESETS_KEY, []);
|
||||
}
|
||||
|
||||
presets = [...BUILT_IN_PRESETS, ...userPresets];
|
||||
|
||||
if (db) {
|
||||
activePresetId = await db.getSetting(ACTIVE_PRESET_KEY, '_default');
|
||||
} else {
|
||||
activePresetId = '_default';
|
||||
}
|
||||
|
||||
state.set('presets', presets);
|
||||
state.set('activePresetId', activePresetId);
|
||||
|
||||
const activePreset = presets.find(p => p.id === activePresetId);
|
||||
if (activePreset && activePreset.id !== '_default') {
|
||||
applyPresetToState(activePreset, false);
|
||||
}
|
||||
}
|
||||
|
||||
export function getPresets(): Preset[] {
|
||||
return presets;
|
||||
}
|
||||
|
||||
export function getActivePreset(): Preset {
|
||||
return presets.find(p => p.id === activePresetId) || presets[0];
|
||||
}
|
||||
|
||||
export function getActivePresetId(): string | null {
|
||||
return activePresetId;
|
||||
}
|
||||
|
||||
export async function activatePreset(presetId: string): Promise<void> {
|
||||
const preset = presets.find(p => p.id === presetId);
|
||||
if (!preset) return;
|
||||
|
||||
activePresetId = presetId;
|
||||
state.set('activePresetId', presetId);
|
||||
applyPresetToState(preset, true);
|
||||
|
||||
const db = state.get<ChatDB | null>(KEYS.DB);
|
||||
if (db) {
|
||||
await db.saveSetting(ACTIVE_PRESET_KEY, presetId);
|
||||
}
|
||||
}
|
||||
|
||||
function applyPresetToState(preset: Preset, saveToDb: boolean): void {
|
||||
const db = state.get<ChatDB | null>(KEYS.DB);
|
||||
|
||||
if (preset.systemPrompt) {
|
||||
state.set(KEYS.SYSTEM_PROMPT_ENABLED, true);
|
||||
state.set(KEYS.SYSTEM_PROMPT, preset.systemPrompt);
|
||||
} else {
|
||||
state.set(KEYS.SYSTEM_PROMPT_ENABLED, false);
|
||||
state.set(KEYS.SYSTEM_PROMPT, '');
|
||||
}
|
||||
|
||||
if (preset.numCtx) state.set(KEYS.NUM_CTX, preset.numCtx);
|
||||
state.set('temperature', preset.temperature ?? 0.7);
|
||||
state.set('thinkEnabled', preset.think ?? false);
|
||||
|
||||
syncPresetToUI(preset);
|
||||
|
||||
if (saveToDb && db) {
|
||||
db.saveSetting('systemPromptEnabled', !!preset.systemPrompt);
|
||||
db.saveSetting('systemPrompt', preset.systemPrompt || '');
|
||||
db.saveSetting('numCtx', preset.numCtx || 24576);
|
||||
db.saveSetting('temperature', preset.temperature ?? 0.7);
|
||||
}
|
||||
}
|
||||
|
||||
function syncPresetToUI(preset: Preset): void {
|
||||
const toggleSP = document.querySelector<HTMLInputElement>('#toggleSystemPrompt');
|
||||
const inputSP = document.querySelector<HTMLTextAreaElement>('#inputSystemPrompt');
|
||||
if (toggleSP) toggleSP.checked = !!preset.systemPrompt;
|
||||
if (inputSP) { inputSP.disabled = !preset.systemPrompt; inputSP.value = preset.systemPrompt || ''; }
|
||||
|
||||
const inputCtx = document.querySelector<HTMLInputElement>('#inputNumCtx');
|
||||
if (inputCtx) inputCtx.value = String(preset.numCtx || 24576);
|
||||
|
||||
const tempSlider = document.querySelector<HTMLInputElement>('#inputTemperature');
|
||||
const tempDisplay = document.querySelector('#tempValue');
|
||||
if (tempSlider) tempSlider.value = String(preset.temperature ?? 0.7);
|
||||
if (tempDisplay) tempDisplay.textContent = (preset.temperature ?? 0.7).toFixed(1);
|
||||
|
||||
const thinkCheckbox = document.querySelector<HTMLInputElement>('#toggleThink');
|
||||
if (thinkCheckbox) {
|
||||
const modelSupportsThink = !thinkCheckbox.disabled;
|
||||
thinkCheckbox.checked = modelSupportsThink && (preset.think ?? false);
|
||||
}
|
||||
}
|
||||
|
||||
export async function createPreset(data: { name: string; icon: string; systemPrompt: string; temperature: number; numCtx: number; think: boolean }): Promise<Preset> {
|
||||
const preset: Preset = {
|
||||
id: `preset_${generateId()}`,
|
||||
name: data.name || '新预设',
|
||||
icon: data.icon || '🤖',
|
||||
systemPrompt: data.systemPrompt || '',
|
||||
temperature: data.temperature ?? 0.7,
|
||||
numCtx: data.numCtx ?? 24576,
|
||||
think: data.think ?? false,
|
||||
createdAt: Date.now(),
|
||||
updatedAt: Date.now()
|
||||
};
|
||||
|
||||
presets.push(preset);
|
||||
state.set('presets', [...presets]);
|
||||
await saveUserPresets();
|
||||
return preset;
|
||||
}
|
||||
|
||||
export async function updatePreset(presetId: string, updates: Partial<Preset>): Promise<void> {
|
||||
const idx = presets.findIndex(p => p.id === presetId);
|
||||
if (idx === -1) return;
|
||||
if (presets[idx].builtIn) return;
|
||||
|
||||
presets[idx] = { ...presets[idx], ...updates, updatedAt: Date.now() };
|
||||
state.set('presets', [...presets]);
|
||||
|
||||
if (activePresetId === presetId) {
|
||||
applyPresetToState(presets[idx], true);
|
||||
}
|
||||
|
||||
await saveUserPresets();
|
||||
}
|
||||
|
||||
export async function deletePreset(presetId: string): Promise<void> {
|
||||
const idx = presets.findIndex(p => p.id === presetId);
|
||||
if (idx === -1) return;
|
||||
if (presets[idx].builtIn) return;
|
||||
|
||||
presets.splice(idx, 1);
|
||||
state.set('presets', [...presets]);
|
||||
|
||||
if (activePresetId === presetId) {
|
||||
await activatePreset('_default');
|
||||
}
|
||||
|
||||
await saveUserPresets();
|
||||
}
|
||||
|
||||
async function saveUserPresets(): Promise<void> {
|
||||
const db = state.get<ChatDB | null>(KEYS.DB);
|
||||
if (!db) return;
|
||||
|
||||
const userPresets = presets
|
||||
.filter(p => !p.builtIn)
|
||||
.map(({ builtIn, ...rest }) => rest);
|
||||
|
||||
await db.saveSetting(PRESETS_KEY, userPresets);
|
||||
}
|
||||
@@ -2,7 +2,7 @@
|
||||
* State - 轻量级响应式状态管理
|
||||
*/
|
||||
|
||||
import type { StateKey, ChatSession, ChatDB, OllamaAPI, Preset } from '../types.js';
|
||||
import type { StateKey, ChatSession, ChatDB, OllamaAPI } from '../types.js';
|
||||
|
||||
function shallowEqual(a: unknown, b: unknown): boolean {
|
||||
if (a === b) return true;
|
||||
|
||||
@@ -376,116 +376,8 @@ html, body {
|
||||
border-color: rgba(179, 136, 255, 0.15);
|
||||
}
|
||||
|
||||
/* ── 预设栏 ── */
|
||||
.preset-bar {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 8px;
|
||||
padding: 6px 16px;
|
||||
background: var(--bg-layer-alt);
|
||||
border-bottom: 1px solid var(--border-subtle);
|
||||
flex-shrink: 0;
|
||||
}
|
||||
|
||||
.preset-list {
|
||||
display: flex;
|
||||
gap: 6px;
|
||||
flex: 1;
|
||||
overflow-x: auto;
|
||||
overflow-y: hidden;
|
||||
scrollbar-width: none;
|
||||
-ms-overflow-style: none;
|
||||
padding: 2px 0;
|
||||
}
|
||||
|
||||
.preset-list::-webkit-scrollbar { display: none; }
|
||||
|
||||
.preset-chip {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 5px;
|
||||
padding: 4px 12px;
|
||||
border-radius: var(--radius-control);
|
||||
border: 1px solid var(--border-default);
|
||||
background: var(--bg-layer);
|
||||
color: var(--text-secondary);
|
||||
font-size: 12px;
|
||||
font-family: var(--font);
|
||||
white-space: nowrap;
|
||||
cursor: pointer;
|
||||
transition: var(--transition);
|
||||
flex-shrink: 0;
|
||||
user-select: none;
|
||||
}
|
||||
|
||||
.preset-chip:hover {
|
||||
background: var(--bg-card-hover);
|
||||
border-color: var(--border-strong);
|
||||
color: var(--text-primary);
|
||||
}
|
||||
|
||||
.preset-chip.active {
|
||||
background: var(--accent-subtle);
|
||||
border-color: var(--accent);
|
||||
color: var(--accent);
|
||||
font-weight: 600;
|
||||
}
|
||||
|
||||
.preset-chip.active:hover {
|
||||
background: var(--accent-subtle-hover);
|
||||
}
|
||||
|
||||
.preset-chip-icon {
|
||||
font-size: 13px;
|
||||
line-height: 1;
|
||||
}
|
||||
|
||||
.preset-chip-name {
|
||||
line-height: 1;
|
||||
}
|
||||
|
||||
.preset-add-btn {
|
||||
width: 28px;
|
||||
height: 28px;
|
||||
border-radius: var(--radius-control);
|
||||
border: 1px dashed var(--border-default);
|
||||
background: transparent;
|
||||
color: var(--text-tertiary);
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
cursor: pointer;
|
||||
transition: var(--transition);
|
||||
flex-shrink: 0;
|
||||
}
|
||||
|
||||
.preset-add-btn:hover {
|
||||
border-color: var(--accent);
|
||||
color: var(--accent);
|
||||
background: var(--accent-subtle);
|
||||
}
|
||||
|
||||
.preset-add-btn svg {
|
||||
width: 14px;
|
||||
height: 14px;
|
||||
}
|
||||
|
||||
/* ── 预设模态框 ── */
|
||||
.preset-name-row {
|
||||
display: flex;
|
||||
gap: 8px;
|
||||
align-items: center;
|
||||
}
|
||||
|
||||
.preset-icon-input {
|
||||
width: 52px !important;
|
||||
text-align: center;
|
||||
font-size: 16px;
|
||||
flex-shrink: 0;
|
||||
padding: 6px 4px !important;
|
||||
}
|
||||
|
||||
.preset-temp-slider {
|
||||
/* ── 温度滑块 ── */
|
||||
.temp-slider {
|
||||
width: 100%;
|
||||
-webkit-appearance: none;
|
||||
appearance: none;
|
||||
@@ -496,7 +388,7 @@ html, body {
|
||||
margin: 8px 0;
|
||||
}
|
||||
|
||||
.preset-temp-slider::-webkit-slider-thumb {
|
||||
.temp-slider::-webkit-slider-thumb {
|
||||
-webkit-appearance: none;
|
||||
width: 16px;
|
||||
height: 16px;
|
||||
@@ -507,11 +399,11 @@ html, body {
|
||||
transition: box-shadow var(--transition);
|
||||
}
|
||||
|
||||
.preset-temp-slider::-webkit-slider-thumb:hover {
|
||||
.temp-slider::-webkit-slider-thumb:hover {
|
||||
box-shadow: 0 0 0 3px var(--accent-subtle);
|
||||
}
|
||||
|
||||
.preset-temp-slider::-moz-range-thumb {
|
||||
.temp-slider::-moz-range-thumb {
|
||||
width: 16px;
|
||||
height: 16px;
|
||||
border-radius: 50%;
|
||||
@@ -520,7 +412,7 @@ html, body {
|
||||
border: none;
|
||||
}
|
||||
|
||||
.preset-modal-actions {
|
||||
.modal-actions {
|
||||
display: flex;
|
||||
gap: 8px;
|
||||
align-items: center;
|
||||
|
||||
Vendored
-17
@@ -116,21 +116,6 @@ export interface ChatSession {
|
||||
updatedAt: number;
|
||||
}
|
||||
|
||||
// ── 预设类型(v3.0 废弃,保留用于兼容)──
|
||||
|
||||
export interface Preset {
|
||||
id: string;
|
||||
name: string;
|
||||
icon: string;
|
||||
systemPrompt: string;
|
||||
temperature: number;
|
||||
numCtx: number;
|
||||
think: boolean;
|
||||
builtIn?: boolean;
|
||||
createdAt: number;
|
||||
updatedAt: number;
|
||||
}
|
||||
|
||||
// ── Agent 记忆系统类型 ──
|
||||
|
||||
export type MemoryType = 'fact' | 'preference' | 'rule' | 'episode';
|
||||
@@ -278,8 +263,6 @@ export type StateKey =
|
||||
| 'historySearchQuery'
|
||||
| 'temperature'
|
||||
| 'thinkEnabled'
|
||||
| 'presets'
|
||||
| 'activePresetId'
|
||||
| 'toolCallingEnabled'
|
||||
| 'runCommandEnabled'
|
||||
| 'memoryEnabled'
|
||||
|
||||
Reference in New Issue
Block a user