import { setSessionId, setCurrentPersona, getNewChatDefaultPersona, dom, } from './state.js'; import { initWizard, GENRE_LABELS, bindGenreGrid, resetGenreGrid, fillGreetingSelect, getSelectedGreeting, } from './utils.js'; import { personaIndex } from './personas.js'; let newChatPersonaId = getNewChatDefaultPersona(); 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 = getNewChatDefaultPersona(); 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; } } export function openNewChatWizard() { import('./personas.js').then(({ refreshPersonaBarHighlight }) => refreshPersonaBarHighlight()); 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, showImageGenerating, removeImageGenerating, updateQuestPanel, updateAffinityDisplay, renderChoices, } = await import('./chat.js'); const { loadSessions, applySessionUi, renderSystemBlob } = 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 { const sessionPatch = { persona_id: newChatPersonaId, rpg_enabled: rpg }; if (rpg) { sessionPatch.genre = [...newChatGenres].join(',') || 'adventure'; sessionPatch.rpg_settings_json = JSON.stringify({ 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 fetch(`/sessions/${sid}`, { method: 'PATCH', headers: { 'Content-Type': 'application/json' }, body: JSON.stringify(sessionPatch), }); 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} — новый чат`; } const { highlightPersonaBar } = await import('./personas.js'); highlightPersonaBar(newChatPersonaId); const greetingOverride = getNewChatFirstMesOverride(); await initChat(greetingOverride ? { first_mes_override: greetingOverride } : {}); const assistantWrapper = dom.messagesEl.querySelector('.message.assistant'); showImageGenerating(assistantWrapper); let openingData = null; try { const openingRes = await fetch('/chat/opening/process', { method: 'POST', headers: { 'Content-Type': 'application/json' }, body: JSON.stringify({ session_id: sid, persona_id: newChatPersonaId, rpg, }), }); openingData = await openingRes.json(); if (!openingRes.ok) { console.error('opening/process failed:', openingData.detail || openingRes.statusText); } } finally { removeImageGenerating(assistantWrapper); } await reloadChatFromServer(sid); if (openingData?.quests?.length) { updateQuestPanel(openingData.quests); } if (openingData?.affinity !== undefined) { updateAffinityDisplay(openingData.affinity); } if (openingData?.choices?.length) { const wrapper = dom.messagesEl.querySelector('.message.assistant'); if (wrapper) renderChoices(wrapper, openingData.choices); } if (openingData?.image_error) { const wrapper = dom.messagesEl.querySelector('.message.assistant'); if (wrapper) { const err = document.createElement('div'); err.className = 'image-error'; err.textContent = '🖼 ' + openingData.image_error; wrapper.appendChild(err); } } const sessionRes = await fetch(`/sessions/${sid}`); if (sessionRes.ok) applySessionUi(await sessionRes.json()); const blobRes = await fetch(`/chat/system/${sid}`); if (blobRes.ok) renderSystemBlob(await blobRes.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); }