diff --git a/public/css/assistant.css b/public/css/assistant.css
index 8d6b886b..40ba0c19 100644
--- a/public/css/assistant.css
+++ b/public/css/assistant.css
@@ -1,20 +1,15 @@
.assistant-header { display:flex; justify-content:space-between; gap:12px; align-items:flex-start; }
.assistant-mobile-menu { display:none; }
-.assistant-topbar { display:flex; align-items:center; gap:12px; padding:6px 12px; border-bottom:1px solid var(--g200); background:var(--g50); position:sticky; top:0; z-index:4; }
-.assistant-topbar-title { flex:1; min-width:0; position:relative; }
-.assistant-topbar-title h2 { font-size:15px; margin:0; color:var(--g800); }
/* Inside the assistant workspace the page IS the assistant; naming it again in
the topbar just costs a row of space. Kept in the DOM for screen readers. */
-body.assistant-workspace .assistant-topbar-title h2 { position:absolute; width:1px; height:1px; margin:-1px; padding:0; overflow:hidden; clip:rect(0 0 0 0); white-space:nowrap; border:0; }
-.assistant-topbar-title h2 i { margin-right:6px; }
-.assistant-status { display:flex; align-items:center; gap:6px; font-size:12px; color:var(--g500); background:white; border:1px solid var(--g200); border-radius:999px; padding:5px 10px; box-shadow:var(--shadow); }
+.assistant-status { display:inline-flex; align-items:center; gap:5px; font-size:11px; color:var(--g400); }
.assistant-dot { width:8px; height:8px; border-radius:50%; background:var(--green); display:inline-block; }
.assistant-status.busy .assistant-dot { background:var(--amber); animation:pulse 1.5s infinite; }
.assistant-status.error .assistant-dot { background:var(--red); }
.assistant-layout { display:grid; grid-template-columns:260px minmax(0,1fr) 330px; gap:14px; align-items:stretch; }
.assistant-layout > * { min-width:0; }
-.assistant-history { display:grid; gap:10px; align-content:start; position:sticky; top:52px; }
+.assistant-history { display:grid; gap:10px; align-content:start; position:sticky; top:0; }
.assistant-new-chat { display:flex; align-items:center; justify-content:center; gap:8px; width:100%; border:1px solid var(--blue-light); background:var(--blue-light); color:var(--blue); border-radius:10px; padding:10px 12px; font-size:13px; font-weight:700; cursor:pointer; }
.assistant-new-chat:hover { border-color:var(--blue); background:var(--blue-light); }
.assistant-main { display:flex; flex-direction:column; min-height:calc(100vh - 200px); min-width:0; }
@@ -192,12 +187,9 @@ body.assistant-workspace .assistant-topbar-title h2 { position:absolute; width:1
@media (max-width: 960px) { .assistant-layout { grid-template-columns:minmax(0,1fr) 300px; } .assistant-history { position:static; grid-column:1 / -1; grid-template-columns:auto 1fr; align-items:start; } .assistant-new-chat { grid-column:1; } .assistant-view-switch { grid-column:1; } .assistant-history .card { grid-column:2; grid-row:1 / span 2; } .assistant-main { min-height:auto; } #assistant-chat-view { min-height:auto; grid-template-rows:minmax(0,1fr) auto; } }
@media (max-width: 640px) {
/* ── ChatGPT/OWUI mobile: independent full-screen overlay; the page behind never scrolls ── */
- .assistant-topbar { flex-wrap:nowrap; gap:8px; padding:10px 14px; }
.assistant-mobile-menu { display:inline-flex; }
- .assistant-topbar .btn-sm { padding:6px 8px; }
#btn-assistant-goback span, #btn-assistant-goback { font-size:0; }
#btn-assistant-goback { padding:8px; }
- .assistant-topbar-title h2 { font-size:15px; }
.assistant-status { margin-left:auto; font-size:11px; }
/* The assistant fills the viewport exactly; nothing behind can scroll */
.assistant-layout { display:flex; flex-direction:column; height:100dvh; min-height:0; overflow:hidden; }
@@ -417,7 +409,6 @@ body.assistant-workspace #assistant-chat-view { min-height: 0; }
.assistant-goback-rail, .assistant-create-image, .assistant-new-chat { font-size:13px; padding:11px 12px; white-space:nowrap; overflow:hidden; text-overflow:ellipsis; }
.assistant-history .card-header h3 { font-size:11px; white-space:nowrap; overflow:hidden; text-overflow:ellipsis; }
/* The topbar keeps only the hamburger; Go back is in the menu */
- .assistant-topbar #btn-assistant-goback { display:none; }
}
/* Library item hover download */
.assistant-gallery-item-wrap { position:relative; }
@@ -503,3 +494,15 @@ body.assistant-mode-workspace .assistant-rail-workspace { display:flex; }
#assistant-chat-view:has(.assistant-messages:not(:has(.assistant-msg))) .assistant-composer {
margin:0 10px 10px; width:auto; max-width:none; }
}
+
+/* The drawer button floats on phones instead of occupying a bar the desktop
+ layout does not have — that bar was the dead strip at the top and the reason
+ the assistant and the workspace were different heights. */
+.assistant-mobile-menu { display:none; }
+@media (max-width:640px) {
+ .assistant-mobile-menu { display:inline-flex; align-items:center; justify-content:center; position:absolute; top:8px; left:8px; z-index:70; width:36px; height:36px; border:1px solid var(--g200); border-radius:10px; background:white; color:var(--g600); font-size:14px; box-shadow:var(--shadow); }
+ .assistant-layout { position:relative; }
+}
+/* Both toggles point the way they act, and flip when collapsed. */
+.assistant-menu-toggle i { transition:transform .15s ease; }
+.assistant-menu-toggle.is-collapsed i { transform:rotate(180deg); }
diff --git a/public/index.html b/public/index.html
index 7efa1bad..6fbf5382 100644
--- a/public/index.html
+++ b/public/index.html
@@ -197,7 +197,7 @@
-
+
diff --git a/test/assistant-workspace-layout.test.js b/test/assistant-workspace-layout.test.js
index df2fb779..d586ae7b 100644
--- a/test/assistant-workspace-layout.test.js
+++ b/test/assistant-workspace-layout.test.js
@@ -60,8 +60,9 @@ test('assistant area is an OWUI-style three-column workspace with a slim go-back
assert.equal(right.querySelector('#assistant-visual-output'), null, 'no image display in the right column — sources only');
assert.ok(right.querySelector('#assistant-sources'), 'sources panel in the right column');
assert.equal(right.querySelector('#assistant-saved-chats'), null, 'saved chats moved out of the right column');
- const topbar = app.document.querySelector('.assistant-topbar');
- assert.ok(topbar);
+ // No bar above the layout at all: the app page has only the blue header, so a
+ // second bar here made the assistant taller and left dead space on top.
+ assert.equal(app.document.querySelector('.assistant-topbar'), null, 'no assistant-only top bar');
// Go back is gone: the Assistant/Workspace switch is how you leave, so a
// second exit control in the topbar was one affordance too many.
assert.equal(app.document.querySelector('#btn-assistant-goback'), null, 'no Go back in the topbar');
@@ -154,15 +155,31 @@ test('collapsing the rail does not hand the chat column to the citations panel',
assert.match(track, /grid-template-columns:0 minmax\(0,1fr\) 330px/, 'three tracks for three items');
});
-test('the assistant workspace does not repeat its own name in the topbar', () => {
- const fs = require('node:fs');
- const path = require('node:path');
- const css = fs.readFileSync(path.join(__dirname, '..', 'public/css/assistant.css'), 'utf8');
- const rule = css.split('\n').find(l => l.startsWith('body.assistant-workspace .assistant-topbar-title h2'));
- assert.ok(rule, 'the title is hidden inside the workspace');
- assert.match(rule, /clip:rect\(0 0 0 0\)/, 'visually hidden, still read by screen readers');
- const html = fs.readFileSync(path.join(__dirname, '..', 'public/components/assistant.html'), 'utf8');
- assert.match(html, /AI Clinical Assistant/, 'and still present in the DOM');
+test('the assistant and the app start at the same top edge', () => {
+ const rail = read('public/components/assistant.html');
+ const css = read('public/css/assistant.css');
+ // The assistant used to carry its own bar on top of the shared blue header,
+ // so switching modes changed the height of everything below it.
+ assert.doesNotMatch(rail, /assistant-topbar/, 'no second bar in the markup');
+ assert.doesNotMatch(css, /assistant-topbar/, 'and none left in the stylesheet');
+ assert.match(css, /\.assistant-history \{ display:grid; gap:10px; align-content:start; position:sticky; top:0; \}/,
+ 'the rail sticks to the top of the layout, not below a bar');
+ // The status moved into the composer, next to the work it describes.
+ assert.match(rail, /assistant-composer-footer[\s\S]*?id="assistant-status"/,
+ 'the ready/busy indicator lives in the composer now');
+});
+
+test('the menu toggle uses an icon that reads as a menu', () => {
+ for (const file of ['public/index.html', 'public/components/assistant.html']) {
+ const markup = read(file);
+ const button = markup.split('\n').find(l => l.includes('data-menu-toggle'));
+ assert.ok(button, file + ' has the toggle');
+ // fa-table-columns draws a split-pane glyph, which does not say "menu".
+ assert.doesNotMatch(button, /fa-table-columns/, file + ' uses a menu icon');
+ assert.match(button, /fa-bars/, file);
+ }
+ assert.match(read('public/css/assistant.css'), /\.assistant-menu-toggle\.is-collapsed i \{ transform:rotate\(180deg\); \}/,
+ 'and it turns to show the direction it will act in');
});
test('workspace mode does not override the mobile layout with a desktop header offset', () => {