import { personaFormNotice, roleplayCharacterSelect, roleplayUserPersonaSelect, roleplayStarterSuggestions, roleplayScenarioInput, roleplayStarterNotice, roleplayStarterStepOne, roleplayStarterStepTwo, roleplayStarterBack, roleplayStarterTitle, roleplayStarterIntroCopy, roleplayStarterCancel, roleplayStarterConfirm, activeUserPersonaStatus, chatDrawerPersonaValue, headerPersonaName, clearUserPersonaBtn, personaModal, personaNameInput, personaFormTitle, personaTypeSelect, personaPronounsInput, personaAppearanceInput, personaBackgroundInput, personaDetailsInput, personaExampleDialoguesInput, personaExamplesField, roleplayStarterModal, userPersonaList, modelSelect } from "./dom.js?v=20260909-account-controls"; import {closeChatDrawer} from "./chatDrawer.js?v=20260909-account-controls"; import {openModal, closeModal} from "./modalFocus.js"; import {get, post, put, del} from "./api.js?v=20260909-account-controls"; export function createPersonaController({ state, setNotice, closePopup, updateWorkspaceCopy, loadChatSessions, confirmPopup, setChatLoading, setChatActivity, setActiveChat }) { function setPersonaFormNotice(message = "", noticeState = "") { if (!personaFormNotice) return; if (!message) { personaFormNotice.textContent = ""; personaFormNotice.className = "status hidden"; return; } personaFormNotice.textContent = message; personaFormNotice.className = noticeState ? `status ${noticeState}` : "status"; } 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, state.assistantPersonas) || "the character"; const userName = userPersonaId ? getPersonaName(userPersonaId, state.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 setRoleplayStarterNotice(message = "", noticeState = "") { if (!roleplayStarterNotice) return; if (!message) { roleplayStarterNotice.textContent = ""; roleplayStarterNotice.className = "status hidden"; return; } roleplayStarterNotice.textContent = message; roleplayStarterNotice.className = noticeState ? `status ${noticeState}` : "status"; } function renderRoleplayStarterStep() { const isSceneStep = state.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 renderPersonaSection() { const activeChat = state.chatSessions.find((chat) => chat.id === state.activeChatId); const chatPersona = state.userPersonas.find((persona) => persona.id === activeChat?.user_persona_id); if (activeUserPersonaStatus) { activeUserPersonaStatus.textContent = activeChat ? `Using in this chat: ${chatPersona?.name || "yourself"}. Default: ${getPersonaName(state.activeUserPersonaId, state.userPersonas) || "yourself"}.` : `Default user persona: ${getPersonaName(state.activeUserPersonaId, state.userPersonas) || "yourself"}.`; } if (chatDrawerPersonaValue) { chatDrawerPersonaValue.textContent = chatPersona?.name || "Yourself"; if (headerPersonaName) headerPersonaName.textContent = chatPersona?.name || "Yourself"; } if (clearUserPersonaBtn) clearUserPersonaBtn.disabled = !activeChat?.user_persona_id; } function maybeShowPersonaNudge() { if (localStorage.getItem("krishd-persona-nudge-shown") === "true") return; const assistantOnlyChats = state.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 openPersonaForm(persona = null, personaType = "assistant") { closeChatDrawer(); closePopup(false); openModal(personaModal, personaNameInput); if (persona) { state.editingPersonaId = persona.id; state.editingPersonaType = persona.persona_type || "assistant"; personaFormTitle.textContent = `Edit ${state.editingPersonaType === "user" ? "your persona" : "AI Character"}`; personaTypeSelect.value = state.editingPersonaType; personaTypeSelect.disabled = true; personaNameInput.value = persona.name || ""; personaPronounsInput.value = persona.pronouns || ""; personaAppearanceInput.value = persona.appearance || ""; personaBackgroundInput.value = persona.background || ""; personaDetailsInput.value = persona.details || ""; personaExampleDialoguesInput.value = persona.example_dialogues || ""; } else { state.editingPersonaId = null; state.editingPersonaType = personaType; personaFormTitle.textContent = `Create ${personaType === "user" ? "your persona" : "AI Character"}`; personaTypeSelect.value = personaType; personaTypeSelect.disabled = false; personaNameInput.value = ""; personaPronounsInput.value = ""; personaAppearanceInput.value = ""; personaBackgroundInput.value = ""; personaDetailsInput.value = ""; personaExampleDialoguesInput.value = ""; } personaExamplesField.classList.toggle("hidden", personaTypeSelect.value !== "assistant"); setPersonaFormNotice(""); personaNameInput.focus(); } function closePersonaForm() { closeModal(personaModal); state.editingPersonaId = null; state.editingPersonaType = "assistant"; personaTypeSelect.disabled = false; setPersonaFormNotice(""); } 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); state.assistantPersonas.forEach((persona) => { const option = document.createElement("option"); option.value = String(persona.id); option.textContent = persona.name; option.selected = persona.id === state.roleplayPresetCharacterId; roleplayCharacterSelect.appendChild(option); }); if (state.roleplayPresetCharacterId) { roleplayCharacterSelect.value = String(state.roleplayPresetCharacterId); } roleplayUserPersonaSelect.innerHTML = ""; const noneOption = document.createElement("option"); noneOption.value = ""; noneOption.textContent = "Myself (no user persona)"; roleplayUserPersonaSelect.appendChild(noneOption); state.userPersonas.forEach((persona) => { const option = document.createElement("option"); option.value = String(persona.id); option.textContent = persona.name; option.selected = persona.id === state.activeUserPersonaId; roleplayUserPersonaSelect.appendChild(option); }); if (state.assistantPersonas.length === 0) { setRoleplayStarterNotice("Create an AI Character first, then start roleplay.", "error"); roleplayStarterConfirm.disabled = true; } else { setRoleplayStarterNotice(""); roleplayStarterConfirm.disabled = false; } updateRoleplaySuggestions(); renderRoleplayStarterStep(); } function openRoleplayStarter(characterId = null) { closeChatDrawer(); state.roleplayPresetCharacterId = characterId; state.roleplayStarterStep = 1; populateRoleplayStarter(); if (roleplayScenarioInput) roleplayScenarioInput.value = ""; setRoleplayStarterNotice(""); openModal(roleplayStarterModal, roleplayCharacterSelect); } function closeRoleplayStarter() { if (!roleplayStarterModal) return; closeModal(roleplayStarterModal); state.roleplayPresetCharacterId = null; state.roleplayStarterStep = 1; roleplayStarterConfirm.disabled = false; if (roleplayScenarioInput) roleplayScenarioInput.value = ""; setRoleplayStarterNotice(""); renderRoleplayStarterStep(); } function goToRoleplayStarterSceneStep() { if (!state.assistantPersonas.length) { return setRoleplayStarterNotice("Create an AI Character first, then start roleplay.", "error"); } if (!roleplayCharacterSelect.value) { return setRoleplayStarterNotice("Choose a character to continue.", "error"); } state.roleplayStarterStep = 2; setRoleplayStarterNotice(""); renderRoleplayStarterStep(); if (roleplayScenarioInput) roleplayScenarioInput.focus(); } function renderPersonaList(items, activeId, listElement, personaType) { if (!listElement) { renderPersonaSection(); return; } listElement.innerHTML = ""; if (!items.length) { const empty = document.createElement("p"); empty.className = "status persona-status"; empty.textContent = "No personas yet. Create one to get started."; listElement.appendChild(empty); renderPersonaSection(); return; } const selectedId = personaType === "user" ? state.chatSessions.find((chat) => chat.id === state.activeChatId)?.user_persona_id ?? null : activeId; items.forEach((persona) => { const item = document.createElement("div"), titleRow = document.createElement("div"), title = document.createElement("h4"); const tag = document.createElement("span"), meta = document.createElement("p"), actions = document.createElement("div"); item.className = `persona-item${personaType === "user" && persona.id === selectedId ? " active" : ""}`; titleRow.className = "persona-title-row"; actions.className = "persona-item-actions"; tag.className = `persona-role-tag ${personaType === "user" ? "user" : "ai"}`; tag.textContent = personaType === "user" ? "You" : "AI"; title.textContent = persona.name; meta.textContent = persona.pronouns ? `Pronouns: ${persona.pronouns}` : "Pronouns: n/a"; [[personaType === "assistant" ? "Start roleplay" : "Use in this chat", () => personaType === "user" ? setChatUserPersona(persona.id) : equipPersona(persona.id, personaType), personaType === "user" && persona.id === selectedId], ...(personaType === "user" ? [["Set default", () => equipPersona(persona.id, personaType), persona.id === state.activeUserPersonaId]] : []), ["Edit", () => openPersonaForm(persona), false], [state.publishedPersonaIds.has(persona.id) ? "Update listing" : "Publish", () => publishPersona(persona.id), false], ["Delete", () => deletePersona(persona.id), false]] .forEach(([label, handler, disabled]) => { const btn = document.createElement("button"); btn.type = "button"; btn.textContent = label; btn.disabled = disabled; btn.addEventListener("click", handler); actions.appendChild(btn); }); if (state.publishedPersonaIds.has(persona.id)) { const btn = document.createElement("button"); btn.type = "button"; btn.textContent = "Unpublish"; btn.addEventListener("click", () => unpublishPersona(persona.id)); actions.appendChild(btn); } titleRow.append(title, tag); item.append(titleRow, meta, actions); listElement.appendChild(item); }); renderPersonaSection(); } async function loadPersonas() { const res = await get("/personas"); if (res.error) return setNotice(res.error, "error"); state.assistantPersonas = res.assistantPersonas || []; state.userPersonas = res.userPersonas || []; state.activeUserPersonaId = res.activeUserPersonaId || null; state.publishedPersonaIds = new Set(res.publishedPersonaIds || []); renderPersonaList(state.userPersonas, state.activeUserPersonaId, userPersonaList, "user"); populateRoleplayStarter(); renderPersonaSection(); updateWorkspaceCopy(); } async function equipPersona(id, type) { if (type === "assistant") { openRoleplayStarter(id); return; } const res = await post(`/personas/${id}/equip-user`, {}); if (res.error) return setNotice(res.error, "error"); state.activeUserPersonaId = id; renderPersonaList(state.userPersonas, state.activeUserPersonaId, userPersonaList, "user"); setNotice("User persona updated.", "success"); } async function setChatUserPersona(id) { if (!state.activeChatId) return setNotice("Open a chat before choosing a user persona.", "error"); const res = await put(`/chats/${state.activeChatId}/user-persona`, {userPersonaId: id}); if (res.error) return setNotice(res.error, "error"); const chat = state.chatSessions.find((item) => item.id === state.activeChatId); if (chat) Object.assign(chat, res.chat); renderPersonaList(state.userPersonas, res.chat.user_persona_id, userPersonaList, "user"); setNotice("User persona switched for this chat.", "success"); } async function clearChatUserPersona() { if (!state.activeChatId) return; const res = await put(`/chats/${state.activeChatId}/user-persona`, {userPersonaId: null}); if (res.error) return setNotice(res.error, "error"); const chat = state.chatSessions.find((item) => item.id === state.activeChatId); if (chat) Object.assign(chat, res.chat); renderPersonaList(state.userPersonas, null, userPersonaList, "user"); setNotice("This chat will use yourself.", "success"); } async function savePersona() { const payload = { personaType: personaTypeSelect.value, name: personaNameInput.value.trim(), pronouns: personaPronounsInput.value.trim(), appearance: personaAppearanceInput.value.trim(), background: personaBackgroundInput.value.trim(), details: personaDetailsInput.value.trim(), exampleDialogues: personaTypeSelect.value === "assistant" ? personaExampleDialoguesInput.value.trim() : "" }; if (!payload.name) return setPersonaFormNotice("Persona name is required.", "error"); const res = state.editingPersonaId ? await put(`/personas/${state.editingPersonaId}`, payload) : await post("/personas", payload); if (res.error) return setPersonaFormNotice(res.error, "error"); const wasEditing = Boolean(state.editingPersonaId); await loadPersonas(); if (payload.personaType === "assistant") await loadChatSessions(); closePersonaForm(); setNotice(wasEditing ? "Persona updated." : "Persona created.", "success"); } async function deletePersona(id) { const persona = state.assistantPersonas.find((item) => item.id === id) || state.userPersonas.find((item) => item.id === id); if (!persona) return; const confirmed = await confirmPopup({ eyebrow: persona.persona_type === "user" ? "Your persona" : "AI Character", title: "Delete persona", description: `Delete "${persona.name}"? This removes it from your library and any active slot.`, confirmLabel: "Delete", danger: true }); if (!confirmed) return; const res = await del(`/personas/${id}`); if (res.error) return setNotice(res.error, "error"); await loadPersonas(); if (persona.persona_type === "assistant") await loadChatSessions(); setNotice("Persona deleted.", "success"); } async function publishPersona(id) { const res = await post(`/personas/${id}/publish`, {}); if (res.error) return setNotice(res.error, "error"); await loadPersonas(); setNotice("Persona published.", "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"); } async function startRoleplay() { if (state.roleplayStarterStep === 1) { goToRoleplayStarterSceneStep(); return; } setChatLoading(true, "Starting roleplay..."); setChatActivity(true, { eyebrow: "Starting roleplay", title: "Building the opening scene", detail: "Generating the first beat and opening the roleplay chat." }); const assistantPersonaId = Number(roleplayCharacterSelect.value); const userPersonaId = roleplayUserPersonaSelect.value ? Number(roleplayUserPersonaSelect.value) : null; const scenarioPrompt = roleplayScenarioInput?.value.trim() || ""; if (!assistantPersonaId) { setChatLoading(false); setChatActivity(false); return setRoleplayStarterNotice("Choose a character to start the roleplay.", "error"); } roleplayStarterConfirm.disabled = true; setRoleplayStarterNotice("Building the opening scene..."); const res = await post("/roleplays/start", { assistantPersonaId, userPersonaId, scenarioPrompt, model: modelSelect.value }, {timeoutMs: 660000}); roleplayStarterConfirm.disabled = false; if (res.error || !res.chat) { setChatLoading(false); setChatActivity(false); return setRoleplayStarterNotice(res.error || "Unable to start roleplay.", "error"); } closeRoleplayStarter(); await loadChatSessions(); await setActiveChat(res.chat.id); setNotice( res.degraded ? "Roleplay started, but the opening scene timed out. Send the first message to continue." : res.generatedInitialMessage ? "Roleplay started with a fresh scene." : "Roleplay reopened.", res.degraded ? "error" : "success" ); setChatLoading(false); setChatActivity(false); } return { renderPersonaSection, updateRoleplaySuggestions, setRoleplayStarterNotice, renderRoleplayStarterStep, maybeShowPersonaNudge, openPersonaForm, closePersonaForm, openRoleplayStarter, closeRoleplayStarter, loadPersonas, clearChatUserPersona, savePersona, startRoleplay }; }