Fix persona market publishing and add search
This commit is contained in:
@@ -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
@@ -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();
|
||||||
});
|
});
|
||||||
|
|||||||
@@ -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;
|
||||||
|
|||||||
@@ -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) {
|
||||||
|
|||||||
Reference in New Issue
Block a user