implement an entire base refacotr and alot of styling overhauls and remakes.
This commit is contained in:
@@ -0,0 +1,52 @@
|
||||
export async function request(url, data, method = "POST") {
|
||||
try {
|
||||
const res = await fetch(url, {
|
||||
method,
|
||||
headers: {"Content-Type": "application/json"},
|
||||
body: data ? JSON.stringify(data) : undefined
|
||||
});
|
||||
return await res.json();
|
||||
} catch {
|
||||
return {error: "Network error - please try again."};
|
||||
}
|
||||
}
|
||||
|
||||
export const get = (url) => request(url, null, "GET");
|
||||
export const post = (url, data) => request(url, data, "POST");
|
||||
export const put = (url, data) => request(url, data, "PUT");
|
||||
export const del = (url) => request(url, null, "DELETE");
|
||||
|
||||
export async function stream(url, data, handlers = {}) {
|
||||
const response = await fetch(url, {
|
||||
method: "POST",
|
||||
headers: {"Content-Type": "application/json"},
|
||||
body: JSON.stringify(data || {})
|
||||
});
|
||||
|
||||
if (!response.ok || !response.body) {
|
||||
throw new Error("Streaming request failed");
|
||||
}
|
||||
|
||||
const reader = response.body.getReader();
|
||||
const decoder = new TextDecoder();
|
||||
let buffer = "";
|
||||
|
||||
while (true) {
|
||||
const {value, done} = await reader.read();
|
||||
if (done) break;
|
||||
buffer += decoder.decode(value, {stream: true});
|
||||
const parts = buffer.split("\n\n");
|
||||
buffer = parts.pop() || "";
|
||||
for (const part of parts) {
|
||||
const lines = part.split("\n");
|
||||
const eventLine = lines.find((line) => line.startsWith("event:"));
|
||||
const dataLine = lines.find((line) => line.startsWith("data:"));
|
||||
if (!eventLine || !dataLine) continue;
|
||||
const event = eventLine.slice(6).trim();
|
||||
const payload = JSON.parse(dataLine.slice(5).trim());
|
||||
if (event === "chunk" && handlers.onChunk) handlers.onChunk(payload);
|
||||
if (event === "done" && handlers.onDone) handlers.onDone(payload);
|
||||
if (event === "error" && handlers.onError) handlers.onError(payload);
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,21 @@
|
||||
export const requestedChatId = Number(new URLSearchParams(window.location.search).get("chat")) || null;
|
||||
|
||||
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."]
|
||||
};
|
||||
|
||||
export const themes = {
|
||||
"fakegpt": {name: "FakeGPT", short: "FG"},
|
||||
"fraud": {name: "Fraud", short: "FR"},
|
||||
"germini": {name: "Germini", short: "GE"},
|
||||
"slopilot": {name: "Slopilot", short: "SP"},
|
||||
"beta-ai": {name: "Beta AI", short: "BA"},
|
||||
"confusity": {name: "Confusity", short: "CF"}
|
||||
};
|
||||
|
||||
export const defaultModelProfile = {
|
||||
badge: "General",
|
||||
summary: "General-purpose chat model. Start here if you are unsure which model to use."
|
||||
};
|
||||
@@ -0,0 +1,226 @@
|
||||
const $ = (id) => document.getElementById(id);
|
||||
|
||||
const authDiv = $("auth");
|
||||
const chatDiv = $("chat");
|
||||
const authMsg = $("authMsg");
|
||||
const logoutButton = $("logout");
|
||||
const messagesDiv = $("messages");
|
||||
const modelSelect = $("model");
|
||||
const modelMenuButton = $("modelMenuButton");
|
||||
const modelPopover = $("modelPopover");
|
||||
const modelBadgeName = $("modelBadgeName");
|
||||
const modelCount = $("modelCount");
|
||||
const modelHelpTitle = $("modelHelpTitle");
|
||||
const modelHelpBadge = $("modelHelpBadge");
|
||||
const modelHelpSummary = $("modelHelpSummary");
|
||||
const loginForm = $("loginForm");
|
||||
const registerForm = $("registerForm");
|
||||
const msgInput = $("msgInput");
|
||||
const sendBtn = $("send");
|
||||
const loginUsernameInput = $("loginUsername");
|
||||
const loginPasswordInput = $("loginPassword");
|
||||
const registerUsernameInput = $("registerUsername");
|
||||
const registerPasswordInput = $("registerPassword");
|
||||
const loginSubmit = $("loginSubmit");
|
||||
const registerSubmit = $("registerSubmit");
|
||||
const chatList = $("chatList");
|
||||
const chatSearchInput = $("chatSearch");
|
||||
const chatSidebar = $("chatSidebar");
|
||||
const chatDrawerCloseBtn = $("chatDrawerClose");
|
||||
const newChatBtn = $("newChat");
|
||||
const chatListLoading = $("chatListLoading");
|
||||
const chatListLoadingText = $("chatListLoadingText");
|
||||
const renameChatBtn = $("renameChat");
|
||||
const clearChatBtn = $("clearChat");
|
||||
const exportChatBtn = $("exportChat");
|
||||
const pinChatBtn = $("pinChat");
|
||||
const moveChatFolderBtn = $("moveChatFolder");
|
||||
const archiveChatBtn = $("archiveChat");
|
||||
const backupWorkspaceBtn = $("backupWorkspace");
|
||||
const activeChatTitle = $("activeChatTitle");
|
||||
const sessionUser = $("sessionUser");
|
||||
const chatActivityOverlay = $("chatActivityOverlay");
|
||||
const chatActivityEyebrow = $("chatActivityEyebrow");
|
||||
const chatActivityTitle = $("chatActivityTitle");
|
||||
const chatActivityDetail = $("chatActivityDetail");
|
||||
const contextChipButton = $("contextChipButton");
|
||||
const contextPopover = $("contextPopover");
|
||||
const contextSummaryLabel = $("contextSummaryLabel");
|
||||
const chatActionsMenuButton = $("chatActionsMenuButton");
|
||||
const chatActionsPopover = $("chatActionsPopover");
|
||||
const chatModePill = $("chatModePill");
|
||||
const chatCharacterPill = $("chatCharacterPill");
|
||||
const chatUserPersonaPill = $("chatUserPersonaPill");
|
||||
const personaList = $("personaList");
|
||||
const userPersonaList = $("userPersonaList");
|
||||
const personaForm = $("personaForm");
|
||||
const personaFormTitle = $("personaFormTitle");
|
||||
const personaTypeSelect = $("personaType");
|
||||
const personaNameInput = $("personaName");
|
||||
const personaPronounsInput = $("personaPronouns");
|
||||
const personaAppearanceInput = $("personaAppearance");
|
||||
const personaBackgroundInput = $("personaBackground");
|
||||
const personaDetailsInput = $("personaDetails");
|
||||
const personaExamplesField = $("personaExamplesField");
|
||||
const personaExampleDialoguesInput = $("personaExampleDialogues");
|
||||
const personaFormNotice = $("personaFormNotice");
|
||||
const roleplayNewPersonaBtn = $("roleplayNewPersona");
|
||||
const clearUserPersonaBtn = $("clearUserPersona");
|
||||
const activePersonaStatus = $("activePersonaStatus");
|
||||
const activeUserPersonaStatus = $("activeUserPersonaStatus");
|
||||
const personaModal = $("personaModal");
|
||||
const personaCloseBtn = $("personaClose");
|
||||
const personaMenuButton = $("personaMenuButton");
|
||||
const personaPopover = $("personaPopover");
|
||||
const roleplayStarterModal = $("roleplayStarterModal");
|
||||
const roleplayStarterClose = $("roleplayStarterClose");
|
||||
const roleplayStarterCancel = $("roleplayStarterCancel");
|
||||
const roleplayStarterConfirm = $("roleplayStarterConfirm");
|
||||
const roleplayStarterNotice = $("roleplayStarterNotice");
|
||||
const roleplayCharacterSelect = $("roleplayCharacterSelect");
|
||||
const roleplayUserPersonaSelect = $("roleplayUserPersonaSelect");
|
||||
const roleplayStarterTitle = $("roleplayStarterTitle");
|
||||
const roleplayStarterIntroCopy = $("roleplayStarterIntroCopy");
|
||||
const roleplayStarterBack = $("roleplayStarterBack");
|
||||
const roleplayStarterStepOne = $("roleplayStarterStepOne");
|
||||
const roleplayStarterStepTwo = $("roleplayStarterStepTwo");
|
||||
const roleplayScenarioInput = $("roleplayScenarioInput");
|
||||
const roleplayStarterSuggestions = $("roleplayStarterSuggestions");
|
||||
const popupModal = $("popupModal");
|
||||
const popupCloseBtn = $("popupClose");
|
||||
const popupCancelBtn = $("popupCancel");
|
||||
const popupConfirmBtn = $("popupConfirm");
|
||||
const popupTitle = $("popupTitle");
|
||||
const popupEyebrow = $("popupEyebrow");
|
||||
const popupDescription = $("popupDescription");
|
||||
const popupField = $("popupField");
|
||||
const popupInputLabel = $("popupInputLabel");
|
||||
const popupInput = $("popupInput");
|
||||
const appNotice = $("appNotice");
|
||||
const onboardingModal = $("onboardingModal");
|
||||
const onboardingTitle = $("onboardingTitle");
|
||||
const onboardingSubtitle = $("onboardingSubtitle");
|
||||
const onboardingChoices = $("onboardingChoices");
|
||||
const onboardingBack = $("onboardingBack");
|
||||
const onboardingContinue = $("onboardingContinue");
|
||||
const onboardingSkip = $("onboardingSkip");
|
||||
const authScreens = document.querySelectorAll(".auth-screen");
|
||||
const toggleButtons = document.querySelectorAll(".auth-toggle .toggle");
|
||||
const themeNameTargets = document.querySelectorAll("[data-theme-name]");
|
||||
const themeLogoTargets = document.querySelectorAll("[data-theme-logo]");
|
||||
const modelMenu = document.querySelector(".model-menu");
|
||||
const personaMenu = document.querySelector(".persona-menu");
|
||||
|
||||
export {
|
||||
$,
|
||||
activeChatTitle,
|
||||
activePersonaStatus,
|
||||
activeUserPersonaStatus,
|
||||
appNotice,
|
||||
authDiv,
|
||||
authMsg,
|
||||
authScreens,
|
||||
chatActionsMenuButton,
|
||||
chatActionsPopover,
|
||||
chatActivityDetail,
|
||||
chatActivityEyebrow,
|
||||
chatActivityOverlay,
|
||||
chatActivityTitle,
|
||||
chatCharacterPill,
|
||||
chatDiv,
|
||||
chatList,
|
||||
chatListLoading,
|
||||
chatListLoadingText,
|
||||
chatSidebar,
|
||||
chatDrawerCloseBtn,
|
||||
chatModePill,
|
||||
chatSearchInput,
|
||||
chatUserPersonaPill,
|
||||
clearChatBtn,
|
||||
clearUserPersonaBtn,
|
||||
contextChipButton,
|
||||
contextPopover,
|
||||
contextSummaryLabel,
|
||||
exportChatBtn,
|
||||
pinChatBtn,
|
||||
moveChatFolderBtn,
|
||||
archiveChatBtn,
|
||||
backupWorkspaceBtn,
|
||||
loginForm,
|
||||
loginPasswordInput,
|
||||
loginSubmit,
|
||||
loginUsernameInput,
|
||||
logoutButton,
|
||||
messagesDiv,
|
||||
modelBadgeName,
|
||||
modelCount,
|
||||
modelHelpBadge,
|
||||
modelHelpSummary,
|
||||
modelHelpTitle,
|
||||
modelMenu,
|
||||
modelMenuButton,
|
||||
modelPopover,
|
||||
modelSelect,
|
||||
msgInput,
|
||||
newChatBtn,
|
||||
onboardingBack,
|
||||
onboardingChoices,
|
||||
onboardingContinue,
|
||||
onboardingModal,
|
||||
onboardingSkip,
|
||||
onboardingSubtitle,
|
||||
onboardingTitle,
|
||||
personaCloseBtn,
|
||||
personaDetailsInput,
|
||||
personaExampleDialoguesInput,
|
||||
personaExamplesField,
|
||||
personaForm,
|
||||
personaFormNotice,
|
||||
personaFormTitle,
|
||||
personaList,
|
||||
personaMenu,
|
||||
personaMenuButton,
|
||||
personaModal,
|
||||
personaNameInput,
|
||||
personaPopover,
|
||||
personaPronounsInput,
|
||||
personaTypeSelect,
|
||||
personaAppearanceInput,
|
||||
personaBackgroundInput,
|
||||
popupCancelBtn,
|
||||
popupCloseBtn,
|
||||
popupConfirmBtn,
|
||||
popupDescription,
|
||||
popupEyebrow,
|
||||
popupField,
|
||||
popupInput,
|
||||
popupInputLabel,
|
||||
popupModal,
|
||||
popupTitle,
|
||||
registerForm,
|
||||
registerPasswordInput,
|
||||
registerSubmit,
|
||||
registerUsernameInput,
|
||||
renameChatBtn,
|
||||
roleplayCharacterSelect,
|
||||
roleplayNewPersonaBtn,
|
||||
roleplayScenarioInput,
|
||||
roleplayStarterBack,
|
||||
roleplayStarterCancel,
|
||||
roleplayStarterClose,
|
||||
roleplayStarterConfirm,
|
||||
roleplayStarterIntroCopy,
|
||||
roleplayStarterModal,
|
||||
roleplayStarterNotice,
|
||||
roleplayStarterStepOne,
|
||||
roleplayStarterStepTwo,
|
||||
roleplayStarterSuggestions,
|
||||
roleplayStarterTitle,
|
||||
roleplayUserPersonaSelect,
|
||||
sendBtn,
|
||||
sessionUser,
|
||||
themeLogoTargets,
|
||||
themeNameTargets,
|
||||
toggleButtons,
|
||||
userPersonaList
|
||||
};
|
||||
+115
-109
@@ -16,13 +16,13 @@
|
||||
<div class="brand">
|
||||
<div class="logo" data-theme-logo>FG</div>
|
||||
<div>
|
||||
<p class="eyebrow">Private chat</p>
|
||||
<p class="eyebrow">Private workspace</p>
|
||||
<h1 data-theme-name>FakeGPT</h1>
|
||||
</div>
|
||||
</div>
|
||||
<div class="hero-grid">
|
||||
<article class="hero-panel">
|
||||
<p>Sign in to start a chat immediately, then add personas when you want richer roleplay.</p>
|
||||
<p>Private chat and roleplay.</p>
|
||||
</article>
|
||||
</div>
|
||||
</div>
|
||||
@@ -75,44 +75,33 @@
|
||||
|
||||
<section id="chat" class="card chat-card chat-focus-card" style="display:none;">
|
||||
<div class="chat-layout chat-layout-focus">
|
||||
<aside class="chat-sidebar">
|
||||
<aside id="chatSidebar" class="chat-sidebar">
|
||||
<div class="sidebar-top">
|
||||
<div class="sidebar-header">
|
||||
<div class="sidebar-workspace-card">
|
||||
<div>
|
||||
<p class="eyebrow">Chats</p>
|
||||
<h2>Conversations</h2>
|
||||
<p class="eyebrow">Workspace</p>
|
||||
<h2 data-theme-name>FakeGPT</h2>
|
||||
<p id="sessionUser" class="sidebar-account-name">Signed out</p>
|
||||
</div>
|
||||
<div class="sidebar-header-actions action-pair">
|
||||
<button id="newChat" class="secondary-action" type="button">New chat</button>
|
||||
<button id="startRoleplay" class="secondary-action" type="button">Start roleplay</button>
|
||||
<div class="sidebar-header-actions sidebar-workspace-actions">
|
||||
<button id="newChat" class="primary-action" type="button">New chat</button>
|
||||
</div>
|
||||
</div>
|
||||
<label class="field compact-field">
|
||||
<span>Search</span>
|
||||
<input id="chatSearch" type="search" placeholder="Filter chat titles"/>
|
||||
<input id="chatSearch" type="search" placeholder="Search chats"/>
|
||||
</label>
|
||||
</div>
|
||||
<div id="chatList" class="chat-list"></div>
|
||||
<div id="chatListLoading" class="chat-loading hidden" role="status" aria-live="polite">
|
||||
<span class="chat-loading-dot" aria-hidden="true"></span>
|
||||
<span id="chatListLoadingText">Loading chats...</span>
|
||||
<div class="sidebar-middle">
|
||||
<div id="chatList" class="chat-list chat-list-grouped"></div>
|
||||
<div id="chatListLoading" class="chat-loading hidden" role="status" aria-live="polite">
|
||||
<span class="chat-loading-dot" aria-hidden="true"></span>
|
||||
<span id="chatListLoadingText">Loading chats...</span>
|
||||
</div>
|
||||
</div>
|
||||
<div class="sidebar-footer sidebar-footer-stack">
|
||||
<details id="toolsDetails" class="tools-details">
|
||||
<summary>Tools</summary>
|
||||
<div class="tools-links">
|
||||
<a href="/market" class="secondary-action">Browse personas</a>
|
||||
<a href="/settings" class="secondary-action">Settings</a>
|
||||
</div>
|
||||
</details>
|
||||
<details id="accountDetails" class="tools-details">
|
||||
<summary>Account</summary>
|
||||
<div class="session-chip">
|
||||
<span class="session-chip-label">Signed in as</span>
|
||||
<strong id="sessionUser">-</strong>
|
||||
</div>
|
||||
<button id="logout" type="button">Logout</button>
|
||||
</details>
|
||||
<div class="sidebar-footer">
|
||||
<a href="/settings" class="secondary-action">Settings</a>
|
||||
<a href="/market" class="secondary-action">Market</a>
|
||||
</div>
|
||||
</aside>
|
||||
|
||||
@@ -120,90 +109,106 @@
|
||||
<header class="chat-header chat-header-focus">
|
||||
<div class="chat-title-block">
|
||||
<div class="chat-title">
|
||||
<p class="eyebrow">Conversation</p>
|
||||
<h2 id="activeChatTitle">New chat</h2>
|
||||
<div class="chat-context">
|
||||
<button id="contextChipButton" class="context-chip-button" type="button" aria-expanded="false" aria-controls="contextPopover">
|
||||
<span>Context</span>
|
||||
<strong id="contextSummaryLabel">Assistant</strong>
|
||||
</button>
|
||||
<div id="contextPopover" class="persona-popover hidden" role="dialog">
|
||||
<div id="chatParticipants" class="chat-participants">
|
||||
<span id="chatModePill" class="context-pill chat-mode-pill">Assistant mode</span>
|
||||
<span id="chatCharacterPill" class="context-pill hidden">Character: none</span>
|
||||
<span id="chatUserPersonaPill" class="context-pill hidden">User persona: none</span>
|
||||
<span id="chatScenePill" class="context-pill hidden">Scene: none</span>
|
||||
</div>
|
||||
</div>
|
||||
<div id="chatParticipants" class="chat-participants chat-participants-inline">
|
||||
<span id="chatModePill" class="context-pill chat-mode-pill">Assistant mode</span>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<div class="chat-header-actions">
|
||||
<div class="model-menu">
|
||||
<button id="modelMenuButton" class="model-badge-button" type="button" aria-expanded="false" aria-controls="modelPopover">
|
||||
<span class="model-badge-label"></span>
|
||||
<strong id="modelBadgeName">Loading...</strong>
|
||||
</button>
|
||||
<div id="modelPopover" class="model-popover hidden" role="dialog">
|
||||
<div class="model-select">
|
||||
<div class="model-select-header">
|
||||
<label for="model">Model</label>
|
||||
<span id="modelCount" class="model-count">Loading</span>
|
||||
</div>
|
||||
<div class="model-select-control">
|
||||
<select id="model">
|
||||
<option disabled selected>Loading models...</option>
|
||||
</select>
|
||||
<span class="model-select-chevron" aria-hidden="true"></span>
|
||||
</div>
|
||||
</div>
|
||||
<div id="modelHelp" class="model-help" aria-live="polite">
|
||||
<div class="model-help-header">
|
||||
<strong id="modelHelpTitle">Choose a model</strong>
|
||||
<span id="modelHelpBadge" class="model-help-badge">Waiting</span>
|
||||
</div>
|
||||
<p id="modelHelpSummary" class="model-help-summary">Pick a model to see what it is best suited for.</p>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<div class="persona-menu">
|
||||
<button id="personaMenuButton" class="secondary-action" type="button" aria-expanded="false" aria-controls="personaPopover">
|
||||
You
|
||||
</button>
|
||||
<div id="personaPopover" class="persona-popover hidden" role="dialog">
|
||||
<div class="persona-section">
|
||||
<div class="persona-section-header">
|
||||
<div>
|
||||
<p class="eyebrow">User</p>
|
||||
<h2>Your persona</h2>
|
||||
<p class="persona-guidance">Adds context for how the assistant should read you.</p>
|
||||
</div>
|
||||
<div class="persona-section-actions">
|
||||
<button id="clearUserPersona" class="secondary-action" type="button">Clear</button>
|
||||
<a href="/settings" 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>
|
||||
</div>
|
||||
<div class="chat-actions-menu">
|
||||
<button id="chatActionsMenuButton" class="secondary-action" type="button" aria-expanded="false" aria-controls="chatActionsPopover">
|
||||
More
|
||||
<button id="chatActionsMenuButton" class="secondary-action overflow-trigger" type="button" aria-expanded="false" aria-controls="chatActionsPopover">
|
||||
Menu
|
||||
</button>
|
||||
<div id="chatActionsPopover" class="persona-popover hidden" role="dialog">
|
||||
<div class="persona-section">
|
||||
<div class="persona-section-header">
|
||||
<div id="chatActionsPopover" class="chat-drawer hidden" role="dialog" aria-modal="false" aria-labelledby="chatDrawerTitle">
|
||||
<div class="chat-drawer-panel">
|
||||
<div class="chat-drawer-header">
|
||||
<div>
|
||||
<p class="eyebrow">Conversation</p>
|
||||
<h2>Actions</h2>
|
||||
<h2 id="chatDrawerTitle">Controls</h2>
|
||||
</div>
|
||||
<button id="chatDrawerClose" class="icon-button" type="button" aria-label="Close panel">Close</button>
|
||||
</div>
|
||||
<div class="persona-item-actions">
|
||||
<button id="renameChat" class="icon-button" type="button">Rename</button>
|
||||
<button id="clearChat" class="icon-button" type="button">Clear</button>
|
||||
<button id="exportChat" class="icon-button" type="button">Export</button>
|
||||
<div class="chat-drawer-body">
|
||||
<div class="chat-drawer-section chat-drawer-summary">
|
||||
<p class="dropdown-section-label">Current mode</p>
|
||||
<div class="chat-drawer-pills">
|
||||
<span id="chatCharacterPill" class="context-pill hidden">Character: none</span>
|
||||
<span id="chatUserPersonaPill" class="context-pill hidden">User persona: none</span>
|
||||
</div>
|
||||
</div>
|
||||
<div class="chat-drawer-section model-menu">
|
||||
<div class="dropdown-header">
|
||||
<p class="eyebrow">Model</p>
|
||||
<p class="dropdown-subtitle">Choose the engine for this chat.</p>
|
||||
</div>
|
||||
<div id="modelPopover" class="model-popover" data-static-panel="true">
|
||||
<div class="model-select">
|
||||
<div class="model-select-header">
|
||||
<label for="model">Model</label>
|
||||
<span id="modelCount" class="model-count">Loading</span>
|
||||
</div>
|
||||
<div class="model-select-control">
|
||||
<select id="model">
|
||||
<option disabled selected>Loading models...</option>
|
||||
</select>
|
||||
<span class="model-select-chevron" aria-hidden="true"></span>
|
||||
</div>
|
||||
</div>
|
||||
<div id="modelHelp" class="model-help" aria-live="polite">
|
||||
<div class="model-help-header">
|
||||
<strong id="modelHelpTitle">Choose a model</strong>
|
||||
<span id="modelHelpBadge" class="model-help-badge">Waiting</span>
|
||||
</div>
|
||||
<p id="modelHelpSummary" class="model-help-summary">Pick a model to see what it is best suited for.</p>
|
||||
</div>
|
||||
<div class="model-inline-badge">
|
||||
<span class="model-badge-label">Current</span>
|
||||
<strong id="modelBadgeName">Loading...</strong>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<div class="chat-drawer-section persona-menu">
|
||||
<div class="dropdown-header">
|
||||
<p class="eyebrow">Persona</p>
|
||||
<p class="dropdown-subtitle">Set who you are in roleplay chats.</p>
|
||||
</div>
|
||||
<div id="personaPopover" class="persona-popover" 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</button>
|
||||
<a href="/settings" 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>
|
||||
</div>
|
||||
<div class="chat-drawer-section">
|
||||
<p class="dropdown-section-label">Workspace</p>
|
||||
<div class="dropdown-action-grid">
|
||||
<a href="/market" class="secondary-action">Market</a>
|
||||
<a href="/settings" class="secondary-action">Settings</a>
|
||||
<button id="logout" type="button" class="secondary-action">Logout</button>
|
||||
<button id="backupWorkspace" class="secondary-action" type="button">Backup</button>
|
||||
</div>
|
||||
</div>
|
||||
<div class="chat-drawer-section">
|
||||
<p class="dropdown-section-label">Current chat</p>
|
||||
<div class="dropdown-action-grid">
|
||||
<button id="renameChat" class="secondary-action" type="button">Rename</button>
|
||||
<button id="pinChat" class="secondary-action" type="button">Pin</button>
|
||||
<button id="moveChatFolder" class="secondary-action" type="button">Folder</button>
|
||||
<button id="archiveChat" class="secondary-action" type="button">Archive</button>
|
||||
<button id="clearChat" class="secondary-action" type="button">Clear</button>
|
||||
<button id="exportChat" class="secondary-action" type="button">Export</button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
@@ -211,6 +216,8 @@
|
||||
</div>
|
||||
</header>
|
||||
|
||||
<p id="appNotice" class="status inline-status hidden"></p>
|
||||
|
||||
<div id="chatActivityOverlay" 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">
|
||||
@@ -230,7 +237,6 @@
|
||||
<div class="input-meta">
|
||||
<p class="composer-hint">Enter to send · Shift+Enter for new line</p>
|
||||
</div>
|
||||
<div id="quickPrompts" class="quick-prompts" aria-label="Suggested starters"></div>
|
||||
<div class="input-container">
|
||||
<textarea id="msgInput" placeholder="Type your message..."></textarea>
|
||||
<button id="send" type="button" aria-label="Send message"></button>
|
||||
@@ -241,8 +247,8 @@
|
||||
</section>
|
||||
</div>
|
||||
|
||||
<div id="personaModal" class="modal-overlay hidden">
|
||||
<div class="modal-card persona-modal-card">
|
||||
<div id="personaModal" class="modal-overlay side-panel-overlay hidden">
|
||||
<div class="modal-card persona-modal-card side-panel-card">
|
||||
<div class="modal-header">
|
||||
<div class="persona-modal-heading">
|
||||
<p class="eyebrow">Persona editor</p>
|
||||
@@ -301,8 +307,8 @@
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div id="roleplayStarterModal" class="modal-overlay hidden">
|
||||
<div class="modal-card roleplay-starter-card">
|
||||
<div id="roleplayStarterModal" class="modal-overlay side-panel-overlay hidden">
|
||||
<div class="modal-card roleplay-starter-card side-panel-card">
|
||||
<div class="modal-header">
|
||||
<div class="persona-modal-heading">
|
||||
<p class="eyebrow">Start roleplay</p>
|
||||
@@ -386,6 +392,6 @@
|
||||
|
||||
<script src="https://cdn.jsdelivr.net/npm/marked/marked.min.js" defer></script>
|
||||
<script src="https://cdn.jsdelivr.net/npm/dompurify@3.0.6/dist/purify.min.js" defer></script>
|
||||
<script src="script.js" defer></script>
|
||||
<script type="module" src="script.js"></script>
|
||||
</body>
|
||||
</html>
|
||||
|
||||
+54
-8
@@ -15,7 +15,6 @@
|
||||
<div>
|
||||
<p class="eyebrow">Shared personas</p>
|
||||
<h1><span data-theme-name>FakeGPT</span> Market</h1>
|
||||
<p class="subtitle">Find characters and personas for roleplay, then start with an explicit persona choice.</p>
|
||||
</div>
|
||||
<div class="market-header-actions">
|
||||
<button id="refreshMarket" class="secondary-action" type="button">Refresh</button>
|
||||
@@ -25,7 +24,7 @@
|
||||
</header>
|
||||
|
||||
<div class="market-search">
|
||||
<label for="marketSearch">Search personas</label>
|
||||
<label for="marketSearch">Search</label>
|
||||
<input id="marketSearch" type="search" placeholder="Search by name, creator, or description"/>
|
||||
</div>
|
||||
|
||||
@@ -33,8 +32,8 @@
|
||||
|
||||
<section class="settings-layout market-settings-layout">
|
||||
<aside class="settings-nav" aria-label="Market sections">
|
||||
<button class="settings-nav-item active" type="button" data-market-view="ai-characters">AI Characters</button>
|
||||
<button class="settings-nav-item" type="button" data-market-view="user-personas">User Personas</button>
|
||||
<button class="settings-nav-item active" type="button" data-market-view="ai-characters">Characters</button>
|
||||
<button class="settings-nav-item" type="button" data-market-view="user-personas">User personas</button>
|
||||
</aside>
|
||||
|
||||
<div class="settings-content">
|
||||
@@ -44,9 +43,21 @@
|
||||
<div>
|
||||
<p class="eyebrow">Discover</p>
|
||||
<h2>AI Characters</h2>
|
||||
<p class="subtitle">Pick a character, choose your persona, and start roleplay.</p>
|
||||
</div>
|
||||
<button id="marketCreateAiCharacter" type="button" class="secondary-action">Create AI Character</button>
|
||||
<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>
|
||||
@@ -58,9 +69,21 @@
|
||||
<div>
|
||||
<p class="eyebrow">Discover</p>
|
||||
<h2>User Personas</h2>
|
||||
<p class="subtitle">Collect personas you can use as your identity in roleplay scenes.</p>
|
||||
</div>
|
||||
<button id="marketCreateUserPersona" type="button" class="secondary-action">Create User Persona</button>
|
||||
<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>
|
||||
@@ -191,6 +214,29 @@
|
||||
<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 src="market.js" defer></script>
|
||||
</body>
|
||||
</html>
|
||||
|
||||
+203
-20
@@ -4,6 +4,8 @@ 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");
|
||||
@@ -42,6 +44,16 @@ 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 themeNameTargets = document.querySelectorAll("[data-theme-name]");
|
||||
|
||||
const themes = {
|
||||
@@ -53,15 +65,18 @@ const themes = {
|
||||
"confusity": {name: "Confusity"}
|
||||
};
|
||||
|
||||
let allMarketPersonas = [];
|
||||
const validMarketSorts = new Set(["best", "newest", "most_favorited", "most_popular", "top_rated", "alphabetical"]);
|
||||
let marketPersonasByType = {assistant: [], user: []};
|
||||
let assistantPersonas = [];
|
||||
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;
|
||||
|
||||
async function request(url, data, method = "POST") {
|
||||
try {
|
||||
@@ -94,6 +109,40 @@ function setMarketStatus(message, state = "") {
|
||||
marketStatus.className = state ? `status persona-status ${state}` : "status persona-status";
|
||||
}
|
||||
|
||||
function closeMarketPopup(value = null) {
|
||||
marketPopupModal.classList.add("hidden");
|
||||
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");
|
||||
marketPopupModal.classList.remove("hidden");
|
||||
marketPopupInput.focus();
|
||||
marketPopupInput.select();
|
||||
return new Promise((resolve) => {
|
||||
marketPopupResolver = resolve;
|
||||
});
|
||||
}
|
||||
|
||||
function setPersonaFormNotice(message = "", state = "") {
|
||||
if (!message) {
|
||||
marketPersonaFormNotice.textContent = "";
|
||||
@@ -132,12 +181,35 @@ function setMarketView(view) {
|
||||
activeMarketView = normalizedView;
|
||||
marketViewButtons.forEach((item) => item.classList.toggle("active", item.dataset.marketView === normalizedView));
|
||||
marketPanels.forEach((panel) => panel.classList.toggle("active", panel.dataset.marketPanel === normalizedView));
|
||||
const params = new URLSearchParams(window.location.search);
|
||||
params.set("view", normalizedView);
|
||||
history.replaceState(null, "", `${window.location.pathname}?${params.toString()}`);
|
||||
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";
|
||||
@@ -299,8 +371,10 @@ function renderMarketList(items, listElement, type) {
|
||||
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";
|
||||
@@ -309,10 +383,36 @@ function renderMarketList(items, listElement, type) {
|
||||
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");
|
||||
@@ -325,29 +425,27 @@ function renderMarketList(items, listElement, type) {
|
||||
}
|
||||
|
||||
titleRow.append(title, tag);
|
||||
actions.appendChild(detailsBtn);
|
||||
item.append(titleRow, meta, actions);
|
||||
item.append(titleRow, meta, stats, actions);
|
||||
listElement.appendChild(item);
|
||||
});
|
||||
}
|
||||
|
||||
function filterMarketPersonas() {
|
||||
function filterMarketPersonas(items) {
|
||||
const query = marketSearchInput.value.trim().toLowerCase();
|
||||
if (!query) return allMarketPersonas;
|
||||
return allMarketPersonas.filter((persona) =>
|
||||
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 filtered = filterMarketPersonas();
|
||||
const assistantItems = filtered.filter((persona) => persona.persona_type === "assistant");
|
||||
const userItems = filtered.filter((persona) => persona.persona_type === "user");
|
||||
const assistantItems = filterMarketPersonas(marketPersonasByType.assistant);
|
||||
const userItems = filterMarketPersonas(marketPersonasByType.user);
|
||||
renderMarketList(assistantItems, marketPersonaList, "assistant");
|
||||
renderMarketList(userItems, marketUserPersonaList, "user");
|
||||
|
||||
if (marketSearchInput.value.trim() && !filtered.length) {
|
||||
if (marketSearchInput.value.trim() && !assistantItems.length && !userItems.length) {
|
||||
setMarketStatus("No personas match your search.", "error");
|
||||
return;
|
||||
}
|
||||
@@ -362,6 +460,72 @@ function applyMarketFilter() {
|
||||
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;
|
||||
@@ -373,17 +537,12 @@ async function loadOwnedPersonas() {
|
||||
|
||||
async function loadMarketPersonas() {
|
||||
setMarketStatus("Loading personas...");
|
||||
const [marketRes, ownedOk] = await Promise.all([get("/personas/market"), loadOwnedPersonas()]);
|
||||
if (marketRes.error) {
|
||||
setMarketStatus(marketRes.error, "error");
|
||||
return;
|
||||
}
|
||||
const ownedOk = await loadOwnedPersonas();
|
||||
if (!ownedOk) {
|
||||
setMarketStatus("Unable to load your personas.", "error");
|
||||
return;
|
||||
}
|
||||
allMarketPersonas = marketRes.personas || [];
|
||||
applyMarketFilter();
|
||||
await refreshMarket();
|
||||
}
|
||||
|
||||
async function collectMarketPersona(marketId) {
|
||||
@@ -460,7 +619,29 @@ async function savePersona() {
|
||||
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();
|
||||
closeMarketPopup(null);
|
||||
}
|
||||
});
|
||||
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));
|
||||
});
|
||||
@@ -521,6 +702,8 @@ marketPersonaForm.addEventListener("submit", (event) => {
|
||||
window.addEventListener("load", async () => {
|
||||
applyTheme(localStorage.getItem("krishd-theme") || "fakegpt");
|
||||
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();
|
||||
if (params.get("create") === "assistant") {
|
||||
|
||||
+359
-164
@@ -1,46 +1,117 @@
|
||||
const $ = (id) => document.getElementById(id);
|
||||
const authDiv = $("auth"), chatDiv = $("chat"), authMsg = $("authMsg");
|
||||
const messagesDiv = $("messages"), modelSelect = $("model");
|
||||
const modelMenuButton = $("modelMenuButton"), modelPopover = $("modelPopover"), modelBadgeName = $("modelBadgeName");
|
||||
const modelCount = $("modelCount");
|
||||
const modelHelpTitle = $("modelHelpTitle"), modelHelpBadge = $("modelHelpBadge"), modelHelpSummary = $("modelHelpSummary");
|
||||
const loginForm = $("loginForm"), registerForm = $("registerForm"), msgInput = $("msgInput"), sendBtn = $("send");
|
||||
const loginUsernameInput = $("loginUsername"), loginPasswordInput = $("loginPassword");
|
||||
const registerUsernameInput = $("registerUsername"), registerPasswordInput = $("registerPassword");
|
||||
const loginSubmit = $("loginSubmit"), registerSubmit = $("registerSubmit");
|
||||
const chatList = $("chatList"), chatSearchInput = $("chatSearch"), newChatBtn = $("newChat");
|
||||
const startRoleplayBtn = $("startRoleplay");
|
||||
const chatListLoading = $("chatListLoading"), chatListLoadingText = $("chatListLoadingText");
|
||||
const renameChatBtn = $("renameChat"), clearChatBtn = $("clearChat"), exportChatBtn = $("exportChat");
|
||||
const activeChatTitle = $("activeChatTitle"), sessionUser = $("sessionUser");
|
||||
const chatActivityOverlay = $("chatActivityOverlay"), chatActivityEyebrow = $("chatActivityEyebrow"), chatActivityTitle = $("chatActivityTitle"), chatActivityDetail = $("chatActivityDetail");
|
||||
const contextChipButton = $("contextChipButton"), contextPopover = $("contextPopover"), contextSummaryLabel = $("contextSummaryLabel");
|
||||
const chatActionsMenuButton = $("chatActionsMenuButton"), chatActionsPopover = $("chatActionsPopover");
|
||||
const chatModePill = $("chatModePill"), chatCharacterPill = $("chatCharacterPill"), chatUserPersonaPill = $("chatUserPersonaPill"), chatScenePill = $("chatScenePill");
|
||||
const personaList = $("personaList"), userPersonaList = $("userPersonaList");
|
||||
const personaForm = $("personaForm"), personaFormTitle = $("personaFormTitle"), personaTypeSelect = $("personaType");
|
||||
const personaNameInput = $("personaName"), personaPronounsInput = $("personaPronouns"), personaAppearanceInput = $("personaAppearance");
|
||||
const personaBackgroundInput = $("personaBackground"), personaDetailsInput = $("personaDetails");
|
||||
const personaExamplesField = $("personaExamplesField"), personaExampleDialoguesInput = $("personaExampleDialogues");
|
||||
const personaFormNotice = $("personaFormNotice");
|
||||
const roleplayNewPersonaBtn = $("roleplayNewPersona"), clearUserPersonaBtn = $("clearUserPersona");
|
||||
const activePersonaStatus = $("activePersonaStatus"), activeUserPersonaStatus = $("activeUserPersonaStatus");
|
||||
const personaModal = $("personaModal"), personaCloseBtn = $("personaClose"), personaMenuButton = $("personaMenuButton"), personaPopover = $("personaPopover");
|
||||
const roleplayStarterModal = $("roleplayStarterModal"), roleplayStarterClose = $("roleplayStarterClose"), roleplayStarterCancel = $("roleplayStarterCancel"), roleplayStarterConfirm = $("roleplayStarterConfirm");
|
||||
const roleplayStarterNotice = $("roleplayStarterNotice"), roleplayCharacterSelect = $("roleplayCharacterSelect"), roleplayUserPersonaSelect = $("roleplayUserPersonaSelect");
|
||||
const roleplayStarterTitle = $("roleplayStarterTitle"), roleplayStarterIntroCopy = $("roleplayStarterIntroCopy"), roleplayStarterBack = $("roleplayStarterBack");
|
||||
const roleplayStarterStepOne = $("roleplayStarterStepOne"), roleplayStarterStepTwo = $("roleplayStarterStepTwo");
|
||||
const roleplayScenarioInput = $("roleplayScenarioInput"), roleplayStarterSuggestions = $("roleplayStarterSuggestions");
|
||||
const popupModal = $("popupModal"), popupCloseBtn = $("popupClose"), popupCancelBtn = $("popupCancel"), popupConfirmBtn = $("popupConfirm");
|
||||
const popupTitle = $("popupTitle"), popupEyebrow = $("popupEyebrow"), popupDescription = $("popupDescription"), popupField = $("popupField");
|
||||
const popupInputLabel = $("popupInputLabel"), popupInput = $("popupInput");
|
||||
const appNotice = $("appNotice"), quickPrompts = $("quickPrompts");
|
||||
const onboardingModal = $("onboardingModal"), onboardingTitle = $("onboardingTitle"), onboardingSubtitle = $("onboardingSubtitle");
|
||||
const onboardingChoices = $("onboardingChoices"), onboardingBack = $("onboardingBack"), onboardingContinue = $("onboardingContinue"), onboardingSkip = $("onboardingSkip");
|
||||
const authScreens = document.querySelectorAll(".auth-screen"), toggleButtons = document.querySelectorAll(".auth-toggle .toggle");
|
||||
const themeNameTargets = document.querySelectorAll("[data-theme-name]"), themeLogoTargets = document.querySelectorAll("[data-theme-logo]");
|
||||
const modelMenu = document.querySelector(".model-menu"), personaMenu = document.querySelector(".persona-menu");
|
||||
const requestedChatId = Number(new URLSearchParams(window.location.search).get("chat")) || null;
|
||||
import {del, get, post, put, stream} from "./app/api.js";
|
||||
import {defaultModelProfile, onboardingPrompts, requestedChatId, themes} from "./app/constants.js";
|
||||
import {
|
||||
activeChatTitle,
|
||||
activePersonaStatus,
|
||||
activeUserPersonaStatus,
|
||||
appNotice,
|
||||
authDiv,
|
||||
authMsg,
|
||||
authScreens,
|
||||
chatActionsMenuButton,
|
||||
chatActionsPopover,
|
||||
chatActivityDetail,
|
||||
chatActivityEyebrow,
|
||||
chatActivityOverlay,
|
||||
chatActivityTitle,
|
||||
chatCharacterPill,
|
||||
chatDiv,
|
||||
chatList,
|
||||
chatListLoading,
|
||||
chatListLoadingText,
|
||||
chatSidebar,
|
||||
chatDrawerCloseBtn,
|
||||
chatModePill,
|
||||
chatSearchInput,
|
||||
chatUserPersonaPill,
|
||||
clearChatBtn,
|
||||
archiveChatBtn,
|
||||
backupWorkspaceBtn,
|
||||
clearUserPersonaBtn,
|
||||
contextChipButton,
|
||||
contextPopover,
|
||||
contextSummaryLabel,
|
||||
exportChatBtn,
|
||||
moveChatFolderBtn,
|
||||
loginForm,
|
||||
loginPasswordInput,
|
||||
loginSubmit,
|
||||
loginUsernameInput,
|
||||
logoutButton,
|
||||
messagesDiv,
|
||||
modelBadgeName,
|
||||
modelCount,
|
||||
modelHelpBadge,
|
||||
modelHelpSummary,
|
||||
modelHelpTitle,
|
||||
modelMenu,
|
||||
modelMenuButton,
|
||||
modelPopover,
|
||||
modelSelect,
|
||||
msgInput,
|
||||
newChatBtn,
|
||||
onboardingBack,
|
||||
onboardingChoices,
|
||||
onboardingContinue,
|
||||
onboardingModal,
|
||||
onboardingSkip,
|
||||
onboardingSubtitle,
|
||||
onboardingTitle,
|
||||
pinChatBtn,
|
||||
personaCloseBtn,
|
||||
personaDetailsInput,
|
||||
personaExampleDialoguesInput,
|
||||
personaExamplesField,
|
||||
personaForm,
|
||||
personaFormNotice,
|
||||
personaFormTitle,
|
||||
personaList,
|
||||
personaMenu,
|
||||
personaMenuButton,
|
||||
personaModal,
|
||||
personaNameInput,
|
||||
personaPopover,
|
||||
personaPronounsInput,
|
||||
personaTypeSelect,
|
||||
personaAppearanceInput,
|
||||
personaBackgroundInput,
|
||||
popupCancelBtn,
|
||||
popupCloseBtn,
|
||||
popupConfirmBtn,
|
||||
popupDescription,
|
||||
popupEyebrow,
|
||||
popupField,
|
||||
popupInput,
|
||||
popupInputLabel,
|
||||
popupModal,
|
||||
popupTitle,
|
||||
registerForm,
|
||||
registerPasswordInput,
|
||||
registerSubmit,
|
||||
registerUsernameInput,
|
||||
renameChatBtn,
|
||||
roleplayCharacterSelect,
|
||||
roleplayNewPersonaBtn,
|
||||
roleplayScenarioInput,
|
||||
roleplayStarterBack,
|
||||
roleplayStarterCancel,
|
||||
roleplayStarterClose,
|
||||
roleplayStarterConfirm,
|
||||
roleplayStarterIntroCopy,
|
||||
roleplayStarterModal,
|
||||
roleplayStarterNotice,
|
||||
roleplayStarterStepOne,
|
||||
roleplayStarterStepTwo,
|
||||
roleplayStarterSuggestions,
|
||||
roleplayStarterTitle,
|
||||
roleplayUserPersonaSelect,
|
||||
sendBtn,
|
||||
sessionUser,
|
||||
themeLogoTargets,
|
||||
themeNameTargets,
|
||||
toggleButtons,
|
||||
userPersonaList
|
||||
} from "./app/dom.js";
|
||||
|
||||
let isProcessing = false, activeChatId = null, editingPersonaId = null, editingPersonaType = "assistant", currentUsername = "";
|
||||
let chatSessions = [], currentMessages = [], assistantPersonas = [], userPersonas = [];
|
||||
@@ -55,38 +126,6 @@ let chatLoadingDepth = 0;
|
||||
let chatActivityDepth = 0;
|
||||
let messageRetryState = new Map();
|
||||
|
||||
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."]
|
||||
};
|
||||
|
||||
const themes = {
|
||||
"fakegpt": {name: "FakeGPT", short: "FG"},
|
||||
"fraud": {name: "Fraud", short: "FR"},
|
||||
"germini": {name: "Germini", short: "GE"},
|
||||
"slopilot": {name: "Slopilot", short: "SP"},
|
||||
"beta-ai": {name: "Beta AI", short: "BA"},
|
||||
"confusity": {name: "Confusity", short: "CF"}
|
||||
};
|
||||
|
||||
const defaultModelProfile = {
|
||||
badge: "General",
|
||||
summary: "General-purpose chat model. Start here if you are unsure which model to use."
|
||||
};
|
||||
|
||||
async function request(url, data, method = "POST") {
|
||||
try {
|
||||
const res = await fetch(url, {method, headers: {"Content-Type": "application/json"}, body: data ? JSON.stringify(data) : undefined});
|
||||
return await res.json();
|
||||
} catch {
|
||||
return {error: "Network error - please try again."};
|
||||
}
|
||||
}
|
||||
const get = (url) => request(url, null, "GET");
|
||||
const post = (url, data) => request(url, data, "POST");
|
||||
const put = (url, data) => request(url, data, "PUT");
|
||||
const del = (url) => request(url, null, "DELETE");
|
||||
|
||||
function setAuthMessage(message, state = "") { authMsg.textContent = message; authMsg.className = state ? `status ${state}` : "status"; }
|
||||
function setNotice(message = "", state = "") {
|
||||
@@ -166,7 +205,15 @@ function applyWorkspaceMode(nextMode, persist = true) {
|
||||
if (modelMenu) modelMenu.classList.toggle("hidden", workspaceMode === "basic");
|
||||
if (personaMenu) personaMenu.classList.toggle("hidden", workspaceMode === "basic");
|
||||
updateComposerPlaceholder();
|
||||
renderQuickPrompts();
|
||||
}
|
||||
|
||||
function isMobileLayout() {
|
||||
return window.matchMedia("(max-width: 820px)").matches;
|
||||
}
|
||||
|
||||
function closeMobileSidebar() {
|
||||
if (!chatSidebar) return;
|
||||
document.body.classList.remove("sidebar-open");
|
||||
}
|
||||
|
||||
function hasCompletedOnboarding() {
|
||||
@@ -286,29 +333,6 @@ function updateRoleplaySuggestions() {
|
||||
roleplayStarterSuggestions.appendChild(button);
|
||||
});
|
||||
}
|
||||
function getQuickPromptsForChat(chat) {
|
||||
return [];
|
||||
}
|
||||
function renderQuickPrompts() {
|
||||
if (!quickPrompts) return;
|
||||
const activeChat = getChatById(activeChatId);
|
||||
const prompts = getQuickPromptsForChat(activeChat);
|
||||
const visiblePrompts = workspaceMode === "basic" ? prompts.slice(0, 3) : prompts;
|
||||
quickPrompts.innerHTML = "";
|
||||
quickPrompts.classList.toggle("hidden", visiblePrompts.length === 0);
|
||||
visiblePrompts.forEach((prompt) => {
|
||||
const button = document.createElement("button");
|
||||
button.type = "button";
|
||||
button.className = "prompt-chip";
|
||||
button.textContent = prompt;
|
||||
button.addEventListener("click", () => {
|
||||
msgInput.value = prompt;
|
||||
msgInput.dispatchEvent(new Event("input"));
|
||||
msgInput.focus();
|
||||
});
|
||||
quickPrompts.appendChild(button);
|
||||
});
|
||||
}
|
||||
function updateComposerPlaceholder() {
|
||||
const activeChat = getChatById(activeChatId);
|
||||
if (!activeChat) {
|
||||
@@ -484,7 +508,16 @@ async function retryLatestAssistantMessage(messageId, fallbackIndex = -1) {
|
||||
try {
|
||||
const endpoint = getMessageEndpoint(target, "/retry");
|
||||
if (!endpoint) return setNotice("Message is not ready yet. Try again.", "error");
|
||||
const res = await post(endpoint, {model: modelSelect.value});
|
||||
const style = await promptPopup({
|
||||
eyebrow: "Retry style",
|
||||
title: "Retry modifier",
|
||||
description: "Leave empty for a normal retry, or use shorter, direct, emotional, stay-in-character, or dialogue.",
|
||||
label: "Style",
|
||||
value: "",
|
||||
placeholder: "shorter",
|
||||
confirmLabel: "Retry"
|
||||
});
|
||||
const res = await post(endpoint, {model: modelSelect.value, style: String(style || "").trim()});
|
||||
if (res.error || !res.message?.content) {
|
||||
return setNotice(res.error || "Unable to retry message.", "error");
|
||||
}
|
||||
@@ -636,7 +669,6 @@ function renderMessages() {
|
||||
}
|
||||
updateChatParticipants();
|
||||
updateChatActionState();
|
||||
renderQuickPrompts();
|
||||
updateComposerPlaceholder();
|
||||
}
|
||||
|
||||
@@ -671,26 +703,26 @@ function renderMessageEmptyState() {
|
||||
actions.className = "messages-empty-actions";
|
||||
|
||||
if (!chat) {
|
||||
title.textContent = "Start your first conversation";
|
||||
description.textContent = "Create a chat, or jump directly into roleplay.";
|
||||
title.textContent = "Start a chat";
|
||||
description.textContent = "Choose a starting point.";
|
||||
actions.append(
|
||||
createStarterAction("New chat", "primary-action", () => { void createNewChat(); }),
|
||||
createStarterAction("Start roleplay", "secondary-action", () => {
|
||||
createStarterAction("Roleplay", "secondary-action", () => {
|
||||
if (assistantPersonas.length) {
|
||||
openRoleplayStarter();
|
||||
return;
|
||||
}
|
||||
openPersonaForm(null, "assistant");
|
||||
}),
|
||||
createMessageStarterPrompt("Try a starter prompt", "Help me plan my day in 3 steps.")
|
||||
createMessageStarterPrompt("Try a prompt", "Help me plan my day in 3 steps.")
|
||||
);
|
||||
} else if (chat.assistant_persona_id) {
|
||||
title.textContent = "Your scene is ready";
|
||||
description.textContent = "Send your first line, or use a prompt to set the tone.";
|
||||
title.textContent = "Roleplay is ready";
|
||||
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("Pick another roleplay setup", "secondary-action", () => openRoleplayStarter())
|
||||
createStarterAction("Change setup", "secondary-action", () => openRoleplayStarter())
|
||||
);
|
||||
} else {
|
||||
return;
|
||||
@@ -709,7 +741,6 @@ function updateWorkspaceCopy() {
|
||||
sessionUser.textContent = currentUsername || "-";
|
||||
}
|
||||
updateChatParticipants();
|
||||
renderQuickPrompts();
|
||||
updateComposerPlaceholder();
|
||||
}
|
||||
|
||||
@@ -747,12 +778,11 @@ function renderRoleplayStarterStep() {
|
||||
|
||||
function updateChatParticipants() {
|
||||
const chat = getChatById(activeChatId);
|
||||
if (!chatModePill || !chatCharacterPill || !chatUserPersonaPill || !chatScenePill) return;
|
||||
if (!chatModePill || !chatCharacterPill || !chatUserPersonaPill) return;
|
||||
if (!chat) {
|
||||
chatModePill.textContent = "No chat selected";
|
||||
chatCharacterPill.classList.add("hidden");
|
||||
chatUserPersonaPill.classList.add("hidden");
|
||||
chatScenePill.classList.add("hidden");
|
||||
if (contextSummaryLabel) contextSummaryLabel.textContent = "None";
|
||||
return;
|
||||
}
|
||||
@@ -767,19 +797,12 @@ function updateChatParticipants() {
|
||||
chatUserPersonaPill.textContent = "You as: yourself";
|
||||
chatUserPersonaPill.classList.remove("hidden");
|
||||
}
|
||||
if (chat.scenario_summary) {
|
||||
chatScenePill.textContent = `Scene: ${summarizeText(chat.scenario_summary, 112)}`;
|
||||
chatScenePill.classList.remove("hidden");
|
||||
} else {
|
||||
chatScenePill.classList.add("hidden");
|
||||
}
|
||||
if (contextSummaryLabel) contextSummaryLabel.textContent = chat.assistant_persona_name || "Roleplay";
|
||||
return;
|
||||
}
|
||||
chatModePill.textContent = "Assistant mode";
|
||||
chatCharacterPill.classList.add("hidden");
|
||||
chatUserPersonaPill.classList.add("hidden");
|
||||
chatScenePill.classList.add("hidden");
|
||||
if (contextSummaryLabel) contextSummaryLabel.textContent = "Assistant";
|
||||
}
|
||||
|
||||
@@ -864,8 +887,18 @@ function updateChatActionState() {
|
||||
const activeChat = getChatById(activeChatId);
|
||||
const hasChat = Boolean(activeChat);
|
||||
renameChatBtn.disabled = !hasChat || Boolean(activeChat?.assistant_persona_id);
|
||||
if (pinChatBtn) {
|
||||
pinChatBtn.disabled = !hasChat;
|
||||
if (hasChat) pinChatBtn.textContent = activeChat.is_pinned ? "Unpin" : "Pin";
|
||||
}
|
||||
if (moveChatFolderBtn) moveChatFolderBtn.disabled = !hasChat;
|
||||
if (archiveChatBtn) {
|
||||
archiveChatBtn.disabled = !hasChat;
|
||||
if (hasChat) archiveChatBtn.textContent = activeChat.archived_at ? "Unarchive" : "Archive";
|
||||
}
|
||||
clearChatBtn.disabled = !hasChat;
|
||||
exportChatBtn.disabled = !hasChat || currentMessages.length === 0;
|
||||
if (backupWorkspaceBtn) backupWorkspaceBtn.disabled = !currentUsername;
|
||||
}
|
||||
|
||||
function maybeShowPersonaNudge() {
|
||||
@@ -878,7 +911,9 @@ function maybeShowPersonaNudge() {
|
||||
|
||||
function renderChatList() {
|
||||
const query = chatSearchInput.value.trim().toLowerCase();
|
||||
const filtered = chatSessions.filter((chat) => !query || chat.title.toLowerCase().includes(query));
|
||||
const filtered = chatSessions.filter((chat) => !query
|
||||
|| chat.title.toLowerCase().includes(query)
|
||||
|| String(chat.matched_content || "").toLowerCase().includes(query));
|
||||
chatList.innerHTML = "";
|
||||
if (!filtered.length) {
|
||||
const empty = document.createElement("p");
|
||||
@@ -887,21 +922,51 @@ function renderChatList() {
|
||||
chatList.appendChild(empty);
|
||||
return;
|
||||
}
|
||||
filtered.forEach((chat) => {
|
||||
const item = document.createElement("div"), content = document.createElement("div"), title = document.createElement("span"), meta = document.createElement("small"), remove = document.createElement("button");
|
||||
item.className = `chat-item${chat.id === activeChatId ? " active" : ""}`;
|
||||
content.className = "chat-item-copy";
|
||||
title.textContent = chat.title;
|
||||
meta.className = "chat-item-meta";
|
||||
meta.textContent = chat.assistant_persona_id
|
||||
? `Roleplay${chat.user_persona_name ? ` · ${chat.user_persona_name}` : ""}`
|
||||
: "Assistant chat";
|
||||
remove.type = "button"; remove.textContent = "Delete";
|
||||
remove.addEventListener("click", async (event) => { event.stopPropagation(); await deleteChat(chat.id); });
|
||||
content.append(title, meta);
|
||||
item.append(content, remove);
|
||||
item.addEventListener("click", () => setActiveChat(chat.id));
|
||||
chatList.appendChild(item);
|
||||
const folderGroups = filtered
|
||||
.filter((chat) => chat.folder_name && !chat.archived_at)
|
||||
.reduce((map, chat) => {
|
||||
const key = String(chat.folder_name).trim();
|
||||
const items = map.get(key) || [];
|
||||
items.push(chat);
|
||||
map.set(key, items);
|
||||
return map;
|
||||
}, new Map());
|
||||
const groups = [
|
||||
{label: "Pinned", items: filtered.filter((chat) => chat.is_pinned && !chat.archived_at)},
|
||||
...Array.from(folderGroups.entries()).map(([label, items]) => ({label, items})),
|
||||
{label: "Recent", items: filtered.filter((chat) => !chat.is_pinned && !chat.folder_name && !chat.archived_at)},
|
||||
{label: "Archived", items: filtered.filter((chat) => chat.archived_at)}
|
||||
].filter((group) => group.items.length);
|
||||
groups.forEach((group) => {
|
||||
const section = document.createElement("section");
|
||||
const heading = document.createElement("div");
|
||||
const list = document.createElement("div");
|
||||
section.className = "chat-list-section";
|
||||
heading.className = "chat-list-section-header";
|
||||
heading.textContent = group.label;
|
||||
list.className = "chat-list-section-body";
|
||||
section.append(heading, list);
|
||||
group.items.forEach((chat) => {
|
||||
const item = document.createElement("div"), content = document.createElement("div"), title = document.createElement("span"), meta = document.createElement("small"), remove = document.createElement("button");
|
||||
item.className = `chat-item${chat.id === activeChatId ? " active" : ""}`;
|
||||
content.className = "chat-item-copy";
|
||||
title.textContent = chat.title;
|
||||
meta.className = "chat-item-meta";
|
||||
meta.textContent = chat.assistant_persona_id
|
||||
? `Roleplay${chat.user_persona_name ? ` · ${chat.user_persona_name}` : ""}`
|
||||
: "Assistant chat";
|
||||
remove.type = "button";
|
||||
remove.textContent = "Delete";
|
||||
remove.addEventListener("click", async (event) => {
|
||||
event.stopPropagation();
|
||||
await deleteChat(chat.id);
|
||||
});
|
||||
content.append(title, meta);
|
||||
item.append(content, remove);
|
||||
item.addEventListener("click", () => setActiveChat(chat.id));
|
||||
list.appendChild(item);
|
||||
});
|
||||
chatList.appendChild(section);
|
||||
});
|
||||
}
|
||||
|
||||
@@ -945,7 +1010,11 @@ function openPersonaForm(persona = null, personaType = "assistant") {
|
||||
}
|
||||
|
||||
function closePersonaForm() { personaModal.classList.add("hidden"); editingPersonaId = null; editingPersonaType = "assistant"; personaTypeSelect.disabled = false; setPersonaFormNotice(""); }
|
||||
function closePersonaPopover() { personaPopover.classList.add("hidden"); personaMenuButton.setAttribute("aria-expanded", "false"); }
|
||||
function closePersonaPopover() {
|
||||
if (!personaPopover || personaPopover.dataset.staticPanel === "true") return;
|
||||
personaPopover.classList.add("hidden");
|
||||
if (personaMenuButton) personaMenuButton.setAttribute("aria-expanded", "false");
|
||||
}
|
||||
function closeContextPopover() {
|
||||
if (!contextPopover || !contextChipButton) return;
|
||||
contextPopover.classList.add("hidden");
|
||||
@@ -955,6 +1024,7 @@ function closeChatActionsPopover() {
|
||||
if (!chatActionsPopover || !chatActionsMenuButton) return;
|
||||
chatActionsPopover.classList.add("hidden");
|
||||
chatActionsMenuButton.setAttribute("aria-expanded", "false");
|
||||
document.body.classList.remove("chat-drawer-open");
|
||||
}
|
||||
function closeAuxiliaryPopovers() {
|
||||
closeModelPopover();
|
||||
@@ -1037,9 +1107,9 @@ function goToRoleplayStarterSceneStep() {
|
||||
if (roleplayScenarioInput) roleplayScenarioInput.focus();
|
||||
}
|
||||
function closeModelPopover() {
|
||||
if (!modelPopover || !modelMenuButton) return;
|
||||
if (!modelPopover || modelPopover.dataset.staticPanel === "true") return;
|
||||
modelPopover.classList.add("hidden");
|
||||
modelMenuButton.setAttribute("aria-expanded", "false");
|
||||
if (modelMenuButton) modelMenuButton.setAttribute("aria-expanded", "false");
|
||||
}
|
||||
function isPopupOpen() { return popupModal && !popupModal.classList.contains("hidden"); }
|
||||
function finishPopup(result) {
|
||||
@@ -1231,6 +1301,7 @@ function showAuthScreen(target) {
|
||||
}
|
||||
|
||||
async function setActiveChat(id) {
|
||||
if (isMobileLayout()) closeMobileSidebar();
|
||||
setChatLoading(true, "Opening chat...");
|
||||
setChatActivity(true, {
|
||||
eyebrow: "Opening chat",
|
||||
@@ -1256,7 +1327,8 @@ async function setActiveChat(id) {
|
||||
|
||||
async function loadChatSessions() {
|
||||
setChatLoading(true, "Loading chats...");
|
||||
const res = await get("/chats");
|
||||
const query = chatSearchInput?.value.trim();
|
||||
const res = query ? await get(`/chats/search?q=${encodeURIComponent(query)}`) : await get("/chats");
|
||||
if (res.error) {
|
||||
setChatLoading(false);
|
||||
return setNotice(res.error, "error");
|
||||
@@ -1354,6 +1426,23 @@ async function renameChat(id) {
|
||||
chat.title = title; updateWorkspaceCopy(); renderChatList(); setNotice("Chat renamed.", "success");
|
||||
}
|
||||
|
||||
async function updateChatOrganization(id, changes) {
|
||||
const chat = getChatById(id);
|
||||
if (!chat) return;
|
||||
const payload = {
|
||||
folderName: changes.folderName !== undefined ? changes.folderName : chat.folder_name,
|
||||
isPinned: changes.isPinned !== undefined ? changes.isPinned : Boolean(chat.is_pinned),
|
||||
archived: changes.archived !== undefined ? changes.archived : Boolean(chat.archived_at)
|
||||
};
|
||||
const res = await put(`/chats/${id}/organization`, payload);
|
||||
if (res.error) return setNotice(res.error, "error");
|
||||
const index = chatSessions.findIndex((item) => item.id === id);
|
||||
if (index >= 0) chatSessions[index] = res.chat;
|
||||
updateWorkspaceCopy();
|
||||
renderChatList();
|
||||
updateChatActionState();
|
||||
}
|
||||
|
||||
async function clearChat(id) {
|
||||
const chat = getChatById(id);
|
||||
if (!chat) return;
|
||||
@@ -1374,11 +1463,46 @@ async function clearChat(id) {
|
||||
function exportCurrentChat() {
|
||||
const chat = getChatById(activeChatId);
|
||||
if (!chat || !currentMessages.length) return;
|
||||
const lines = [`Chat: ${chat.title}`, `Exported: ${new Date().toISOString()}`, ""];
|
||||
currentMessages.forEach((msg) => { lines.push(`${msg.role === "user" ? "You" : "Assistant"}:`); lines.push(msg.content, ""); });
|
||||
const blob = new Blob([lines.join("\n")], {type: "text/plain;charset=utf-8"}), url = URL.createObjectURL(blob), link = document.createElement("a");
|
||||
link.href = url; link.download = `${(chat.title.replace(/[^a-z0-9-_]+/gi, "-").replace(/^-+|-+$/g, "") || "chat")}.txt`;
|
||||
document.body.appendChild(link); link.click(); link.remove(); URL.revokeObjectURL(url); setNotice("Chat exported.", "success");
|
||||
void (async () => {
|
||||
const format = await promptPopup({
|
||||
eyebrow: "Export",
|
||||
title: "Export format",
|
||||
description: "Use txt, md, or json.",
|
||||
label: "Format",
|
||||
value: "md",
|
||||
placeholder: "md",
|
||||
confirmLabel: "Download"
|
||||
});
|
||||
const normalized = ["txt", "md", "json"].includes(String(format || "").trim().toLowerCase())
|
||||
? String(format).trim().toLowerCase()
|
||||
: "md";
|
||||
const response = await fetch(`/exports/chats/${activeChatId}?format=${normalized}`);
|
||||
const blob = await response.blob();
|
||||
const url = URL.createObjectURL(blob);
|
||||
const link = document.createElement("a");
|
||||
link.href = url;
|
||||
link.download = `${(chat.title.replace(/[^a-z0-9-_]+/gi, "-").replace(/^-+|-+$/g, "") || "chat")}.${normalized}`;
|
||||
document.body.appendChild(link);
|
||||
link.click();
|
||||
link.remove();
|
||||
URL.revokeObjectURL(url);
|
||||
setNotice(`Chat exported as ${normalized}.`, "success");
|
||||
})();
|
||||
}
|
||||
|
||||
async function backupWorkspace() {
|
||||
const response = await fetch("/exports/workspace");
|
||||
const data = await response.json();
|
||||
const blob = new Blob([JSON.stringify(data.workspace, null, 2)], {type: "application/json;charset=utf-8"});
|
||||
const url = URL.createObjectURL(blob);
|
||||
const link = document.createElement("a");
|
||||
link.href = url;
|
||||
link.download = `workspace-backup-${new Date().toISOString().slice(0, 10)}.json`;
|
||||
document.body.appendChild(link);
|
||||
link.click();
|
||||
link.remove();
|
||||
URL.revokeObjectURL(url);
|
||||
setNotice("Workspace backup downloaded.", "success");
|
||||
}
|
||||
|
||||
async function deleteChat(id) {
|
||||
@@ -1479,19 +1603,30 @@ async function sendMessage() {
|
||||
title: "Generating response",
|
||||
detail: "The assistant is reading your message and preparing a reply."
|
||||
}); setNotice("Generating reply...");
|
||||
const loadingMsg = addMessage("", false, true);
|
||||
const loadingMsg = addMessage({role: "bot", content: ""}, false, false, {isNewest: true});
|
||||
const loadingBody = loadingMsg.querySelector(".msg-content");
|
||||
try {
|
||||
const res = await post("/chat", {message, model: modelSelect.value, chatId: activeChatId});
|
||||
let streamError = null;
|
||||
await stream("/chat/stream", {message, model: modelSelect.value, chatId: activeChatId}, {
|
||||
onChunk: ({fullReply}) => {
|
||||
if (loadingBody) setMessageContent(loadingBody, fullReply);
|
||||
},
|
||||
onDone: () => {},
|
||||
onError: ({error}) => {
|
||||
streamError = error || "Failed to generate reply.";
|
||||
}
|
||||
});
|
||||
if (messagesDiv.contains(loadingMsg)) messagesDiv.removeChild(loadingMsg);
|
||||
if (res.reply) {
|
||||
await setActiveChat(activeChatId);
|
||||
await loadSummary();
|
||||
markOnboardingCompleted();
|
||||
maybeShowPersonaNudge();
|
||||
setNotice("Reply received.", "success");
|
||||
} else {
|
||||
addMessage(`[Error: ${res.error}]`); setNotice(res.error || "Failed to generate reply.", "error");
|
||||
if (streamError) {
|
||||
addMessage(`[Error: ${streamError}]`);
|
||||
setNotice(streamError, "error");
|
||||
return;
|
||||
}
|
||||
await setActiveChat(activeChatId);
|
||||
await loadSummary();
|
||||
markOnboardingCompleted();
|
||||
maybeShowPersonaNudge();
|
||||
setNotice("Reply received.", "success");
|
||||
} catch {
|
||||
if (messagesDiv.contains(loadingMsg)) messagesDiv.removeChild(loadingMsg);
|
||||
addMessage("[Error: Network error]"); setNotice("Network error while sending message.", "error");
|
||||
@@ -1537,15 +1672,18 @@ function toggleChatActionsPopover(event) {
|
||||
closeModelPopover();
|
||||
closePersonaPopover();
|
||||
closeContextPopover();
|
||||
if (isMobileLayout()) closeMobileSidebar();
|
||||
const isHidden = chatActionsPopover.classList.contains("hidden");
|
||||
chatActionsPopover.classList.toggle("hidden", !isHidden);
|
||||
chatActionsMenuButton.setAttribute("aria-expanded", String(isHidden));
|
||||
document.body.classList.toggle("chat-drawer-open", isHidden);
|
||||
}
|
||||
|
||||
toggleButtons.forEach((btn) => btn.addEventListener("click", () => showAuthScreen(btn.dataset.target)));
|
||||
loginForm.addEventListener("submit", (event) => { event.preventDefault(); void handleAuth("login", {username: loginUsernameInput.value.trim(), password: loginPasswordInput.value.trim()}); });
|
||||
registerForm.addEventListener("submit", (event) => { event.preventDefault(); void handleAuth("register", {username: registerUsernameInput.value.trim(), password: registerPasswordInput.value.trim()}); });
|
||||
$("logout").addEventListener("click", async () => {
|
||||
logoutButton.addEventListener("click", async () => {
|
||||
closeChatActionsPopover();
|
||||
await post("/logout", {}); chatDiv.style.display = "none"; authDiv.style.display = "grid";
|
||||
activeChatId = null; currentUsername = ""; currentSummary = null; chatSessions = []; currentMessages = []; assistantPersonas = []; userPersonas = [];
|
||||
messageRetryState = new Map();
|
||||
@@ -1561,17 +1699,64 @@ msgInput.addEventListener("input", function () {
|
||||
this.style.height = `${Math.min(this.scrollHeight, 180)}px`;
|
||||
updateSendState();
|
||||
});
|
||||
newChatBtn.addEventListener("click", () => { void createNewChat(); });
|
||||
if (startRoleplayBtn) {
|
||||
startRoleplayBtn.addEventListener("click", () => {
|
||||
window.location.href = "/market?view=ai-characters";
|
||||
newChatBtn.addEventListener("click", () => {
|
||||
closeChatActionsPopover();
|
||||
closeMobileSidebar();
|
||||
void createNewChat();
|
||||
});
|
||||
renameChatBtn.addEventListener("click", () => {
|
||||
closeChatActionsPopover();
|
||||
if (activeChatId) void renameChat(activeChatId);
|
||||
});
|
||||
clearChatBtn.addEventListener("click", () => {
|
||||
closeChatActionsPopover();
|
||||
if (activeChatId) void clearChat(activeChatId);
|
||||
});
|
||||
exportChatBtn.addEventListener("click", () => {
|
||||
closeChatActionsPopover();
|
||||
exportCurrentChat();
|
||||
});
|
||||
if (pinChatBtn) pinChatBtn.addEventListener("click", () => {
|
||||
closeChatActionsPopover();
|
||||
const chat = getChatById(activeChatId);
|
||||
if (chat) void updateChatOrganization(chat.id, {isPinned: !chat.is_pinned});
|
||||
});
|
||||
if (moveChatFolderBtn) moveChatFolderBtn.addEventListener("click", async () => {
|
||||
closeChatActionsPopover();
|
||||
const chat = getChatById(activeChatId);
|
||||
if (!chat) return;
|
||||
const folderName = await promptPopup({
|
||||
eyebrow: "Folder",
|
||||
title: "Move chat to folder",
|
||||
description: "Leave blank to remove the folder.",
|
||||
label: "Folder name",
|
||||
value: chat.folder_name || "",
|
||||
placeholder: "Research"
|
||||
});
|
||||
}
|
||||
renameChatBtn.addEventListener("click", () => { if (activeChatId) void renameChat(activeChatId); });
|
||||
clearChatBtn.addEventListener("click", () => { if (activeChatId) void clearChat(activeChatId); }); exportChatBtn.addEventListener("click", exportCurrentChat);
|
||||
chatSearchInput.addEventListener("input", renderChatList); modelSelect.addEventListener("change", () => { updateModelHelp(); updateContextRail(); });
|
||||
if (folderName === null) return;
|
||||
void updateChatOrganization(chat.id, {folderName: String(folderName || "").trim() || null});
|
||||
});
|
||||
if (archiveChatBtn) archiveChatBtn.addEventListener("click", () => {
|
||||
closeChatActionsPopover();
|
||||
const chat = getChatById(activeChatId);
|
||||
if (chat) void updateChatOrganization(chat.id, {archived: !chat.archived_at});
|
||||
});
|
||||
if (backupWorkspaceBtn) backupWorkspaceBtn.addEventListener("click", () => {
|
||||
closeChatActionsPopover();
|
||||
void backupWorkspace();
|
||||
});
|
||||
chatSearchInput.addEventListener("input", () => { void loadChatSessions(); });
|
||||
if (chatDrawerCloseBtn) chatDrawerCloseBtn.addEventListener("click", closeChatActionsPopover);
|
||||
modelSelect.addEventListener("change", () => {
|
||||
updateModelHelp();
|
||||
updateContextRail();
|
||||
closeModelPopover();
|
||||
});
|
||||
roleplayNewPersonaBtn.addEventListener("click", () => openPersonaForm(null, "assistant"));
|
||||
clearUserPersonaBtn.addEventListener("click", () => { void clearPersona("user"); });
|
||||
clearUserPersonaBtn.addEventListener("click", () => {
|
||||
closePersonaPopover();
|
||||
void clearPersona("user");
|
||||
});
|
||||
personaForm.addEventListener("submit", (event) => { event.preventDefault(); void savePersona(); }); personaCloseBtn.addEventListener("click", closePersonaForm);
|
||||
personaTypeSelect.addEventListener("change", () => {
|
||||
const isAssistant = personaTypeSelect.value === "assistant";
|
||||
@@ -1625,6 +1810,9 @@ if (chatActionsMenuButton && chatActionsPopover) {
|
||||
chatActionsMenuButton.addEventListener("click", toggleChatActionsPopover);
|
||||
chatActionsPopover.addEventListener("click", (event) => event.stopPropagation());
|
||||
}
|
||||
if (chatSidebar) {
|
||||
chatSidebar.addEventListener("click", (event) => event.stopPropagation());
|
||||
}
|
||||
if (onboardingContinue) {
|
||||
onboardingContinue.addEventListener("click", () => {
|
||||
onboardingStep = 2;
|
||||
@@ -1645,10 +1833,17 @@ if (onboardingModal) {
|
||||
if (event.target === onboardingModal) markOnboardingCompleted();
|
||||
});
|
||||
}
|
||||
document.addEventListener("click", () => { closeAuxiliaryPopovers(); });
|
||||
document.addEventListener("click", () => {
|
||||
closeAuxiliaryPopovers();
|
||||
closeMobileSidebar();
|
||||
});
|
||||
window.addEventListener("resize", () => {
|
||||
if (!isMobileLayout()) closeMobileSidebar();
|
||||
});
|
||||
document.addEventListener("keydown", (event) => {
|
||||
if (event.key === "Escape") {
|
||||
closeAuxiliaryPopovers();
|
||||
closeMobileSidebar();
|
||||
closeRoleplayStarter();
|
||||
closePersonaForm();
|
||||
closePopup(false);
|
||||
|
||||
+27
-6
@@ -15,7 +15,6 @@
|
||||
<div>
|
||||
<p class="eyebrow">Settings</p>
|
||||
<h1>Preferences</h1>
|
||||
<p class="subtitle">Manage your account, AI Characters, user personas, and appearance.</p>
|
||||
</div>
|
||||
<div class="market-header-actions">
|
||||
<a href="/" class="secondary-action">Back to chat</a>
|
||||
@@ -27,9 +26,9 @@
|
||||
|
||||
<section class="settings-layout">
|
||||
<aside class="settings-nav" aria-label="Settings sections">
|
||||
<button class="settings-nav-item active" type="button" data-settings-view="personal">Personal</button>
|
||||
<button class="settings-nav-item" type="button" data-settings-view="my-roleplay-companions">My Roleplay Companions</button>
|
||||
<button class="settings-nav-item" type="button" data-settings-view="design">Design</button>
|
||||
<button class="settings-nav-item active" type="button" data-settings-view="personal">Account</button>
|
||||
<button class="settings-nav-item" type="button" data-settings-view="my-roleplay-companions">Personas</button>
|
||||
<button class="settings-nav-item" type="button" data-settings-view="design">Appearance</button>
|
||||
</aside>
|
||||
|
||||
<div class="settings-content">
|
||||
@@ -88,8 +87,7 @@
|
||||
<div class="settings-section-header">
|
||||
<div>
|
||||
<p class="eyebrow">Create & manage</p>
|
||||
<h2>My Roleplay Companions</h2>
|
||||
<p class="subtitle">Build characters for roleplay chats and publish them to the market when they are ready.</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>
|
||||
@@ -197,6 +195,29 @@
|
||||
</form>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div id="settingsPopupModal" class="modal-overlay hidden">
|
||||
<div class="modal-card popup-card" role="dialog" aria-modal="true" aria-labelledby="settingsPopupTitle" aria-describedby="settingsPopupDescription">
|
||||
<div class="modal-header">
|
||||
<div class="persona-modal-heading">
|
||||
<p id="settingsPopupEyebrow" class="eyebrow">Action</p>
|
||||
<h3 id="settingsPopupTitle">Confirm action</h3>
|
||||
</div>
|
||||
<button id="settingsPopupClose" type="button" class="icon-button">Close</button>
|
||||
</div>
|
||||
<div class="popup-content">
|
||||
<p id="settingsPopupDescription" class="subtitle">Please confirm this action.</p>
|
||||
<label id="settingsPopupField" class="field">
|
||||
<span id="settingsPopupInputLabel">Value</span>
|
||||
<input id="settingsPopupInput" type="text" autocomplete="off"/>
|
||||
</label>
|
||||
</div>
|
||||
<div class="popup-actions">
|
||||
<button id="settingsPopupCancel" type="button" class="secondary-action">Cancel</button>
|
||||
<button id="settingsPopupConfirm" type="button">Confirm</button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<script src="settings.js" defer></script>
|
||||
</body>
|
||||
</html>
|
||||
|
||||
+106
-1
@@ -30,6 +30,16 @@ const settingsPersonaBackground = $("settingsPersonaBackground");
|
||||
const settingsPersonaDetails = $("settingsPersonaDetails");
|
||||
const settingsPersonaExamplesField = $("settingsPersonaExamplesField");
|
||||
const settingsPersonaExampleDialogues = $("settingsPersonaExampleDialogues");
|
||||
const settingsPopupModal = $("settingsPopupModal");
|
||||
const settingsPopupClose = $("settingsPopupClose");
|
||||
const settingsPopupCancel = $("settingsPopupCancel");
|
||||
const settingsPopupConfirm = $("settingsPopupConfirm");
|
||||
const settingsPopupTitle = $("settingsPopupTitle");
|
||||
const settingsPopupEyebrow = $("settingsPopupEyebrow");
|
||||
const settingsPopupDescription = $("settingsPopupDescription");
|
||||
const settingsPopupField = $("settingsPopupField");
|
||||
const settingsPopupInputLabel = $("settingsPopupInputLabel");
|
||||
const settingsPopupInput = $("settingsPopupInput");
|
||||
|
||||
const themes = {
|
||||
"fakegpt": {name: "FakeGPT"},
|
||||
@@ -44,6 +54,7 @@ let assistantPersonas = [];
|
||||
let userPersonas = [];
|
||||
let publishedPersonaIds = new Set();
|
||||
let editingPersonaId = null;
|
||||
let settingsPopupResolver = null;
|
||||
|
||||
async function request(url, data, method = "POST") {
|
||||
try {
|
||||
@@ -78,6 +89,39 @@ function setPersonaNotice(message = "", state = "") {
|
||||
settingsPersonaFormNotice.className = state ? `status ${state}` : "status";
|
||||
}
|
||||
|
||||
function closeSettingsPopup(value = null) {
|
||||
settingsPopupModal.classList.add("hidden");
|
||||
if (settingsPopupResolver) {
|
||||
const resolve = settingsPopupResolver;
|
||||
settingsPopupResolver = null;
|
||||
resolve(value);
|
||||
}
|
||||
}
|
||||
|
||||
function promptSettingsPopup({
|
||||
eyebrow = "Action",
|
||||
title = "Enter a value",
|
||||
description = "",
|
||||
label = "Value",
|
||||
value = "",
|
||||
placeholder = "",
|
||||
confirmLabel = "Confirm"
|
||||
} = {}) {
|
||||
settingsPopupEyebrow.textContent = eyebrow;
|
||||
settingsPopupTitle.textContent = title;
|
||||
settingsPopupDescription.textContent = description;
|
||||
settingsPopupInputLabel.textContent = label;
|
||||
settingsPopupInput.value = value;
|
||||
settingsPopupInput.placeholder = placeholder;
|
||||
settingsPopupConfirm.textContent = confirmLabel;
|
||||
settingsPopupModal.classList.remove("hidden");
|
||||
settingsPopupInput.focus();
|
||||
settingsPopupInput.select();
|
||||
return new Promise((resolve) => {
|
||||
settingsPopupResolver = resolve;
|
||||
});
|
||||
}
|
||||
|
||||
function applyTheme(themeKey, persist = true) {
|
||||
const nextTheme = themes[themeKey] ? themeKey : "fakegpt";
|
||||
document.body.dataset.theme = nextTheme;
|
||||
@@ -169,6 +213,8 @@ function renderPersonaList(personas, listElement, type) {
|
||||
|
||||
[
|
||||
["Edit", () => openPersonaModal(persona)],
|
||||
["Clone", () => clonePersona(persona.id)],
|
||||
["History", () => showPersonaHistory(persona.id)],
|
||||
[publishedPersonaIds.has(persona.id) ? "Update listing" : "Publish", () => publishPersona(persona.id)],
|
||||
["Delete", () => deletePersona(persona.id)]
|
||||
]
|
||||
@@ -263,7 +309,17 @@ async function deletePersona(id) {
|
||||
}
|
||||
|
||||
async function publishPersona(id) {
|
||||
const res = await post(`/personas/${id}/publish`, {});
|
||||
const tagsInput = await promptSettingsPopup({
|
||||
eyebrow: "Marketplace",
|
||||
title: "Persona tags",
|
||||
description: "Add optional comma-separated tags to improve discovery.",
|
||||
label: "Tags",
|
||||
placeholder: "mentor, fantasy, villain",
|
||||
confirmLabel: "Publish"
|
||||
});
|
||||
if (tagsInput === null) return;
|
||||
const tags = String(tagsInput || "").split(",").map((tag) => tag.trim()).filter(Boolean);
|
||||
const res = await post(`/personas/${id}/publish`, {tags});
|
||||
if (res.error) {
|
||||
setNotice(res.error, "error");
|
||||
return;
|
||||
@@ -272,6 +328,39 @@ async function publishPersona(id) {
|
||||
setNotice("Persona published.", "success");
|
||||
}
|
||||
|
||||
async function clonePersona(id) {
|
||||
const res = await post(`/personas/${id}/clone`, {});
|
||||
if (res.error) return setNotice(res.error, "error");
|
||||
await loadPersonas();
|
||||
setNotice("Persona cloned.", "success");
|
||||
}
|
||||
|
||||
async function showPersonaHistory(id) {
|
||||
const res = await get(`/personas/${id}/versions`);
|
||||
if (res.error) return setNotice(res.error, "error");
|
||||
const versions = res.versions || [];
|
||||
const summary = versions.length
|
||||
? versions.map((version) => `v${version.version_number} - ${new Date(version.created_at).toLocaleString()}`).join("\n")
|
||||
: "No saved versions yet.";
|
||||
const choice = await promptSettingsPopup({
|
||||
eyebrow: "Persona history",
|
||||
title: "Restore a version",
|
||||
description: summary,
|
||||
label: "Version number",
|
||||
placeholder: "1",
|
||||
confirmLabel: "Restore"
|
||||
});
|
||||
if (choice === null) return;
|
||||
const selected = Number(choice);
|
||||
if (!selected) return;
|
||||
const version = versions.find((item) => item.version_number === selected);
|
||||
if (!version) return setNotice("Version not found.", "error");
|
||||
const restore = await post(`/personas/${id}/versions/${version.id}/restore`, {});
|
||||
if (restore.error) return setNotice(restore.error, "error");
|
||||
await loadPersonas();
|
||||
setNotice(`Restored persona version ${selected}.`, "success");
|
||||
}
|
||||
|
||||
async function unpublishPersona(id) {
|
||||
const res = await post(`/personas/${id}/unpublish`, {});
|
||||
if (res.error) {
|
||||
@@ -347,6 +436,22 @@ settingsPersonaForm.addEventListener("submit", (event) => {
|
||||
event.preventDefault();
|
||||
void savePersona();
|
||||
});
|
||||
settingsPopupConfirm.addEventListener("click", () => closeSettingsPopup(settingsPopupInput.value));
|
||||
settingsPopupCancel.addEventListener("click", () => closeSettingsPopup(null));
|
||||
settingsPopupClose.addEventListener("click", () => closeSettingsPopup(null));
|
||||
settingsPopupModal.addEventListener("click", (event) => {
|
||||
if (event.target === settingsPopupModal) closeSettingsPopup(null);
|
||||
});
|
||||
settingsPopupInput.addEventListener("keydown", (event) => {
|
||||
if (event.key === "Enter") {
|
||||
event.preventDefault();
|
||||
closeSettingsPopup(settingsPopupInput.value);
|
||||
}
|
||||
if (event.key === "Escape") {
|
||||
event.preventDefault();
|
||||
closeSettingsPopup(null);
|
||||
}
|
||||
});
|
||||
|
||||
window.addEventListener("load", async () => {
|
||||
applyTheme(localStorage.getItem("krishd-theme") || "fakegpt", false);
|
||||
|
||||
+1313
File diff suppressed because it is too large
Load Diff
Reference in New Issue
Block a user