Remove unused persona marketplace and discover page files.
CI / check (22) (push) Failing after 16m32s
CI / check (24) (push) Failing after 15m50s

This commit is contained in:
2026-10-07 23:41:27 +02:00
parent 10ec76d437
commit 5ca611c113
34 changed files with 368 additions and 1216 deletions
+2 -38
View File
@@ -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">
+12
View File
@@ -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; } }
+4 -1
View File
@@ -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; }
-43
View File
@@ -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 &middot; 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&hellip;</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
View File
@@ -30,17 +30,17 @@
<p>Make space for a question or an idea.</p>
<span>Start chatting &rarr;</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 &rarr;</span>
<h2>Choose a personality</h2>
<p>Pick a specialized assistant for writing, coding, learning, research, or planning.</p>
<span>Explore personalities &rarr;</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 &rarr;</span>
<h2>Start writing</h2>
<p>Open a conversation with a focused creative-writing assistant.</p>
<span>Write something &rarr;</span>
</a>
</div>
<section class="site-section">
+2 -1
View File
@@ -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.
+3 -2
View File
@@ -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");
-698
View File
@@ -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();
}
});
+3 -12
View File
@@ -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?";
-27
View File
@@ -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);
}
+45
View File
@@ -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
View File
@@ -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
View File
@@ -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");
});
+1 -1
View File
@@ -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", () => {
+3 -3
View File
@@ -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
View File
@@ -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">&rarr;</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>
-262
View File
@@ -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>
+3
View File
@@ -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
View File
@@ -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>