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>
<div class="hero-grid"> <div class="hero-grid">
<article class="hero-panel"> <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> </article>
</div> </div>
</div> </div>
@@ -80,10 +80,11 @@
<div class="sidebar-header"> <div class="sidebar-header">
<div> <div>
<p class="eyebrow">Chats</p> <p class="eyebrow">Chats</p>
<h2>Stories & chats</h2> <h2>Conversations</h2>
</div> </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="newChat" class="secondary-action" type="button">New chat</button>
<button id="startRoleplay" class="secondary-action" type="button">Start roleplay</button>
</div> </div>
</div> </div>
<label class="field compact-field"> <label class="field compact-field">
@@ -92,14 +93,26 @@
</label> </label>
</div> </div>
<div id="chatList" class="chat-list"></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"> <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> <a href="/settings" class="secondary-action">Settings</a>
</div>
</details>
<details id="accountDetails" class="tools-details">
<summary>Account</summary>
<div class="session-chip"> <div class="session-chip">
<span class="session-chip-label">Signed in as</span> <span class="session-chip-label">Signed in as</span>
<strong id="sessionUser">-</strong> <strong id="sessionUser">-</strong>
</div> </div>
<button id="logout" type="button">Logout</button> <button id="logout" type="button">Logout</button>
</details>
</div> </div>
</aside> </aside>
@@ -109,6 +122,12 @@
<div class="chat-title"> <div class="chat-title">
<p class="eyebrow">Conversation</p> <p class="eyebrow">Conversation</p>
<h2 id="activeChatTitle">New chat</h2> <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"> <div id="chatParticipants" class="chat-participants">
<span id="chatModePill" class="context-pill chat-mode-pill">Assistant mode</span> <span id="chatModePill" class="context-pill chat-mode-pill">Assistant mode</span>
<span id="chatCharacterPill" class="context-pill hidden">Character: none</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> <span id="chatScenePill" class="context-pill hidden">Scene: none</span>
</div> </div>
</div> </div>
<div class="chat-toolbar"> </div>
<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 class="chat-header-actions"> <div class="chat-header-actions">
@@ -172,6 +188,26 @@
</div> </div>
</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>
<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> </div>
</header> </header>
@@ -179,7 +215,7 @@
<div class="input-shell input-shell-focus"> <div class="input-shell input-shell-focus">
<div class="input-meta"> <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> <p id="appNotice" class="status inline-status hidden" aria-live="polite"></p>
</div> </div>
<div id="quickPrompts" class="quick-prompts" aria-label="Suggested starters"></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> <button id="roleplayStarterClose" type="button" class="icon-button">Close</button>
</div> </div>
<div class="roleplay-starter-intro"> <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> </div>
<p id="roleplayStarterNotice" class="status hidden"></p> <p id="roleplayStarterNotice" class="status hidden"></p>
<div class="roleplay-starter-actions"> <div class="roleplay-starter-actions">
<button id="roleplayNewPersona" class="secondary-action" type="button">Create AI Character</button> <button id="roleplayNewPersona" class="secondary-action" type="button">Create AI Character</button>
<a href="/settings" class="secondary-action">Settings</a>
</div> </div>
<div class="persona-form-grid"> <div class="persona-form-grid">
<label class="field field-wide"> <label class="field field-wide">
<span>Character</span> <span>1. Character</span>
<select id="roleplayCharacterSelect"></select> <select id="roleplayCharacterSelect"></select>
</label> </label>
<label class="field field-wide"> <label class="field field-wide">
<span>User persona</span> <span>2. You in the scene</span>
<select id="roleplayUserPersonaSelect"></select> <select id="roleplayUserPersonaSelect"></select>
</label> </label>
<label class="field field-wide"> <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> <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> </label>
</div> </div>
<div id="roleplayStarterSuggestions" class="quick-prompts" aria-label="Scenario suggestions"></div> <div id="roleplayStarterSuggestions" class="quick-prompts" aria-label="Scenario suggestions"></div>
<div class="popup-actions"> <div class="popup-actions">
<button id="roleplayStarterCancel" type="button" class="secondary-action">Cancel</button> <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> </div>
</div> </div>
@@ -315,6 +350,24 @@
</div> </div>
</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/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="https://cdn.jsdelivr.net/npm/dompurify@3.0.6/dist/purify.min.js" defer></script>
<script src="script.js" defer></script> <script src="script.js" defer></script>
+6 -19
View File
@@ -15,7 +15,7 @@
<div> <div>
<p class="eyebrow">Shared personas</p> <p class="eyebrow">Shared personas</p>
<h1><span data-theme-name>FakeGPT</span> Market</h1> <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>
<div class="market-header-actions"> <div class="market-header-actions">
<button id="refreshMarket" class="secondary-action" type="button">Refresh</button> <button id="refreshMarket" class="secondary-action" type="button">Refresh</button>
@@ -35,7 +35,6 @@
<aside class="settings-nav" aria-label="Market sections"> <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 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="user-personas">User Personas</button>
<button class="settings-nav-item" type="button" data-market-view="my-personas">My Personas</button>
</aside> </aside>
<div class="settings-content"> <div class="settings-content">
@@ -45,8 +44,9 @@
<div> <div>
<p class="eyebrow">Discover</p> <p class="eyebrow">Discover</p>
<h2>AI Characters</h2> <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> </div>
<button id="marketCreateAiCharacter" type="button" class="secondary-action">Create AI Character</button>
</div> </div>
<div id="marketPersonaList" class="persona-list"></div> <div id="marketPersonaList" class="persona-list"></div>
</div> </div>
@@ -58,26 +58,13 @@
<div> <div>
<p class="eyebrow">Discover</p> <p class="eyebrow">Discover</p>
<h2>User Personas</h2> <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> </div>
<button id="marketCreateUserPersona" type="button" class="secondary-action">Create User Persona</button>
</div> </div>
<div id="marketUserPersonaList" class="persona-list"></div> <div id="marketUserPersonaList" class="persona-list"></div>
</div> </div>
</section> </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> </div>
</section> </section>
</div> </div>
@@ -118,7 +105,7 @@
</div> </div>
<div class="market-preview-actions"> <div class="market-preview-actions">
<label id="marketPreviewUserPersonaField" class="field market-preview-user-field hidden"> <label id="marketPreviewUserPersonaField" class="field market-preview-user-field hidden">
<span>Talk as</span> <span>You in this roleplay</span>
<select id="marketPreviewUserPersonaSelect"></select> <select id="marketPreviewUserPersonaSelect"></select>
</label> </label>
<button id="marketPreviewConfirm" type="button" class="primary-action">Take persona</button> <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 $ = (id) => document.getElementById(id);
const marketPersonaList = $("marketPersonaList"); const marketPersonaList = $("marketPersonaList");
const marketUserPersonaList = $("marketUserPersonaList"); const marketUserPersonaList = $("marketUserPersonaList");
const marketOwnedPersonaList = $("marketOwnedPersonaList");
const refreshMarketBtn = $("refreshMarket"); const refreshMarketBtn = $("refreshMarket");
const marketStatus = $("marketStatus"); const marketStatus = $("marketStatus");
const marketSearchInput = $("marketSearch"); const marketSearchInput = $("marketSearch");
const marketViewButtons = document.querySelectorAll("[data-market-view]"); const marketViewButtons = document.querySelectorAll("[data-market-view]");
const marketPanels = document.querySelectorAll("[data-market-panel]"); const marketPanels = document.querySelectorAll("[data-market-panel]");
const marketCreatePersonaBtn = $("marketCreatePersona"); const marketCreateAiCharacterBtn = $("marketCreateAiCharacter");
const marketCreateUserPersonaBtn = $("marketCreateUserPersona");
const marketPreviewModal = $("marketPreviewModal"); const marketPreviewModal = $("marketPreviewModal");
const marketPreviewName = $("marketPreviewName"); const marketPreviewName = $("marketPreviewName");
const marketPreviewMeta = $("marketPreviewMeta"); const marketPreviewMeta = $("marketPreviewMeta");
@@ -45,9 +45,10 @@ const themes = {
let allMarketPersonas = []; let allMarketPersonas = [];
let assistantPersonas = []; let assistantPersonas = [];
let userPersonas = []; let userPersonas = [];
let publishedPersonaIds = new Set();
let pendingMarketPersona = null; let pendingMarketPersona = null;
let editingPersonaId = null; let editingPersonaId = null;
let activeUserPersonaId = null;
let activeMarketView = "ai-characters";
async function request(url, data, method = "POST") { async function request(url, data, method = "POST") {
try { try {
@@ -65,7 +66,6 @@ async function request(url, data, method = "POST") {
const get = (url) => request(url, null, "GET"); const get = (url) => request(url, null, "GET");
const post = (url, data) => request(url, data, "POST"); const post = (url, data) => request(url, data, "POST");
const put = (url, data) => request(url, data, "PUT"); const put = (url, data) => request(url, data, "PUT");
const del = (url) => request(url, null, "DELETE");
function applyTheme(themeKey) { function applyTheme(themeKey) {
const nextTheme = themes[themeKey] ? themeKey : "fakegpt"; const nextTheme = themes[themeKey] ? themeKey : "fakegpt";
@@ -92,11 +92,14 @@ function setPersonaFormNotice(message = "", state = "") {
} }
function setMarketView(view) { function setMarketView(view) {
marketViewButtons.forEach((item) => item.classList.toggle("active", item.dataset.marketView === view)); const normalizedView = view === "user-personas" ? "user-personas" : "ai-characters";
marketPanels.forEach((panel) => panel.classList.toggle("active", panel.dataset.marketPanel === 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); const params = new URLSearchParams(window.location.search);
params.set("view", view); params.set("view", normalizedView);
history.replaceState(null, "", `${window.location.pathname}?${params.toString()}`); history.replaceState(null, "", `${window.location.pathname}?${params.toString()}`);
applyMarketFilter();
} }
function buildPersonaMeta(persona) { function buildPersonaMeta(persona) {
@@ -113,7 +116,6 @@ function closeMarketPreview() {
marketPreviewModal.classList.add("hidden"); marketPreviewModal.classList.add("hidden");
pendingMarketPersona = null; pendingMarketPersona = null;
marketPreviewUserPersonaSelect.innerHTML = ""; marketPreviewUserPersonaSelect.innerHTML = "";
marketPreviewConfirm.classList.add("hidden");
} }
function populateUserPersonaChoices() { function populateUserPersonaChoices() {
@@ -121,7 +123,7 @@ function populateUserPersonaChoices() {
const promptOption = document.createElement("option"); const promptOption = document.createElement("option");
promptOption.value = ""; promptOption.value = "";
promptOption.textContent = "Choose who you are"; promptOption.textContent = "Choose who you are in this roleplay";
promptOption.disabled = true; promptOption.disabled = true;
promptOption.selected = true; promptOption.selected = true;
marketPreviewUserPersonaSelect.appendChild(promptOption); marketPreviewUserPersonaSelect.appendChild(promptOption);
@@ -135,6 +137,7 @@ function populateUserPersonaChoices() {
const option = document.createElement("option"); const option = document.createElement("option");
option.value = String(persona.id); option.value = String(persona.id);
option.textContent = persona.name; option.textContent = persona.name;
option.selected = persona.id === activeUserPersonaId;
marketPreviewUserPersonaSelect.appendChild(option); marketPreviewUserPersonaSelect.appendChild(option);
}); });
@@ -156,9 +159,13 @@ function openMarketPreview(persona) {
const isAssistant = persona.persona_type === "assistant"; const isAssistant = persona.persona_type === "assistant";
marketPreviewUserPersonaField.classList.toggle("hidden", !isAssistant); marketPreviewUserPersonaField.classList.toggle("hidden", !isAssistant);
marketPreviewConfirm.textContent = isAssistant ? "Start chat" : "Collect as user persona"; marketPreviewConfirm.textContent = isAssistant ? "Start roleplay" : "Collect persona";
marketPreviewConfirm.classList.toggle("hidden", isAssistant); if (isAssistant) {
if (isAssistant) populateUserPersonaChoices(); populateUserPersonaChoices();
marketPreviewConfirm.classList.add("hidden");
} else {
marketPreviewConfirm.classList.remove("hidden");
}
marketPreviewModal.classList.remove("hidden"); marketPreviewModal.classList.remove("hidden");
} }
@@ -210,9 +217,19 @@ function renderMarketList(items, listElement, type) {
meta.textContent = buildPersonaMeta(persona); meta.textContent = buildPersonaMeta(persona);
detailsBtn.type = "button"; detailsBtn.type = "button";
detailsBtn.textContent = "View"; detailsBtn.textContent = "Preview";
detailsBtn.addEventListener("click", () => openMarketPreview(persona)); 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); titleRow.append(title, tag);
actions.appendChild(detailsBtn); actions.appendChild(detailsBtn);
item.append(titleRow, meta, actions); 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() { function filterMarketPersonas() {
const query = marketSearchInput.value.trim().toLowerCase(); const query = marketSearchInput.value.trim().toLowerCase();
if (!query) return allMarketPersonas; if (!query) return allMarketPersonas;
@@ -283,15 +248,24 @@ function filterMarketPersonas() {
function applyMarketFilter() { function applyMarketFilter() {
const filtered = filterMarketPersonas(); const filtered = filterMarketPersonas();
renderMarketList(filtered.filter((persona) => persona.persona_type === "assistant"), marketPersonaList, "assistant"); const assistantItems = filtered.filter((persona) => persona.persona_type === "assistant");
renderMarketList(filtered.filter((persona) => persona.persona_type === "user"), marketUserPersonaList, "user"); const userItems = filtered.filter((persona) => persona.persona_type === "user");
renderOwnedPersonaList(); renderMarketList(assistantItems, marketPersonaList, "assistant");
renderMarketList(userItems, marketUserPersonaList, "user");
if (marketSearchInput.value.trim() && !filtered.length) { if (marketSearchInput.value.trim() && !filtered.length) {
setMarketStatus("No personas match your search.", "error"); setMarketStatus("No personas match your search.", "error");
return; 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() { async function loadOwnedPersonas() {
@@ -299,7 +273,7 @@ async function loadOwnedPersonas() {
if (res.error) return false; if (res.error) return false;
assistantPersonas = res.assistantPersonas || []; assistantPersonas = res.assistantPersonas || [];
userPersonas = res.userPersonas || []; userPersonas = res.userPersonas || [];
publishedPersonaIds = new Set(res.publishedPersonaIds || []); activeUserPersonaId = res.activeUserPersonaId || null;
return true; return true;
} }
@@ -330,8 +304,12 @@ async function collectMarketPersona(marketId) {
} }
async function startMarketPersonaChat(marketId, userPersonaSelection) { async function startMarketPersonaChat(marketId, userPersonaSelection) {
if (!userPersonaSelection) {
setMarketStatus("Choose who you are before starting roleplay.", "error");
return;
}
if (userPersonaSelection === "create_new") { if (userPersonaSelection === "create_new") {
window.location.href = "/settings?view=my-personas&create=user"; window.location.href = "/settings?view=my-roleplay-companions&create=user";
return; return;
} }
@@ -372,40 +350,10 @@ async function savePersona() {
await loadMarketPersonas(); await loadMarketPersonas();
closePersonaForm(); closePersonaForm();
setMarketView("my-personas"); setMarketView("ai-characters");
setMarketStatus(editingPersonaId ? "AI Character updated." : "AI Character created.", "success"); 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", () => { refreshMarketBtn.addEventListener("click", () => {
void loadMarketPersonas(); void loadMarketPersonas();
}); });
@@ -413,7 +361,14 @@ marketSearchInput.addEventListener("input", applyMarketFilter);
marketViewButtons.forEach((button) => { marketViewButtons.forEach((button) => {
button.addEventListener("click", () => setMarketView(button.dataset.marketView)); 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); marketPreviewClose.addEventListener("click", closeMarketPreview);
marketPreviewModal.addEventListener("click", (event) => { marketPreviewModal.addEventListener("click", (event) => {
if (event.target === marketPreviewModal) closeMarketPreview(); if (event.target === marketPreviewModal) closeMarketPreview();
@@ -421,7 +376,7 @@ marketPreviewModal.addEventListener("click", (event) => {
marketPreviewUserPersonaSelect.addEventListener("change", () => { marketPreviewUserPersonaSelect.addEventListener("change", () => {
if (!pendingMarketPersona || pendingMarketPersona.persona_type !== "assistant") return; if (!pendingMarketPersona || pendingMarketPersona.persona_type !== "assistant") return;
if (marketPreviewUserPersonaSelect.value === "create_new") { 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; return;
} }
marketPreviewConfirm.classList.toggle("hidden", !marketPreviewUserPersonaSelect.value); marketPreviewConfirm.classList.toggle("hidden", !marketPreviewUserPersonaSelect.value);
@@ -430,7 +385,7 @@ marketPreviewConfirm.addEventListener("click", async () => {
if (!pendingMarketPersona) return; if (!pendingMarketPersona) return;
const marketId = pendingMarketPersona.id; const marketId = pendingMarketPersona.id;
if (pendingMarketPersona.persona_type === "assistant") { if (pendingMarketPersona.persona_type === "assistant") {
await startMarketPersonaChat(marketId, marketPreviewUserPersonaSelect.value || ""); await startMarketPersonaChat(marketId, marketPreviewUserPersonaSelect.value || "self");
closeMarketPreview(); closeMarketPreview();
return; return;
} }
@@ -451,5 +406,8 @@ window.addEventListener("load", async () => {
const params = new URLSearchParams(window.location.search); const params = new URLSearchParams(window.location.search);
setMarketView(params.get("view") || "ai-characters"); setMarketView(params.get("view") || "ai-characters");
await loadMarketPersonas(); 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 registerUsernameInput = $("registerUsername"), registerPasswordInput = $("registerPassword");
const loginSubmit = $("loginSubmit"), registerSubmit = $("registerSubmit"); const loginSubmit = $("loginSubmit"), registerSubmit = $("registerSubmit");
const chatList = $("chatList"), chatSearchInput = $("chatSearch"), newChatBtn = $("newChat"); const chatList = $("chatList"), chatSearchInput = $("chatSearch"), newChatBtn = $("newChat");
const startRoleplayBtn = $("startRoleplay");
const renameChatBtn = $("renameChat"), clearChatBtn = $("clearChat"), exportChatBtn = $("exportChat"); const renameChatBtn = $("renameChat"), clearChatBtn = $("clearChat"), exportChatBtn = $("exportChat");
const activeChatTitle = $("activeChatTitle"), sessionUser = $("sessionUser"); 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 chatModePill = $("chatModePill"), chatCharacterPill = $("chatCharacterPill"), chatUserPersonaPill = $("chatUserPersonaPill"), chatScenePill = $("chatScenePill");
const personaList = $("personaList"), userPersonaList = $("userPersonaList"); const personaList = $("personaList"), userPersonaList = $("userPersonaList");
const personaForm = $("personaForm"), personaFormTitle = $("personaFormTitle"), personaTypeSelect = $("personaType"); 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 popupTitle = $("popupTitle"), popupEyebrow = $("popupEyebrow"), popupDescription = $("popupDescription"), popupField = $("popupField");
const popupInputLabel = $("popupInputLabel"), popupInput = $("popupInput"); const popupInputLabel = $("popupInputLabel"), popupInput = $("popupInput");
const appNotice = $("appNotice"), quickPrompts = $("quickPrompts"); 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 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 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; const requestedChatId = Number(new URLSearchParams(window.location.search).get("chat")) || null;
let isProcessing = false, activeChatId = null, editingPersonaId = null, editingPersonaType = "assistant", currentUsername = ""; 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 popupResolver = null, popupMode = null, popupLastFocus = null;
let roleplayPresetCharacterId = null; let roleplayPresetCharacterId = null;
let noticeTimer = 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 = { const themes = {
"fakegpt": {name: "FakeGPT", short: "FG"}, "fakegpt": {name: "FakeGPT", short: "FG"},
@@ -98,6 +112,90 @@ function setPersonaFormNotice(message = "", state = "") {
personaFormNotice.textContent = message; personaFormNotice.textContent = message;
personaFormNotice.className = state ? `status ${state}` : "status"; 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) { function setMessageContent(el, content) {
const markdownParser = window.marked; const markdownParser = window.marked;
const purifier = window.DOMPurify; const purifier = window.DOMPurify;
@@ -163,9 +261,10 @@ function renderQuickPrompts() {
if (!quickPrompts) return; if (!quickPrompts) return;
const activeChat = getChatById(activeChatId); const activeChat = getChatById(activeChatId);
const prompts = getQuickPromptsForChat(activeChat); const prompts = getQuickPromptsForChat(activeChat);
const visiblePrompts = workspaceMode === "basic" ? prompts.slice(0, 3) : prompts;
quickPrompts.innerHTML = ""; quickPrompts.innerHTML = "";
quickPrompts.classList.toggle("hidden", prompts.length === 0); quickPrompts.classList.toggle("hidden", visiblePrompts.length === 0);
prompts.forEach((prompt) => { visiblePrompts.forEach((prompt) => {
const button = document.createElement("button"); const button = document.createElement("button");
button.type = "button"; button.type = "button";
button.className = "prompt-chip"; button.className = "prompt-chip";
@@ -180,9 +279,20 @@ function renderQuickPrompts() {
} }
function updateComposerPlaceholder() { function updateComposerPlaceholder() {
const activeChat = getChatById(activeChatId); const activeChat = getChatById(activeChatId);
msgInput.placeholder = activeChat?.assistant_persona_id if (!activeChat) {
? "Reply in the scene..." msgInput.placeholder = "Create a chat to begin...";
: "Type your message..."; 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) { function addMessage(content, isUser = false, isLoading = false) {
@@ -216,13 +326,78 @@ function addMessage(content, isUser = false, isLoading = false) {
function renderMessages() { function renderMessages() {
messagesDiv.innerHTML = ""; 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")); currentMessages.forEach((msg) => addMessage(msg.content, msg.role === "user"));
}
updateChatParticipants(); updateChatParticipants();
updateChatActionState(); updateChatActionState();
renderQuickPrompts(); renderQuickPrompts();
updateComposerPlaceholder(); 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() { function updateWorkspaceCopy() {
const activeChat = getChatById(activeChatId); const activeChat = getChatById(activeChatId);
if (activeChatTitle) { if (activeChatTitle) {
@@ -255,6 +430,7 @@ function updateChatParticipants() {
chatCharacterPill.classList.add("hidden"); chatCharacterPill.classList.add("hidden");
chatUserPersonaPill.classList.add("hidden"); chatUserPersonaPill.classList.add("hidden");
chatScenePill.classList.add("hidden"); chatScenePill.classList.add("hidden");
if (contextSummaryLabel) contextSummaryLabel.textContent = "None";
return; return;
} }
if (chat.assistant_persona_id) { if (chat.assistant_persona_id) {
@@ -274,12 +450,14 @@ function updateChatParticipants() {
} else { } else {
chatScenePill.classList.add("hidden"); chatScenePill.classList.add("hidden");
} }
if (contextSummaryLabel) contextSummaryLabel.textContent = chat.assistant_persona_name || "Roleplay";
return; return;
} }
chatModePill.textContent = "Assistant mode"; chatModePill.textContent = "Assistant mode";
chatCharacterPill.classList.add("hidden"); chatCharacterPill.classList.add("hidden");
chatUserPersonaPill.classList.add("hidden"); chatUserPersonaPill.classList.add("hidden");
chatScenePill.classList.add("hidden"); chatScenePill.classList.add("hidden");
if (contextSummaryLabel) contextSummaryLabel.textContent = "Assistant";
} }
function updateContextRail() { function updateContextRail() {
@@ -367,6 +545,14 @@ function updateChatActionState() {
exportChatBtn.disabled = !hasChat || currentMessages.length === 0; 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() { function renderChatList() {
const query = chatSearchInput.value.trim().toLowerCase(); 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));
@@ -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 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() { 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() { function populateRoleplayStarter() {
if (!roleplayCharacterSelect || !roleplayUserPersonaSelect) return; if (!roleplayCharacterSelect || !roleplayUserPersonaSelect) return;
roleplayCharacterSelect.innerHTML = ""; 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) => { assistantPersonas.forEach((persona) => {
const option = document.createElement("option"); const option = document.createElement("option");
option.value = String(persona.id); option.value = String(persona.id);
@@ -447,10 +655,13 @@ function populateRoleplayStarter() {
option.selected = persona.id === roleplayPresetCharacterId; option.selected = persona.id === roleplayPresetCharacterId;
roleplayCharacterSelect.appendChild(option); roleplayCharacterSelect.appendChild(option);
}); });
if (roleplayPresetCharacterId) {
roleplayCharacterSelect.value = String(roleplayPresetCharacterId);
}
roleplayUserPersonaSelect.innerHTML = ""; roleplayUserPersonaSelect.innerHTML = "";
const noneOption = document.createElement("option"); const noneOption = document.createElement("option");
noneOption.value = ""; noneOption.value = "";
noneOption.textContent = "No user persona"; noneOption.textContent = "Myself (no user persona)";
roleplayUserPersonaSelect.appendChild(noneOption); roleplayUserPersonaSelect.appendChild(noneOption);
userPersonas.forEach((persona) => { userPersonas.forEach((persona) => {
const option = document.createElement("option"); const option = document.createElement("option");
@@ -459,11 +670,17 @@ function populateRoleplayStarter() {
option.selected = persona.id === activeUserPersonaId; option.selected = persona.id === activeUserPersonaId;
roleplayUserPersonaSelect.appendChild(option); 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(); updateRoleplaySuggestions();
} }
function openRoleplayStarter(characterId = null) { function openRoleplayStarter(characterId = null) {
closePersonaPopover(); closeAuxiliaryPopovers();
closeModelPopover();
roleplayPresetCharacterId = characterId; roleplayPresetCharacterId = characterId;
populateRoleplayStarter(); populateRoleplayStarter();
if (roleplayScenarioInput) roleplayScenarioInput.value = ""; if (roleplayScenarioInput) roleplayScenarioInput.value = "";
@@ -480,6 +697,7 @@ function closeRoleplayStarter() {
setRoleplayStarterNotice(""); setRoleplayStarterNotice("");
} }
function closeModelPopover() { function closeModelPopover() {
if (!modelPopover || !modelMenuButton) return;
modelPopover.classList.add("hidden"); modelPopover.classList.add("hidden");
modelMenuButton.setAttribute("aria-expanded", "false"); 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"); } 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) { function setLoadingState(loading) {
isProcessing = loading; sendBtn.disabled = loading; msgInput.disabled = loading; isProcessing = loading;
msgInput.disabled = loading;
sendBtn.classList.toggle("loading", loading); sendBtn.classList.toggle("loading", loading);
if (loading) { if (loading) {
msgInput.placeholder = "Processing response..."; msgInput.placeholder = "Processing response...";
} else { } else {
updateComposerPlaceholder(); updateComposerPlaceholder();
} }
updateSendState();
} }
function showAuthScreen(target) { function showAuthScreen(target) {
@@ -676,6 +896,7 @@ async function setActiveChat(id) {
const existing = getChatById(id); const existing = getChatById(id);
if (res.chat && existing) Object.assign(existing, res.chat); if (res.chat && existing) Object.assign(existing, res.chat);
currentMessages = res.messages || []; renderMessages(); currentMessages = res.messages || []; renderMessages();
updateSendState();
} }
async function loadChatSessions() { async function loadChatSessions() {
@@ -686,13 +907,19 @@ async function loadChatSessions() {
const preferredChatId = (requestedChatId && getChatById(requestedChatId)) ? requestedChatId : activeChatId && getChatById(activeChatId) ? activeChatId : chatSessions[0].id; const preferredChatId = (requestedChatId && getChatById(requestedChatId)) ? requestedChatId : activeChatId && getChatById(activeChatId) ? activeChatId : chatSessions[0].id;
return setActiveChat(preferredChatId); return setActiveChat(preferredChatId);
} }
return createNewChat(); activeChatId = null;
currentMessages = [];
renderMessages();
updateWorkspaceCopy();
updateSendState();
return setNotice("Pick a starting action to begin.", "success");
} }
async function createNewChat() { async function createNewChat() {
const res = await post("/chats", {title: "New chat"}); const res = await post("/chats", {title: "New chat"});
if (res.error || !res.chat) return setNotice(res.error || "Unable to create chat.", "error"); 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"); chatSessions.unshift(res.chat); await loadSummary(); renderChatList(); await setActiveChat(res.chat.id); setNotice("New chat created.", "success");
msgInput.focus();
} }
async function startRoleplay() { async function startRoleplay() {
@@ -807,6 +1034,7 @@ async function checkSession() {
if (!res.user) return false; if (!res.user) return false;
currentUsername = res.user; authDiv.style.display = "none"; chatDiv.style.display = "block"; currentUsername = res.user; authDiv.style.display = "none"; chatDiv.style.display = "block";
await Promise.all([displayModels(), loadSummary(), loadChatSessions(), loadPersonas()]); await Promise.all([displayModels(), loadSummary(), loadChatSessions(), loadPersonas()]);
maybeShowOnboarding();
msgInput.focus(); return true; msgInput.focus(); return true;
} }
@@ -820,6 +1048,7 @@ async function handleAuth(endpoint, credentials) {
if (endpoint === "login") { if (endpoint === "login") {
currentUsername = username; authDiv.style.display = "none"; chatDiv.style.display = "block"; currentUsername = username; authDiv.style.display = "none"; chatDiv.style.display = "block";
await Promise.all([displayModels(), loadSummary(), loadChatSessions(), loadPersonas()]); await Promise.all([displayModels(), loadSummary(), loadChatSessions(), loadPersonas()]);
maybeShowOnboarding();
setNotice("Ready.", "success"); msgInput.focus(); setNotice("Ready.", "success"); msgInput.focus();
} else { } else {
setAuthMessage("Registration successful. You can now log in.", "success"); 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}); const res = await post("/chat", {message, model: modelSelect.value, chatId: activeChatId});
if (messagesDiv.contains(loadingMsg)) messagesDiv.removeChild(loadingMsg); if (messagesDiv.contains(loadingMsg)) messagesDiv.removeChild(loadingMsg);
if (res.reply) { 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 { } else {
addMessage(`[Error: ${res.error}]`); setNotice(res.error || "Failed to generate reply.", "error"); addMessage(`[Error: ${res.error}]`); setNotice(res.error || "Failed to generate reply.", "error");
} }
@@ -859,6 +1093,8 @@ async function sendMessage() {
function togglePersonaPopover(event) { function togglePersonaPopover(event) {
event.stopPropagation(); event.stopPropagation();
closeModelPopover(); closeModelPopover();
closeContextPopover();
closeChatActionsPopover();
const isHidden = personaPopover.classList.contains("hidden"); const isHidden = personaPopover.classList.contains("hidden");
personaPopover.classList.toggle("hidden", !isHidden); personaPopover.classList.toggle("hidden", !isHidden);
personaMenuButton.setAttribute("aria-expanded", String(isHidden)); personaMenuButton.setAttribute("aria-expanded", String(isHidden));
@@ -867,11 +1103,35 @@ function togglePersonaPopover(event) {
function toggleModelPopover(event) { function toggleModelPopover(event) {
event.stopPropagation(); event.stopPropagation();
closePersonaPopover(); closePersonaPopover();
closeContextPopover();
closeChatActionsPopover();
const isHidden = modelPopover.classList.contains("hidden"); const isHidden = modelPopover.classList.contains("hidden");
modelPopover.classList.toggle("hidden", !isHidden); modelPopover.classList.toggle("hidden", !isHidden);
modelMenuButton.setAttribute("aria-expanded", String(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))); 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()}); }); 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()}); }); 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"; await post("/logout", {}); chatDiv.style.display = "none"; authDiv.style.display = "grid";
activeChatId = null; currentUsername = ""; currentSummary = null; chatSessions = []; currentMessages = []; assistantPersonas = []; userPersonas = []; activeChatId = null; currentUsername = ""; currentSummary = null; chatSessions = []; currentMessages = []; assistantPersonas = []; userPersonas = [];
activeUserPersonaId = null; publishedPersonaIds = new Set(); messagesDiv.innerHTML = ""; renderChatList(); updateChatActionState(); activeUserPersonaId = null; publishedPersonaIds = new Set(); messagesDiv.innerHTML = ""; renderChatList(); updateChatActionState();
updateComposerPlaceholder();
updateSendState();
showAuthScreen("login"); setAuthMessage("Logged out.", "success"); setNotice("Ready."); showAuthScreen("login"); setAuthMessage("Logged out.", "success"); setNotice("Ready.");
}); });
sendBtn.addEventListener("click", () => { void sendMessage(); }); sendBtn.addEventListener("click", () => { void sendMessage(); });
msgInput.addEventListener("keydown", (event) => { if (event.key === "Enter" && !event.shiftKey) { event.preventDefault(); 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(); }); 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); }); renameChatBtn.addEventListener("click", () => { if (activeChatId) void renameChat(activeChatId); });
clearChatBtn.addEventListener("click", () => { if (activeChatId) void clearChat(activeChatId); }); exportChatBtn.addEventListener("click", exportCurrentChat); clearChatBtn.addEventListener("click", () => { if (activeChatId) void clearChat(activeChatId); }); exportChatBtn.addEventListener("click", exportCurrentChat);
chatSearchInput.addEventListener("input", renderChatList); modelSelect.addEventListener("change", () => { updateModelHelp(); updateContextRail(); }); chatSearchInput.addEventListener("input", renderChatList); modelSelect.addEventListener("change", () => { updateModelHelp(); updateContextRail(); });
@@ -919,19 +1190,57 @@ popupInput.addEventListener("keydown", (event) => {
closePopup(popupInput.value); closePopup(popupInput.value);
} }
}); });
modelMenuButton.addEventListener("click", toggleModelPopover); modelPopover.addEventListener("click", (event) => event.stopPropagation()); if (modelMenuButton && modelPopover) {
personaMenuButton.addEventListener("click", togglePersonaPopover); personaPopover.addEventListener("click", (event) => event.stopPropagation()); modelMenuButton.addEventListener("click", toggleModelPopover);
document.addEventListener("click", () => { closeModelPopover(); closePersonaPopover(); }); 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) => { document.addEventListener("keydown", (event) => {
if (event.key === "Escape") { if (event.key === "Escape") {
closeModelPopover(); closeAuxiliaryPopovers();
closePersonaPopover();
closeRoleplayStarter(); closeRoleplayStarter();
closePersonaForm(); closePersonaForm();
closePopup(false); closePopup(false);
closeOnboarding();
} }
}); });
window.addEventListener("load", async () => { window.addEventListener("load", async () => {
applyTheme(localStorage.getItem("krishd-theme") || "fakegpt", false); 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> <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"> <section class="settings-layout">
<aside class="settings-nav" aria-label="Settings sections"> <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 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-roleplay-companions">My Roleplay Companions</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="design">Design</button> <button class="settings-nav-item" type="button" data-settings-view="design">Design</button>
</aside> </aside>
@@ -89,30 +83,22 @@
</div> </div>
</section> </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-panel">
<div class="settings-section-header"> <div class="settings-section-header">
<div> <div>
<p class="eyebrow">Create & manage</p> <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> <p class="subtitle">Build characters for roleplay chats and publish them to the market when they are ready.</p>
</div> </div>
<div class="market-header-actions action-pair">
<button id="createAiCharacter" type="button" class="secondary-action">Create AI Character</button> <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>
</div>
<h3>AI Characters</h3>
<div id="settingsAiCharacterList" class="persona-list"></div> <div id="settingsAiCharacterList" class="persona-list"></div>
</div> <h3>User Personas</h3>
</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>
<div id="settingsUserPersonaList" class="persona-list"></div> <div id="settingsUserPersonaList" class="persona-list"></div>
</div> </div>
</section> </section>
@@ -138,6 +124,13 @@
<option value="confusity">Confusity</option> <option value="confusity">Confusity</option>
</select> </select>
</label> </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>
</div> </div>
</section> </section>
+14 -16
View File
@@ -1,6 +1,7 @@
const $ = (id) => document.getElementById(id); const $ = (id) => document.getElementById(id);
const settingsNotice = $("settingsNotice"); const settingsNotice = $("settingsNotice");
const settingsThemeSelect = $("settingsThemeSelect"); const settingsThemeSelect = $("settingsThemeSelect");
const workspaceModeSelect = $("workspaceModeSelect");
const currentUsernameInput = $("currentUsername"); const currentUsernameInput = $("currentUsername");
const newUsernameInput = $("newUsername"); const newUsernameInput = $("newUsername");
const usernamePasswordInput = $("usernamePassword"); const usernamePasswordInput = $("usernamePassword");
@@ -13,8 +14,6 @@ const settingsNavItems = document.querySelectorAll("[data-settings-view]");
const settingsPanels = document.querySelectorAll("[data-settings-panel]"); const settingsPanels = document.querySelectorAll("[data-settings-panel]");
const createAiCharacterBtn = $("createAiCharacter"); const createAiCharacterBtn = $("createAiCharacter");
const createUserPersonaBtn = $("createUserPersona"); const createUserPersonaBtn = $("createUserPersona");
const quickCreateAiCharacterBtn = $("quickCreateAiCharacter");
const quickCreateUserPersonaBtn = $("quickCreateUserPersona");
const settingsAiCharacterList = $("settingsAiCharacterList"); const settingsAiCharacterList = $("settingsAiCharacterList");
const settingsUserPersonaList = $("settingsUserPersonaList"); const settingsUserPersonaList = $("settingsUserPersonaList");
const settingsPersonaModal = $("settingsPersonaModal"); const settingsPersonaModal = $("settingsPersonaModal");
@@ -88,10 +87,11 @@ function applyTheme(themeKey, persist = true) {
} }
function setSettingsView(view) { function setSettingsView(view) {
settingsNavItems.forEach((item) => item.classList.toggle("active", item.dataset.settingsView === view)); const normalizedView = ["my-ai-characters", "my-personas"].includes(view) ? "my-roleplay-companions" : view;
settingsPanels.forEach((panel) => panel.classList.toggle("active", panel.dataset.settingsPanel === 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); const params = new URLSearchParams(window.location.search);
params.set("view", view); params.set("view", normalizedView);
history.replaceState(null, "", `${window.location.pathname}?${params.toString()}`); history.replaceState(null, "", `${window.location.pathname}?${params.toString()}`);
} }
@@ -248,7 +248,7 @@ async function savePersona() {
await loadPersonas(); await loadPersonas();
closePersonaModal(); closePersonaModal();
setSettingsView(payload.personaType === "assistant" ? "my-ai-characters" : "my-personas"); setSettingsView("my-roleplay-companions");
setNotice(wasEditing ? "Persona updated." : "Persona created.", "success"); setNotice(wasEditing ? "Persona updated." : "Persona created.", "success");
} }
@@ -329,16 +329,13 @@ settingsThemeSelect.addEventListener("change", (event) => {
applyTheme(event.target.value); applyTheme(event.target.value);
setNotice("Theme updated.", "success"); 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))); 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")); createAiCharacterBtn.addEventListener("click", () => openPersonaModal(null, "assistant"));
createUserPersonaBtn.addEventListener("click", () => openPersonaModal(null, "user")); createUserPersonaBtn.addEventListener("click", () => openPersonaModal(null, "user"));
settingsPersonaClose.addEventListener("click", closePersonaModal); settingsPersonaClose.addEventListener("click", closePersonaModal);
@@ -353,6 +350,7 @@ settingsPersonaForm.addEventListener("submit", (event) => {
window.addEventListener("load", async () => { window.addEventListener("load", async () => {
applyTheme(localStorage.getItem("krishd-theme") || "fakegpt", false); applyTheme(localStorage.getItem("krishd-theme") || "fakegpt", false);
workspaceModeSelect.value = localStorage.getItem("krishd-workspace-mode") || "basic";
const params = new URLSearchParams(window.location.search); const params = new URLSearchParams(window.location.search);
const view = params.get("view") || "personal"; const view = params.get("view") || "personal";
setSettingsView(view); setSettingsView(view);
@@ -361,10 +359,10 @@ window.addEventListener("load", async () => {
const create = params.get("create"); const create = params.get("create");
if (create === "assistant") { if (create === "assistant") {
setSettingsView("my-ai-characters"); setSettingsView("my-roleplay-companions");
openPersonaModal(null, "assistant"); openPersonaModal(null, "assistant");
} else if (create === "user") { } else if (create === "user") {
setSettingsView("my-personas"); setSettingsView("my-roleplay-companions");
openPersonaModal(null, "user"); openPersonaModal(null, "user");
} }
}); });
+152 -1
View File
@@ -631,6 +631,26 @@ select:focus {
padding-right: 4px; 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, .chat-item,
.persona-item { .persona-item {
display: grid; display: grid;
@@ -704,6 +724,24 @@ select:focus {
box-shadow: none; 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 { .danger {
background: linear-gradient(145deg, #b34b40, #8f3026); background: linear-gradient(145deg, #b34b40, #8f3026);
} }
@@ -717,6 +755,30 @@ select:focus {
gap: 10px; 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 { .session-chip {
display: grid; display: grid;
gap: 4px; gap: 4px;
@@ -818,6 +880,34 @@ select:focus {
margin-top: 8px; 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 { .sidebar-header-actions {
justify-content: flex-end; justify-content: flex-end;
flex-wrap: wrap; flex-wrap: wrap;
@@ -829,6 +919,10 @@ select:focus {
align-self: end; align-self: end;
} }
.chat-actions-menu {
position: relative;
}
.chat-header { .chat-header {
display: grid; display: grid;
grid-template-columns: minmax(0, 1fr) auto; grid-template-columns: minmax(0, 1fr) auto;
@@ -1066,6 +1160,37 @@ select:focus {
min-height: 0; 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 { .msg {
max-width: min(78%, 780px); max-width: min(78%, 780px);
padding: 14px 16px; padding: 14px 16px;
@@ -1283,7 +1408,8 @@ select:focus {
} }
.chat-header-actions .model-menu, .chat-header-actions .model-menu,
.chat-header-actions .persona-menu { .chat-header-actions .persona-menu,
.chat-header-actions .chat-actions-menu {
display: flex; display: flex;
align-items: stretch; align-items: stretch;
align-self: stretch; align-self: stretch;
@@ -1391,6 +1517,17 @@ select:focus {
display: none !important; display: none !important;
} }
.onboarding-card {
width: min(640px, 94vw);
display: grid;
gap: 14px;
}
.onboarding-choices {
display: grid;
gap: 10px;
}
.modal-overlay { .modal-overlay {
position: fixed; position: fixed;
inset: 0; inset: 0;
@@ -2395,6 +2532,16 @@ body[data-theme="beta-ai"] .persona-form .field {
display: grid; display: grid;
grid-template-columns: 1fr; 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) { @media (max-width: 560px) {
@@ -2433,6 +2580,10 @@ body[data-theme="beta-ai"] .persona-form .field {
min-width: 0; min-width: 0;
} }
.action-pair {
grid-template-columns: 1fr;
}
.model-popover { .model-popover {
left: 0; left: 0;
right: auto; right: auto;