pediatric-ai-scribe-v3/public/components/notes.html
Daniel ac39554c3a feat(notes): personal notes with rich-text editor + voice dictation
New "Notes" tab under Clinical Tools — a per-user scratchpad that's
explicitly NOT fed into AI prompts (distinct from user_memories).
Two-pane layout: searchable list on the left, rich-text editor with
title + save/edit/delete on the right.

Backend:
  migrations/…_add-personal-notes.js  — personal_notes table
    (id, user_id → users ON DELETE CASCADE, title, body, created_at,
    updated_at) with indexes on user_id + (user_id, updated_at).
  src/routes/notes.js — CRUD + one AI endpoint:
    GET    /api/notes           list, newest-updated first
    GET    /api/notes/:id       fetch one
    POST   /api/notes           create (title + body required)
    PUT    /api/notes/:id       update
    DELETE /api/notes/:id       remove
    POST   /api/notes/from-voice  transcript → { title, body }
                                   via callAI (admin-controlled
                                   provider — never selectable by
                                   the clinician).
    Body + title encrypted at rest via the same cryptoUtil used for
    user_memories; 500-note per-user cap; 200-char title / 50 KB
    body limits.

Frontend:
  public/components/notes.html — empty-state card ("Hello 👋"),
    sidebar list with search, editor head with voice-bar (Dictate /
    Pause / Resume / Stop + live timer + pulse indicator), Tiptap
    body, metadata footer. Uses existing .tp-* toolbar classes.
  public/js/notes.js — lazy-init on first tab activation; Tiptap
    editor built from window.Tiptap (same bundle the Content
    Manager uses); delegated list clicks; Ctrl/Cmd+S to save;
    uses app.js's AudioRecorder + transcribeAudio so the STT
    pipeline is shared. On stop → transcribe → /api/notes/from-
    voice → drop the AI-structured title + body into the editor;
    clinician reviews then saves.
  public/css/styles.css — 70 lines of .notes-* styles (card
    layout, warm empty-state with gradient icon + tips, pulse
    animation for the recording indicator, focus states, hover
    nudges).
  public/sw.js — bump cache from pedscribe-v12 → pedscribe-v12-
    notes1 so clients pick up the new module/component.

Admin-controlled STT provider: the recorder posts its audio blob
to the existing /api/transcribe (Google / LiteLLM / ElevenLabs /
Browser Whisper — whatever admin wired up in Settings). Users
cannot pick the provider from this UI.
2026-04-24 06:18:41 +02:00

87 lines
4.7 KiB
HTML

<div class="module-header">
<h2><i class="fas fa-note-sticky" style="color:#f59e0b;"></i> My Notes</h2>
<p>A quiet place for your thoughts — jot ideas, paste references, or dictate and let AI tidy it up. Only you can see these.</p>
</div>
<div class="notes-layout">
<!-- Left pane: list + new-note button + search -->
<aside class="notes-sidebar">
<div class="notes-sidebar-head">
<button id="btn-notes-new" class="btn-primary notes-new-btn" type="button">
<i class="fas fa-plus"></i> New note
</button>
<div class="notes-search">
<i class="fas fa-search"></i>
<input type="text" id="notes-search" placeholder="Search notes…" autocomplete="off">
</div>
</div>
<div id="notes-list" class="notes-list">
<div class="notes-empty">Loading…</div>
</div>
</aside>
<!-- Right pane: editor (hidden until a note is picked / created) -->
<section id="notes-editor" class="notes-editor hidden">
<div class="notes-editor-head">
<input type="text" id="note-title"
class="notes-title-input"
placeholder="Note title…"
maxlength="200"
autocomplete="off">
<div class="notes-editor-actions">
<!-- Voice → AI note: transcribes via the admin-configured
STT provider, then asks the AI to produce a clean note.
Pause/Resume available natively through MediaRecorder. -->
<div class="notes-voice-bar" id="notes-voice-bar">
<button id="btn-note-rec-start" class="btn-sm btn-ghost" type="button" title="Record voice → AI note">
<i class="fas fa-microphone" style="color:var(--red);"></i> Dictate
</button>
<button id="btn-note-rec-pause" class="btn-sm btn-ghost hidden" type="button">
<i class="fas fa-pause"></i> Pause
</button>
<button id="btn-note-rec-stop" class="btn-sm hidden" type="button" style="background:var(--red);color:white;border:none;">
<i class="fas fa-stop"></i> Stop
</button>
<span id="notes-rec-indicator" class="notes-rec-indicator hidden">
<span class="pulse-dot"></span>
<span id="notes-rec-state">Recording</span>
<span class="notes-rec-timer" id="notes-rec-timer">00:00</span>
</span>
</div>
<span id="notes-status" class="notes-status"></span>
<button id="btn-note-save" class="btn-sm btn-primary" type="button">
<i class="fas fa-floppy-disk"></i> Save
</button>
<button id="btn-note-delete" class="btn-sm" type="button" style="background:var(--red-light);color:var(--red);border:1px solid var(--red);">
<i class="fas fa-trash"></i> Delete
</button>
<button id="btn-note-close" class="btn-sm btn-ghost" type="button" title="Close editor">
<i class="fas fa-times"></i>
</button>
</div>
</div>
<div id="note-body-editor" class="notes-body-editor"></div>
<div class="notes-editor-foot">
<span id="note-meta" class="notes-meta"></span>
</div>
</section>
<!-- Empty placeholder — shown when no note is picked -->
<section id="notes-empty-state" class="notes-empty-state">
<div class="notes-empty-card">
<div class="notes-empty-icon"><i class="fas fa-feather"></i></div>
<h3>Hello 👋</h3>
<p>Pick a note on the left, or start a fresh one. You can type, paste, or tap <strong>Dictate</strong> to let AI clean up your voice into a polished note.</p>
<button id="btn-notes-new-empty" type="button" class="btn-primary notes-empty-cta">
<i class="fas fa-plus"></i> Create your first note
</button>
<div class="notes-empty-tips">
<div><i class="fas fa-bolt"></i> <span>Press <kbd>Ctrl</kbd>+<kbd>S</kbd> to save</span></div>
<div><i class="fas fa-microphone"></i> <span>Dictate works even offline with Browser Whisper</span></div>
<div><i class="fas fa-lock"></i> <span>Encrypted at rest — only you can read them</span></div>
</div>
</div>
</section>
</div>