import {
sessionId, setSessionId, setCurrentPersona, currentPersona, dom, setRpgEnabled,
} from './state.js';
import {
updateQuestPanel, updateAffinityDisplay, updateStatsDisplay, hideStatsDisplay,
} from './chat.js';
import { highlightPersonaBar, 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.stats) {
try {
updateStatsDisplay(JSON.parse(session.narrative_stats_json || '{}'));
} catch {
hideStatsDisplay();
}
} else {
hideStatsDisplay();
}
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);
highlightPersonaBar(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 = {};
export 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 personaLine = blob.persona_id
? `[persona] ${blob.persona_name || blob.persona_id} (${blob.persona_id})`
: '';
const ctx = blob.context_usage;
const ctxLine = ctx
? `[context] ~${ctx.tokens_est} / ${ctx.max_tokens_est} tokens (${ctx.percent}%) · ${ctx.chars} chars`
: '';
const sections = {
context: ctxLine,
persona: personaLine,
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}` : '',
scene: blob.scene_json && blob.scene_json !== '{}' ? `[scene]\n${tryFmt(blob.scene_json)}` : '',
stats: blob.narrative_stats_json && blob.narrative_stats_json !== '{}' ? `[stats]\n${tryFmt(blob.narrative_stats_json)}` : '',
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: `[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 (key === 'context' && ctx && ctx.percent > 80) {
span.className = 'blob-context-warn';
} else 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 };
}