Fixed RPG

This commit is contained in:
2026-06-01 07:44:38 +03:00
parent 600ad78f05
commit d4cd8f02f4
30 changed files with 1516 additions and 816 deletions
+257
View File
@@ -0,0 +1,257 @@
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 = `<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;
}
}
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);
}