Four changes landing together so the UX flows properly.
1. Fix voice-generation model error.
/api/notes/from-voice was passing `req.body.model` (undefined)
through to callAI, which resolved to LITELLM_DEFAULT_MODEL (empty
for LiteLLM deployments with no explicit default) → `model=""` →
LiteLLM 400 "Invalid model name". Server now reads the admin-
configured `models.default` setting, falls back to the
LITELLM_DEFAULT_MODEL env var, and only passes a model if one
resolved. Empty string never reaches the provider.
2. Read mode as the default post-save.
Opening an existing note or saving a new one now lands on a
clean, read-only rendering of the body (sanitized HTML with an
allowlist — p/h2/h3/strong/em/u/s/a/ul/ol/li/blockquote/code/pre).
Click the "Edit" button to switch to the Tiptap editor. Matches
the mental model of "notes are documents I review, not drafts I'm
always editing."
3. Autosave during edit.
Title-input + Tiptap onUpdate trigger a 1.2-second-debounced save.
In-flight saves coalesce: if the user types more while a save is
in progress, a follow-up fires right after it lands so the last
keystroke never gets stranded. beforeunload uses navigator.
sendBeacon to best-effort flush on tab close. The manual Save
button is kept as a "save + switch to reader" shortcut. Ctrl/
Cmd+S still works in the editor.
4. Mobile layout.
The two-pane layout collapses to a single-pane view below 900px,
driven by a data-view attribute on .notes-layout. Back buttons
appear in the reader + editor heads on mobile to return to the
list. Desktop layout unchanged (sidebar + right pane always
visible).
Also:
• CSS specificity fix — .hidden{display:none} was losing to
.notes-rec-indicator{display:inline-flex} on source order, so
the "Recording" indicator was showing when idle. Added explicit
.notes-rec-indicator.hidden + .notes-voice-bar .btn-sm.hidden
overrides at higher specificity.
• Reader-body sanitizer — allowlist of safe tags + attributes;
<a> links get target=_blank + rel=noopener.
• SW cache bumped to pedscribe-v12-notes2 so clients pick up the
new module / component / CSS.
115 lines
6.2 KiB
HTML
115 lines
6.2 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>
|
|
|
|
<!-- data-view controls which pane is visible on mobile (<900px):
|
|
"list" — sidebar only, "reader" — read pane only,
|
|
"editor" — edit pane only. On desktop the sidebar is always
|
|
visible and the right pane flips between reader/editor. -->
|
|
<div class="notes-layout" id="notes-layout" data-view="list">
|
|
|
|
<!-- 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>
|
|
|
|
<!-- Reader pane — shown by default after opening any saved note -->
|
|
<section id="notes-reader" class="notes-reader hidden">
|
|
<div class="notes-reader-head">
|
|
<button type="button" class="btn-sm btn-ghost notes-back" id="btn-notes-reader-back" title="Back to list">
|
|
<i class="fas fa-arrow-left"></i>
|
|
</button>
|
|
<h2 id="notes-reader-title" class="notes-reader-title">Note title</h2>
|
|
<div class="notes-reader-actions">
|
|
<button id="btn-note-edit" class="btn-sm btn-primary" type="button">
|
|
<i class="fas fa-pen"></i> Edit
|
|
</button>
|
|
<button id="btn-note-delete-read" class="btn-sm" type="button"
|
|
style="background:var(--red-light);color:var(--red);border:1px solid var(--red);">
|
|
<i class="fas fa-trash"></i>
|
|
</button>
|
|
</div>
|
|
</div>
|
|
<div id="notes-reader-body" class="notes-reader-body"></div>
|
|
<div class="notes-reader-foot">
|
|
<span id="notes-reader-meta" class="notes-meta"></span>
|
|
</div>
|
|
</section>
|
|
|
|
<!-- Editor pane — shown when creating a new note or editing an existing one -->
|
|
<section id="notes-editor" class="notes-editor hidden">
|
|
<div class="notes-editor-head">
|
|
<button type="button" class="btn-sm btn-ghost notes-back" id="btn-notes-editor-back" title="Back">
|
|
<i class="fas fa-arrow-left"></i>
|
|
</button>
|
|
<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>
|
|
</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 on desktop when nothing's 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 turns your voice into a clean note</span></div>
|
|
<div><i class="fas fa-lock"></i> <span>Encrypted at rest — only you can read them</span></div>
|
|
</div>
|
|
</div>
|
|
</section>
|
|
|
|
</div>
|