Claude Opus 5.5
Switch to Devseis AI Act Classifier v6; 'uncertain' label for split predictions
14cb8df verified Download index.html from Devseis/Caveat: direct link, hf CLI and curl.
- Browser
- Download file 141 kB
-
https://huggingface.co/spaces/Devseis/Caveat/resolve/main/index.html
- Command line
-
hf download hf://spaces/Devseis/Caveat/index.html
-
curl -L -o index.html https://huggingface.co/spaces/Devseis/Caveat/resolve/main/index.html
141 kB
| <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 ;} | |
| 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 ;} | |
| body > *:not(#printSheet){display:none ;} | |
| #printSheet{display:block ;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 & 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 & card</a> · <a href="https://huggingface.co/datasets/Devseis/devseis-ai-act-classifier-v6-data" target="_blank" rel="noopener">training & 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) => ({ "&": "&", "<": "<", ">": ">", '"': """ }[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> | |