Files
ChatAIBot/static/js/personas.js
T
2026-06-04 08:05:06 +03:00

408 lines
18 KiB
JavaScript
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
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 ? `<img class="avatar" src="${avatar}" alt="">` : `<span class="emoji">${p.emoji}</span>`}
<span class="pname">${p.name}</span>
${p.custom ? `<button class="del-btn" type="button">✕</button>` : ''}
${isCard ? `<button class="edit-btn" type="button">✏️</button>` : ''}
${isCustomPersona ? `<button class="edit-persona-btn" type="button">✏️</button>` : ''}
`;
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 = '<span>Создать</span>';
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 = '📥<span>Chub</span>';
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();
});
}
}