The page had grown by accretion: model discovery scattered across five cards with a search box each, one Save writing eight keys from the bottom of a card that also held a second Save for something else, and a banner apologising that a button "applies only the settings above it". Now it reads in groups — Accounts, Models, Assistant & prompts, Site — and every card is a <details> that folds, so Save & Close means something. The rule is that each card saves exactly what it shows, which is what removed the need for the banner. Models is one workflow in three steps. Discover & test has a single search box and a kind switch (chat / image / speech / transcription / embedding); the five discovery calls are unchanged, the switch only decides which one answers. Roster is what has been added, including the image roster, which had no visible list before. Availability is the chat model, the two allowed lists, the per-workflow image settings and the slide reviewer, under one Save. Splitting the eight-key save follows from that rule: Save & Close writes the five retrieval and citation keys; Save availability writes the chat model, both allowed lists, the reviewer and the three image-settings PUTs. No route, request shape or setting key changed. Switching kind clears the results first — a row button would otherwise add an image model to the chat roster. The kind switch dispatches its event through document.defaultView's CustomEvent. jsdom refuses one built from another realm, and the existing announceModelsChanged() has exactly that bug: its event is built from the Node global, dispatchEvent refuses it, and a try/catch swallows the error — so models-changed propagation has only ever been source-grepped, never actually tested. Left alone here to keep this change to one subject. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01Dv6sqaY6Vq3ChZHMem3cnU
719 lines
50 KiB
HTML
719 lines
50 KiB
HTML
<div class="module-header">
|
|
<h2><i class="fas fa-user-shield"></i> Admin Panel</h2>
|
|
<p>Manage users, app settings, and view usage statistics</p>
|
|
</div>
|
|
|
|
<!-- Stats -->
|
|
<div class="admin-stats" id="admin-stats">
|
|
<div class="stat-card"><div class="stat-value" id="stat-users">—</div><div class="stat-label">Total Users</div></div>
|
|
<div class="stat-card"><div class="stat-value" id="stat-api-total">—</div><div class="stat-label">Total API Calls</div></div>
|
|
<div class="stat-card"><div class="stat-value" id="stat-api-today">—</div><div class="stat-label">API Calls Today</div></div>
|
|
</div>
|
|
|
|
<!-- Every card is a <details> so a long page can be folded, and so a
|
|
"Save & Close" has something to close. All open by default: nothing
|
|
is hidden from an admin who never notices the chevron. Cards are
|
|
grouped under a heading by what they are about, in the order an
|
|
admin setting up a fresh install would need them. -->
|
|
|
|
<!-- ═══ Accounts ═══════════════════════════════════════════════ -->
|
|
<h2 class="admin-section-title">Accounts</h2>
|
|
|
|
<!-- Registration ─────────────────────────────────────────────
|
|
One card, read top to bottom: open registration at all, then decide
|
|
whether it needs an invitation, then hand out the codes. These were
|
|
two cards far apart, which made "enabled but invite-only" look like
|
|
two unrelated settings instead of one decision. -->
|
|
<details class="card" open>
|
|
<summary class="card-header"><h3><i class="fas fa-door-open"></i> Registration</h3></summary>
|
|
<div class="admin-card-body" style="gap:14px;">
|
|
<div style="display:flex;align-items:center;gap:16px;flex-wrap:wrap;">
|
|
<span id="reg-status-text" style="font-size:13px;color:var(--g600);">Loading...</span>
|
|
<button id="btn-toggle-reg" class="btn-sm btn-primary" type="button">Toggle</button>
|
|
</div>
|
|
|
|
<div class="admin-row">
|
|
<strong class="admin-row-label">Invite only</strong>
|
|
<div style="flex:1;display:flex;flex-direction:column;gap:4px;min-width:0;">
|
|
<label style="display:flex;align-items:center;gap:8px;font-size:13px;">
|
|
<input type="checkbox" id="admin-invite-only">
|
|
Require an invitation code to register
|
|
</label>
|
|
</div>
|
|
</div>
|
|
|
|
<div style="border-top:1px solid var(--g100);padding-top:12px;">
|
|
<label for="admin-invite-note" class="admin-subhead" style="display:block;">Create an invitation</label>
|
|
<div style="display:flex;gap:8px;flex-wrap:wrap;align-items:center;">
|
|
<input type="text" id="admin-invite-note" placeholder="Who is this for? (optional)" style="font-size:13px;padding:6px 10px;border:1px solid var(--g300);border-radius:6px;flex:1;min-width:180px;">
|
|
<label style="font-size:12px;color:var(--g600);display:flex;align-items:center;gap:6px;">Valid for
|
|
<input type="number" id="admin-invite-days" value="7" min="1" max="90" style="width:70px;font-size:13px;padding:6px 8px;border:1px solid var(--g300);border-radius:6px;"> days
|
|
</label>
|
|
<button id="btn-create-invite" class="btn-sm btn-primary" type="button"><i class="fas fa-plus"></i> Create</button>
|
|
</div>
|
|
<div id="admin-invite-new" style="margin-top:10px;"></div>
|
|
<p class="admin-note" style="margin-top:6px;">The code is shown once, here. Only its hash is stored, so it cannot be read again afterwards.</p>
|
|
</div>
|
|
|
|
<div id="admin-invites-list" style="display:flex;flex-direction:column;gap:4px;max-height:340px;overflow-y:auto;"></div>
|
|
<!-- Only ever clears spent codes. Hidden until there are some. -->
|
|
<div style="display:flex;justify-content:flex-end;margin-top:8px;">
|
|
<button id="btn-clear-used-invites" class="btn-sm btn-ghost" type="button" hidden style="color:var(--red);"></button>
|
|
</div>
|
|
</div>
|
|
</details>
|
|
|
|
<!-- Users. The search and refresh sit in a toolbar under the heading
|
|
rather than inside it: a control inside <summary> toggles the card
|
|
when clicked. -->
|
|
<details class="card" open>
|
|
<summary class="card-header"><h3><i class="fas fa-users"></i> Users</h3></summary>
|
|
<div class="admin-toolbar">
|
|
<input id="admin-users-search" type="search" placeholder="Search name or email" style="flex:1;max-width:260px;font-size:13px;padding:5px 10px;border:1px solid var(--g300);border-radius:6px;" aria-label="Search users">
|
|
<button id="btn-refresh-users" class="btn-sm btn-ghost" type="button"><i class="fas fa-rotate"></i> Refresh</button>
|
|
</div>
|
|
<div style="overflow:auto;max-height:420px;">
|
|
<table class="admin-table" id="admin-users-table">
|
|
<thead style="position:sticky;top:0;background:white;z-index:1;">
|
|
<tr>
|
|
<th>Name / Email</th>
|
|
<th>Role</th>
|
|
<th>Status</th>
|
|
<th>Joined</th>
|
|
<th>Actions</th>
|
|
</tr>
|
|
</thead>
|
|
<tbody id="admin-users-body">
|
|
<tr><td colspan="5" style="text-align:center;color:var(--g400);padding:20px;">Loading...</td></tr>
|
|
</tbody>
|
|
</table>
|
|
</div>
|
|
</details>
|
|
|
|
<!-- ── OIDC / SSO Configuration ──────────────────────────────── -->
|
|
<details class="card" open>
|
|
<summary class="card-header"><h3><i class="fas fa-shield-halved"></i> Single Sign-On (OIDC)</h3></summary>
|
|
<div class="admin-card-body">
|
|
<p class="admin-note">Configure OpenID Connect for SSO with Azure AD, Okta, Keycloak, PocketID, Google, etc.<br>Callback URL: <code style="font-size:11px;background:var(--g100);padding:2px 6px;border-radius:4px;" id="oidc-callback-url"></code></p>
|
|
<div class="admin-row">
|
|
<label for="oidc-enabled" class="admin-row-label">Enable SSO:</label>
|
|
<select id="oidc-enabled" class="admin-control">
|
|
<option value="false">Disabled</option>
|
|
<option value="true">Enabled</option>
|
|
</select>
|
|
</div>
|
|
<div class="admin-row">
|
|
<label for="oidc-issuer" class="admin-row-label">Issuer URL:</label>
|
|
<input type="url" id="oidc-issuer" placeholder="https://id.example.com" class="admin-control">
|
|
</div>
|
|
<div class="admin-row">
|
|
<label for="oidc-client-id" class="admin-row-label">Client ID:</label>
|
|
<input type="text" id="oidc-client-id" placeholder="your-client-id" class="admin-control">
|
|
</div>
|
|
<div class="admin-row">
|
|
<label for="oidc-client-secret" class="admin-row-label">Client Secret:</label>
|
|
<input type="password" id="oidc-client-secret" placeholder="Leave blank to keep current" class="admin-control">
|
|
</div>
|
|
<div class="admin-row">
|
|
<label for="oidc-button-label" class="admin-row-label">Button Label:</label>
|
|
<input type="text" id="oidc-button-label" placeholder="Sign in with SSO" class="admin-control">
|
|
</div>
|
|
<div class="admin-row">
|
|
<label for="oidc-disable-local" class="admin-row-label">Disable local login:</label>
|
|
<select id="oidc-disable-local" class="admin-control">
|
|
<option value="false">No (both SSO and local login)</option>
|
|
<option value="true">Yes (SSO only)</option>
|
|
</select>
|
|
</div>
|
|
<div class="admin-save-row">
|
|
<button id="btn-save-oidc" class="btn-sm btn-primary" type="button"><i class="fas fa-floppy-disk"></i> Save OIDC Settings</button>
|
|
<span id="oidc-save-status" role="status" style="font-size:12px;color:var(--g500);"></span>
|
|
</div>
|
|
</div>
|
|
</details>
|
|
|
|
<!-- ── CMS: Email Templates ───────────────────────────────────── -->
|
|
<details class="card" open>
|
|
<summary class="card-header"><h3><i class="fas fa-envelope"></i> Email Templates</h3></summary>
|
|
<div class="admin-card-body" style="gap:16px;">
|
|
<div style="display:flex;align-items:center;gap:12px;flex-wrap:wrap;">
|
|
<label for="cms-email-template" style="font-size:13px;font-weight:600;">Template:</label>
|
|
<select id="cms-email-template" style="font-size:13px;padding:4px 8px;border:1px solid var(--g300);border-radius:6px;">
|
|
<option value="verify">Email Verification</option>
|
|
<option value="reset">Password Reset</option>
|
|
</select>
|
|
</div>
|
|
<div>
|
|
<label for="cms-email-subject" class="admin-subhead" style="display:block;margin-bottom:4px;">Subject</label>
|
|
<input type="text" id="cms-email-subject" style="width:100%;font-size:13px;padding:8px;border:1px solid var(--g300);border-radius:6px;box-sizing:border-box;" placeholder="Email subject...">
|
|
</div>
|
|
<div>
|
|
<label for="cms-email-body" class="admin-subhead" style="display:block;margin-bottom:4px;">Body (plain text — a button link will be appended automatically)</label>
|
|
<textarea id="cms-email-body" rows="4" style="width:100%;font-size:13px;padding:8px;border:1px solid var(--g300);border-radius:6px;resize:vertical;box-sizing:border-box;" placeholder="Email body text..."></textarea>
|
|
</div>
|
|
<div style="display:flex;gap:8px;flex-wrap:wrap;">
|
|
<button id="btn-save-email" class="btn-sm btn-primary" type="button">Save Template</button>
|
|
<button id="btn-test-email" class="btn-sm btn-ghost" type="button"><i class="fas fa-paper-plane"></i> Send Test</button>
|
|
</div>
|
|
</div>
|
|
</details>
|
|
|
|
<!-- ── CMS: SMTP Email Server ─────────────────────────────────── -->
|
|
<details class="card" open>
|
|
<summary class="card-header">
|
|
<h3><i class="fas fa-server"></i> SMTP Email Server</h3>
|
|
<span id="smtp-source-badge" class="admin-badge">Loading...</span>
|
|
</summary>
|
|
<div class="admin-card-body">
|
|
<div style="display:grid;grid-template-columns:1fr 1fr;gap:10px;">
|
|
<div>
|
|
<label for="cms-smtp-host" class="admin-subhead" style="display:block;margin-bottom:4px;">SMTP Host</label>
|
|
<input type="text" id="cms-smtp-host" style="width:100%;font-size:13px;padding:7px;border:1px solid var(--g300);border-radius:6px;box-sizing:border-box;" placeholder="smtp.gmail.com">
|
|
</div>
|
|
<div>
|
|
<label for="cms-smtp-port" class="admin-subhead" style="display:block;margin-bottom:4px;">Port</label>
|
|
<input type="number" id="cms-smtp-port" style="width:100%;font-size:13px;padding:7px;border:1px solid var(--g300);border-radius:6px;box-sizing:border-box;" placeholder="587" value="587">
|
|
</div>
|
|
<div>
|
|
<label for="cms-smtp-user" class="admin-subhead" style="display:block;margin-bottom:4px;">Username / Email</label>
|
|
<input type="text" id="cms-smtp-user" style="width:100%;font-size:13px;padding:7px;border:1px solid var(--g300);border-radius:6px;box-sizing:border-box;" placeholder="user@example.com">
|
|
</div>
|
|
<div>
|
|
<label for="cms-smtp-pass" class="admin-subhead" style="display:block;margin-bottom:4px;">Password / App Password</label>
|
|
<input type="password" id="cms-smtp-pass" style="width:100%;font-size:13px;padding:7px;border:1px solid var(--g300);border-radius:6px;box-sizing:border-box;" placeholder="Leave blank to keep current">
|
|
</div>
|
|
<div>
|
|
<label for="cms-smtp-from" class="admin-subhead" style="display:block;margin-bottom:4px;">From Address</label>
|
|
<input type="text" id="cms-smtp-from" style="width:100%;font-size:13px;padding:7px;border:1px solid var(--g300);border-radius:6px;box-sizing:border-box;" placeholder="Pediatric Scribe <noreply@example.com>">
|
|
</div>
|
|
<div style="display:flex;align-items:flex-end;gap:8px;">
|
|
<label for="cms-smtp-secure" class="admin-subhead" style="display:block;">TLS/SSL (port 465):</label>
|
|
<select id="cms-smtp-secure" style="font-size:13px;padding:7px;border:1px solid var(--g300);border-radius:6px;">
|
|
<option value="false">STARTTLS (587)</option>
|
|
<option value="true">SSL/TLS (465)</option>
|
|
</select>
|
|
</div>
|
|
</div>
|
|
<div style="display:flex;gap:8px;flex-wrap:wrap;">
|
|
<button id="btn-save-smtp" class="btn-sm btn-primary" type="button">Save SMTP</button>
|
|
<button id="btn-clear-smtp" class="btn-sm btn-ghost" type="button"><i class="fas fa-trash"></i> Clear DB (use env)</button>
|
|
</div>
|
|
<p class="admin-note"><i class="fas fa-info-circle"></i> Environment variables take precedence over DB settings. Storing password in DB is convenient but consider using env vars for production security.</p>
|
|
</div>
|
|
</details>
|
|
|
|
<!-- ═══ Models ═════════════════════════════════════════════════
|
|
Three cards, one workflow, read top to bottom. Discover & test finds
|
|
a model on the gateway and adds it; Roster is what has been added;
|
|
Availability says which of the roster each feature may use. These
|
|
used to be spread over three unrelated-looking cards in different
|
|
parts of the page, with a paragraph apologising for it. -->
|
|
<h2 class="admin-section-title">Models</h2>
|
|
<p class="admin-section-intro">
|
|
<strong>1. Discover & test</strong> a model the gateway offers and add it →
|
|
<strong>2. Roster</strong> is everything added, with the default and on/off switches →
|
|
<strong>3. Availability</strong> is which roster models each feature may use.
|
|
</p>
|
|
|
|
<!-- ── Discover & test ───────────────────────────────────────── -->
|
|
<details class="card" open id="admin-discover-card">
|
|
<summary class="card-header">
|
|
<h3><i class="fas fa-magnifying-glass"></i> Discover & test</h3>
|
|
<span class="card-header-note">Search the gateway, try a model, add it to the roster</span>
|
|
</summary>
|
|
<div class="admin-card-body">
|
|
<!-- One search for every kind of model. The kind decides which
|
|
gateway list is asked and what the row buttons do: chat and
|
|
image models are added to a roster; speech, transcription and
|
|
embedding models are single defaults, so their button is Set. -->
|
|
<div class="admin-kind-switch" role="group" aria-label="Kind of model">
|
|
<button type="button" class="admin-discover-kind" id="admin-discover-kind-chat" data-kind="chat" aria-pressed="true"><i class="fas fa-comments"></i> Chat</button>
|
|
<button type="button" class="admin-discover-kind" id="admin-discover-kind-image" data-kind="image" aria-pressed="false"><i class="fas fa-image"></i> Image</button>
|
|
<button type="button" class="admin-discover-kind" id="admin-discover-kind-tts" data-kind="tts" aria-pressed="false"><i class="fas fa-volume-up"></i> Speech (TTS)</button>
|
|
<button type="button" class="admin-discover-kind" id="admin-discover-kind-stt" data-kind="stt" aria-pressed="false"><i class="fas fa-microphone"></i> Transcription (STT)</button>
|
|
<button type="button" class="admin-discover-kind" id="admin-discover-kind-embedding" data-kind="embedding" aria-pressed="false"><i class="fas fa-network-wired"></i> Embedding</button>
|
|
</div>
|
|
|
|
<!-- Provider status for the chosen kind. -->
|
|
<div class="admin-kind-panel admin-kind-status" data-kind="chat">
|
|
<span id="admin-model-provider-badge" class="admin-badge">Loading...</span>
|
|
<span class="admin-note">Chat models write notes, answer in the Clinical Assistant and draft resources. <strong>+ Add</strong> puts one on the roster.</span>
|
|
</div>
|
|
<div class="admin-kind-panel admin-kind-status" data-kind="image" hidden>
|
|
<span id="admin-image-provider-badge" class="admin-badge">Gateway</span>
|
|
<span id="admin-image-info" class="admin-note">Image models come from the LiteLLM gateway. <strong>+ Add</strong> puts one on the image roster; each workflow then picks its own under Availability.</span>
|
|
</div>
|
|
<div class="admin-kind-panel admin-kind-status" data-kind="tts" hidden>
|
|
<span id="admin-tts-provider-badge" class="admin-badge">Loading...</span>
|
|
<span id="admin-tts-info" class="admin-note">Loading...</span>
|
|
<span class="admin-note"><strong>Set</strong> makes a voice or model the default for Read Aloud.</span>
|
|
</div>
|
|
<div class="admin-kind-panel admin-kind-status" data-kind="stt" hidden>
|
|
<span id="admin-stt-provider-badge" class="admin-badge">Loading...</span>
|
|
<span id="admin-stt-info" class="admin-note">Loading...</span>
|
|
<span class="admin-note"><strong>Set</strong> makes a model the default for dictation.</span>
|
|
</div>
|
|
<div class="admin-kind-panel admin-kind-status" data-kind="embedding" hidden>
|
|
<span id="admin-embed-provider-badge" class="admin-badge">Loading...</span>
|
|
<span id="admin-embed-info" class="admin-note">Loading...</span>
|
|
<span class="admin-note">Learning Hub semantic search. Corpus (MCP) embeddings are configured on the indexing service, not here. <strong>Set</strong> makes a model the default.</span>
|
|
</div>
|
|
|
|
<div class="admin-search-row">
|
|
<input type="search" id="admin-discover-search" placeholder="Filter by name (e.g. gemini, gpt, llama)" aria-label="Filter models by name" style="font-size:13px;padding:6px 10px;border:1px solid var(--g300);border-radius:6px;flex:1;min-width:200px;">
|
|
<button id="btn-discover" class="btn-sm btn-primary" type="button"><i class="fas fa-magnifying-glass"></i> Search gateway</button>
|
|
</div>
|
|
<div id="admin-discover-results" style="display:flex;flex-direction:column;gap:4px;max-height:400px;overflow-y:auto;"></div>
|
|
<p id="admin-discover-hint" class="admin-note">Press <strong>Search gateway</strong> to list what it offers for this kind. The box filters by name.</p>
|
|
|
|
<!-- Test. Chat and image accept an id the gateway does not
|
|
advertise, because a model can be routable without being
|
|
listed. -->
|
|
<div style="border-top:1px solid var(--g100);padding-top:12px;">
|
|
<div class="admin-subhead">Test</div>
|
|
<div class="admin-kind-panel" data-kind="chat">
|
|
<p class="admin-note" style="margin-bottom:8px;">Press Test on a model above or on the roster, or type an id the gateway does not advertise.</p>
|
|
<div style="display:flex;gap:8px;align-items:center;flex-wrap:wrap;">
|
|
<input type="text" id="admin-chat-test-model" placeholder="Model id to test" aria-label="Chat model id to test" style="font-size:13px;padding:6px 10px;border:1px solid var(--g300);border-radius:6px;flex:1;min-width:180px;">
|
|
<button id="btn-test-chat-model" class="btn-sm btn-primary" type="button"><i class="fas fa-vial"></i> Test</button>
|
|
</div>
|
|
<div id="admin-chat-test-result" role="status" class="admin-note" style="margin-top:8px;"></div>
|
|
</div>
|
|
<div class="admin-kind-panel" data-kind="image" hidden>
|
|
<p class="admin-note" style="margin-bottom:8px;">Press Test on a model above, or type an id the gateway does not advertise. A picture is generated, so this costs one request.</p>
|
|
<div style="display:flex;gap:8px;align-items:center;flex-wrap:wrap;">
|
|
<input type="text" id="admin-image-test-model" placeholder="Model id to test" aria-label="Image model id to test" style="font-size:13px;padding:6px 10px;border:1px solid var(--g300);border-radius:6px;flex:1;min-width:180px;">
|
|
<button id="btn-test-image-model" class="btn-sm btn-primary" type="button"><i class="fas fa-vial"></i> Test</button>
|
|
</div>
|
|
<div id="admin-image-test-result" role="status" class="admin-note" style="margin-top:8px;"></div>
|
|
</div>
|
|
<div class="admin-kind-panel" data-kind="tts" hidden>
|
|
<div style="display:flex;align-items:center;gap:12px;flex-wrap:wrap;margin-bottom:8px;">
|
|
<label for="admin-tts-voice" style="font-size:13px;font-weight:600;min-width:80px;">Voice:</label>
|
|
<select id="admin-tts-voice" style="font-size:13px;padding:5px 8px;border:1px solid var(--g300);border-radius:6px;flex:1;max-width:280px;"></select>
|
|
</div>
|
|
<textarea id="admin-tts-test-text" rows="2" aria-label="Text to synthesize" style="width:100%;font-size:13px;padding:8px;border:1px solid var(--g300);border-radius:6px;resize:vertical;box-sizing:border-box;" placeholder="Text to synthesize...">Hello, this is a text to speech test for the Pediatric AI Scribe.</textarea>
|
|
<div style="margin-top:8px;display:flex;gap:8px;align-items:center;flex-wrap:wrap;">
|
|
<button id="btn-test-tts" class="btn-sm btn-primary" type="button"><i class="fas fa-play"></i> Synthesize & Play</button>
|
|
<audio id="admin-tts-audio" controls style="display:none;height:32px;flex:1;min-width:180px;"></audio>
|
|
</div>
|
|
<p id="admin-tts-result" role="status" class="admin-note" style="margin-top:6px;"></p>
|
|
</div>
|
|
<div class="admin-kind-panel" data-kind="stt" hidden>
|
|
<div style="display:flex;gap:8px;align-items:center;flex-wrap:wrap;">
|
|
<button id="btn-stt-record" class="btn-sm btn-primary" type="button"><i class="fas fa-microphone"></i> Start Recording</button>
|
|
<span id="admin-stt-recording-status" role="status" style="font-size:12px;color:var(--g500);"></span>
|
|
</div>
|
|
<div id="admin-stt-result" style="margin-top:10px;padding:10px;background:var(--g50);border-radius:6px;font-size:13px;color:var(--g700);display:none;">
|
|
<strong>Transcription:</strong> <span id="admin-stt-text"></span>
|
|
<div id="admin-stt-meta" style="font-size:11px;color:var(--g400);margin-top:4px;"></div>
|
|
</div>
|
|
</div>
|
|
<div class="admin-kind-panel" data-kind="embedding" hidden>
|
|
<div style="display:flex;gap:8px;flex-wrap:wrap;align-items:center;">
|
|
<input type="text" id="admin-embed-test-text" value="Pediatric patient with fever and cough" aria-label="Sample text to embed" style="font-size:13px;padding:6px 10px;border:1px solid var(--g300);border-radius:6px;flex:1;min-width:200px;" placeholder="Sample text to embed...">
|
|
<button id="btn-test-embedding" class="btn-sm btn-primary" type="button"><i class="fas fa-code-branch"></i> Generate</button>
|
|
</div>
|
|
<div id="admin-embed-result" role="status" class="admin-note" style="margin-top:8px;"></div>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
</details>
|
|
|
|
<!-- ── Roster ────────────────────────────────────────────────── -->
|
|
<details class="card" open id="admin-roster-card">
|
|
<summary class="card-header">
|
|
<h3><i class="fas fa-layer-group"></i> Roster</h3>
|
|
<span class="card-header-note">What has been added. Availability draws from this list.</span>
|
|
</summary>
|
|
<div class="admin-card-body" style="gap:14px;">
|
|
<h4>Chat models</h4>
|
|
<div class="admin-row">
|
|
<label for="admin-default-model" class="admin-row-label">Default model</label>
|
|
<select id="admin-default-model" class="admin-control"></select>
|
|
<button id="btn-save-default-model" class="btn-sm btn-primary" type="button">Set default</button>
|
|
</div>
|
|
<p class="admin-note">Used wherever nobody chose a model explicitly. Only enabled models can be the default.</p>
|
|
<div>
|
|
<div class="admin-subhead">Built-in models (untick to hide one from users)</div>
|
|
<div id="admin-builtin-models" style="display:flex;flex-direction:column;gap:4px;max-height:300px;overflow-y:auto;padding-right:4px;">
|
|
<p style="color:var(--g400);font-size:13px;">Loading...</p>
|
|
</div>
|
|
</div>
|
|
<div id="admin-custom-models-list" style="display:flex;flex-direction:column;gap:4px;"></div>
|
|
|
|
<h4>Image models</h4>
|
|
<p class="admin-note">Added with <strong>+ Add</strong> under Discover & test. Under Availability, tick one to offer it in chat and pick one per workflow.</p>
|
|
<div id="admin-image-roster" style="display:flex;flex-direction:column;gap:4px;">
|
|
<p class="admin-note">Loading...</p>
|
|
</div>
|
|
|
|
<p class="admin-note" style="border-top:1px solid var(--g100);padding-top:10px;">Speech, transcription and embedding models are single defaults rather than a roster: choose them with <strong>Set</strong> under Discover & test.</p>
|
|
</div>
|
|
</details>
|
|
|
|
<!-- ── Availability ──────────────────────────────────────────── -->
|
|
<details class="card" open id="admin-availability-card">
|
|
<summary class="card-header">
|
|
<h3><i class="fas fa-list-check"></i> Availability</h3>
|
|
<span class="card-header-note">Which roster models each feature may use</span>
|
|
</summary>
|
|
<div class="admin-card-body">
|
|
<p class="admin-note">Only models on the roster appear here. If one is missing, add it under Discover & test first. One <strong>Save availability</strong> at the bottom applies everything in this card.</p>
|
|
|
|
<h4>Clinical Assistant</h4>
|
|
<div class="admin-row">
|
|
<label for="assistant-chat-model" class="admin-row-label">Chat model</label>
|
|
<select id="assistant-chat-model" class="admin-control"></select>
|
|
<button id="btn-test-assistant-chat-model" class="btn-sm btn-primary" type="button"><i class="fas fa-vial"></i> Test</button>
|
|
</div>
|
|
<div id="assistant-chat-test-result" role="status" class="admin-note"></div>
|
|
<div class="admin-row" style="align-items:flex-start;">
|
|
<strong class="admin-row-label">Chat models users may pick</strong>
|
|
<div style="flex:1;display:flex;flex-direction:column;gap:4px;min-width:0;">
|
|
<div id="assistant-allowed-chat-models" aria-label="Chat models available to the Clinical Assistant"></div>
|
|
<p class="admin-note">Leave every box empty to offer only the chat model above.</p>
|
|
</div>
|
|
</div>
|
|
<div class="admin-row" style="align-items:flex-start;">
|
|
<strong class="admin-row-label">Image models users may pick</strong>
|
|
<div style="flex:1;display:flex;flex-direction:column;gap:4px;min-width:0;">
|
|
<div id="assistant-allowed-image-models" aria-label="Image models available to the Clinical Assistant"></div>
|
|
<p class="admin-note">Leave every box empty to offer only the workflow's own image model.</p>
|
|
</div>
|
|
</div>
|
|
|
|
<h4>Image generation, per workflow</h4>
|
|
<section id="workflow-image-settings" aria-label="Workflow image settings"></section>
|
|
|
|
<!-- The one setting that spends money on every generation without
|
|
a user asking for anything, so it stays Off until a model is
|
|
named here on purpose. -->
|
|
<h4>Slide review (My Resources)</h4>
|
|
<p class="admin-note">
|
|
The model that writes a deck never sees it, so it cannot tell that a slide
|
|
overflowed or that a long list wants two columns. With a reviewer chosen, each
|
|
generated deck is rendered, looked at, and its layout corrected once. Wording and
|
|
figures are never changed — a review that alters them is discarded.
|
|
</p>
|
|
<div class="admin-row">
|
|
<label for="mr-review-model" class="admin-row-label">Reviewer</label>
|
|
<div style="flex:1;display:flex;flex-direction:column;gap:4px;min-width:0;">
|
|
<select id="mr-review-model" class="admin-control" style="max-width:360px;"></select>
|
|
<p class="admin-note">
|
|
Needs to be able to see images. One image per slide is sent on every
|
|
presentation generated, so this costs money each time — a cheap capable
|
|
vision model is the right choice here, not the best one available.
|
|
</p>
|
|
</div>
|
|
</div>
|
|
|
|
<div id="assistant-config-error" class="assistant-config-error" role="alert" hidden
|
|
style="display:flex;align-items:center;gap:10px;flex-wrap:wrap;padding:10px 12px;border:1px solid var(--red);border-radius:8px;background:rgba(220,38,38,0.06);">
|
|
<span style="font-size:13px;color:var(--g700);">
|
|
<i class="fas fa-triangle-exclamation" style="color:var(--red);"></i>
|
|
These settings could not be loaded, so saving is disabled. Nothing you have typed has been lost.
|
|
</span>
|
|
<button id="btn-retry-assistant-config" class="btn-sm btn-ghost btn-retry-assistant-config" type="button">Try again</button>
|
|
</div>
|
|
<div class="admin-save-row">
|
|
<button id="btn-save-availability" class="btn-sm btn-primary" type="button" disabled><i class="fas fa-floppy-disk"></i> Save availability</button>
|
|
<span id="assistant-availability-status" role="status" style="font-size:12px;color:var(--g500);"></span>
|
|
</div>
|
|
</div>
|
|
</details>
|
|
|
|
<!-- ═══ Assistant & prompts ════════════════════════════════════ -->
|
|
<h2 class="admin-section-title">Assistant & prompts</h2>
|
|
|
|
<!-- ── Clinical Assistant ─────────────────────────────────────── -->
|
|
<details class="card" open id="admin-assistant-card">
|
|
<summary class="card-header">
|
|
<h3><i class="fas fa-brain"></i> Clinical Assistant</h3>
|
|
<span class="card-header-note">Retrieval, citations, translation, budget and starter prompts</span>
|
|
</summary>
|
|
<div class="admin-card-body">
|
|
<div style="display:grid;grid-template-columns:1fr 1fr;gap:10px;">
|
|
<div class="admin-row">
|
|
<label for="assistant-search-limit" class="admin-row-label">Retrieval result limit</label>
|
|
<input id="assistant-search-limit" type="number" min="3" max="20" value="8" class="admin-control">
|
|
</div>
|
|
<div class="admin-row">
|
|
<label for="assistant-context-chars" class="admin-row-label">Context per excerpt (characters)</label>
|
|
<input id="assistant-context-chars" type="number" min="300" max="4000" value="1400" class="admin-control">
|
|
</div>
|
|
</div>
|
|
<div class="admin-row">
|
|
<strong class="admin-row-label">Citations</strong>
|
|
<div style="flex:1;display:flex;flex-direction:column;gap:4px;min-width:0;">
|
|
<label style="display:flex;align-items:center;gap:8px;font-size:13px;">
|
|
<input type="checkbox" id="assistant-show-sources" checked>
|
|
Show the Sources panel and numbered citations
|
|
</label>
|
|
<p class="admin-note">Display only. The AI prompt, retrieval and the stored answer are byte-for-byte identical either way, so turning this off cannot change what the model says. Citations are hidden, not removed — turning it back on restores them.</p>
|
|
</div>
|
|
</div>
|
|
<div class="admin-row">
|
|
<label for="assistant-translate-provider" class="admin-row-label">Translation provider</label>
|
|
<select id="assistant-translate-provider" class="admin-control">
|
|
<option value="libretranslate">Local LibreTranslate (patient text stays on this network)</option>
|
|
</select>
|
|
</div>
|
|
<div class="admin-row">
|
|
<label for="assistant-conversation-budget" class="admin-row-label">Conversation input budget</label>
|
|
<div style="flex:1;display:flex;flex-direction:column;gap:4px;min-width:0;">
|
|
<input id="assistant-conversation-budget" type="number" min="1000" max="1000000" class="admin-control" placeholder="e.g. 120000" aria-label="Conversation input budget (UTF-16 code units)">
|
|
<p id="assistant-conversation-budget-meta" role="status" class="admin-note">UTF-16 code units. Leave empty to use CLINICAL_ASSISTANT_CONVERSATION_CHARS.</p>
|
|
</div>
|
|
</div>
|
|
<div style="border:1px solid var(--g100);border-radius:8px;padding:10px;display:flex;gap:10px;align-items:center;justify-content:space-between;flex-wrap:wrap;">
|
|
<div>
|
|
<div style="font-size:13px;font-weight:600;color:var(--g700);">Starter prompt pool</div>
|
|
<div id="assistant-prompt-pool-status" role="status" class="admin-note" style="margin-top:3px;">Checking prompt pool...</div>
|
|
</div>
|
|
<button id="btn-regenerate-assistant-prompt-pool" class="btn-sm btn-ghost" type="button"><i class="fas fa-rotate"></i> Regenerate Pool</button>
|
|
<div style="display:flex;gap:8px;align-items:center;flex-wrap:wrap;width:100%;">
|
|
<select id="assistant-prompt-pool-snapshots" class="admin-control" style="flex:1;min-width:220px;" aria-label="Prompt pool snapshots"><option value="">Loading snapshots...</option></select>
|
|
<button id="btn-restore-assistant-prompt-pool" class="btn-sm btn-ghost" type="button"><i class="fas fa-clock-rotate-left"></i> Restore Snapshot</button>
|
|
</div>
|
|
</div>
|
|
<!-- Same settings load as Availability, so the same failure is
|
|
shown here rather than a Save that is disabled for no stated
|
|
reason. -->
|
|
<div class="assistant-config-error" role="alert" hidden
|
|
style="display:flex;align-items:center;gap:10px;flex-wrap:wrap;padding:10px 12px;border:1px solid var(--red);border-radius:8px;background:rgba(220,38,38,0.06);">
|
|
<span style="font-size:13px;color:var(--g700);">
|
|
<i class="fas fa-triangle-exclamation" style="color:var(--red);"></i>
|
|
These settings could not be loaded, so saving is disabled. Nothing you have typed has been lost.
|
|
</span>
|
|
<button class="btn-sm btn-ghost btn-retry-assistant-config" type="button">Try again</button>
|
|
</div>
|
|
<div class="admin-save-row">
|
|
<button id="btn-save-assistant-config" class="btn-sm btn-primary" type="button" disabled><i class="fas fa-floppy-disk"></i> Save & Close</button>
|
|
<span id="assistant-admin-status" role="status" style="font-size:12px;color:var(--g500);"></span>
|
|
</div>
|
|
</div>
|
|
</details>
|
|
|
|
<!-- ── Prompts ───────────────────────────────────────────────── -->
|
|
<details class="card" open>
|
|
<summary class="card-header">
|
|
<h3><i class="fas fa-robot"></i> Prompts</h3>
|
|
<span class="card-header-note">Each prompt saves on its own</span>
|
|
</summary>
|
|
<div class="admin-card-body" style="gap:16px;">
|
|
<section aria-labelledby="scribe-prompts-heading">
|
|
<h4 id="scribe-prompts-heading">Scribe prompts</h4>
|
|
<div id="cms-scribe-prompts">Loading Scribe prompts...</div>
|
|
</section>
|
|
<section aria-labelledby="clinical-prompts-heading">
|
|
<h4 id="clinical-prompts-heading">Clinical Assistant prompts</h4>
|
|
<div id="cms-clinical-prompts">Loading clinical prompts...</div>
|
|
</section>
|
|
<section aria-labelledby="learning-prompts-heading">
|
|
<h4 id="learning-prompts-heading">Learning prompts</h4>
|
|
<div id="cms-learning-prompts">Loading Learning prompts...</div>
|
|
</section>
|
|
</div>
|
|
</details>
|
|
|
|
<!-- ── Web search ─────────────────────────────────────────────── -->
|
|
<details class="card" open>
|
|
<summary class="card-header">
|
|
<h3><i class="fas fa-globe"></i> Search Sources</h3>
|
|
<span class="card-header-note">Off by default</span>
|
|
</summary>
|
|
<div class="admin-card-body" style="gap:14px;">
|
|
<p style="margin:0;padding:8px 10px;background:rgba(224,142,69,0.10);border:1px solid var(--orange, #e08e45);border-radius:6px;font-size:12px;color:var(--g700);">
|
|
<strong>This sends text outside the building.</strong> When a resource author turns
|
|
it on, the model may send a short search query to the provider below. Only the query
|
|
leaves — never library excerpts, never the generated content, never anything
|
|
about the user — but a query written while drafting clinical material can still
|
|
carry clinical detail, and the provider keeps its own logs. SearXNG is the only
|
|
option here that you host yourself.
|
|
</p>
|
|
|
|
<div class="admin-row">
|
|
<label for="ws-enabled" class="admin-row-label">Enabled</label>
|
|
<div style="flex:1;min-width:0;">
|
|
<select id="ws-enabled" class="admin-control" style="max-width:200px;">
|
|
<option value="false">Disabled</option>
|
|
<option value="true">Enabled</option>
|
|
</select>
|
|
</div>
|
|
</div>
|
|
|
|
<div class="admin-row">
|
|
<label for="ws-provider" class="admin-row-label">Provider</label>
|
|
<div style="flex:1;min-width:0;">
|
|
<select id="ws-provider" class="admin-control" style="max-width:260px;">
|
|
<option value="tavily">Tavily — built for AI, clean extracts</option>
|
|
<option value="serper">Serper — Google results</option>
|
|
<option value="brave">Brave Search API</option>
|
|
<option value="searxng">SearXNG — self-hosted, no third party</option>
|
|
</select>
|
|
</div>
|
|
</div>
|
|
|
|
<div class="admin-row">
|
|
<label for="ws-api-key" class="admin-row-label">API key</label>
|
|
<div style="flex:1;display:flex;flex-direction:column;gap:4px;min-width:0;">
|
|
<input id="ws-api-key" type="password" class="admin-control" placeholder="Leave blank to keep the current key" autocomplete="off">
|
|
<p class="admin-note">Not needed for SearXNG. Shown masked once saved.</p>
|
|
</div>
|
|
</div>
|
|
|
|
<div class="admin-row">
|
|
<label for="ws-base-url" class="admin-row-label">SearXNG URL</label>
|
|
<div style="flex:1;display:flex;flex-direction:column;gap:4px;min-width:0;">
|
|
<input id="ws-base-url" type="text" class="admin-control" placeholder="https://searxng.example.com">
|
|
<p class="admin-note">Only used when the provider is SearXNG. Its <code>/search?format=json</code> endpoint must be enabled.</p>
|
|
</div>
|
|
</div>
|
|
|
|
<div style="border-top:1px solid var(--g100);padding-top:12px;">
|
|
<div style="font-size:13px;font-weight:600;color:var(--g700);margin-bottom:8px;">
|
|
<i class="fas fa-book-medical"></i> PubMed
|
|
</div>
|
|
<p class="admin-note" style="margin-bottom:10px;">
|
|
A separate source from the web providers above, and a separate tick box for
|
|
authors. It returns structured records — title, journal, year, PMID —
|
|
so a reference can be exact rather than reconstructed from a page title. Queries
|
|
go to NCBI, not to a commercial provider.
|
|
</p>
|
|
|
|
<div class="admin-row">
|
|
<label for="pm-enabled" class="admin-row-label">Enabled</label>
|
|
<div style="flex:1;min-width:0;">
|
|
<select id="pm-enabled" class="admin-control" style="max-width:200px;">
|
|
<option value="false">Disabled</option>
|
|
<option value="true">Enabled</option>
|
|
</select>
|
|
</div>
|
|
</div>
|
|
|
|
<div class="admin-row">
|
|
<label for="pm-api-key" class="admin-row-label">NCBI API key</label>
|
|
<div style="flex:1;display:flex;flex-direction:column;gap:4px;min-width:0;">
|
|
<input id="pm-api-key" type="password" class="admin-control" placeholder="Optional — leave blank to keep the current key" autocomplete="off">
|
|
<p class="admin-note">
|
|
Optional. PubMed works without one; a key raises the rate limit from 3
|
|
requests per second to 10, which matters for bulk work rather than for one
|
|
person writing a resource. Create one at
|
|
<a href="https://account.ncbi.nlm.nih.gov/settings/" target="_blank" rel="noopener noreferrer">account.ncbi.nlm.nih.gov/settings</a> — sign in, then <strong>API Key Management</strong> → <strong>Create an API Key</strong>.
|
|
</p>
|
|
</div>
|
|
</div>
|
|
|
|
<div class="admin-row">
|
|
<label for="pm-email" class="admin-row-label">Contact email</label>
|
|
<div style="flex:1;display:flex;flex-direction:column;gap:4px;min-width:0;">
|
|
<input id="pm-email" type="email" class="admin-control" placeholder="you@example.com">
|
|
<p class="admin-note">NCBI asks callers to identify themselves so they can contact you before blocking you. Optional but courteous.</p>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
|
|
<div class="admin-save-row">
|
|
<button id="btn-save-websearch" class="btn-sm btn-primary" type="button"><i class="fas fa-floppy-disk"></i> Save search settings</button>
|
|
<button id="btn-test-websearch" class="btn-sm btn-ghost" type="button"><i class="fas fa-vial"></i> Test</button>
|
|
<span id="ws-status" role="status" style="font-size:12px;color:var(--g500);"></span>
|
|
</div>
|
|
</div>
|
|
</details>
|
|
|
|
<!-- ── Citation quality ───────────────────────────────────────── -->
|
|
<details class="card" open>
|
|
<summary class="card-header">
|
|
<h3><i class="fas fa-quote-right"></i> Citation Quality</h3>
|
|
<span id="admin-citation-summary" class="admin-badge">Loading...</span>
|
|
</summary>
|
|
<div class="admin-card-body">
|
|
<p class="admin-note">Answers where the assistant wrote a citation number that no returned source matched. Those markers are never turned into links, so a reader sees plain text — this is where they are recorded. Consistent entries usually mean retrieval is returning fewer sources than the answer assumes.</p>
|
|
<div><button id="btn-view-citation-audit" class="btn-sm btn-primary" type="button"><i class="fas fa-list"></i> View recent</button></div>
|
|
</div>
|
|
</details>
|
|
|
|
<!-- ═══ Site ═══════════════════════════════════════════════════ -->
|
|
<h2 class="admin-section-title">Site</h2>
|
|
|
|
<!-- ── CMS: Announcement Banner ──────────────────────────────── -->
|
|
<details class="card" open>
|
|
<summary class="card-header"><h3><i class="fas fa-bullhorn"></i> Announcement Banner</h3></summary>
|
|
<div class="admin-card-body">
|
|
<div style="display:flex;align-items:center;gap:12px;flex-wrap:wrap;">
|
|
<label for="cms-ann-enabled" style="font-size:13px;font-weight:600;">Show banner:</label>
|
|
<select id="cms-ann-enabled" style="font-size:13px;padding:4px 8px;border:1px solid var(--g300);border-radius:6px;">
|
|
<option value="false">Hidden</option>
|
|
<option value="true">Visible</option>
|
|
</select>
|
|
<select id="cms-ann-type" aria-label="Banner style" style="font-size:13px;padding:4px 8px;border:1px solid var(--g300);border-radius:6px;">
|
|
<option value="info">Info (blue)</option>
|
|
<option value="warning">Warning (amber)</option>
|
|
<option value="error">Error (red)</option>
|
|
<option value="success">Success (green)</option>
|
|
</select>
|
|
</div>
|
|
<textarea id="cms-ann-text" rows="2" aria-label="Announcement message" style="font-size:13px;padding:8px;border:1px solid var(--g300);border-radius:6px;resize:vertical;" placeholder="Announcement message..."></textarea>
|
|
<div>
|
|
<button id="btn-save-announcement" class="btn-sm btn-primary" type="button">Save & Publish</button>
|
|
</div>
|
|
</div>
|
|
</details>
|
|
|
|
<!-- ── CMS: Feature Flags ─────────────────────────────────────── -->
|
|
<details class="card" open>
|
|
<summary class="card-header"><h3><i class="fas fa-toggle-on"></i> Feature Flags</h3></summary>
|
|
<div class="admin-card-body" style="gap:10px;" id="cms-feature-flags">
|
|
<div style="display:flex;align-items:center;gap:12px;">
|
|
<label for="cms-flag-read-aloud" style="font-size:13px;font-weight:600;min-width:160px;">Read Aloud (TTS):</label>
|
|
<select id="cms-flag-read-aloud" style="font-size:13px;padding:4px 8px;border:1px solid var(--g300);border-radius:6px;">
|
|
<option value="true">Enabled</option>
|
|
<option value="false">Disabled</option>
|
|
</select>
|
|
</div>
|
|
<div style="display:flex;align-items:center;gap:12px;">
|
|
<label for="cms-flag-nextcloud" style="font-size:13px;font-weight:600;min-width:160px;">Nextcloud Integration:</label>
|
|
<select id="cms-flag-nextcloud" style="font-size:13px;padding:4px 8px;border:1px solid var(--g300);border-radius:6px;">
|
|
<option value="true">Enabled</option>
|
|
<option value="false">Disabled</option>
|
|
</select>
|
|
</div>
|
|
<div>
|
|
<button id="btn-save-flags" class="btn-sm btn-primary" type="button"><i class="fas fa-floppy-disk"></i> Save feature flags</button>
|
|
<span id="cms-flags-status" role="status" style="font-size:12px;color:var(--g500);margin-left:8px;"></span>
|
|
</div>
|
|
</div>
|
|
</details>
|
|
|
|
<!-- ── CMS: Saved Encounters (Admin View) ─────────────────────── -->
|
|
<details class="card" open>
|
|
<summary class="card-header">
|
|
<h3><i class="fas fa-database"></i> Saved Encounters (Site-wide)</h3>
|
|
<span class="card-header-note">Auto-deleted after <span id="admin-auto-delete-days">7</span> days</span>
|
|
</summary>
|
|
<div class="admin-card-body" style="gap:10px;">
|
|
<div style="display:flex;align-items:center;gap:12px;flex-wrap:wrap;">
|
|
<label for="cms-auto-delete-days" style="font-size:13px;font-weight:600;">Auto-delete after:</label>
|
|
<select id="cms-auto-delete-days" style="font-size:13px;padding:4px 8px;border:1px solid var(--g300);border-radius:6px;">
|
|
<option value="1">1 day</option>
|
|
<option value="3">3 days</option>
|
|
<option value="7" selected>7 days</option>
|
|
<option value="14">14 days</option>
|
|
<option value="30">30 days</option>
|
|
</select>
|
|
<button id="btn-save-auto-delete" class="btn-sm btn-primary" type="button">Save</button>
|
|
</div>
|
|
</div>
|
|
</details>
|
|
|
|
<!-- ── CMS: Reset to Defaults ─────────────────────────────────── -->
|
|
<details class="card" open style="border:1px solid var(--red-light);">
|
|
<summary class="card-header"><h3 style="color:var(--red);"><i class="fas fa-triangle-exclamation"></i> Reset Settings</h3></summary>
|
|
<div class="admin-card-body" style="gap:10px;">
|
|
<p style="font-size:13px;color:var(--g600);margin:0;">Reset all announcement, feature flag, and email settings back to factory defaults. SMTP configuration and custom models are preserved.</p>
|
|
<div>
|
|
<button id="btn-reset-all-defaults" class="btn-sm" type="button" style="background:var(--red);color:white;border:none;border-radius:6px;padding:6px 14px;font-size:13px;cursor:pointer;"><i class="fas fa-rotate-left"></i> Reset All to Defaults</button>
|
|
</div>
|
|
</div>
|
|
</details>
|