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

Upload index.html

Browse files
Files changed (1) hide show
  1. index.html +200 -2
index.html CHANGED
@@ -20,12 +20,27 @@
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
  </style></head>
24
  <body>
25
  <div class="app">
26
  <main class="main">
27
  <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>
28
  <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>
 
 
 
 
 
 
29
  <section class="orchestra panel">
30
  <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>
31
  <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>
@@ -48,7 +63,7 @@
48
  <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>
49
  <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>
50
  </div>
51
- <footer>REAKT INLOOP INTELLIGENCE <span>ORCHESTRA PROTOTYPE · FICTIEVE DATA · GEEN PRODUCTIEOMGEVING</span></footer>
52
  </main>
53
  </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>
54
  <script>
@@ -85,10 +100,193 @@ function signalFor(agent){
85
  }
86
  }
87
  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%'});}
88
- async function start(){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')}}
89
  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.'}
90
  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;}
91
  function ask(q){if(!q.trim())return;addMessage('user',q);addMessage('assistant',reply(q));}
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
92
  $('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());
93
  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'));
94
 
 
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>
 
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>
 
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