Remove unused persona marketplace and discover page files.
This commit is contained in:
+2
-38
@@ -61,8 +61,7 @@
|
||||
aria-controls="chatDrawer"
|
||||
aria-label="Change AI model"><span>Model</span><strong
|
||||
id="headerModelName">Loading...</strong></button>
|
||||
<span class="chat-context-item"><span>Persona</span><strong
|
||||
id="headerPersonaName">None</strong></span>
|
||||
<label class="chat-context-item"><span>Chat with</span><select id="personalitySelect" aria-label="Conversation personality"></select></label>
|
||||
</div>
|
||||
</div>
|
||||
<div class="chat-header-actions">
|
||||
@@ -151,41 +150,6 @@
|
||||
</details>
|
||||
</div>
|
||||
</details>
|
||||
<details
|
||||
class="chat-drawer-section chat-drawer-disclosure chat-drawer-section--persona">
|
||||
<summary class="chat-drawer-row">
|
||||
<span class="chat-drawer-row-label">
|
||||
<span class="eyebrow">Your identity</span>
|
||||
<strong>Persona</strong>
|
||||
</span>
|
||||
<span class="chat-drawer-row-meta">
|
||||
<strong id="chatDrawerPersonaValue">None</strong>
|
||||
<span class="chat-drawer-row-hint">Change</span>
|
||||
</span>
|
||||
</summary>
|
||||
<div id="personaPanel"
|
||||
class="persona-panel chat-drawer-disclosure-panel"
|
||||
data-static-panel="true">
|
||||
<div class="persona-section">
|
||||
<div class="persona-section-header">
|
||||
<div>
|
||||
<h2>Your persona</h2>
|
||||
</div>
|
||||
<div class="persona-section-actions">
|
||||
<button id="clearUserPersona"
|
||||
class="secondary-action"
|
||||
type="button">Clear for this chat</button>
|
||||
<a href="/app/personas"
|
||||
class="secondary-action">Manage</a>
|
||||
</div>
|
||||
</div>
|
||||
<p id="activeUserPersonaStatus"
|
||||
class="status persona-status">No default user persona
|
||||
set.</p>
|
||||
<div id="userPersonaList" class="persona-list"></div>
|
||||
</div>
|
||||
</div>
|
||||
</details>
|
||||
<div class="chat-drawer-section">
|
||||
<p class="dropdown-section-label">Chat</p>
|
||||
<div class="chat-drawer-action-list">
|
||||
@@ -399,7 +363,7 @@
|
||||
</div>
|
||||
<p id="roleplayStarterNotice" class="status hidden"></p>
|
||||
<div class="roleplay-starter-actions">
|
||||
<button id="roleplayNewPersona" class="secondary-action" type="button">Create AI Character</button>
|
||||
<a id="roleplayNewPersona" class="secondary-action" href="/app/personas">Browse Personalities</a>
|
||||
</div>
|
||||
<div id="roleplayStarterStepOne" class="persona-form-grid">
|
||||
<label class="field field-wide">
|
||||
|
||||
@@ -68,3 +68,15 @@ body[data-theme-mode="dark"] #accountForm input[aria-invalid="true"] { border-co
|
||||
#accountForm .field > label { font-size:.82rem; font-weight:600; color:var(--text-soft); }
|
||||
.account-status:focus { outline:none; }
|
||||
#accountSwitch { margin-top:20px; }
|
||||
.personality-grid { display:grid; grid-template-columns:repeat(auto-fit,minmax(min(100%,260px),1fr)); gap:18px; margin:28px 0 48px; }
|
||||
.personality-card { display:flex; flex-direction:column; align-items:flex-start; gap:12px; min-height:300px; border-top:3px solid var(--personality-accent,#168b87); }
|
||||
.personality-card h2,.personality-card p { margin:0; }
|
||||
.personality-card .subtitle { flex:1; }
|
||||
.personality-icon { display:grid; place-items:center; width:52px; height:52px; border-radius:16px; background:color-mix(in srgb,var(--personality-accent,#168b87) 14%,transparent); color:var(--personality-accent,#168b87); font-size:1.6rem; font-family:"Space Grotesk",sans-serif; }
|
||||
.personality-specialization { color:var(--personality-accent,#168b87); font-size:.78rem; font-weight:800; letter-spacing:.04em; text-transform:uppercase; }
|
||||
.personality-details { width:100%; color:var(--text-soft); font-size:.88rem; line-height:1.55; }
|
||||
.personality-details summary { color:var(--personality-accent,#168b87); cursor:pointer; font-weight:700; }
|
||||
.personality-details p { margin-top:9px; }
|
||||
.personality-inspiration { font-size:.78rem; opacity:.8; }
|
||||
.personality-card .primary-action { margin-top:8px; text-decoration:none; }
|
||||
@media (max-width:640px) { .personality-grid { grid-template-columns:1fr; } }
|
||||
|
||||
@@ -418,6 +418,7 @@ input:focus, textarea:focus, select:focus { border-color: var(--brand); }
|
||||
}
|
||||
.chat-context-item > span { color: var(--text-muted); font-size: .58rem; font-weight: 700; letter-spacing: .1em; text-transform: uppercase; }
|
||||
.chat-context-item > strong { max-width: 140px; overflow: hidden; color: var(--text-soft); font-size: .72rem; text-overflow: ellipsis; white-space: nowrap; }
|
||||
.chat-context-item select { min-width: 0; max-width: 260px; padding: 0; border: 0; background: transparent; color: var(--personality-accent,var(--text-soft)); font: 700 .72rem "Manrope",sans-serif; }
|
||||
.chat-context-button { min-height: 0; border: 0; color: inherit; text-align: left; justify-content: start; }
|
||||
.chat-context-button:hover:not(:disabled) { background: var(--surface-hover); }
|
||||
.chat-header-actions { display: flex; align-items: center; }
|
||||
@@ -654,7 +655,9 @@ input:focus, textarea:focus, select:focus { border-color: var(--brand); }
|
||||
.chat-sidebar { position: fixed; inset: 0 auto 0 0; z-index: 80; width: min(340px, 90vw); transform: translateX(-104%); transition: transform .2s ease; box-shadow: var(--shadow-lg); }
|
||||
body.sidebar-open .chat-sidebar { transform: translateX(0); }
|
||||
.sidebar-toggle { display: inline-flex; }
|
||||
.chat-header-context { display: none; }
|
||||
.chat-header-context { display: flex; }
|
||||
.chat-title-block { flex-direction: column; align-items: flex-start; justify-content: center; gap: 6px; }
|
||||
.chat-context-item select { max-width: min(68vw,260px); }
|
||||
.settings-layout { grid-template-columns: 1fr; }
|
||||
.settings-nav { position: static; display: flex; overflow-x: auto; }
|
||||
.settings-nav-item { flex: 0 0 auto; }
|
||||
|
||||
@@ -1,43 +0,0 @@
|
||||
<!doctype html>
|
||||
<html lang="en">
|
||||
|
||||
<head>
|
||||
<meta charset="UTF-8">
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1">
|
||||
<title>Discover personas · Elsewhere</title>
|
||||
<link rel="icon" href="/img/favicon.svg">
|
||||
<script src="/js/theme-bootstrap.js?v=20260909-account-controls">
|
||||
</script>
|
||||
<link rel="stylesheet" href="/css/style.css?v=20260909-account-controls">
|
||||
<link rel="stylesheet" href="/css/pages.css?v=20260909-account-controls">
|
||||
<link rel="stylesheet" href="/css/shell.css?v=20260909-account-controls">
|
||||
<link
|
||||
href="https://fonts.googleapis.com/css2?family=Space+Grotesk:wght@400;500;700&family=Manrope:wght@400;500;600;700;800&display=swap"
|
||||
rel="stylesheet">
|
||||
</head>
|
||||
|
||||
<body class="site-page discover-page">
|
||||
<!-- site-header -->
|
||||
<main id="main" class="site-section page-content">
|
||||
<p class="eyebrow">From the community</p>
|
||||
<h1>Find your next conversation.</h1>
|
||||
<p class="hero-copy">Discover characters and personas made by the community. Sign in when you're ready to bring
|
||||
one into your workspace.</p>
|
||||
<label class="field">
|
||||
<span>Search personas</span>
|
||||
<input id="publicSearch" type="search" placeholder="Search by name or description">
|
||||
</label>
|
||||
<div id="featuredPersonas" class="feature-grid" aria-live="polite">Loading personas…</div>
|
||||
</main>
|
||||
<footer class="site-footer">
|
||||
<a class="site-brand" href="/">elsewhere</a>
|
||||
<span>A space for every conversation.</span>
|
||||
<a href="/privacy.html">Privacy</a>
|
||||
<a href="/tos.html">Terms</a>
|
||||
</footer>
|
||||
<script type="module" src="/js/pages.js?v=20260909-account-controls">
|
||||
</script>
|
||||
<script type="module" src="/js/workspaceNavigation.js?v=20260909-account-controls"></script>
|
||||
</body>
|
||||
|
||||
</html>
|
||||
+8
-8
@@ -30,17 +30,17 @@
|
||||
<p>Make space for a question or an idea.</p>
|
||||
<span>Start chatting →</span>
|
||||
</a>
|
||||
<a class="feature-card action-card" href="/market">
|
||||
<a class="feature-card action-card" href="/app/personas">
|
||||
<span class="feature-number">02 / EXPLORE</span>
|
||||
<h2>Chat with a character</h2>
|
||||
<p>Find someone with a different perspective.</p>
|
||||
<span>Discover personas →</span>
|
||||
<h2>Choose a personality</h2>
|
||||
<p>Pick a specialized assistant for writing, coding, learning, research, or planning.</p>
|
||||
<span>Explore personalities →</span>
|
||||
</a>
|
||||
<a class="feature-card action-card" href="/app/personas?create=assistant">
|
||||
<a class="feature-card action-card" href="/app/chats/new?personality=creative-writer">
|
||||
<span class="feature-number">03 / CREATE</span>
|
||||
<h2>Create a persona</h2>
|
||||
<p>Give a new character a voice of their own.</p>
|
||||
<span>Create persona →</span>
|
||||
<h2>Start writing</h2>
|
||||
<p>Open a conversation with a focused creative-writing assistant.</p>
|
||||
<span>Write something →</span>
|
||||
</a>
|
||||
</div>
|
||||
<section class="site-section">
|
||||
|
||||
@@ -6,7 +6,8 @@ export const requestedChatId = Number(window.location.pathname.match(/^\/app\/ch
|
||||
export const onboardingPrompts = {
|
||||
ask: ["Explain this topic in simple terms.", "Compare these options and recommend one."],
|
||||
brainstorm: ["Give me 10 practical ideas for this problem.", "Suggest 3 creative directions and tradeoffs."],
|
||||
roleplay: ["Start a roleplay scene with immediate tension.", "Give me a dramatic opening with clear stakes."]
|
||||
write: ["Write an opening scene with a strong point of view.", "Help me revise this paragraph while preserving my voice."],
|
||||
learn: ["Teach me this concept step by step.", "Give me a short practice question on this topic."]
|
||||
};
|
||||
|
||||
// Stable keys preserve existing saved appearance preferences.
|
||||
|
||||
@@ -19,7 +19,8 @@ export function createConversationController({
|
||||
updateJumpToLatest,
|
||||
updateSendState,
|
||||
loadSummary,
|
||||
updateHistoryControls = () => {}
|
||||
updateHistoryControls = () => {},
|
||||
getPersonalityId = () => "general-assistant"
|
||||
}) {
|
||||
let opening = false;
|
||||
function getChatById(id) { return state.chatSessions.find((chat) => chat.id === id); }
|
||||
@@ -203,7 +204,7 @@ export function createConversationController({
|
||||
detail: "Setting up a fresh chat so you can start sending messages."
|
||||
});
|
||||
try {
|
||||
const res = await post("/chats", {title: "New chat"}, {signal});
|
||||
const res = await post("/chats", {title: "New chat", personalityId: getPersonalityId()}, {signal});
|
||||
if (res.error || !res.chat) {
|
||||
const error = res.error || "Unable to create chat.";
|
||||
setNotice(error, "error");
|
||||
|
||||
@@ -1,698 +0,0 @@
|
||||
/**
|
||||
* @file Steuert die Persona-Marktplatzoberfläche mit Suche, Vorschauen und Persona-Verwaltung.
|
||||
*/
|
||||
import { get, post, put } from "./api.js?v=20260909-account-controls";
|
||||
import { initializeAppearance } from "./themeController.js?v=20260909-account-controls";
|
||||
import { clearStatus, setStatus } from "./statusNotice.js?v=20260909-account-controls";
|
||||
import { populatePersonaForm, readPersonaForm } from "./personaForm.js?v=20260909-account-controls";
|
||||
import { openModal, closeModal, topModal } from "./modalFocus.js";
|
||||
|
||||
const $ = (id) => document.getElementById(id);
|
||||
const marketPersonaList = $("marketPersonaList");
|
||||
const marketUserPersonaList = $("marketUserPersonaList");
|
||||
const refreshMarketBtn = $("refreshMarket");
|
||||
const marketStatus = $("marketStatus");
|
||||
const marketSearchInput = $("marketSearch");
|
||||
const marketAssistantSort = $("marketAssistantSort");
|
||||
const marketUserSort = $("marketUserSort");
|
||||
const marketViewButtons = document.querySelectorAll("[data-market-view]");
|
||||
const marketPanels = document.querySelectorAll("[data-market-panel]");
|
||||
const marketCreateAiCharacterBtn = $("marketCreateAiCharacter");
|
||||
const marketCreateUserPersonaBtn = $("marketCreateUserPersona");
|
||||
const marketPreviewModal = $("marketPreviewModal");
|
||||
const marketPreviewTitle = $("marketPreviewTitle");
|
||||
const marketPreviewName = $("marketPreviewName");
|
||||
const marketPreviewMeta = $("marketPreviewMeta");
|
||||
const marketPreviewBody = $("marketPreviewBody");
|
||||
const marketPreviewPronouns = $("marketPreviewPronouns");
|
||||
const marketPreviewAppearance = $("marketPreviewAppearance");
|
||||
const marketPreviewBackground = $("marketPreviewBackground");
|
||||
const marketPreviewDetails = $("marketPreviewDetails");
|
||||
const marketPreviewExampleDialogues = $("marketPreviewExampleDialogues");
|
||||
const marketPreviewIntro = $("marketPreviewIntro");
|
||||
const marketPreviewUserPersonaField = $("marketPreviewUserPersonaField");
|
||||
const marketPreviewUserPersonaSelect = $("marketPreviewUserPersonaSelect");
|
||||
const marketPreviewScenario = $("marketPreviewScenario");
|
||||
const marketPreviewStepOne = $("marketPreviewStepOne");
|
||||
const marketPreviewStepTwo = $("marketPreviewStepTwo");
|
||||
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 marketPopupModal = $("marketPopupModal");
|
||||
const marketPopupClose = $("marketPopupClose");
|
||||
const marketPopupCancel = $("marketPopupCancel");
|
||||
const marketPopupConfirm = $("marketPopupConfirm");
|
||||
const marketPopupTitle = $("marketPopupTitle");
|
||||
const marketPopupEyebrow = $("marketPopupEyebrow");
|
||||
const marketPopupDescription = $("marketPopupDescription");
|
||||
const marketPopupField = $("marketPopupField");
|
||||
const marketPopupInputLabel = $("marketPopupInputLabel");
|
||||
const marketPopupInput = $("marketPopupInput");
|
||||
const brandNameTargets = document.querySelectorAll("[data-brand-name]");
|
||||
const marketPersonaFields = {
|
||||
name: marketPersonaName,
|
||||
pronouns: marketPersonaPronouns,
|
||||
appearance: marketPersonaAppearance,
|
||||
background: marketPersonaBackground,
|
||||
details: marketPersonaDetails,
|
||||
exampleDialogues: marketPersonaExampleDialogues
|
||||
};
|
||||
|
||||
const validMarketSorts = new Set(["best", "newest", "most_favorited", "most_popular", "top_rated", "alphabetical"]);
|
||||
let marketPersonasByType = { assistant: [], user: [] };
|
||||
let userPersonas = [];
|
||||
let pendingMarketPersona = null;
|
||||
let editingPersonaId = null;
|
||||
let activeUserPersonaId = null;
|
||||
let activeMarketView = "ai-characters";
|
||||
let marketSorts = { assistant: "best", user: "best" };
|
||||
let marketActivityDepth = 0;
|
||||
let marketPreviewStep = 1;
|
||||
let marketPopupResolver = null;
|
||||
|
||||
function setMarketStatus(message, state = "") {
|
||||
setStatus(marketStatus, message, state, "status persona-status");
|
||||
}
|
||||
|
||||
function closeMarketPopup(value = null) {
|
||||
closeModal(marketPopupModal);
|
||||
if (marketPopupResolver) {
|
||||
const resolve = marketPopupResolver;
|
||||
marketPopupResolver = null;
|
||||
resolve(value);
|
||||
}
|
||||
}
|
||||
|
||||
function promptMarketPopup({
|
||||
eyebrow = "Action",
|
||||
title = "Enter a value",
|
||||
description = "",
|
||||
label = "Value",
|
||||
value = "",
|
||||
placeholder = "",
|
||||
confirmLabel = "Confirm"
|
||||
} = {}) {
|
||||
marketPopupEyebrow.textContent = eyebrow;
|
||||
marketPopupTitle.textContent = title;
|
||||
marketPopupDescription.textContent = description;
|
||||
marketPopupInputLabel.textContent = label;
|
||||
marketPopupInput.value = value;
|
||||
marketPopupInput.placeholder = placeholder;
|
||||
marketPopupConfirm.textContent = confirmLabel;
|
||||
marketPopupField.classList.remove("hidden");
|
||||
openModal(marketPopupModal, marketPopupInput);
|
||||
marketPopupInput.focus();
|
||||
marketPopupInput.select();
|
||||
return new Promise((resolve) => {
|
||||
marketPopupResolver = resolve;
|
||||
});
|
||||
}
|
||||
|
||||
function setPersonaFormNotice(message = "", state = "") {
|
||||
if (!message) {
|
||||
clearStatus(marketPersonaFormNotice);
|
||||
return;
|
||||
}
|
||||
setStatus(marketPersonaFormNotice, message, state);
|
||||
}
|
||||
|
||||
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) => {
|
||||
const isActive = item.dataset.marketView === normalizedView;
|
||||
item.classList.toggle("active", isActive);
|
||||
item.setAttribute("aria-selected", String(isActive));
|
||||
});
|
||||
marketPanels.forEach((panel) => {
|
||||
const isActive = panel.dataset.marketPanel === normalizedView;
|
||||
panel.classList.toggle("active", isActive);
|
||||
panel.setAttribute("aria-hidden", String(!isActive));
|
||||
});
|
||||
syncMarketQuery();
|
||||
applyMarketFilter();
|
||||
}
|
||||
|
||||
function normalizeMarketSort(sort) {
|
||||
const normalized = String(sort || "best").trim().toLowerCase().replace(/-/g, "_");
|
||||
return validMarketSorts.has(normalized) ? normalized : "best";
|
||||
}
|
||||
|
||||
function syncMarketQuery() {
|
||||
const params = new URLSearchParams(window.location.search);
|
||||
params.set("view", activeMarketView);
|
||||
params.set("assistantSort", marketSorts.assistant);
|
||||
params.set("userSort", marketSorts.user);
|
||||
history.replaceState(null, "", `${window.location.pathname}?${params.toString()}`);
|
||||
}
|
||||
|
||||
function setMarketSort(personaType, sort, { reload = true } = {}) {
|
||||
const normalizedType = personaType === "user" ? "user" : "assistant";
|
||||
const normalizedSort = normalizeMarketSort(sort);
|
||||
marketSorts[normalizedType] = normalizedSort;
|
||||
if (normalizedType === "assistant" && marketAssistantSort) marketAssistantSort.value = normalizedSort;
|
||||
if (normalizedType === "user" && marketUserSort) marketUserSort.value = normalizedSort;
|
||||
syncMarketQuery();
|
||||
if (reload) {
|
||||
void loadMarketPersonas();
|
||||
}
|
||||
}
|
||||
|
||||
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() {
|
||||
closeModal(marketPreviewModal);
|
||||
pendingMarketPersona = null;
|
||||
marketPreviewStep = 1;
|
||||
marketPreviewUserPersonaSelect.innerHTML = "";
|
||||
marketPreviewScenario.value = "";
|
||||
}
|
||||
|
||||
function renderMarketPreviewStep() {
|
||||
const isScenarioStep = marketPreviewStep === 2;
|
||||
const isAssistant = pendingMarketPersona?.persona_type === "assistant";
|
||||
if (marketPreviewBody) marketPreviewBody.classList.toggle("hidden", isScenarioStep && isAssistant);
|
||||
if (marketPreviewStepOne) marketPreviewStepOne.classList.toggle("hidden", isScenarioStep || pendingMarketPersona?.persona_type !== "assistant");
|
||||
if (marketPreviewStepTwo) marketPreviewStepTwo.classList.toggle("hidden", !isScenarioStep || pendingMarketPersona?.persona_type !== "assistant");
|
||||
if (marketPreviewTitle) {
|
||||
marketPreviewTitle.textContent = isAssistant && isScenarioStep ? "Set the opening scene" : "Persona details";
|
||||
}
|
||||
if (marketPreviewIntro) {
|
||||
marketPreviewIntro.textContent = pendingMarketPersona?.persona_type === "assistant"
|
||||
? (isScenarioStep
|
||||
? "Add an optional custom scenario. If left empty, the AI Character will invent its own opening scene."
|
||||
: "Choose who you are in this roleplay before moving on to the opening scene.")
|
||||
: "Review this persona and choose how you want to use it.";
|
||||
}
|
||||
if (marketPreviewConfirm) {
|
||||
marketPreviewConfirm.textContent = pendingMarketPersona?.persona_type !== "assistant" ? "Collect persona" : "Start roleplay";
|
||||
}
|
||||
}
|
||||
|
||||
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;
|
||||
marketPreviewStep = 1;
|
||||
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);
|
||||
if (isAssistant) {
|
||||
populateUserPersonaChoices();
|
||||
marketPreviewConfirm.classList.add("hidden");
|
||||
} else {
|
||||
marketPreviewConfirm.classList.remove("hidden");
|
||||
}
|
||||
renderMarketPreviewStep();
|
||||
openModal(marketPreviewModal, isAssistant ? marketPreviewUserPersonaSelect : marketPreviewConfirm);
|
||||
}
|
||||
|
||||
function openPersonaForm(persona = null) {
|
||||
editingPersonaId = persona ? persona.id : null;
|
||||
marketPersonaFormTitle.textContent = persona ? "Edit AI Character" : "Create AI Character";
|
||||
populatePersonaForm(marketPersonaFields, persona || {});
|
||||
setPersonaFormNotice("");
|
||||
openModal(marketPersonaModal, marketPersonaName);
|
||||
}
|
||||
|
||||
function closePersonaForm() {
|
||||
closeModal(marketPersonaModal);
|
||||
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 stats = document.createElement("p");
|
||||
const actions = document.createElement("div");
|
||||
const detailsBtn = document.createElement("button");
|
||||
const favoriteBtn = 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);
|
||||
const tags = Array.isArray(persona.tags) && persona.tags.length ? `Tags: ${persona.tags.join(", ")}` : "Tags: none";
|
||||
const rating = persona.rating_count ? `${Number(persona.rating_average || 0).toFixed(1)}★` : "Unrated";
|
||||
stats.className = "subtitle";
|
||||
stats.textContent = `${tags} | Favorites: ${persona.favorite_count || 0} | Uses: ${persona.usage_count || 0} | Rating: ${rating}`;
|
||||
|
||||
detailsBtn.type = "button";
|
||||
detailsBtn.textContent = "Preview";
|
||||
detailsBtn.addEventListener("click", () => openMarketPreview(persona));
|
||||
actions.appendChild(detailsBtn);
|
||||
|
||||
favoriteBtn.type = "button";
|
||||
favoriteBtn.className = persona.is_favorite ? "icon-button market-favorite-button active" : "icon-button market-favorite-button";
|
||||
favoriteBtn.textContent = persona.is_favorite ? "♥" : "♡";
|
||||
favoriteBtn.title = persona.is_favorite ? "Unfavorite persona" : "Favorite persona";
|
||||
favoriteBtn.setAttribute("aria-label", favoriteBtn.title);
|
||||
favoriteBtn.setAttribute("aria-pressed", persona.is_favorite ? "true" : "false");
|
||||
favoriteBtn.addEventListener("click", () => { void toggleFavorite(persona.id); });
|
||||
actions.appendChild(favoriteBtn);
|
||||
|
||||
const rateBtn = document.createElement("button");
|
||||
rateBtn.type = "button";
|
||||
rateBtn.textContent = "Rate";
|
||||
rateBtn.addEventListener("click", () => { void ratePersona(persona.id); });
|
||||
actions.appendChild(rateBtn);
|
||||
|
||||
const reportBtn = document.createElement("button");
|
||||
reportBtn.type = "button";
|
||||
reportBtn.textContent = "Report";
|
||||
reportBtn.addEventListener("click", () => { void reportPersona(persona.id); });
|
||||
actions.appendChild(reportBtn);
|
||||
|
||||
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);
|
||||
item.append(titleRow, meta, stats, actions);
|
||||
listElement.appendChild(item);
|
||||
});
|
||||
}
|
||||
|
||||
function filterMarketPersonas(items) {
|
||||
const query = marketSearchInput.value.trim().toLowerCase();
|
||||
if (!query) return items;
|
||||
return items.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 assistantItems = filterMarketPersonas(marketPersonasByType.assistant);
|
||||
const userItems = filterMarketPersonas(marketPersonasByType.user);
|
||||
renderMarketList(assistantItems, marketPersonaList, "assistant");
|
||||
renderMarketList(userItems, marketUserPersonaList, "user");
|
||||
|
||||
if (marketSearchInput.value.trim() && !assistantItems.length && !userItems.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 refreshMarket() {
|
||||
const [assistantRes, userRes] = await Promise.all([
|
||||
get(`/personas/market?personaType=assistant&sort=${encodeURIComponent(marketSorts.assistant)}`),
|
||||
get(`/personas/market?personaType=user&sort=${encodeURIComponent(marketSorts.user)}`)
|
||||
]);
|
||||
if (assistantRes.error || userRes.error) {
|
||||
setMarketStatus(assistantRes.error || userRes.error, "error");
|
||||
return;
|
||||
}
|
||||
marketPersonasByType = {
|
||||
assistant: assistantRes.personas || [],
|
||||
user: userRes.personas || []
|
||||
};
|
||||
applyMarketFilter();
|
||||
}
|
||||
|
||||
async function toggleFavorite(marketId) {
|
||||
const res = await post(`/personas/market/${marketId}/favorite`, {});
|
||||
if (res.error) return setMarketStatus(res.error, "error");
|
||||
await refreshMarket();
|
||||
setMarketStatus(res.favorite ? "Favorited persona." : "Removed favorite.", "success");
|
||||
}
|
||||
|
||||
async function ratePersona(marketId) {
|
||||
const raw = await promptMarketPopup({
|
||||
eyebrow: "Rating",
|
||||
title: "Rate this persona",
|
||||
description: "Enter a whole number from 1 to 5.",
|
||||
label: "Rating",
|
||||
value: "5",
|
||||
placeholder: "5",
|
||||
confirmLabel: "Save rating"
|
||||
});
|
||||
if (raw === null) return;
|
||||
const rating = Number(raw);
|
||||
if (!rating) return;
|
||||
const res = await post(`/personas/market/${marketId}/rate`, { rating });
|
||||
if (res.error) return setMarketStatus(res.error, "error");
|
||||
await refreshMarket();
|
||||
setMarketStatus("Rating saved.", "success");
|
||||
}
|
||||
|
||||
async function reportPersona(marketId) {
|
||||
const reason = await promptMarketPopup({
|
||||
eyebrow: "Report",
|
||||
title: "Report persona",
|
||||
description: "Describe the main issue.",
|
||||
label: "Reason",
|
||||
placeholder: "Abusive content",
|
||||
confirmLabel: "Continue"
|
||||
});
|
||||
if (!reason) return;
|
||||
const details = await promptMarketPopup({
|
||||
eyebrow: "Report",
|
||||
title: "Extra details",
|
||||
description: "Add optional context for the report.",
|
||||
label: "Details",
|
||||
placeholder: "Optional",
|
||||
confirmLabel: "Submit report"
|
||||
});
|
||||
if (details === null) return;
|
||||
const res = await post(`/personas/market/${marketId}/report`, { reason, details });
|
||||
if (res.error) return setMarketStatus(res.error, "error");
|
||||
setMarketStatus("Report submitted.", "success");
|
||||
}
|
||||
|
||||
async function loadOwnedPersonas() {
|
||||
const res = await get("/personas");
|
||||
if (res.error) return false;
|
||||
userPersonas = res.userPersonas || [];
|
||||
activeUserPersonaId = res.activeUserPersonaId || null;
|
||||
return true;
|
||||
}
|
||||
|
||||
async function loadMarketPersonas() {
|
||||
setMarketStatus("Loading personas...");
|
||||
const ownedOk = await loadOwnedPersonas();
|
||||
if (!ownedOk) {
|
||||
setMarketStatus("Unable to load your personas.", "error");
|
||||
return;
|
||||
}
|
||||
await refreshMarket();
|
||||
}
|
||||
|
||||
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 = "/app/personas?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()
|
||||
}, {timeoutMs: 660000});
|
||||
if (res.error || !res.chat) {
|
||||
setMarketActivity(false);
|
||||
setMarketStatus(res.error || "Unable to start chat.", "error");
|
||||
return;
|
||||
}
|
||||
window.location.href = `/app/chats/${res.chat.id}`;
|
||||
}
|
||||
|
||||
async function savePersona() {
|
||||
const payload = {
|
||||
...readPersonaForm(marketPersonaFields),
|
||||
personaType: "assistant",
|
||||
};
|
||||
|
||||
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();
|
||||
});
|
||||
marketPopupConfirm.addEventListener("click", () => closeMarketPopup(marketPopupInput.value));
|
||||
marketPopupCancel.addEventListener("click", () => closeMarketPopup(null));
|
||||
marketPopupClose.addEventListener("click", () => closeMarketPopup(null));
|
||||
marketPopupModal.addEventListener("click", (event) => {
|
||||
if (event.target === marketPopupModal) closeMarketPopup(null);
|
||||
});
|
||||
marketPopupInput.addEventListener("keydown", (event) => {
|
||||
if (event.key === "Enter") {
|
||||
event.preventDefault();
|
||||
closeMarketPopup(marketPopupInput.value);
|
||||
}
|
||||
if (event.key === "Escape") {
|
||||
event.preventDefault();
|
||||
event.stopPropagation();
|
||||
closeMarketPopup(null);
|
||||
}
|
||||
});
|
||||
document.addEventListener("keydown", event => {
|
||||
if (event.key !== "Escape") return;
|
||||
const modal = topModal();
|
||||
if (!modal) return;
|
||||
event.preventDefault();
|
||||
if (modal === marketPopupModal) closeMarketPopup(null);
|
||||
else if (modal === marketPersonaModal) closePersonaForm();
|
||||
else if (modal === marketPreviewModal) closeMarketPreview();
|
||||
});
|
||||
marketSearchInput.addEventListener("input", applyMarketFilter);
|
||||
if (marketAssistantSort) {
|
||||
marketAssistantSort.addEventListener("change", () => setMarketSort("assistant", marketAssistantSort.value));
|
||||
}
|
||||
if (marketUserSort) {
|
||||
marketUserSort.addEventListener("change", () => setMarketSort("user", marketUserSort.value));
|
||||
}
|
||||
marketViewButtons.forEach((button) => {
|
||||
button.addEventListener("click", () => setMarketView(button.dataset.marketView));
|
||||
});
|
||||
if (marketCreateAiCharacterBtn) {
|
||||
marketCreateAiCharacterBtn.addEventListener("click", () => openPersonaForm());
|
||||
}
|
||||
if (marketCreateUserPersonaBtn) {
|
||||
marketCreateUserPersonaBtn.addEventListener("click", () => {
|
||||
window.location.href = "/app/personas?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 = "/app/personas?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") {
|
||||
if (marketPreviewStep === 1) {
|
||||
if (!marketPreviewUserPersonaSelect.value) {
|
||||
setMarketStatus("Choose who you are before continuing.", "error");
|
||||
return;
|
||||
}
|
||||
marketPreviewStep = 2;
|
||||
renderMarketPreviewStep();
|
||||
marketPreviewScenario.focus();
|
||||
return;
|
||||
}
|
||||
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 () => {
|
||||
initializeAppearance({ title: "Elsewhere Market", nameTargets: brandNameTargets });
|
||||
const params = new URLSearchParams(window.location.search);
|
||||
setMarketSort("assistant", params.get("assistantSort") || "best", { reload: false });
|
||||
setMarketSort("user", params.get("userSort") || "best", { reload: false });
|
||||
setMarketView(params.get("view") || "ai-characters");
|
||||
await loadMarketPersonas();
|
||||
const selectedPersona = [...marketPersonasByType.assistant, ...marketPersonasByType.user].find(persona => persona.id === Number(params.get("persona")));
|
||||
if (selectedPersona) openMarketPreview(selectedPersona);
|
||||
if (params.get("create") === "assistant") {
|
||||
setMarketView("ai-characters");
|
||||
openPersonaForm();
|
||||
}
|
||||
});
|
||||
@@ -17,9 +17,7 @@ export function createMessageRenderer({
|
||||
renderDrawerSummary,
|
||||
updateChatActionState,
|
||||
updateComposerPlaceholder,
|
||||
createNewChat,
|
||||
openRoleplayStarter,
|
||||
openPersonaForm
|
||||
createNewChat
|
||||
}) {
|
||||
const jumpToLatest = document.getElementById("jumpToLatest");
|
||||
|
||||
@@ -301,13 +299,7 @@ export function createMessageRenderer({
|
||||
description.textContent = "Choose a starting point.";
|
||||
actions.append(
|
||||
createStarterAction("New chat", "primary-action", () => { void createNewChat(); }),
|
||||
createStarterAction("Roleplay", "secondary-action", () => {
|
||||
if (state.assistantPersonas.length) {
|
||||
openRoleplayStarter();
|
||||
return;
|
||||
}
|
||||
openPersonaForm(null, "assistant");
|
||||
}),
|
||||
createStarterAction("Browse Personalities", "secondary-action", () => { location.assign("/app/personas"); }),
|
||||
createMessageStarterPrompt("Try a prompt", "Help me plan my day in 3 steps.")
|
||||
);
|
||||
} else if (chat.assistant_persona_id) {
|
||||
@@ -315,8 +307,7 @@ export function createMessageRenderer({
|
||||
description.textContent = "Send a line or pick a prompt.";
|
||||
actions.append(
|
||||
createMessageStarterPrompt("Add tension", "Open with a tense line that raises the stakes immediately."),
|
||||
createMessageStarterPrompt("Set the scene", "Describe what your character notices first in this scene."),
|
||||
createStarterAction("Change setup", "secondary-action", () => openRoleplayStarter())
|
||||
createMessageStarterPrompt("Set the scene", "Describe what your character notices first in this scene.")
|
||||
);
|
||||
} else {
|
||||
title.textContent = "What can I help with?";
|
||||
|
||||
@@ -109,30 +109,3 @@ if (recent) {
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
const featured = document.querySelector("#featuredPersonas");
|
||||
if (featured) {
|
||||
const result = await get("/api/discover");
|
||||
const search = document.querySelector("#publicSearch");
|
||||
function render() {
|
||||
featured.replaceChildren();
|
||||
if (result.error) { featured.textContent = result.error; return; }
|
||||
const query = search?.value.trim().toLowerCase() || "";
|
||||
let personas = (result.personas || []).filter(persona => `${persona.name} ${persona.description || ""}`.toLowerCase().includes(query));
|
||||
if (!search) personas = personas.slice(0, 3);
|
||||
if (!personas.length) { featured.textContent = query ? "No personas match your search." : "The community is just getting started. Create a persona and give the next story its voice."; return; }
|
||||
for (const persona of personas) {
|
||||
const card = document.createElement("article"); card.className = "feature-card";
|
||||
const category = document.createElement("span"); category.className = "feature-number"; category.textContent = persona.persona_type === "assistant" ? "AI CHARACTER" : "USER PERSONA";
|
||||
const title = document.createElement("h3"); title.textContent = persona.name;
|
||||
const description = document.createElement("p"); description.textContent = persona.description || "A community persona, ready for a new conversation.";
|
||||
const creator = document.createElement("p"); creator.textContent = `By ${persona.creator_username}`;
|
||||
const link = document.createElement("a"); const destination = `/market?persona=${persona.id}`;
|
||||
link.href = session.user ? destination : `/login?next=${encodeURIComponent(destination)}`;
|
||||
link.textContent = session.user ? "Explore persona \u2192" : "Sign in to use persona \u2192";
|
||||
card.append(category, title, description, creator, link); featured.append(card);
|
||||
}
|
||||
}
|
||||
render(); search?.addEventListener("input", render);
|
||||
}
|
||||
|
||||
|
||||
@@ -0,0 +1,45 @@
|
||||
import {get} from "./api.js";
|
||||
|
||||
const grid = document.getElementById("personalityGrid");
|
||||
const icons = {spark:"✥", quill:"✒", code:"⌘", flask:"⚗", "book-open":"▤", logic:"∴"};
|
||||
const result = await get("/personalities");
|
||||
if (result.error) {
|
||||
grid.textContent = result.error;
|
||||
} else {
|
||||
for (const item of result.personalities || []) {
|
||||
const card = document.createElement("article");
|
||||
card.className = "settings-panel personality-card";
|
||||
card.style.setProperty("--personality-accent", item.accentColor);
|
||||
const icon = document.createElement("div");
|
||||
icon.className = "personality-icon";
|
||||
icon.textContent = icons[item.icon] || "✦";
|
||||
icon.title = item.name;
|
||||
icon.setAttribute("aria-hidden", "true");
|
||||
const specialization = document.createElement("p");
|
||||
specialization.className = "personality-specialization";
|
||||
specialization.textContent = `${item.specialization}${item.isDefault ? " · Default" : ""}`;
|
||||
const name = document.createElement("h2");
|
||||
name.textContent = item.name;
|
||||
name.title = `Inspired by ${item.historicalInspiration}`;
|
||||
const description = document.createElement("p");
|
||||
description.className = "subtitle";
|
||||
description.textContent = item.description;
|
||||
const details = document.createElement("details");
|
||||
details.className = "personality-details";
|
||||
const summary = document.createElement("summary");
|
||||
summary.textContent = "Learn more";
|
||||
const longDescription = document.createElement("p");
|
||||
longDescription.textContent = item.longDescription;
|
||||
const inspiration = document.createElement("p");
|
||||
inspiration.className = "personality-inspiration";
|
||||
inspiration.textContent = `Thematic inspiration: ${item.historicalInspiration}`;
|
||||
details.append(summary, longDescription, inspiration);
|
||||
const action = document.createElement("a");
|
||||
action.className = "primary-action";
|
||||
action.textContent = "Start Chat";
|
||||
action.setAttribute("aria-label", `Start a chat with ${item.name}, ${item.specialization}`);
|
||||
action.href = `/app/chats/new?personality=${encodeURIComponent(item.id)}`;
|
||||
card.append(icon, specialization, name, description, details, action);
|
||||
grid.append(card);
|
||||
}
|
||||
}
|
||||
+51
-20
@@ -121,6 +121,8 @@ let chatLoadingDepth = 0;
|
||||
let chatActivityDepth = 0;
|
||||
|
||||
let availableModelsById = new Map();
|
||||
const personalitySelect = document.getElementById("personalitySelect");
|
||||
let personalities = [];
|
||||
|
||||
const {setMessageContent, scrollMessagesToBottom, updateJumpToLatest, addMessage, renderMessages} = createMessageRenderer({
|
||||
getChatById: (...args) => getChatById(...args),
|
||||
@@ -136,9 +138,7 @@ const {setMessageContent, scrollMessagesToBottom, updateJumpToLatest, addMessage
|
||||
renderDrawerSummary: (...args) => renderDrawerSummary(...args),
|
||||
updateChatActionState: (...args) => updateChatActionState(...args),
|
||||
updateComposerPlaceholder: (...args) => updateComposerPlaceholder(...args),
|
||||
createNewChat: (...args) => createNewChat(...args),
|
||||
openRoleplayStarter: (...args) => openRoleplayStarter(...args),
|
||||
openPersonaForm: (...args) => openPersonaForm(...args)
|
||||
createNewChat: (...args) => createNewChat(...args)
|
||||
});
|
||||
|
||||
const {
|
||||
@@ -146,11 +146,8 @@ const {
|
||||
setRoleplayStarterNotice,
|
||||
renderRoleplayStarterStep,
|
||||
maybeShowPersonaNudge,
|
||||
openPersonaForm,
|
||||
closePersonaForm,
|
||||
openRoleplayStarter,
|
||||
closeRoleplayStarter,
|
||||
loadPersonas,
|
||||
clearChatUserPersona,
|
||||
renderPersonaSection,
|
||||
savePersona,
|
||||
@@ -192,7 +189,8 @@ const {
|
||||
updateJumpToLatest: (...args) => updateJumpToLatest(...args),
|
||||
updateSendState: (...args) => updateSendState(...args),
|
||||
loadSummary: (...args) => loadSummary(...args),
|
||||
updateHistoryControls: () => updateHistoryControls()
|
||||
updateHistoryControls: () => updateHistoryControls(),
|
||||
getPersonalityId: () => personalitySelect?.value || "general-assistant"
|
||||
});
|
||||
|
||||
const {updateSendState, resizeComposerInput, setLoadingState, stopGeneration, sendMessage} = createComposerController({
|
||||
@@ -220,7 +218,7 @@ const {updateSendState, resizeComposerInput, setLoadingState, stopGeneration, se
|
||||
});
|
||||
|
||||
const {exportCurrentChat, backupWorkspace, importWorkspaceFromFile} = createWorkspaceTransfer({
|
||||
conversation, getChatById, setNotice, promptPopup, confirmPopup, setChatLoading, loadPersonas, loadChatSessions
|
||||
conversation, getChatById, setNotice, promptPopup, confirmPopup, setChatLoading, loadChatSessions
|
||||
});
|
||||
const {editChatMessage, deleteChatMessage, retryLatestAssistantMessage, switchResponseVersion, switchRetryVariant, branchFromMessage} = createMessageActions({
|
||||
conversation, setNotice, promptPopup, confirmPopup, setActiveChat, openCreatedChat, setLoadingState, loadSummary, renderMessages, addMessage, setMessageContent, webSearchToggle, reasoningToggle, modelSelect, messagesDiv, msgInput
|
||||
@@ -334,7 +332,7 @@ function renderOnboardingStep() {
|
||||
if (onboardingStep === 1) {
|
||||
onboardingTitle.textContent = "Where shall we start?";
|
||||
onboardingSubtitle.textContent = "Ask a question, explore an idea, or step into a story.";
|
||||
[["ask", "Ask questions"], ["brainstorm", "Brainstorm ideas"], ["roleplay", "Roleplay with a character"]].forEach(([key, label]) => {
|
||||
[["ask", "Ask questions"], ["brainstorm", "Explore ideas"], ["write", "Write something"], ["learn", "Learn a topic"]].forEach(([key, label]) => {
|
||||
const button = document.createElement("button");
|
||||
button.type = "button";
|
||||
button.className = `secondary-action${onboardingIntent === key ? " active" : ""}`;
|
||||
@@ -356,15 +354,6 @@ function renderOnboardingStep() {
|
||||
button.className = "secondary-action";
|
||||
button.textContent = template;
|
||||
button.addEventListener("click", async () => {
|
||||
if (onboardingIntent === "roleplay") {
|
||||
closeOnboarding();
|
||||
if (conversation.assistantPersonas.length) {
|
||||
openRoleplayStarter();
|
||||
} else {
|
||||
openPersonaForm(null, "assistant");
|
||||
}
|
||||
return;
|
||||
}
|
||||
if (!conversation.activeChatId) await createNewChat();
|
||||
msgInput.value = template;
|
||||
msgInput.dispatchEvent(new Event("input"));
|
||||
@@ -447,12 +436,54 @@ function updateWorkspaceCopy() {
|
||||
if (activeChatTitle) {
|
||||
activeChatTitle.textContent = activeChat ? activeChat.title : "New chat";
|
||||
}
|
||||
if (personalitySelect) {
|
||||
personalitySelect.value = activeChat?.personality_id || personalitySelect.value || "general-assistant";
|
||||
syncPersonalityIdentity();
|
||||
}
|
||||
|
||||
renderDrawerSummary();
|
||||
renderPersonaSection();
|
||||
updateComposerPlaceholder();
|
||||
}
|
||||
|
||||
function syncPersonalityIdentity() {
|
||||
const personality = personalities.find(item => item.id === personalitySelect?.value);
|
||||
if (personality && personalitySelect) personalitySelect.style.setProperty("--personality-accent", personality.accentColor);
|
||||
}
|
||||
|
||||
async function loadPersonalities() {
|
||||
const response = await get("/personalities");
|
||||
if (response.error) return setNotice(response.error, "error");
|
||||
personalities = response.personalities || [];
|
||||
if (!personalitySelect) return;
|
||||
personalitySelect.replaceChildren(...personalities.map(item => {
|
||||
const option = document.createElement("option");
|
||||
option.value = item.id;
|
||||
option.textContent = `${item.name} · ${item.specialization}${item.isDefault ? " · Default" : ""}`;
|
||||
option.title = `${item.description} Inspired by ${item.historicalInspiration}.`;
|
||||
return option;
|
||||
}));
|
||||
const requested = new URLSearchParams(location.search).get("personality");
|
||||
personalitySelect.value = getChatById(conversation.activeChatId)?.personality_id ||
|
||||
(personalities.some(item => item.id === requested) ? requested : response.defaultPersonalityId) || "general-assistant";
|
||||
syncPersonalityIdentity();
|
||||
}
|
||||
|
||||
personalitySelect?.addEventListener("change", async () => {
|
||||
const chatId = conversation.activeChatId;
|
||||
const selected = personalitySelect.value;
|
||||
syncPersonalityIdentity();
|
||||
if (!chatId) return;
|
||||
const response = await put(`/chats/${chatId}/personality`, {personalityId: selected});
|
||||
if (response.error || !response.chat) {
|
||||
updateWorkspaceCopy();
|
||||
return setNotice(response.error || "Unable to change personality.", "error");
|
||||
}
|
||||
Object.assign(getChatById(chatId) || {}, response.chat);
|
||||
updateWorkspaceCopy();
|
||||
setNotice(`Personality changed to ${response.personality.name}.`, "success");
|
||||
});
|
||||
|
||||
function renderDrawerSummary() {
|
||||
const chat = getChatById(conversation.activeChatId);
|
||||
if (!chatCharacterPill || !chatUserPersonaPill) return;
|
||||
@@ -888,7 +919,7 @@ async function checkSession() {
|
||||
attachmentComposer.restoreDraft(null);
|
||||
resizeComposerInput();
|
||||
}
|
||||
await Promise.all([displayModels(), loadSummary(), loadChatSessions(), loadPersonas()]);
|
||||
await Promise.all([displayModels(), loadSummary(), loadChatSessions(), loadPersonalities()]);
|
||||
// Starters are offered on workspace home.
|
||||
const starter = new URLSearchParams(location.search).get("prompt");
|
||||
if (starter && !msgInput.value) { msgInput.value = starter; msgInput.dispatchEvent(new Event("input")); }
|
||||
@@ -985,7 +1016,7 @@ if (headerModelButton) headerModelButton.addEventListener("click", (event) => {
|
||||
openModelSettings();
|
||||
});
|
||||
saveGenerationSettingsBtn.addEventListener("click", () => { void saveGenerationSettings(); });
|
||||
roleplayNewPersonaBtn.addEventListener("click", () => { closeRoleplayStarter(); openPersonaForm(null, "assistant"); });
|
||||
roleplayNewPersonaBtn.addEventListener("click", closeRoleplayStarter);
|
||||
clearUserPersonaBtn.addEventListener("click", () => {
|
||||
closeChatDrawer();
|
||||
void clearChatUserPersona();
|
||||
|
||||
+5
-14
@@ -547,8 +547,8 @@ deleteAccountBtn.addEventListener("click", () => { void deleteAccount(); });
|
||||
refreshDiagnosticsBtn.addEventListener("click", () => { void loadDiagnostics(); });
|
||||
|
||||
settingsNavItems.forEach((item) => item.addEventListener("click", () => setSettingsView(item.dataset.settingsView)));
|
||||
createAiCharacterBtn.addEventListener("click", () => openPersonaModal(null, "assistant"));
|
||||
createUserPersonaBtn.addEventListener("click", () => openPersonaModal(null, "user"));
|
||||
createAiCharacterBtn?.addEventListener("click", () => openPersonaModal(null, "assistant"));
|
||||
createUserPersonaBtn?.addEventListener("click", () => openPersonaModal(null, "user"));
|
||||
settingsPersonaClose.addEventListener("click", closePersonaModal);
|
||||
settingsPersonaModal.addEventListener("click", (event) => {
|
||||
if (event.target === settingsPersonaModal) closePersonaModal();
|
||||
@@ -588,20 +588,11 @@ window.addEventListener("load", async () => {
|
||||
initializeAppearance({ title: "Elsewhere Settings", themeSelect: settingsThemeSelect, modeSelect: themeModeSelect });
|
||||
workspaceModeSelect.value = localStorage.getItem("krishd-workspace-mode") || "basic";
|
||||
const params = new URLSearchParams(window.location.search);
|
||||
const personaPage = location.pathname === "/app/personas";
|
||||
const view = personaPage ? "my-roleplay-companions" : params.get("view") || "personal";
|
||||
if (personaPage) { document.querySelector(".market-header .eyebrow").textContent = "Your collection"; document.querySelector("h1").textContent = "My personas"; document.title = "My personas - Elsewhere"; document.querySelector(".settings-nav").hidden = true; document.querySelector(".settings-layout").style.gridTemplateColumns = "1fr"; }
|
||||
const view = params.get("view") || "personal";
|
||||
const profileOk = await loadProfile();
|
||||
setSettingsView(view);
|
||||
const personasOk = await loadPersonas();
|
||||
if (profileOk && personasOk && view !== "diagnostics") setNotice(personaPage ? "Personas loaded." : "Settings loaded.");
|
||||
if (profileOk && view !== "diagnostics") setNotice("Settings loaded.");
|
||||
|
||||
const create = params.get("create");
|
||||
if (create === "assistant") {
|
||||
setSettingsView("my-roleplay-companions");
|
||||
openPersonaModal(null, "assistant");
|
||||
} else if (create === "user") {
|
||||
setSettingsView("my-roleplay-companions");
|
||||
openPersonaModal(null, "user");
|
||||
}
|
||||
if (create === "assistant" || create === "user") location.replace("/app/personas");
|
||||
});
|
||||
|
||||
@@ -9,7 +9,7 @@ export function createWebSearchToggle(button) {
|
||||
let enabled = true;
|
||||
const render = () => {
|
||||
button.setAttribute("aria-pressed", String(enabled));
|
||||
button.title = enabled ? "Automatic web search on — click to disable" : "Web search off — click to enable";
|
||||
button.title = enabled ? "Automatic web search on - click to disable" : "Web search off - click to enable";
|
||||
button.setAttribute("aria-label", button.title);
|
||||
};
|
||||
button.addEventListener("click", () => {
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
import {download, multipart, post} from "./api.js";
|
||||
|
||||
export function createWorkspaceTransfer({conversation, getChatById, setNotice, promptPopup, confirmPopup, setChatLoading, loadPersonas, loadChatSessions}) {
|
||||
export function createWorkspaceTransfer({conversation, getChatById, setNotice, promptPopup, confirmPopup, setChatLoading, loadChatSessions}) {
|
||||
async function exportCurrentChat() {
|
||||
const chat = getChatById(conversation.activeChatId);
|
||||
if (!chat || !conversation.currentMessages.length) return;
|
||||
@@ -78,7 +78,7 @@ export function createWorkspaceTransfer({conversation, getChatById, setNotice, p
|
||||
setChatLoading(true, "Importing workspace and images...");
|
||||
const result = await submit("/imports/workspace/archive", Boolean(preview.preview?.duplicates?.length));
|
||||
if (result.error) return setNotice(result.error, "error");
|
||||
await Promise.all([loadPersonas(), loadChatSessions()]);
|
||||
await loadChatSessions();
|
||||
setNotice(`Workspace imported: ${result.imported.chats} chats with their images.`, "success");
|
||||
} catch { setNotice("Workspace archive could not be imported.", "error"); }
|
||||
finally { setChatLoading(false); }
|
||||
@@ -118,7 +118,7 @@ export function createWorkspaceTransfer({conversation, getChatById, setNotice, p
|
||||
const detail = (imported.errors || imported.warnings || []).slice(0, 4).join("; ");
|
||||
return setNotice(detail || imported.error, "error");
|
||||
}
|
||||
await Promise.all([loadPersonas(), loadChatSessions()]);
|
||||
await loadChatSessions();
|
||||
setChatLoading(false);
|
||||
setNotice(`Workspace imported: ${imported.imported.personas} personas, ${imported.imported.chats} chats, ${imported.imported.templates} templates.`, "success");
|
||||
}
|
||||
|
||||
+2
-3
@@ -22,11 +22,10 @@
|
||||
<div class="entry-copy">
|
||||
<p class="eyebrow">A space for every conversation.</p>
|
||||
<h1>Take a thought<br>somewhere.</h1>
|
||||
<p>Talk it through. Make something up.<br>Meet a character and see where it goes.</p>
|
||||
<p>Talk it through. Make something up.<br>Choose an assistant for the work ahead.</p>
|
||||
<div class="site-actions">
|
||||
<a data-start-link class="primary-action" href="/register">Step inside <span
|
||||
aria-hidden="true">→</span></a>
|
||||
<a href="/market">Meet the personas</a>
|
||||
</div>
|
||||
</div>
|
||||
</main>
|
||||
@@ -36,4 +35,4 @@
|
||||
<script type="module" src="/js/workspaceNavigation.js?v=20260909-account-controls"></script>
|
||||
</body>
|
||||
|
||||
</html>
|
||||
</html>
|
||||
|
||||
@@ -1,262 +0,0 @@
|
||||
<!DOCTYPE html>
|
||||
<html lang="en">
|
||||
|
||||
<head>
|
||||
<meta charset="UTF-8" />
|
||||
<link rel="icon" type="image/svg+xml" href="/img/favicon.svg">
|
||||
<title>Elsewhere Discover</title>
|
||||
<link rel="preconnect" href="https://fonts.googleapis.com">
|
||||
<link rel="preconnect" href="https://fonts.gstatic.com" crossorigin>
|
||||
<link
|
||||
href="https://fonts.googleapis.com/css2?family=Space+Grotesk:wght@400;500;700&family=Manrope:wght@400;500;600;700;800&display=swap"
|
||||
rel="stylesheet">
|
||||
<script src="/js/theme-bootstrap.js?v=20260909-account-controls"></script>
|
||||
<link rel="stylesheet" href="/css/style.css?v=20260909-account-controls" />
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1.0">
|
||||
<link rel="stylesheet" href="/css/shell.css?v=20260909-account-controls">
|
||||
</head>
|
||||
|
||||
<body class="site-page market-page" data-theme="fakegpt" data-theme-mode="light">
|
||||
<!-- site-header -->
|
||||
<main id="main" class="market-shell page-content">
|
||||
<header class="market-header market-header-simple">
|
||||
<div>
|
||||
<p class="eyebrow">Shared personas</p>
|
||||
<h1>Discover</h1>
|
||||
</div>
|
||||
<div class="market-header-actions"><button id="refreshMarket" class="secondary-action"
|
||||
type="button">Refresh</button></div>
|
||||
</header>
|
||||
|
||||
<div class="market-search">
|
||||
<label for="marketSearch">Search</label>
|
||||
<input id="marketSearch" type="search" placeholder="Search by name, creator, or description" />
|
||||
</div>
|
||||
|
||||
<p id="marketStatus" class="status persona-status">Loading market personas...</p>
|
||||
|
||||
<section class="settings-layout market-settings-layout">
|
||||
<aside class="settings-nav" aria-label="Market sections" role="tablist">
|
||||
<button class="settings-nav-item active" type="button" role="tab" aria-selected="true"
|
||||
data-market-view="ai-characters">Characters</button>
|
||||
<button class="settings-nav-item" type="button" role="tab" aria-selected="false"
|
||||
data-market-view="user-personas">User personas</button>
|
||||
</aside>
|
||||
|
||||
<div class="settings-content">
|
||||
<section class="settings-view active" role="tabpanel" data-market-panel="ai-characters">
|
||||
<div class="settings-panel">
|
||||
<div class="settings-section-header">
|
||||
<div>
|
||||
<p class="eyebrow">Discover</p>
|
||||
<h2>AI Characters</h2>
|
||||
</div>
|
||||
<div class="market-section-actions">
|
||||
<label class="market-sort-control" for="marketAssistantSort">
|
||||
<span>Sort</span>
|
||||
<select id="marketAssistantSort">
|
||||
<option value="best">Best</option>
|
||||
<option value="newest">Newest</option>
|
||||
<option value="most_favorited">Most Favorited</option>
|
||||
<option value="most_popular">Most Popular</option>
|
||||
<option value="top_rated">Top Rated</option>
|
||||
<option value="alphabetical">A-Z</option>
|
||||
</select>
|
||||
</label>
|
||||
<button id="marketCreateAiCharacter" type="button" class="secondary-action">Create AI
|
||||
Character</button>
|
||||
</div>
|
||||
</div>
|
||||
<div id="marketPersonaList" class="persona-list"></div>
|
||||
</div>
|
||||
</section>
|
||||
|
||||
<section class="settings-view" role="tabpanel" aria-hidden="true" data-market-panel="user-personas">
|
||||
<div class="settings-panel">
|
||||
<div class="settings-section-header">
|
||||
<div>
|
||||
<p class="eyebrow">Discover</p>
|
||||
<h2>User Personas</h2>
|
||||
</div>
|
||||
<div class="market-section-actions">
|
||||
<label class="market-sort-control" for="marketUserSort">
|
||||
<span>Sort</span>
|
||||
<select id="marketUserSort">
|
||||
<option value="best">Best</option>
|
||||
<option value="newest">Newest</option>
|
||||
<option value="most_favorited">Most Favorited</option>
|
||||
<option value="most_popular">Most Popular</option>
|
||||
<option value="top_rated">Top Rated</option>
|
||||
<option value="alphabetical">A-Z</option>
|
||||
</select>
|
||||
</label>
|
||||
<button id="marketCreateUserPersona" type="button" class="secondary-action">Create User
|
||||
Persona</button>
|
||||
</div>
|
||||
</div>
|
||||
<div id="marketUserPersonaList" class="persona-list"></div>
|
||||
</div>
|
||||
</section>
|
||||
</div>
|
||||
</section>
|
||||
</main>
|
||||
|
||||
<div id="marketPreviewModal" class="modal-overlay hidden">
|
||||
<div class="modal-card market-preview-card" role="dialog" aria-modal="true" aria-labelledby="marketPreviewTitle" tabindex="-1">
|
||||
<div class="modal-header">
|
||||
<div>
|
||||
<p class="eyebrow">Persona preview</p>
|
||||
<h3 id="marketPreviewTitle">Persona details</h3>
|
||||
<h4 id="marketPreviewName" class="market-preview-name">Persona details</h4>
|
||||
<p id="marketPreviewIntro" class="subtitle">Review this persona and choose how you want to use it.
|
||||
</p>
|
||||
</div>
|
||||
<button id="marketPreviewClose" type="button" class="icon-button">Close</button>
|
||||
</div>
|
||||
<div id="marketPreviewBody" 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>
|
||||
<h4>Example dialogues</h4>
|
||||
<p id="marketPreviewExampleDialogues"></p>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<div class="market-preview-actions">
|
||||
<div id="marketPreviewStepOne" class="roleplay-starter-page hidden">
|
||||
<label id="marketPreviewUserPersonaField" class="field market-preview-user-field hidden">
|
||||
<span>You in this roleplay</span>
|
||||
<select id="marketPreviewUserPersonaSelect"></select>
|
||||
</label>
|
||||
</div>
|
||||
<div id="marketPreviewStepTwo" class="roleplay-starter-page hidden">
|
||||
<label id="marketPreviewScenarioField" class="field market-preview-scenario-field">
|
||||
<span>Scenario (optional)</span>
|
||||
<textarea id="marketPreviewScenario" rows="4"
|
||||
placeholder="Describe the opening scene you want. Leave it empty and the AI Character will invent its own scenario."></textarea>
|
||||
</label>
|
||||
<p class="subtitle">Leave this empty if you want the AI Character to invent the scenario on its own.
|
||||
No character limit.</p>
|
||||
</div>
|
||||
<div class="market-preview-footer">
|
||||
<button id="marketPreviewConfirm" type="button" class="primary-action">Take persona</button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div id="marketPersonaModal" class="modal-overlay hidden">
|
||||
<div class="modal-card persona-modal-card" role="dialog" aria-modal="true" aria-labelledby="marketPersonaFormTitle" tabindex="-1">
|
||||
<div class="modal-header">
|
||||
<div class="persona-modal-heading">
|
||||
<p class="eyebrow">AI Character editor</p>
|
||||
<h3 id="marketPersonaFormTitle">Create AI Character</h3>
|
||||
</div>
|
||||
<button id="marketPersonaClose" type="button" class="icon-button">Close</button>
|
||||
</div>
|
||||
<form id="marketPersonaForm" class="persona-form">
|
||||
<div class="persona-form-intro">
|
||||
<p class="persona-helper">Create or update your AI Characters for roleplay chats and marketplace
|
||||
publishing.</p>
|
||||
</div>
|
||||
<p id="marketPersonaFormNotice" class="status hidden"></p>
|
||||
<div class="persona-form-grid">
|
||||
<label class="field">
|
||||
<span>Character name</span>
|
||||
<input id="marketPersonaName" placeholder="Character name" required />
|
||||
</label>
|
||||
<label class="field">
|
||||
<span>Pronouns</span>
|
||||
<input id="marketPersonaPronouns" placeholder="e.g. she/her, they/them" />
|
||||
</label>
|
||||
<label class="field field-wide">
|
||||
<span>Appearance</span>
|
||||
<textarea id="marketPersonaAppearance" rows="3" placeholder="Describe their look"></textarea>
|
||||
</label>
|
||||
<label class="field field-wide">
|
||||
<span>Background</span>
|
||||
<textarea id="marketPersonaBackground" rows="3"
|
||||
placeholder="Backstory, history, role, or origins"></textarea>
|
||||
</label>
|
||||
<label class="field field-wide">
|
||||
<span>Details</span>
|
||||
<textarea id="marketPersonaDetails" rows="4"
|
||||
placeholder="Personality traits, voice, goals, quirks, boundaries, style"></textarea>
|
||||
</label>
|
||||
<label class="field field-wide">
|
||||
<span>Example dialogues</span>
|
||||
<div class="dialogue-format-card" aria-hidden="true">
|
||||
<p class="dialogue-format-label">Format</p>
|
||||
<pre class="dialogue-format-example">{{character}}: Hello, my name is marc
|
||||
{{user}}: Hi, Marc!
|
||||
{{character}}: Nice to meet you</pre>
|
||||
</div>
|
||||
<textarea id="marketPersonaExampleDialogues" rows="5"
|
||||
placeholder="Write example exchanges that define the character's tone, phrasing, pacing, and personality."></textarea>
|
||||
</label>
|
||||
</div>
|
||||
<div class="persona-actions">
|
||||
<button id="marketPersonaSave" type="submit" class="primary-action">Save AI Character</button>
|
||||
</div>
|
||||
</form>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div id="marketActivityOverlay" class="chat-activity-overlay hidden" role="status" aria-live="polite"
|
||||
aria-atomic="true">
|
||||
<div class="chat-activity-card">
|
||||
<div class="chat-activity-mark" aria-hidden="true">
|
||||
<span></span>
|
||||
<span></span>
|
||||
<span></span>
|
||||
</div>
|
||||
<p id="marketActivityEyebrow" class="eyebrow">Please wait</p>
|
||||
<h3 id="marketActivityTitle">Opening roleplay</h3>
|
||||
<p id="marketActivityDetail" class="subtitle">The AI character is preparing the first message.</p>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div id="marketPopupModal" class="modal-overlay hidden">
|
||||
<div class="modal-card popup-card" role="dialog" aria-modal="true" aria-labelledby="marketPopupTitle"
|
||||
aria-describedby="marketPopupDescription">
|
||||
<div class="modal-header">
|
||||
<div class="persona-modal-heading">
|
||||
<p id="marketPopupEyebrow" class="eyebrow">Action</p>
|
||||
<h3 id="marketPopupTitle">Confirm action</h3>
|
||||
</div>
|
||||
<button id="marketPopupClose" type="button" class="icon-button">Close</button>
|
||||
</div>
|
||||
<div class="popup-content">
|
||||
<p id="marketPopupDescription" class="subtitle">Please confirm this action.</p>
|
||||
<label id="marketPopupField" class="field">
|
||||
<span id="marketPopupInputLabel">Value</span>
|
||||
<input id="marketPopupInput" type="text" autocomplete="off" />
|
||||
</label>
|
||||
</div>
|
||||
<div class="popup-actions">
|
||||
<button id="marketPopupCancel" type="button" class="secondary-action">Cancel</button>
|
||||
<button id="marketPopupConfirm" type="button">Confirm</button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<script type="module" src="/js/market.js?v=20260923-asset-paths"></script>
|
||||
<script type="module" src="/js/workspaceNavigation.js?v=20260909-account-controls"></script>
|
||||
</body>
|
||||
|
||||
</html>
|
||||
@@ -0,0 +1,3 @@
|
||||
<!DOCTYPE html>
|
||||
<html lang="en"><head><meta charset="UTF-8"><meta name="viewport" content="width=device-width, initial-scale=1"><link rel="icon" href="/img/favicon.svg"><title>Personalities - Elsewhere</title><link rel="preconnect" href="https://fonts.googleapis.com"><link rel="preconnect" href="https://fonts.gstatic.com" crossorigin><link href="https://fonts.googleapis.com/css2?family=Space+Grotesk:wght@400;500;700&family=Manrope:wght@400;500;600;700;800&display=swap" rel="stylesheet"><script src="/js/theme-bootstrap.js"></script><link rel="stylesheet" href="/css/style.css"><link rel="stylesheet" href="/css/pages.css"><link rel="stylesheet" href="/css/shell.css"></head>
|
||||
<body class="site-page" data-theme="fakegpt" data-theme-mode="light"><!-- site-header --><main id="main" class="page-shell"><section class="market-header"><p class="eyebrow">Choose your approach</p><h1>Personalities</h1><p class="subtitle">Specialized assistants for different kinds of work. Choose one to start a conversation.</p></section><section id="personalityGrid" class="personality-grid" aria-live="polite"></section></main><script type="module" src="/js/personalities.js"></script></body></html>
|
||||
+4
-12
@@ -126,20 +126,12 @@
|
||||
<div class="settings-panel">
|
||||
<div class="settings-section-header">
|
||||
<div>
|
||||
<p class="eyebrow">Create & manage</p>
|
||||
<h2>My personas</h2>
|
||||
</div>
|
||||
<div class="market-header-actions action-pair">
|
||||
<button id="createAiCharacter" type="button" class="secondary-action">Create AI
|
||||
Character</button>
|
||||
<button id="createUserPersona" type="button" class="secondary-action">Create User
|
||||
Persona</button>
|
||||
<p class="eyebrow">Curated assistants</p>
|
||||
<h2>Personalities</h2>
|
||||
<p class="subtitle">Choose a focused assistant for writing, coding, research, learning, or planning.</p>
|
||||
</div>
|
||||
<a class="secondary-action" href="/app/personas">Browse Personalities</a>
|
||||
</div>
|
||||
<h3>AI Characters</h3>
|
||||
<div id="settingsAiCharacterList" class="persona-list"></div>
|
||||
<h3>User Personas</h3>
|
||||
<div id="settingsUserPersonaList" class="persona-list"></div>
|
||||
</div>
|
||||
</section>
|
||||
|
||||
|
||||
Reference in New Issue
Block a user