Fixed RPG
This commit is contained in:
@@ -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);
|
||||
}
|
||||
Reference in New Issue
Block a user