Add persona market publishing and collection

This commit is contained in:
2026-01-14 13:55:48 +01:00
parent 3c87fd842f
commit 4be9536a86
4 changed files with 261 additions and 1 deletions
+21
View File
@@ -123,6 +123,27 @@
<p id="activeUserPersonaStatus" class="status persona-status">No user persona set.</p> <p id="activeUserPersonaStatus" class="status persona-status">No user persona set.</p>
<div id="userPersonaList" class="persona-list"></div> <div id="userPersonaList" class="persona-list"></div>
</div> </div>
<div class="persona-section">
<div class="persona-section-header">
<div>
<p class="eyebrow">Community</p>
<h2>Persona Market</h2>
</div>
<div class="persona-section-actions">
<button id="refreshMarket" class="secondary-action" type="button">Refresh</button>
</div>
</div>
<div class="persona-market-columns">
<div class="persona-market-column">
<h3>AI personas</h3>
<div id="marketPersonaList" class="persona-list"></div>
</div>
<div class="persona-market-column">
<h3>User personas</h3>
<div id="marketUserPersonaList" class="persona-list"></div>
</div>
</div>
</div>
</div> </div>
</div> </div>
</div> </div>
+80
View File
@@ -41,6 +41,9 @@ const personaModal = document.getElementById("personaModal");
const personaCloseBtn = document.getElementById("personaClose"); const personaCloseBtn = document.getElementById("personaClose");
const personaMenuButton = document.getElementById("personaMenuButton"); const personaMenuButton = document.getElementById("personaMenuButton");
const personaPopover = document.getElementById("personaPopover"); const personaPopover = document.getElementById("personaPopover");
const marketPersonaList = document.getElementById("marketPersonaList");
const marketUserPersonaList = document.getElementById("marketUserPersonaList");
const refreshMarketBtn = document.getElementById("refreshMarket");
let isProcessing = false; let isProcessing = false;
let chatSessions = []; let chatSessions = [];
@@ -51,6 +54,8 @@ let activePersonaId = null;
let activeUserPersonaId = null; let activeUserPersonaId = null;
let editingPersonaId = null; let editingPersonaId = null;
let editingPersonaType = "assistant"; let editingPersonaType = "assistant";
let marketPersonas = [];
let publishedPersonaIds = new Set();
const displayModels = async () => { const displayModels = async () => {
const select = document.getElementById("model"); const select = document.getElementById("model");
@@ -291,6 +296,12 @@ function renderPersonaList(personaItems, activeId, listElement, personaType) {
editBtn.textContent = 'Edit'; editBtn.textContent = 'Edit';
editBtn.addEventListener('click', () => openPersonaForm(persona)); editBtn.addEventListener('click', () => openPersonaForm(persona));
const publishBtn = document.createElement('button');
publishBtn.type = 'button';
const isPublished = publishedPersonaIds.has(persona.id);
publishBtn.textContent = isPublished ? 'Update listing' : 'Publish';
publishBtn.addEventListener('click', () => publishPersona(persona.id));
const deleteBtn = document.createElement('button'); const deleteBtn = document.createElement('button');
deleteBtn.type = 'button'; deleteBtn.type = 'button';
deleteBtn.textContent = 'Delete'; deleteBtn.textContent = 'Delete';
@@ -298,6 +309,7 @@ function renderPersonaList(personaItems, activeId, listElement, personaType) {
actions.appendChild(equipBtn); actions.appendChild(equipBtn);
actions.appendChild(editBtn); actions.appendChild(editBtn);
actions.appendChild(publishBtn);
actions.appendChild(deleteBtn); actions.appendChild(deleteBtn);
item.appendChild(title); item.appendChild(title);
@@ -315,10 +327,54 @@ async function loadPersonas() {
userPersonas = res.userPersonas || []; userPersonas = res.userPersonas || [];
activePersonaId = res.activePersonaId || null; activePersonaId = res.activePersonaId || null;
activeUserPersonaId = res.activeUserPersonaId || null; activeUserPersonaId = res.activeUserPersonaId || null;
publishedPersonaIds = new Set(res.publishedPersonaIds || []);
renderPersonaList(assistantPersonas, activePersonaId, personaList, "assistant"); renderPersonaList(assistantPersonas, activePersonaId, personaList, "assistant");
renderPersonaList(userPersonas, activeUserPersonaId, userPersonaList, "user"); renderPersonaList(userPersonas, activeUserPersonaId, userPersonaList, "user");
} }
function renderMarketList(personaItems, listElement) {
listElement.innerHTML = '';
if (!personaItems.length) {
const empty = document.createElement('p');
empty.className = 'status persona-status';
empty.textContent = 'No market personas yet. Be the first to publish!';
listElement.appendChild(empty);
return;
}
personaItems.forEach(persona => {
const item = document.createElement('div');
item.className = 'persona-item';
const title = document.createElement('h4');
title.textContent = persona.name;
const meta = document.createElement('p');
const pronouns = persona.pronouns ? `Pronouns: ${persona.pronouns}` : 'Pronouns: —';
meta.textContent = `By ${persona.creator_username} • ${pronouns}`;
const actions = document.createElement('div');
actions.className = 'persona-item-actions';
const getBtn = document.createElement('button');
getBtn.type = 'button';
getBtn.textContent = 'Get & equip';
getBtn.addEventListener('click', () => collectMarketPersona(persona.id, persona.persona_type));
actions.appendChild(getBtn);
item.appendChild(title);
item.appendChild(meta);
item.appendChild(actions);
listElement.appendChild(item);
});
}
async function loadMarketPersonas() {
const res = await get('/personas/market');
if (res.error) return;
marketPersonas = res.personas || [];
const assistantMarket = marketPersonas.filter(persona => persona.persona_type === 'assistant');
const userMarket = marketPersonas.filter(persona => persona.persona_type === 'user');
renderMarketList(assistantMarket, marketPersonaList);
renderMarketList(userMarket, marketUserPersonaList);
}
async function equipPersona(personaId, personaType) { async function equipPersona(personaId, personaType) {
const endpoint = personaType === "user" const endpoint = personaType === "user"
? `/personas/${personaId}/equip-user` ? `/personas/${personaId}/equip-user`
@@ -379,6 +435,24 @@ async function deletePersona(personaId) {
await loadPersonas(); await loadPersonas();
} }
async function publishPersona(personaId) {
const res = await post(`/personas/${personaId}/publish`, {});
if (res.error) return;
await loadPersonas();
await loadMarketPersonas();
}
async function collectMarketPersona(marketId, personaType) {
const res = await post(`/personas/market/${marketId}/collect`, {equip: true});
if (res.error) return;
if (personaType === "user") {
activeUserPersonaId = res.activeUserPersonaId || res.persona?.id || null;
} else {
activePersonaId = res.activePersonaId || res.persona?.id || null;
}
await loadPersonas();
}
function setLoadingState(loading) { function setLoadingState(loading) {
isProcessing = loading; isProcessing = loading;
sendBtn.disabled = loading; sendBtn.disabled = loading;
@@ -494,6 +568,7 @@ async function checkSession() {
chatDiv.style.display = "block"; chatDiv.style.display = "block";
await loadChatSessions(); await loadChatSessions();
await loadPersonas(); await loadPersonas();
await loadMarketPersonas();
msgInput.focus(); msgInput.focus();
return true; return true;
} }
@@ -503,6 +578,7 @@ async function checkSession() {
async function loadServerChat() { async function loadServerChat() {
await loadChatSessions(); await loadChatSessions();
await loadPersonas(); await loadPersonas();
await loadMarketPersonas();
} }
async function handleAuth(endpoint, credentials) { async function handleAuth(endpoint, credentials) {
@@ -613,6 +689,8 @@ document.getElementById("logout").onclick = async () => {
userPersonas = []; userPersonas = [];
activePersonaId = null; activePersonaId = null;
activeUserPersonaId = null; activeUserPersonaId = null;
marketPersonas = [];
publishedPersonaIds = new Set();
setAuthMessage("Logged out.", 'success'); setAuthMessage("Logged out.", 'success');
showAuthScreen('login'); showAuthScreen('login');
loginUsernameInput.value = ""; loginUsernameInput.value = "";
@@ -655,6 +733,8 @@ clearPersonaBtn.addEventListener('click', () => clearPersona("assistant"));
clearUserPersonaBtn.addEventListener('click', () => clearPersona("user")); clearUserPersonaBtn.addEventListener('click', () => clearPersona("user"));
refreshMarketBtn.addEventListener('click', () => loadMarketPersonas());
personaForm.addEventListener('submit', (event) => { personaForm.addEventListener('submit', (event) => {
event.preventDefault(); event.preventDefault();
savePersona(); savePersona();
+12
View File
@@ -842,6 +842,18 @@ button:disabled {
flex-wrap: wrap; flex-wrap: wrap;
} }
.persona-market-columns {
display: grid;
grid-template-columns: repeat(auto-fit, minmax(210px, 1fr));
gap: 12px;
}
.persona-market-column h3 {
margin: 0 0 6px;
font-size: 0.95rem;
color: var(--text-secondary);
}
.persona-popover .persona-list { .persona-popover .persona-list {
max-height: 220px; max-height: 220px;
} }
+148 -1
View File
@@ -214,6 +214,51 @@ db.prepare(`
) ON DELETE SET NULL ) ON DELETE SET NULL
) )
`).run(); `).run();
db.prepare(`
CREATE TABLE IF NOT EXISTS persona_market
(
id
INTEGER
PRIMARY
KEY
AUTOINCREMENT,
persona_id
INTEGER,
creator_username
TEXT
NOT
NULL,
name
TEXT
NOT
NULL,
pronouns
TEXT,
appearance
TEXT,
background
TEXT,
details
TEXT,
persona_type
TEXT
DEFAULT
'assistant',
created_at
DATETIME
DEFAULT
CURRENT_TIMESTAMP,
updated_at
DATETIME
DEFAULT
CURRENT_TIMESTAMP,
UNIQUE
(
persona_id,
creator_username
)
)
`).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 = ?");
@@ -342,6 +387,43 @@ const getActiveUserPersonaStmt = db.prepare(`
WHERE us.username = ? WHERE us.username = ?
`); `);
const listMarketPersonasStmt = db.prepare(`
SELECT id, persona_id, creator_username, name, pronouns, appearance, background, details, persona_type, created_at, updated_at
FROM persona_market
ORDER BY datetime(updated_at) DESC
`);
const getMarketPersonaStmt = db.prepare(`
SELECT id, persona_id, creator_username, name, pronouns, appearance, background, details, persona_type
FROM persona_market
WHERE id = ?
`);
const getMarketPersonaByPersonaIdStmt = db.prepare(`
SELECT id, persona_id, creator_username, name, pronouns, appearance, background, details, persona_type
FROM persona_market
WHERE persona_id = ? AND creator_username = ?
`);
const upsertMarketPersonaStmt = db.prepare(`
INSERT INTO persona_market (persona_id, creator_username, name, pronouns, appearance, background, details, persona_type)
VALUES (?, ?, ?, ?, ?, ?, ?, ?)
ON CONFLICT(persona_id, creator_username) DO UPDATE SET
name = excluded.name,
pronouns = excluded.pronouns,
appearance = excluded.appearance,
background = excluded.background,
details = excluded.details,
persona_type = excluded.persona_type,
updated_at = CURRENT_TIMESTAMP
`);
const listPublishedPersonaIdsStmt = db.prepare(`
SELECT persona_id
FROM persona_market
WHERE creator_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();
@@ -377,6 +459,12 @@ if (!hasActiveUserPersona) {
db.prepare("ALTER TABLE user_settings ADD COLUMN active_user_persona_id INTEGER").run(); db.prepare("ALTER TABLE user_settings ADD COLUMN active_user_persona_id INTEGER").run();
} }
const marketColumns = db.prepare("PRAGMA table_info(persona_market)").all();
const hasPersonaIdColumn = marketColumns.some(column => column.name === "persona_id");
if (!hasPersonaIdColumn) {
db.prepare("ALTER TABLE persona_market ADD COLUMN persona_id INTEGER").run();
}
const FileStore = FileStoreFactory(session); const FileStore = FileStoreFactory(session);
app.use(bodyParser.json()); app.use(bodyParser.json());
app.use(express.static("public")); app.use(express.static("public"));
@@ -536,11 +624,13 @@ app.get("/personas", requireLogin, (req, res) => {
const assistantPersonas = listPersonasByTypeStmt.all(user, "assistant"); const assistantPersonas = listPersonasByTypeStmt.all(user, "assistant");
const userPersonas = listPersonasByTypeStmt.all(user, "user"); const userPersonas = listPersonasByTypeStmt.all(user, "user");
const activePersonas = getActivePersonaIdStmt.get(user) || {}; const activePersonas = getActivePersonaIdStmt.get(user) || {};
const publishedPersonaIds = listPublishedPersonaIdsStmt.all(user).map(row => row.persona_id);
res.json({ res.json({
assistantPersonas, assistantPersonas,
userPersonas, userPersonas,
activePersonaId: activePersonas.active_persona_id ?? null, activePersonaId: activePersonas.active_persona_id ?? null,
activeUserPersonaId: activePersonas.active_user_persona_id ?? null activeUserPersonaId: activePersonas.active_user_persona_id ?? null,
publishedPersonaIds
}); });
}); });
@@ -649,6 +739,63 @@ app.post("/personas/user/clear", requireLogin, (req, res) => {
res.json({activeUserPersonaId: null}); res.json({activeUserPersonaId: null});
}); });
app.get("/personas/market", requireLogin, (req, res) => {
const personas = listMarketPersonasStmt.all();
res.json({personas});
});
app.post("/personas/:id/publish", 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"});
}
upsertMarketPersonaStmt.run(
personaId,
user,
persona.name,
persona.pronouns,
persona.appearance,
persona.background,
persona.details,
persona.persona_type
);
const marketPersona = getMarketPersonaByPersonaIdStmt.get(personaId, user);
res.json({persona: marketPersona});
});
app.post("/personas/market/:id/collect", requireLogin, (req, res) => {
const user = req.session.user;
const marketId = Number(req.params.id);
const marketPersona = getMarketPersonaStmt.get(marketId);
if (!marketPersona) {
return res.status(404).json({error: "Market persona not found"});
}
const personaId = insertPersonaStmt.run(
user,
marketPersona.name,
marketPersona.pronouns,
marketPersona.appearance,
marketPersona.background,
marketPersona.details,
marketPersona.persona_type
).lastInsertRowid;
let activePersonaId = null;
let activeUserPersonaId = null;
if (req.body?.equip) {
if (marketPersona.persona_type === "user") {
setActiveUserPersonaStmt.run(user, personaId);
activeUserPersonaId = personaId;
} else {
setActivePersonaStmt.run(user, personaId);
activePersonaId = personaId;
}
}
const persona = getPersonaStmt.get(personaId, user);
res.json({persona, activePersonaId, activeUserPersonaId});
});
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;