diff --git a/public/components/assistant.html b/public/components/assistant.html index 8efbe0dd..4c0ec274 100644 --- a/public/components/assistant.html +++ b/public/components/assistant.html @@ -21,11 +21,12 @@ - +
-
+
+ +

Chats save automatically as you go.

@@ -60,9 +61,8 @@
- -

Evidence-first pediatric assistant

-

Ask a real clinical question. Citations stay linked to the source cards on the right.

+

Pediatric Clinical Assistant

+

Answers built only from your indexed library

diff --git a/public/css/assistant.css b/public/css/assistant.css index 70445771..91c49b2b 100644 --- a/public/css/assistant.css +++ b/public/css/assistant.css @@ -134,6 +134,27 @@ body.assistant-workspace .assistant-topbar-title h2 { position:absolute; width:1 .assistant-sources { padding:10px 12px; display:grid; gap:8px; flex:1 1 auto; min-height:0; overflow-y:auto; } .assistant-saved-chats { flex:1 1 auto; overflow-y:auto; min-height:0; padding:10px 12px; display:flex; flex-direction:column; gap:8px; } /* Open WebUI-style recency headings above each run of chats. */ +/* Composed empty state: a title, one status pill and the composer — nothing + else competing for attention before the first question is asked. */ +.assistant-empty h3 { color:var(--g900); font-size:26px; font-weight:600; letter-spacing:-.01em; margin:0 0 12px; } +.assistant-empty-badge { display:inline-flex; align-items:center; gap:7px; margin:0; padding:7px 14px; border:1px solid var(--g200); border-radius:999px; background:white; box-shadow:var(--shadow); font-size:12.5px; color:var(--g600); } +.assistant-empty-badge i { color:var(--purple); font-size:12px; } +.assistant-examples button { border:1px solid var(--g200); background:white; color:var(--g700); border-radius:10px; padding:9px 14px; font-size:12.5px; box-shadow:var(--shadow); } +.assistant-examples button:hover { border-color:var(--purple-light); background:#faf5ff; color:var(--purple); } + +/* A faint graph-paper ground so the composer card reads as a raised surface. */ +.assistant-messages { background-image:linear-gradient(var(--g100) 1px, transparent 1px), linear-gradient(90deg, var(--g100) 1px, transparent 1px); background-size:28px 28px; background-position:-1px -1px; } +.assistant-messages:has(.assistant-msg) { background-image:none; } + +/* New chat carries its shortcut, the way a command row does. */ +.assistant-new-chat { justify-content:flex-start; } +.assistant-kbd { margin-left:auto; font:inherit; font-size:10px; color:var(--g400); background:var(--g100); border:1px solid var(--g200); border-radius:5px; padding:2px 5px; white-space:nowrap; } + +/* Saved Chats collapses, like the Chats group in the reference UI. */ +.assistant-chats-header { display:flex; align-items:center; justify-content:space-between; width:100%; border:0; background:none; cursor:pointer; text-align:left; } +.assistant-chats-caret { font-size:10px; color:var(--g400); transition:transform .15s ease; } +.assistant-chats-header[aria-expanded="false"] .assistant-chats-caret { transform:rotate(-90deg); } + /* Two-mode switch at the top of the rail, the way Home and Code swap the page. */ .assistant-mode-switch { display:grid; grid-template-columns:1fr 1fr; gap:4px; background:var(--g100); border:1px solid var(--g200); border-radius:10px; padding:4px; margin-bottom:8px; } .assistant-mode-switch button { display:flex; align-items:center; justify-content:center; gap:6px; border:0; border-radius:7px; padding:7px 6px; font-size:12px; font-weight:600; color:var(--g500); background:transparent; cursor:pointer; } diff --git a/public/js/clinicalAssistant.js b/public/js/clinicalAssistant.js index 2432c64b..fc7d1b6a 100644 --- a/public/js/clinicalAssistant.js +++ b/public/js/clinicalAssistant.js @@ -234,6 +234,28 @@ import { if (typeof window.activateTab === 'function') window.activateTab(link.getAttribute('data-assistant-workspace-tab')); }); + var chatsToggle = document.getElementById('btn-assistant-chats-toggle'); + if (chatsToggle) chatsToggle.addEventListener('click', function() { + var list = document.getElementById('assistant-saved-chats'); + if (!list) return; + var open = list.hidden; + list.hidden = !open; + chatsToggle.setAttribute('aria-expanded', open ? 'true' : 'false'); + try { localStorage.setItem('ped_assistant_chats_open', open ? '1' : '0'); } catch (e) {} + }); + try { + if (localStorage.getItem('ped_assistant_chats_open') === '0' && chatsToggle) chatsToggle.click(); + } catch (e) {} + + // Ctrl+Shift+O starts a new chat, as advertised next to the button. + document.addEventListener('keydown', function(event) { + if (!event.ctrlKey || !event.shiftKey || String(event.key).toLowerCase() !== 'o') return; + if (!document.body.classList.contains('assistant-workspace')) return; + event.preventDefault(); + var button = document.getElementById('btn-assistant-clear'); + if (button) button.click(); + }); + var closeDrawer = function() { var layout = document.getElementById('assistant-layout'); if (layout) layout.classList.remove('mobile-chats-open'); diff --git a/test/assistant-component-css.test.js b/test/assistant-component-css.test.js index a2b3f60d..0b85c577 100644 --- a/test/assistant-component-css.test.js +++ b/test/assistant-component-css.test.js @@ -150,3 +150,31 @@ test('out-of-order CSS completion does not initialize an inactive tab; returning assert.equal(tab.querySelector('[role="status"]'), null); dom.window.close(); }); + +test('the empty state is composed, not a stack of competing blocks', () => { + const fs = require('node:fs'); + const path = require('node:path'); + const root = path.join(__dirname, '..'); + const html = fs.readFileSync(path.join(root, 'public/components/assistant.html'), 'utf8'); + assert.match(html, /assistant-empty-badge/, 'one status pill under the title'); + assert.doesNotMatch(html, /<\/i>\s*

/, 'no oversized icon above the title'); + + const css = fs.readFileSync(path.join(root, 'public/css/assistant.css'), 'utf8'); + // The graph-paper ground reads as a surface only while the transcript is + // empty; once there are messages it would fight the text. + assert.match(css, /\.assistant-messages:has\(\.assistant-msg\) \{ background-image:none; \}/); + assert.match(css, /\.assistant-kbd/, 'New chat advertises its shortcut'); + assert.match(css, /\.assistant-chats-header\[aria-expanded="false"\] \.assistant-chats-caret/, 'the chats group collapses'); +}); + +test('the advertised New chat shortcut actually works, and only inside the assistant', () => { + const fs = require('node:fs'); + const path = require('node:path'); + const js = fs.readFileSync(path.join(__dirname, '..', 'public/js/clinicalAssistant.js'), 'utf8'); + const handler = js.slice(js.indexOf('// Ctrl+Shift+O starts a new chat'), js.indexOf('var closeDrawer')); + assert.match(handler, /event\.ctrlKey && event\.shiftKey|!event\.ctrlKey \|\| !event\.shiftKey/); + assert.match(handler, /toLowerCase\(\) !== 'o'/); + // Must not hijack Ctrl+Shift+O for the rest of the app. + assert.match(handler, /classList\.contains\('assistant-workspace'\)/); + assert.match(handler, /btn-assistant-clear/); +});