Files
ChatAIBot/static/js/personas.js
T
2026-05-29 08:52:33 +03:00

276 lines
13 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, setCurrentPersona, sessionId } from './state.js';
import { initChat } from './chat.js';
import { initWizard } from './utils.js';
export let personaIndex = new Map();
let createWizard;
export function highlightPersona(personaId) {
document.querySelectorAll('.persona-card').forEach(c => {
c.classList.toggle('active', c.dataset.id === personaId);
});
}
export async function loadPersonas() {
const res = await fetch('/personas/');
const personas = await res.json();
personaIndex = new Map(personas.map(p => [p.persona_id, p]));
const bar = document.getElementById('personaBar');
bar.innerHTML = '';
personas.forEach(p => {
const card = document.createElement('div');
card.className = 'persona-card' + (p.persona_id === currentPersona ? ' 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;
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', () => document.getElementById('cardModalOverlay').classList.add('open'));
bar.appendChild(importBtn);
}
export async function selectPersona(personaId) {
setCurrentPersona(personaId);
highlightPersona(personaId);
if (sessionId) {
await fetch(`/sessions/${sessionId}`, {
method: 'PATCH',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify({ persona_id: personaId }),
});
await initChat();
}
}
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();
});
document.getElementById('cardModalCancel').addEventListener('click', () => {
document.getElementById('cardModalOverlay').classList.remove('open');
});
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', async () => {
const fileInput = document.getElementById('cardFile');
if (!fileInput.files?.length) {
alert('Выберите файл карточки (JSON или PNG)');
return;
}
const form = new FormData();
form.append('file', fileInput.files[0]);
form.append('lora_name', document.getElementById('cardLora').value.trim());
form.append('lora_weight', document.getElementById('cardLoraWeight').value || '0.8');
const res = await fetch('/characters/import', { method: 'POST', body: form });
const data = await res.json();
if (!res.ok) {
alert(data.detail || 'Ошибка импорта');
return;
}
document.getElementById('cardModalOverlay').classList.remove('open');
fileInput.value = '';
await loadPersonas();
await selectPersona(data.persona_id);
});
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();
});
}
}