From dd25e0edb798be3a628682ed2025414188a6fa28 Mon Sep 17 00:00:00 2001 From: Daniel Date: Thu, 10 Sep 2026 02:39:42 +0200 Subject: [PATCH] feat: one Assistant/Workspace switch and one menu toggle, shared by both screens The app IS workspace mode, so the switch belongs in both places and the only difference between the two screens is which pill is highlighted: - The app sidebar's "AI Assistant" nav entry is replaced by the switch, with Workspace highlighted. The assistant rail carries the same control with Assistant highlighted. Clicking Assistant from the app navigates to /assistant; inside the assistant, Workspace opens the launcher in place. - Accent corrected: I had it backwards last round and turned the assistant purple. The app's accent is blue (.tab-btn.active in styles.css); the assistant now uses it throughout, so neither screen looks like a different product. Citation chips keep purple so evidence stays distinct from interactive blue. - One menu toggle labelled "Hide menu", shared by the app sidebar and the assistant rail. It replaces three separate controls: a pin button, a floating expand button, and the assistant's own rail toggle. The toggle stays reachable when the rail it collapses is hidden, or there would be no way back. - Workspace cards list clinical work only. Settings, FAQ, Admin, Docs and Content Manager stay in the app sidebar under Account, where they belong. - Model availability is discovery-only again: a hand-typed id cannot be verified at that point, so the manual entry field is gone. The in-page mode state machine is removed along with the whole class of hidden-versus-display bugs it carried. Co-Authored-By: Claude Opus 5 Claude-Session: https://claude.ai/code/session_01WjVc5oaAaYFNbZGLeJp6TX --- public/components/admin.html | 9 - public/components/assistant.html | 14 +- public/css/assistant.css | 143 +++++++------- public/css/styles.css | 4 - public/index.html | 20 +- public/js/admin/clinicalAssistant.js | 32 ---- public/js/app.js | 67 ++++--- public/js/clinicalAssistant.js | 74 +++----- test/admin-clinical-assistant-wiring.test.js | 41 +--- test/assistant-workspace-layout.test.js | 185 ++++++++----------- 10 files changed, 233 insertions(+), 356 deletions(-) diff --git a/public/components/admin.html b/public/components/admin.html index e8356bf6..68c3dae8 100644 --- a/public/components/admin.html +++ b/public/components/admin.html @@ -299,20 +299,11 @@
Chat models
-
- - -
Image models
-
- - -
-

Discovery only lists what the gateway advertises. Add any id your gateway routes — it is checked on first use, and stays in the list once saved.

diff --git a/public/components/assistant.html b/public/components/assistant.html index 87bbcdcf..1c6f9c48 100644 --- a/public/components/assistant.html +++ b/public/components/assistant.html @@ -1,5 +1,4 @@
-

AI Clinical Assistant

@@ -14,19 +13,15 @@
+ diff --git a/public/css/assistant.css b/public/css/assistant.css index e8365c0d..f86cbdd0 100644 --- a/public/css/assistant.css +++ b/public/css/assistant.css @@ -15,32 +15,32 @@ body.assistant-workspace .assistant-topbar-title h2 { position:absolute; width:1 .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-new-chat { display:flex; align-items:center; justify-content:center; gap:8px; width:100%; border:1px solid var(--purple-light); background:#faf5ff; color:var(--purple); border-radius:10px; padding:10px 12px; font-size:13px; font-weight:700; cursor:pointer; } -.assistant-new-chat:hover { border-color:var(--purple); background:var(--purple-light); } +.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; } #assistant-chat-view { display:grid; grid-template-rows:auto minmax(420px,1fr) auto; min-height:calc(100vh - 200px); min-width:0; } .assistant-toolbar { display:flex; justify-content:space-between; align-items:center; gap:10px; padding:10px 14px; border-bottom:1px solid var(--g200); background:var(--g50); } .assistant-toolbar-actions { display:flex; gap:6px; flex-wrap:wrap; } .assistant-messages { padding:16px; max-height:65vh; overflow-y:auto; overflow-x:hidden; background:linear-gradient(180deg,#fff,var(--g50)); min-width:0; } .assistant-empty { max-width:680px; margin:50px auto; text-align:center; color:var(--g500); } -.assistant-empty i { font-size:34px; color:var(--purple); margin-bottom:10px; } +.assistant-empty i { font-size:34px; color:var(--blue); margin-bottom:10px; } .assistant-empty h3 { color:var(--g800); font-size:18px; margin-bottom:6px; } .assistant-examples { display:flex; gap:8px; flex-wrap:wrap; justify-content:center; margin-top:16px; } .assistant-examples button { border:1px solid var(--g200); background:white; color:var(--g700); border-radius:999px; padding:7px 10px; font-size:12px; cursor:pointer; } .assistant-suggestion-buttons { display:flex; gap:8px; flex-wrap:wrap; margin-top:12px; } -.assistant-suggestion-buttons button { border:1px solid var(--purple-light); background:#faf5ff; color:var(--purple); border-radius:999px; padding:7px 10px; font-size:12px; cursor:pointer; text-align:left; } -.assistant-suggestion-buttons button:hover { border-color:var(--purple); background:var(--purple-light); } +.assistant-suggestion-buttons button { border:1px solid var(--blue-light); background:var(--blue-light); color:var(--blue); border-radius:999px; padding:7px 10px; font-size:12px; cursor:pointer; text-align:left; } +.assistant-suggestion-buttons button:hover { border-color:var(--blue); background:var(--blue-light); } .assistant-msg { max-width:900px; min-width:0; margin:0 0 14px; display:grid; gap:6px; position:relative; } .assistant-msg.user { margin-left:auto; max-width:760px; } .assistant-msg-label { font-size:11px; font-weight:700; color:var(--g400); text-transform:uppercase; letter-spacing:.04em; } .assistant-msg-actions { display:flex; gap:4px; opacity:0; transition:opacity .12s ease; } .assistant-msg:hover .assistant-msg-actions, .assistant-msg-actions:focus-within { opacity:1; } .assistant-msg-actions button { border:1px solid var(--g200); background:white; color:var(--g500); border-radius:7px; padding:3px 7px; font-size:11px; cursor:pointer; } -.assistant-msg-actions button:hover { color:var(--purple); border-color:var(--purple-light); } +.assistant-msg-actions button:hover { color:var(--blue); border-color:var(--blue-light); } .assistant-translate-pop { display:grid; gap:4px; border:1px solid var(--g200); background:white; border-radius:10px; padding:8px; box-shadow:var(--shadow); position:absolute; right:0; top:100%; z-index:6; min-width:200px; } .assistant-translate-providers { display:flex; gap:4px; border-bottom:1px solid var(--g100); padding-bottom:6px; } .assistant-translate-providers button { flex:1; border:1px solid var(--g200); background:var(--g50); color:var(--g600); border-radius:7px; padding:4px; font-size:11px; font-weight:700; cursor:pointer; } -.assistant-translate-providers button.active { background:var(--purple); color:white; border-color:var(--purple); } +.assistant-translate-providers button.active { background:var(--blue); color:white; border-color:var(--blue); } .assistant-translate-pop [data-assistant-translate-lang] { border:0; background:transparent; text-align:left; padding:4px 6px; font-size:12px; color:var(--g700); border-radius:6px; cursor:pointer; } .assistant-translate-pop [data-assistant-translate-lang]:hover { background:var(--g50); } .assistant-translated-sources { margin-top:10px; padding-top:8px; border-top:1px solid var(--g200); font-size:12px; color:var(--g500); } @@ -50,7 +50,7 @@ body.assistant-workspace .assistant-topbar-title h2 { position:absolute; width:1 .assistant-message-attachment img { width:64px; height:64px; object-fit:cover; border-radius:8px; border:1px solid var(--g200); background:var(--g50); display:block; } .assistant-message-attachment figcaption { font-size:10px; color:var(--g500); max-width:72px; overflow:hidden; text-overflow:ellipsis; white-space:nowrap; } .assistant-bubble { border:none; border-radius:0; padding:2px 4px; background:transparent; box-shadow:none; font-size:13px; line-height:1.75; overflow-wrap:anywhere; min-width:0; max-width:100%; } -.assistant-msg.user .assistant-bubble { white-space:pre-wrap; background:var(--purple); color:white; border-radius:16px; padding:10px 14px; } +.assistant-msg.user .assistant-bubble { white-space:pre-wrap; background:var(--blue); color:white; border-radius:16px; padding:10px 14px; } .assistant-bubble h1, .assistant-bubble h2, .assistant-bubble h3 { margin:16px 0 8px; line-height:1.25; color:var(--g900); } .assistant-bubble h1:first-child, .assistant-bubble h2:first-child, .assistant-bubble h3:first-child { margin-top:0; } .assistant-bubble h1 { font-size:20px; } @@ -60,7 +60,7 @@ body.assistant-workspace .assistant-topbar-title h2 { position:absolute; width:1 .assistant-bubble p:last-child { margin-bottom:0; } .assistant-bubble ul, .assistant-bubble ol { padding-left:20px; margin:8px 0; } .assistant-bubble li { margin:4px 0; } -.assistant-bubble blockquote { margin:10px 0; padding:8px 12px; border-left:3px solid var(--purple); background:var(--purple-light); color:var(--g700); border-radius:8px; } +.assistant-bubble blockquote { margin:10px 0; padding:8px 12px; border-left:3px solid var(--blue); background:var(--blue-light); color:var(--g700); border-radius:8px; } .assistant-bubble hr { border:0; border-top:1px solid var(--g200); margin:18px 0; } .assistant-bubble img { max-width:100%; border-radius:8px; margin:10px 0; } .assistant-msg.user .assistant-bubble code { background:rgba(255,255,255,.25); color:#fff; } @@ -81,7 +81,7 @@ body.assistant-workspace .assistant-topbar-title h2 { position:absolute; width:1 .assistant-table-actions { position:absolute; top:6px; right:8px; z-index:2; display:flex; gap:4px; opacity:0; transition:opacity .12s ease; } .assistant-table-scroll:hover .assistant-table-actions, .assistant-table-scroll:focus-within .assistant-table-actions { opacity:1; } .assistant-table-actions button { border:1px solid var(--g200); background:white; color:var(--g600); border-radius:6px; padding:2px 7px; font-size:10px; font-weight:700; cursor:pointer; } -.assistant-table-actions button:hover { color:var(--purple); border-color:var(--purple-light); } +.assistant-table-actions button:hover { color:var(--blue); border-color:var(--blue-light); } .assistant-codeblock { position:relative; margin:10px 0; } .assistant-codeblock pre { margin:0; } .assistant-code-copy { position:absolute; top:6px; right:8px; border:1px solid rgba(255,255,255,.25); background:rgba(17,24,39,.7); color:#fff; border-radius:6px; padding:2px 8px; font-size:10px; font-weight:700; cursor:pointer; } @@ -92,7 +92,7 @@ body.assistant-workspace .assistant-topbar-title h2 { position:absolute; width:1 .assistant-thinking { background:linear-gradient(90deg,#fff,#f8fafc,#fff); background-size:220% 100%; animation:assistantShimmer 1.8s ease-in-out infinite; } .assistant-thinking-line { display:flex; align-items:center; gap:6px; color:var(--g800); } .assistant-thinking-detail { color:var(--g500); font-size:12px; margin-top:3px; } -.assistant-thinking-dot { width:7px; height:7px; border-radius:50%; background:var(--purple); display:inline-block; animation:assistantBounce 1.2s infinite ease-in-out; } +.assistant-thinking-dot { width:7px; height:7px; border-radius:50%; background:var(--blue); display:inline-block; animation:assistantBounce 1.2s infinite ease-in-out; } .assistant-thinking-dot:nth-child(2) { animation-delay:.15s; } .assistant-thinking-dot:nth-child(3) { animation-delay:.3s; margin-right:3px; } @keyframes assistantBounce { 0%,80%,100% { transform:scale(.65); opacity:.45; } 40% { transform:scale(1); opacity:1; } } @@ -107,9 +107,9 @@ body.assistant-workspace .assistant-topbar-title h2 { position:absolute; width:1 .assistant-attachment-remove { position:absolute; top:3px; right:3px; width:18px; height:18px; border:none; border-radius:50%; background:rgba(0,0,0,.65); color:#fff; font-size:10px; line-height:18px; cursor:pointer; padding:0; } .assistant-attachment-remove:hover { background:rgba(0,0,0,.85); } .assistant-attach { display:inline-flex; align-items:center; gap:6px; font-size:12px; color:var(--g600); cursor:pointer; padding:6px 10px; border:1px dashed var(--g300); border-radius:8px; } -.assistant-attach:hover { border-color:var(--purple); color:var(--purple); } +.assistant-attach:hover { border-color:var(--blue); color:var(--blue); } .assistant-composer textarea, .assistant-side textarea { width:100%; border:1.5px solid var(--g300); border-radius:10px; padding:10px 12px; resize:vertical; font-family:inherit; font-size:13px; outline:none; } -.assistant-composer textarea:focus, .assistant-side textarea:focus { border-color:var(--purple); box-shadow:0 0 0 3px var(--purple-light); } +.assistant-composer textarea:focus, .assistant-side textarea:focus { border-color:var(--blue); box-shadow:0 0 0 3px var(--blue-light); } .assistant-composer-footer { display:flex; justify-content:space-between; align-items:center; gap:10px; } .assistant-composer-footer .btn-generate { width:auto; margin:0; padding:9px 18px; } .assistant-composer-footer #btn-assistant-cancel[hidden] { display:none !important; } @@ -121,7 +121,7 @@ body.assistant-workspace .assistant-topbar-title h2 { position:absolute; width:1 .assistant-generated-image { display:grid; gap:8px; justify-items:start; } /* Thumbnail in the transcript; Preview (or clicking it) opens full resolution. */ .assistant-generated-image img { width:auto; max-width:min(100%,320px); max-height:240px; object-fit:contain; border-radius:10px; border:1px solid var(--g200); background:white; cursor:zoom-in; } -.assistant-generated-image img:focus-visible { outline:2px solid var(--purple); outline-offset:2px; } +.assistant-generated-image img:focus-visible { outline:2px solid var(--blue); outline-offset:2px; } @media (max-width:960px) { .assistant-generated-image img { max-width:100%; max-height:200px; } } .assistant-image-actions { display:flex; gap:8px; flex-wrap:wrap; } .assistant-image-preview-open { overflow:hidden; } @@ -137,9 +137,9 @@ body.assistant-workspace .assistant-topbar-title h2 { position:absolute; width:1 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-empty-badge i { color:var(--blue); 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); } +.assistant-examples button:hover { border-color:var(--blue-light); background:var(--blue-light); color:var(--blue); } /* 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; } @@ -158,34 +158,13 @@ body.assistant-workspace .assistant-topbar-title h2 { position:absolute; width:1 .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; } .assistant-mode-switch button i { font-size:11px; } -.assistant-mode-switch button.active { background:white; color:var(--purple); box-shadow:var(--shadow); } +.assistant-mode-switch button.active { background:white; color:var(--blue); box-shadow:var(--shadow); } -/* Workspace view: same ground, same card language and same spacing as the empty - state, so switching modes reads as the same app rather than a separate page. */ -.assistant-workspace-view { padding:24px 18px; overflow-y:auto; min-height:0; 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-workspace-cards { display:grid; grid-template-columns:repeat(auto-fill,minmax(190px,1fr)); gap:10px; max-width:860px; margin:0 auto; } -.assistant-workspace-card { display:flex; align-items:center; gap:10px; border:1px solid var(--g200); background:white; border-radius:10px; padding:12px 14px; font-size:12.5px; color:var(--g700); text-align:left; cursor:pointer; box-shadow:var(--shadow); } -.assistant-workspace-card:hover { border-color:var(--purple-light); background:#faf5ff; color:var(--purple); } -.assistant-workspace-card i { width:16px; text-align:center; color:var(--g400); font-size:13px; } -.assistant-workspace-card:hover i { color:var(--purple); } /* On a phone the rail IS the drawer menu; a second copy as cards is redundant and pushes the real list off the screen. */ - -/* Workspace section in the rail: the app's own menu, one level down, the way - Claude's sidebar carries both its chats and its top-level navigation. */ -.assistant-rail-workspace { display:flex; flex-direction:column; gap:1px; margin-top:8px; padding-top:8px; border-top:1px solid var(--g100); } -.assistant-rail-section { display:flex; align-items:center; gap:8px; width:100%; border:none; background:none; cursor:pointer; padding:6px 10px; border-radius:8px; font-size:11px; font-weight:700; letter-spacing:.04em; text-transform:uppercase; color:var(--g400); text-align:left; } -.assistant-rail-section:hover { background:var(--g100); color:var(--g600); } -.assistant-rail-caret { transition:transform .15s ease; font-size:9px; } -.assistant-rail-section.is-open .assistant-rail-caret { transform:rotate(90deg); } -.assistant-rail-links { display:flex; flex-direction:column; gap:1px; } -.assistant-rail-link { display:flex; align-items:center; gap:10px; width:100%; border:none; background:none; cursor:pointer; padding:8px 10px; border-radius:8px; font-size:13px; color:var(--g700); text-align:left; } -.assistant-rail-link:hover { background:var(--g100); color:var(--g900); } -.assistant-rail-link i { width:16px; text-align:center; color:var(--g400); font-size:12px; } -.assistant-rail-link:hover i { color:var(--purple); } .assistant-saved-chat-group { display:block; } .assistant-saved-chat-group + .assistant-saved-chat-group { margin-top:10px; } .assistant-saved-chat-heading { margin:0 0 2px; padding:0 10px; font-size:11px; font-weight:700; letter-spacing:.04em; text-transform:uppercase; color:var(--g400); } @@ -196,11 +175,11 @@ body.assistant-workspace .assistant-topbar-title h2 { position:absolute; width:1 .assistant-saved-chat-meta { font-size:11px; color:var(--g500); display:block; white-space:nowrap; overflow:hidden; text-overflow:ellipsis; } .assistant-save-actions { display:flex; gap:6px; flex-wrap:wrap; } .assistant-source { border:1px solid var(--g200); border-radius:10px; padding:10px 12px; background:white; box-shadow:var(--shadow); font-size:12px; line-height:1.5; transition:border-color .12s ease; } -.assistant-source:hover { border-color:var(--purple-light); } -.assistant-source:target { border-color:var(--purple); box-shadow:0 0 0 3px var(--purple-light); } +.assistant-source:hover { border-color:var(--blue-light); } +.assistant-source:target { border-color:var(--blue); box-shadow:0 0 0 3px var(--blue-light); } .assistant-source strong { color:var(--g800); } .assistant-source-badges { display:flex; gap:5px; flex-wrap:wrap; margin-top:6px; } -.assistant-source-badges span { border:1px solid var(--purple-light); border-radius:999px; background:#faf5ff; color:var(--purple); padding:2px 7px; font-size:10px; font-weight:700; text-transform:uppercase; letter-spacing:.03em; } +.assistant-source-badges span { border:1px solid var(--blue-light); border-radius:999px; background:var(--blue-light); color:var(--blue); padding:2px 7px; font-size:10px; font-weight:700; text-transform:uppercase; letter-spacing:.03em; } .assistant-source-meta { color:var(--g500); font-size:11px; margin-top:3px; } .assistant-source-preview { margin-top:8px; } .assistant-source-preview button { border:0; padding:0; background:transparent; cursor:pointer; width:100%; display:block; } @@ -228,7 +207,6 @@ body.assistant-workspace .assistant-topbar-title h2 { position:absolute; width:1 #btn-assistant-goback { padding:8px; } .assistant-topbar-title h2 { font-size:15px; } .assistant-status { margin-left:auto; font-size:11px; } - .assistant-toggle-history { display:none; } /* The assistant fills the viewport exactly; nothing behind can scroll */ .assistant-layout { display:flex; flex-direction:column; height:100dvh; min-height:0; overflow:hidden; } .assistant-main { flex:1 1 auto; min-height:0; display:flex; } @@ -262,8 +240,8 @@ body.assistant-workspace .assistant-topbar-title h2 { position:absolute; width:1 .assistant-layout .assistant-saved-chat-menu { position:absolute; right:6px; top:50%; transform:translateY(-50%); opacity:1; pointer-events:auto; } .assistant-new-chat { display:flex; align-items:center; gap:10px; width:100%; border:none; background:none; color:var(--g700); font-size:14px; padding:10px 12px; border-radius:8px; text-align:left; } .assistant-new-chat:hover { background:var(--g100); } - .assistant-create-image { flex:0 0 auto; width:40px; padding:0; justify-content:center; background:#faf5ff; color:var(--purple); border-radius:8px; } - .assistant-create-image:hover { background:var(--purple-light,#faf5ff); } + .assistant-create-image { flex:0 0 auto; width:40px; padding:0; justify-content:center; background:var(--blue-light); color:var(--blue); border-radius:8px; } + .assistant-create-image:hover { background:var(--blue-light); } .assistant-view-switch { margin:6px 8px; } .assistant-empty { margin:28px auto; max-width:100%; } .assistant-empty i { font-size:26px; } @@ -287,7 +265,6 @@ body.assistant-workspace .assistant-topbar-title h2 { position:absolute; width:1 .assistant-mic i { font-size:18px; } } - /* Patient take home modal + actions */ .assistant-takehome-modal { position: fixed; inset: 0; background: rgba(15, 23, 42, .45); display: flex; align-items: center; justify-content: center; z-index: 120; } .assistant-takehome-modal .modal-content { width: min(640px, 92vw); max-height: 82vh; display: flex; flex-direction: column; } @@ -332,9 +309,6 @@ body.assistant-workspace .assistant-layout { height: calc(100vh - 64px); min-hei body.assistant-workspace .assistant-learning-view { height:100dvh; } } /* Desktop: the saved-chats rail collapses like ChatGPT's sidebar */ -.assistant-toggle-history { border:none; background:none; color:var(--g600); padding:6px 8px; border-radius:8px; cursor:pointer; line-height:1; } -.assistant-toggle-history:hover { background:var(--g100); color:var(--g800); } -.assistant-toggle-history.is-collapsed { color:var(--purple); background:var(--purple-light); } .assistant-layout { transition:grid-template-columns .18s ease; } /* Open WebUI-style rail: the column collapses to zero and the chat takes the space, animated so the change reads as a fold rather than a jump. */ @@ -362,12 +336,11 @@ body.assistant-workspace #assistant-chat-view { min-height: 0; } .assistant-saved-chat-delete:hover { color:var(--danger, #dc2626); background:rgba(220,38,38,.08); } /* Create image entry + Images gallery (ChatGPT-style left rail) */ -/* New chat is the row; Create image rides beside it as a square icon rather than a second full-width banner competing with it. */ .assistant-rail-actions { display:flex; align-items:stretch; gap:6px; } .assistant-rail-actions .assistant-new-chat { flex:1 1 auto; min-width:0; } -.assistant-create-image { flex:0 0 auto; display:inline-flex; align-items:center; justify-content:center; width:36px; border:1px solid var(--purple-light); border-radius:10px; padding:0; background:#faf5ff; color:var(--purple); font-size:13px; cursor:pointer; } -.assistant-create-image:hover { background:var(--purple-light); border-color:var(--purple); } +.assistant-create-image { flex:0 0 auto; display:inline-flex; align-items:center; justify-content:center; width:36px; border:1px solid var(--blue-light); border-radius:10px; padding:0; background:var(--blue-light); color:var(--blue); font-size:13px; cursor:pointer; } +.assistant-create-image:hover { border-color:var(--blue); } .assistant-gallery-card { flex:0 1 auto; } .assistant-image-gallery { display:grid; grid-template-columns:repeat(auto-fill,minmax(56px,1fr)); gap:6px; padding:10px 12px; max-height:220px; overflow-y:auto; } .assistant-gallery-item { border:none; background:none; padding:0; border-radius:8px; overflow:hidden; cursor:pointer; border:1px solid var(--g200); } @@ -390,7 +363,7 @@ body.assistant-workspace #assistant-chat-view { min-height: 0; } .assistant-check { display:flex; align-items:center; justify-content:center; width:38px; height:38px; border-radius:50%; cursor:pointer; color:var(--g500); position:relative; } .assistant-check i { font-size:13px; } .assistant-check input { position:absolute; inset:0; opacity:0; cursor:pointer; } -.assistant-check:has(input:checked) { color:var(--purple); background:var(--purple-light,#faf5ff); } +.assistant-check:has(input:checked) { color:var(--blue); background:var(--blue-light); } .btn-send { width:40px; height:40px; border:none; border-radius:50%; background:var(--accent,#0f766e); color:#fff; display:flex; align-items:center; justify-content:center; cursor:pointer; font-size:16px; } .btn-send:disabled { opacity:.5; cursor:not-allowed; } .assistant-composer .assistant-muted { flex-basis:100%; } @@ -413,7 +386,7 @@ body.assistant-workspace #assistant-chat-view { min-height: 0; } .assistant-chat-menu button:hover { background:var(--g100); } .assistant-chat-menu button[data-chat-action="delete"] { color:var(--danger,#dc2626); } .assistant-chat-menu button i { width:14px; text-align:center; } -.assistant-saved-chat-rename { width:100%; border:1.5px solid var(--purple); border-radius:6px; padding:4px 6px; font-size:12px; } +.assistant-saved-chat-rename { width:100%; border:1.5px solid var(--blue); border-radius:6px; padding:4px 6px; font-size:12px; } /* Create-image popup: generation progress + full history grid */ .create-image-progress { margin-top:10px; padding:8px 10px; border-radius:8px; background:var(--g50,#f9fafb); color:var(--g600); font-size:13px; } @@ -464,33 +437,53 @@ body.assistant-workspace #assistant-chat-view { min-height: 0; } .assistant-history .card-header { padding:2px 4px; } .assistant-history .card-header h3 { font-size:11px; font-weight:700; color:var(--g500); text-transform:uppercase; letter-spacing:.04em; } +/* ── Assistant / Workspace switch ──────────────────────────────────────────── + Rendered identically in the app sidebar and the assistant rail, so moving + between them changes which pill is highlighted and nothing else. */ +.assistant-mode-switch { display:grid; grid-template-columns:1fr 1fr; gap:4px; margin:8px 12px 10px; padding:4px; background:var(--g100); border:1px solid var(--g200); border-radius:10px; } +.assistant-mode-switch button { display:flex; align-items:center; justify-content:center; gap:6px; border:0; border-radius:7px; padding:7px 6px; font-family:inherit; font-size:12px; font-weight:600; color:var(--g600); background:transparent; cursor:pointer; } +.assistant-mode-switch button i { font-size:11px; } +.assistant-mode-switch button:hover { color:var(--g900); } +.assistant-mode-switch button.active { background:white; color:var(--blue); box-shadow:var(--shadow); } +/* Inside the assistant rail the gutters already exist, so it only needs the gap. */ +.assistant-history .assistant-mode-switch { margin:0 0 8px; } -/* ── The two modes, in one place ───────────────────────────────────────────── - Everything below is driven by a single body class. These elements all carry - their own `display:` rules, so toggling the [hidden] attribute from JS was - silently overridden — which is why the chat list never came back. Owning both - modes here keeps them one interface rather than two that half-hide each other. */ +/* The one menu toggle, identical in the app sidebar and the assistant rail. */ +.assistant-menu-toggle { display:flex; align-items:center; justify-content:center; width:32px; height:32px; margin:10px 12px 0; border:1px solid var(--g200); border-radius:8px; background:white; color:var(--g500); font-size:12px; cursor:pointer; } +.assistant-menu-toggle:hover { color:var(--blue); border-color:var(--blue-light); background:var(--blue-light); } +.assistant-history .assistant-menu-toggle { margin:0 0 8px; } -/* Assistant mode: the workspace surfaces are simply absent. */ -.assistant-workspace-view, -.assistant-rail-workspace { display:none; } +/* Hidden menu: the rail collapses in both places, and the layout takes the + space back rather than leaving a gutter. */ +body.menu-hidden .sidebar { width:0; border:none; overflow:hidden; } +body.assistant-workspace.menu-hidden .assistant-layout { grid-template-columns:0 minmax(0,1fr) 330px; } +body.assistant-workspace.menu-hidden.assistant-no-citations .assistant-layout { grid-template-columns:0 minmax(0,1fr); } +body.assistant-workspace.menu-hidden .assistant-history > *:not(.assistant-menu-toggle) { display:none; } +body.assistant-workspace.menu-hidden .assistant-history { width:0; min-width:0; overflow:visible; padding:0; } +/* The toggle must survive its own collapse, or there is no way back. */ +body.assistant-workspace.menu-hidden .assistant-history .assistant-menu-toggle { position:absolute; left:8px; top:8px; z-index:5; } +@media (max-width:640px) { .assistant-menu-toggle { display:none; } body.menu-hidden .sidebar { width:auto; } } -/* Workspace mode: the chat, its list and its sources all step aside together. */ -body.assistant-mode-workspace #assistant-chat-view, -body.assistant-mode-workspace .assistant-history .card, -body.assistant-mode-workspace .assistant-create-image, -body.assistant-mode-workspace .assistant-new-chat, -body.assistant-mode-workspace .assistant-side { display:none; } +/* Workspace launcher: same ground, card language and spacing as the empty state, + so the two views are one interface. Desktop only. */ +.assistant-workspace-view { flex:1 1 auto; min-height:0; padding:24px 18px; overflow-y:auto; + 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-workspace-cards { display:grid; grid-template-columns:repeat(auto-fill,minmax(190px,1fr)); gap:10px; max-width:860px; margin:0 auto; } +.assistant-workspace-card { display:flex; align-items:center; gap:10px; border:1px solid var(--g200); background:white; border-radius:10px; padding:12px 14px; font-size:12.5px; color:var(--g700); text-align:left; cursor:pointer; box-shadow:var(--shadow); } +.assistant-workspace-card:hover { border-color:var(--blue-light); background:var(--blue-light); color:var(--blue); } +.assistant-workspace-card i { width:16px; text-align:center; color:var(--g400); font-size:13px; } +.assistant-workspace-card:hover i { color:var(--blue); } + +/* Both views occupy the same box, so switching never moves the page. */ +.assistant-workspace-view { display:none; } body.assistant-mode-workspace .assistant-workspace-view { display:block; } -body.assistant-mode-workspace .assistant-rail-workspace { display:flex; } - -/* The sources track goes with the panel; hiding one and keeping the other left - an empty 330px gutter. */ +body.assistant-mode-workspace #assistant-chat-view { display:none; } +body.assistant-mode-workspace .assistant-history .card, +body.assistant-mode-workspace .assistant-rail-actions, +body.assistant-mode-workspace .assistant-side { display:none; } body.assistant-mode-workspace .assistant-layout { grid-template-columns:260px minmax(0,1fr); } -body.assistant-mode-workspace .assistant-layout.history-collapsed { grid-template-columns:0 minmax(0,1fr); } - -/* Phones: the drawer already is the menu, so the card grid is redundant there - and the rail list carries workspace on its own. */ +body.assistant-mode-workspace.menu-hidden .assistant-layout { grid-template-columns:0 minmax(0,1fr); } @media (max-width:640px) { body.assistant-mode-workspace .assistant-workspace-view { display:none; } body.assistant-mode-workspace #assistant-chat-view { display:flex; } diff --git a/public/css/styles.css b/public/css/styles.css index edbd2db8..1bb90e6f 100644 --- a/public/css/styles.css +++ b/public/css/styles.css @@ -80,10 +80,6 @@ body{font-family:'Inter',system-ui,sans-serif;background:var(--g50);color:var(-- .sidebar-overlay{display:none;position:fixed;inset:0;background:rgba(0,0,0,0.45);z-index:199;} .btn-menu-toggle{display:none;} /* Sidebar collapse/expand buttons (desktop only) */ -.sidebar-pin-btn{display:flex;align-items:center;justify-content:center;padding:10px;border:none;background:none;color:var(--g400);cursor:pointer;font-size:13px;border-top:1px solid var(--g200);transition:color 0.15s;flex-shrink:0;} -.sidebar-pin-btn:hover{color:var(--blue);} -.sidebar-expand-btn{position:fixed;left:0;top:50%;transform:translateY(-50%);z-index:101;background:white;border:1.5px solid var(--g200);border-left:none;border-radius:0 8px 8px 0;padding:10px 6px;color:var(--g500);cursor:pointer;font-size:13px;box-shadow:2px 0 8px rgba(0,0,0,0.06);transition:color 0.15s;} -.sidebar-expand-btn:hover{color:var(--blue);} /* MAIN */ .main-content{flex:1;min-width:0;padding:20px 20px;} diff --git a/public/index.html b/public/index.html index eab1f51a..7efa1bad 100644 --- a/public/index.html +++ b/public/index.html @@ -197,10 +197,14 @@
- - - - diff --git a/public/js/admin/clinicalAssistant.js b/public/js/admin/clinicalAssistant.js index 33deafa1..59070dbd 100644 --- a/public/js/admin/clinicalAssistant.js +++ b/public/js/admin/clinicalAssistant.js @@ -51,38 +51,6 @@ export function initClinicalAssistantAdmin(adminEscapeHtml) { container.appendChild(row); }); } - // Discovery only returns what the gateway advertises, so an admin could never - // offer a model it does not list. Typed ids join the same checkbox list and - // persist through the normal allowed_models save. - function addAssistantModel(containerId, id) { - var container = document.getElementById(containerId); - id = String(id || '').trim(); - if (!container || !id) return false; - var existing = Array.prototype.map.call(container.querySelectorAll('input[type="checkbox"]'), function(b) { return b.value; }); - if (existing.indexOf(id) !== -1) { - var already = container.querySelector('input[value="' + (window.CSS && CSS.escape ? CSS.escape(id) : id) + '"]'); - if (already) already.checked = true; - return true; - } - var roster = containerId === 'assistant-allowed-chat-models' ? chatRoster : imageRoster; - roster.push(id); - renderAssistantCheckboxList(containerId, roster, existing.filter(function(v) { - var box = container.querySelector('input[value="' + (window.CSS && CSS.escape ? CSS.escape(v) : v) + '"]'); - return box && box.checked; - }).concat([id])); - return true; - } - - document.addEventListener('click', function(event) { - var button = event.target.closest && event.target.closest('[data-assistant-add-model]'); - if (!button) return; - var containerId = button.getAttribute('data-assistant-add-model'); - var field = document.getElementById(containerId === 'assistant-allowed-chat-models' ? 'assistant-add-chat-model' : 'assistant-add-image-model'); - if (!field) return; - if (addAssistantModel(containerId, field.value)) field.value = ''; - else if (typeof window.showToast === 'function') window.showToast('Enter a model id first', 'error'); - }); - function checkedAssistantModels(containerId) { var container = document.getElementById(containerId); if (!container) return []; diff --git a/public/js/app.js b/public/js/app.js index d9e80f77..7e442e53 100644 --- a/public/js/app.js +++ b/public/js/app.js @@ -158,6 +158,27 @@ document.addEventListener('DOMContentLoaded', function() { activateTab(fallback && fallback !== 'assistant' ? fallback : 'encounter'); }); + // Assistant / Workspace switch. The app IS workspace mode, so switching is + // navigation between two pages rather than an in-page state machine — which + // is also what keeps the two looking identical: the only difference is which + // pill is highlighted. A reload is fine; each is its own document. + document.addEventListener('click', function(event) { + var pill = event.target.closest && event.target.closest('[data-assistant-mode]'); + if (!pill) return; + var wantsAssistant = pill.getAttribute('data-assistant-mode') === 'assistant'; + var onAssistant = window.location.pathname === '/assistant'; + if (!onAssistant) { + // In the app you ARE the workspace, so only the Assistant pill navigates. + if (wantsAssistant) window.location.href = '/assistant'; + return; + } + // Inside the assistant, Workspace opens the launcher in place rather than + // reloading into whichever tab happened to be last. + if (typeof window.assistantShowWorkspaceLauncher === 'function') { + window.assistantShowWorkspaceLauncher(!wantsAssistant); + } + }); + // Expose activateTab globally so auth.js can call it after login window.activateTab = activateTab; @@ -189,33 +210,31 @@ document.addEventListener('DOMContentLoaded', function() { } // --- DESKTOP SIDEBAR COLLAPSE --- - var sidebarPinBtn = document.getElementById('btn-sidebar-pin'); - var sidebarExpandBtn = document.getElementById('btn-sidebar-expand'); - // Restore collapse state - try { - if (localStorage.getItem('ped_sidebar_collapsed') === '1') { - if (sidebar) sidebar.classList.add('collapsed'); - if (sidebarExpandBtn) sidebarExpandBtn.style.display = 'flex'; - if (sidebarPinBtn) sidebarPinBtn.style.display = 'none'; - } - } catch(e) {} - if (sidebarPinBtn) { - sidebarPinBtn.addEventListener('click', function() { - if (sidebar) sidebar.classList.add('collapsed'); - if (sidebarExpandBtn) sidebarExpandBtn.style.display = 'flex'; - if (sidebarPinBtn) sidebarPinBtn.style.display = 'none'; - try { localStorage.setItem('ped_sidebar_collapsed', '1'); } catch(e) {} - }); - } - if (sidebarExpandBtn) { - sidebarExpandBtn.addEventListener('click', function() { - if (sidebar) sidebar.classList.remove('collapsed'); - if (sidebarExpandBtn) sidebarExpandBtn.style.display = 'none'; - if (sidebarPinBtn) sidebarPinBtn.style.display = 'flex'; - try { localStorage.setItem('ped_sidebar_collapsed', '0'); } catch(e) {} + // One menu toggle, shared by the app sidebar and the assistant rail. It used + // to be a pin button plus a separate floating expand button here, and a third + // control inside the assistant — three affordances for one idea. + function syncMenuToggles(hidden) { + var label = hidden ? 'Show menu' : 'Hide menu'; + document.querySelectorAll('[data-menu-toggle]').forEach(function(button) { + button.setAttribute('aria-expanded', hidden ? 'false' : 'true'); + button.setAttribute('title', label); + button.setAttribute('aria-label', label); + button.classList.toggle('is-collapsed', hidden); }); } + var menuHidden = false; + try { menuHidden = localStorage.getItem('ped_sidebar_collapsed') === '1'; } catch (e) {} + document.body.classList.toggle('menu-hidden', menuHidden); + syncMenuToggles(menuHidden); + + document.addEventListener('click', function(event) { + if (!(event.target.closest && event.target.closest('[data-menu-toggle]'))) return; + var nowHidden = document.body.classList.toggle('menu-hidden'); + syncMenuToggles(nowHidden); + try { localStorage.setItem('ped_sidebar_collapsed', nowHidden ? '1' : '0'); } catch (e) {} + }); + // Set footer year var yearEl = document.getElementById('app-year'); if (yearEl) yearEl.textContent = new Date().getFullYear(); diff --git a/public/js/clinicalAssistant.js b/public/js/clinicalAssistant.js index 5a7b89bd..36952e15 100644 --- a/public/js/clinicalAssistant.js +++ b/public/js/clinicalAssistant.js @@ -165,72 +165,48 @@ import { // The rail mirrors the app's real tab list rather than restating it, so a tab // added, renamed or hidden in index.html shows up here with no extra work. - // Cards are the clinical work surface: account and administration tabs stay - // in the rail list, where someone looking for them will go, but they do not - // sit alongside Encounter HPI and Calculators. - var NON_WORK_TABS = ['settings', 'admin', 'docs', 'faq']; + // Clinical work only. Account and content-management tabs stay in the app + // sidebar, where someone looking for them will go; they are not work + // surfaces to launch into beside Encounter HPI. + var NON_WORK_TABS = ['settings', 'admin', 'docs', 'faq', 'cms']; - var renderWorkspaceLinks = function() { - var host = document.getElementById('assistant-workspace-links'); + var renderWorkspaceCards = function() { var cards = document.getElementById('assistant-workspace-cards'); - if (!host) return; - host.innerHTML = ''; - if (cards) cards.innerHTML = ''; + if (!cards) return; + cards.innerHTML = ''; Array.prototype.forEach.call(document.querySelectorAll('.tab-btn'), function(tab) { var name = tab.getAttribute('data-tab'); - if (!name || name === 'assistant' || tab.classList.contains('hidden')) return; + if (!name || tab.classList.contains('hidden') || NON_WORK_TABS.indexOf(name) !== -1) return; var icon = tab.querySelector('i'); var label = tab.querySelector('span'); - var link = document.createElement('button'); - link.type = 'button'; - link.className = 'assistant-rail-link'; - link.setAttribute('data-assistant-workspace-tab', name); - link.innerHTML = '' + + var card = document.createElement('button'); + card.type = 'button'; + card.className = 'assistant-workspace-card'; + card.setAttribute('data-assistant-workspace-tab', name); + card.innerHTML = '' + '' + escapeHtml(label ? label.textContent : name) + ''; - host.appendChild(link); - if (cards && NON_WORK_TABS.indexOf(name) === -1) { - var card = document.createElement('button'); - card.type = 'button'; - card.className = 'assistant-workspace-card'; - card.setAttribute('data-assistant-workspace-tab', name); - card.innerHTML = '' + - '' + escapeHtml(label ? label.textContent : name) + ''; - cards.appendChild(card); - } + cards.appendChild(card); }); }; - // Two modes, the way Home and Code swap the whole page: Assistant keeps the - // chat and its saved-chat list; Workspace replaces both the rail contents and - // the chat column with the app's own menu. - // One switch, one class. Every element this affects already carries a - // `display:` rule, and a class selector beats the UA stylesheet's - // [hidden]{display:none} — so toggling .hidden here silently did nothing and - // the chat list never came back. CSS owns what each mode shows. - var setAssistantMode = function(mode) { - var workspace = mode === 'workspace'; - if (workspace) renderWorkspaceLinks(); - document.body.classList.toggle('assistant-mode-workspace', workspace); - try { localStorage.setItem('ped_assistant_mode', workspace ? 'workspace' : 'assistant'); } catch (e) {} + // In the assistant, Workspace shows the launcher; in the app you are already + // there. Either way the switch only changes which pill is highlighted. + var showWorkspaceLauncher = function(show) { + renderWorkspaceCards(); + document.body.classList.toggle('assistant-mode-workspace', !!show); document.querySelectorAll('[data-assistant-mode]').forEach(function(button) { - var on = button.getAttribute('data-assistant-mode') === mode; + var on = (button.getAttribute('data-assistant-mode') === 'workspace') === !!show; button.classList.toggle('active', on); button.setAttribute('aria-selected', on ? 'true' : 'false'); }); }; + window.assistantShowWorkspaceLauncher = showWorkspaceLauncher; onceOnDocument('click', function(event) { - var pill = event.target.closest && event.target.closest('[data-assistant-mode]'); - if (pill) setAssistantMode(pill.getAttribute('data-assistant-mode')); - }); - onceOnDocument('click', function(event) { - var link = event.target.closest && event.target.closest('[data-assistant-workspace-tab]'); - if (!link) return; - var layout = document.getElementById('assistant-layout'); - if (layout) layout.classList.remove('mobile-chats-open'); - document.body.classList.remove('assistant-workspace'); - setAssistantMode('assistant'); // coming back lands on the chat, not an empty rail - if (typeof window.activateTab === 'function') window.activateTab(link.getAttribute('data-assistant-workspace-tab')); + var card = event.target.closest && event.target.closest('[data-assistant-workspace-tab]'); + if (!card) return; + document.body.classList.remove('assistant-workspace', 'assistant-mode-workspace'); + if (typeof window.activateTab === 'function') window.activateTab(card.getAttribute('data-assistant-workspace-tab')); }); var chatsToggle = document.getElementById('btn-assistant-chats-toggle'); diff --git a/test/admin-clinical-assistant-wiring.test.js b/test/admin-clinical-assistant-wiring.test.js index b5577762..725824eb 100644 --- a/test/admin-clinical-assistant-wiring.test.js +++ b/test/admin-clinical-assistant-wiring.test.js @@ -126,36 +126,13 @@ test('real extracted initializer displays the server default only when returned "source 'default' must not be reported as coming from the environment variable"); }); -test('an admin can add a model id discovery never returned', async t => { - const dom = new JSDOM(read('public/components/admin.html')); - browserGlobals(t, dom, async () => ({ ok: true, json: async () => ({ - success: true, config: [], models: [{ id: 'discovered-model', name: 'Discovered' }], - conversationBudget: { limit: 120000, source: 'default' } - }) }), []); - const { initClinicalAssistantAdmin } = await import(pathToFileURL(path.join(root, 'public/js/admin/clinicalAssistant.js')).href); - initClinicalAssistantAdmin(value => value); - document.dispatchEvent(new dom.window.CustomEvent('tabChanged', { detail: { tab: 'admin' } })); - await tick(); - - const list = document.getElementById('assistant-allowed-chat-models'); - const field = document.getElementById('assistant-add-chat-model'); - assert.ok(field, 'there is somewhere to type a model id'); - const before = list.querySelectorAll('input[type="checkbox"]').length; - - field.value = 'openrouter-deepseek-v4-pro'; - document.querySelector('[data-assistant-add-model="assistant-allowed-chat-models"]').click(); - await tick(); - - const values = Array.from(list.querySelectorAll('input[type="checkbox"]')).map(b => b.value); - assert.ok(values.includes('openrouter-deepseek-v4-pro'), 'the typed id joins the list'); - assert.equal(values.length, before + 1); - const added = list.querySelector('input[value="openrouter-deepseek-v4-pro"]'); - assert.equal(added.checked, true, 'and is enabled, since that is why it was added'); - assert.equal(field.value, '', 'the field clears for the next one'); - - // Adding the same id twice must not duplicate the row. - field.value = 'openrouter-deepseek-v4-pro'; - document.querySelector('[data-assistant-add-model="assistant-allowed-chat-models"]').click(); - await tick(); - assert.equal(list.querySelectorAll('input[value="openrouter-deepseek-v4-pro"]').length, 1); +test('model availability comes from discovery, never hand-typed', () => { + const html = read('public/components/admin.html'); + // A hand-typed id cannot be verified here, so the roster is exactly what the + // gateway advertises. + assert.doesNotMatch(html, /assistant-add-chat-model|assistant-add-image-model/, 'no manual entry field'); + assert.doesNotMatch(html, /data-assistant-add-model/, 'and no Add button'); + assert.match(html, /id="assistant-allowed-chat-models"/, 'the discovered roster stays'); + assert.match(html, /id="assistant-allowed-image-models"/); }); + diff --git a/test/assistant-workspace-layout.test.js b/test/assistant-workspace-layout.test.js index 63a5047c..2a9c5f06 100644 --- a/test/assistant-workspace-layout.test.js +++ b/test/assistant-workspace-layout.test.js @@ -89,30 +89,6 @@ test('Learning Hub is a top-level main-menu entry, not part of the assistant wor assert.match(indexHtml, /
<\/section>/, 'assistant tab is a clean standalone section'); }); -test('a workspace entry leaves the assistant and lands back on the chat next time', t => { - const app = workspace(t); - app.document.dispatchEvent(new app.window.CustomEvent('tabChanged', { detail: { tab: 'assistant' } })); - app.document.querySelector('[data-assistant-mode="workspace"]').click(); - assert.ok(app.document.body.classList.contains('assistant-mode-workspace'), 'workspace mode engaged'); - - const entry = app.document.querySelector('#assistant-workspace-links [data-assistant-workspace-tab="notes"]'); - assert.ok(entry, 'the rail lists the app tabs'); - // Cards are the clinical work surface; the rail keeps everything. - const cardTabs = [...app.document.querySelectorAll('#assistant-workspace-cards [data-assistant-workspace-tab]')] - .map(b => b.getAttribute('data-assistant-workspace-tab')); - assert.deepEqual(cardTabs, ['notes', 'calculators'], 'Settings is not a card; hidden Admin is nowhere'); - assert.ok(app.document.querySelector('#assistant-workspace-links [data-assistant-workspace-tab="settings"]'), - 'but Settings is still reachable from the rail'); - assert.equal(app.document.querySelector('[data-assistant-workspace-tab="admin"]'), null, - 'a hidden tab stays hidden in both'); - entry.click(); - assert.deepEqual(app.activated, ['notes'], 'opens that part of the app'); - assert.ok(!app.document.body.classList.contains('assistant-workspace'), 'and leaves the assistant chrome'); - // Returning must show the chat, not the empty rail the mode was left in. - assert.ok(!app.document.body.classList.contains('assistant-mode-workspace'), - 'the assistant reopens on its chat rather than in workspace mode'); -}); - test('the assistant tab keeps its clean structure next to the restored learning tab', () => { const indexHtml = read('public/index.html'); assert.match(indexHtml, /