Skip to content

Commit fa5e8ac

Browse files
authored
Merge pull request #165 from boettiger-lab/spec/sidebar-layout
feat: opt-in full-height resizable sidebar layout
2 parents 3c0bd92 + 856b399 commit fa5e8ac

10 files changed

Lines changed: 2456 additions & 131 deletions

File tree

.gitignore

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -58,3 +58,6 @@ docs/.vitepress/cache/
5858
*.tmp
5959
duck.db
6060
.streamlit
61+
62+
# Superpowers brainstorming / mockups
63+
.superpowers/

app/chat-ui.js

Lines changed: 48 additions & 110 deletions
Original file line numberDiff line numberDiff line change
@@ -9,20 +9,27 @@ export class ChatUI {
99
/**
1010
* @param {import('./agent.js').Agent} agent
1111
* @param {Object} config - app config (for model list)
12+
* @param {Object} mount - DOM refs from layout-manager.buildLayout()
13+
* {
14+
* container, messages, input, send, mic, header, footer, footerRight,
15+
* }
1216
*/
13-
constructor(agent, config) {
17+
constructor(agent, config, mount) {
1418
this.agent = agent;
1519
this.config = config;
1620
this.busy = false;
1721

18-
// Cache DOM refs
19-
this.container = document.getElementById('chat-container');
20-
this.messagesEl = document.getElementById('chat-messages');
21-
this.inputEl = document.getElementById('chat-input');
22-
this.sendBtn = document.getElementById('chat-send');
23-
this.modelSelector = document.getElementById('model-selector');
24-
this.toggleBtn = document.getElementById('chat-toggle');
25-
this.micBtn = document.getElementById('chat-mic');
22+
// Cache DOM refs from layout-manager (no getElementById here).
23+
this.container = mount.container;
24+
this.messagesEl = mount.messages;
25+
this.inputEl = mount.input;
26+
this.sendBtn = mount.send;
27+
this.micBtn = mount.mic;
28+
this.toggleBtn = mount.container.querySelector('#chat-toggle'); // floating-mode only
29+
this.headerEl = mount.header;
30+
this.footerEl = mount.footer;
31+
this.footerRightEl = mount.footerRight;
32+
this.modelSelector = mount.footerRight.querySelector('#model-selector');
2633

2734
// Voice input state. The voice + transcriber modules are loaded
2835
// lazily via dynamic import() — only when `config.transcription_model`
@@ -64,9 +71,6 @@ export class ChatUI {
6471
// modules are never loaded).
6572
this.initVoiceInput();
6673

67-
// Restructure footer into left + right zones before adding buttons
68-
this.restructureFooter();
69-
7074
// If in user-provided API key mode, add settings button
7175
if (this.config._userProvidedMode) {
7276
this.initSettingsUI();
@@ -79,9 +83,6 @@ export class ChatUI {
7983
// Auto-approve toggle (always shown)
8084
this.initAutoApproveToggle();
8185

82-
// Drag-to-resize handle
83-
this.initResize();
84-
8586
// Optional header/footer links (github, docs, carbon)
8687
this.initLinks();
8788

@@ -237,24 +238,6 @@ export class ChatUI {
237238
this.messagesEl.appendChild(el);
238239
}
239240

240-
/* ------------------------------------------------------------------ */
241-
/* Footer restructuring (left / right zones) */
242-
/* ------------------------------------------------------------------ */
243-
244-
restructureFooter() {
245-
const footer = document.getElementById('chat-footer');
246-
if (!footer) return;
247-
248-
const right = document.createElement('div');
249-
right.id = 'chat-footer-right';
250-
251-
// Move model-selector into right zone
252-
const modelSelector = footer.querySelector('#model-selector');
253-
if (modelSelector) right.appendChild(modelSelector);
254-
255-
footer.appendChild(right);
256-
}
257-
258241
/* ------------------------------------------------------------------ */
259242
/* Optional links: github, docs (header), carbon (footer left) */
260243
/* ------------------------------------------------------------------ */
@@ -263,64 +246,56 @@ export class ChatUI {
263246
const links = this.config.links;
264247
if (!links) return;
265248

266-
// Header links: About (docs) + GitHub octocat
267-
if (links.docs || links.github) {
268-
const headerLinks = document.createElement('div');
269-
headerLinks.className = 'header-links';
270-
271-
if (links.docs) {
272-
const a = document.createElement('a');
273-
a.href = links.docs;
274-
a.target = '_blank';
275-
a.rel = 'noopener noreferrer';
276-
a.className = 'header-link docs-link';
277-
a.textContent = 'About';
278-
a.title = 'Documentation';
279-
headerLinks.appendChild(a);
280-
}
281-
282-
if (links.github) {
283-
const a = document.createElement('a');
284-
a.href = links.github;
285-
a.target = '_blank';
286-
a.rel = 'noopener noreferrer';
287-
a.className = 'header-link github-link';
288-
a.title = 'Source code';
289-
// GitHub mark SVG (official)
290-
a.innerHTML = `<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 16 16" width="16" height="16" aria-hidden="true"><path fill-rule="evenodd" d="M8 0C3.58 0 0 3.58 0 8c0 3.54 2.29 6.53 5.47 7.59.4.07.55-.17.55-.38 0-.19-.01-.82-.01-1.49-2.01.37-2.53-.49-2.69-.94-.09-.23-.48-.94-.82-1.13-.28-.15-.68-.52-.01-.53.63-.01 1.08.58 1.23.82.72 1.21 1.87.87 2.33.66.07-.52.28-.87.51-1.07-1.78-.2-3.64-.89-3.64-3.95 0-.87.31-1.59.82-2.15-.08-.2-.36-1.02.08-2.12 0 0 .67-.21 2.2.82.64-.18 1.32-.27 2-.27.68 0 1.36.09 2 .27 1.53-1.04 2.2-.82 2.2-.82.44 1.1.16 1.92.08 2.12.51.56.82 1.27.82 2.15 0 3.07-1.87 3.75-3.65 3.95.29.25.54.73.54 1.48 0 1.07-.01 1.93-.01 2.2 0 .21.15.46.55.38A8.013 8.013 0 0016 8c0-4.42-3.58-8-8-8z"/></svg>`;
291-
headerLinks.appendChild(a);
292-
}
249+
// All links live in the footer-left zone in both floating and sidebar
250+
// modes. The header is kept link-free.
251+
const footer = this.footerEl;
252+
if (!footer) return;
293253

294-
const header = document.getElementById('chat-header');
295-
const toggleBtn = document.getElementById('chat-toggle');
296-
if (header && toggleBtn) {
297-
header.insertBefore(headerLinks, toggleBtn);
298-
}
299-
}
254+
// Reverse append order: we prepend each link to the footer so that the
255+
// final left-to-right ordering is docs | github | carbon.
256+
// (prepend reverses insertion order — insert carbon first, then github,
257+
// then docs.)
300258

301-
// Footer left: carbon dashboard (NRP deployments only)
302259
if (links.carbon) {
303-
const footer = document.getElementById('chat-footer');
304-
if (!footer) return;
305-
306260
const a = document.createElement('a');
307261
a.href = 'https://carbon-api.nrp-nautilus.io/';
308262
a.target = '_blank';
309263
a.rel = 'noopener noreferrer';
310264
a.className = 'footer-link carbon-link';
311265
a.title = 'Carbon dashboard — energy use for this deployment';
312-
// Leaf SVG
313266
a.innerHTML = `<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 24 24" width="13" height="13" fill="currentColor" aria-hidden="true"><path d="M17 8C8 10 5.9 16.17 3.82 21.34L5.71 22l1-2.3A4.49 4.49 0 008 20C19 20 22 3 22 3c-1 2-8 5.5-8.5 11.5-2.05-1.05-3.72-3.07-3.72-5.5 0-.67.19-1.3.52-1.83A4.89 4.89 0 0017 8z"/></svg>`;
314267
footer.prepend(a);
315268
}
269+
270+
if (links.github) {
271+
const a = document.createElement('a');
272+
a.href = links.github;
273+
a.target = '_blank';
274+
a.rel = 'noopener noreferrer';
275+
a.className = 'footer-link github-link';
276+
a.title = 'Source code';
277+
a.innerHTML = `<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 16 16" width="16" height="16" aria-hidden="true"><path fill-rule="evenodd" d="M8 0C3.58 0 0 3.58 0 8c0 3.54 2.29 6.53 5.47 7.59.4.07.55-.17.55-.38 0-.19-.01-.82-.01-1.49-2.01.37-2.53-.49-2.69-.94-.09-.23-.48-.94-.82-1.13-.28-.15-.68-.52-.01-.53.63-.01 1.08.58 1.23.82.72 1.21 1.87.87 2.33.66.07-.52.28-.87.51-1.07-1.78-.2-3.64-.89-3.64-3.95 0-.87.31-1.59.82-2.15-.08-.2-.36-1.02.08-2.12 0 0 .67-.21 2.2.82.64-.18 1.32-.27 2-.27.68 0 1.36.09 2 .27 1.53-1.04 2.2-.82 2.2-.82.44 1.1.16 1.92.08 2.12.51.56.82 1.27.82 2.15 0 3.07-1.87 3.75-3.65 3.95.29.25.54.73.54 1.48 0 1.07-.01 1.93-.01 2.2 0 .21.15.46.55.38A8.013 8.013 0 0016 8c0-4.42-3.58-8-8-8z"/></svg>`;
278+
footer.prepend(a);
279+
}
280+
281+
if (links.docs) {
282+
const a = document.createElement('a');
283+
a.href = links.docs;
284+
a.target = '_blank';
285+
a.rel = 'noopener noreferrer';
286+
a.className = 'footer-link docs-link';
287+
a.textContent = 'About';
288+
a.title = 'Documentation';
289+
footer.prepend(a);
290+
}
316291
}
317292

318293
/* ------------------------------------------------------------------ */
319294
/* Settings panel (user-provided API key mode) */
320295
/* ------------------------------------------------------------------ */
321296

322297
initSettingsUI() {
323-
const footer = document.getElementById('chat-footer-right');
298+
const footer = this.footerRightEl;
324299
if (!footer) return;
325300

326301
const btn = document.createElement('button');
@@ -433,7 +408,7 @@ export class ChatUI {
433408
/* ------------------------------------------------------------------ */
434409

435410
initAutoApproveToggle() {
436-
const footer = document.getElementById('chat-footer-right');
411+
const footer = this.footerRightEl;
437412
if (!footer) return;
438413

439414
// Resolve initial state: localStorage > config
@@ -456,43 +431,6 @@ export class ChatUI {
456431
footer.prepend(btn);
457432
}
458433

459-
/* ------------------------------------------------------------------ */
460-
461-
initResize() {
462-
const handle = document.createElement('div');
463-
handle.className = 'resize-handle';
464-
this.container.prepend(handle);
465-
466-
let startX, startY, startW, startH;
467-
468-
const onMove = (e) => {
469-
const dx = startX - e.clientX; // positive = dragging left → wider
470-
const dy = startY - e.clientY; // positive = dragging up → taller
471-
const maxW = window.innerWidth - 40;
472-
const maxH = window.innerHeight - 100;
473-
this.container.style.width = Math.min(maxW, Math.max(280, startW + dx)) + 'px';
474-
this.container.style.height = Math.min(maxH, Math.max(200, startH + dy)) + 'px';
475-
};
476-
477-
const onUp = () => {
478-
document.removeEventListener('mousemove', onMove);
479-
document.removeEventListener('mouseup', onUp);
480-
document.body.style.userSelect = '';
481-
};
482-
483-
handle.addEventListener('mousedown', (e) => {
484-
e.preventDefault();
485-
startX = e.clientX;
486-
startY = e.clientY;
487-
startW = this.container.offsetWidth;
488-
startH = this.container.offsetHeight;
489-
document.body.style.userSelect = 'none';
490-
document.addEventListener('mousemove', onMove);
491-
document.addEventListener('mouseup', onUp);
492-
});
493-
}
494-
495-
/* ------------------------------------------------------------------ */
496434
/* Send handler */
497435
/* ------------------------------------------------------------------ */
498436

app/chat.css

Lines changed: 22 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -272,6 +272,28 @@
272272
opacity: 1;
273273
}
274274

275+
/* Footer links (github, docs, carbon) live in #chat-footer's left zone. */
276+
#chat-footer .footer-link {
277+
display: inline-flex;
278+
align-items: center;
279+
padding: 2px 6px;
280+
margin-right: 4px;
281+
color: rgba(255, 255, 255, 0.75);
282+
text-decoration: none;
283+
font-size: 11px;
284+
border-radius: 3px;
285+
transition: color 120ms ease, background 120ms ease;
286+
}
287+
288+
#chat-footer .footer-link:hover {
289+
color: rgba(255, 255, 255, 1);
290+
background: rgba(255, 255, 255, 0.08);
291+
}
292+
293+
#chat-footer .footer-link svg {
294+
display: block;
295+
}
296+
275297
#model-selector {
276298
font-size: 9px;
277299
padding: 2px 8px;

app/index.html

Lines changed: 1 addition & 17 deletions
Original file line numberDiff line numberDiff line change
@@ -41,6 +41,7 @@
4141
<!-- App styles -->
4242
<link rel="stylesheet" href="style.css">
4343
<link rel="stylesheet" href="chat.css">
44+
<link rel="stylesheet" href="sidebar.css">
4445
</head>
4546

4647
<body>
@@ -50,23 +51,6 @@
5051
<!-- Layer controls — generated by MapManager.generateMenu() -->
5152
<div id="menu"></div>
5253

53-
<!-- Chat interface -->
54-
<div id="chat-container">
55-
<div id="chat-header">
56-
<h3>Data Assistant</h3>
57-
<button id="chat-toggle" title="Toggle chat"></button>
58-
</div>
59-
<div id="chat-messages"></div>
60-
<div id="chat-input-container">
61-
<input type="text" id="chat-input" placeholder="Ask about the data…" autocomplete="off">
62-
<button id="chat-mic" title="Hold to record voice input" hidden>🎤</button>
63-
<button id="chat-send">Send</button>
64-
</div>
65-
<div id="chat-footer">
66-
<select id="model-selector" title="Select model"></select>
67-
</div>
68-
</div>
69-
7054
<!-- App bootstrap -->
7155
<script type="module" src="main.js"></script>
7256
</body>

0 commit comments

Comments
 (0)