288 lines
13 KiB
JavaScript
288 lines
13 KiB
JavaScript
import { sessionId, currentPersona, setCurrentPersona, dom } from './state.js';
|
|
import { GENRE_LABELS, bindGenreGrid, resetGenreGrid } from './utils.js';
|
|
import { personaIndex } from './personas.js';
|
|
|
|
const chatSettingsGenres = new Set();
|
|
let chatSettingsPersonaId = 'default';
|
|
let chatSettingsInitialPersonaId = 'default';
|
|
|
|
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 fillChatSettingsPersonaGrid() {
|
|
const grid = document.getElementById('chatSettingsPersonaGrid');
|
|
if (!grid) return;
|
|
grid.innerHTML = '';
|
|
for (const p of personaIndex.values()) {
|
|
const card = document.createElement('button');
|
|
card.type = 'button';
|
|
card.className = 'persona-pick-card' + (p.persona_id === chatSettingsPersonaId ? ' selected' : '');
|
|
card.dataset.id = p.persona_id;
|
|
card.innerHTML = `<span class="emoji">${p.emoji || '🤖'}</span>${p.name}`;
|
|
card.addEventListener('click', () => {
|
|
chatSettingsPersonaId = p.persona_id;
|
|
grid.querySelectorAll('.persona-pick-card').forEach(c => {
|
|
c.classList.toggle('selected', c.dataset.id === chatSettingsPersonaId);
|
|
});
|
|
});
|
|
grid.appendChild(card);
|
|
}
|
|
}
|
|
|
|
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}SettingStats`).checked = settings.stats === true;
|
|
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,
|
|
stats: document.getElementById(`${prefix}SettingStats`)?.checked ?? false,
|
|
choices: document.getElementById(`${prefix}SettingChoices`)?.checked ?? true,
|
|
};
|
|
}
|
|
|
|
async function bootstrapRpg(sid, personaId, genreValue, settings) {
|
|
const { updateQuestPanel, addMessage } = await import('./chat.js');
|
|
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.affinity !== undefined) {
|
|
const { updateAffinityDisplay } = await import('./chat.js');
|
|
updateAffinityDisplay(data.affinity);
|
|
}
|
|
if (data.quests) updateQuestPanel(data.quests, data.plot_arc ?? null);
|
|
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 : ''}`);
|
|
}
|
|
}
|
|
}
|
|
|
|
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 || '';
|
|
chatSettingsPersonaId = s.persona_id || 'default';
|
|
chatSettingsInitialPersonaId = chatSettingsPersonaId;
|
|
fillChatSettingsPersonaGrid();
|
|
|
|
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 */ }
|
|
let stats = { lust: 0, stamina: 10, tension: 0 };
|
|
try {
|
|
stats = { ...stats, ...JSON.parse(s.narrative_stats_json || '{}') };
|
|
} catch { /* ignore */ }
|
|
|
|
document.getElementById('chatSettingsMeta').innerHTML = [
|
|
`Симпатия: ${s.affinity ?? 0}`,
|
|
settings.stats
|
|
? `Шкалы: lust ${stats.lust ?? 0}, stamina ${stats.stamina ?? 10}, tension ${stats.tension ?? 0}`
|
|
: '',
|
|
s.genre ? `Жанр: ${(s.genre || '').split(',').map(g => GENRE_LABELS[g.trim()] || g).join(' + ')}` : '',
|
|
phase ? `Фаза арки: ${phase}` : '',
|
|
].filter(Boolean).join('<br>');
|
|
|
|
const dbg = document.getElementById('chatSettingsRpgDebug');
|
|
if (dbg) dbg.open = rpgOn;
|
|
const affIn = document.getElementById('debugAffinity');
|
|
const lustIn = document.getElementById('debugLust');
|
|
const stamIn = document.getElementById('debugStamina');
|
|
const tensIn = document.getElementById('debugTension');
|
|
if (affIn) affIn.value = String(s.affinity ?? 0);
|
|
if (lustIn) lustIn.value = String(stats.lust ?? 0);
|
|
if (stamIn) stamIn.value = String(stats.stamina ?? 10);
|
|
if (tensIn) tensIn.value = String(stats.tension ?? 0);
|
|
const st = document.getElementById('debugRpgStateStatus');
|
|
if (st) st.textContent = '';
|
|
|
|
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('debugRpgStateApply')?.addEventListener('click', async () => {
|
|
if (!sessionId) return;
|
|
const statusEl = document.getElementById('debugRpgStateStatus');
|
|
const body = {};
|
|
const aff = document.getElementById('debugAffinity')?.value;
|
|
const lust = document.getElementById('debugLust')?.value;
|
|
const stam = document.getElementById('debugStamina')?.value;
|
|
const tens = document.getElementById('debugTension')?.value;
|
|
if (aff !== '' && aff != null) body.affinity = Number(aff);
|
|
if (lust !== '' && lust != null) body.lust = Number(lust);
|
|
if (stam !== '' && stam != null) body.stamina = Number(stam);
|
|
if (tens !== '' && tens != null) body.tension = Number(tens);
|
|
try {
|
|
const res = await fetch(`/sessions/${sessionId}/rpg-state`, {
|
|
method: 'PATCH',
|
|
headers: { 'Content-Type': 'application/json' },
|
|
body: JSON.stringify(body),
|
|
});
|
|
if (!res.ok) {
|
|
const err = await res.json().catch(() => ({}));
|
|
if (statusEl) {
|
|
statusEl.textContent = err.detail || 'Ошибка';
|
|
statusEl.style.color = '#e74c3c';
|
|
}
|
|
return;
|
|
}
|
|
const data = await res.json();
|
|
const { updateAffinityDisplay, updateStatsDisplay } = await import('./chat.js');
|
|
if (data.affinity !== undefined) updateAffinityDisplay(data.affinity);
|
|
if (data.narrative_stats) updateStatsDisplay(data.narrative_stats);
|
|
if (statusEl) {
|
|
statusEl.textContent = 'Сохранено';
|
|
statusEl.style.color = '#2ecc71';
|
|
}
|
|
const blobRes = await fetch(`/chat/system/${sessionId}`);
|
|
if (blobRes.ok) {
|
|
const { renderSystemBlob } = await import('./sessions.js');
|
|
renderSystemBlob(await blobRes.json());
|
|
}
|
|
} catch {
|
|
if (statusEl) {
|
|
statusEl.textContent = 'Сеть';
|
|
statusEl.style.color = '#e74c3c';
|
|
}
|
|
}
|
|
});
|
|
|
|
document.getElementById('chatSettingsSave')?.addEventListener('click', async () => {
|
|
if (!sessionId) return;
|
|
const { loadSessions, applySessionUi, renderSystemBlob } = await import('./sessions.js');
|
|
const { reloadChatFromServer } = await import('./chat.js');
|
|
const { highlightPersonaBar } = await import('./personas.js');
|
|
|
|
const title = document.getElementById('chatSettingsTitle').value.trim();
|
|
const rpgOn = document.getElementById('chatSettingsRpg').checked;
|
|
const genreValue = [...chatSettingsGenres].join(',') || 'adventure';
|
|
const settings = readRpgSettingsFromDom('cs');
|
|
|
|
if (chatSettingsPersonaId !== chatSettingsInitialPersonaId) {
|
|
const pName = personaIndex.get(chatSettingsPersonaId)?.name || chatSettingsPersonaId;
|
|
const keepHistory = confirm(
|
|
`Перепривязать чат к «${pName}»?\n\n`
|
|
+ 'OK — сохранить историю сообщений (персонаж в старых репликах может не совпадать).\n'
|
|
+ 'Отмена — очистить историю и начать с приветствия нового персонажа.',
|
|
);
|
|
const clearHistory = !keepHistory;
|
|
|
|
const rebindRes = await fetch(`/sessions/${sessionId}/rebind-persona`, {
|
|
method: 'POST',
|
|
headers: { 'Content-Type': 'application/json' },
|
|
body: JSON.stringify({
|
|
persona_id: chatSettingsPersonaId,
|
|
clear_history: clearHistory,
|
|
}),
|
|
});
|
|
if (!rebindRes.ok) {
|
|
const err = await rebindRes.json().catch(() => ({}));
|
|
alert(err.detail || 'Не удалось сменить персонажа');
|
|
return;
|
|
}
|
|
setCurrentPersona(chatSettingsPersonaId);
|
|
chatSettingsInitialPersonaId = chatSettingsPersonaId;
|
|
highlightPersonaBar(chatSettingsPersonaId);
|
|
await reloadChatFromServer(sessionId);
|
|
const blobRes = await fetch(`/chat/system/${sessionId}`);
|
|
if (blobRes.ok) renderSystemBlob(await blobRes.json());
|
|
}
|
|
|
|
await fetch(`/sessions/${sessionId}`, {
|
|
method: 'PATCH',
|
|
headers: { 'Content-Type': 'application/json' },
|
|
body: JSON.stringify({
|
|
title: title || undefined,
|
|
rpg_enabled: rpgOn,
|
|
genre: genreValue,
|
|
rpg_settings_json: JSON.stringify(settings),
|
|
}),
|
|
});
|
|
|
|
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, chatSettingsPersonaId, 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();
|
|
});
|
|
}
|