Fixed RPG
This commit is contained in:
+70
-329
@@ -1,13 +1,13 @@
|
||||
import {
|
||||
sessionId, setSessionId, setCurrentPersona, currentPersona, dom, setRpgEnabled,
|
||||
} from './state.js';
|
||||
import {
|
||||
clearMessages, addMessage, initChat, updateQuestPanel, updateAffinityDisplay, reloadChatFromServer,
|
||||
} from './chat.js';
|
||||
import { highlightPersona, personaIndex, loadPersonas } from './personas.js';
|
||||
import {
|
||||
initWizard, GENRE_LABELS, bindGenreGrid, resetGenreGrid, formatSessionDate,
|
||||
} from './utils.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');
|
||||
@@ -15,10 +15,6 @@ function escapeTitle(t) {
|
||||
return d.innerHTML;
|
||||
}
|
||||
|
||||
let newChatPersonaId = currentPersona;
|
||||
const newChatGenres = new Set();
|
||||
const chatSettingsGenres = new Set();
|
||||
|
||||
export function applySessionUi(session) {
|
||||
if (!session) return;
|
||||
dom.headerTitle.textContent = session.title || 'Новый чат';
|
||||
@@ -106,6 +102,7 @@ export async function loadSessions() {
|
||||
|
||||
export async function switchSession(id) {
|
||||
setSessionId(id);
|
||||
const { clearMessages } = await import('./chat.js');
|
||||
clearMessages();
|
||||
await loadSessions();
|
||||
await loadChatHistory(id);
|
||||
@@ -113,337 +110,27 @@ export async function switchSession(id) {
|
||||
|
||||
export async function loadChatHistory(id) {
|
||||
const sessionRes = await fetch(`/sessions/${id}`);
|
||||
let session = null;
|
||||
if (sessionRes.ok) {
|
||||
session = await sessionRes.json();
|
||||
if (session.persona_id) {
|
||||
setCurrentPersona(session.persona_id);
|
||||
highlightPersona(session.persona_id);
|
||||
const s = await sessionRes.json();
|
||||
if (s.persona_id) {
|
||||
setCurrentPersona(s.persona_id);
|
||||
highlightPersona(s.persona_id);
|
||||
}
|
||||
applySessionUi(session);
|
||||
applySessionUi(s);
|
||||
}
|
||||
|
||||
try {
|
||||
const blobRes = await fetch(`/chat/system/${id}`);
|
||||
if (blobRes.ok) {
|
||||
const blob = await blobRes.json();
|
||||
const parts = [];
|
||||
if (blob.system_prompt) parts.push(blob.system_prompt);
|
||||
if (blob.status_quo) parts.push(`--- Status quo ---\n${blob.status_quo}\n---`);
|
||||
if (blob.facts_json) parts.push(`facts_json: ${blob.facts_json}`);
|
||||
if (blob.plot_arc_json) parts.push(`plot_arc_json: ${blob.plot_arc_json}`);
|
||||
dom.systemBlobContent.textContent = parts.filter(Boolean).join('\n\n') || '—';
|
||||
_prevBlobSections = {}; // reset on session switch to avoid false highlights
|
||||
renderSystemBlob(await blobRes.json());
|
||||
}
|
||||
} catch { /* ignore */ }
|
||||
|
||||
const { reloadChatFromServer } = await import('./chat.js');
|
||||
await reloadChatFromServer(id);
|
||||
}
|
||||
|
||||
async function bootstrapRpg(sid, personaId, genreValue, settings) {
|
||||
await fetch(`/sessions/${sid}`, {
|
||||
method: 'PATCH',
|
||||
headers: { 'Content-Type': 'application/json' },
|
||||
body: JSON.stringify({
|
||||
rpg_enabled: true,
|
||||
genre: genreValue,
|
||||
rpg_settings_json: JSON.stringify(settings),
|
||||
}),
|
||||
});
|
||||
const res = await fetch('/chat/rpg/bootstrap', {
|
||||
method: 'POST',
|
||||
headers: { 'Content-Type': 'application/json' },
|
||||
body: JSON.stringify({ session_id: sid, persona_id: personaId, genre: genreValue }),
|
||||
});
|
||||
if (res.ok) {
|
||||
const data = await res.json();
|
||||
if (data.quests) updateQuestPanel(data.quests);
|
||||
if (data.plot_arc) {
|
||||
const title = data.plot_arc.title || '';
|
||||
const hint = data.plot_arc.next_beat_hint || '';
|
||||
if (title || hint) addMessage('assistant', `📖 ${title}${hint ? '\n' + hint : ''}`);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
function fillNewChatPersonaGrid() {
|
||||
const grid = document.getElementById('newChatPersonaGrid');
|
||||
if (!grid) return;
|
||||
grid.innerHTML = '';
|
||||
newChatPersonaId = currentPersona;
|
||||
for (const p of personaIndex.values()) {
|
||||
const card = document.createElement('button');
|
||||
card.type = 'button';
|
||||
card.className = 'persona-pick-card' + (p.persona_id === newChatPersonaId ? ' selected' : '');
|
||||
card.dataset.id = p.persona_id;
|
||||
card.innerHTML = `<span class="emoji">${p.emoji || '🤖'}</span>${p.name}`;
|
||||
card.addEventListener('click', () => {
|
||||
newChatPersonaId = p.persona_id;
|
||||
grid.querySelectorAll('.persona-pick-card').forEach(c => {
|
||||
c.classList.toggle('selected', c.dataset.id === newChatPersonaId);
|
||||
});
|
||||
});
|
||||
grid.appendChild(card);
|
||||
}
|
||||
}
|
||||
|
||||
function updateNewChatGenresLabel() {
|
||||
const el = document.getElementById('newChatGenresLabel');
|
||||
const nextBtn = document.getElementById('newChatNext');
|
||||
const labels = [...newChatGenres].map(g => GENRE_LABELS[g] || g);
|
||||
if (el) {
|
||||
if (labels.length) {
|
||||
el.textContent = `Выбрано: ${labels.join(' + ')}`;
|
||||
el.classList.remove('hidden');
|
||||
} else {
|
||||
el.classList.add('hidden');
|
||||
}
|
||||
}
|
||||
const isRpg = document.querySelector('input[name="newChatRpg"]:checked')?.value === '1';
|
||||
if (nextBtn && isRpg) {
|
||||
const wizard = newChatModalEl?.querySelector('.modal-wizard');
|
||||
const onStep3 = wizard?.querySelector('.wizard-page[data-step="3"]')?.classList.contains('active');
|
||||
if (onStep3) nextBtn.disabled = newChatGenres.size === 0;
|
||||
}
|
||||
}
|
||||
|
||||
const newChatModalEl = document.getElementById('newChatModal');
|
||||
let newChatWizard;
|
||||
|
||||
function isNewChatRpg() {
|
||||
return document.querySelector('input[name="newChatRpg"]:checked')?.value === '1';
|
||||
}
|
||||
|
||||
function syncNewChatStep3() {
|
||||
const plain = document.getElementById('newChatPlainStep');
|
||||
const rpg = document.getElementById('newChatRpgStep');
|
||||
if (isNewChatRpg()) {
|
||||
plain?.classList.add('hidden');
|
||||
rpg?.classList.remove('hidden');
|
||||
} else {
|
||||
plain?.classList.remove('hidden');
|
||||
rpg?.classList.add('hidden');
|
||||
}
|
||||
}
|
||||
|
||||
export function openNewChatWizard() {
|
||||
fillNewChatPersonaGrid();
|
||||
resetGenreGrid(document.getElementById('newChatGenreGrid'), newChatGenres);
|
||||
updateNewChatGenresLabel();
|
||||
document.querySelector('input[name="newChatRpg"][value="0"]')?.click();
|
||||
document.getElementById('newChatTitle').value = '';
|
||||
syncNewChatStep3();
|
||||
newChatWizard?.reset();
|
||||
newChatModalEl?.classList.add('open');
|
||||
}
|
||||
|
||||
export async function createNewChatFromWizard() {
|
||||
const sid = 'sess_' + Math.random().toString(36).slice(2, 10);
|
||||
setSessionId(sid);
|
||||
setCurrentPersona(newChatPersonaId);
|
||||
clearMessages();
|
||||
|
||||
const customTitle = document.getElementById('newChatTitle')?.value.trim();
|
||||
const rpg = isNewChatRpg();
|
||||
|
||||
await fetch(`/sessions/${sid}`, {
|
||||
method: 'PATCH',
|
||||
headers: { 'Content-Type': 'application/json' },
|
||||
body: JSON.stringify({ persona_id: newChatPersonaId, rpg_enabled: rpg }),
|
||||
});
|
||||
|
||||
if (customTitle) {
|
||||
await fetch(`/sessions/${sid}`, {
|
||||
method: 'PATCH',
|
||||
headers: { 'Content-Type': 'application/json' },
|
||||
body: JSON.stringify({ title: customTitle }),
|
||||
});
|
||||
dom.headerTitle.textContent = customTitle;
|
||||
} else {
|
||||
const pName = personaIndex.get(newChatPersonaId)?.name || newChatPersonaId;
|
||||
dom.headerTitle.textContent = rpg ? `${pName} — RPG` : `${pName} — новый чат`;
|
||||
}
|
||||
|
||||
highlightPersona(newChatPersonaId);
|
||||
await initChat();
|
||||
|
||||
if (rpg) {
|
||||
const genreValue = [...newChatGenres].join(',') || 'adventure';
|
||||
const settings = {
|
||||
dice: document.getElementById('ncSettingDice')?.checked ?? true,
|
||||
narrator: document.getElementById('ncSettingNarrator')?.checked ?? true,
|
||||
quests: document.getElementById('ncSettingQuests')?.checked ?? true,
|
||||
affinity: document.getElementById('ncSettingAffinity')?.checked ?? true,
|
||||
choices: document.getElementById('ncSettingChoices')?.checked ?? true,
|
||||
};
|
||||
await bootstrapRpg(sid, newChatPersonaId, genreValue, settings);
|
||||
}
|
||||
|
||||
await reloadChatFromServer(sid);
|
||||
|
||||
newChatModalEl?.classList.remove('open');
|
||||
const sessionRes = await fetch(`/sessions/${sid}`);
|
||||
if (sessionRes.ok) applySessionUi(await sessionRes.json());
|
||||
await loadSessions();
|
||||
}
|
||||
|
||||
export function initNewChatWizard() {
|
||||
if (!newChatModalEl) return;
|
||||
|
||||
newChatWizard = initWizard(newChatModalEl.querySelector('.modal-wizard'), {
|
||||
totalSteps: 3,
|
||||
onStepChange(step) {
|
||||
syncNewChatStep3();
|
||||
const nextBtn = document.getElementById('newChatNext');
|
||||
if (step === 3 && isNewChatRpg()) {
|
||||
nextBtn.disabled = newChatGenres.size === 0;
|
||||
} else {
|
||||
nextBtn.disabled = false;
|
||||
}
|
||||
},
|
||||
validateStep(step) {
|
||||
if (step === 1 && !newChatPersonaId) {
|
||||
alert('Выбери персонажа');
|
||||
return false;
|
||||
}
|
||||
if (step === 3 && isNewChatRpg() && newChatGenres.size === 0) {
|
||||
alert('Выбери хотя бы один жанр');
|
||||
return false;
|
||||
}
|
||||
return true;
|
||||
},
|
||||
});
|
||||
|
||||
bindGenreGrid(document.getElementById('newChatGenreGrid'), newChatGenres, updateNewChatGenresLabel);
|
||||
|
||||
document.getElementById('newChatCancel')?.addEventListener('click', () => {
|
||||
newChatModalEl.classList.remove('open');
|
||||
newChatWizard.reset();
|
||||
});
|
||||
document.getElementById('newChatCreate')?.addEventListener('click', createNewChatFromWizard);
|
||||
}
|
||||
|
||||
function updateChatSettingsGenresLabel() {
|
||||
const el = document.getElementById('chatSettingsGenresLabel');
|
||||
const labels = [...chatSettingsGenres].map(g => GENRE_LABELS[g] || g);
|
||||
if (!el) return;
|
||||
if (labels.length) {
|
||||
el.textContent = `Выбрано: ${labels.join(' + ')}`;
|
||||
el.classList.remove('hidden');
|
||||
} else {
|
||||
el.classList.add('hidden');
|
||||
}
|
||||
}
|
||||
|
||||
function loadRpgSettingsToDom(prefix, settings) {
|
||||
document.getElementById(`${prefix}SettingDice`).checked = settings.dice !== false;
|
||||
document.getElementById(`${prefix}SettingNarrator`).checked = settings.narrator !== false;
|
||||
document.getElementById(`${prefix}SettingQuests`).checked = settings.quests !== false;
|
||||
document.getElementById(`${prefix}SettingAffinity`).checked = settings.affinity !== false;
|
||||
document.getElementById(`${prefix}SettingChoices`).checked = settings.choices !== false;
|
||||
}
|
||||
|
||||
function readRpgSettingsFromDom(prefix) {
|
||||
return {
|
||||
dice: document.getElementById(`${prefix}SettingDice`)?.checked ?? true,
|
||||
narrator: document.getElementById(`${prefix}SettingNarrator`)?.checked ?? true,
|
||||
quests: document.getElementById(`${prefix}SettingQuests`)?.checked ?? true,
|
||||
affinity: document.getElementById(`${prefix}SettingAffinity`)?.checked ?? true,
|
||||
choices: document.getElementById(`${prefix}SettingChoices`)?.checked ?? true,
|
||||
};
|
||||
}
|
||||
|
||||
export async function openChatSettings() {
|
||||
if (!sessionId) return;
|
||||
const res = await fetch(`/sessions/${sessionId}`);
|
||||
if (!res.ok) return;
|
||||
const s = await res.json();
|
||||
|
||||
document.getElementById('chatSettingsTitle').value = s.title || '';
|
||||
const rpgOn = !!s.rpg_enabled;
|
||||
document.getElementById('chatSettingsRpg').checked = rpgOn;
|
||||
document.getElementById('chatSettingsRpgBlock').classList.toggle('hidden', !rpgOn);
|
||||
|
||||
chatSettingsGenres.clear();
|
||||
(s.genre || 'adventure').split(',').forEach(g => {
|
||||
const t = g.trim();
|
||||
if (t) chatSettingsGenres.add(t);
|
||||
});
|
||||
resetGenreGrid(document.getElementById('chatSettingsGenreGrid'), chatSettingsGenres);
|
||||
document.getElementById('chatSettingsGenreGrid')?.querySelectorAll('.genre-btn').forEach(btn => {
|
||||
if (chatSettingsGenres.has(btn.dataset.genre)) btn.classList.add('selected');
|
||||
});
|
||||
updateChatSettingsGenresLabel();
|
||||
|
||||
let settings = {};
|
||||
try { settings = JSON.parse(s.rpg_settings_json || '{}'); } catch { /* ignore */ }
|
||||
loadRpgSettingsToDom('cs', settings);
|
||||
|
||||
let phase = '';
|
||||
try {
|
||||
const arc = JSON.parse(s.plot_arc_json || '{}');
|
||||
phase = arc.phase || '';
|
||||
} catch { /* ignore */ }
|
||||
document.getElementById('chatSettingsMeta').innerHTML = [
|
||||
`Симпатия: ${s.affinity ?? 0}`,
|
||||
s.genre ? `Жанр: ${(s.genre || '').split(',').map(g => GENRE_LABELS[g.trim()] || g).join(' + ')}` : '',
|
||||
phase ? `Фаза арки: ${phase}` : '',
|
||||
].filter(Boolean).join('<br>');
|
||||
|
||||
document.getElementById('chatSettingsModal').classList.add('open');
|
||||
}
|
||||
|
||||
export function initChatSettings() {
|
||||
bindGenreGrid(
|
||||
document.getElementById('chatSettingsGenreGrid'),
|
||||
chatSettingsGenres,
|
||||
updateChatSettingsGenresLabel,
|
||||
);
|
||||
|
||||
document.getElementById('chatSettingsRpg')?.addEventListener('change', (e) => {
|
||||
document.getElementById('chatSettingsRpgBlock').classList.toggle('hidden', !e.target.checked);
|
||||
});
|
||||
|
||||
document.getElementById('chatSettingsCancel')?.addEventListener('click', () => {
|
||||
document.getElementById('chatSettingsModal').classList.remove('open');
|
||||
});
|
||||
|
||||
document.getElementById('chatSettingsSave')?.addEventListener('click', async () => {
|
||||
if (!sessionId) return;
|
||||
const title = document.getElementById('chatSettingsTitle').value.trim();
|
||||
const rpgOn = document.getElementById('chatSettingsRpg').checked;
|
||||
const genreValue = [...chatSettingsGenres].join(',') || 'adventure';
|
||||
const settings = readRpgSettingsFromDom('cs');
|
||||
|
||||
const body = {
|
||||
title: title || undefined,
|
||||
rpg_enabled: rpgOn,
|
||||
genre: genreValue,
|
||||
rpg_settings_json: JSON.stringify(settings),
|
||||
};
|
||||
await fetch(`/sessions/${sessionId}`, {
|
||||
method: 'PATCH',
|
||||
headers: { 'Content-Type': 'application/json' },
|
||||
body: JSON.stringify(body),
|
||||
});
|
||||
|
||||
if (rpgOn) {
|
||||
const sessionRes = await fetch(`/sessions/${sessionId}`);
|
||||
const s = sessionRes.ok ? await sessionRes.json() : {};
|
||||
let arc = {};
|
||||
try { arc = JSON.parse(s.plot_arc_json || '{}'); } catch { /* ignore */ }
|
||||
if (!arc || !Object.keys(arc).length) {
|
||||
await bootstrapRpg(sessionId, currentPersona, genreValue, settings);
|
||||
}
|
||||
}
|
||||
|
||||
document.getElementById('chatSettingsModal').classList.remove('open');
|
||||
const updated = await (await fetch(`/sessions/${sessionId}`)).json();
|
||||
applySessionUi(updated);
|
||||
dom.headerTitle.textContent = updated.title || 'Новый чат';
|
||||
await loadSessions();
|
||||
});
|
||||
}
|
||||
|
||||
export async function initSessions() {
|
||||
await loadSessions();
|
||||
if (sessionId) {
|
||||
@@ -453,4 +140,58 @@ export async function initSessions() {
|
||||
} 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 };
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user