Fix persona market publishing and add search

This commit is contained in:
2026-01-14 18:08:40 +01:00
parent 8600d7fabe
commit 5943647c57
4 changed files with 146 additions and 11 deletions
+4
View File
@@ -13,6 +13,10 @@
<p class="eyebrow">Community</p> <p class="eyebrow">Community</p>
<h1>Persona Market</h1> <h1>Persona Market</h1>
<p class="subtitle">Browse shared personas and choose who should embody them.</p> <p class="subtitle">Browse shared personas and choose who should embody them.</p>
<div class="market-search">
<label for="marketSearch">Search personas</label>
<input id="marketSearch" type="search" placeholder="Search by name, creator, or details"/>
</div>
</div> </div>
<div class="market-header-actions"> <div class="market-header-actions">
<button id="refreshMarket" class="secondary-action" type="button">Refresh listings</button> <button id="refreshMarket" class="secondary-action" type="button">Refresh listings</button>
+41 -7
View File
@@ -2,6 +2,8 @@ const marketPersonaList = document.getElementById("marketPersonaList");
const marketUserPersonaList = document.getElementById("marketUserPersonaList"); const marketUserPersonaList = document.getElementById("marketUserPersonaList");
const refreshMarketBtn = document.getElementById("refreshMarket"); const refreshMarketBtn = document.getElementById("refreshMarket");
const marketStatus = document.getElementById("marketStatus"); const marketStatus = document.getElementById("marketStatus");
const marketSearchInput = document.getElementById("marketSearch");
let allMarketPersonas = [];
async function request(url, data, method = "POST") { async function request(url, data, method = "POST") {
try { try {
@@ -37,7 +39,9 @@ function setMarketStatus(message, state = "") {
function buildPersonaMeta(persona) { function buildPersonaMeta(persona) {
const pronouns = persona.pronouns ? `Pronouns: ${persona.pronouns}` : "Pronouns: —"; const pronouns = persona.pronouns ? `Pronouns: ${persona.pronouns}` : "Pronouns: —";
return `By ${persona.creator_username} • ${pronouns}`; const usageCount = Number(persona.usage_count || 0);
const usageLabel = `Used ${usageCount} ${usageCount === 1 ? "time" : "times"}`;
return `By ${persona.creator_username} • ${pronouns} • ${usageLabel}`;
} }
function renderMarketList(personaItems, listElement, personaType) { function renderMarketList(personaItems, listElement, personaType) {
@@ -88,6 +92,35 @@ function renderMarketList(personaItems, listElement, personaType) {
}); });
} }
function filterMarketPersonas() {
const query = marketSearchInput?.value.trim().toLowerCase() || "";
if (!query) return allMarketPersonas;
return allMarketPersonas.filter(persona => {
const fields = [
persona.name,
persona.creator_username,
persona.pronouns,
persona.appearance,
persona.background,
persona.details
];
return fields.some(field => field && field.toLowerCase().includes(query));
});
}
function applyMarketFilter() {
const filtered = filterMarketPersonas();
const assistantMarket = filtered.filter(persona => persona.persona_type === "assistant");
const userMarket = filtered.filter(persona => persona.persona_type === "user");
renderMarketList(assistantMarket, marketPersonaList, "assistant");
renderMarketList(userMarket, marketUserPersonaList, "user");
if (marketSearchInput?.value.trim() && !filtered.length) {
setMarketStatus("No personas match your search.", "error");
return;
}
setMarketStatus("Choose an AI persona or a user persona to equip.");
}
async function loadMarketPersonas() { async function loadMarketPersonas() {
setMarketStatus("Loading market personas..."); setMarketStatus("Loading market personas...");
const res = await get("/personas/market"); const res = await get("/personas/market");
@@ -95,12 +128,8 @@ async function loadMarketPersonas() {
setMarketStatus(res.error, "error"); setMarketStatus(res.error, "error");
return; return;
} }
const marketPersonas = res.personas || []; allMarketPersonas = res.personas || [];
const assistantMarket = marketPersonas.filter(persona => persona.persona_type === "assistant"); applyMarketFilter();
const userMarket = marketPersonas.filter(persona => persona.persona_type === "user");
renderMarketList(assistantMarket, marketPersonaList, "assistant");
renderMarketList(userMarket, marketUserPersonaList, "user");
setMarketStatus("Choose an AI persona or a user persona to equip.");
} }
async function collectMarketPersona(marketId, personaType) { async function collectMarketPersona(marketId, personaType) {
@@ -114,6 +143,7 @@ async function collectMarketPersona(marketId, personaType) {
setMarketStatus(res.error, "error"); setMarketStatus(res.error, "error");
return; return;
} }
await loadMarketPersonas();
setMarketStatus( setMarketStatus(
personaType === "assistant" personaType === "assistant"
? "AI persona equipped. Head back to chat to use it." ? "AI persona equipped. Head back to chat to use it."
@@ -126,6 +156,10 @@ if (refreshMarketBtn) {
refreshMarketBtn.addEventListener("click", () => loadMarketPersonas()); refreshMarketBtn.addEventListener("click", () => loadMarketPersonas());
} }
if (marketSearchInput) {
marketSearchInput.addEventListener("input", () => applyMarketFilter());
}
window.addEventListener("load", () => { window.addEventListener("load", () => {
loadMarketPersonas(); loadMarketPersonas();
}); });
+27
View File
@@ -928,6 +928,33 @@ a.secondary-action {
margin: 4px 0; margin: 4px 0;
} }
.market-search {
display: grid;
gap: 6px;
margin-top: 12px;
max-width: 320px;
}
.market-search label {
font-size: 0.78rem;
color: var(--text-muted);
}
.market-search input {
padding: 10px 12px;
border-radius: 10px;
border: 1px solid rgba(255, 255, 255, 0.1);
background: rgba(10, 10, 25, 0.8);
color: var(--text-primary);
font-size: 0.9rem;
}
.market-search input:focus {
outline: none;
border-color: var(--accent);
box-shadow: 0 0 0 2px rgba(112, 86, 255, 0.25);
}
.market-header-actions { .market-header-actions {
display: flex; display: flex;
gap: 10px; gap: 10px;
+74 -4
View File
@@ -203,6 +203,10 @@ db.prepare(`
TEXT TEXT
DEFAULT DEFAULT
'assistant', 'assistant',
source_market_id
INTEGER,
source_creator_username
TEXT,
created_at created_at
DATETIME DATETIME
DEFAULT DEFAULT
@@ -272,6 +276,10 @@ db.prepare(`
TEXT TEXT
DEFAULT DEFAULT
'assistant', 'assistant',
usage_count
INTEGER
DEFAULT
0,
created_at created_at
DATETIME DATETIME
DEFAULT DEFAULT
@@ -362,6 +370,8 @@ const listPersonasByTypeStmt = db.prepare(`
background, background,
details, details,
persona_type, persona_type,
source_market_id,
source_creator_username,
created_at, created_at,
updated_at updated_at
FROM personas FROM personas
@@ -377,10 +387,36 @@ const getPersonaStmt = db.prepare(`
AND username = ? AND username = ?
`); `);
const getPersonaForPublishStmt = db.prepare(`
SELECT id,
name,
pronouns,
appearance,
background,
details,
persona_type,
source_market_id,
source_creator_username
FROM personas
WHERE id = ?
AND username = ?
`);
const insertPersonaStmt = db.prepare( const insertPersonaStmt = db.prepare(
"INSERT INTO personas (username, name, pronouns, appearance, background, details, persona_type) VALUES (?, ?, ?, ?, ?, ?, ?)" "INSERT INTO personas (username, name, pronouns, appearance, background, details, persona_type) VALUES (?, ?, ?, ?, ?, ?, ?)"
); );
const insertMarketPersonaStmt = db.prepare(
"INSERT INTO personas (username, name, pronouns, appearance, background, details, persona_type, source_market_id, source_creator_username) VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?)"
);
const getPersonaBySourceMarketStmt = db.prepare(`
SELECT id
FROM personas
WHERE username = ?
AND source_market_id = ?
`);
const updatePersonaStmt = db.prepare(` const updatePersonaStmt = db.prepare(`
UPDATE personas UPDATE personas
SET name = ?, SET name = ?,
@@ -437,6 +473,7 @@ const listMarketPersonasStmt = db.prepare(`
background, background,
details, details,
persona_type, persona_type,
usage_count,
created_at, created_at,
updated_at updated_at
FROM persona_market FROM persona_market
@@ -452,7 +489,8 @@ const getMarketPersonaStmt = db.prepare(`
appearance, appearance,
background, background,
details, details,
persona_type persona_type,
usage_count
FROM persona_market FROM persona_market
WHERE id = ? WHERE id = ?
`); `);
@@ -491,6 +529,12 @@ const listPublishedPersonaIdsStmt = db.prepare(`
WHERE creator_username = ? WHERE creator_username = ?
`); `);
const incrementMarketUsageCountStmt = db.prepare(`
UPDATE persona_market
SET usage_count = usage_count + 1
WHERE id = ?
`);
const migrationNeeded = db.prepare( const migrationNeeded = db.prepare(
"SELECT COUNT(*) as count FROM chat_messages" "SELECT COUNT(*) as count FROM chat_messages"
).get(); ).get();
@@ -519,6 +563,14 @@ const hasPersonaType = personaColumns.some(column => column.name === "persona_ty
if (!hasPersonaType) { if (!hasPersonaType) {
db.prepare("ALTER TABLE personas ADD COLUMN persona_type TEXT DEFAULT 'assistant'").run(); db.prepare("ALTER TABLE personas ADD COLUMN persona_type TEXT DEFAULT 'assistant'").run();
} }
const hasSourceMarketId = personaColumns.some(column => column.name === "source_market_id");
if (!hasSourceMarketId) {
db.prepare("ALTER TABLE personas ADD COLUMN source_market_id INTEGER").run();
}
const hasSourceCreatorUsername = personaColumns.some(column => column.name === "source_creator_username");
if (!hasSourceCreatorUsername) {
db.prepare("ALTER TABLE personas ADD COLUMN source_creator_username TEXT").run();
}
const userSettingsColumns = db.prepare("PRAGMA table_info(user_settings)").all(); const userSettingsColumns = db.prepare("PRAGMA table_info(user_settings)").all();
const hasActiveUserPersona = userSettingsColumns.some(column => column.name === "active_user_persona_id"); const hasActiveUserPersona = userSettingsColumns.some(column => column.name === "active_user_persona_id");
@@ -531,6 +583,11 @@ const hasPersonaIdColumn = marketColumns.some(column => column.name === "persona
if (!hasPersonaIdColumn) { if (!hasPersonaIdColumn) {
db.prepare("ALTER TABLE persona_market ADD COLUMN persona_id INTEGER").run(); db.prepare("ALTER TABLE persona_market ADD COLUMN persona_id INTEGER").run();
} }
const hasUsageCount = marketColumns.some(column => column.name === "usage_count");
if (!hasUsageCount) {
db.prepare("ALTER TABLE persona_market ADD COLUMN usage_count INTEGER DEFAULT 0").run();
}
db.prepare("UPDATE persona_market SET usage_count = 0 WHERE usage_count IS NULL").run();
const FileStore = FileStoreFactory(session); const FileStore = FileStoreFactory(session);
app.use(bodyParser.json()); app.use(bodyParser.json());
@@ -823,10 +880,13 @@ app.get("/personas/market", requireLogin, (req, res) => {
app.post("/personas/:id/publish", requireLogin, (req, res) => { app.post("/personas/:id/publish", requireLogin, (req, res) => {
const user = req.session.user; const user = req.session.user;
const personaId = Number(req.params.id); const personaId = Number(req.params.id);
const persona = getPersonaStmt.get(personaId, user); const persona = getPersonaForPublishStmt.get(personaId, user);
if (!persona) { if (!persona) {
return res.status(404).json({error: "Persona not found"}); return res.status(404).json({error: "Persona not found"});
} }
if (persona.source_market_id) {
return res.status(403).json({error: "Collected personas cannot be published."});
}
upsertMarketPersonaStmt.run( upsertMarketPersonaStmt.run(
personaId, personaId,
user, user,
@@ -848,15 +908,25 @@ app.post("/personas/market/:id/collect", requireLogin, (req, res) => {
if (!marketPersona) { if (!marketPersona) {
return res.status(404).json({error: "Market persona not found"}); return res.status(404).json({error: "Market persona not found"});
} }
const personaId = insertPersonaStmt.run( if (marketPersona.creator_username === user) {
return res.status(400).json({error: "You already own this persona."});
}
const existingPersona = getPersonaBySourceMarketStmt.get(user, marketId);
if (existingPersona) {
return res.status(400).json({error: "You already collected this persona."});
}
const personaId = insertMarketPersonaStmt.run(
user, user,
marketPersona.name, marketPersona.name,
marketPersona.pronouns, marketPersona.pronouns,
marketPersona.appearance, marketPersona.appearance,
marketPersona.background, marketPersona.background,
marketPersona.details, marketPersona.details,
marketPersona.persona_type marketPersona.persona_type,
marketId,
marketPersona.creator_username
).lastInsertRowid; ).lastInsertRowid;
incrementMarketUsageCountStmt.run(marketId);
let activePersonaId = null; let activePersonaId = null;
let activeUserPersonaId = null; let activeUserPersonaId = null;
if (req.body?.equip) { if (req.body?.equip) {