Add persona management and equip flow
This commit is contained in:
@@ -75,6 +75,48 @@
|
|||||||
<button id="newChat" class="secondary-action">New</button>
|
<button id="newChat" class="secondary-action">New</button>
|
||||||
</div>
|
</div>
|
||||||
<div id="chatList" class="chat-list"></div>
|
<div id="chatList" class="chat-list"></div>
|
||||||
|
<div class="persona-panel">
|
||||||
|
<div class="sidebar-header persona-header">
|
||||||
|
<div>
|
||||||
|
<p class="eyebrow">Roleplay</p>
|
||||||
|
<h2>Personas</h2>
|
||||||
|
</div>
|
||||||
|
<div class="persona-header-actions">
|
||||||
|
<button id="newPersona" class="secondary-action" type="button">New</button>
|
||||||
|
<button id="clearPersona" class="secondary-action" type="button">Clear</button>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<p id="activePersonaStatus" class="status persona-status">No persona equipped.</p>
|
||||||
|
<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>
|
||||||
</aside>
|
</aside>
|
||||||
<section class="chat-main">
|
<section class="chat-main">
|
||||||
<div class="chat-header">
|
<div class="chat-header">
|
||||||
|
|||||||
+168
-1
@@ -20,10 +20,25 @@ const newChatBtn = document.getElementById("newChat");
|
|||||||
const renameChatBtn = document.getElementById("renameChat");
|
const renameChatBtn = document.getElementById("renameChat");
|
||||||
const deleteChatBtn = document.getElementById("deleteChat");
|
const deleteChatBtn = document.getElementById("deleteChat");
|
||||||
const activeChatTitle = document.getElementById("activeChatTitle");
|
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 newPersonaBtn = document.getElementById("newPersona");
|
||||||
|
const clearPersonaBtn = document.getElementById("clearPersona");
|
||||||
|
const personaCancelBtn = document.getElementById("personaCancel");
|
||||||
|
const activePersonaStatus = document.getElementById("activePersonaStatus");
|
||||||
|
|
||||||
let isProcessing = false;
|
let isProcessing = false;
|
||||||
let chatSessions = [];
|
let chatSessions = [];
|
||||||
let activeChatId = null;
|
let activeChatId = null;
|
||||||
|
let personas = [];
|
||||||
|
let activePersonaId = null;
|
||||||
|
let editingPersonaId = null;
|
||||||
|
|
||||||
const displayModels = async () => {
|
const displayModels = async () => {
|
||||||
const select = document.getElementById("model");
|
const select = document.getElementById("model");
|
||||||
@@ -168,6 +183,143 @@ 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) {
|
||||||
|
personaForm.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 || '';
|
||||||
|
} else {
|
||||||
|
editingPersonaId = null;
|
||||||
|
personaFormTitle.textContent = 'Create persona';
|
||||||
|
personaNameInput.value = '';
|
||||||
|
personaPronounsInput.value = '';
|
||||||
|
personaAppearanceInput.value = '';
|
||||||
|
personaBackgroundInput.value = '';
|
||||||
|
personaDetailsInput.value = '';
|
||||||
|
}
|
||||||
|
personaNameInput.focus();
|
||||||
|
}
|
||||||
|
|
||||||
|
function closePersonaForm() {
|
||||||
|
personaForm.classList.add('hidden');
|
||||||
|
editingPersonaId = null;
|
||||||
|
}
|
||||||
|
|
||||||
|
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 || [];
|
||||||
|
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()
|
||||||
|
};
|
||||||
|
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();
|
||||||
|
}
|
||||||
|
|
||||||
function setLoadingState(loading) {
|
function setLoadingState(loading) {
|
||||||
isProcessing = loading;
|
isProcessing = loading;
|
||||||
sendBtn.disabled = loading;
|
sendBtn.disabled = loading;
|
||||||
@@ -282,6 +434,7 @@ async function checkSession() {
|
|||||||
authDiv.style.display = "none";
|
authDiv.style.display = "none";
|
||||||
chatDiv.style.display = "block";
|
chatDiv.style.display = "block";
|
||||||
await loadChatSessions();
|
await loadChatSessions();
|
||||||
|
await loadPersonas();
|
||||||
msgInput.focus();
|
msgInput.focus();
|
||||||
return true;
|
return true;
|
||||||
}
|
}
|
||||||
@@ -290,6 +443,7 @@ async function checkSession() {
|
|||||||
|
|
||||||
async function loadServerChat() {
|
async function loadServerChat() {
|
||||||
await loadChatSessions();
|
await loadChatSessions();
|
||||||
|
await loadPersonas();
|
||||||
}
|
}
|
||||||
|
|
||||||
async function handleAuth(endpoint, credentials) {
|
async function handleAuth(endpoint, credentials) {
|
||||||
@@ -396,6 +550,8 @@ document.getElementById("logout").onclick = async () => {
|
|||||||
clearMessages();
|
clearMessages();
|
||||||
chatSessions = [];
|
chatSessions = [];
|
||||||
activeChatId = null;
|
activeChatId = null;
|
||||||
|
personas = [];
|
||||||
|
activePersonaId = null;
|
||||||
setAuthMessage("Logged out.", 'success');
|
setAuthMessage("Logged out.", 'success');
|
||||||
showAuthScreen('login');
|
showAuthScreen('login');
|
||||||
loginUsernameInput.value = "";
|
loginUsernameInput.value = "";
|
||||||
@@ -424,12 +580,23 @@ deleteChatBtn.addEventListener('click', () => {
|
|||||||
deleteChat(activeChatId);
|
deleteChat(activeChatId);
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
+
|
|
||||||
msgInput.addEventListener('input', function () {
|
msgInput.addEventListener('input', function () {
|
||||||
this.style.height = 'auto';
|
this.style.height = 'auto';
|
||||||
this.style.height = Math.min(this.scrollHeight, 120) + 'px';
|
this.style.height = Math.min(this.scrollHeight, 120) + 'px';
|
||||||
});
|
});
|
||||||
|
|
||||||
|
newPersonaBtn.addEventListener('click', () => openPersonaForm());
|
||||||
|
|
||||||
|
personaCancelBtn.addEventListener('click', () => closePersonaForm());
|
||||||
|
|
||||||
|
clearPersonaBtn.addEventListener('click', () => clearPersona());
|
||||||
|
|
||||||
|
personaForm.addEventListener('submit', (event) => {
|
||||||
|
event.preventDefault();
|
||||||
|
savePersona();
|
||||||
|
});
|
||||||
|
|
||||||
window.addEventListener('load', () => {
|
window.addEventListener('load', () => {
|
||||||
checkSession();
|
checkSession();
|
||||||
displayModels();
|
displayModels();
|
||||||
|
|||||||
@@ -387,6 +387,101 @@ button:disabled {
|
|||||||
background: rgba(255, 255, 255, 0.08);
|
background: rgba(255, 255, 255, 0.08);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
.persona-panel {
|
||||||
|
display: flex;
|
||||||
|
flex-direction: column;
|
||||||
|
gap: 12px;
|
||||||
|
border-top: 1px solid rgba(255, 255, 255, 0.06);
|
||||||
|
padding-top: 16px;
|
||||||
|
min-height: 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
.persona-header {
|
||||||
|
align-items: flex-start;
|
||||||
|
}
|
||||||
|
|
||||||
|
.persona-header-actions {
|
||||||
|
display: flex;
|
||||||
|
gap: 8px;
|
||||||
|
flex-wrap: wrap;
|
||||||
|
}
|
||||||
|
|
||||||
|
.persona-status {
|
||||||
|
text-align: left;
|
||||||
|
margin: 0;
|
||||||
|
padding: 10px 12px;
|
||||||
|
font-size: 13px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.persona-list {
|
||||||
|
display: grid;
|
||||||
|
gap: 10px;
|
||||||
|
max-height: 200px;
|
||||||
|
overflow-y: auto;
|
||||||
|
padding-right: 4px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.persona-item {
|
||||||
|
background: rgba(15, 15, 35, 0.7);
|
||||||
|
border: 1px solid rgba(255, 255, 255, 0.06);
|
||||||
|
border-radius: 14px;
|
||||||
|
padding: 10px 12px;
|
||||||
|
display: grid;
|
||||||
|
gap: 8px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.persona-item.active {
|
||||||
|
border-color: rgba(99, 102, 241, 0.6);
|
||||||
|
box-shadow: 0 8px 18px rgba(99, 102, 241, 0.18);
|
||||||
|
}
|
||||||
|
|
||||||
|
.persona-item h4 {
|
||||||
|
margin: 0;
|
||||||
|
font-size: 0.98rem;
|
||||||
|
}
|
||||||
|
|
||||||
|
.persona-item p {
|
||||||
|
margin: 0;
|
||||||
|
font-size: 0.82rem;
|
||||||
|
color: var(--text-secondary);
|
||||||
|
}
|
||||||
|
|
||||||
|
.persona-item-actions {
|
||||||
|
display: flex;
|
||||||
|
gap: 8px;
|
||||||
|
flex-wrap: wrap;
|
||||||
|
}
|
||||||
|
|
||||||
|
.persona-item-actions button {
|
||||||
|
padding: 6px 10px;
|
||||||
|
font-size: 12px;
|
||||||
|
border-radius: 10px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.persona-form {
|
||||||
|
display: grid;
|
||||||
|
gap: 10px;
|
||||||
|
background: rgba(10, 10, 20, 0.75);
|
||||||
|
border: 1px solid rgba(255, 255, 255, 0.06);
|
||||||
|
border-radius: 16px;
|
||||||
|
padding: 12px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.persona-form.hidden {
|
||||||
|
display: none;
|
||||||
|
}
|
||||||
|
|
||||||
|
.persona-form h3 {
|
||||||
|
margin: 0;
|
||||||
|
font-size: 1rem;
|
||||||
|
}
|
||||||
|
|
||||||
|
.persona-actions {
|
||||||
|
display: flex;
|
||||||
|
gap: 8px;
|
||||||
|
flex-wrap: wrap;
|
||||||
|
}
|
||||||
|
|
||||||
.chat-main {
|
.chat-main {
|
||||||
display: grid;
|
display: grid;
|
||||||
grid-template-rows: auto 1fr auto auto;
|
grid-template-rows: auto 1fr auto auto;
|
||||||
|
|||||||
@@ -147,6 +147,59 @@ db.prepare(`
|
|||||||
) ON DELETE CASCADE
|
) ON DELETE CASCADE
|
||||||
)
|
)
|
||||||
`).run();
|
`).run();
|
||||||
|
db.prepare(`
|
||||||
|
CREATE TABLE IF NOT EXISTS personas
|
||||||
|
(
|
||||||
|
id
|
||||||
|
INTEGER
|
||||||
|
PRIMARY
|
||||||
|
KEY
|
||||||
|
AUTOINCREMENT,
|
||||||
|
username
|
||||||
|
TEXT
|
||||||
|
NOT
|
||||||
|
NULL,
|
||||||
|
name
|
||||||
|
TEXT
|
||||||
|
NOT
|
||||||
|
NULL,
|
||||||
|
pronouns
|
||||||
|
TEXT,
|
||||||
|
appearance
|
||||||
|
TEXT,
|
||||||
|
background
|
||||||
|
TEXT,
|
||||||
|
details
|
||||||
|
TEXT,
|
||||||
|
created_at
|
||||||
|
DATETIME
|
||||||
|
DEFAULT
|
||||||
|
CURRENT_TIMESTAMP,
|
||||||
|
updated_at
|
||||||
|
DATETIME
|
||||||
|
DEFAULT
|
||||||
|
CURRENT_TIMESTAMP
|
||||||
|
)
|
||||||
|
`).run();
|
||||||
|
db.prepare(`
|
||||||
|
CREATE TABLE IF NOT EXISTS user_settings
|
||||||
|
(
|
||||||
|
username
|
||||||
|
TEXT
|
||||||
|
PRIMARY
|
||||||
|
KEY,
|
||||||
|
active_persona_id
|
||||||
|
INTEGER,
|
||||||
|
FOREIGN
|
||||||
|
KEY
|
||||||
|
(
|
||||||
|
active_persona_id
|
||||||
|
) REFERENCES personas
|
||||||
|
(
|
||||||
|
id
|
||||||
|
) ON DELETE SET NULL
|
||||||
|
)
|
||||||
|
`).run();
|
||||||
|
|
||||||
const insertUserStmt = db.prepare("INSERT INTO users (username, password) VALUES (?, ?)");
|
const insertUserStmt = db.prepare("INSERT INTO users (username, password) VALUES (?, ?)");
|
||||||
const getUserStmt = db.prepare("SELECT username, password FROM users WHERE username = ?");
|
const getUserStmt = db.prepare("SELECT username, password FROM users WHERE username = ?");
|
||||||
@@ -218,6 +271,50 @@ const deleteChatMessagesStmt = db.prepare(
|
|||||||
"DELETE FROM chat_messages WHERE chat_id = ?"
|
"DELETE FROM chat_messages WHERE chat_id = ?"
|
||||||
);
|
);
|
||||||
|
|
||||||
|
const listPersonasStmt = db.prepare(`
|
||||||
|
SELECT id, name, pronouns, appearance, background, details, created_at, updated_at
|
||||||
|
FROM personas
|
||||||
|
WHERE username = ?
|
||||||
|
ORDER BY datetime(updated_at) DESC
|
||||||
|
`);
|
||||||
|
|
||||||
|
const getPersonaStmt = db.prepare(`
|
||||||
|
SELECT id, name, pronouns, appearance, background, details
|
||||||
|
FROM personas
|
||||||
|
WHERE id = ? AND username = ?
|
||||||
|
`);
|
||||||
|
|
||||||
|
const insertPersonaStmt = db.prepare(
|
||||||
|
"INSERT INTO personas (username, name, pronouns, appearance, background, details) VALUES (?, ?, ?, ?, ?, ?)"
|
||||||
|
);
|
||||||
|
|
||||||
|
const updatePersonaStmt = db.prepare(`
|
||||||
|
UPDATE personas
|
||||||
|
SET name = ?, pronouns = ?, appearance = ?, background = ?, details = ?, updated_at = CURRENT_TIMESTAMP
|
||||||
|
WHERE id = ? AND username = ?
|
||||||
|
`);
|
||||||
|
|
||||||
|
const deletePersonaStmt = db.prepare(
|
||||||
|
"DELETE FROM personas WHERE id = ? AND username = ?"
|
||||||
|
);
|
||||||
|
|
||||||
|
const getActivePersonaIdStmt = db.prepare(
|
||||||
|
"SELECT active_persona_id FROM user_settings WHERE username = ?"
|
||||||
|
);
|
||||||
|
|
||||||
|
const setActivePersonaStmt = db.prepare(`
|
||||||
|
INSERT INTO user_settings (username, active_persona_id)
|
||||||
|
VALUES (?, ?)
|
||||||
|
ON CONFLICT(username) DO UPDATE SET active_persona_id = excluded.active_persona_id
|
||||||
|
`);
|
||||||
|
|
||||||
|
const getActivePersonaStmt = db.prepare(`
|
||||||
|
SELECT p.id, p.name, p.pronouns, p.appearance, p.background, p.details
|
||||||
|
FROM user_settings us
|
||||||
|
JOIN personas p ON p.id = us.active_persona_id
|
||||||
|
WHERE us.username = ?
|
||||||
|
`);
|
||||||
|
|
||||||
const migrationNeeded = db.prepare(
|
const migrationNeeded = db.prepare(
|
||||||
"SELECT COUNT(*) as count FROM chat_messages"
|
"SELECT COUNT(*) as count FROM chat_messages"
|
||||||
).get();
|
).get();
|
||||||
@@ -279,14 +376,38 @@ app.post("/logout", (req, res) => {
|
|||||||
req.session.destroy(() => res.json({message: "Logged out"}));
|
req.session.destroy(() => res.json({message: "Logged out"}));
|
||||||
});
|
});
|
||||||
|
|
||||||
|
const normalizePersonaField = (value) => {
|
||||||
|
const trimmed = (value || "").trim();
|
||||||
|
return trimmed ? trimmed : null;
|
||||||
|
};
|
||||||
|
|
||||||
|
const buildPersonaPrompt = (persona) => {
|
||||||
|
const lines = [
|
||||||
|
"You are roleplaying with the following persona. Stay in character, be engaging, and align with these details."
|
||||||
|
];
|
||||||
|
if (persona.name) lines.push(`Name: ${persona.name}`);
|
||||||
|
if (persona.pronouns) lines.push(`Pronouns: ${persona.pronouns}`);
|
||||||
|
if (persona.appearance) lines.push(`Appearance: ${persona.appearance}`);
|
||||||
|
if (persona.background) lines.push(`Background: ${persona.background}`);
|
||||||
|
if (persona.details) lines.push(`Details: ${persona.details}`);
|
||||||
|
return lines.join("\n");
|
||||||
|
};
|
||||||
|
|
||||||
const requireLogin = (req, res, next) => {
|
const requireLogin = (req, res, next) => {
|
||||||
if (!req.session.user) return res.status(403).json({error: "Not logged in"});
|
if (!req.session.user) return res.status(403).json({error: "Not logged in"});
|
||||||
next();
|
next();
|
||||||
};
|
};
|
||||||
|
|
||||||
app.get("/models", async (req, res) => {
|
app.get("/models", async (req, res) => {
|
||||||
|
try {
|
||||||
const response = await fetch("https://ai.krishd.ch/api/tags");
|
const response = await fetch("https://ai.krishd.ch/api/tags");
|
||||||
|
if (!response.ok) {
|
||||||
|
return res.status(500).json({error: "Failed to load models"});
|
||||||
|
}
|
||||||
res.json(await response.json());
|
res.json(await response.json());
|
||||||
|
} catch (error) {
|
||||||
|
res.status(500).json({error: "Failed to load models"});
|
||||||
|
}
|
||||||
});
|
});
|
||||||
|
|
||||||
app.get("/chats", requireLogin, (req, res) => {
|
app.get("/chats", requireLogin, (req, res) => {
|
||||||
@@ -355,6 +476,93 @@ app.post("/chats/:id/clear", requireLogin, (req, res) => {
|
|||||||
res.json({message: "Chat cleared"});
|
res.json({message: "Chat cleared"});
|
||||||
});
|
});
|
||||||
|
|
||||||
|
app.get("/personas", requireLogin, (req, res) => {
|
||||||
|
const user = req.session.user;
|
||||||
|
const personas = listPersonasStmt.all(user);
|
||||||
|
const activePersonaId = getActivePersonaIdStmt.get(user)?.active_persona_id ?? null;
|
||||||
|
res.json({personas, activePersonaId});
|
||||||
|
});
|
||||||
|
|
||||||
|
app.post("/personas", requireLogin, (req, res) => {
|
||||||
|
const user = req.session.user;
|
||||||
|
const name = (req.body?.name || "").trim();
|
||||||
|
if (!name) {
|
||||||
|
return res.status(400).json({error: "Name is required"});
|
||||||
|
}
|
||||||
|
const pronouns = normalizePersonaField(req.body?.pronouns);
|
||||||
|
const appearance = normalizePersonaField(req.body?.appearance);
|
||||||
|
const background = normalizePersonaField(req.body?.background);
|
||||||
|
const details = normalizePersonaField(req.body?.details);
|
||||||
|
const personaId = insertPersonaStmt.run(
|
||||||
|
user,
|
||||||
|
name,
|
||||||
|
pronouns,
|
||||||
|
appearance,
|
||||||
|
background,
|
||||||
|
details
|
||||||
|
).lastInsertRowid;
|
||||||
|
const persona = getPersonaStmt.get(personaId, user);
|
||||||
|
res.json({persona});
|
||||||
|
});
|
||||||
|
|
||||||
|
app.put("/personas/:id", requireLogin, (req, res) => {
|
||||||
|
const user = req.session.user;
|
||||||
|
const personaId = Number(req.params.id);
|
||||||
|
const name = (req.body?.name || "").trim();
|
||||||
|
if (!name) {
|
||||||
|
return res.status(400).json({error: "Name is required"});
|
||||||
|
}
|
||||||
|
const pronouns = normalizePersonaField(req.body?.pronouns);
|
||||||
|
const appearance = normalizePersonaField(req.body?.appearance);
|
||||||
|
const background = normalizePersonaField(req.body?.background);
|
||||||
|
const details = normalizePersonaField(req.body?.details);
|
||||||
|
const result = updatePersonaStmt.run(
|
||||||
|
name,
|
||||||
|
pronouns,
|
||||||
|
appearance,
|
||||||
|
background,
|
||||||
|
details,
|
||||||
|
personaId,
|
||||||
|
user
|
||||||
|
);
|
||||||
|
if (result.changes === 0) {
|
||||||
|
return res.status(404).json({error: "Persona not found"});
|
||||||
|
}
|
||||||
|
const persona = getPersonaStmt.get(personaId, user);
|
||||||
|
res.json({persona});
|
||||||
|
});
|
||||||
|
|
||||||
|
app.delete("/personas/:id", requireLogin, (req, res) => {
|
||||||
|
const user = req.session.user;
|
||||||
|
const personaId = Number(req.params.id);
|
||||||
|
const activePersonaId = getActivePersonaIdStmt.get(user)?.active_persona_id ?? null;
|
||||||
|
const result = deletePersonaStmt.run(personaId, user);
|
||||||
|
if (result.changes === 0) {
|
||||||
|
return res.status(404).json({error: "Persona not found"});
|
||||||
|
}
|
||||||
|
if (activePersonaId === personaId) {
|
||||||
|
setActivePersonaStmt.run(user, null);
|
||||||
|
}
|
||||||
|
res.json({message: "Persona deleted"});
|
||||||
|
});
|
||||||
|
|
||||||
|
app.post("/personas/:id/equip", requireLogin, (req, res) => {
|
||||||
|
const user = req.session.user;
|
||||||
|
const personaId = Number(req.params.id);
|
||||||
|
const persona = getPersonaStmt.get(personaId, user);
|
||||||
|
if (!persona) {
|
||||||
|
return res.status(404).json({error: "Persona not found"});
|
||||||
|
}
|
||||||
|
setActivePersonaStmt.run(user, personaId);
|
||||||
|
res.json({activePersonaId: personaId});
|
||||||
|
});
|
||||||
|
|
||||||
|
app.post("/personas/clear", requireLogin, (req, res) => {
|
||||||
|
const user = req.session.user;
|
||||||
|
setActivePersonaStmt.run(user, null);
|
||||||
|
res.json({activePersonaId: null});
|
||||||
|
});
|
||||||
|
|
||||||
app.post("/chat", requireLogin, async (req, res) => {
|
app.post("/chat", requireLogin, async (req, res) => {
|
||||||
const user = req.session.user;
|
const user = req.session.user;
|
||||||
const {message, model, chatId} = req.body;
|
const {message, model, chatId} = req.body;
|
||||||
@@ -377,15 +585,25 @@ app.post("/chat", requireLogin, async (req, res) => {
|
|||||||
state.activeRequests++;
|
state.activeRequests++;
|
||||||
|
|
||||||
try {
|
try {
|
||||||
|
const activePersona = getActivePersonaStmt.get(user);
|
||||||
const history = getRecentChatMessagesStmt.all(targetChatId);
|
const history = getRecentChatMessagesStmt.all(targetChatId);
|
||||||
const conversation = [...history].reverse();
|
const conversation = [...history].reverse();
|
||||||
conversation.push({role: "user", content: message});
|
conversation.push({role: "user", content: message});
|
||||||
insertChatMessageStmt.run(targetChatId, "user", message);
|
insertChatMessageStmt.run(targetChatId, "user", message);
|
||||||
touchChatSessionStmt.run(targetChatId, user);
|
touchChatSessionStmt.run(targetChatId, user);
|
||||||
const messagesPayload = conversation.map(m => ({
|
const messagesPayload = [];
|
||||||
|
if (activePersona) {
|
||||||
|
messagesPayload.push({
|
||||||
|
role: "system",
|
||||||
|
content: buildPersonaPrompt(activePersona)
|
||||||
|
});
|
||||||
|
}
|
||||||
|
messagesPayload.push(
|
||||||
|
...conversation.map(m => ({
|
||||||
role: m.role === "bot" ? "assistant" : "user",
|
role: m.role === "bot" ? "assistant" : "user",
|
||||||
content: m.content
|
content: m.content
|
||||||
}));
|
}))
|
||||||
|
);
|
||||||
|
|
||||||
const response = await fetch("https://ai.krishd.ch/api/chat", {
|
const response = await fetch("https://ai.krishd.ch/api/chat", {
|
||||||
method: "POST",
|
method: "POST",
|
||||||
|
|||||||
Reference in New Issue
Block a user