Fix admin authorization and harden chat requests
This commit is contained in:
@@ -0,0 +1,422 @@
|
||||
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() {
|
||||
updateActivePersonaStatus(state.userPersonas, state.activeUserPersonaId, activeUserPersonaStatus, "No default user persona set.", "Default user persona");
|
||||
if (chatDrawerPersonaValue) {
|
||||
const active = state.userPersonas.find((persona) => persona.id === state.activeUserPersonaId);
|
||||
chatDrawerPersonaValue.textContent = active ? active.name : "None";
|
||||
if (headerPersonaName) headerPersonaName.textContent = active ? active.name : "None";
|
||||
}
|
||||
if (clearUserPersonaBtn) clearUserPersonaBtn.disabled = !state.activeUserPersonaId;
|
||||
}
|
||||
|
||||
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 updateActivePersonaStatus(items, activeId, statusEl, emptyText, prefix) {
|
||||
if (!statusEl) return;
|
||||
const active = items.find((persona) => persona.id === activeId);
|
||||
statusEl.textContent = active ? `${prefix}: ${active.name}` : emptyText;
|
||||
}
|
||||
|
||||
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;
|
||||
}
|
||||
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 === activeId ? " 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" : "Set default", () => equipPersona(persona.id, personaType), personaType === "user" && persona.id === activeId],
|
||||
["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 clearPersona(type) {
|
||||
if (type !== "user") return;
|
||||
const res = await post("/personas/user/clear", {});
|
||||
if (res.error) return setNotice(res.error, "error");
|
||||
state.activeUserPersonaId = null;
|
||||
renderPersonaList(state.userPersonas, state.activeUserPersonaId, userPersonaList, "user");
|
||||
setNotice("Persona cleared.", "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
|
||||
});
|
||||
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 {
|
||||
updateRoleplaySuggestions,
|
||||
setRoleplayStarterNotice,
|
||||
renderRoleplayStarterStep,
|
||||
maybeShowPersonaNudge,
|
||||
openPersonaForm,
|
||||
closePersonaForm,
|
||||
openRoleplayStarter,
|
||||
closeRoleplayStarter,
|
||||
loadPersonas,
|
||||
clearPersona,
|
||||
savePersona,
|
||||
startRoleplay
|
||||
};
|
||||
}
|
||||
Reference in New Issue
Block a user