diff --git a/public/index.html b/public/index.html
index f8c9592..21172d0 100644
--- a/public/index.html
+++ b/public/index.html
@@ -22,7 +22,7 @@
diff --git a/public/market.js b/public/market.js
index 98d968b..1961767 100644
--- a/public/market.js
+++ b/public/market.js
@@ -1,13 +1,13 @@
const $ = (id) => document.getElementById(id);
const marketPersonaList = $("marketPersonaList");
const marketUserPersonaList = $("marketUserPersonaList");
-const marketOwnedPersonaList = $("marketOwnedPersonaList");
const refreshMarketBtn = $("refreshMarket");
const marketStatus = $("marketStatus");
const marketSearchInput = $("marketSearch");
const marketViewButtons = document.querySelectorAll("[data-market-view]");
const marketPanels = document.querySelectorAll("[data-market-panel]");
-const marketCreatePersonaBtn = $("marketCreatePersona");
+const marketCreateAiCharacterBtn = $("marketCreateAiCharacter");
+const marketCreateUserPersonaBtn = $("marketCreateUserPersona");
const marketPreviewModal = $("marketPreviewModal");
const marketPreviewName = $("marketPreviewName");
const marketPreviewMeta = $("marketPreviewMeta");
@@ -45,9 +45,10 @@ const themes = {
let allMarketPersonas = [];
let assistantPersonas = [];
let userPersonas = [];
-let publishedPersonaIds = new Set();
let pendingMarketPersona = null;
let editingPersonaId = null;
+let activeUserPersonaId = null;
+let activeMarketView = "ai-characters";
async function request(url, data, method = "POST") {
try {
@@ -65,7 +66,6 @@ async function request(url, data, method = "POST") {
const get = (url) => request(url, null, "GET");
const post = (url, data) => request(url, data, "POST");
const put = (url, data) => request(url, data, "PUT");
-const del = (url) => request(url, null, "DELETE");
function applyTheme(themeKey) {
const nextTheme = themes[themeKey] ? themeKey : "fakegpt";
@@ -92,11 +92,14 @@ function setPersonaFormNotice(message = "", state = "") {
}
function setMarketView(view) {
- marketViewButtons.forEach((item) => item.classList.toggle("active", item.dataset.marketView === view));
- marketPanels.forEach((panel) => panel.classList.toggle("active", panel.dataset.marketPanel === view));
+ const normalizedView = view === "user-personas" ? "user-personas" : "ai-characters";
+ activeMarketView = normalizedView;
+ marketViewButtons.forEach((item) => item.classList.toggle("active", item.dataset.marketView === normalizedView));
+ marketPanels.forEach((panel) => panel.classList.toggle("active", panel.dataset.marketPanel === normalizedView));
const params = new URLSearchParams(window.location.search);
- params.set("view", view);
+ params.set("view", normalizedView);
history.replaceState(null, "", `${window.location.pathname}?${params.toString()}`);
+ applyMarketFilter();
}
function buildPersonaMeta(persona) {
@@ -113,7 +116,6 @@ function closeMarketPreview() {
marketPreviewModal.classList.add("hidden");
pendingMarketPersona = null;
marketPreviewUserPersonaSelect.innerHTML = "";
- marketPreviewConfirm.classList.add("hidden");
}
function populateUserPersonaChoices() {
@@ -121,7 +123,7 @@ function populateUserPersonaChoices() {
const promptOption = document.createElement("option");
promptOption.value = "";
- promptOption.textContent = "Choose who you are";
+ promptOption.textContent = "Choose who you are in this roleplay";
promptOption.disabled = true;
promptOption.selected = true;
marketPreviewUserPersonaSelect.appendChild(promptOption);
@@ -135,6 +137,7 @@ function populateUserPersonaChoices() {
const option = document.createElement("option");
option.value = String(persona.id);
option.textContent = persona.name;
+ option.selected = persona.id === activeUserPersonaId;
marketPreviewUserPersonaSelect.appendChild(option);
});
@@ -156,9 +159,13 @@ function openMarketPreview(persona) {
const isAssistant = persona.persona_type === "assistant";
marketPreviewUserPersonaField.classList.toggle("hidden", !isAssistant);
- marketPreviewConfirm.textContent = isAssistant ? "Start chat" : "Collect as user persona";
- marketPreviewConfirm.classList.toggle("hidden", isAssistant);
- if (isAssistant) populateUserPersonaChoices();
+ marketPreviewConfirm.textContent = isAssistant ? "Start roleplay" : "Collect persona";
+ if (isAssistant) {
+ populateUserPersonaChoices();
+ marketPreviewConfirm.classList.add("hidden");
+ } else {
+ marketPreviewConfirm.classList.remove("hidden");
+ }
marketPreviewModal.classList.remove("hidden");
}
@@ -210,9 +217,19 @@ function renderMarketList(items, listElement, type) {
meta.textContent = buildPersonaMeta(persona);
detailsBtn.type = "button";
- detailsBtn.textContent = "View";
+ detailsBtn.textContent = "Preview";
detailsBtn.addEventListener("click", () => openMarketPreview(persona));
+ if (type !== "assistant") {
+ const quickActionBtn = document.createElement("button");
+ quickActionBtn.type = "button";
+ quickActionBtn.textContent = "Collect";
+ quickActionBtn.addEventListener("click", () => {
+ void collectMarketPersona(persona.id);
+ });
+ actions.appendChild(quickActionBtn);
+ }
+
titleRow.append(title, tag);
actions.appendChild(detailsBtn);
item.append(titleRow, meta, actions);
@@ -220,58 +237,6 @@ function renderMarketList(items, listElement, type) {
});
}
-function renderOwnedPersonaList() {
- marketOwnedPersonaList.innerHTML = "";
- if (!assistantPersonas.length) {
- const empty = document.createElement("p");
- empty.className = "status persona-status";
- empty.textContent = "No AI Characters yet. Create one to get started.";
- marketOwnedPersonaList.appendChild(empty);
- return;
- }
-
- assistantPersonas.forEach((persona) => {
- const item = document.createElement("div");
- const titleRow = document.createElement("div");
- const title = document.createElement("h4");
- const tag = document.createElement("span");
- const meta = document.createElement("p");
- const actions = document.createElement("div");
-
- item.className = "persona-item";
- titleRow.className = "persona-title-row";
- actions.className = "persona-item-actions";
- tag.className = "persona-role-tag ai";
- tag.textContent = "AI Character";
- title.textContent = persona.name;
- meta.textContent = formatPersonaField(persona.pronouns) === "Not provided." ? "Pronouns: n/a" : `Pronouns: ${persona.pronouns}`;
-
- [
- ["Edit", () => openPersonaForm(persona)],
- [publishedPersonaIds.has(persona.id) ? "Update listing" : "Publish", () => publishPersona(persona.id)],
- ["Delete", () => deletePersona(persona.id)]
- ].forEach(([label, handler]) => {
- const button = document.createElement("button");
- button.type = "button";
- button.textContent = label;
- button.addEventListener("click", handler);
- actions.appendChild(button);
- });
-
- if (publishedPersonaIds.has(persona.id)) {
- const unpublish = document.createElement("button");
- unpublish.type = "button";
- unpublish.textContent = "Unpublish";
- unpublish.addEventListener("click", () => unpublishPersona(persona.id));
- actions.appendChild(unpublish);
- }
-
- titleRow.append(title, tag);
- item.append(titleRow, meta, actions);
- marketOwnedPersonaList.appendChild(item);
- });
-}
-
function filterMarketPersonas() {
const query = marketSearchInput.value.trim().toLowerCase();
if (!query) return allMarketPersonas;
@@ -283,15 +248,24 @@ function filterMarketPersonas() {
function applyMarketFilter() {
const filtered = filterMarketPersonas();
- renderMarketList(filtered.filter((persona) => persona.persona_type === "assistant"), marketPersonaList, "assistant");
- renderMarketList(filtered.filter((persona) => persona.persona_type === "user"), marketUserPersonaList, "user");
- renderOwnedPersonaList();
+ const assistantItems = filtered.filter((persona) => persona.persona_type === "assistant");
+ const userItems = filtered.filter((persona) => persona.persona_type === "user");
+ renderMarketList(assistantItems, marketPersonaList, "assistant");
+ renderMarketList(userItems, marketUserPersonaList, "user");
if (marketSearchInput.value.trim() && !filtered.length) {
setMarketStatus("No personas match your search.", "error");
return;
}
- setMarketStatus(`${filtered.length} persona${filtered.length === 1 ? "" : "s"} shown.`);
+ if (activeMarketView === "ai-characters") {
+ setMarketStatus(`${assistantItems.length} AI Character${assistantItems.length === 1 ? "" : "s"} ready for roleplay.`);
+ return;
+ }
+ if (activeMarketView === "user-personas") {
+ setMarketStatus(`${userItems.length} User Persona${userItems.length === 1 ? "" : "s"} available to collect.`);
+ return;
+ }
+ setMarketStatus(`${assistantItems.length + userItems.length} persona${assistantItems.length + userItems.length === 1 ? "" : "s"} shown.`);
}
async function loadOwnedPersonas() {
@@ -299,7 +273,7 @@ async function loadOwnedPersonas() {
if (res.error) return false;
assistantPersonas = res.assistantPersonas || [];
userPersonas = res.userPersonas || [];
- publishedPersonaIds = new Set(res.publishedPersonaIds || []);
+ activeUserPersonaId = res.activeUserPersonaId || null;
return true;
}
@@ -330,8 +304,12 @@ async function collectMarketPersona(marketId) {
}
async function startMarketPersonaChat(marketId, userPersonaSelection) {
+ if (!userPersonaSelection) {
+ setMarketStatus("Choose who you are before starting roleplay.", "error");
+ return;
+ }
if (userPersonaSelection === "create_new") {
- window.location.href = "/settings?view=my-personas&create=user";
+ window.location.href = "/settings?view=my-roleplay-companions&create=user";
return;
}
@@ -372,40 +350,10 @@ async function savePersona() {
await loadMarketPersonas();
closePersonaForm();
- setMarketView("my-personas");
+ setMarketView("ai-characters");
setMarketStatus(editingPersonaId ? "AI Character updated." : "AI Character created.", "success");
}
-async function deletePersona(id) {
- const res = await del(`/personas/${id}`);
- if (res.error) {
- setMarketStatus(res.error, "error");
- return;
- }
- await loadMarketPersonas();
- setMarketStatus("AI Character deleted.", "success");
-}
-
-async function publishPersona(id) {
- const res = await post(`/personas/${id}/publish`, {});
- if (res.error) {
- setMarketStatus(res.error, "error");
- return;
- }
- await loadMarketPersonas();
- setMarketStatus("AI Character published.", "success");
-}
-
-async function unpublishPersona(id) {
- const res = await post(`/personas/${id}/unpublish`, {});
- if (res.error) {
- setMarketStatus(res.error, "error");
- return;
- }
- await loadMarketPersonas();
- setMarketStatus("AI Character unpublished.", "success");
-}
-
refreshMarketBtn.addEventListener("click", () => {
void loadMarketPersonas();
});
@@ -413,7 +361,14 @@ marketSearchInput.addEventListener("input", applyMarketFilter);
marketViewButtons.forEach((button) => {
button.addEventListener("click", () => setMarketView(button.dataset.marketView));
});
-marketCreatePersonaBtn.addEventListener("click", () => openPersonaForm());
+if (marketCreateAiCharacterBtn) {
+ marketCreateAiCharacterBtn.addEventListener("click", () => openPersonaForm());
+}
+if (marketCreateUserPersonaBtn) {
+ marketCreateUserPersonaBtn.addEventListener("click", () => {
+ window.location.href = "/settings?view=my-roleplay-companions&create=user";
+ });
+}
marketPreviewClose.addEventListener("click", closeMarketPreview);
marketPreviewModal.addEventListener("click", (event) => {
if (event.target === marketPreviewModal) closeMarketPreview();
@@ -421,7 +376,7 @@ marketPreviewModal.addEventListener("click", (event) => {
marketPreviewUserPersonaSelect.addEventListener("change", () => {
if (!pendingMarketPersona || pendingMarketPersona.persona_type !== "assistant") return;
if (marketPreviewUserPersonaSelect.value === "create_new") {
- window.location.href = "/settings?view=my-personas&create=user";
+ window.location.href = "/settings?view=my-roleplay-companions&create=user";
return;
}
marketPreviewConfirm.classList.toggle("hidden", !marketPreviewUserPersonaSelect.value);
@@ -430,7 +385,7 @@ marketPreviewConfirm.addEventListener("click", async () => {
if (!pendingMarketPersona) return;
const marketId = pendingMarketPersona.id;
if (pendingMarketPersona.persona_type === "assistant") {
- await startMarketPersonaChat(marketId, marketPreviewUserPersonaSelect.value || "");
+ await startMarketPersonaChat(marketId, marketPreviewUserPersonaSelect.value || "self");
closeMarketPreview();
return;
}
@@ -451,5 +406,8 @@ window.addEventListener("load", async () => {
const params = new URLSearchParams(window.location.search);
setMarketView(params.get("view") || "ai-characters");
await loadMarketPersonas();
- if (params.get("create") === "assistant") openPersonaForm();
+ if (params.get("create") === "assistant") {
+ setMarketView("ai-characters");
+ openPersonaForm();
+ }
});
diff --git a/public/script.js b/public/script.js
index ec8f039..4ff20ab 100644
--- a/public/script.js
+++ b/public/script.js
@@ -9,8 +9,11 @@ const loginUsernameInput = $("loginUsername"), loginPasswordInput = $("loginPass
const registerUsernameInput = $("registerUsername"), registerPasswordInput = $("registerPassword");
const loginSubmit = $("loginSubmit"), registerSubmit = $("registerSubmit");
const chatList = $("chatList"), chatSearchInput = $("chatSearch"), newChatBtn = $("newChat");
+const startRoleplayBtn = $("startRoleplay");
const renameChatBtn = $("renameChat"), clearChatBtn = $("clearChat"), exportChatBtn = $("exportChat");
const activeChatTitle = $("activeChatTitle"), sessionUser = $("sessionUser");
+const contextChipButton = $("contextChipButton"), contextPopover = $("contextPopover"), contextSummaryLabel = $("contextSummaryLabel");
+const chatActionsMenuButton = $("chatActionsMenuButton"), chatActionsPopover = $("chatActionsPopover");
const chatModePill = $("chatModePill"), chatCharacterPill = $("chatCharacterPill"), chatUserPersonaPill = $("chatUserPersonaPill"), chatScenePill = $("chatScenePill");
const personaList = $("personaList"), userPersonaList = $("userPersonaList");
const personaForm = $("personaForm"), personaFormTitle = $("personaFormTitle"), personaTypeSelect = $("personaType");
@@ -28,8 +31,11 @@ const popupModal = $("popupModal"), popupCloseBtn = $("popupClose"), popupCancel
const popupTitle = $("popupTitle"), popupEyebrow = $("popupEyebrow"), popupDescription = $("popupDescription"), popupField = $("popupField");
const popupInputLabel = $("popupInputLabel"), popupInput = $("popupInput");
const appNotice = $("appNotice"), quickPrompts = $("quickPrompts");
+const onboardingModal = $("onboardingModal"), onboardingTitle = $("onboardingTitle"), onboardingSubtitle = $("onboardingSubtitle");
+const onboardingChoices = $("onboardingChoices"), onboardingBack = $("onboardingBack"), onboardingContinue = $("onboardingContinue"), onboardingSkip = $("onboardingSkip");
const authScreens = document.querySelectorAll(".auth-screen"), toggleButtons = document.querySelectorAll(".auth-toggle .toggle");
const themeNameTargets = document.querySelectorAll("[data-theme-name]"), themeLogoTargets = document.querySelectorAll("[data-theme-logo]");
+const modelMenu = document.querySelector(".model-menu"), personaMenu = document.querySelector(".persona-menu");
const requestedChatId = Number(new URLSearchParams(window.location.search).get("chat")) || null;
let isProcessing = false, activeChatId = null, editingPersonaId = null, editingPersonaType = "assistant", currentUsername = "";
@@ -38,6 +44,14 @@ let activeUserPersonaId = null, currentSummary = null, publishedPersonaIds = new
let popupResolver = null, popupMode = null, popupLastFocus = null;
let roleplayPresetCharacterId = null;
let noticeTimer = null;
+let workspaceMode = localStorage.getItem("krishd-workspace-mode") || "basic";
+let onboardingStep = 1, onboardingIntent = "ask";
+
+const onboardingPrompts = {
+ ask: ["Explain this topic in simple terms.", "Compare these options and recommend one."],
+ brainstorm: ["Give me 10 practical ideas for this problem.", "Suggest 3 creative directions and tradeoffs."],
+ roleplay: ["Start a roleplay scene with immediate tension.", "Give me a dramatic opening with clear stakes."]
+};
const themes = {
"fakegpt": {name: "FakeGPT", short: "FG"},
@@ -98,6 +112,90 @@ function setPersonaFormNotice(message = "", state = "") {
personaFormNotice.textContent = message;
personaFormNotice.className = state ? `status ${state}` : "status";
}
+
+function applyWorkspaceMode(nextMode, persist = true) {
+ workspaceMode = nextMode === "advanced" ? "advanced" : "basic";
+ document.body.dataset.workspaceMode = workspaceMode;
+ if (persist) localStorage.setItem("krishd-workspace-mode", workspaceMode);
+ if (workspaceMode === "basic") {
+ closeModelPopover();
+ closePersonaPopover();
+ }
+ if (modelMenu) modelMenu.classList.toggle("hidden", workspaceMode === "basic");
+ if (personaMenu) personaMenu.classList.toggle("hidden", workspaceMode === "basic");
+ updateComposerPlaceholder();
+ renderQuickPrompts();
+}
+
+function hasCompletedOnboarding() {
+ return localStorage.getItem("krishd-onboarding-complete") === "true";
+}
+
+function markOnboardingCompleted() {
+ localStorage.setItem("krishd-onboarding-complete", "true");
+ closeOnboarding();
+}
+
+function closeOnboarding() {
+ if (!onboardingModal) return;
+ onboardingModal.classList.add("hidden");
+}
+
+function renderOnboardingStep() {
+ if (!onboardingChoices || !onboardingTitle || !onboardingSubtitle) return;
+ onboardingChoices.innerHTML = "";
+ onboardingBack.classList.toggle("hidden", onboardingStep === 1);
+ onboardingContinue.classList.toggle("hidden", onboardingStep === 2);
+ if (onboardingStep === 1) {
+ onboardingTitle.textContent = "What do you want to do?";
+ onboardingSubtitle.textContent = "Choose one path. You can switch anytime.";
+ [["ask", "Ask questions"], ["brainstorm", "Brainstorm ideas"], ["roleplay", "Roleplay with a character"]].forEach(([key, label]) => {
+ const button = document.createElement("button");
+ button.type = "button";
+ button.className = `secondary-action${onboardingIntent === key ? " active" : ""}`;
+ button.textContent = label;
+ button.addEventListener("click", () => {
+ onboardingIntent = key;
+ renderOnboardingStep();
+ });
+ onboardingChoices.appendChild(button);
+ });
+ return;
+ }
+ onboardingTitle.textContent = "Choose a starter";
+ onboardingSubtitle.textContent = "Pick one template and send your first message.";
+ onboardingPrompts[onboardingIntent].forEach((template) => {
+ const button = document.createElement("button");
+ button.type = "button";
+ button.className = "secondary-action";
+ button.textContent = template;
+ button.addEventListener("click", async () => {
+ if (onboardingIntent === "roleplay") {
+ closeOnboarding();
+ if (assistantPersonas.length) {
+ openRoleplayStarter();
+ } else {
+ openPersonaForm(null, "assistant");
+ }
+ return;
+ }
+ if (!activeChatId) await createNewChat();
+ msgInput.value = template;
+ msgInput.dispatchEvent(new Event("input"));
+ msgInput.focus();
+ closeOnboarding();
+ });
+ onboardingChoices.appendChild(button);
+ });
+}
+
+function maybeShowOnboarding() {
+ if (!onboardingModal || hasCompletedOnboarding()) return;
+ onboardingStep = 1;
+ onboardingIntent = "ask";
+ renderOnboardingStep();
+ onboardingModal.classList.remove("hidden");
+}
function setMessageContent(el, content) {
const markdownParser = window.marked;
const purifier = window.DOMPurify;
@@ -163,9 +261,10 @@ function renderQuickPrompts() {
if (!quickPrompts) return;
const activeChat = getChatById(activeChatId);
const prompts = getQuickPromptsForChat(activeChat);
+ const visiblePrompts = workspaceMode === "basic" ? prompts.slice(0, 3) : prompts;
quickPrompts.innerHTML = "";
- quickPrompts.classList.toggle("hidden", prompts.length === 0);
- prompts.forEach((prompt) => {
+ quickPrompts.classList.toggle("hidden", visiblePrompts.length === 0);
+ visiblePrompts.forEach((prompt) => {
const button = document.createElement("button");
button.type = "button";
button.className = "prompt-chip";
@@ -180,9 +279,20 @@ function renderQuickPrompts() {
}
function updateComposerPlaceholder() {
const activeChat = getChatById(activeChatId);
- msgInput.placeholder = activeChat?.assistant_persona_id
- ? "Reply in the scene..."
- : "Type your message...";
+ if (!activeChat) {
+ msgInput.placeholder = "Create a chat to begin...";
+ return;
+ }
+ if (activeChat.assistant_persona_id) {
+ msgInput.placeholder = "Continue the scene...";
+ return;
+ }
+ msgInput.placeholder = workspaceMode === "basic" ? "Ask anything..." : "Type your message...";
+}
+
+function updateSendState() {
+ const hasText = Boolean(msgInput.value.trim());
+ sendBtn.disabled = isProcessing || !hasText;
}
function addMessage(content, isUser = false, isLoading = false) {
@@ -216,13 +326,78 @@ function addMessage(content, isUser = false, isLoading = false) {
function renderMessages() {
messagesDiv.innerHTML = "";
- currentMessages.forEach((msg) => addMessage(msg.content, msg.role === "user"));
+ const chat = getChatById(activeChatId);
+ if (!currentMessages.length && (!chat || chat.assistant_persona_id)) {
+ renderMessageEmptyState();
+ } else {
+ currentMessages.forEach((msg) => addMessage(msg.content, msg.role === "user"));
+ }
updateChatParticipants();
updateChatActionState();
renderQuickPrompts();
updateComposerPlaceholder();
}
+function createStarterAction(label, className, handler) {
+ const button = document.createElement("button");
+ button.type = "button";
+ button.className = className;
+ button.textContent = label;
+ button.addEventListener("click", handler);
+ return button;
+}
+
+function createMessageStarterPrompt(label, prompt) {
+ return createStarterAction(label, "secondary-action", async () => {
+ if (!activeChatId) await createNewChat();
+ msgInput.value = prompt;
+ msgInput.dispatchEvent(new Event("input"));
+ msgInput.focus();
+ });
+}
+
+function renderMessageEmptyState() {
+ const empty = document.createElement("section");
+ const title = document.createElement("h3");
+ const description = document.createElement("p");
+ const actions = document.createElement("div");
+ const chat = getChatById(activeChatId);
+
+ empty.className = "messages-empty";
+ title.className = "messages-empty-title";
+ description.className = "messages-empty-copy";
+ actions.className = "messages-empty-actions";
+
+ if (!chat) {
+ title.textContent = "Start your first conversation";
+ description.textContent = "Create a chat, or jump directly into roleplay.";
+ actions.append(
+ createStarterAction("New chat", "primary-action", () => { void createNewChat(); }),
+ createStarterAction("Start roleplay", "secondary-action", () => {
+ if (assistantPersonas.length) {
+ openRoleplayStarter();
+ return;
+ }
+ openPersonaForm(null, "assistant");
+ }),
+ createMessageStarterPrompt("Try a starter prompt", "Help me plan my day in 3 steps.")
+ );
+ } else if (chat.assistant_persona_id) {
+ title.textContent = "Your scene is ready";
+ description.textContent = "Send your first line, or use a prompt to set the tone.";
+ actions.append(
+ createMessageStarterPrompt("Add tension", "Open with a tense line that raises the stakes immediately."),
+ createMessageStarterPrompt("Set the scene", "Describe what your character notices first in this scene."),
+ createStarterAction("Pick another roleplay setup", "secondary-action", () => openRoleplayStarter())
+ );
+ } else {
+ return;
+ }
+
+ empty.append(title, description, actions);
+ messagesDiv.appendChild(empty);
+}
+
function updateWorkspaceCopy() {
const activeChat = getChatById(activeChatId);
if (activeChatTitle) {
@@ -255,6 +430,7 @@ function updateChatParticipants() {
chatCharacterPill.classList.add("hidden");
chatUserPersonaPill.classList.add("hidden");
chatScenePill.classList.add("hidden");
+ if (contextSummaryLabel) contextSummaryLabel.textContent = "None";
return;
}
if (chat.assistant_persona_id) {
@@ -274,12 +450,14 @@ function updateChatParticipants() {
} else {
chatScenePill.classList.add("hidden");
}
+ if (contextSummaryLabel) contextSummaryLabel.textContent = chat.assistant_persona_name || "Roleplay";
return;
}
chatModePill.textContent = "Assistant mode";
chatCharacterPill.classList.add("hidden");
chatUserPersonaPill.classList.add("hidden");
chatScenePill.classList.add("hidden");
+ if (contextSummaryLabel) contextSummaryLabel.textContent = "Assistant";
}
function updateContextRail() {
@@ -367,6 +545,14 @@ function updateChatActionState() {
exportChatBtn.disabled = !hasChat || currentMessages.length === 0;
}
+function maybeShowPersonaNudge() {
+ if (localStorage.getItem("krishd-persona-nudge-shown") === "true") return;
+ const assistantOnlyChats = chatSessions.filter((chat) => !chat.assistant_persona_id).length;
+ if (assistantOnlyChats < 3) return;
+ localStorage.setItem("krishd-persona-nudge-shown", "true");
+ setNotice("Want to personalize responses? Try personas in Tools.", "success");
+}
+
function renderChatList() {
const query = chatSearchInput.value.trim().toLowerCase();
const filtered = chatSessions.filter((chat) => !query || chat.title.toLowerCase().includes(query));
@@ -437,9 +623,31 @@ function openPersonaForm(persona = null, personaType = "assistant") {
function closePersonaForm() { personaModal.classList.add("hidden"); editingPersonaId = null; editingPersonaType = "assistant"; personaTypeSelect.disabled = false; setPersonaFormNotice(""); }
function closePersonaPopover() { personaPopover.classList.add("hidden"); personaMenuButton.setAttribute("aria-expanded", "false"); }
+function closeContextPopover() {
+ if (!contextPopover || !contextChipButton) return;
+ contextPopover.classList.add("hidden");
+ contextChipButton.setAttribute("aria-expanded", "false");
+}
+function closeChatActionsPopover() {
+ if (!chatActionsPopover || !chatActionsMenuButton) return;
+ chatActionsPopover.classList.add("hidden");
+ chatActionsMenuButton.setAttribute("aria-expanded", "false");
+}
+function closeAuxiliaryPopovers() {
+ closeModelPopover();
+ closePersonaPopover();
+ closeContextPopover();
+ closeChatActionsPopover();
+}
function populateRoleplayStarter() {
if (!roleplayCharacterSelect || !roleplayUserPersonaSelect) return;
roleplayCharacterSelect.innerHTML = "";
+ const placeholder = document.createElement("option");
+ placeholder.value = "";
+ placeholder.textContent = "Choose AI Character";
+ placeholder.disabled = true;
+ placeholder.selected = true;
+ roleplayCharacterSelect.appendChild(placeholder);
assistantPersonas.forEach((persona) => {
const option = document.createElement("option");
option.value = String(persona.id);
@@ -447,10 +655,13 @@ function populateRoleplayStarter() {
option.selected = persona.id === roleplayPresetCharacterId;
roleplayCharacterSelect.appendChild(option);
});
+ if (roleplayPresetCharacterId) {
+ roleplayCharacterSelect.value = String(roleplayPresetCharacterId);
+ }
roleplayUserPersonaSelect.innerHTML = "";
const noneOption = document.createElement("option");
noneOption.value = "";
- noneOption.textContent = "No user persona";
+ noneOption.textContent = "Myself (no user persona)";
roleplayUserPersonaSelect.appendChild(noneOption);
userPersonas.forEach((persona) => {
const option = document.createElement("option");
@@ -459,11 +670,17 @@ function populateRoleplayStarter() {
option.selected = persona.id === activeUserPersonaId;
roleplayUserPersonaSelect.appendChild(option);
});
+ if (assistantPersonas.length === 0) {
+ setRoleplayStarterNotice("Create an AI Character first, then start roleplay.", "error");
+ roleplayStarterConfirm.disabled = true;
+ } else {
+ setRoleplayStarterNotice("");
+ roleplayStarterConfirm.disabled = false;
+ }
updateRoleplaySuggestions();
}
function openRoleplayStarter(characterId = null) {
- closePersonaPopover();
- closeModelPopover();
+ closeAuxiliaryPopovers();
roleplayPresetCharacterId = characterId;
populateRoleplayStarter();
if (roleplayScenarioInput) roleplayScenarioInput.value = "";
@@ -480,6 +697,7 @@ function closeRoleplayStarter() {
setRoleplayStarterNotice("");
}
function closeModelPopover() {
+ if (!modelPopover || !modelMenuButton) return;
modelPopover.classList.add("hidden");
modelMenuButton.setAttribute("aria-expanded", "false");
}
@@ -653,13 +871,15 @@ async function publishPersona(id) { const res = await post(`/personas/${id}/publ
async function unpublishPersona(id) { const res = await post(`/personas/${id}/unpublish`, {}); if (res.error) return setNotice(res.error, "error"); await loadPersonas(); setNotice("Persona unpublished.", "success"); }
function setLoadingState(loading) {
- isProcessing = loading; sendBtn.disabled = loading; msgInput.disabled = loading;
+ isProcessing = loading;
+ msgInput.disabled = loading;
sendBtn.classList.toggle("loading", loading);
if (loading) {
msgInput.placeholder = "Processing response...";
} else {
updateComposerPlaceholder();
}
+ updateSendState();
}
function showAuthScreen(target) {
@@ -676,6 +896,7 @@ async function setActiveChat(id) {
const existing = getChatById(id);
if (res.chat && existing) Object.assign(existing, res.chat);
currentMessages = res.messages || []; renderMessages();
+ updateSendState();
}
async function loadChatSessions() {
@@ -686,13 +907,19 @@ async function loadChatSessions() {
const preferredChatId = (requestedChatId && getChatById(requestedChatId)) ? requestedChatId : activeChatId && getChatById(activeChatId) ? activeChatId : chatSessions[0].id;
return setActiveChat(preferredChatId);
}
- return createNewChat();
+ activeChatId = null;
+ currentMessages = [];
+ renderMessages();
+ updateWorkspaceCopy();
+ updateSendState();
+ return setNotice("Pick a starting action to begin.", "success");
}
async function createNewChat() {
const res = await post("/chats", {title: "New chat"});
if (res.error || !res.chat) return setNotice(res.error || "Unable to create chat.", "error");
chatSessions.unshift(res.chat); await loadSummary(); renderChatList(); await setActiveChat(res.chat.id); setNotice("New chat created.", "success");
+ msgInput.focus();
}
async function startRoleplay() {
@@ -807,6 +1034,7 @@ async function checkSession() {
if (!res.user) return false;
currentUsername = res.user; authDiv.style.display = "none"; chatDiv.style.display = "block";
await Promise.all([displayModels(), loadSummary(), loadChatSessions(), loadPersonas()]);
+ maybeShowOnboarding();
msgInput.focus(); return true;
}
@@ -820,6 +1048,7 @@ async function handleAuth(endpoint, credentials) {
if (endpoint === "login") {
currentUsername = username; authDiv.style.display = "none"; chatDiv.style.display = "block";
await Promise.all([displayModels(), loadSummary(), loadChatSessions(), loadPersonas()]);
+ maybeShowOnboarding();
setNotice("Ready.", "success"); msgInput.focus();
} else {
setAuthMessage("Registration successful. You can now log in.", "success");
@@ -844,7 +1073,12 @@ async function sendMessage() {
const res = await post("/chat", {message, model: modelSelect.value, chatId: activeChatId});
if (messagesDiv.contains(loadingMsg)) messagesDiv.removeChild(loadingMsg);
if (res.reply) {
- currentMessages.push({role: "bot", content: res.reply}); addMessage(res.reply); await loadSummary(); setNotice("Reply received.", "success");
+ currentMessages.push({role: "bot", content: res.reply});
+ addMessage(res.reply);
+ await loadSummary();
+ markOnboardingCompleted();
+ maybeShowPersonaNudge();
+ setNotice("Reply received.", "success");
} else {
addMessage(`[Error: ${res.error}]`); setNotice(res.error || "Failed to generate reply.", "error");
}
@@ -859,6 +1093,8 @@ async function sendMessage() {
function togglePersonaPopover(event) {
event.stopPropagation();
closeModelPopover();
+ closeContextPopover();
+ closeChatActionsPopover();
const isHidden = personaPopover.classList.contains("hidden");
personaPopover.classList.toggle("hidden", !isHidden);
personaMenuButton.setAttribute("aria-expanded", String(isHidden));
@@ -867,11 +1103,35 @@ function togglePersonaPopover(event) {
function toggleModelPopover(event) {
event.stopPropagation();
closePersonaPopover();
+ closeContextPopover();
+ closeChatActionsPopover();
const isHidden = modelPopover.classList.contains("hidden");
modelPopover.classList.toggle("hidden", !isHidden);
modelMenuButton.setAttribute("aria-expanded", String(isHidden));
}
+function toggleContextPopover(event) {
+ if (!contextPopover || !contextChipButton) return;
+ event.stopPropagation();
+ closeModelPopover();
+ closePersonaPopover();
+ closeChatActionsPopover();
+ const isHidden = contextPopover.classList.contains("hidden");
+ contextPopover.classList.toggle("hidden", !isHidden);
+ contextChipButton.setAttribute("aria-expanded", String(isHidden));
+}
+
+function toggleChatActionsPopover(event) {
+ if (!chatActionsPopover || !chatActionsMenuButton) return;
+ event.stopPropagation();
+ closeModelPopover();
+ closePersonaPopover();
+ closeContextPopover();
+ const isHidden = chatActionsPopover.classList.contains("hidden");
+ chatActionsPopover.classList.toggle("hidden", !isHidden);
+ chatActionsMenuButton.setAttribute("aria-expanded", String(isHidden));
+}
+
toggleButtons.forEach((btn) => btn.addEventListener("click", () => showAuthScreen(btn.dataset.target)));
loginForm.addEventListener("submit", (event) => { event.preventDefault(); void handleAuth("login", {username: loginUsernameInput.value.trim(), password: loginPasswordInput.value.trim()}); });
registerForm.addEventListener("submit", (event) => { event.preventDefault(); void handleAuth("register", {username: registerUsernameInput.value.trim(), password: registerPasswordInput.value.trim()}); });
@@ -879,12 +1139,23 @@ $("logout").addEventListener("click", async () => {
await post("/logout", {}); chatDiv.style.display = "none"; authDiv.style.display = "grid";
activeChatId = null; currentUsername = ""; currentSummary = null; chatSessions = []; currentMessages = []; assistantPersonas = []; userPersonas = [];
activeUserPersonaId = null; publishedPersonaIds = new Set(); messagesDiv.innerHTML = ""; renderChatList(); updateChatActionState();
+ updateComposerPlaceholder();
+ updateSendState();
showAuthScreen("login"); setAuthMessage("Logged out.", "success"); setNotice("Ready.");
});
sendBtn.addEventListener("click", () => { void sendMessage(); });
msgInput.addEventListener("keydown", (event) => { if (event.key === "Enter" && !event.shiftKey) { event.preventDefault(); void sendMessage(); } });
-msgInput.addEventListener("input", function () { this.style.height = "auto"; this.style.height = `${Math.min(this.scrollHeight, 180)}px`; });
+msgInput.addEventListener("input", function () {
+ this.style.height = "auto";
+ this.style.height = `${Math.min(this.scrollHeight, 180)}px`;
+ updateSendState();
+});
newChatBtn.addEventListener("click", () => { void createNewChat(); });
+if (startRoleplayBtn) {
+ startRoleplayBtn.addEventListener("click", () => {
+ window.location.href = "/market?view=ai-characters";
+ });
+}
renameChatBtn.addEventListener("click", () => { if (activeChatId) void renameChat(activeChatId); });
clearChatBtn.addEventListener("click", () => { if (activeChatId) void clearChat(activeChatId); }); exportChatBtn.addEventListener("click", exportCurrentChat);
chatSearchInput.addEventListener("input", renderChatList); modelSelect.addEventListener("change", () => { updateModelHelp(); updateContextRail(); });
@@ -919,19 +1190,57 @@ popupInput.addEventListener("keydown", (event) => {
closePopup(popupInput.value);
}
});
-modelMenuButton.addEventListener("click", toggleModelPopover); modelPopover.addEventListener("click", (event) => event.stopPropagation());
-personaMenuButton.addEventListener("click", togglePersonaPopover); personaPopover.addEventListener("click", (event) => event.stopPropagation());
-document.addEventListener("click", () => { closeModelPopover(); closePersonaPopover(); });
+if (modelMenuButton && modelPopover) {
+ modelMenuButton.addEventListener("click", toggleModelPopover);
+ modelPopover.addEventListener("click", (event) => event.stopPropagation());
+}
+if (personaMenuButton && personaPopover) {
+ personaMenuButton.addEventListener("click", togglePersonaPopover);
+ personaPopover.addEventListener("click", (event) => event.stopPropagation());
+}
+if (contextChipButton && contextPopover) {
+ contextChipButton.addEventListener("click", toggleContextPopover);
+ contextPopover.addEventListener("click", (event) => event.stopPropagation());
+}
+if (chatActionsMenuButton && chatActionsPopover) {
+ chatActionsMenuButton.addEventListener("click", toggleChatActionsPopover);
+ chatActionsPopover.addEventListener("click", (event) => event.stopPropagation());
+}
+if (onboardingContinue) {
+ onboardingContinue.addEventListener("click", () => {
+ onboardingStep = 2;
+ renderOnboardingStep();
+ });
+}
+if (onboardingBack) {
+ onboardingBack.addEventListener("click", () => {
+ onboardingStep = 1;
+ renderOnboardingStep();
+ });
+}
+if (onboardingSkip) {
+ onboardingSkip.addEventListener("click", markOnboardingCompleted);
+}
+if (onboardingModal) {
+ onboardingModal.addEventListener("click", (event) => {
+ if (event.target === onboardingModal) markOnboardingCompleted();
+ });
+}
+document.addEventListener("click", () => { closeAuxiliaryPopovers(); });
document.addEventListener("keydown", (event) => {
if (event.key === "Escape") {
- closeModelPopover();
- closePersonaPopover();
+ closeAuxiliaryPopovers();
closeRoleplayStarter();
closePersonaForm();
closePopup(false);
+ closeOnboarding();
}
});
window.addEventListener("load", async () => {
applyTheme(localStorage.getItem("krishd-theme") || "fakegpt", false);
- setNotice("Ready."); updateChatActionState(); await checkSession();
+ applyWorkspaceMode(localStorage.getItem("krishd-workspace-mode") || "basic", false);
+ setNotice("Ready.");
+ updateChatActionState();
+ updateSendState();
+ await checkSession();
});
diff --git a/public/settings.html b/public/settings.html
index e8a7734..1baef27 100644
--- a/public/settings.html
+++ b/public/settings.html
@@ -25,16 +25,10 @@
Loading settings...
-
-
-
-
-
@@ -89,30 +83,22 @@
-