Move persona editor into modal

This commit is contained in:
2026-01-07 21:06:04 +01:00
parent c9fbc1a55a
commit 2cd6c9c4d7
4 changed files with 740 additions and 12 deletions
+242 -5
View File
@@ -20,10 +20,29 @@ const newChatBtn = document.getElementById("newChat");
const renameChatBtn = document.getElementById("renameChat");
const deleteChatBtn = document.getElementById("deleteChat");
const activeChatTitle = document.getElementById("activeChatTitle");
const personaList = document.getElementById("personaList");
const personaForm = document.getElementById("personaForm");
const personaFormTitle = document.getElementById("personaFormTitle");
const personaNameInput = document.getElementById("personaName");
const personaPronounsInput = document.getElementById("personaPronouns");
const personaAppearanceInput = document.getElementById("personaAppearance");
const personaBackgroundInput = document.getElementById("personaBackground");
const personaDetailsInput = document.getElementById("personaDetails");
const personaModal = document.getElementById("personaModal");
const personaCloseBtn = document.getElementById("personaClose");
const addCustomFieldBtn = document.getElementById("addCustomField");
const customFieldsList = document.getElementById("customFieldsList");
const newPersonaBtn = document.getElementById("newPersona");
const clearPersonaBtn = document.getElementById("clearPersona");
const personaCancelBtn = document.getElementById("personaCancel");
const activePersonaStatus = document.getElementById("activePersonaStatus");
let isProcessing = false;
let chatSessions = [];
let activeChatId = null;
let personas = [];
let activePersonaId = null;
let editingPersonaId = null;
const displayModels = async () => {
const select = document.getElementById("model");
@@ -168,6 +187,199 @@ function renderChatList() {
});
}
function setActivePersonaStatus() {
const activePersona = personas.find(persona => persona.id === activePersonaId);
if (activePersona) {
activePersonaStatus.textContent = `Equipped: ${activePersona.name}`;
} else {
activePersonaStatus.textContent = 'No persona equipped.';
}
clearPersonaBtn.disabled = !activePersonaId;
}
function openPersonaForm(persona = null) {
personaModal.classList.remove('hidden');
if (persona) {
editingPersonaId = persona.id;
personaFormTitle.textContent = 'Edit persona';
personaNameInput.value = persona.name || '';
personaPronounsInput.value = persona.pronouns || '';
personaAppearanceInput.value = persona.appearance || '';
personaBackgroundInput.value = persona.background || '';
personaDetailsInput.value = persona.details || '';
renderCustomFields(persona.customFields || []);
} else {
editingPersonaId = null;
personaFormTitle.textContent = 'Create persona';
personaNameInput.value = '';
personaPronounsInput.value = '';
personaAppearanceInput.value = '';
personaBackgroundInput.value = '';
personaDetailsInput.value = '';
renderCustomFields([]);
}
personaNameInput.focus();
}
function closePersonaForm() {
personaModal.classList.add('hidden');
editingPersonaId = null;
}
function renderCustomFields(fields) {
customFieldsList.innerHTML = '';
fields.forEach((field, index) => {
addCustomFieldRow(field.label || '', field.value || '', index);
});
}
function addCustomFieldRow(label = '', value = '') {
const row = document.createElement('div');
row.className = 'custom-field-row';
const labelInput = document.createElement('input');
labelInput.type = 'text';
labelInput.placeholder = 'Label';
labelInput.value = label;
const valueInput = document.createElement('input');
valueInput.type = 'text';
valueInput.placeholder = 'Value';
valueInput.value = value;
const removeBtn = document.createElement('button');
removeBtn.type = 'button';
removeBtn.textContent = 'Remove';
removeBtn.className = 'secondary-action';
removeBtn.addEventListener('click', () => row.remove());
row.appendChild(labelInput);
row.appendChild(valueInput);
row.appendChild(removeBtn);
customFieldsList.appendChild(row);
}
function collectCustomFields() {
const rows = customFieldsList.querySelectorAll('.custom-field-row');
return Array.from(rows).map(row => {
const [labelInput, valueInput] = row.querySelectorAll('input');
return {
label: labelInput.value.trim(),
value: valueInput.value.trim()
};
}).filter(field => field.label || field.value);
}
function renderPersonaList() {
personaList.innerHTML = '';
if (!personas.length) {
const empty = document.createElement('p');
empty.className = 'status persona-status';
empty.textContent = 'No personas yet. Create one to get started.';
personaList.appendChild(empty);
setActivePersonaStatus();
return;
}
personas.forEach(persona => {
const item = document.createElement('div');
item.className = `persona-item${persona.id === activePersonaId ? ' active' : ''}`;
const title = document.createElement('h4');
title.textContent = persona.name;
const meta = document.createElement('p');
meta.textContent = persona.pronouns ? `Pronouns: ${persona.pronouns}` : 'Pronouns: —';
const actions = document.createElement('div');
actions.className = 'persona-item-actions';
const equipBtn = document.createElement('button');
equipBtn.type = 'button';
equipBtn.textContent = persona.id === activePersonaId ? 'Equipped' : 'Equip';
equipBtn.disabled = persona.id === activePersonaId;
equipBtn.addEventListener('click', () => equipPersona(persona.id));
const editBtn = document.createElement('button');
editBtn.type = 'button';
editBtn.textContent = 'Edit';
editBtn.addEventListener('click', () => openPersonaForm(persona));
const deleteBtn = document.createElement('button');
deleteBtn.type = 'button';
deleteBtn.textContent = 'Delete';
deleteBtn.addEventListener('click', () => deletePersona(persona.id));
actions.appendChild(equipBtn);
actions.appendChild(editBtn);
actions.appendChild(deleteBtn);
item.appendChild(title);
item.appendChild(meta);
item.appendChild(actions);
personaList.appendChild(item);
});
setActivePersonaStatus();
}
async function loadPersonas() {
const res = await get('/personas');
if (res.error) return;
personas = (res.personas || []).map(persona => ({
...persona,
customFields: persona.customFields || []
}));
activePersonaId = res.activePersonaId || null;
renderPersonaList();
}
async function equipPersona(personaId) {
const res = await post(`/personas/${personaId}/equip`, {});
if (res.error) return;
activePersonaId = personaId;
renderPersonaList();
}
async function clearPersona() {
const res = await post('/personas/clear', {});
if (res.error) return;
activePersonaId = null;
renderPersonaList();
}
async function savePersona() {
const payload = {
name: personaNameInput.value.trim(),
pronouns: personaPronounsInput.value.trim(),
appearance: personaAppearanceInput.value.trim(),
background: personaBackgroundInput.value.trim(),
details: personaDetailsInput.value.trim(),
customFields: collectCustomFields()
};
if (!payload.name) return;
let res;
if (editingPersonaId) {
res = await request(`/personas/${editingPersonaId}`, payload, "PUT");
} else {
res = await post('/personas', payload);
}
if (res.error) return;
await loadPersonas();
closePersonaForm();
}
async function deletePersona(personaId) {
const persona = personas.find(item => item.id === personaId);
if (!persona) return;
const confirmDelete = confirm(`Delete persona "${persona.name}"?`);
if (!confirmDelete) return;
const res = await request(`/personas/${personaId}`, null, "DELETE");
if (res.error) return;
await loadPersonas();
}
document.addEventListener('keydown', (event) => {
if (event.key === 'Escape' && !personaModal.classList.contains('hidden')) {
closePersonaForm();
}
});
function setLoadingState(loading) {
isProcessing = loading;
sendBtn.disabled = loading;
@@ -282,6 +494,7 @@ async function checkSession() {
authDiv.style.display = "none";
chatDiv.style.display = "block";
await loadChatSessions();
await loadPersonas();
msgInput.focus();
return true;
}
@@ -290,6 +503,7 @@ async function checkSession() {
async function loadServerChat() {
await loadChatSessions();
await loadPersonas();
}
async function handleAuth(endpoint, credentials) {
@@ -396,6 +610,8 @@ document.getElementById("logout").onclick = async () => {
clearMessages();
chatSessions = [];
activeChatId = null;
personas = [];
activePersonaId = null;
setAuthMessage("Logged out.", 'success');
showAuthScreen('login');
loginUsernameInput.value = "";
@@ -424,11 +640,32 @@ deleteChatBtn.addEventListener('click', () => {
deleteChat(activeChatId);
}
});
+
msgInput.addEventListener('input', function () {
this.style.height = 'auto';
this.style.height = Math.min(this.scrollHeight, 120) + 'px';
});
msgInput.addEventListener('input', function () {
this.style.height = 'auto';
this.style.height = Math.min(this.scrollHeight, 120) + 'px';
});
newPersonaBtn.addEventListener('click', () => openPersonaForm());
personaCancelBtn.addEventListener('click', () => closePersonaForm());
personaCloseBtn.addEventListener('click', () => closePersonaForm());
personaModal.addEventListener('click', (event) => {
if (event.target === personaModal) {
closePersonaForm();
}
});
addCustomFieldBtn.addEventListener('click', () => addCustomFieldRow());
clearPersonaBtn.addEventListener('click', () => clearPersona());
personaForm.addEventListener('submit', (event) => {
event.preventDefault();
savePersona();
});
window.addEventListener('load', () => {
checkSession();