151 lines
8.4 KiB
JavaScript
151 lines
8.4 KiB
JavaScript
// Settings reuses the account, appearance, voice and memory owners.
|
|
// Moving nodes preserves state and handlers; this module never writes profiles or roles.
|
|
(() => {
|
|
'use strict';
|
|
const $ = id => document.getElementById(id);
|
|
const sections = new Set(['account', 'appearance', 'preferences', 'privacy', 'security', 'admin']);
|
|
let initialized = false, dialog, current = 'account', opener = null, generation = 0;
|
|
let memory = null, statusHome = null;
|
|
const panel = section => dialog?.querySelector(`[data-settings-panel="${section}"]`);
|
|
const text = (id, value) => { if ($(id)) $(id).textContent = value; };
|
|
|
|
function restoreStatus() {
|
|
if (statusHome && $('featureStatus')) {
|
|
const {parent, next} = statusHome;
|
|
parent.insertBefore($('featureStatus'), next?.parentNode === parent ? next : null);
|
|
statusHome = null;
|
|
}
|
|
}
|
|
function updateMemoryStatus() {
|
|
text('settingsMemoryStatus', !memory || memory.hidden
|
|
? 'Memory controls are unavailable for this account. No memory has been changed.'
|
|
: 'Review or edit your saved memory below. Clearing memory does not delete conversations.');
|
|
}
|
|
function identity(user, changed) {
|
|
text('settingsAccountName', user?.display_name || user?.username || '');
|
|
text('settingsAccountUsername', user ? `@${user.username}` : '');
|
|
text('settingsAccountRole', user ? (user.role === 'admin' ? 'Administrator' : 'User') : '');
|
|
dialog.querySelectorAll('[data-settings-target="admin"], [data-settings-open="admin"]').forEach(button => { button.hidden = user?.role !== 'admin'; });
|
|
if (!user || changed) close(false);
|
|
else if (current === 'admin' && user.role !== 'admin') select('account');
|
|
updateMemoryStatus();
|
|
}
|
|
async function loadAppearance(version) {
|
|
const message = $('preferencesMessage');
|
|
if (message) message.textContent = 'Loading your saved appearance…';
|
|
try {
|
|
if (window.HopeAppearance?.load) await window.HopeAppearance.load();
|
|
else throw new Error('Appearance settings are unavailable. Reload Hope to try again.');
|
|
} catch (error) {
|
|
if (version === generation && window.HopeAuth?.user && message) message.textContent = error.message;
|
|
}
|
|
}
|
|
function select(section) {
|
|
if (!sections.has(section)) section = 'account';
|
|
if (section === 'admin' && window.HopeAuth?.user?.role !== 'admin') section = 'account';
|
|
current = section; const version = ++generation;
|
|
restoreStatus();
|
|
dialog.querySelectorAll('[data-settings-panel]').forEach(item => { item.hidden = item.dataset.settingsPanel !== section; });
|
|
dialog.querySelectorAll('[data-settings-target]').forEach(button => {
|
|
const active = button.dataset.settingsTarget === section;
|
|
button.classList.toggle('selected', active);
|
|
if (active) button.setAttribute('aria-current', 'page'); else button.removeAttribute('aria-current');
|
|
});
|
|
// auth.js keeps its polling and provisioning handlers; never copy them.
|
|
if ($('adminSection')) $('adminSection').hidden = section !== 'admin';
|
|
$('adminBtn')?.setAttribute('aria-expanded', String(section === 'admin'));
|
|
if (section === 'admin') $('refreshUsers')?.click();
|
|
if (section === 'appearance') loadAppearance(version);
|
|
if (section === 'preferences') {
|
|
if (window.HopeVoiceSettings?.load) window.HopeVoiceSettings.load();
|
|
else {
|
|
if (typeof renderVoiceList === 'function') renderVoiceList();
|
|
if (typeof loadVoiceCatalog === 'function') loadVoiceCatalog();
|
|
if (typeof loadModelSettings === 'function') loadModelSettings();
|
|
}
|
|
}
|
|
if (section === 'privacy') {
|
|
if (!memory) memory = window.HopeFeatures?.mountMemory?.($('privacyMemory') || panel('privacy')) || null;
|
|
const status = $('featureStatus');
|
|
if (status && status.parentNode !== panel('privacy')) {
|
|
statusHome = {parent: status.parentNode, next: status.nextSibling};
|
|
panel('privacy').append(status);
|
|
}
|
|
updateMemoryStatus();
|
|
}
|
|
}
|
|
function open(section = 'account') {
|
|
initialize();
|
|
if (!dialog || !window.HopeAuth?.user) return;
|
|
if (!dialog.open) opener = document.activeElement;
|
|
if ($('chatSidebar')?.classList.contains('drawer-open')) $('drawerClose')?.click();
|
|
select(section);
|
|
if (!dialog.open) dialog.showModal();
|
|
$('settingsClose')?.focus();
|
|
}
|
|
function close(restoreFocus = true) {
|
|
generation++; restoreStatus();
|
|
if (!dialog?.open) return;
|
|
dialog.close();
|
|
if ($('adminSection')) $('adminSection').hidden = true;
|
|
$('adminBtn')?.setAttribute('aria-expanded', 'false');
|
|
if (restoreFocus) {
|
|
const target = [opener, $('settingsBtn'), $('drawerOpen')].find(item => item?.isConnected && !item.closest('[hidden], [inert]') && item.getClientRects().length);
|
|
target?.focus();
|
|
}
|
|
}
|
|
function initialize() {
|
|
if (initialized) return;
|
|
dialog = $('settingsDialog');
|
|
if (!dialog || !window.HopeAuth) return;
|
|
initialized = true;
|
|
const sheet = $('settingsSheet');
|
|
if (sheet && panel('preferences')) {
|
|
panel('preferences').append(sheet);
|
|
sheet.hidden = false; sheet.inert = false;
|
|
sheet.classList.remove('open');
|
|
sheet.removeAttribute('role'); sheet.removeAttribute('aria-modal');
|
|
}
|
|
if ($('preferencesForm') && panel('appearance')) panel('appearance').append($('preferencesForm'));
|
|
if ($('adminSection') && panel('admin')) panel('admin').append($('adminSection'));
|
|
for (const id of ['closeSheetBtn', 'closePreferences']) {
|
|
if ($(id)) $(id).addEventListener('click', () => close(), true);
|
|
}
|
|
$('settingsClose')?.addEventListener('click', () => close());
|
|
dialog.addEventListener('cancel', event => { event.preventDefault(); close(); });
|
|
dialog.addEventListener('keydown', event => {
|
|
if (event.key !== 'Tab' || $('accountDialog')?.open) return;
|
|
const controls = [...dialog.querySelectorAll('button, input, select, textarea, a[href], [tabindex]')].filter(item => !item.disabled && item.tabIndex >= 0 && !item.closest('[hidden], [inert]') && item.getClientRects().length);
|
|
const first = controls[0], last = controls[controls.length - 1];
|
|
if (event.shiftKey && document.activeElement === first) { event.preventDefault(); last?.focus(); }
|
|
else if (!event.shiftKey && document.activeElement === last) { event.preventDefault(); first?.focus(); }
|
|
});
|
|
dialog.addEventListener('close', () => {
|
|
// Native close events are queued; a later open must not be dismantled.
|
|
if (dialog.open) return;
|
|
restoreStatus();
|
|
if ($('adminSection')) $('adminSection').hidden = true;
|
|
$('adminBtn')?.setAttribute('aria-expanded', 'false');
|
|
});
|
|
dialog.addEventListener('click', event => {
|
|
const button = event.target.closest('[data-settings-target]');
|
|
if (!button || !dialog.contains(button)) return;
|
|
event.preventDefault(); event.stopPropagation(); select(button.dataset.settingsTarget);
|
|
}, true);
|
|
document.addEventListener('click', event => {
|
|
const button = event.target.closest('[data-settings-open], #adminBtn, #openPreferences');
|
|
if (!button || dialog.contains(button)) return;
|
|
event.preventDefault(); event.stopImmediatePropagation();
|
|
open(button.dataset.settingsOpen || (button.id === 'adminBtn' ? 'admin' : 'appearance'));
|
|
}, true);
|
|
dialog.querySelectorAll('[data-settings-signout]').forEach(button => {
|
|
button.addEventListener('click', () => { close(false); $('logoutBtn')?.click(); });
|
|
});
|
|
memory = window.HopeFeatures?.mountMemory?.($('privacyMemory') || panel('privacy')) || null;
|
|
if (memory) new MutationObserver(updateMemoryStatus).observe(memory, {attributes: true, attributeFilter: ['hidden']});
|
|
window.HopeAuth.onChange(identity);
|
|
}
|
|
window.HopeSettings = {open, close, initialize, get section() {return current;}, get isOpen() {return !!dialog?.open;}};
|
|
initialize();
|
|
})();
|