Compare commits

Author SHA1 Message Date
Codex 08f42a5f48 Refine presentation flow and speaker notes 2026-09-24 22:28:58 +02:00
Codex d5310d5a3f Update ThothII presentation tour, annotated screenshots and speaker notes 2026-09-24 12:09:20 +02:00
Codex 5217c574e9 Update interactive presentation and presenter console 2026-09-20 19:43:33 +02:00
Codex 348f4a9738 Revert "presentation: publishable Reveal.js root — index.html at presentation root with dist/ and plugin/ (copy of deck/ adjusted), per remote deployment by clone; deck/ stays the working source"
This reverts commit d54f1ca8b5.
2026-09-08 00:38:13 +02:00
Codex d54f1ca8b5 presentation: publishable Reveal.js root — index.html at presentation root with dist/ and plugin/ (copy of deck/ adjusted), per remote deployment by clone; deck/ stays the working source 2026-09-08 00:24:21 +02:00
Codex 32ce52706f deck slide 02 impeccable pass: island group centered on slide axis (+39px, was 601 vs 640) — gaps to middle column evened 70/70 (was 109/31) 2026-09-07 23:54:30 +02:00
Codex a1b75c0946 deck slide 08 impeccable pass: left column starts flush with panel top (zero first-child onto-points margin), out-header gets 10px air before first beat; columns top- and bottom-aligned (delta 0 verified); css v40 2026-09-07 23:45:02 +02:00
Codex 8f59da412c deck slide 08: split layout — current content (4 numbered points + quiet quality-gate box) contained in left column, new right panel 'The advantage: speed, precision, determinism' (machine speed / audited precision / deterministic by design); css v39 2026-09-07 23:41:44 +02:00
Codex 32ed2d1c14 overview: slide 07 approved as FINAL (user confirms ported variant D) 2026-09-07 23:23:01 +02:00
Codex 6f58be6cbd deck slide 07 impeccable layout pass: tier groups separated (22px tier1->tier2, 18px before points), split hugs content and re-centers vertically (no full-height stretch), output panel beats tightened to 13px padding rhythm; css v37 2026-09-07 23:15:52 +02:00
Codex 4127c1e590 deck slide 07: port approved prototype variant D — EN-first category pills (tier1 tinted, tier2 white, counts in badges), numbered points, right output panel 'The output: text becomes recorded data'; drop orphaned tier/chipc css; css v36 2026-09-07 23:07:18 +02:00
Codex bdfdfca2e5 prototype slide 07: variant D — ontology left, right panel 'The output: text becomes recorded data' (quantitative data on records / into DWH flow / as if typed at the visit) 2026-09-07 22:58:19 +02:00
Codex 446c39b372 deck slide 03: pillars row cleared from footer line — flow-slide scoped compression (fbox 8px, fcol gap 6, zone 26/12, flow mt 10) restores designed 22px gap (was -2.5px overlap); css v35 2026-09-07 22:55:27 +02:00
Codex 8df9535afa prototype slide 07: 3 layout variants (?variant=A|B|C) — three columns / editorial bands / ontology+why-panel; all EN-primary per foreign audience 2026-09-07 22:52:06 +02:00
Codex 67d96a44de deck slide 04: challenges list stretches to rail bottom (midrow stretch + citem flex:1, delta 0 verified); rail lbl padding-left 16px clears the descending spine; css v34 2026-09-07 22:25:06 +02:00
Codex 1c22e4e60b deck slide 04: single alignment axis at x=692 — rail spine widened to 540px (coincides with ThothII box left edge), midrow gap 0 so four-challenges hairlines end flush on the rail line; css v33 2026-09-07 22:10:56 +02:00
Codex a5bedfbae4 overview: slide 06 approved as FINAL (user sign-off) 2026-09-07 22:04:59 +02:00
Codex b5c35eda24 deck slide 07: tier 2 chips same bordeaux as tier 1 (drop .chipc.t2 override + t2 classes); css v32 2026-09-07 22:04:59 +02:00
Codex 164df8986f overview: slide 05 approved as FINAL (user sign-off on numbers strip + component cycle) 2026-09-07 21:50:18 +02:00
Codex 5a18c546ce deck slide 05: JS-drawn edge-to-edge cycle arrows (precise start/land on measured component boxes); note as two wide bullets, 14px, clinician wording 2026-09-07 21:47:12 +02:00
Codex 7177c346ab deck slide 05: numbers as slim top strip; analysis components on ellipse vertices with arc arrows; stat note explained in plain language 2026-09-07 21:38:13 +02:00
Codex c28e2e2eba deck slide 05: compact numeral band on top (+monthly letters rate), analysis components as clinician-friendly flow with icons in the center 2026-09-07 21:21:16 +02:00
Codex e60f60261b deck: circuit-brain medallions on Sources and Staging too; fix centering (.stage span specificity overrode inline-flex) 2026-09-07 21:10:37 +02:00
Codex 744af1d8c7 deck: rail connector starts from Marts bottom edge, not top (was crossing the box) 2026-09-07 21:05:49 +02:00
Codex 54da406971 prototype: deck overview cells clickable — jump to full-size slide 2026-09-07 20:52:05 +02:00
Codex 0635e99b5d overview: slides 01-04 approved as FINAL (user sign-off on 'The problem' composition) 2026-09-07 20:45:02 +02:00
Codex da60b6af36 deck: port approved 'The problem' composition — four challenges + outputs rail from Marts + ThothII right with drawn arrow + AI circuit-brain medallions (.brain-ai, no clash with slide-3 .brain) 2026-09-07 20:42:05 +02:00
Codex 197c165a0a prototype: deck overview — live contact sheet of all 14 slides with status 2026-09-07 20:30:07 +02:00
Codex 3327018a87 prototype: dedicated AI circuit-brain SVG medallion (36px, center seam + circuit traces) — human 🧠 emoji stays reserved for human-in-the-loop points 2026-09-07 18:31:48 +02:00
Codex 534c424d88 prototype: replace the two red AI badges with the slide-3 brain medallion (Integration stage + ThothII box) 2026-09-07 18:21:11 +02:00
Codex 64530c20af prototype: ThothII bottom-right with arrow up to Marts; center blocks become the four challenges (volume/structure, ambiguity, ontology, reliability) 2026-09-07 17:56:52 +02:00
Codex de1524838f prototype: fill empty center with text-analysis deep-dive preview (01-04 concepts) beside the outputs rail 2026-09-07 17:24:04 +02:00
Codex 260bb174a2 deck+prototype: EN translations same size as Italian; prototype option C revised — rail drops from Marts, no status badges, uniform intensity, capitalized box texts 2026-09-07 17:15:06 +02:00
Codex 8743f0776b prototype: slide 'The problem' — 3 options for datamart outputs zone (branch / products band / maturity rail) 2026-09-07 17:01:53 +02:00
Codex 1e139c16ec deck: delete slide 04 (AI mappings), renumber to 14; IT/EN translation pairs on text-analysis slides; Sources = four islands 2026-09-07 17:01:53 +02:00
Codex 9bef40bcf3 deck: text-analysis block 05-09 impeccable pass — quote hero (side-stripe removed), editorial stat band, code panel hug, vertical centering, ontology tier counts 2026-09-07 16:30:25 +02:00
Codex 8856ec7274 deck: slide 01 — title lead on one line at 19px, centered (drop 40em measure that forced the wrap) 2026-09-07 16:10:06 +02:00
Codex 91889da042 deck: slide 01 — vertical centering via fixed dead selector + 19px title lead on 40em measure 2026-09-07 15:58:26 +02:00
Codex 747215042c deck: slide 02 port-completion — ? on all four pieces, 114px islands, overlap-free layout, hint inside scatter, four-pieces speaker notes 2026-09-07 15:38:13 +02:00
Codex f8496af3c9 fix: commit reveal dist files excluded by root dist/ gitignore (404 in worktree preview) 2026-09-07 15:27:49 +02:00
Codex 0ad8577621 deck: slide 02 ported to Orbit composition + missing-piece popups
- scattered island map replaces the syscard grid: four systems with
  emblem icons, names and scopes, clickable (problem popups)
- central 'Missing Pieces' column: four dashed rects with '?' marks,
  clickable (popups explaining each gap with 'The gap' box)
- title 'four missing pieces' to match the four destinations
- deck.css: scatter/isle/todo/missing-title styles
2026-09-07 12:36:39 +02:00
41 changed files with 2631 additions and 341 deletions
+64 -1
View File
@@ -1,6 +1,6 @@
# ThothII — Project State
Last updated: 2026-09-06.
Last updated: 2026-09-24 (11-slide deck, consolidated project opening and final contacts).
This file is the short operational snapshot. Stable commands and the architecture mental model
live in `AGENTS.md`; current design and runtime contracts live under `docs/architecture/`,
@@ -12,6 +12,69 @@ Authentik, internal catalog/embedding services, and the PSD workspace repository
`docs/operations/server-upgrade-gitea-workspace-v2.md`. Treat its operator gates and rollback
requirements as mandatory; do not replace the running server stack in place.
## Presentation publishing
The deck now has 11 slides. The cover remains; “Where we started” is folded into
“What we wanted to build”, now slide 2. Its speaker notes open with the four goals,
and its existing popups include the disconnected sources and missing capabilities.
Speaker notes now read as continuous prose without titles. On slide 2 and slides
7–10, parenthesized cues identify the corresponding popup button at the start of
its paragraph or dedicated popup text. The closing slide has no popup cues.
The console reading area also omits slide and popup headings; popup controls
remain below the preview. Edits preserve the content and keep the spoken text close
to its previous length; actual delivery time still depends on rehearsal. The former
penultimate “One datamart — many questions answered” slide is removed. The ThothII
tour leads directly to “Thank you”, with both contact emails in bold 32px type.
Slide 8 now has four popups: Home page, Patient data (original PNG 3),
Dashboard catalogue (PNG 6), and Brugada dashboard (PNG 7), numbered 1–4.
Each has dedicated presenter notes; the opening explains the portal's breadth,
the limited time, and Marco and Sara's availability for an in-person or remote
follow-up. The overview uses a two-by-two grid; unused PNGs remain on disk.
Presentation PNG source (user instruction, 2026-09-23): all PNGs to import come from
`/Users/mp/Desktop/ScreenshotPresentation/ThothIIScreens/`. This is the verified on-disk
path the user refers to as `desktop/screenshot/presentation/thothIIscreens`.
Copy supplied files unchanged into `presentation/deck/screenshots/thothii/`, preserving
their embedded annotations. The 12 source PNGs retain their `01` through `12` filenames.
The shortened slide 10 tour uses PNGs 01, 02, 05, 07, 08 and 09, numbered 1–6
in the interface; unused images remain on disk. The 284-word presenter script
covers all eight phases, explicitly introduces Human in the Loop, and includes
the invitation to a longer presentation at the conference or remotely.
The planned duration is 2:50, to be confirmed by rehearsal.
Slide 11 prominently displays Marco Pancotti's and Sara Paratico's email addresses.
Local preview uses `python3 -m http.server 8000 --bind 127.0.0.1 --directory presentation`.
The first popup uses
`01-StartingPoint.png`; popup 2 uses `02-Disambiguation01.png`. Refresh the image
cache version when replacing a PNG. During the current editing session, refresh both
operational browser windows after every modification, as explicitly requested.
Popup 3 uses `05-CloseSchemaLinking.png`, popup 4 uses `07-CTE01.png`,
popup 5 uses `08-FinalSQL.png`, and popup 6 uses `09-DatamartProduction.png`.
The HTML references PNGs by relative URL: they are external static assets, not
compiled or embedded. Keep `presentation/deck/screenshots/thothii/` with the deck;
the Desktop source folder is not a runtime dependency.
On 2026-09-23, the complete source folder was backed up to
`/Users/mp/Desktop/ThothIIScreens-backup-20260923-FkCqV3/` and verified by SHA-256.
The four unused PNGs (original prefixes 02, 05, 09, and 10) were removed from the
source folder and, on 2026-09-24, from the deck after verifying backup equality.
The backup retains all 16 original PNGs under their original names. Renumbering
preserved each file's bytes; the existing Starting point images in the source and
deck differ and were each retained unchanged.
On 2026-09-24, annotated PNGs 08–12 were updated directly in the deck and verified
visually; these deck copies are newer than the Desktop source copies. Preserve
them when importing images. Their image cache version is `v=5`. Presenter notes
for popups 09–12 are also updated in `presentation/deck/index.html`. The two Snagit
comment boxes in PNG 12 share the same horizontal centre (1676.5 px in the
3830 px source image); their text is centred within each box.
The user confirmed the public AritmoLab presentation and presenter console working on
2026-09-20. Updates use the `feat/ai-etl-presentation` branch on Gitea: commit and push
locally, then the user runs `git pull --ff-only origin feat/ai-etl-presentation` in
`/var/www/aritmolab/presentation` on the remote server. No SSH tunnel is available;
the user can operate an authenticated remote terminal in VS Code. Nginx routes are
already configured, so ordinary slide updates need no Nginx reload. Exact URLs,
paths, checks and cache behavior: [Presentation publishing](docs/operations/presentation-publishing.md).
## Current product shape
ThothII is a human-in-the-loop datamart builder with three independently built layers:
+105
View File
@@ -0,0 +1,105 @@
# Updating the AritmoLab presentation
Last confirmed by the user: 2026-09-20, after deploying commit `5217c574`.
The user reported both the presentation and presenter console working. This is
the static presentation deployment, separate from the ThothII application stack.
## Locations
- Local worktree: `/Users/mp/projects/ThothII-presentazione`.
- Source: `presentation/deck/` and `presentation/presenter/`.
- Git remote: `origin`, `https://git.tylconsulting.it/mptyl/ThothII.git`.
- Publication branch: `feat/ai-etl-presentation`.
- Remote checkout: `/var/www/aritmolab/presentation`.
Presentation:
```text
https://aritmolab.policlinicosandonato.it/presentation/
```
Presenter console:
```text
https://aritmolab.policlinicosandonato.it/presentation/presenter/
```
Give these URLs as copyable plain text when providing instructions: the user
reported difficulty clicking Markdown links.
## Routine update
1. In the local worktree, review and validate the requested presentation changes,
then commit and push the intended files to `origin/feat/ai-etl-presentation`.
Include new images, styles, scripts and presenter files. A remote pull cannot
receive changes that remain uncommitted or unpushed. Leave unrelated changes alone.
2. The user connects to the remote server and runs:
```bash
cd /var/www/aritmolab/presentation
git branch --show-current
git status --short
```
Continue only when the branch is `feat/ai-etl-presentation` and there are no
unexpected local changes. If either differs, inspect before updating; do not
reset, discard files or merge the presentation branch into another branch.
```bash
git pull --ff-only origin feat/ai-etl-presentation
git log -1 --oneline
```
3. Refresh both browser windows. Static responses advertise
`Cache-Control: public, max-age=300`, so a browser may reuse assets for five
minutes. Use a hard refresh if the old version remains visible; bump asset
query versions when changing cached JavaScript or CSS.
4. When network access is available, verify the expected version at both URLs,
including screenshots and synchronization of presenter and audience popups.
Distinguish a successful Git push from a completed remote deployment.
Changes to slides, notes, images, CSS and JavaScript need no build, application
restart or Nginx reload. A reload is necessary only if Nginx configuration changes.
## Remote access
The site was reachable from the user's Mac while connected to the VPN.
Do not use SSH tunnels for this deployment. A non-interactive direct SSH attempt
on 2026-09-20 failed authentication. The user offered an authenticated SSH session
inside VS Code, then chose to execute the remote commands personally. The normal
workflow is to push locally and provide the remote pull command to the user;
do not assume autonomous remote access or ask for passwords in chat.
## Nginx routing already configured
The existing `/presentation` route redirects to `/presentation/`.
The user supplied the original deck alias and subsequently confirmed successful
setup of the two additional routes for the console:
| URL prefix | Filesystem alias |
| --- | --- |
| `/presentation/` | `/var/www/aritmolab/presentation/presentation/deck/` |
| `/presentation/deck/` | `/var/www/aritmolab/presentation/presentation/deck/` |
| `/presentation/presenter/` | `/var/www/aritmolab/presentation/presentation/presenter/` |
These are sibling `location ^~` blocks within the site's `server` block. The
specific `deck/` route supports the console's relative links to fonts and slide
iframes. The configuration filename was not recorded.
The presentation is intentionally public static content, bypassing Django and
Authentik. Preserve its existing routing and protections: directory listings off,
`index index.html`, GET/HEAD only, five-minute cache, `nosniff`, `SAMEORIGIN`, and
`noindex, nofollow, noarchive`. Do not expose the entire repository or change
unrelated application routes.
Only if routing needs diagnosis:
```bash
sudo nginx -T 2>/dev/null | grep -n -A 12 -B 3 '/presentation'
```
Only after an intentional configuration edit:
```bash
sudo nginx -t && sudo systemctl reload nginx
```
+39
View File
@@ -0,0 +1,39 @@
/* Artificial-brain controls and clearly separated explanatory callouts. */
.arit.flow-slide .fcol.ai-hit { flex-basis: 62px; }
.arit.flow-slide .brain-btn {
width: 56px; height: 56px; border-radius: 9px;
border: 1.5px solid var(--bordeaux); background: var(--surface);
box-shadow: 0 3px 8px oklch(26% .02 15 / .1);
}
.arit.flow-slide .fcol.ai > .brain-btn { top: -54px; right: 6px; }
.arit.flow-slide .brain-btn:hover, .arit.flow-slide .brain-btn:focus-visible {
transform: none; background: var(--bordeaux-tint);
box-shadow: 0 0 0 4px var(--bordeaux-tint);
}
.arit.flow-slide .brain-btn[aria-pressed="true"] { outline: 3px solid var(--bordeaux); outline-offset: 4px; }
.artificial-brain { display: block; width: 46px; height: 46px; }
.arit.flow-slide .brain-btn .ai-label {
position: absolute; left: 50%; top: -24px; transform: translateX(-50%);
display: grid; place-items: center; width: 42px; height: 25px;
border: 1.5px solid var(--bordeaux); border-radius: 5px 5px 0 0;
background: var(--bordeaux); color: var(--surface);
font: 800 18px/1 var(--sans); letter-spacing: .04em;
}
.arit.flow-slide .brain-btn .ai-number {
display: none;
}
/* The first AI control is in normal flow, but still anchors its AI label. */
.arit.flow-slide .fcol.ai-hit .brain-btn { position: relative; top: auto; right: auto; }
.ai-pop.is-ai .ai-pop-card {
box-sizing: border-box;
width: min(540px, calc(100vw - 48px)); max-height: calc(100vh - 48px);
overflow-y: auto; border: 2px solid var(--bordeaux); background: var(--surface);
transform: none; transition: none;
}
.ai-pop.is-ai .ai-line line { stroke: var(--bordeaux); stroke-width: 4; }
.ai-pop.is-ai .ai-line circle { fill: var(--bordeaux); }
.ai-pop-title .artificial-brain { width: 40px; height: 40px; }
.ai-pop-title #aiPopBrain { flex: 0 0 44px; width: 44px; height: 44px; border: 0; background: none; }
@media (prefers-reduced-motion: reduce) {
.arit.flow-slide .brain-btn { transition: none; }
}
+9
View File
@@ -0,0 +1,9 @@
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 64 64" fill="none" stroke="#8f1204" stroke-width="2.5" stroke-linecap="round" stroke-linejoin="round">
<path d="M29 9h-9l-8 8v8l-5 5v12l8 8h6l5 5h6V15zm6 0h9l8 8v8l5 5v12l-8 8h-6l-5 5h-6" fill="#f4dfe1"/>
<path d="M20 17v9h12m-19 9h10v10m-7-1h7m21-27v9H32m19 9H41v10m7-1h-7M26 8V4m12 4V4M7 30H3m54 0h4M26 55v5m12-5v5"/>
<rect x="25" y="27" width="14" height="14" rx="2" fill="#fdfcfc"/>
<path d="M29 23v4m6-4v4m-6 14v4m6-4v4m-14-14h4m-4 6h4m14-6h4m-4 6h4"/>
<circle cx="20" cy="17" r="2.5" fill="#fdfcfc"/><circle cx="44" cy="17" r="2.5" fill="#fdfcfc"/>
<circle cx="23" cy="46" r="2.5" fill="#fdfcfc"/><circle cx="41" cy="46" r="2.5" fill="#fdfcfc"/>
<path d="M30 32h4v4h-4z" fill="#8f1204" stroke="none"/>
</svg>

After

Width:  |  Height:  |  Size: 782 B

+168 -48
View File
@@ -44,16 +44,19 @@
font: 600 10px/1.4 var(--sans); letter-spacing: .03em; color: var(--ink-muted);
}
.arit .foot span { white-space: nowrap; flex-shrink: 0; }
.presenter-preview .arit .foot { display: none; }
.arit .foot span:first-child { overflow: hidden; text-overflow: ellipsis; flex-shrink: 1; min-width: 0; }
.arit .foot span:first-child { overflow: hidden; text-overflow: ellipsis; flex-shrink: 1; }
.arit .sbody { flex: 1; min-height: 0; display: flex; flex-direction: column; padding: 30px 48px 22px; text-align: left; }
.arit .title-slide .sbody { justify-content: center; }
.arit.title-slide .sbody { justify-content: center; }
.arit.center-v .sbody { justify-content: center; }
/* ---------- typography ---------- */
.arit .kicker { font: 700 12px/1 var(--sans); letter-spacing: .16em; text-transform: uppercase; color: var(--bordeaux); margin-bottom: 14px; }
.arit h1 { font: 700 46px/1.15 var(--sans); letter-spacing: -.01em; color: var(--ink); margin: 0; }
.arit h2 { font: 700 30px/1.15 var(--sans); letter-spacing: -.01em; color: var(--ink); margin: 0 0 8px; }
.arit .lead { font: 400 16px/1.5 var(--sans); color: var(--ink-muted); margin: 8px 0 0; }
.arit.title-slide .lead { font-size: 19px; text-align: center; }
.arit .hgap { height: 16px; flex: 0 0 16px; }
/* title slide */
@@ -71,27 +74,83 @@
/* quote */
.arit .quote {
margin: 14px 0 20px; padding: 16px 22px; border-left: 3px solid var(--bordeaux);
background: var(--surface-alt); border-radius: 0 8px 8px 0;
font: italic 400 16px/1.5 var(--display); color: var(--ink);
position: relative; margin: 14px 0 20px; padding: 20px 26px 18px 68px; border: 1px solid var(--line);
background: var(--surface-alt); border-radius: 8px;
font: italic 400 19px/1.55 var(--display); color: var(--ink);
}
.arit .quote::before { content: '\201C'; position: absolute; left: 22px; top: 10px;
font: 700 52px/1 var(--display); color: var(--bordeaux); }
/* EN translations alongside Italian clinical text */
.arit .tr-grid { display: grid; grid-template-columns: 1.12fr 1fr; gap: 24px; align-items: center; }
.arit .tr-grid .it, .arit .tr-grid .en { margin: 0; }
.arit .tr-grid .en { padding-left: 20px; border-left: 1px solid var(--line-strong);
font: 400 19px/1.5 var(--sans); color: var(--bordeaux-deep); }
.arit .en-tag { display: inline-block; margin-right: 8px; padding: 2.5px 8px; border-radius: 4px;
background: var(--bordeaux); color: #fdfcfc; font: 700 9.5px/1 var(--sans); letter-spacing: .12em;
vertical-align: 2px; }
.arit .points .g { color: var(--bordeaux-deep); }
.arit .chipc .g { font-weight: 400; opacity: .78; }
.arit .code .en { font: 400 13px/1.5 var(--sans); color: var(--bordeaux-deep); }
.arit .code .en::before { content: 'EN'; display: inline-block; margin-right: 8px; padding: 2px 6px;
border-radius: 3px; background: var(--bordeaux); color: #fdfcfc;
font: 700 8.5px/1 var(--sans); letter-spacing: .1em; }
/* ---------- slide "The problem": challenges + outputs rail + ThothII right ---------- */
.arit.problem .sbody { padding-top: 20px; padding-bottom: 14px; }
.arit.problem .kicker { margin-bottom: 10px; }
.arit.problem .quote { margin: 0 0 10px; padding: 12px 22px 10px 64px; font-size: 16.5px; }
.arit.problem .tr-grid .en { font-size: 16.5px; }
.arit.problem .stage { padding: 11px 10px; }
.arit.problem .stage b { font-size: 13px; text-transform: uppercase; }
.arit.problem .stage span { font-size: 10.5px; text-transform: uppercase; letter-spacing: .02em; }
.arit.problem .stage.hot { background: #fdf1f2; border-color: var(--bordeaux); }
.arit .brain-ai { display: inline-flex; align-items: center; justify-content: center; width: 34px; height: 34px;
border-radius: 9999px; background: #fff; border: 1.5px solid var(--bordeaux); }
.arit .brain-ai svg { width: 24px; height: 24px; color: var(--bordeaux); }
.arit.problem .stage span.brain-ai { display: inline-flex; margin: 3px auto 0; }
.arit.problem .midrow { display: flex; gap: 0; margin-top: 8px; align-items: stretch; }
.arit.problem .concepts { flex: 1; min-width: 0; display: flex; flex-direction: column; }
.arit.problem .concepts .citem { flex: 1; }
.arit .concepts .lbl, .arit .rail .lbl { display: block; font: 700 10px/1 var(--mono); letter-spacing: .14em;
text-transform: uppercase; color: var(--bordeaux); margin: 0 0 5px; }
.arit .rail .lbl { padding-left: 16px; }
.arit .citem { display: flex; gap: 12px; align-items: baseline; padding: 5px 0; border-bottom: 1px solid var(--line); }
.arit .citem:last-child { border-bottom: none; }
.arit .citem .n { font: 700 11px/1 var(--mono); color: var(--bordeaux); }
.arit .citem b { font: 700 12.5px/1.3 var(--sans); color: var(--ink); text-transform: uppercase; letter-spacing: .03em; }
.arit .citem span.d { font: 400 11px/1.4 var(--sans); color: var(--ink-muted); margin-left: 8px; }
.arit .railwrap { position: relative; display: flex; justify-content: flex-end; }
.arit .conn-drop, .arit .t-conn { position: absolute; width: 2px; background: var(--bordeaux); }
.arit .conn-elbow { position: absolute; height: 2px; background: var(--bordeaux); }
.arit .t-head { position: absolute; width: 0; height: 0; border: 5px solid transparent;
border-bottom: 7px solid var(--bordeaux); transform: translateX(-50%); }
.arit .rail { width: 540px; border-left: 2px solid var(--bordeaux); padding-top: 12px; }
.arit .rail .row { display: flex; align-items: center; gap: 12px; padding: 5px 14px 5px 16px; position: relative; }
.arit .rail .row::before { content: ''; position: absolute; left: -2px; top: 50%; width: 14px; height: 2px; background: var(--bordeaux); }
.arit .rail .row .oicon { flex: 0 0 26px; width: 26px; height: 26px; color: var(--bordeaux); display: block; }
.arit .rail .row b { font: 700 13px/1.2 var(--sans); color: var(--ink); text-transform: uppercase; letter-spacing: .03em; }
.arit .rail .row span.d { font: 400 11px/1.3 var(--sans); color: var(--ink-muted); }
.arit.problem .thoth-wrap { position: relative; justify-content: flex-end; margin-top: 10px; }
.arit.problem .thoth-box { padding: 8px 14px; font-size: 11px; text-transform: uppercase; letter-spacing: .02em; }
.arit.problem .thoth-box .brain-ai { margin-right: 9px; vertical-align: middle; }
.arit .quote small { display: block; margin-top: 6px; font: 600 11px/1.4 var(--sans); font-style: normal;
letter-spacing: .06em; text-transform: uppercase; color: var(--ink-muted); }
/* mini pipeline (problem / thoth slides) */
.arit .pipeline { display: flex; align-items: stretch; gap: 10px; }
.arit .stage { flex: 1; padding: 12px 10px; text-align: center; border: 1px solid var(--line); border-radius: 8px; background: var(--surface-alt); }
.arit .stage b { display: block; font: 700 13.5px/1.2 var(--sans); margin-bottom: 4px; }
.arit .stage span { display: block; font: 400 11px/1.35 var(--sans); color: var(--ink-muted); }
.arit .pipeline { display: flex; align-items: stretch; gap: 12px; }
.arit .stage { flex: 1; padding: 15px 12px; text-align: center; border: 1px solid var(--line); border-radius: 8px; background: var(--surface-alt); }
.arit .stage b { display: block; font: 700 14px/1.2 var(--sans); margin-bottom: 4px; }
.arit .stage span { display: block; font: 400 11.5px/1.35 var(--sans); color: var(--ink-muted); }
.arit .stage .tag { display: inline-block; margin-top: 6px; padding: 3px 10px; border-radius: 9999px;
background: var(--bordeaux); color: #fdfcfc; font: 700 9.5px/1 var(--sans); letter-spacing: .1em; text-transform: uppercase; }
.arit .stage.ai { background: var(--bordeaux-tint); border-color: var(--bordeaux); }
.arit .arrow { align-self: center; color: var(--line-strong); font: 700 18px/1 var(--sans); }
.arit .thoth-wrap { display: flex; justify-content: flex-end; margin-top: 14px; }
.arit .thoth-col { width: 470px; text-align: center; }
.arit .thoth-arrow { color: var(--line-strong); font: 700 16px/1 var(--sans); margin: 2px 0; }
.arit .thoth-box { border: 1px solid var(--bordeaux); border-radius: 8px; padding: 10px 14px;
background: var(--surface-alt); font: 600 12.5px/1.4 var(--sans); }
.arit .arrow { align-self: center; color: var(--line-strong); font: 700 20px/1 var(--sans); }
.arit .thoth-wrap { display: flex; justify-content: flex-end; margin-top: 18px; }
.arit .thoth-col { width: 540px; text-align: center; }
.arit .thoth-arrow { color: var(--line-strong); font: 700 18px/1 var(--sans); margin: 2px 0; }
.arit .thoth-box { border: 1px solid var(--bordeaux); border-radius: 8px; padding: 12px 16px;
background: var(--surface-alt); font: 600 13px/1.4 var(--sans); }
.arit .thoth-box .tag { margin-right: 8px; }
/* points */
@@ -108,6 +167,8 @@
.arit .split { display: flex; gap: 40px; flex: 1; min-height: 0; }
.arit .split .left { flex: 0 0 430px; display: flex; flex-direction: column; }
.arit .split .right { flex: 1; min-width: 0; display: flex; }
.arit.nlp .split .right { align-items: center; }
.arit.nlp .code { flex: 0 0 auto; }
.arit .code { width: 100%; display: flex; flex-direction: column; overflow: hidden;
border: 1px solid var(--line-strong); border-radius: 8px; background: var(--surface-sunken); }
.arit .code-head { flex: 0 0 38px; display: flex; align-items: center; justify-content: space-between;
@@ -117,14 +178,36 @@
.arit .code b { font-weight: 700; color: var(--bordeaux-deep); }
.arit .code i { font-style: normal; color: #2f6f4f; }
/* stats */
.arit .stat-grid { display: grid; grid-template-columns: repeat(4, 1fr); gap: 18px; flex: 1; align-content: center; }
.arit .stat { border: 1px solid var(--line); border-radius: 8px; background: var(--surface-alt);
padding: 26px 22px; display: flex; flex-direction: column; gap: 10px; }
.arit .stat .num { font: 700 46px/1 var(--sans); color: var(--bordeaux); letter-spacing: -.02em; }
.arit .stat .lbl { font: 600 13.5px/1.35 var(--sans); color: var(--ink); }
.arit .stat .sub { font: 400 11.5px/1.4 var(--sans); color: var(--ink-muted); }
.arit .stat-note { margin-top: 20px; font: 400 12.5px/1.5 var(--sans); color: var(--ink-muted); }
/* stats — editorial numeral band */
.arit .stat-grid { display: grid; grid-template-columns: repeat(4, 1fr); }
.arit .stat { padding: 8px 24px; display: flex; flex-direction: column; gap: 10px; }
.arit .stat:first-child { padding-left: 0; }
.arit .stat + .stat { border-left: 1px solid var(--line); }
.arit .stat .num { font: 700 62px/1 var(--sans); color: var(--bordeaux); letter-spacing: -.02em; }
.arit .stat .lbl { font: 600 14.5px/1.35 var(--sans); color: var(--ink); }
.arit .stat .sub { font: 400 12px/1.45 var(--sans); color: var(--ink-muted); }
.arit .stat-note { margin-top: 26px; font: 400 13px/1.5 var(--sans); color: var(--ink-muted); }
/* slide "What the text miner reads": slim numbers strip + elliptical component cycle */
.arit.stats .sbody { padding-top: 22px; }
.arit.stats .strip { display: flex; justify-content: space-between; align-items: baseline;
border-top: 1px solid var(--line); border-bottom: 1px solid var(--line); padding: 9px 2px; }
.arit.stats .sstat { display: flex; align-items: baseline; gap: 9px; }
.arit.stats .sstat .n { font: 700 25px/1 var(--sans); color: var(--bordeaux); letter-spacing: -.02em; }
.arit.stats .sstat .l { font: 600 11.5px/1.2 var(--sans); color: var(--ink); }
.arit.stats .cycle { position: relative; width: 1184px; height: 270px; margin: 4px auto 0; }
.arit.stats .cycle-svg { position: absolute; inset: 0; width: 100%; height: 100%; }
.arit.stats .vcomp { position: absolute; transform: translate(-50%, -50%); width: 250px;
text-align: center; background: var(--surface); padding: 4px 6px; }
.arit.stats .vcomp .cicon { width: 46px; height: 46px; color: var(--bordeaux); display: block; margin: 0 auto 7px; }
.arit.stats .vcomp b { display: block; font: 700 14.5px/1.25 var(--sans); text-transform: uppercase;
letter-spacing: .04em; color: var(--ink); }
.arit.stats .vcomp span.d { display: block; margin-top: 4px; font: 400 11.5px/1.4 var(--sans); color: var(--ink-muted); }
.arit.stats .note-row { display: flex; gap: 30px; margin-top: 6px; }
.arit.stats .nitem { flex: 1; position: relative; padding-left: 16px; font: 400 14px/1.45 var(--sans); color: var(--ink); }
.arit.stats .nitem::before { content: ''; position: absolute; left: 0; top: 7px; width: 8px; height: 8px;
border-radius: 9999px; background: var(--bordeaux); }
.arit.stats .nitem b { color: var(--bordeaux-deep); }
/* divider */
.arit.divider .sbody { justify-content: center; }
@@ -132,14 +215,39 @@
.arit .divider-h { font: 700 40px/1.15 var(--sans); margin-top: 10px; }
.arit .cue { margin-top: 22px; font: 600 12px/1 var(--sans); letter-spacing: .12em; text-transform: uppercase; color: var(--ink-muted); }
/* ontology chips */
.arit .tier { margin-bottom: 18px; }
.arit .tier-label { display: block; font: 700 11px/1 var(--mono); letter-spacing: .12em; text-transform: uppercase;
color: var(--ink-muted); margin-bottom: 8px; }
.arit .chips { display: flex; flex-wrap: wrap; gap: 8px; }
.arit .chipc { border: 1px solid var(--bordeaux); color: var(--bordeaux-deep); background: #fff;
border-radius: 9999px; padding: 7px 14px; font: 700 13px/1 var(--sans); }
.arit .chipc.t2 { border-color: var(--line-strong); color: var(--ink-muted); }
/* ontology slide: EN-first category pills + output panel */
.arit .onto-split { flex: 0 0 auto; display: grid; grid-template-columns: 1.25fr 1fr;
gap: 26px; align-items: stretch; margin-top: 14px; }
.arit .tier-h { display: flex; align-items: center; font: 700 10px/1 var(--mono); letter-spacing: .14em;
text-transform: uppercase; color: var(--bordeaux-deep); margin: 22px 0 9px; }
.arit .onto-left .tier-h:first-child { margin-top: 0; }
.arit .tier-h .cnt { margin-left: auto; font: 700 9px/1 var(--mono); color: var(--bordeaux);
border: 1px solid var(--bordeaux); border-radius: 9999px; padding: 2px 7px; }
.arit .cats { display: flex; flex-wrap: wrap; gap: 8px; }
.arit .cat { display: inline-flex; align-items: baseline; gap: 7px; border: 1px solid var(--bordeaux);
border-radius: 9999px; padding: 6px 13px; background: #fff; white-space: nowrap; }
.arit .cat b { font: 700 13px/1 var(--sans); color: var(--ink); }
.arit .cat .it { font: 400 11px/1 var(--sans); color: var(--ink-muted); }
.arit .cat.fill { background: var(--bordeaux-tint); }
.arit .onto-points { margin-top: 18px; }
.arit .onto-left .onto-points:first-child { margin-top: 0; }
.arit .onto-left { display: flex; flex-direction: column; }
.arit .onto-left .roi { margin-top: auto; }
.arit .onto-left .onto-points { margin-bottom: 14px; }
.arit .oitem { display: flex; gap: 10px; padding: 8px 0; border-bottom: 1px solid var(--line); }
.arit .oitem:last-child { border-bottom: none; }
.arit .oitem .n { flex: 0 0 22px; font: 700 11px/1.6 var(--mono); color: var(--bordeaux); }
.arit .oitem b { font: 700 13px/1.35 var(--sans); color: var(--ink); }
.arit .oitem .d { font: 400 12.5px/1.45 var(--sans); color: var(--ink-muted); }
.arit .out-panel { border: 1px solid var(--bordeaux); background: var(--bordeaux-tint); border-radius: 10px;
padding: 15px 18px 8px; align-self: stretch; display: flex; flex-direction: column; }
.arit .out-h { display: block; font: 700 10px/1 var(--mono); letter-spacing: .14em; text-transform: uppercase;
color: var(--bordeaux-deep); margin-bottom: 10px; }
.arit .out-item { display: flex; gap: 10px; padding: 13px 0; border-bottom: 1px solid rgba(143,18,4,.18); flex: 1; }
.arit .out-item:last-child { border-bottom: none; }
.arit .out-item .n { flex: 0 0 22px; font: 700 11px/1.6 var(--mono); color: var(--bordeaux-deep); }
.arit .out-item b { font: 700 13px/1.35 var(--sans); color: var(--bordeaux-deep); }
.arit .out-item .d { display: block; margin-top: 2px; font: 400 12.5px/1.45 var(--sans); color: var(--ink); }
/* flow */
.arit .flow { margin-top: 16px; display: flex; align-items: stretch; gap: 14px; }
@@ -179,12 +287,12 @@ button.brain-btn { cursor: pointer; padding: 0; transition: transform .15s ease,
button.brain-btn:hover, button.brain-btn:focus-visible { transform: scale(1.18); box-shadow: 0 0 0 5px rgba(203,51,59,.14); outline: none; }
/* popup */
.ai-pop { position: fixed; inset: 0; z-index: 100; background: rgba(42,36,38,.45); backdrop-filter: blur(2px);
.ai-pop { position: fixed; inset: 0; z-index: 100; background: rgba(42,36,38,.12);
display: block; opacity: 0; pointer-events: none; transition: opacity .2s ease; }
.ai-pop.open { opacity: 1; pointer-events: auto; }
.ai-line { position: absolute; left: 0; top: 0; width: 100%; height: 100%; pointer-events: none; }
.ai-line line { stroke: var(--bordeaux); stroke-width: 2.5; }
.ai-line circle { fill: var(--bordeaux); }
.ai-line line { stroke: var(--bordeaux-deep); stroke-width: 5; stroke-linecap: round; filter: drop-shadow(0 0 2px #fff); }
.ai-line circle { fill: var(--bordeaux-deep); stroke: #fff; stroke-width: 1.5; }
.ai-pop-card { position: absolute; width: min(580px, 92vw); background: #fff; border: 1px solid var(--line);
border-radius: 12px; box-shadow: 0 18px 50px rgba(42,36,38,.28); padding: 24px 28px 22px;
transform: translateY(14px) scale(.95); transition: transform .24s cubic-bezier(.2,.9,.3,1.15); text-align: left; }
@@ -203,6 +311,7 @@ button.brain-btn:hover, button.brain-btn:focus-visible { transform: scale(1.18);
.ai-pop-list .n { color: var(--bordeaux-deep); font-weight: 800; }
.roi { margin-top: 14px; border: 1.5px solid var(--bordeaux); background: #faf4f5; border-radius: 8px;
padding: 10px 14px; font: 400 13.5px/1.55 var(--sans); color: var(--ink); }
.roi-quiet { border-color: var(--line-strong); border-width: 1px; background: var(--surface-alt); }
.roi .k { display: block; margin-bottom: 4px; font: 800 10.5px/1 var(--mono); letter-spacing: .14em;
text-transform: uppercase; color: var(--bordeaux-deep); }
.ai-pop-x { position: absolute; top: 12px; right: 14px; width: 30px; height: 30px; border: none;
@@ -213,20 +322,24 @@ button.brain-btn:hover, button.brain-btn:focus-visible { transform: scale(1.18);
.arit .chip.tbd { color: var(--bordeaux-deep); border: 1px solid var(--bordeaux); background: #fff;
border-radius: 9999px; padding: 6px 12px; font: 700 10.5px/1 var(--sans); letter-spacing: .06em; }
/* where we started */
.arit .sysmap { display: grid; grid-template-columns: repeat(3, 1fr); gap: 16px; flex: 1; align-content: center; }
.arit .syscard { position: relative; display: block; width: 100%; border: 1.5px solid var(--line-strong);
border-radius: 8px; background: var(--surface-alt); padding: 20px 16px 14px; text-align: left;
cursor: pointer; font-family: var(--sans); transition: transform .15s ease, box-shadow .15s ease; }
.arit button.syscard:hover, .arit button.syscard:focus-visible { transform: translateY(-3px);
box-shadow: 0 6px 18px rgba(42,36,38,.14); outline: none; }
.arit .syscard b { display: block; font: 700 17px/1.3 var(--sans); color: var(--ink); margin-bottom: 6px; }
.arit .syscard .d { display: block; font: 400 12.5px/1.45 var(--sans); color: var(--ink-muted); }
.arit .syscard .tag20 { position: absolute; top: -10px; right: 12px; background: var(--bordeaux); color: #fff;
border-radius: 9999px; padding: 3px 10px; font: 700 9.5px/1 var(--sans); letter-spacing: .08em; }
.arit .syscard.ghost { border: 1.5px dashed var(--bordeaux); background: #fff; cursor: default; }
.arit .syscard.ghost b { color: var(--bordeaux-deep); }
.arit .syscard.ghost .tag20 { background: var(--bordeaux-deep); }
/* where we started — scattered orbit map */
.arit .scatter { position: relative; flex: 1; min-height: 0; }
.arit .todo-stack { position: absolute; z-index: 2; }
.arit .todo { display: flex; align-items: center; gap: 12px; border: 2px dashed var(--bordeaux);
border-radius: 8px; background: #fff; padding: 8px 12px; margin-bottom: 8px;
box-shadow: 0 2px 8px rgba(42,36,38,.06); cursor: pointer; }
.arit .todo .q { font: 800 22px/1 var(--sans); color: var(--bordeaux-deep); }
.arit .todo .tx { display: flex; flex-direction: column; }
.arit .todo b { font: 700 13.5px/1.3 var(--sans); color: var(--bordeaux-deep); }
.arit .todo .tx span { font: 400 12px/1.4 var(--sans); color: var(--ink-muted); margin-top: 2px; }
.arit .missing-title { position: absolute; font: 700 30px/1.2 var(--sans); color: var(--bordeaux-deep); }
.arit .isle { position: absolute; display: flex; flex-direction: column; align-items: center; gap: 2px;
cursor: pointer; transform: rotate(var(--rot, 0deg)); transition: transform .18s ease;
border: none; background: none; font-family: var(--sans); width: 230px; }
.arit .isle:hover, .arit .isle:focus-visible { transform: rotate(var(--rot, 0deg)) scale(1.05); outline: none; }
.arit .isle .ico { width: var(--size, 120px); height: var(--size, 120px); color: var(--bordeaux); display: block; }
.arit .isle .nm { font: 700 16px/1.3 var(--sans); color: var(--ink); margin-top: 4px; }
.arit .isle .sc { font: 400 13.5px/1.4 var(--sans); color: var(--ink-muted); max-width: 240px; }
.arit .hint { margin-top: 12px; font: italic 400 12px/1.4 var(--sans); color: var(--ink-muted); }
/* snapshot tours + chart placeholders */
@@ -262,5 +375,12 @@ button.brain-btn:hover, button.brain-btn:focus-visible { transform: scale(1.18);
.arit.flow-slide .kicker { margin-bottom: 10px; }
.arit.flow-slide h2 { margin-bottom: 6px; }
.arit.flow-slide .lead { margin-top: 6px; font-size: 15px; }
.arit.flow-slide .flow { margin-top: 14px; }
.arit.flow-slide .flow { margin-top: 10px; }
.arit.flow-slide .zone { padding: 26px 14px 12px; }
.arit.flow-slide .fbox { padding: 8px 10px; }
.arit.flow-slide .fbox.end { padding: 12px 10px; }
.arit.flow-slide .fcol { gap: 6px; }
.arit.flow-slide .pillars { padding-top: 8px; }
/* popup: lead + list (missing-pieces popups) */
.ai-pop-card p.lead { font-style: italic; color: var(--ink-muted); }
File diff suppressed because it is too large Load Diff
+76
View File
@@ -0,0 +1,76 @@
/* Opt-in, same-browser synchronization. The regular deck has no channel or session. */
(() => {
const params = new URLSearchParams(location.search);
const session = params.get('session');
const role = params.get('view');
if (!session || !['preview', 'audience'].includes(role)) return;
const channel = new BroadcastChannel(`aritmolab-presenter:${session}`);
const sender = crypto.randomUUID();
const storageKey = `aritmolab-state:${session}`;
let applying = false;
let pending = false;
const send = (type, extra = {}) => channel.postMessage({ type, role, sender, ...extra });
const api = window.PresentationControls;
const valid = state => state && Number.isInteger(state.index) && state.index >= 0 && state.index < Reveal.getTotalSlides();
function save(state) { try { localStorage.setItem(storageKey, JSON.stringify(state)); } catch {} }
function publish() {
if (applying || pending) return;
pending = true;
queueMicrotask(() => {
pending = false;
const state = api.snapshot();
save(state);
send('state', { state });
});
}
function apply(state) {
if (!valid(state)) return;
applying = true;
Reveal.slide(state.index);
api.closePopup();
if (state.popup) api.openPopup(state.popup);
applying = false;
save(api.snapshot());
}
function command(message) {
if (message.action === 'next') Reveal.next();
if (message.action === 'previous') Reveal.prev();
if (message.action === 'slide' && valid({ index: message.index })) Reveal.slide(message.index);
if (message.action === 'popup' && message.popup) api.openPopup(message.popup);
if (message.action === 'close') api.closePopup();
publish();
}
function ready() {
Reveal.configure({ overview: false, pause: false });
if (role === 'preview') {
// Remove only the presenter's footer and its 34px, preserving the slide body geometry.
document.documentElement.classList.add('presenter-preview');
Reveal.configure({ height: 686 });
}
try { apply(JSON.parse(localStorage.getItem(storageKey))); } catch {}
addEventListener('presentationchange', publish);
Reveal.on('slidechanged', publish);
channel.onmessage = ({ data }) => {
if (!data || data.sender === sender) return;
if (data.type === 'state') apply(data.state);
if (data.type === 'command' && role === 'preview') command(data);
if (data.type === 'request' && role === 'preview') publish();
};
if (role === 'preview') publish();
else send('request');
send('presence');
setInterval(() => send('presence'), 1000);
addEventListener('pagehide', () => send('departed'));
if (role === 'audience') {
document.title = 'AritmoLab | Audience';
const button = document.createElement('button');
button.textContent = 'Full screen';
button.setAttribute('aria-label', 'Enter audience full screen');
Object.assign(button.style, { position: 'fixed', right: '16px', top: '16px', zIndex: '200', padding: '10px 18px', borderRadius: '6px', border: '1px solid #b9afb1', background: '#faf7f7', color: '#482d34', cursor: 'pointer', font: '15px system-ui' });
button.onclick = () => document.documentElement.requestFullscreen().catch(() => { button.textContent = 'Use the browser full-screen command'; });
document.addEventListener('fullscreenchange', () => { button.hidden = Boolean(document.fullscreenElement); });
document.body.append(button);
}
}
if (Reveal.isReady()) ready(); else Reveal.on('ready', ready);
})();
+49
View File
@@ -0,0 +1,49 @@
/* Ordered tour stops; screenshot originals keep their full aspect ratio. */
.arit .screenshot-tour {
display: grid; grid-template-columns: repeat(4, 1fr); grid-template-rows: repeat(2, minmax(0, 1fr));
gap: 16px; flex: 1; min-height: 0; margin-top: 10px;
}
.arit .screenshot-tour-four { grid-template-columns: repeat(2, minmax(0, 1fr)); }
.arit .screenshot-tour button {
display: flex; flex-direction: column; min-width: 0; min-height: 0; padding: 0;
overflow: hidden; border: 1px solid var(--line-strong); border-radius: 8px;
background: var(--surface-alt); color: var(--ink); cursor: pointer; text-align: left;
}
.arit .screenshot-tour button:hover { border-color: var(--bordeaux); background: var(--bordeaux-tint); }
.arit .screenshot-tour button:focus-visible { outline: 3px solid var(--bordeaux); outline-offset: 3px; }
.arit .screenshot-tour img {
display: block; width: 100%; height: 0; flex: 1; min-height: 0; max-width: none; max-height: none;
margin: 0; object-fit: contain; border: 0; box-shadow: none; background: #fff;
}
.arit .screenshot-tour span {
display: flex; align-items: center; gap: 10px; padding: 9px 12px;
border-top: 1px solid var(--line); font: 600 16px/1.2 var(--sans);
}
.arit .screenshot-tour b { color: var(--bordeaux); font: 700 22px/1 var(--sans); }
.arit .screenshot-tour-compact {
grid-template-columns: repeat(3, minmax(0, 1fr));
grid-template-rows: repeat(2, minmax(0, 1fr)); gap: 10px;
}
.arit .screenshot-tour-compact span { gap: 8px; padding: 6px 10px; font-size: 14px; }
.arit .screenshot-tour-compact b { font-size: 18px; }
.screenshot-dialog {
box-sizing: border-box; width: calc(100vw - 24px); height: calc(100vh - 24px);
max-width: none; max-height: none; padding: 8px; margin: auto;
border: 1px solid var(--line-strong); border-radius: 8px; background: var(--surface);
color: var(--ink); font-family: var(--sans); overflow: hidden;
}
.screenshot-dialog[open] { display: flex; flex-direction: column; gap: 6px; }
.screenshot-dialog::backdrop { background: rgb(42 36 38 / 70%); }
.screenshot-dialog header { display: flex; align-items: center; gap: 12px; min-height: 32px; flex: 0 0 auto; }
.screenshot-dialog h2 { margin: 0 auto 0 4px; font: 600 16px/1.2 var(--sans); }
.screenshot-dialog button {
border: 1px solid var(--line-strong); border-radius: 4px; background: var(--surface);
color: var(--ink); padding: 5px 12px; font: 600 14px/1.2 var(--sans); cursor: pointer;
}
.screenshot-dialog button:hover { background: var(--bordeaux-tint); }
.screenshot-dialog button:focus-visible { outline: 3px solid var(--bordeaux); outline-offset: 1px; }
.screenshot-stage { position: relative; flex: 1; min-height: 0; }
.screenshot-stage img { position: absolute; inset: 0; width: 100%; height: 100%; }
.screenshot-stage img { object-fit: contain; }
.screenshot-dialog .screenshot-error { margin: auto; text-align: center; }
@media print { .screenshot-dialog { display: none !important; } }
+44
View File
@@ -0,0 +1,44 @@
/* Uses the deck's shared popup state, so preview and audience stay synchronized. */
(() => {
const dialog = document.createElement('dialog');
dialog.className = 'screenshot-dialog';
dialog.setAttribute('aria-labelledby', 'screenshot-title');
dialog.innerHTML = `<header><h2 id="screenshot-title"></h2><button type="button" aria-label="Close screenshot">Close · Esc</button></header>
<div class="screenshot-stage">
<img alt="">
</div>
<p class="screenshot-error" hidden>Screenshot unavailable. Close and reopen to retry.</p>`;
document.body.append(dialog);
const title = dialog.querySelector('h2');
const image = dialog.querySelector('img');
const error = dialog.querySelector('.screenshot-error');
let active = null;
let trigger = null;
const close = () => {
active = null;
if (dialog.open) dialog.close();
if (trigger?.closest('section.present')) trigger.focus({ preventScroll: true });
trigger = null;
};
const requestClose = () => window.PresentationControls.closePopup();
dialog.querySelector('button').addEventListener('click', requestClose);
dialog.addEventListener('cancel', event => { event.preventDefault(); requestClose(); });
dialog.addEventListener('click', event => { if (event.target === dialog) requestClose(); });
image.addEventListener('error', () => { image.hidden = true; error.hidden = false; });
window.ScreenshotTour = {
get active() { return active; },
open(button) {
trigger = button;
active = { kind: 'screenshot', key: button.dataset.screenshot };
const total = button.closest('.screenshot-tour').querySelectorAll('[data-screenshot]').length;
title.textContent = `${button.dataset.screenshotNumber} / ${total} · ${button.dataset.screenshotTitle}`;
image.hidden = false;
error.hidden = true;
image.alt = button.dataset.screenshotTitle;
image.src = button.querySelector('img').src;
if (!dialog.open) dialog.showModal();
window.dispatchEvent(new Event('presentationchange'));
},
close,
};
})();
Binary file not shown.

After

Width:  |  Height:  |  Size: 630 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 680 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 551 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 721 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 723 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 834 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 596 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 470 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.0 MiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 976 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.1 MiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 959 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 948 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 704 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 802 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 820 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 925 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 707 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 472 KiB

+127
View File
@@ -0,0 +1,127 @@
/* A rectangular optical enlargement, anchored to the original diagram element. */
.arit .flow .lens-target {
display: block; width: 100%; color: inherit; font: inherit; cursor: pointer;
appearance: none;
transition: transform 180ms cubic-bezier(.16, 1, .3, 1), box-shadow 180ms ease,
border-color 180ms ease, opacity 220ms ease;
}
.arit .flow .lens-target:hover {
transform: translateY(-2px); border-color: var(--bordeaux);
box-shadow: 0 5px 12px oklch(26% .02 15 / .13);
}
.arit .flow .lens-target:focus-visible, .arit .slide-lens-close:focus-visible {
outline: 3px solid var(--bordeaux); outline-offset: 4px;
}
.arit .flow .zone.built, .arit .flow > .ai-hit, .arit .flow .fbox:not(.lens-target) {
transition: opacity 220ms ease;
}
.arit.lens-active .flow .zone.built:not(:has(.lens-source)), .arit.lens-active .flow > .ai-hit,
.arit.lens-active .flow .zone.built:has(.lens-source) :is(.fbox:not(.lens-source), .farrow, .brain-btn, .fcap),
.arit.lens-active .flow .zone.existed:not(.lens-source) { opacity: .18; }
.arit .flow .lens-source {
border-color: var(--bordeaux); box-shadow: 0 0 0 3px var(--bordeaux-tint);
}
.arit .oitem.lens-target {
width: 100%; color: inherit; font: inherit; text-align: left;
background: transparent; border: 0; border-bottom: 1px solid var(--line);
appearance: none; cursor: pointer;
transition: background 180ms ease, opacity 220ms ease;
}
.arit .oitem.lens-target:last-child { border-bottom: 0; }
.arit .oitem.lens-target:hover, .arit .oitem.lens-source { background: var(--bordeaux-tint); }
.arit .oitem.lens-target:focus-visible { outline: 3px solid var(--bordeaux); outline-offset: 3px; }
.arit.crisis .crisis-points { margin-bottom: 4px; }
.arit.crisis .oitem { padding: 12px 10px; gap: 14px; }
.arit.crisis .oitem .n { flex-basis: 28px; font-size: 15px; }
.arit.crisis .oitem b { display: block; font-size: 18px; }
.arit.crisis .oitem .d { display: block; margin-top: 3px; font-size: 16px; }
.arit.crisis .sbody > .roi { padding: 12px 18px; font-size: 22px; line-height: 1.3; font-weight: 600; }
.arit.crisis .sbody > .roi .k { margin-bottom: 8px; font-size: 14px; line-height: 1.2; }
.arit.crisis.lens-active .crisis-points .oitem:not(.lens-source),
.arit.crisis.lens-active .sbody > .roi { opacity: .18; }
.arit.crisis.lens-active .sbody > :is(h2, .kicker) { opacity: 0; }
.arit.validation .onto-split :is(.oitem, .roi, .out-panel) { transition: opacity 220ms ease; }
.arit.validation.lens-active .onto-split :is(.oitem:not(.lens-source), .roi, .out-panel:not(.lens-source)) { opacity: .18; }
.arit.validation .out-panel.lens-target {
appearance: none; width: 100%; color: inherit; font: inherit; text-align: left;
cursor: pointer;
transition: transform 180ms cubic-bezier(.16, 1, .3, 1), box-shadow 180ms ease,
background 180ms ease, opacity 220ms ease;
}
.arit.validation .out-panel.lens-target:hover {
transform: translateY(-2px); background: var(--surface-alt);
box-shadow: 0 0 0 3px var(--bordeaux-tint), 0 6px 16px oklch(26% .02 15 / .14);
}
.arit.validation .out-panel.lens-target:focus-visible {
outline: 3px solid var(--bordeaux); outline-offset: 4px;
}
.arit.validation .out-panel.lens-source { box-shadow: 0 0 0 3px var(--bordeaux-tint); }
.arit.validation .out-panel.lens-target .out-item:last-of-type { border-bottom: none; }
.arit .slide-lens-guides {
position: absolute; inset: 0; width: 100%; height: 100%;
pointer-events: none; z-index: 8; overflow: visible;
}
.arit .slide-lens-guides path {
fill: none; stroke: var(--bordeaux); stroke-width: 1; opacity: .45;
}
.arit .slide-lens {
position: absolute; z-index: 9; transform-origin: 0 0;
border: 3px solid var(--bordeaux); border-radius: 9px;
background: var(--surface-alt); padding: 9px;
box-shadow: 0 22px 48px oklch(26% .02 15 / .2),
0 4px 10px oklch(26% .02 15 / .12),
inset 0 0 0 2px var(--surface), inset 0 0 0 3px var(--line-strong);
}
/* A beveled rim and a restrained reflection suggest a lens without an icon. */
.arit .slide-lens::after {
content: ''; position: absolute; inset: 5px; pointer-events: none;
border-radius: 4px; border: 1px solid oklch(99% .004 20 / .85);
background: linear-gradient(125deg, oklch(99% .004 20 / .45), transparent 26%,
transparent 78%, oklch(99% .004 20 / .25));
}
.arit .slide-lens-content { overflow: hidden; width: 100%; height: 100%; border-radius: 2px; }
.arit .slide-lens .slide-lens-copy {
margin: 0; transform-origin: 0 0; border-radius: 1px;
border-color: transparent; position: static;
}
.arit .slide-lens .slide-lens-copy.zone { position: relative; }
.arit .slide-lens.has-details .slide-lens-content { overflow-y: auto; }
.arit .slide-lens-detail { padding: 16px 18px; text-align: left; color: var(--ink); }
.arit .slide-lens-detail h3 {
margin: 0 0 12px; font: 700 30px/1.15 var(--sans); color: var(--bordeaux-deep);
}
.arit .slide-lens-detail p { margin: 0 0 10px; font: 400 17px/1.35 var(--sans); }
.arit .slide-lens-detail p:last-child { margin-bottom: 0; }
.arit .slide-lens-detail p b {
display: block; margin-bottom: 4px; font-weight: 700; color: var(--bordeaux-deep);
}
.arit .slide-lens-detail .detail-intro { font-size: 19px; line-height: 1.3; }
.arit .slide-lens-detail .detail-example { padding-top: 12px; border-top: 1px solid var(--line-strong); }
.arit .slide-lens-caption {
position: absolute; left: 0; top: -28px; color: var(--bordeaux-deep);
font: 700 11px/1 var(--mono); letter-spacing: .08em; text-transform: uppercase;
}
.arit .slide-lens-close {
position: absolute; right: -15px; top: -15px; z-index: 1;
display: grid; place-items: center; width: 30px; height: 30px;
border: 1px solid var(--bordeaux); border-radius: 5px;
padding: 0; background: var(--surface); color: var(--bordeaux-deep);
font: 400 22px/1 var(--sans); cursor: pointer;
}
@media (prefers-reduced-motion: reduce) {
.arit.validation .out-panel.lens-target { transition: none; }
.arit.validation .out-panel.lens-target:hover { transform: none; }
.arit.validation .onto-split :is(.oitem, .roi, .out-panel) { transition: none; }
.arit .flow .zone, .arit .flow > .ai-hit, .arit .flow .fbox { transition: none; }
.arit .flow .lens-target:hover { transform: none; }
}
@media print {
.arit.crisis.lens-active .sbody > :is(h2, .kicker) { opacity: 1; }
.arit.crisis.lens-active .crisis-points .oitem:not(.lens-source),
.arit.crisis.lens-active .sbody > .roi { opacity: 1; }
.arit.validation.lens-active .onto-split :is(.oitem, .roi, .out-panel) { opacity: 1; }
.arit .slide-lens, .arit .slide-lens-guides { display: none !important; }
.arit.lens-active .flow .zone, .arit.lens-active .flow > .ai-hit,
.arit.lens-active .flow .fbox,
.arit.lens-active .flow .zone.built:has(.lens-source) :is(.fbox, .farrow, .brain-btn, .fcap) { opacity: 1; }
}
+121
View File
@@ -0,0 +1,121 @@
/* Reuse with a source button carrying its id and data-lens="source-id".
Content is cloned from the source, with optional explanatory content in a template. */
(() => {
let current = null;
const changed = () => window.dispatchEvent(new Event('presentationchange'));
const reducedMotion = () => matchMedia('(prefers-reduced-motion: reduce)').matches;
function layout() {
if (!current) return;
const { source, slide, frame, copy, guides, caption, detailed } = current;
const sr = slide.getBoundingClientRect();
const rect = source.getBoundingClientRect();
const scale = sr.width / slide.offsetWidth;
const area = (slide.querySelector('.flow') || slide.querySelector('.sbody')).getBoundingClientRect();
const centered = source.dataset.lensPlacement === 'center';
const x = (rect.left - sr.left) / scale, y = (rect.top - sr.top) / scale;
const w = rect.width / scale, h = rect.height / scale;
const isGroup = source.classList.contains('zone');
const areaTop = isGroup || detailed ? 160 : (area.top - sr.top) / scale;
const areaHeight = isGroup || detailed ? slide.offsetHeight - areaTop - 50 : area.height / scale;
// Choose the roomier side so the original stays visible, including middle/right nodes.
const margin = 65, gap = 38;
const roomLeft = x - gap - margin;
const roomRight = slide.offsetWidth - margin - (x + w + gap);
const onRight = roomRight >= roomLeft;
const maxWidth = centered ? Math.min(760, slide.offsetWidth - 2 * margin)
: Math.min(slide.offsetWidth * .58, Math.max(roomLeft, roomRight));
const zoom = Math.min(4, (maxWidth - 24) / w, (areaHeight - 40) / h);
let width = w * zoom + 24, height = h * zoom + 24;
if (detailed) {
width = maxWidth;
Object.assign(copy.style, { width: `${width - 24}px`, height: 'auto', transform: 'none' });
height = Math.min(copy.scrollHeight + 24, areaHeight - 16);
}
const left = centered ? (slide.offsetWidth - width) / 2 : onRight
? Math.min(slide.offsetWidth - width - margin, Math.max(slide.offsetWidth * .36, x + w + gap))
: Math.max(margin, Math.min(slide.offsetWidth * .36, x - gap - width));
const top = areaTop + (areaHeight - height) / 2 + 10;
Object.assign(frame.style, { left: `${left}px`, top: `${top}px`, width: `${width}px`, height: `${height}px` });
if (!detailed) Object.assign(copy.style, { width: `${w}px`, height: `${h}px`, transform: `scale(${zoom})` });
const zoneTitle = source.dataset.lensCaption || source.closest('.zone')?.querySelector('.zlabel')?.textContent || 'Detail';
caption.textContent = detailed ? zoneTitle : `${zoneTitle} · ${zoom.toFixed(1)}×`;
guides.setAttribute('viewBox', `0 0 ${slide.offsetWidth} ${slide.offsetHeight}`);
const sourceEdge = onRight ? x + w : x;
const lensEdge = onRight ? left : left + width;
guides.querySelector('path').setAttribute('d', `M ${sourceEdge} ${y} L ${lensEdge} ${top} M ${sourceEdge} ${y + h} L ${lensEdge} ${top + height}`);
current.origin = `translate(${x - left}px, ${y - top}px) scale(${w / width}, ${h / height})`;
}
function close() {
if (!current) return;
const { slide, source, trigger, frame, guides, animation } = current;
animation?.cancel();
const restoreFocus = frame.contains(document.activeElement);
frame.remove(); guides.remove();
slide.classList.remove('lens-active'); source.classList.remove('lens-source');
trigger.setAttribute('aria-expanded', 'false');
current = null;
if (restoreFocus && slide.classList.contains('present')) trigger.focus({ preventScroll: true });
changed();
}
function open(trigger) {
const source = document.getElementById(trigger.dataset.lens);
const slide = source?.closest('section');
if (!source || !slide?.classList.contains('present')) return;
close();
const details = source.querySelector('template.lens-details');
const title = source.dataset.lensTitle || source.querySelector('b').textContent;
const copy = document.createElement('div');
if (details) {
copy.className = 'slide-lens-detail';
const heading = document.createElement('h3');
heading.textContent = title;
copy.append(heading, details.content.cloneNode(true));
} else {
copy.className = source.className;
copy.classList.remove('lens-target', 'lens-source');
copy.innerHTML = source.innerHTML;
}
copy.querySelectorAll('[id]').forEach(el => el.removeAttribute('id'));
copy.querySelectorAll('button').forEach(el => el.remove());
copy.classList.add('slide-lens-copy');
const frame = document.createElement('div');
frame.id = 'slide-lens'; frame.className = 'slide-lens';
frame.classList.toggle('has-details', Boolean(details));
frame.setAttribute('role', 'region');
frame.setAttribute('aria-label', details ? `${title} explained` : `${title} enlarged`);
const caption = document.createElement('div'); caption.className = 'slide-lens-caption';
const content = document.createElement('div'); content.className = 'slide-lens-content';
content.append(copy);
const closeButton = document.createElement('button');
closeButton.className = 'slide-lens-close'; closeButton.textContent = '×';
closeButton.setAttribute('aria-label', 'Close detail');
frame.append(caption, content, closeButton);
const guides = document.createElementNS('http://www.w3.org/2000/svg', 'svg');
guides.classList.add('slide-lens-guides'); guides.setAttribute('aria-hidden', 'true');
guides.append(document.createElementNS(guides.namespaceURI, 'path'));
slide.append(guides, frame);
current = { source, slide, trigger, frame, copy, guides, caption, detailed: Boolean(details) };
layout();
source.classList.add('lens-source'); slide.classList.add('lens-active');
trigger.setAttribute('aria-expanded', 'true');
if (!reducedMotion()) {
current.animation = frame.animate([
{ transform: current.origin, opacity: .5 },
{ transform: 'none', opacity: 1 },
], { duration: 480, easing: 'cubic-bezier(.16, 1, .3, 1)' });
guides.animate([{ opacity: 0 }, { opacity: 1 }], { duration: 480 });
}
changed();
}
window.SlideLens = {
open, close, layout,
get active() { return current ? { kind: 'lens', key: current.source.id } : null; },
};
addEventListener('resize', layout);
document.addEventListener('transitionend', event => {
if (event.target === current?.source && event.propertyName === 'transform') layout();
});
})();
File diff suppressed because one or more lines are too long
File diff suppressed because one or more lines are too long
+70
View File
@@ -0,0 +1,70 @@
# Presenter console
For the published AritmoLab site and routine Git updates, see
[Presentation publishing](../../docs/operations/presentation-publishing.md).
Serve `presentation/` using the same local server as the deck:
```sh
python3 -m http.server 8000 --bind 127.0.0.1 --directory presentation
```
Open <http://localhost:8000/presenter/> in Chrome or another modern desktop browser.
Select **Open audience window**, move the new window to the HDMI display, then click
**Full screen** there. Keep the console on the Mac display. Use extended displays,
not mirroring. Both windows must use the same browser profile and origin.
The console provides interactive slide preview, notes with adjustable type size,
slide navigation, per-slide popup buttons, a manually started timer, and a choice
between notes beside or below the preview. Notes fill the available horizontal space.
Up next includes a live, non-interactive thumbnail of the following slide in both
layouts; at the last slide it is replaced by the end-of-presentation message.
With notes beside the preview, the current preview keeps the height calculated as
65% of the space available without popup controls. It resizes with the window,
not with the number of popups. The next preview takes the space left after the
current preview, navigation, popup buttons and labels, and never renders larger
than the current one.
Popup controls contain only numbered
buttons and disappear entirely on slides without popups. The current preview omits
the 34px slide footer; the audience slide remains unchanged.
Arrow keys change slides; Escape closes
the popup. Changing slide closes any open popup. Notes scroll independently and
return to the beginning when the slide changes. The reading area contains only the
spoken narrative, without slide titles or popup headings. Parenthesized numbers
on slide 2 and slides 7–10 identify the popup button for each passage.
Screenshot-specific notes still follow the selected popup; use the controls below
the preview to open popups.
Popup controls and clickable images open only on click
or explicit keyboard activation (Enter, Space or the numbered shortcuts).
Hover and keyboard focus may highlight a control but never open or switch a popup.
On slide 07,
the five numbered controls open the corresponding Validation detail, centered on
the slide with the shared lens effect. The fifth
detail explains all three advantages; on the slide, the advantages panel highlights
on hover and opens on click. A detail
stays open when the pointer leaves, until another detail is selected, Escape or
Close is used, or the slide changes. The preview and audience show the same detail.
Slide 08 has four numbered screenshot controls, in the supplied tour order.
Click a thumbnail or a console button to open
the original screenshot almost full screen. Keys 1–4 also select screenshots;
Escape or Close returns to the slide. Preview and audience share the selection.
The four English descriptions form one continuous script.
The preview and audience communicate over a session-specific `BroadcastChannel`.
The session is in the console URL; the latest slide and popup are retained in local
storage for refresh recovery. A fresh `/presenter/` URL starts a separate session.
The normal `/deck/index.html` page does not join any session. This console is for
windows in one browser on one computer, not remote tablet synchronization.
If the audience window is blocked, allow popups for localhost and click the button
again. Closing that window leaves the console usable for rehearsal; reopen it to
resume at the current slide. The audience also remains navigable if the console
is closed. Keep the local server running throughout the presentation.
Source files:
- `index.html`, `presenter.css`, `presenter.js`: presenter interface.
- `../deck/presenter-bridge.js`: optional synchronization bridge.
- `PresentationControls` in `../deck/index.html`: slide notes and popup interface.
+41
View File
@@ -0,0 +1,41 @@
<!doctype html>
<html lang="en">
<head>
<meta charset="utf-8">
<meta name="viewport" content="width=device-width, initial-scale=1">
<title>AritmoLab | Presenter console</title>
<link rel="stylesheet" href="../deck/fonts.css">
<link rel="stylesheet" href="presenter.css?v=6">
<script src="presenter.js?v=23" defer></script>
</head>
<body>
<header class="toolbar">
<div class="identity"><strong>AritmoLab</strong><span>Presenter console</span></div>
<div class="timer"><output id="elapsed" aria-label="Elapsed time">00:00</output><button id="timer-toggle">Start timer</button><button id="timer-reset" aria-label="Reset timer">Reset</button></div>
<button id="open-audience" class="primary">Open audience window ↗</button>
</header>
<div class="connection"><span id="status" role="status">Loading presentation…</span><span>Keep this window on your Mac. Move the audience window to HDMI, then choose Full screen.</span></div>
<main>
<section class="stage" aria-label="Slide preview and controls">
<div class="section-heading"><h2>On screen <span id="counter">01 / 14</span></h2><label class="sr-only" for="slide-picker">Jump to slide</label><select id="slide-picker" disabled><option>Loading slides…</option></select></div>
<div class="preview-shell"><iframe id="preview" title="Interactive slide preview" allow="fullscreen"></iframe></div>
<div class="slide-controls">
<div class="navigation"><button id="previous" disabled>← Previous</button><button id="next" class="primary" disabled>Next slide →</button><button id="close-popup" disabled hidden>Close popup <kbd>Esc</kbd></button></div>
<div id="popup-controls" aria-label="Slide popups" hidden></div>
</div>
<div class="up-next">
<div class="next-preview" id="next-preview-shell" inert aria-hidden="true">
<iframe id="next-preview" title="Next slide thumbnail" tabindex="-1"></iframe>
</div>
<div><span>UP NEXT</span><p id="next-title">Loading…</p></div>
</div>
<p id="error" role="alert" hidden></p>
</section>
<section class="script" aria-labelledby="notes-heading">
<div class="section-heading"><h2 id="notes-heading">Speaker notes</h2><div class="reading-controls"><button id="smaller" aria-label="Smaller notes">A−</button><button id="larger" aria-label="Larger notes">A+</button><label class="sr-only" for="layout">Notes layout</label><select id="layout"><option value="side">Notes beside preview</option><option value="below">Notes below preview</option></select></div></div>
<div id="reading" tabindex="0" aria-label="Scrollable speaker notes"><article id="notes">The slide and its speaker notes will appear here.</article></div>
<footer><span>Notes are visible only in this window.</span><span><kbd>←</kbd> <kbd>→</kbd> Change slide · <kbd>Esc</kbd> Close popup</span></footer>
</section>
</main>
</body>
</html>
+87
View File
@@ -0,0 +1,87 @@
:root { color-scheme: light; --bg: oklch(96.9% .006 20); --paper: oklch(99.1% .003 20); --ink: oklch(28% .012 10); --muted: oklch(49% .015 10); --line: oklch(86% .009 10); --accent: oklch(43% .105 12); --tint: oklch(93% .025 12); --notes-size: 25px; }
* { box-sizing: border-box; }
body { margin: 0; width: 100%; background: var(--bg); color: var(--ink); font: 16px/1.45 'Source Sans 3', system-ui, sans-serif; height: 100dvh; display: flex; flex-direction: column; }
button, select { font: inherit; color: inherit; background: var(--paper); border: 1px solid var(--line); border-radius: 6px; min-height: 38px; padding: 7px 12px; }
button { cursor: pointer; } button:hover:not(:disabled), select:hover { background: var(--tint); } button:disabled { opacity: .45; cursor: default; }
button.primary { background: var(--accent); color: var(--paper); border-color: var(--accent); font-weight: 600; } button.primary:hover { background: oklch(36% .10 12); }
:focus-visible { outline: 3px solid oklch(62% .14 12); outline-offset: 3px; }
.toolbar { display: flex; align-items: center; justify-content: space-between; gap: 24px; padding: 16px 24px; background: var(--paper); border-bottom: 1px solid var(--line); }
.identity { display: flex; gap: 16px; align-items: baseline; } .identity strong { font-size: 23px; letter-spacing: -.6px; } .identity span { color: var(--muted); }
.timer { display: flex; align-items: center; gap: 8px; } .timer output { font-variant-numeric: tabular-nums; font-size: 24px; margin-right: 8px; } .timer button { border: 0; background: transparent; padding-inline: 7px; }
.connection { padding: 9px 24px; display: flex; flex-wrap: wrap; gap: 6px 24px; font-size: 13px; color: var(--muted); border-bottom: 1px solid var(--line); }
#status { font-weight: 600; color: var(--accent); } #status.connected { color: oklch(40% .075 160); }
main { display: grid; grid-template-columns: minmax(340px, 44%) minmax(0, 1fr); flex: 1; min-height: 0; }
.stage { padding: 16px 20px; overflow-y: auto; min-width: 0; display: grid; grid-template-rows: auto var(--current-preview-height, 0px) auto minmax(0, 1fr) auto; gap: 10px; } .section-heading { display: flex; gap: 12px; align-items: center; justify-content: space-between; margin-bottom: 0; }
h2 { margin: 0; font-size: 16px; font-weight: 600; white-space: nowrap; } #counter { color: var(--muted); font-weight: 400; font-variant-numeric: tabular-nums; margin-left: 8px; }
#slide-picker { min-width: 0; width: 52%; text-overflow: ellipsis; font-size: 14px; }
.preview-shell, .next-preview { position: relative; width: 100%; aspect-ratio: 16/9; overflow: hidden; background: var(--paper); border: 1px solid var(--line); }
.stage > .preview-shell { width: 100%; height: 100%; min-height: 0; aspect-ratio: auto; justify-self: center; border: 0; background: transparent; }
#preview, #next-preview { position: absolute; left: 50%; top: 50%; width: 1280px; height: 720px; transform-origin: center; border: 0; display: block; }
#next-preview { pointer-events: none; }
#preview { height: 686px; }
.next-preview[hidden] { display: none; }
.navigation { display: grid; grid-template-columns: 1fr 1fr; gap: 8px; } .navigation button { min-height: 36px; }
.navigation:has(#close-popup:not([hidden])) { grid-template-columns: 1fr 1fr auto; }
#close-popup { font-size: 12px; padding-inline: 8px; }
#popup-controls { display: grid; grid-template-columns: repeat(2, minmax(0, 1fr)); gap: 14px; }
#popup-controls:not([hidden]) { margin-top: 8px; }
#popup-controls[hidden] { display: none; }
#popup-controls button { display: flex; align-items: center; gap: 3px; text-align: left; font-size: 15px; font-weight: 700; line-height: 1.2; min-height: 38px; padding: 4px 6px; width: 100%; overflow-wrap: anywhere; }
#popup-controls button > span:last-child { min-width: 0; }
.popup-number { font-size: 23px; font-weight: 800; line-height: 1; flex: none; }
#popup-controls button[aria-pressed="true"] { background: var(--tint); border-color: var(--accent); color: var(--accent); }
.popup-group { min-width: 0; }
.popup-buttons { display: grid; grid-template-columns: repeat(2, minmax(0, 1fr)); gap: 5px; }
.popup-group-lens { grid-column: 1 / -1; }
.popup-group-lens .popup-buttons { grid-template-columns: repeat(5, minmax(0, 1fr)); }
.popup-group-lens .popup-buttons:has(> button:nth-child(4):last-child) { grid-template-columns: repeat(4, minmax(0, 1fr)); }
.popup-group-screenshot { grid-column: 1 / -1; }
.popup-group-screenshot .popup-buttons { grid-template-columns: repeat(7, minmax(0, 1fr)); }
.popup-group-screenshot .popup-buttons:has(> button:nth-child(12):last-child) { grid-template-columns: repeat(6, minmax(0, 1fr)); }
#popup-controls .popup-group-screenshot button { justify-content: center; }
.popup-group-screenshot button > span { display: none; }
#popup-controls .popup-group-lens button { flex-direction: column; justify-content: center; text-align: center; }
.popup-group-lens button > span:nth-child(2) { display: none; }
.popup-group-ai { grid-column: 1 / -1; } .popup-group-ai .popup-buttons { grid-template-columns: repeat(3, minmax(0, 1fr)); }
.control-ai-icon { width: 27px; height: 27px; flex: 0 0 27px; margin-right: 3px; }
.up-next { border-top: 1px solid var(--line); padding-top: 10px; display: grid; grid-template-rows: minmax(0, 1fr) auto; gap: 6px; min-height: 0; }
.up-next .next-preview { height: 100%; min-height: 0; aspect-ratio: auto; border: 0; background: transparent; }
.up-next > div:last-child { display: flex; align-items: baseline; gap: 12px; min-width: 0; }
.up-next span, .slide-label { color: var(--muted); font-size: 12px; letter-spacing: .09em; }
.up-next span { flex: none; } .up-next p { margin: 0; font-size: 16px; overflow-wrap: anywhere; }
.up-next:has(.next-preview[hidden]) { grid-template-rows: auto; align-content: start; }
@media (min-width: 621px) {
/* The current preview keeps its no-popup height; the next takes the remainder. */
body:not([data-layout="below"]) .up-next { display: contents; }
body:not([data-layout="below"]) .next-preview { grid-row: 4; }
body:not([data-layout="below"]) .up-next > div:last-child { grid-row: 5; }
body:not([data-layout="below"]) #next-title { white-space: nowrap; overflow: hidden; text-overflow: ellipsis; }
}
.script { background: var(--paper); border-left: 1px solid var(--line); display: flex; flex-direction: column; min-height: 0; min-width: 0; }
.script > .section-heading { margin: 0; padding: 20px 28px 16px; border-bottom: 1px solid var(--line); flex-wrap: wrap; }
.reading-controls { display: flex; align-items: center; gap: 6px; } .reading-controls select { font-size: 13px; max-width: 185px; } .reading-controls button { font-size: 17px; }
#reading { overflow: auto; padding: 22px 28px 40px; flex: 1; min-width: 0; scroll-behavior: auto; } h1 { font-size: 27px; line-height: 1.2; letter-spacing: -.3px; margin: 8px 0 24px; overflow-wrap: anywhere; }
#notes { font-size: var(--notes-size); line-height: 1.48; white-space: normal; width: 100%; overflow-wrap: anywhere; } #notes p { margin: 0 0 .85em; } #notes strong { font-weight: 600; } #notes .notes-authored { white-space: pre-line; } .notes-draft { font-size: 13px; color: var(--muted); margin-bottom: 16px; }
#notes [data-note-lens], #notes [data-note-screenshot] {
min-height: 0; padding: 0; border: 0; border-bottom: 1px dotted currentColor; border-radius: 0;
background: transparent; color: var(--accent); font: inherit; font-weight: 600;
text-align: left; cursor: pointer;
}
#notes [data-note-lens]:hover, #notes [data-note-screenshot]:hover { background: var(--tint); }
#notes [data-note-lens]:focus-visible, #notes [data-note-screenshot]:focus-visible { outline: 2px solid currentColor; outline-offset: 3px; }
footer { border-top: 1px solid var(--line); padding: 12px 28px; font-size: 12px; color: var(--muted); display: flex; flex-wrap: wrap; gap: 6px 20px; justify-content: space-between; }
kbd { font: 12px system-ui; background: var(--bg); border: 1px solid var(--line); border-radius: 3px; padding: 1px 4px; } #error { color: var(--accent); }
.sr-only { position: absolute; width: 1px; height: 1px; padding: 0; overflow: hidden; clip: rect(0,0,0,0); white-space: nowrap; border: 0; }
body[data-layout="below"] main { grid-template-columns: 1fr; grid-template-rows: minmax(240px, 42%) minmax(0, 1fr); }
body[data-layout="below"] .stage { display: grid; grid-template-columns: minmax(0, 1fr) minmax(0, 1.2fr) minmax(150px, .65fr); grid-template-rows: auto minmax(0, 1fr); column-gap: 24px; padding: 14px 24px; }
body[data-layout="below"] .stage > .section-heading { grid-column: 1; }
body[data-layout="below"] .preview-shell { grid-column: 1; grid-row: 2; width: 100%; height: 100%; aspect-ratio: auto; min-height: 100px; }
body[data-layout="below"] .slide-controls { grid-column: 2; grid-row: 1 / 3; overflow: auto; min-height: 0; }
body[data-layout="below"] .navigation { margin: 0 0 16px; }
body[data-layout="below"] .up-next { grid-column: 3; grid-row: 1 / 3; border-top: 0; padding-top: 0; overflow: auto; }
body[data-layout="below"] .up-next > div:last-child { display: block; }
body[data-layout="below"] .stage .hint { display: none; } body[data-layout="below"] .script { border-left: 0; border-top: 1px solid var(--line); }
@media (max-width: 850px) { .identity span, .timer #timer-reset { display: none; } .toolbar { gap: 10px; padding: 12px; } main { grid-template-columns: minmax(280px, 42%) minmax(0,1fr); } .stage { padding: 16px; } #reading { padding: 22px; } .script > .section-heading { padding: 16px; } }
@media (max-width: 1000px) { body[data-layout="below"] .stage { grid-template-columns: minmax(0, 1fr) minmax(0, 1fr); grid-template-rows: auto auto auto; overflow: auto; } body[data-layout="below"] .preview-shell { height: auto; aspect-ratio: 16/9; } body[data-layout="below"] .up-next { grid-column: 1; grid-row: 3; display: flex; padding-top: 14px; } body[data-layout="below"] .up-next .next-preview { margin: 0; } body[data-layout="below"] .slide-controls { grid-row: 1 / 4; overflow: visible; } }
@media (max-width: 620px) { body { height: auto; min-height: 100dvh; } .toolbar { flex-wrap: wrap; } main, body[data-layout="below"] main { display: block; } body[data-layout="below"] .stage { display: block; } body[data-layout="below"] .navigation { margin-top: 14px; } .script { min-height: 65vh; } .connection { padding: 10px 12px; } #reading { max-height: 70vh; } .timer output { font-size: 20px; } .section-heading { flex-wrap: wrap; } .reading-controls { flex-wrap: wrap; } #slide-picker { width: 100%; } }
@media (max-width: 620px) { .stage { display: block; } .stage > .preview-shell, body[data-layout="below"] .stage > .preview-shell { width: 100%; aspect-ratio: 1280/686; height: auto; margin: 12px 0; } .up-next, body[data-layout="below"] .up-next { display: grid; grid-template-rows: auto auto; margin-top: 12px; } .up-next .next-preview { aspect-ratio: 16/9; height: auto; } }
+262
View File
@@ -0,0 +1,262 @@
(() => {
const $ = id => document.getElementById(id);
const params = new URLSearchParams(location.search);
const session = params.get('session') || crypto.randomUUID();
params.set('session', session);
history.replaceState(null, '', `${location.pathname}?${params}`);
const channel = new BroadcastChannel(`aritmolab-presenter:${session}`);
const sender = crypto.randomUUID();
const send = (type, extra = {}) => channel.postMessage({ type, role: 'console', sender, ...extra });
const command = (action, extra = {}) => send('command', { action, ...extra });
const preview = $('preview');
const nextPreview = $('next-preview');
let nextPreviewReady = false;
let slides = [];
let current = null;
let currentNoteKey = null;
let audience = null;
let lastAudienceSeen = 0;
let lastPreviewSeen = 0;
let notesSize = 25;
let blocked = false;
const urlFor = role => `../deck/index.html?v=19&view=${role}&session=${encodeURIComponent(session)}`;
const pad = value => String(value).padStart(2, '0');
function renderNotes(html) {
const template = document.createElement('template');
template.innerHTML = html;
const target = $('notes');
target.replaceChildren();
// Preserve authored rich text when available; plain scripts get reading paragraphs.
if (template.content.querySelector('p, ul, ol, h3, blockquote')) {
target.append(template.content.cloneNode(true));
return;
}
if (template.content.querySelector('*')) {
const script = document.createElement('div');
script.className = 'notes-authored';
script.append(template.content.cloneNode(true));
target.append(script);
return;
}
let text = template.content.textContent.trim();
const draft = text.match(/^\[DRAFT[^\]]*\]/);
if (draft) {
const label = document.createElement('div');
label.className = 'notes-draft';
label.textContent = draft[0];
target.append(label, document.createTextNode('\n'));
text = text.slice(draft[0].length).trim();
}
const segmenter = new Intl.Segmenter('en', { granularity: 'sentence' });
const paragraphs = [];
for (const line of text.split(/\n+/).map(line => line.trim()).filter(Boolean)) {
let paragraph = '';
for (const { segment } of segmenter.segment(line)) {
if (paragraph.length >= 240 && paragraph.length + segment.length > 390) {
paragraphs.push(paragraph.trim());
paragraph = '';
}
paragraph += segment;
}
if (paragraph.trim()) paragraphs.push(paragraph.trim());
}
for (const text of paragraphs.length ? paragraphs : ['No speaker notes for this slide.']) {
const paragraph = document.createElement('p');
for (const part of text.split(/(\b(?:Cardioref|Omics Portal|AritmoLab|ThothII|Tier 1|Tier 2)\b)/g)) {
if (/^(Cardioref|Omics Portal|AritmoLab|ThothII|Tier 1|Tier 2)$/.test(part)) {
const strong = document.createElement('strong'); strong.textContent = part; paragraph.append(strong);
} else paragraph.append(document.createTextNode(part));
}
target.append(paragraph, document.createTextNode('\n'));
}
}
function updateNextPreview() {
const index = (current?.index ?? 0) + 1;
const hasNext = index < slides.length;
$('next-preview-shell').hidden = !hasNext;
if (hasNext && nextPreviewReady) nextPreview.contentWindow.Reveal.slide(index);
}
function render(state) {
if (!state || !slides[state.index]) return;
const changed = !current || current.index !== state.index;
current = state;
const slide = slides[state.index];
$('counter').textContent = `${pad(state.index + 1)} / ${pad(slides.length)}`;
$('slide-picker').value = state.index;
$('previous').disabled = state.index === 0;
$('next').disabled = state.index === slides.length - 1;
$('close-popup').disabled = !state.popup;
if (changed) {
// Trusted, same-origin authored notes, never channel-provided HTML.
if (!slide.popupNotes?.length) renderNotes(slide.notes);
currentNoteKey = null;
$('reading').scrollTop = 0;
$('next-title').textContent = slides[state.index + 1]?.title || 'End of presentation';
updateNextPreview();
$('popup-controls').replaceChildren();
$('popup-controls').hidden = !slide.popups.length;
$('close-popup').hidden = !slide.popups.length;
let popupNumber = 0;
const groups = ['lens', 'screenshot', 'start', 'missing', 'ai'].flatMap(kind => {
const popups = slide.popups.filter(popup => popup.kind === kind);
return popups.length ? [{ kind, popups }] : [];
});
for (const { kind, popups } of groups) {
const group = document.createElement('section');
group.className = `popup-group popup-group-${kind}`;
group.setAttribute('aria-label', { lens: 'Enlargements', screenshot: 'Screenshots', start: 'Source systems', missing: 'Missing pieces', ai: 'AI contributions' }[kind]);
const buttons = document.createElement('div');
buttons.className = 'popup-buttons';
group.append(buttons);
$('popup-controls').append(group);
for (const popup of popups) {
const button = document.createElement('button');
const number = document.createElement('strong');
number.className = 'popup-number';
number.textContent = ++popupNumber;
const separator = document.createElement('span');
separator.textContent = ' - ';
const label = document.createElement('span');
label.textContent = popup.title.split(' — ')[0];
button.append(number, separator, label);
if (kind === 'ai') {
const icon = document.createElement('img');
icon.src = '../deck/artificial-brain.svg'; icon.alt = ''; icon.className = 'control-ai-icon';
button.insertBefore(icon, number);
}
button.title = popup.title;
button.dataset.kind = popup.kind;
button.dataset.key = popup.key;
button.onclick = () => {
if (kind === 'lens') { command('popup', { popup: { kind: popup.kind, key: popup.key } }); return; }
if (current?.popup?.kind === popup.kind && current.popup.key === popup.key) command('close');
else command('popup', { popup: { kind: popup.kind, key: popup.key } });
};
buttons.append(button);
}
}
}
if (slide.popupNotes?.length) {
// Keep the last selected notes readable when its screenshot is closed.
const note = slide.popupNotes.find(note => state.popup?.kind === 'screenshot' && note.key === state.popup.key)
|| slide.popupNotes.find(note => note.key === currentNoteKey)
|| slide.popupNotes[0];
if (note.key !== currentNoteKey) {
currentNoteKey = note.key;
renderNotes(note.html);
$('reading').scrollTop = 0;
}
}
$('popup-controls').querySelectorAll('button').forEach(button => {
button.setAttribute('aria-pressed', String(state.popup?.kind === button.dataset.kind && state.popup?.key === button.dataset.key));
});
}
channel.onmessage = ({ data }) => {
if (!data || data.sender === sender) return;
if (data.role === 'audience') lastAudienceSeen = data.type === 'departed' ? 0 : Date.now();
if (data.role === 'preview') lastPreviewSeen = data.type === 'departed' ? 0 : Date.now();
if (data.type === 'state') render(data.state);
updateStatus();
};
function updateStatus() {
const connected = Date.now() - lastAudienceSeen < 4500;
const previewReady = slides.length > 0 && Date.now() - lastPreviewSeen < 4500;
$('status').textContent = !previewReady ? 'Connecting to slide preview…' : connected ? 'Audience connected' : blocked ? 'Window blocked. Allow popups and click Open audience window again.' : 'Preview only. Open the audience window to present.';
$('status').classList.toggle('connected', connected);
}
$('open-audience').onclick = () => {
if (audience && !audience.closed) { audience.focus(); return; }
audience = window.open(urlFor('audience'), `aritmolab-audience-${session}`, 'popup=yes,width=1280,height=760');
blocked = !audience;
updateStatus();
};
preview.addEventListener('load', async () => {
try {
const win = preview.contentWindow;
if (!win.Reveal) throw new Error('Slide preview could not load. Check that the local server is running.');
if (!win.Reveal.isReady()) await new Promise(resolve => win.Reveal.on('ready', resolve));
slides = win.PresentationControls.slides();
$('slide-picker').replaceChildren(...slides.map((slide, index) => new Option(`${pad(index + 1)} · ${slide.title}`, index)));
$('slide-picker').disabled = false;
current = null;
render(win.PresentationControls.snapshot());
send('request');
} catch (error) { $('error').hidden = false; $('error').textContent = error.message; }
});
preview.src = urlFor('preview');
nextPreview.addEventListener('load', async () => {
const reveal = nextPreview.contentWindow.Reveal;
if (!reveal) return;
if (!reveal.isReady()) await new Promise(resolve => reveal.on('ready', resolve));
reveal.configure({ keyboard: false, touch: false, overview: false, transition: 'none' });
nextPreviewReady = true;
updateNextPreview();
});
// No session token: the thumbnail must never navigate or publish to the audience.
nextPreview.src = '../deck/index.html?v=19&view=thumbnail#/1';
const stage = document.querySelector('.stage');
function sizeCurrentPreview() {
const style = getComputedStyle(stage);
const navigation = getComputedStyle($('previous'));
// Use the one-line navigation height, never the popup-dependent controls height.
const navigationHeight = Math.max(parseFloat(navigation.minHeight),
parseFloat(navigation.lineHeight) + parseFloat(navigation.paddingTop) +
parseFloat(navigation.paddingBottom) + parseFloat(navigation.borderTopWidth) +
parseFloat(navigation.borderBottomWidth));
const available = stage.clientHeight - parseFloat(style.paddingTop) - parseFloat(style.paddingBottom)
- stage.querySelector('.section-heading').getBoundingClientRect().height
- navigationHeight - parseFloat(getComputedStyle($('next-title')).lineHeight)
- 4 * parseFloat(style.rowGap);
// Do not reserve empty bands when the slide is already limited by column width.
const widthLimitedHeight = (stage.clientWidth - parseFloat(style.paddingLeft) - parseFloat(style.paddingRight)) * 686 / 1280;
stage.style.setProperty('--current-preview-height', `${Math.max(0, Math.min(available * .65, widthLimitedHeight))}px`);
}
const stageSizes = new ResizeObserver(sizeCurrentPreview);
stageSizes.observe(stage);
stageSizes.observe(stage.querySelector('.section-heading'));
stageSizes.observe($('previous'));
sizeCurrentPreview();
function fitPreviews() {
const currentBox = preview.parentElement;
const nextBox = nextPreview.parentElement;
const currentScale = Math.min(currentBox.clientWidth / 1280, currentBox.clientHeight / 686);
// The next slide includes its footer: cap its rendered height as well as its width.
const nextScale = Math.min(nextBox.clientWidth / 1280, nextBox.clientHeight / 720, currentScale * 686 / 720);
preview.style.transform = `translate(-50%, -50%) scale(${currentScale})`;
nextPreview.style.transform = `translate(-50%, -50%) scale(${nextScale})`;
}
const previewSizes = new ResizeObserver(fitPreviews);
for (const frame of [preview, nextPreview]) previewSizes.observe(frame.parentElement);
$('previous').onclick = () => command('previous');
$('next').onclick = () => command('next');
$('close-popup').onclick = () => command('close');
$('slide-picker').onchange = event => command('slide', { index: Number(event.target.value) });
$('layout').onchange = event => { document.body.dataset.layout = event.target.value; };
function fontSize(delta) {
notesSize = Math.min(40, Math.max(18, notesSize + delta));
document.documentElement.style.setProperty('--notes-size', `${notesSize}px`);
$('smaller').disabled = notesSize === 18; $('larger').disabled = notesSize === 40;
}
$('smaller').onclick = () => fontSize(-2); $('larger').onclick = () => fontSize(2);
document.addEventListener('keydown', event => {
if (event.target.closest('input, select, textarea, [contenteditable="true"]')) return;
if (/^[1-9]$/.test(event.key) && !event.repeat && !event.altKey && !event.ctrlKey && !event.metaKey) {
const lens = [...$('popup-controls').querySelectorAll('[data-kind="lens"], [data-kind="screenshot"]')]
.find(button => button.querySelector('.popup-number').textContent === event.key);
if (lens) { event.preventDefault(); lens.click(); return; }
}
const action = { ArrowRight: 'next', ArrowLeft: 'previous', Escape: 'close' }[event.key];
if (action) { event.preventDefault(); command(action); }
});
let elapsed = 0, started = null;
$('timer-toggle').onclick = () => {
if (started === null) { started = Date.now(); $('timer-toggle').textContent = 'Pause timer'; }
else { elapsed += Date.now() - started; started = null; $('timer-toggle').textContent = 'Start timer'; }
};
$('timer-reset').onclick = () => { elapsed = 0; if (started !== null) started = Date.now(); };
setInterval(() => {
const seconds = Math.floor((elapsed + (started === null ? 0 : Date.now() - started)) / 1000);
$('elapsed').textContent = `${pad(Math.floor(seconds / 60))}:${pad(seconds % 60)}`;
updateStatus();
}, 500);
})();
+58
View File
@@ -0,0 +1,58 @@
<!doctype html>
<!-- PROTOTYPE (throwaway) — contact sheet of the 14 deck slides, live iframes scaled down.
Slide 04 shows the OLD deck layout: the approved new composition lives in slide05-outputs.html. -->
<html lang="en">
<head>
<meta charset="utf-8">
<title>PROTOTYPE · Deck overview — 14 slides</title>
<link rel="stylesheet" href="/deck/fonts.css">
<style>
:root { --bordeaux: #cb333b; --ink: #2a2426; --ink-muted: #6b6568; --line: #e3e0e1;
--sans: 'Source Sans 3', system-ui, sans-serif; --mono: ui-monospace, 'SF Mono', Menlo, Consolas, monospace; }
body { margin: 0; background: #e9e7e7; font-family: var(--sans); padding: 22px 0 60px; }
h1 { font: 700 13px/1 var(--mono); letter-spacing: .14em; text-transform: uppercase;
color: var(--bordeaux); text-align: center; margin: 0 0 6px; }
p.note { text-align: center; font: 400 12.5px/1.5 var(--sans); color: var(--ink-muted); margin: 0 0 22px; }
.grid { display: grid; grid-template-columns: repeat(3, 1fr); gap: 22px 18px; max-width: 1180px; margin: 0 auto; padding: 0 16px; }
.cell { background: #fff; box-shadow: 0 4px 16px rgba(42,36,38,.14); border-radius: 6px; overflow: hidden;
cursor: pointer; }
.cell:hover { outline: 3px solid var(--bordeaux); }
.cell .bar { display: flex; align-items: baseline; gap: 8px; padding: 7px 10px 0;
font: 600 11px/1.2 var(--sans); color: var(--ink); }
.cell .bar .num { font: 700 10px/1 var(--mono); color: var(--bordeaux); }
.cell .bar .st { margin-left: auto; font: 700 8.5px/1 var(--mono); letter-spacing: .1em; }
.st.final { color: #2f6f4f; } .st.refined { color: var(--bordeaux); } .st.draft { color: #a08a3c; } .st.proto { color: var(--bordeaux-deep); }
.shot { width: 100%; aspect-ratio: 16/9; position: relative; overflow: hidden; }
.shot iframe { width: 1280px; height: 720px; border: 0; transform: scale(0.277); transform-origin: 0 0;
pointer-events: none; position: absolute; top: 0; left: 0; }
.cell.wide { grid-column: span 1; }
</style>
</head>
<body>
<h1>Deck overview — 14 slides · AritmoLab</h1>
<p class="note">Live previews of <b>/deck/index.html</b> · <b>click any slide to open it full-size</b>, then flip with the arrow keys.</p>
<p class="note"><a href="/presenter/">Open presenter console</a> · Speaker notes, interactive preview and a separate audience window.</p>
<div class="grid" id="grid"></div>
<script>
const TITLES = ['Title', 'Where we started', 'What we wanted to build', 'The problem',
'What the text miner reads', 'Hard problems of clinical NLP', 'The clinical ontology',
'Trust the text', 'AritmoLab today', 'All good? Not yet', 'ThothII', 'A tour of ThothII',
'From datamarts to models', 'Thank you'];
const STATUS = ['FINAL', 'FINAL', 'FINAL', 'FINAL', 'FINAL', 'FINAL', 'FINAL', 'REFINED',
'DRAFT', 'DRAFT', 'DRAFT', 'DRAFT', 'DRAFT', 'DRAFT'];
const CLS = ['final', 'final', 'final', 'final', 'final', 'final', 'final', 'refined',
'draft', 'draft', 'draft', 'draft', 'draft', 'draft'];
const grid = document.getElementById('grid');
TITLES.forEach((t, i) => {
const cell = document.createElement('div');
cell.className = 'cell';
cell.title = 'Open slide ' + (i + 1) + ' full-size';
cell.onclick = () => { location.href = '/deck/index.html?r=' + Date.now() + '#/' + i; };
cell.innerHTML = `<div class="bar"><span class="num">${String(i + 1).padStart(2, '0')}</span><span>${t}</span>` +
`<span class="st ${CLS[i]}">${STATUS[i]}</span></div>` +
`<div class="shot"><iframe loading="lazy" src="/deck/index.html?v=28&ov=1#/${i}" title="${t}"></iframe></div>`;
grid.appendChild(cell);
});
</script>
</body>
</html>
+229
View File
@@ -0,0 +1,229 @@
<!doctype html>
<!-- PROTOTYPE (throwaway) — Option C "Maturity rail", revised per user feedback:
connector starts from the Marts stage · no live/next badges · uniform stroke
intensity · texts inside boxes capitalized · EN translation same size as Italian.
This is the only variant carried forward; A and B were discarded. -->
<html lang="en">
<head>
<meta charset="utf-8">
<title>PROTOTYPE · Slide “The problem” — outputs rail from the datamarts (option C, revised)</title>
<link rel="stylesheet" href="/deck/fonts.css">
<style>
:root {
--bordeaux: #cb333b; --bordeaux-deep: #8f1204; --bordeaux-tint: #f4dfe1;
--surface: #fdfcfc; --surface-alt: #f6f5f5; --surface-sunken: #ecebeb;
--ink: #2a2426; --ink-muted: #6b6568;
--line: #e3e0e1; --line-strong: #c9c5c6;
--sans: 'Source Sans 3', system-ui, sans-serif;
--display: 'Source Serif 4', Georgia, serif;
--mono: ui-monospace, 'SF Mono', Menlo, Consolas, monospace;
}
* { box-sizing: border-box; }
body { margin: 0; background: #e9e7e7; font-family: var(--sans); padding: 26px 0 90px; }
.proto-head { text-align: center; margin: 0 0 14px; }
.proto-head b { font: 700 12px/1 var(--mono); letter-spacing: .14em; text-transform: uppercase; color: var(--bordeaux); }
.proto-head span { display: block; margin-top: 6px; font: 400 13px/1.4 var(--sans); color: var(--ink-muted); }
/* ---- slide frame (deck-like, 1200×690) ---- */
.frame { width: 1200px; height: 690px; margin: 0 auto; background: var(--surface);
display: flex; flex-direction: column; box-shadow: 0 8px 30px rgba(42,36,38,.18); overflow: hidden; }
.head { flex: 0 0 74px; display: flex; align-items: center; justify-content: space-between;
padding: 0 40px; border-bottom: 1px solid var(--line); }
.head img { height: 54px; display: block; }
.head-org { font: 700 11px/1 var(--sans); letter-spacing: .14em; text-transform: uppercase; color: var(--ink-muted); }
.band { flex: 0 0 42px; display: flex; align-items: center; justify-content: space-between;
padding: 0 40px; background: var(--bordeaux); color: var(--surface); }
.band b { font: 700 12.5px/1 var(--sans); letter-spacing: .14em; text-transform: uppercase; }
.sbody { flex: 1; min-height: 0; display: flex; flex-direction: column;
padding: 20px 48px 14px; text-align: left; }
.foot { flex: 0 0 30px; display: flex; align-items: center; justify-content: space-between; gap: 24px;
padding: 0 40px; border-top: 1px solid var(--line);
font: 600 9.5px/1.4 var(--sans); letter-spacing: .03em; color: var(--ink-muted); }
.kicker { font: 700 11.5px/1 var(--sans); letter-spacing: .16em; text-transform: uppercase; color: var(--bordeaux); margin-bottom: 10px; }
h2 { font: 700 27px/1.15 var(--sans); letter-spacing: -.01em; color: var(--ink); margin: 0 0 10px; }
.quote { position: relative; margin: 0 0 10px; padding: 14px 22px 11px 64px; border: 1px solid var(--line);
background: var(--surface-alt); border-radius: 8px;
font: italic 400 16.5px/1.5 var(--display); color: var(--ink); }
.quote::before { content: '\201C'; position: absolute; left: 18px; top: 6px;
font: 700 44px/1 var(--display); color: var(--bordeaux); }
.tr-grid { display: grid; grid-template-columns: 1.12fr 1fr; gap: 22px; align-items: center; }
.tr-grid .it, .tr-grid .en { margin: 0; }
.tr-grid .en { padding-left: 18px; border-left: 1px solid var(--line-strong);
font: 400 16.5px/1.5 var(--sans); color: var(--bordeaux-deep); }
.en-tag { display: inline-block; margin-right: 7px; padding: 2px 7px; border-radius: 4px;
background: var(--bordeaux); color: #fdfcfc; font: 700 9px/1 var(--sans); letter-spacing: .12em; vertical-align: 3px; }
.pipeline { display: flex; align-items: stretch; gap: 10px; }
.stage { flex: 1; padding: 10px 8px; text-align: center; border: 1px solid var(--line); border-radius: 8px; background: var(--surface-alt); }
.stage b { display: block; font: 700 12.5px/1.2 var(--sans); margin-bottom: 3px; text-transform: uppercase; }
.stage span { display: block; font: 400 10.5px/1.35 var(--sans); color: var(--ink-muted); text-transform: uppercase; letter-spacing: .02em; }
.stage .tag { display: inline-block; margin-top: 4px; padding: 2px 9px; border-radius: 9999px;
background: var(--bordeaux); color: #fdfcfc; font: 700 8.5px/1 var(--sans); letter-spacing: .1em; text-transform: uppercase; }
.brain { display: inline-flex; align-items: center; justify-content: center; width: 36px; height: 36px;
border-radius: 9999px; background: #fff; border: 1.5px solid var(--bordeaux); }
.brain svg { width: 26px; height: 26px; color: var(--bordeaux); }
.stage.ai { background: var(--bordeaux-tint); border-color: var(--bordeaux); }
.arrow { align-self: center; color: var(--line-strong); font: 700 17px/1 var(--sans); }
.stage.hot { background: #fdf1f2; border-color: var(--bordeaux); }
.thoth-wrap { position: relative; display: flex; justify-content: flex-end; margin-top: 10px; }
.thoth-col { width: 500px; text-align: center; }
.thoth-box { border: 1px solid var(--bordeaux); border-radius: 8px; padding: 9px 14px;
background: var(--surface-alt); font: 600 11.5px/1.4 var(--sans); text-transform: uppercase; letter-spacing: .02em; }
.thoth-box .tag { display: inline-block; margin-right: 7px; padding: 2px 9px; border-radius: 9999px;
background: var(--bordeaux); color: #fdfcfc; font: 700 8.5px/1 var(--sans); letter-spacing: .1em; text-transform: uppercase; }
.t-conn { position: absolute; width: 2px; background: var(--bordeaux); }
.t-head { position: absolute; width: 0; height: 0; border: 5px solid transparent;
border-bottom: 7px solid var(--bordeaux); transform: translateX(-50%); }
/* ---- deep-dive concepts (center-left) ---- */
.midrow { display: flex; gap: 28px; margin-top: 10px; align-items: flex-start; }
.concepts { flex: 1; min-width: 0; }
.concepts .lbl { font: 700 10px/1 var(--mono); letter-spacing: .14em; text-transform: uppercase;
color: var(--bordeaux); margin: 0 0 4px; }
.citem { display: flex; gap: 12px; align-items: baseline; padding: 6px 0; border-bottom: 1px solid var(--line); }
.citem:last-child { border-bottom: none; }
.citem .n { font: 700 11px/1 var(--mono); color: var(--bordeaux); }
.citem b { font: 700 12.5px/1.3 var(--sans); color: var(--ink); text-transform: uppercase; letter-spacing: .03em; }
.citem span.d { font: 400 11px/1.4 var(--sans); color: var(--ink-muted); margin-left: 8px; }
/* ---- outputs rail (option C, revised) ---- */
.railwrap { position: relative; display: flex; justify-content: flex-end; }
.conn-drop { position: absolute; top: -14px; width: 2px; background: var(--bordeaux); }
.conn-elbow { position: absolute; height: 2px; background: var(--bordeaux); }
.rail { width: 540px; border-left: 2px solid var(--bordeaux); padding-top: 14px; }
.rail .lbl { font: 700 10px/1 var(--mono); letter-spacing: .14em; text-transform: uppercase;
color: var(--bordeaux); margin: 0 0 6px 16px; }
.rail .row { display: flex; align-items: center; gap: 12px; padding: 8px 14px 8px 16px; position: relative; }
.rail .row::before { content: ''; position: absolute; left: -2px; top: 50%; width: 14px; height: 2px; background: var(--bordeaux); }
.rail .row .oicon { flex: 0 0 26px; width: 26px; height: 26px; color: var(--bordeaux); display: block; }
.rail .row b { font: 700 13.5px/1.2 var(--sans); color: var(--ink); text-transform: uppercase; letter-spacing: .03em; }
.rail .row span.d { font: 400 11px/1.3 var(--sans); color: var(--ink-muted); }
.caption { max-width: 1200px; margin: 12px auto 0; text-align: center;
font: 400 12.5px/1.5 var(--sans); color: var(--ink-muted); }
.caption b { color: var(--ink); }
</style>
</head>
<body>
<div class="proto-head">
<b>Prototype (revised) — slide “The problem” · outputs rail from the datamarts</b>
<span>Option C only: connector from Marts · no status badges · uniform intensity · capitalized box texts.</span>
</div>
<div class="frame">
<div class="head">
<img src="/deck/logo.png" alt="">
<span class="head-org">AritmoLab · Policlinico San Donato</span>
</div>
<div class="band"><b>The problem</b><span style="font:700 11px/1 var(--sans)">04 · The problem</span></div>
<div class="sbody">
<div class="kicker">From free text to a research platform</div>
<h2>The clinical truth lives in unstructured columns</h2>
<div class="quote">
<div class="tr-grid">
<p class="it">“Il paziente riferisce sincope ricorrente; ECG basale con sopraslivellamento ST in V1–V3; test provocativo con flecainide positivo per pattern Brugada.”</p>
<p class="en"><span class="en-tag">EN</span>“The patient reports recurrent syncope; baseline ECG with ST-segment elevation in V1–V3; flecainide provocation test positive for a Brugada pattern.”</p>
</div>
</div>
<div class="pipeline" id="pipeline">
<div class="stage"><b>Sources</b><span>Cardioref · Genetic data<br>Omics Portal · ECG</span></div>
<div class="arrow">→</div>
<div class="stage"><b>Staging</b><span>raw replica</span></div>
<div class="arrow">→</div>
<div class="stage ai"><b>Integration</b><span>3NF · text mining</span><span class="brain" style="margin-top:5px"><svg viewBox="0 0 32 32" fill="none" stroke="currentColor" stroke-width="2.4" stroke-linecap="round" stroke-linejoin="round"><path d="M16 4 C11 2 5.5 4 6 8.5 C2.5 10 2 14.5 4.5 17 C2.5 20 4 24.5 8 25 C9 28 14 29.5 16 27"/><path d="M16 4 C21 2 26.5 4 26 8.5 C29.5 10 30 14.5 27.5 17 C29.5 20 28 24.5 24 25 C23 28 18 29.5 16 27"/><path d="M16 4.5 V26.5"/><path d="M16 10 h4.5"/><circle cx="23" cy="10" r="1.7" fill="currentColor" stroke="none"/><path d="M16 15.5 h-4.5"/><circle cx="8.5" cy="15.5" r="1.7" fill="currentColor" stroke="none"/><path d="M16 21 h4.5"/><circle cx="23" cy="21" r="1.7" fill="currentColor" stroke="none"/></svg></span></div>
<div class="arrow">→</div>
<div class="stage"><b>Data Warehouse</b><span>star schema</span></div>
<div class="arrow">→</div>
<div class="stage hot" id="marts"><b>Marts</b><span>dbt · Superset</span></div>
</div>
<div class="midrow">
<div class="concepts">
<div class="lbl">four challenges to face · the answers come next</div>
<div class="citem"><span class="n">01</span><div><b>Volume without structure</b><span class="d">twenty years of records locked in free-text columns</span></div></div>
<div class="citem"><span class="n">02</span><div><b>Ambiguous language</b><span class="d">negation · family history · bilingual shorthand</span></div></div>
<div class="citem"><span class="n">03</span><div><b>No shared ontology</b><span class="d">every system names conditions its own way</span></div></div>
<div class="citem"><span class="n">04</span><div><b>Proving reliability</b><span class="d">extraction must be demonstrably correct</span></div></div>
</div>
<div class="railwrap" id="railwrap">
<div class="conn-drop" id="conn-drop"></div>
<div class="conn-elbow" id="conn-elbow"></div>
<div class="rail" id="rail">
<div class="lbl">outputs of the datamarts</div>
<div class="row">
<svg class="oicon" viewBox="0 0 34 34" fill="none" stroke="currentColor" stroke-width="2.2" stroke-linecap="round"><path d="M4 29 h26"/><rect x="7" y="17" width="5" height="12" rx="1"/><rect x="15" y="9" width="5" height="20" rx="1"/><rect x="23" y="13" width="5" height="16" rx="1"/></svg>
<b>Dashboards</b><span class="d">clinical review on Superset</span>
</div>
<div class="row">
<svg class="oicon" viewBox="0 0 34 34" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round"><circle cx="7" cy="7" r="2.6"/><circle cx="7" cy="27" r="2.6"/><circle cx="17" cy="17" r="3.4"/><circle cx="27" cy="7" r="2.6"/><circle cx="27" cy="27" r="2.6"/><path d="M9.3 8.8 L14.6 14.6"/><path d="M9.3 25.2 L14.6 19.4"/><path d="M19.4 14.6 L24.7 8.8"/><path d="M19.4 19.4 L24.7 25.2"/></svg>
<b>Machine Learning</b><span class="d">cohort tables for model training</span>
</div>
<div class="row">
<svg class="oicon" viewBox="0 0 34 34" fill="none" stroke="currentColor" stroke-width="2.2" stroke-linecap="round"><path d="M3 27 L11 19 L17 22 L23 13"/><path d="M23 13 L30 6" stroke-dasharray="3 3.4"/><circle cx="30" cy="6" r="2.2" fill="currentColor" stroke="none"/><path d="M3 31 h28" opacity=".35"/></svg>
<b>Predictive Statistics</b><span class="d">outcomes &amp; risk analysis</span>
</div>
</div>
</div>
</div>
<div class="thoth-wrap" id="thothwrap">
<div class="t-conn" id="t-conn"></div>
<div class="t-head" id="t-head"></div>
<div class="thoth-col">
<div class="thoth-box"><span class="brain" style="margin-right:9px; vertical-align:middle"><svg viewBox="0 0 32 32" fill="none" stroke="currentColor" stroke-width="2.4" stroke-linecap="round" stroke-linejoin="round"><path d="M16 4 C11 2 5.5 4 6 8.5 C2.5 10 2 14.5 4.5 17 C2.5 20 4 24.5 8 25 C9 28 14 29.5 16 27"/><path d="M16 4 C21 2 26.5 4 26 8.5 C29.5 10 30 14.5 27.5 17 C29.5 20 28 24.5 24 25 C23 28 18 29.5 16 27"/><path d="M16 4.5 V26.5"/><path d="M16 10 h4.5"/><circle cx="23" cy="10" r="1.7" fill="currentColor" stroke="none"/><path d="M16 15.5 h-4.5"/><circle cx="8.5" cy="15.5" r="1.7" fill="currentColor" stroke="none"/><path d="M16 21 h4.5"/><circle cx="23" cy="21" r="1.7" fill="currentColor" stroke="none"/></svg></span>ThothII — natural-language → SQL over the DWH, builds the final datamarts</div>
</div>
</div>
</div>
<div class="foot"><span>Role of AI in the Analysis of Unstructured Clinical Databases</span><span>Dr. Marco Pancotti - MultiPhysixLab</span><span>Dr. Sara Paratico - Gruppo San Donato</span><span>San Donato Milanese, Milan, Italy · 2–3 October 2026</span><span>04 / 14</span></div>
</div>
<p class="caption"><b>Option C, revised.</b> The rail drops straight from the Marts stage; the three outputs have equal
visual weight and no status labels; texts inside the diagram boxes are capitalized; the EN translation is now the same size as the Italian.</p>
<script>
// Draw the connector: vertical drop from the center of the Marts stage,
// elbow leftward, into the top of the rail.
function alignConnector() {
const marts = document.getElementById('marts');
const rail = document.getElementById('rail');
const wrap = document.getElementById('railwrap');
const drop = document.getElementById('conn-drop');
const elbow = document.getElementById('conn-elbow');
const mr = marts.getBoundingClientRect();
const rr = rail.getBoundingClientRect();
const wr = wrap.getBoundingClientRect();
const cx = mr.left + mr.width / 2 - wr.left; // Marts center, wrap coords
const railLeft = rr.left - wr.left; // rail border x, wrap coords
const railTop = rr.top - wr.top; // rail top, wrap coords
drop.style.left = (cx - 1) + 'px';
drop.style.top = '-' + (rr.top - mr.bottom + 2) + 'px';
drop.style.height = (rr.top - mr.bottom + 2 + railTop) + 'px';
elbow.style.left = railLeft + 'px';
elbow.style.width = Math.max(0, cx - railLeft) + 'px';
elbow.style.top = (railTop - 1) + 'px';
// ThothII arrow: from the box up to the Marts stage, along the right margin
const pipe = document.getElementById('pipeline');
const twrap = document.getElementById('thothwrap');
const box = twrap.querySelector('.thoth-box');
const conn = document.getElementById('t-conn');
const head = document.getElementById('t-head');
const pr = pipe.getBoundingClientRect();
const tr = twrap.getBoundingClientRect();
const br = box.getBoundingClientRect();
const x = tr.width - 10; // right margin, under Marts
const top = pr.bottom - tr.top + 2; // just under the pipeline
const boxTop = br.top - tr.top;
conn.style.left = x + 'px';
conn.style.top = top + 'px';
conn.style.height = Math.max(0, boxTop - top) + 'px';
head.style.left = x + 'px';
head.style.top = (top - 7) + 'px';
}
addEventListener('load', alignConnector);
addEventListener('resize', alignConnector);
if (document.fonts && document.fonts.ready) document.fonts.ready.then(alignConnector);
alignConnector();
</script>
</body>
</html>
@@ -0,0 +1,295 @@
<!doctype html>
<!-- PROTOTYPE (throwaway) — Slide 07 "The clinical ontology": three variants, switchable
via ?variant=A|B|C + floating bar (← → keys too).
Question: how to present 11+5 categories so the slide is more readable, livelier,
and with ENGLISH more prominent than Italian (audience mostly foreign).
A = three columns of 5-6 pills (user's idea) · B = editorial bands, EN-first pills
· C = split: ontology left, "why the order matters" panel right. -->
<html lang="en">
<head>
<meta charset="utf-8">
<title>PROTOTYPE · Slide 07 "The clinical ontology" — 4 layout variants</title>
<link rel="stylesheet" href="/deck/fonts.css">
<style>
:root {
--bordeaux: #cb333b; --bordeaux-deep: #8f1204; --bordeaux-tint: #f4dfe1;
--surface: #fdfcfc; --surface-alt: #f6f5f5; --surface-sunken: #ecebeb;
--ink: #2a2426; --ink-muted: #6b6568;
--line: #e3e0e1; --line-strong: #c9c5c6;
--sans: 'Source Sans 3', system-ui, sans-serif;
--display: 'Source Serif 4', Georgia, serif;
--mono: ui-monospace, 'SF Mono', Menlo, Consolas, monospace;
}
* { box-sizing: border-box; }
body { margin: 0; background: #e9e7e7; font-family: var(--sans); padding: 26px 0 90px; }
.proto-head { text-align: center; margin: 0 0 14px; }
.proto-head b { font: 700 12px/1 var(--mono); letter-spacing: .14em; text-transform: uppercase; color: var(--bordeaux); }
.proto-head span { display: block; margin-top: 6px; font: 400 13px/1.4 var(--sans); color: var(--ink-muted); }
/* ---- slide frame (deck-like, 1200×690) ---- */
.frame { width: 1200px; height: 690px; margin: 0 auto; background: var(--surface);
display: flex; flex-direction: column; box-shadow: 0 8px 30px rgba(42,36,38,.18); overflow: hidden; }
.head { flex: 0 0 74px; display: flex; align-items: center; justify-content: space-between;
padding: 0 40px; border-bottom: 1px solid var(--line); }
.head img { height: 54px; display: block; }
.head-org { font: 700 11px/1 var(--sans); letter-spacing: .14em; text-transform: uppercase; color: var(--ink-muted); }
.band { flex: 0 0 42px; display: flex; align-items: center; justify-content: space-between;
padding: 0 40px; background: var(--bordeaux); color: var(--surface); }
.band b { font: 700 12.5px/1 var(--sans); letter-spacing: .14em; text-transform: uppercase; }
.band .jump { font: 700 11px/1 var(--mono); letter-spacing: .1em; }
.sbody { flex: 1; min-height: 0; display: flex; flex-direction: column;
padding: 22px 48px 16px; text-align: left; }
.vwrap { margin: auto 0; width: 100%; }
.foot { flex: 0 0 30px; display: flex; align-items: center; justify-content: space-between; gap: 24px;
padding: 0 40px; border-top: 1px solid var(--line);
font: 600 9.5px/1.4 var(--sans); letter-spacing: .03em; color: var(--ink-muted); }
.kicker { font: 700 11.5px/1 var(--sans); letter-spacing: .16em; text-transform: uppercase; color: var(--bordeaux); margin-bottom: 10px; }
h2 { font: 700 27px/1.15 var(--sans); letter-spacing: -.01em; color: var(--ink); margin: 0 0 14px; }
/* ---- shared: EN-first pills (EN bold dark = primary, IT gloss = secondary) ---- */
.pill { display: inline-flex; align-items: baseline; gap: 7px; border: 1px solid var(--bordeaux);
border-radius: 9999px; padding: 6px 13px; background: #fff; white-space: nowrap; }
.pill .en { font: 700 13px/1 var(--sans); color: var(--ink); }
.pill .it { font: 400 11px/1 var(--sans); color: var(--ink-muted); }
.pill.fill { background: var(--bordeaux-tint); }
.col-h { display: flex; align-items: center; gap: 8px; font: 700 10px/1 var(--mono);
letter-spacing: .14em; text-transform: uppercase; color: var(--bordeaux-deep); margin-bottom: 9px; }
.col-h .n { margin-left: auto; font: 700 9px/1 var(--mono); color: var(--bordeaux);
border: 1px solid var(--bordeaux); border-radius: 9999px; padding: 2px 7px; letter-spacing: .08em; }
/* ---- shared: numbered points ---- */
.pt .num { flex: 0 0 22px; font: 700 11px/1.6 var(--mono); color: var(--bordeaux); }
.pt b { font: 700 13px/1.35 var(--sans); color: var(--ink); }
.pt span.d { font: 400 12.5px/1.45 var(--sans); color: var(--ink-muted); }
code { font: 700 11.5px/1 var(--mono); color: var(--bordeaux-deep); background: var(--bordeaux-tint);
padding: 2px 6px; border-radius: 4px; }
/* ================= VARIANT A — three columns (user's idea) ================= */
.va-grid { display: grid; grid-template-columns: 1fr 1fr 1fr; gap: 0 26px; align-items: start; }
.va-col + .va-col { border-left: 1px solid var(--line); padding-left: 26px; }
.va-col .stack { display: flex; flex-direction: column; gap: 7px; align-items: stretch; }
.va-col .pill { display: flex; justify-content: flex-start; }
.va-points { display: grid; grid-template-columns: 1fr 1fr 1fr; gap: 26px; margin-top: 20px;
padding-top: 14px; border-top: 1px solid var(--line); }
.va-points .pt { display: flex; gap: 10px; }
/* ================= VARIANT B — editorial bands ================= */
.vb-band { display: flex; gap: 20px; align-items: flex-start; padding: 13px 0 15px; border-bottom: 1px solid var(--line); }
.vb-band:first-of-type { padding-top: 4px; }
.vb-num { flex: 0 0 96px; }
.vb-num b { display: block; font: 800 30px/1 var(--sans); letter-spacing: -.02em; color: var(--bordeaux); }
.vb-num span { display: block; margin-top: 5px; font: 700 9px/1.4 var(--mono); letter-spacing: .12em;
text-transform: uppercase; color: var(--ink-muted); }
.vb-chips { display: flex; flex-wrap: wrap; gap: 8px; align-content: flex-start; }
.vb-points { margin-top: 16px; }
.vb-points .pt { display: flex; gap: 10px; padding: 8px 0; border-bottom: 1px solid var(--line); }
.vb-points .pt:last-child { border-bottom: none; }
/* ================= VARIANT C — split: ontology / why order matters ================= */
.vc-split { display: grid; grid-template-columns: 1.42fr 1fr; gap: 26px; align-items: start; flex: 1; min-height: 0; }
.vc-tier { margin-bottom: 16px; }
.vc-tier .col-h { margin-bottom: 10px; }
.vc-chips { display: flex; flex-wrap: wrap; gap: 8px; }
.vc-why { border: 1px solid var(--bordeaux); background: var(--bordeaux-tint); border-radius: 10px;
padding: 15px 18px 8px; }
.vc-why-h { font: 700 10px/1 var(--mono); letter-spacing: .14em; text-transform: uppercase;
color: var(--bordeaux-deep); margin-bottom: 4px; }
.vc-why .pt { display: flex; gap: 10px; padding: 10px 0; border-bottom: 1px solid rgba(143,18,4,.18); }
.vc-why .pt:last-child { border-bottom: none; }
.vc-why .pt .num { color: var(--bordeaux-deep); }
/* ================= VARIANT D — ontology left, output panel right ================= */
.vd-split { display: grid; grid-template-columns: 1.25fr 1fr; gap: 26px; align-items: stretch; flex: 1; min-height: 0; }
.vd-points { margin-top: 4px; }
.vd-points .pt { display: flex; gap: 10px; padding: 7px 0; border-bottom: 1px solid var(--line); }
.vd-points .pt:last-child { border-bottom: none; }
.vd-out { border: 1px solid var(--bordeaux); background: var(--bordeaux-tint); border-radius: 10px;
padding: 16px 18px 10px; display: flex; flex-direction: column; }
.vd-out .pt { display: flex; gap: 10px; padding: 11px 0; border-bottom: 1px solid rgba(143,18,4,.18); }
.vd-out .pt:last-child { border-bottom: none; }
.vd-out .pt .num { color: var(--bordeaux-deep); }
.vd-out .pt b { color: var(--bordeaux-deep); }
.vd-out .pt .d { font: 400 12.5px/1.45 var(--sans); color: var(--ink); }
/* ---- floating switcher (not part of the design) ---- */
.switcher { position: fixed; left: 50%; bottom: 18px; transform: translateX(-50%); z-index: 99;
display: flex; align-items: center; gap: 10px; background: #1d1a1b; color: #fff;
border-radius: 9999px; padding: 8px 10px; box-shadow: 0 6px 22px rgba(0,0,0,.35); }
.switcher button { all: unset; cursor: pointer; width: 30px; height: 30px; border-radius: 9999px;
display: grid; place-items: center; background: rgba(255,255,255,.14); font: 700 14px/1 var(--sans); }
.switcher button:hover { background: rgba(255,255,255,.28); }
.switcher .lbl { font: 700 12px/1 var(--mono); letter-spacing: .06em; min-width: 240px; text-align: center; }
</style>
</head>
<body>
<div class="proto-head">
<b>PROTOTYPE · slide 07 — the clinical ontology</b>
<span>Four layout variants · EN primary (bold dark) over IT gloss (secondary) · A = three columns (your idea) · B = editorial bands · C = ontology + "why order matters" panel · D = ontology + output panel (right)</span>
</div>
<div class="frame">
<header class="head">
<img src="/deck/logo.png" alt="">
<span class="head-org">AritmoLab · Policlinico San Donato</span>
</header>
<nav class="band"><b>Text analysis</b><span class="jump">07 · The clinical ontology</span></nav>
<div class="sbody" id="sbody"></div>
<footer class="foot">
<span>Role of AI in the Analysis of Unstructured Clinical Databases</span>
<span>Dr. Marco Pancotti - MultiPhysixLab</span>
<span>Dr. Sara Paratico - Gruppo San Donato</span>
<span>San Donato Milanese, Milan, Italy · 2–3 October 2026</span>
<span>07 / 14</span>
</footer>
</div>
<div class="switcher">
<button id="prev" aria-label="previous variant">←</button>
<span class="lbl" id="lbl"></span>
<button id="next" aria-label="next variant">→</button>
</div>
<script>
// ---- data (EN-first: EN bold primary, IT gloss secondary) ----
const T1 = [
{ en: 'AF', it: 'FA' },
{ en: 'Brugada', it: '' },
{ en: 'Flutter', it: '' },
{ en: 'AT', it: 'TA' },
{ en: 'AVNRT', it: 'TRN' },
{ en: 'PSVT', it: 'WPW / TPSV' },
{ en: 'VT', it: 'TV' },
{ en: 'VF', it: 'FV' },
{ en: 'Ventricular ectopy', it: 'Extrasistolia V.' },
{ en: 'Syncope', it: 'Sincope' },
{ en: 'Long QT', it: 'QT lungo' },
];
const T2 = [
{ en: 'AV block', it: 'Blocco AV' },
{ en: 'Bundle branch block', it: 'Blocco di branca' },
{ en: 'Cardiomyopathy', it: 'Cardiomiopatia' },
{ en: 'Heart failure', it: 'Scompenso' },
{ en: 'Valvular disease', it: 'Valvulopatia' },
];
const pill = (c, fill) =>
`<span class="pill${fill ? ' fill' : ''}"><span class="en">${c.en}</span>${c.it ? `<span class="it">${c.it}</span>` : ''}</span>`;
const POINTS = [
{ n: '01', b: 'Order encodes clinical precedence', d: '“Brugada” is matched before “TV”, so “substrato per TV” <span style="color:var(--bordeaux-deep)">("substrate for VT")</span> can\'t mask a Brugada pattern' },
{ n: '02', b: 'Synonyms live inline', d: 'FA / f.a. / fib. atriale / atrial fibrillation → one canonical label' },
{ n: '03', b: 'Versioned like software', d: 'semver for the pattern library, <code>pattern_version</code> on every extracted row' },
];
// ---- variant A: three columns of pills (user's idea) ----
function variantA() {
const col = (h, count, items, fill) => `
<div class="va-col">
<div class="col-h">${h}<span class="n">${count}</span></div>
<div class="stack">${items.map(c => pill(c, fill)).join('')}</div>
</div>`;
return `
<div class="va-grid">
${col('Tier 1 · arrhythmic — 1/2', '6 of 11', T1.slice(0, 6), true)}
${col('Tier 1 · arrhythmic — 2/2', '5 of 11', T1.slice(6), true)}
${col('Tier 2 · structural', '5 of 5', T2, false)}
</div>
<div class="va-points">${POINTS.map(p => `
<div class="pt"><span class="num">${p.n}</span><div><b>${p.b}</b> <span class="d">— ${p.d}</span></div></div>`).join('')}
</div>`;
}
// ---- variant B: editorial bands with big numerals ----
function variantB() {
const band = (num, name, count, items, fill) => `
<div class="vb-band">
<div class="vb-num"><b>${num}</b><span>${name} · ${count} categories</span></div>
<div class="vb-chips">${items.map(c => pill(c, fill)).join('')}</div>
</div>`;
return `
${band('01', 'Arrhythmic', 11, T1, true)}
${band('02', 'Structural', 5, T2, false)}
<div class="vb-points">${POINTS.map(p => `
<div class="pt"><span class="num">${p.n}</span><div><b>${p.b}</b> <span class="d">— ${p.d}</span></div></div>`).join('')}
</div>`;
}
// ---- variant C: ontology left, "why the order matters" right ----
function variantC() {
return `
<div class="vc-split">
<div>
<div class="vc-tier">
<div class="col-h">Tier 1 · arrhythmic<span class="n">11</span></div>
<div class="vc-chips">${T1.map(c => pill(c, true)).join('')}</div>
</div>
<div class="vc-tier" style="margin-bottom:0">
<div class="col-h">Tier 2 · structural<span class="n">5</span></div>
<div class="vc-chips">${T2.map(c => pill(c, false)).join('')}</div>
</div>
</div>
<div class="vc-why">
<div class="vc-why-h">Why one clinical order matters</div>
${POINTS.map(p => `
<div class="pt"><span class="num">${p.n}</span><div><b>${p.b}</b> <span class="d">— ${p.d}</span></div></div>`).join('')}
</div>
</div>`;
}
// ---- variant D: ontology left, "the output" right (user request) ----
const OUTPUT = [
{ n: '01', b: 'Quantitative data on the records', d: 'every procedure and implant the text analysis reads is written back as structured, quantitative values on the record that describes it' },
{ n: '02', b: 'Straight into the DWH flow', d: 'these records enter the data-warehouse generation like any other source' },
{ n: '03', b: 'As if typed at the visit', d: 'the data lands exactly as if clinicians had keyed it in themselves during the visits' },
];
function variantD() {
return `
<div class="vd-split">
<div>
<div class="vc-tier">
<div class="col-h">Tier 1 · arrhythmic<span class="n">11</span></div>
<div class="vc-chips">${T1.map(c => pill(c, true)).join('')}</div>
</div>
<div class="vc-tier">
<div class="col-h">Tier 2 · structural<span class="n">5</span></div>
<div class="vc-chips">${T2.map(c => pill(c, false)).join('')}</div>
</div>
<div class="vd-points">${POINTS.map(p => `
<div class="pt"><span class="num">${p.n}</span><div><b>${p.b}</b> <span class="d">— ${p.d}</span></div></div>`).join('')}
</div>
</div>
<div class="vd-out">
<div class="vc-why-h" style="margin-bottom:8px">The output: text becomes recorded data</div>
${OUTPUT.map(p => `
<div class="pt"><span class="num">${p.n}</span><div><b>${p.b}</b><div class="d" style="margin-top:2px">${p.d}</div></div></div>`).join('')}
</div>
</div>`;
}
// ---- switcher ----
const VARIANTS = [
{ key: 'A', name: 'Three columns (5-6 per column)', render: variantA },
{ key: 'B', name: 'Editorial bands, big numerals', render: variantB },
{ key: 'C', name: 'Ontology + why-order panel', render: variantC },
{ key: 'D', name: 'Ontology + output panel (right)', render: variantD },
];
function current() {
const k = (location.search.match(/variant=([ABCD])/) || [])[1] || 'A';
return Math.max(0, VARIANTS.findIndex(v => v.key === k));
}
function show(i) {
const v = VARIANTS[i];
document.getElementById('sbody').innerHTML =
'<div class="vwrap"><div class="kicker">The clinical ontology</div><h2>Two tiers, one clinical order</h2>' + v.render() + '</div>';
document.getElementById('lbl').textContent = `${v.key} · ${v.name}`;
history.replaceState(null, '', location.pathname + '?variant=' + v.key);
}
document.getElementById('prev').onclick = () => show((current() + VARIANTS.length - 1) % VARIANTS.length);
document.getElementById('next').onclick = () => show((current() + 1) % VARIANTS.length);
addEventListener('keydown', e => {
if (e.key === 'ArrowLeft') document.getElementById('prev').click();
if (e.key === 'ArrowRight') document.getElementById('next').click();
});
show(current());
</script>
</body>
</html>
+95 -38
View File
@@ -1,44 +1,101 @@
# Outline v3 (FINAL structure — proposal A) — Role of AI in the Analysis of Unstructured Clinical Databases
# Outline v4 — Role of AI in the Analysis of Unstructured Clinical Databases
Updated 2026-09-18 against the actual deck, rather than the earlier proposed structure.
Deck: [`../deck/index.html`](../deck/index.html) — Reveal.js 5.1.0, **14 slides**, 16:9
(1280 × 720), with local fonts and speaker notes on every slide.
Review status: [`../prototype/deck-overview.html`](../prototype/deck-overview.html).
The titles below are the actual slide headings; some overview labels are shorter or older.
Deck: `deck/index.html` (Reveal.js, 15 slides, fonts embedded).
Presenters: **MP** = Dr. Marco Pancotti · **SP** = Dr.ssa Sara Paratico.
The notes explicitly identify Sara on slides 05–08. The final handoff around slide 04
and the remaining speaker allocation should be confirmed during rehearsal.
Per-slide timings and total running time need a new rehearsal; the previous 15-slide
timing estimate no longer describes this deck.
| # | Slide | Master | By | Time | Status |
|----|------------------------------------------------|--------------|----|------|--------|
| 01 | Title | title | MP | 40s | ✅ final |
| 02 | Where we started (4 systems + 2 missing) | interactive | MP | 65s | ✅ final |
| 03 | The project at a glance (flow + brain popups) | architecture | MP | 75s | ✅ final |
| 04 | What the text miner reads (numbers) | stats | SP | 60s | ✅ real data |
| 05 | The hard problems of clinical NLP | concept | SP | 75s | 🟡 draft |
| 06 | Building the clinical ontology | concept | SP | 75s | 🟡 draft |
| 07 | Trust the text: validation & human review | concept | SP | 75s | 🟡 draft |
| 08 | AI as co-engineer (mapping YAMLs) | artifact | MP | 70s | ✅ real |
| 09 | Genetic data — many sources, one patient | architecture | MP | 60s | ⚠️ wording TBD (a/b) |
| 10 | ThothII: ask the warehouse in plain English | concept | MP | 75s | 🟡 draft |
| 11 | From datamarts to predictive statistics & ML | concept | MP | 60s | 🟡 draft |
| 12 | Live demo cue (star schema · Superset · ThothII)| divider | MP | ~2min live | ✅ |
| 13 | Lessons learned | concept | MP | 60s | 🟡 draft |
| 14 | (merged into 15) | — | — | — | — |
| 15 | Thank you + Q&A (+ Substack pointer) | closing | MP | 25s | 🟡 draft |
## Actual slide sequence
Talk ≈ 13.4 min + ~2 min demo. 15 slides = user cap reached.
| # | Actual slide title | Content / format | Slide review status | Speaker notes |
|---|---|---|---|---|
| 01 | Role of AI in the Analysis of Unstructured Clinical Databases | AritmoLab introduction, clinical data platform and presenters | FINAL | Present; no DRAFT marker |
| 02 | Four islands and four missing pieces | Interactive map: Cardioref, Genetic data, Omics Portal and ECG subsystems; Clinical Intelligence, Datawarehouse, ML-ready data and Professional Portal as missing pieces | FINAL | Present; no DRAFT marker |
| 03 | What we wanted to build | Architecture and data flow toward the patient portal and research warehouse; clickable AI contribution cards | FINAL | Present; no DRAFT marker |
| 04 | The clinical truth lives in unstructured columns | Italian clinical sentence with English translation; why free text needs structured interpretation | FINAL | DRAFT |
| 05 | Deterministic AI, audited numbers | Text-mining outputs: 58,438 letters, 73,389 pathologies, 10,908 tests and 2,307 Brugada patients; extraction pipeline | FINAL | DRAFT — Sara |
| 06 | Reading clinical text is not keyword matching | Clinical NLP challenges, including negation and distinguishing family history from the patient's conditions | FINAL | DRAFT — Sara |
| 07 | Two tiers, one clinical order | Clinical ontology: 11 arrhythmic and 5 structural categories; text becomes recorded data | FINAL | DRAFT — Sara |
| 08 | Trusting the text is a process, not a promise | Five centered detail lenses: four validation controls and the three advantages | REFINED | ~120 words; five hover cues |
| 09 | AritmoLab — a quick tour | Portal tour; currently six screenshot placeholders | DRAFT | DRAFT |
| 10 | The warehouse speaks SQL. Research needs more. | Gap between the warehouse and clinical/research use: SQL access, aggregates and research cohorts | DRAFT | DRAFT |
| 11 | Ask the warehouse in plain English | ThothII: natural-language question, SQL proposal, human review and a research-ready datamart | DRAFT | DRAFT |
| 12 | From question to datamart — the app | ThothII walkthrough; currently six screenshot placeholders | DRAFT | DRAFT |
| 13 | One datamart — many questions answered | Multivariate analysis and machine learning; two chart placeholders with an illustrative-data disclosure | DRAFT | DRAFT — charts still to be created |
| 14 | Thank you | Questions and invitation to future technical walkthroughs on Substack | DRAFT | DRAFT |
## Confirmed decisions
- Project named **AritmoLab** everywhere (internal codename never appears).
- Slide 02 (Where we started): map of legacy systems — Cardioref / Genetic data /
Omics Portal / ECG subsystems, each clickable → popup (usage + problem box);
two dashed "MISSING" cards (Health Intelligence, ML-ready datamarts).
- Slide 03 flow: 3 sources + "Future sources" (dashed) → staging → integration 🧠
→ star schema → datamarts 🧠 → AritmoLab (DWH + Portal); brain popups =
The mappings / Reading the clinical text / Datamarts from plain English.
- Text-analysis block (SP): slides 04–07. Scripts to be validated by SP.
- Speaker view (S): Tall layout customized — upcoming 25%, timer bottom-left,
notes 1.45em. Default: upcoming 20%, notes 80%.
- Fonts embedded (Source Sans 3 + Source Serif 4, OFL) — no Google dependency.
`FINAL` and `REFINED` reproduce the overview's recorded review status. They do not imply
that speaker notes or clinical claims have received a separate final validation.
Figures above describe the content currently displayed in the deck.
## OPEN
- Slide 09 genetics wording: (a) deterministic reconciliation [recommended] vs
(b) AI upstream (only if MP confirms it happens outside the ETL).
- Draft slides 05-07, 10-11, 13-15: review one by one.
- Dark theme of the whole deck: final pass.
- "twenty years" (slide 01 script + slide 02): confirm real data span.
## Current presentation behavior
- Slide 02 has four clickable source systems and four clickable missing pieces,
each opening an explanatory popup.
- Slide 03 has clickable brain icons that open AI contribution cards with a connector
to the selected icon.
- Slide 08 has four centered lens popups. In the presenter console, hovering over
a numbered control or its corresponding heading in the notes opens the detail
in both preview and audience view. Escape closes it. Its quality wording separates
the original procedure-accuracy target from pathology coverage. The ~893
false-positive records and v1.3.1 negation fix are confirmed in the ETL sources;
see [source notes](08-validation-sources.md) for precise scope and limitations.
- Its advantages panel highlights on hover and opens a fifth centered lens on
click, explaining speed, precision and determinism. The fifth speaker-note cue
and presenter control open the same popup.
- Slide changes use a fade transition. Popups fade in and their cards move/scale into
place; clickable source icons and brain buttons have hover effects.
- A dropdown on each slide jumps to another slide. Reveal.js provides keyboard
navigation and the overview.
- The `S` key opens the Reveal.js speaker view when served locally. Speaker scripts
live in each slide's `<aside class="notes">` in `deck/index.html`.
- Source Sans 3 and Source Serif 4 are bundled under `deck/fonts/`; the deck does not
depend on Google Fonts.
## Remaining work
- Review slide 08 for final approval and slides 09–14 individually.
- Replace the six placeholders on slide 09 with real anonymized or demo portal
screenshots. The in-slide authoring note still requests 6–7 screenshots; settle
that count when inserting the images.
- Replace the six placeholders on slide 12 with screenshots of the ThothII workflow.
Its authoring note likewise still requests 6–7 screenshots.
- Create the two charts on slide 13, identify the supporting literature, and preserve
the disclosure if the data remain illustrative rather than measured results.
- Validate the speaker scripts on slides 04–14, including Sara's clinical section
on slides 05–08, and confirm the handoff between presenters.
- Confirm the “twenty years” wording, clinical figures, and quality-gate claims before
presenting them as final facts.
- Confirm the closing Substack reference and whether the promised videos will be
available by the talk.
- Rehearse the actual 14-slide sequence and set timings, including popup explanations
and any live demo chosen in addition to the screenshot walkthroughs.
The previous standalone slides on AI co-engineering, genetic reconciliation, lessons
learned and a live-demo divider are not separate slides in the current sequence.
Their old slide numbers, review tasks and timings should not be reused.
## Local preview
From the repository root:
```bash
python3 -m http.server 8000 --bind 127.0.0.1 --directory presentation
```
- Deck: <http://localhost:8000/deck/index.html>
- Review overview: <http://localhost:8000/prototype/deck-overview.html>
- Presenter console: <http://localhost:8000/presenter/> — notes, interactive preview,
popup controls and a separate audience window for an extended HDMI display.
See [`../presenter/README.md`](../presenter/README.md) for operation and scope.
Keep `presentation/` as the server root: the overview uses absolute `/deck/` URLs.
Stop the server with Ctrl+C.
@@ -0,0 +1,69 @@
# Slide 08: validation sources
Verified on 2026-09-20 against `aritmolab/chirone-etl` on the hospital Gitea,
main snapshot `2ab00106188f298c1a0c1e2c48b3a4dbc66c45b5`. These are source-code
and documentation checks, not a new audit of patient data or a production test run.
## Quality gates
[Extraction specification, SC-003 and SC-004](https://git-aritmolab.policlinicosandonato.it/aritmolab/chirone-etl/src/commit/2ab00106188f298c1a0c1e2c48b3a4dbc66c45b5/include/textanalysis/specs/001-cr-conclusioni-extraction/spec.md)
defines procedure classification accuracy of at least 95% against a random sample
of 100 manually reviewed records. The separate 85% target is coverage: at least one
recognized arrhythmia in that proportion of records, assuming most procedures
concern known arrhythmias. It is not diagnostic accuracy, sensitivity or precision.
The repository search also confirms these definitions in the TextAnalysis README.
The inspected sources do not establish measured results or a completed 100-record
review for every release. The slide therefore calls these figures targets.
## Clinical criteria and cohort selection
[Confirmed-diagnosis mart documentation](https://git-aritmolab.policlinicosandonato.it/aritmolab/chirone-etl/src/commit/2ab00106188f298c1a0c1e2c48b3a4dbc66c45b5/docs/marts/procedure-event-marts/mart_diagnosi_lettera_confermata.md)
identifies the Superset “Diagnosi Lettere” charts (570/571), in the “Test Provocativi
& Diagnosi Lettere” dashboard, as consumers of this dataset.
[The SQL implementation](https://git-aritmolab.policlinicosandonato.it/aritmolab/chirone-etl/src/commit/2ab00106188f298c1a0c1e2c48b3a4dbc66c45b5/dbt/models/marts/mart_diagnosi_lettera_confermata.sql)
first requires patient attribution and a non-negated finding. For Brugada and
long QT syndrome it additionally requires a positive provocative test for that
condition OR an ablation recorded for that pathology, at patient level. It does
not impose that extra condition on other pathologies. These are operational
cohort-selection rules, not a universal clinical diagnostic standard.
No formal sign-off or individual clinical validation of the entire Brugada cohort
was established by the consulted sources. The point is titled “Clinical criteria”:
the queries apply the selection rules and Superset displays their result. The
speaker script explains that a disease mention alone is insufficient for cohort
inclusion; the popup retains the Brugada/long QT rule as a project-specific example.
## Feedback loop: the approximately 893 records
[Known limits and version changelog](https://git-aritmolab.policlinicosandonato.it/aritmolab/chirone-etl/src/commit/2ab00106188f298c1a0c1e2c48b3a4dbc66c45b5/docs/architecture/textanalysis-known-limits.md)
records an audit on 2026-06-11 and the v1.3.1 correction on 2026-06-12. It reports
approximately 893 Brugada false positives in the newer letter format (8.4% of
non-negated Brugada records). This is a record count, not a distinct-patient count.
[Regression tests, class TestNegationExtended](https://git-aritmolab.policlinicosandonato.it/aritmolab/chirone-etl/src/commit/2ab00106188f298c1a0c1e2c48b3a4dbc66c45b5/include/textanalysis/tests/unit/test_letter_cascade_analyzer.py)
explain the defect: the negation patterns recognized “negato” and “negata”, but
omitted “negativo”. The fix adds `negativ[oai]` to both pre- and post-context
negation detection. The test sentence “Test alla flecainide negativo per sindrome
di Brugada” must still yield a Brugada finding, now with `negato=True`; a separate
test preserves the affirmed classification of a positive Brugada diagnosis.
Thus “reclassified” is more accurate than “removed”. These tests were inspected,
not executed in this presentation workspace.
## Retained context and limits
The mart documentation describes propagation of `attribuzione` and `negato`
through integration, warehouse bridge and the mentions mart. The confirmed view
filters them without deleting the underlying mentions dataset.
The known-limits document describes a context window of up to 60 characters on
either side of a match, bounded by a full stop, semicolon or newline. Uncertainty
phrases such as “sospetto di” and “per escludere” are not automatically definitive
negations. The same-pathology deduplication tests show that an affirmed patient
finding takes priority over a negated or family finding. Therefore the slide does
not claim verbatim preservation of every repeated match or complete extraction
of all clinical information.
The mart page's historical “first matching field wins” caveat predates the v1.3.0
multi-field union. For current field handling, the later known-limits document
and unit tests take precedence.