Add persona market publishing and collection
This commit is contained in:
@@ -123,6 +123,27 @@
|
||||
<p id="activeUserPersonaStatus" class="status persona-status">No user persona set.</p>
|
||||
<div id="userPersonaList" class="persona-list"></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>
|
||||
|
||||
@@ -41,6 +41,9 @@ const personaModal = document.getElementById("personaModal");
|
||||
const personaCloseBtn = document.getElementById("personaClose");
|
||||
const personaMenuButton = document.getElementById("personaMenuButton");
|
||||
const personaPopover = document.getElementById("personaPopover");
|
||||
const marketPersonaList = document.getElementById("marketPersonaList");
|
||||
const marketUserPersonaList = document.getElementById("marketUserPersonaList");
|
||||
const refreshMarketBtn = document.getElementById("refreshMarket");
|
||||
|
||||
let isProcessing = false;
|
||||
let chatSessions = [];
|
||||
@@ -51,6 +54,8 @@ let activePersonaId = null;
|
||||
let activeUserPersonaId = null;
|
||||
let editingPersonaId = null;
|
||||
let editingPersonaType = "assistant";
|
||||
let marketPersonas = [];
|
||||
let publishedPersonaIds = new Set();
|
||||
|
||||
const displayModels = async () => {
|
||||
const select = document.getElementById("model");
|
||||
@@ -291,6 +296,12 @@ function renderPersonaList(personaItems, activeId, listElement, personaType) {
|
||||
editBtn.textContent = 'Edit';
|
||||
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');
|
||||
deleteBtn.type = 'button';
|
||||
deleteBtn.textContent = 'Delete';
|
||||
@@ -298,6 +309,7 @@ function renderPersonaList(personaItems, activeId, listElement, personaType) {
|
||||
|
||||
actions.appendChild(equipBtn);
|
||||
actions.appendChild(editBtn);
|
||||
actions.appendChild(publishBtn);
|
||||
actions.appendChild(deleteBtn);
|
||||
|
||||
item.appendChild(title);
|
||||
@@ -315,10 +327,54 @@ async function loadPersonas() {
|
||||
userPersonas = res.userPersonas || [];
|
||||
activePersonaId = res.activePersonaId || null;
|
||||
activeUserPersonaId = res.activeUserPersonaId || null;
|
||||
publishedPersonaIds = new Set(res.publishedPersonaIds || []);
|
||||
renderPersonaList(assistantPersonas, activePersonaId, personaList, "assistant");
|
||||
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) {
|
||||
const endpoint = personaType === "user"
|
||||
? `/personas/${personaId}/equip-user`
|
||||
@@ -379,6 +435,24 @@ async function deletePersona(personaId) {
|
||||
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) {
|
||||
isProcessing = loading;
|
||||
sendBtn.disabled = loading;
|
||||
@@ -494,6 +568,7 @@ async function checkSession() {
|
||||
chatDiv.style.display = "block";
|
||||
await loadChatSessions();
|
||||
await loadPersonas();
|
||||
await loadMarketPersonas();
|
||||
msgInput.focus();
|
||||
return true;
|
||||
}
|
||||
@@ -503,6 +578,7 @@ async function checkSession() {
|
||||
async function loadServerChat() {
|
||||
await loadChatSessions();
|
||||
await loadPersonas();
|
||||
await loadMarketPersonas();
|
||||
}
|
||||
|
||||
async function handleAuth(endpoint, credentials) {
|
||||
@@ -613,6 +689,8 @@ document.getElementById("logout").onclick = async () => {
|
||||
userPersonas = [];
|
||||
activePersonaId = null;
|
||||
activeUserPersonaId = null;
|
||||
marketPersonas = [];
|
||||
publishedPersonaIds = new Set();
|
||||
setAuthMessage("Logged out.", 'success');
|
||||
showAuthScreen('login');
|
||||
loginUsernameInput.value = "";
|
||||
@@ -655,6 +733,8 @@ clearPersonaBtn.addEventListener('click', () => clearPersona("assistant"));
|
||||
|
||||
clearUserPersonaBtn.addEventListener('click', () => clearPersona("user"));
|
||||
|
||||
refreshMarketBtn.addEventListener('click', () => loadMarketPersonas());
|
||||
|
||||
personaForm.addEventListener('submit', (event) => {
|
||||
event.preventDefault();
|
||||
savePersona();
|
||||
|
||||
@@ -842,6 +842,18 @@ button:disabled {
|
||||
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 {
|
||||
max-height: 220px;
|
||||
}
|
||||
|
||||
@@ -214,6 +214,51 @@ db.prepare(`
|
||||
) ON DELETE SET NULL
|
||||
)
|
||||
`).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 getUserStmt = db.prepare("SELECT username, password FROM users WHERE username = ?");
|
||||
@@ -342,6 +387,43 @@ const getActiveUserPersonaStmt = db.prepare(`
|
||||
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(
|
||||
"SELECT COUNT(*) as count FROM chat_messages"
|
||||
).get();
|
||||
@@ -377,6 +459,12 @@ if (!hasActiveUserPersona) {
|
||||
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);
|
||||
app.use(bodyParser.json());
|
||||
app.use(express.static("public"));
|
||||
@@ -536,11 +624,13 @@ app.get("/personas", requireLogin, (req, res) => {
|
||||
const assistantPersonas = listPersonasByTypeStmt.all(user, "assistant");
|
||||
const userPersonas = listPersonasByTypeStmt.all(user, "user");
|
||||
const activePersonas = getActivePersonaIdStmt.get(user) || {};
|
||||
const publishedPersonaIds = listPublishedPersonaIdsStmt.all(user).map(row => row.persona_id);
|
||||
res.json({
|
||||
assistantPersonas,
|
||||
userPersonas,
|
||||
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});
|
||||
});
|
||||
|
||||
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) => {
|
||||
const user = req.session.user;
|
||||
const {message, model, chatId} = req.body;
|
||||
|
||||
Reference in New Issue
Block a user