import { sessionId, setSessionId, setCurrentPersona, currentPersona, dom, setRpgEnabled, } from './state.js'; import { updateQuestPanel, updateAffinityDisplay } from './chat.js'; import { highlightPersona, personaIndex } from './personas.js'; import { formatSessionDate } from './utils.js'; import { openNewChatWizard } from './newChatWizard.js'; export { openNewChatWizard, initNewChatWizard } from './newChatWizard.js'; export { openChatSettings, initChatSettings } from './chatSettings.js'; function escapeTitle(t) { const d = document.createElement('div'); d.textContent = t; return d.innerHTML; } export function applySessionUi(session) { if (!session) return; dom.headerTitle.textContent = session.title || 'Новый чат'; const rpgOn = !!session.rpg_enabled; setRpgEnabled(rpgOn); document.getElementById('rpgBadge')?.classList.toggle('hidden', !rpgOn); let settings = { quests: true, affinity: true }; try { settings = { ...settings, ...JSON.parse(session.rpg_settings_json || '{}') }; } catch { /* ignore */ } document.getElementById('questPanel')?.classList.toggle('hidden', !rpgOn || !settings.quests); if (rpgOn && settings.affinity) { updateAffinityDisplay(session.affinity ?? 0); dom.affinityDisplay?.classList.remove('hidden'); } else { dom.affinityDisplay?.classList.add('hidden'); } if (rpgOn && settings.quests) { fetch(`/sessions/${session.session_id}/quests`) .then(r => r.ok ? r.json() : []) .then(q => updateQuestPanel(q)) .catch(() => {}); } } export async function loadSessions() { const res = await fetch('/sessions/'); const sessions = await res.json(); dom.sessionList.innerHTML = ''; sessions.forEach(s => { const item = document.createElement('div'); item.className = 'session-item' + (s.session_id === sessionId ? ' active' : ''); const personaName = personaIndex.get(s.persona_id)?.name || s.persona_id || 'default'; const dateStr = formatSessionDate(s.updated_at); item.innerHTML = `
${escapeTitle(s.title || 'Новый чат')}
${s.rpg_enabled ? 'RPG' : ''} ${dateStr ? `${dateStr}` : ''}
С: ${escapeTitle(personaName)}
${escapeTitle(s.last_message_preview || '')}
${s.message_count} сообщ.
`; item.addEventListener('click', (e) => { if (e.target.closest('.s-del') || item.querySelector('.s-title')?.isContentEditable) return; switchSession(s.session_id); }); const titleEl = item.querySelector('.s-title'); titleEl.addEventListener('dblclick', (e) => { e.stopPropagation(); titleEl.contentEditable = 'true'; titleEl.focus(); }); titleEl.addEventListener('blur', async () => { titleEl.contentEditable = 'false'; const t = titleEl.textContent.trim(); if (t && t !== (s.title || 'Новый чат')) { await fetch(`/sessions/${s.session_id}`, { method: 'PATCH', headers: { 'Content-Type': 'application/json' }, body: JSON.stringify({ title: t }), }); if (s.session_id === sessionId) dom.headerTitle.textContent = t; loadSessions(); } }); titleEl.addEventListener('keydown', (e) => { if (e.key === 'Enter') { e.preventDefault(); titleEl.blur(); } }); item.querySelector('.s-del').addEventListener('click', async (e) => { e.stopPropagation(); await fetch(`/sessions/${s.session_id}`, { method: 'DELETE' }); if (s.session_id === sessionId) openNewChatWizard(); else loadSessions(); }); dom.sessionList.appendChild(item); }); } export async function switchSession(id) { setSessionId(id); const { clearMessages } = await import('./chat.js'); clearMessages(); await loadSessions(); await loadChatHistory(id); } export async function loadChatHistory(id) { const sessionRes = await fetch(`/sessions/${id}`); if (sessionRes.ok) { const s = await sessionRes.json(); if (s.persona_id) { setCurrentPersona(s.persona_id); highlightPersona(s.persona_id); } applySessionUi(s); } try { const blobRes = await fetch(`/chat/system/${id}`); if (blobRes.ok) { _prevBlobSections = {}; // reset on session switch to avoid false highlights renderSystemBlob(await blobRes.json()); } } catch { /* ignore */ } const { reloadChatFromServer } = await import('./chat.js'); await reloadChatFromServer(id); } export async function initSessions() { await loadSessions(); if (sessionId) { const check = await fetch(`/sessions/${sessionId}`); if (check.ok) await switchSession(sessionId); else openNewChatWizard(); } else { openNewChatWizard(); } dom.systemBlobRefresh?.addEventListener('click', async () => { if (!sessionId) return; dom.systemBlobRefresh.classList.add('spinning'); try { const res = await fetch(`/chat/system/${sessionId}`); if (res.ok) renderSystemBlob(await res.json()); } finally { dom.systemBlobRefresh.classList.remove('spinning'); } }); } let _prevBlobSections = {}; function renderSystemBlob(blob) { const tryFmt = (str, fallback = '') => { try { return JSON.stringify(JSON.parse(str), null, 2); } catch { return str || fallback; } }; const questLines = (blob.quests || []).map(q => { const icon = q.status === 'done' ? '✓' : q.status === 'failed' ? '✗' : '◆'; return ` ${icon} [${q.status}] ${q.title}`; }).join('\n'); const sections = { system_prompt: blob.system_prompt ? `[system_prompt]\n${blob.system_prompt}` : '', status_quo: blob.status_quo ? `[status_quo]\n${blob.status_quo}` : '', affinity: blob.affinity != null ? `[affinity] ${blob.affinity}` : '', genre: blob.genre ? `[genre] ${blob.genre}` : '', rpg_settings: blob.rpg_settings_json && blob.rpg_settings_json !== '{}' ? `[rpg_settings]\n${tryFmt(blob.rpg_settings_json)}` : '', outfit: blob.outfit_json && blob.outfit_json !== '[]' ? `[outfit]\n${tryFmt(blob.outfit_json)}` : '', facts: blob.facts_json && blob.facts_json !== '[]' ? `[facts]\n${tryFmt(blob.facts_json)}` : '', plot_arc: blob.plot_arc_json && blob.plot_arc_json !== '{}' ? `[plot_arc]\n${tryFmt(blob.plot_arc_json)}` : '', quests: questLines ? `[quests]\n${questLines}` : '', }; const el = dom.systemBlobContent; el.innerHTML = ''; for (const [key, text] of Object.entries(sections)) { if (!text) continue; const span = document.createElement('span'); span.textContent = text; if (_prevBlobSections[key] && _prevBlobSections[key] !== text) { span.className = 'blob-changed'; setTimeout(() => span.classList.remove('blob-changed'), 3000); } el.appendChild(span); el.appendChild(document.createTextNode('\n\n')); } if (!el.textContent.trim()) el.textContent = '—'; _prevBlobSections = { ...sections }; }