Files
ChatAIBot/static/js/sessions.js
T
2026-06-01 07:44:38 +03:00

198 lines
7.7 KiB
JavaScript
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
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 = `
<div class="s-row">
<div class="s-title" title="Двойной клик — переименовать">${escapeTitle(s.title || 'Новый чат')}</div>
${s.rpg_enabled ? '<span class="s-badge">RPG</span>' : ''}
${dateStr ? `<span class="s-date">${dateStr}</span>` : ''}
</div>
<div class="s-companion">С: ${escapeTitle(personaName)}</div>
<div class="s-preview">${escapeTitle(s.last_message_preview || '')}</div>
<div class="s-meta">${s.message_count} сообщ.</div>
<button class="s-del" type="button">🗑</button>
`;
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 };
}