Studio -> Magazines: library, contents tree grouped by section with
confidence/pages/listening-time/audio status, the Stage D review
screen (tick/untick layout blocks, correct title/author, approve or
reject), a reflowed article view with click-to-seek sentence
highlighting, a linked original-page view showing the source bounding
box, and a podcast panel (2-3 host pickers, script generation,
turn-by-turn playback).
Wired into the existing lazy-module system (module-manifest.json,
loader.js, nav.js) rather than a separate app. Reuses /api/tts-preview
for both narration and podcast turns -- no new audio pipeline.
Fixes found building this:
- The pane toggle used role="tab" without the full ARIA tabs pattern
(aria-selected, roving tabindex, arrow-key nav) -- fails WCAG 2.2 AA
and hid the buttons from role-based tooling. Replaced with a button
group + aria-pressed, matching the plain-click interaction it
actually has.
- Print layout cues ("(continued on page 8)") were being sent to TTS
verbatim -- narration would have spoken the parenthetical aloud.
Stripped from the narration copy only; layout.json keeps the
original for the review screen.
- has_podcast now comes from the articles listing so opening an
article without a generated script skips a doomed GET instead of a
routine 404 on every open.
Verified against the real API contract with Playwright (library,
review pre-selection, approval, sentence highlighting + PDF-page
linking, podcast approval gate, zero console errors across the flow)
and manually end-to-end against a real ingested fixture, screenshots
included. Full suite: 94 Python tests, 33/34 desktop Playwright tests
(one unrelated pre-existing flaky DOM-count assertion).
Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
120 lines
6.0 KiB
HTML
120 lines
6.0 KiB
HTML
<div class="section-head">
|
|
<span class="section-icon"><span class="mdi mdi-newspaper-variant-outline"></span></span>
|
|
<div class="section-title">
|
|
<h2>Magazines</h2>
|
|
<p>Listen to a magazine one article at a time — from its true beginning to its true end, without adverts, sidebars or a neighbouring piece spliced into the middle.</p>
|
|
</div>
|
|
</div>
|
|
|
|
<!-- ── Library ──────────────────────────────────────────────────────── -->
|
|
<div id="mag-view-library">
|
|
<div class="card mag-new">
|
|
<h3>Add a magazine or paper</h3>
|
|
<div class="mag-new-grid">
|
|
<label for="mag-new-title">Title
|
|
<input id="mag-new-title" type="text" placeholder="Scientific American" autocomplete="off">
|
|
</label>
|
|
<label for="mag-new-issue">Issue
|
|
<input id="mag-new-issue" type="text" placeholder="September 2026" autocomplete="off">
|
|
</label>
|
|
<label for="mag-new-file">PDF
|
|
<input id="mag-new-file" type="file" accept="application/pdf">
|
|
</label>
|
|
<button type="button" class="btn primary" onclick="magCreate()">
|
|
<span class="mdi mdi-plus"></span> Add magazine
|
|
</button>
|
|
</div>
|
|
<p class="muted">Scanned issues work too — pages without selectable text are read with OCR.</p>
|
|
</div>
|
|
|
|
<h3>Your magazines</h3>
|
|
<div id="mag-library-list" class="mag-grid"></div>
|
|
</div>
|
|
|
|
<!-- ── Contents ─────────────────────────────────────────────────────── -->
|
|
<div id="mag-view-contents" hidden>
|
|
<div class="mag-bar">
|
|
<button type="button" class="btn-sm" onclick="magShow('library')">
|
|
<span class="mdi mdi-arrow-left"></span> All magazines
|
|
</button>
|
|
<h3 id="mag-doc-title">Magazine</h3>
|
|
<div class="mag-bar-actions">
|
|
<button type="button" class="btn-sm" onclick="magIngest()">
|
|
<span class="mdi mdi-refresh"></span> Re-run detection
|
|
</button>
|
|
</div>
|
|
</div>
|
|
<div id="mag-ingest-status" class="mag-status" hidden></div>
|
|
<div id="mag-contents"></div>
|
|
</div>
|
|
|
|
<!-- ── Review (Stage D) ─────────────────────────────────────────────── -->
|
|
<div id="mag-view-review" hidden>
|
|
<div class="mag-bar">
|
|
<button type="button" class="btn-sm" onclick="magBackToContents()">
|
|
<span class="mdi mdi-arrow-left"></span> Contents
|
|
</button>
|
|
<h3>Review article boundaries</h3>
|
|
</div>
|
|
<div id="mag-review"></div>
|
|
</div>
|
|
|
|
<!-- ── Reading ──────────────────────────────────────────────────────── -->
|
|
<div id="mag-view-read" hidden>
|
|
<div class="mag-bar">
|
|
<button type="button" class="btn-sm" onclick="magBackToContents()">
|
|
<span class="mdi mdi-arrow-left"></span> Contents
|
|
</button>
|
|
<div class="mag-read-id">
|
|
<h3 id="mag-read-title">Article</h3>
|
|
<span id="mag-read-by" class="muted"></span>
|
|
<span id="mag-read-pages" class="muted"></span>
|
|
</div>
|
|
</div>
|
|
|
|
<div class="mag-controls">
|
|
<label for="mag-voice" class="mag-voice-pick">Narrator
|
|
<select id="mag-voice"></select>
|
|
</label>
|
|
<button type="button" class="btn primary" id="mag-play" onclick="magPlay()"
|
|
aria-label="Play narration">
|
|
<span class="mdi mdi-play"></span> Listen
|
|
</button>
|
|
<span id="mag-read-progress" class="muted" aria-live="polite"></span>
|
|
<div class="mag-pane-tabs" role="group" aria-label="Reading view">
|
|
<button type="button" class="btn-sm active" data-mag-pane="reflow"
|
|
aria-pressed="true" onclick="magSetPane('reflow')">Article</button>
|
|
<button type="button" class="btn-sm" data-mag-pane="pdf"
|
|
aria-pressed="false" onclick="magSetPane('pdf')">Original page</button>
|
|
</div>
|
|
</div>
|
|
|
|
<div id="mag-pane-reflow" class="mag-pane">
|
|
<div id="mag-article" class="mag-article"></div>
|
|
</div>
|
|
|
|
<div id="mag-pane-pdf" class="mag-pane" hidden>
|
|
<div class="mag-page-wrap">
|
|
<img id="mag-page-img" alt="">
|
|
<div id="mag-page-box" class="mag-page-box" hidden></div>
|
|
</div>
|
|
<p id="mag-page-label" class="muted"></p>
|
|
</div>
|
|
|
|
<!-- Podcast mode -->
|
|
<details class="card mag-podcast">
|
|
<summary><span class="mdi mdi-podcast"></span> Podcast mode — discuss this article</summary>
|
|
<p class="muted">Two or three hosts discuss the article for someone who has not read it. The script is written by the configured LLM from the article text; it will not invent facts, but it is a generated discussion, not the author’s words.</p>
|
|
<div class="mag-hosts">
|
|
<label for="mag-host-a">Host A<select id="mag-host-a"></select></label>
|
|
<label for="mag-host-b">Host B<select id="mag-host-b"></select></label>
|
|
<label for="mag-host-c">Host C <span class="muted">(optional)</span><select id="mag-host-c"></select></label>
|
|
</div>
|
|
<div class="mag-podcast-actions">
|
|
<button type="button" class="btn" id="mag-podcast-gen" onclick="magGeneratePodcast()">Generate script</button>
|
|
<button type="button" class="btn primary" id="mag-podcast-play" onclick="magPlayPodcast()" hidden>Play podcast</button>
|
|
</div>
|
|
<div id="mag-podcast-script"></div>
|
|
</details>
|
|
</div>
|