Michel-OpenWebUI / config /minimal_ui_patch.js
Nanny7's picture
Use custom guest registration modal
f98edcd
Raw History Blame Contribute Delete
13.4 kB
(() => {
const MONTAIGNE_SUGGESTIONS = [
{
title: ['What occupies your thoughts', 'most lately?'],
content: 'What occupies your thoughts most lately?'
},
{
title: ['Which of your habits', 'deserve examination?'],
content: 'Which of my habits deserve the closest examination, and how might I begin?'
},
{
title: ['What do solitude and', 'conversation each reveal?'],
content: "What do solitude and conversation each reveal about a person's character?"
},
{
title: ['How have you changed', 'without noticing?'],
content: 'How can someone recognize the ways they have changed without noticing?'
},
{
title: ['Where does ambition', 'help or mislead you?'],
content: 'Where does ambition improve a life, and where does it quietly mislead us?'
},
{
title: ['What truth about yourself', 'is hardest to admit?'],
content: 'What kinds of truths about ourselves are the hardest to admit, and why?'
}
];
const HIDE_TEXT = new Set([
'Settings',
'Archived Chats',
'Keyboard shortcuts',
'Update your status',
'Playground',
'Admin Panel',
'Documentation',
'Releases',
'Set as default'
]);
const HIDE_HREF = new Set(['/playground', '/admin']);
const HIDE_ARIA = new Set(['Controls', 'Add Model', 'Remove Model', 'Voice Input', 'Voice mode']);
const CHAT_COMPLETIONS_PATH = '/api/chat/completions';
const GUEST_TOKEN_KEY = 'montaigne_guest_upgrade_token';
const GUEST_CLAIM_URL_KEY = 'montaigne_guest_claim_url';
const MONTAIGNE_TOKEN_KEY = 'montaigne_access_token';
function normalize(text) {
return (text || '').replace(/\s+/g, ' ').trim();
}
function removeGuestBanner() {
const banner = document.getElementById('montaigne-guest-banner');
if (banner) {
banner.remove();
}
}
function closeGuestModal() {
const modal = document.getElementById('montaigne-guest-modal');
if (modal) {
modal.remove();
}
}
async function submitGuestClaim(form, statusEl) {
const guestUpgradeToken = localStorage.getItem(GUEST_TOKEN_KEY);
const claimUrl = localStorage.getItem(GUEST_CLAIM_URL_KEY);
const username = form.querySelector('[name="username"]').value.trim();
const password = form.querySelector('[name="password"]').value;
if (!guestUpgradeToken || !claimUrl) {
statusEl.textContent = 'This guest session can no longer be claimed. Please refresh and try again.';
return;
}
if (username.length < 3) {
statusEl.textContent = 'Choose a username with at least 3 characters.';
return;
}
if (password.length < 6) {
statusEl.textContent = 'Choose a password with at least 6 characters.';
return;
}
statusEl.textContent = 'Creating your account...';
try {
const response = await fetch(claimUrl, {
method: 'POST',
headers: {
'Content-Type': 'application/json'
},
body: JSON.stringify({
guest_upgrade_token: guestUpgradeToken,
username,
password
})
});
const payload = await response.json();
if (!response.ok) {
throw new Error(payload.detail || 'Could not create your account.');
}
localStorage.setItem(MONTAIGNE_TOKEN_KEY, payload.access_token);
localStorage.removeItem(GUEST_TOKEN_KEY);
localStorage.removeItem(GUEST_CLAIM_URL_KEY);
closeGuestModal();
removeGuestBanner();
const toast = document.createElement('div');
toast.style.cssText =
'position:fixed;bottom:18px;left:50%;transform:translateX(-50%);z-index:100000;' +
'background:#16311f;color:#e6f6ea;padding:10px 14px;border-radius:10px;' +
'font:13px Inter,system-ui,sans-serif;box-shadow:0 10px 30px rgba(0,0,0,.25);';
toast.textContent = 'Account created. Send your next message to continue.';
document.body.appendChild(toast);
setTimeout(() => toast.remove(), 4000);
} catch (error) {
statusEl.textContent = error instanceof Error ? error.message : 'Could not create your account.';
}
}
function openGuestModal() {
closeGuestModal();
const overlay = document.createElement('div');
overlay.id = 'montaigne-guest-modal';
overlay.style.cssText =
'position:fixed;inset:0;z-index:100000;display:flex;align-items:center;justify-content:center;' +
'background:rgba(22,16,12,.62);padding:20px;';
const card = document.createElement('div');
card.style.cssText =
'width:min(420px,100%);border-radius:18px;padding:22px;background:#f5ede0;color:#2b2118;' +
'box-shadow:0 20px 60px rgba(0,0,0,.35);font:14px Inter,system-ui,sans-serif;';
const title = document.createElement('h2');
title.textContent = 'Create your account';
title.style.cssText = 'margin:0 0 8px;font:600 22px Georgia,serif;';
const intro = document.createElement('p');
intro.textContent = 'Keep this conversation and continue chatting with Montaigne.';
intro.style.cssText = 'margin:0 0 16px;line-height:1.45;color:#5b4a3c;';
const form = document.createElement('form');
form.style.cssText = 'display:grid;gap:12px;';
const username = document.createElement('input');
username.name = 'username';
username.placeholder = 'Username';
username.autocomplete = 'username';
username.style.cssText =
'width:100%;padding:12px 14px;border:1px solid #cfb89b;border-radius:10px;background:#fffdf9;';
const password = document.createElement('input');
password.name = 'password';
password.type = 'password';
password.placeholder = 'Password';
password.autocomplete = 'new-password';
password.style.cssText = username.style.cssText;
const status = document.createElement('div');
status.style.cssText = 'min-height:18px;font-size:13px;color:#8a2f2f;';
const actions = document.createElement('div');
actions.style.cssText = 'display:flex;justify-content:flex-end;gap:10px;margin-top:4px;';
const cancel = document.createElement('button');
cancel.type = 'button';
cancel.textContent = 'Cancel';
cancel.style.cssText =
'padding:10px 14px;border:1px solid #cfb89b;border-radius:10px;background:#fff8ef;cursor:pointer;';
cancel.onclick = () => closeGuestModal();
const submit = document.createElement('button');
submit.type = 'submit';
submit.textContent = 'Create account';
submit.style.cssText =
'padding:10px 14px;border:none;border-radius:10px;background:#8b6243;color:#fff;cursor:pointer;font-weight:600;';
form.addEventListener('submit', async (event) => {
event.preventDefault();
await submitGuestClaim(form, status);
});
actions.appendChild(cancel);
actions.appendChild(submit);
form.appendChild(username);
form.appendChild(password);
form.appendChild(status);
form.appendChild(actions);
card.appendChild(title);
card.appendChild(intro);
card.appendChild(form);
overlay.appendChild(card);
overlay.addEventListener('click', (event) => {
if (event.target === overlay) {
closeGuestModal();
}
});
document.body.appendChild(overlay);
username.focus();
}
function showGuestBanner() {
if (document.getElementById('montaigne-guest-banner')) {
return;
}
const banner = document.createElement('div');
banner.id = 'montaigne-guest-banner';
banner.style.cssText =
'position:fixed;bottom:0;left:0;right:0;z-index:99999;background:linear-gradient(135deg,#3d2b1f,#5c3d2e);' +
'color:#f5e6d3;padding:14px 20px;display:flex;align-items:center;justify-content:center;gap:16px;' +
'font:14px Inter,system-ui,sans-serif;box-shadow:0 -2px 12px rgba(0,0,0,.3);';
const message = document.createElement('span');
message.textContent = 'You have used your 5 free messages. Create a free account to continue chatting with Montaigne.';
const button = document.createElement('button');
button.type = 'button';
button.textContent = 'Create free account';
button.style.cssText =
'background:#c9a96e;color:#2b1d0e;padding:8px 18px;border:none;border-radius:6px;' +
'font-weight:600;white-space:nowrap;cursor:pointer;';
button.onclick = () => openGuestModal();
const close = document.createElement('button');
close.type = 'button';
close.textContent = 'x';
close.style.cssText =
'background:none;border:none;color:#f5e6d3;font-size:20px;cursor:pointer;padding:0 4px;line-height:1;';
close.onclick = () => removeGuestBanner();
banner.appendChild(message);
banner.appendChild(button);
banner.appendChild(close);
document.body.appendChild(banner);
}
function installFetchPatch() {
if (window.__montaigneFetchPatched) {
return;
}
window.__montaigneFetchPatched = true;
const originalFetch = window.fetch.bind(window);
window.fetch = async (...args) => {
try {
const input = args[0];
const rawUrl = input instanceof Request ? input.url : String(input);
const url = new URL(rawUrl, window.location.origin);
if (url.pathname === CHAT_COMPLETIONS_PATH) {
const body = args[1] && args[1].body;
if (typeof body === 'string') {
try {
const parsed = JSON.parse(body);
parsed.variables = parsed.variables || {};
const montaigneToken = localStorage.getItem(MONTAIGNE_TOKEN_KEY);
if (montaigneToken) {
parsed.variables.montaigne_token = montaigneToken;
}
args[1] = { ...args[1], body: JSON.stringify(parsed) };
} catch (_) {
// Ignore non-JSON payloads.
}
}
}
} catch (_) {
// Ignore URL parsing errors.
}
const response = await originalFetch(...args);
try {
const input = args[0];
const rawUrl = input instanceof Request ? input.url : String(input);
const url = new URL(rawUrl, window.location.origin);
if (url.pathname === '/api/config') {
const payload = await response.clone().json();
payload.default_prompt_suggestions = MONTAIGNE_SUGGESTIONS;
const headers = new Headers(response.headers);
headers.set('content-type', 'application/json');
headers.delete('content-length');
return new Response(JSON.stringify(payload), {
status: response.status,
statusText: response.statusText,
headers
});
}
if (url.pathname === CHAT_COMPLETIONS_PATH && response.status === 403) {
const payload = await response.clone().json().catch(() => null);
if (payload && payload.code === 'guest_limit_reached' && payload.guest_upgrade_token && payload.claim_guest_url) {
localStorage.setItem(GUEST_TOKEN_KEY, payload.guest_upgrade_token);
localStorage.setItem(GUEST_CLAIM_URL_KEY, payload.claim_guest_url);
showGuestBanner();
}
}
} catch (_) {
// Keep the UI resilient.
}
return response;
};
}
function hide(el) {
if (!el || el.dataset.montaigneHidden === '1') {
return;
}
el.dataset.montaigneHidden = '1';
el.style.display = 'none';
}
function hideMenuItem(el) {
hide(el.closest('button, a, [role="menuitem"]') || el);
}
function hideModelSelector(trigger) {
hide(trigger);
let node = trigger;
for (let i = 0; i < 6 && node; i += 1) {
const className = typeof node.className === 'string' ? node.className : '';
if (className.includes('overflow-hidden')) {
hide(node.parentElement || node);
return;
}
node = node.parentElement;
}
hide(trigger);
}
function applyMinimalUi() {
document
.querySelectorAll('[id^="model-selector-"][id$="-button"]')
.forEach(hideModelSelector);
document.querySelectorAll('#voice-input-button, [aria-label="Voice mode"]').forEach(hide);
document.querySelectorAll('[aria-label^="Get information on "]').forEach(hide);
document.querySelectorAll('[role="dialog"], dialog').forEach((el) => {
const text = normalize(el.textContent);
if (text.includes("What's New") || text.includes('Release Notes')) {
const closeButton = el.querySelector('button[aria-label="Close"]');
if (closeButton) {
closeButton.click();
}
hide(el);
}
});
document.querySelectorAll('button, a, [role="menuitem"]').forEach((el) => {
const text = normalize(el.textContent);
const aria = normalize(el.getAttribute('aria-label'));
const href = el.getAttribute('href');
if (HIDE_TEXT.has(text) || HIDE_ARIA.has(aria) || HIDE_HREF.has(href)) {
hideMenuItem(el);
}
});
document.querySelectorAll('#temporary-chat-button').forEach(hide);
}
installFetchPatch();
const run = () => window.requestAnimationFrame(applyMinimalUi);
if (document.readyState === 'loading') {
document.addEventListener('DOMContentLoaded', run, { once: true });
} else {
run();
}
new MutationObserver(run).observe(document.documentElement, {
childList: true,
subtree: true
});
})();