Changeset 0.37.5 (#217)

Co-authored-by: gobha <jasafpro@gmail.com>
Co-committed-by: gobha <jasafpro@gmail.com>
This commit is contained in:
2026-03-21 13:41:25 +00:00
committed by xcaliber
parent 05b5affdac
commit 74f3cb84e9
27 changed files with 2211 additions and 667 deletions

View File

@@ -0,0 +1,223 @@
/**
* SettingsSurface — root settings surface
*
* Reads globals:
* __SECTION__ — active section name (string)
* __BASE__ — base path
* __PAGE_DATA__ — { BYOKEnabled, ... } from Go template
*
* Layout: topbar + left nav + content area (same CSS classes as before).
* Preact sections are loaded lazily. Bridge sections delegate to old JS.
*/
const { html } = window;
const { useState, useEffect, useCallback, useMemo } = hooks;
const { render } = preact;
import { BridgeSection } from './bridge-section.js';
import { ToastContainer } from '../../primitives/toast.js';
import { DialogStack } from '../../shell/dialog-stack.js';
// ── Lazy section imports ────────────────────
// Each returns a Promise<{ default: Component }> or { SectionName: Component }
const sectionModules = {
general: () => import('./general.js'),
appearance: () => import('./appearance.js'),
profile: () => import('./profile.js'),
teams: () => import('./teams.js'),
models: () => import('./models.js'),
providers: () => import('./providers.js'),
personas: () => import('./personas.js'),
roles: () => import('./roles.js'),
usage: () => import('./usage.js'),
};
// ── Bridge section config (deferred to old JS) ──
const bridgeSections = {
workflows: { id: 'settingsDynamic', loader: () => typeof loadTeamWorkflows === 'function' && loadTeamWorkflows() },
tasks: { id: 'settingsTasksMount', loader: () => typeof _loadSettingsTasks === 'function' && _loadSettingsTasks() },
gitkeys: { id: 'settingsDynamic', loader: () => typeof _loadSettingsGitKeys === 'function' && _loadSettingsGitKeys() },
data: { id: 'dpMount', loader: () => typeof loadDataPrivacy === 'function' && loadDataPrivacy() },
knowledge: { id: 'settingsDynamic', loader: () => typeof KnowledgeUI !== 'undefined' && KnowledgeUI.openManagePanel?.() },
memory: { id: 'settingsDynamic', loader: () => typeof MemoryUI !== 'undefined' && MemoryUI.openSettingsPanel?.() },
notifications: { id: 'settingsDynamic', loader: () => typeof NotifPrefs !== 'undefined' && NotifPrefs.load?.() },
};
// ── Nav structure ───────────────────────────
const NAV_ITEMS = [
{ key: 'general', label: 'General' },
{ key: 'appearance', label: 'Appearance' },
{ key: 'models', label: 'Models' },
{ key: 'personas', label: 'Personas', gate: 'personas' },
{ key: 'profile', label: 'Profile' },
{ key: 'teams', label: 'Teams' },
{ key: 'workflows', label: 'Workflows' },
{ key: 'tasks', label: 'Tasks' },
{ key: 'gitkeys', label: 'Git Keys' },
{ key: 'data', label: 'Data & Privacy' },
];
const BYOK_ITEMS = [
{ key: 'providers', label: 'My Providers' },
{ key: 'roles', label: 'Model Roles' },
{ key: 'usage', label: 'My Usage' },
];
// ── Section title map ───────────────────────
const SECTION_TITLES = {
general: 'General', appearance: 'Appearance', models: 'Models',
personas: 'Personas', profile: 'Profile', teams: 'Teams',
workflows: 'Workflows', tasks: 'Tasks', gitkeys: 'Git Keys',
data: 'Data & Privacy', providers: 'My Providers', roles: 'Model Roles',
usage: 'My Usage', knowledge: 'Knowledge Bases', memory: 'Memory',
notifications: 'Notifications',
};
function SettingsSurface() {
const BASE = window.__BASE__ || '';
const section = window.__SECTION__ || 'general';
const pageData = window.__PAGE_DATA__ || {};
const [byokEnabled, setByokEnabled] = useState(!!pageData.BYOKEnabled);
const [personasEnabled, setPersonasEnabled] = useState(!!pageData.UserPersonasEnabled);
const [SectionComponent, setSectionComponent] = useState(null);
// Fetch policies if not provided by template
useEffect(() => {
if (!pageData.BYOKEnabled || !pageData.UserPersonasEnabled) {
sw.api.admin?.settings?.public?.().then(data => {
const policies = data?.policies || {};
if (policies.allow_user_byok === 'true') setByokEnabled(true);
if (policies.allow_user_personas === 'true') setPersonasEnabled(true);
}).catch(() => {});
}
}, []);
// Load the section component
useEffect(() => {
setSectionComponent(null);
if (sectionModules[section]) {
sectionModules[section]().then(mod => {
// Module exports the component as the first named export
const Comp = mod.default || Object.values(mod)[0];
setSectionComponent(() => Comp);
}).catch(e => {
console.error('[settings] Failed to load section:', section, e);
});
}
// Bridge sections are handled inline
}, [section]);
const onNavClick = useCallback((e) => {
e.preventDefault();
const href = e.currentTarget.getAttribute('href');
if (href) location.replace(href);
}, []);
const backClick = useCallback((e) => {
e.preventDefault();
const RETURN_KEY = 'sb_settings_return';
const returnURL = sessionStorage.getItem(RETURN_KEY);
sessionStorage.removeItem(RETURN_KEY);
location.href = returnURL || BASE + '/';
}, []);
// Stash return URL on first load
useEffect(() => {
const RETURN_KEY = 'sb_settings_return';
if (!sessionStorage.getItem(RETURN_KEY)) {
const ref = document.referrer;
if (ref && ref.startsWith(location.origin) && !ref.includes('/settings')) {
sessionStorage.setItem(RETURN_KEY, ref);
} else {
sessionStorage.setItem(RETURN_KEY, location.origin + BASE + '/');
}
}
}, []);
const title = SECTION_TITLES[section] || 'Settings';
return html`
<div class="surface-settings" style="flex-direction:column;">
${/* Top Bar */''}
<div class="settings-topbar">
<a href="${BASE}/" class="settings-topbar-back" onClick=${backClick}>
<svg width="15" height="15" viewBox="0 0 24 24" fill="none" stroke="currentColor"
stroke-width="2" stroke-linecap="round" stroke-linejoin="round">
<line x1="19" y1="12" x2="5" y2="12"/>
<polyline points="12 19 5 12 12 5"/>
</svg>
Back
</a>
<div class="settings-topbar-sep"></div>
<svg width="16" height="16" viewBox="0 0 24 24" fill="none" stroke="currentColor"
stroke-width="2" stroke-linecap="round" stroke-linejoin="round"
style="color:var(--text-2)">
<path d="M17 21v-2a4 4 0 0 0-4-4H5a4 4 0 0 0-4 4v2"/>
<circle cx="9" cy="7" r="4"/>
</svg>
<span class="settings-topbar-title">Settings</span>
</div>
<div style="display:flex;flex:1;min-height:0;">
${/* Left Nav */''}
<div class="settings-nav">
${NAV_ITEMS
.filter(item => !item.gate || (item.gate === 'personas' && personasEnabled))
.map(item => html`
<a href="${BASE}/settings/${item.key}"
class="settings-nav-link ${section === item.key ? 'active' : ''}"
onClick=${onNavClick}>
${item.label}
</a>
`)}
${byokEnabled && html`
<div>
<div class="settings-nav-sep"></div>
${BYOK_ITEMS.map(item => html`
<a href="${BASE}/settings/${item.key}"
class="settings-nav-link ${section === item.key ? 'active' : ''}"
onClick=${onNavClick}>
${item.label}
</a>
`)}
</div>
<div class="settings-nav-footer">
<div style="display:flex;align-items:center;gap:6px;margin-bottom:4px;">
<span style="font-size:11px;font-weight:600;color:var(--success);">BYOK Enabled</span>
</div>
<p style="font-size:10px;color:var(--text-3);margin:0;line-height:1.5;">
Admin has enabled Bring Your Own Key
</p>
</div>
`}
</div>
${/* Content */''}
<div class="settings-content">
<h2>${title}</h2>
${bridgeSections[section]
? html`<${BridgeSection} id=${bridgeSections[section].id}
loader=${bridgeSections[section].loader} />`
: SectionComponent
? html`<${SectionComponent} />`
: html`<div class="settings-placeholder">Loading\u2026</div>`
}
</div>
</div>
</div>
<${ToastContainer} />
<${DialogStack} />
`;
}
// ── Mount ────────────────────────────────────
const mount = document.getElementById('settings-mount');
if (mount) {
render(html`<${SettingsSurface} />`, mount);
console.log('[settings] Surface mounted');
}
export { SettingsSurface };