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>
|
<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>
|
||||||
|
|||||||
@@ -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();
|
||||||
|
|||||||
@@ -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;
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -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;
|
||||||
|
|||||||
Reference in New Issue
Block a user