291 lines
11 KiB
JavaScript
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);
|
|
}
|
|
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);
|
|
}
|