try to make app more intuitive

This commit is contained in:
2026-04-09 22:53:19 +02:00
parent 602e29e54b
commit 057f59ceef
6 changed files with 344 additions and 39 deletions
+9 -1
View File
@@ -113,6 +113,7 @@
<span id="chatModePill" class="context-pill chat-mode-pill">Assistant mode</span>
<span id="chatCharacterPill" class="context-pill hidden">Character: none</span>
<span id="chatUserPersonaPill" class="context-pill hidden">User persona: none</span>
<span id="chatScenePill" class="context-pill hidden">Scene: none</span>
</div>
</div>
<div class="chat-toolbar">
@@ -179,7 +180,9 @@
<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 id="appNotice" class="status inline-status hidden" aria-live="polite"></p>
</div>
<div id="quickPrompts" class="quick-prompts" aria-label="Suggested starters"></div>
<div class="input-container">
<textarea id="msgInput" placeholder="Type your message..."></textarea>
<button id="send" type="button" aria-label="Send message"></button>
@@ -264,7 +267,7 @@
</div>
<p id="roleplayStarterNotice" class="status hidden"></p>
<div class="roleplay-starter-actions">
<button id="roleplayNewPersona" class="secondary-action" type="button">New persona</button>
<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">
@@ -276,7 +279,12 @@
<span>User persona</span>
<select id="roleplayUserPersonaSelect"></select>
</label>
<label class="field field-wide">
<span>Scenario seed</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>
+133 -9
View File
@@ -11,7 +11,7 @@ const loginSubmit = $("loginSubmit"), registerSubmit = $("registerSubmit");
const chatList = $("chatList"), chatSearchInput = $("chatSearch"), newChatBtn = $("newChat");
const renameChatBtn = $("renameChat"), clearChatBtn = $("clearChat"), exportChatBtn = $("exportChat");
const activeChatTitle = $("activeChatTitle"), sessionUser = $("sessionUser");
const chatModePill = $("chatModePill"), chatCharacterPill = $("chatCharacterPill"), chatUserPersonaPill = $("chatUserPersonaPill");
const chatModePill = $("chatModePill"), chatCharacterPill = $("chatCharacterPill"), chatUserPersonaPill = $("chatUserPersonaPill"), chatScenePill = $("chatScenePill");
const personaList = $("personaList"), userPersonaList = $("userPersonaList");
const personaForm = $("personaForm"), personaFormTitle = $("personaFormTitle"), personaTypeSelect = $("personaType");
const personaNameInput = $("personaName"), personaPronounsInput = $("personaPronouns"), personaAppearanceInput = $("personaAppearance");
@@ -23,9 +23,11 @@ const activePersonaStatus = $("activePersonaStatus"), activeUserPersonaStatus =
const personaModal = $("personaModal"), personaCloseBtn = $("personaClose"), personaMenuButton = $("personaMenuButton"), personaPopover = $("personaPopover");
const roleplayStarterModal = $("roleplayStarterModal"), roleplayStarterClose = $("roleplayStarterClose"), roleplayStarterCancel = $("roleplayStarterCancel"), roleplayStarterConfirm = $("roleplayStarterConfirm");
const roleplayStarterNotice = $("roleplayStarterNotice"), roleplayCharacterSelect = $("roleplayCharacterSelect"), roleplayUserPersonaSelect = $("roleplayUserPersonaSelect");
const roleplayScenarioInput = $("roleplayScenarioInput"), roleplayStarterSuggestions = $("roleplayStarterSuggestions");
const popupModal = $("popupModal"), popupCloseBtn = $("popupClose"), popupCancelBtn = $("popupCancel"), popupConfirmBtn = $("popupConfirm");
const popupTitle = $("popupTitle"), popupEyebrow = $("popupEyebrow"), popupDescription = $("popupDescription"), popupField = $("popupField");
const popupInputLabel = $("popupInputLabel"), popupInput = $("popupInput");
const appNotice = $("appNotice"), quickPrompts = $("quickPrompts");
const 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 requestedChatId = Number(new URLSearchParams(window.location.search).get("chat")) || null;
@@ -35,6 +37,7 @@ let chatSessions = [], currentMessages = [], assistantPersonas = [], userPersona
let activeUserPersonaId = null, currentSummary = null, publishedPersonaIds = new Set();
let popupResolver = null, popupMode = null, popupLastFocus = null;
let roleplayPresetCharacterId = null;
let noticeTimer = null;
const themes = {
"fakegpt": {name: "FakeGPT", short: "FG"},
@@ -64,7 +67,27 @@ const put = (url, data) => request(url, data, "PUT");
const del = (url) => request(url, null, "DELETE");
function setAuthMessage(message, state = "") { authMsg.textContent = message; authMsg.className = state ? `status ${state}` : "status"; }
function setNotice(message, state = "") { return {message, state}; }
function setNotice(message = "", state = "") {
if (!appNotice) return {message, state};
if (noticeTimer) {
clearTimeout(noticeTimer);
noticeTimer = null;
}
if (!message) {
appNotice.textContent = "";
appNotice.className = "status inline-status hidden";
return {message, state};
}
appNotice.textContent = message;
appNotice.className = state ? `status inline-status ${state}` : "status inline-status";
noticeTimer = window.setTimeout(() => {
if (appNotice.textContent === message) {
appNotice.textContent = "";
appNotice.className = "status inline-status hidden";
}
}, state === "error" ? 6000 : 3200);
return {message, state};
}
function setPersonaFormNotice(message = "", state = "") {
if (!personaFormNotice) return;
if (!message) {
@@ -85,6 +108,82 @@ function setMessageContent(el, content) {
}
}
function getChatById(id) { return chatSessions.find((chat) => chat.id === id); }
function summarizeText(value, limit = 96) {
const normalized = String(value || "").replace(/\s+/g, " ").trim();
if (!normalized) return "";
return normalized.length > limit ? `${normalized.slice(0, limit - 3).trimEnd()}...` : normalized;
}
function getPersonaName(id, items) {
return items.find((persona) => persona.id === id)?.name || "";
}
function getRoleplayStarterSuggestions() {
const assistantPersonaId = Number(roleplayCharacterSelect?.value);
const userPersonaId = roleplayUserPersonaSelect?.value ? Number(roleplayUserPersonaSelect.value) : null;
const assistantName = getPersonaName(assistantPersonaId, assistantPersonas) || "the character";
const userName = userPersonaId ? getPersonaName(userPersonaId, userPersonas) : "me";
return [
`${assistantName} unexpectedly crosses paths with ${userName} in a place that matters to them both.`,
`${assistantName} already knows something important about ${userName} and confronts them with it.`,
`${assistantName} and ${userName} are forced to cooperate while something goes wrong around them.`,
`${assistantName} meets ${userName} in a quiet, intimate scene with unresolved tension.`,
`${assistantName} draws ${userName} into a high-stakes problem that starts immediately.`
];
}
function updateRoleplaySuggestions() {
if (!roleplayStarterSuggestions) return;
roleplayStarterSuggestions.innerHTML = "";
getRoleplayStarterSuggestions().forEach((suggestion) => {
const button = document.createElement("button");
button.type = "button";
button.className = "prompt-chip";
button.textContent = suggestion;
button.addEventListener("click", () => {
if (roleplayScenarioInput) {
roleplayScenarioInput.value = suggestion;
roleplayScenarioInput.focus();
}
});
roleplayStarterSuggestions.appendChild(button);
});
}
function getQuickPromptsForChat(chat) {
if (chat?.assistant_persona_id) {
const characterName = chat.assistant_persona_name || chat.title || "the character";
const userName = chat.user_persona_name || "me";
return [
`Stay in character and describe what ${characterName} notices about ${userName} right now.`,
`Push the scene forward with a new complication.`,
`Respond with sharper tension and more specific detail.`,
`Let ${characterName} reveal what they actually want from me.`
];
}
return [];
}
function renderQuickPrompts() {
if (!quickPrompts) return;
const activeChat = getChatById(activeChatId);
const prompts = getQuickPromptsForChat(activeChat);
quickPrompts.innerHTML = "";
quickPrompts.classList.toggle("hidden", prompts.length === 0);
prompts.forEach((prompt) => {
const button = document.createElement("button");
button.type = "button";
button.className = "prompt-chip";
button.textContent = prompt;
button.addEventListener("click", () => {
msgInput.value = prompt;
msgInput.dispatchEvent(new Event("input"));
msgInput.focus();
});
quickPrompts.appendChild(button);
});
}
function updateComposerPlaceholder() {
const activeChat = getChatById(activeChatId);
msgInput.placeholder = activeChat?.assistant_persona_id
? "Reply in the scene..."
: "Type your message...";
}
function addMessage(content, isUser = false, isLoading = false) {
const msgDiv = document.createElement("div");
@@ -105,7 +204,8 @@ function addMessage(content, isUser = false, isLoading = false) {
} else {
const header = document.createElement("div"), body = document.createElement("div");
header.className = "msg-header"; body.className = "msg-content";
header.textContent = isUser ? "You" : "Assistant";
const activeChat = getChatById(activeChatId);
header.textContent = isUser ? "You" : (activeChat?.assistant_persona_name || "Assistant");
setMessageContent(body, content);
msgDiv.append(header, body);
}
@@ -119,6 +219,8 @@ function renderMessages() {
currentMessages.forEach((msg) => addMessage(msg.content, msg.role === "user"));
updateChatParticipants();
updateChatActionState();
renderQuickPrompts();
updateComposerPlaceholder();
}
function updateWorkspaceCopy() {
@@ -130,6 +232,8 @@ function updateWorkspaceCopy() {
sessionUser.textContent = currentUsername || "-";
}
updateChatParticipants();
renderQuickPrompts();
updateComposerPlaceholder();
}
function setRoleplayStarterNotice(message = "", state = "") {
@@ -145,11 +249,12 @@ function setRoleplayStarterNotice(message = "", state = "") {
function updateChatParticipants() {
const chat = getChatById(activeChatId);
if (!chatModePill || !chatCharacterPill || !chatUserPersonaPill) return;
if (!chatModePill || !chatCharacterPill || !chatUserPersonaPill || !chatScenePill) return;
if (!chat) {
chatModePill.textContent = "No chat selected";
chatCharacterPill.classList.add("hidden");
chatUserPersonaPill.classList.add("hidden");
chatScenePill.classList.add("hidden");
return;
}
if (chat.assistant_persona_id) {
@@ -163,11 +268,18 @@ function updateChatParticipants() {
chatUserPersonaPill.textContent = "You as: yourself";
chatUserPersonaPill.classList.remove("hidden");
}
if (chat.scenario_summary) {
chatScenePill.textContent = `Scene: ${summarizeText(chat.scenario_summary, 112)}`;
chatScenePill.classList.remove("hidden");
} else {
chatScenePill.classList.add("hidden");
}
return;
}
chatModePill.textContent = "Assistant mode";
chatCharacterPill.classList.add("hidden");
chatUserPersonaPill.classList.add("hidden");
chatScenePill.classList.add("hidden");
}
function updateContextRail() {
@@ -292,8 +404,8 @@ function updateActivePersonaStatus(items, activeId, statusEl, emptyText, prefix)
function setActivePersonaStatus() {
if (activePersonaStatus) {
activePersonaStatus.textContent = assistantPersonas.length
? "Pick a character, choose a user persona, and start the roleplay."
: "No characters yet. Create one to start a roleplay.";
? "Choose an AI Character, pair it with a user persona if needed, and start the scene."
: "No AI Characters yet. Create one to start a roleplay.";
}
updateActivePersonaStatus(userPersonas, activeUserPersonaId, activeUserPersonaStatus, "No default user persona set.", "Default user persona");
if (clearUserPersonaBtn) clearUserPersonaBtn.disabled = !activeUserPersonaId;
@@ -347,12 +459,14 @@ function populateRoleplayStarter() {
option.selected = persona.id === activeUserPersonaId;
roleplayUserPersonaSelect.appendChild(option);
});
updateRoleplaySuggestions();
}
function openRoleplayStarter(characterId = null) {
closePersonaPopover();
closeModelPopover();
roleplayPresetCharacterId = characterId;
populateRoleplayStarter();
if (roleplayScenarioInput) roleplayScenarioInput.value = "";
setRoleplayStarterNotice("");
roleplayStarterModal.classList.remove("hidden");
if (roleplayCharacterSelect.options.length) roleplayCharacterSelect.focus();
@@ -362,6 +476,7 @@ function closeRoleplayStarter() {
roleplayStarterModal.classList.add("hidden");
roleplayPresetCharacterId = null;
roleplayStarterConfirm.disabled = false;
if (roleplayScenarioInput) roleplayScenarioInput.value = "";
setRoleplayStarterNotice("");
}
function closeModelPopover() {
@@ -539,7 +654,12 @@ async function unpublishPersona(id) { const res = await post(`/personas/${id}/un
function setLoadingState(loading) {
isProcessing = loading; sendBtn.disabled = loading; msgInput.disabled = loading;
sendBtn.classList.toggle("loading", loading); msgInput.placeholder = loading ? "Processing response..." : "Type your message...";
sendBtn.classList.toggle("loading", loading);
if (loading) {
msgInput.placeholder = "Processing response...";
} else {
updateComposerPlaceholder();
}
}
function showAuthScreen(target) {
@@ -578,14 +698,16 @@ async function createNewChat() {
async function startRoleplay() {
const assistantPersonaId = Number(roleplayCharacterSelect.value);
const userPersonaId = roleplayUserPersonaSelect.value ? Number(roleplayUserPersonaSelect.value) : null;
const scenarioPrompt = roleplayScenarioInput?.value.trim() || "";
if (!assistantPersonaId) {
return setRoleplayStarterNotice("Choose a character to start the roleplay.", "error");
}
roleplayStarterConfirm.disabled = true;
setRoleplayStarterNotice("Starting roleplay...");
setRoleplayStarterNotice("Building the opening scene...");
const res = await post("/roleplays/start", {
assistantPersonaId,
userPersonaId,
scenarioPrompt,
model: modelSelect.value
});
roleplayStarterConfirm.disabled = false;
@@ -595,7 +717,7 @@ async function startRoleplay() {
closeRoleplayStarter();
await loadChatSessions();
await setActiveChat(res.chat.id);
setNotice(res.generatedInitialMessage ? "Roleplay started." : "Roleplay reopened.", "success");
setNotice(res.generatedInitialMessage ? "Roleplay started with a fresh scene." : "Roleplay reopened.", "success");
}
async function renameChat(id) {
@@ -778,6 +900,8 @@ personaModal.addEventListener("click", (event) => { if (event.target === persona
roleplayStarterClose.addEventListener("click", closeRoleplayStarter);
roleplayStarterCancel.addEventListener("click", closeRoleplayStarter);
roleplayStarterConfirm.addEventListener("click", () => { void startRoleplay(); });
roleplayCharacterSelect.addEventListener("change", updateRoleplaySuggestions);
roleplayUserPersonaSelect.addEventListener("change", updateRoleplaySuggestions);
roleplayStarterModal.addEventListener("click", (event) => { if (event.target === roleplayStarterModal) closeRoleplayStarter(); });
popupConfirmBtn.addEventListener("click", () => {
if (popupMode === "prompt") {
+11 -6
View File
@@ -25,6 +25,11 @@
<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>
@@ -88,11 +93,11 @@
<div class="settings-panel">
<div class="settings-section-header">
<div>
<p class="eyebrow">Manage</p>
<p class="eyebrow">Create & manage</p>
<h2>My AI Characters</h2>
<p class="subtitle">Create, edit, publish, and delete your AI Characters.</p>
<p class="subtitle">Build characters for roleplay chats and publish them to the market when they are ready.</p>
</div>
<button id="createAiCharacter" type="button" class="secondary-action">Create persona</button>
<button id="createAiCharacter" type="button" class="secondary-action">Create AI Character</button>
</div>
<div id="settingsAiCharacterList" class="persona-list"></div>
</div>
@@ -102,11 +107,11 @@
<div class="settings-panel">
<div class="settings-section-header">
<div>
<p class="eyebrow">Manage</p>
<p class="eyebrow">Create & manage</p>
<h2>My Personas</h2>
<p class="subtitle">Create and manage the user personas you can speak as in roleplay chats.</p>
<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 persona</button>
<button id="createUserPersona" type="button" class="secondary-action">Create user persona</button>
</div>
<div id="settingsUserPersonaList" class="persona-list"></div>
</div>
+25 -8
View File
@@ -13,6 +13,8 @@ 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");
@@ -101,8 +103,8 @@ function syncPersonaExamplesField() {
const isAssistant = settingsPersonaType.value === "assistant";
settingsPersonaExamplesField.classList.toggle("hidden", !isAssistant);
settingsPersonaHelper.textContent = isAssistant
? "AI Characters define who the character is, how they speak, and what kind of scene they lead."
: "User personas define who you are when starting roleplay chats with AI Characters.";
? "Create the character you want to roleplay with."
: "Create the persona you want to speak as in roleplay.";
$("settingsPersonaSave").textContent = isAssistant ? "Save AI Character" : "Save persona";
}
@@ -134,11 +136,17 @@ function closePersonaModal() {
function renderPersonaList(personas, listElement, type) {
listElement.innerHTML = "";
if (!personas.length) {
const empty = document.createElement("p");
empty.className = "status persona-status";
empty.textContent = type === "assistant"
? "No AI Characters yet. Create one to get started."
: "No user personas yet. Create one to talk as someone specific.";
const empty = document.createElement("div");
const message = document.createElement("p");
const button = document.createElement("button");
empty.className = "persona-empty-state";
message.className = "status persona-status";
message.textContent = type === "assistant" ? "No AI Characters yet." : "No user personas yet.";
button.type = "button";
button.className = "secondary-action";
button.textContent = type === "assistant" ? "Create AI Character" : "Create user persona";
button.addEventListener("click", () => openPersonaModal(null, type));
empty.append(message, button);
listElement.appendChild(empty);
return;
}
@@ -213,6 +221,7 @@ async function loadPersonas() {
}
async function savePersona() {
const wasEditing = Boolean(editingPersonaId);
const payload = {
personaType: settingsPersonaType.value,
name: settingsPersonaName.value.trim(),
@@ -240,7 +249,7 @@ async function savePersona() {
await loadPersonas();
closePersonaModal();
setSettingsView(payload.personaType === "assistant" ? "my-ai-characters" : "my-personas");
setNotice(editingPersonaId ? "Persona updated." : "Persona created.", "success");
setNotice(wasEditing ? "Persona updated." : "Persona created.", "success");
}
async function deletePersona(id) {
@@ -322,6 +331,14 @@ settingsThemeSelect.addEventListener("change", (event) => {
});
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);
+27
View File
@@ -530,6 +530,7 @@ select:focus {
.inline-status {
min-height: auto;
padding: 10px 12px;
flex: 1 1 260px;
}
.consent-container {
@@ -1043,6 +1044,9 @@ select:focus {
}
.prompt-chip {
display: inline-flex;
align-items: center;
justify-content: center;
padding: 9px 12px;
border-radius: 999px;
background: rgba(255, 251, 245, 0.65);
@@ -1453,6 +1457,8 @@ select:focus {
border-radius: 16px;
background: rgba(24, 32, 31, 0.04);
border: 1px solid rgba(24, 32, 31, 0.08);
display: grid;
gap: 14px;
}
.persona-form > .status {
@@ -1594,6 +1600,17 @@ select:focus {
gap: 16px;
}
.settings-quick-actions {
display: flex;
gap: 12px;
flex-wrap: wrap;
}
.settings-quick-actions .primary-action,
.settings-quick-actions .secondary-action {
min-width: 210px;
}
.settings-shell .market-header-simple {
display: grid;
grid-template-columns: minmax(0, 1fr) auto;
@@ -1710,6 +1727,16 @@ select:focus {
color: var(--text-muted);
}
.persona-empty-state {
display: grid;
justify-items: start;
gap: 10px;
padding: 16px;
border-radius: 14px;
background: rgba(24, 32, 31, 0.04);
border: 1px dashed rgba(24, 32, 31, 0.14);
}
body[data-theme="fakegpt"] {
--bg: #0b0f0d;
--bg-soft: #111715;