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>
<h1>Persona Market</h1>
<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 class="market-header-actions">
<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 refreshMarketBtn = document.getElementById("refreshMarket");
const marketStatus = document.getElementById("marketStatus");
const marketSearchInput = document.getElementById("marketSearch");
let allMarketPersonas = [];
async function request(url, data, method = "POST") {
try {
@@ -37,7 +39,9 @@ function setMarketStatus(message, state = "") {
function buildPersonaMeta(persona) {
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) {
@@ -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() {
setMarketStatus("Loading market personas...");
const res = await get("/personas/market");
@@ -95,12 +128,8 @@ async function loadMarketPersonas() {
setMarketStatus(res.error, "error");
return;
}
const marketPersonas = res.personas || [];
const assistantMarket = marketPersonas.filter(persona => persona.persona_type === "assistant");
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.");
allMarketPersonas = res.personas || [];
applyMarketFilter();
}
async function collectMarketPersona(marketId, personaType) {
@@ -114,6 +143,7 @@ async function collectMarketPersona(marketId, personaType) {
setMarketStatus(res.error, "error");
return;
}
await loadMarketPersonas();
setMarketStatus(
personaType === "assistant"
? "AI persona equipped. Head back to chat to use it."
@@ -126,6 +156,10 @@ if (refreshMarketBtn) {
refreshMarketBtn.addEventListener("click", () => loadMarketPersonas());
}
if (marketSearchInput) {
marketSearchInput.addEventListener("input", () => applyMarketFilter());
}
window.addEventListener("load", () => {
loadMarketPersonas();
});
+27
View File
@@ -928,6 +928,33 @@ a.secondary-action {
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 {
display: flex;
gap: 10px;
+74 -4
View File
@@ -203,6 +203,10 @@ db.prepare(`
TEXT
DEFAULT
'assistant',
source_market_id
INTEGER,
source_creator_username
TEXT,
created_at
DATETIME
DEFAULT
@@ -272,6 +276,10 @@ db.prepare(`
TEXT
DEFAULT
'assistant',
usage_count
INTEGER
DEFAULT
0,
created_at
DATETIME
DEFAULT
@@ -362,6 +370,8 @@ const listPersonasByTypeStmt = db.prepare(`
background,
details,
persona_type,
source_market_id,
source_creator_username,
created_at,
updated_at
FROM personas
@@ -377,10 +387,36 @@ const getPersonaStmt = db.prepare(`
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(
"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(`
UPDATE personas
SET name = ?,
@@ -437,6 +473,7 @@ const listMarketPersonasStmt = db.prepare(`
background,
details,
persona_type,
usage_count,
created_at,
updated_at
FROM persona_market
@@ -452,7 +489,8 @@ const getMarketPersonaStmt = db.prepare(`
appearance,
background,
details,
persona_type
persona_type,
usage_count
FROM persona_market
WHERE id = ?
`);
@@ -491,6 +529,12 @@ const listPublishedPersonaIdsStmt = db.prepare(`
WHERE creator_username = ?
`);
const incrementMarketUsageCountStmt = db.prepare(`
UPDATE persona_market
SET usage_count = usage_count + 1
WHERE id = ?
`);
const migrationNeeded = db.prepare(
"SELECT COUNT(*) as count FROM chat_messages"
).get();
@@ -519,6 +563,14 @@ const hasPersonaType = personaColumns.some(column => column.name === "persona_ty
if (!hasPersonaType) {
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 hasActiveUserPersona = userSettingsColumns.some(column => column.name === "active_user_persona_id");
@@ -531,6 +583,11 @@ const hasPersonaIdColumn = marketColumns.some(column => column.name === "persona
if (!hasPersonaIdColumn) {
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);
app.use(bodyParser.json());
@@ -823,10 +880,13 @@ app.get("/personas/market", requireLogin, (req, res) => {
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);
const persona = getPersonaForPublishStmt.get(personaId, user);
if (!persona) {
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(
personaId,
user,
@@ -848,15 +908,25 @@ app.post("/personas/market/:id/collect", requireLogin, (req, res) => {
if (!marketPersona) {
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,
marketPersona.name,
marketPersona.pronouns,
marketPersona.appearance,
marketPersona.background,
marketPersona.details,
marketPersona.persona_type
marketPersona.persona_type,
marketId,
marketPersona.creator_username
).lastInsertRowid;
incrementMarketUsageCountStmt.run(marketId);
let activePersonaId = null;
let activeUserPersonaId = null;
if (req.body?.equip) {