Marcel0123 commited on
Commit
fabf837
·
verified ·
1 Parent(s): 1a40f6f

Upload index .html

Browse files
Files changed (1) hide show
  1. index .html +293 -0
index .html ADDED
@@ -0,0 +1,293 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ <!doctype html>
2
+ <html lang="nl"><head><meta charset="utf-8"><meta name="viewport" content="width=device-width,initial-scale=1"><title>Reakt | ORCHESTRA</title><style>
3
+ :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}}
4
+
5
+
6
+ /* ORCHESTRA v0.3 — alleen de dirigent: subtiele activiteit en gestuurde lichtsignalen */
7
+ .conductor-icon {position:relative;isolation:isolate;flex:none;}
8
+ .conductor-icon::before,.conductor-icon::after {content:"";position:absolute;border-radius:50%;pointer-events:none;}
9
+ .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;}
10
+ .conductor-icon::after {inset:-3px;border:1px dashed #9470db;animation:orbitConductor 18s linear infinite reverse;opacity:.38;}
11
+ .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;}
12
+ .conductor.running .conductor-icon::before {animation-duration:2.1s;border-top-color:#e6d3ff;border-right-color:#be98ff;filter:drop-shadow(0 0 9px #c99aff);}
13
+ .conductor.running .conductor-icon::after {animation-duration:5s;opacity:.75;}
14
+ .conductor.running .brain-symbol {animation-duration:1.7s;}
15
+ .conductor.attention .conductor-icon::before {border-top-color:#ffbc6d;border-right-color:#f59c4b;filter:drop-shadow(0 0 6px #ffb167);}
16
+ .conductor.attention .brain-symbol {color:#ffd3a0;filter:drop-shadow(0 0 7px #ffb167);}
17
+ .connections .signal-layer .signal-particle {stroke:none;filter:drop-shadow(0 0 4px #c69aff);}
18
+ .connections .signal-layer .signal-halo {stroke:none;opacity:.24;filter:drop-shadow(0 0 7px #b78cff);}
19
+ @keyframes orbitConductor {to{transform:rotate(360deg)}}
20
+ @keyframes brainBreath {0%,100% {opacity:.78;transform:scale(.94)}50% {opacity:1;transform:scale(1.08)}}
21
+ @media (prefers-reduced-motion:reduce) { .conductor-icon::before,.conductor-icon::after,.conductor-icon .brain-symbol {animation:none!important} .connections .signal-layer {display:none} }
22
+
23
+
24
+ /* v0.4 – compacte invoerbalk; bestaande dashboardstijl bewust ongewijzigd */
25
+ .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)}
26
+ .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}
27
+ .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}
28
+ .import-file-label span{color:#53d9bf;font-size:16px;margin-right:7px}
29
+ #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}
30
+ #siteSelect:disabled{opacity:.55}.import-state{color:#78e0c3;font-size:11px;max-width:210px;line-height:1.4}
31
+ @media(max-width:760px){.import-panel{align-items:start}.import-intro{flex-basis:100%}.import-state{flex-basis:100%;max-width:none}}
32
+ </style></head>
33
+ <body>
34
+ <div class="app">
35
+ <main class="main">
36
+ <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>
37
+ <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>
38
+ <section class="import-panel panel" aria-label="Excel-proef met fictieve gegevens">
39
+ <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>
40
+ <label class="import-file-label" for="scanFile"><span>▤</span> Kies testbestand (.xlsx)</label><input type="file" id="scanFile" accept=".xlsx" />
41
+ <label class="site-label" for="siteSelect">Locatie</label><select id="siteSelect" disabled><option value="*">Alle locaties</option></select>
42
+ <span id="importState" class="import-state">Demo staat klaar</span>
43
+ </section>
44
+ <section class="orchestra panel">
45
+ <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>
46
+ <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>
47
+ <div class="agents">
48
+ <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>
49
+ <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>
50
+ <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>
51
+ <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">✓ &nbsp; ✓ &nbsp; ! &nbsp; ✓</div><div class="agent-bottom"><div class="track"><i></i></div><span class="percent">0%</span></div></article>
52
+ <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>
53
+ </div>
54
+ <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>
55
+ </section>
56
+ <div class="content-grid">
57
+ <div class="left-content">
58
+ <div class="kpi-grid" id="kpis"></div>
59
+ <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>
60
+ <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>
61
+ <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">◌ &nbsp; Klik op ‘Start demo-analyse’ om de workflow te bekijken.</div></div></section>
62
+ </div>
63
+ <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>
64
+ <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>
65
+ </div>
66
+ <footer>REAKT INLOOP INTELLIGENCE <span>ORCHESTRA PROTOTYPE · ALLEEN FICTIEVE TESTGEGEVENS · GEEN PRODUCTIEOMGEVING</span></footer>
67
+ </main>
68
+ </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>
69
+ <script>
70
+ window.addEventListener('error', function(event){
71
+ var status=document.getElementById('flowState');
72
+ if(status) status.textContent='Scriptfout – meld dit bij de ontwikkelaar';
73
+ console.error('ORCHESTRA scriptfout:', event.message);
74
+ });
75
+ 'use strict';
76
+ let demoData=null, running=false, completed=0;
77
+ const $=id=>document.getElementById(id);
78
+ const ids=['data','analysis','insight','verify','report'];
79
+ const sleep=ms=>new Promise(resolve=>setTimeout(resolve,ms));
80
+ const escapeHTML=s=>String(s).replace(/[&<>"']/g,c=>({'&':'&amp;','<':'&lt;','>':'&gt;','"':'&quot;',"'":'&#39;'}[c]));
81
+ function clock(){ $('clock').textContent=new Intl.DateTimeFormat('nl-NL',{hour:'2-digit',minute:'2-digit',day:'numeric',month:'short'}).format(new Date()); }
82
+ clock();setInterval(clock,60000);
83
+ 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('');}
84
+ 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')} &nbsp; <strong class="${delta?'bad':''}">${delta?'⚠ '+(delta>0?'+':'')+delta:'✓ 0'}</strong></span></div>`}).join('');}
85
+ 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));}
86
+ 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>`)}
87
+ // De lichtpunten volgen de vijf bestaande SVG-verbindingen. Ze lopen alleen tijdens een actieve processtap.
88
+ 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'};
89
+ const signalColors={data:'#34e6b3',analysis:'#ffcb62',insight:'#51b9ff',verify:'#b18aff',report:'#ff9a65'};
90
+ function clearSignal(){const layer=document.querySelector('.signal-layer');if(layer)layer.replaceChildren();}
91
+ function signalFor(agent){
92
+ clearSignal();
93
+ const layer=document.querySelector('.signal-layer');
94
+ if(!layer||!signalPaths[agent]||window.matchMedia('(prefers-reduced-motion: reduce)').matches)return;
95
+ const ns='http://www.w3.org/2000/svg';
96
+ for(const [radius,cls,delay] of [[7,'signal-halo','0s'],[3.2,'signal-particle','0s']]){
97
+ const circle=document.createElementNS(ns,'circle');circle.setAttribute('r',radius);circle.setAttribute('class',cls);circle.setAttribute('fill',signalColors[agent]);
98
+ const motion=document.createElementNS(ns,'animateMotion');motion.setAttribute('path',signalPaths[agent]);motion.setAttribute('dur','1.35s');motion.setAttribute('repeatCount','indefinite');motion.setAttribute('begin',delay);
99
+ circle.appendChild(motion);layer.appendChild(circle);
100
+ }
101
+ }
102
+ 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">◌ &nbsp; 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%'});}
103
+ 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')}}
104
+ 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.'}
105
+ 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;}
106
+ function ask(q){if(!q.trim())return;addMessage('user',q);addMessage('assistant',reply(q));}
107
+ // v0.4: veilige browser-lokale Excel-proef, uitsluitend met fictieve / geanonimiseerde voorbeelden.
108
+ // Er vindt GEEN netwerkverzoek, bestandsoverdracht of externe AI-aanroep plaats.
109
+ let importedData=null;
110
+ const excelLimit=2*1024*1024; // om echte grote (en mogelijk cliëntgevoelige) dumps nog niet te verwerken
111
+ const excelNS='http://schemas.openxmlformats.org/spreadsheetml/2006/main';
112
+ const decoder=new TextDecoder('utf-8');
113
+ function bytesText(a){return decoder.decode(a)}
114
+ function zipDirectory(buffer){
115
+ const bytes=new Uint8Array(buffer), view=new DataView(buffer);
116
+ let eocd=-1;
117
+ for(let i=bytes.length-22;i>=Math.max(0,bytes.length-65557);i--){if(view.getUint32(i,true)===0x06054b50){eocd=i;break}}
118
+ if(eocd<0)throw Error('Geen geldig XLSX-/ZIP-bestand');
119
+ const count=view.getUint16(eocd+10,true), offset=view.getUint32(eocd+16,true);
120
+ if(count>1000)throw Error('Dit testbestand heeft te veel onderdelen');
121
+ const entries=new Map();let pos=offset;
122
+ for(let i=0;i<count;i++){
123
+ if(view.getUint32(pos,true)!==0x02014b50)throw Error('ZIP-structuur ongeldig');
124
+ 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);
125
+ const name=bytesText(bytes.subarray(pos+46,pos+46+len));
126
+ if(name.startsWith('xl/'))entries.set(name,{method,size,local});
127
+ pos+=46+len+extra+comment;
128
+ }
129
+ return {view,bytes,entries};
130
+ }
131
+ async function xmlEntry(zip,name){
132
+ const entry=zip.entries.get(name);if(!entry)return '';
133
+ const {view,bytes}=zip, off=entry.local;
134
+ if(view.getUint32(off,true)!==0x04034b50)throw Error('ZIP-bestandskop ongeldig');
135
+ const start=off+30+view.getUint16(off+26,true)+view.getUint16(off+28,true);
136
+ const compressed=bytes.slice(start,start+entry.size);
137
+ if(entry.method===0)return bytesText(compressed);
138
+ if(entry.method!==8)throw Error('Niet-ondersteunde Excel-compressie');
139
+ if(typeof DecompressionStream!=='function')throw Error('Deze browser ondersteunt lokale Excel-decompressie niet');
140
+ return await new Response(new Blob([compressed]).stream().pipeThrough(new DecompressionStream('deflate-raw'))).text();
141
+ }
142
+ function xmlDoc(str){
143
+ const doc=new DOMParser().parseFromString(str,'application/xml');
144
+ if(doc.getElementsByTagName('parsererror').length)throw Error('Excel-XML niet leesbaar');return doc;
145
+ }
146
+ function cellsToDict(row,shared){
147
+ const values={};
148
+ for(const c of row.getElementsByTagNameNS(excelNS,'c')){
149
+ const key=(c.getAttribute('r')||'').match(/^[A-Z]+/);if(!key)continue;
150
+ const v=c.getElementsByTagNameNS(excelNS,'v')[0],is=c.getElementsByTagNameNS(excelNS,'is')[0];let value=v?.textContent??'';
151
+ if(c.getAttribute('t')==='s')value=shared[Number(value)]??'';
152
+ else if(c.getAttribute('t')==='inlineStr')value=is?.textContent??'';
153
+ values[key[0]]=value;
154
+ }return values;
155
+ }
156
+ function scanDate(v){
157
+ if(v===''||v===undefined||v===null)return '';
158
+ const numeric=Number(v);
159
+ if(v!==''&&Number.isFinite(numeric)&&numeric>30000&&numeric<80000){
160
+ const d=new Date(Date.UTC(1899,11,30)+Math.floor(numeric)*86400000);
161
+ return d.toISOString().slice(0,10);
162
+ }
163
+ const str=String(v).trim(),iso=str.match(/^(\d{4})-(\d{2})-(\d{2})/);
164
+ if(iso)return `${iso[1]}-${iso[2]}-${iso[3]}`;
165
+ const nl=str.match(/^(\d{1,2})[-/](\d{1,2})[-/](\d{4})/);
166
+ if(nl)return `${nl[3]}-${nl[2].padStart(2,'0')}-${nl[1].padStart(2,'0')}`;
167
+ return '';
168
+ }
169
+ async function readExcelLocal(file){
170
+ if(!file.name.toLowerCase().endsWith('.xlsx'))throw Error('Kies een XLSX-testbestand');
171
+ if(file.size>excelLimit)throw Error('Dit is een grote productiedump. Gebruik voorlopig uitsluitend het bijgeleverde kleine fictieve testbestand.');
172
+ const buf=await file.arrayBuffer(),zip=zipDirectory(buf);
173
+ const shared=[];
174
+ const sharedXML=await xmlEntry(zip,'xl/sharedStrings.xml');
175
+ if(sharedXML){
176
+ const doc=xmlDoc(sharedXML);
177
+ for(const si of doc.getElementsByTagNameNS(excelNS,'si'))shared.push(si.textContent??'');
178
+ }
179
+ const sheetXML=await xmlEntry(zip,'xl/worksheets/sheet1.xml');
180
+ if(!sheetXML)throw Error('Werkblad Data dump niet gevonden');
181
+ const doc=xmlDoc(sheetXML),rows=doc.getElementsByTagNameNS(excelNS,'row');
182
+ if(rows.length<2)throw Error('Geen scanregels gevonden');
183
+ if(rows.length>18000)throw Error('Voor deze eerste proef maximaal 18.000 testregels');
184
+ const header=cellsToDict(rows[0],shared),lookup={};
185
+ for(const [col,headerText] of Object.entries(header))lookup[String(headerText).toLowerCase().trim()]=col;
186
+ for(const mandatory of ['scantijd','locatie','clientnummer'])if(!lookup[mandatory])throw Error('Verplichte kolom ontbreekt: '+mandatory);
187
+ const all=[],invalid={datum:0,locatie:0,client:0};
188
+ for(let i=1;i<rows.length;i++){
189
+ const c=cellsToDict(rows[i],shared),site=String(c[lookup.locatie]??'').trim(),id=String(c[lookup.clientnummer]??'').trim(),date=scanDate(c[lookup.scantijd]);
190
+ if(!date)invalid.datum++;
191
+ if(!site)invalid.locatie++;
192
+ if(!id)invalid.client++;
193
+ if(!site||!id||!date)continue;
194
+ all.push({date,site,id});
195
+ }
196
+ if(!all.length)throw Error('Geen bruikbare scanregels');
197
+ const months=[...new Set(all.map(r=>r.date.slice(0,7)))].sort();
198
+ const sites=[...new Set(all.map(r=>r.site))].sort((a,b)=>a.localeCompare(b,'nl'));
199
+ return {all,sites,months,rawCount:rows.length-1,invalid};
200
+ }
201
+ function calculateImported(site='*'){
202
+ const all=importedData.all.filter(r=>site==='*'||r.site===site),keyset=new Set(),ids=new Set(),monthSite=new Map();
203
+ for(const r of all){
204
+ const k=r.site+'|'+r.date+'|'+r.id;
205
+ if(keyset.has(k))continue;
206
+ keyset.add(k);ids.add(r.id);
207
+ const mk=r.site+'|'+r.date.slice(0,7);monthSite.set(mk,(monthSite.get(mk)||0)+1);
208
+ }
209
+ const ordered=[...new Set(all.map(x=>x.date.slice(0,7)))].sort(),chosen=ordered.slice(-3);
210
+ const locations=(site==='*'?importedData.sites:[site]).map(name=>({name,visits:chosen.map(m=>monthSite.get(name+'|'+m)||0)}));
211
+ return {site,rows:all.length,visits:keyset.size,unique:ids.size,duplicates:all.length-keyset.size,months:chosen,locations,invalid:importedData.invalid};
212
+ }
213
+ function drawImportedChart(result){
214
+ const cvs=$('chart'),ctx=cvs.getContext('2d'),w=cvs.width,h=cvs.height;
215
+ ctx.clearRect(0,0,w,h);ctx.font='13px Segoe UI';
216
+ const series=result.locations.slice(0,4),max=Math.max(10,...series.flatMap(x=>x.visits))*1.15;
217
+ ctx.strokeStyle='#29405a';ctx.fillStyle='#8aa4c3';ctx.textAlign='left';
218
+ 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)}
219
+ const colors=['#3ee1b6','#ffc66a','#5b9cff','#ad88ff'];
220
+ series.forEach((loc,li)=>{
221
+ ctx.strokeStyle=colors[li];ctx.lineWidth=3;ctx.beginPath();
222
+ 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();
223
+ 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()});
224
+ });ctx.fillStyle='#a6b9d0';ctx.textAlign='center';
225
+ result.months.forEach((m,i)=>ctx.fillText(m,90+i*235,239));
226
+ const legend=document.querySelector('.legend');
227
+ legend.innerHTML=series.map((loc,i)=>`<span><i style="background:${colors[i]}"></i> ${escapeHTML(loc.name)}</span>`).join('');
228
+ document.querySelector('.chart-panel .subtle').textContent=result.months.join(' · ')+(result.locations.length>4?' · 4 van '+result.locations.length+' locaties':'');
229
+ }
230
+ function renderImported(result){
231
+ const num=n=>n.toLocaleString('nl-NL');
232
+ 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'}];
233
+ renderKPIs({kpis});drawImportedChart(result);
234
+ $('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>';
235
+ document.querySelector('.verification .warning').textContent='⚠ Proefberekening. Definities en externe controle nog niet gevalideerd.';
236
+ document.querySelector('.paper-watermark').textContent='FICTIEVE TESTGEGEVENS · NIET GEVALIDEERD';
237
+ document.querySelector('.paper p').textContent='Lokale Excel-proef · niet voor publicatie';
238
+ const pk=document.querySelectorAll('.paper-kpis strong');
239
+ [num(result.visits),num(result.unique),'—'].forEach((x,i)=>{if(pk[i])pk[i].textContent=x});
240
+ const pl=document.querySelectorAll('.paper-kpis small');if(pl[2])pl[2].textContent='Instroom niet bepaald';
241
+ }
242
+ async function startImported(){
243
+ if(running)return;
244
+ running=true;$('startBtn').disabled=true;$('resetBtn').disabled=true;completed=0;clearSignal();
245
+ $('logs').innerHTML='';$('completed').textContent='0';$('conductor').classList.remove('attention');
246
+ $('conductor').classList.add('running');document.querySelector('.orchestra').classList.add('running');
247
+ $('conductorStatus').textContent='Lokale Excel-proef actief';$('logStatus').textContent='Actief';
248
+ 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%'}
249
+ const result=calculateImported($('siteSelect').value);importedData.last=result;
250
+ const steps=[
251
+ {agent:'data',text:`${result.rows.toLocaleString('nl-NL')} scanregels gecontroleerd; ${result.duplicates} herhaalde scans aangetroffen`},
252
+ {agent:'analysis',text:`${result.visits} bezoeken (voorlopig) en ${result.unique} unieke cliëntnummers berekend`},
253
+ {agent:'insight',text:'Maandtrends berekend; AI-duiding nog niet verbonden'},
254
+ {agent:'verify',text:'Interne telling gecontroleerd; geen externe schaduwberekening beschikbaar'},
255
+ {agent:'report',text:'Conceptpreview bijgewerkt · NIET GEVALIDEERD'}];
256
+ try{
257
+ renderImported(result);
258
+ 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);
259
+ for(let p=0;p<=100;p+=5){el.querySelector('.track i').style.width=p+'%';el.querySelector('.percent').textContent=p+'%';await sleep(30)}
260
+ 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)
261
+ }
262
+ $('conductor').classList.add('attention');$('flowState').textContent='Concept · NIET GEVALIDEERD';$('conductorStatus').textContent='Proefberekening voltooid';$('logStatus').textContent='Afgerond';
263
+ }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')}
264
+ }
265
+ $('scanFile').addEventListener('change',async e=>{
266
+ importedData=null;const file=e.target.files?.[0];if(!file)return;
267
+ const state=$('importState');state.textContent='Bestand wordt lokaal gelezen...';$('startBtn').disabled=true;
268
+ try{
269
+ importedData=await readExcelLocal(file);
270
+ const sel=$('siteSelect');sel.innerHTML='<option value="*">Alle locaties</option>';
271
+ importedData.sites.forEach(loc=>{const opt=document.createElement('option');opt.value=loc;opt.textContent=loc;sel.appendChild(opt)});
272
+ sel.disabled=false;
273
+ state.textContent=`${importedData.rawCount} testregels · ${importedData.sites.length} locaties`;
274
+ $('startBtn').textContent='▶ Analyseer testbestand';
275
+ $('flowState').textContent='Testbestand lokaal ingelezen · start de analyse';
276
+ log('Fictief Excel-testbestand is lokaal ingelezen; nog geen berekeningen gepubliceerd');
277
+ }catch(err){$('startBtn').textContent='▶ Start demo-analyse';state.textContent='⚠ '+err.message;$('siteSelect').disabled=true;log('Excel-proef: '+err.message)}finally{$('startBtn').disabled=false}
278
+ });
279
+ // Bestaande chat blijft demo-gericht. Voor echte Excel-proef antwoorden we uitsluitend met controleerbare berekende cijfers.
280
+ const originalReply=reply;
281
+ reply=function(q){
282
+ if(!importedData)return originalReply(q);
283
+ const r=importedData.last;
284
+ if(!r)return 'Klik op Start demo-analyse om de lokaal ingelezen testgegevens te berekenen.';
285
+ 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.';
286
+ 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.`;
287
+ return 'De AI-API is nog niet gekoppeld. Ik kan voorlopig alleen de gecontroleerde basisgetallen van het fictieve Excel-testbestand toelichten.';
288
+ };
289
+
290
+ $('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());
291
+ 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'));
292
+
293
+ </script></body></html>