Merge branch 'main' into add-user-persona-creation-feature

This commit is contained in:
2026-01-07 21:08:29 +01:00
committed by GitHub
4 changed files with 49 additions and 207 deletions
+28
View File
@@ -88,6 +88,34 @@
</div> </div>
<p id="activePersonaStatus" class="status persona-status">No persona equipped.</p> <p id="activePersonaStatus" class="status persona-status">No persona equipped.</p>
<div id="personaList" class="persona-list"></div> <div id="personaList" class="persona-list"></div>
<form id="personaForm" class="persona-form hidden">
<h3 id="personaFormTitle">Create persona</h3>
<label class="field">
<span>Name</span>
<input id="personaName" placeholder="Persona name" required/>
</label>
<label class="field">
<span>Pronouns</span>
<input id="personaPronouns" placeholder="e.g. she/her, they/them"/>
</label>
<label class="field">
<span>Appearance</span>
<textarea id="personaAppearance" rows="2" placeholder="Describe their look"></textarea>
</label>
<label class="field">
<span>Background</span>
<textarea id="personaBackground" rows="2" placeholder="Backstory or origins"></textarea>
</label>
<label class="field">
<span>Details</span>
<textarea id="personaDetails" rows="3"
placeholder="Personality traits, voice, or quirks"></textarea>
</label>
<div class="persona-actions">
<button id="personaSave" type="submit" class="primary-action">Save persona</button>
<button id="personaCancel" type="button" class="secondary-action">Cancel</button>
</div>
</form>
</div> </div>
</aside> </aside>
<section class="chat-main"> <section class="chat-main">
+4 -74
View File
@@ -28,10 +28,6 @@ const personaPronounsInput = document.getElementById("personaPronouns");
const personaAppearanceInput = document.getElementById("personaAppearance"); const personaAppearanceInput = document.getElementById("personaAppearance");
const personaBackgroundInput = document.getElementById("personaBackground"); const personaBackgroundInput = document.getElementById("personaBackground");
const personaDetailsInput = document.getElementById("personaDetails"); 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 newPersonaBtn = document.getElementById("newPersona");
const clearPersonaBtn = document.getElementById("clearPersona"); const clearPersonaBtn = document.getElementById("clearPersona");
const personaCancelBtn = document.getElementById("personaCancel"); const personaCancelBtn = document.getElementById("personaCancel");
@@ -198,7 +194,7 @@ function setActivePersonaStatus() {
} }
function openPersonaForm(persona = null) { function openPersonaForm(persona = null) {
personaModal.classList.remove('hidden'); personaForm.classList.remove('hidden');
if (persona) { if (persona) {
editingPersonaId = persona.id; editingPersonaId = persona.id;
personaFormTitle.textContent = 'Edit persona'; personaFormTitle.textContent = 'Edit persona';
@@ -207,7 +203,6 @@ function openPersonaForm(persona = null) {
personaAppearanceInput.value = persona.appearance || ''; personaAppearanceInput.value = persona.appearance || '';
personaBackgroundInput.value = persona.background || ''; personaBackgroundInput.value = persona.background || '';
personaDetailsInput.value = persona.details || ''; personaDetailsInput.value = persona.details || '';
renderCustomFields(persona.customFields || []);
} else { } else {
editingPersonaId = null; editingPersonaId = null;
personaFormTitle.textContent = 'Create persona'; personaFormTitle.textContent = 'Create persona';
@@ -216,60 +211,15 @@ function openPersonaForm(persona = null) {
personaAppearanceInput.value = ''; personaAppearanceInput.value = '';
personaBackgroundInput.value = ''; personaBackgroundInput.value = '';
personaDetailsInput.value = ''; personaDetailsInput.value = '';
renderCustomFields([]);
} }
personaNameInput.focus(); personaNameInput.focus();
} }
function closePersonaForm() { function closePersonaForm() {
personaModal.classList.add('hidden'); personaForm.classList.add('hidden');
editingPersonaId = null; 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() { function renderPersonaList() {
personaList.innerHTML = ''; personaList.innerHTML = '';
if (!personas.length) { if (!personas.length) {
@@ -321,10 +271,7 @@ function renderPersonaList() {
async function loadPersonas() { async function loadPersonas() {
const res = await get('/personas'); const res = await get('/personas');
if (res.error) return; if (res.error) return;
personas = (res.personas || []).map(persona => ({ personas = res.personas || [];
...persona,
customFields: persona.customFields || []
}));
activePersonaId = res.activePersonaId || null; activePersonaId = res.activePersonaId || null;
renderPersonaList(); renderPersonaList();
} }
@@ -349,8 +296,7 @@ async function savePersona() {
pronouns: personaPronounsInput.value.trim(), pronouns: personaPronounsInput.value.trim(),
appearance: personaAppearanceInput.value.trim(), appearance: personaAppearanceInput.value.trim(),
background: personaBackgroundInput.value.trim(), background: personaBackgroundInput.value.trim(),
details: personaDetailsInput.value.trim(), details: personaDetailsInput.value.trim()
customFields: collectCustomFields()
}; };
if (!payload.name) return; if (!payload.name) return;
let res; let res;
@@ -374,12 +320,6 @@ async function deletePersona(personaId) {
await loadPersonas(); await loadPersonas();
} }
document.addEventListener('keydown', (event) => {
if (event.key === 'Escape' && !personaModal.classList.contains('hidden')) {
closePersonaForm();
}
});
function setLoadingState(loading) { function setLoadingState(loading) {
isProcessing = loading; isProcessing = loading;
sendBtn.disabled = loading; sendBtn.disabled = loading;
@@ -650,16 +590,6 @@ newPersonaBtn.addEventListener('click', () => openPersonaForm());
personaCancelBtn.addEventListener('click', () => closePersonaForm()); 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()); clearPersonaBtn.addEventListener('click', () => clearPersona());
personaForm.addEventListener('submit', (event) => { personaForm.addEventListener('submit', (event) => {
+8 -80
View File
@@ -461,9 +461,14 @@ button:disabled {
.persona-form { .persona-form {
display: grid; display: grid;
gap: 10px; gap: 10px;
max-height: 65vh; background: rgba(10, 10, 20, 0.75);
overflow-y: auto; border: 1px solid rgba(255, 255, 255, 0.06);
padding-right: 6px; border-radius: 16px;
padding: 12px;
}
.persona-form.hidden {
display: none;
} }
.persona-form h3 { .persona-form h3 {
@@ -477,83 +482,6 @@ button:disabled {
flex-wrap: wrap; flex-wrap: wrap;
} }
.modal-overlay {
position: fixed;
inset: 0;
background: rgba(5, 5, 20, 0.75);
display: grid;
place-items: center;
z-index: 1000;
padding: 24px;
}
.modal-overlay.hidden {
display: none;
}
.modal-card {
width: min(520px, 92vw);
background: rgba(19, 19, 42, 0.96);
border: 1px solid rgba(255, 255, 255, 0.08);
border-radius: 22px;
padding: 18px;
box-shadow: 0 25px 60px -12px var(--shadow);
display: grid;
gap: 14px;
}
.modal-header {
display: flex;
align-items: center;
justify-content: space-between;
gap: 12px;
}
.modal-header h3 {
margin: 0;
font-size: 1.2rem;
}
.custom-fields {
display: grid;
gap: 10px;
background: rgba(10, 10, 20, 0.6);
border: 1px solid rgba(255, 255, 255, 0.06);
border-radius: 14px;
padding: 10px;
}
.custom-fields-header {
display: flex;
align-items: center;
justify-content: space-between;
font-size: 0.9rem;
color: var(--text-secondary);
}
.custom-fields-list {
display: grid;
gap: 8px;
}
.custom-field-row {
display: grid;
grid-template-columns: 1fr 1fr auto;
gap: 8px;
align-items: center;
}
.custom-field-row input {
padding: 10px 12px;
font-size: 14px;
}
.custom-field-row button {
padding: 8px 10px;
font-size: 12px;
border-radius: 10px;
}
.chat-main { .chat-main {
display: grid; display: grid;
grid-template-rows: auto 1fr auto auto; grid-template-rows: auto 1fr auto auto;
+9 -53
View File
@@ -171,8 +171,6 @@ db.prepare(`
TEXT, TEXT,
details details
TEXT, TEXT,
custom_fields
TEXT,
created_at created_at
DATETIME DATETIME
DEFAULT DEFAULT
@@ -274,25 +272,25 @@ const deleteChatMessagesStmt = db.prepare(
); );
const listPersonasStmt = db.prepare(` const listPersonasStmt = db.prepare(`
SELECT id, name, pronouns, appearance, background, details, custom_fields, created_at, updated_at SELECT id, name, pronouns, appearance, background, details, created_at, updated_at
FROM personas FROM personas
WHERE username = ? WHERE username = ?
ORDER BY datetime(updated_at) DESC ORDER BY datetime(updated_at) DESC
`); `);
const getPersonaStmt = db.prepare(` const getPersonaStmt = db.prepare(`
SELECT id, name, pronouns, appearance, background, details, custom_fields SELECT id, name, pronouns, appearance, background, details
FROM personas FROM personas
WHERE id = ? AND username = ? WHERE id = ? AND username = ?
`); `);
const insertPersonaStmt = db.prepare( const insertPersonaStmt = db.prepare(
"INSERT INTO personas (username, name, pronouns, appearance, background, details, custom_fields) VALUES (?, ?, ?, ?, ?, ?, ?)" "INSERT INTO personas (username, name, pronouns, appearance, background, details) VALUES (?, ?, ?, ?, ?, ?)"
); );
const updatePersonaStmt = db.prepare(` const updatePersonaStmt = db.prepare(`
UPDATE personas UPDATE personas
SET name = ?, pronouns = ?, appearance = ?, background = ?, details = ?, custom_fields = ?, updated_at = CURRENT_TIMESTAMP SET name = ?, pronouns = ?, appearance = ?, background = ?, details = ?, updated_at = CURRENT_TIMESTAMP
WHERE id = ? AND username = ? WHERE id = ? AND username = ?
`); `);
@@ -311,7 +309,7 @@ const setActivePersonaStmt = db.prepare(`
`); `);
const getActivePersonaStmt = db.prepare(` const getActivePersonaStmt = db.prepare(`
SELECT p.id, p.name, p.pronouns, p.appearance, p.background, p.details, p.custom_fields SELECT p.id, p.name, p.pronouns, p.appearance, p.background, p.details
FROM user_settings us FROM user_settings us
JOIN personas p ON p.id = us.active_persona_id JOIN personas p ON p.id = us.active_persona_id
WHERE us.username = ? WHERE us.username = ?
@@ -389,27 +387,6 @@ const normalizePersonaField = (value) => {
return trimmed ? trimmed : null; return trimmed ? trimmed : null;
}; };
const normalizeCustomFields = (fields) => {
if (!Array.isArray(fields)) return null;
const cleaned = fields
.map(field => ({
label: (field?.label || "").trim(),
value: (field?.value || "").trim()
}))
.filter(field => field.label || field.value);
return cleaned.length ? JSON.stringify(cleaned) : null;
};
const parseCustomFields = (value) => {
if (!value) return [];
try {
const parsed = JSON.parse(value);
return Array.isArray(parsed) ? parsed : [];
} catch (error) {
return [];
}
};
const buildPersonaPrompt = (persona) => { const buildPersonaPrompt = (persona) => {
const lines = [ const lines = [
"You are roleplaying with the following persona. Stay in character, be engaging, and align with these details." "You are roleplaying with the following persona. Stay in character, be engaging, and align with these details."
@@ -419,10 +396,6 @@ const buildPersonaPrompt = (persona) => {
if (persona.appearance) lines.push(`Appearance: ${persona.appearance}`); if (persona.appearance) lines.push(`Appearance: ${persona.appearance}`);
if (persona.background) lines.push(`Background: ${persona.background}`); if (persona.background) lines.push(`Background: ${persona.background}`);
if (persona.details) lines.push(`Details: ${persona.details}`); if (persona.details) lines.push(`Details: ${persona.details}`);
const customFields = parseCustomFields(persona.custom_fields);
customFields.forEach(field => {
lines.push(`${field.label || "Custom"}: ${field.value}`);
});
return lines.join("\n"); return lines.join("\n");
}; };
@@ -511,10 +484,7 @@ app.post("/chats/:id/clear", requireLogin, (req, res) => {
app.get("/personas", requireLogin, (req, res) => { app.get("/personas", requireLogin, (req, res) => {
const user = req.session.user; const user = req.session.user;
const personas = listPersonasStmt.all(user).map(persona => ({ const personas = listPersonasStmt.all(user);
...persona,
customFields: parseCustomFields(persona.custom_fields)
}));
const activePersonaId = getActivePersonaIdStmt.get(user)?.active_persona_id ?? null; const activePersonaId = getActivePersonaIdStmt.get(user)?.active_persona_id ?? null;
res.json({personas, activePersonaId}); res.json({personas, activePersonaId});
}); });
@@ -529,23 +499,16 @@ app.post("/personas", requireLogin, (req, res) => {
const appearance = normalizePersonaField(req.body?.appearance); const appearance = normalizePersonaField(req.body?.appearance);
const background = normalizePersonaField(req.body?.background); const background = normalizePersonaField(req.body?.background);
const details = normalizePersonaField(req.body?.details); const details = normalizePersonaField(req.body?.details);
const customFields = normalizeCustomFields(req.body?.customFields);
const personaId = insertPersonaStmt.run( const personaId = insertPersonaStmt.run(
user, user,
name, name,
pronouns, pronouns,
appearance, appearance,
background, background,
details, details
customFields
).lastInsertRowid; ).lastInsertRowid;
const persona = getPersonaStmt.get(personaId, user); const persona = getPersonaStmt.get(personaId, user);
res.json({ res.json({persona});
persona: {
...persona,
customFields: parseCustomFields(persona.custom_fields)
}
});
}); });
app.put("/personas/:id", requireLogin, (req, res) => { app.put("/personas/:id", requireLogin, (req, res) => {
@@ -559,14 +522,12 @@ app.put("/personas/:id", requireLogin, (req, res) => {
const appearance = normalizePersonaField(req.body?.appearance); const appearance = normalizePersonaField(req.body?.appearance);
const background = normalizePersonaField(req.body?.background); const background = normalizePersonaField(req.body?.background);
const details = normalizePersonaField(req.body?.details); const details = normalizePersonaField(req.body?.details);
const customFields = normalizeCustomFields(req.body?.customFields);
const result = updatePersonaStmt.run( const result = updatePersonaStmt.run(
name, name,
pronouns, pronouns,
appearance, appearance,
background, background,
details, details,
customFields,
personaId, personaId,
user user
); );
@@ -574,12 +535,7 @@ app.put("/personas/:id", requireLogin, (req, res) => {
return res.status(404).json({error: "Persona not found"}); return res.status(404).json({error: "Persona not found"});
} }
const persona = getPersonaStmt.get(personaId, user); const persona = getPersonaStmt.get(personaId, user);
res.json({ res.json({persona});
persona: {
...persona,
customFields: parseCustomFields(persona.custom_fields)
}
});
}); });
app.delete("/personas/:id", requireLogin, (req, res) => { app.delete("/personas/:id", requireLogin, (req, res) => {