Files
2026-06-05 14:57:15 +03:00

291 lines
11 KiB
JavaScript

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 = `<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);
});
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,
stats: document.getElementById('ncSettingStats')?.checked ?? false,
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, openingData.plot_arc ?? null);
}
if (openingData?.affinity !== undefined) {
updateAffinityDisplay(openingData.affinity);
}
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);
}