import { currentPersona, sessionId, getNewChatDefaultPersona, setNewChatDefaultPersona, } from './state.js'; import { initWizard, fillGreetingSelect, getSelectedGreeting } from './utils.js'; export let personaIndex = new Map(); function parseAlternateGreetings(p) { if (Array.isArray(p?.alternate_greetings) && p.alternate_greetings.length) { return p.alternate_greetings; } try { const parsed = JSON.parse(p?.alternate_greetings_json || '[]'); return Array.isArray(parsed) ? parsed : []; } catch { return []; } } let createWizard; let cardImportWizard; let cardPreview = null; let cardImportFile = null; export function highlightPersonaBar(personaId) { document.querySelectorAll('.persona-card').forEach(c => { c.classList.toggle('active', c.dataset.id === personaId); }); } /** Active session → session persona; otherwise new-chat preset. */ export function refreshPersonaBarHighlight() { const id = sessionId ? currentPersona : getNewChatDefaultPersona(); highlightPersonaBar(id); } export async function loadPersonas() { const res = await fetch('/personas/'); const personas = await res.json(); personaIndex = new Map(personas.map(p => { const alternate_greetings = parseAlternateGreetings(p); return [p.persona_id, { ...p, alternate_greetings }]; })); const bar = document.getElementById('personaBar'); bar.innerHTML = ''; const barActiveId = sessionId ? currentPersona : getNewChatDefaultPersona(); personas.forEach(p => { const card = document.createElement('div'); card.className = 'persona-card' + (p.persona_id === barActiveId ? ' active' : ''); card.dataset.id = p.persona_id; const isCard = p.persona_id.startsWith('card_'); const isCustomPersona = p.custom && !isCard; const avatar = p.avatar_path ? `/static/${p.avatar_path}` : ''; card.innerHTML = ` ${avatar ? `` : `${p.emoji}`} ${p.name} ${p.custom ? `` : ''} ${isCard ? `` : ''} ${isCustomPersona ? `` : ''} `; card.addEventListener('click', () => selectPersona(p.persona_id)); card.querySelector('.del-btn')?.addEventListener('click', async (e) => { e.stopPropagation(); await fetch(`/personas/${p.persona_id}`, { method: 'DELETE' }); if (currentPersona === p.persona_id) await selectPersona('default'); loadPersonas(); }); card.querySelector('.edit-btn')?.addEventListener('click', async (e) => { e.stopPropagation(); const cardId = p.persona_id.slice(5); const r = await fetch(`/characters/${cardId}`); const data = await r.json(); document.getElementById('editCardId').value = cardId; document.getElementById('editName').value = data.name || ''; document.getElementById('editDescription').value = data.description || ''; document.getElementById('editPersonality').value = data.personality || ''; document.getElementById('editScenario').value = data.scenario || ''; document.getElementById('editFirstMes').value = data.first_mes || ''; document.getElementById('editMesExample').value = data.mes_example || ''; document.getElementById('editAppearance').value = data.appearance_tags || ''; document.getElementById('editLora').value = data.lora_name || ''; document.getElementById('editLoraWeight').value = data.lora_weight ?? 0.8; const alts = data.alternate_greetings || []; const altBlock = document.getElementById('editCardAltBlock'); const altSelect = document.getElementById('editCardGreetingSelect'); if (alts.length) { altBlock?.classList.remove('hidden'); fillGreetingSelect(altSelect, data.first_mes, alts); altSelect.onchange = () => { document.getElementById('editFirstMes').value = getSelectedGreeting(altSelect, data.first_mes, alts); }; } else { altBlock?.classList.add('hidden'); } document.getElementById('cardEditOverlay').classList.add('open'); }); card.querySelector('.edit-persona-btn')?.addEventListener('click', async (e) => { e.stopPropagation(); const r = await fetch(`/personas/${p.persona_id}`); const data = await r.json(); document.getElementById('editPersonaId').value = p.persona_id; document.getElementById('editPName').value = data.name || ''; document.getElementById('editPEmoji').value = data.emoji || ''; document.getElementById('editPDesc').value = data.description || ''; document.getElementById('editPPersonality').value = data.personality || ''; document.getElementById('editPScenario').value = data.scenario || ''; document.getElementById('editPFirstMes').value = data.first_mes || ''; document.getElementById('editPMesExample').value = data.mes_example || ''; document.getElementById('editPLorebook').value = data.lorebook_json || '[]'; document.getElementById('editPPrompt').value = data.prompt || ''; document.getElementById('editPSdEnabled').checked = !!data.sd_enabled; document.getElementById('editPLora').value = data.lora_name || ''; document.getElementById('editPLoraWeight').value = data.lora_weight ?? 0.8; document.getElementById('editPAppearance').value = data.appearance_tags || ''; document.getElementById('personaEditOverlay').classList.add('open'); }); bar.appendChild(card); }); const addBtn = document.createElement('button'); addBtn.type = 'button'; addBtn.className = 'persona-add'; addBtn.innerHTML = '➕Создать'; addBtn.addEventListener('click', () => { document.getElementById('modalOverlay').classList.add('open'); createWizard?.reset(); }); bar.appendChild(addBtn); const importBtn = document.createElement('button'); importBtn.type = 'button'; importBtn.className = 'card-import-btn'; importBtn.innerHTML = '📥Chub'; importBtn.addEventListener('click', () => openCardImportModal()); bar.appendChild(importBtn); } export async function selectPersona(personaId) { setNewChatDefaultPersona(personaId); highlightPersonaBar(personaId); } function fillImpCardForm(preview) { document.getElementById('impCardName').value = preview.name || ''; document.getElementById('impCardDescription').value = preview.description || ''; document.getElementById('impCardPersonality').value = preview.personality || ''; document.getElementById('impCardScenario').value = preview.scenario || ''; document.getElementById('impCardMesExample').value = preview.mes_example || ''; document.getElementById('impCardAppearance').value = preview.appearance_tags || ''; const alts = preview.alternate_greetings || []; const selectEl = document.getElementById('impCardGreetingSelect'); const firstMesEl = document.getElementById('impCardFirstMes'); fillGreetingSelect(selectEl, preview.first_mes, alts); firstMesEl.value = preview.first_mes || ''; const altHint = document.getElementById('impCardAltHint'); if (alts.length) { altHint.textContent = `В карточке ${alts.length} альтернативных приветствий — выбери в списке или отредактируй текст ниже`; altHint.classList.remove('hidden'); } else { altHint.classList.add('hidden'); } selectEl.onchange = () => { firstMesEl.value = getSelectedGreeting(selectEl, preview.first_mes, alts); }; } async function loadCardPreview() { const fileInput = document.getElementById('cardFile'); if (!fileInput.files?.length) { alert('Выберите файл карточки (JSON или PNG)'); return false; } const form = new FormData(); form.append('file', fileInput.files[0]); const res = await fetch('/characters/preview', { method: 'POST', body: form }); const data = await res.json(); if (!res.ok) { alert(data.detail || 'Ошибка чтения карточки'); return false; } cardPreview = data; cardImportFile = fileInput.files[0]; fillImpCardForm(data); const hint = document.getElementById('cardPreviewHint'); if (hint) { hint.textContent = `${data.name} · ${data.alternate_count || 0} альт. приветствий`; } return true; } function openCardImportModal() { cardPreview = null; cardImportFile = null; document.getElementById('cardFile').value = ''; document.getElementById('cardPreviewHint').textContent = ''; document.getElementById('cardLora').value = ''; document.getElementById('cardLoraWeight').value = '0.8'; cardImportWizard?.reset(); document.getElementById('cardModalOverlay').classList.add('open'); } function closeCardImportModal() { document.getElementById('cardModalOverlay').classList.remove('open'); cardImportWizard?.reset(); cardPreview = null; cardImportFile = null; } function initCardImportWizard() { const modal = document.getElementById('cardModalOverlay')?.querySelector('.modal-wizard'); if (!modal) return; cardImportWizard = initWizard(modal, { totalSteps: 2, validateStep(step) { if (step !== 1) return true; return loadCardPreview(); }, }); } async function submitCardImport() { if (!cardImportFile || !cardPreview) { alert('Сначала загрузите и проверьте карточку'); return; } const form = new FormData(); form.append('file', cardImportFile); form.append('card_id', cardPreview.card_id || ''); form.append('lora_name', document.getElementById('cardLora').value.trim()); form.append('lora_weight', document.getElementById('cardLoraWeight').value || '0.8'); form.append('name', document.getElementById('impCardName').value.trim()); form.append('description', document.getElementById('impCardDescription').value.trim()); form.append('personality', document.getElementById('impCardPersonality').value.trim()); form.append('scenario', document.getElementById('impCardScenario').value.trim()); form.append('first_mes', document.getElementById('impCardFirstMes').value.trim()); form.append('mes_example', document.getElementById('impCardMesExample').value.trim()); form.append('appearance_tags', document.getElementById('impCardAppearance').value.trim()); form.append( 'alternate_greetings_json', JSON.stringify(cardPreview.alternate_greetings || []), ); const res = await fetch('/characters/import', { method: 'POST', body: form }); const data = await res.json(); if (!res.ok) { alert(data.detail || 'Ошибка импорта'); return; } closeCardImportModal(); document.getElementById('cardFile').value = ''; await loadPersonas(); await selectPersona(data.persona_id); } export function initPersonaModals() { const createModal = document.getElementById('modalOverlay'); createWizard = initWizard(createModal.querySelector('.modal-wizard'), { totalSteps: 3, validateStep(step) { if (step !== 1) return true; const id = document.getElementById('pId').value.trim(); const name = document.getElementById('pName').value.trim(); if (!id || !name) { alert('Заполни ID и имя'); return false; } return true; }, }); document.getElementById('modalCancel').addEventListener('click', () => { createModal.classList.remove('open'); createWizard.reset(); }); initCardImportWizard(); document.getElementById('cardModalCancel').addEventListener('click', () => { closeCardImportModal(); }); document.getElementById('cardEditCancel').addEventListener('click', () => { document.getElementById('cardEditOverlay').classList.remove('open'); }); // custom persona editor (reuses create modal fields) const personaEditCancel = document.getElementById('personaEditCancel'); if (personaEditCancel) { personaEditCancel.addEventListener('click', () => { document.getElementById('personaEditOverlay').classList.remove('open'); }); } document.getElementById('modalSave').addEventListener('click', async () => { const data = { persona_id: document.getElementById('pId').value.trim(), name: document.getElementById('pName').value.trim(), emoji: document.getElementById('pEmoji').value.trim() || '🤖', description: document.getElementById('pDesc').value.trim(), prompt: document.getElementById('pPrompt').value.trim(), sd_enabled: document.getElementById('pSdEnabled').checked, lora_name: document.getElementById('pLora').value.trim(), appearance_tags: document.getElementById('pAppearance').value.trim(), personality: document.getElementById('pPersonality').value.trim(), scenario: document.getElementById('pScenario').value.trim(), first_mes: document.getElementById('pFirstMes').value.trim(), mes_example: document.getElementById('pMesExample').value.trim(), lorebook_json: document.getElementById('pLorebook').value.trim() || '[]', }; if (!data.persona_id || !data.name) { alert('Заполни ID и имя'); return; } await fetch('/personas/', { method: 'POST', headers: { 'Content-Type': 'application/json' }, body: JSON.stringify(data), }); document.getElementById('modalOverlay').classList.remove('open'); createWizard.reset(); await loadPersonas(); await selectPersona(data.persona_id); }); document.getElementById('cardEditSave').addEventListener('click', async () => { const cardId = document.getElementById('editCardId').value; const body = { name: document.getElementById('editName').value.trim() || undefined, description: document.getElementById('editDescription').value.trim() || undefined, personality: document.getElementById('editPersonality').value.trim() || undefined, scenario: document.getElementById('editScenario').value.trim() || undefined, first_mes: document.getElementById('editFirstMes').value.trim() || undefined, mes_example: document.getElementById('editMesExample').value.trim() || undefined, appearance_tags: document.getElementById('editAppearance').value.trim() || undefined, lora_name: document.getElementById('editLora').value.trim() || undefined, lora_weight: parseFloat(document.getElementById('editLoraWeight').value) || undefined, }; const res = await fetch(`/characters/${cardId}`, { method: 'PATCH', headers: { 'Content-Type': 'application/json' }, body: JSON.stringify(body), }); if (!res.ok) { alert('Ошибка сохранения'); return; } const avatarFile = document.getElementById('editCardAvatar')?.files?.[0]; if (avatarFile) { const form = new FormData(); form.append('file', avatarFile); await fetch(`/characters/${cardId}/avatar`, { method: 'POST', body: form }); document.getElementById('editCardAvatar').value = ''; } document.getElementById('cardEditOverlay').classList.remove('open'); await loadPersonas(); }); document.getElementById('cardModalImport').addEventListener('click', submitCardImport); const personaEditSave = document.getElementById('personaEditSave'); if (personaEditSave) { personaEditSave.addEventListener('click', async () => { const personaId = document.getElementById('editPersonaId').value; const body = { name: document.getElementById('editPName').value.trim() || undefined, emoji: document.getElementById('editPEmoji').value.trim() || undefined, description: document.getElementById('editPDesc').value.trim() || undefined, personality: document.getElementById('editPPersonality').value.trim() || undefined, scenario: document.getElementById('editPScenario').value.trim() || undefined, first_mes: document.getElementById('editPFirstMes').value.trim() || undefined, mes_example: document.getElementById('editPMesExample').value.trim() || undefined, lorebook_json: document.getElementById('editPLorebook').value.trim() || undefined, prompt: document.getElementById('editPPrompt').value.trim() || undefined, sd_enabled: document.getElementById('editPSdEnabled').checked, lora_name: document.getElementById('editPLora').value.trim() || undefined, lora_weight: parseFloat(document.getElementById('editPLoraWeight').value) || undefined, appearance_tags: document.getElementById('editPAppearance').value.trim() || undefined, }; const res = await fetch(`/personas/${personaId}`, { method: 'PATCH', headers: { 'Content-Type': 'application/json' }, body: JSON.stringify(body), }); if (!res.ok) { alert('Ошибка сохранения'); return; } const avatarFile = document.getElementById('editPAvatar')?.files?.[0]; if (avatarFile) { const form = new FormData(); form.append('file', avatarFile); await fetch(`/personas/${personaId}/avatar`, { method: 'POST', body: form }); document.getElementById('editPAvatar').value = ''; } document.getElementById('personaEditOverlay').classList.remove('open'); await loadPersonas(); }); } }