Skip to content

Commit c134989

Browse files
committed
Interface updates
1 parent d322e09 commit c134989

4 files changed

Lines changed: 573 additions & 78 deletions

File tree

public/app.css

Lines changed: 7 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -35,6 +35,13 @@
3535
height: calc(100vh - 56px);
3636
overflow: hidden;
3737
}
38+
39+
/* nav.js Open WebUI shell: main fills space below top bar */
40+
.app-main-wrap > .app-main {
41+
height: auto;
42+
flex: 1;
43+
min-height: 0;
44+
}
3845
.chat-sidebar {
3946
width: 240px;
4047
min-width: 240px;

public/nav.js

Lines changed: 196 additions & 78 deletions
Original file line numberDiff line numberDiff line change
@@ -2,100 +2,215 @@
22
const PATH = window.location.pathname || '/';
33

44
function isActive(href) {
5-
return PATH === href;
5+
return PATH === href || (href !== '/' && PATH.startsWith(href + '/'));
66
}
77

8-
function navLink(href, label) {
9-
const cls = 'nav-link' + (isActive(href) ? ' active' : '');
10-
return `<a href="${href}" class="${cls}">${label}</a>`;
8+
/** Small inline SVG icons (Open WebUI–style, neutral) */
9+
function icon(name) {
10+
const common = ' xmlns="http://www.w3.org/2000/svg" fill="none" stroke="currentColor" stroke-width="1.75" stroke-linecap="round" stroke-linejoin="round"';
11+
const svg = (path) => `<svg class="app-nav-icon-svg" viewBox="0 0 24 24"${common}>${path}</svg>`;
12+
const icons = {
13+
home: svg('<path d="M3 9l9-7 9 7v11a2 2 0 0 1-2 2H5a2 2 0 0 1-2-2z"/><polyline points="9 22 9 12 15 12 15 22"/>'),
14+
layout: svg('<rect x="3" y="3" width="7" height="9"/><rect x="14" y="3" width="7" height="5"/><rect x="14" y="12" width="7" height="9"/><rect x="3" y="16" width="7" height="5"/>'),
15+
message: svg('<path d="M21 15a2 2 0 0 1-2 2H7l-4 4V5a2 2 0 0 1 2-2h14a2 2 0 0 1 2 2z"/>'),
16+
folder: svg('<path d="M22 19a2 2 0 0 1-2 2H4a2 2 0 0 1-2-2V5a2 2 0 0 1 2-2h5l2 3h9a2 2 0 0 1 2 2z"/>'),
17+
zap: svg('<polygon points="13 2 3 14 12 14 11 22 21 10 12 10 13 2"/>'),
18+
book: svg('<path d="M4 19.5A2.5 2.5 0 0 1 6.5 17H20"/><path d="M6.5 2H20v20H6.5A2.5 2.5 0 0 1 4 19.5v-15A2.5 2.5 0 0 1 6.5 2z"/>'),
19+
database: svg('<ellipse cx="12" cy="5" rx="9" ry="3"/><path d="M21 12c0 1.66-4 3-9 3s-9-1.34-9-3"/><path d="M3 5v14c0 1.66 4 3 9 3s9-1.34 9-3V5"/>'),
20+
user: svg('<path d="M20 21v-2a4 4 0 0 0-4-4H8a4 4 0 0 0-4 4v2"/><circle cx="12" cy="7" r="4"/>'),
21+
settings: svg('<circle cx="12" cy="12" r="3"/><path d="M12 1v2M12 21v2M4.22 4.22l1.42 1.42M18.36 18.36l1.42 1.42M1 12h2M21 12h2M4.22 19.78l1.42-1.42M18.36 5.64l1.42-1.42"/>'),
22+
sliders: svg('<line x1="4" y1="21" x2="4" y2="14"/><line x1="4" y1="10" x2="4" y2="3"/><line x1="12" y1="21" x2="12" y2="12"/><line x1="12" y1="8" x2="12" y2="3"/><line x1="20" y1="21" x2="20" y2="16"/><line x1="20" y1="12" x2="20" y2="3"/>'),
23+
cpu: svg('<rect x="4" y="4" width="16" height="16" rx="2"/><rect x="9" y="9" width="6" height="6"/><line x1="9" y1="1" x2="9" y2="4"/><line x1="15" y1="1" x2="15" y2="4"/><line x1="9" y1="20" x2="9" y2="23"/><line x1="15" y1="20" x2="15" y2="23"/><line x1="20" y1="9" x2="23" y2="9"/><line x1="20" y1="15" x2="23" y2="15"/><line x1="1" y1="9" x2="4" y2="9"/><line x1="1" y1="15" x2="4" y2="15"/>'),
24+
logOut: svg('<path d="M9 21H5a2 2 0 0 1-2-2V5a2 2 0 0 1 2-2h4"/><polyline points="16 17 21 12 16 7"/><line x1="21" y1="12" x2="9" y2="12"/>')
25+
};
26+
return icons[name] || icons.home;
1127
}
1228

13-
function buildHeader(appName, isAdmin) {
14-
const systemMenu = isAdmin ? `
15-
<div class="nav-dropdown">
16-
<span class="nav-link nav-dropdown-toggle${PATH.startsWith('/config') || PATH.startsWith('/personality') || PATH.startsWith('/heartbeat') || PATH.startsWith('/agents') || PATH.startsWith('/pipelines') || PATH.startsWith('/autoagent') || PATH.startsWith('/users') || PATH.startsWith('/editor') ? ' has-active' : ''}">SYSTEM</span>
17-
<div class="nav-dropdown-menu">
18-
${navLink('/config', 'CONFIG')}
19-
${navLink('/personality', 'PERSONALITY')}
20-
${navLink('/heartbeat', 'HEARTBEAT')}
21-
${navLink('/agents', 'AGENTS')}
22-
${navLink('/pipelines', 'PIPELINES')}
23-
${navLink('/autoagent', 'AUTOAGENT')}
24-
${navLink('/users', 'USERS')}
25-
${navLink('/editor', 'EDITOR')}
26-
</div>
29+
function navItem(href, label, icn) {
30+
const active = isActive(href);
31+
const cls = 'app-nav-item' + (active ? ' active' : '');
32+
const Tag = active ? 'span' : 'a';
33+
const inner = `${icon(icn)}<span class="app-nav-label">${label}</span>`;
34+
if (active) return `<span class="${cls}" aria-current="page">${inner}</span>`;
35+
return `<a href="${href}" class="${cls}">${inner}</a>`;
36+
}
37+
38+
function systemPathActive() {
39+
return ['/config', '/personality', '/heartbeat', '/agents', '/pipelines', '/autoagent', '/users', '/editor'].some(p => PATH === p || PATH.startsWith(p + '/'));
40+
}
41+
42+
function buildSidebar(appName, isAdmin) {
43+
const sysOpen = systemPathActive();
44+
const systemBlock = isAdmin ? `
45+
<div class="app-nav-section">
46+
<div class="app-nav-section-label">Administration</div>
47+
<details class="app-nav-details" ${sysOpen ? 'open' : ''}>
48+
<summary class="app-nav-details-summary">
49+
${icon('settings')}
50+
<span class="app-nav-label">System</span>
51+
<span class="app-nav-chevron" aria-hidden="true"></span>
52+
</summary>
53+
<div class="app-nav-sub">
54+
${navItem('/config', 'Config', 'sliders')}
55+
${navItem('/personality', 'Personality', 'user')}
56+
${navItem('/heartbeat', 'Heartbeat', 'cpu')}
57+
${navItem('/agents', 'Agents', 'cpu')}
58+
${navItem('/pipelines', 'Pipelines', 'layout')}
59+
${navItem('/autoagent', 'AutoAgent', 'zap')}
60+
${navItem('/users', 'Users', 'user')}
61+
${navItem('/editor', 'Editor', 'layout')}
62+
</div>
63+
</details>
2764
</div>` : '';
2865

2966
return `
30-
<header class="app-header">
31-
<div class="header-left">
32-
<a href="/dashboard" class="logo">${appName}</a>
33-
</div>
34-
<nav class="header-nav">
35-
${navLink('/dashboard', 'DASHBOARD')}
36-
${navLink('/command-center', 'COMMAND CENTER')}
37-
${navLink('/app', 'CHAT')}
38-
${navLink('/projects', 'PROJECTS')}
39-
${navLink('/skills', 'SKILLS')}
40-
${navLink('/rag', 'KNOWLEDGE')}
41-
${systemMenu}
42-
${navLink('/my-data', 'MY DATA')}
43-
${navLink('/profile', 'ACCOUNT')}
44-
<button type="button" id="logoutBtn" class="btn btn-small">LOGOUT</button>
45-
</nav>
46-
</header>
67+
<div class="app-sidebar-brand">
68+
<a href="/dashboard" class="app-sidebar-logo">${appName.replace(/_/g, ' ')}</a>
69+
</div>
70+
<nav class="app-sidebar-nav" aria-label="Main">
71+
${navItem('/dashboard', 'Dashboard', 'home')}
72+
${navItem('/command-center', 'Command Center', 'layout')}
73+
${navItem('/app', 'Chat', 'message')}
74+
${navItem('/projects', 'Projects', 'folder')}
75+
${navItem('/skills', 'Skills', 'zap')}
76+
${navItem('/rag', 'Knowledge', 'book')}
77+
${navItem('/my-data', 'My Data', 'database')}
78+
</nav>
79+
${systemBlock}
80+
<div class="app-sidebar-footer">
81+
${navItem('/profile', 'Account', 'user')}
82+
<button type="button" class="app-nav-item app-nav-logout" id="logoutBtn">
83+
${icon('logOut')}
84+
<span class="app-nav-label">Logout</span>
85+
</button>
86+
</div>
4787
`;
4888
}
4989

50-
function wireInteractions() {
51-
const btn = document.getElementById('logoutBtn');
52-
if (btn) btn.addEventListener('click', async () => {
53-
await fetch('/api/logout', { method: 'POST' });
54-
window.location.href = '/login';
55-
});
90+
function pageTitleFromDocument() {
91+
const t = document.title || '';
92+
const m = t.match(/[\-]\s*(.+)$/);
93+
return (m && m[1] ? m[1].trim() : t.replace(/^ShadowAI\s*[\-]?\s*/i, '').trim()) || 'Shadow';
94+
}
95+
96+
function mountOpenWebUILayout(appName, isAdmin) {
97+
const header = document.querySelector('.app-header');
98+
if (!header) return false;
5699

57-
const appHeader = document.querySelector('.app-header');
58-
const headerNav = document.querySelector('.header-nav');
59-
if (!appHeader || !headerNav) return;
60-
61-
const hamburger = document.createElement('button');
62-
hamburger.className = 'nav-hamburger';
63-
hamburger.setAttribute('aria-label', 'Toggle navigation');
64-
hamburger.setAttribute('aria-expanded', 'false');
65-
hamburger.textContent = '\u2630';
66-
appHeader.insertBefore(hamburger, headerNav);
67-
68-
function closeHamburgerNav() {
69-
headerNav.classList.remove('nav-open');
70-
hamburger.setAttribute('aria-expanded', 'false');
71-
hamburger.textContent = '\u2630';
100+
let main = document.querySelector('main');
101+
if (!main) {
102+
const toWrap = [];
103+
let n = header.nextElementSibling;
104+
while (n && n.tagName !== 'SCRIPT') {
105+
const next = n.nextElementSibling;
106+
toWrap.push(n);
107+
n = next;
108+
}
109+
if (!toWrap.length) return false;
110+
main = document.createElement('main');
111+
main.className = 'app-fallback-main';
112+
toWrap.forEach((el) => main.appendChild(el));
72113
}
73114

74-
hamburger.addEventListener('click', function (e) {
75-
e.stopPropagation();
76-
const isOpen = headerNav.classList.toggle('nav-open');
77-
hamburger.setAttribute('aria-expanded', String(isOpen));
78-
hamburger.textContent = isOpen ? '\u2715' : '\u2630';
79-
});
115+
document.body.classList.add('has-app-shell');
116+
117+
const extras = [];
118+
const agentSelect = document.getElementById('agentSelect');
119+
const clearBtn = document.getElementById('clearChatBtn');
120+
const exportBtn = document.getElementById('exportChatBtn');
121+
if (agentSelect) extras.push(agentSelect);
122+
if (clearBtn) extras.push(clearBtn);
123+
if (exportBtn) extras.push(exportBtn);
124+
125+
const shell = document.createElement('div');
126+
shell.className = 'app-shell';
127+
128+
const sidebar = document.createElement('aside');
129+
sidebar.className = 'app-sidebar';
130+
sidebar.setAttribute('aria-label', 'Application');
131+
sidebar.innerHTML = buildSidebar(appName, isAdmin);
132+
133+
const wrap = document.createElement('div');
134+
wrap.className = 'app-main-wrap';
135+
136+
const topbar = document.createElement('header');
137+
topbar.className = 'app-topbar';
138+
topbar.innerHTML = `
139+
<button type="button" class="app-sidebar-toggle" id="appSidebarToggle" aria-label="Toggle sidebar" aria-expanded="true">
140+
<svg viewBox="0 0 24 24" width="22" height="22" fill="none" stroke="currentColor" stroke-width="2" aria-hidden="true"><line x1="3" y1="6" x2="21" y2="6"/><line x1="3" y1="12" x2="21" y2="12"/><line x1="3" y1="18" x2="21" y2="18"/></svg>
141+
</button>
142+
<h1 class="app-topbar-title">${pageTitleFromDocument()}</h1>
143+
<div class="app-topbar-actions" id="appTopbarActions"></div>
144+
`;
145+
146+
const actions = topbar.querySelector('#appTopbarActions');
147+
extras.forEach((el) => actions.appendChild(el));
80148

81-
headerNav.addEventListener('click', function (e) {
82-
if (e.target.closest('a.nav-link')) closeHamburgerNav();
149+
header.remove();
150+
151+
shell.appendChild(sidebar);
152+
shell.appendChild(wrap);
153+
wrap.appendChild(topbar);
154+
wrap.appendChild(main);
155+
156+
document.body.insertBefore(shell, document.body.firstChild);
157+
return true;
158+
}
159+
160+
function wireSidebarToggle() {
161+
const toggle = document.getElementById('appSidebarToggle');
162+
const shell = document.querySelector('.app-shell');
163+
if (!toggle || !shell) return;
164+
165+
const mq = window.matchMedia('(max-width: 900px)');
166+
function sync() {
167+
if (mq.matches) {
168+
shell.classList.remove('sidebar-collapsed');
169+
shell.classList.remove('sidebar-overlay-open');
170+
toggle.setAttribute('aria-expanded', 'false');
171+
} else {
172+
shell.classList.remove('sidebar-overlay-open');
173+
toggle.setAttribute('aria-expanded', 'true');
174+
}
175+
}
176+
177+
toggle.addEventListener('click', () => {
178+
if (mq.matches) {
179+
shell.classList.toggle('sidebar-overlay-open');
180+
toggle.setAttribute('aria-expanded', String(shell.classList.contains('sidebar-overlay-open')));
181+
} else {
182+
shell.classList.toggle('sidebar-collapsed');
183+
const collapsed = shell.classList.contains('sidebar-collapsed');
184+
toggle.setAttribute('aria-expanded', String(!collapsed));
185+
}
83186
});
84187

85-
document.addEventListener('click', function (e) {
86-
if (!e.target.closest('.app-header')) closeHamburgerNav();
188+
document.addEventListener('click', (e) => {
189+
if (!mq.matches) return;
190+
if (!shell.classList.contains('sidebar-overlay-open')) return;
191+
if (e.target.closest('.app-sidebar') || e.target.closest('#appSidebarToggle')) return;
192+
shell.classList.remove('sidebar-overlay-open');
193+
toggle.setAttribute('aria-expanded', 'false');
87194
});
88195

89-
document.querySelectorAll('.nav-dropdown-toggle').forEach(function (toggle) {
90-
toggle.addEventListener('click', function (e) {
91-
e.stopPropagation();
92-
toggle.closest('.nav-dropdown').classList.toggle('open');
196+
mq.addEventListener('change', sync);
197+
sync();
198+
}
199+
200+
function wireDetailsCloseOnNavigate() {
201+
document.querySelectorAll('.app-sidebar a.app-nav-item').forEach((a) => {
202+
a.addEventListener('click', () => {
203+
document.querySelector('.app-shell')?.classList.remove('sidebar-overlay-open');
93204
});
94205
});
95-
document.addEventListener('click', function (e) {
96-
if (!e.target.closest('.nav-dropdown')) {
97-
document.querySelectorAll('.nav-dropdown.open').forEach(function (d) { d.classList.remove('open'); });
98-
}
206+
}
207+
208+
function wireLogout() {
209+
const btn = document.getElementById('logoutBtn');
210+
if (!btn) return;
211+
btn.addEventListener('click', async () => {
212+
await fetch('/api/logout', { method: 'POST' });
213+
window.location.href = '/login';
99214
});
100215
}
101216

@@ -105,13 +220,16 @@
105220
]).then(([me, app]) => {
106221
const appName = (app && app.appName && String(app.appName).trim()) || 'SHADOW_AI';
107222
const isAdmin = !!(me && me.role === 'admin');
108-
const existing = document.querySelector('.app-header');
109-
if (existing) existing.outerHTML = buildHeader(appName, isAdmin);
110-
else document.body.insertAdjacentHTML('afterbegin', buildHeader(appName, isAdmin));
111223

112224
if (document.title && document.title.indexOf('ShadowAI') !== -1) {
113225
document.title = document.title.replace(/ShadowAI/g, appName.replace(/_/g, ' '));
114226
}
115-
wireInteractions();
227+
228+
const mounted = mountOpenWebUILayout(appName, isAdmin);
229+
if (!mounted) return;
230+
231+
wireLogout();
232+
wireSidebarToggle();
233+
wireDetailsCloseOnNavigate();
116234
});
117235
})();

public/pipelines.css

Lines changed: 7 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -4,6 +4,13 @@
44
overflow: hidden;
55
}
66

7+
/* nav.js shell: fill below top bar */
8+
.app-main-wrap .pipelines-layout {
9+
height: auto;
10+
flex: 1;
11+
min-height: 0;
12+
}
13+
714
/* Sidebar — pipeline list */
815
.pipelines-sidebar {
916
width: 220px;

0 commit comments

Comments
 (0)