pediatric-ai-scribe-v3/public/components/my-resources.html
Daniel 03621752e8
Some checks failed
Forgejo Android APK / Root app tests (push) Successful in 55s
Forgejo Docker Build / Root app tests (push) Successful in 45s
Forgejo Android APK / Build signed APK (push) Successful in 2m5s
Forgejo Docker Build / Build Docker image (push) Successful in 19s
Forgejo Docker Build / Deploy to the host (push) Failing after 0s
feat: image fallback chains for every workflow, and a library worth looking at
**Fallbacks.** One image model meant a refusal, a rate limit or a model the
gateway had since dropped ended as a missing picture. Every workflow now tries
its model, then each fallback in order, stopping at the first that produces an
image. Primary plus two, capped: each hop is a paid request, and a chain long
enough to need a cap is long enough to surprise someone.

My Resources previously had no fallback at all — only the Clinical Assistant
did, and only one. That is backwards: a missing figure is most visible in a
deck, where it leaves a hole in a slide.

The retry rule is now a classifier that says *why*, rather than a boolean.
Transient faults, a 404 for a model the gateway does not have, and a content
refusal all move to the next model — a refusal because policy is a vendor
decision, not a fact about the request. 401/403 stop immediately (one gateway,
one set of credentials, the next model fails identically), as do 413 and any
other 4xx, which are malformed everywhere. Refusals are recognised from the
message: no provider sends a machine-readable reason and the status varies.

Each hop re-leases the job, so a chain cannot outlive its claim and let a second
worker repeat the same paid work, and the row records the model actually being
paid for so a picture made by the third model is not attributed to the first.

The old singular `fallback_image_model` is still read, so an existing
configuration keeps working without anyone re-entering it.

**Library.** Documents/Images tabs in My Resources, with a real grid: fixed
aspect tiles so the rows line up whatever shape the pictures are, a source badge
on the picture, two-line prompt, hover lift, shimmer skeletons while thumbnails
land, and a lightbox that closes on Escape or the backdrop and restores focus.
Actions are hidden on hover only behind `@media (hover:hover)` — hiding delete
behind :hover would put it out of reach on touch and keyboard.

Downloads go through privateImageBlob rather than a bare `<a download href>`: a
mobile client's session is a bearer token an anchor cannot send, and these
assets are served no-store on purpose.

The gallery lives in My Resources only. Assistant images appear in it, which was
the point; the assistant page does not grow a gallery of its own, and a test
asserts no assistant module lists the endpoint.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01Dv6sqaY6Vq3ChZHMem3cnU
2026-09-12 14:53:04 +02:00

185 lines
11 KiB
HTML

<div class="card">
<div class="card-header">
<h3><i class="fas fa-folder-open"></i> My Resources</h3>
<span style="font-size:12px;color:var(--g500);">Only you can see these</span>
</div>
<div style="padding:16px;display:flex;flex-direction:column;gap:14px;">
<p style="margin:0;font-size:13px;color:var(--g600);line-height:1.5;">
Build a teaching deck or handout on any topic &mdash; grounded in the clinical library,
the literature, or both &mdash; then revise it below and download it as PowerPoint,
Word or PDF.
</p>
<div class="admin-row">
<label for="mr-topic" class="admin-row-label">Topic</label>
<input id="mr-topic" type="text" class="admin-control" placeholder="e.g. febrile seizures in under-fives" maxlength="300">
</div>
<div class="admin-row">
<label for="mr-kind" class="admin-row-label">Format</label>
<div style="flex:1;display:flex;gap:10px;align-items:center;flex-wrap:wrap;min-width:0;">
<select id="mr-kind" class="admin-control" style="max-width:220px;">
<option value="presentation">Presentation (slides)</option>
<option value="article">Article (prose)</option>
</select>
<label style="font-size:12px;color:var(--g600);display:flex;align-items:center;gap:6px;">
Slides
<input id="mr-slide-count" type="number" min="3" max="30" value="8" style="width:70px;font-size:13px;padding:5px 8px;border:1px solid var(--g300);border-radius:6px;">
</label>
<label style="font-size:12px;color:var(--g600);display:flex;align-items:center;gap:6px;" id="mr-word-wrap" hidden>
Words
<input id="mr-word-count" type="number" min="200" max="3000" step="100" value="800" style="width:90px;font-size:13px;padding:5px 8px;border:1px solid var(--g300);border-radius:6px;">
</label>
</div>
</div>
<div class="admin-row" id="mr-model-row" hidden>
<label for="mr-model" class="admin-row-label">Model</label>
<div style="flex:1;display:flex;flex-direction:column;gap:4px;min-width:0;">
<select id="mr-model" class="admin-control" style="max-width:320px;"></select>
</div>
</div>
<!-- One group rather than four rows. Each option hides itself when an
administrator has not enabled it, so nothing appears that a person
could tick and then be refused. -->
<div class="admin-row" style="align-items:flex-start;">
<strong class="admin-row-label">Draw on</strong>
<div style="flex:1;display:flex;flex-direction:column;gap:8px;min-width:0;">
<label style="display:flex;align-items:center;gap:8px;font-size:13px;">
<input type="checkbox" id="mr-use-corpus" checked>
The clinical library
</label>
<label style="display:flex;align-items:center;gap:8px;font-size:13px;" id="mr-pubmed-row" hidden>
<input type="checkbox" id="mr-pubmed">
PubMed &mdash; cite published literature
<span style="font-size:12px;color:var(--g500);">The query goes to NCBI.</span>
</label>
<label style="display:flex;align-items:center;gap:8px;font-size:13px;flex-wrap:wrap;" id="mr-web-row" hidden>
<input type="checkbox" id="mr-web-search">
The web &mdash; for anything newer than the library
<span style="font-size:12px;color:var(--g500);">The query leaves this network; keep the topic non-identifying.</span>
</label>
<label style="display:flex;align-items:center;gap:8px;font-size:13px;" id="mr-images-row" hidden>
<input type="checkbox" id="mr-with-images">
Add illustrations &mdash; say how many in Instructions, or leave it to the model
</label>
<!-- Filled in when the instructions ask for a figure the options do not
allow. Silence there is the failure worth avoiding. -->
<p id="mr-image-hint" style="margin:0;font-size:12px;color:#92400e;" hidden></p>
</div>
</div>
<div class="admin-row" style="align-items:flex-start;">
<label for="mr-refinement" class="admin-row-label">Instructions <span style="color:var(--g400);font-weight:400;">(optional)</span></label>
<textarea id="mr-refinement" class="admin-control" style="min-height:70px;resize:vertical;font-family:inherit;" placeholder="e.g. for FY1s, case-based, emphasise red flags, use 3 diagrams"></textarea>
</div>
<div style="border-top:1px solid var(--g100);padding-top:12px;display:flex;align-items:center;gap:10px;flex-wrap:wrap;">
<button id="btn-mr-generate" class="btn-sm btn-primary" type="button"><i class="fas fa-wand-magic-sparkles"></i> Generate</button>
<span id="mr-status" role="status" style="font-size:12px;color:var(--g600);"></span>
</div>
<!-- An illustration the model asked for. It renders here rather than in an
image history, because this feature does not have one and telling
someone to look somewhere that does not exist is worse than silence. -->
<div id="mr-images" style="display:flex;flex-direction:column;gap:10px;"></div>
</div>
</div>
<!-- Modify — change something already generated rather than starting again.
The markdown is what is stored, so "redo slide 4" is a text edit; the
resource keeps its id, its downloads and its place in the library. -->
<div class="card" style="margin-top:14px;">
<div class="card-header">
<h3><i class="fas fa-pen-to-square"></i> Modify</h3>
<span style="font-size:12px;color:var(--g500);">Revise one you already have</span>
</div>
<div style="padding:16px;display:flex;flex-direction:column;gap:14px;">
<div class="admin-row">
<label for="mr-modify-target" class="admin-row-label">Resource</label>
<select id="mr-modify-target" class="admin-control" style="max-width:420px;"></select>
</div>
<!-- The same choices as generating. A modification can need material the
original never had — "add what the 2024 trial showed" is a request for
sources, not a rewording — so the options are offered again rather
than inherited from whatever was ticked at the time. -->
<div class="admin-row" style="align-items:flex-start;">
<strong class="admin-row-label">Draw on</strong>
<div style="flex:1;display:flex;flex-direction:column;gap:8px;min-width:0;">
<label style="display:flex;align-items:center;gap:8px;font-size:13px;">
<input type="checkbox" id="mr-modify-corpus" checked>
The clinical library
</label>
<label style="display:flex;align-items:center;gap:8px;font-size:13px;" id="mr-modify-pubmed-row" hidden>
<input type="checkbox" id="mr-modify-pubmed">
PubMed &mdash; cite published literature
<span style="font-size:12px;color:var(--g500);">The query goes to NCBI.</span>
</label>
<label style="display:flex;align-items:center;gap:8px;font-size:13px;flex-wrap:wrap;" id="mr-modify-web-row" hidden>
<input type="checkbox" id="mr-modify-web-search">
The web &mdash; for anything newer than the library
<span style="font-size:12px;color:var(--g500);">The query leaves this network.</span>
</label>
<label style="display:flex;align-items:center;gap:8px;font-size:13px;" id="mr-modify-images-row" hidden>
<input type="checkbox" id="mr-modify-images">
Add illustrations
</label>
<p id="mr-modify-image-hint" style="margin:0;font-size:12px;color:#92400e;" hidden></p>
</div>
</div>
<div class="admin-row" style="align-items:flex-start;">
<label for="mr-modify-instructions" class="admin-row-label">What to change</label>
<div style="flex:1;display:flex;flex-direction:column;gap:4px;min-width:0;">
<textarea id="mr-modify-instructions" class="admin-control" style="min-height:70px;resize:vertical;font-family:inherit;" placeholder="e.g. split slide 4 into two, drop the pharmacology, add a summary slide at the end"></textarea>
<p style="margin:0;font-size:12px;color:var(--g500);">
The previous version is replaced.
</p>
</div>
</div>
<div style="border-top:1px solid var(--g100);padding-top:12px;display:flex;align-items:center;gap:10px;flex-wrap:wrap;">
<button id="btn-mr-modify" class="btn-sm btn-primary" type="button"><i class="fas fa-pen-to-square"></i> Apply changes</button>
<span id="mr-modify-status" role="status" style="font-size:12px;color:var(--g600);"></span>
</div>
</div>
</div>
<div class="card" style="margin-top:14px;">
<div class="card-header">
<h3><i class="fas fa-book"></i> Library</h3>
<div style="display:flex;align-items:center;gap:6px;">
<!-- Two views of the same library. Documents is what this card has always
shown; Images is every picture this account has generated, wherever it
was made, because a figure is worth keeping after the deck it was
drawn for has been replaced. -->
<div role="tablist" aria-label="Library view" style="display:flex;gap:2px;background:var(--g100);border-radius:6px;padding:2px;">
<button id="tab-mr-docs" role="tab" aria-selected="true" aria-controls="mr-list" class="btn-sm" type="button"
style="border:0;border-radius:4px;background:var(--white);font-size:12px;padding:4px 10px;">Documents</button>
<button id="tab-mr-images" role="tab" aria-selected="false" aria-controls="mr-images-panel" class="btn-sm" type="button"
style="border:0;border-radius:4px;background:transparent;font-size:12px;padding:4px 10px;">Images</button>
</div>
<button id="btn-mr-refresh" class="btn-sm btn-ghost" type="button"><i class="fas fa-rotate"></i> Refresh</button>
</div>
</div>
<div id="mr-docs-search" style="padding:12px 16px 0;">
<input id="mr-search" type="search" class="admin-control" placeholder="Search your resources by title or topic" autocomplete="off">
</div>
<!-- Bounded rather than unlimited: a long library otherwise pushes everything
else off the page. Tall enough to show several at a glance, and it
collapses to the content when there are only a few. -->
<div id="mr-list" role="tabpanel" aria-labelledby="tab-mr-docs" style="padding:12px 16px;display:flex;flex-direction:column;gap:6px;max-height:360px;overflow-y:auto;"></div>
<!-- A grid rather than a list: a picture is recognised by looking at it, and a
row of filenames would defeat the point of keeping them. Auto-fill so it
reflows to one or two columns on a phone without a breakpoint. -->
<div id="mr-images-panel" role="tabpanel" aria-labelledby="tab-mr-images" hidden
style="padding:12px 16px;max-height:420px;overflow-y:auto;">
<p id="mr-images-empty" style="margin:0;font-size:12px;color:var(--g500);">Loading…</p>
<div id="mr-images-grid" class="img-grid"></div>
<div style="display:flex;justify-content:center;padding-top:10px;">
<button id="btn-mr-images-more" class="btn-sm btn-ghost" type="button" hidden>Load more</button>
</div>
</div>
</div>