Fix quiz SQL bug, security hardening, componentize HTML, speed improvements

- Fix: "missing FROM-clause entry for table q" in quiz submission query
- Security: replace broken sanitizeHtml with allowlist-based sanitizer (prevents stored XSS)
- Security: whitelist table names in uniqueSlug() to prevent SQL injection pattern
- Security: replace raw err.message responses with generic errors in learning routes
- Security: cap admin logs query LIMIT to 500
- Refactor: break 1685-line index.html into 12 lazy-loaded component files (280 lines shell)
- Speed: add preconnect hints for Google Fonts and CDN
- Speed: defer all script tags for faster initial paint
- Speed: lazy-load tab HTML on activation (only active tab's DOM is parsed)
- Update: replace HIPAA notice with institution-guidelines cautionary note
This commit is contained in:
Daniel Onyejesi 2026-03-23 20:01:54 -04:00
parent 4af622913f
commit 4064ccf516
19 changed files with 1529 additions and 1467 deletions

View file

@ -0,0 +1,307 @@
<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>
<!-- Registration -->
<div class="card">
<div class="card-header"><h3><i class="fas fa-door-open"></i> Registration</h3></div>
<div style="padding:16px;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">Toggle</button>
</div>
</div>
<!-- Users -->
<div class="card">
<div class="card-header">
<h3><i class="fas fa-users"></i> Users</h3>
<button id="btn-refresh-users" class="btn-sm btn-ghost"><i class="fas fa-rotate"></i> Refresh</button>
</div>
<div style="overflow-x:auto;">
<table class="admin-table" id="admin-users-table">
<thead>
<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>
</div>
<!-- ── CMS: Announcement Banner ──────────────────────────────── -->
<div class="card">
<div class="card-header"><h3><i class="fas fa-bullhorn"></i> Announcement Banner</h3></div>
<div style="padding:16px;display:flex;flex-direction:column;gap:12px;">
<div style="display:flex;align-items:center;gap:12px;flex-wrap:wrap;">
<label 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" 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" 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">Save &amp; Publish</button>
</div>
</div>
</div>
<!-- ── CMS: Feature Flags ─────────────────────────────────────── -->
<div class="card">
<div class="card-header"><h3><i class="fas fa-toggle-on"></i> Feature Flags</h3></div>
<div style="padding:16px;display:flex;flex-direction:column;gap:10px;" id="cms-feature-flags">
<div style="display:flex;align-items:center;gap:12px;">
<label 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 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">Save Flags</button>
</div>
</div>
</div>
<!-- ── CMS: Email Templates ───────────────────────────────────── -->
<div class="card">
<div class="card-header"><h3><i class="fas fa-envelope"></i> Email Templates</h3></div>
<div style="padding:16px;display:flex;flex-direction:column;gap:16px;">
<!-- Email template selector -->
<div style="display:flex;align-items:center;gap:12px;flex-wrap:wrap;">
<label 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 style="font-size:12px;font-weight:600;color:var(--g600);margin-bottom:4px;display:block;">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 style="font-size:12px;font-weight:600;color:var(--g600);margin-bottom:4px;display:block;">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">Save Template</button>
<button id="btn-test-email" class="btn-sm btn-ghost"><i class="fas fa-paper-plane"></i> Send Test</button>
</div>
</div>
</div>
<!-- ── CMS: AI Prompts ────────────────────────────────────────── -->
<div class="card">
<div class="card-header">
<h3><i class="fas fa-robot"></i> AI Prompts</h3>
<span style="font-size:12px;color:var(--g500);">Changes take effect immediately, no restart needed</span>
</div>
<div style="padding:16px;display:flex;flex-direction:column;gap:12px;">
<div style="display:flex;align-items:center;gap:12px;flex-wrap:wrap;">
<label style="font-size:13px;font-weight:600;">Prompt:</label>
<select id="cms-prompt-select" style="font-size:13px;padding:4px 8px;border:1px solid var(--g300);border-radius:6px;max-width:320px;"></select>
</div>
<textarea id="cms-prompt-text" rows="10" style="width:100%;font-size:12px;font-family:monospace;padding:8px;border:1px solid var(--g300);border-radius:6px;resize:vertical;box-sizing:border-box;" placeholder="Loading..."></textarea>
<div style="display:flex;gap:8px;flex-wrap:wrap;">
<button id="btn-save-prompt" class="btn-sm btn-primary">Save Prompt</button>
<button id="btn-reset-prompt" class="btn-sm btn-ghost"><i class="fas fa-rotate-left"></i> Reset to Default</button>
</div>
</div>
</div>
<!-- ── CMS: SMTP Email Server ─────────────────────────────────── -->
<div class="card">
<div class="card-header">
<h3><i class="fas fa-server"></i> SMTP Email Server</h3>
<span id="smtp-source-badge" style="font-size:11px;padding:2px 8px;border-radius:10px;background:var(--g100);color:var(--g600);">Loading...</span>
</div>
<div style="padding:16px;display:flex;flex-direction:column;gap:12px;">
<div style="display:grid;grid-template-columns:1fr 1fr;gap:10px;">
<div>
<label style="font-size:12px;font-weight:600;color:var(--g600);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 style="font-size:12px;font-weight:600;color:var(--g600);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 style="font-size:12px;font-weight:600;color:var(--g600);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 style="font-size:12px;font-weight:600;color:var(--g600);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 style="font-size:12px;font-weight:600;color:var(--g600);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 &lt;noreply@example.com&gt;">
</div>
<div style="display:flex;align-items:flex-end;gap:8px;">
<label style="font-size:12px;font-weight:600;color:var(--g600);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">Save SMTP</button>
<button id="btn-clear-smtp" class="btn-sm btn-ghost"><i class="fas fa-trash"></i> Clear DB (use env)</button>
</div>
<p style="font-size:12px;color:var(--g500);margin:0;"><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>
</div>
<!-- ── CMS: Saved Encounters (Admin View) ─────────────────────── -->
<div class="card">
<div class="card-header">
<h3><i class="fas fa-database"></i> Saved Encounters (Site-wide)</h3>
<span style="font-size:12px;color:var(--g500);">Auto-deleted after <span id="admin-auto-delete-days">7</span> days</span>
</div>
<div style="padding:16px;display:flex;flex-direction:column;gap:10px;">
<div style="display:flex;align-items:center;gap:12px;flex-wrap:wrap;">
<label 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">Save</button>
</div>
</div>
</div>
<!-- ── CMS: Learning Hub ──────────────────────────────────────── -->
<div class="card">
<div class="card-header">
<h3><i class="fas fa-graduation-cap"></i> Learning Hub CMS</h3>
<span id="lh-cms-stats" style="font-size:12px;color:var(--g500);"></span>
</div>
<div style="padding:16px;display:flex;flex-direction:column;gap:16px;">
<!-- Categories management -->
<div>
<h4 style="margin:0 0 8px;font-size:14px;"><i class="fas fa-folder"></i> Categories</h4>
<div id="lh-cms-categories" style="margin-bottom:8px;"></div>
<div style="display:flex;gap:8px;align-items:center;flex-wrap:wrap;">
<input type="text" id="lh-cms-cat-name" placeholder="New category name" style="font-size:13px;padding:6px 10px;border:1px solid var(--g300);border-radius:6px;flex:1;min-width:150px;">
<button id="btn-lh-add-cat" class="btn-sm btn-primary"><i class="fas fa-plus"></i> Add Category</button>
</div>
</div>
<hr style="border:none;border-top:1px solid var(--g200);margin:0;">
<!-- Content management -->
<div>
<div style="display:flex;justify-content:space-between;align-items:center;margin-bottom:8px;">
<h4 style="margin:0;font-size:14px;"><i class="fas fa-file-alt"></i> Content</h4>
<button id="btn-lh-new-content" class="btn-sm btn-primary"><i class="fas fa-plus"></i> New Content</button>
</div>
<div id="lh-cms-content-list" style="max-height:400px;overflow-y:auto;"></div>
</div>
</div>
</div>
<!-- Learning Hub Content Editor (hidden by default) -->
<div id="lh-cms-editor" class="card hidden">
<div class="card-header">
<h3 id="lh-cms-editor-title"><i class="fas fa-edit"></i> New Content</h3>
<button id="btn-lh-close-editor" class="btn-sm btn-ghost"><i class="fas fa-times"></i> Close</button>
</div>
<div style="padding:16px;display:flex;flex-direction:column;gap:12px;">
<input type="hidden" id="lh-cms-edit-id" value="">
<div style="display:grid;grid-template-columns:1fr 1fr;gap:10px;">
<div>
<label style="font-size:12px;font-weight:600;color:var(--g600);display:block;margin-bottom:4px;">Title</label>
<input type="text" id="lh-cms-edit-title" style="width:100%;font-size:13px;padding:7px;border:1px solid var(--g300);border-radius:6px;box-sizing:border-box;" placeholder="Content title">
</div>
<div>
<label style="font-size:12px;font-weight:600;color:var(--g600);display:block;margin-bottom:4px;">Subject</label>
<input type="text" id="lh-cms-edit-subject" style="width:100%;font-size:13px;padding:7px;border:1px solid var(--g300);border-radius:6px;box-sizing:border-box;" placeholder="e.g., Development, Cardiology">
</div>
<div>
<label style="font-size:12px;font-weight:600;color:var(--g600);display:block;margin-bottom:4px;">Category</label>
<select id="lh-cms-edit-category" style="width:100%;font-size:13px;padding:7px;border:1px solid var(--g300);border-radius:6px;box-sizing:border-box;">
<option value="">Uncategorized</option>
</select>
</div>
<div>
<label style="font-size:12px;font-weight:600;color:var(--g600);display:block;margin-bottom:4px;">Type</label>
<select id="lh-cms-edit-type" style="width:100%;font-size:13px;padding:7px;border:1px solid var(--g300);border-radius:6px;box-sizing:border-box;">
<option value="article">Article</option>
<option value="quiz">Quiz Only</option>
<option value="pearl">Clinical Pearl</option>
</select>
</div>
</div>
<div>
<label style="font-size:12px;font-weight:600;color:var(--g600);display:block;margin-bottom:4px;">Body (supports basic HTML)</label>
<textarea id="lh-cms-edit-body" rows="10" style="width:100%;font-size:13px;padding:8px;border:1px solid var(--g300);border-radius:6px;resize:vertical;box-sizing:border-box;font-family:inherit;" placeholder="Content body..."></textarea>
</div>
<div style="display:flex;align-items:center;gap:12px;">
<label style="font-size:13px;font-weight:600;">Published:</label>
<select id="lh-cms-edit-published" style="font-size:13px;padding:4px 8px;border:1px solid var(--g300);border-radius:6px;">
<option value="false">Draft</option>
<option value="true">Published</option>
</select>
</div>
<!-- Quiz Questions Builder -->
<hr style="border:none;border-top:1px solid var(--g200);margin:0;">
<div>
<div style="display:flex;justify-content:space-between;align-items:center;margin-bottom:8px;">
<h4 style="margin:0;font-size:14px;"><i class="fas fa-clipboard-question"></i> Quiz Questions</h4>
<button id="btn-lh-add-question" class="btn-sm btn-ghost"><i class="fas fa-plus"></i> Add Question</button>
</div>
<div id="lh-cms-questions"></div>
</div>
<div style="display:flex;gap:8px;">
<button id="btn-lh-save-content" class="btn-sm btn-primary"><i class="fas fa-save"></i> Save Content</button>
<button id="btn-lh-delete-content" class="btn-sm hidden" style="background:var(--red-light);color:var(--red);border:none;border-radius:6px;padding:6px 14px;font-size:13px;cursor:pointer;"><i class="fas fa-trash"></i> Delete</button>
</div>
</div>
</div>
<!-- ── CMS: Reset to Defaults ─────────────────────────────────── -->
<div class="card" style="border:1px solid var(--red-light);">
<div class="card-header"><h3 style="color:var(--red);"><i class="fas fa-triangle-exclamation"></i> Reset Settings</h3></div>
<div style="padding:16px;display:flex;flex-direction:column;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" 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>
</div>

View file

@ -0,0 +1,7 @@
<div class="module-header">
<h2><i class="fas fa-rotate"></i> Catch-Up Schedule</h2>
<p>ACIP catch-up immunization schedule for persons aged 4 months18 years</p>
</div>
<div id="wv-panel-catchup">
<p class="wv-loading">Loading catch-up schedule...</p>
</div>

View file

@ -0,0 +1,104 @@
<div class="module-header">
<h2><i class="fas fa-clipboard-list"></i> Chart Review / Precharting</h2>
<p>Paste clinic notes, subspecialty notes, ED notes, labs → AI summarizes</p>
</div>
<div class="demographics-bar">
<div class="demo-field"><label>Patient Age</label><input type="text" id="cr-age" placeholder="e.g., 8 years"></div>
<div class="demo-field"><label>Gender</label><select id="cr-gender"><option value="">Select</option><option>Male</option><option>Female</option></select></div>
<div class="demo-field"><label>PMH</label><input type="text" id="cr-pmh" placeholder="e.g., hypothyroidism, vitiligo"></div>
<div class="demo-field">
<label>Review Type</label>
<select id="cr-type">
<option value="outpatient">Outpatient Clinic</option>
<option value="subspecialty">Subspecialty</option>
<option value="ed">ED Visit</option>
</select>
</div>
<div class="demo-field demo-field-model">
<label><i class="fas fa-robot"></i> Model</label>
<select class="tab-model-select"></select>
</div>
</div>
<!-- Save/Load bar -->
<div class="save-bar-wrap">
<div class="save-bar" id="chart-save-bar">
<div style="display:flex;align-items:center;gap:8px;flex:1;">
<i class="fas fa-tag" style="color:var(--g400);font-size:13px;"></i>
<input type="text" id="chart-label" class="save-label-input" placeholder="Patient label (e.g. John D, Visit #42)">
</div>
<button id="btn-chart-save" class="btn-sm btn-ghost"><i class="fas fa-floppy-disk"></i> Save</button>
<button id="btn-chart-load" class="btn-sm btn-ghost"><i class="fas fa-folder-open"></i> Load</button>
<button id="btn-chart-new" class="btn-sm btn-ghost" title="Clear all and start new patient" style="color:var(--red);"><i class="fas fa-rotate-left"></i> New</button>
</div>
<div id="chart-load-popover" class="enc-load-popover hidden">
<div class="enc-load-popover-inner">
<input type="text" id="chart-load-search" class="enc-load-search" placeholder="Search saved encounters...">
<button class="enc-pop-close btn-sm btn-ghost"><i class="fas fa-times"></i></button>
</div>
<div id="chart-pop-list" class="enc-pop-list"></div>
</div>
</div>
<!-- Visits -->
<div id="cr-visits-container">
<div class="card visit-card">
<div class="card-header">
<h3><i class="fas fa-calendar"></i> Visit / Note #1</h3>
<button class="btn-sm btn-ghost remove-visit-btn"><i class="fas fa-trash"></i></button>
</div>
<div class="note-entry">
<div class="note-meta">
<input type="date" class="visit-date">
<select class="visit-type">
<option value="outpatient">Outpatient Visit</option>
<option value="subspecialty">Subspecialty Note</option>
<option value="ed">ED Visit</option>
</select>
<input type="text" class="visit-specialist" placeholder="Specialist name (if subspecialty)">
<input type="text" class="visit-specialty" placeholder="Specialty (e.g., Endocrinology)">
</div>
<div class="editable-box visit-content" contenteditable="true" data-placeholder="Paste note here..."></div>
<textarea class="visit-labs" placeholder="Labs from this visit (optional)" rows="3"></textarea>
</div>
</div>
</div>
<button id="cr-add-visit" class="btn-sm btn-ghost" style="margin:8px 0"><i class="fas fa-plus"></i> Add Visit/Note</button>
<!-- Labs -->
<div class="card">
<div class="card-header"><h3><i class="fas fa-flask"></i> Additional Labs</h3><button id="cr-add-lab" class="btn-sm btn-ghost"><i class="fas fa-plus"></i> Add</button></div>
<div id="cr-labs-container">
<div class="lab-entry">
<input type="date" class="lab-date">
<textarea class="lab-values" placeholder="Lab results..." rows="3"></textarea>
</div>
</div>
</div>
<div class="card">
<div class="card-header"><h3><i class="fas fa-comment-dots"></i> Instructions (optional)</h3></div>
<input type="text" id="cr-instructions" class="full-input" placeholder="e.g., 'Focus on thyroid management', 'Include medication changes'">
</div>
<button id="cr-generate-btn" class="btn-generate btn-generate-green"><i class="fas fa-wand-magic-sparkles"></i> Generate Chart Review</button>
<div id="cr-output" class="card output-card hidden">
<div class="card-header output-header">
<h3><i class="fas fa-file-medical"></i> Chart Review</h3>
<div class="output-actions">
<span id="cr-model-tag" class="model-tag"></span>
<button class="btn-sm btn-primary" onclick="copyText('cr-review-text')"><i class="fas fa-copy"></i> Copy</button>
<button class="btn-sm btn-ghost" onclick="speakText('cr-review-text')"><i class="fas fa-volume-high"></i> Read</button>
<button class="btn-sm btn-ghost" onclick="exportToNextcloud('cr-review-text','chart-review')"><i class="fas fa-cloud-arrow-up"></i></button>
</div>
</div>
<div id="cr-review-text" class="output-text" contenteditable="true"></div>
<div class="refine-bar">
<input type="text" id="cr-refine-input" class="refine-input" placeholder="Tell AI to modify...">
<button class="btn-sm btn-primary" id="cr-refine-btn"><i class="fas fa-edit"></i> Refine</button>
<button class="btn-sm btn-ghost" id="cr-shorten-btn"><i class="fas fa-compress"></i> Shorter</button>
</div>
</div>

View file

@ -0,0 +1,83 @@
<div class="module-header">
<h2><i class="fas fa-microphone"></i> Voice Dictation → HPI</h2>
<p>Dictate your narrative → AI restructures into polished HPI</p>
</div>
<div class="demographics-bar">
<div class="demo-field"><label>Patient Age</label><input type="text" id="dict-age" placeholder="e.g., 8 months"></div>
<div class="demo-field"><label>Gender</label><select id="dict-gender"><option value="">Select</option><option>Male</option><option>Female</option></select></div>
<div class="demo-field">
<label>Setting</label>
<select id="dict-setting">
<option value="outpatient">Outpatient</option>
<option value="inpatient">Inpatient/Floors</option>
<option value="ed">Emergency Dept</option>
</select>
</div>
<div class="demo-field demo-field-model">
<label><i class="fas fa-robot"></i> Model</label>
<select class="tab-model-select"></select>
</div>
</div>
<!-- Save/Load bar -->
<div class="save-bar-wrap">
<div class="save-bar" id="dict-save-bar">
<div style="display:flex;align-items:center;gap:8px;flex:1;">
<i class="fas fa-tag" style="color:var(--g400);font-size:13px;"></i>
<input type="text" id="dict-label" class="save-label-input" placeholder="Patient label (e.g. John D, Visit #42)">
</div>
<button id="btn-dict-save" class="btn-sm btn-ghost"><i class="fas fa-floppy-disk"></i> Save</button>
<button id="btn-dict-load" class="btn-sm btn-ghost"><i class="fas fa-folder-open"></i> Load</button>
<button id="btn-dict-new" class="btn-sm btn-ghost" title="Clear all and start new patient" style="color:var(--red);"><i class="fas fa-rotate-left"></i> New</button>
</div>
<div id="dict-load-popover" class="enc-load-popover hidden">
<div class="enc-load-popover-inner">
<input type="text" id="dict-load-search" class="enc-load-search" placeholder="Search saved encounters...">
<button class="enc-pop-close btn-sm btn-ghost"><i class="fas fa-times"></i></button>
</div>
<div id="dict-pop-list" class="enc-pop-list"></div>
</div>
</div>
<div class="card">
<div class="record-controls">
<button id="dict-record-btn" class="record-btn btn-purple"><i class="fas fa-microphone"></i><span>Start Dictation</span></button>
<button id="dict-pause-btn" class="btn-sm btn-ghost hidden" style="margin-left:8px;"><i class="fas fa-pause"></i> Pause</button>
<div id="dict-recording-indicator" class="recording-indicator hidden"><div class="pulse-dot pulse-purple"></div><span>Dictating... <span id="dict-timer">00:00</span></span></div>
</div>
</div>
<div class="card">
<div class="card-header"><h3><i class="fas fa-file-lines"></i> Dictation</h3><button id="dict-clear" class="btn-sm btn-ghost"><i class="fas fa-eraser"></i> Clear</button></div>
<div id="dict-transcript" class="editable-box" contenteditable="true" data-placeholder="Dictation appears here or type directly..."></div>
</div>
<div class="soap-options">
<label>Generate as:</label>
<select id="dict-output-type">
<option value="hpi">HPI Only</option>
<option value="soap-full">Full SOAP Note</option>
<option value="soap-subjective">SOAP Subjective Only</option>
</select>
</div>
<button id="dict-generate-btn" class="btn-generate btn-generate-purple"><i class="fas fa-wand-magic-sparkles"></i> Generate</button>
<div id="dict-output" class="card output-card hidden">
<div class="card-header output-header">
<h3><i class="fas fa-file-medical"></i> Output</h3>
<div class="output-actions">
<span id="dict-model-tag" class="model-tag"></span>
<button class="btn-sm btn-primary" onclick="copyText('dict-hpi-text')"><i class="fas fa-copy"></i> Copy</button>
<button class="btn-sm btn-ghost" onclick="speakText('dict-hpi-text')"><i class="fas fa-volume-high"></i> Read</button>
<button class="btn-sm btn-ghost" onclick="exportToNextcloud('dict-hpi-text','hpi-dictation')"><i class="fas fa-cloud-arrow-up"></i></button>
</div>
</div>
<div id="dict-hpi-text" class="output-text" contenteditable="true"></div>
<div class="refine-bar">
<input type="text" id="dict-refine-input" class="refine-input" placeholder="Tell AI to modify...">
<button class="btn-sm btn-primary" id="dict-refine-btn"><i class="fas fa-edit"></i> Refine</button>
<button class="btn-sm btn-ghost" id="dict-shorten-btn"><i class="fas fa-compress"></i> Shorter</button>
</div>
</div>

View file

@ -0,0 +1,80 @@
<div class="module-header">
<h2><i class="fas fa-comments"></i> Live Encounter → HPI</h2>
<p>Record doctor-patient conversation → AI generates structured HPI</p>
</div>
<div class="demographics-bar">
<div class="demo-field">
<label>Patient Age</label>
<input type="text" id="enc-age" placeholder="e.g., 4 years">
</div>
<div class="demo-field">
<label>Gender</label>
<select id="enc-gender"><option value="">Select</option><option>Male</option><option>Female</option></select>
</div>
<div class="demo-field">
<label>Setting</label>
<select id="enc-setting">
<option value="outpatient">Outpatient</option>
<option value="inpatient">Inpatient/Floors</option>
<option value="ed">Emergency Dept</option>
</select>
</div>
<div class="demo-field demo-field-model">
<label><i class="fas fa-robot"></i> Model</label>
<select class="tab-model-select"></select>
</div>
</div>
<!-- Save/Load bar -->
<div class="save-bar-wrap">
<div class="save-bar" id="enc-save-bar">
<div style="display:flex;align-items:center;gap:8px;flex:1;">
<i class="fas fa-tag" style="color:var(--g400);font-size:13px;"></i>
<input type="text" id="enc-label" class="save-label-input" placeholder="Patient label (e.g. John D, Visit #42)">
</div>
<button id="btn-enc-save" class="btn-sm btn-ghost"><i class="fas fa-floppy-disk"></i> Save</button>
<button id="btn-enc-load" class="btn-sm btn-ghost"><i class="fas fa-folder-open"></i> Load</button>
<button id="btn-enc-new" class="btn-sm btn-ghost" title="Clear all and start new patient" style="color:var(--red);"><i class="fas fa-rotate-left"></i> New</button>
</div>
<div id="enc-load-popover" class="enc-load-popover hidden">
<div class="enc-load-popover-inner">
<input type="text" id="enc-load-search" class="enc-load-search" placeholder="Search saved encounters...">
<button class="enc-pop-close btn-sm btn-ghost"><i class="fas fa-times"></i></button>
</div>
<div id="enc-pop-list" class="enc-pop-list"></div>
</div>
</div>
<div class="card">
<div class="record-controls">
<button id="enc-record-btn" class="record-btn"><i class="fas fa-microphone"></i><span>Start Recording</span></button>
<button id="enc-pause-btn" class="btn-sm btn-ghost hidden" style="margin-left:8px;"><i class="fas fa-pause"></i> Pause</button>
<div id="enc-recording-indicator" class="recording-indicator hidden"><div class="pulse-dot"></div><span>Recording... <span id="enc-timer">00:00</span></span></div>
</div>
</div>
<div class="card">
<div class="card-header"><h3><i class="fas fa-file-lines"></i> Transcript</h3><button id="enc-clear" class="btn-sm btn-ghost"><i class="fas fa-eraser"></i> Clear</button></div>
<div id="enc-transcript" class="editable-box" contenteditable="true" data-placeholder="Transcript appears here or type/paste directly..."></div>
</div>
<button id="enc-generate-btn" class="btn-generate"><i class="fas fa-wand-magic-sparkles"></i> Generate HPI</button>
<div id="enc-output" class="card output-card hidden">
<div class="card-header output-header">
<h3><i class="fas fa-file-medical"></i> Generated HPI</h3>
<div class="output-actions">
<span id="enc-model-tag" class="model-tag"></span>
<button class="btn-sm btn-primary" onclick="copyText('enc-hpi-text')"><i class="fas fa-copy"></i> Copy</button>
<button class="btn-sm btn-ghost" onclick="speakText('enc-hpi-text')"><i class="fas fa-volume-high"></i> Read</button>
<button class="btn-sm btn-ghost" onclick="exportToNextcloud('enc-hpi-text','hpi-encounter')"><i class="fas fa-cloud-arrow-up"></i></button>
</div>
</div>
<div id="enc-hpi-text" class="output-text" contenteditable="true"></div>
<div class="refine-bar">
<input type="text" id="enc-refine-input" class="refine-input" placeholder="Tell AI to modify (e.g., 'make it shorter', 'add that patient has asthma history')">
<button class="btn-sm btn-primary" id="enc-refine-btn"><i class="fas fa-edit"></i> Refine</button>
<button class="btn-sm btn-ghost" id="enc-shorten-btn"><i class="fas fa-compress"></i> Shorter</button>
</div>
</div>

View file

@ -0,0 +1,154 @@
<div class="module-header">
<h2><i class="fas fa-hospital"></i> Hospital Course Generator</h2>
<p>Upload notes by date → AI generates organized hospital course</p>
</div>
<div class="demographics-bar">
<div class="demo-field"><label>Patient Age</label><input type="text" id="hc-age" placeholder="e.g., 3 years"></div>
<div class="demo-field"><label>Gender</label><select id="hc-gender"><option value="">Select</option><option>Male</option><option>Female</option></select></div>
<div class="demo-field"><label>PMH</label><input type="text" id="hc-pmh" placeholder="e.g., asthma, obesity"></div>
<div class="demo-field">
<label>Unit</label>
<select id="hc-setting">
<option value="floor">General Floor</option>
<option value="picu">PICU</option>
<option value="nicu">NICU</option>
<option value="psych">Psych/Behavioral</option>
</select>
</div>
<div class="demo-field"><label>LOS (days)</label><input type="number" id="hc-los" placeholder="e.g., 3" min="1"></div>
<div class="demo-field">
<label>Format</label>
<select id="hc-format">
<option value="auto">Auto (AI decides)</option>
<option value="prose">Prose Summary</option>
<option value="dayByDay">Day by Day</option>
<option value="organSystem">Organ System (ICU)</option>
</select>
</div>
<div class="demo-field demo-field-model">
<label><i class="fas fa-robot"></i> Model</label>
<select class="tab-model-select"></select>
</div>
</div>
<!-- Save/Load bar -->
<div class="save-bar-wrap">
<div class="save-bar" id="hosp-save-bar">
<div style="display:flex;align-items:center;gap:8px;flex:1;">
<i class="fas fa-tag" style="color:var(--g400);font-size:13px;"></i>
<input type="text" id="hosp-label" class="save-label-input" placeholder="Patient label (e.g. John D, Visit #42)">
</div>
<button id="btn-hosp-save" class="btn-sm btn-ghost"><i class="fas fa-floppy-disk"></i> Save</button>
<button id="btn-hosp-load" class="btn-sm btn-ghost"><i class="fas fa-folder-open"></i> Load</button>
<button id="btn-hosp-new" class="btn-sm btn-ghost" title="Clear all and start new patient" style="color:var(--red);"><i class="fas fa-rotate-left"></i> New</button>
</div>
<div id="hosp-load-popover" class="enc-load-popover hidden">
<div class="enc-load-popover-inner">
<input type="text" id="hosp-load-search" class="enc-load-search" placeholder="Search saved encounters...">
<button class="enc-pop-close btn-sm btn-ghost"><i class="fas fa-times"></i></button>
</div>
<div id="hosp-pop-list" class="enc-pop-list"></div>
</div>
</div>
<!-- ED Note -->
<div class="card">
<div class="card-header"><h3><i class="fas fa-truck-medical"></i> ED Note (before admission)</h3></div>
<div class="note-entry">
<div class="note-meta">
<input type="date" id="hc-ed-date">
<input type="text" id="hc-ed-labs" placeholder="ED Labs (e.g., WBC 15, BMP normal...)">
</div>
<div id="hc-ed-content" class="editable-box" contenteditable="true" data-placeholder="Paste or type ED note here..."></div>
<div class="note-dictate">
<button class="btn-sm btn-ghost hc-dictate-btn" data-target="hc-ed-content"><i class="fas fa-microphone"></i> Dictate</button>
</div>
</div>
</div>
<!-- H&P -->
<div class="card">
<div class="card-header"><h3><i class="fas fa-file-medical"></i> H&P (Admission Note)</h3></div>
<div class="note-entry">
<div class="note-meta">
<input type="date" id="hc-hp-date">
</div>
<div id="hc-hp-content" class="editable-box" contenteditable="true" data-placeholder="Paste or type H&P here..."></div>
<div class="note-dictate">
<button class="btn-sm btn-ghost hc-dictate-btn" data-target="hc-hp-content"><i class="fas fa-microphone"></i> Dictate</button>
</div>
</div>
</div>
<!-- Progress Notes (dynamic) -->
<div id="hc-notes-container">
<div class="card note-card" data-note-index="0">
<div class="card-header">
<h3><i class="fas fa-notes-medical"></i> Progress Note #1</h3>
<button class="btn-sm btn-ghost remove-note-btn"><i class="fas fa-trash"></i></button>
</div>
<div class="note-entry">
<div class="note-meta">
<input type="date" class="note-date">
<select class="note-type">
<option value="progress-attending">Progress - Attending</option>
<option value="progress-resident">Progress - Resident</option>
<option value="progress-np">Progress - NP/PA</option>
<option value="consult">Consult Note</option>
<option value="procedure">Procedure Note</option>
<option value="discharge-summary">Discharge Summary</option>
</select>
</div>
<div class="editable-box note-content" contenteditable="true" data-placeholder="Paste or type note..."></div>
<div class="note-dictate">
<button class="btn-sm btn-ghost hc-dictate-btn" data-target-class="note-content"><i class="fas fa-microphone"></i> Dictate</button>
</div>
</div>
</div>
</div>
<button id="hc-add-note" class="btn-sm btn-ghost" style="margin:8px 0"><i class="fas fa-plus"></i> Add Progress Note</button>
<!-- Labs -->
<div class="card">
<div class="card-header"><h3><i class="fas fa-flask"></i> Labs</h3><button id="hc-add-lab" class="btn-sm btn-ghost"><i class="fas fa-plus"></i> Add</button></div>
<div id="hc-labs-container">
<div class="lab-entry">
<input type="date" class="lab-date">
<input type="text" class="lab-values" placeholder="e.g., WBC 12.5, H/H 10.2/31, BMP: Na 138, K 3.5, BUN 11, Cr 0.5">
</div>
</div>
</div>
<!-- AI Instructions -->
<div class="card">
<div class="card-header"><h3><i class="fas fa-comment-dots"></i> Additional Instructions (optional)</h3></div>
<input type="text" id="hc-instructions" class="full-input" placeholder="e.g., 'Focus on respiratory course', 'Patient was transferred from outside hospital'">
</div>
<div class="action-row">
<button id="hc-generate-btn" class="btn-generate btn-generate-blue"><i class="fas fa-wand-magic-sparkles"></i> Generate Hospital Course</button>
</div>
<!-- Output -->
<div id="hc-output" class="card output-card hidden">
<div class="card-header output-header">
<h3><i class="fas fa-file-medical"></i> Hospital Course</h3>
<div class="output-actions">
<span id="hc-format-tag" class="model-tag"></span>
<span id="hc-model-tag" class="model-tag"></span>
<button class="btn-sm btn-primary" onclick="copyText('hc-course-text')"><i class="fas fa-copy"></i> Copy</button>
<button class="btn-sm btn-ghost" onclick="speakText('hc-course-text')"><i class="fas fa-volume-high"></i> Read</button>
<button class="btn-sm btn-ghost" onclick="exportToNextcloud('hc-course-text','hospital-course')"><i class="fas fa-cloud-arrow-up"></i></button>
</div>
</div>
<div id="hc-course-text" class="output-text" contenteditable="true"></div>
<div class="refine-bar">
<input type="text" id="hc-refine-input" class="refine-input" placeholder="Tell AI to modify or add discharge day info...">
<button class="btn-sm btn-primary" id="hc-refine-btn"><i class="fas fa-edit"></i> Refine</button>
<button class="btn-sm btn-ghost" id="hc-shorten-btn"><i class="fas fa-compress"></i> Shorter</button>
<button class="btn-sm btn-warning" id="hc-clarify-btn"><i class="fas fa-circle-question"></i> What's Missing?</button>
</div>
<div id="hc-clarify-output" class="clarify-box hidden"></div>
</div>

View file

@ -0,0 +1,62 @@
<div class="module-header">
<h2><i class="fas fa-graduation-cap"></i> Learning Hub</h2>
<p>Pediatric education, clinical pearls, and self-assessment quizzes</p>
</div>
<!-- Search bar -->
<div class="card" style="padding:12px 16px;">
<div style="display:flex;gap:8px;align-items:center;">
<i class="fas fa-search" style="color:var(--g400);"></i>
<input type="text" id="lh-search" class="full-input" placeholder="Search topics, subjects..." style="border:none;flex:1;">
</div>
</div>
<!-- Category pills -->
<div id="lh-categories" class="lh-category-bar"></div>
<!-- Content feed -->
<div id="lh-feed" class="lh-feed"></div>
<!-- Content viewer (hidden until item clicked) -->
<div id="lh-viewer" class="lh-viewer hidden">
<button id="lh-back" class="btn-sm btn-ghost" style="margin-bottom:12px;"><i class="fas fa-arrow-left"></i> Back to Feed</button>
<div class="card">
<div class="card-header">
<h3 id="lh-viewer-title"></h3>
<span id="lh-viewer-meta" style="font-size:12px;color:var(--g500);"></span>
</div>
<div id="lh-viewer-body" class="lh-content-body" style="padding:16px;"></div>
</div>
<!-- Quiz section (if content has questions) -->
<div id="lh-quiz-section" class="hidden" style="margin-top:16px;">
<div class="card">
<div class="card-header">
<h3><i class="fas fa-clipboard-question"></i> Quiz</h3>
<span id="lh-quiz-count" style="font-size:12px;color:var(--g500);"></span>
</div>
<div id="lh-quiz-questions" style="padding:16px;"></div>
<div style="padding:0 16px 16px;">
<button id="lh-submit-quiz" class="btn-generate"><i class="fas fa-check"></i> Submit Answers</button>
</div>
</div>
<!-- Results -->
<div id="lh-quiz-results" class="hidden" style="margin-top:12px;">
<div class="card">
<div class="card-header">
<h3><i class="fas fa-chart-bar"></i> Results</h3>
<span id="lh-quiz-score" class="model-tag"></span>
</div>
<div id="lh-quiz-explanations" style="padding:16px;"></div>
</div>
</div>
</div>
<!-- Past attempts -->
<div id="lh-progress-section" class="hidden" style="margin-top:12px;">
<div class="card" style="padding:12px 16px;">
<h4 style="margin:0 0 8px;font-size:13px;color:var(--g600);"><i class="fas fa-history"></i> Your Past Attempts</h4>
<div id="lh-progress-list" style="font-size:13px;"></div>
</div>
</div>
</div>

View file

@ -0,0 +1,98 @@
<div class="module-header">
<h2><i class="fas fa-cog"></i> Settings</h2>
<p>Account security, integrations, and personal templates</p>
</div>
<div class="settings-page">
<!-- 2FA -->
<div class="settings-section card">
<h3><i class="fas fa-shield-halved"></i> Two-Factor Authentication</h3>
<p id="2fa-status">Status: Loading...</p>
<button id="btn-setup-2fa" class="btn-sm btn-primary">Enable 2FA</button>
<button id="btn-disable-2fa" class="btn-sm btn-ghost hidden">Disable 2FA</button>
<div id="2fa-setup" class="hidden">
<p>Scan this QR code with your authenticator app:</p>
<img id="2fa-qr" src="" alt="QR Code">
<p>Or enter manually: <code id="2fa-secret"></code></p>
<div class="form-group">
<label>Enter the 6-digit code to verify:</label>
<input type="text" id="2fa-verify-code" maxlength="6" placeholder="123456">
<button id="btn-verify-2fa" class="btn-sm btn-success">Verify & Enable</button>
</div>
</div>
</div>
<!-- Nextcloud -->
<div class="settings-section card">
<h3><i class="fas fa-cloud"></i> Nextcloud Integration</h3>
<p>Export generated documents to your Nextcloud.</p>
<div id="nc-status">Not connected</div>
<div class="form-group">
<label>Nextcloud URL</label>
<input type="text" id="nc-url" placeholder="https://cloud.example.com">
</div>
<div class="form-group">
<label>Username</label>
<input type="text" id="nc-user" placeholder="your-username">
</div>
<div class="form-group">
<label>App Password</label>
<input type="password" id="nc-pass" placeholder="Generate in Nextcloud → Settings → Security">
<small>Go to Nextcloud → Settings → Security → Create new app password</small>
</div>
<button id="btn-nc-connect" class="btn-sm btn-primary">Connect</button>
<button id="btn-nc-disconnect" class="btn-sm btn-ghost hidden">Disconnect</button>
</div>
<!-- My Templates / Memories -->
<div class="settings-section card">
<h3><i class="fas fa-book-medical"></i> My Templates</h3>
<p style="font-size:13px;color:var(--g600);">Save reusable templates for physical exam, ROS, encounter format, etc. The AI will use these when generating notes. You can reference them by saying "use my normal physical exam" in dictation.</p>
<div style="margin-bottom:10px;display:flex;gap:8px;flex-wrap:wrap;align-items:center;">
<select id="mem-category" style="font-size:13px;padding:5px 8px;border:1px solid var(--g300);border-radius:6px;">
<option value="physical_exam">Physical Exam Template</option>
<option value="ros">Review of Systems Template</option>
<option value="encounter_format">Encounter Note Format</option>
<option value="family_history">Family History Format</option>
<option value="assessment_plan">Assessment &amp; Plan Format</option>
<option value="custom">Custom</option>
</select>
<input type="text" id="mem-name" style="font-size:13px;padding:5px 8px;border:1px solid var(--g300);border-radius:6px;flex:1;min-width:150px;" placeholder="Template name (e.g. Normal PE)">
</div>
<textarea id="mem-content" rows="5" style="width:100%;font-size:12px;padding:8px;border:1px solid var(--g300);border-radius:6px;resize:vertical;box-sizing:border-box;" placeholder="Paste your template here. Example: HEENT: Normocephalic, atraumatic. Eyes: PERRL. Ears: TMs clear. Throat: clear..."></textarea>
<div style="margin-top:8px;display:flex;gap:8px;">
<button id="btn-mem-save" class="btn-sm btn-primary"><i class="fas fa-plus"></i> Add Template</button>
</div>
<div id="mem-list" style="margin-top:12px;display:flex;flex-direction:column;gap:6px;">
<p style="color:var(--g400);font-size:13px;">Loading templates...</p>
</div>
</div>
<!-- Saved Encounters -->
<div class="settings-section card">
<h3><i class="fas fa-floppy-disk"></i> Saved Encounters</h3>
<p style="font-size:13px;color:var(--g600);">Encounters are automatically deleted after 7 days per site policy.</p>
<div id="saved-enc-list" style="display:flex;flex-direction:column;gap:6px;">
<p style="color:var(--g400);font-size:13px;">Loading...</p>
</div>
</div>
<!-- HIPAA -->
<div class="settings-section card">
<h3><i class="fas fa-lock"></i> HIPAA & Privacy</h3>
<div class="hipaa-info">
<p><strong>Current Status:</strong> This tool processes data through third-party AI APIs.</p>
<ul>
<li>✅ All connections use HTTPS/TLS encryption</li>
<li>✅ Authentication required</li>
<li>✅ No patient data stored on server</li>
<li>✅ 2FA available</li>
<li>⚠️ OpenRouter does not currently offer BAA</li>
<li>⚠️ For full HIPAA: use Azure OpenAI or AWS Bedrock with BAA</li>
</ul>
<p><strong>Recommendation:</strong> Do not enter real PHI until your organization has executed BAAs with all AI providers.</p>
</div>
</div>
</div>

View file

@ -0,0 +1,107 @@
<div class="module-header">
<h2><i class="fas fa-thermometer-half"></i> Sick Visit Note</h2>
<p>Quick sick visit documentation with auto-suggested ROS and PE systems</p>
</div>
<!-- Save/Load bar -->
<div class="save-bar-wrap">
<div class="save-bar" id="sick-save-bar">
<div style="display:flex;align-items:center;gap:8px;flex:1;">
<i class="fas fa-tag" style="color:var(--g400);font-size:13px;"></i>
<input type="text" id="sick-label" class="save-label-input" placeholder="Patient label (e.g. John D, 5y fever)">
</div>
<button id="btn-sick-save" class="btn-sm btn-ghost"><i class="fas fa-floppy-disk"></i> Save</button>
<button id="btn-sick-load" class="btn-sm btn-ghost"><i class="fas fa-folder-open"></i> Load</button>
<button id="btn-sick-new" class="btn-sm btn-ghost" title="Clear and start new patient" style="color:var(--red);"><i class="fas fa-rotate-left"></i> New</button>
</div>
<div id="sick-load-popover" class="enc-load-popover hidden">
<div class="enc-load-popover-inner">
<input type="text" id="sick-load-search" class="enc-load-search" placeholder="Search saved encounters...">
<button class="enc-pop-close btn-sm btn-ghost"><i class="fas fa-times"></i></button>
</div>
<div id="sick-pop-list" class="enc-pop-list"></div>
</div>
</div>
<!-- Demographics -->
<div class="card" style="margin-bottom:10px;">
<div class="card-header"><h3><i class="fas fa-user"></i> Patient Info</h3></div>
<div style="padding:12px 16px;display:flex;flex-wrap:wrap;gap:10px;align-items:flex-end;">
<div class="demo-field"><label>Age</label><input type="text" id="sick-age" placeholder="e.g., 5 years" style="width:110px;"></div>
<div class="demo-field"><label>Gender</label><select id="sick-gender"><option value="">Select</option><option>Male</option><option>Female</option><option>Non-binary/Other</option></select></div>
<div class="demo-field" style="flex:1;min-width:200px;"><label>Chief Complaint</label><input type="text" id="sick-cc" placeholder="e.g., fever, ear pain, cough" style="width:100%;"></div>
<div class="demo-field demo-field-model"><label><i class="fas fa-robot"></i> Model</label><select id="sick-model-select" class="tab-model-select"></select></div>
<button id="btn-sick-infer" class="btn-sm btn-primary" style="align-self:flex-end;"><i class="fas fa-wand-magic-sparkles"></i> Update ROS/PE</button>
</div>
</div>
<!-- Recording -->
<div class="card" style="margin-bottom:10px;">
<div class="card-header">
<h3><i class="fas fa-microphone"></i> Encounter Recording / Dictation</h3>
<div style="display:flex;gap:6px;align-items:center;">
<button id="sick-record-btn" class="btn-sm btn-ghost"><i class="fas fa-microphone"></i> Listen In</button>
<button id="sick-pause-btn" class="btn-sm btn-ghost hidden"><i class="fas fa-pause"></i> Pause</button>
<div id="sick-rec-indicator" class="recording-indicator hidden" style="font-size:12px;"><div class="pulse-dot"></div><span>Recording... <span id="sick-timer">00:00</span></span></div>
</div>
</div>
<div style="padding:8px 16px;">
<div id="sick-transcript" class="editable-box" contenteditable="true" data-placeholder="Transcript appears here. Physician can dictate the HPI and encounter details."></div>
</div>
</div>
<!-- ROS (4 systems auto-inferred) -->
<div class="card" style="margin-bottom:10px;">
<div class="card-header" style="display:flex;align-items:center;justify-content:space-between;">
<h3><i class="fas fa-list-check"></i> Review of Systems</h3>
<div style="display:flex;gap:6px;align-items:center;">
<button id="sick-ros-all-wnl" class="btn-sm btn-ghost" style="font-size:11px;"><i class="fas fa-check-double"></i> All WNL</button>
<select id="sick-ros-add" class="btn-sm" style="font-size:11px;padding:3px 6px;border:1px solid var(--g300);border-radius:6px;">
<option value="">+ Add system</option>
</select>
</div>
</div>
<div id="sick-ros-container" style="padding:8px 16px;"></div>
</div>
<!-- PE (7 systems auto-inferred) -->
<div class="card" style="margin-bottom:10px;">
<div class="card-header" style="display:flex;align-items:center;justify-content:space-between;">
<h3><i class="fas fa-stethoscope"></i> Physical Examination</h3>
<div style="display:flex;gap:6px;align-items:center;">
<button id="sick-pe-all-normal" class="btn-sm btn-ghost" style="font-size:11px;"><i class="fas fa-check-double"></i> All Normal</button>
<select id="sick-pe-add" class="btn-sm" style="font-size:11px;padding:3px 6px;border:1px solid var(--g300);border-radius:6px;">
<option value="">+ Add system</option>
</select>
</div>
</div>
<div id="sick-pe-container" style="padding:8px 16px;"></div>
</div>
<!-- Diagnoses -->
<div class="card" style="margin-bottom:10px;">
<div class="card-header"><h3><i class="fas fa-tag"></i> Diagnoses (ICD-10)</h3></div>
<div id="sick-dx-container" style="padding:12px 16px;"></div>
</div>
<button id="btn-sick-generate" class="btn-generate"><i class="fas fa-wand-magic-sparkles"></i> Generate Sick Visit Note</button>
<!-- Output -->
<div id="sick-note-output" class="card output-card hidden">
<div class="card-header output-header">
<h3><i class="fas fa-file-medical"></i> Sick Visit Note</h3>
<div class="output-actions">
<span id="sick-note-model-tag" class="model-tag"></span>
<button class="btn-sm btn-ghost" id="btn-sick-new-patient"><i class="fas fa-rotate-left"></i> New Patient</button>
<button class="btn-sm btn-primary" onclick="copyText('sick-note-text')"><i class="fas fa-copy"></i> Copy</button>
<button class="btn-sm btn-ghost" onclick="speakText('sick-note-text')"><i class="fas fa-volume-high"></i> Read</button>
</div>
</div>
<div id="sick-note-text" class="output-text" contenteditable="true"></div>
<div class="refine-bar">
<input type="text" id="sick-refine-input" class="refine-input" placeholder="Tell AI to modify (e.g., 'add that patient received amoxicillin')">
<button class="btn-sm btn-primary" id="sick-refine-btn"><i class="fas fa-edit"></i> Refine</button>
<button class="btn-sm btn-ghost" id="sick-shorten-btn"><i class="fas fa-compress"></i> Shorter</button>
</div>
</div>

View file

@ -0,0 +1,57 @@
<div class="module-header">
<h2><i class="fas fa-file-waveform"></i> SOAP Note Generator</h2>
<p>Generate full SOAP note or just Subjective from dictation/text</p>
</div>
<div class="demographics-bar">
<div class="demo-field"><label>Patient Age</label><input type="text" id="soap-age" placeholder="e.g., 5 years"></div>
<div class="demo-field"><label>Gender</label><select id="soap-gender"><option value="">Select</option><option>Male</option><option>Female</option></select></div>
<div class="demo-field">
<label>Generate</label>
<select id="soap-type">
<option value="full">Full SOAP Note</option>
<option value="subjective">Subjective Only</option>
</select>
</div>
<div class="demo-field demo-field-model">
<label><i class="fas fa-robot"></i> Model</label>
<select class="tab-model-select"></select>
</div>
</div>
<div class="card">
<div class="record-controls">
<button id="soap-record-btn" class="record-btn btn-teal"><i class="fas fa-microphone"></i><span>Dictate</span></button>
<div id="soap-recording-indicator" class="recording-indicator hidden"><div class="pulse-dot pulse-teal"></div><span>Dictating... <span id="soap-timer">00:00</span></span></div>
</div>
</div>
<div class="card">
<div class="card-header"><h3><i class="fas fa-file-lines"></i> Input</h3><button id="soap-clear" class="btn-sm btn-ghost"><i class="fas fa-eraser"></i> Clear</button></div>
<div id="soap-transcript" class="editable-box" contenteditable="true" data-placeholder="Dictate or type encounter details..."></div>
</div>
<div class="card">
<div class="card-header"><h3><i class="fas fa-comment-dots"></i> Instructions (optional)</h3></div>
<input type="text" id="soap-instructions" class="full-input" placeholder="e.g., 'Include assessment for otitis media', 'Add anticipatory guidance'">
</div>
<button id="soap-generate-btn" class="btn-generate btn-generate-teal"><i class="fas fa-wand-magic-sparkles"></i> Generate SOAP Note</button>
<div id="soap-output" class="card output-card hidden">
<div class="card-header output-header">
<h3><i class="fas fa-file-medical"></i> SOAP Note</h3>
<div class="output-actions">
<span id="soap-model-tag" class="model-tag"></span>
<button class="btn-sm btn-primary" onclick="copyText('soap-text')"><i class="fas fa-copy"></i> Copy</button>
<button class="btn-sm btn-ghost" onclick="speakText('soap-text')"><i class="fas fa-volume-high"></i> Read</button>
<button class="btn-sm btn-ghost" onclick="exportToNextcloud('soap-text','soap-note')"><i class="fas fa-cloud-arrow-up"></i></button>
</div>
</div>
<div id="soap-text" class="output-text" contenteditable="true"></div>
<div class="refine-bar">
<input type="text" id="soap-refine-input" class="refine-input" placeholder="Tell AI to modify...">
<button class="btn-sm btn-primary" id="soap-refine-btn"><i class="fas fa-edit"></i> Refine</button>
<button class="btn-sm btn-ghost" id="soap-shorten-btn"><i class="fas fa-compress"></i> Shorter</button>
</div>
</div>

View file

@ -0,0 +1,7 @@
<div class="module-header">
<h2><i class="fas fa-table"></i> Full Vaccine Schedule</h2>
<p>AAP/ACIP 2025 — Complete immunization schedule for 018 years</p>
</div>
<div id="wv-panel-schedule">
<p class="wv-loading">Loading schedule...</p>
</div>

View file

@ -0,0 +1,306 @@
<div class="module-header">
<h2><i class="fas fa-calendar-check"></i> Well Visit / Preventive Care</h2>
<p>AAP 2025 Bright Futures periodicity — vaccines, screenings, billing codes, and catch-up schedule</p>
</div>
<!-- Sub-tab navigation -->
<div class="wv-subtab-bar">
<button class="wv-subtab-btn active" data-subtab="byvisit">
<i class="fas fa-child"></i> By Visit Age
</button>
<button class="wv-subtab-btn" data-subtab="milestones">
<i class="fas fa-baby"></i> Milestones
</button>
<button class="wv-subtab-btn" data-subtab="shadess" style="display:none;">
<i class="fas fa-brain"></i> SSHADESS (12+)
</button>
<button class="wv-subtab-btn" data-subtab="note">
<i class="fas fa-file-medical"></i> Visit Note
</button>
</div>
<!-- By Visit sub-panel -->
<div id="wv-panel-byvisit" class="wv-subpanel">
<div class="wv-visit-selector-row">
<label for="wv-visit-select"><i class="fas fa-calendar-day"></i> Select Visit Age:</label>
<select id="wv-visit-select" class="wv-visit-select"></select>
</div>
<div id="wv-visit-detail" class="wv-visit-detail">
<p class="wv-empty">Select a visit age above to see recommendations.</p>
</div>
</div>
<!-- Milestones sub-panel -->
<div id="wv-panel-milestones" class="wv-subpanel hidden">
<div class="demographics-bar">
<div class="demo-field"><label>Patient Age</label><input type="text" id="ms-age" placeholder="e.g., 9 months"></div>
<div class="demo-field"><label>Gender</label><select id="ms-gender"><option value="">Select</option><option>Male</option><option>Female</option></select></div>
<div class="demo-field">
<label>Age Group</label>
<select id="ms-age-group">
<option value="">-- Select --</option>
<option value="Newborn / 1 month">Newborn / 1 Month</option>
<option value="2 months">2 Months</option>
<option value="4 months">4 Months</option>
<option value="6 months">6 Months</option>
<option value="9 months">9 Months</option>
<option value="12 months">12 Months</option>
<option value="15 months">15 Months</option>
<option value="18 months">18 Months</option>
<option value="24 months">2 Years</option>
<option value="30 months">2.5 Years</option>
<option value="36 months">3 Years</option>
<option value="48 months">4 Years</option>
<option value="60 months">5 Years</option>
<option value="6 years">6 Years</option>
<option value="7 years">7 Years</option>
<option value="8 years">8 Years</option>
<option value="9 years">9 Years</option>
<option value="10 years">10 Years</option>
<option value="11 years">11 Years</option>
</select>
</div>
<div class="demo-field">
<label>Output Format</label>
<select id="ms-format">
<option value="narrative">Narrative (paragraphs)</option>
<option value="list">Structured List</option>
</select>
</div>
<div class="demo-field demo-field-model">
<label><i class="fas fa-robot"></i> Model</label>
<select class="tab-model-select"></select>
</div>
</div>
<div class="legend">
<div class="legend-item"><span class="legend-icon legend-yes"></span> Achieved</div>
<div class="legend-item"><span class="legend-icon legend-no"></span> Not Achieved</div>
<div class="legend-item"><span class="legend-icon legend-blank"></span> Not Assessed (omitted)</div>
</div>
<div id="milestone-checklist"></div>
<div class="milestone-actions" id="milestone-actions" style="display:none">
<button id="ms-all-yes" class="btn-sm btn-success"><i class="fas fa-check-double"></i> All Yes</button>
<button id="ms-all-clear" class="btn-sm btn-ghost"><i class="fas fa-eraser"></i> Clear All</button>
<button id="ms-generate-btn" class="btn-generate btn-generate-teal"><i class="fas fa-wand-magic-sparkles"></i> Generate Narrative</button>
</div>
<div id="ms-output" class="card output-card hidden">
<div class="card-header output-header">
<h3><i class="fas fa-file-medical"></i> Developmental Assessment</h3>
<div class="output-actions">
<span id="ms-model-tag" class="model-tag"></span>
<button class="btn-sm btn-primary" onclick="copyText('ms-narrative-text')"><i class="fas fa-copy"></i> Copy</button>
<button class="btn-sm btn-success" id="btn-ms-copy-to-note"><i class="fas fa-clipboard-check"></i> Copy to Note</button>
<button class="btn-sm btn-ghost" onclick="speakText('ms-narrative-text')"><i class="fas fa-volume-high"></i> Read</button>
<button class="btn-sm btn-ghost" onclick="exportToNextcloud('ms-narrative-text','milestones')"><i class="fas fa-cloud-arrow-up"></i></button>
</div>
</div>
<div id="ms-summary-bar" class="summary-bar hidden"></div>
<div id="ms-narrative-text" class="output-text" contenteditable="true"></div>
<div class="summary-section">
<div class="summary-section-header">
<button id="ms-quick-summary-btn" class="btn-generate-summary"><i class="fas fa-compress"></i> 3-Sentence Summary</button>
</div>
<div id="ms-quick-summary" class="hidden">
<div class="card-header summary-output-header">
<h3><i class="fas fa-clipboard-list"></i> Quick Summary</h3>
<div class="output-actions">
<button class="btn-sm btn-primary" onclick="copyText('ms-summary-text')"><i class="fas fa-copy"></i> Copy</button>
</div>
</div>
<div id="ms-summary-text" class="summary-output-text" contenteditable="true"></div>
</div>
</div>
</div>
</div>
<!-- SSHADESS sub-panel (age 12+) -->
<div id="wv-panel-shadess" class="wv-subpanel hidden">
<div class="card" style="margin-bottom:10px;">
<div class="card-header">
<h3><i class="fas fa-brain"></i> SSHADESS Psychosocial Screening</h3>
<span style="font-size:12px;color:var(--g500);">Recommended age 12 and older</span>
</div>
<div style="padding:12px 16px;display:flex;align-items:center;gap:10px;flex-wrap:wrap;border-bottom:1px solid var(--g100);">
<div class="demo-field"><label>Patient Age</label><input type="text" id="shadess-age" placeholder="e.g., 14 years" style="width:120px;"></div>
<div class="demo-field"><label>Gender</label><select id="shadess-gender"><option value="">Select</option><option>Male</option><option>Female</option><option>Non-binary/Other</option></select></div>
<div class="demo-field demo-field-model"><label><i class="fas fa-robot"></i> Model</label><select class="tab-model-select"></select></div>
<div style="margin-left:auto;display:flex;gap:8px;">
<button id="shadess-record-btn" class="btn-sm btn-ghost"><i class="fas fa-microphone"></i> Listen In</button>
<button id="shadess-pause-btn" class="btn-sm btn-ghost hidden"><i class="fas fa-pause"></i> Pause</button>
<div id="shadess-rec-indicator" class="recording-indicator hidden" style="font-size:12px;"><div class="pulse-dot"></div><span>Recording... <span id="shadess-timer">00:00</span></span></div>
</div>
</div>
<!-- Domains will be rendered by JS -->
<div id="shadess-domains" style="padding:8px 0;"></div>
<div style="padding:12px 16px;display:flex;gap:8px;flex-wrap:wrap;border-top:1px solid var(--g100);">
<button id="btn-shadess-generate" class="btn-generate" style="flex:1;"><i class="fas fa-wand-magic-sparkles"></i> Generate SSHADESS Assessment</button>
</div>
</div>
<!-- SSHADESS Output -->
<div id="shadess-output" class="card output-card hidden">
<div class="card-header output-header">
<h3><i class="fas fa-file-medical"></i> SSHADESS Assessment</h3>
<div class="output-actions">
<span id="shadess-model-tag" class="model-tag"></span>
<button class="btn-sm btn-ghost" id="btn-shadess-new-patient"><i class="fas fa-rotate-left"></i> New Patient</button>
<button class="btn-sm btn-primary" onclick="copyText('shadess-result-text')"><i class="fas fa-copy"></i> Copy</button>
<button class="btn-sm btn-ghost" onclick="speakText('shadess-result-text')"><i class="fas fa-volume-high"></i> Read</button>
</div>
</div>
<div id="shadess-result-text" class="output-text" contenteditable="true"></div>
<div style="padding:10px 16px;font-size:12px;color:var(--g500);">
<i class="fas fa-info-circle"></i> Use "Copy" to paste this into the Visit Note tab for a complete encounter note.
</div>
</div>
</div>
<!-- Visit Note sub-panel -->
<div id="wv-panel-note" class="wv-subpanel hidden">
<!-- Save/Load bar -->
<div class="save-bar-wrap">
<div class="save-bar" id="wv-save-bar">
<div style="display:flex;align-items:center;gap:8px;flex:1;">
<i class="fas fa-tag" style="color:var(--g400);font-size:13px;"></i>
<input type="text" id="wv-label" class="save-label-input" placeholder="Patient label (e.g. Jane D, Visit #7)">
</div>
<button id="btn-wv-save" class="btn-sm btn-ghost"><i class="fas fa-floppy-disk"></i> Save</button>
<button id="btn-wv-load" class="btn-sm btn-ghost"><i class="fas fa-folder-open"></i> Load</button>
<button id="btn-wv-new" class="btn-sm btn-ghost" title="Clear and start new patient" style="color:var(--red);"><i class="fas fa-rotate-left"></i> New</button>
</div>
<div id="wv-load-popover" class="enc-load-popover hidden">
<div class="enc-load-popover-inner">
<input type="text" id="wv-load-search" class="enc-load-search" placeholder="Search saved encounters...">
<button class="enc-pop-close btn-sm btn-ghost"><i class="fas fa-times"></i></button>
</div>
<div id="wv-pop-list" class="enc-pop-list"></div>
</div>
</div>
<div class="card" style="margin-bottom:10px;">
<div class="card-header"><h3><i class="fas fa-stethoscope"></i> Visit Details</h3></div>
<div style="padding:12px 16px;display:flex;flex-wrap:wrap;gap:10px;">
<div class="demo-field"><label>Visit Age</label><input type="text" id="wv-note-age" placeholder="e.g., 9 years"></div>
<div class="demo-field"><label>Gender</label><select id="wv-note-gender"><option value="">Select</option><option>Male</option><option>Female</option></select></div>
<div class="demo-field demo-field-model"><label><i class="fas fa-robot"></i> Model</label><select class="tab-model-select"></select></div>
<div style="width:100%;display:flex;gap:8px;align-items:center;">
<label style="font-size:12px;font-weight:600;color:var(--g600);">Note style:</label>
<select id="wv-note-style" style="font-size:13px;padding:4px 8px;border:1px solid var(--g300);border-radius:6px;">
<option value="full">Full Encounter Note</option>
<option value="short">Brief SOAP</option>
</select>
</div>
</div>
</div>
<div class="card" style="margin-bottom:10px;">
<div class="card-header">
<h3><i class="fas fa-ruler-combined"></i> Vitals &amp; Measurements</h3>
</div>
<div style="padding:12px 16px;">
<textarea id="wv-vitals" rows="3" style="width:100%;font-size:13px;padding:8px;border:1px solid var(--g300);border-radius:6px;resize:vertical;box-sizing:border-box;" placeholder="e.g., Temp 98.6, HR 78, RR 18, BP 106/68, O2 99%&#10;Ht: 135cm (50th%), Wt: 32kg (55th%), BMI: 17.6 (60th%)"></textarea>
</div>
</div>
<div class="card" style="margin-bottom:10px;">
<div class="card-header">
<h3><i class="fas fa-microphone"></i> Encounter Recording / Dictation</h3>
<div style="display:flex;gap:6px;align-items:center;">
<button id="wv-record-btn" class="btn-sm btn-ghost"><i class="fas fa-microphone"></i> Listen In</button>
<button id="wv-pause-btn" class="btn-sm btn-ghost hidden"><i class="fas fa-pause"></i> Pause</button>
<div id="wv-rec-indicator" class="recording-indicator hidden" style="font-size:12px;"><div class="pulse-dot"></div><span>Recording... <span id="wv-timer">00:00</span></span></div>
</div>
</div>
<div style="padding:8px 16px;">
<div id="wv-transcript" class="editable-box" contenteditable="true" style="min-height:80px;" data-placeholder="Transcript of the encounter appears here. You can also type or paste directly. Physician can dictate: 'All physical exam normal aside from...'"></div>
</div>
</div>
<div class="card" style="margin-bottom:10px;">
<div class="card-header"><h3><i class="fas fa-syringe"></i> Screenings &amp; Vaccines</h3></div>
<div style="padding:12px 16px;display:flex;flex-direction:column;gap:8px;">
<div>
<label style="font-size:12px;font-weight:600;color:var(--g600);display:block;margin-bottom:4px;">Screenings completed today:</label>
<textarea id="wv-screenings" rows="2" style="width:100%;font-size:13px;padding:8px;border:1px solid var(--g300);border-radius:6px;resize:vertical;box-sizing:border-box;" placeholder="e.g., Depression screen (PHQ-A): negative, Vision screen: 20/20 bilateral, Lead level: 2 ug/dL"></textarea>
</div>
<div>
<label style="font-size:12px;font-weight:600;color:var(--g600);display:block;margin-bottom:4px;">Vaccines given today:</label>
<textarea id="wv-vaccines" rows="2" style="width:100%;font-size:13px;padding:8px;border:1px solid var(--g300);border-radius:6px;resize:vertical;box-sizing:border-box;" placeholder="e.g., Tdap #1, Meningococcal (MenACWY) #1, HPV #1"></textarea>
</div>
</div>
</div>
<!-- SSHADESS carry-over -->
<div class="card" style="margin-bottom:10px;" id="wv-shadess-card">
<div class="card-header">
<h3><i class="fas fa-brain"></i> SSHADESS Assessment</h3>
<span style="font-size:12px;color:var(--g500);">From SSHADESS tab (age 12+)</span>
</div>
<div style="padding:8px 16px;">
<textarea id="wv-shadess-text" rows="4" style="width:100%;font-size:12px;padding:8px;border:1px solid var(--g300);border-radius:6px;resize:vertical;box-sizing:border-box;" placeholder="Paste SSHADESS assessment here, or it auto-fills from the SSHADESS tab..."></textarea>
</div>
</div>
<!-- Milestones carry-over -->
<div class="card" style="margin-bottom:10px;" id="wv-milestones-card">
<div class="card-header">
<h3><i class="fas fa-baby"></i> Developmental Assessment</h3>
<span style="font-size:12px;color:var(--g500);">From Milestones tab</span>
</div>
<div style="padding:8px 16px;">
<textarea id="wv-milestones-text" rows="3" style="width:100%;font-size:12px;padding:8px;border:1px solid var(--g300);border-radius:6px;resize:vertical;box-sizing:border-box;" placeholder="Developmental assessment auto-fills when you click 'Copy to Note' on Milestones tab..."></textarea>
</div>
</div>
<!-- ROS Card -->
<div class="card" style="margin-bottom:10px;">
<div class="card-header" style="display:flex;align-items:center;justify-content:space-between;">
<h3><i class="fas fa-list-check"></i> Review of Systems (ROS)</h3>
<button id="wv-ros-all-wnl" class="btn-sm btn-ghost" style="font-size:11px;"><i class="fas fa-check-double"></i> All WNL</button>
</div>
<div id="wv-ros-container" style="padding:8px 16px;"></div>
</div>
<!-- Physical Exam Card -->
<div class="card" style="margin-bottom:10px;">
<div class="card-header" style="display:flex;align-items:center;justify-content:space-between;">
<h3><i class="fas fa-stethoscope"></i> Physical Examination (PE)</h3>
<button id="wv-pe-all-normal" class="btn-sm btn-ghost" style="font-size:11px;"><i class="fas fa-check-double"></i> All Normal</button>
</div>
<div id="wv-pe-container" style="padding:8px 16px;"></div>
</div>
<!-- Diagnoses Card -->
<div class="card" style="margin-bottom:10px;">
<div class="card-header"><h3><i class="fas fa-tag"></i> Diagnoses (ICD-10)</h3></div>
<div id="wv-dx-container" style="padding:12px 16px;"></div>
</div>
<button id="btn-wv-generate" class="btn-generate"><i class="fas fa-wand-magic-sparkles"></i> Generate Well Visit Note</button>
<div id="wv-note-output" class="card output-card hidden">
<div class="card-header output-header">
<h3><i class="fas fa-file-medical"></i> Well Visit Note</h3>
<div class="output-actions">
<span id="wv-note-model-tag" class="model-tag"></span>
<button class="btn-sm btn-ghost" id="btn-wv-new-patient"><i class="fas fa-rotate-left"></i> New Patient</button>
<button class="btn-sm btn-primary" onclick="copyText('wv-note-text')"><i class="fas fa-copy"></i> Copy</button>
<button class="btn-sm btn-ghost" onclick="speakText('wv-note-text')"><i class="fas fa-volume-high"></i> Read</button>
<button class="btn-sm btn-ghost" onclick="exportToNextcloud('wv-note-text','well-visit')"><i class="fas fa-cloud-arrow-up"></i></button>
</div>
</div>
<div id="wv-note-text" class="output-text" contenteditable="true"></div>
<div class="refine-bar">
<input type="text" id="wv-refine-input" class="refine-input" placeholder="Tell AI to modify (e.g., 'add that patient has asthma', 'expand the plan section')">
<button class="btn-sm btn-primary" id="wv-refine-btn"><i class="fas fa-edit"></i> Refine</button>
<button class="btn-sm btn-ghost" id="wv-shorten-btn"><i class="fas fa-compress"></i> Shorter</button>
</div>
</div>
</div>

File diff suppressed because it is too large Load diff

View file

@ -26,6 +26,44 @@
document.addEventListener('DOMContentLoaded', function() {
// --- COMPONENT LOADER (lazy-load tab HTML from /components/) ---
var _componentCache = {};
var _componentLoading = {};
function loadComponent(tabEl) {
var component = tabEl.getAttribute('data-component');
if (!component || tabEl.dataset.loaded) return Promise.resolve();
if (_componentCache[component]) {
tabEl.innerHTML = _componentCache[component];
tabEl.dataset.loaded = '1';
return Promise.resolve();
}
if (_componentLoading[component]) return _componentLoading[component];
_componentLoading[component] = fetch('/components/' + component + '.html')
.then(function(r) { if (!r.ok) throw new Error(r.status); return r.text(); })
.then(function(html) {
_componentCache[component] = html;
tabEl.innerHTML = html;
tabEl.dataset.loaded = '1';
// Re-attach per-tab model selectors
tabEl.querySelectorAll('.tab-model-select').forEach(function(sel) {
if (typeof window._buildModelOptions === 'function') window._buildModelOptions(sel);
});
delete _componentLoading[component];
})
.catch(function(err) {
console.warn('[Component] Failed to load ' + component + ':', err);
tabEl.innerHTML = '<div style="padding:40px;text-align:center;color:var(--g400);">Failed to load. Please refresh.</div>';
delete _componentLoading[component];
});
return _componentLoading[component];
}
// Preload the first tab immediately
var firstTab = document.querySelector('.tab-content[data-component]');
if (firstTab) loadComponent(firstTab);
// --- TAB NAVIGATION ---
function activateTab(tabName) {
var btn = document.querySelector('.tab-btn[data-tab="' + tabName + '"]');
@ -34,7 +72,11 @@ document.addEventListener('DOMContentLoaded', function() {
document.querySelectorAll('.tab-content').forEach(function(c) { c.classList.remove('active'); });
btn.classList.add('active');
var tabEl = document.getElementById(tabName + '-tab');
if (tabEl) tabEl.classList.add('active');
if (tabEl) {
tabEl.classList.add('active');
// Lazy-load component HTML if not yet loaded
loadComponent(tabEl);
}
// Persist last tab in localStorage (restored after login in auth.js)
try { localStorage.setItem('ped_last_tab', tabName); } catch(e) {}
document.dispatchEvent(new CustomEvent('tabChanged', { detail: { tab: tabName } }));
@ -128,7 +170,7 @@ document.addEventListener('DOMContentLoaded', function() {
var categories = { free: '🆓 Free', fast: '⚡ Fast & Cheap', smart: '🧠 Smart', premium: '👑 Premium' };
function buildModelOptions(selectEl) {
window._buildModelOptions = function buildModelOptions(selectEl) {
selectEl.innerHTML = '';
var grouped = {};
window._currentModels.forEach(function(m) {
@ -151,13 +193,13 @@ document.addEventListener('DOMContentLoaded', function() {
}
if (modelSelect && window._currentModels.length > 0) {
buildModelOptions(modelSelect);
window._buildModelOptions(modelSelect);
if (costBadge) costBadge.textContent = window._currentProvider.toUpperCase() + ' | ' + window._currentModels[0].cost;
}
// Populate all per-tab model selectors
// Populate all per-tab model selectors already in DOM
document.querySelectorAll('.tab-model-select').forEach(function(sel) {
buildModelOptions(sel);
window._buildModelOptions(sel);
});
})
.catch(function(err) { console.warn('Models load failed:', err); });

View file

@ -737,10 +737,54 @@
}
function sanitizeHtml(html) {
// Allow basic formatting tags, strip scripts
// Allowlist-based sanitizer: only safe tags and no event handlers
var ALLOWED_TAGS = ['p','br','b','strong','i','em','u','s','h1','h2','h3','h4','h5','h6',
'ul','ol','li','a','blockquote','code','pre','table','thead','tbody','tr','th','td',
'hr','div','span','sub','sup','dl','dt','dd'];
var ALLOWED_ATTRS = { 'a': ['href'], 'td': ['colspan','rowspan'], 'th': ['colspan','rowspan'] };
var div = document.createElement('div');
div.innerHTML = html;
div.querySelectorAll('script,iframe,object,embed,form').forEach(function(el) { el.remove(); });
function clean(node) {
var children = Array.prototype.slice.call(node.childNodes);
for (var i = 0; i < children.length; i++) {
var child = children[i];
if (child.nodeType === 3) continue; // text nodes OK
if (child.nodeType !== 1) { child.remove(); continue; } // remove comments etc
var tag = child.tagName.toLowerCase();
if (ALLOWED_TAGS.indexOf(tag) === -1) {
// Replace with text content
var text = document.createTextNode(child.textContent);
node.replaceChild(text, child);
continue;
}
// Remove all attributes except explicitly allowed ones
var allowed = ALLOWED_ATTRS[tag] || [];
var attrs = Array.prototype.slice.call(child.attributes);
for (var j = 0; j < attrs.length; j++) {
if (allowed.indexOf(attrs[j].name) === -1) {
child.removeAttribute(attrs[j].name);
}
}
// For <a> tags, validate href is not javascript:
if (tag === 'a') {
var href = (child.getAttribute('href') || '').trim().toLowerCase();
if (href.indexOf('javascript:') === 0 || href.indexOf('data:') === 0 || href.indexOf('vbscript:') === 0) {
child.setAttribute('href', '#');
}
child.setAttribute('rel', 'noopener noreferrer');
child.setAttribute('target', '_blank');
}
clean(child);
}
}
clean(div);
return div.innerHTML;
}

View file

@ -94,6 +94,9 @@ app.use(express.static(path.join(__dirname, 'public'), {
} else if (filePath.match(/\.(js|css)$/)) {
// Short cache for JS/CSS — 1 hour
res.setHeader('Cache-Control', 'public, max-age=3600');
} else if (filePath.indexOf('/components/') !== -1) {
// Component HTML — 1 hour cache
res.setHeader('Cache-Control', 'public, max-age=3600');
}
}
}));

View file

@ -219,7 +219,7 @@ router.get('/settings', async function(req, res) {
// ============================================================
router.get('/logs/all', async function(req, res) {
try {
var limit = parseInt(req.query.limit) || 100;
var limit = Math.min(parseInt(req.query.limit) || 100, 500);
var category = req.query.category;
var sql = 'SELECT a.*, u.email as user_email, u.name as user_name FROM audit_log a LEFT JOIN users u ON a.user_id = u.id';

View file

@ -15,7 +15,10 @@ function slugify(text) {
return text.toLowerCase().replace(/[^a-z0-9]+/g, '-').replace(/^-|-$/g, '').substring(0, 100);
}
var ALLOWED_SLUG_TABLES = ['learning_categories', 'learning_content'];
async function uniqueSlug(table, base) {
if (ALLOWED_SLUG_TABLES.indexOf(table) === -1) throw new Error('Invalid table for slug generation');
var slug = slugify(base);
var exists = await db.get('SELECT id FROM ' + table + ' WHERE slug = ?', [slug]);
if (!exists) return slug;
@ -41,7 +44,7 @@ router.get('/categories', async function(req, res) {
[]
);
res.json({ success: true, categories: categories });
} catch (err) { res.status(500).json({ error: err.message }); }
} catch (err) { console.error('[LearningAdmin]', err.message); res.status(500).json({ error: 'Internal server error' }); }
});
router.post('/categories', async function(req, res) {
@ -57,7 +60,7 @@ router.post('/categories', async function(req, res) {
);
res.json({ success: true, id: result.lastInsertRowid, slug: slug });
} catch (err) { res.status(500).json({ error: err.message }); }
} catch (err) { console.error('[LearningAdmin]', err.message); res.status(500).json({ error: 'Internal server error' }); }
});
router.put('/categories/:id', async function(req, res) {
@ -72,7 +75,7 @@ router.put('/categories/:id', async function(req, res) {
);
res.json({ success: true });
} catch (err) { res.status(500).json({ error: err.message }); }
} catch (err) { console.error('[LearningAdmin]', err.message); res.status(500).json({ error: 'Internal server error' }); }
});
router.delete('/categories/:id', async function(req, res) {
@ -81,7 +84,7 @@ router.delete('/categories/:id', async function(req, res) {
await db.run('UPDATE learning_content SET category_id = NULL WHERE category_id = ?', [req.params.id]);
await db.run('DELETE FROM learning_categories WHERE id = ?', [req.params.id]);
res.json({ success: true });
} catch (err) { res.status(500).json({ error: err.message }); }
} catch (err) { console.error('[LearningAdmin]', err.message); res.status(500).json({ error: 'Internal server error' }); }
});
// ============================================================
@ -103,7 +106,7 @@ router.get('/content', async function(req, res) {
[]
);
res.json({ success: true, content: content });
} catch (err) { res.status(500).json({ error: err.message }); }
} catch (err) { console.error('[LearningAdmin]', err.message); res.status(500).json({ error: 'Internal server error' }); }
});
router.get('/content/:id', async function(req, res) {
@ -131,7 +134,7 @@ router.get('/content/:id', async function(req, res) {
item.questions = questions;
res.json({ success: true, content: item });
} catch (err) { res.status(500).json({ error: err.message }); }
} catch (err) { console.error('[LearningAdmin]', err.message); res.status(500).json({ error: 'Internal server error' }); }
});
router.post('/content', async function(req, res) {
@ -147,7 +150,7 @@ router.post('/content', async function(req, res) {
);
res.json({ success: true, id: result.lastInsertRowid, slug: slug });
} catch (err) { res.status(500).json({ error: err.message }); }
} catch (err) { console.error('[LearningAdmin]', err.message); res.status(500).json({ error: 'Internal server error' }); }
});
router.put('/content/:id', async function(req, res) {
@ -171,14 +174,14 @@ router.put('/content/:id', async function(req, res) {
);
res.json({ success: true });
} catch (err) { res.status(500).json({ error: err.message }); }
} catch (err) { console.error('[LearningAdmin]', err.message); res.status(500).json({ error: 'Internal server error' }); }
});
router.delete('/content/:id', async function(req, res) {
try {
await db.run('DELETE FROM learning_content WHERE id = ?', [req.params.id]);
res.json({ success: true });
} catch (err) { res.status(500).json({ error: err.message }); }
} catch (err) { console.error('[LearningAdmin]', err.message); res.status(500).json({ error: 'Internal server error' }); }
});
// ============================================================
@ -213,7 +216,7 @@ router.post('/content/:contentId/questions', async function(req, res) {
}
res.json({ success: true, id: questionId });
} catch (err) { res.status(500).json({ error: err.message }); }
} catch (err) { console.error('[LearningAdmin]', err.message); res.status(500).json({ error: 'Internal server error' }); }
});
router.put('/questions/:id', async function(req, res) {
@ -241,14 +244,14 @@ router.put('/questions/:id', async function(req, res) {
}
res.json({ success: true });
} catch (err) { res.status(500).json({ error: err.message }); }
} catch (err) { console.error('[LearningAdmin]', err.message); res.status(500).json({ error: 'Internal server error' }); }
});
router.delete('/questions/:id', async function(req, res) {
try {
await db.run('DELETE FROM learning_questions WHERE id = ?', [req.params.id]);
res.json({ success: true });
} catch (err) { res.status(500).json({ error: err.message }); }
} catch (err) { console.error('[LearningAdmin]', err.message); res.status(500).json({ error: 'Internal server error' }); }
});
// ============================================================
@ -272,7 +275,7 @@ router.get('/stats', async function(req, res) {
totalAttempts: parseInt(totalAttempts.count)
}
});
} catch (err) { res.status(500).json({ error: err.message }); }
} catch (err) { console.error('[LearningAdmin]', err.message); res.status(500).json({ error: 'Internal server error' }); }
});
module.exports = router;

View file

@ -19,7 +19,7 @@ router.get('/categories', async function(req, res) {
[]
);
res.json({ success: true, categories: categories });
} catch (err) { res.status(500).json({ error: err.message }); }
} catch (err) { console.error('[LearningHub]', err.message); res.status(500).json({ error: 'Internal server error' }); }
});
// ============================================================
@ -47,7 +47,7 @@ router.get('/feed', async function(req, res) {
var total = await db.get('SELECT COUNT(*) as count FROM learning_content WHERE published = true', []);
res.json({ success: true, content: content, total: total ? parseInt(total.count) : 0 });
} catch (err) { res.status(500).json({ error: err.message }); }
} catch (err) { console.error('[LearningHub]', err.message); res.status(500).json({ error: 'Internal server error' }); }
});
// ============================================================
@ -70,7 +70,7 @@ router.get('/category/:slug', async function(req, res) {
);
res.json({ success: true, category: cat, content: content });
} catch (err) { res.status(500).json({ error: err.message }); }
} catch (err) { console.error('[LearningHub]', err.message); res.status(500).json({ error: 'Internal server error' }); }
});
// ============================================================
@ -112,7 +112,7 @@ router.get('/content/:slug', async function(req, res) {
item.progress = progress;
res.json({ success: true, content: item });
} catch (err) { res.status(500).json({ error: err.message }); }
} catch (err) { console.error('[LearningHub]', err.message); res.status(500).json({ error: 'Internal server error' }); }
});
// ============================================================
@ -129,7 +129,7 @@ router.post('/submit-quiz', async function(req, res) {
// Get all questions for this content
var questions = await db.all(
'SELECT q.id, q.question_text, q.explanation as general_explanation FROM learning_questions WHERE content_id = ? ORDER BY sort_order ASC',
'SELECT id, question_text, explanation as general_explanation FROM learning_questions WHERE content_id = ? ORDER BY sort_order ASC',
[contentId]
);
@ -177,7 +177,7 @@ router.post('/submit-quiz', async function(req, res) {
percentage: questions.length > 0 ? Math.round((score / questions.length) * 100) : 0,
results: results
});
} catch (err) { res.status(500).json({ error: err.message }); }
} catch (err) { console.error('[LearningHub]', err.message); res.status(500).json({ error: 'Internal server error' }); }
});
// ============================================================
@ -201,7 +201,7 @@ router.get('/search', async function(req, res) {
);
res.json({ success: true, content: content });
} catch (err) { res.status(500).json({ error: err.message }); }
} catch (err) { console.error('[LearningHub]', err.message); res.status(500).json({ error: 'Internal server error' }); }
});
module.exports = router;