Commit graph

219 commits

Author SHA1 Message Date
Daniel
9788b167f2 refactor: retrieval is text-only; admins can add model ids discovery never returns
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
2026-09-09 23:35:06 +02:00
Daniel
b992c6600b feat: admin can turn citations off
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
2026-09-09 23:06:13 +02:00
Daniel
022869f01d fix: mobile layout regression, workspace menu in the assistant rail, prompt as principles
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
2026-09-09 22:24:10 +02:00
Daniel
a95236e363 refactor: the model decides about acknowledgements and images, not route heuristics
The previous fix put a hand-maintained multilingual word list in the route. That
does not generalise — the bug report itself was "Окей", and the next one would be
a language not on the list. The model already has the tool, the system prompt and
the whole conversation, so the policy belongs where it reads it.

Removed from src/routes/clinicalAssistant.js:
- GREETING_RE and its short-circuit (an ASCII keyword list that decided, before
  the model ever saw the message, that a greeting deserved a canned reply).
- The IMAGE_NOUN / IMAGE_REPEAT vocabulary lists and the answer-repetition
  backstop added earlier today.

Moved into the prompt and the tool definition:
- DEFAULT_BEHAVIOR now says that a greeting, acknowledgement or thanks in ANY
  language gets a one-sentence request for a clinical question, and that a
  previous turn's answer must never be repeated to justify a second image.
- The generate_image description says to call it ONLY when the user's latest
  message asks for a picture or a change to one just made, and that an
  acknowledgement of an existing image is not such a request.

Both are admin-editable (clinical_assistant.system_behavior), so this can now be
tuned without a deploy.

dispatchImageRequestFallback and IMAGE_REQUEST_PATTERN stay: that is the
compatibility path for a serving model that writes the image prompt as text
instead of calling the tool. It reads the USER's message only, never the model's
answer, so it cannot replay a previous turn — it was not the cause of this bug.

The DEFAULT_BEHAVIOR byte-hash lock in prompt-administration.test.js is updated
deliberately, which is what that guard is for.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01BkfrkQwA4YGrGw9LZSpeAq
2026-09-09 19:06:28 +02:00
Daniel
d790d427ac feat: saved chats grouped by recency, a visible sidebar toggle, and acknowledgements answered cheaply
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
2026-09-09 18:59:45 +02:00
Daniel
d04a3fe53b fix: translate as HTML so tables and emphasis survive; stop repeat image generation
Translation formatting
LibreTranslate's text mode destroys markdown syntax. Verified against the live
container: table pipes come back as "←", the |---| delimiter row is translated
as prose ("Silencio."), and "**bold**" returns as "** bold**" which no longer
renders. Its html mode leaves tags — and bare [n] markers — completely intact.
Messages and the patient take home are now rendered to HTML, simplified (maths
and UI chrome flattened to text), and translated as HTML. Citation chips are
re-linked from the returned markers afterwards, which is the step the original
html path was missing. A text-mode fallback remains for builds that reject html.

Repeat image generation
Typing "Окей" or "Nice" after an image turn produced another image every time:
the model saw its own "I'll generate an educational image…" in the history and
repeated it. Recognising acknowledgements in every language is not possible, so
the rule is inverted — a short follow-up (<=3 words) that mentions nothing about
a picture does not get the image tool offered at all when the previous assistant
turn produced an image. Terse repeat requests ("again", "ещё", "another one")
still work. The worst case is that a terse question is answered in text.

In-chat images
Generated images render as a 320x240 thumbnail instead of filling the bubble,
and the image itself opens the full-resolution preview.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01BkfrkQwA4YGrGw9LZSpeAq
2026-09-09 18:49:58 +02:00
Daniel
4d92488f0c fix: translated answers keep their source chips; the patient take home can be translated
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
2026-09-09 18:40:23 +02:00
Daniel
9a42b96ec9 feat: admin controls the conversation budget — saved override wins over CLINICAL_ASSISTANT_CONVERSATION_CHARS, validated 1000-1000000, empty falls back to env
All checks were successful
Forgejo Android APK / Root app tests (push) Successful in 58s
Forgejo Android APK / Build signed APK (push) Successful in 2m7s
2026-09-09 17:49:57 +02:00
Daniel
598731e170 fix: conversation budget is env-only — admin panel shows CLINICAL_ASSISTANT_CONVERSATION_CHARS read-only and never saves the key; server ignores saved overrides
All checks were successful
Forgejo Android APK / Root app tests (push) Successful in 48s
Forgejo Android APK / Build signed APK (push) Successful in 2m17s
2026-09-09 17:32:15 +02:00
Daniel
07af95b89a feat: device TTS preferred in voice mode with model fallback; permanent src-garbage scrub; simplified-HTML translation with text fallback; New chat closes the drawer; real FA icons; audio unlock restored 2026-09-09 05:06:40 +02:00
Daniel
143030bf00 fix: usable mobile chat menu — left drawer with close X and backdrop, main-menu style rows, internal message scroll to the question, taller sources scroll; context metadata line removed from image cards 2026-09-09 04:39:20 +02:00
Daniel
d4418f8a80 feat: OWUI-style image handoff — the model calls the tool and announces preparation; enforced instruction independent of stored prompts; stream status announces generating image 2026-09-09 04:26:41 +02:00
Daniel
5e6de10d0a fix: delegated model-selection persistence, admin-settable conversation budget (DB overrides env), unbounded image poll 2026-09-09 03:20:21 +02:00
Daniel
73955dd5f9 feat: model selection in chat (composer pill + popup image model), deterministic image dispatch when the model writes text instead of calling the tool, markdown-rendered email, OWUI stacked composer, left mobile drawer, Go back in topbar, DeepL removed, image history inside the Create image popup 2026-09-09 02:55:28 +02:00
Daniel
7fdbcd51e6 feat: ChatGPT-style chat list — abbreviated rows with relative dates, pinned-first ordering, ⋮ menu (Rename/Pin/Export/Delete); clinical context always on; warn-only budget; mic live-transcript fallback; learning-hub page mode; composer autosize; include-context icon fix 2026-09-09 00:14:34 +02:00
Daniel
bb218750d9 feat: Create image tool at rail top (describe or pick a chat), Images gallery of past creations, ChatGPT-style composer icons (headset/mic/attach/send), example tap asks immediately 2026-09-08 23:40:53 +02:00
Daniel
09a4e02e39 feat: patient take home (copy/export/email), composer mic dictation, voice conversation mode, provider-aware translate languages 2026-09-08 20:01:46 +02:00
Daniel
8b072496e2 feat: Open WebUI-style assistant workspace — 3-column layout, markdown/math/code/tables, autosave with images, translation (LibreTranslate+DeepL), citation modal, Learning Hub moved in, handoff removed 2026-09-08 18:52:35 +02:00
Daniel
9bbcf0049a feat: instruct the assistant to call the generate_image tool for image requests 2026-09-08 17:41:06 +02:00
Daniel
6c9ac58ce3 chore: log image parts attached to provider requests
All checks were successful
Forgejo Android APK / Root app tests (push) Successful in 22s
Forgejo Android APK / Build signed APK (push) Successful in 2m17s
2026-09-08 16:45:07 +02:00
Daniel
94210ac0a1 feat: attach images to clinical search with strict validation and multimodal routing 2026-09-08 15:53:05 +02:00
Daniel
2dbb446a9c feat: env-driven clinical search tool name with hard stop and deprecation warning
All checks were successful
Forgejo Android APK / Root app tests (push) Successful in 24s
Forgejo Android APK / Build signed APK (push) Successful in 1m53s
2026-09-07 19:40:41 +02:00
Daniel
13fca7887d feat: unify Admin model/prompt UI — single prompt dropdown, two image-model dropdowns, searchable/scrollable users, remove classification/custom-add, consistent font, concise labels 2026-09-07 18:18:48 +02:00
Daniel
6be2d1375a feat: integrate durable image jobs/private assets into current core 2026-09-07 16:53:42 +02:00
Daniel
3a8dbbcce3 fix: keep the sidebar generated image session-only, never stored in saved chats
All checks were successful
Forgejo Android APK / Root app tests (push) Successful in 26s
Forgejo Android APK / Build signed APK (push) Successful in 4m4s
2026-09-07 16:23:55 +02:00
Daniel
6f63b8f538 feat: image prompt adds full-content detail and bans citations/reference numbers
All checks were successful
Forgejo Android APK / Root app tests (push) Successful in 34s
Forgejo Android APK / Build signed APK (push) Successful in 2m23s
2026-09-07 16:13:09 +02:00
Daniel
9902ee07a6 fix: render clinical handoff Markdown with a concise heading
All checks were successful
Forgejo Android APK / Root app tests (push) Successful in 26s
Forgejo Android APK / Build signed APK (push) Successful in 2m2s
2026-09-07 14:23:56 +02:00
Daniel
b0bebe6970 fix: preserve saved clinical tables and per-turn citation targets 2026-09-07 05:58:12 +02:00
Daniel
cfaf8e957b feat: ship reviewed prompt history, conversation limits and account protections 2026-09-07 04:01:01 +02:00
Daniel
baab83e50e fix: enforce sessions, escape quiz metadata and isolate ED drafts
All checks were successful
Forgejo Android APK / Build signed APK (push) Successful in 1m47s
2026-09-06 07:44:08 +02:00
Daniel
2942d4f6b3 Revert "Sanitize citations from clinical image prompts"
All checks were successful
Forgejo Android APK / Build signed APK (push) Successful in 1m43s
This reverts commit 7833018695.
2026-09-02 01:50:44 +02:00
Daniel
7833018695 Sanitize citations from clinical image prompts
All checks were successful
Forgejo Android APK / Build signed APK (push) Successful in 1m47s
2026-09-02 01:47:32 +02:00
Daniel
54d49dd28a Forbid LaTeX escaped clinical citations
All checks were successful
Forgejo Android APK / Build signed APK (push) Successful in 1m55s
2026-09-01 19:29:40 +02:00
Daniel
c1ba6fa798 Close MCP sessions when they are replaced
All checks were successful
Forgejo Android APK / Build signed APK (push) Successful in 1m48s
The MCP server builds a Nextcloud client per session and closes it only
when the session ends. The clinical assistant replaced its cached session
every time the ten-minute TTL lapsed but never ended the old one, so each
abandoned client held its Nextcloud connections open. The production MCP
container was holding 708 sockets in CLOSE-WAIT against a 1024 descriptor
ceiling, roughly 300 from the point where every clinical search fails.

Expired and server-rejected sessions are now deleted. Session reuse is
unchanged: a live session is still shared across calls, since closing one
still in use would force a re-initialize on every search.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
2026-08-28 23:45:26 +02:00
Daniel
604f6abb49 Remove clinical assistant source lock
All checks were successful
Forgejo Android APK / Build signed APK (push) Successful in 1m54s
2026-08-27 21:33:58 +02:00
Daniel
018913a845 Lock clinical assistant retrieval to configured source
All checks were successful
Forgejo Android APK / Build signed APK (push) Successful in 1m46s
2026-08-27 21:30:29 +02:00
Daniel
2c02e6eca7 Send reasoning options only to supported model
All checks were successful
Forgejo Android APK / Build signed APK (push) Successful in 1m53s
2026-08-27 21:25:38 +02:00
Daniel
e710b1c7bd Configure clinical assistant reasoning profile
All checks were successful
Forgejo Android APK / Build signed APK (push) Successful in 1m58s
2026-08-27 20:47:59 +02:00
Daniel
524ad40d49 Match TTS voices to the selected LiteLLM model
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.
2026-07-30 17:34:34 +02:00
Daniel
82ed46d01b Fix Turnstile in the mobile app; drop it from login
The Turnstile challenge failed reliably inside the Capacitor WebView,
which blocked login and registration from the Android app.

Three separate causes:

1. Android WebView blocks third-party cookies by default. Turnstile runs
   in a cross-origin iframe from challenges.cloudflare.com and needs its
   own storage, so the widget never emitted a token. MainActivity now
   calls setAcceptThirdPartyCookies on the app's own WebView.

2. The register handler read the Turnstile response with an unscoped
   document.querySelector, which matched the *login* widget's input (it
   comes first in the DOM). Registration therefore submitted the login
   widget's token — single-use with a 5 minute expiry, so any prior login
   attempt or slow signup made it fail server-side.

3. The register and forgot-password widgets auto-rendered inside forms
   that start at display:none, where Turnstile does not reliably complete
   a challenge, and nothing re-rendered them when the form was shown.

Widgets are now rendered explicitly when their form first becomes
visible, and tokens are captured from the render callback instead of
being read back out of the injected input — which makes the unscoped
lookup in (2) structurally impossible. Added error/expired/timeout
callbacks so a widget failure surfaces the Cloudflare error code instead
of failing silently behind a generic toast.

Login is no longer gated at all. It is the path mobile users hit
constantly, and it is already covered by a 10-per-15-min per-IP rate
limit, a constant-time credential check, and TOTP 2FA. Registration and
password reset — the endpoints that actually attract bots — stay gated.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
2026-07-30 17:22:03 +02:00
Daniel
2ca969e099 Fix generated image mobile downloads
All checks were successful
Forgejo Android APK / Build signed APK (push) Successful in 2m21s
2026-06-09 15:20:06 +02:00
Daniel
b0e1f4969a feat: improve clinical assistant prompts
All checks were successful
Forgejo Android APK / Build signed APK (push) Successful in 2m15s
2026-06-06 00:01:07 +02:00
Daniel
d02b9e2771 Fix clinical prompt pool fallback seeding
All checks were successful
Forgejo Android APK / Build signed APK (push) Successful in 2m14s
2026-05-22 16:53:53 +02:00
Daniel
cb63729656 Expand clinical prompt pool taxonomy
Some checks are pending
Forgejo Android APK / Build signed APK (push) Waiting to run
2026-05-22 05:56:08 +02:00
Daniel
97ddd87449 fix mobile assistant table streaming
Some checks failed
Forgejo Android APK / Build signed APK (push) Has been cancelled
2026-05-11 16:15:45 +02:00
Daniel
f871384063 fix mobile assistant export and image actions
Some checks failed
Forgejo Android APK / Build signed APK (push) Has been cancelled
2026-05-10 20:28:46 +02:00
Daniel
046b07a84a fix clinical assistant mobile exports 2026-05-10 17:23:54 +02:00
Daniel
83d9a77160 fix table source citation links 2026-05-09 20:50:54 +02:00
Daniel
baf0020981 prefer file names for clinical sources 2026-05-09 20:10:24 +02:00
Daniel
795ad9ffae harden clinical assistant source handling 2026-05-09 19:59:04 +02:00