implement custom popups for alerts, inputs and confirmations
This commit is contained in:
@@ -248,6 +248,29 @@
|
|||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
|
<div id="popupModal" class="modal-overlay hidden">
|
||||||
|
<div class="modal-card popup-card" role="dialog" aria-modal="true" aria-labelledby="popupTitle" aria-describedby="popupDescription">
|
||||||
|
<div class="modal-header">
|
||||||
|
<div class="persona-modal-heading">
|
||||||
|
<p id="popupEyebrow" class="eyebrow">Action</p>
|
||||||
|
<h3 id="popupTitle">Confirm action</h3>
|
||||||
|
</div>
|
||||||
|
<button id="popupClose" type="button" class="icon-button">Close</button>
|
||||||
|
</div>
|
||||||
|
<div class="popup-content">
|
||||||
|
<p id="popupDescription" class="subtitle">Please confirm this action.</p>
|
||||||
|
<label id="popupField" class="field hidden">
|
||||||
|
<span id="popupInputLabel">Value</span>
|
||||||
|
<input id="popupInput" type="text" autocomplete="off"/>
|
||||||
|
</label>
|
||||||
|
</div>
|
||||||
|
<div class="popup-actions">
|
||||||
|
<button id="popupCancel" type="button" class="secondary-action">Cancel</button>
|
||||||
|
<button id="popupConfirm" type="button">Confirm</button>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
<script src="https://cdn.jsdelivr.net/npm/marked/marked.min.js" defer></script>
|
<script src="https://cdn.jsdelivr.net/npm/marked/marked.min.js" defer></script>
|
||||||
<script src="https://cdn.jsdelivr.net/npm/dompurify@3.0.6/dist/purify.min.js" defer></script>
|
<script src="https://cdn.jsdelivr.net/npm/dompurify@3.0.6/dist/purify.min.js" defer></script>
|
||||||
<script src="script.js" defer></script>
|
<script src="script.js" defer></script>
|
||||||
|
|||||||
+121
-5
@@ -19,12 +19,16 @@ const personaFormNotice = $("personaFormNotice");
|
|||||||
const newPersonaBtn = $("newPersona"), newUserPersonaBtn = $("newUserPersona"), clearPersonaBtn = $("clearPersona"), clearUserPersonaBtn = $("clearUserPersona");
|
const newPersonaBtn = $("newPersona"), newUserPersonaBtn = $("newUserPersona"), clearPersonaBtn = $("clearPersona"), clearUserPersonaBtn = $("clearUserPersona");
|
||||||
const activePersonaStatus = $("activePersonaStatus"), activeUserPersonaStatus = $("activeUserPersonaStatus");
|
const activePersonaStatus = $("activePersonaStatus"), activeUserPersonaStatus = $("activeUserPersonaStatus");
|
||||||
const personaModal = $("personaModal"), personaCloseBtn = $("personaClose"), personaMenuButton = $("personaMenuButton"), personaPopover = $("personaPopover");
|
const personaModal = $("personaModal"), personaCloseBtn = $("personaClose"), personaMenuButton = $("personaMenuButton"), personaPopover = $("personaPopover");
|
||||||
|
const popupModal = $("popupModal"), popupCloseBtn = $("popupClose"), popupCancelBtn = $("popupCancel"), popupConfirmBtn = $("popupConfirm");
|
||||||
|
const popupTitle = $("popupTitle"), popupEyebrow = $("popupEyebrow"), popupDescription = $("popupDescription"), popupField = $("popupField");
|
||||||
|
const popupInputLabel = $("popupInputLabel"), popupInput = $("popupInput");
|
||||||
const authScreens = document.querySelectorAll(".auth-screen"), toggleButtons = document.querySelectorAll(".auth-toggle .toggle");
|
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 themeNameTargets = document.querySelectorAll("[data-theme-name]"), themeLogoTargets = document.querySelectorAll("[data-theme-logo]");
|
||||||
|
|
||||||
let isProcessing = false, activeChatId = null, editingPersonaId = null, editingPersonaType = "assistant", currentUsername = "";
|
let isProcessing = false, activeChatId = null, editingPersonaId = null, editingPersonaType = "assistant", currentUsername = "";
|
||||||
let chatSessions = [], currentMessages = [], assistantPersonas = [], userPersonas = [];
|
let chatSessions = [], currentMessages = [], assistantPersonas = [], userPersonas = [];
|
||||||
let activePersonaId = null, activeUserPersonaId = null, currentSummary = null, publishedPersonaIds = new Set();
|
let activePersonaId = null, activeUserPersonaId = null, currentSummary = null, publishedPersonaIds = new Set();
|
||||||
|
let popupResolver = null, popupMode = null, popupLastFocus = null;
|
||||||
|
|
||||||
const themes = {
|
const themes = {
|
||||||
"fakegpt": {name: "FakeGPT", short: "FG"},
|
"fakegpt": {name: "FakeGPT", short: "FG"},
|
||||||
@@ -241,6 +245,7 @@ function setActivePersonaStatus() {
|
|||||||
|
|
||||||
function openPersonaForm(persona = null, personaType = "assistant") {
|
function openPersonaForm(persona = null, personaType = "assistant") {
|
||||||
closePersonaPopover();
|
closePersonaPopover();
|
||||||
|
closePopup(false);
|
||||||
personaModal.classList.remove("hidden");
|
personaModal.classList.remove("hidden");
|
||||||
if (persona) {
|
if (persona) {
|
||||||
editingPersonaId = persona.id; editingPersonaType = persona.persona_type || "assistant";
|
editingPersonaId = persona.id; editingPersonaType = persona.persona_type || "assistant";
|
||||||
@@ -264,6 +269,61 @@ function closeModelPopover() {
|
|||||||
modelPopover.classList.add("hidden");
|
modelPopover.classList.add("hidden");
|
||||||
modelMenuButton.setAttribute("aria-expanded", "false");
|
modelMenuButton.setAttribute("aria-expanded", "false");
|
||||||
}
|
}
|
||||||
|
function isPopupOpen() { return popupModal && !popupModal.classList.contains("hidden"); }
|
||||||
|
function finishPopup(result) {
|
||||||
|
if (typeof popupResolver === "function") popupResolver(result);
|
||||||
|
popupResolver = null;
|
||||||
|
popupMode = null;
|
||||||
|
}
|
||||||
|
function closePopup(resolveValue = null) {
|
||||||
|
if (!popupModal || popupModal.classList.contains("hidden")) return;
|
||||||
|
popupModal.classList.add("hidden");
|
||||||
|
popupField.classList.add("hidden");
|
||||||
|
popupInput.value = "";
|
||||||
|
popupConfirmBtn.disabled = false;
|
||||||
|
const nextFocus = popupLastFocus;
|
||||||
|
popupLastFocus = null;
|
||||||
|
finishPopup(resolveValue);
|
||||||
|
if (nextFocus instanceof HTMLElement) nextFocus.focus();
|
||||||
|
}
|
||||||
|
function openPopup(options) {
|
||||||
|
if (!popupModal) return Promise.resolve(null);
|
||||||
|
if (isPopupOpen()) closePopup(null);
|
||||||
|
popupLastFocus = document.activeElement instanceof HTMLElement ? document.activeElement : null;
|
||||||
|
popupMode = options.mode;
|
||||||
|
popupEyebrow.textContent = options.eyebrow || "Action";
|
||||||
|
popupTitle.textContent = options.title || "Confirm action";
|
||||||
|
popupDescription.textContent = options.description || "";
|
||||||
|
popupConfirmBtn.textContent = options.confirmLabel || "Confirm";
|
||||||
|
popupCancelBtn.textContent = options.cancelLabel || "Cancel";
|
||||||
|
popupConfirmBtn.classList.toggle("danger", Boolean(options.danger));
|
||||||
|
popupField.classList.toggle("hidden", options.mode !== "prompt");
|
||||||
|
if (options.mode === "prompt") {
|
||||||
|
popupInputLabel.textContent = options.label || "Value";
|
||||||
|
popupInput.value = options.value || "";
|
||||||
|
popupInput.placeholder = options.placeholder || "";
|
||||||
|
} else {
|
||||||
|
popupInput.value = "";
|
||||||
|
}
|
||||||
|
popupModal.classList.remove("hidden");
|
||||||
|
return new Promise((resolve) => {
|
||||||
|
popupResolver = resolve;
|
||||||
|
requestAnimationFrame(() => {
|
||||||
|
if (options.mode === "prompt") {
|
||||||
|
popupInput.focus();
|
||||||
|
popupInput.select();
|
||||||
|
} else {
|
||||||
|
popupConfirmBtn.focus();
|
||||||
|
}
|
||||||
|
});
|
||||||
|
});
|
||||||
|
}
|
||||||
|
function confirmPopup(options) {
|
||||||
|
return openPopup({...options, mode: "confirm"}).then((result) => Boolean(result));
|
||||||
|
}
|
||||||
|
function promptPopup(options) {
|
||||||
|
return openPopup({...options, mode: "prompt"}).then((result) => typeof result === "string" ? result : null);
|
||||||
|
}
|
||||||
|
|
||||||
function renderPersonaList(items, activeId, listElement, personaType) {
|
function renderPersonaList(items, activeId, listElement, personaType) {
|
||||||
listElement.innerHTML = "";
|
listElement.innerHTML = "";
|
||||||
@@ -342,7 +402,15 @@ async function savePersona() {
|
|||||||
|
|
||||||
async function deletePersona(id) {
|
async function deletePersona(id) {
|
||||||
const persona = assistantPersonas.find((item) => item.id === id) || userPersonas.find((item) => item.id === id);
|
const persona = assistantPersonas.find((item) => item.id === id) || userPersonas.find((item) => item.id === id);
|
||||||
if (!persona || !window.confirm(`Delete persona "${persona.name}"?`)) return;
|
if (!persona) return;
|
||||||
|
const confirmed = await confirmPopup({
|
||||||
|
eyebrow: persona.persona_type === "user" ? "Your persona" : "AI persona",
|
||||||
|
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}`);
|
const res = await del(`/personas/${id}`);
|
||||||
if (res.error) return setNotice(res.error, "error");
|
if (res.error) return setNotice(res.error, "error");
|
||||||
await loadPersonas(); setNotice("Persona deleted.", "success");
|
await loadPersonas(); setNotice("Persona deleted.", "success");
|
||||||
@@ -384,7 +452,16 @@ async function createNewChat() {
|
|||||||
}
|
}
|
||||||
|
|
||||||
async function renameChat(id) {
|
async function renameChat(id) {
|
||||||
const chat = getChatById(id), nextTitle = chat ? window.prompt("Rename chat", chat.title) : "";
|
const chat = getChatById(id);
|
||||||
|
const nextTitle = chat ? await promptPopup({
|
||||||
|
eyebrow: "Conversation",
|
||||||
|
title: "Rename chat",
|
||||||
|
description: "Choose a new title for this thread.",
|
||||||
|
label: "Chat title",
|
||||||
|
value: chat.title,
|
||||||
|
placeholder: "New chat",
|
||||||
|
confirmLabel: "Save"
|
||||||
|
}) : "";
|
||||||
const title = (nextTitle || "").trim();
|
const title = (nextTitle || "").trim();
|
||||||
if (!chat || !title) return;
|
if (!chat || !title) return;
|
||||||
const res = await put(`/chats/${id}`, {title});
|
const res = await put(`/chats/${id}`, {title});
|
||||||
@@ -394,7 +471,15 @@ async function renameChat(id) {
|
|||||||
|
|
||||||
async function clearChat(id) {
|
async function clearChat(id) {
|
||||||
const chat = getChatById(id);
|
const chat = getChatById(id);
|
||||||
if (!chat || !window.confirm(`Clear all messages in "${chat.title}"?`)) return;
|
if (!chat) return;
|
||||||
|
const confirmed = await confirmPopup({
|
||||||
|
eyebrow: "Conversation",
|
||||||
|
title: "Clear messages",
|
||||||
|
description: `Clear all messages in "${chat.title}"? The chat will stay, but its history will be removed.`,
|
||||||
|
confirmLabel: "Clear",
|
||||||
|
danger: true
|
||||||
|
});
|
||||||
|
if (!confirmed) return;
|
||||||
const res = await post(`/chats/${id}/clear`, {});
|
const res = await post(`/chats/${id}/clear`, {});
|
||||||
if (res.error) return setNotice(res.error, "error");
|
if (res.error) return setNotice(res.error, "error");
|
||||||
currentMessages = []; renderMessages(); await loadSummary(); setNotice("Chat cleared.", "success");
|
currentMessages = []; renderMessages(); await loadSummary(); setNotice("Chat cleared.", "success");
|
||||||
@@ -412,7 +497,15 @@ function exportCurrentChat() {
|
|||||||
|
|
||||||
async function deleteChat(id) {
|
async function deleteChat(id) {
|
||||||
const chat = getChatById(id);
|
const chat = getChatById(id);
|
||||||
if (!chat || !window.confirm(`Delete "${chat.title}"? This cannot be undone.`)) return;
|
if (!chat) return;
|
||||||
|
const confirmed = await confirmPopup({
|
||||||
|
eyebrow: "Conversation",
|
||||||
|
title: "Delete chat",
|
||||||
|
description: `Delete "${chat.title}"? This permanently removes the conversation.`,
|
||||||
|
confirmLabel: "Delete",
|
||||||
|
danger: true
|
||||||
|
});
|
||||||
|
if (!confirmed) return;
|
||||||
const res = await del(`/chats/${id}`);
|
const res = await del(`/chats/${id}`);
|
||||||
if (res.error) return setNotice(res.error, "error");
|
if (res.error) return setNotice(res.error, "error");
|
||||||
chatSessions = chatSessions.filter((item) => item.id !== id);
|
chatSessions = chatSessions.filter((item) => item.id !== id);
|
||||||
@@ -530,10 +623,33 @@ newPersonaBtn.addEventListener("click", () => openPersonaForm()); newUserPersona
|
|||||||
clearPersonaBtn.addEventListener("click", () => { void clearPersona("assistant"); }); clearUserPersonaBtn.addEventListener("click", () => { void clearPersona("user"); });
|
clearPersonaBtn.addEventListener("click", () => { void clearPersona("assistant"); }); clearUserPersonaBtn.addEventListener("click", () => { void clearPersona("user"); });
|
||||||
personaForm.addEventListener("submit", (event) => { event.preventDefault(); void savePersona(); }); personaCloseBtn.addEventListener("click", closePersonaForm);
|
personaForm.addEventListener("submit", (event) => { event.preventDefault(); void savePersona(); }); personaCloseBtn.addEventListener("click", closePersonaForm);
|
||||||
personaModal.addEventListener("click", (event) => { if (event.target === personaModal) closePersonaForm(); });
|
personaModal.addEventListener("click", (event) => { if (event.target === personaModal) closePersonaForm(); });
|
||||||
|
popupConfirmBtn.addEventListener("click", () => {
|
||||||
|
if (popupMode === "prompt") {
|
||||||
|
closePopup(popupInput.value);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
closePopup(true);
|
||||||
|
});
|
||||||
|
popupCancelBtn.addEventListener("click", () => closePopup(false));
|
||||||
|
popupCloseBtn.addEventListener("click", () => closePopup(false));
|
||||||
|
popupModal.addEventListener("click", (event) => { if (event.target === popupModal) closePopup(false); });
|
||||||
|
popupInput.addEventListener("keydown", (event) => {
|
||||||
|
if (event.key === "Enter") {
|
||||||
|
event.preventDefault();
|
||||||
|
closePopup(popupInput.value);
|
||||||
|
}
|
||||||
|
});
|
||||||
modelMenuButton.addEventListener("click", toggleModelPopover); modelPopover.addEventListener("click", (event) => event.stopPropagation());
|
modelMenuButton.addEventListener("click", toggleModelPopover); modelPopover.addEventListener("click", (event) => event.stopPropagation());
|
||||||
personaMenuButton.addEventListener("click", togglePersonaPopover); personaPopover.addEventListener("click", (event) => event.stopPropagation());
|
personaMenuButton.addEventListener("click", togglePersonaPopover); personaPopover.addEventListener("click", (event) => event.stopPropagation());
|
||||||
document.addEventListener("click", () => { closeModelPopover(); closePersonaPopover(); });
|
document.addEventListener("click", () => { closeModelPopover(); closePersonaPopover(); });
|
||||||
document.addEventListener("keydown", (event) => { if (event.key === "Escape") { closeModelPopover(); closePersonaPopover(); closePersonaForm(); } });
|
document.addEventListener("keydown", (event) => {
|
||||||
|
if (event.key === "Escape") {
|
||||||
|
closeModelPopover();
|
||||||
|
closePersonaPopover();
|
||||||
|
closePersonaForm();
|
||||||
|
closePopup(false);
|
||||||
|
}
|
||||||
|
});
|
||||||
window.addEventListener("load", async () => {
|
window.addEventListener("load", async () => {
|
||||||
applyTheme(localStorage.getItem("krishd-theme") || "fakegpt", false);
|
applyTheme(localStorage.getItem("krishd-theme") || "fakegpt", false);
|
||||||
setNotice("Ready."); updateChatActionState(); await checkSession();
|
setNotice("Ready."); updateChatActionState(); await checkSession();
|
||||||
|
|||||||
@@ -1352,6 +1352,29 @@ select:focus {
|
|||||||
gap: 18px;
|
gap: 18px;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
.popup-card {
|
||||||
|
width: min(520px, 95vw);
|
||||||
|
display: grid;
|
||||||
|
gap: 18px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.popup-content {
|
||||||
|
display: grid;
|
||||||
|
gap: 14px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.popup-actions {
|
||||||
|
display: flex;
|
||||||
|
justify-content: flex-end;
|
||||||
|
gap: 10px;
|
||||||
|
flex-wrap: wrap;
|
||||||
|
}
|
||||||
|
|
||||||
|
.popup-actions .secondary-action,
|
||||||
|
.popup-actions button {
|
||||||
|
min-width: 132px;
|
||||||
|
}
|
||||||
|
|
||||||
.persona-modal-heading {
|
.persona-modal-heading {
|
||||||
display: grid;
|
display: grid;
|
||||||
gap: 4px;
|
gap: 4px;
|
||||||
|
|||||||
Reference in New Issue
Block a user