Merge pull request #11 from Fretuks/implement-persona-management-features

Add persona unpublish, remove market listing on persona delete, and market preview modal; improve responsiveness
This commit is contained in:
2026-01-14 19:42:36 +01:00
committed by GitHub
5 changed files with 222 additions and 2 deletions
+36
View File
@@ -37,6 +37,42 @@
</div> </div>
</div> </div>
</div> </div>
<div id="marketPreviewModal" class="modal-overlay hidden">
<div class="modal-card market-preview-card">
<div class="modal-header">
<div>
<p class="eyebrow">Persona preview</p>
<h3 id="marketPreviewName">Persona details</h3>
</div>
<button id="marketPreviewClose" type="button" class="icon-button">Close</button>
</div>
<div class="market-preview-body">
<p id="marketPreviewMeta" class="persona-guidance"></p>
<div class="market-preview-details">
<div>
<h4>Pronouns</h4>
<p id="marketPreviewPronouns"></p>
</div>
<div>
<h4>Appearance</h4>
<p id="marketPreviewAppearance"></p>
</div>
<div>
<h4>Background</h4>
<p id="marketPreviewBackground"></p>
</div>
<div>
<h4>Details</h4>
<p id="marketPreviewDetails"></p>
</div>
</div>
</div>
<div class="market-preview-actions">
<button id="marketPreviewConfirm" type="button" class="primary-action">Take persona</button>
<button id="marketPreviewCancel" type="button" class="secondary-action">Cancel</button>
</div>
</div>
</div>
<script src="market.js" defer></script> <script src="market.js" defer></script>
</body> </body>
</html> </html>
+62 -2
View File
@@ -3,7 +3,18 @@ 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"); const marketSearchInput = document.getElementById("marketSearch");
const marketPreviewModal = document.getElementById("marketPreviewModal");
const marketPreviewName = document.getElementById("marketPreviewName");
const marketPreviewMeta = document.getElementById("marketPreviewMeta");
const marketPreviewPronouns = document.getElementById("marketPreviewPronouns");
const marketPreviewAppearance = document.getElementById("marketPreviewAppearance");
const marketPreviewBackground = document.getElementById("marketPreviewBackground");
const marketPreviewDetails = document.getElementById("marketPreviewDetails");
const marketPreviewConfirm = document.getElementById("marketPreviewConfirm");
const marketPreviewCancel = document.getElementById("marketPreviewCancel");
const marketPreviewClose = document.getElementById("marketPreviewClose");
let allMarketPersonas = []; let allMarketPersonas = [];
let pendingMarketPersona = null;
async function request(url, data, method = "POST") { async function request(url, data, method = "POST") {
try { try {
@@ -81,8 +92,8 @@ function renderMarketList(personaItems, listElement, personaType) {
const getBtn = document.createElement("button"); const getBtn = document.createElement("button");
getBtn.type = "button"; getBtn.type = "button";
getBtn.textContent = personaType === "assistant" ? "Get & equip as AI" : "Get & equip as You"; getBtn.textContent = "View details";
getBtn.addEventListener("click", () => collectMarketPersona(persona.id, persona.persona_type)); getBtn.addEventListener("click", () => openMarketPreview(persona));
actions.appendChild(getBtn); actions.appendChild(getBtn);
item.appendChild(titleRow); item.appendChild(titleRow);
@@ -92,6 +103,30 @@ function renderMarketList(personaItems, listElement, personaType) {
}); });
} }
function formatPersonaField(value) {
return value && value.trim() ? value.trim() : "Not provided.";
}
function openMarketPreview(persona) {
if (!marketPreviewModal) return;
pendingMarketPersona = persona;
marketPreviewName.textContent = persona.name;
marketPreviewMeta.textContent = buildPersonaMeta(persona);
marketPreviewPronouns.textContent = formatPersonaField(persona.pronouns);
marketPreviewAppearance.textContent = formatPersonaField(persona.appearance);
marketPreviewBackground.textContent = formatPersonaField(persona.background);
marketPreviewDetails.textContent = formatPersonaField(persona.details);
marketPreviewConfirm.textContent =
persona.persona_type === "assistant" ? "Get & equip as AI" : "Get & equip as You";
marketPreviewModal.classList.remove("hidden");
}
function closeMarketPreview() {
if (!marketPreviewModal) return;
marketPreviewModal.classList.add("hidden");
pendingMarketPersona = null;
}
function filterMarketPersonas() { function filterMarketPersonas() {
const query = marketSearchInput?.value.trim().toLowerCase() || ""; const query = marketSearchInput?.value.trim().toLowerCase() || "";
if (!query) return allMarketPersonas; if (!query) return allMarketPersonas;
@@ -160,6 +195,31 @@ if (marketSearchInput) {
marketSearchInput.addEventListener("input", () => applyMarketFilter()); marketSearchInput.addEventListener("input", () => applyMarketFilter());
} }
if (marketPreviewCancel) {
marketPreviewCancel.addEventListener("click", closeMarketPreview);
}
if (marketPreviewClose) {
marketPreviewClose.addEventListener("click", closeMarketPreview);
}
if (marketPreviewModal) {
marketPreviewModal.addEventListener("click", (event) => {
if (event.target === marketPreviewModal) {
closeMarketPreview();
}
});
}
if (marketPreviewConfirm) {
marketPreviewConfirm.addEventListener("click", async () => {
if (!pendingMarketPersona) return;
const {id, persona_type} = pendingMarketPersona;
closeMarketPreview();
await collectMarketPersona(id, persona_type);
});
}
window.addEventListener("load", () => { window.addEventListener("load", () => {
loadMarketPersonas(); loadMarketPersonas();
}); });
+14
View File
@@ -306,6 +306,14 @@ function renderPersonaList(personaItems, activeId, listElement, personaType) {
publishBtn.textContent = isPublished ? 'Update listing' : 'Publish'; publishBtn.textContent = isPublished ? 'Update listing' : 'Publish';
publishBtn.addEventListener('click', () => publishPersona(persona.id)); publishBtn.addEventListener('click', () => publishPersona(persona.id));
if (isPublished) {
const unpublishBtn = document.createElement('button');
unpublishBtn.type = 'button';
unpublishBtn.textContent = 'Unpublish';
unpublishBtn.addEventListener('click', () => unpublishPersona(persona.id));
actions.appendChild(unpublishBtn);
}
const deleteBtn = document.createElement('button'); const deleteBtn = document.createElement('button');
deleteBtn.type = 'button'; deleteBtn.type = 'button';
deleteBtn.textContent = 'Delete'; deleteBtn.textContent = 'Delete';
@@ -402,6 +410,12 @@ async function publishPersona(personaId) {
await loadPersonas(); await loadPersonas();
} }
async function unpublishPersona(personaId) {
const res = await post(`/personas/${personaId}/unpublish`, {});
if (res.error) return;
await loadPersonas();
}
function setLoadingState(loading) { function setLoadingState(loading) {
isProcessing = loading; isProcessing = loading;
sendBtn.disabled = loading; sendBtn.disabled = loading;
+86
View File
@@ -992,6 +992,16 @@ a.secondary-action {
grid-template-columns: 1fr; grid-template-columns: 1fr;
} }
.chat-header {
flex-direction: column;
align-items: flex-start;
}
.chat-header-actions {
width: 100%;
justify-content: flex-start;
}
.chat-sidebar { .chat-sidebar {
order: 2; order: 2;
} }
@@ -1020,6 +1030,35 @@ a.secondary-action {
h1 { h1 {
font-size: 1.6rem; font-size: 1.6rem;
} }
.persona-popover {
width: 95vw;
right: auto;
left: 0;
}
.persona-item-actions {
flex-direction: column;
align-items: stretch;
}
.market-shell {
padding: 20px;
}
.market-header {
flex-direction: column;
align-items: stretch;
}
.market-header-actions {
width: 100%;
justify-content: flex-start;
}
.market-search {
max-width: none;
}
} }
@media (max-width: 480px) { @media (max-width: 480px) {
@@ -1039,6 +1078,15 @@ a.secondary-action {
#messages { #messages {
padding: 14px; padding: 14px;
} }
.market-preview-actions {
flex-direction: column;
align-items: stretch;
}
.market-preview-details {
grid-template-columns: 1fr;
}
} }
.consent-container { .consent-container {
@@ -1109,6 +1157,44 @@ html, body {
padding: 16px; padding: 16px;
} }
.market-preview-card {
width: min(640px, 95vw);
display: grid;
gap: 12px;
}
.market-preview-body {
display: grid;
gap: 12px;
}
.market-preview-details {
display: grid;
grid-template-columns: repeat(auto-fit, minmax(220px, 1fr));
gap: 12px;
}
.market-preview-details h4 {
margin: 0 0 4px;
font-size: 0.85rem;
color: var(--text-secondary);
text-transform: uppercase;
letter-spacing: 0.08em;
}
.market-preview-details p {
margin: 0;
font-size: 0.9rem;
color: var(--text-primary);
}
.market-preview-actions {
display: flex;
gap: 12px;
flex-wrap: wrap;
justify-content: flex-end;
}
.modal-header { .modal-header {
display: flex; display: flex;
align-items: center; align-items: center;
+24
View File
@@ -529,6 +529,12 @@ const listPublishedPersonaIdsStmt = db.prepare(`
WHERE creator_username = ? WHERE creator_username = ?
`); `);
const deleteMarketPersonaByPersonaIdStmt = db.prepare(`
DELETE FROM persona_market
WHERE persona_id = ?
AND creator_username = ?
`);
const incrementMarketUsageCountStmt = db.prepare(` const incrementMarketUsageCountStmt = db.prepare(`
UPDATE persona_market UPDATE persona_market
SET usage_count = usage_count + 1 SET usage_count = usage_count + 1
@@ -849,6 +855,7 @@ app.delete("/personas/:id", requireLogin, (req, res) => {
if (activePersonas.active_user_persona_id === personaId) { if (activePersonas.active_user_persona_id === personaId) {
setActiveUserPersonaStmt.run(user, null); setActiveUserPersonaStmt.run(user, null);
} }
deleteMarketPersonaByPersonaIdStmt.run(personaId, user);
res.json({message: "Persona deleted"}); res.json({message: "Persona deleted"});
}); });
@@ -915,6 +922,23 @@ app.post("/personas/:id/publish", requireLogin, (req, res) => {
res.json({persona: marketPersona}); res.json({persona: marketPersona});
}); });
app.post("/personas/:id/unpublish", requireLogin, (req, res) => {
const user = req.session.user;
const personaId = Number(req.params.id);
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 unpublished."});
}
const result = deleteMarketPersonaByPersonaIdStmt.run(personaId, user);
if (result.changes === 0) {
return res.status(404).json({error: "Persona is not published."});
}
res.json({message: "Persona unpublished"});
});
app.post("/personas/market/:id/collect", requireLogin, (req, res) => { app.post("/personas/market/:id/collect", requireLogin, (req, res) => {
const user = req.session.user; const user = req.session.user;
const marketId = Number(req.params.id); const marketId = Number(req.params.id);