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
The rail's Workspace section was a collapsible extra sitting under the chats.
It is now the second of two modes, selected by a segmented switch at the top of
the rail:
- Assistant keeps the chat column, the saved-chat list, New chat and Create image.
- Workspace REPLACES the chat column and the Sources panel with the app's own
menu — a card grid in the main column and the same list in the rail — so
nothing from the assistant is left half-visible beside it.
Both the rail list and the cards are built from the app's real .tab-btn
elements, so a tab added, renamed or hidden in index.html follows automatically.
Learning Hub moved down from second place to sit immediately before Content
Manager, with the other content tools rather than above the clinical ones.
Also removed dead CSS for .assistant-view-switch, .assistant-learning-view and
#assistant-learning-root: that markup no longer exists anywhere in the app.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01BkfrkQwA4YGrGw9LZSpeAq
Multimodal removal
The multimodal path called nc_multimodal_search against a second hardcoded
collection whose embedding service (multimodal-embeddings:7999) was never
deployed and ENABLE_MULTIMODAL_RAG has always been false, so it only ever logged
"multimodal search skipped". Removed rather than left as dead weight:
- clinicalRetrieval: normalizeMcpMultimodalResponse, isVisualSourceQuery,
isRadiologyQuery, buildMultimodalSearchQuery, classifyAndRerankMultimodalResults,
selectMultimodalResults, visualIntent, visualMetadataScore,
shouldRejectVisualSource, allowsFrontMatterQuery, looksLikeFrontMatterPage,
looksLikeTextOnlyPage and MULTIMODAL_CANDIDATE_LIMIT (~140 lines).
- clinicalMcpClient: multimodalSearch.
- The route's visual/text slot split is gone; the whole search limit is text.
- The "[visual PDF page match]" prompt label and the "visual PDF page" source
badge are gone with it.
Adding models
Model availability could only be ticked from what the gateway advertised, so an
admin could never offer a model discovery did not list. Each list now has a text
field: a typed id joins the same checkbox list, is enabled by default, is
de-duplicated, and persists through the normal allowed_models save.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01BkfrkQwA4YGrGw9LZSpeAq
New setting clinical_assistant.citations_enabled (default true, admin checkbox).
With it off, retrieval, grounding and every other rule are unchanged — answers
are still built only from retrieved sources — but:
- buildSystemPrompt swaps only the citation block: the "cite factual claims with
[1]" rules are replaced with "do not include citations, source numbers or
bracketed markers", and the note that the sourcing requirement itself is
unchanged. Grounding, scope, table formatting and tone rules are byte-identical
between the two modes.
- The server strips any stray [n] the model emits anyway, from the stored answer
rather than only the view, so saved chats and exports match what was shown.
- No sources are sent to the client at all, and the status endpoint reports the
mode so the UI hides the Sources panel and gives its 330px column back to the
chat instead of showing an empty rail.
Validated as a boolean in adminConfig, like the feature.* keys.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01BkfrkQwA4YGrGw9LZSpeAq
Mobile
body.assistant-workspace .assistant-layout is both later in the file and more
specific than the .assistant-layout rule inside @media (max-width:640px), so
phones were inheriting height: calc(100vh - 64px) — an offset for the app header
that workspace mode already hides — plus grid rules on a flex container. The
topbar then overlapped the content. Added a matching-specificity mobile override
restoring 100dvh and the flex layout.
Workspace menu in the rail
The saved-chats rail gains a collapsible Workspace section listing the app's own
tabs, so the assistant sidebar carries both its chats and the workspace
navigation. It is built from the real .tab-btn elements rather than a second copy
of the menu, so tabs added, renamed or hidden in index.html follow automatically
and admin-only tabs stay hidden.
Prompt
Rewritten as principles instead of an enumerated rulebook: "respond to the user's
latest message, not to an earlier one", "if it carries no question, ask what they
would like you to look up", "never repeat a previous answer", and for the tool
"use it when the latest message asks for a picture, or for a change to one you
just made, and not otherwise". No example words in any language remain — the
model reads the message as written. 1032 -> 788 characters.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01BkfrkQwA4YGrGw9LZSpeAq
Saved chats
Grouped the way Open WebUI groups them: Pinned, Today, Yesterday, Previous 3
days, Previous 7 days, Previous 30 days, then calendar months (with the year
once it is not the current one). Chats with no usable timestamp land in Undated
rather than disappearing.
Sidebar toggle
The collapse button used <i class="fas fa-sidebar">, which is a Font Awesome PRO
icon; on the Free 6.5.0 build this app loads it rendered nothing, so the toggle
has been an invisible button since it shipped. It now uses fa-table-columns,
keeps aria-expanded/aria-controls in sync, flips its label between "Hide saved
chats" and "Show saved chats" so the collapsed state is escapable, and animates.
Acknowledgements
"Окей", "Nice", "Perfect", "gracias" and friends now get the existing
"What clinical question would you like me to look up?" reply instead of a
retrieval and a paid generation. "yes", "sure", "no" and "more" are deliberately
excluded: answers end by offering more detail, so those must still be answered.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01BkfrkQwA4YGrGw9LZSpeAq
Translation
- Stop scrubbing markdown before sending it to LibreTranslate. The scrub
deleted ordered-list numbering ("1. Give amoxicillin" -> "Give amoxicillin"),
flattened tables into ambiguous whitespace and ate underscores inside
identifiers. Raw markdown now goes to the translator unchanged.
- Render the translation through the same markdown pipeline as the original
bubble, with the message's own sources, so [n] markers come back as the usual
clickable .assistant-cite chips instead of escaped literal text. Headings,
lists and tables survive with them.
- When the translator drops citation markers, surface the affected sources in a
recovery block rather than letting the evidence disappear.
- Image cards are live nodes: they are now re-attached on every path out of a
translation (success, failure and Show original), so a failed translation no
longer silently removes a generating image from the message.
Patient take home
- Add a language selector to the take-home modal, reusing the existing
/translate endpoint and offering only what the local LibreTranslate reports.
- Copy, Export and Email carry what the caregiver is actually reading; the
original stays canonical behind "Original".
Conversation budget
- The admin field no longer prefills with the environment value, which turned
the next Save into an accidental override and made the documented "leave
empty to use the environment" path unreachable. The effective limit is shown
as a placeholder instead.
- Report source 'default' honestly instead of naming an unset env var.
- The load-failure notice now lands on the <p> instead of an <input>'s
textContent, where it rendered nothing.
- One validator for the budget everywhere: conversationLimit() replaces a
parseInt that accepted "120000abc".
Other
- /assistant is addressed by its URL, not by ped_last_tab, so "/" no longer
reopens the assistant; the URL follows tab changes and Back leaves it.
- Remove the dead DeepL path (it referenced an undefined DEEPL_BASES) and stop
offering admins a provider the server silently ignores.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01BkfrkQwA4YGrGw9LZSpeAq
Voice lists were a single flat set from LITELLM_TTS_VOICES, so picking a
model could leave an incompatible voice selected and the request would
fail at the gateway. Voices are now resolved per model family (Kokoro,
Kitten, Supertonic, Groq Orpheus EN/AR), with a compatibility check that
falls back through user → admin → env → first valid voice. Groq Orpheus
requests also pin response_format to wav.
Also refreshes the cardiac/respiratory auscultation samples, extends the
well-visit component, and fixes the Android launch theme background
(@null → colorPrimary) so the splash does not flash through.
NOTE: this is in-progress work that was already sitting uncommitted in
the working tree; it is committed here as-is so the tree was clean for
the release bump.