243 lines
8.3 KiB
JavaScript
243 lines
8.3 KiB
JavaScript
/**
|
|
* App - 主入口模块
|
|
* 负责初始化所有组件、协调事件流、管理生命周期
|
|
*/
|
|
|
|
import { ChatDB } from './chat-db.js';
|
|
import { OllamaAPI } from './ollama-api.js';
|
|
import { state, KEYS } from './state.js';
|
|
import { generateId } from './utils.js';
|
|
|
|
// 组件
|
|
import { initToast, showToast } from './components/toast.js';
|
|
import { initLightbox, closeLightbox } from './components/lightbox.js';
|
|
import { initHeader, checkConnection } from './components/header.js';
|
|
import { initModelBar, loadModels, setSelectedModel } from './components/model-bar.js';
|
|
import { initChatArea, renderMessages, clearMessages, enableAutoScroll } from './components/chat-area.js';
|
|
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 './preset-manager.js';
|
|
import { initVectorStore } from './rag.js';
|
|
|
|
function initHelpModal() {
|
|
const helpModal = document.querySelector('#helpModal');
|
|
document.querySelector('#btnHelp').addEventListener('click', () => {
|
|
helpModal.style.display = '';
|
|
});
|
|
document.querySelector('#btnCloseHelp').addEventListener('click', () => {
|
|
helpModal.style.display = 'none';
|
|
});
|
|
helpModal.addEventListener('click', (e) => {
|
|
if (e.target === helpModal) helpModal.style.display = 'none';
|
|
});
|
|
}
|
|
|
|
console.log('[Metona] 模块化版本 v4.0.3 ' + new Date().toISOString());
|
|
|
|
// ── 会话管理 ──
|
|
|
|
function createNewSession() {
|
|
const selectedModel = document.querySelector('#modelSelect')?.value || '';
|
|
const defaultModel = selectedModel || state.get('_defaultModel', '');
|
|
return {
|
|
id: generateId(),
|
|
title: '新会话',
|
|
model: defaultModel,
|
|
messages: [],
|
|
createdAt: Date.now(),
|
|
updatedAt: Date.now()
|
|
};
|
|
}
|
|
|
|
async function startNewSession() {
|
|
// 先保存当前会话(如果有的话)
|
|
const db = state.get(KEYS.DB);
|
|
const currentSession = state.get(KEYS.CURRENT_SESSION);
|
|
if (db && currentSession && currentSession.messages.length > 0) {
|
|
currentSession.updatedAt = Date.now();
|
|
await db.saveSession(currentSession);
|
|
}
|
|
|
|
// 释放显存,清理模型上下文
|
|
const api = state.get(KEYS.API);
|
|
const model = currentSession?.model || state.get('_defaultModel', '');
|
|
if (api && model) {
|
|
try {
|
|
await api.chat({ model, messages: [], keep_alive: 0 });
|
|
} catch (e) {
|
|
console.warn('[App] 释放显存失败:', e);
|
|
}
|
|
}
|
|
|
|
const session = createNewSession();
|
|
state.set(KEYS.CURRENT_SESSION, session);
|
|
state.set(KEYS.IS_HISTORY_VIEW, false);
|
|
|
|
document.querySelector('#historyBar').style.display = 'none';
|
|
document.querySelector('#inputArea').style.display = '';
|
|
|
|
clearMessages();
|
|
enableAutoScroll();
|
|
renderMessages();
|
|
document.querySelector('#chatInput')?.focus();
|
|
}
|
|
|
|
// ── 初始化 ──
|
|
|
|
async function init() {
|
|
let db, api;
|
|
|
|
try {
|
|
// 1. 初始化 IndexedDB
|
|
db = new ChatDB();
|
|
await db.init();
|
|
state.set(KEYS.DB, db);
|
|
|
|
// 2. 读取保存的设置
|
|
const serverUrl = await db.getSetting('serverUrl', 'http://127.0.0.1:11434');
|
|
api = new OllamaAPI(serverUrl);
|
|
state.set(KEYS.API, api);
|
|
|
|
// 3. 创建初始会话
|
|
state.set(KEYS.CURRENT_SESSION, createNewSession());
|
|
state.set(KEYS.IS_STREAMING, false);
|
|
state.set(KEYS.IS_HISTORY_VIEW, false);
|
|
state.set(KEYS.NUM_CTX, 24576);
|
|
|
|
// 4. 初始化所有组件
|
|
initToast();
|
|
initLightbox();
|
|
initHeader();
|
|
initModelBar();
|
|
initChatArea();
|
|
initInputArea();
|
|
initSettingsModal();
|
|
initHistoryModal();
|
|
initKBModal();
|
|
initPresetBar();
|
|
initPresetModal();
|
|
initHelpModal();
|
|
|
|
// 5. 绑定全局事件
|
|
bindGlobalEvents();
|
|
|
|
// 6. 检测连接 + 加载模型
|
|
await checkConnection();
|
|
await loadModels();
|
|
|
|
// 6.1 初始化向量存储
|
|
await initVectorStore();
|
|
|
|
// 6.2 初始化预设管理器(会从 DB 加载并应用上次激活的预设)
|
|
await initPresetManager();
|
|
|
|
// 7. 恢复上次选择的模型
|
|
const savedModel = await db.getSetting('selectedModel', '');
|
|
if (savedModel) {
|
|
setSelectedModel(savedModel);
|
|
state.set('_defaultModel', savedModel);
|
|
await db.saveSetting('selectedModel', savedModel);
|
|
}
|
|
|
|
// 8. 恢复系统提示词、上下文长度、温度
|
|
const systemPromptEnabled = await db.getSetting('systemPromptEnabled', false);
|
|
const systemPrompt = await db.getSetting('systemPrompt', '');
|
|
const numCtx = await db.getSetting('numCtx', 24576);
|
|
const temperature = await db.getSetting('temperature', 0.7);
|
|
|
|
state.set(KEYS.SYSTEM_PROMPT_ENABLED, systemPromptEnabled);
|
|
state.set(KEYS.SYSTEM_PROMPT, systemPrompt);
|
|
state.set(KEYS.NUM_CTX, numCtx);
|
|
state.set('temperature', temperature);
|
|
|
|
document.querySelector('#toggleSystemPrompt').checked = systemPromptEnabled;
|
|
document.querySelector('#inputSystemPrompt').disabled = !systemPromptEnabled;
|
|
document.querySelector('#inputSystemPrompt').value = systemPrompt;
|
|
document.querySelector('#inputNumCtx').value = numCtx;
|
|
document.querySelector('#inputTemperature').value = temperature;
|
|
document.querySelector('#tempValue').textContent = parseFloat(temperature).toFixed(1);
|
|
|
|
// 9. 聚焦输入框
|
|
document.querySelector('#chatInput')?.focus();
|
|
|
|
// 10. 注册 Service Worker (PWA)
|
|
initServiceWorker();
|
|
|
|
console.log('[App] 初始化完成');
|
|
} catch (err) {
|
|
console.error('[App] 初始化失败:', err);
|
|
// 即使 DB 失败也尝试运行
|
|
if (!api) {
|
|
api = new OllamaAPI();
|
|
state.set(KEYS.API, api);
|
|
}
|
|
state.set(KEYS.CURRENT_SESSION, createNewSession());
|
|
bindGlobalEvents();
|
|
initToast();
|
|
initLightbox();
|
|
initHeader();
|
|
initModelBar();
|
|
initChatArea();
|
|
initInputArea();
|
|
initSettingsModal();
|
|
initHistoryModal();
|
|
initPresetBar();
|
|
initPresetModal();
|
|
initPresetManager();
|
|
checkConnection().then(loadModels);
|
|
}
|
|
}
|
|
|
|
function bindGlobalEvents() {
|
|
// 新建会话
|
|
document.querySelector('#btnNewChat')?.addEventListener('click', startNewSession);
|
|
|
|
// ESC 关闭模态框
|
|
document.addEventListener('keydown', (e) => {
|
|
if (e.key === 'Escape') {
|
|
closeSettingsModal();
|
|
closeHistoryModal();
|
|
closeLightbox();
|
|
document.querySelector('#helpModal').style.display = 'none';
|
|
document.querySelector('#kbModal').style.display = 'none';
|
|
}
|
|
});
|
|
|
|
// 全局错误捕获
|
|
window.addEventListener('error', (e) => {
|
|
console.error('[App] 未捕获错误:', e.error || e.message);
|
|
showToast(`发生错误: ${e.message}`, 'error', 5000);
|
|
});
|
|
|
|
window.addEventListener('unhandledrejection', (e) => {
|
|
console.error('[App] 未处理 Promise 拒绝:', e.reason);
|
|
const msg = e.reason?.message || String(e.reason);
|
|
showToast(`操作失败: ${msg}`, 'error', 5000);
|
|
});
|
|
}
|
|
|
|
function initServiceWorker() {
|
|
if (!('serviceWorker' in navigator)) return;
|
|
|
|
navigator.serviceWorker.getRegistrations()
|
|
.then(regs => regs.forEach(r => r.unregister()))
|
|
.then(() => caches.keys().then(keys => Promise.all(keys.map(k => caches.delete(k)))))
|
|
.then(() => navigator.serviceWorker.register('sw.js?v=3', { updateViaCache: 'none' }))
|
|
.then(reg => {
|
|
reg.addEventListener('updatefound', () => {
|
|
const w = reg.installing;
|
|
if (w) w.addEventListener('statechange', () => {
|
|
if (w.state === 'activated') window.location.reload();
|
|
});
|
|
});
|
|
reg.update();
|
|
})
|
|
.catch(err => console.warn('[App] Service Worker 注册失败:', err));
|
|
}
|
|
|
|
// 启动
|
|
init();
|