make app more intuitive

This commit is contained in:
2026-04-09 23:54:41 +02:00
parent 057f59ceef
commit 6e2aa2b353
7 changed files with 660 additions and 211 deletions
+68 -15
View File
@@ -22,7 +22,7 @@
</div>
<div class="hero-grid">
<article class="hero-panel">
<p>Log in to continue your chats. Personas, shared presets, and theme changes are available after sign-in.</p>
<p>Sign in to start a chat immediately, then add personas when you want richer roleplay.</p>
</article>
</div>
</div>
@@ -80,10 +80,11 @@
<div class="sidebar-header">
<div>
<p class="eyebrow">Chats</p>
<h2>Stories & chats</h2>
<h2>Conversations</h2>
</div>
<div class="sidebar-header-actions">
<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>
</div>
<label class="field compact-field">
@@ -92,14 +93,26 @@
</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>
<div class="sidebar-footer sidebar-footer-stack">
<a href="/market" class="secondary-action">Persona Market</a>
<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>
</aside>
@@ -109,6 +122,12 @@
<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>
@@ -116,10 +135,7 @@
<span id="chatScenePill" class="context-pill hidden">Scene: none</span>
</div>
</div>
<div class="chat-toolbar">
<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>
</div>
</div>
<div class="chat-header-actions">
@@ -172,6 +188,26 @@
</div>
</div>
</div>
<div class="chat-actions-menu">
<button id="chatActionsMenuButton" class="secondary-action" type="button" aria-expanded="false" aria-controls="chatActionsPopover">
More
</button>
<div id="chatActionsPopover" class="persona-popover hidden" role="dialog">
<div class="persona-section">
<div class="persona-section-header">
<div>
<p class="eyebrow">Conversation</p>
<h2>Actions</h2>
</div>
</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>
</div>
</div>
</div>
</div>
</header>
@@ -179,7 +215,7 @@
<div class="input-shell input-shell-focus">
<div class="input-meta">
<p class="composer-hint">Enter sends. Shift+Enter adds a new line.</p>
<p class="composer-hint">Enter to send · Shift+Enter for new line</p>
<p id="appNotice" class="status inline-status hidden" aria-live="polite"></p>
</div>
<div id="quickPrompts" class="quick-prompts" aria-label="Suggested starters"></div>
@@ -263,31 +299,30 @@
<button id="roleplayStarterClose" type="button" class="icon-button">Close</button>
</div>
<div class="roleplay-starter-intro">
<p class="persona-helper">Pick a character, optionally choose who you are in the scene, and the character will open the conversation with the first message.</p>
<p class="persona-helper">Pick a character, choose who you are in the scene, and generate an opening message in one step.</p>
</div>
<p id="roleplayStarterNotice" class="status hidden"></p>
<div class="roleplay-starter-actions">
<button id="roleplayNewPersona" class="secondary-action" type="button">Create AI Character</button>
<a href="/settings" class="secondary-action">Settings</a>
</div>
<div class="persona-form-grid">
<label class="field field-wide">
<span>Character</span>
<span>1. Character</span>
<select id="roleplayCharacterSelect"></select>
</label>
<label class="field field-wide">
<span>User persona</span>
<span>2. You in the scene</span>
<select id="roleplayUserPersonaSelect"></select>
</label>
<label class="field field-wide">
<span>Scenario seed</span>
<span>3. Optional opening direction</span>
<textarea id="roleplayScenarioInput" rows="3" placeholder="Optional. Describe the kind of opening scene you want, such as a setting, conflict, relationship dynamic, or mood."></textarea>
</label>
</div>
<div id="roleplayStarterSuggestions" class="quick-prompts" aria-label="Scenario suggestions"></div>
<div class="popup-actions">
<button id="roleplayStarterCancel" type="button" class="secondary-action">Cancel</button>
<button id="roleplayStarterConfirm" type="button">Start roleplay</button>
<button id="roleplayStarterConfirm" type="button">Create opening scene</button>
</div>
</div>
</div>
@@ -315,6 +350,24 @@
</div>
</div>
<div id="onboardingModal" class="modal-overlay hidden">
<div class="modal-card onboarding-card">
<div class="modal-header">
<div class="persona-modal-heading">
<p class="eyebrow">Get started</p>
<h3 id="onboardingTitle">What do you want to do?</h3>
</div>
<button id="onboardingSkip" type="button" class="icon-button">Skip</button>
</div>
<p id="onboardingSubtitle" class="subtitle">Pick one path to set up your first successful chat.</p>
<div id="onboardingChoices" class="onboarding-choices"></div>
<div class="popup-actions">
<button id="onboardingBack" type="button" class="secondary-action hidden">Back</button>
<button id="onboardingContinue" type="button">Continue</button>
</div>
</div>
</div>
<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>
+6 -19
View File
@@ -15,7 +15,7 @@
<div>
<p class="eyebrow">Shared personas</p>
<h1><span data-theme-name>FakeGPT</span> Market</h1>
<p class="subtitle">Browse shared AI Characters and User Personas, or manage your own AI Characters.</p>
<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>
@@ -35,7 +35,6 @@
<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" type="button" data-market-view="my-personas">My Personas</button>
</aside>
<div class="settings-content">
@@ -45,8 +44,9 @@
<div>
<p class="eyebrow">Discover</p>
<h2>AI Characters</h2>
<p class="subtitle">Find shared characters to start roleplay chats with.</p>
<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>
<div id="marketPersonaList" class="persona-list"></div>
</div>
@@ -58,26 +58,13 @@
<div>
<p class="eyebrow">Discover</p>
<h2>User Personas</h2>
<p class="subtitle">Browse shared user personas you can collect for roleplay context.</p>
<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>
<div id="marketUserPersonaList" class="persona-list"></div>
</div>
</section>
<section class="settings-view" data-market-panel="my-personas">
<div class="settings-panel">
<div class="settings-section-header">
<div>
<p class="eyebrow">Manage</p>
<h2>My Personas</h2>
<p class="subtitle">Create and manage your own AI Characters.</p>
</div>
<button id="marketCreatePersona" type="button" class="secondary-action">Create persona</button>
</div>
<div id="marketOwnedPersonaList" class="persona-list"></div>
</div>
</section>
</div>
</section>
</div>
@@ -118,7 +105,7 @@
</div>
<div class="market-preview-actions">
<label id="marketPreviewUserPersonaField" class="field market-preview-user-field hidden">
<span>Talk as</span>
<span>You in this roleplay</span>
<select id="marketPreviewUserPersonaSelect"></select>
</label>
<button id="marketPreviewConfirm" type="button" class="primary-action">Take persona</button>
+64 -106
View File
@@ -1,13 +1,13 @@
const $ = (id) => document.getElementById(id);
const marketPersonaList = $("marketPersonaList");
const marketUserPersonaList = $("marketUserPersonaList");
const marketOwnedPersonaList = $("marketOwnedPersonaList");
const refreshMarketBtn = $("refreshMarket");
const marketStatus = $("marketStatus");
const marketSearchInput = $("marketSearch");
const marketViewButtons = document.querySelectorAll("[data-market-view]");
const marketPanels = document.querySelectorAll("[data-market-panel]");
const marketCreatePersonaBtn = $("marketCreatePersona");
const marketCreateAiCharacterBtn = $("marketCreateAiCharacter");
const marketCreateUserPersonaBtn = $("marketCreateUserPersona");
const marketPreviewModal = $("marketPreviewModal");
const marketPreviewName = $("marketPreviewName");
const marketPreviewMeta = $("marketPreviewMeta");
@@ -45,9 +45,10 @@ const themes = {
let allMarketPersonas = [];
let assistantPersonas = [];
let userPersonas = [];
let publishedPersonaIds = new Set();
let pendingMarketPersona = null;
let editingPersonaId = null;
let activeUserPersonaId = null;
let activeMarketView = "ai-characters";
async function request(url, data, method = "POST") {
try {
@@ -65,7 +66,6 @@ async function request(url, data, method = "POST") {
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 applyTheme(themeKey) {
const nextTheme = themes[themeKey] ? themeKey : "fakegpt";
@@ -92,11 +92,14 @@ function setPersonaFormNotice(message = "", state = "") {
}
function setMarketView(view) {
marketViewButtons.forEach((item) => item.classList.toggle("active", item.dataset.marketView === view));
marketPanels.forEach((panel) => panel.classList.toggle("active", panel.dataset.marketPanel === view));
const normalizedView = view === "user-personas" ? "user-personas" : "ai-characters";
activeMarketView = normalizedView;
marketViewButtons.forEach((item) => item.classList.toggle("active", item.dataset.marketView === normalizedView));
marketPanels.forEach((panel) => panel.classList.toggle("active", panel.dataset.marketPanel === normalizedView));
const params = new URLSearchParams(window.location.search);
params.set("view", view);
params.set("view", normalizedView);
history.replaceState(null, "", `${window.location.pathname}?${params.toString()}`);
applyMarketFilter();
}
function buildPersonaMeta(persona) {
@@ -113,7 +116,6 @@ function closeMarketPreview() {
marketPreviewModal.classList.add("hidden");
pendingMarketPersona = null;
marketPreviewUserPersonaSelect.innerHTML = "";
marketPreviewConfirm.classList.add("hidden");
}
function populateUserPersonaChoices() {
@@ -121,7 +123,7 @@ function populateUserPersonaChoices() {
const promptOption = document.createElement("option");
promptOption.value = "";
promptOption.textContent = "Choose who you are";
promptOption.textContent = "Choose who you are in this roleplay";
promptOption.disabled = true;
promptOption.selected = true;
marketPreviewUserPersonaSelect.appendChild(promptOption);
@@ -135,6 +137,7 @@ function populateUserPersonaChoices() {
const option = document.createElement("option");
option.value = String(persona.id);
option.textContent = persona.name;
option.selected = persona.id === activeUserPersonaId;
marketPreviewUserPersonaSelect.appendChild(option);
});
@@ -156,9 +159,13 @@ function openMarketPreview(persona) {
const isAssistant = persona.persona_type === "assistant";
marketPreviewUserPersonaField.classList.toggle("hidden", !isAssistant);
marketPreviewConfirm.textContent = isAssistant ? "Start chat" : "Collect as user persona";
marketPreviewConfirm.classList.toggle("hidden", isAssistant);
if (isAssistant) populateUserPersonaChoices();
marketPreviewConfirm.textContent = isAssistant ? "Start roleplay" : "Collect persona";
if (isAssistant) {
populateUserPersonaChoices();
marketPreviewConfirm.classList.add("hidden");
} else {
marketPreviewConfirm.classList.remove("hidden");
}
marketPreviewModal.classList.remove("hidden");
}
@@ -210,9 +217,19 @@ function renderMarketList(items, listElement, type) {
meta.textContent = buildPersonaMeta(persona);
detailsBtn.type = "button";
detailsBtn.textContent = "View";
detailsBtn.textContent = "Preview";
detailsBtn.addEventListener("click", () => openMarketPreview(persona));
if (type !== "assistant") {
const quickActionBtn = document.createElement("button");
quickActionBtn.type = "button";
quickActionBtn.textContent = "Collect";
quickActionBtn.addEventListener("click", () => {
void collectMarketPersona(persona.id);
});
actions.appendChild(quickActionBtn);
}
titleRow.append(title, tag);
actions.appendChild(detailsBtn);
item.append(titleRow, meta, actions);
@@ -220,58 +237,6 @@ function renderMarketList(items, listElement, type) {
});
}
function renderOwnedPersonaList() {
marketOwnedPersonaList.innerHTML = "";
if (!assistantPersonas.length) {
const empty = document.createElement("p");
empty.className = "status persona-status";
empty.textContent = "No AI Characters yet. Create one to get started.";
marketOwnedPersonaList.appendChild(empty);
return;
}
assistantPersonas.forEach((persona) => {
const item = document.createElement("div");
const titleRow = document.createElement("div");
const title = document.createElement("h4");
const tag = document.createElement("span");
const meta = document.createElement("p");
const actions = document.createElement("div");
item.className = "persona-item";
titleRow.className = "persona-title-row";
actions.className = "persona-item-actions";
tag.className = "persona-role-tag ai";
tag.textContent = "AI Character";
title.textContent = persona.name;
meta.textContent = formatPersonaField(persona.pronouns) === "Not provided." ? "Pronouns: n/a" : `Pronouns: ${persona.pronouns}`;
[
["Edit", () => openPersonaForm(persona)],
[publishedPersonaIds.has(persona.id) ? "Update listing" : "Publish", () => publishPersona(persona.id)],
["Delete", () => deletePersona(persona.id)]
].forEach(([label, handler]) => {
const button = document.createElement("button");
button.type = "button";
button.textContent = label;
button.addEventListener("click", handler);
actions.appendChild(button);
});
if (publishedPersonaIds.has(persona.id)) {
const unpublish = document.createElement("button");
unpublish.type = "button";
unpublish.textContent = "Unpublish";
unpublish.addEventListener("click", () => unpublishPersona(persona.id));
actions.appendChild(unpublish);
}
titleRow.append(title, tag);
item.append(titleRow, meta, actions);
marketOwnedPersonaList.appendChild(item);
});
}
function filterMarketPersonas() {
const query = marketSearchInput.value.trim().toLowerCase();
if (!query) return allMarketPersonas;
@@ -283,15 +248,24 @@ function filterMarketPersonas() {
function applyMarketFilter() {
const filtered = filterMarketPersonas();
renderMarketList(filtered.filter((persona) => persona.persona_type === "assistant"), marketPersonaList, "assistant");
renderMarketList(filtered.filter((persona) => persona.persona_type === "user"), marketUserPersonaList, "user");
renderOwnedPersonaList();
const assistantItems = filtered.filter((persona) => persona.persona_type === "assistant");
const userItems = filtered.filter((persona) => persona.persona_type === "user");
renderMarketList(assistantItems, marketPersonaList, "assistant");
renderMarketList(userItems, marketUserPersonaList, "user");
if (marketSearchInput.value.trim() && !filtered.length) {
setMarketStatus("No personas match your search.", "error");
return;
}
setMarketStatus(`${filtered.length} persona${filtered.length === 1 ? "" : "s"} shown.`);
if (activeMarketView === "ai-characters") {
setMarketStatus(`${assistantItems.length} AI Character${assistantItems.length === 1 ? "" : "s"} ready for roleplay.`);
return;
}
if (activeMarketView === "user-personas") {
setMarketStatus(`${userItems.length} User Persona${userItems.length === 1 ? "" : "s"} available to collect.`);
return;
}
setMarketStatus(`${assistantItems.length + userItems.length} persona${assistantItems.length + userItems.length === 1 ? "" : "s"} shown.`);
}
async function loadOwnedPersonas() {
@@ -299,7 +273,7 @@ async function loadOwnedPersonas() {
if (res.error) return false;
assistantPersonas = res.assistantPersonas || [];
userPersonas = res.userPersonas || [];
publishedPersonaIds = new Set(res.publishedPersonaIds || []);
activeUserPersonaId = res.activeUserPersonaId || null;
return true;
}
@@ -330,8 +304,12 @@ async function collectMarketPersona(marketId) {
}
async function startMarketPersonaChat(marketId, userPersonaSelection) {
if (!userPersonaSelection) {
setMarketStatus("Choose who you are before starting roleplay.", "error");
return;
}
if (userPersonaSelection === "create_new") {
window.location.href = "/settings?view=my-personas&create=user";
window.location.href = "/settings?view=my-roleplay-companions&create=user";
return;
}
@@ -372,40 +350,10 @@ async function savePersona() {
await loadMarketPersonas();
closePersonaForm();
setMarketView("my-personas");
setMarketView("ai-characters");
setMarketStatus(editingPersonaId ? "AI Character updated." : "AI Character created.", "success");
}
async function deletePersona(id) {
const res = await del(`/personas/${id}`);
if (res.error) {
setMarketStatus(res.error, "error");
return;
}
await loadMarketPersonas();
setMarketStatus("AI Character deleted.", "success");
}
async function publishPersona(id) {
const res = await post(`/personas/${id}/publish`, {});
if (res.error) {
setMarketStatus(res.error, "error");
return;
}
await loadMarketPersonas();
setMarketStatus("AI Character published.", "success");
}
async function unpublishPersona(id) {
const res = await post(`/personas/${id}/unpublish`, {});
if (res.error) {
setMarketStatus(res.error, "error");
return;
}
await loadMarketPersonas();
setMarketStatus("AI Character unpublished.", "success");
}
refreshMarketBtn.addEventListener("click", () => {
void loadMarketPersonas();
});
@@ -413,7 +361,14 @@ marketSearchInput.addEventListener("input", applyMarketFilter);
marketViewButtons.forEach((button) => {
button.addEventListener("click", () => setMarketView(button.dataset.marketView));
});
marketCreatePersonaBtn.addEventListener("click", () => openPersonaForm());
if (marketCreateAiCharacterBtn) {
marketCreateAiCharacterBtn.addEventListener("click", () => openPersonaForm());
}
if (marketCreateUserPersonaBtn) {
marketCreateUserPersonaBtn.addEventListener("click", () => {
window.location.href = "/settings?view=my-roleplay-companions&create=user";
});
}
marketPreviewClose.addEventListener("click", closeMarketPreview);
marketPreviewModal.addEventListener("click", (event) => {
if (event.target === marketPreviewModal) closeMarketPreview();
@@ -421,7 +376,7 @@ marketPreviewModal.addEventListener("click", (event) => {
marketPreviewUserPersonaSelect.addEventListener("change", () => {
if (!pendingMarketPersona || pendingMarketPersona.persona_type !== "assistant") return;
if (marketPreviewUserPersonaSelect.value === "create_new") {
window.location.href = "/settings?view=my-personas&create=user";
window.location.href = "/settings?view=my-roleplay-companions&create=user";
return;
}
marketPreviewConfirm.classList.toggle("hidden", !marketPreviewUserPersonaSelect.value);
@@ -430,7 +385,7 @@ marketPreviewConfirm.addEventListener("click", async () => {
if (!pendingMarketPersona) return;
const marketId = pendingMarketPersona.id;
if (pendingMarketPersona.persona_type === "assistant") {
await startMarketPersonaChat(marketId, marketPreviewUserPersonaSelect.value || "");
await startMarketPersonaChat(marketId, marketPreviewUserPersonaSelect.value || "self");
closeMarketPreview();
return;
}
@@ -451,5 +406,8 @@ window.addEventListener("load", async () => {
const params = new URLSearchParams(window.location.search);
setMarketView(params.get("view") || "ai-characters");
await loadMarketPersonas();
if (params.get("create") === "assistant") openPersonaForm();
if (params.get("create") === "assistant") {
setMarketView("ai-characters");
openPersonaForm();
}
});
+327 -18
View File
@@ -9,8 +9,11 @@ const loginUsernameInput = $("loginUsername"), loginPasswordInput = $("loginPass
const registerUsernameInput = $("registerUsername"), registerPasswordInput = $("registerPassword");
const loginSubmit = $("loginSubmit"), registerSubmit = $("registerSubmit");
const chatList = $("chatList"), chatSearchInput = $("chatSearch"), newChatBtn = $("newChat");
const startRoleplayBtn = $("startRoleplay");
const renameChatBtn = $("renameChat"), clearChatBtn = $("clearChat"), exportChatBtn = $("exportChat");
const activeChatTitle = $("activeChatTitle"), sessionUser = $("sessionUser");
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");
@@ -28,8 +31,11 @@ const popupModal = $("popupModal"), popupCloseBtn = $("popupClose"), popupCancel
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;
let isProcessing = false, activeChatId = null, editingPersonaId = null, editingPersonaType = "assistant", currentUsername = "";
@@ -38,6 +44,14 @@ let activeUserPersonaId = null, currentSummary = null, publishedPersonaIds = new
let popupResolver = null, popupMode = null, popupLastFocus = null;
let roleplayPresetCharacterId = null;
let noticeTimer = null;
let workspaceMode = localStorage.getItem("krishd-workspace-mode") || "basic";
let onboardingStep = 1, onboardingIntent = "ask";
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"},
@@ -98,6 +112,90 @@ function setPersonaFormNotice(message = "", state = "") {
personaFormNotice.textContent = message;
personaFormNotice.className = state ? `status ${state}` : "status";
}
function applyWorkspaceMode(nextMode, persist = true) {
workspaceMode = nextMode === "advanced" ? "advanced" : "basic";
document.body.dataset.workspaceMode = workspaceMode;
if (persist) localStorage.setItem("krishd-workspace-mode", workspaceMode);
if (workspaceMode === "basic") {
closeModelPopover();
closePersonaPopover();
}
if (modelMenu) modelMenu.classList.toggle("hidden", workspaceMode === "basic");
if (personaMenu) personaMenu.classList.toggle("hidden", workspaceMode === "basic");
updateComposerPlaceholder();
renderQuickPrompts();
}
function hasCompletedOnboarding() {
return localStorage.getItem("krishd-onboarding-complete") === "true";
}
function markOnboardingCompleted() {
localStorage.setItem("krishd-onboarding-complete", "true");
closeOnboarding();
}
function closeOnboarding() {
if (!onboardingModal) return;
onboardingModal.classList.add("hidden");
}
function renderOnboardingStep() {
if (!onboardingChoices || !onboardingTitle || !onboardingSubtitle) return;
onboardingChoices.innerHTML = "";
onboardingBack.classList.toggle("hidden", onboardingStep === 1);
onboardingContinue.classList.toggle("hidden", onboardingStep === 2);
if (onboardingStep === 1) {
onboardingTitle.textContent = "What do you want to do?";
onboardingSubtitle.textContent = "Choose one path. You can switch anytime.";
[["ask", "Ask questions"], ["brainstorm", "Brainstorm ideas"], ["roleplay", "Roleplay with a character"]].forEach(([key, label]) => {
const button = document.createElement("button");
button.type = "button";
button.className = `secondary-action${onboardingIntent === key ? " active" : ""}`;
button.textContent = label;
button.addEventListener("click", () => {
onboardingIntent = key;
renderOnboardingStep();
});
onboardingChoices.appendChild(button);
});
return;
}
onboardingTitle.textContent = "Choose a starter";
onboardingSubtitle.textContent = "Pick one template and send your first message.";
onboardingPrompts[onboardingIntent].forEach((template) => {
const button = document.createElement("button");
button.type = "button";
button.className = "secondary-action";
button.textContent = template;
button.addEventListener("click", async () => {
if (onboardingIntent === "roleplay") {
closeOnboarding();
if (assistantPersonas.length) {
openRoleplayStarter();
} else {
openPersonaForm(null, "assistant");
}
return;
}
if (!activeChatId) await createNewChat();
msgInput.value = template;
msgInput.dispatchEvent(new Event("input"));
msgInput.focus();
closeOnboarding();
});
onboardingChoices.appendChild(button);
});
}
function maybeShowOnboarding() {
if (!onboardingModal || hasCompletedOnboarding()) return;
onboardingStep = 1;
onboardingIntent = "ask";
renderOnboardingStep();
onboardingModal.classList.remove("hidden");
}
function setMessageContent(el, content) {
const markdownParser = window.marked;
const purifier = window.DOMPurify;
@@ -163,9 +261,10 @@ 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", prompts.length === 0);
prompts.forEach((prompt) => {
quickPrompts.classList.toggle("hidden", visiblePrompts.length === 0);
visiblePrompts.forEach((prompt) => {
const button = document.createElement("button");
button.type = "button";
button.className = "prompt-chip";
@@ -180,9 +279,20 @@ function renderQuickPrompts() {
}
function updateComposerPlaceholder() {
const activeChat = getChatById(activeChatId);
msgInput.placeholder = activeChat?.assistant_persona_id
? "Reply in the scene..."
: "Type your message...";
if (!activeChat) {
msgInput.placeholder = "Create a chat to begin...";
return;
}
if (activeChat.assistant_persona_id) {
msgInput.placeholder = "Continue the scene...";
return;
}
msgInput.placeholder = workspaceMode === "basic" ? "Ask anything..." : "Type your message...";
}
function updateSendState() {
const hasText = Boolean(msgInput.value.trim());
sendBtn.disabled = isProcessing || !hasText;
}
function addMessage(content, isUser = false, isLoading = false) {
@@ -216,13 +326,78 @@ function addMessage(content, isUser = false, isLoading = false) {
function renderMessages() {
messagesDiv.innerHTML = "";
const chat = getChatById(activeChatId);
if (!currentMessages.length && (!chat || chat.assistant_persona_id)) {
renderMessageEmptyState();
} else {
currentMessages.forEach((msg) => addMessage(msg.content, msg.role === "user"));
}
updateChatParticipants();
updateChatActionState();
renderQuickPrompts();
updateComposerPlaceholder();
}
function createStarterAction(label, className, handler) {
const button = document.createElement("button");
button.type = "button";
button.className = className;
button.textContent = label;
button.addEventListener("click", handler);
return button;
}
function createMessageStarterPrompt(label, prompt) {
return createStarterAction(label, "secondary-action", async () => {
if (!activeChatId) await createNewChat();
msgInput.value = prompt;
msgInput.dispatchEvent(new Event("input"));
msgInput.focus();
});
}
function renderMessageEmptyState() {
const empty = document.createElement("section");
const title = document.createElement("h3");
const description = document.createElement("p");
const actions = document.createElement("div");
const chat = getChatById(activeChatId);
empty.className = "messages-empty";
title.className = "messages-empty-title";
description.className = "messages-empty-copy";
actions.className = "messages-empty-actions";
if (!chat) {
title.textContent = "Start your first conversation";
description.textContent = "Create a chat, or jump directly into roleplay.";
actions.append(
createStarterAction("New chat", "primary-action", () => { void createNewChat(); }),
createStarterAction("Start roleplay", "secondary-action", () => {
if (assistantPersonas.length) {
openRoleplayStarter();
return;
}
openPersonaForm(null, "assistant");
}),
createMessageStarterPrompt("Try a starter 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.";
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())
);
} else {
return;
}
empty.append(title, description, actions);
messagesDiv.appendChild(empty);
}
function updateWorkspaceCopy() {
const activeChat = getChatById(activeChatId);
if (activeChatTitle) {
@@ -255,6 +430,7 @@ function updateChatParticipants() {
chatCharacterPill.classList.add("hidden");
chatUserPersonaPill.classList.add("hidden");
chatScenePill.classList.add("hidden");
if (contextSummaryLabel) contextSummaryLabel.textContent = "None";
return;
}
if (chat.assistant_persona_id) {
@@ -274,12 +450,14 @@ function updateChatParticipants() {
} 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";
}
function updateContextRail() {
@@ -367,6 +545,14 @@ function updateChatActionState() {
exportChatBtn.disabled = !hasChat || currentMessages.length === 0;
}
function maybeShowPersonaNudge() {
if (localStorage.getItem("krishd-persona-nudge-shown") === "true") return;
const assistantOnlyChats = chatSessions.filter((chat) => !chat.assistant_persona_id).length;
if (assistantOnlyChats < 3) return;
localStorage.setItem("krishd-persona-nudge-shown", "true");
setNotice("Want to personalize responses? Try personas in Tools.", "success");
}
function renderChatList() {
const query = chatSearchInput.value.trim().toLowerCase();
const filtered = chatSessions.filter((chat) => !query || chat.title.toLowerCase().includes(query));
@@ -437,9 +623,31 @@ 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 closeContextPopover() {
if (!contextPopover || !contextChipButton) return;
contextPopover.classList.add("hidden");
contextChipButton.setAttribute("aria-expanded", "false");
}
function closeChatActionsPopover() {
if (!chatActionsPopover || !chatActionsMenuButton) return;
chatActionsPopover.classList.add("hidden");
chatActionsMenuButton.setAttribute("aria-expanded", "false");
}
function closeAuxiliaryPopovers() {
closeModelPopover();
closePersonaPopover();
closeContextPopover();
closeChatActionsPopover();
}
function populateRoleplayStarter() {
if (!roleplayCharacterSelect || !roleplayUserPersonaSelect) return;
roleplayCharacterSelect.innerHTML = "";
const placeholder = document.createElement("option");
placeholder.value = "";
placeholder.textContent = "Choose AI Character";
placeholder.disabled = true;
placeholder.selected = true;
roleplayCharacterSelect.appendChild(placeholder);
assistantPersonas.forEach((persona) => {
const option = document.createElement("option");
option.value = String(persona.id);
@@ -447,10 +655,13 @@ function populateRoleplayStarter() {
option.selected = persona.id === roleplayPresetCharacterId;
roleplayCharacterSelect.appendChild(option);
});
if (roleplayPresetCharacterId) {
roleplayCharacterSelect.value = String(roleplayPresetCharacterId);
}
roleplayUserPersonaSelect.innerHTML = "";
const noneOption = document.createElement("option");
noneOption.value = "";
noneOption.textContent = "No user persona";
noneOption.textContent = "Myself (no user persona)";
roleplayUserPersonaSelect.appendChild(noneOption);
userPersonas.forEach((persona) => {
const option = document.createElement("option");
@@ -459,11 +670,17 @@ function populateRoleplayStarter() {
option.selected = persona.id === activeUserPersonaId;
roleplayUserPersonaSelect.appendChild(option);
});
if (assistantPersonas.length === 0) {
setRoleplayStarterNotice("Create an AI Character first, then start roleplay.", "error");
roleplayStarterConfirm.disabled = true;
} else {
setRoleplayStarterNotice("");
roleplayStarterConfirm.disabled = false;
}
updateRoleplaySuggestions();
}
function openRoleplayStarter(characterId = null) {
closePersonaPopover();
closeModelPopover();
closeAuxiliaryPopovers();
roleplayPresetCharacterId = characterId;
populateRoleplayStarter();
if (roleplayScenarioInput) roleplayScenarioInput.value = "";
@@ -480,6 +697,7 @@ function closeRoleplayStarter() {
setRoleplayStarterNotice("");
}
function closeModelPopover() {
if (!modelPopover || !modelMenuButton) return;
modelPopover.classList.add("hidden");
modelMenuButton.setAttribute("aria-expanded", "false");
}
@@ -653,13 +871,15 @@ async function publishPersona(id) { const res = await post(`/personas/${id}/publ
async function unpublishPersona(id) { const res = await post(`/personas/${id}/unpublish`, {}); if (res.error) return setNotice(res.error, "error"); await loadPersonas(); setNotice("Persona unpublished.", "success"); }
function setLoadingState(loading) {
isProcessing = loading; sendBtn.disabled = loading; msgInput.disabled = loading;
isProcessing = loading;
msgInput.disabled = loading;
sendBtn.classList.toggle("loading", loading);
if (loading) {
msgInput.placeholder = "Processing response...";
} else {
updateComposerPlaceholder();
}
updateSendState();
}
function showAuthScreen(target) {
@@ -676,6 +896,7 @@ async function setActiveChat(id) {
const existing = getChatById(id);
if (res.chat && existing) Object.assign(existing, res.chat);
currentMessages = res.messages || []; renderMessages();
updateSendState();
}
async function loadChatSessions() {
@@ -686,13 +907,19 @@ async function loadChatSessions() {
const preferredChatId = (requestedChatId && getChatById(requestedChatId)) ? requestedChatId : activeChatId && getChatById(activeChatId) ? activeChatId : chatSessions[0].id;
return setActiveChat(preferredChatId);
}
return createNewChat();
activeChatId = null;
currentMessages = [];
renderMessages();
updateWorkspaceCopy();
updateSendState();
return setNotice("Pick a starting action to begin.", "success");
}
async function createNewChat() {
const res = await post("/chats", {title: "New chat"});
if (res.error || !res.chat) return setNotice(res.error || "Unable to create chat.", "error");
chatSessions.unshift(res.chat); await loadSummary(); renderChatList(); await setActiveChat(res.chat.id); setNotice("New chat created.", "success");
msgInput.focus();
}
async function startRoleplay() {
@@ -807,6 +1034,7 @@ async function checkSession() {
if (!res.user) return false;
currentUsername = res.user; authDiv.style.display = "none"; chatDiv.style.display = "block";
await Promise.all([displayModels(), loadSummary(), loadChatSessions(), loadPersonas()]);
maybeShowOnboarding();
msgInput.focus(); return true;
}
@@ -820,6 +1048,7 @@ async function handleAuth(endpoint, credentials) {
if (endpoint === "login") {
currentUsername = username; authDiv.style.display = "none"; chatDiv.style.display = "block";
await Promise.all([displayModels(), loadSummary(), loadChatSessions(), loadPersonas()]);
maybeShowOnboarding();
setNotice("Ready.", "success"); msgInput.focus();
} else {
setAuthMessage("Registration successful. You can now log in.", "success");
@@ -844,7 +1073,12 @@ async function sendMessage() {
const res = await post("/chat", {message, model: modelSelect.value, chatId: activeChatId});
if (messagesDiv.contains(loadingMsg)) messagesDiv.removeChild(loadingMsg);
if (res.reply) {
currentMessages.push({role: "bot", content: res.reply}); addMessage(res.reply); await loadSummary(); setNotice("Reply received.", "success");
currentMessages.push({role: "bot", content: res.reply});
addMessage(res.reply);
await loadSummary();
markOnboardingCompleted();
maybeShowPersonaNudge();
setNotice("Reply received.", "success");
} else {
addMessage(`[Error: ${res.error}]`); setNotice(res.error || "Failed to generate reply.", "error");
}
@@ -859,6 +1093,8 @@ async function sendMessage() {
function togglePersonaPopover(event) {
event.stopPropagation();
closeModelPopover();
closeContextPopover();
closeChatActionsPopover();
const isHidden = personaPopover.classList.contains("hidden");
personaPopover.classList.toggle("hidden", !isHidden);
personaMenuButton.setAttribute("aria-expanded", String(isHidden));
@@ -867,11 +1103,35 @@ function togglePersonaPopover(event) {
function toggleModelPopover(event) {
event.stopPropagation();
closePersonaPopover();
closeContextPopover();
closeChatActionsPopover();
const isHidden = modelPopover.classList.contains("hidden");
modelPopover.classList.toggle("hidden", !isHidden);
modelMenuButton.setAttribute("aria-expanded", String(isHidden));
}
function toggleContextPopover(event) {
if (!contextPopover || !contextChipButton) return;
event.stopPropagation();
closeModelPopover();
closePersonaPopover();
closeChatActionsPopover();
const isHidden = contextPopover.classList.contains("hidden");
contextPopover.classList.toggle("hidden", !isHidden);
contextChipButton.setAttribute("aria-expanded", String(isHidden));
}
function toggleChatActionsPopover(event) {
if (!chatActionsPopover || !chatActionsMenuButton) return;
event.stopPropagation();
closeModelPopover();
closePersonaPopover();
closeContextPopover();
const isHidden = chatActionsPopover.classList.contains("hidden");
chatActionsPopover.classList.toggle("hidden", !isHidden);
chatActionsMenuButton.setAttribute("aria-expanded", String(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()}); });
@@ -879,12 +1139,23 @@ $("logout").addEventListener("click", async () => {
await post("/logout", {}); chatDiv.style.display = "none"; authDiv.style.display = "grid";
activeChatId = null; currentUsername = ""; currentSummary = null; chatSessions = []; currentMessages = []; assistantPersonas = []; userPersonas = [];
activeUserPersonaId = null; publishedPersonaIds = new Set(); messagesDiv.innerHTML = ""; renderChatList(); updateChatActionState();
updateComposerPlaceholder();
updateSendState();
showAuthScreen("login"); setAuthMessage("Logged out.", "success"); setNotice("Ready.");
});
sendBtn.addEventListener("click", () => { void sendMessage(); });
msgInput.addEventListener("keydown", (event) => { if (event.key === "Enter" && !event.shiftKey) { event.preventDefault(); void sendMessage(); } });
msgInput.addEventListener("input", function () { this.style.height = "auto"; this.style.height = `${Math.min(this.scrollHeight, 180)}px`; });
msgInput.addEventListener("input", function () {
this.style.height = "auto";
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";
});
}
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(); });
@@ -919,19 +1190,57 @@ popupInput.addEventListener("keydown", (event) => {
closePopup(popupInput.value);
}
});
modelMenuButton.addEventListener("click", toggleModelPopover); modelPopover.addEventListener("click", (event) => event.stopPropagation());
personaMenuButton.addEventListener("click", togglePersonaPopover); personaPopover.addEventListener("click", (event) => event.stopPropagation());
document.addEventListener("click", () => { closeModelPopover(); closePersonaPopover(); });
if (modelMenuButton && modelPopover) {
modelMenuButton.addEventListener("click", toggleModelPopover);
modelPopover.addEventListener("click", (event) => event.stopPropagation());
}
if (personaMenuButton && personaPopover) {
personaMenuButton.addEventListener("click", togglePersonaPopover);
personaPopover.addEventListener("click", (event) => event.stopPropagation());
}
if (contextChipButton && contextPopover) {
contextChipButton.addEventListener("click", toggleContextPopover);
contextPopover.addEventListener("click", (event) => event.stopPropagation());
}
if (chatActionsMenuButton && chatActionsPopover) {
chatActionsMenuButton.addEventListener("click", toggleChatActionsPopover);
chatActionsPopover.addEventListener("click", (event) => event.stopPropagation());
}
if (onboardingContinue) {
onboardingContinue.addEventListener("click", () => {
onboardingStep = 2;
renderOnboardingStep();
});
}
if (onboardingBack) {
onboardingBack.addEventListener("click", () => {
onboardingStep = 1;
renderOnboardingStep();
});
}
if (onboardingSkip) {
onboardingSkip.addEventListener("click", markOnboardingCompleted);
}
if (onboardingModal) {
onboardingModal.addEventListener("click", (event) => {
if (event.target === onboardingModal) markOnboardingCompleted();
});
}
document.addEventListener("click", () => { closeAuxiliaryPopovers(); });
document.addEventListener("keydown", (event) => {
if (event.key === "Escape") {
closeModelPopover();
closePersonaPopover();
closeAuxiliaryPopovers();
closeRoleplayStarter();
closePersonaForm();
closePopup(false);
closeOnboarding();
}
});
window.addEventListener("load", async () => {
applyTheme(localStorage.getItem("krishd-theme") || "fakegpt", false);
setNotice("Ready."); updateChatActionState(); await checkSession();
applyWorkspaceMode(localStorage.getItem("krishd-workspace-mode") || "basic", false);
setNotice("Ready.");
updateChatActionState();
updateSendState();
await checkSession();
});
+15 -22
View File
@@ -25,16 +25,10 @@
<p id="settingsNotice" class="status">Loading settings...</p>
<section class="settings-quick-actions">
<button id="quickCreateAiCharacter" type="button" class="primary-action">Create AI Character</button>
<button id="quickCreateUserPersona" type="button" class="secondary-action">Create User Persona</button>
</section>
<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-ai-characters">My AI Characters</button>
<button class="settings-nav-item" type="button" data-settings-view="my-personas">My Personas</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>
</aside>
@@ -89,30 +83,22 @@
</div>
</section>
<section class="settings-view" data-settings-panel="my-ai-characters">
<section class="settings-view" data-settings-panel="my-roleplay-companions">
<div class="settings-panel">
<div class="settings-section-header">
<div>
<p class="eyebrow">Create & manage</p>
<h2>My AI Characters</h2>
<h2>My Roleplay Companions</h2>
<p class="subtitle">Build characters for roleplay chats and publish them to the market when they are ready.</p>
</div>
<div class="market-header-actions action-pair">
<button id="createAiCharacter" type="button" class="secondary-action">Create AI Character</button>
<button id="createUserPersona" type="button" class="secondary-action">Create User Persona</button>
</div>
</div>
<h3>AI Characters</h3>
<div id="settingsAiCharacterList" class="persona-list"></div>
</div>
</section>
<section class="settings-view" data-settings-panel="my-personas">
<div class="settings-panel">
<div class="settings-section-header">
<div>
<p class="eyebrow">Create & manage</p>
<h2>My Personas</h2>
<p class="subtitle">Create the personas you want to speak as, then set one as default for faster roleplay setup.</p>
</div>
<button id="createUserPersona" type="button" class="secondary-action">Create user persona</button>
</div>
<h3>User Personas</h3>
<div id="settingsUserPersonaList" class="persona-list"></div>
</div>
</section>
@@ -138,6 +124,13 @@
<option value="confusity">Confusity</option>
</select>
</label>
<label class="field">
<span>Workspace mode</span>
<select id="workspaceModeSelect">
<option value="basic">Basic</option>
<option value="advanced">Advanced</option>
</select>
</label>
</div>
</div>
</section>
+14 -16
View File
@@ -1,6 +1,7 @@
const $ = (id) => document.getElementById(id);
const settingsNotice = $("settingsNotice");
const settingsThemeSelect = $("settingsThemeSelect");
const workspaceModeSelect = $("workspaceModeSelect");
const currentUsernameInput = $("currentUsername");
const newUsernameInput = $("newUsername");
const usernamePasswordInput = $("usernamePassword");
@@ -13,8 +14,6 @@ const settingsNavItems = document.querySelectorAll("[data-settings-view]");
const settingsPanels = document.querySelectorAll("[data-settings-panel]");
const createAiCharacterBtn = $("createAiCharacter");
const createUserPersonaBtn = $("createUserPersona");
const quickCreateAiCharacterBtn = $("quickCreateAiCharacter");
const quickCreateUserPersonaBtn = $("quickCreateUserPersona");
const settingsAiCharacterList = $("settingsAiCharacterList");
const settingsUserPersonaList = $("settingsUserPersonaList");
const settingsPersonaModal = $("settingsPersonaModal");
@@ -88,10 +87,11 @@ function applyTheme(themeKey, persist = true) {
}
function setSettingsView(view) {
settingsNavItems.forEach((item) => item.classList.toggle("active", item.dataset.settingsView === view));
settingsPanels.forEach((panel) => panel.classList.toggle("active", panel.dataset.settingsPanel === view));
const normalizedView = ["my-ai-characters", "my-personas"].includes(view) ? "my-roleplay-companions" : view;
settingsNavItems.forEach((item) => item.classList.toggle("active", item.dataset.settingsView === normalizedView));
settingsPanels.forEach((panel) => panel.classList.toggle("active", panel.dataset.settingsPanel === normalizedView));
const params = new URLSearchParams(window.location.search);
params.set("view", view);
params.set("view", normalizedView);
history.replaceState(null, "", `${window.location.pathname}?${params.toString()}`);
}
@@ -248,7 +248,7 @@ async function savePersona() {
await loadPersonas();
closePersonaModal();
setSettingsView(payload.personaType === "assistant" ? "my-ai-characters" : "my-personas");
setSettingsView("my-roleplay-companions");
setNotice(wasEditing ? "Persona updated." : "Persona created.", "success");
}
@@ -329,16 +329,13 @@ settingsThemeSelect.addEventListener("change", (event) => {
applyTheme(event.target.value);
setNotice("Theme updated.", "success");
});
workspaceModeSelect.addEventListener("change", (event) => {
const mode = event.target.value === "advanced" ? "advanced" : "basic";
localStorage.setItem("krishd-workspace-mode", mode);
setNotice(`Workspace mode set to ${mode}.`, "success");
});
settingsNavItems.forEach((item) => item.addEventListener("click", () => setSettingsView(item.dataset.settingsView)));
quickCreateAiCharacterBtn.addEventListener("click", () => {
setSettingsView("my-ai-characters");
openPersonaModal(null, "assistant");
});
quickCreateUserPersonaBtn.addEventListener("click", () => {
setSettingsView("my-personas");
openPersonaModal(null, "user");
});
createAiCharacterBtn.addEventListener("click", () => openPersonaModal(null, "assistant"));
createUserPersonaBtn.addEventListener("click", () => openPersonaModal(null, "user"));
settingsPersonaClose.addEventListener("click", closePersonaModal);
@@ -353,6 +350,7 @@ settingsPersonaForm.addEventListener("submit", (event) => {
window.addEventListener("load", async () => {
applyTheme(localStorage.getItem("krishd-theme") || "fakegpt", false);
workspaceModeSelect.value = localStorage.getItem("krishd-workspace-mode") || "basic";
const params = new URLSearchParams(window.location.search);
const view = params.get("view") || "personal";
setSettingsView(view);
@@ -361,10 +359,10 @@ window.addEventListener("load", async () => {
const create = params.get("create");
if (create === "assistant") {
setSettingsView("my-ai-characters");
setSettingsView("my-roleplay-companions");
openPersonaModal(null, "assistant");
} else if (create === "user") {
setSettingsView("my-personas");
setSettingsView("my-roleplay-companions");
openPersonaModal(null, "user");
}
});
+152 -1
View File
@@ -631,6 +631,26 @@ select:focus {
padding-right: 4px;
}
.chat-loading {
display: inline-flex;
align-items: center;
gap: 10px;
padding: 10px 12px;
border-radius: 12px;
border: 1px solid rgba(24, 32, 31, 0.08);
background: rgba(24, 32, 31, 0.04);
color: var(--text-soft);
font-size: 0.9rem;
}
.chat-loading-dot {
width: 10px;
height: 10px;
border-radius: 999px;
background: var(--brand);
animation: pulse 1s ease-in-out infinite;
}
.chat-item,
.persona-item {
display: grid;
@@ -704,6 +724,24 @@ select:focus {
box-shadow: none;
}
.secondary-action.active {
background: rgba(164, 85, 45, 0.18);
border-color: rgba(164, 85, 45, 0.34);
}
.action-pair {
display: grid;
grid-template-columns: repeat(2, minmax(0, 1fr));
gap: 10px;
width: 100%;
}
.action-pair .secondary-action,
.action-pair button {
width: 100%;
min-width: 0;
}
.danger {
background: linear-gradient(145deg, #b34b40, #8f3026);
}
@@ -717,6 +755,30 @@ select:focus {
gap: 10px;
}
.tools-details {
border: 1px solid rgba(24, 32, 31, 0.08);
border-radius: 14px;
background: rgba(24, 32, 31, 0.04);
padding: 8px 10px;
}
.tools-details summary {
cursor: pointer;
font-weight: 700;
color: var(--text);
}
.tools-links {
display: grid;
gap: 8px;
margin-top: 10px;
}
.tools-details .session-chip,
.tools-details button {
margin-top: 10px;
}
.session-chip {
display: grid;
gap: 4px;
@@ -818,6 +880,34 @@ select:focus {
margin-top: 8px;
}
.chat-context {
position: relative;
margin-top: 8px;
}
.context-chip-button {
display: inline-flex;
align-items: center;
gap: 8px;
min-height: 40px;
padding: 8px 12px;
border-radius: 999px;
background: rgba(24, 32, 31, 0.06);
color: var(--text);
border: 1px solid rgba(24, 32, 31, 0.12);
}
.context-chip-button span {
font-size: 0.75rem;
letter-spacing: 0.1em;
text-transform: uppercase;
color: var(--text-muted);
}
.context-chip-button strong {
font-size: 0.9rem;
}
.sidebar-header-actions {
justify-content: flex-end;
flex-wrap: wrap;
@@ -829,6 +919,10 @@ select:focus {
align-self: end;
}
.chat-actions-menu {
position: relative;
}
.chat-header {
display: grid;
grid-template-columns: minmax(0, 1fr) auto;
@@ -1066,6 +1160,37 @@ select:focus {
min-height: 0;
}
.messages-empty {
margin: auto 0;
padding: 20px;
border-radius: 16px;
background: var(--surface-strong);
border: 1px dashed var(--line-strong);
display: grid;
gap: 10px;
}
.messages-empty-title {
font-size: 1.2rem;
}
.messages-empty-copy {
margin: 0;
color: var(--text-soft);
line-height: 1.5;
}
.messages-empty-actions {
display: flex;
flex-wrap: wrap;
gap: 10px;
}
.messages-empty-actions .primary-action,
.messages-empty-actions .secondary-action {
width: auto;
}
.msg {
max-width: min(78%, 780px);
padding: 14px 16px;
@@ -1283,7 +1408,8 @@ select:focus {
}
.chat-header-actions .model-menu,
.chat-header-actions .persona-menu {
.chat-header-actions .persona-menu,
.chat-header-actions .chat-actions-menu {
display: flex;
align-items: stretch;
align-self: stretch;
@@ -1391,6 +1517,17 @@ select:focus {
display: none !important;
}
.onboarding-card {
width: min(640px, 94vw);
display: grid;
gap: 14px;
}
.onboarding-choices {
display: grid;
gap: 10px;
}
.modal-overlay {
position: fixed;
inset: 0;
@@ -2395,6 +2532,16 @@ body[data-theme="beta-ai"] .persona-form .field {
display: grid;
grid-template-columns: 1fr;
}
.messages-empty-actions {
display: grid;
grid-template-columns: 1fr;
}
.messages-empty-actions .primary-action,
.messages-empty-actions .secondary-action {
width: 100%;
}
}
@media (max-width: 560px) {
@@ -2433,6 +2580,10 @@ body[data-theme="beta-ai"] .persona-form .field {
min-width: 0;
}
.action-pair {
grid-template-columns: 1fr;
}
.model-popover {
left: 0;
right: auto;