/** * Swarm Assistent — disk persistence for chats + UI state (Assistent/chats/, Assistent/ui-state.json). * Loaded after assistent.api.js and before assistent.js. */ window.SA = window.SA || {}; (function () { const LS_CHATS = 'swarm_assistent_chats_v1'; const LS_MIGRATED = 'swarm_assistent_chats_on_disk_v1'; const SAVE_DEBOUNCE_MS = 700; const timers = { chats: new Map(), ui: null }; function request(name, body) { if (typeof SA.request === 'function') { return SA.request(name, body); } return new Promise((resolve, reject) => { if (typeof genericRequest !== 'function') { reject(new Error('genericRequest unavailable')); return; } genericRequest( name, body || {}, (data) => (data && data.error ? reject(new Error(String(data.error))) : resolve(data)), 0, (err) => reject(err instanceof Error ? err : new Error(String(err || 'request failed'))), ); }); } function normalizeChat(raw) { if (!raw || !raw.id) { return null; } return { id: String(raw.id), title: String(raw.title || 'Новый чат'), createdAt: Number(raw.createdAt) || Date.now(), updatedAt: Number(raw.updatedAt) || Number(raw.createdAt) || Date.now(), messages: Array.isArray(raw.messages) ? raw.messages : [], params: raw.params && typeof raw.params === 'object' ? raw.params : null, }; } function readLocalChats() { try { const parsed = JSON.parse(localStorage.getItem(LS_CHATS) || 'null'); if (Array.isArray(parsed?.chats)) { return parsed.chats.map(normalizeChat).filter(Boolean); } } catch (e) { /* ignore */ } return []; } /** One-shot lift of the browser-only history onto the data volume. */ async function migrateLocalChatsToDisk() { if (localStorage.getItem(LS_MIGRATED) === '1') { return []; } const local = readLocalChats().filter((c) => (c.messages || []).length > 0); localStorage.setItem(LS_MIGRATED, '1'); if (!local.length) { return []; } for (const chat of local) { try { await saveChat(chat, { immediate: true }); } catch (e) { console.warn('Assistent: chat migration failed', chat.id, e); } } return local; } /** Disk chats, newest first. Falls back to a localStorage migration when the volume is empty. */ async function loadChats() { let chats = []; try { const data = await request('AssistentListChats', { with_messages: true }); chats = (data?.chats || []).map(normalizeChat).filter(Boolean); } catch (e) { console.warn('Assistent: disk chats unavailable', e); return null; } if (!chats.length) { const migrated = await migrateLocalChatsToDisk(); if (migrated.length) { chats = migrated; } } else { localStorage.setItem(LS_MIGRATED, '1'); } return chats.sort((a, b) => (b.updatedAt || 0) - (a.updatedAt || 0)); } async function getChat(id) { if (!id) { return null; } const data = await request('AssistentGetChat', { id }); return normalizeChat(data?.chat); } function saveChat(chat, { immediate = false } = {}) { const clean = normalizeChat(chat); if (!clean) { return Promise.resolve(null); } const send = () => { timers.chats.delete(clean.id); return request('AssistentSaveChat', { id: clean.id, title: clean.title, messages: clean.messages, params: clean.params, createdAt: clean.createdAt, updatedAt: clean.updatedAt, }); }; if (immediate) { const pending = timers.chats.get(clean.id); if (pending) { clearTimeout(pending); } return send(); } const pending = timers.chats.get(clean.id); if (pending) { clearTimeout(pending); } timers.chats.set(clean.id, setTimeout(() => { send().catch((e) => console.warn('Assistent: chat save failed', clean.id, e)); }, SAVE_DEBOUNCE_MS)); return Promise.resolve(null); } function deleteChat(id) { if (!id) { return Promise.resolve(null); } const pending = timers.chats.get(id); if (pending) { clearTimeout(pending); timers.chats.delete(id); } return request('AssistentDeleteChat', { id }); } async function loadUiState() { try { const data = await request('AssistentGetUiState', {}); const ui = data?.ui_state; return ui && typeof ui === 'object' ? ui : null; } catch (e) { return null; } } function saveUiState(uiState, { immediate = false } = {}) { if (!uiState || typeof uiState !== 'object') { return Promise.resolve(null); } const send = () => { timers.ui = null; return request('AssistentSaveUiState', { ui_state: uiState }); }; if (timers.ui) { clearTimeout(timers.ui); timers.ui = null; } if (immediate) { return send(); } timers.ui = setTimeout(() => { send().catch((e) => console.warn('Assistent: ui-state save failed', e)); }, SAVE_DEBOUNCE_MS); return Promise.resolve(null); } SA.persist = { LS_CHATS, loadChats, getChat, saveChat, deleteChat, loadUiState, saveUiState, }; })();