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 = `${p.emoji || '🤖'}${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}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, }; } 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.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 : ''}`); } } } 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 */ } 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('
'); 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 { 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(); }); }