Add persona unpublish and market preview
This commit is contained in:
@@ -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
@@ -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();
|
||||||
});
|
});
|
||||||
|
|||||||
@@ -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;
|
||||||
|
|||||||
@@ -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;
|
||||||
|
|||||||
@@ -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
|
||||||
@@ -835,6 +841,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"});
|
||||||
});
|
});
|
||||||
|
|
||||||
@@ -901,6 +908,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);
|
||||||
|
|||||||
Reference in New Issue
Block a user