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:
@@ -37,6 +37,42 @@
|
||||
</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>
|
||||
</body>
|
||||
</html>
|
||||
|
||||
+62
-2
@@ -3,7 +3,18 @@ const marketUserPersonaList = document.getElementById("marketUserPersonaList");
|
||||
const refreshMarketBtn = document.getElementById("refreshMarket");
|
||||
const marketStatus = document.getElementById("marketStatus");
|
||||
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 pendingMarketPersona = null;
|
||||
|
||||
async function request(url, data, method = "POST") {
|
||||
try {
|
||||
@@ -81,8 +92,8 @@ function renderMarketList(personaItems, listElement, personaType) {
|
||||
|
||||
const getBtn = document.createElement("button");
|
||||
getBtn.type = "button";
|
||||
getBtn.textContent = personaType === "assistant" ? "Get & equip as AI" : "Get & equip as You";
|
||||
getBtn.addEventListener("click", () => collectMarketPersona(persona.id, persona.persona_type));
|
||||
getBtn.textContent = "View details";
|
||||
getBtn.addEventListener("click", () => openMarketPreview(persona));
|
||||
|
||||
actions.appendChild(getBtn);
|
||||
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() {
|
||||
const query = marketSearchInput?.value.trim().toLowerCase() || "";
|
||||
if (!query) return allMarketPersonas;
|
||||
@@ -160,6 +195,31 @@ if (marketSearchInput) {
|
||||
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", () => {
|
||||
loadMarketPersonas();
|
||||
});
|
||||
|
||||
@@ -306,6 +306,14 @@ function renderPersonaList(personaItems, activeId, listElement, personaType) {
|
||||
publishBtn.textContent = isPublished ? 'Update listing' : 'Publish';
|
||||
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');
|
||||
deleteBtn.type = 'button';
|
||||
deleteBtn.textContent = 'Delete';
|
||||
@@ -402,6 +410,12 @@ async function publishPersona(personaId) {
|
||||
await loadPersonas();
|
||||
}
|
||||
|
||||
async function unpublishPersona(personaId) {
|
||||
const res = await post(`/personas/${personaId}/unpublish`, {});
|
||||
if (res.error) return;
|
||||
await loadPersonas();
|
||||
}
|
||||
|
||||
function setLoadingState(loading) {
|
||||
isProcessing = loading;
|
||||
sendBtn.disabled = loading;
|
||||
|
||||
@@ -992,6 +992,16 @@ a.secondary-action {
|
||||
grid-template-columns: 1fr;
|
||||
}
|
||||
|
||||
.chat-header {
|
||||
flex-direction: column;
|
||||
align-items: flex-start;
|
||||
}
|
||||
|
||||
.chat-header-actions {
|
||||
width: 100%;
|
||||
justify-content: flex-start;
|
||||
}
|
||||
|
||||
.chat-sidebar {
|
||||
order: 2;
|
||||
}
|
||||
@@ -1020,6 +1030,35 @@ a.secondary-action {
|
||||
h1 {
|
||||
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) {
|
||||
@@ -1039,6 +1078,15 @@ a.secondary-action {
|
||||
#messages {
|
||||
padding: 14px;
|
||||
}
|
||||
|
||||
.market-preview-actions {
|
||||
flex-direction: column;
|
||||
align-items: stretch;
|
||||
}
|
||||
|
||||
.market-preview-details {
|
||||
grid-template-columns: 1fr;
|
||||
}
|
||||
}
|
||||
|
||||
.consent-container {
|
||||
@@ -1109,6 +1157,44 @@ html, body {
|
||||
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 {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
|
||||
@@ -529,6 +529,12 @@ const listPublishedPersonaIdsStmt = db.prepare(`
|
||||
WHERE creator_username = ?
|
||||
`);
|
||||
|
||||
const deleteMarketPersonaByPersonaIdStmt = db.prepare(`
|
||||
DELETE FROM persona_market
|
||||
WHERE persona_id = ?
|
||||
AND creator_username = ?
|
||||
`);
|
||||
|
||||
const incrementMarketUsageCountStmt = db.prepare(`
|
||||
UPDATE persona_market
|
||||
SET usage_count = usage_count + 1
|
||||
@@ -849,6 +855,7 @@ app.delete("/personas/:id", requireLogin, (req, res) => {
|
||||
if (activePersonas.active_user_persona_id === personaId) {
|
||||
setActiveUserPersonaStmt.run(user, null);
|
||||
}
|
||||
deleteMarketPersonaByPersonaIdStmt.run(personaId, user);
|
||||
res.json({message: "Persona deleted"});
|
||||
});
|
||||
|
||||
@@ -915,6 +922,23 @@ app.post("/personas/:id/publish", requireLogin, (req, res) => {
|
||||
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) => {
|
||||
const user = req.session.user;
|
||||
const marketId = Number(req.params.id);
|
||||
|
||||
Reference in New Issue
Block a user