Changeset 0.7.3 (#41)
This commit is contained in:
262
src/js/app.js
262
src/js/app.js
@@ -24,6 +24,7 @@ const App = {
|
||||
|
||||
async function init() {
|
||||
console.log('🔀 Chat Switchboard initializing...');
|
||||
initBranding(); // Apply branding before splash is visible
|
||||
API.loadTokens();
|
||||
|
||||
let health = null;
|
||||
@@ -102,6 +103,59 @@ async function saveSettings() {
|
||||
} catch (e) { console.warn('Settings sync failed:', e.message); }
|
||||
}
|
||||
|
||||
// ── Avatar Preview ──────────────────────────
|
||||
function updateAvatarPreview(dataURI) {
|
||||
const preview = document.getElementById('avatarPreview');
|
||||
const letter = document.getElementById('avatarPreviewLetter');
|
||||
const removeBtn = document.getElementById('avatarRemoveBtn');
|
||||
const existingImg = preview.querySelector('img');
|
||||
|
||||
if (dataURI) {
|
||||
letter.style.display = 'none';
|
||||
if (existingImg) {
|
||||
existingImg.src = dataURI;
|
||||
} else {
|
||||
const img = document.createElement('img');
|
||||
img.src = dataURI;
|
||||
img.alt = 'Avatar';
|
||||
preview.insertBefore(img, letter);
|
||||
}
|
||||
removeBtn.style.display = '';
|
||||
} else {
|
||||
if (existingImg) existingImg.remove();
|
||||
const name = API.user?.display_name || API.user?.username || '?';
|
||||
letter.textContent = name[0].toUpperCase();
|
||||
letter.style.display = '';
|
||||
removeBtn.style.display = 'none';
|
||||
}
|
||||
}
|
||||
|
||||
// Preset avatar preview (admin form)
|
||||
function updatePresetAvatarPreview(dataURI) {
|
||||
const preview = document.getElementById('presetAvatarPreview');
|
||||
const placeholder = document.getElementById('presetAvatarPlaceholder');
|
||||
const removeBtn = document.getElementById('presetAvatarRemoveBtn');
|
||||
if (!preview) return;
|
||||
const existingImg = preview.querySelector('img');
|
||||
|
||||
if (dataURI) {
|
||||
placeholder.style.display = 'none';
|
||||
if (existingImg) {
|
||||
existingImg.src = dataURI;
|
||||
} else {
|
||||
const img = document.createElement('img');
|
||||
img.src = dataURI;
|
||||
img.alt = 'Preset avatar';
|
||||
preview.insertBefore(img, placeholder);
|
||||
}
|
||||
removeBtn.style.display = '';
|
||||
} else {
|
||||
if (existingImg) existingImg.remove();
|
||||
placeholder.style.display = '';
|
||||
removeBtn.style.display = 'none';
|
||||
}
|
||||
}
|
||||
|
||||
// ── Models ───────────────────────────────────
|
||||
|
||||
// Client-side known model capabilities — used when backend hasn't synced yet.
|
||||
@@ -188,6 +242,7 @@ async function fetchModels() {
|
||||
isPreset,
|
||||
presetId: m.preset_id || null,
|
||||
presetScope: m.preset_scope || null,
|
||||
presetAvatar: m.preset_avatar || null,
|
||||
};
|
||||
});
|
||||
|
||||
@@ -843,6 +898,41 @@ function initListeners() {
|
||||
document.getElementById('profileChangePwForm').style.display = '';
|
||||
});
|
||||
document.getElementById('profileSavePwBtn').addEventListener('click', handleChangePassword);
|
||||
|
||||
// Avatar upload
|
||||
document.getElementById('avatarUploadBtn').addEventListener('click', () => {
|
||||
document.getElementById('avatarFileInput').click();
|
||||
});
|
||||
document.getElementById('avatarFileInput').addEventListener('change', async function() {
|
||||
const file = this.files[0];
|
||||
if (!file) return;
|
||||
const reader = new FileReader();
|
||||
reader.onload = async () => {
|
||||
try {
|
||||
const data = await API.uploadAvatar(reader.result);
|
||||
if (data.avatar) {
|
||||
API.user.avatar = data.avatar;
|
||||
API.saveTokens();
|
||||
updateAvatarPreview(data.avatar);
|
||||
UI.updateUser();
|
||||
UI.toast('Avatar updated');
|
||||
}
|
||||
} catch (e) { UI.toast(e.message || 'Upload failed', 'error'); }
|
||||
};
|
||||
reader.readAsDataURL(file);
|
||||
this.value = '';
|
||||
});
|
||||
document.getElementById('avatarRemoveBtn').addEventListener('click', async () => {
|
||||
try {
|
||||
await API.deleteAvatar();
|
||||
API.user.avatar = null;
|
||||
API.saveTokens();
|
||||
updateAvatarPreview(null);
|
||||
UI.updateUser();
|
||||
UI.toast('Avatar removed');
|
||||
} catch (e) { UI.toast(e.message || 'Remove failed', 'error'); }
|
||||
});
|
||||
|
||||
document.getElementById('providerShowAddBtn').addEventListener('click', UI.showProviderForm);
|
||||
document.getElementById('providerCancelBtn').addEventListener('click', UI.hideProviderForm);
|
||||
document.getElementById('providerCreateBtn').addEventListener('click', handleCreateProvider);
|
||||
@@ -907,6 +997,7 @@ function initListeners() {
|
||||
document.getElementById('adminPresetIcon').value = '';
|
||||
document.getElementById('adminPresetTemp').value = '';
|
||||
document.getElementById('adminPresetMaxTokens').value = '';
|
||||
updatePresetAvatarPreview(null);
|
||||
const f = document.getElementById('adminAddPresetForm');
|
||||
f.style.display = f.style.display === 'none' ? '' : 'none';
|
||||
});
|
||||
@@ -914,9 +1005,55 @@ function initListeners() {
|
||||
document.getElementById('adminAddPresetForm').style.display = 'none';
|
||||
_editingPresetId = null;
|
||||
document.getElementById('adminCreatePresetBtn').textContent = 'Create';
|
||||
updatePresetAvatarPreview(null);
|
||||
});
|
||||
document.getElementById('adminCreatePresetBtn')?.addEventListener('click', createAdminPreset);
|
||||
|
||||
// Admin — preset avatar
|
||||
document.getElementById('presetAvatarUploadBtn')?.addEventListener('click', () => {
|
||||
document.getElementById('presetAvatarFileInput').click();
|
||||
});
|
||||
document.getElementById('presetAvatarFileInput')?.addEventListener('change', async function() {
|
||||
const file = this.files[0];
|
||||
if (!file) return;
|
||||
if (!_editingPresetId) {
|
||||
// For new presets: show preview, upload after create
|
||||
const reader = new FileReader();
|
||||
reader.onload = () => updatePresetAvatarPreview(reader.result);
|
||||
reader.readAsDataURL(file);
|
||||
this.value = '';
|
||||
return;
|
||||
}
|
||||
// For existing presets: upload immediately
|
||||
const reader = new FileReader();
|
||||
reader.onload = async () => {
|
||||
try {
|
||||
const data = await API.adminUploadPresetAvatar(_editingPresetId, reader.result);
|
||||
if (data.avatar) {
|
||||
updatePresetAvatarPreview(data.avatar);
|
||||
await UI.loadAdminPresets(true);
|
||||
await fetchModels();
|
||||
UI.toast('Preset avatar updated');
|
||||
}
|
||||
} catch (e) { UI.toast(e.message || 'Upload failed', 'error'); }
|
||||
};
|
||||
reader.readAsDataURL(file);
|
||||
this.value = '';
|
||||
});
|
||||
document.getElementById('presetAvatarRemoveBtn')?.addEventListener('click', async () => {
|
||||
if (!_editingPresetId) {
|
||||
updatePresetAvatarPreview(null);
|
||||
return;
|
||||
}
|
||||
try {
|
||||
await API.adminDeletePresetAvatar(_editingPresetId);
|
||||
updatePresetAvatarPreview(null);
|
||||
await UI.loadAdminPresets(true);
|
||||
await fetchModels();
|
||||
UI.toast('Preset avatar removed');
|
||||
} catch (e) { UI.toast(e.message || 'Remove failed', 'error'); }
|
||||
});
|
||||
|
||||
// Admin — banner controls
|
||||
document.getElementById('adminBannerEnabled')?.addEventListener('change', (e) => {
|
||||
document.getElementById('bannerConfigFields').style.display = e.target.checked ? '' : 'none';
|
||||
@@ -1195,6 +1332,8 @@ function editAdminPreset(id) {
|
||||
if (modelSel) modelSel.value = p.base_model_id || '';
|
||||
const cfgSel = document.getElementById('adminPresetConfig');
|
||||
if (cfgSel) cfgSel.value = p.api_config_id || '';
|
||||
// Avatar preview
|
||||
updatePresetAvatarPreview(p.avatar || null);
|
||||
document.getElementById('adminCreatePresetBtn').textContent = 'Update';
|
||||
}
|
||||
|
||||
@@ -1221,13 +1360,24 @@ async function createAdminPreset() {
|
||||
if (!isNaN(maxTok) && maxTok > 0) preset.max_tokens = maxTok;
|
||||
|
||||
try {
|
||||
let presetId = _editingPresetId;
|
||||
if (_editingPresetId) {
|
||||
await API.adminUpdatePreset(_editingPresetId, preset);
|
||||
UI.toast('Preset updated', 'success');
|
||||
} else {
|
||||
await API.adminCreatePreset(preset);
|
||||
const result = await API.adminCreatePreset(preset);
|
||||
presetId = result?.id || result?.preset?.id;
|
||||
UI.toast('Preset created', 'success');
|
||||
}
|
||||
|
||||
// Upload pending avatar for new presets
|
||||
const pendingImg = document.querySelector('#presetAvatarPreview img');
|
||||
if (!_editingPresetId && presetId && pendingImg?.src?.startsWith('data:')) {
|
||||
try {
|
||||
await API.adminUploadPresetAvatar(presetId, pendingImg.src);
|
||||
} catch (e) { console.warn('Preset avatar upload failed:', e.message); }
|
||||
}
|
||||
|
||||
_editingPresetId = null;
|
||||
document.getElementById('adminAddPresetForm').style.display = 'none';
|
||||
document.getElementById('adminCreatePresetBtn').textContent = 'Create';
|
||||
@@ -1237,6 +1387,7 @@ async function createAdminPreset() {
|
||||
document.getElementById('adminPresetIcon').value = '';
|
||||
document.getElementById('adminPresetTemp').value = '';
|
||||
document.getElementById('adminPresetMaxTokens').value = '';
|
||||
updatePresetAvatarPreview(null);
|
||||
await UI.loadAdminPresets();
|
||||
await fetchModels();
|
||||
} catch (e) { UI.toast(e.message, 'error'); }
|
||||
@@ -1563,6 +1714,115 @@ async function deleteNote() {
|
||||
} catch (e) { UI.toast(e.message, 'error'); }
|
||||
}
|
||||
|
||||
// ── Branding ────────────────────────────────
|
||||
|
||||
function initBranding() {
|
||||
const b = window.__BRANDING__;
|
||||
if (!b || typeof b !== 'object') return;
|
||||
|
||||
const brandBase = (window.__BASE__ || '') + '/branding/';
|
||||
|
||||
// Accent color → CSS custom property
|
||||
if (b.accent_color) {
|
||||
document.documentElement.style.setProperty('--accent-color', b.accent_color);
|
||||
}
|
||||
|
||||
// Page title
|
||||
if (b.org_name) {
|
||||
document.title = b.org_name;
|
||||
}
|
||||
|
||||
// Sidebar brand text
|
||||
const sidebarText = document.getElementById('brandSidebarText');
|
||||
if (sidebarText && b.org_name) sidebarText.textContent = b.org_name;
|
||||
|
||||
// Splash hero wordmark
|
||||
const wordmark = document.getElementById('brandWordmark');
|
||||
if (wordmark && b.org_name) wordmark.textContent = b.org_name;
|
||||
|
||||
// Splash headline — replace if branding provides it
|
||||
if (b.headline) {
|
||||
const headline = document.getElementById('brandHeadline');
|
||||
if (headline) headline.textContent = b.headline;
|
||||
}
|
||||
|
||||
// Splash tagline
|
||||
const tagline = document.getElementById('brandTagline');
|
||||
if (tagline && b.tagline) tagline.textContent = b.tagline;
|
||||
|
||||
// Auth card text
|
||||
if (b.org_name) {
|
||||
const authHeader = document.querySelector('.auth-card-header h2');
|
||||
if (authHeader) authHeader.textContent = 'Welcome to ' + b.org_name;
|
||||
const authSub = document.querySelector('.auth-card-header p');
|
||||
if (authSub) authSub.textContent = 'Sign in to continue';
|
||||
}
|
||||
const authFooter = document.getElementById('brandAuthFooter');
|
||||
if (authFooter && b.tagline) authFooter.textContent = b.tagline;
|
||||
|
||||
// Logo — replace SVG with <img> in splash hero
|
||||
if (b.logo) {
|
||||
const logoUrl = brandBase + b.logo;
|
||||
|
||||
const logoEl = document.getElementById('brandLogo');
|
||||
if (logoEl) {
|
||||
const img = document.createElement('img');
|
||||
img.src = logoUrl;
|
||||
img.alt = b.org_name || 'Logo';
|
||||
img.className = 'hero-logo-img';
|
||||
img.onerror = function() {
|
||||
this.style.display = 'none';
|
||||
const svg = logoEl.querySelector('svg');
|
||||
if (svg) svg.style.display = '';
|
||||
};
|
||||
const svg = logoEl.querySelector('svg');
|
||||
if (svg) svg.style.display = 'none';
|
||||
logoEl.appendChild(img);
|
||||
}
|
||||
|
||||
// Sidebar logo — replace emoji with small img
|
||||
const sidebarLogo = document.getElementById('brandSidebarLogo');
|
||||
if (sidebarLogo) {
|
||||
const originalEmoji = sidebarLogo.textContent;
|
||||
const img = document.createElement('img');
|
||||
img.src = logoUrl;
|
||||
img.alt = '';
|
||||
img.className = 'brand-logo-img';
|
||||
img.onerror = function() {
|
||||
this.remove();
|
||||
sidebarLogo.textContent = originalEmoji;
|
||||
};
|
||||
sidebarLogo.textContent = '';
|
||||
sidebarLogo.appendChild(img);
|
||||
}
|
||||
}
|
||||
|
||||
// Favicon
|
||||
if (b.favicon) {
|
||||
const favUrl = brandBase + b.favicon;
|
||||
document.querySelectorAll('link[rel="icon"], link[rel="apple-touch-icon"]').forEach(link => {
|
||||
link.href = favUrl;
|
||||
});
|
||||
}
|
||||
|
||||
// Feature pills — custom array replaces defaults, empty array hides them
|
||||
if (b.pills !== undefined) {
|
||||
const pillsEl = document.getElementById('brandPills');
|
||||
if (pillsEl) {
|
||||
if (!Array.isArray(b.pills) || b.pills.length === 0) {
|
||||
pillsEl.style.display = 'none';
|
||||
} else {
|
||||
pillsEl.innerHTML = b.pills.map(p => {
|
||||
const style = p.style === 'accent' ? ' accent' : p.style === 'purple' ? ' purple' : '';
|
||||
return `<div class="hero-pill${style}"><span class="pill-icon">${p.icon || ''}</span> ${p.text}</div>`;
|
||||
}).join('');
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
console.log('🎨 Branding applied:', b.org_name || '(defaults)');
|
||||
}
|
||||
|
||||
// ── Banners ──────────────────────────────────
|
||||
|
||||
async function initBanners() {
|
||||
|
||||
Reference in New Issue
Block a user