import { setSessionId, setCurrentPersona, currentPersona, dom } from './state.js'; import { initWizard, GENRE_LABELS, bindGenreGrid, resetGenreGrid, fillGreetingSelect, getSelectedGreeting, } from './utils.js'; import { personaIndex, highlightPersona } from './personas.js'; let newChatPersonaId = currentPersona; let newChatGreetingCtx = null; const newChatGenres = new Set(); const newChatModalEl = document.getElementById('newChatModal'); let newChatWizard; async function resolveGreetingContext(personaId) { const p = personaIndex.get(personaId); let firstMes = p?.first_mes || ''; let alternates = p?.alternate_greetings || []; if (personaId.startsWith('card_') && (!alternates.length || !firstMes)) { try { const r = await fetch(`/characters/${personaId.slice(5)}`); if (r.ok) { const c = await r.json(); firstMes = c.first_mes || firstMes; alternates = c.alternate_greetings?.length ? c.alternate_greetings : alternates; } } catch { /* ignore */ } } return { firstMes, alternates }; } async function syncNewChatGreetingBlock() { const block = document.getElementById('newChatGreetingBlock'); const select = document.getElementById('newChatGreetingSelect'); const text = document.getElementById('newChatGreetingText'); if (!block || !select || !text) return; newChatGreetingCtx = await resolveGreetingContext(newChatPersonaId); const { firstMes, alternates } = newChatGreetingCtx; if (!alternates.length) { block.classList.add('hidden'); return; } block.classList.remove('hidden'); fillGreetingSelect(select, firstMes, alternates); text.value = firstMes; select.onchange = () => { text.value = getSelectedGreeting(select, firstMes, alternates); }; } function getNewChatFirstMesOverride() { const block = document.getElementById('newChatGreetingBlock'); if (!block || block.classList.contains('hidden') || !newChatGreetingCtx) return null; const edited = document.getElementById('newChatGreetingText')?.value.trim(); if (edited) return edited; const select = document.getElementById('newChatGreetingSelect'); const { firstMes, alternates } = newChatGreetingCtx; return getSelectedGreeting(select, firstMes, alternates) || null; } 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'); } } 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 = `${p.emoji || '🤖'}${p.name}`; card.addEventListener('click', () => { newChatPersonaId = p.persona_id; grid.querySelectorAll('.persona-pick-card').forEach(c => { c.classList.toggle('selected', c.dataset.id === newChatPersonaId); }); syncNewChatGreetingBlock(); }); 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'); } } if (nextBtn && isNewChatRpg()) { 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; } } 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 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'); syncNewChatGreetingBlock(); } export async function createNewChatFromWizard() { const { clearMessages, initChat, reloadChatFromServer } = await import('./chat.js'); const { loadSessions, applySessionUi } = await import('./sessions.js'); 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(); newChatModalEl?.classList.remove('open'); newChatWizard?.reset(); try { 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); const greetingOverride = getNewChatFirstMesOverride(); await initChat(greetingOverride ? { first_mes_override: greetingOverride } : {}); 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); const sessionRes = await fetch(`/sessions/${sid}`); if (sessionRes.ok) applySessionUi(await sessionRes.json()); await loadSessions(); } catch (e) { console.error('createNewChat error:', e); } } export function initNewChatWizard() { if (!newChatModalEl) return; newChatWizard = initWizard(newChatModalEl.querySelector('.modal-wizard'), { totalSteps: 3, onStepChange(step) { syncNewChatStep3(); if (step === 3 && !isNewChatRpg()) syncNewChatGreetingBlock(); 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); }