const $ = (id) => document.getElementById(id); const marketPersonaList = $("marketPersonaList"); const marketUserPersonaList = $("marketUserPersonaList"); const refreshMarketBtn = $("refreshMarket"); const marketStatus = $("marketStatus"); const marketSearchInput = $("marketSearch"); const marketViewButtons = document.querySelectorAll("[data-market-view]"); const marketPanels = document.querySelectorAll("[data-market-panel]"); const marketCreateAiCharacterBtn = $("marketCreateAiCharacter"); const marketCreateUserPersonaBtn = $("marketCreateUserPersona"); const marketPreviewModal = $("marketPreviewModal"); const marketPreviewName = $("marketPreviewName"); const marketPreviewMeta = $("marketPreviewMeta"); const marketPreviewPronouns = $("marketPreviewPronouns"); const marketPreviewAppearance = $("marketPreviewAppearance"); const marketPreviewBackground = $("marketPreviewBackground"); const marketPreviewDetails = $("marketPreviewDetails"); const marketPreviewExampleDialogues = $("marketPreviewExampleDialogues"); const marketPreviewUserPersonaField = $("marketPreviewUserPersonaField"); const marketPreviewUserPersonaSelect = $("marketPreviewUserPersonaSelect"); const marketPreviewScenarioField = $("marketPreviewScenarioField"); const marketPreviewScenario = $("marketPreviewScenario"); const marketPreviewConfirm = $("marketPreviewConfirm"); const marketPreviewClose = $("marketPreviewClose"); const marketPersonaModal = $("marketPersonaModal"); const marketPersonaClose = $("marketPersonaClose"); const marketPersonaForm = $("marketPersonaForm"); const marketPersonaFormTitle = $("marketPersonaFormTitle"); const marketPersonaFormNotice = $("marketPersonaFormNotice"); const marketPersonaName = $("marketPersonaName"); const marketPersonaPronouns = $("marketPersonaPronouns"); const marketPersonaAppearance = $("marketPersonaAppearance"); const marketPersonaBackground = $("marketPersonaBackground"); const marketPersonaDetails = $("marketPersonaDetails"); const marketPersonaExampleDialogues = $("marketPersonaExampleDialogues"); const marketActivityOverlay = $("marketActivityOverlay"); const marketActivityEyebrow = $("marketActivityEyebrow"); const marketActivityTitle = $("marketActivityTitle"); const marketActivityDetail = $("marketActivityDetail"); const themeNameTargets = document.querySelectorAll("[data-theme-name]"); const themes = { "fakegpt": {name: "FakeGPT"}, "fraud": {name: "Fraud"}, "germini": {name: "Germini"}, "slopilot": {name: "Slopilot"}, "beta-ai": {name: "Beta AI"}, "confusity": {name: "Confusity"} }; let allMarketPersonas = []; let assistantPersonas = []; let userPersonas = []; let pendingMarketPersona = null; let editingPersonaId = null; let activeUserPersonaId = null; let activeMarketView = "ai-characters"; let marketActivityDepth = 0; async function request(url, data, method = "POST") { try { const res = await fetch(url, { method, headers: {"Content-Type": "application/json"}, body: data ? JSON.stringify(data) : undefined }); return await res.json(); } catch { return {error: "Network error - please try again."}; } } const get = (url) => request(url, null, "GET"); const post = (url, data) => request(url, data, "POST"); const put = (url, data) => request(url, data, "PUT"); function applyTheme(themeKey) { const nextTheme = themes[themeKey] ? themeKey : "fakegpt"; document.body.dataset.theme = nextTheme; document.title = `${themes[nextTheme].name} Market`; themeNameTargets.forEach((target) => { target.textContent = themes[nextTheme].name; }); } function setMarketStatus(message, state = "") { marketStatus.textContent = message; marketStatus.className = state ? `status persona-status ${state}` : "status persona-status"; } function setPersonaFormNotice(message = "", state = "") { if (!message) { marketPersonaFormNotice.textContent = ""; marketPersonaFormNotice.className = "status hidden"; return; } marketPersonaFormNotice.textContent = message; marketPersonaFormNotice.className = state ? `status ${state}` : "status"; } function setMarketActivity(active, { eyebrow = "Please wait", title = "Opening roleplay", detail = "The AI character is preparing the first message." } = {}) { if (!marketActivityOverlay || !marketActivityEyebrow || !marketActivityTitle || !marketActivityDetail) return; if (active) { marketActivityDepth += 1; marketActivityEyebrow.textContent = eyebrow; marketActivityTitle.textContent = title; marketActivityDetail.textContent = detail; marketActivityOverlay.classList.remove("hidden"); return; } marketActivityDepth = Math.max(0, marketActivityDepth - 1); if (marketActivityDepth === 0) { marketActivityOverlay.classList.add("hidden"); marketActivityEyebrow.textContent = "Please wait"; marketActivityTitle.textContent = "Opening roleplay"; marketActivityDetail.textContent = "The AI character is preparing the first message."; } } function setMarketView(view) { const normalizedView = view === "user-personas" ? "user-personas" : "ai-characters"; activeMarketView = normalizedView; marketViewButtons.forEach((item) => item.classList.toggle("active", item.dataset.marketView === normalizedView)); marketPanels.forEach((panel) => panel.classList.toggle("active", panel.dataset.marketPanel === normalizedView)); const params = new URLSearchParams(window.location.search); params.set("view", normalizedView); history.replaceState(null, "", `${window.location.pathname}?${params.toString()}`); applyMarketFilter(); } function buildPersonaMeta(persona) { const creator = persona.creator_username || "You"; const pronouns = persona.pronouns ? `Pronouns: ${persona.pronouns}` : "Pronouns: n/a"; return `${creator} | ${pronouns}`; } function formatPersonaField(value) { return value && value.trim() ? value.trim() : "Not provided."; } function setCollapsiblePreviewText(element, value, maxLength = 220) { const text = formatPersonaField(value); element.dataset.fullText = text; let toggle = element.nextElementSibling; if (!toggle || !toggle.classList.contains("market-preview-toggle")) { toggle = document.createElement("button"); toggle.type = "button"; toggle.className = "market-preview-toggle hidden"; element.insertAdjacentElement("afterend", toggle); toggle.addEventListener("click", () => { const isExpanded = element.dataset.expanded === "true"; element.dataset.expanded = isExpanded ? "false" : "true"; if (element.dataset.expanded === "true") { element.textContent = element.dataset.fullText; toggle.textContent = "Show less"; } else { element.textContent = `${element.dataset.fullText.slice(0, maxLength).trimEnd()}...`; toggle.textContent = "Show more"; } }); } const shouldCollapse = text !== "Not provided." && text.length > maxLength; element.dataset.expanded = "false"; if (shouldCollapse) { element.textContent = `${text.slice(0, maxLength).trimEnd()}...`; toggle.textContent = "Show more"; toggle.classList.remove("hidden"); } else { element.textContent = text; toggle.classList.add("hidden"); } } function closeMarketPreview() { marketPreviewModal.classList.add("hidden"); pendingMarketPersona = null; marketPreviewUserPersonaSelect.innerHTML = ""; marketPreviewScenario.value = ""; } function populateUserPersonaChoices() { marketPreviewUserPersonaSelect.innerHTML = ""; const promptOption = document.createElement("option"); promptOption.value = ""; promptOption.textContent = "Choose who you are in this roleplay"; promptOption.disabled = true; promptOption.selected = true; marketPreviewUserPersonaSelect.appendChild(promptOption); const selfOption = document.createElement("option"); selfOption.value = "self"; selfOption.textContent = "Yourself (no persona)"; marketPreviewUserPersonaSelect.appendChild(selfOption); userPersonas.forEach((persona) => { const option = document.createElement("option"); option.value = String(persona.id); option.textContent = persona.name; option.selected = persona.id === activeUserPersonaId; marketPreviewUserPersonaSelect.appendChild(option); }); const createOption = document.createElement("option"); createOption.value = "create_new"; createOption.textContent = "Create new persona"; marketPreviewUserPersonaSelect.appendChild(createOption); } function openMarketPreview(persona) { pendingMarketPersona = persona; marketPreviewName.textContent = persona.name; marketPreviewMeta.textContent = buildPersonaMeta(persona); setCollapsiblePreviewText(marketPreviewPronouns, persona.pronouns, 80); setCollapsiblePreviewText(marketPreviewAppearance, persona.appearance); setCollapsiblePreviewText(marketPreviewBackground, persona.background); setCollapsiblePreviewText(marketPreviewDetails, persona.details); setCollapsiblePreviewText(marketPreviewExampleDialogues, persona.example_dialogues); const isAssistant = persona.persona_type === "assistant"; marketPreviewUserPersonaField.classList.toggle("hidden", !isAssistant); marketPreviewScenarioField.classList.toggle("hidden", !isAssistant); marketPreviewConfirm.textContent = isAssistant ? "Start roleplay" : "Collect persona"; if (isAssistant) { populateUserPersonaChoices(); marketPreviewConfirm.classList.add("hidden"); } else { marketPreviewConfirm.classList.remove("hidden"); } marketPreviewModal.classList.remove("hidden"); } function openPersonaForm(persona = null) { editingPersonaId = persona ? persona.id : null; marketPersonaFormTitle.textContent = persona ? "Edit AI Character" : "Create AI Character"; marketPersonaName.value = persona?.name || ""; marketPersonaPronouns.value = persona?.pronouns || ""; marketPersonaAppearance.value = persona?.appearance || ""; marketPersonaBackground.value = persona?.background || ""; marketPersonaDetails.value = persona?.details || ""; marketPersonaExampleDialogues.value = persona?.example_dialogues || ""; setPersonaFormNotice(""); marketPersonaModal.classList.remove("hidden"); marketPersonaName.focus(); } function closePersonaForm() { marketPersonaModal.classList.add("hidden"); editingPersonaId = null; setPersonaFormNotice(""); } function renderMarketList(items, listElement, type) { listElement.innerHTML = ""; if (!items.length) { const empty = document.createElement("p"); empty.className = "status persona-status"; empty.textContent = type === "assistant" ? "No AI Characters found." : "No User Personas found."; listElement.appendChild(empty); return; } items.forEach((persona) => { const item = document.createElement("div"); const titleRow = document.createElement("div"); const title = document.createElement("h4"); const tag = document.createElement("span"); const meta = document.createElement("p"); const actions = document.createElement("div"); const detailsBtn = document.createElement("button"); item.className = "persona-item"; titleRow.className = "persona-title-row"; actions.className = "persona-item-actions"; tag.className = `persona-role-tag ${type === "assistant" ? "ai" : "user"}`; tag.textContent = type === "assistant" ? "AI Character" : "User"; title.textContent = persona.name; meta.textContent = buildPersonaMeta(persona); detailsBtn.type = "button"; detailsBtn.textContent = "Preview"; detailsBtn.addEventListener("click", () => openMarketPreview(persona)); if (type !== "assistant") { const quickActionBtn = document.createElement("button"); quickActionBtn.type = "button"; quickActionBtn.textContent = "Collect"; quickActionBtn.addEventListener("click", () => { void collectMarketPersona(persona.id); }); actions.appendChild(quickActionBtn); } titleRow.append(title, tag); actions.appendChild(detailsBtn); item.append(titleRow, meta, actions); listElement.appendChild(item); }); } function filterMarketPersonas() { const query = marketSearchInput.value.trim().toLowerCase(); if (!query) return allMarketPersonas; return allMarketPersonas.filter((persona) => [persona.name, persona.creator_username, persona.pronouns, persona.appearance, persona.background, persona.details, persona.example_dialogues] .some((field) => field && field.toLowerCase().includes(query)) ); } function applyMarketFilter() { const filtered = filterMarketPersonas(); const assistantItems = filtered.filter((persona) => persona.persona_type === "assistant"); const userItems = filtered.filter((persona) => persona.persona_type === "user"); renderMarketList(assistantItems, marketPersonaList, "assistant"); renderMarketList(userItems, marketUserPersonaList, "user"); if (marketSearchInput.value.trim() && !filtered.length) { setMarketStatus("No personas match your search.", "error"); return; } if (activeMarketView === "ai-characters") { setMarketStatus(`${assistantItems.length} AI Character${assistantItems.length === 1 ? "" : "s"} ready for roleplay.`); return; } if (activeMarketView === "user-personas") { setMarketStatus(`${userItems.length} User Persona${userItems.length === 1 ? "" : "s"} available to collect.`); return; } setMarketStatus(`${assistantItems.length + userItems.length} persona${assistantItems.length + userItems.length === 1 ? "" : "s"} shown.`); } async function loadOwnedPersonas() { const res = await get("/personas"); if (res.error) return false; assistantPersonas = res.assistantPersonas || []; userPersonas = res.userPersonas || []; activeUserPersonaId = res.activeUserPersonaId || null; return true; } async function loadMarketPersonas() { setMarketStatus("Loading personas..."); const [marketRes, ownedOk] = await Promise.all([get("/personas/market"), loadOwnedPersonas()]); if (marketRes.error) { setMarketStatus(marketRes.error, "error"); return; } if (!ownedOk) { setMarketStatus("Unable to load your personas.", "error"); return; } allMarketPersonas = marketRes.personas || []; applyMarketFilter(); } async function collectMarketPersona(marketId) { setMarketStatus("Collecting User Persona..."); const res = await post(`/personas/market/${marketId}/collect`, {equip: true}); if (res.error) { setMarketStatus(res.error, "error"); return; } await loadMarketPersonas(); setMarketStatus("User Persona collected.", "success"); } async function startMarketPersonaChat(marketId, userPersonaSelection, scenarioPrompt = "") { if (!userPersonaSelection) { setMarketStatus("Choose who you are before starting roleplay.", "error"); return; } if (userPersonaSelection === "create_new") { window.location.href = "/settings?view=my-roleplay-companions&create=user"; return; } setMarketStatus("Starting AI Character chat..."); setMarketActivity(true, { eyebrow: "Starting roleplay", title: "Opening AI Character chat", detail: "Generating the initial message and preparing the chat window." }); const userPersonaId = userPersonaSelection === "self" ? null : Number(userPersonaSelection); const res = await post(`/personas/market/${marketId}/chat`, { userPersonaId, scenarioPrompt: scenarioPrompt.trim() }); if (res.error || !res.chat) { setMarketActivity(false); setMarketStatus(res.error || "Unable to start chat.", "error"); return; } window.location.href = `/?chat=${res.chat.id}`; } async function savePersona() { const payload = { personaType: "assistant", name: marketPersonaName.value.trim(), pronouns: marketPersonaPronouns.value.trim(), appearance: marketPersonaAppearance.value.trim(), background: marketPersonaBackground.value.trim(), details: marketPersonaDetails.value.trim(), exampleDialogues: marketPersonaExampleDialogues.value.trim() }; if (!payload.name) { setPersonaFormNotice("Character name is required.", "error"); return; } const res = editingPersonaId ? await put(`/personas/${editingPersonaId}`, payload) : await post("/personas", payload); if (res.error) { setPersonaFormNotice(res.error, "error"); return; } await loadMarketPersonas(); closePersonaForm(); setMarketView("ai-characters"); setMarketStatus(editingPersonaId ? "AI Character updated." : "AI Character created.", "success"); } refreshMarketBtn.addEventListener("click", () => { void loadMarketPersonas(); }); marketSearchInput.addEventListener("input", applyMarketFilter); marketViewButtons.forEach((button) => { button.addEventListener("click", () => setMarketView(button.dataset.marketView)); }); if (marketCreateAiCharacterBtn) { marketCreateAiCharacterBtn.addEventListener("click", () => openPersonaForm()); } if (marketCreateUserPersonaBtn) { marketCreateUserPersonaBtn.addEventListener("click", () => { window.location.href = "/settings?view=my-roleplay-companions&create=user"; }); } marketPreviewClose.addEventListener("click", closeMarketPreview); marketPreviewModal.addEventListener("click", (event) => { if (event.target === marketPreviewModal) closeMarketPreview(); }); marketPreviewUserPersonaSelect.addEventListener("change", () => { if (!pendingMarketPersona || pendingMarketPersona.persona_type !== "assistant") return; if (marketPreviewUserPersonaSelect.value === "create_new") { window.location.href = "/settings?view=my-roleplay-companions&create=user"; return; } marketPreviewConfirm.classList.toggle("hidden", !marketPreviewUserPersonaSelect.value); }); marketPreviewConfirm.addEventListener("click", async () => { if (!pendingMarketPersona) return; const marketId = pendingMarketPersona.id; if (pendingMarketPersona.persona_type === "assistant") { await startMarketPersonaChat( marketId, marketPreviewUserPersonaSelect.value || "self", marketPreviewScenario.value || "" ); closeMarketPreview(); return; } closeMarketPreview(); await collectMarketPersona(marketId); }); marketPersonaClose.addEventListener("click", closePersonaForm); marketPersonaModal.addEventListener("click", (event) => { if (event.target === marketPersonaModal) closePersonaForm(); }); marketPersonaForm.addEventListener("submit", (event) => { event.preventDefault(); void savePersona(); }); window.addEventListener("load", async () => { applyTheme(localStorage.getItem("krishd-theme") || "fakegpt"); const params = new URLSearchParams(window.location.search); setMarketView(params.get("view") || "ai-characters"); await loadMarketPersonas(); if (params.get("create") === "assistant") { setMarketView("ai-characters"); openPersonaForm(); } });