Caveat / index.html
umer-wasim's picture
Claude Opus 5.5
Switch to Devseis AI Act Classifier v6; 'uncertain' label for split predictions
14cb8df verified
Raw History Blame Contribute Delete
141 kB
<!doctype html>
<html lang="en">
<head>
<meta charset="utf-8" />
<meta name="viewport" content="width=device-width, initial-scale=1.0, viewport-fit=cover" />
<title>Caveat — AI Act risk check experiment, by Devseis</title>
<meta name="description" content="Check an AI use case against the EU AI Act tiers with two experimental models, then learn step by step how they were built." />
<meta property="og:type" content="website" />
<meta property="og:title" content="Caveat — by Devseis" />
<meta property="og:description" content="Check an AI system against the EU AI Act with two AI models side by side, see honest results, and learn how it works in a free course with a certificate. By Devseis." />
<meta property="og:url" content="https://devseis-caveat.static.hf.space/" />
<meta property="og:image" content="https://devseis-caveat.static.hf.space/og-image.png" />
<meta property="og:image:width" content="2400" />
<meta property="og:image:height" content="1260" />
<meta property="og:image:alt" content="Caveat by Devseis: check an AI system against the EU AI Act. Two AI models side by side, honest results, free course and certificate." />
<meta name="twitter:card" content="summary_large_image" />
<meta name="twitter:image" content="https://devseis-caveat.static.hf.space/og-image.png" />
<meta name="twitter:title" content="Caveat — by Devseis" />
<meta name="twitter:description" content="Check an AI system against the EU AI Act with two AI models side by side, see honest results, and learn how it works in a free course with a certificate. By Devseis." />
<link rel="manifest" href="manifest.json" />
<meta name="theme-color" content="#0b0e14" />
<link rel="icon" href="icon-192.png" id="favicon" />
<meta name="apple-mobile-web-app-capable" content="yes" />
<link href="fonts/fonts.css" rel="stylesheet" />
<style>
:root{
--bg:#0b0e14; --panel:#11151d; --panel-2:#161c27; --border:#232a37; --text:#e8ecf4; --muted:#8b94a7;
--accent:#ff914d; --accent-soft:rgba(255,145,77,.16); --accent-border:rgba(255,145,77,.4);
--danger:#ef7a8c; --danger-soft:rgba(239,122,140,.14); --danger-border:rgba(239,122,140,.4);
--warn:#e5b155; --warn-soft:rgba(229,177,85,.14); --warn-border:rgba(229,177,85,.4);
--good:#5fd39a; --good-soft:rgba(95,211,154,.12); --good-border:rgba(95,211,154,.4);
--info:#7fb2ff; --info-soft:rgba(127,178,255,.13); --info-border:rgba(127,178,255,.4);
--chip-bg:#1a2029; --input-bg:#161c27; --shadow:0 12px 32px rgba(0,0,0,.45);
}
@media (prefers-color-scheme: light){
:root:not([data-theme="dark"]){
--bg:#f4f6fb; --panel:#ffffff; --panel-2:#f8f9fc; --border:#e2e6ee; --text:#171a21; --muted:#5f6878;
--accent:#004aae; --accent-soft:rgba(0,74,174,.08); --accent-border:rgba(0,74,174,.35);
--danger:#b23a4d; --danger-soft:rgba(178,58,77,.09); --danger-border:rgba(178,58,77,.35);
--warn:#9a6a12; --warn-soft:rgba(201,138,31,.12); --warn-border:rgba(201,138,31,.4);
--good:#227a4e; --good-soft:rgba(34,122,78,.1); --good-border:rgba(34,122,78,.35);
--info:#2c5fb3; --info-soft:rgba(44,95,179,.08); --info-border:rgba(44,95,179,.32);
--chip-bg:#eef1f7; --input-bg:#ffffff; --shadow:0 12px 32px rgba(20,30,50,.14);
}
}
:root[data-theme="light"]{
--bg:#f4f6fb; --panel:#ffffff; --panel-2:#f8f9fc; --border:#e2e6ee; --text:#171a21; --muted:#5f6878;
--accent:#004aae; --accent-soft:rgba(0,74,174,.08); --accent-border:rgba(0,74,174,.35);
--danger:#b23a4d; --danger-soft:rgba(178,58,77,.09); --danger-border:rgba(178,58,77,.35);
--warn:#9a6a12; --warn-soft:rgba(201,138,31,.12); --warn-border:rgba(201,138,31,.4);
--good:#227a4e; --good-soft:rgba(34,122,78,.1); --good-border:rgba(34,122,78,.35);
--info:#2c5fb3; --info-soft:rgba(44,95,179,.08); --info-border:rgba(44,95,179,.32);
--chip-bg:#eef1f7; --input-bg:#ffffff; --shadow:0 12px 32px rgba(20,30,50,.14);
}
:root[data-theme="dark"]{
--bg:#0b0e14; --panel:#11151d; --panel-2:#161c27; --border:#232a37; --text:#e8ecf4; --muted:#8b94a7;
--accent:#ff914d; --accent-soft:rgba(255,145,77,.16); --accent-border:rgba(255,145,77,.4);
--danger:#ef7a8c; --danger-soft:rgba(239,122,140,.14); --danger-border:rgba(239,122,140,.4);
--warn:#e5b155; --warn-soft:rgba(229,177,85,.14); --warn-border:rgba(229,177,85,.4);
--good:#5fd39a; --good-soft:rgba(95,211,154,.12); --good-border:rgba(95,211,154,.4);
--info:#7fb2ff; --info-soft:rgba(127,178,255,.13); --info-border:rgba(127,178,255,.4);
--chip-bg:#1a2029; --input-bg:#161c27; --shadow:0 12px 32px rgba(0,0,0,.45);
}
*{box-sizing:border-box;}
[hidden]{display:none !important;}
html{color-scheme:light dark;}
body{margin:0;background:var(--bg);color:var(--text);font-family:"Space Grotesk",-apple-system,BlinkMacSystemFont,"Segoe UI",Roboto,sans-serif;line-height:1.55;
padding-top:env(safe-area-inset-top,0px);padding-bottom:env(safe-area-inset-bottom,0px);}
.wrap{max-width:980px;margin:0 auto;padding:22px 16px 64px;}
a{color:var(--accent);}
h1,h2,h3,h4{line-height:1.25;}
button{font-family:inherit;}
:focus-visible{outline:2px solid var(--accent);outline-offset:2px;}
.mockup-tag{background:#1a1a1a;color:#fff;font-size:11px;font-weight:600;letter-spacing:.04em;text-transform:uppercase;text-align:center;padding:6px 12px;}
header.top{display:flex;align-items:center;justify-content:space-between;gap:12px;margin-bottom:16px;}
.brand h1{font-size:19px;margin:0;}
.brand p{margin:1px 0 0;font-size:12px;color:var(--muted);}
.icon-btn{background:var(--panel);border:1px solid var(--border);color:var(--text);width:36px;height:36px;border-radius:10px;
display:flex;align-items:center;justify-content:center;cursor:pointer;font-size:16px;flex:0 0 auto;}
/* ---- tabs ---- */
.tabs{display:flex;gap:4px;background:var(--panel);border:1px solid var(--border);border-radius:12px;padding:4px;margin-bottom:26px;overflow-x:auto;scrollbar-width:none;}
.tabs::-webkit-scrollbar{display:none;}
.tab-btn{flex:1 0 auto;font-size:13.5px;font-weight:600;padding:10px 14px;border-radius:9px;border:none;background:transparent;color:var(--muted);cursor:pointer;white-space:nowrap;}
.tab-btn[aria-selected="true"]{background:var(--accent-soft);color:var(--accent);}
section[role="tabpanel"][hidden]{display:none;}
.tab-btn .ti{margin-right:4px;}
@media (max-width:520px){
.tabs{display:grid;grid-template-columns:repeat(4,1fr);gap:2px;overflow:visible;}
.tab-btn{display:flex;flex-direction:column;align-items:center;gap:2px;padding:8px 2px;font-size:12px;min-width:0;}
.tab-btn .ti{margin:0;font-size:17px;line-height:1;}
}
/* ---- shared bits ---- */
.panel{background:var(--panel);border:1px solid var(--border);border-radius:16px;padding:20px;}
.eyebrow{font-size:11px;font-weight:700;letter-spacing:.06em;text-transform:uppercase;color:var(--accent);margin:0 0 6px;}
.lead{font-size:15px;color:var(--muted);max-width:66ch;margin:0 0 22px;}
.section-title{font-size:20px;margin:0 0 6px;}
.sub{font-size:13.5px;color:var(--muted);margin:0 0 16px;max-width:66ch;}
.muted{color:var(--muted);}
.callout{border-radius:12px;padding:14px 16px;font-size:13.5px;border:1px solid var(--border);background:var(--panel-2);}
.callout.tip{background:var(--info-soft);border-color:var(--info-border);}
.callout.warn{background:var(--warn-soft);border-color:var(--warn-border);}
.callout.good{background:var(--good-soft);border-color:var(--good-border);}
.callout.bad{background:var(--danger-soft);border-color:var(--danger-border);}
.callout .ctitle{display:block;font-weight:700;margin-bottom:4px;font-size:12px;text-transform:uppercase;letter-spacing:.04em;}
.callout.tip .ctitle{color:var(--info);} .callout.warn .ctitle{color:var(--warn);} .callout.good .ctitle{color:var(--good);} .callout.bad .ctitle{color:var(--danger);}
.callout p{margin:0 0 6px;} .callout p:last-child{margin:0;}
code{background:var(--chip-bg);padding:1px 6px;border-radius:6px;font-size:.9em;}
/* glossary terms */
.term{font:inherit;color:inherit;background:none;border:none;padding:0;cursor:help;border-bottom:1.5px dotted var(--accent);}
.term:hover{color:var(--accent);}
.popover{position:absolute;z-index:50;max-width:320px;background:var(--panel);border:1px solid var(--accent-border);border-radius:12px;
box-shadow:var(--shadow);padding:14px 16px;font-size:13px;}
.popover h5{margin:0 0 4px;font-size:14px;color:var(--accent);}
.popover p{margin:0 0 6px;}
.popover .eg{color:var(--muted);font-size:12.5px;}
.popover .eg b{color:var(--text);}
.popover .more{font-size:12px;}
/* ---- TAB 1: check ---- */
.check{max-width:800px;margin:0 auto;}
.check-hero{text-align:center;margin:18px 0 20px;}
.check-hero h2{font-size:24px;margin:0;}
textarea#scenario{width:100%;min-height:120px;resize:vertical;border-radius:14px;border:1px solid var(--border);
background:var(--input-bg);color:var(--text);padding:16px 18px;font-size:16px;font-family:inherit;line-height:1.5;}
textarea#scenario:focus{outline:2px solid var(--accent-border);outline-offset:1px;}
.chip-row{display:grid;grid-template-columns:repeat(4,1fr);gap:8px;margin-top:12px;}
.chip{display:flex;align-items:center;justify-content:center;gap:6px;min-width:0;font-size:13px;font-weight:500;line-height:1.25;text-align:center;
padding:8px 10px;border-radius:999px;background:var(--chip-bg);border:1px solid var(--border);color:var(--text);cursor:pointer;}
.chip .ce{flex:0 0 auto;}
.chip:hover{border-color:var(--accent-border);}
.chip.active{border-color:var(--accent);background:var(--accent-soft);color:var(--accent);}
@media (max-width:640px){
.chip-row{grid-template-columns:repeat(2,1fr);gap:6px;}
.chip{font-size:13px;padding:9px 8px;border-radius:12px;min-height:42px;}
}
@media (max-width:380px){ .chip{font-size:12.5px;padding:8px 6px;gap:5px;} }
.run-row{display:flex;flex-direction:column;align-items:center;gap:8px;margin-top:18px;}
button.primary{font-size:15px;font-weight:700;padding:12px 30px;border-radius:12px;background:var(--accent);color:var(--bg);border:none;cursor:pointer;min-width:200px;}
button.primary:disabled{opacity:.55;cursor:default;}
button.ghost{font-size:13.5px;font-weight:600;padding:9px 16px;border-radius:10px;background:var(--panel);color:var(--text);border:1px solid var(--border);cursor:pointer;}
button.ghost:hover{border-color:var(--accent-border);}
button.ghost:disabled{opacity:.45;cursor:default;}
.fine{font-size:11.5px;color:var(--muted);}
.results{margin-top:26px;scroll-margin-top:16px;}
#panel-check.has-results{padding-bottom:40vh;} /* room to centre the results on short pages */
.results[hidden]{display:none;}
.cards{display:grid;grid-template-columns:1fr 1fr;gap:14px;}
@media (max-width:640px){.cards{grid-template-columns:1fr;}}
.card{background:var(--panel);border:1px solid var(--border);border-radius:16px;padding:18px;display:flex;flex-direction:column;gap:10px;min-height:150px;}
.card-title{display:flex;align-items:center;justify-content:space-between;gap:8px;}
.card-title h3{font-size:13px;margin:0;color:var(--muted);font-weight:600;}
.model-meta{display:flex;flex-wrap:wrap;gap:4px 6px;margin:-4px 0 2px;}
.model-meta span{font-size:11.5px;padding:2px 8px;border-radius:999px;background:var(--chip-bg);color:var(--muted);border:1px solid var(--border);}
.model-meta span.name{color:var(--text);font-weight:600;}
.info-link{font-size:12px;text-decoration:none;border:1px solid var(--border);border-radius:999px;padding:2px 9px;color:var(--muted);}
.info-link:hover{color:var(--accent);border-color:var(--accent-border);}
.verdict-pill{font-size:15px;font-weight:700;padding:7px 15px;border-radius:999px;border:1px solid;display:inline-block;align-self:flex-start;}
.tier-prohibited{color:var(--danger);background:var(--danger-soft);border-color:var(--danger-border);}
.tier-high_risk{color:var(--warn);background:var(--warn-soft);border-color:var(--warn-border);}
.tier-limited_risk{color:var(--info);background:var(--info-soft);border-color:var(--info-border);}
.tier-minimal_risk{color:var(--good);background:var(--good-soft);border-color:var(--good-border);}
.tier-gpai{color:var(--info);background:var(--info-soft);border-color:var(--info-border);}
.tier-uncertain{color:var(--muted);background:var(--chip-bg);border-color:var(--border);border-style:dashed;}
.tier-gpai_systemic_risk{color:var(--warn);background:var(--warn-soft);border-color:var(--warn-border);}
.tier-meaning{font-size:13.5px;margin:0;}
details.why{font-size:12.5px;color:var(--muted);}
details.why summary{cursor:pointer;color:var(--muted);font-weight:600;}
details.why p{margin:6px 0 0;}
.status-line{font-size:13px;color:var(--muted);display:flex;align-items:center;gap:8px;}
.spinner{width:13px;height:13px;border-radius:50%;border:2px solid var(--border);border-top-color:var(--accent);animation:spin .8s linear infinite;flex:0 0 auto;}
@keyframes spin{to{transform:rotate(360deg);}}
.agree{margin-top:12px;text-align:center;font-size:13px;padding:10px 14px;border-radius:12px;}
.agree[hidden]{display:none;}
.agree.yes{background:var(--good-soft);border:1px solid var(--good-border);}
.agree.no{background:var(--warn-soft);border:1px solid var(--warn-border);}
/* ---- TAB 2: results ---- */
.tiles{display:grid;grid-template-columns:repeat(4,1fr);gap:12px;margin-bottom:26px;}
@media (max-width:760px){.tiles{grid-template-columns:repeat(2,1fr);}}
.tile{background:var(--panel);border:1px solid var(--border);border-radius:14px;padding:16px;}
.tile .num{font-size:28px;font-weight:700;font-variant-numeric:tabular-nums;line-height:1.1;}
.tile .lbl{font-size:12.5px;color:var(--muted);margin-top:6px;}
.block{margin-bottom:30px;}
.bars{display:flex;flex-direction:column;gap:12px;}
.bar-row{display:grid;grid-template-columns:130px 1fr 52px;gap:12px;align-items:center;font-size:13px;}
@media (max-width:520px){.bar-row{grid-template-columns:96px 1fr 44px;}}
.bar-track{height:12px;background:var(--chip-bg);border-radius:999px;overflow:hidden;}
.bar-fill{height:100%;border-radius:999px;}
.bar-val{text-align:right;font-variant-numeric:tabular-nums;font-weight:600;}
.table-wrap{overflow-x:auto;border:1px solid var(--border);border-radius:14px;background:var(--panel);}
table{border-collapse:collapse;width:100%;font-size:13px;min-width:560px;}
th,td{padding:11px 14px;text-align:left;border-bottom:1px solid var(--border);}
th{font-size:11.5px;text-transform:uppercase;letter-spacing:.04em;color:var(--muted);font-weight:600;}
tr:last-child td{border-bottom:none;}
td.num,th.num{text-align:right;font-variant-numeric:tabular-nums;}
tr.hl td{background:var(--accent-soft);}
.limits{display:grid;grid-template-columns:1fr 1fr;gap:12px;}
@media (max-width:640px){.limits{grid-template-columns:1fr;}}
.limit{background:var(--panel);border:1px solid var(--border);border-radius:14px;padding:14px 16px;font-size:13.5px;}
.limit h4{margin:0 0 4px;font-size:14px;}
.limit p{margin:0;color:var(--muted);}
.pipeline{display:grid;grid-template-columns:repeat(3,1fr);gap:12px;}
@media (max-width:700px){.pipeline{grid-template-columns:1fr;}}
.pipe{background:var(--panel);border:1px solid var(--border);border-radius:14px;padding:16px;font-size:13.5px;}
.pipe .tag{font-size:10.5px;font-weight:700;letter-spacing:.05em;text-transform:uppercase;color:var(--accent);}
.pipe h4{margin:4px 0 6px;font-size:15px;}
.pipe p{margin:0;color:var(--muted);}
/* ---- TAB 3: story ---- */
.timeline{position:relative;padding-left:34px;}
.timeline::before{content:"";position:absolute;left:12px;top:6px;bottom:6px;width:2px;background:var(--border);}
.chapter{position:relative;margin-bottom:22px;}
.chapter .dot{position:absolute;left:-34px;top:2px;width:26px;height:26px;border-radius:50%;background:var(--panel);border:2px solid var(--accent);
display:flex;align-items:center;justify-content:center;font-size:11px;font-weight:700;color:var(--accent);}
.chapter .when{font-size:11px;font-weight:700;letter-spacing:.05em;text-transform:uppercase;color:var(--muted);}
.chapter h3{margin:2px 0 10px;font-size:17px;}
.chapter-body{background:var(--panel);border:1px solid var(--border);border-radius:14px;padding:16px 18px;}
.ch-grid{display:grid;grid-template-columns:1fr 1fr;gap:14px;}
@media (max-width:640px){.ch-grid{grid-template-columns:1fr;}}
.ch-grid h4{margin:0 0 4px;font-size:12px;text-transform:uppercase;letter-spacing:.04em;color:var(--muted);}
.ch-grid p{margin:0;font-size:13.5px;}
.ch-stat{display:inline-flex;gap:8px;align-items:baseline;margin-top:10px;}
.ch-stat b{font-size:22px;font-variant-numeric:tabular-nums;}
.ch-lesson{margin-top:12px;font-size:13px;border-top:1px dashed var(--border);padding-top:10px;display:flex;gap:10px;justify-content:space-between;flex-wrap:wrap;align-items:center;}
.ch-lesson a{font-weight:600;text-decoration:none;}
/* ---- TAB 4: learn ---- */
.learn{display:grid;grid-template-columns:250px 1fr;gap:22px;align-items:start;}
@media (max-width:820px){.learn{grid-template-columns:1fr;}}
.lesson-nav{position:sticky;top:12px;background:var(--panel);border:1px solid var(--border);border-radius:14px;padding:10px;}
@media (max-width:820px){.lesson-nav{position:static;}}
.progress{padding:6px 8px 12px;}
.progress .lbl{font-size:12px;color:var(--muted);display:flex;justify-content:space-between;}
.progress-track{height:8px;background:var(--chip-bg);border-radius:999px;overflow:hidden;margin-top:6px;}
.progress-fill{height:100%;background:var(--accent);border-radius:999px;transition:width .3s;}
.lesson-list{list-style:none;margin:0;padding:0;display:flex;flex-direction:column;gap:2px;}
@media (max-width:820px){.lesson-list{display:none;} .lesson-list.open{display:flex;}}
.lesson-list button{width:100%;text-align:left;display:flex;gap:10px;align-items:center;padding:8px 10px;border-radius:9px;border:none;background:none;color:var(--text);font-size:13px;cursor:pointer;}
.lesson-list button:hover{background:var(--chip-bg);}
.lesson-list button[aria-current="true"]{background:var(--accent-soft);color:var(--accent);font-weight:600;}
.lesson-list .n{width:22px;height:22px;border-radius:50%;border:1px solid var(--border);display:flex;align-items:center;justify-content:center;font-size:11px;flex:0 0 auto;color:var(--muted);}
.lesson-list .done .n{background:var(--good-soft);border-color:var(--good-border);color:var(--good);}
.nav-toggle{display:none;width:100%;}
@media (max-width:820px){.nav-toggle{display:block;}}
.lesson{background:var(--panel);border:1px solid var(--border);border-radius:16px;padding:24px;min-width:0;}
@media (max-width:520px){.lesson{padding:18px 16px;}}
.lesson-head{display:flex;gap:14px;align-items:flex-start;margin-bottom:14px;}
.lesson-icon{font-size:30px;line-height:1;}
.lesson-head h2{margin:0;font-size:22px;}
.lesson-meta{font-size:12px;color:var(--muted);margin-top:4px;}
.lesson-body{font-size:15px;}
.lesson-body p{margin:0 0 12px;}
.lesson-body h3{font-size:16px;margin:22px 0 8px;}
.lesson-body ul,.lesson-body ol{margin:0 0 12px;padding-left:22px;}
.lesson-body li{margin-bottom:6px;}
.lesson-body .callout{margin:14px 0;}
.analogy{display:flex;gap:12px;align-items:flex-start;background:var(--accent-soft);border:1px solid var(--accent-border);border-radius:12px;padding:14px 16px;margin:14px 0;font-size:14px;}
.analogy .ico{font-size:22px;line-height:1;}
.example{border:1px solid var(--border);border-radius:12px;overflow:hidden;margin:14px 0;}
.example .ex-head{background:var(--panel-2);padding:8px 14px;font-size:11.5px;font-weight:700;letter-spacing:.05em;text-transform:uppercase;color:var(--muted);}
.example .ex-body{padding:12px 14px;font-size:14px;}
.ex-row{display:flex;gap:10px;align-items:center;justify-content:space-between;padding:8px 0;border-bottom:1px dashed var(--border);flex-wrap:wrap;}
.ex-row:last-child{border-bottom:none;}
.ex-row .txt{flex:1 1 260px;}
.mini-pill{font-size:12px;font-weight:700;padding:3px 10px;border-radius:999px;border:1px solid;white-space:nowrap;}
.keywords{margin-top:22px;border-top:1px solid var(--border);padding-top:16px;}
.keywords h3{font-size:12px;text-transform:uppercase;letter-spacing:.05em;color:var(--muted);margin:0 0 10px;}
.kw-grid{display:grid;grid-template-columns:repeat(auto-fill,minmax(210px,1fr));gap:10px;}
.kw{background:var(--panel-2);border:1px solid var(--border);border-radius:12px;padding:12px 14px;font-size:13px;}
.kw b{display:block;color:var(--accent);margin-bottom:3px;font-size:13.5px;}
.quiz{margin-top:22px;background:var(--panel-2);border:1px solid var(--border);border-radius:14px;padding:16px;}
.quiz h3{margin:0 0 4px;font-size:12px;text-transform:uppercase;letter-spacing:.05em;color:var(--muted);}
.quiz .q{font-size:15px;font-weight:600;margin:0 0 12px;}
.quiz-opts{display:flex;flex-direction:column;gap:8px;}
.quiz-opts button{text-align:left;padding:10px 14px;border-radius:10px;border:1px solid var(--border);background:var(--panel);color:var(--text);font-size:14px;cursor:pointer;}
.quiz-opts button:hover:not(:disabled){border-color:var(--accent-border);}
.quiz-opts button.right{background:var(--good-soft);border-color:var(--good-border);}
.quiz-opts button.wrong{background:var(--danger-soft);border-color:var(--danger-border);}
.quiz-opts button:disabled{cursor:default;}
.quiz .fb{margin-top:10px;font-size:13.5px;}
.quiz .fb[hidden]{display:none;}
.lesson-foot{display:flex;justify-content:space-between;gap:10px;margin-top:24px;flex-wrap:wrap;}
.widget{border:1px solid var(--accent-border);border-radius:14px;padding:16px;margin:16px 0;background:var(--panel-2);}
.widget .w-title{font-size:11.5px;font-weight:700;letter-spacing:.05em;text-transform:uppercase;color:var(--accent);margin-bottom:8px;}
.sort-card{font-size:15px;font-weight:600;margin:6px 0 12px;}
.sort-btns{display:flex;flex-wrap:wrap;gap:8px;}
.sort-btns button{padding:8px 12px;border-radius:999px;border:1px solid;cursor:pointer;font-size:13px;font-weight:600;}
.sort-fb{margin-top:10px;font-size:13.5px;min-height:1.4em;}
.split-bar{display:flex;height:34px;border-radius:10px;overflow:hidden;margin:8px 0;font-size:12px;font-weight:700;}
.split-bar div{display:flex;align-items:center;justify-content:center;color:#fff;}
.dots{display:grid;grid-template-columns:repeat(20,1fr);gap:4px;margin:10px 0;max-width:460px;}
.dots span{aspect-ratio:1;border-radius:50%;}
.seg{display:inline-flex;border:1px solid var(--border);border-radius:10px;overflow:hidden;margin-bottom:8px;}
.seg button{border:none;background:var(--panel);color:var(--muted);padding:8px 12px;font-size:13px;font-weight:600;cursor:pointer;}
.seg button[aria-pressed="true"]{background:var(--accent-soft);color:var(--accent);}
.w-stats{display:flex;gap:18px;flex-wrap:wrap;font-size:13px;}
.w-stats b{font-size:20px;display:block;font-variant-numeric:tabular-nums;}
.checklist{list-style:none;padding:0;margin:10px 0;}
.checklist li{display:flex;gap:10px;align-items:flex-start;padding:10px 0;border-bottom:1px dashed var(--border);}
.checklist input{margin-top:4px;width:18px;height:18px;accent-color:var(--accent);flex:0 0 auto;}
.checklist label{cursor:pointer;}
.checklist .step-detail{display:block;font-size:13px;color:var(--muted);margin-top:2px;}
.gloss-search{width:100%;padding:11px 14px;border-radius:10px;border:1px solid var(--border);background:var(--input-bg);color:var(--text);font-size:14px;font-family:inherit;margin-bottom:14px;}
.gloss-list{display:grid;grid-template-columns:repeat(auto-fill,minmax(250px,1fr));gap:10px;}
/* call to action after a check */
.cta{margin-top:14px;border:1px solid var(--accent-border);background:var(--accent-soft);border-radius:16px;padding:18px;text-align:center;}
.cta-lead{font-size:15px;font-weight:600;margin:0 0 12px;}
.cta-btns{display:flex;gap:10px;justify-content:center;flex-wrap:wrap;}
a.primary{display:inline-block;text-decoration:none;font-size:15px;font-weight:700;padding:12px 22px;border-radius:12px;background:var(--accent);color:var(--bg);}
.cta-opt{display:inline-flex;gap:8px;align-items:center;font-size:12.5px;color:var(--muted);margin-top:12px;cursor:pointer;}
.cta-opt input{accent-color:var(--accent);}
@media (max-width:520px){ .cta-btns > *{width:100%;} }
/* final assessment */
.exam-q{border:1px solid var(--border);border-radius:12px;padding:12px 14px;margin:0 0 10px;background:var(--panel);}
.exam-q legend{padding:0 4px;font-size:14.5px;}
.exam-opt{display:flex;gap:10px;align-items:flex-start;padding:7px 4px;cursor:pointer;font-size:14px;border-radius:8px;}
.exam-opt:hover{background:var(--chip-bg);}
.exam-opt input{margin-top:4px;accent-color:var(--accent);flex:0 0 auto;}
.exam-score{border-radius:12px;padding:12px 14px;font-size:15px;}
.exam-score.pass{background:var(--good-soft);border:1px solid var(--good-border);}
.exam-score.fail{background:var(--warn-soft);border:1px solid var(--warn-border);}
.exam-review{border-left:3px solid var(--warn);padding:6px 12px;margin:8px 0;font-size:13.5px;}
/* certificate */
.cert-row{display:flex;gap:8px;flex-wrap:wrap;margin:10px 0;}
.cert-row input{flex:1 1 220px;padding:10px 12px;border-radius:10px;border:1px solid var(--border);background:var(--input-bg);color:var(--text);font:inherit;font-size:14px;}
.cert-preview{width:100%;max-width:640px;border-radius:10px;border:1px solid var(--border);display:block;margin:12px 0;}
.cert-actions{display:flex;gap:8px;flex-wrap:wrap;}
.cert-actions a{text-decoration:none;}
/* printable summary: invisible on screen, the only thing printed */
#printSheet{display:none;}
@media print{
body{background:#fff !important;}
body > *:not(#printSheet){display:none !important;}
#printSheet{display:block !important;color:#111;font-size:11.5pt;line-height:1.45;}
@page{margin:16mm;}
}
.ps-head{display:flex;justify-content:space-between;align-items:baseline;border-bottom:2px solid #004aae;padding-bottom:8px;margin-bottom:14px;}
.ps-brand{font-weight:700;font-size:16pt;color:#004aae;letter-spacing:.02em;}
.ps-sub{font-size:9.5pt;color:#555;}
#printSheet h1{font-size:17pt;margin:0 0 4px;}
#printSheet h2{font-size:12pt;margin:16px 0 6px;color:#004aae;}
#printSheet table{width:100%;border-collapse:collapse;min-width:0;font-size:10.5pt;}
#printSheet th,#printSheet td{border:1px solid #ccc;padding:6px 8px;text-align:left;vertical-align:top;}
#printSheet th{background:#f1f4fa;color:#333;text-transform:none;letter-spacing:0;font-size:10pt;}
.ps-quote{border-left:3px solid #004aae;padding:6px 10px;background:#f5f7fb;margin:0;}
.ps-note{font-size:9.5pt;color:#444;border:1px solid #ddd;padding:8px 10px;margin-top:14px;}
.ps-foot{margin-top:18px;font-size:9pt;color:#666;border-top:1px solid #ccc;padding-top:8px;}
/* expert vs model */
.vs{display:grid;grid-template-columns:1fr 1fr;gap:12px;}
@media (max-width:640px){.vs{grid-template-columns:1fr;}}
.vs-col{border-radius:14px;padding:14px 16px;border:1px solid var(--border);background:var(--panel);font-size:13.5px;}
.vs-col ul{margin:8px 0 0;padding-left:18px;} .vs-col li{margin-bottom:6px;}
.vs-model{border-color:var(--warn-border);} .vs-expert{border-color:var(--good-border);background:var(--good-soft);}
.vs-tag{font-weight:700;font-size:14px;}
/* key dates */
.dates{list-style:none;margin:0;padding:0;border-left:2px solid var(--border);margin-left:8px;}
.dates li{position:relative;padding:0 0 14px 20px;}
.dates li::before{content:"";position:absolute;left:-7px;top:5px;width:12px;height:12px;border-radius:50%;background:var(--panel);border:2px solid var(--border);}
.dates li.past::before{background:var(--muted);border-color:var(--muted);}
.dates li.next::before{background:var(--accent);border-color:var(--accent);}
.dates .d{font-weight:700;font-variant-numeric:tabular-nums;}
.dates li.past .d{color:var(--muted);}
.dates .what{display:block;font-size:13.5px;margin-top:2px;}
.dates li.past .what{color:var(--muted);}
.dates .tagx{display:inline-block;font-size:11px;font-weight:700;padding:1px 8px;border-radius:999px;margin-left:6px;vertical-align:1px;}
.tag-next{background:var(--accent-soft);color:var(--accent);border:1px solid var(--accent-border);}
.tag-changed{background:var(--warn-soft);color:var(--warn);border:1px solid var(--warn-border);}
.tag-done{background:var(--chip-bg);color:var(--muted);border:1px solid var(--border);}
/* Devseis block */
.devseis{background:var(--panel);border:1px solid var(--border);border-radius:16px;padding:20px;}
.svc-grid{display:grid;grid-template-columns:repeat(3,1fr);gap:10px;}
@media (max-width:760px){.svc-grid{grid-template-columns:repeat(2,1fr);}}
@media (max-width:420px){.svc-grid{grid-template-columns:1fr;}}
.svc{background:var(--panel-2);border:1px solid var(--border);border-radius:12px;padding:10px 12px;font-size:13px;}
.svc b{display:block;font-size:13.5px;margin-bottom:2px;} .svc span{color:var(--muted);}
.badges{display:flex;flex-wrap:wrap;gap:8px;margin-top:12px;}
.badge{font-size:12.5px;font-weight:600;padding:5px 11px;border-radius:999px;background:var(--chip-bg);border:1px solid var(--border);}
.more-grid{display:grid;grid-template-columns:repeat(3,1fr);gap:10px;}
@media (max-width:760px){.more-grid{grid-template-columns:repeat(2,1fr);}}
@media (max-width:420px){.more-grid{grid-template-columns:1fr;}}
.more{display:block;text-decoration:none;color:var(--text);background:var(--panel-2);border:1px solid var(--border);border-radius:12px;padding:10px 12px;font-size:12.5px;}
.more:hover{border-color:var(--accent-border);}
.more b{display:block;font-size:13.5px;margin-bottom:2px;} .more span{color:var(--muted);}
/* consent banner: not a wall — the page stays usable; "Decline" is as easy as "Accept" */
.consent{position:fixed;left:16px;right:16px;bottom:16px;z-index:60;max-width:560px;margin:0 auto;background:var(--panel);border:1px solid var(--border);
border-radius:16px;box-shadow:var(--shadow);padding:16px 18px;font-size:13.5px;}
.consent p{margin:0 0 12px;}
.consent .c-btns{display:flex;gap:8px;flex-wrap:wrap;align-items:center;}
.consent .c-btns button{flex:1 1 140px;font-size:14px;font-weight:600;padding:10px 14px;border-radius:10px;cursor:pointer;border:1px solid var(--border);background:var(--chip-bg);color:var(--text);}
.consent .c-btns button:hover{border-color:var(--accent-border);}
.consent a{font-size:12.5px;}
.link-btn{background:none;border:none;padding:0;color:var(--muted);text-decoration:underline;cursor:pointer;font:inherit;}
footer.page{margin-top:40px;padding-top:18px;border-top:1px solid var(--border);color:var(--muted);font-size:12px;
display:flex;flex-wrap:wrap;gap:6px 16px;justify-content:space-between;}
footer.page a{color:var(--muted);}
</style>
</head>
<body>
<div class="mockup-tag">EXPERIMENTAL — not a compliance tool</div>
<div class="wrap">
<header class="top">
<div class="brand">
<h1>Caveat</h1>
<p>EU AI Act risk check experiment, by Devseis</p>
</div>
<button class="icon-btn" id="themeToggle" type="button" aria-label="Toggle dark/light theme" title="Toggle theme">🌓</button>
</header>
<nav class="tabs" role="tablist" aria-label="Sections">
<button class="tab-btn" role="tab" id="tab-check" aria-controls="panel-check" aria-selected="true" data-tab="check" type="button"><span class="ti" aria-hidden="true">🔍</span>Check</button>
<button class="tab-btn" role="tab" id="tab-results" aria-controls="panel-results" aria-selected="false" data-tab="results" type="button"><span class="ti" aria-hidden="true">📊</span>Results</button>
<button class="tab-btn" role="tab" id="tab-story" aria-controls="panel-story" aria-selected="false" data-tab="story" type="button"><span class="ti" aria-hidden="true">📖</span>Story</button>
<button class="tab-btn" role="tab" id="tab-learn" aria-controls="panel-learn" aria-selected="false" data-tab="learn" type="button"><span class="ti" aria-hidden="true">🎓</span>Learn</button>
</nav>
<!-- ================= TAB 1: CHECK ================= -->
<section id="panel-check" role="tabpanel" aria-labelledby="tab-check">
<div class="check">
<div class="check-hero"><h2>What does your AI system do?</h2></div>
<label for="scenario" class="sr-only" style="position:absolute;left:-9999px;">Describe an AI system</label>
<textarea id="scenario" placeholder="e.g. An app that screens job applicants' CVs and ranks them before a recruiter sees them."></textarea>
<div class="chip-row" role="group" aria-label="Example systems">
<button class="chip" type="button" data-ex="0"><span class="ce">👮</span>Street face ID</button>
<button class="chip" type="button" data-ex="1"><span class="ce">📄</span>CV screening</button>
<button class="chip" type="button" data-ex="2"><span class="ce">📞</span>Phone order bot</button>
<button class="chip" type="button" data-ex="3"><span class="ce">🎬</span>Movie picks</button>
<button class="chip" type="button" data-ex="4"><span class="ce">🧠</span>Large AI model</button>
<button class="chip" type="button" data-ex="5"><span class="ce">😠</span>Emotion AI at work</button>
<button class="chip" type="button" data-ex="6"><span class="ce">🏦</span>Loan scoring</button>
<button class="chip" type="button" data-ex="7"><span class="ce">🎭</span>Deepfake CEO</button>
</div>
<div class="run-row">
<button class="primary" id="runBtn" type="button">Check</button>
<span class="fine" id="runHint">Experimental · not legal advice · runs in your browser</span>
</div>
<div class="results" id="results" hidden>
<div class="cards">
<div class="card">
<div class="card-title"><h3>Trained model</h3><a class="info-link" href="#learn-6" title="How this model was trained">what is this?</a></div>
<div class="model-meta" aria-label="Model details">
<span class="name">Devseis AI Act Classifier v6</span><span>35M parameters</span><span>built on legal-bert-small</span><span>runs in your browser</span>
</div>
<div id="p1status" class="status-line"><span class="spinner"></span> Thinking…</div>
<div id="p1result" hidden>
<span class="verdict-pill" id="p1pill">—</span>
<p class="tier-meaning" id="p1meaning" style="margin-top:10px;"></p>
<details class="why"><summary>Why?</summary><p id="p1rationale"></p></details>
</div>
</div>
<div class="card">
<div class="card-title"><h3>Reasoning AI</h3><a class="info-link" href="#story-3" title="How the reasoning check works">what is this?</a></div>
<div class="model-meta" aria-label="Model details">
<span class="name">Llama 3.1 8B Instruct</span><span>8B parameters</span><span>not trained on this task</span><span>Cloudflare Workers AI</span>
</div>
<div id="p2status" class="status-line"><span class="spinner"></span> Thinking…</div>
<div id="p2result" hidden>
<span class="verdict-pill" id="p2pill">—</span>
<p class="tier-meaning" id="p2meaning" style="margin-top:10px;"></p>
<details class="why"><summary>Why?</summary><p id="p2rationale"></p></details>
</div>
</div>
</div>
<div class="agree" id="agree" hidden></div>
<div class="cta" id="cta" hidden>
<p class="cta-lead" id="ctaLead"></p>
<div class="cta-btns">
<a class="primary cta-main" id="ctaMail" href="#">📅 Book a free 30-min AI Act readiness call</a>
<button class="ghost" type="button" id="printBtn">🖨️ Save or print this summary</button>
</div>
<label class="cta-opt"><input type="checkbox" id="ctaInclude" checked /> Include my description and both results in the email</label>
<p class="fine" style="margin:6px 0 0">Your email app opens with a draft to Devseis — nothing is sent until you press send.</p>
</div>
</div>
</div>
</section>
<!-- ================= TAB 2: RESULTS ================= -->
<section id="panel-results" role="tabpanel" aria-labelledby="tab-results" hidden>
<p class="eyebrow">How good is it, honestly?</p>
<h2 class="section-title">The numbers behind the trained model</h2>
<p class="lead">Measured on 70 example systems the model never saw while learning. Tap any <button class="term" data-term="test set">underlined word</button> for a plain explanation.</p>
<div class="tiles">
<div class="tile"><div class="num" style="color:var(--good)">99%</div><div class="lbl">of banned practices caught (<button class="term" data-term="recall">recall</button>)</div></div>
<div class="tile"><div class="num">82%</div><div class="lbl">of all answers right (<button class="term" data-term="accuracy">accuracy</button>)</div></div>
<div class="tile"><div class="num">0</div><div class="lbl">banned practices called harmless (<button class="term" data-term="severe error">severe errors</button>)</div></div>
<div class="tile"><div class="num">36 MB</div><div class="lbl">download, runs on your device (<button class="term" data-term="quantization">compressed</button>)</div></div>
</div>
<div class="block">
<h3 class="section-title" style="font-size:16px;">How often it's right, per risk tier</h3>
<p class="sub">Out of all test cases that truly belong to each tier, the share the model got right. Average of 3 training runs.</p>
<div class="bars" id="tierBars"></div>
</div>
<div class="block">
<h3 class="section-title" style="font-size:16px;">Compared with the previous versions</h3>
<p class="sub">Same 66 test systems for everyone. The ranges show how much the score could move by chance (<button class="term" data-term="confidence interval">95% confidence interval</button>) — they overlap, so these gaps are not proven.</p>
<div class="table-wrap">
<table>
<thead><tr><th>Version</th><th class="num">Banned caught</th><th class="num">Balanced score</th><th class="num">Accuracy</th><th class="num">Chatbots &amp; voice agents</th></tr></thead>
<tbody>
<tr class="hl"><td><b>v6 — running now</b><br><span class="muted" style="font-size:12px;">legal-text model, 242 training cases, more writing styles</span></td><td class="num"><b>99%</b></td><td class="num">0.84 <span class="muted">(0.76–0.91)</span></td><td class="num">82%</td><td class="num">96%</td></tr>
<tr><td>v5 (previous)<br><span class="muted" style="font-size:12px;">same model, before the weak-spot fixes</span></td><td class="num">98%</td><td class="num">0.82 <span class="muted">(0.74–0.89)</span></td><td class="num">80%</td><td class="num">89%</td></tr>
<tr><td>Original live model<br><span class="muted" style="font-size:12px;">general model, v2 data</span></td><td class="num">91%</td><td class="num">0.78 <span class="muted">(0.67–0.86)</span></td><td class="num">75%</td><td class="num">100%</td></tr>
</tbody>
</table>
</div>
<p class="fine" style="margin-top:8px;">"Balanced score" = <button class="term" data-term="macro-F1">macro-F1</button>, which punishes a model for ignoring any one tier.</p>
</div>
<div class="block">
<h3 class="section-title" style="font-size:16px;">Where it still goes wrong</h3>
<div class="limits">
<div class="limit"><h4>😬 Over-cautious on harmless systems</h4><p>About 1 in 4 truly harmless systems gets pushed up to "high risk" (down from 1 in 3). Safer than the reverse, but still wrong.</p></div>
<div class="limit"><h4>🗣️ Weaker on unfamiliar wording</h4><p>On 35 cases written in a different style it scored 80% (up from 76%), and once called an exploitative ad engine aimed at people with learning disabilities "limited risk" — a real miss. (<button class="term" data-term="out-of-distribution">out-of-distribution</button>)</p></div>
<div class="limit"><h4>🧪 Small, made-up test</h4><p>Only 12 banned examples in the test. Even a perfect score there can't rule out missing 1 in 4 in real life.</p></div>
<div class="limit"><h4>🔤 Pattern-matching, not reasoning</h4><p>It reacts to words it learned, not to the legal test. It gives one tier, even when several rules apply at once.</p></div>
</div>
</div>
<div class="block">
<h3 class="section-title" style="font-size:16px;">What runs when you press "Check"</h3>
<div class="pipeline">
<div class="pipe"><span class="tag">Step 1 · rule</span><h4>GPAI check</h4><p>A small <button class="term" data-term="rule-based">rule</button> spots descriptions of big general-purpose AI models and decides those from the training-compute figure (<button class="term" data-term="FLOPs">FLOPs</button>). Tested on 655 cases, all correct.</p></div>
<div class="pipe"><span class="tag">Step 2 · trained model</span><h4>Devseis AI Act Classifier v6</h4><p>Devseis's own model: the small legal-text model legal-bert-small, <button class="term" data-term="fine-tuning">fine-tuned</button> by Devseis on 938 examples. Runs entirely in your browser — nothing is sent anywhere.</p></div>
<div class="pipe"><span class="tag">Step 3 · reasoning AI</span><h4>Llama 3.1 8B</h4><p>A general AI given the EU AI Act rules as instructions and asked to reason. Not trained on this task, never independently checked. Runs on Cloudflare's free tier.</p></div>
</div>
<p class="fine" style="margin-top:12px;">Check it yourself: <a href="https://huggingface.co/Devseis/devseis-ai-act-classifier-v6" target="_blank" rel="noopener">model &amp; card</a> · <a href="https://huggingface.co/datasets/Devseis/devseis-ai-act-classifier-v6-data" target="_blank" rel="noopener">training &amp; test data</a> · <a href="https://huggingface.co/Devseis/devseis-ai-act-classifier-v6/tree/main/evaluation" target="_blank" rel="noopener">full evaluation reports</a> · <a href="https://huggingface.co/Devseis/eu-ai-act-classifier-experiment" target="_blank" rel="noopener">earlier versions</a></p>
</div>
<div class="block">
<h3 class="section-title" style="font-size:16px;">What the model can't do — and what an expert does</h3>
<p class="sub">The model is useful for a first look. The decisions that matter need someone who can ask questions.</p>
<div class="vs">
<div class="vs-col vs-model"><span class="vs-tag">🤖 The model</span><ul>
<li>Reads only the words you typed</li>
<li>Gives one label, even when several rules apply at once</li>
<li>Can't ask who uses the system, for what purpose, or with which safeguards</li>
<li>Misses exceptions, such as the Art 6(3) derogations or the police-only scope of some bans</li>
<li>Learned from synthetic examples; weaker on unusual wording</li>
<li>Takes no responsibility for its answer</li></ul></div>
<div class="vs-col vs-expert"><span class="vs-tag">🧑‍⚖️ A Devseis expert</span><ul>
<li>Asks the questions that decide the tier: intended purpose, deployer, data, affected people</li>
<li>Checks the exceptions, derogations and the dates that apply to you</li>
<li>Maps the overlaps with GDPR (DPIA), NIS2, DORA, the Cyber Resilience Act and product-safety law</li>
<li>Gives a documented classification you can defend to a regulator or customer</li>
<li>Turns it into a costed action plan: risk management, documentation, human oversight, conformity assessment</li>
<li>Trains your team as an accredited training provider</li></ul></div>
</div>
</div>
<div class="block" id="dates">
<h3 class="section-title" style="font-size:16px;">EU AI Act — key dates</h3>
<p class="sub">From the Official Journal texts of the AI Act (Regulation (EU) 2024/1689, Art 111 and 113) and the AI Omnibus (Regulation (EU) 2026/1744).
Dates the Omnibus changed are marked.</p>
<ol class="dates" id="datesList"></ol>
<p class="fine" style="margin-top:8px">Simplified overview, not legal advice. Which dates apply to your system depends on its role and category.</p>
</div>
<div class="block devseis" id="devseisBlock">
<p class="eyebrow">Built by Devseis</p>
<h3 class="section-title" style="font-size:18px;">Cybersecurity, data protection and AI governance — Luxembourg 🇱🇺</h3>
<p class="sub" style="margin-bottom:12px">Devseis is a Luxembourg-based consultancy and accredited training provider, founded and directed by Dr. Muhammad Umer Wasim (PhD).
We help SMEs, startups and technology companies assess and reduce cyber and compliance risk, and explain it in plain language for management.</p>
<div class="svc-grid">
<div class="svc"><b>EU AI Act readiness</b><span>Classification, gap analysis, compliance plan</span></div>
<div class="svc"><b>GDPR</b><span>CNPD-CARPA compliance, RoPA, DPIA</span></div>
<div class="svc"><b>ISO/IEC 27001</b><span>ISMS implementation and certification readiness</span></div>
<div class="svc"><b>NIS2 · DORA · CRA</b><span>Regulatory readiness</span></div>
<div class="svc"><b>Risk assessment</b><span>MONARC and EBIOS Risk Manager</span></div>
<div class="svc"><b>Technical audits</b><span>Architecture, code, configuration, penetration testing of SaaS and AI systems</span></div>
</div>
<div class="badges">
<span class="badge">🏆 Cyber4Growth Laureate 2021</span>
<span class="badge">🏅 EU Cybersecurity STARtup Award finalist 2021</span>
<span class="badge">🚀 Fit 4 Start 2022</span>
<span class="badge">🎓 Accredited training provider</span>
</div>
<div class="cta-btns" style="justify-content:flex-start;margin-top:14px">
<a class="primary devseis-call" href="mailto:umer.wasim@devseis.com?subject=Free%20AI%20Act%20readiness%20call%20(via%20Caveat)">📅 Book a free 30-min AI Act readiness call</a>
<a class="ghost" href="https://devseis.com" target="_blank" rel="noopener" style="text-decoration:none">🌐 devseis.com</a>
<a class="ghost" href="https://www.linkedin.com/company/12603388" target="_blank" rel="noopener" style="text-decoration:none">💼 Follow Devseis on LinkedIn</a>
</div>
<h4 style="margin:18px 0 8px;font-size:13px;text-transform:uppercase;letter-spacing:.05em;color:var(--muted)">More from Devseis on Hugging Face</h4>
<div class="more-grid">
<a class="more" href="https://huggingface.co/spaces/Devseis/Nox" target="_blank" rel="noopener"><b>🤖 Nox</b><span>A free general-purpose AI assistant on free-tier infrastructure</span></a>
<a class="more" href="https://huggingface.co/datasets/Devseis/eu-ai-act-risk-classification" target="_blank" rel="noopener"><b>📊 EU AI Act risk classification</b><span>Reference table of use cases, tiers and citations</span></a>
<a class="more" href="https://huggingface.co/datasets/Devseis/nis2-dora-cra-applicability" target="_blank" rel="noopener"><b>📋 NIS2 / DORA / CRA applicability</b><span>Which EU cyber law applies, by sector and product</span></a>
<a class="more" href="https://huggingface.co/datasets/Devseis/gdpr-ropa-dpia-templates" target="_blank" rel="noopener"><b>📝 GDPR RoPA & DPIA templates</b><span>Article 30 and Article 35 field templates</span></a>
<a class="more" href="https://huggingface.co/datasets/Devseis/devseis-ai-act-classifier-v6-data" target="_blank" rel="noopener"><b>🧪 Classifier v6 data</b><span>366 law-grounded scenarios behind this page</span></a>
<a class="more" href="https://huggingface.co/collections/Devseis/eu-ai-act-and-ai-governance-toolkit-curated-by-devseis-6aba57c0c9f48f5baf62d3be" target="_blank" rel="noopener"><b>🗂️ AI governance toolkit</b><span>Curated collection of datasets and models</span></a>
</div>
</div>
</section>
<!-- ================= TAB 3: STORY ================= -->
<section id="panel-story" role="tabpanel" aria-labelledby="tab-story" hidden>
<p class="eyebrow">How this was built</p>
<h2 class="section-title">Six versions, three wrong turns, one honest result</h2>
<p class="lead">What we tried, what happened, and what each step taught us — including the mistakes.</p>
<div class="timeline" id="timeline"></div>
<div id="devseisStory"></div>
</section>
<!-- ================= TAB 4: LEARN ================= -->
<section id="panel-learn" role="tabpanel" aria-labelledby="tab-learn" hidden>
<div class="learn">
<aside class="lesson-nav" aria-label="Lessons">
<div class="progress">
<div class="lbl"><span>Your progress</span><span id="progressTxt">0 / 0</span></div>
<div class="progress-track"><div class="progress-fill" id="progressFill" style="width:0%"></div></div>
</div>
<button class="ghost nav-toggle" id="navToggle" type="button" aria-expanded="false">☰ All lessons</button>
<ol class="lesson-list" id="lessonList"></ol>
</aside>
<article class="lesson" id="lesson" aria-live="polite"></article>
</div>
</section>
<footer class="page">
<span>Experimental research demo · not legal advice.</span>
<span><a href="privacy.html">Privacy</a> · <button class="link-btn" type="button" id="consentSettings">Analytics settings</button> · <a href="https://huggingface.co/Devseis" target="_blank" rel="noopener">More by Devseis</a> · <a href="https://www.linkedin.com/company/12603388" target="_blank" rel="noopener">LinkedIn</a></span>
</footer>
</div>
<div id="printSheet" aria-hidden="true"></div>
<div class="consent" id="consent" role="dialog" aria-labelledby="consentText" hidden>
<p id="consentText">May we use <b>Microsoft Clarity</b> to see how visitors use this page (clicks, scrolling, session recordings)? It sets cookies and is <b>off unless you agree</b>. What you type in the checker is not needed for this, and the page works either way.</p>
<div class="c-btns">
<button type="button" id="consentDecline">Decline</button>
<button type="button" id="consentAccept">Accept analytics</button>
</div>
<p style="margin:10px 0 0"><a href="privacy.html">Privacy details</a> · you can change this any time from the page footer.</p>
</div>
<script>
// Analytics consent. Clarity is loaded ONLY after an explicit "Accept"; nothing is sent to Microsoft before that.
// The choice is kept in this browser for 12 months, then we ask again. Withdrawing is as easy as agreeing.
(function () {
var KEY = "caveat-consent", MAX_AGE = 365 * 24 * 3600 * 1000;
function read() {
try { var v = JSON.parse(localStorage.getItem(KEY)); return v && Date.now() - v.at < MAX_AGE ? v.choice : null; } catch (e) { return null; }
}
function save(choice) { try { localStorage.setItem(KEY, JSON.stringify({ choice: choice, at: Date.now() })); } catch (e) {} }
function loadClarity() {
if (window.__clarityLoaded) return; window.__clarityLoaded = true;
(function(c,l,a,r,i,t,y){
c[a]=c[a]||function(){(c[a].q=c[a].q||[]).push(arguments)};
t=l.createElement(r);t.async=1;t.src="https://www.clarity.ms/tag/"+i;
y=l.getElementsByTagName(r)[0];y.parentNode.insertBefore(t,y);
})(window, document, "clarity", "script", "yqfpt9xwwk");
window.clarity("consentv2", { ad_Storage: "denied", analytics_Storage: "granted" });
}
function clearClarityCookies() {
var host = location.hostname, parts = host.split(".");
["_clck", "_clsk"].forEach(function (n) {
for (var i = 0; i < parts.length - 1; i++) {
var d = parts.slice(i).join(".");
document.cookie = n + "=; Max-Age=0; path=/; domain=." + d;
}
document.cookie = n + "=; Max-Age=0; path=/";
});
}
var banner = document.getElementById("consent");
document.getElementById("consentAccept").addEventListener("click", function () { save("granted"); banner.hidden = true; loadClarity(); });
document.getElementById("consentDecline").addEventListener("click", function () {
var wasOn = window.__clarityLoaded; save("denied"); banner.hidden = true;
if (wasOn) { try { window.clarity("consentv2", { ad_Storage: "denied", analytics_Storage: "denied" }); } catch (e) {} clearClarityCookies(); location.reload(); }
});
document.getElementById("consentSettings").addEventListener("click", function () { banner.hidden = false; document.getElementById("consentDecline").focus(); });
var c = read();
if (c === "granted") loadClarity();
else { clearClarityCookies(); if (c !== "denied") banner.hidden = false; } // also removes cookies left by the old always-on snippet
})();
</script>
<script type="module">
import { pipeline, env } from "./vendor/transformers-3.0.0/transformers.min.js"; // self-hosted, byte-identical to npm 3.0.0
import { gpaiCheck } from "./gpai_rule.js";
// Part 1's model ships inside this Space (models/devseis-ai-act-classifier-v6/, a copy of Devseis/devseis-ai-act-classifier-v6), so nothing is fetched from another repo.
env.allowLocalModels = true;
env.allowRemoteModels = false;
env.localModelPath = "./models/";
// The WebAssembly engine is self-hosted too (the library would otherwise fetch it from jsDelivr).
env.backends.onnx.wasm.wasmPaths = new URL("./vendor/transformers-3.0.0/", location.href).href;
const PART2_ENDPOINT = "https://devseis-aiact-reasoning-proxy.umer-wasim.workers.dev";
const TIERS = ["minimal_risk", "limited_risk", "high_risk", "prohibited"];
const $ = (id) => document.getElementById(id);
const store = {
get(k, d) { try { const v = localStorage.getItem(k); return v === null ? d : JSON.parse(v); } catch (e) { return d; } },
set(k, v) { try { localStorage.setItem(k, JSON.stringify(v)); } catch (e) {} },
};
// ================= tier meanings =================
const TIER = {
prohibited: { name: "Prohibited", meaning: "Banned in the EU. It can't be sold or used (Article 5)." },
high_risk: { name: "High risk", meaning: "Allowed, but with strict duties before launch: risk checks, documentation, human oversight." },
limited_risk: { name: "Limited risk", meaning: "Allowed. People must be told they're dealing with AI or AI-made content (Article 50)." },
minimal_risk: { name: "Minimal risk", meaning: "No special AI Act duties." },
gpai: { name: "General-purpose AI model", meaning: "Duties for the company that makes the model: documentation, copyright policy, training-data summary (Article 53)." },
gpai_systemic_risk: { name: "GPAI with systemic risk", meaning: "Extra duties for the most powerful models: safety testing, incident reporting, cybersecurity (Article 55)." },
uncertain: { name: "Uncertain", meaning: "The model is split between tiers. This case needs expert review." },
};
const pillClass = (t) => "tier-" + t;
// ================= glossary =================
const GLOSSARY = {
"AI model": ["A computer program that learned patterns from examples, instead of being given step-by-step rules.", "A spam filter that learned from millions of emails what spam looks like.", 1],
"input": ["What you give the model — here, a text describing an AI system.", "\"An app that ranks job applicants.\"", 1],
"output": ["What the model gives back — here, one of four risk tiers.", "\"High risk\".", 1],
"prediction": ["The model's answer. It's a guess based on patterns, not a fact.", "The model predicts \"high risk\" with 92% confidence.", 1],
"EU AI Act": ["The European Union's law on artificial intelligence (Regulation 2024/1689). It sorts AI systems by how much harm they could do.", "Social scoring is banned; CV screening is allowed with strict duties.", 2],
"prohibited": ["The top tier: practices so harmful they are banned outright (Article 5).", "Scoring citizens on their social behaviour; untargeted scraping of face images.", 2],
"high risk": ["Allowed, but only with strict duties before launch: risk management, good data, documentation, human oversight (Annex III, Annex I).", "CV screening, credit scoring, AI inside medical devices.", 2],
"limited risk": ["Allowed, but people must be told: that they're talking to an AI, or that content is AI-made (Article 50).", "A customer-service chatbot or voice agent; a deepfake video.", 2],
"minimal risk": ["Everything else. No special AI Act duties.", "A movie recommender, a spam filter.", 2],
"GPAI": ["General-purpose AI model: a big model that can do many tasks and that others build products on.", "A large language model released through an API.", 2],
"FLOPs": ["Floating-point operations: a count of the calculations used to train a model. The AI Act treats more than 10^25 as a sign of a very powerful model.", "\"Trained with 3×10^25 FLOPs\" → presumed systemic risk.", 10],
"classification": ["Sorting things into fixed boxes. The model reads a text and picks one box.", "Sorting emails into spam / not spam.", 3],
"label": ["The correct answer attached to an example, so the model can learn from it.", "\"CV screening app\" → label: high risk.", 3],
"confidence": ["How sure the model says it is, from 0 to 100%. High confidence is not the same as being right.", "92% confident — and still wrong sometimes.", 3],
"dataset": ["A collection of examples used to teach and test a model. Often just a spreadsheet: one column of text, one of labels.", "1,310 rows describing AI systems, each with a tier.", 4],
"scenario": ["One distinct use case. We wrote each scenario 3 ways, so the real number of examples is the scenario count, not the row count.", "366 scenarios became 1,310 rows.", 4],
"synthetic data": ["Examples written on purpose (here, by an AI) rather than collected from the real world. Cheap and fast, but tidier than reality.", "All of Caveat's examples are synthetic.", 4],
"template": ["A fixed opening phrase added to a scenario to make variations.", "\"Our client wants to deploy…\" + the scenario.", 4],
"training set": ["The examples the model learns from — its homework.", "242 scenarios (938 rows).", 5],
"validation set": ["Examples used while building, to choose settings — the practice exam.", "54 scenarios.", 5],
"test set": ["Examples locked away until the very end and used once to measure the final model — the real exam.", "70 scenarios the model never saw.", 5],
"data leakage": ["When test examples (or near-copies) sneak into training. The score looks great but is fake, like seeing the exam in advance.", "The same scenario, reworded, in both training and test.", 5],
"pre-trained model": ["A model that already learned general language from huge amounts of text, before you ever touch it.", "legal-bert-small read lots of legal text, including EU legislation.", 6],
"fine-tuning": ["Taking a pre-trained model and training it a little more on your own examples, so it learns your specific task.", "Teaching legal-bert-small the four AI Act tiers.", 6],
"base model": ["The pre-trained model you start fine-tuning from.", "DistilBERT, legal-bert-small, bge-small.", 6],
"epoch": ["One full pass through all training examples. Models usually need several.", "We allowed up to 15 epochs and kept the best one.", 6],
"learning rate": ["How big a step the model takes when it corrects itself. Too big and it overshoots; too small and it barely learns.", "We tried 0.00002, 0.00003 and 0.00005.", 6],
"accuracy": ["The share of all answers that are right. Easy to understand, but it can hide the mistakes that matter most.", "80 right out of 100 → 80%.", 7],
"recall": ["Out of all cases that truly belong to a tier, the share the model found. For banned practices, this is the number that matters most.", "11 banned test cases, 10 caught → 91% recall.", 7],
"precision": ["Out of all the times the model said a tier, the share that were right.", "It said \"prohibited\" 12 times, 10 were → 83% precision.", 7],
"macro-F1": ["A balanced score (0 to 1) that averages how well the model does on every tier, so it can't score well by ignoring a small tier.", "0.85 for v6.", 7],
"confusion matrix": ["A table of true answers versus the model's answers. It shows exactly which mistakes happen.", "Row \"prohibited\", column \"high risk\" = banned cases called high risk.", 7],
"severe error": ["Our name for the worst mistake: calling a banned practice minimal or limited risk.", "v6: 0 in the test, 1 on differently worded text.", 7],
"random seed": ["A number that fixes the random choices during training. Different seeds give slightly different models — so we train 3 and average.", "Seeds 42, 43 and 44.", 8],
"confidence interval": ["A range the true score probably sits in, given how few test examples there are. Wide range = uncertain.", "Balanced score 0.85, range 0.77–0.91.", 8],
"noise": ["Differences in scores that come from chance, not real improvement.", "Two models 3 points apart on 61 test cases: probably noise.", 8],
"overfitting": ["When a model memorises its training examples instead of learning the general idea, so it does worse on new cases.", "Perfect on homework, poor on the exam.", 8],
"out-of-distribution": ["Inputs that look different from what the model was trained on — a different writing style, new topics.", "Short sales-pitch style text vs our tidy templates.", 9],
"coverage gap": ["A kind of case the training data never included, so the model has no pattern for it.", "No phone agents in training → every phone agent called minimal risk.", 9],
"label error": ["An example with the wrong answer attached. The model learns the mistake, and a wrong test answer distorts the score.", "Stadium face ID wrongly labelled prohibited.", 9],
"rule-based": ["Hand-written if-then logic instead of a learned model. Exact and explainable, but only covers what you wrote down.", "If compute > 10^25 FLOPs → systemic-risk GPAI.", 10],
"quantization": ["Storing a model's numbers with less precision to make the file much smaller, like saving a photo as a smaller JPEG.", "140 MB → 36 MB, same answer 98.6% of the time.", 11],
"ONNX": ["A standard file format for AI models, so they can run in many places — including a web browser.", "model_quantized.onnx", 11],
"inference": ["Using a trained model to get an answer. Training is learning; inference is answering.", "Pressing Check runs inference in your browser.", 11],
"static site": ["A website that's just files — no server running code. Very cheap or free to host.", "This page is a static Hugging Face Space.", 11],
"model card": ["A public document describing a model: what it's for, how it was trained, how well it does, and where it fails.", "The v6 model card.", 12],
};
const GLOSS_KEYS = Object.keys(GLOSSARY);
const findTerm = (t) => GLOSS_KEYS.find((k) => k.toLowerCase() === String(t).toLowerCase());
let pop = null;
function closePop() { if (pop) { pop.remove(); pop = null; } }
function openPop(btn) {
closePop();
const key = findTerm(btn.dataset.term);
if (!key) return;
const [def, eg, lessonNo] = GLOSSARY[key];
pop = document.createElement("div");
pop.className = "popover"; pop.setAttribute("role", "dialog"); pop.setAttribute("aria-label", key);
pop.innerHTML = `<h5></h5><p class="d"></p><p class="eg"><b>Example:</b> <span></span></p>` +
(lessonNo ? `<a class="more" href="#learn-${lessonNo}">Learn more in lesson ${lessonNo} →</a>` : "");
pop.querySelector("h5").textContent = key;
pop.querySelector(".d").textContent = def;
pop.querySelector(".eg span").textContent = eg;
document.body.appendChild(pop);
const r = btn.getBoundingClientRect(), pw = pop.offsetWidth;
let left = r.left + window.scrollX;
left = Math.max(8 + window.scrollX, Math.min(left, window.scrollX + document.documentElement.clientWidth - pw - 8));
pop.style.left = left + "px";
pop.style.top = (r.bottom + window.scrollY + 8) + "px";
pop.querySelector("a.more")?.addEventListener("click", closePop);
}
document.addEventListener("click", (e) => {
const t = e.target.closest(".term");
if (t) { e.preventDefault(); openPop(t); return; }
if (pop && !e.target.closest(".popover")) closePop();
});
document.addEventListener("keydown", (e) => { if (e.key === "Escape") closePop(); });
const T = (key, shown) => `<button class="term" type="button" data-term="${key}">${shown || key}</button>`; // T(glossary key, optional display text)
// ================= theme =================
const root = document.documentElement;
const savedTheme = store.get("caveat-theme", null);
if (savedTheme === "light" || savedTheme === "dark") root.setAttribute("data-theme", savedTheme);
$("themeToggle").addEventListener("click", () => {
const current = root.getAttribute("data-theme");
const effective = current || (window.matchMedia("(prefers-color-scheme: dark)").matches ? "dark" : "light");
const next = effective === "dark" ? "light" : "dark";
root.setAttribute("data-theme", next); store.set("caveat-theme", next);
});
// ================= tabs + routing =================
const TABS = ["check", "results", "story", "learn"];
function showTab(name, { scrollTo } = {}) {
TABS.forEach((t) => {
$("panel-" + t).hidden = t !== name;
$("tab-" + t).setAttribute("aria-selected", String(t === name));
});
closePop();
if (scrollTo) setTimeout(() => document.getElementById(scrollTo)?.scrollIntoView({ behavior: "smooth", block: "start" }), 30);
else window.scrollTo({ top: 0 });
}
document.querySelectorAll(".tab-btn").forEach((b) => b.addEventListener("click", () => {
const t = b.dataset.tab;
history.replaceState(null, "", t === "check" ? location.pathname : "#" + (t === "learn" ? "learn-" + currentLesson : t));
showTab(t);
}));
function route() {
const h = location.hash.replace("#", "");
if (h.startsWith("learn")) { const n = parseInt(h.split("-")[1], 10); showTab("learn"); renderLesson(isNaN(n) ? currentLesson : n); return; }
if (h.startsWith("story")) { showTab("story", { scrollTo: h.includes("-") ? "ch-" + h.split("-")[1] : null }); return; }
if (h === "results") { showTab("results"); return; }
if (h === "dates") { showTab("results", { scrollTo: "dates" }); return; }
showTab("check");
}
window.addEventListener("hashchange", route);
// ================= TAB 1: check =================
const examples = [
"Real-time facial recognition on public street cameras so police can identify passers-by for general policing.",
"An app that screens job applicants' CVs and ranks them before a human recruiter sees them.",
"A phone-based voice assistant that takes restaurant orders in conversation with callers.",
"A service that recommends which movie to watch next based on viewing history.",
"We are releasing a general-purpose language model trained with 3×10^25 FLOPs to EU developers.",
"Office cameras that read employees' facial expressions to track their mood during meetings.",
"A bank's AI that calculates each applicant's credit score and approves or rejects their loan.",
"A marketing video of our real CEO announcing a product, generated with a cloned voice and synthetic footage.",
];
const scenarioEl = $("scenario");
const chips = document.querySelectorAll(".chip");
chips.forEach((chip) => chip.addEventListener("click", () => {
scenarioEl.value = examples[Number(chip.dataset.ex)];
chips.forEach((c) => c.classList.toggle("active", c === chip));
if (window.matchMedia("(hover: hover)").matches) scenarioEl.focus({ preventScroll: true }); // no keyboard pop-up on phones
}));
scenarioEl.addEventListener("input", () => chips.forEach((c) => c.classList.remove("active")));
const runBtn = $("runBtn"), runHint = $("runHint");
// The trained model (~36 MB, plus the ~22 MB engine) is downloaded only when someone shows intent to use the
// checker — focusing the text box, tapping an example, or pressing Check — not for visitors who only read.
const MODEL_ID = "devseis-ai-act-classifier-v6";
// Below this top probability the trained model's answer is shown as "uncertain". A stricter cut-off was tested on
// validation data and rejected: this model's mistakes are mostly confident, so it would hide more right answers than wrong ones.
const UNSURE_BELOW = 0.5;
const HINT = runHint.textContent;
let classifier = null, modelFailed = false, modelPromise = null, loadPct = null;
function ensureModel() {
if (!modelPromise) {
runHint.textContent = "Preparing the model in your browser…";
modelPromise = pipeline("text-classification", MODEL_ID, {
dtype: "q8",
progress_callback: (pr) => {
if (pr.status === "progress" && pr.file && /\.(onnx|wasm)$/.test(pr.file) && pr.total) {
loadPct = Math.round((pr.loaded / pr.total) * 100);
runHint.textContent = `Preparing the model in your browser… ${loadPct}% (first visit only)`;
const st = $("p1status");
if (!$("p1result").hidden || st.hidden) return;
if (results.p1 === null) st.innerHTML = `<span class="spinner"></span> Downloading the model… ${loadPct}% (first visit only)`;
}
},
}).then((clf) => { classifier = clf; runHint.textContent = HINT; return clf; })
.catch((err) => {
modelFailed = true;
runHint.textContent = "The trained model couldn't load (" + err.message + "). The reasoning AI still works.";
throw err;
});
}
return modelPromise;
}
const warmUp = () => { ensureModel().catch(() => {}); };
scenarioEl.addEventListener("focus", warmUp, { once: true });
scenarioEl.addEventListener("input", warmUp, { once: true });
chips.forEach((chip) => chip.addEventListener("click", warmUp, { once: true }));
const results = { p1: null, p2: null, text: "", why: {}, run: 0 };
function showVerdictNow(part, tier, rationale) {
results.why[part] = rationale || "";
$(part + "status").hidden = true; $(part + "result").hidden = false;
const pill = $(part + "pill");
pill.textContent = TIER[tier]?.name || tier;
pill.className = "verdict-pill " + pillClass(tier);
$(part + "meaning").textContent = TIER[tier]?.meaning || "";
$(part + "rationale").textContent = rationale || "";
results[part] = tier; afterAnswers();
}
function showErrorNow(part, msg) {
$(part + "status").hidden = false; $(part + "status").textContent = msg; $(part + "result").hidden = true;
results[part] = "error"; afterAnswers();
}
function updateAgree() {
const el = $("agree"), { p1, p2 } = results;
if (p1 && p2) setTimeout(recentreIfUntouched, 50);
if (!p1 || !p2 || p1 === "error" || p2 === "error") { el.hidden = true; return; }
el.hidden = false;
if (p1 === "uncertain") { el.className = "agree no"; el.textContent = "The trained model is split on this case — a sign it needs an expert's eye."; return; }
el.className = "agree " + (p1 === p2 ? "yes" : "no");
el.textContent = p1 === p2
? "Both agree. That's reassuring, but still not legal advice."
: "They disagree — a sign this case needs an expert's eye.";
}
function afterAnswers() { updateAgree(); renderCta(); }
// ---- call to action: expert review, pre-filled email, printable summary ----
const CONTACT = "umer.wasim@devseis.com";
const PAGE_URL = "https://huggingface.co/spaces/Devseis/Caveat";
const SEVERITY = ["minimal_risk", "limited_risk", "gpai", "high_risk", "gpai_systemic_risk", "prohibited"];
const NEXT_STEPS = {
prohibited: "Do not place or use this system in the EU in this form. Check with an expert whether an exception applies or the design can change.",
high_risk: "Before launch, plan for risk management, data governance, technical documentation, logging, human oversight, accuracy and robustness, conformity assessment and EU database registration.",
limited_risk: "Make sure people know they are interacting with an AI system, and that AI-generated or manipulated content is labelled (Article 50).",
minimal_risk: "No specific AI Act obligations, but other laws such as the GDPR still apply. Voluntary codes of conduct are encouraged.",
gpai: "As the model provider: technical documentation, information for downstream providers, a copyright policy and a public training-content summary (Article 53).",
gpai_systemic_risk: "In addition to the standard GPAI duties: model evaluation and adversarial testing, systemic-risk assessment and mitigation, incident reporting and cybersecurity (Article 55).",
uncertain: "The trained model could not settle on a tier. Have the case reviewed by an expert before relying on any classification.",
};
const track = (name) => { try { if (window.__clarityLoaded && window.clarity) window.clarity("event", name); } catch (e) {} };
const tierName = (t) => (t && t !== "error" ? TIER[t]?.name || t : "not available");
const worstOf = (ts) => ts.filter((t) => t !== "uncertain").sort((a, b) => SEVERITY.indexOf(b) - SEVERITY.indexOf(a))[0] || "uncertain";
function mailHref() {
const { p1, p2, text } = results;
let body = "Hello Devseis,\r\n\r\nI'd like to book a free 30-minute AI Act readiness call.\r\n";
if ($("ctaInclude").checked && text) {
body += `\r\nMy use case (checked on Caveat):\r\n"${text}"\r\n\r\nTrained model: ${tierName(p1)}\r\nReasoning AI: ${tierName(p2)}\r\n`;
}
body += "\r\nName:\r\nCompany:\r\nBest times for a call:\r\n";
return `mailto:${CONTACT}?subject=${encodeURIComponent("Free AI Act readiness call (via Caveat)")}&body=${encodeURIComponent(body)}`;
}
function renderCta() {
const cta = $("cta"), { p1, p2 } = results;
if (!p1 || !p2) { cta.hidden = true; return; }
const ok = [p1, p2].filter((t) => t !== "error");
if (!ok.length) { cta.hidden = true; return; }
const worst = worstOf(ok);
let lead;
if (ok.length < 2) lead = "One of the checks couldn't run. An expert can give you a proper answer.";
else if (p1 === "uncertain") lead = "The trained model is split on this one. That's exactly where expert review matters.";
else if (p1 !== p2) lead = "The two models disagree. That's exactly where expert review matters.";
else if (["prohibited", "high_risk", "gpai_systemic_risk"].includes(worst)) lead = `Both say “${tierName(worst)}”. Getting this wrong is costly, so have it checked by an expert.`;
else lead = `Both say “${tierName(worst)}”. Want a second opinion before you launch?`;
$("ctaLead").textContent = lead;
$("ctaMail").href = mailHref();
cta.hidden = false;
}
$("ctaInclude").addEventListener("change", () => { $("ctaMail").href = mailHref(); });
$("ctaMail").addEventListener("click", () => track("cta_book_call"));
function buildPrintSheet() {
const { p1, p2, text, why } = results;
const esc = (v) => String(v ?? "").replace(/[&<>"]/g, (c) => ({ "&": "&amp;", "<": "&lt;", ">": "&gt;", '"': "&quot;" }[c]));
const row = (label, meta, t, w) => `<tr><td><b>${label}</b><br><span style="color:#666;font-size:9.5pt">${meta}</span></td>
<td><b>${esc(tierName(t))}</b><br>${esc(t && t !== "error" ? TIER[t]?.meaning : "This check could not run.")}</td></tr>`;
const tiers = [...new Set([p1, p2].filter((t) => t && t !== "error"))].sort((a, b) => SEVERITY.indexOf(b) - SEVERITY.indexOf(a));
const agreeLine = p1 && p2 && p1 !== "error" && p2 !== "error" ? (p1 === p2 ? "Both models agree." : "The two models disagree — treat this case as uncertain.") : "";
const date = new Date().toLocaleDateString("en-GB", { day: "numeric", month: "long", year: "numeric" });
$("printSheet").innerHTML = `
<div class="ps-head"><span class="ps-brand">DEVSEIS</span><span class="ps-sub">Caveat · EU AI Act risk check · ${esc(date)}</span></div>
<h1>EU AI Act risk check — summary</h1>
<p class="ps-sub" style="margin:0 0 12px">Produced with Caveat, an experimental demonstration by Devseis. Not legal advice.</p>
<h2>The system checked</h2>
<p class="ps-quote">${esc(text)}</p>
<h2>Results</h2>
<table><tr><th style="width:38%">Checked by</th><th>Result and what it means</th></tr>
${row("Devseis AI Act Classifier v6", "fine-tuned model (35M parameters), built on legal-bert-small", p1, why.p1)}
${row("Reasoning AI", "Llama 3.1 8B Instruct, given the AI Act rules", p2, why.p2)}
</table>
${agreeLine ? `<p style="margin:8px 0 0"><b>${esc(agreeLine)}</b></p>` : ""}
<h2>Possible next steps</h2>
<ul>${tiers.map((t) => `<li><b>If ${esc(tierName(t))}:</b> ${esc(NEXT_STEPS[t] || "")}</li>`).join("")}
<li><b>Get it confirmed:</b> automated checks miss context. Book a free 30-minute AI Act readiness call with Devseis: ${esc(CONTACT)}.</li></ul>
<div class="ps-note"><b>Important:</b> these results come from experimental AI models that make mistakes — on its test set the trained model
misses about 1 in 100 banned practices (more on unfamiliar wording) and over-flags many harmless systems. The EU AI Act classification of a real system depends on its full
facts and context. This summary is not legal advice.</div>
<div class="ps-foot">Devseis Consultancy SARL-S · RCS B241407 · Ettelbruck, Luxembourg · devseis.com · ${esc(CONTACT)}<br>${esc(PAGE_URL)}</div>`;
}
$("printBtn").addEventListener("click", () => { buildPrintSheet(); track("print_summary"); window.print(); });
async function runPart1(text, run) {
const showVerdict = (...a) => run === results.run && showVerdictNow(...a);
const showError = (...a) => run === results.run && showErrorNow(...a);
try {
const g = gpaiCheck(text);
if (g && g.tier !== "downstream") {
showVerdict("p1", g.tier, `Decided by a rule, not the trained model (${g.citation}; confidence ${g.confidence}). ` + g.reasons.join(" "));
return;
}
if (!classifier) await ensureModel().catch(() => { throw new Error("the trained model isn't available"); });
const ranked = await classifier(text, { top_k: TIERS.length });
const [top, second] = ranked;
const note = g ? " Note: " + g.reasons.join(" ") : "";
if (top.score < UNSURE_BELOW) { // genuinely split between tiers
showVerdict("p1", "uncertain", `Split between ${TIER[top.label].name.toLowerCase()} (${(top.score * 100).toFixed(0)}%) and ${TIER[second.label].name.toLowerCase()} (${(second.score * 100).toFixed(0)}%). A learned pattern-match, not legal reasoning.` + note);
return;
}
showVerdict("p1", top.label, `Model confidence ${(top.score * 100).toFixed(0)}%. This is a learned pattern-match, not legal reasoning.` + note);
} catch (err) { showError("p1", "Couldn't check: " + err.message); }
}
async function runPart2(text, run) {
const showVerdict = (...a) => run === results.run && showVerdictNow(...a);
const showError = (...a) => run === results.run && showErrorNow(...a);
try {
const res = await fetch(PART2_ENDPOINT, { method: "POST", headers: { "Content-Type": "application/json" }, body: JSON.stringify({ text }) });
const data = await res.json();
if (!res.ok || data.error) throw new Error(data.error || "request failed");
showVerdict("p2", data.tier, data.rationale || "");
} catch (err) { showError("p2", "Couldn't check: " + err.message); }
}
// Bring the results into view: centred when they fit on screen, top-aligned when taller (stacked cards on phones).
// The cards grow when answers arrive, so we re-centre once both are in — unless the visitor has scrolled meanwhile.
let autoScrollY = null, scrolling = false, recentreQueued = false;
function scrollToResults() {
$("panel-check").classList.add("has-results");
requestAnimationFrame(() => {
const el = $("results");
const reduce = window.matchMedia("(prefers-reduced-motion: reduce)").matches;
const fits = el.offsetHeight + 24 < window.innerHeight;
el.scrollIntoView({ behavior: reduce ? "auto" : "smooth", block: fits ? "center" : "start" });
autoScrollY = null; scrolling = true;
setTimeout(() => {
scrolling = false; autoScrollY = window.scrollY;
if (recentreQueued) { recentreQueued = false; recentreIfUntouched(); }
}, reduce ? 50 : 700);
});
}
function recentreIfUntouched() {
if (scrolling) { recentreQueued = true; return; }
if (autoScrollY !== null && Math.abs(window.scrollY - autoScrollY) < 4) { autoScrollY = null; scrollToResults(); }
}
runBtn.addEventListener("click", () => {
const text = scenarioEl.value.trim() || examples[1];
if (!scenarioEl.value.trim()) scenarioEl.value = text;
$("results").hidden = false;
results.p1 = results.p2 = null; results.text = text; results.why = {}; results.run++; afterAnswers();
["p1", "p2"].forEach((p) => { $(p + "status").hidden = false; $(p + "status").innerHTML = '<span class="spinner"></span> Thinking…'; $(p + "result").hidden = true; });
scrollToResults();
runPart1(text, results.run); runPart2(text, results.run);
});
// ================= TAB 2: key dates (verified against the Official Journal texts) =================
const DATES = [
["2024-08-01", "The AI Act enters into force (Regulation (EU) 2024/1689)."],
["2025-02-02", "Bans on prohibited practices (Art 5) and AI-literacy duties apply."],
["2025-08-02", "Rules for general-purpose AI models, governance and penalties apply."],
["2026-07-27", "The AI Omnibus (Regulation (EU) 2026/1744) enters into force."],
["2026-08-02", "General date of application, including the Art 50 transparency duties (chatbots, deepfakes, AI-generated content)."],
["2026-12-02", "New bans apply: AI that generates non-consensual intimate imagery or child sexual abuse material (Art 5(1)(ba), (bb)). Generative AI placed on the market before 2 Aug 2026 must mark its outputs (Art 50(2)).", "added by the AI Omnibus"],
["2027-08-02", "General-purpose AI models placed on the market before 2 Aug 2025 must comply."],
["2027-12-02", "High-risk obligations apply to Annex III systems (e.g. hiring, credit scoring, education, biometrics).", "changed by the Omnibus — was 2 Aug 2026"],
["2028-08-02", "High-risk obligations apply to AI in products under Annex I (e.g. medical devices, machinery, toys).", "changed by the Omnibus — was 2 Aug 2027"],
["2030-08-02", "High-risk AI systems already used by public authorities must comply."],
];
(function renderDates() {
const today = new Date().toISOString().slice(0, 10);
const nextIdx = DATES.findIndex(([d]) => d > today);
const fmt = (d) => new Date(d + "T00:00:00").toLocaleDateString("en-GB", { day: "numeric", month: "short", year: "numeric" });
$("datesList").innerHTML = DATES.map(([d, what, changed], k) => {
const cls = d <= today ? "past" : k === nextIdx ? "next" : "";
const tags = (d <= today ? '<span class="tagx tag-done">in effect</span>' : k === nextIdx ? '<span class="tagx tag-next">next</span>' : "") +
(changed ? `<span class="tagx tag-changed">${changed}</span>` : "");
return `<li class="${cls}"><span class="d">${fmt(d)}</span>${tags}<span class="what">${what}</span></li>`;
}).join("");
})();
// the Devseis block also closes the Story tab
$("devseisStory").appendChild($("devseisBlock").cloneNode(true)).removeAttribute("id");
document.querySelectorAll(".devseis-call").forEach((a) => a.addEventListener("click", () => track("cta_book_call_devseis")));
// ================= TAB 2: tier bars =================
const BARS = [["Prohibited", 0.991, "var(--danger)"], ["Limited risk", 0.967, "var(--info)"], ["High risk", 0.761, "var(--warn)"], ["Minimal risk", 0.735, "var(--good)"]];
$("tierBars").innerHTML = BARS.map(([n, v, c]) =>
`<div class="bar-row"><span>${n}</span><div class="bar-track"><div class="bar-fill" style="width:${v * 100}%;background:${c}"></div></div><span class="bar-val">${Math.round(v * 100)}%</span></div>`).join("");
// ================= TAB 3: story =================
const STORY = [
{ when: "The question", title: "Can a small AI sort AI systems into EU AI Act tiers?",
did: "Picked the four tiers of the EU AI Act as boxes, and set out to teach a small model to put a text description into the right box.",
happened: "We also decided the tool had to be free to run for anyone — no paid servers, no paid AI service.",
lesson: "Start with the question and the boxes, not the technology.", link: 2 },
{ when: "Version 1", title: "A first model on 401 examples",
did: `Fine-tuned a general ${T("base model")} (DistilBERT) on 401 AI-written examples — but they came from only 64 distinct ${T("scenario", "scenario")}s.`,
happened: "61% accuracy. Worse: it called 60% of banned practices merely \"high risk\" — a tool that says a banned practice just needs paperwork.",
stat: ["61%", "accuracy"], lesson: `Few distinct examples → the model memorised words (${T("overfitting")}).`, link: 8 },
{ when: "A different idea", title: "What if a big AI just reasons instead?",
did: "Gave a much larger AI the rules and asked it to reason through 12 test cases, with no training at all.",
happened: "12 of 12 matched — but the same AI wrote the test and graded itself, so it proves nothing. The live page uses a smaller free AI for this part.",
stat: ["12/12", "but self-graded"], lesson: "A result nobody independently checked is a hint, not evidence.", link: 5 },
{ when: "Version 2–3", title: "Real legal text — and more data made it worse",
did: "Rewrote the examples from the actual law and the European Commission's worked examples. Grew from 64 to 162, then 268 scenarios.",
happened: "Accuracy rose to 86%, then fell to 71% with more data. The real bug: the training script kept the last attempt, not the best one. Keeping the most balanced attempt gave 76.5% and caught 90% of banned cases.",
stat: ["90%", "banned caught"], lesson: `More data isn't automatically better; how you pick the final model matters (${T("macro-F1")}).`, link: 7 },
{ when: "Going live", title: "Free to run, on your own device",
did: `${T("quantization", "quantization")} shrank the model 268 → 67 MB, converted to ${T("ONNX")} so it runs in a web browser, hosted as a ${T("static site")}.`,
happened: "Costs Devseis nothing, whether 1 or 10,000 people visit. The reasoning AI runs on Cloudflare's free tier.",
stat: ["$0", "per month"], lesson: "The cheapest server is the visitor's own device.", link: 11 },
{ when: "The audit", title: "Six of our own answers were wrong",
did: "Checked every label against the Act. Six were wrong — including 3 test answers (a stadium face-ID system is high risk, not banned: the ban covers police use).",
happened: `With the answer key fixed, the old model's 76.5% became 74.7%. Also fixed 661 ungrammatical template rows.`,
stat: ["6", "wrong labels"], lesson: `Check the answer key before trusting the score (${T("label error")}).`, link: 9 },
{ when: "Choosing a base model", title: "Three candidates, one headline trap",
did: "Compared DistilBERT, legal-bert-small (pretrained on legal text) and SetFit, each trained 3 times with different seeds.",
happened: "SetFit had the best accuracy — and the worst habit: it labelled 16 banned cases \"minimal risk\". legal-bert-small was slightly better overall and half the size.",
stat: ["16", "banned → harmless"], lesson: `One headline number can hide the one mistake that matters (${T("recall")}).`, link: 7 },
{ when: "Version 4", title: "Wider coverage, and a blind spot",
did: "Added medical devices and machinery, the new bans on non-consensual intimate deepfakes and AI child-abuse material (from 2 Dec 2026), and profiling cases.",
happened: "Banned cases caught: 95%. But every phone and voice agent was called \"minimal risk\" — 0 out of 18 tries.",
stat: ["0/18", "phone agents right"], lesson: `Why? Every chatbot in training typed; none talked (${T("coverage gap")}).`, link: 9 },
{ when: "Version 5", title: "Fixing the gap",
did: "Added 23 scenarios: voice, phone, SMS, email and avatar agents, consented synthetic media, and look-alike cases that aren't covered.",
happened: "98% of banned cases caught, no banned case called harmless, phone agents mostly right (7 of 9 each), and a 36 MB download.",
stat: ["98%", "banned caught"], lesson: "Find the failure, find the missing examples, add them, measure again.", link: 9 },
{ when: "Version 6 — live now", title: "More look-alikes, more writing styles",
did: "Added 39 scenarios aimed at weak spots found on practice data: harmless systems in sensitive areas the Act leaves out, varied wording for banned face-scraping, consented synthetic media — and a second writing style for every training example.",
happened: "99% of banned test cases caught, 80% on unfamiliar wording (up from 76%), and the face-scraping miss fixed. Harmless systems are still over-flagged about 1 in 4 times.",
stat: ["80%", "on unfamiliar wording"], lesson: "Write examples the way real people write — not just one tidy template.", link: 9 },
{ when: "A rule, not a model", title: "Big general-purpose models are decided by a rule",
did: `Models trained with more than 10^25 ${T("FLOPs")} face extra duties. Small text models read numbers badly, so a hand-written ${T("rule-based", "rule")} handles these.`,
happened: "Tested on 655 descriptions: all correct, and it never fires on ordinary AI systems.",
stat: ["655/655", "correct"], lesson: "Use rules where the law is a number; use models where it's judgement.", link: 10 },
{ when: "What's next", title: "Still not a compliance tool",
did: "The test is small and synthetic. On differently worded text the model is weaker, and it missed untargeted face-scraping.",
happened: "The real test would be real system descriptions checked by qualified lawyers. Until then, this is a demonstration.",
lesson: "Design tools to fail safely: flag uncertainty, send hard cases to an expert.", link: 13 },
];
$("timeline").innerHTML = STORY.map((c, i) => `
<div class="chapter" id="ch-${i + 1}">
<div class="dot">${i + 1}</div>
<div class="when">${c.when}</div>
<h3>${c.title}</h3>
<div class="chapter-body">
<div class="ch-grid">
<div><h4>What we did</h4><p>${c.did}</p></div>
<div><h4>What happened</h4><p>${c.happened}</p>${c.stat ? `<div class="ch-stat"><b>${c.stat[0]}</b><span class="muted">${c.stat[1]}</span></div>` : ""}</div>
</div>
<div class="ch-lesson"><span>💡 ${c.lesson}</span><a href="#learn-${c.link}">Lesson ${c.link} →</a></div>
</div>
</div>`).join("");
// ================= TAB 4: learn =================
const pill = (tier) => `<span class="mini-pill ${pillClass(tier)}">${TIER[tier].name}</span>`;
const LESSONS = [
{ icon: "👋", title: "Welcome", min: 2, body: `
<p>This short course explains, in plain language, how an AI tool like Caveat is built — and how you could build one yourself. No coding or maths needed to follow along.</p>
<div class="analogy"><span class="ico">🧭</span><div><b>How it works:</b> 14 short lessons, each with an everyday comparison, a real example from this project, the key words, and a one-question practice check. At the end, pass a 12-question final assessment (75% or more) to get a certificate. Your progress is saved in this browser.</div></div>
<h3>What you'll be able to do by the end</h3>
<ul>
<li>Explain what an ${T("AI model")} is and how it learns from examples.</li>
<li>Understand the four risk tiers of the ${T("EU AI Act")}.</li>
<li>Read an AI result critically: what ${T("accuracy")} and ${T("recall")} really tell you.</li>
<li>Plan your own small classifier, step by step.</li>
</ul>
<div class="callout tip"><span class="ctitle">Tip</span><p>Any word with a dotted underline can be tapped for a quick explanation. The full list is in the Glossary at the end.</p></div>`,
terms: ["AI model", "EU AI Act"],
quiz: { q: "What do you need to follow this course?", opts: ["Programming skills", "Advanced maths", "Nothing — just curiosity"], a: 2, why: "Everything is explained from scratch. The do-it-yourself lesson points to no-code tools too." } },
{ icon: "🤖", title: "What is an AI model?", min: 4, body: `
<p>Normal software follows rules a person wrote: <i>"if the email contains 'lottery', mark it spam"</i>. An ${T("AI model")} is different: it is shown lots of examples and finds the patterns itself.</p>
<div class="analogy"><span class="ico">📬</span><div><b>Think of a new post-room worker.</b> Nobody gives them a rulebook for every letter. They sort a few hundred letters with a supervisor correcting them, and gradually get the feel for it. A model learns the same way — and, like the worker, can be confidently wrong on an unusual letter.</div></div>
<h3>Three words you'll see everywhere</h3>
<div class="example"><div class="ex-head">Example from Caveat</div><div class="ex-body">
<div class="ex-row"><span class="txt"><b>${T("input")}</b> — "An app that ranks job applicants' CVs."</span></div>
<div class="ex-row"><span class="txt"><b>${T("prediction")}</b> — the model's best guess</span>${pill("high_risk")}</div>
<div class="ex-row"><span class="txt"><b>${T("output")}</b> — shown to you, with how sure it is</span><span class="muted">confidence 92%</span></div>
</div></div>
<div class="callout warn"><span class="ctitle">Remember</span><p>A model doesn't understand the law. It recognises patterns in words. That's why Caveat shows its weaknesses up front.</p></div>`,
terms: ["AI model", "input", "output", "prediction"],
quiz: { q: "How does an AI model learn?", opts: ["Someone writes a rule for every case", "It finds patterns in many labelled examples", "It reads the law and understands it"], a: 1, why: "It learns from examples with answers attached — which is also why bad examples produce a bad model." } },
{ icon: "⚖️", title: "The EU AI Act in 5 minutes", min: 5, body: `
<p>The ${T("EU AI Act")} doesn't treat all AI the same. It sorts AI systems by how much harm they could do, and the duties grow with the risk.</p>
<div class="example"><div class="ex-head">The four tiers, from most to least serious</div><div class="ex-body">
<div class="ex-row"><span class="txt"><b>${T("prohibited", "prohibited")}</b> — banned. Example: social scoring of citizens; scraping faces from the internet to build a face database.</span>${pill("prohibited")}</div>
<div class="ex-row"><span class="txt"><b>${T("high risk", "high risk")}</b> — allowed with strict duties. Example: CV screening, credit scoring, AI in medical devices.</span>${pill("high_risk")}</div>
<div class="ex-row"><span class="txt"><b>${T("limited risk", "limited risk")}</b> — tell people it's AI. Example: a chatbot or phone agent; a deepfake video.</span>${pill("limited_risk")}</div>
<div class="ex-row"><span class="txt"><b>${T("minimal risk", "minimal risk")}</b> — no special duties. Example: a movie recommender, a spam filter.</span>${pill("minimal_risk")}</div>
</div></div>
<p>There's also a separate track for ${T("GPAI")} — big general-purpose models that others build on. Those duties fall on the company that makes the model.</p>
<div class="callout tip"><span class="ctitle">Dates matter</span><p>New bans on AI that creates non-consensual intimate images or child sexual abuse material were added by the "AI Omnibus" in 2026 and apply from 2 December 2026. See all the <a href="#dates">key dates</a>, including the high-risk deadlines the Omnibus moved.</p></div>
<div class="widget" data-widget="sort"></div>`,
terms: ["prohibited", "high risk", "limited risk", "minimal risk", "GPAI"],
quiz: { q: "A customer-service chatbot that talks to the public. Which tier?", opts: ["Prohibited", "High risk", "Limited risk", "Minimal risk"], a: 2, why: "People must be told they're talking to an AI (Article 50) — that's limited risk." } },
{ icon: "🗂️", title: "Classification: sorting into boxes", min: 3, body: `
<p>Caveat does ${T("classification")}: it reads a text and picks one of four fixed boxes. Every example it learned from came with the right box attached — its ${T("label")}.</p>
<div class="analogy"><span class="ico">🧺</span><div><b>Like sorting laundry.</b> Four baskets, one item at a time. The model doesn't invent a new basket — if something doesn't fit, it still has to pick one. That's why it can't say "this is two tiers at once".</div></div>
<p>The model also reports its ${T("confidence")}. It's useful, but treat it with care: a model can be 95% confident and wrong, especially on cases unlike anything it saw.</p>
<div class="example"><div class="ex-head">One training example</div><div class="ex-body">
<div class="ex-row"><span class="txt">"Our client wants to deploy an AI tool that scores loan applicants to approve or reject mortgages."</span>${pill("high_risk")}</div>
</div></div>`,
terms: ["classification", "label", "confidence"],
quiz: { q: "The model says \"high risk, 95% confident\". What does that mean?", opts: ["It's definitely high risk", "The model is sure of its guess — which can still be wrong", "95% of lawyers agree"], a: 1, why: "Confidence is the model's own certainty, not a guarantee." } },
{ icon: "📚", title: "Training data: the examples", min: 5, body: `
<p>A ${T("dataset")} is usually just a spreadsheet: one column of text, one column of labels. The quality of this spreadsheet decides almost everything.</p>
<h3>How Caveat's examples were made</h3>
<ol>
<li>Write one ${T("scenario", "scenario")} per real-world use case, based on the text of the law.</li>
<li>Give it the correct tier.</li>
<li>Add 3 variations using opening phrases — a ${T("template")} — like "Our client wants to deploy…" or "A startup is building…".</li>
</ol>
<div class="example"><div class="ex-head">One scenario, three rows</div><div class="ex-body">
<div class="ex-row"><span class="txt">"A startup is building an AI tool ranking job applicants…"</span>${pill("high_risk")}</div>
<div class="ex-row"><span class="txt">"Our client wants to deploy an AI tool ranking job applicants…"</span>${pill("high_risk")}</div>
<div class="ex-row"><span class="txt">"An AI tool ranking job applicants…"</span>${pill("high_risk")}</div>
</div></div>
<div class="callout warn"><span class="ctitle">Watch out</span><p>Three rows of the same scenario are not three examples — they're one idea reworded. Caveat has 1,310 rows but only 366 real scenarios. Always count the scenarios.</p></div>
<p>All of Caveat's data is ${T("synthetic data")}, written by an AI. It's fast and cheap, but real descriptions are messier — so real-world performance will be lower than the test scores.</p>`,
terms: ["dataset", "scenario", "template", "synthetic data"],
quiz: { q: "You have 300 rows, but each scenario appears 3 times. How many real examples?", opts: ["300", "100", "900"], a: 1, why: "300 rows ÷ 3 variations = 100 distinct scenarios. That's your real sample size." } },
{ icon: "✂️", title: "Homework, practice exam, real exam", min: 4, body: `
<p>You can't grade a student with the exact questions they practised on. So the examples are split three ways before training starts:</p>
<div class="widget" data-widget="split"></div>
<ul>
<li><b>${T("training set")}</b> — the homework. The model learns from these.</li>
<li><b>${T("validation set")}</b> — the practice exam. Used to choose settings, like how long to train.</li>
<li><b>${T("test set")}</b> — the real exam. Locked away and used <i>once</i>, at the very end.</li>
</ul>
<div class="callout bad"><span class="ctitle">The classic mistake</span><p>${T("data leakage")}: if a reworded copy of a test scenario is in the training set, the model has seen the exam. Caveat splits <b>by scenario</b>, so all 3 rows of a scenario always land in the same group.</p></div>`,
terms: ["training set", "validation set", "test set", "data leakage"],
quiz: { q: "Why must the test set be used only once, at the end?", opts: ["It's slow to run", "If you keep adjusting until the test score looks good, it stops being a fair exam", "The law requires it"], a: 1, why: "Tuning on the test set quietly turns it into more homework, and the score becomes too optimistic." } },
{ icon: "🎓", title: "Pre-trained models & fine-tuning", min: 5, body: `
<p>Nobody trains a language model from scratch for a task like this. You start from a ${T("pre-trained model")} that already learned general language from huge amounts of text, and then ${T("fine-tuning", "fine-tune")} it on your own examples.</p>
<div class="analogy"><span class="ico">👩‍🎓</span><div><b>Like hiring a law graduate.</b> They already read and write well and know legal vocabulary. You don't teach them English — you give them a few weeks of examples of <i>your</i> specific job.</div></div>
<h3>What we fine-tuned</h3>
<p>The ${T("base model")} is <code>legal-bert-small</code>: small enough for a browser, and pretrained on legal text including EU legislation. We compared it with two other candidates before choosing it. The fine-tuned result is Devseis's own model, the <b>Devseis AI Act Classifier</b>.</p>
<h3>Two settings that matter</h3>
<ul>
<li><b>${T("epoch", "Epochs")}</b> — how many times it studies all the examples. We allowed up to 15 and kept the best round.</li>
<li><b>${T("learning rate", "Learning rate")}</b> — how big a correction it makes after each mistake. We tried three values and chose using the practice exam only.</li>
</ul>
<div class="callout tip"><span class="ctitle">On a laptop</span><p>Fine-tuning Caveat's model took about 7 minutes per run on an ordinary laptop, with no special hardware.</p></div>`,
terms: ["pre-trained model", "fine-tuning", "base model", "epoch", "learning rate"],
quiz: { q: "What does fine-tuning do?", opts: ["Builds a model from nothing", "Teaches an already-trained model your specific task", "Makes the model file smaller"], a: 1, why: "Fine-tuning adapts a pre-trained model to your task with a relatively small set of examples." } },
{ icon: "📏", title: "Measuring: which number matters?", min: 6, body: `
<p>${T("accuracy")} — the share of answers that are right — is the number everyone quotes. It can badly mislead you.</p>
<div class="widget" data-widget="accuracy"></div>
<p>For Caveat, the mistake that matters most is missing a banned practice. So the headline number is ${T("recall")} on the prohibited tier: out of all truly banned cases, how many were caught.</p>
<ul>
<li>${T("precision")} — when the model says "prohibited", how often is it right?</li>
<li>${T("macro-F1")} — a balanced 0–1 score across all four tiers, so a model can't look good by ignoring a small one.</li>
<li>${T("confusion matrix")} — a table showing exactly which tier gets mistaken for which.</li>
<li>${T("severe error")} — our name for the worst case: a banned practice called minimal or limited risk.</li>
</ul>
<div class="callout good"><span class="ctitle">From this project</span><p>One candidate model had the best accuracy — and called 16 banned cases "minimal risk". The accuracy number hid it; recall and severe errors exposed it.</p></div>`,
terms: ["accuracy", "recall", "precision", "macro-F1", "confusion matrix", "severe error"],
quiz: { q: "100 cases, 5 of them banned. A model always answers \"harmless\". Its accuracy?", opts: ["5%", "50%", "95% — while catching zero banned cases"], a: 2, why: "It's right on the 95 harmless cases, so accuracy is 95% — but its recall on banned cases is 0%." } },
{ icon: "🎲", title: "Why one number can lie", min: 5, body: `
<p>Training involves randomness. Change one ${T("random seed")} and you get a slightly different model with a different score. On Caveat's older data, the same setup scored anywhere from 65% to 74% just by changing the seed.</p>
<div class="analogy"><span class="ico">🪙</span><div><b>Like judging a coin from 10 flips.</b> 7 heads doesn't prove the coin is unfair. With few examples, big-looking differences are often just ${T("noise")}.</div></div>
<h3>What we do about it</h3>
<ul>
<li>Train 3 times with different seeds and report the average and spread.</li>
<li>Give a ${T("confidence interval")}: the range the true score probably sits in.</li>
<li>Say plainly when two models aren't really different.</li>
</ul>
<div class="callout warn"><span class="ctitle">A useful rule of thumb</span><p>If a model gets all of <i>n</i> test cases right, its true error rate could still be up to about 3 ÷ <i>n</i>. With 12 banned test cases, a perfect score still allows missing roughly 1 in 4 in real life. To show "fewer than 1 in 100" you'd need about 300.</p></div>
<p>And watch for ${T("overfitting")}: a model that memorises its homework can score brilliantly on training examples and poorly on new ones.</p>`,
terms: ["random seed", "noise", "confidence interval", "overfitting"],
quiz: { q: "Model A scores 82%, model B 79%, on 60 test cases. What can you conclude?", opts: ["A is clearly better", "Probably nothing yet — the gap may be noise", "B is broken"], a: 1, why: "With 60 cases, a 3-point gap is well within chance. Check the confidence intervals and repeat with more seeds." } },
{ icon: "🔍", title: "When the data is the problem", min: 5, body: `
<p>When a model fails, the cause is very often the examples, not the model. Two kinds of problem turned up in this project:</p>
<h3>1. Wrong answers in the answer key</h3>
<p>An audit found six ${T("label error", "label errors")}. For example, a stadium's live face-ID system was labelled "banned", but the ban only covers police use — it's high risk. Three wrong answers were in the test itself, so the old published score was a little too high.</p>
<h3>2. Missing kinds of example</h3>
<p>Version 4 called every phone and voice agent "minimal risk" — 0 out of 18 tries. Why? Every "talk to an AI" example in training was a typed chat. Nothing showed a voice. That's a ${T("coverage gap")}.</p>
<div class="example"><div class="ex-head">The fix, in version 5</div><div class="ex-body">
<div class="ex-row"><span class="txt">"A city council's automated voice line that talks with residents about bin collection."</span>${pill("limited_risk")}</div>
<div class="ex-row"><span class="txt">"Software analysing the council's call volumes to plan phone staffing." <span class="muted">(look-alike, not covered)</span></span>${pill("minimal_risk")}</div>
</div></div>
<p>Adding pairs like this — one covered, one look-alike that isn't — teaches the difference. Phone agents went from 0 of 9 to 7 of 9 right.</p>
<div class="callout tip"><span class="ctitle">Also test differently written text</span><p>Caveat is also checked on 35 cases written in a different style (${T("out-of-distribution")}). It does worse there — a sign it partly learned our writing style, not just the law.</p></div>`,
terms: ["label error", "coverage gap", "out-of-distribution"],
quiz: { q: "Your model gets all voice assistants wrong. What should you check first?", opts: ["Buy a bigger computer", "Whether the training data contains any voice assistants", "Change the colour of the app"], a: 1, why: "A model can't learn a pattern it has never seen. Look for the missing examples first." } },
{ icon: "📐", title: "Rules vs models", min: 3, body: `
<p>Not everything should be learned. When the law is a clear number, a ${T("rule-based", "rule")} is better: exact, explainable, and testable.</p>
<div class="example"><div class="ex-head">The GPAI rule in Caveat</div><div class="ex-body">
<div class="ex-row"><span class="txt">"Our model was trained with 3×10^25 FLOPs." → more than 10^25 ${T("FLOPs")}</span>${pill("gpai_systemic_risk")}</div>
<div class="ex-row"><span class="txt">"Our model was trained with 6×10^24 FLOPs." → below the threshold</span>${pill("gpai")}</div>
<div class="ex-row"><span class="txt">"We built a chatbot on top of GPT." → not a model maker; the classifier decides</span><span class="muted">→ trained model</span></div>
</div></div>
<p>Small text models read numbers like "10^25" poorly, so this check runs first. It was tested on 655 descriptions — all correct.</p>
<div class="analogy"><span class="ico">🧰</span><div><b>Use a ruler for measuring, and judgement for everything else.</b> Rules for thresholds and dates; models for fuzzy language.</div></div>`,
terms: ["rule-based", "FLOPs", "GPAI"],
quiz: { q: "Which job suits a hand-written rule best?", opts: ["Deciding if a text sounds manipulative", "Checking whether a number is above 10^25", "Judging if a use case is 'high impact'"], a: 1, why: "A clear numeric threshold is exact — a rule gets it right every time." } },
{ icon: "📦", title: "Shrinking it & putting it online", min: 4, body: `
<p>The trained model was 140 MB — too big for a quick web page. ${T("quantization")} stores its numbers with less precision: 36 MB, and the same answer as the full model on 98.6% of test cases.</p>
<div class="analogy"><span class="ico">🖼️</span><div><b>Like saving a photo as a smaller JPEG.</b> Much lighter, almost no visible difference — but check the details that matter.</div></div>
<ol>
<li>Convert the model to ${T("ONNX")}, a standard format that web browsers can run.</li>
<li>Put the files on a ${T("static site")} — here, a free Hugging Face Space.</li>
<li>A small library (transformers.js) downloads the model once and runs ${T("inference")} on the visitor's own device.</li>
</ol>
<div class="callout good"><span class="ctitle">Why this matters</span><p>No server, no per-question fee, and your text never leaves your browser (for the trained model). It costs the same whether 1 or 10,000 people use it: nothing.</p></div>`,
terms: ["quantization", "ONNX", "static site", "inference"],
quiz: { q: "What does quantization do?", opts: ["Makes the model smarter", "Makes the model file much smaller, with a small risk of changed answers", "Translates the model into another language"], a: 1, why: "It trades a little precision for a much smaller file — so always re-check the answers afterwards." } },
{ icon: "🛠️", title: "Build your own, step by step", min: 10, body: `
<p>You can build a small text classifier for your own problem. Here's the path this project followed, in order. Tick the steps off as you go — they're saved in this browser.</p>
<div class="widget" data-widget="checklist"></div>
<div class="callout tip"><span class="ctitle">No-code and low-code options</span><p>No-code training tools (such as Hugging Face AutoTrain) let you upload a spreadsheet and fine-tune without writing code. A free Google Colab notebook is the next step up. For running in a browser, transformers.js and a static Hugging Face Space cost nothing.</p></div>
<div class="callout warn"><span class="ctitle">Be honest about limits</span><p>Write a ${T("model card")}: what it's for, the data, the scores with their ranges, and where it fails. It protects your users — and you.</p></div>`,
terms: ["model card", "dataset", "test set", "recall"],
quiz: { q: "Which step comes first?", opts: ["Pick a model", "Decide the categories and write labelled examples", "Publish the website"], a: 1, why: "Your boxes and your examples define the task. Everything else follows from them." } },
{ icon: "🏁", title: "Wrap-up & glossary", min: 3, body: `
<p>You've covered the whole journey: examples, labels, splits, fine-tuning, measuring honestly, fixing data, rules, and shipping.</p>
<div class="callout bad"><span class="ctitle">The most important lesson</span><p>A model that catches 99% of banned cases on a small, tidy test is still not a compliance tool. The responsible design is to flag uncertainty and send hard or disputed cases to a qualified expert.</p></div>
<div class="callout tip"><span class="ctitle">Next</span><p>Ready? The <a href="#learn-14">final assessment</a> has 12 questions. Score 9 or more (75%) to unlock your certificate.</p></div>
<h3>Glossary — every term in this course</h3>
<input class="gloss-search" id="glossSearch" type="search" placeholder="Search a word… e.g. recall" aria-label="Search the glossary" />
<div class="gloss-list" id="glossList"></div>`,
terms: [],
quiz: { q: "Caveat says \"minimal risk\". What should you do before launching?", opts: ["Launch — the AI said it's fine", "Treat it as a hint and get expert review", "Ask the same question until it says what you want"], a: 1, why: "Caveat is a demonstration. Real classification needs expert review of the full facts." } },
{ icon: "📝", title: "Final assessment & certificate", min: 10, body: `
<p>12 questions covering the whole course. <b>Score 9 or more (75%) to pass</b> and unlock your certificate. You can retake it as often
as you like; questions and answers are shuffled each time, and you'll see an explanation for anything you get wrong.</p>
<div class="widget" data-widget="exam"></div>
<div class="widget" data-widget="certificate"></div>`,
terms: [], quiz: null },
];
// lesson numbering in URLs starts at 1 for "What is an AI model?" (Welcome is 0)
let currentLesson = Math.min(store.get("caveat-lesson", 0), LESSONS.length - 1);
let done = new Set(store.get("caveat-done", []));
function renderNav() {
$("lessonList").innerHTML = LESSONS.map((l, i) =>
`<li class="${done.has(i) ? "done" : ""}"><button type="button" data-i="${i}" ${i === currentLesson ? 'aria-current="true"' : ""}><span class="n">${done.has(i) ? "✓" : i}</span><span>${l.icon} ${l.title}</span></button></li>`).join("");
$("lessonList").querySelectorAll("button").forEach((b) => b.addEventListener("click", () => {
location.hash = "learn-" + b.dataset.i; $("lessonList").classList.remove("open"); $("navToggle").setAttribute("aria-expanded", "false");
}));
const pct = Math.round((done.size / LESSONS.length) * 100);
$("progressFill").style.width = pct + "%";
$("progressTxt").textContent = `${done.size} / ${LESSONS.length}`;
}
$("navToggle").addEventListener("click", () => {
const open = $("lessonList").classList.toggle("open");
$("navToggle").setAttribute("aria-expanded", String(open));
});
function renderLesson(i) {
i = Math.max(0, Math.min(LESSONS.length - 1, i));
currentLesson = i; store.set("caveat-lesson", i);
const l = LESSONS[i];
const kw = l.terms.filter(findTerm).map((t) => { const k = findTerm(t); return `<div class="kw"><b>${k}</b>${GLOSSARY[k][0]}</div>`; }).join("");
$("lesson").innerHTML = `
<div class="lesson-head"><span class="lesson-icon">${l.icon}</span><div>
<p class="eyebrow" style="margin:0">${l.quiz ? `Lesson ${i} of ${LESSONS.length - 2}` : "Final step"}</p>
<h2>${l.title}</h2><div class="lesson-meta">⏱ about ${l.min} min</div></div></div>
<div class="lesson-body">${l.body}</div>
${kw ? `<div class="keywords"><h3>Key words in this lesson</h3><div class="kw-grid">${kw}</div></div>` : ""}
${l.quiz ? `<div class="quiz"><h3>Quick check</h3><p class="q">${l.quiz.q}</p>
<div class="quiz-opts">${l.quiz.opts.map((o, j) => `<button type="button" data-j="${j}">${o}</button>`).join("")}</div>
<p class="fb" hidden></p></div>` : ""}
<div class="lesson-foot">
<button class="ghost" type="button" id="prevL" ${i === 0 ? "disabled" : ""}>← Previous</button>
${i < LESSONS.length - 1 ? `<button class="primary" type="button" id="nextL" style="min-width:0">${i === LESSONS.length - 2 ? "Go to the final assessment →" : "Next lesson →"}</button>` : ""}
</div>`;
const quiz = $("lesson").querySelector(".quiz");
if (quiz) quiz.querySelectorAll(".quiz-opts button").forEach((b) => b.addEventListener("click", () => {
const j = Number(b.dataset.j), fb = quiz.querySelector(".fb");
quiz.querySelectorAll(".quiz-opts button").forEach((x) => { x.disabled = true; if (Number(x.dataset.j) === l.quiz.a) x.classList.add("right"); });
if (j !== l.quiz.a) b.classList.add("wrong");
fb.hidden = false; fb.innerHTML = (j === l.quiz.a ? "✅ <b>Correct.</b> " : "Not quite. ") + l.quiz.why;
markDone(i);
}));
$("prevL").addEventListener("click", () => { location.hash = "learn-" + (i - 1); });
$("nextL")?.addEventListener("click", () => { markDone(i); location.hash = "learn-" + (i + 1); });
mountWidgets();
renderNav();
if (!$("panel-learn").hidden) window.scrollTo({ top: 0 });
}
function markDone(i) { done.add(i); store.set("caveat-done", [...done]); renderNav(); mountCertificate(); }
// ---- final assessment: 12 questions, pass mark 9 (75%), unlimited retakes, shuffled each attempt ----
const EXAM_PASS = 9;
const EXAM = [
{ q: "How does an AI model like Caveat's learn to sort texts into risk tiers?", opts: ["Someone writes a rule for every possible case", "It finds patterns in many examples that come with the correct answer", "It reads the law and understands it like a lawyer"], a: 1, why: "Models learn from labelled examples. That's also why wrong or missing examples produce a wrong model. (Lesson 1)" },
{ q: "A system that scores citizens on their social behaviour to limit their access to public services is…", opts: ["Prohibited", "High risk", "Limited risk", "Minimal risk"], a: 0, why: "Social scoring by or for public authorities is a banned practice under Article 5. (Lesson 2)" },
{ q: "An AI tool that ranks job applicants before a recruiter sees them is…", opts: ["Prohibited", "High risk", "Limited risk", "Minimal risk"], a: 1, why: "Employment decisions such as CV screening are listed in Annex III, so they are high risk. (Lesson 2)" },
{ q: "A phone voice agent that talks to customers to take their orders is…", opts: ["Prohibited", "High risk", "Limited risk", "Minimal risk"], a: 2, why: "People must be told they are interacting with an AI (Article 50), so it is limited risk. (Lessons 2 and 9)" },
{ q: "Your dataset has 600 rows, and every scenario is written 3 different ways. How many real examples do you have?", opts: ["600", "200", "1,800"], a: 1, why: "Rewordings of one scenario are one idea. 600 ÷ 3 = 200 distinct scenarios. (Lesson 4)" },
{ q: "What is the test set for?", opts: ["Teaching the model", "Choosing settings like how long to train", "Measuring the final model once, on examples it has never seen"], a: 2, why: "Training teaches, validation chooses settings, and the test set is the final exam, used once. (Lesson 5)" },
{ q: "A reworded copy of a test scenario ends up in the training data. What happens?", opts: ["Nothing, because the wording is different", "The test score looks better than the model really is", "The model gets slower"], a: 1, why: "That's data leakage: the model has effectively seen the exam, so the score is too optimistic. (Lesson 5)" },
{ q: "What does fine-tuning do?", opts: ["Builds a language model from nothing", "Teaches an already pre-trained model your specific task with your own examples", "Makes the model file smaller"], a: 1, why: "Fine-tuning adapts a pre-trained model to your task. Making the file smaller is quantization. (Lessons 6 and 11)" },
{ q: "100 cases, 5 of them banned. A model always answers \"harmless\". Which number exposes it?", opts: ["Accuracy (95%)", "Recall on the banned tier (0%)", "The model's confidence"], a: 1, why: "It is right 95% of the time but catches none of the banned cases. Recall on the tier that matters shows it. (Lesson 7)" },
{ q: "Model A scores 82% and model B 79% on 60 test cases. What can you conclude?", opts: ["A is clearly better", "Probably nothing yet: the gap may be chance, so check the confidence intervals", "B is broken"], a: 1, why: "With so few test cases, a 3-point gap is within noise. (Lesson 8)" },
{ q: "Your model gets every voice assistant wrong. What should you check first?", opts: ["Whether the training data contains any voice assistants", "Buy a bigger computer", "Train for many more epochs"], a: 0, why: "A model can't learn a pattern it has never seen. That's a coverage gap, fixed by adding examples. (Lesson 9)" },
{ q: "Which job is best done by a hand-written rule rather than a trained model?", opts: ["Judging whether a text sounds manipulative", "Checking whether training compute is above 10^25 FLOPs", "Deciding whether a use case is high impact"], a: 1, why: "A clear numeric threshold is exact, and text models read exponents badly. (Lesson 10)" },
];
const examResult = () => { const r = store.get("caveat-exam", null); return r && r.score >= EXAM_PASS ? r : null; };
const shuffle = (arr) => { const a = arr.slice(); for (let i = a.length - 1; i > 0; i--) { const j = Math.floor(Math.random() * (i + 1)); [a[i], a[j]] = [a[j], a[i]]; } return a; };
function mountExam() {
const el = $("lesson").querySelector('[data-widget="exam"]');
if (!el) return;
const best = store.get("caveat-exam", null);
const intro = () => {
el.innerHTML = `<div class="w-title">📝 Final assessment</div>
<p style="margin:0 0 10px">12 questions · pass mark 9 (75%) · unlimited retakes.${best ? ` Your best score so far: <b>${best.score}/${best.total}</b>${best.score >= EXAM_PASS ? " ✅ passed" : ""}.` : ""}</p>
<button class="primary" type="button" id="examStart" style="min-width:0">${best ? "Take it again" : "Start the assessment"}</button>`;
$("examStart").addEventListener("click", start);
};
const start = () => {
const qs = shuffle(EXAM).map((q) => { const order = shuffle(q.opts.map((_, k) => k)); return { ...q, order }; });
el.innerHTML = `<div class="w-title">📝 Final assessment</div>` + qs.map((q, n) => `
<fieldset class="exam-q" data-n="${n}"><legend><b>${n + 1}.</b> ${q.q}</legend>
${q.order.map((k) => `<label class="exam-opt"><input type="radio" name="eq${n}" value="${k}" /> <span>${q.opts[k]}</span></label>`).join("")}
</fieldset>`).join("") +
`<p class="fine" id="examLeft" style="margin:8px 0">Answer all 12 questions to submit.</p>
<button class="primary" type="button" id="examSubmit" disabled style="min-width:0">Submit answers</button>`;
const update = () => {
const answered = qs.filter((_, n) => el.querySelector(`input[name="eq${n}"]:checked`)).length;
$("examSubmit").disabled = answered < qs.length;
$("examLeft").textContent = answered < qs.length ? `${answered} of ${qs.length} answered.` : "All answered — ready to submit.";
};
el.querySelectorAll("input[type=radio]").forEach((r) => r.addEventListener("change", update));
$("examSubmit").addEventListener("click", () => {
const picks = qs.map((_, n) => Number(el.querySelector(`input[name="eq${n}"]:checked`).value));
const score = picks.filter((k, n) => k === qs[n].a).length, passed = score >= EXAM_PASS;
const prev = store.get("caveat-exam", null);
if (!prev || score > prev.score) store.set("caveat-exam", { score, total: qs.length, at: Date.now() });
const wrong = qs.map((q, n) => ({ q, pick: picks[n] })).filter((x) => x.pick !== x.q.a);
el.innerHTML = `<div class="w-title">📝 Your result</div>
<div class="exam-score ${passed ? "pass" : "fail"}"><b>${score}/${qs.length}</b> · ${passed ? "Passed ✅ — your certificate is unlocked below." : `Not yet — you need ${EXAM_PASS}. Review the answers and try again.`}</div>
${wrong.length ? `<h4 style="margin:14px 0 6px">What to review</h4>` + wrong.map(({ q, pick }) => `
<div class="exam-review"><p style="margin:0 0 4px"><b>${q.q}</b></p>
<p style="margin:0">You answered: <span style="color:var(--danger)">${q.opts[pick]}</span><br>Correct: <span style="color:var(--good)"><b>${q.opts[q.a]}</b></span></p>
<p class="fine" style="margin:4px 0 0">${q.why}</p></div>`).join("") : `<p>Perfect score — every answer right.</p>`}
<button class="ghost" type="button" id="examRetry" style="margin-top:12px">Take it again</button>`;
$("examRetry").addEventListener("click", start);
track(passed ? "exam_passed" : "exam_failed");
if (passed) markDone(LESSONS.length - 1); else mountCertificate();
el.scrollIntoView({ behavior: "smooth", block: "start" });
});
};
intro();
}
// ---- course certificate: drawn in the browser; the name never leaves the device ----
const COURSE = "AI Classifiers & the EU AI Act";
function mountCertificate() {
const el = $("lesson").querySelector('[data-widget="certificate"]');
if (!el) return;
const exam = examResult();
if (!exam) {
el.innerHTML = `<div class="w-title">🎓 Your certificate</div>
<p style="margin:0">Pass the final assessment above (9 or more out of 12) to unlock a certificate you can download and add to LinkedIn.</p>`;
delete el.dataset.ready;
return;
}
if (el.dataset.ready) return;
el.dataset.ready = "1";
el.innerHTML = `<div class="w-title">🎓 Your certificate of completion</div>
<p style="margin:0">Well done — you passed the final assessment with <b>${exam.score}/${exam.total}</b>. Type your name as you'd like it to appear.
<span class="muted">It is only used to draw the certificate in your browser; it's never sent anywhere.</span></p>
<div class="cert-row"><input id="certName" maxlength="60" placeholder="Your name" autocomplete="name" aria-label="Your name" />
<button class="primary" type="button" id="certMake" style="min-width:0">Create certificate</button></div>
<div id="certOut" hidden>
<img class="cert-preview" id="certImg" alt="Certificate of completion" />
<div class="cert-actions">
<a class="primary" id="certDl" download="Devseis-certificate-AI-classifiers-EU-AI-Act.png" style="min-width:0">⬇️ Download</a>
<a class="ghost" id="certLi" target="_blank" rel="noopener">➕ Add to LinkedIn profile</a>
<a class="ghost" id="certShare" target="_blank" rel="noopener">🔗 Share on LinkedIn</a>
</div>
<p class="fine" style="margin:8px 0 0">A self-paced learning module by Devseis — not an accredited qualification.</p>
</div>`;
$("certMake").addEventListener("click", makeCertificate);
$("certName").addEventListener("keydown", (e) => { if (e.key === "Enter") makeCertificate(); });
}
async function makeCertificate() {
const name = $("certName").value.trim().replace(/\s+/g, " ").slice(0, 60);
if (!name) { $("certName").focus(); return; }
try { await document.fonts.load('700 64px "Space Grotesk"'); await document.fonts.load('400 28px "Space Grotesk"'); } catch (e) {}
// A4 landscape proportions, large enough to print sharply
const W = 2000, H = 1414, c = document.createElement("canvas"); c.width = W; c.height = H;
const g = c.getContext("2d");
const SANS = '"Space Grotesk", Arial, sans-serif', SERIF = 'Georgia, "Times New Roman", serif';
const NAVY = "#0b2a5b", BLUE = "#004aae", GOLD = "#b8862b", GOLD_L = "#e0b65a", INK = "#1d2330", GREY = "#5f6878";
// paper
g.fillStyle = "#fcfaf4"; g.fillRect(0, 0, W, H);
// fine guilloche band between the outer and inner frames
g.save(); g.beginPath(); g.rect(36, 36, W - 72, H - 72); g.rect(92, 92, W - 184, H - 184); g.clip("evenodd");
g.strokeStyle = "rgba(184,134,43,0.22)"; g.lineWidth = 1;
for (let k = -H; k < W; k += 9) { g.beginPath(); g.moveTo(k, 0); g.lineTo(k + H, H); g.stroke(); g.beginPath(); g.moveTo(k + H, 0); g.lineTo(k, H); g.stroke(); }
g.restore();
// frames
g.strokeStyle = NAVY; g.lineWidth = 16; g.strokeRect(36, 36, W - 72, H - 72);
g.strokeStyle = GOLD; g.lineWidth = 4; g.strokeRect(92, 92, W - 184, H - 184);
g.strokeStyle = NAVY; g.lineWidth = 1.5; g.strokeRect(104, 104, W - 208, H - 208);
// corner ornaments
const corner = (x, y, sx, sy) => {
g.save(); g.translate(x, y); g.scale(sx, sy); g.strokeStyle = GOLD; g.lineWidth = 3;
g.beginPath(); g.moveTo(0, 70); g.lineTo(0, 0); g.lineTo(70, 0); g.stroke();
g.fillStyle = GOLD; g.beginPath(); g.moveTo(16, 6); g.lineTo(26, 16); g.lineTo(16, 26); g.lineTo(6, 16); g.closePath(); g.fill();
g.restore();
};
corner(124, 124, 1, 1); corner(W - 124, 124, -1, 1); corner(124, H - 124, 1, -1); corner(W - 124, H - 124, -1, -1);
// text helpers
const spaced = (txt, x, y, font, color, gap) => {
g.font = font; g.fillStyle = color; g.textAlign = "left";
const widths = [...txt].map((ch) => g.measureText(ch).width);
const total = widths.reduce((a, b) => a + b, 0) + gap * (txt.length - 1);
let cx = x - total / 2; [...txt].forEach((ch, i) => { g.fillText(ch, cx, y); cx += widths[i] + gap; });
};
const center = (txt, y, font, color) => { g.font = font; g.fillStyle = color; g.textAlign = "center"; g.fillText(txt, W / 2, y); };
const fit = (txt, max, style, start, family) => { let sz = start; do { g.font = `${style} ${sz}px ${family}`; sz -= 2; } while (g.measureText(txt).width > max && sz > 30); return g.font; };
// heading
spaced("CERTIFICATE", W / 2, 300, `400 104px ${SERIF}`, NAVY, 14);
spaced("OF COMPLETION", W / 2, 368, `600 30px ${SANS}`, GOLD, 9);
// recipient
center("This is to certify that", 480, `italic 36px ${SERIF}`, GREY);
g.font = fit(name, W - 560, "bold", 100, SERIF); g.fillStyle = NAVY; g.textAlign = "center"; g.fillText(name, W / 2, 605);
const rule = g.createLinearGradient(W / 2 - 380, 0, W / 2 + 380, 0);
rule.addColorStop(0, "rgba(184,134,43,0)"); rule.addColorStop(0.5, GOLD); rule.addColorStop(1, "rgba(184,134,43,0)");
g.fillStyle = rule; g.fillRect(W / 2 - 380, 642, 760, 3);
// course
center("has successfully completed the course", 728, `italic 36px ${SERIF}`, GREY);
center(COURSE, 825, `bold 66px ${SERIF}`, INK);
center("14 lessons and a final assessment", 885, `400 30px ${SANS}`, GREY);
// bottom row: date — seal — signatory
const now = new Date(), dateTxt = now.toLocaleDateString("en-GB", { day: "numeric", month: "long", year: "numeric" });
const block = (x, value, label) => {
g.textAlign = "center"; g.fillStyle = INK; g.font = `600 32px ${SANS}`; g.fillText(value, x, 1130);
g.fillStyle = NAVY; g.fillRect(x - 230, 1152, 460, 2);
g.fillStyle = GREY; g.font = `400 24px ${SANS}`; g.fillText(label, x, 1192);
};
block(540, dateTxt, "Date of completion");
block(W - 540, "Dr. Muhammad Umer Wasim", "Director");
// Devseis logo in the centre of the bottom row (hosted in this Space, so the canvas can still be saved)
try {
const logo = new Image(); logo.src = "devseis-logo.png"; await logo.decode();
const gap = (W - 540 - 230) - (540 + 230) - 80; // space between the two blocks, minus margins
const lw = Math.min(400, gap), lh = lw * logo.naturalHeight / logo.naturalWidth;
center("by", 952, `italic 32px ${SERIF}`, GREY); // "14 lessons and a final assessment — by Devseis"
g.drawImage(logo, W / 2 - lw / 2, 975, lw, lh);
} catch (e) { center("by", 952, `italic 32px ${SERIF}`, GREY); center("DEVSEIS", 1020, `700 40px ${SANS}`, BLUE); } // text fallback
// footer
center("Self-paced learning module — not an accredited qualification · huggingface.co/spaces/Devseis/Caveat", 1300, `400 21px ${SANS}`, "#8a93a4");
const url = c.toDataURL("image/png");
$("certImg").src = url; $("certDl").href = url;
$("certLi").href = "https://www.linkedin.com/profile/add?" + new URLSearchParams({
startTask: "CERTIFICATION_NAME", name: COURSE, organizationId: "12603388", // Devseis LinkedIn company page
issueYear: String(now.getFullYear()), issueMonth: String(now.getMonth() + 1), certUrl: PAGE_URL }).toString();
$("certShare").href = "https://www.linkedin.com/sharing/share-offsite/?url=" + encodeURIComponent(PAGE_URL);
$("certOut").hidden = false;
track("certificate_created");
}
// ---------- interactive widgets ----------
function mountWidgets() {
const root = $("lesson");
mountExam();
mountCertificate();
const sortEl = root.querySelector('[data-widget="sort"]');
if (sortEl) {
const items = [
["A system that scores citizens on their social behaviour to limit their access to public services.", "prohibited"],
["An AI that ranks candidates for a job and decides who gets interviewed.", "high_risk"],
["A website avatar that talks shoppers through size choices.", "limited_risk"],
["An AI that tags product photos by colour for a catalogue.", "minimal_risk"],
["An app that turns any photo of a woman into a nude image without her consent.", "prohibited"],
];
let k = 0;
const draw = () => {
const [txt, ans] = items[k];
sortEl.innerHTML = `<div class="w-title">Try it — sort ${k + 1} of ${items.length}</div><div class="sort-card">"${txt}"</div>
<div class="sort-btns">${TIERS.slice().reverse().map((t) => `<button type="button" class="${pillClass(t)}" data-t="${t}">${TIER[t].name}</button>`).join("")}</div>
<div class="sort-fb"></div>`;
sortEl.querySelectorAll(".sort-btns button").forEach((b) => b.addEventListener("click", () => {
const ok = b.dataset.t === ans, fb = sortEl.querySelector(".sort-fb");
fb.innerHTML = (ok ? "✅ Right — " : `Not quite — it's <b>${TIER[ans].name}</b>. `) + TIER[ans].meaning +
(k < items.length - 1 ? ` <button class="ghost" type="button" style="margin-left:6px;padding:4px 10px">Next →</button>` : " 🎉 Done!");
fb.querySelector("button")?.addEventListener("click", () => { k++; draw(); });
}));
};
draw();
}
const splitEl = root.querySelector('[data-widget="split"]');
if (splitEl) {
splitEl.innerHTML = `<div class="w-title">How Caveat's 366 scenarios were split</div>
<div class="split-bar"><div style="width:66%;background:#3b82f6">Training · 242</div><div style="width:15%;background:#a855f7">Val · 54</div><div style="width:19%;background:#f97316">Test · 70</div></div>
<p class="fine" style="margin:0">Homework 66% · practice exam 15% · real exam 19%. Each scenario's 3 rows stay together.</p>`;
}
const accEl = root.querySelector('[data-widget="accuracy"]');
if (accEl) {
const models = {
lazy: { name: "Lazy model", caught: 0, falseAlarm: 0, note: "Always answers \"harmless\"." },
careful: { name: "Careful model", caught: 5, falseAlarm: 6, note: "Flags anything suspicious." },
};
const draw = (key) => {
const m = models[key];
const dots = Array.from({ length: 100 }, (_, i) => {
const banned = i < 5, flagged = banned ? i < m.caught : i < 5 + m.falseAlarm;
const c = banned ? (flagged ? "var(--good)" : "var(--danger)") : (flagged ? "var(--warn)" : "var(--border)");
return `<span style="background:${c}"></span>`;
}).join("");
const correct = m.caught + (95 - m.falseAlarm);
accEl.innerHTML = `<div class="w-title">Try it — the accuracy trap</div>
<div class="seg">${Object.entries(models).map(([k2, v]) => `<button type="button" data-k="${k2}" aria-pressed="${k2 === key}">${v.name}</button>`).join("")}</div>
<p class="fine" style="margin:0">100 cases, 5 of them banned. ${m.note}</p>
<div class="dots">${dots}</div>
<p class="fine" style="margin:0 0 10px">🟢 banned & caught · 🔴 banned & missed · 🟡 harmless but flagged · ⚪ harmless & correct</p>
<div class="w-stats"><div><b>${correct}%</b>accuracy</div><div><b>${Math.round((m.caught / 5) * 100)}%</b>banned caught (recall)</div></div>`;
accEl.querySelectorAll(".seg button").forEach((b) => b.addEventListener("click", () => draw(b.dataset.k)));
};
draw("lazy");
}
const clEl = root.querySelector('[data-widget="checklist"]');
if (clEl) {
const steps = [
["Define your boxes", "Write down your categories and one sentence explaining each. Caveat: the 4 AI Act tiers."],
["Collect or write examples", "Aim for at least 30–50 distinct scenarios per category in a spreadsheet: text | label. Include look-alike cases that belong elsewhere."],
["Check your labels", "Have someone else review a sample. Fix wrong answers before training — they poison both learning and scoring."],
["Split by scenario", "About 65% training, 15% validation, 20% test. Keep every variation of a scenario in the same group."],
["Pick a small pre-trained model", "Choose one close to your topic (legal, medical…) and small enough for where it will run."],
["Fine-tune", "Use a no-code tool or a notebook. Choose settings using the validation set only."],
["Train 3 times, test once", "Different seeds; report the average and spread; open the test set only at the end."],
["Look at the mistakes that matter", "Not just accuracy: the recall on your most important category, and the confusion matrix."],
["Fix the data, repeat", "Missing kinds of example? Add them — and new test cases for them — then measure again."],
["Shrink and publish", "Export to ONNX, quantize, re-check the answers, and host it (e.g. a static Hugging Face Space)."],
["Write the model card", "Purpose, data, scores with ranges, known failures, and \"not advice\" where relevant."],
];
const saved = new Set(store.get("caveat-checklist", []));
clEl.innerHTML = `<div class="w-title">Your build checklist</div><ul class="checklist">${steps.map(([h, d], i) =>
`<li><input type="checkbox" id="cl${i}" ${saved.has(i) ? "checked" : ""}/><label for="cl${i}"><b>${i + 1}. ${h}</b><span class="step-detail">${d}</span></label></li>`).join("")}</ul>`;
clEl.querySelectorAll("input").forEach((c, i) => c.addEventListener("change", () => {
c.checked ? saved.add(i) : saved.delete(i); store.set("caveat-checklist", [...saved]);
}));
}
const gl = root.querySelector("#glossList");
if (gl) {
const draw = (q) => {
const qq = (q || "").toLowerCase();
gl.innerHTML = GLOSS_KEYS.slice().sort((a, b) => a.localeCompare(b))
.filter((k) => !qq || k.toLowerCase().includes(qq) || GLOSSARY[k][0].toLowerCase().includes(qq))
.map((k) => `<div class="kw"><b>${k}</b>${GLOSSARY[k][0]}<div class="muted" style="margin-top:4px;font-size:12px">e.g. ${GLOSSARY[k][1]}</div></div>`).join("") ||
`<p class="muted">No match.</p>`;
};
draw("");
root.querySelector("#glossSearch").addEventListener("input", (e) => draw(e.target.value));
}
}
renderNav();
renderLesson(currentLesson);
route();
</script>
</body>
</html>