diff --git a/public/index.html b/public/index.html index 2713e1a..9fe1fb4 100644 --- a/public/index.html +++ b/public/index.html @@ -306,18 +306,18 @@
-

Pick a character, choose who you are in the scene, and generate an opening message in one step.

+

Pick a character and choose who you are before moving on to the opening scene.

-
+
-
-
+
diff --git a/public/market.html b/public/market.html index a19386d..111b729 100644 --- a/public/market.html +++ b/public/market.html @@ -75,6 +75,7 @@

Persona preview

Persona details

+

Review this persona and choose how you want to use it.

@@ -104,14 +105,20 @@
- - + + +
diff --git a/public/market.js b/public/market.js index 79d3c42..24a01fe 100644 --- a/public/market.js +++ b/public/market.js @@ -16,10 +16,14 @@ const marketPreviewAppearance = $("marketPreviewAppearance"); const marketPreviewBackground = $("marketPreviewBackground"); const marketPreviewDetails = $("marketPreviewDetails"); const marketPreviewExampleDialogues = $("marketPreviewExampleDialogues"); +const marketPreviewIntro = $("marketPreviewIntro"); const marketPreviewUserPersonaField = $("marketPreviewUserPersonaField"); const marketPreviewUserPersonaSelect = $("marketPreviewUserPersonaSelect"); const marketPreviewScenarioField = $("marketPreviewScenarioField"); const marketPreviewScenario = $("marketPreviewScenario"); +const marketPreviewStepOne = $("marketPreviewStepOne"); +const marketPreviewStepTwo = $("marketPreviewStepTwo"); +const marketPreviewBack = $("marketPreviewBack"); const marketPreviewConfirm = $("marketPreviewConfirm"); const marketPreviewClose = $("marketPreviewClose"); const marketPersonaModal = $("marketPersonaModal"); @@ -56,6 +60,7 @@ let editingPersonaId = null; let activeUserPersonaId = null; let activeMarketView = "ai-characters"; let marketActivityDepth = 0; +let marketPreviewStep = 1; async function request(url, data, method = "POST") { try { @@ -179,10 +184,32 @@ function setCollapsiblePreviewText(element, value, maxLength = 220) { function closeMarketPreview() { marketPreviewModal.classList.add("hidden"); pendingMarketPersona = null; + marketPreviewStep = 1; marketPreviewUserPersonaSelect.innerHTML = ""; marketPreviewScenario.value = ""; } +function renderMarketPreviewStep() { + const isScenarioStep = marketPreviewStep === 2; + if (marketPreviewStepOne) marketPreviewStepOne.classList.toggle("hidden", isScenarioStep || pendingMarketPersona?.persona_type !== "assistant"); + if (marketPreviewStepTwo) marketPreviewStepTwo.classList.toggle("hidden", !isScenarioStep || pendingMarketPersona?.persona_type !== "assistant"); + if (marketPreviewBack) marketPreviewBack.classList.toggle("hidden", !isScenarioStep || pendingMarketPersona?.persona_type !== "assistant"); + if (marketPreviewIntro) { + marketPreviewIntro.textContent = pendingMarketPersona?.persona_type === "assistant" + ? (isScenarioStep + ? "Add an optional custom scenario. If left empty, the AI Character will invent its own opening scene." + : "Choose who you are in this roleplay before moving on to the opening scene.") + : "Review this persona and choose how you want to use it."; + } + if (marketPreviewConfirm) { + if (pendingMarketPersona?.persona_type !== "assistant") { + marketPreviewConfirm.textContent = "Collect persona"; + } else { + marketPreviewConfirm.textContent = isScenarioStep ? "Start roleplay" : "Continue"; + } + } +} + function populateUserPersonaChoices() { marketPreviewUserPersonaSelect.innerHTML = ""; @@ -214,6 +241,7 @@ function populateUserPersonaChoices() { function openMarketPreview(persona) { pendingMarketPersona = persona; + marketPreviewStep = 1; marketPreviewName.textContent = persona.name; marketPreviewMeta.textContent = buildPersonaMeta(persona); setCollapsiblePreviewText(marketPreviewPronouns, persona.pronouns, 80); @@ -224,14 +252,13 @@ function openMarketPreview(persona) { const isAssistant = persona.persona_type === "assistant"; marketPreviewUserPersonaField.classList.toggle("hidden", !isAssistant); - marketPreviewScenarioField.classList.toggle("hidden", !isAssistant); - marketPreviewConfirm.textContent = isAssistant ? "Start roleplay" : "Collect persona"; if (isAssistant) { populateUserPersonaChoices(); marketPreviewConfirm.classList.add("hidden"); } else { marketPreviewConfirm.classList.remove("hidden"); } + renderMarketPreviewStep(); marketPreviewModal.classList.remove("hidden"); } @@ -460,6 +487,16 @@ marketPreviewConfirm.addEventListener("click", async () => { if (!pendingMarketPersona) return; const marketId = pendingMarketPersona.id; if (pendingMarketPersona.persona_type === "assistant") { + if (marketPreviewStep === 1) { + if (!marketPreviewUserPersonaSelect.value) { + setMarketStatus("Choose who you are before continuing.", "error"); + return; + } + marketPreviewStep = 2; + renderMarketPreviewStep(); + marketPreviewScenario.focus(); + return; + } await startMarketPersonaChat( marketId, marketPreviewUserPersonaSelect.value || "self", @@ -471,6 +508,13 @@ marketPreviewConfirm.addEventListener("click", async () => { closeMarketPreview(); await collectMarketPersona(marketId); }); +if (marketPreviewBack) { + marketPreviewBack.addEventListener("click", () => { + marketPreviewStep = 1; + renderMarketPreviewStep(); + marketPreviewUserPersonaSelect.focus(); + }); +} marketPersonaClose.addEventListener("click", closePersonaForm); marketPersonaModal.addEventListener("click", (event) => { if (event.target === marketPersonaModal) closePersonaForm(); diff --git a/public/script.js b/public/script.js index 3fa45a4..9bf5c5f 100644 --- a/public/script.js +++ b/public/script.js @@ -28,6 +28,8 @@ 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 roleplayStarterTitle = $("roleplayStarterTitle"), roleplayStarterIntroCopy = $("roleplayStarterIntroCopy"), roleplayStarterBack = $("roleplayStarterBack"); +const roleplayStarterStepOne = $("roleplayStarterStepOne"), roleplayStarterStepTwo = $("roleplayStarterStepTwo"); const roleplayScenarioInput = $("roleplayScenarioInput"), roleplayStarterSuggestions = $("roleplayStarterSuggestions"); const popupModal = $("popupModal"), popupCloseBtn = $("popupClose"), popupCancelBtn = $("popupCancel"), popupConfirmBtn = $("popupConfirm"); const popupTitle = $("popupTitle"), popupEyebrow = $("popupEyebrow"), popupDescription = $("popupDescription"), popupField = $("popupField"); @@ -45,6 +47,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 roleplayStarterStep = 1; let noticeTimer = null; let workspaceMode = localStorage.getItem("krishd-workspace-mode") || "basic"; let onboardingStep = 1, onboardingIntent = "ask"; @@ -721,6 +724,27 @@ function setRoleplayStarterNotice(message = "", state = "") { roleplayStarterNotice.className = state ? `status ${state}` : "status"; } +function renderRoleplayStarterStep() { + const isSceneStep = roleplayStarterStep === 2; + if (roleplayStarterStepOne) roleplayStarterStepOne.classList.toggle("hidden", isSceneStep); + if (roleplayStarterStepTwo) roleplayStarterStepTwo.classList.toggle("hidden", !isSceneStep); + if (roleplayStarterBack) roleplayStarterBack.classList.toggle("hidden", !isSceneStep); + if (roleplayStarterTitle) { + roleplayStarterTitle.textContent = isSceneStep ? "Shape the opening scene" : "Choose your roleplay"; + } + if (roleplayStarterIntroCopy) { + roleplayStarterIntroCopy.textContent = isSceneStep + ? "Add an optional custom scenario. If you leave it empty, the AI Character will invent its own opening scene." + : "Pick a character and choose who you are before moving on to the opening scene."; + } + if (roleplayStarterCancel) { + roleplayStarterCancel.textContent = isSceneStep ? "Close" : "Cancel"; + } + if (roleplayStarterConfirm) { + roleplayStarterConfirm.textContent = isSceneStep ? "Start roleplay" : "Continue"; + } +} + function updateChatParticipants() { const chat = getChatById(activeChatId); if (!chatModePill || !chatCharacterPill || !chatUserPersonaPill || !chatScenePill) return; @@ -977,10 +1001,12 @@ function populateRoleplayStarter() { roleplayStarterConfirm.disabled = false; } updateRoleplaySuggestions(); + renderRoleplayStarterStep(); } function openRoleplayStarter(characterId = null) { closeAuxiliaryPopovers(); roleplayPresetCharacterId = characterId; + roleplayStarterStep = 1; populateRoleplayStarter(); if (roleplayScenarioInput) roleplayScenarioInput.value = ""; setRoleplayStarterNotice(""); @@ -991,9 +1017,24 @@ function closeRoleplayStarter() { if (!roleplayStarterModal) return; roleplayStarterModal.classList.add("hidden"); roleplayPresetCharacterId = null; + roleplayStarterStep = 1; roleplayStarterConfirm.disabled = false; if (roleplayScenarioInput) roleplayScenarioInput.value = ""; setRoleplayStarterNotice(""); + renderRoleplayStarterStep(); +} + +function goToRoleplayStarterSceneStep() { + if (!assistantPersonas.length) { + return setRoleplayStarterNotice("Create an AI Character first, then start roleplay.", "error"); + } + if (!roleplayCharacterSelect.value) { + return setRoleplayStarterNotice("Choose a character to continue.", "error"); + } + roleplayStarterStep = 2; + setRoleplayStarterNotice(""); + renderRoleplayStarterStep(); + if (roleplayScenarioInput) roleplayScenarioInput.focus(); } function closeModelPopover() { if (!modelPopover || !modelMenuButton) return; @@ -1255,6 +1296,10 @@ async function createNewChat() { } async function startRoleplay() { + if (roleplayStarterStep === 1) { + goToRoleplayStarterSceneStep(); + return; + } setChatLoading(true, "Starting roleplay..."); setChatActivity(true, { eyebrow: "Starting roleplay", @@ -1378,9 +1423,14 @@ async function displayModels() { return setNotice("Models could not be loaded.", "error"); } modelSelect.innerHTML = ""; + const preferredModelId = "mistral:latest"; + const preferredIndex = models.findIndex((model) => model.model === preferredModelId); models.forEach((model, index) => { const option = document.createElement("option"); - option.value = model.model; option.textContent = model.name; option.selected = index === 0; modelSelect.appendChild(option); + option.value = model.model; + option.textContent = model.name; + option.selected = preferredIndex >= 0 ? index === preferredIndex : index === 0; + modelSelect.appendChild(option); }); updateModelHelp(); updateContextRail(); @@ -1531,6 +1581,14 @@ personaTypeSelect.addEventListener("change", () => { personaModal.addEventListener("click", (event) => { if (event.target === personaModal) closePersonaForm(); }); roleplayStarterClose.addEventListener("click", closeRoleplayStarter); roleplayStarterCancel.addEventListener("click", closeRoleplayStarter); +if (roleplayStarterBack) { + roleplayStarterBack.addEventListener("click", () => { + roleplayStarterStep = 1; + setRoleplayStarterNotice(""); + renderRoleplayStarterStep(); + roleplayCharacterSelect.focus(); + }); +} roleplayStarterConfirm.addEventListener("click", () => { void startRoleplay(); }); roleplayCharacterSelect.addEventListener("change", updateRoleplaySuggestions); roleplayUserPersonaSelect.addEventListener("change", updateRoleplaySuggestions); diff --git a/public/style.css b/public/style.css index aade007..1f35c45 100644 --- a/public/style.css +++ b/public/style.css @@ -1559,6 +1559,11 @@ select:focus { border: 1px solid rgba(24, 32, 31, 0.08); } +.roleplay-starter-page { + display: grid; + gap: 14px; +} + .persona-status { text-align: left; }