feat: compose the assistant's empty state after the reference UI
Borrowed from the Tinfoil chat layout, keeping our own palette: - The empty state leads with a title and a single status pill instead of an oversized icon plus two paragraphs, so the composer is what the eye lands on. - Example prompts are raised pill buttons rather than flat outlines. - A faint graph-paper ground behind the transcript makes the composer read as a lifted surface; it switches off as soon as there are messages, where it would fight the text. - New chat carries its Ctrl+Shift+O shortcut on the right, and the shortcut now actually works — scoped to the assistant workspace so it does not hijack the key elsewhere in the app. - Saved Chats is a collapsible group with a chevron, remembered per browser. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01BkfrkQwA4YGrGw9LZSpeAq
This commit is contained in:
parent
750efc4106
commit
9d6bfed3b5
4 changed files with 77 additions and 6 deletions
|
|
@ -21,11 +21,12 @@
|
|||
</div>
|
||||
<button id="btn-assistant-goback-rail" class="btn-sm btn-ghost assistant-goback-rail" type="button" title="Back to the main menu"><i class="fas fa-arrow-left"></i> Go back</button>
|
||||
<button id="btn-assistant-create-image" class="assistant-create-image" type="button"><i class="fas fa-wand-magic-sparkles"></i> Create image</button>
|
||||
<button id="btn-assistant-clear" class="assistant-new-chat" type="button"><i class="fas fa-plus"></i> New chat</button>
|
||||
<button id="btn-assistant-clear" class="assistant-new-chat" type="button"><i class="fas fa-plus"></i> <span>New chat</span><kbd class="assistant-kbd">Ctrl+Shift+O</kbd></button>
|
||||
<div class="card">
|
||||
<div class="card-header">
|
||||
<button type="button" class="card-header assistant-chats-header" id="btn-assistant-chats-toggle" aria-expanded="true" aria-controls="assistant-saved-chats">
|
||||
<h3><i class="fas fa-bookmark"></i> Saved Chats</h3>
|
||||
</div>
|
||||
<i class="fas fa-chevron-down assistant-chats-caret"></i>
|
||||
</button>
|
||||
<div id="assistant-saved-chats" class="assistant-saved-chats">
|
||||
<p class="assistant-muted">Chats save automatically as you go.</p>
|
||||
</div>
|
||||
|
|
@ -60,9 +61,8 @@
|
|||
|
||||
<div id="assistant-messages" class="assistant-messages" aria-live="polite">
|
||||
<div class="assistant-empty">
|
||||
<i class="fas fa-book-medical"></i>
|
||||
<h3>Evidence-first pediatric assistant</h3>
|
||||
<p>Ask a real clinical question. Citations stay linked to the source cards on the right.</p>
|
||||
<h3>Pediatric Clinical Assistant</h3>
|
||||
<p class="assistant-empty-badge"><i class="fas fa-book-medical"></i> Answers built only from your indexed library</p>
|
||||
<div class="assistant-examples">
|
||||
<button type="button" data-assistant-example="In a 4-year-old with acute wheeze, when should magnesium sulfate be considered and what dose is recommended?">Status asthma escalation</button>
|
||||
<button type="button" data-assistant-example="What are the red flags for bilious vomiting in neonates, and what immediate workup is recommended?">Bilious vomiting</button>
|
||||
|
|
|
|||
|
|
@ -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; }
|
||||
|
|
|
|||
|
|
@ -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');
|
||||
|
|
|
|||
|
|
@ -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 class="fas fa-book-medical"><\/i>\s*<h3>/, '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/);
|
||||
});
|
||||
|
|
|
|||
Loading…
Reference in a new issue