chore: preserve presentation sources and exporter

This commit is contained in:
Codex
2026-08-31 15:35:35 +02:00
parent 11f78c4467
commit 919d408c81
9 changed files with 1211 additions and 0 deletions
+72
View File
@@ -0,0 +1,72 @@
---
name: html-to-pptx
description: Convert the ThothII HTML slide decks in presentations/ into a pixel-faithful PowerPoint (.pptx) and re-export after edits. Use whenever the user mentions pptx, PowerPoint, Keynote import, "esporta in PowerPoint", "converti le slide", or wants the deck in Office format while keeping the exact look and feel of the HTML version.
---
# HTML deck → PPTX (pixel-faithful)
The decks in `presentations/*.html` are the editable source of truth. The PPTX is a
rendered artifact: one full-bleed, high-resolution PNG per slide inside a 13.33×7.5in
(16:9) deck, plus native speaker notes and document metadata.
**Why images instead of native PowerPoint shapes:** the decks depend on web fonts
(Fraunces, Manrope) and hairline CSS details that PowerPoint silently substitutes on
any machine without those fonts installed. Rendering bakes the fonts into pixels, so
the file projects identically everywhere. The tradeoff: slide text is not editable in
PowerPoint. To change a slide, edit the HTML, then re-export here. Speaker notes stay
native and editable.
## Pipeline
Run from the repo root. The scripts resolve `playwright`/`pptxgenjs` from this
skill's own `node_modules`, falling back to `npm -g`:
1. **Render** every slide to PNG (2× ≈ 240dpi) and auto-extract per-slide titles:
```bash
node .agents/skills/html-to-pptx/scripts/render.mjs \
presentations/<deck>.html <tmpdir> --hide ".hud button,.hint"
```
`--hide` takes a comma-separated selector list for browser-only chrome (nav
buttons, keyboard hints) that must not be baked into a static file. The renderer
assumes the deck exposes `.slide` sections navigable via `#N` hash routes (the
ThothII deck does).
2. **Build** the deck:
```bash
node .agents/skills/html-to-pptx/scripts/build.mjs \
<tmpdir> presentations/<deck>.pptx \
--notes presentations/<deck>.notes.json \
--title "..." --author "..." --subject "..."
```
`render.mjs` writes a raw `<tmpdir>/notes.json` (slide titles); keep the refined,
speaker-ready notes in `presentations/<deck>.notes.json` (same shape:
`{"1": "...", "2": "..."}`) and pass it with `--notes`. Notes are in Italian,
2-4 speakable sentences per slide, and are the only editable text in the PPTX.
## QA before handing over
- python-pptx walk: reopen the file, assert slide count equals the PNG count, slide
size equals 12192000×6858000 EMU (13.333×7.5in), and every slide carries exactly
one picture plus (optionally) a notes slide. Report file size.
- Spot-check one PNG at full size: crisp text, no clipping, serif headlines
(Fraunces) and sans body (Manrope) clearly rendered, no browser buttons visible.
## Requirements (first run only)
```bash
npm install --prefix .agents/skills/html-to-pptx
npx --prefix .agents/skills/html-to-pptx playwright install chromium # only if launch fails
```
QA needs `python-pptx` (any venv: `python3 -m venv /tmp/pptxqa && /tmp/pptxqa/bin/pip install python-pptx`).
## When NOT to use this skill
If the user needs natively editable PowerPoint objects (they will rewrite slide text
inside PowerPoint), build the deck with the `document-skills:pptx` skill instead and
accept the font-substitution risk. Do not silently mix the two outputs: pick one,
state the tradeoff, and deliver one file.
+214
View File
@@ -0,0 +1,214 @@
{
"name": "html-to-pptx-skill",
"lockfileVersion": 3,
"requires": true,
"packages": {
"": {
"name": "html-to-pptx-skill",
"dependencies": {
"playwright": "1.58.0",
"pptxgenjs": "^3.12.0"
}
},
"node_modules/@types/node": {
"version": "18.19.130",
"resolved": "https://registry.npmjs.org/@types/node/-/node-18.19.130.tgz",
"integrity": "sha512-GRaXQx6jGfL8sKfaIDD6OupbIHBr9jv7Jnaml9tB7l4v068PAOXqfcujMMo5PhbIs6ggR1XODELqahT2R8v0fg==",
"license": "MIT",
"dependencies": {
"undici-types": "~5.26.4"
}
},
"node_modules/core-util-is": {
"version": "1.0.3",
"resolved": "https://registry.npmjs.org/core-util-is/-/core-util-is-1.0.3.tgz",
"integrity": "sha512-ZQBvi1DcpJ4GDqanjucZ2Hj3wEO5pZDS89BWbkcrvdxksJorwUDDZamX9ldFkp9aw2lmBDLgkObEA4DWNJ9FYQ==",
"license": "MIT"
},
"node_modules/fsevents": {
"version": "2.3.2",
"resolved": "https://registry.npmjs.org/fsevents/-/fsevents-2.3.2.tgz",
"integrity": "sha512-xiqMQR4xAeHTuB9uWm+fFRcIOgKBMiOBP+eXiyT7jsgVCq1bkVygt00oASowB7EdtpOHaaPgKt812P9ab+DDKA==",
"hasInstallScript": true,
"license": "MIT",
"optional": true,
"os": [
"darwin"
],
"engines": {
"node": "^8.16.0 || ^10.6.0 || >=11.0.0"
}
},
"node_modules/https": {
"version": "1.0.0",
"resolved": "https://registry.npmjs.org/https/-/https-1.0.0.tgz",
"integrity": "sha512-4EC57ddXrkaF0x83Oj8sM6SLQHAWXw90Skqu2M4AEWENZ3F02dFJE/GARA8igO79tcgYqGrD7ae4f5L3um2lgg==",
"license": "ISC"
},
"node_modules/image-size": {
"version": "1.2.1",
"resolved": "https://registry.npmjs.org/image-size/-/image-size-1.2.1.tgz",
"integrity": "sha512-rH+46sQJ2dlwfjfhCyNx5thzrv+dtmBIhPHk0zgRUukHzZ/kRueTJXoYYsclBaKcSMBWuGbOFXtioLpzTb5euw==",
"license": "MIT",
"dependencies": {
"queue": "6.0.2"
},
"bin": {
"image-size": "bin/image-size.js"
},
"engines": {
"node": ">=16.x"
}
},
"node_modules/immediate": {
"version": "3.0.6",
"resolved": "https://registry.npmjs.org/immediate/-/immediate-3.0.6.tgz",
"integrity": "sha512-XXOFtyqDjNDAQxVfYxuF7g9Il/IbWmmlQg2MYKOH8ExIT1qg6xc4zyS3HaEEATgs1btfzxq15ciUiY7gjSXRGQ==",
"license": "MIT"
},
"node_modules/inherits": {
"version": "2.0.4",
"resolved": "https://registry.npmjs.org/inherits/-/inherits-2.0.4.tgz",
"integrity": "sha512-k/vGaX4/Yla3WzyMCvTQOXYeIHvqOKtnqBduzTHpzpQZzAskKMhZ2K+EnBiSM9zGSoIFeMpXKxa4dYeZIQqewQ==",
"license": "ISC"
},
"node_modules/isarray": {
"version": "1.0.0",
"resolved": "https://registry.npmjs.org/isarray/-/isarray-1.0.0.tgz",
"integrity": "sha512-VLghIWNM6ELQzo7zwmcg0NmTVyWKYjvIeM83yjp0wRDTmUnrM678fQbcKBo6n2CJEF0szoG//ytg+TKla89ALQ==",
"license": "MIT"
},
"node_modules/jszip": {
"version": "3.10.1",
"resolved": "https://registry.npmjs.org/jszip/-/jszip-3.10.1.tgz",
"integrity": "sha512-xXDvecyTpGLrqFrvkrUSoxxfJI5AH7U8zxxtVclpsUtMCq4JQ290LY8AW5c7Ggnr/Y/oK+bQMbqK2qmtk3pN4g==",
"license": "(MIT OR GPL-3.0-or-later)",
"dependencies": {
"lie": "~3.3.0",
"pako": "~1.0.2",
"readable-stream": "~2.3.6",
"setimmediate": "^1.0.5"
}
},
"node_modules/lie": {
"version": "3.3.0",
"resolved": "https://registry.npmjs.org/lie/-/lie-3.3.0.tgz",
"integrity": "sha512-UaiMJzeWRlEujzAuw5LokY1L5ecNQYZKfmyZ9L7wDHb/p5etKaxXhohBcrw0EYby+G/NA52vRSN4N39dxHAIwQ==",
"license": "MIT",
"dependencies": {
"immediate": "~3.0.5"
}
},
"node_modules/pako": {
"version": "1.0.11",
"resolved": "https://registry.npmjs.org/pako/-/pako-1.0.11.tgz",
"integrity": "sha512-4hLB8Py4zZce5s4yd9XzopqwVv/yGNhV1Bl8NTmCq1763HeK2+EwVTv+leGeL13Dnh2wfbqowVPXCIO0z4taYw==",
"license": "(MIT AND Zlib)"
},
"node_modules/playwright": {
"version": "1.58.0",
"resolved": "https://registry.npmjs.org/playwright/-/playwright-1.58.0.tgz",
"integrity": "sha512-2SVA0sbPktiIY/MCOPX8e86ehA/e+tDNq+e5Y8qjKYti2Z/JG7xnronT/TXTIkKbYGWlCbuucZ6dziEgkoEjQQ==",
"license": "Apache-2.0",
"dependencies": {
"playwright-core": "1.58.0"
},
"bin": {
"playwright": "cli.js"
},
"engines": {
"node": ">=18"
},
"optionalDependencies": {
"fsevents": "2.3.2"
}
},
"node_modules/playwright-core": {
"version": "1.58.0",
"resolved": "https://registry.npmjs.org/playwright-core/-/playwright-core-1.58.0.tgz",
"integrity": "sha512-aaoB1RWrdNi3//rOeKuMiS65UCcgOVljU46At6eFcOFPFHWtd2weHRRow6z/n+Lec0Lvu0k9ZPKJSjPugikirw==",
"license": "Apache-2.0",
"bin": {
"playwright-core": "cli.js"
},
"engines": {
"node": ">=18"
}
},
"node_modules/pptxgenjs": {
"version": "3.12.0",
"resolved": "https://registry.npmjs.org/pptxgenjs/-/pptxgenjs-3.12.0.tgz",
"integrity": "sha512-ZozkYKWb1MoPR4ucw3/aFYlHkVIJxo9czikEclcUVnS4Iw/M+r+TEwdlB3fyAWO9JY1USxJDt0Y0/r15IR/RUA==",
"license": "MIT",
"dependencies": {
"@types/node": "^18.7.3",
"https": "^1.0.0",
"image-size": "^1.0.0",
"jszip": "^3.7.1"
}
},
"node_modules/process-nextick-args": {
"version": "2.0.1",
"resolved": "https://registry.npmjs.org/process-nextick-args/-/process-nextick-args-2.0.1.tgz",
"integrity": "sha512-3ouUOpQhtgrbOa17J7+uxOTpITYWaGP7/AhoR3+A+/1e9skrzelGi/dXzEYyvbxubEF6Wn2ypscTKiKJFFn1ag==",
"license": "MIT"
},
"node_modules/queue": {
"version": "6.0.2",
"resolved": "https://registry.npmjs.org/queue/-/queue-6.0.2.tgz",
"integrity": "sha512-iHZWu+q3IdFZFX36ro/lKBkSvfkztY5Y7HMiPlOUjhupPcG2JMfst2KKEpu5XndviX/3UhFbRngUPNKtgvtZiA==",
"license": "MIT",
"dependencies": {
"inherits": "~2.0.3"
}
},
"node_modules/readable-stream": {
"version": "2.3.8",
"resolved": "https://registry.npmjs.org/readable-stream/-/readable-stream-2.3.8.tgz",
"integrity": "sha512-8p0AUk4XODgIewSi0l8Epjs+EVnWiK7NoDIEGU0HhE7+ZyY8D1IMY7odu5lRrFXGg71L15KG8QrPmum45RTtdA==",
"license": "MIT",
"dependencies": {
"core-util-is": "~1.0.0",
"inherits": "~2.0.3",
"isarray": "~1.0.0",
"process-nextick-args": "~2.0.0",
"safe-buffer": "~5.1.1",
"string_decoder": "~1.1.1",
"util-deprecate": "~1.0.1"
}
},
"node_modules/safe-buffer": {
"version": "5.1.2",
"resolved": "https://registry.npmjs.org/safe-buffer/-/safe-buffer-5.1.2.tgz",
"integrity": "sha512-Gd2UZBJDkXlY7GbJxfsE8/nvKkUEU1G38c1siN6QP6a9PT9MmHB8GnpscSmMJSoF8LOIrt8ud/wPtojys4G6+g==",
"license": "MIT"
},
"node_modules/setimmediate": {
"version": "1.0.5",
"resolved": "https://registry.npmjs.org/setimmediate/-/setimmediate-1.0.5.tgz",
"integrity": "sha512-MATJdZp8sLqDl/68LfQmbP8zKPLQNV6BIZoIgrscFDQ+RsvK/BxeDQOgyxKKoh0y/8h3BqVFnCqQ/gd+reiIXA==",
"license": "MIT"
},
"node_modules/string_decoder": {
"version": "1.1.1",
"resolved": "https://registry.npmjs.org/string_decoder/-/string_decoder-1.1.1.tgz",
"integrity": "sha512-n/ShnvDi6FHbbVfviro+WojiFzv+s8MPMHBczVePfUpDJLwoLT0ht1l4YwBCbi8pJAveEEdnkHyPyTP/mzRfwg==",
"license": "MIT",
"dependencies": {
"safe-buffer": "~5.1.0"
}
},
"node_modules/undici-types": {
"version": "5.26.5",
"resolved": "https://registry.npmjs.org/undici-types/-/undici-types-5.26.5.tgz",
"integrity": "sha512-JlCMO+ehdEIKqlFxk6IfVoAUVmgz7cU7zD/h9XZ0qzeosSHmUJVOzSQvvYSYWXkFXC+IfLKSIffhv0sVZup6pA==",
"license": "MIT"
},
"node_modules/util-deprecate": {
"version": "1.0.2",
"resolved": "https://registry.npmjs.org/util-deprecate/-/util-deprecate-1.0.2.tgz",
"integrity": "sha512-EPD5q1uXyFxJpCrLnCc1nHnq3gOa6DZBocAIiI2TaSCA7VCJ1UJDMagCzIkXNsUYfD1daK//LTEQ8xiIbrHtcw==",
"license": "MIT"
}
}
}
+10
View File
@@ -0,0 +1,10 @@
{
"name": "html-to-pptx-skill",
"private": true,
"type": "module",
"description": "Dependencies for the html-to-pptx skill (render HTML slides via Playwright, assemble PPTX via pptxgenjs).",
"dependencies": {
"playwright": "1.58.0",
"pptxgenjs": "^3.12.0"
}
}
@@ -0,0 +1,49 @@
#!/usr/bin/env node
// Assemble rendered slide PNGs into a 16:9 PPTX: one full-bleed image per slide,
// native speaker notes, document metadata.
//
// Usage:
// node build.mjs <pngdir> <out.pptx> [--notes notes.json] [--title T] [--author A] [--subject S]
import { createRequire } from 'node:module';
import { execSync } from 'node:child_process';
import { readdirSync, readFileSync, existsSync } from 'node:fs';
import path from 'node:path';
const require = createRequire(import.meta.url);
function req(name) {
try { return require(name); } catch {}
const g = execSync('npm root -g').toString().trim();
return require(path.join(g, name));
}
const args = process.argv.slice(2);
const dir = path.resolve(args[0]);
const out = path.resolve(args[1]);
if (!args[0] || !args[1]) {
console.error('usage: node build.mjs <pngdir> <out.pptx> [--notes notes.json] [--title T] [--author A] [--subject S]');
process.exit(1);
}
const opt = (f, d) => { const i = args.indexOf(f); return i >= 0 ? args[i + 1] : d; };
const pptxgen = req('pptxgenjs');
const P = new pptxgen();
P.layout = 'LAYOUT_WIDE'; // 13.333 x 7.5 in
P.title = opt('--title', path.basename(out, '.pptx'));
P.author = opt('--author', 'ThothII');
P.subject = opt('--subject', '');
const notesPath = opt('--notes', path.join(dir, 'notes.json'));
const notes = existsSync(notesPath) ? JSON.parse(readFileSync(notesPath, 'utf8')) : {};
const pngs = readdirSync(dir).filter(f => /^\d+\.png$/.test(f)).sort();
if (!pngs.length) { console.error('no NN.png files in ' + dir); process.exit(1); }
for (const f of pngs) {
const n = parseInt(f, 10);
const s = P.addSlide();
s.background = { color: 'FCFAFA' };
s.addImage({ path: path.join(dir, f), x: 0, y: 0, w: 13.333, h: 7.5 });
if (notes[n]) s.addNotes(notes[n]);
}
await P.writeFile({ fileName: out });
console.log('built', out, '-', pngs.length, 'slides');
@@ -0,0 +1,70 @@
#!/usr/bin/env node
// Render each slide of a ThothII-style HTML deck to a high-res PNG.
// Assumes the deck exposes .slide sections navigable via #N hash routes.
//
// Usage:
// node render.mjs <deck.html> <outdir> [--hide "sel1,sel2"] [--scale 2] [--width 1600] [--height 900]
//
// Output: <outdir>/01.png .. NN.png + <outdir>/notes.json (slide number -> title text)
import { createRequire } from 'node:module';
import { execSync } from 'node:child_process';
import { mkdirSync, writeFileSync } from 'node:fs';
import path from 'node:path';
const require = createRequire(import.meta.url);
function req(name) {
try { return require(name); } catch {}
const g = execSync('npm root -g').toString().trim();
return require(path.join(g, name));
}
const args = process.argv.slice(2);
const html = path.resolve(args[0]);
const outdir = path.resolve(args[1]);
if (!args[0] || !args[1]) {
console.error('usage: node render.mjs <deck.html> <outdir> [--hide "sel1,sel2"] [--scale 2] [--width 1600] [--height 900]');
process.exit(1);
}
const opt = (f, d) => { const i = args.indexOf(f); return i >= 0 ? args[i + 1] : d; };
const hide = (opt('--hide', '') || '').split(',').map(s => s.trim()).filter(Boolean);
const scale = Number(opt('--scale', '2'));
const width = Number(opt('--width', '1600'));
const height = Number(opt('--height', '900'));
const { chromium } = req('playwright');
mkdirSync(outdir, { recursive: true });
const browser = await chromium.launch();
const page = await browser.newPage({
viewport: { width, height },
deviceScaleFactor: scale,
reducedMotion: 'reduce',
});
await page.goto('file://' + html, { waitUntil: 'networkidle' });
await page.evaluate(() => document.fonts.ready);
if (hide.length) {
await page.addStyleTag({ content: hide.map(s => `${s}{display:none!important}`).join('\n') });
}
const count = await page.locator('.slide').count();
const notes = {};
for (let i = 1; i <= count; i++) {
await page.evaluate(n => { location.hash = '#' + n; }, i);
await page.waitForFunction(n => {
const s = document.querySelectorAll('.slide')[n - 1];
return !!s && s.classList.contains('on');
}, i);
await page.waitForTimeout(450); // let the crossfade settle
const file = path.join(outdir, String(i).padStart(2, '0') + '.png');
await page.screenshot({ path: file });
notes[i] = await page.evaluate(n => {
const s = document.querySelectorAll('.slide')[n - 1];
const t = s.querySelector('.title, .cover-mark');
const txt = t ? t.innerText.trim().replace(/\s+/g, ' ') : '';
return txt || s.getAttribute('aria-label') || '';
}, i);
console.log('rendered', file);
}
await browser.close();
writeFileSync(path.join(outdir, 'notes.json'), JSON.stringify(notes, null, 2));
console.log('done:', count, 'slides ->', outdir);
View File
+784
View File
@@ -0,0 +1,784 @@
<!doctype html>
<html lang="it">
<head>
<meta charset="utf-8">
<meta name="viewport" content="width=device-width, initial-scale=1">
<title>ThothII · Dal quesito clinico al dato tracciabile</title>
<meta name="description" content="Presentazione congressuale: come ThothII trasforma una domanda clinica in linguaggio naturale in SQL verificato, con evidenza citabile e un revisore umano a ogni decisione.">
<link rel="preconnect" href="https://fonts.googleapis.com">
<link rel="preconnect" href="https://fonts.gstatic.com" crossorigin>
<link href="https://fonts.googleapis.com/css2?family=Fraunces:ital,opsz,wght@0,9..144,400..700;1,9..144,400..700&family=Manrope:wght@400..800&display=swap" rel="stylesheet">
<style>
/* ============================================================
ThothII · "The Clinical Workbench"
Palette derivata dai token OKLCH di DESIGN.md (porcellana calda,
grafite calda, rosso strumento scarso: mai più del ~10% della slide).
Tre registri tipografici: serif = autorità, sans = lettura,
mono = identità macchina (artefatti, metadati, SQL).
============================================================ */
:root{
--bg:#fcfafa; /* porcelain background */
--card:#fefefe; /* porcelain card */
--surface:#f7f5f5; /* warm surface */
--sunken:#f0edec; /* sunken surface */
--ink:#3e3538; /* warm graphite */
--muted:#7e7176; /* muted graphite */
--line:#e5e0e1; /* quiet border */
--red:#cb333b; /* instrument red */
--red-deep:#b52b33; /* instrument red pressed */
--mint:#3fae85; /* success (scuro abbastanza per il testo) */
--amber:#a97b1f; /* warning (versione leggibile) */
--info:#3f7fae; /* information */
--shadow-sm:0 1px 2px rgba(62,53,56,.05),0 2px 10px rgba(62,53,56,.05);
--shadow-md:0 2px 6px rgba(62,53,56,.07),0 14px 36px rgba(62,53,56,.09);
--ease:cubic-bezier(.22,1,.36,1);
--serif:"Fraunces","Source Serif Pro",Georgia,"Times New Roman",serif;
--sans:"Manrope",-apple-system,BlinkMacSystemFont,"Segoe UI",system-ui,Arial,sans-serif;
--mono:ui-monospace,"SF Mono","Cascadia Code",Menlo,Consolas,monospace;
color-scheme:light;
}
*{box-sizing:border-box;margin:0;padding:0}
html,body{height:100%}
body{
background:var(--bg);color:var(--ink);
font-family:var(--sans);font-size:16px;line-height:1.6;
overflow:hidden;-webkit-font-smoothing:antialiased;font-kerning:normal;
}
::selection{background:rgba(203,51,59,.16)}
/* ---------- deck meccanica ---------- */
.deck{position:fixed;inset:0}
.slide{
position:absolute;inset:0;background:var(--bg);
opacity:0;visibility:hidden;pointer-events:none;
transition:opacity .24s var(--ease),visibility 0s linear .24s;
overflow:hidden;
}
.slide.on{opacity:1;visibility:visible;pointer-events:auto;transition:opacity .24s var(--ease)}
.frame{
height:100%;max-width:1300px;margin-inline:auto;
display:flex;flex-direction:column;
padding:clamp(34px,6.2vh,64px) clamp(28px,5vw,88px) clamp(56px,9vh,84px);
}
/* reveal scaglionato: entra con translate+fade, esce coperto dal fade della slide */
.rv{opacity:0;transform:translateY(14px);
transition:opacity .5s var(--ease),transform .5s var(--ease);
transition-delay:calc(var(--d,0)*70ms)}
.slide.on .rv{opacity:1;transform:none}
/* ---------- tipografia ---------- */
.kicker{
display:flex;align-items:center;gap:14px;
font-family:var(--mono);font-size:11.5px;font-weight:600;
letter-spacing:.09em;text-transform:uppercase;color:var(--muted);
margin-bottom:clamp(20px,3.6vh,40px);
}
.kicker .n{color:var(--red);font-variant-numeric:tabular-nums}
.ledger{position:relative;flex:1;height:1px;background:var(--line)}
.ledger::before{content:"";position:absolute;left:0;top:-2.5px;width:6px;height:6px;
border-radius:50%;background:var(--red)}
h1,h2{text-wrap:balance;font-optical-sizing:auto}
.title{
font-family:var(--serif);font-weight:600;
font-size:clamp(30px,4.1vw,56px);line-height:1.08;letter-spacing:-.015em;
max-width:22ch;
}
.title em{font-style:italic;font-weight:600}
.title .acc{color:var(--red)}
.lede{
font-size:clamp(16px,1.4vw,21px);line-height:1.55;color:var(--ink);
max-width:58ch;margin-top:clamp(14px,2.4vh,26px);
}
.lede .dim{color:var(--muted)}
p{max-width:62ch}
.small{font-size:13.5px;color:var(--muted)}
.mono{font-family:var(--mono)}
.chip{
display:inline-block;font-family:var(--mono);font-size:12.5px;font-weight:600;
color:var(--ink);background:var(--sunken);border:1px solid var(--line);
border-radius:6px;padding:3px 9px;white-space:nowrap;
}
.chip.red{color:var(--red-deep);background:rgba(203,51,59,.07);border-color:rgba(203,51,59,.22)}
/* ---------- componenti ricorrenti ---------- */
.rule-list{display:flex;flex-direction:column}
.rule-row{
display:grid;grid-template-columns:86px 1fr;gap:clamp(14px,2vw,28px);
padding:clamp(14px,2.4vh,24px) 0;border-top:1px solid var(--line);
}
.rule-row:last-child{border-bottom:1px solid var(--line)}
.num{
font-family:var(--serif);font-weight:600;font-style:italic;
font-size:clamp(26px,2.6vw,38px);line-height:1;color:var(--muted);
font-variant-numeric:tabular-nums;
}
.rule-row h3{font-size:clamp(15.5px,1.25vw,19px);font-weight:700;line-height:1.35}
.rule-row p{font-size:clamp(13.5px,1.05vw,15.5px);color:var(--muted);margin-top:5px}
.chain{display:flex;align-items:center;flex-wrap:wrap;gap:10px;
font-family:var(--mono);font-size:12.5px;font-weight:600}
.chain .node{background:var(--card);border:1px solid var(--line);border-radius:8px;
padding:7px 13px;box-shadow:var(--shadow-sm)}
.chain .node.last{border-color:rgba(203,51,59,.35);color:var(--red-deep)}
.chain .arr{color:var(--muted)}
/* centratura verticale del blocco contenuto nelle slide che non la ereditano */
.s-workflow .phases{margin-top:auto}
.s-workflow .callout{margin-bottom:auto}
.s-dialog .gates{margin-top:auto}
.s-dialog .truth{margin-bottom:auto}
/* barra HUD */
.hud{
position:fixed;left:0;right:0;bottom:0;z-index:10;
display:flex;align-items:center;gap:18px;
padding:14px clamp(28px,5vw,88px) 16px;pointer-events:none;
}
.hud .brand{font-family:var(--mono);font-size:10.5px;font-weight:600;
letter-spacing:.12em;text-transform:uppercase;color:var(--muted)}
.hud .brand b{color:var(--red);font-weight:600}
.hud .count{font-family:var(--mono);font-size:11px;color:var(--muted);
font-variant-numeric:tabular-nums;letter-spacing:.08em}
.hud .track{flex:1;height:2px;background:var(--line);border-radius:2px;overflow:hidden}
.hud .fill{height:100%;width:0;background:var(--red);border-radius:2px;
transition:width .28s var(--ease)}
.hud button{
pointer-events:auto;cursor:pointer;width:34px;height:34px;border-radius:8px;
border:1px solid var(--line);background:var(--card);color:var(--muted);
display:grid;place-items:center;box-shadow:var(--shadow-sm);
transition:background .14s var(--ease),color .14s var(--ease),transform .14s var(--ease);
}
.hud button:hover{background:var(--surface);color:var(--ink)}
.hud button:active{transform:scale(.96)}
.hud button:focus-visible{outline:3px solid rgba(203,51,59,.35);outline-offset:1px}
.hud button:disabled{opacity:.4;cursor:default}
/* ============================================================
SLIDE 1 · copertina
============================================================ */
.s-cover .frame{justify-content:center}
.cover-mark{font-family:var(--serif);font-weight:600;letter-spacing:-.025em;
line-height:.95;font-size:clamp(64px,11vw,148px)}
.cover-mark .ii{color:var(--red)}
.cover-sub{
font-family:var(--serif);font-style:italic;font-weight:400;
font-size:clamp(19px,2.1vw,29px);line-height:1.3;max-width:34ch;
margin-top:clamp(18px,3vh,30px);color:var(--ink);
}
.cover-meta{display:flex;align-items:center;gap:16px;flex-wrap:wrap;
margin-top:clamp(26px,5vh,48px);
font-family:var(--mono);font-size:11.5px;font-weight:600;
letter-spacing:.09em;text-transform:uppercase;color:var(--muted)}
.cover-meta .dot{width:4px;height:4px;border-radius:50%;background:var(--line)}
.hint{margin-top:clamp(22px,4.4vh,44px);font-family:var(--mono);font-size:11px;
color:var(--muted);letter-spacing:.06em}
/* ============================================================
SLIDE 2 · contesto: griglia asimmetrica 5/7
============================================================ */
.cols{display:grid;grid-template-columns:minmax(0,5fr) minmax(0,7fr);
gap:clamp(30px,5vw,84px);align-items:start;flex:1;min-height:0;
align-content:center}
.s-context .lede{margin-top:18px}
.psychip{display:inline-flex;align-items:center;gap:8px;margin-top:clamp(20px,4vh,36px);
border:1px solid var(--line);background:var(--card);border-radius:8px;
padding:9px 14px;box-shadow:var(--shadow-sm);
font-family:var(--mono);font-size:12px;font-weight:600;color:var(--muted)}
.psychip i{width:7px;height:7px;border-radius:50%;background:var(--red);flex:none}
/* ============================================================
SLIDE 3 · principio
============================================================ */
.s-principle .frame{justify-content:center}
.s-principle .title{font-size:clamp(36px,5vw,68px);max-width:16ch}
.archi{margin-top:clamp(28px,5vh,52px)}
.archi-cap{font-family:var(--mono);font-size:11px;font-weight:600;
letter-spacing:.09em;text-transform:uppercase;color:var(--muted);margin-bottom:14px}
.archi-note{margin-top:16px;font-size:14px;color:var(--muted);max-width:66ch}
/* ============================================================
SLIDE 4 · workflow 8 fasi
============================================================ */
.phases{display:grid;grid-template-columns:repeat(4,1fr);
gap:0 clamp(20px,3vw,44px);margin-top:clamp(8px,2vh,18px)}
.phase{padding:clamp(12px,2vh,20px) 0;border-top:1px solid var(--line);
display:flex;flex-direction:column;gap:6px;min-width:0}
.phase .pn{font-family:var(--serif);font-style:italic;font-weight:600;
font-size:clamp(24px,2.4vw,34px);line-height:1;color:var(--muted);
font-variant-numeric:tabular-nums}
.phase.hot .pn{color:var(--red)}
.phase h3{font-size:clamp(14px,1.15vw,17px);font-weight:700;line-height:1.25}
.phase p{font-size:12.5px;line-height:1.45;color:var(--muted)}
.callout{
margin-top:clamp(20px,4vh,40px);border-radius:12px;
background:var(--surface);border:1px solid var(--line);
padding:clamp(16px,2.6vh,24px) clamp(18px,2.4vw,28px);
display:flex;gap:clamp(16px,2vw,28px);align-items:flex-start;
}
.callout .mark{font-family:var(--serif);font-style:italic;font-weight:600;
font-size:clamp(28px,3vw,44px);line-height:1;color:var(--red);flex:none}
.callout h3{font-size:clamp(15px,1.25vw,18px);font-weight:700;line-height:1.4}
.callout p{font-size:13.5px;color:var(--muted);margin-top:4px}
/* ============================================================
SLIDE 5 · disambiguazione: widget reale
============================================================ */
.widget{
background:var(--card);border:1px solid var(--line);border-radius:12px;
box-shadow:var(--shadow-md);padding:clamp(16px,2.2vh,22px) clamp(16px,1.8vw,22px);
font-size:13px;
}
.widget-head{display:flex;align-items:center;justify-content:space-between;gap:10px;
margin-bottom:12px}
.widget-head .tag{font-family:var(--mono);font-size:10.5px;font-weight:600;
letter-spacing:.1em;color:var(--muted)}
.wait{display:inline-flex;align-items:center;gap:6px;font-family:var(--mono);
font-size:10.5px;font-weight:600;letter-spacing:.05em;color:var(--amber)}
.wait i{width:6px;height:6px;border-radius:50%;background:#e9bd66;flex:none}
.widget .q{font-family:var(--serif);font-weight:600;font-size:15.5px;line-height:1.3;
margin-bottom:12px}
.opt{display:flex;gap:11px;padding:11px 12px;border:1px solid var(--line);
border-radius:8px;background:var(--bg)}
.opt + .opt{margin-top:8px}
.opt .radio{width:15px;height:15px;border-radius:50%;border:1.5px solid var(--line);
flex:none;margin-top:2px;position:relative}
.opt.rec .radio::after{content:"";position:absolute;inset:3px;border-radius:50%;
background:var(--red)}
.opt .radio.red{border-color:rgba(203,51,59,.5)}
.opt b{display:block;font-size:13px;line-height:1.35}
.opt code{display:block;margin-top:4px;font-family:var(--mono);font-size:11px;
color:var(--muted)}
.opt .badge{float:right;margin-left:10px;background:var(--red);color:var(--bg);
font-family:var(--mono);font-size:9.5px;font-weight:600;letter-spacing:.06em;
text-transform:uppercase;border-radius:6px;padding:2px 7px}
.wfoot{display:flex;gap:8px;margin-top:12px;flex-wrap:wrap}
.wfoot span{font-family:var(--mono);font-size:11px;color:var(--muted);
border:1px solid var(--line);border-radius:6px;padding:3px 9px;background:var(--bg)}
/* ============================================================
SLIDE 6 · evidence
============================================================ */
.flow{display:flex;align-items:stretch;gap:0;flex-wrap:wrap;
margin:clamp(14px,2.6vh,26px) 0 0}
.flow .step{flex:1 1 0;min-width:118px;border-top:1px solid var(--line);
padding:12px 14px 0 0;position:relative}
.flow .step + .step{padding-left:14px}
.flow .step::before{content:"";position:absolute;left:0;top:-3.5px;width:6px;height:6px;
border-radius:50%;background:var(--muted)}
.flow .step:first-child::before{background:var(--red)}
.flow b{display:block;font-size:13px;font-weight:700;line-height:1.3}
.flow span{display:block;font-size:11.5px;color:var(--muted);line-height:1.4;margin-top:3px}
.quote{
border-left:none;background:var(--surface);border:1px solid var(--line);
border-radius:12px;padding:clamp(14px,2.2vh,20px) clamp(16px,1.8vw,22px);
}
.quote .qt{font-family:var(--serif);font-style:italic;font-weight:400;
font-size:clamp(15px,1.35vw,18.5px);line-height:1.45}
.quote .src{font-family:var(--mono);font-size:11px;color:var(--muted);margin-top:10px;
letter-spacing:.04em}
.statrow{display:flex;gap:clamp(22px,3.5vw,54px);flex-wrap:wrap;
margin-top:clamp(16px,3vh,30px);align-items:baseline}
.stat .v{font-family:var(--serif);font-weight:600;line-height:1;
font-size:clamp(26px,2.8vw,40px);font-variant-numeric:tabular-nums}
.stat .v small{font-size:.55em;color:var(--muted);font-weight:600}
.stat .l{font-family:var(--mono);font-size:10.5px;font-weight:600;
letter-spacing:.08em;text-transform:uppercase;color:var(--muted);margin-top:6px}
/* ============================================================
SLIDE 7 · metadati DWH: pila stratificata
============================================================ */
.stack{display:flex;flex-direction:column;gap:10px}
.layer{border:1px solid var(--line);border-radius:10px;background:var(--card);
padding:13px 16px;display:grid;grid-template-columns:24px 1fr;gap:12px;
align-items:baseline;box-shadow:var(--shadow-sm)}
.layer .ld{width:7px;height:7px;border-radius:50%;background:var(--muted);
position:relative;top:-1px}
.layer.src .ld{background:var(--red)}
.layer b{font-size:14px;font-weight:700;display:block}
.layer b .mono{font-size:12px;color:var(--muted);font-weight:600}
.layer span{display:block;font-size:12.5px;color:var(--muted);line-height:1.45;margin-top:2px}
.stack .link{align-self:center;color:var(--muted);font-family:var(--mono);
font-size:11px;line-height:1;padding-left:8px}
/* ============================================================
SLIDE 8 · dialogo umano-AI
============================================================ */
.gates{display:flex;flex-direction:column;border-top:1px solid var(--line)}
.gate{display:grid;grid-template-columns:200px 1fr;gap:clamp(14px,2.4vw,32px);
padding:clamp(12px,2vh,19px) 0;border-bottom:1px solid var(--line);align-items:baseline}
.gate .gname{font-family:var(--mono);font-size:13px;font-weight:600;color:var(--red-deep)}
.gate .gname span{display:block;color:var(--muted);font-weight:400;font-size:11px;
margin-top:3px;letter-spacing:.04em}
.gate p{font-size:clamp(13.5px,1.1vw,15.5px);color:var(--ink);max-width:66ch}
.truth{
margin-top:clamp(18px,3.4vh,34px);display:grid;
grid-template-columns:auto 1fr;gap:clamp(16px,2.4vw,30px);align-items:start;
}
.truth .tmark{font-family:var(--serif);font-style:italic;font-weight:600;
font-size:clamp(28px,3vw,44px);line-height:1;color:var(--red)}
.truth h3{font-family:var(--serif);font-style:italic;font-weight:600;
font-size:clamp(19px,1.9vw,27px);line-height:1.25;letter-spacing:-.01em}
.truth p{font-size:13.5px;color:var(--muted);margin-top:8px;max-width:74ch}
/* ============================================================
SLIDE 9 · caso reale
============================================================ */
.trail{display:flex;flex-direction:column;position:relative;padding-left:19px}
.trail::before{content:"";position:absolute;left:5px;top:8px;bottom:8px;width:1px;
background:var(--line)}
.trail .tstep{position:relative;padding:7px 0 7px 6px;
font-family:var(--mono);font-size:12.5px;font-weight:600}
.trail .tstep::before{content:"";position:absolute;left:-18px;top:13px;width:7px;height:7px;
border-radius:50%;background:var(--card);border:1.5px solid var(--muted)}
.trail .tstep:first-child::before{border-color:var(--red);background:var(--red)}
.trail .tstep span{color:var(--muted);font-weight:400}
.outcome{display:flex;align-items:baseline;gap:12px;margin-top:clamp(16px,3vh,28px)}
.outcome .big{font-family:var(--serif);font-weight:600;font-size:clamp(44px,5vw,72px);
line-height:1;color:var(--red);font-variant-numeric:tabular-nums}
.outcome .lbl{font-size:14px;color:var(--muted);max-width:26ch;line-height:1.45}
.guar{display:flex;flex-direction:column}
.guar .g{display:grid;grid-template-columns:20px 1fr;gap:12px;
padding:clamp(11px,1.9vh,17px) 0;border-top:1px solid var(--line);align-items:baseline}
.guar .g:last-child{border-bottom:1px solid var(--line)}
.guar i{width:7px;height:7px;border-radius:50%;background:var(--mint);
position:relative;top:-1px;left:6px}
.guar b{font-size:14px;font-weight:700;display:block}
.guar span{font-size:12.5px;color:var(--muted);display:block;margin-top:2px;line-height:1.45}
/* ============================================================
SLIDE 10 · chiusura
============================================================ */
.s-end .frame{justify-content:center}
.s-end .title{font-size:clamp(40px,5.6vw,76px);max-width:14ch}
.principles{margin-top:clamp(24px,4.5vh,44px);max-width:860px}
.principles .rule-row{grid-template-columns:64px 1fr}
.principles h3{font-family:var(--serif);font-weight:600;font-style:italic;
font-size:clamp(16.5px,1.5vw,21px)}
.endmeta{margin-top:clamp(24px,4.5vh,42px);display:flex;align-items:center;gap:16px;
flex-wrap:wrap;font-family:var(--mono);font-size:11px;font-weight:600;
letter-spacing:.09em;text-transform:uppercase;color:var(--muted)}
/* ---------- ridotto movimento: crossfade, zero spostamenti ---------- */
@media (prefers-reduced-motion:reduce){
.rv{transform:none;transition:opacity .2s linear;transition-delay:0ms}
.slide{transition:opacity .2s linear}
.hud .fill{transition:none}
}
/* ---------- stampato: una slide per pagina ---------- */
@media print{
body{overflow:visible}
.deck{position:static}
.slide{position:relative;opacity:1;visibility:visible;page-break-after:always;
height:100vh}
.hud{display:none}
}
</style>
</head>
<body>
<main class="deck" id="deck">
<!-- ══════════════ 1 · COPERTINA ══════════════ -->
<section class="slide s-cover" aria-label="Copertina">
<div class="frame">
<div class="kicker rv" style="--d:0"><span>Congresso · Informatica clinica</span><span class="ledger"></span></div>
<div class="cover-mark rv" style="--d:1">Thoth<span class="ii">II</span></div>
<p class="cover-sub rv" style="--d:2">L'intelligenza artificiale propone. Il medico decide.
Ogni risposta è tracciata fino all'evidenza.</p>
<div class="cover-meta rv" style="--d:3">
<span>Dal linguaggio naturale all'SQL</span><span class="dot"></span>
<span>Evidence curata</span><span class="dot"></span>
<span>Umano nel ciclo</span>
</div>
<p class="hint rv" style="--d:4">Usa le frecce ← → per navigare</p>
</div>
</section>
<!-- ══════════════ 2 · IL CONTESTO ══════════════ -->
<section class="slide s-context" aria-label="Il contesto">
<div class="frame">
<div class="kicker rv" style="--d:0"><span class="n">02</span><span>Il contesto</span><span class="ledger"></span></div>
<div class="cols">
<div>
<h1 class="title rv" style="--d:1">Il dato c'è. La domanda è fatta di parole.</h1>
<p class="lede rv" style="--d:2">I dati clinici vivono in un data warehouse, ma li
interroga solo chi scrive SQL. Ogni risposta ad hoc consuma il tempo di competenze
rare e non lascia traccia di come è stata costruita.</p>
<div class="psychip rv" style="--d:3"><i></i>DWH clinico · Policlinico San Donato · aritmologia</div>
</div>
<div class="rule-list rv" style="--d:2">
<div class="rule-row">
<span class="num">01</span>
<div><h3>Il linguaggio è ambiguo</h3>
<p>"Ricoveri nel 2023": anno solare o fiscale? Pazienti attivi o tutti?
Ogni parola nasconde una scelta clinica prima che tecnica.</p></div>
</div>
<div class="rule-row">
<span class="num">02</span>
<div><h3>Il significato è locale</h3>
<p>Le definizioni appartengono all'ospedale, non al dizionario:
cosa intende esattamente "fascia pediatrica" in questo dato?</p></div>
</div>
<div class="rule-row">
<span class="num">03</span>
<div><h3>Un numero senza percorso non è evidenza</h3>
<p>Una risposta che non si può verificare, ripetere e citare
non entra in una decisione clinica né in una ricerca.</p></div>
</div>
</div>
</div>
</div>
</section>
<!-- ══════════════ 3 · IL PRINCIPIO ══════════════ -->
<section class="slide s-principle" aria-label="Il principio">
<div class="frame">
<div class="kicker rv" style="--d:0"><span class="n">03</span><span>Il principio</span><span class="ledger"></span></div>
<h1 class="title rv" style="--d:1">Il modello propone.<br>Il medico <em class="acc">decide</em>.</h1>
<p class="lede rv" style="--d:2">ThothII è un banco di lavoro clinico: trasforma una domanda
in linguaggio naturale in SQL verificato, fase dopo fase, con un revisore umano a ogni
passaggio decisivo. Il modello non ha mai l'ultima parola, e non può prendersela:
persino l'avanzamento di fase è un atto umano registrato.</p>
<div class="archi rv" style="--d:3">
<div class="archi-cap">La catena, in un rigo</div>
<div class="chain">
<span class="node">Browser</span><span class="arr">→</span>
<span class="node">Backend</span><span class="arr">→</span>
<span class="node">Pi · agente</span><span class="arr">→</span>
<span class="node">tht · workflow</span><span class="arr">→</span>
<span class="node last">DWH in sola lettura</span>
</div>
<p class="archi-note">Nessuna scrittura sul magazzino dati: ThothII legge, propone e
documenta. Il risultato approvato può diventare un datamart, ma la fonte resta intoccata.</p>
</div>
</div>
</section>
<!-- ══════════════ 4 · IL WORKFLOW ══════════════ -->
<section class="slide s-workflow" aria-label="Il workflow in otto fasi">
<div class="frame">
<div class="kicker rv" style="--d:0"><span class="n">04</span><span>Il workflow</span><span class="ledger"></span></div>
<h1 class="title rv" style="--d:1">Otto fasi, uno stato che si calcola.</h1>
<div class="phases rv" style="--d:2">
<div class="phase hot"><span class="pn">1</span><h3>Chiarimento</h3>
<p>il significato della domanda, un'ambiguità alla volta</p></div>
<div class="phase"><span class="pn">2</span><h3>Memoria</h3>
<p>riuso della conoscenza già approvata in passato</p></div>
<div class="phase"><span class="pn">3</span><h3>Riscrittura</h3>
<p>la domanda diventa esplicita e non ambigua</p></div>
<div class="phase"><span class="pn">4</span><h3>Schema linking</h3>
<p>tabelle, colonne e join scelti sul catalogo</p></div>
<div class="phase"><span class="pn">5</span><h3>Sintesi</h3>
<p>il piano di calcolo, definito prima del codice</p></div>
<div class="phase"><span class="pn">6</span><h3>CTE</h3>
<p>ogni blocco viene testato sul dato reale</p></div>
<div class="phase"><span class="pn">7</span><h3>SQL finale</h3>
<p>approvato dal revisore, poi eseguito</p></div>
<div class="phase"><span class="pn">8</span><h3>Datamart</h3>
<p>export opzionale e promozione in memoria</p></div>
</div>
<div class="callout rv" style="--d:3">
<span class="mark">§</span>
<div>
<h3>La fase corrente non è mai memorizzata: è calcolata.</h3>
<p>Ripiegando il registro delle decisioni (<span class="chip">review_decisions.jsonl</span>)
il sistema ricava sempre lo stato vero. Riaprire una fase ricalcola tutto ciò che
viene dopo: le approvazioni diventate obsolete non contano più.</p>
</div>
</div>
</div>
</section>
<!-- ══════════════ 5 · DISAMBIGUAZIONE ══════════════ -->
<section class="slide s-disamb" aria-label="Disambiguazione">
<div class="frame">
<div class="kicker rv" style="--d:0"><span class="n">05</span><span>Disambiguazione</span><span class="ledger"></span></div>
<h1 class="title rv" style="--d:1">Una ambiguità alla volta,<br>con le conseguenze in vista.</h1>
<div class="cols" style="margin-top:clamp(18px,3.4vh,34px)">
<div>
<p class="lede rv" style="--d:2;margin-top:0">Ogni proposta di interpretazione
porta con sé tutto il necessario per decidere: il significato attribuito alle parole,
le tabelle coinvolte, il filtro che ne deriva sul dato, l'evidenza che la sostiene
e il rischio residuo.</p>
<p class="lede rv" style="--d:3;margin-top:14px"><span class="dim">La scelta
consigliata non è mai pre-confermata. E esistono sempre le vie di uscita:
</span>Altro, Torna indietro, Esci.</p>
<p class="lede rv" style="--d:4;margin-top:14px">Ogni risposta diventa una
<span class="chip">concept_clarified</span> nel registro: la disambiguazione
non è una chiacchierata, è un atto documentato.</p>
</div>
<div class="widget rv" style="--d:3" role="img" aria-label="Esempio di widget di selezione per la disambiguazione di una domanda clinica">
<div class="widget-head">
<span class="tag">REVIEWER_SELECT · F1 CHIARIMENTO</span>
<span class="wait"><i></i>In attesa di revisione</span>
</div>
<div class="q">"Pazienti con ricoveri nell'anno 2023": come va inteso "anno"?</div>
<div class="opt rec">
<span class="radio"></span>
<div style="flex:1;min-width:0">
<b><span class="badge">Consigliata</span>Anno solare</b>
<code>data_ricovero BETWEEN '2023-01-01' AND '2023-12-31'</code>
<code>ricoveri: REPARTI ⋈ RICOVERI · evidenza: calendario PSD</code>
</div>
</div>
<div class="opt">
<span class="radio"></span>
<div style="flex:1;min-width:0">
<b>Anno fiscale</b>
<code>periodo amministrativo definito dalla direzione sanitaria</code>
<code>richiede conferma del periodo esatto prima di procedere</code>
</div>
</div>
<div class="wfoot"><span>Altro</span><span>Torna indietro</span><span>Esci</span></div>
</div>
</div>
</div>
</section>
<!-- ══════════════ 6 · EVIDENCE ══════════════ -->
<section class="slide s-evidence" aria-label="Gestione delle evidence">
<div class="frame">
<div class="kicker rv" style="--d:0"><span class="n">06</span><span>Evidence</span><span class="ledger"></span></div>
<h1 class="title rv" style="--d:1">La conoscenza clinica, curata e citabile.</h1>
<div class="flow rv" style="--d:2">
<div class="step"><b>Fonte</b><span>documento originale, immutato</span></div>
<div class="step"><b>Unità atomiche</b><span>una regola o definizione per unità</span></div>
<div class="step"><b>Manifest</b><span>unità, fonte e impronta sha256</span></div>
<div class="step"><b>Validazione</b><span>controlli che falliscono in chiuso</span></div>
<div class="step"><b>Indicizzazione</b><span>BM25 + vettori densi</span></div>
<div class="step"><b>Generazioni</b><span>immutabili, con puntatore ACTIVE</span></div>
</div>
<div class="cols" style="grid-template-columns:minmax(0,7fr) minmax(0,5fr);margin-top:clamp(16px,3vh,30px)">
<div class="rv" style="--d:3">
<div class="quote">
<p class="qt">"La fascia pediatrica comprende i pazienti con età inferiore a 18 anni."</p>
<p class="src">unità di evidence · fonte tracciata · sha256 verificato</p>
</div>
<p class="lede" style="margin-top:18px;font-size:15px"><span class="dim">Ciò che il modello
recupera sono </span>candidati, non fatti<span class="dim">: i valori vanno verificati
sul dato reale, e ogni uso di un'evidence va citato con la sua provenienza. Niente
entra nella base di conoscenza senza un curatore umano.</span></p>
</div>
<div class="rv" style="--d:4">
<div class="statrow" style="margin-top:0;flex-direction:column;gap:18px">
<div class="stat"><div class="v">35</div><div class="l">Unità approvate · PSD</div></div>
<div class="stat"><div class="v">60</div><div class="l">In revisione del curatore</div></div>
<div class="stat"><div class="v">20<small>/20</small></div><div class="l">Accettazione retrieval · Hit@10</div></div>
</div>
</div>
</div>
</div>
</section>
<!-- ══════════════ 7 · METADATI DWH ══════════════ -->
<section class="slide s-metadata" aria-label="Metadati sul data warehouse">
<div class="frame">
<div class="kicker rv" style="--d:0"><span class="n">07</span><span>Metadati del DWH</span><span class="ledger"></span></div>
<h1 class="title rv" style="--d:1">Una fotografia versionata,<br>mai un bersaglio mobile.</h1>
<div class="cols" style="margin-top:clamp(18px,3.4vh,34px)">
<div class="stack rv" style="--d:2">
<div class="layer src"><span class="ld"></span><div>
<b>DWH clinico <span class="mono">· Policlinico San Donato</span></b>
<span>lo schema fiscale reale, letto una sola volta</span></div></div>
<span class="link">↓ snapshot</span>
<div class="layer"><span class="ld"></span><div>
<b>Snapshot <span class="mono">.tht-dwh · physical.yaml</span></b>
<span>catalogo preparato + firme LSH dei valori per la ricerca fuzzy</span></div></div>
<span class="link">↓ indici</span>
<div class="layer"><span class="ld"></span><div>
<b>Indici ibridi <span class="mono">Qdrant 1024 dim · BM25</span></b>
<span>schema e memoria condividono il denso, l'evidence aggiunge il lessicale</span></div></div>
<span class="link">↓ uso</span>
<div class="layer"><span class="ld"></span><div>
<b>Sessioni <span class="mono">· workspace_revision bloccata</span></b>
<span>ogni domanda interroga una versione precisa dei metadati</span></div></div>
</div>
<div class="rule-list rv" style="--d:3">
<div class="rule-row" style="grid-template-columns:1fr">
<div><h3>Introspezione una volta sola</h3>
<p>Il catalogo non viene interrogato a ogni domanda: si lavora su uno snapshot
preparato, veloce e stabile.</p></div>
</div>
<div class="rule-row" style="grid-template-columns:1fr">
<div><h3>Generazioni immutabili</h3>
<p>Ogni aggiornamento crea una nuova generazione con puntatore ACTIVE; le vecchie
restano leggibili.</p></div>
</div>
<div class="rule-row" style="grid-template-columns:1fr">
<div><h3>Se l'impronta non torna, ci si ferma</h3>
<p>La firma dell'owner viene verificata all'avvio: un ambiente che non corrisponde
fa fallire il sistema in chiuso, senza tentativi alla cieca.</p></div>
</div>
</div>
</div>
</div>
</section>
<!-- ══════════════ 8 · DIALOGO UMANO-AI ══════════════ -->
<section class="slide s-dialog" aria-label="Interazione umano-AI">
<div class="frame">
<div class="kicker rv" style="--d:0"><span class="n">08</span><span>Il dialogo umano-AI</span><span class="ledger"></span></div>
<h1 class="title rv" style="--d:1">Tre gesti per decidere.</h1>
<div class="gates rv" style="--d:2">
<div class="gate">
<div class="gname">reviewer_select<span>una scelta</span></div>
<p>Il modello propone interpretazioni mutuamente esclusive, ognuna con significato,
filtro ed evidenza. La scelta è essa stessa la conferma: niente passaggi inutili.</p>
</div>
<div class="gate">
<div class="gname">reviewer_decide<span>più scelte</span></div>
<p>Quando più letture sono legittime insieme, ogni voce selezionata è una decisione
distinta e registrata nel ledger.</p>
</div>
<div class="gate">
<div class="gname">reviewer_confirm<span>un artefatto</span></div>
<p>Domanda riscritta, schema collegato, SQL finale: i prodotti del lavoro passano
da un gate esplicito prima di valere come verità della sessione.</p>
</div>
</div>
<div class="truth rv" style="--d:3">
<span class="tmark">§</span>
<div>
<h3>"Ciò che non è registrato non è accaduto."</h3>
<p>Non esiste una trascrizione della conversazione: la verità di una sessione sono gli
artefatti su disco e il registro delle decisioni. Riprendere una sessione significa
rientrare nell'ultima fase incompleta, con tutto il filo ricostruibile. E un hook
anti-bypass impedisce al modello di avanzare da solo: la mano sul volante resta umana.</p>
</div>
</div>
</div>
</section>
<!-- ══════════════ 9 · CASO REALE ══════════════ -->
<section class="slide s-case" aria-label="Una sessione reale">
<div class="frame">
<div class="kicker rv" style="--d:0"><span class="n">09</span><span>Una sessione reale</span><span class="ledger"></span></div>
<h1 class="title rv" style="--d:1">Da una domanda a 78 pazienti,<br>senza mai perdere il filo.</h1>
<div class="cols" style="margin-top:clamp(16px,3vh,30px)">
<div class="rv" style="--d:2">
<div class="trail">
<div class="tstep">question.md <span>· la domanda, riscritta e non ambigua</span></div>
<div class="tstep">schema_linking.json <span>· tabelle e join scelti</span></div>
<div class="tstep">cte_plan.json <span>· il piano, a blocchi verificabili</span></div>
<div class="tstep">3 CTE testate <span>· ognuna contro il dato reale</span></div>
<div class="tstep">sql_final.sql <span>· approvata al gate dal revisore</span></div>
<div class="tstep">export <span>· il risultato, con il suo percorso</span></div>
</div>
<div class="outcome">
<span class="big">78</span>
<span class="lbl">pazienti restituiti da una sessione reale,
completata su tutti gli 8 passaggi</span>
</div>
</div>
<div class="rv" style="--d:3">
<div class="guar">
<div class="g"><i></i><div><b>Sola lettura sul magazzino</b>
<span>le credenziali di consultazione non scrivono mai sul DWH</span></div></div>
<div class="g"><i></i><div><b>Esecuzioni solo dall'elenco consentito</b>
<span>test CTE, explain, anteprima: massimo 10 righe, timeout a 30 secondi</span></div></div>
<div class="g"><i></i><div><b>Segreti fuori dal percorso</b>
<span>credenziali e configurazioni sensibili non vivono nel workspace</span></div></div>
<div class="g"><i></i><div><b>Cinque gate superati in questa sessione</b>
<span>ogni approvazione è una ricevuta nel registro delle decisioni</span></div></div>
</div>
</div>
</div>
</div>
</section>
<!-- ══════════════ 10 · CHIUSURA ══════════════ -->
<section class="slide s-end" aria-label="In sintesi">
<div class="frame">
<div class="kicker rv" style="--d:0"><span class="n">10</span><span>In sintesi</span><span class="ledger"></span></div>
<h1 class="title rv" style="--d:1">Proporre,<br>non decidere.</h1>
<div class="principles rv" style="--d:2">
<div class="rule-row"><span class="num">1</span>
<h3>Il modello propone, il clinico decide.</h3></div>
<div class="rule-row"><span class="num">2</span>
<h3>Ciò che non è registrato non è accaduto.</h3></div>
<div class="rule-row"><span class="num">3</span>
<h3>Ogni numero cita la propria provenienza.</h3></div>
<div class="rule-row"><span class="num">4</span>
<h3>In caso di dubbio, il sistema si ferma.</h3></div>
</div>
<div class="endmeta rv" style="--d:3">
<span>Thoth<b style="color:var(--red)">II</b></span><span class="dot" style="width:4px;height:4px;border-radius:50%;background:var(--line)"></span>
<span>Dal quesito clinico al dato tracciabile</span><span class="dot" style="width:4px;height:4px;border-radius:50%;background:var(--line)"></span>
<span>Grazie</span>
</div>
</div>
</section>
</main>
<!-- HUD: brand · progresso · contatore · navigazione -->
<footer class="hud" aria-hidden="false">
<span class="brand">Thoth<b>II</b></span>
<span class="track"><span class="fill" id="fill"></span></span>
<span class="count" id="count">01 / 10</span>
<button id="prev" aria-label="Slide precedente">
<svg width="14" height="14" viewBox="0 0 16 16" fill="none" aria-hidden="true">
<path d="M10 3L5 8l5 5" stroke="currentColor" stroke-width="1.6" stroke-linecap="round" stroke-linejoin="round"/>
</svg>
</button>
<button id="next" aria-label="Slide successiva">
<svg width="14" height="14" viewBox="0 0 16 16" fill="none" aria-hidden="true">
<path d="M6 3l5 5-5 5" stroke="currentColor" stroke-width="1.6" stroke-linecap="round" stroke-linejoin="round"/>
</svg>
</button>
</footer>
<script>
(function(){
const slides=[...document.querySelectorAll('.slide')];
const N=slides.length;
const fill=document.getElementById('fill');
const count=document.getElementById('count');
const prev=document.getElementById('prev');
const next=document.getElementById('next');
let cur=-1;
function pad(n){return String(n).padStart(2,'0')}
function go(i,push){
i=Math.max(0,Math.min(N-1,i));
if(i===cur)return;
if(cur>=0)slides[cur].classList.remove('on');
cur=i;
const s=slides[cur];
s.classList.add('on');
s.setAttribute('aria-hidden','false');
slides.forEach((el,k)=>{if(k!==cur)el.setAttribute('aria-hidden','true')});
fill.style.width=((cur+1)/N*100)+'%';
count.textContent=pad(cur+1)+' / '+pad(N);
prev.disabled=cur===0;next.disabled=cur===N-1;
if(push!==false)history.replaceState(null,'','#'+(cur+1));
}
function fromHash(){
const m=location.hash.match(/^#(\d+)$/);
go(m?parseInt(m[1],10)-1:0,false);
}
window.addEventListener('keydown',e=>{
if(e.target.closest('button')&&(e.key===' '||e.key==='Enter'))return;
if(e.key==='ArrowRight'||e.key==='PageDown'||e.key===' '){e.preventDefault();go(cur+1)}
else if(e.key==='ArrowLeft'||e.key==='PageUp'){e.preventDefault();go(cur-1)}
else if(e.key==='Home'){e.preventDefault();go(0)}
else if(e.key==='End'){e.preventDefault();go(N-1)}
});
prev.addEventListener('click',()=>{go(cur-1);prev.blur()});
next.addEventListener('click',()=>{go(cur+1);next.blur()});
/* swipe per touch */
let x0=null;
window.addEventListener('pointerdown',e=>{if(e.isPrimary)x0=e.clientX});
window.addEventListener('pointerup',e=>{
if(x0===null||!e.isPrimary)return;
const dx=e.clientX-x0;x0=null;
if(Math.abs(dx)>60)go(cur+(dx<0?1:-1));
});
window.addEventListener('hashchange',fromHash);
fromHash();
})();
</script>
</body>
</html>
@@ -0,0 +1,12 @@
{
"1": "Buongiorno. In dieci passaggi vi mostro come ThothII porta una domanda clinica fatta di parole fino al dato del warehouse, senza mai togliere la decisione al clinico. Tre parole chiave per oggi: proposte del modello, decisioni umane, evidenza citabile.",
"2": "Il dato esiste già: è il warehouse clinico del Policlinico San Donato, dominio aritmologia. Il problema non è il dato, è la domanda: \"ricoveri nel 2023\" nasconde scelte cliniche prima che tecniche. E una risposta senza percorso non è utilizzabile in clinica né in ricerca.",
"3": "Questa è l'idea centrale: il modello non è autonomo. Propone interpretazioni e calcoli, ma ogni passaggio decisivo passa da un revisore umano, e persino l'avanzamento di fase è un atto umano registrato. La catena termina sempre in lettura: nessuna scrittura sul warehouse.",
"4": "Otto fasi deterministiche, dal chiarimento del significato all'eventuale datamart. Il dettaglio che conta concettualmente: lo stato non è una variabile salvata, è ricalcolato dal registro delle decisioni. Riaprire una fase invalida tutto ciò che veniva dopo.",
"5": "Ecco come appare una scelta reale. Ogni opzione porta significato, filtro risultante sul dato, tabelle coinvolte ed evidenza a supporto: il medico decide sapendo cosa comporta. La voce consigliata non è mai pre-confermata, e esistono sempre le vie di uscita.",
"6": "La conoscenza locale diventa una collezione curata: unità atomiche con provenienza verificata, validazione che fallisce in chiuso, generazioni immutabili. Il modello può cercare nelle evidenze ma deve citarle, e i candidati vanno verificati sul dato reale. Nel PSD oggi: 35 unità approvate e retrieval 20 su 20.",
"7": "I metadati sono una fotografia versionata, presa una sola volta e firmata: se l'ambiente non corrisponde all'impronta, il sistema si ferma. Ogni sessione lavora su una revisione bloccata del catalogo, quindi domande ravvicinate interrogano lo stesso catalogo, non un bersaglio mobile.",
"8": "Tre soli gesti di revisione, così che l'interfaccia si impara in un attimo. Il punto concettuale è qui sotto: non esiste una trascrizione della conversazione, esistono artefatti e registro delle decisioni. È questo che rende una sessione riprendibile e verificabile a distanza di mesi.",
"9": "Una sessione reale completata su tutti gli otto passaggi: cinque gate approvati, tre CTE verificate sul dato, 78 pazienti restituiti. A destra le garanzie di contorno: sola lettura, esecuzioni limitate a un elenco consentito, segreti fuori dal percorso.",
"10": "Se dovete portarvi a casa una sola frase: ThothII propone, il clinico decide, e ogni numero porta con sé il proprio percorso. Grazie, e ora sono a disposizione per le domande."
}
Binary file not shown.