198 lines
7.7 KiB
JavaScript
198 lines
7.7 KiB
JavaScript
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 };
|
||
}
|