File size: 48,904 Bytes
fabf837 | 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 99 100 101 102 103 104 105 106 107 108 109 110 111 112 113 114 115 116 117 118 119 120 121 122 123 124 125 126 127 128 129 130 131 132 133 134 135 136 137 138 139 140 141 142 143 144 145 146 147 148 149 150 151 152 153 154 155 156 157 158 159 160 161 162 163 164 165 166 167 168 169 170 171 172 173 174 175 176 177 178 179 180 181 182 183 184 185 186 187 188 189 190 191 192 193 194 195 196 197 198 199 200 201 202 203 204 205 206 207 208 209 210 211 212 213 214 215 216 217 218 219 220 221 222 223 224 225 226 227 228 229 230 231 232 233 234 235 236 237 238 239 240 241 242 243 244 245 246 247 248 249 250 251 252 253 254 255 256 257 258 259 260 261 262 263 264 265 266 267 268 269 270 271 272 273 274 275 276 277 278 279 280 281 282 283 284 285 286 287 288 289 290 291 292 293 294 | <!doctype html>
<html lang="nl"><head><meta charset="utf-8"><meta name="viewport" content="width=device-width,initial-scale=1"><title>Reakt | ORCHESTRA</title><style>
:root{font-family:Inter,'Segoe UI',Arial,sans-serif;color:#eaf2ff;background:#060d1d;font-synthesis:none}*{box-sizing:border-box}body{margin:0;background:radial-gradient(ellipse at 50% -15%,#162d58 0%,#081224 43%,#060b18 100%);min-height:100vh}button,input{font:inherit}button{cursor:pointer}button:disabled{opacity:.5;cursor:wait}.app{display:flex;min-height:100vh}.sidebar{width:225px;flex:none;border-right:1px solid #223351;background:#091326;padding:25px 16px;position:sticky;top:0;height:100vh}.brand{display:flex;align-items:center;gap:12px;margin-bottom:55px}.brand-icon{font-size:34px;color:#54d9ff;text-shadow:0 0 20px #38baff}.brand strong{font-size:24px;letter-spacing:1.5px}.brand-dot,.paper-top span{color:#58d8f5}.brand small{display:block;font-size:9px;letter-spacing:1.8px;color:#7791b5;margin-top:3px}.nav-label{font-size:10px;letter-spacing:1.5px;color:#607a9e;margin:0 8px 15px}.nav-label.second{margin-top:42px}.nav{display:flex;gap:13px;align-items:center;padding:13px 12px;color:#8fa6c7;font-size:13px;border-radius:9px;margin-bottom:5px}.nav.active{color:#c5ecff;background:linear-gradient(90deg,#153d68,#102039);border:1px solid #24517c;box-shadow:inset 3px 0 #4fc9ff}.side-note{border:1px solid #244568;border-radius:10px;background:#0d2137;padding:15px;color:#c5e2f7;font-size:12px;line-height:2}.side-note small{color:#839dbb}.dot{display:inline-block;width:7px;height:7px;border-radius:50%;background:#8194ad;margin-right:7px}.dot.green{background:#32e3aa;box-shadow:0 0 10px #32e3aa}.sidebar-bottom{position:absolute;bottom:26px;font-size:12px;color:#8ba3c2;line-height:1.9}.sidebar-bottom span{color:#40cbfb}.sidebar-bottom small{font-size:10px}.main{min-width:0;flex:1;max-width:1900px;padding:24px 30px;margin:auto}header{display:flex;justify-content:space-between;gap:16px;align-items:center;padding-bottom:23px;border-bottom:1px solid #1c3451}.eyebrow,.section-kicker{font-size:10px;letter-spacing:2px;color:#56b8e9;font-weight:700}h1{font-size:29px;letter-spacing:-.7px;margin:8px 0 5px;font-weight:800}h1 span{color:#75b8ff}h1 em{font-size:10px;font-style:normal;border:1px solid #674cb5;border-radius:5px;color:#bda7ff;padding:5px 8px;letter-spacing:1px;vertical-align:middle;margin-left:8px}header p{margin:0;color:#8299b8;font-size:12px}.header-actions{display:flex;align-items:center;gap:20px}.pill{border:1px solid #23866c;color:#7aefd0;background:#0b2e2c;border-radius:30px;padding:9px 14px;font-size:10px;font-weight:700;letter-spacing:1px}.pill i{display:inline-block;width:6px;height:6px;background:#54f6bc;border-radius:50%;margin-right:7px}.clock{color:#879dbc;font-size:12px}.topbar{display:flex;justify-content:space-between;align-items:end;margin:23px 0 13px}.topbar h2{font-size:20px;margin:6px 0 0}.live-dot{display:inline-block;background:#34dca7;width:7px;height:7px;border-radius:50%;vertical-align:middle;box-shadow:0 0 13px #24e6a7}.actions{display:flex;gap:10px}button{border:0;border-radius:8px;padding:11px 15px;color:#e9f4ff;background:#172c48;font-size:12px;font-weight:700}.ghost{border:1px solid #2c4568}.primary{background:linear-gradient(110deg,#1369d9,#6b47d4);box-shadow:0 5px 25px #235be355}.panel{border:1px solid #223a5a;background:linear-gradient(150deg,#101e35ee,#0b1629ee);border-radius:13px;box-shadow:0 10px 35px #0003}.orchestra{position:relative;overflow:hidden;padding:18px 20px 0;isolation:isolate}.mesh{position:absolute;inset:0;z-index:-1;background-image:linear-gradient(#28507914 1px,transparent 1px),linear-gradient(90deg,#28507914 1px,transparent 1px);background-size:24px 24px;mask-image:linear-gradient(to bottom,transparent,#000 20%,#000 80%,transparent)}.conductor-wrap{display:flex;justify-content:center}.conductor{width:285px;display:flex;align-items:center;gap:14px;padding:13px 17px;border:1px solid #8054df;border-radius:13px;background:linear-gradient(110deg,#2d1e57,#161d43);box-shadow:0 0 26px #864de94a, inset 0 0 20px #a063ff17;position:relative}.conductor-icon{width:46px;height:46px;display:grid;place-items:center;border-radius:50%;border:1px solid #9a73ef;color:#ddc5ff;font-size:29px;text-shadow:0 0 15px #b79bff;box-shadow:0 0 20px #a77aff44}.conductor small{display:block;color:#ae9bd7;font-size:9px;letter-spacing:1.5px}.conductor strong{display:block;font-size:17px;margin:4px 0;font-weight:750}.conductor span{font-size:12px;color:#cbbce9}.conductor-indicator{position:absolute;right:14px;top:14px;width:7px;height:7px;border-radius:50%;background:#8e7bad}.conductor.running{animation:conductorPulse 1.5s infinite}.conductor.running .conductor-indicator{background:#c5a2ff;box-shadow:0 0 12px #d1a9ff}.connections{display:block;width:100%;height:90px;overflow:visible}.connections path{fill:none;stroke:#38527a;stroke-width:1.5;vector-effect:non-scaling-stroke}.connections .flowline{stroke:#a77bff;stroke-width:2;stroke-dasharray:7 20;opacity:.2}.orchestra.running .flowline{opacity:1;animation:dash 1.2s linear infinite;filter:drop-shadow(0 0 5px #b38cff)}.agents{display:grid;grid-template-columns:repeat(5,minmax(0,1fr));gap:12px;position:relative}.agent{min-width:0;border:1px solid color-mix(in srgb,var(--accent) 55%,#1d3150);background:linear-gradient(150deg,#142841,#0a182c);border-radius:12px;padding:13px 12px 11px;box-shadow:inset 0 0 25px color-mix(in srgb,var(--accent) 6%,transparent);transition:box-shadow .3s,transform .3s,border-color .3s}.agent.active{transform:translateY(-5px);border-color:var(--accent);box-shadow:0 0 24px color-mix(in srgb,var(--accent) 43%,transparent),inset 0 0 22px color-mix(in srgb,var(--accent) 12%,transparent)}.agent.done{border-color:var(--accent)}.agent-top{display:flex;justify-content:space-between;gap:4px;align-items:center;color:var(--accent);font-size:9px;letter-spacing:1px}.agent .status{font-size:8px;color:#8499b6;letter-spacing:0}.agent.active .status{color:var(--accent)}.agent-symbol{font-size:27px;color:var(--accent);margin-top:13px;text-shadow:0 0 17px var(--accent)}.agent h3{font-size:17px;line-height:1.22;font-weight:750;letter-spacing:.1px;margin:9px 0 5px}.agent p{font-size:13px;line-height:1.35;color:#c2d2e8;margin:0;font-weight:500}.mini-visual{height:42px;margin:13px 0;color:var(--accent);font-size:24px;display:flex;align-items:center;justify-content:center}.mini-visual.bars{align-items:end;gap:6px;padding-bottom:6px}.bars b{background:var(--accent);opacity:.8;width:9px;border-radius:2px 2px 0 0;height:45%}.bars b:nth-child(2){height:80%}.bars b:nth-child(3){height:55%}.bars b:nth-child(4){height:95%}.bars b:nth-child(5){height:65%}.bars b:nth-child(6){height:75%}.bars b:nth-child(7){height:45%}.spark{font-size:27px;letter-spacing:-2px}.rings{font-size:22px;gap:9px}.rings span{font-size:29px}.checkicons{font-size:19px}.reportlines{font-size:13px;line-height:1.1;letter-spacing:3px}.agent-bottom{display:flex;align-items:center;gap:7px}.track{height:5px;flex:1;background:#26344e;border-radius:20px;overflow:hidden}.track i{display:block;height:100%;width:0;background:var(--accent);box-shadow:0 0 9px var(--accent);transition:width .13s linear}.percent{font-size:9px;color:#a3b3cc;width:25px;text-align:right}.orchestra-footer{display:flex;justify-content:space-between;gap:10px;border-top:1px solid #203550;margin-top:20px;padding:13px 0;color:#849bb8;font-size:10px}.content-grid{display:grid;grid-template-columns:minmax(0,1.65fr) minmax(295px,.75fr);gap:14px;margin-top:15px}.left-content,.right-content{display:flex;flex-direction:column;gap:14px;min-width:0}.kpi-grid{display:grid;grid-template-columns:repeat(4,minmax(0,1fr));gap:11px}.kpi{padding:15px;border:1px solid #2b4160;background:linear-gradient(135deg,#11223b,#0c192d);border-radius:10px}.kpi small{display:block;font-size:10px;color:#829bbb}.kpi strong{display:block;font-size:24px;margin:8px 0 3px}.kpi span{color:#55dcb5;font-size:10px}.kpi:last-child span{color:#ffba6b}.charts-grid{display:grid;grid-template-columns:1.15fr 1fr;gap:14px}.chart-panel,.verification,.logpanel,.chatpanel,.preview{padding:17px}.panel-heading{display:flex;align-items:start;justify-content:space-between;gap:10px;margin-bottom:12px}.panel-heading h3{font-size:14px;margin:5px 0}.subtle{font-size:10px;color:#8aa2c0}.chart-panel canvas{display:block;width:100%;height:165px}.legend{display:flex;gap:10px;flex-wrap:wrap;color:#9caec7;font-size:9px;margin-top:10px}.legend i{display:inline-block;width:8px;height:8px;border-radius:50%;margin-right:3px}.checks{display:flex;flex-direction:column;gap:9px}.check-row{display:flex;justify-content:space-between;font-size:10px;color:#aebed3;border-bottom:1px solid #223650;padding-bottom:7px;gap:5px}.check-row strong{color:#61e4b5}.check-row strong.bad{color:#ffb56b}.warning{background:#5b3d1e55;border:1px solid #9c6b37;border-radius:7px;padding:9px;margin-top:12px;color:#ffc48b;font-size:10px}.warn-label{font-size:9px;color:#ffb971;border:1px solid #7b5834;padding:5px 8px;border-radius:5px}.logpanel{min-height:145px}.logs{max-height:150px;overflow:auto;font-size:11px;color:#abc0d9}.logitem{padding:6px 0;border-bottom:1px solid #1d314b}.logitem time{color:#6985a7;margin-right:10px}.logitem.muted{color:#7287a4}.chatpanel{display:flex;flex-direction:column;min-height:425px}.chat-icon{color:#b2a0ff}.demo-badge{font-size:9px;border:1px solid #4a6384;color:#9eb5d3;padding:5px 7px;border-radius:6px}.chat-messages{flex:1;overflow:auto;max-height:275px;min-height:140px;display:flex;flex-direction:column;gap:12px}.message{padding:12px;border-radius:10px;font-size:11px;line-height:1.55}.message.assistant{background:#142c49;border:1px solid #264c74;align-self:flex-start}.message.user{background:#25385c;border:1px solid #3e5681;align-self:flex-end}.message small{color:#6ed0f2;font-size:9px;letter-spacing:1px}.message p{margin:6px 0 0}.quick-questions{display:flex;flex-wrap:wrap;gap:6px;margin:12px 0}.quick-questions button{font-size:9px;padding:7px;background:#152944;border:1px solid #315075}.chat-form{display:flex;gap:7px}.chat-form input{min-width:0;flex:1;background:#0b1b30;border:1px solid #2c4b6e;border-radius:8px;padding:12px;color:white;font-size:11px}.chat-form button{background:#275cba;font-size:16px;padding:7px 13px}.chat-disclaimer{font-size:9px;color:#7e91ac;margin-top:9px}.paper{background:#f3f7fd;color:#163354;border-radius:5px;padding:16px;min-height:225px;position:relative;overflow:hidden}.paper-top{display:flex;justify-content:space-between;align-items:center;border-bottom:2px solid #0e618c;padding-bottom:7px}.paper-top strong{font-size:14px}.paper-top small{font-size:7px;color:#5b7490}.paper h4{font-size:18px;margin:14px 0 2px}.paper p{font-size:9px;color:#67809c;margin:0}.paper-kpis{display:grid;grid-template-columns:repeat(3,1fr);gap:5px;margin:13px 0}.paper-kpis div{background:#e0ebf5;border-radius:4px;padding:7px}.paper-kpis strong{font-size:14px;display:block}.paper-kpis small{font-size:8px;color:#52728c}.paper-bars{height:43px;display:flex;align-items:end;gap:5px}.paper-bars i{flex:1;height:45%;background:#338bb9}.paper-bars i:nth-child(3n){height:80%;background:#f2aa65}.paper-bars i:nth-child(4n){height:65%}.paper-bars i:nth-child(5n){height:95%;background:#42b9a2}.paper-watermark{position:absolute;bottom:0;left:0;right:0;background:#a13c3c;color:white;text-align:center;padding:4px;font-size:9px;letter-spacing:1px}.export{width:100%;margin-top:10px;border:1px solid #345579;background:#183354}.export:disabled{opacity:.6}footer{display:flex;justify-content:space-between;gap:10px;padding:22px 0;color:#6e88a9;font-size:9px;letter-spacing:1px}@keyframes dash{to{stroke-dashoffset:-54}}@keyframes conductorPulse{50%{box-shadow:0 0 42px #9a6bffbb,inset 0 0 28px #a063ff33}}@media(max-width:1250px){.sidebar{width:185px}.main{padding:18px}.agents{gap:7px}.agent{padding:10px 8px}.agent h3{font-size:14px}.agent p{font-size:12px}.charts-grid{grid-template-columns:1fr}}@media(max-width:900px){.sidebar{display:none}.content-grid{grid-template-columns:1fr}.right-content{display:grid;grid-template-columns:1fr 1fr}.agents{grid-template-columns:repeat(5,minmax(0,1fr))}}@media(max-width:680px){.main{padding:12px}header{align-items:start}h1{font-size:21px}.header-actions .clock{display:none}.topbar{align-items:start;gap:10px}.topbar h2{font-size:16px}.actions{flex-direction:column}.agents{grid-template-columns:repeat(2,minmax(0,1fr))}.connections{display:none}.orchestra{padding:12px}.agent:last-child{grid-column:span 2}.kpi-grid{grid-template-columns:repeat(2,1fr)}.right-content{display:flex}.orchestra-footer{flex-wrap:wrap}.clock{display:none}footer span{display:none}}@media print{.sidebar,header,.topbar,.orchestra,.chatpanel,.logpanel,.chart-panel,.verification,.kpi-grid,footer,.export{display:none!important}.content-grid,.right-content{display:block}.preview{border:0;background:white}.paper{max-width:750px;min-height:800px;margin:auto}.paper-watermark{font-size:15px}.paper h4{font-size:40px}}
/* ORCHESTRA v0.3 — alleen de dirigent: subtiele activiteit en gestuurde lichtsignalen */
.conductor-icon {position:relative;isolation:isolate;flex:none;}
.conductor-icon::before,.conductor-icon::after {content:"";position:absolute;border-radius:50%;pointer-events:none;}
.conductor-icon::before {inset:-7px;border:2px solid transparent;border-top-color:#cfb0ff;border-right-color:#9367ff;filter:drop-shadow(0 0 5px #ab74ff);animation:orbitConductor 10s linear infinite;opacity:.9;}
.conductor-icon::after {inset:-3px;border:1px dashed #9470db;animation:orbitConductor 18s linear infinite reverse;opacity:.38;}
.conductor-icon .brain-symbol {width:30px;height:30px;display:block;position:relative;z-index:1;filter:drop-shadow(0 0 5px #ad82ff);animation:brainBreath 3.8s ease-in-out infinite;transform-origin:center;}
.conductor.running .conductor-icon::before {animation-duration:2.1s;border-top-color:#e6d3ff;border-right-color:#be98ff;filter:drop-shadow(0 0 9px #c99aff);}
.conductor.running .conductor-icon::after {animation-duration:5s;opacity:.75;}
.conductor.running .brain-symbol {animation-duration:1.7s;}
.conductor.attention .conductor-icon::before {border-top-color:#ffbc6d;border-right-color:#f59c4b;filter:drop-shadow(0 0 6px #ffb167);}
.conductor.attention .brain-symbol {color:#ffd3a0;filter:drop-shadow(0 0 7px #ffb167);}
.connections .signal-layer .signal-particle {stroke:none;filter:drop-shadow(0 0 4px #c69aff);}
.connections .signal-layer .signal-halo {stroke:none;opacity:.24;filter:drop-shadow(0 0 7px #b78cff);}
@keyframes orbitConductor {to{transform:rotate(360deg)}}
@keyframes brainBreath {0%,100% {opacity:.78;transform:scale(.94)}50% {opacity:1;transform:scale(1.08)}}
@media (prefers-reduced-motion:reduce) { .conductor-icon::before,.conductor-icon::after,.conductor-icon .brain-symbol {animation:none!important} .connections .signal-layer {display:none} }
/* v0.4 – compacte invoerbalk; bestaande dashboardstijl bewust ongewijzigd */
.import-panel{display:flex;align-items:center;gap:13px;flex-wrap:wrap;padding:12px 17px;margin-bottom:13px;border-color:#305278;background:linear-gradient(135deg,#10263c,#0d192f)}
.import-intro{display:flex;flex-direction:column;gap:4px;flex:1;min-width:250px}.import-intro strong{font-size:14px}.import-intro small{font-size:10px;color:#9bb0cd}
.import-file-label{padding:10px 14px;border:1px solid #3666a4;border-radius:9px;background:#162c49;color:#dcf1ff;font-size:12px;font-weight:700;cursor:pointer;white-space:nowrap}
.import-file-label span{color:#53d9bf;font-size:16px;margin-right:7px}
#scanFile{display:none}.site-label{font-size:11px;color:#a6bad8}#siteSelect{background:#132947;border:1px solid #325172;border-radius:8px;color:#eaf2ff;max-width:185px;padding:9px;font:inherit;font-size:12px}
#siteSelect:disabled{opacity:.55}.import-state{color:#78e0c3;font-size:11px;max-width:210px;line-height:1.4}
@media(max-width:760px){.import-panel{align-items:start}.import-intro{flex-basis:100%}.import-state{flex-basis:100%;max-width:none}}
</style></head>
<body>
<div class="app">
<main class="main">
<header><div><div class="eyebrow">REAKT / AI ANALYTICS PLATFORM</div><h1>Inloop <span>Intelligence</span> <em>ORCHESTRA</em></h1><p>Een intelligente blik op ontwikkeling, kwaliteit en herstelondersteunende inloop</p></div><div class="header-actions"><span class="pill"><i></i> DEMO MODE</span><span class="clock" id="clock"></span></div></header>
<div class="topbar"><div><span class="section-kicker">CONTROL CENTER</span><h2>Multi-Agent Orchestration <span class="live-dot"></span></h2></div><div class="actions"><button id="resetBtn" class="ghost">↺ Reset</button><button id="startBtn" class="primary">▶ Start demo-analyse</button></div></div>
<section class="import-panel panel" aria-label="Excel-proef met fictieve gegevens">
<div class="import-intro"><span class="section-kicker">VOLGENDE STAP · EXCEL-PROEF</span><strong>Lokale scandata-test</strong><small>Alleen fictieve of volledig geanonimiseerde testbestanden · geen upload naar een server</small></div>
<label class="import-file-label" for="scanFile"><span>▤</span> Kies testbestand (.xlsx)</label><input type="file" id="scanFile" accept=".xlsx" />
<label class="site-label" for="siteSelect">Locatie</label><select id="siteSelect" disabled><option value="*">Alle locaties</option></select>
<span id="importState" class="import-state">Demo staat klaar</span>
</section>
<section class="orchestra panel">
<div class="mesh"></div><div class="conductor-wrap"><div class="conductor" id="conductor"><div class="conductor-glow"></div><div class="conductor-icon" aria-hidden="true"><svg class="brain-symbol" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="1.55" stroke-linecap="round" stroke-linejoin="round"><path d="M12 5c-2.7-3.2-7.4-.9-7.1 2.4C2 9 2 13.6 5 15.2c-.7 4.4 4.9 6.8 7 2.6M12 5c2.7-3.2 7.4-.9 7.1 2.4 2.9 1.6 2.9 6.2-.1 7.8.7 4.4-4.9 6.8-7 2.6M12 5v13"/><path d="M6.6 10.1 9 12v3M17.4 10.1 15 12v3"/><circle cx="9" cy="15.5" r=".9" fill="currentColor" stroke="none"/><circle cx="15" cy="15.5" r=".9" fill="currentColor" stroke="none"/></svg></div><div><small>MASTER ORCHESTRATOR</small><strong>THE CONDUCTOR</strong><span id="conductorStatus">Gereed voor analyse</span></div><div class="conductor-indicator"></div></div></div>
<svg class="connections" viewBox="0 0 1000 128" preserveAspectRatio="none" aria-hidden="true"><path class="stem" d="M500 0 V30 H100 V90 M500 30 H300 V90 M500 30 V90 M500 30 H700 V90 M500 30 H900 V90"/><path class="flowline" d="M500 0 V30 H100 V90 M500 30 H300 V90 M500 30 V90 M500 30 H700 V90 M500 30 H900 V90"/><g class="signal-layer" aria-hidden="true"></g></svg>
<div class="agents">
<article class="agent" id="agent-data" style="--accent:#34e6b3"><div class="agent-top"><span>01 / DATA</span><span class="status">STANDBY</span></div><div class="agent-symbol">▤</div><h3>Data Agent</h3><p>Structuur & kwaliteit</p><div class="mini-visual bars"><b></b><b></b><b></b><b></b><b></b><b></b><b></b></div><div class="agent-bottom"><div class="track"><i></i></div><span class="percent">0%</span></div></article>
<article class="agent" id="agent-analysis" style="--accent:#ffcb62"><div class="agent-top"><span>02 / ANALYSE</span><span class="status">STANDBY</span></div><div class="agent-symbol">⌁</div><h3>Analysis Agent</h3><p>Trends & vergelijkingen</p><div class="mini-visual spark">▁ ▃ ▂ ▅ ▄ ▆ ▇</div><div class="agent-bottom"><div class="track"><i></i></div><span class="percent">0%</span></div></article>
<article class="agent" id="agent-insight" style="--accent:#51b9ff"><div class="agent-top"><span>03 / INZICHT</span><span class="status">STANDBY</span></div><div class="agent-symbol">✦</div><h3>Insight Agent</h3><p>Signalen & duiding</p><div class="mini-visual rings">◎ <span>◉</span> ◎</div><div class="agent-bottom"><div class="track"><i></i></div><span class="percent">0%</span></div></article>
<article class="agent" id="agent-verify" style="--accent:#b18aff"><div class="agent-top"><span>04 / CONTROLE</span><span class="status">STANDBY</span></div><div class="agent-symbol">⬡</div><h3>Verification Agent</h3><p>Schaduwberekeningen</p><div class="mini-visual checkicons">✓ ✓ ! ✓</div><div class="agent-bottom"><div class="track"><i></i></div><span class="percent">0%</span></div></article>
<article class="agent" id="agent-report" style="--accent:#ff9a65"><div class="agent-top"><span>05 / RAPPORT</span><span class="status">STANDBY</span></div><div class="agent-symbol">▧</div><h3>Report Agent</h3><p>Infographic & export</p><div class="mini-visual reportlines">▰▰▰▰▰<br>▰▰▰▰<br>▰▰▰</div><div class="agent-bottom"><div class="track"><i></i></div><span class="percent">0%</span></div></article>
</div>
<div class="orchestra-footer"><span><i class="dot green"></i> Systeem gereed</span><span id="flowState">Wacht op startcommando</span><span>◌ <span id="completed">0</span> / 5 agenten voltooid</span></div>
</section>
<div class="content-grid">
<div class="left-content">
<div class="kpi-grid" id="kpis"></div>
<div class="charts-grid"><section class="panel chart-panel"><div class="panel-heading"><div><span class="section-kicker">TRENDANALYSE</span><h3>Bezoeken per locatie</h3></div><span class="subtle">Jul – Sep 2026</span></div><canvas id="chart" width="640" height="250" aria-label="Grafiek fictieve bezoeken per locatie"></canvas><div class="legend"><span><i style="background:#3ee1b6"></i> Soeda</span><span><i style="background:#ffc66a"></i> Rietdijk</span><span><i style="background:#5b9cff"></i> Vredehof</span><span><i style="background:#ad88ff"></i> Gouda</span></div></section>
<section class="panel verification"><div class="panel-heading"><div><span class="section-kicker">KWALITEITSBORGING</span><h3>Verificatie Businesscontroller</h3></div><span class="warn-label">DEMO</span></div><div id="checks" class="checks"></div><div class="warning">⚠ 1 fictieve afwijking. Rapport niet gevalideerd.</div></section></div>
<section class="panel logpanel"><div class="panel-heading"><div><span class="section-kicker">REAL-TIME PROCESS</span><h3>Agent activity log</h3></div><span class="subtle" id="logStatus">Stand-by</span></div><div class="logs" id="logs"><div class="logitem muted">◌ Klik op ‘Start demo-analyse’ om de workflow te bekijken.</div></div></section>
</div>
<aside class="right-content"><section class="panel chatpanel"><div class="panel-heading"><div><span class="section-kicker">MANAGEMENT ASSISTENT</span><h3><span class="chat-icon">✧</span> Vraag het aan Inloop AI</h3></div><span class="demo-badge">DEMO</span></div><div class="chat-messages" id="messages"><div class="message assistant"><small>INLOOP AI · DEMONSTRATIE</small><p>Welkom! Start de demo-analyse en stel daarna een vraag over de fictieve locatiecijfers.</p></div></div><div class="quick-questions"><button data-q="Welke locatie groeit het sterkst?">Grootste groei?</button><button data-q="Zijn de cijfers gevalideerd?">Validatiestatus?</button><button data-q="Maak een managementsamenvatting">Samenvatting?</button></div><form id="chatForm" class="chat-form"><input id="chatInput" autocomplete="off" placeholder="Stel een vraag over de demo..." aria-label="Vraag aan Inloop AI"><button aria-label="Verstuur vraag">➤</button></form><div class="chat-disclaimer">Vooraf ingestelde demo-antwoorden · geen echte AI</div></section>
<section class="panel preview"><div class="panel-heading"><div><span class="section-kicker">OUTPUT</span><h3>Infographic preview</h3></div><span class="warn-label">CONCEPT</span></div><div class="paper"><div class="paper-top"><strong>REAKT<span>.</span></strong><small>INLOOPMONITOR / Q3 2026</small></div><h4>Inloop in beeld</h4><p>Illustratieve managementrapportage</p><div class="paper-kpis"><div><strong>12.480</strong><small>Bezoeken</small></div><div><strong>1.286</strong><small>Uniek</small></div><div><strong>164</strong><small>Instroom</small></div></div><div class="paper-bars"><i></i><i></i><i></i><i></i><i></i><i></i><i></i><i></i><i></i><i></i><i></i></div><div class="paper-watermark">FICTIEVE DATA · NIET GEVALIDEERD</div></div><button class="export" id="exportBtn">↓ Print demo-infographic</button></section></aside>
</div>
<footer>REAKT INLOOP INTELLIGENCE <span>ORCHESTRA PROTOTYPE · ALLEEN FICTIEVE TESTGEGEVENS · GEEN PRODUCTIEOMGEVING</span></footer>
</main>
</div><script type="application/json" id="orchestra-demo-data">{"mode": "DEMO", "period": "Q3 2026 · fictief", "kpis": [{"label": "Bezoeken", "value": "12.480", "change": "+8,4%"}, {"label": "Unieke bezoekers", "value": "1.286", "change": "+3,2%"}, {"label": "Nieuwe instroom", "value": "164", "change": "+5,1%"}, {"label": "Verificatie", "value": "4 / 5", "change": "1 afwijking"}], "locations": [{"name": "Soeda", "visits": [350, 395, 428], "unique": 121}, {"name": "Rietdijk", "visits": [285, 305, 334], "unique": 108}, {"name": "Vredehof", "visits": [245, 268, 294], "unique": 93}, {"name": "Gouda", "visits": [320, 343, 365], "unique": 115}], "checks": [{"label": "Bezoeken", "ours": 2845, "shadow": 2845}, {"label": "Unieke bezoekers", "ours": 386, "shadow": 381}, {"label": "Nieuwe instroom", "ours": 42, "shadow": 42}, {"label": "Bezoeken vorig jaar", "ours": 2610, "shadow": 2610}, {"label": "Actieve locaties", "ours": 4, "shadow": 4}], "steps": [{"agent": "data", "text": "Demo-gegevens ingelezen en structuur gecontroleerd"}, {"agent": "analysis", "text": "Maandtrends en locatievergelijkingen berekend"}, {"agent": "insight", "text": "Opvallende ontwikkelingen samengevat (demo)"}, {"agent": "verify", "text": "Schaduwberekeningen vergeleken: 1 afwijking"}, {"agent": "report", "text": "Conceptinfographic voorbereid · NIET GEVALIDEERD"}]}</script>
<script>
window.addEventListener('error', function(event){
var status=document.getElementById('flowState');
if(status) status.textContent='Scriptfout – meld dit bij de ontwikkelaar';
console.error('ORCHESTRA scriptfout:', event.message);
});
'use strict';
let demoData=null, running=false, completed=0;
const $=id=>document.getElementById(id);
const ids=['data','analysis','insight','verify','report'];
const sleep=ms=>new Promise(resolve=>setTimeout(resolve,ms));
const escapeHTML=s=>String(s).replace(/[&<>"']/g,c=>({'&':'&','<':'<','>':'>','"':'"',"'":'''}[c]));
function clock(){ $('clock').textContent=new Intl.DateTimeFormat('nl-NL',{hour:'2-digit',minute:'2-digit',day:'numeric',month:'short'}).format(new Date()); }
clock();setInterval(clock,60000);
function renderKPIs(data){$('kpis').innerHTML=data.kpis.map(k=>`<div class="kpi"><small>${escapeHTML(k.label)}</small><strong>${escapeHTML(k.value)}</strong><span>${escapeHTML(k.change)}</span></div>`).join('');}
function renderChecks(data){$('checks').innerHTML=data.checks.map(c=>{const delta=c.ours-c.shadow;return `<div class="check-row"><span>${escapeHTML(c.label)}</span><span>${c.ours.toLocaleString('nl-NL')} / ${c.shadow.toLocaleString('nl-NL')} <strong class="${delta?'bad':''}">${delta?'⚠ '+(delta>0?'+':'')+delta:'✓ 0'}</strong></span></div>`}).join('');}
function drawChart(data){const canvas=$('chart'),ctx=canvas.getContext('2d'),w=640,h=250;ctx.clearRect(0,0,w,h);ctx.font='13px Segoe UI';ctx.textAlign='left';const max=500;for(let i=0;i<=4;i++){let y=25+i*45;ctx.strokeStyle='#29405a';ctx.beginPath();ctx.moveTo(44,y);ctx.lineTo(w-15,y);ctx.stroke();ctx.fillStyle='#8298b4';ctx.fillText(String(Math.round(max*(4-i)/4)),5,y+4);}const cols=['#3ee1b6','#ffc66a','#5b9cff','#ad88ff'];data.locations.forEach((loc,index)=>{ctx.strokeStyle=cols[index];ctx.lineWidth=3;ctx.beginPath();loc.visits.forEach((v,j)=>{const x=90+j*235,y=205-(v/max)*180;if(j===0)ctx.moveTo(x,y);else ctx.lineTo(x,y)});ctx.stroke();loc.visits.forEach((v,j)=>{const x=90+j*235,y=205-(v/max)*180;ctx.fillStyle=cols[index];ctx.beginPath();ctx.arc(x,y,4,0,Math.PI*2);ctx.fill()})});ctx.textAlign='center';ctx.fillStyle='#a6b9d0';['JUL','AUG','SEP'].forEach((m,i)=>ctx.fillText(m,90+i*235,239));}
function log(text){const now=new Date().toLocaleTimeString('nl-NL',{hour:'2-digit',minute:'2-digit',second:'2-digit'});if($('logs').querySelector('.muted'))$('logs').innerHTML='';$('logs').insertAdjacentHTML('afterbegin',`<div class="logitem"><time>${now}</time>${escapeHTML(text)}</div>`)}
// De lichtpunten volgen de vijf bestaande SVG-verbindingen. Ze lopen alleen tijdens een actieve processtap.
const signalPaths={data:'M500 0 V30 H100 V90',analysis:'M500 0 V30 H300 V90',insight:'M500 0 V90',verify:'M500 0 V30 H700 V90',report:'M500 0 V30 H900 V90'};
const signalColors={data:'#34e6b3',analysis:'#ffcb62',insight:'#51b9ff',verify:'#b18aff',report:'#ff9a65'};
function clearSignal(){const layer=document.querySelector('.signal-layer');if(layer)layer.replaceChildren();}
function signalFor(agent){
clearSignal();
const layer=document.querySelector('.signal-layer');
if(!layer||!signalPaths[agent]||window.matchMedia('(prefers-reduced-motion: reduce)').matches)return;
const ns='http://www.w3.org/2000/svg';
for(const [radius,cls,delay] of [[7,'signal-halo','0s'],[3.2,'signal-particle','0s']]){
const circle=document.createElementNS(ns,'circle');circle.setAttribute('r',radius);circle.setAttribute('class',cls);circle.setAttribute('fill',signalColors[agent]);
const motion=document.createElementNS(ns,'animateMotion');motion.setAttribute('path',signalPaths[agent]);motion.setAttribute('dur','1.35s');motion.setAttribute('repeatCount','indefinite');motion.setAttribute('begin',delay);
circle.appendChild(motion);layer.appendChild(circle);
}
}
function reset(){if(running)return;clearSignal();$('conductor').classList.remove('attention');completed=0;$('completed').textContent='0';$('conductorStatus').textContent='Gereed voor analyse';$('flowState').textContent='Wacht op startcommando';$('logStatus').textContent='Stand-by';$('logs').innerHTML='<div class="logitem muted">◌ Klik op ‘Start demo-analyse’ om de workflow te bekijken.</div>';$('orchestra-root');document.querySelector('.orchestra').classList.remove('running');$('conductor').classList.remove('running');ids.forEach(id=>{let e=$('agent-'+id);e.classList.remove('active','done');e.querySelector('.status').textContent='STANDBY';e.querySelector('.track i').style.width='0%';e.querySelector('.percent').textContent='0%'});}
async function start(){if(importedData)return startImported();if(running)return;clearSignal();$('conductor').classList.remove('attention');running=true;$('startBtn').disabled=true;$('resetBtn').disabled=true;completed=0;ids.forEach(id=>{const e=$('agent-'+id);e.classList.remove('done','active');e.querySelector('.status').textContent='STANDBY';e.querySelector('.track i').style.width='0%';e.querySelector('.percent').textContent='0%'});$('logs').innerHTML='';$('completed').textContent='0';$('conductor').classList.add('running');document.querySelector('.orchestra').classList.add('running');$('conductorStatus').textContent='Workflow wordt aangestuurd';$('logStatus').textContent='Actief';log('Dirigent start de gesimuleerde workflow');try{const response=await Promise.resolve({ok:true,json:async()=>JSON.parse(document.getElementById('orchestra-demo-data').textContent)});if(!response.ok)throw Error('API reageert niet');demoData=await response.json();renderKPIs(demoData);renderChecks(demoData);drawChart(demoData);for(let i=0;i<demoData.steps.length;i++){const step=demoData.steps[i];const e=$('agent-'+step.agent);signalFor(step.agent);e.classList.add('active');e.querySelector('.status').textContent='ACTIEF';$('flowState').textContent=step.text;log('Start: '+e.querySelector('h3').textContent);for(let p=0;p<=100;p+=5){e.querySelector('.track i').style.width=p+'%';e.querySelector('.percent').textContent=p+'%';await sleep(36);}clearSignal();e.classList.remove('active');e.classList.add('done');if(step.agent==='verify' && demoData.checks.some(c=>c.ours!==c.shadow))$('conductor').classList.add('attention');e.querySelector('.status').textContent=step.agent==='verify'?'AFWIJKING':'GEREED';completed++;$('completed').textContent=String(completed);log(step.text);await sleep(280)}$('conductorStatus').textContent='Analyse voltooid · controle vereist';$('flowState').textContent='Concept gereed · NIET GEVALIDEERD';$('logStatus').textContent='Afgerond';log('Eindstatus: concept beschikbaar, niet vrijgegeven');}catch(e){log('FOUT: '+e.message);$('flowState').textContent='Verwerking mislukt';$('conductorStatus').textContent='Fout gedetecteerd';}finally{clearSignal();running=false;$('startBtn').disabled=false;$('resetBtn').disabled=false;$('conductor').classList.remove('running');document.querySelector('.orchestra').classList.remove('running')}}
function reply(question){const q=question.toLowerCase();if(!completed)return 'Start eerst de demo-analyse. Daarna kan ik vragen beantwoorden over de fictieve cijfers.';if(/valid|wouter|controle|betrouw|verschil|akkoord/.test(q))return 'In deze fictieve demo wijken de unieke bezoekers af: monitor 386 tegenover schaduwberekening 381, een verschil van 5. De oorzaak is niet vastgesteld. Daarom blijft de infographic NIET GEVALIDEERD.';if(/groei|stijg|locatie|soeda|rietdijk|vredehof|gouda/.test(q))return 'In de fictieve reeks juli–september stijgt Soeda van 350 naar 428 bezoeken (+22,3%). Rietdijk stijgt van 285 naar 334 (+17,2%), Vredehof van 245 naar 294 (+20,0%) en Gouda van 320 naar 365 (+14,1%). Soeda heeft de grootste relatieve groei.';if(/samenvat|rapport|infographic|management/.test(q))return 'Conceptmanagementsamenvatting (fictief): alle vier getoonde locaties laten in juli–september groei in bezoeken zien. Er is een afwijking van 5 unieke bezoekers bij de schaduwcontrole. Publicatie is niet toegestaan totdat dit verschil is onderzocht en beoordeeld.';if(/bezoek|uniek|instroom|kwartaal|cijfer/.test(q))return 'De fictieve KPI-kaarten tonen 12.480 bezoeken, 1.286 unieke bezoekers en 164 nieuwe instromers. Deze totaalcijfers zijn illustratief en vormen geen optelsom van de vier voorbeeldlocaties in de grafiek.';return 'Dit is een demo-chat zonder AI-API. Ik kan alleen vooraf gedefinieerde vragen over de fictieve locatiegroei, KPI’s, verificatie en managementsamenvatting beantwoorden.'}
function addMessage(role,text){const e=document.createElement('div');e.className='message '+role;if(role==='assistant'){const s=document.createElement('small');s.textContent='INLOOP AI · DEMONSTRATIE';e.appendChild(s)}const p=document.createElement('p');p.textContent=text;e.appendChild(p);$('messages').appendChild(e);$('messages').scrollTop=$('messages').scrollHeight;}
function ask(q){if(!q.trim())return;addMessage('user',q);addMessage('assistant',reply(q));}
// v0.4: veilige browser-lokale Excel-proef, uitsluitend met fictieve / geanonimiseerde voorbeelden.
// Er vindt GEEN netwerkverzoek, bestandsoverdracht of externe AI-aanroep plaats.
let importedData=null;
const excelLimit=2*1024*1024; // om echte grote (en mogelijk cliëntgevoelige) dumps nog niet te verwerken
const excelNS='http://schemas.openxmlformats.org/spreadsheetml/2006/main';
const decoder=new TextDecoder('utf-8');
function bytesText(a){return decoder.decode(a)}
function zipDirectory(buffer){
const bytes=new Uint8Array(buffer), view=new DataView(buffer);
let eocd=-1;
for(let i=bytes.length-22;i>=Math.max(0,bytes.length-65557);i--){if(view.getUint32(i,true)===0x06054b50){eocd=i;break}}
if(eocd<0)throw Error('Geen geldig XLSX-/ZIP-bestand');
const count=view.getUint16(eocd+10,true), offset=view.getUint32(eocd+16,true);
if(count>1000)throw Error('Dit testbestand heeft te veel onderdelen');
const entries=new Map();let pos=offset;
for(let i=0;i<count;i++){
if(view.getUint32(pos,true)!==0x02014b50)throw Error('ZIP-structuur ongeldig');
const method=view.getUint16(pos+10,true), size=view.getUint32(pos+20,true),len=view.getUint16(pos+28,true),extra=view.getUint16(pos+30,true),comment=view.getUint16(pos+32,true),local=view.getUint32(pos+42,true);
const name=bytesText(bytes.subarray(pos+46,pos+46+len));
if(name.startsWith('xl/'))entries.set(name,{method,size,local});
pos+=46+len+extra+comment;
}
return {view,bytes,entries};
}
async function xmlEntry(zip,name){
const entry=zip.entries.get(name);if(!entry)return '';
const {view,bytes}=zip, off=entry.local;
if(view.getUint32(off,true)!==0x04034b50)throw Error('ZIP-bestandskop ongeldig');
const start=off+30+view.getUint16(off+26,true)+view.getUint16(off+28,true);
const compressed=bytes.slice(start,start+entry.size);
if(entry.method===0)return bytesText(compressed);
if(entry.method!==8)throw Error('Niet-ondersteunde Excel-compressie');
if(typeof DecompressionStream!=='function')throw Error('Deze browser ondersteunt lokale Excel-decompressie niet');
return await new Response(new Blob([compressed]).stream().pipeThrough(new DecompressionStream('deflate-raw'))).text();
}
function xmlDoc(str){
const doc=new DOMParser().parseFromString(str,'application/xml');
if(doc.getElementsByTagName('parsererror').length)throw Error('Excel-XML niet leesbaar');return doc;
}
function cellsToDict(row,shared){
const values={};
for(const c of row.getElementsByTagNameNS(excelNS,'c')){
const key=(c.getAttribute('r')||'').match(/^[A-Z]+/);if(!key)continue;
const v=c.getElementsByTagNameNS(excelNS,'v')[0],is=c.getElementsByTagNameNS(excelNS,'is')[0];let value=v?.textContent??'';
if(c.getAttribute('t')==='s')value=shared[Number(value)]??'';
else if(c.getAttribute('t')==='inlineStr')value=is?.textContent??'';
values[key[0]]=value;
}return values;
}
function scanDate(v){
if(v===''||v===undefined||v===null)return '';
const numeric=Number(v);
if(v!==''&&Number.isFinite(numeric)&&numeric>30000&&numeric<80000){
const d=new Date(Date.UTC(1899,11,30)+Math.floor(numeric)*86400000);
return d.toISOString().slice(0,10);
}
const str=String(v).trim(),iso=str.match(/^(\d{4})-(\d{2})-(\d{2})/);
if(iso)return `${iso[1]}-${iso[2]}-${iso[3]}`;
const nl=str.match(/^(\d{1,2})[-/](\d{1,2})[-/](\d{4})/);
if(nl)return `${nl[3]}-${nl[2].padStart(2,'0')}-${nl[1].padStart(2,'0')}`;
return '';
}
async function readExcelLocal(file){
if(!file.name.toLowerCase().endsWith('.xlsx'))throw Error('Kies een XLSX-testbestand');
if(file.size>excelLimit)throw Error('Dit is een grote productiedump. Gebruik voorlopig uitsluitend het bijgeleverde kleine fictieve testbestand.');
const buf=await file.arrayBuffer(),zip=zipDirectory(buf);
const shared=[];
const sharedXML=await xmlEntry(zip,'xl/sharedStrings.xml');
if(sharedXML){
const doc=xmlDoc(sharedXML);
for(const si of doc.getElementsByTagNameNS(excelNS,'si'))shared.push(si.textContent??'');
}
const sheetXML=await xmlEntry(zip,'xl/worksheets/sheet1.xml');
if(!sheetXML)throw Error('Werkblad Data dump niet gevonden');
const doc=xmlDoc(sheetXML),rows=doc.getElementsByTagNameNS(excelNS,'row');
if(rows.length<2)throw Error('Geen scanregels gevonden');
if(rows.length>18000)throw Error('Voor deze eerste proef maximaal 18.000 testregels');
const header=cellsToDict(rows[0],shared),lookup={};
for(const [col,headerText] of Object.entries(header))lookup[String(headerText).toLowerCase().trim()]=col;
for(const mandatory of ['scantijd','locatie','clientnummer'])if(!lookup[mandatory])throw Error('Verplichte kolom ontbreekt: '+mandatory);
const all=[],invalid={datum:0,locatie:0,client:0};
for(let i=1;i<rows.length;i++){
const c=cellsToDict(rows[i],shared),site=String(c[lookup.locatie]??'').trim(),id=String(c[lookup.clientnummer]??'').trim(),date=scanDate(c[lookup.scantijd]);
if(!date)invalid.datum++;
if(!site)invalid.locatie++;
if(!id)invalid.client++;
if(!site||!id||!date)continue;
all.push({date,site,id});
}
if(!all.length)throw Error('Geen bruikbare scanregels');
const months=[...new Set(all.map(r=>r.date.slice(0,7)))].sort();
const sites=[...new Set(all.map(r=>r.site))].sort((a,b)=>a.localeCompare(b,'nl'));
return {all,sites,months,rawCount:rows.length-1,invalid};
}
function calculateImported(site='*'){
const all=importedData.all.filter(r=>site==='*'||r.site===site),keyset=new Set(),ids=new Set(),monthSite=new Map();
for(const r of all){
const k=r.site+'|'+r.date+'|'+r.id;
if(keyset.has(k))continue;
keyset.add(k);ids.add(r.id);
const mk=r.site+'|'+r.date.slice(0,7);monthSite.set(mk,(monthSite.get(mk)||0)+1);
}
const ordered=[...new Set(all.map(x=>x.date.slice(0,7)))].sort(),chosen=ordered.slice(-3);
const locations=(site==='*'?importedData.sites:[site]).map(name=>({name,visits:chosen.map(m=>monthSite.get(name+'|'+m)||0)}));
return {site,rows:all.length,visits:keyset.size,unique:ids.size,duplicates:all.length-keyset.size,months:chosen,locations,invalid:importedData.invalid};
}
function drawImportedChart(result){
const cvs=$('chart'),ctx=cvs.getContext('2d'),w=cvs.width,h=cvs.height;
ctx.clearRect(0,0,w,h);ctx.font='13px Segoe UI';
const series=result.locations.slice(0,4),max=Math.max(10,...series.flatMap(x=>x.visits))*1.15;
ctx.strokeStyle='#29405a';ctx.fillStyle='#8aa4c3';ctx.textAlign='left';
for(let i=0;i<=4;i++){const y=25+i*45;ctx.beginPath();ctx.moveTo(44,y);ctx.lineTo(w-14,y);ctx.stroke();ctx.fillText(String(Math.round(max*(4-i)/4)),7,y+4)}
const colors=['#3ee1b6','#ffc66a','#5b9cff','#ad88ff'];
series.forEach((loc,li)=>{
ctx.strokeStyle=colors[li];ctx.lineWidth=3;ctx.beginPath();
loc.visits.forEach((v,j)=>{const x=90+j*235,y=205-(v/max)*180;j===0?ctx.moveTo(x,y):ctx.lineTo(x,y)});ctx.stroke();
loc.visits.forEach((v,j)=>{const x=90+j*235,y=205-(v/max)*180;ctx.fillStyle=colors[li];ctx.beginPath();ctx.arc(x,y,4,0,Math.PI*2);ctx.fill()});
});ctx.fillStyle='#a6b9d0';ctx.textAlign='center';
result.months.forEach((m,i)=>ctx.fillText(m,90+i*235,239));
const legend=document.querySelector('.legend');
legend.innerHTML=series.map((loc,i)=>`<span><i style="background:${colors[i]}"></i> ${escapeHTML(loc.name)}</span>`).join('');
document.querySelector('.chart-panel .subtle').textContent=result.months.join(' · ')+(result.locations.length>4?' · 4 van '+result.locations.length+' locaties':'');
}
function renderImported(result){
const num=n=>n.toLocaleString('nl-NL');
const kpis=[{label:'Bezoeken (voorlopig)',value:num(result.visits),change:'Unieke cliënt / dag / locatie'},{label:'Unieke cliëntnummers',value:num(result.unique),change:'Binnen geselecteerde periode'},{label:'Scanregels',value:num(result.rows),change:`${num(result.duplicates)} herhaalde scans`},{label:'Verificatie',value:'—',change:'Extern niet uitgevoerd'}];
renderKPIs({kpis});drawImportedChart(result);
$('checks').innerHTML='<div class="check-row"><span>Dubbele scans (voorlopige regel)</span><span>'+num(result.duplicates)+'</span></div><div class="check-row"><span>Unieke cliëntnummers</span><span>'+num(result.unique)+'</span></div><div class="check-row"><span>Schaduwberekening Businesscontroller</span><span>NIET AANGELEVERD</span></div>';
document.querySelector('.verification .warning').textContent='⚠ Proefberekening. Definities en externe controle nog niet gevalideerd.';
document.querySelector('.paper-watermark').textContent='FICTIEVE TESTGEGEVENS · NIET GEVALIDEERD';
document.querySelector('.paper p').textContent='Lokale Excel-proef · niet voor publicatie';
const pk=document.querySelectorAll('.paper-kpis strong');
[num(result.visits),num(result.unique),'—'].forEach((x,i)=>{if(pk[i])pk[i].textContent=x});
const pl=document.querySelectorAll('.paper-kpis small');if(pl[2])pl[2].textContent='Instroom niet bepaald';
}
async function startImported(){
if(running)return;
running=true;$('startBtn').disabled=true;$('resetBtn').disabled=true;completed=0;clearSignal();
$('logs').innerHTML='';$('completed').textContent='0';$('conductor').classList.remove('attention');
$('conductor').classList.add('running');document.querySelector('.orchestra').classList.add('running');
$('conductorStatus').textContent='Lokale Excel-proef actief';$('logStatus').textContent='Actief';
for(const id of ids){let el=$('agent-'+id);el.classList.remove('active','done');el.querySelector('.status').textContent='STANDBY';el.querySelector('.track i').style.width='0%';el.querySelector('.percent').textContent='0%'}
const result=calculateImported($('siteSelect').value);importedData.last=result;
const steps=[
{agent:'data',text:`${result.rows.toLocaleString('nl-NL')} scanregels gecontroleerd; ${result.duplicates} herhaalde scans aangetroffen`},
{agent:'analysis',text:`${result.visits} bezoeken (voorlopig) en ${result.unique} unieke cliëntnummers berekend`},
{agent:'insight',text:'Maandtrends berekend; AI-duiding nog niet verbonden'},
{agent:'verify',text:'Interne telling gecontroleerd; geen externe schaduwberekening beschikbaar'},
{agent:'report',text:'Conceptpreview bijgewerkt · NIET GEVALIDEERD'}];
try{
renderImported(result);
for(const step of steps){const el=$('agent-'+step.agent);signalFor(step.agent);el.classList.add('active');el.querySelector('.status').textContent='ACTIEF';$('flowState').textContent=step.text;log('Start: '+el.querySelector('h3').textContent);
for(let p=0;p<=100;p+=5){el.querySelector('.track i').style.width=p+'%';el.querySelector('.percent').textContent=p+'%';await sleep(30)}
clearSignal();el.classList.remove('active');el.classList.add('done');el.querySelector('.status').textContent=step.agent==='verify'?'NIET EXTERN':'GEREED';completed++;$('completed').textContent=String(completed);log(step.text);await sleep(200)
}
$('conductor').classList.add('attention');$('flowState').textContent='Concept · NIET GEVALIDEERD';$('conductorStatus').textContent='Proefberekening voltooid';$('logStatus').textContent='Afgerond';
}catch(err){log('FOUT: '+err.message);$('flowState').textContent='Proefberekening mislukt';}finally{clearSignal();running=false;$('startBtn').disabled=false;$('resetBtn').disabled=false;$('conductor').classList.remove('running');document.querySelector('.orchestra').classList.remove('running')}
}
$('scanFile').addEventListener('change',async e=>{
importedData=null;const file=e.target.files?.[0];if(!file)return;
const state=$('importState');state.textContent='Bestand wordt lokaal gelezen...';$('startBtn').disabled=true;
try{
importedData=await readExcelLocal(file);
const sel=$('siteSelect');sel.innerHTML='<option value="*">Alle locaties</option>';
importedData.sites.forEach(loc=>{const opt=document.createElement('option');opt.value=loc;opt.textContent=loc;sel.appendChild(opt)});
sel.disabled=false;
state.textContent=`${importedData.rawCount} testregels · ${importedData.sites.length} locaties`;
$('startBtn').textContent='▶ Analyseer testbestand';
$('flowState').textContent='Testbestand lokaal ingelezen · start de analyse';
log('Fictief Excel-testbestand is lokaal ingelezen; nog geen berekeningen gepubliceerd');
}catch(err){$('startBtn').textContent='▶ Start demo-analyse';state.textContent='⚠ '+err.message;$('siteSelect').disabled=true;log('Excel-proef: '+err.message)}finally{$('startBtn').disabled=false}
});
// Bestaande chat blijft demo-gericht. Voor echte Excel-proef antwoorden we uitsluitend met controleerbare berekende cijfers.
const originalReply=reply;
reply=function(q){
if(!importedData)return originalReply(q);
const r=importedData.last;
if(!r)return 'Klik op Start demo-analyse om de lokaal ingelezen testgegevens te berekenen.';
if(/valid|controle|wouter|business|betrouw|verschil|akkoord/.test(q.toLowerCase()))return 'Deze Excel-proef is NIET extern gevalideerd. De schaduwberekening van de businesscontroller ontbreekt en de rekenregels moeten nog worden afgestemd.';
if(/bezoek|uniek|scan|groei|locatie|samenvat|rapport|analyse|kwartaal|cijfer/.test(q.toLowerCase()))return `Testbestand (${r.site==='*'?'alle locaties':r.site}): ${r.rows} scanregels, ${r.visits} voorlopige bezoeken na ${r.duplicates} herhaalde scans, en ${r.unique} unieke cliëntnummers. Dit is een lokale proef zonder AI-API en zonder externe verificatie.`;
return 'De AI-API is nog niet gekoppeld. Ik kan voorlopig alleen de gecontroleerde basisgetallen van het fictieve Excel-testbestand toelichten.';
};
$('startBtn').addEventListener('click',start);$('resetBtn').addEventListener('click',reset);$('chatForm').addEventListener('submit',e=>{e.preventDefault();const q=$('chatInput').value;$('chatInput').value='';ask(q)});document.querySelectorAll('[data-q]').forEach(b=>b.addEventListener('click',()=>ask(b.dataset.q)));$('exportBtn').addEventListener('click',()=>window.print());
Promise.resolve({ok:true,json:async()=>JSON.parse(document.getElementById('orchestra-demo-data').textContent)}).then(r=>r.json()).then(d=>{demoData=d;renderKPIs(d);renderChecks(d);drawChart(d)}).catch(()=>log('Demo-gegevens konden niet worden geladen'));
</script></body></html>
|