Spaces:
Runtime error
Runtime error
| """ | |
| page3_simulation.py โ Live Simulation with Full Canvas Animation | |
| All bills complete in exactly 30 seconds. WCO themed, 5-lane layout. | |
| """ | |
| import streamlit as st | |
| import streamlit.components.v1 as components | |
| import plotly.graph_objects as go | |
| import pandas as pd | |
| from styles import (inject_global_css, page_header, metric_row, | |
| WCO_GOLD, WCO_BLUE, WCO_GREEN, WCO_RED, | |
| WCO_CARD_BG, WCO_BORDER, WCO_MUTED) | |
| from simulation_engine import (generate_declarations, compute_risk_scores, | |
| assign_channels, simulate_inspection_outcomes, | |
| compute_updated_weights, get_default_weights, | |
| compute_efficiency_metrics, RISK_AREAS) | |
| # โโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโ | |
| def build_animation_html(n_bills, bandwidth, exp_ratio, | |
| channel_counts, detected, revenue, efficiency_idx): | |
| red_n = channel_counts.get("RED", 0) | |
| yellow_n = channel_counts.get("YELLOW", 0) | |
| green_n = channel_counts.get("GREEN", 0) | |
| explore_n = int(n_bills * bandwidth * exp_ratio) | |
| return f"""<!DOCTYPE html> | |
| <html lang="en"> | |
| <head> | |
| <meta charset="UTF-8"/> | |
| <style> | |
| * {{ box-sizing:border-box; margin:0; padding:0; }} | |
| body {{ | |
| background:#070E1C; | |
| font-family:'IBM Plex Sans','Segoe UI',sans-serif; | |
| color:#D0DCF0; | |
| overflow:hidden; | |
| height:700px; | |
| display:flex; | |
| flex-direction:column; | |
| }} | |
| /* โโ Top KPI strip โโ */ | |
| #kpi-bar {{ | |
| display:flex; | |
| background:#0B1527; | |
| border-bottom:3px solid #C8A951; | |
| flex-shrink:0; | |
| }} | |
| .kpi {{ | |
| flex:1; padding:9px 6px; text-align:center; | |
| border-right:1px solid #1E3A6E; | |
| }} | |
| .kpi:last-child {{ border-right:none; }} | |
| .kpi-val {{ | |
| font-size:20px; font-weight:700; | |
| font-family:'Georgia',serif; line-height:1; | |
| }} | |
| .kpi-lbl {{ | |
| font-size:9px; text-transform:uppercase; | |
| letter-spacing:0.07em; color:#6B85AA; margin-top:3px; | |
| }} | |
| /* โโ Canvas โโ */ | |
| #canvas-wrap {{ | |
| flex:1; position:relative; overflow:hidden; | |
| min-height:0; | |
| }} | |
| canvas {{ display:block; width:100%; height:100%; }} | |
| /* โโ Counter row โโ */ | |
| #counter-bar {{ | |
| display:flex; background:#0B1527; | |
| border-top:1px solid #1E3A6E; flex-shrink:0; | |
| }} | |
| .ch-cnt {{ | |
| flex:1; padding:7px 6px; text-align:center; | |
| border-right:1px solid #1E3A6E; transition:background .25s; | |
| }} | |
| .ch-cnt:last-child {{ border-right:none; }} | |
| .ch-v {{ font-size:24px; font-weight:700; font-family:'Georgia',serif; line-height:1; }} | |
| .ch-l {{ font-size:9px; color:#6B85AA; margin-top:2px; text-transform:uppercase; }} | |
| /* โโ Progress โโ */ | |
| #prog-wrap {{ | |
| background:#0B1527; border-top:1px solid #1E3A6E; | |
| padding:7px 14px; flex-shrink:0; | |
| }} | |
| #prog-head {{ | |
| font-size:10px; color:#C8A951; | |
| display:flex; justify-content:space-between; margin-bottom:4px; | |
| }} | |
| #prog-bg {{ | |
| background:#111D30; border-radius:4px; height:7px; overflow:hidden; | |
| }} | |
| #prog-fill {{ | |
| height:100%; border-radius:4px; | |
| background:linear-gradient(90deg,#003087,#C8A951); | |
| width:0%; transition:width .15s ease; | |
| }} | |
| </style> | |
| </head> | |
| <body> | |
| <!-- KPI Header --> | |
| <div id="kpi-bar"> | |
| <div class="kpi"> | |
| <div class="kpi-val" style="color:#D0DCF0;">{n_bills}</div> | |
| <div class="kpi-lbl">Total Declarations</div> | |
| </div> | |
| <div class="kpi"> | |
| <div class="kpi-val" style="color:#C8102E;">{red_n}</div> | |
| <div class="kpi-lbl">๐ด RED Target</div> | |
| </div> | |
| <div class="kpi"> | |
| <div class="kpi-val" style="color:#F5A800;">{yellow_n}</div> | |
| <div class="kpi-lbl">๐ก YELLOW Target</div> | |
| </div> | |
| <div class="kpi"> | |
| <div class="kpi-val" style="color:#00843D;">{green_n}</div> | |
| <div class="kpi-lbl">๐ข GREEN Target</div> | |
| </div> | |
| <div class="kpi"> | |
| <div class="kpi-val" style="color:#C8A951;">{efficiency_idx:.3f}</div> | |
| <div class="kpi-lbl">Efficiency Index</div> | |
| </div> | |
| <div class="kpi"> | |
| <div class="kpi-val" style="color:#44CC88;">${revenue:,.0f}</div> | |
| <div class="kpi-lbl">Revenue Recovered</div> | |
| </div> | |
| <div class="kpi"> | |
| <div class="kpi-val" style="color:#CC77FF;">{detected}</div> | |
| <div class="kpi-lbl">Frauds Detected</div> | |
| </div> | |
| </div> | |
| <!-- Canvas --> | |
| <div id="canvas-wrap"> | |
| <canvas id="sim"></canvas> | |
| </div> | |
| <!-- Live counters --> | |
| <div id="counter-bar"> | |
| <div class="ch-cnt" id="cnt-total"> | |
| <div class="ch-v" id="v-total" style="color:#D0DCF0;">0</div> | |
| <div class="ch-l">๐ฆ Processed</div> | |
| </div> | |
| <div class="ch-cnt" id="cnt-red"> | |
| <div class="ch-v" id="v-red" style="color:#C8102E;">0</div> | |
| <div class="ch-l">๐ด Physical Exam</div> | |
| </div> | |
| <div class="ch-cnt" id="cnt-yel"> | |
| <div class="ch-v" id="v-yel" style="color:#F5A800;">0</div> | |
| <div class="ch-l">๐ก Doc Check</div> | |
| </div> | |
| <div class="ch-cnt" id="cnt-grn"> | |
| <div class="ch-v" id="v-grn" style="color:#00843D;">0</div> | |
| <div class="ch-l">๐ข Facilitated</div> | |
| </div> | |
| <div class="ch-cnt" id="cnt-exp"> | |
| <div class="ch-v" id="v-exp" style="color:#CC77FF;">0</div> | |
| <div class="ch-l">๐ gATE Explore</div> | |
| </div> | |
| <div class="ch-cnt" id="cnt-fraud"> | |
| <div class="ch-v" id="v-fraud" style="color:#FF4466;">0</div> | |
| <div class="ch-l">๐จ Fraud Detected</div> | |
| </div> | |
| </div> | |
| <!-- Progress --> | |
| <div id="prog-wrap"> | |
| <div id="prog-head"> | |
| <span id="stage-label">Initialising...</span> | |
| <span id="prog-pct">0%</span> | |
| </div> | |
| <div id="prog-bg"><div id="prog-fill"></div></div> | |
| </div> | |
| <script> | |
| // โโโโโโโโโโโโโโโโโโโ CONFIG โโโโโโโโโโโโโโโโโโโ | |
| const TOTAL = {n_bills}; | |
| const DUR_MS = 30000; | |
| const RED_T = {red_n}; | |
| const YEL_T = {yellow_n}; | |
| const GRN_T = {green_n}; | |
| const FRAUD_T = {detected}; | |
| const EXP_T = {explore_n}; | |
| // โโโโโโโโโโโโโโโโโโโ CANVAS โโโโโโโโโโโโโโโโโโโ | |
| const canvas = document.getElementById('sim'); | |
| const ctx = canvas.getContext('2d'); | |
| let W, H; | |
| function resize() {{ | |
| const wrap = document.getElementById('canvas-wrap'); | |
| W = canvas.width = wrap.clientWidth || 900; | |
| H = canvas.height = wrap.clientHeight || 380; | |
| }} | |
| window.addEventListener('resize', resize); | |
| resize(); | |
| // โโโโโโโโโโโโโโโโโโโ LAYOUT โโโโโโโโโโโโโโโโโโโ | |
| // 5 lanes: 0=INTAKE 1=RED 2=YELLOW 3=GREEN 4=OFFENCEDB | |
| const NUM_LANES = 5; | |
| function laneW() {{ return W / NUM_LANES; }} | |
| function laneCX(i){{ return i * laneW() + laneW() / 2; }} | |
| function gateY() {{ return H * 0.52; }} | |
| // โโโโโโโโโโโโโโโโโโโ PRE-COMPUTE SEQUENCES โโโ | |
| // Build shuffled channel array | |
| const channelSeq = []; | |
| (function() {{ | |
| const a = []; | |
| for(let i=0;i<RED_T;i++) a.push('RED'); | |
| for(let i=0;i<YEL_T;i++) a.push('YELLOW'); | |
| for(let i=0;i<GRN_T;i++) a.push('GREEN'); | |
| for(let i=a.length-1;i>0;i--) {{ | |
| const j=Math.floor(Math.random()*(i+1)); | |
| [a[i],a[j]]=[a[j],a[i]]; | |
| }} | |
| channelSeq.push(...a); | |
| }})(); | |
| // Mark exploration indices | |
| const exploreSet = new Set(); | |
| (function() {{ | |
| const pool=[]; | |
| channelSeq.forEach((c,i)=>{{ if(c==='RED'||c==='YELLOW') pool.push(i); }}); | |
| const need = Math.min(EXP_T, pool.length); | |
| while(exploreSet.size < need) {{ | |
| exploreSet.add(pool[Math.floor(Math.random()*pool.length)]); | |
| }} | |
| }})(); | |
| // Mark fraud indices (subset of inspected) | |
| const fraudSet = new Set(); | |
| (function() {{ | |
| const pool=[]; | |
| channelSeq.forEach((c,i)=>{{ if(c==='RED'||c==='YELLOW') pool.push(i); }}); | |
| const need = Math.min(FRAUD_T, pool.length); | |
| while(fraudSet.size < need) {{ | |
| fraudSet.add(pool[Math.floor(Math.random()*pool.length)]); | |
| }} | |
| }})(); | |
| // โโโโโโโโโโโโโโโโโโโ COUNTERS โโโโโโโโโโโโโโโโโโโ | |
| const C = {{ total:0, RED:0, YELLOW:0, GREEN:0, explore:0, fraud:0 }}; | |
| function updateDOM() {{ | |
| document.getElementById('v-total').textContent = C.total; | |
| document.getElementById('v-red').textContent = C.RED; | |
| document.getElementById('v-yel').textContent = C.YELLOW; | |
| document.getElementById('v-grn').textContent = C.GREEN; | |
| document.getElementById('v-exp').textContent = C.explore; | |
| document.getElementById('v-fraud').textContent = C.fraud; | |
| // flash | |
| const flash = (id, col) => {{ | |
| const el = document.getElementById(id); | |
| el.style.background = col+'25'; | |
| setTimeout(()=>{{ el.style.background=''; }}, 220); | |
| }}; | |
| if(C.RED > 0 && C.RED % 2 === 0) flash('cnt-red', '#C8102E'); | |
| if(C.YELLOW > 0 && C.YELLOW % 2 === 0) flash('cnt-yel', '#F5A800'); | |
| if(C.GREEN > 0 && C.GREEN % 5 === 0) flash('cnt-grn', '#00843D'); | |
| if(C.fraud > 0) flash('cnt-fraud','#FF4466'); | |
| }} | |
| // โโโโโโโโโโโโโโโโโโโ BACKGROUND โโโโโโโโโโโโโโโโโโโ | |
| const LANE_STYLES = [ | |
| {{ top:'#0A1E4A', bot:'#060D28', hdr:'INTAKE', sub:'All Declarations', col:'#8BAAD4' }}, | |
| {{ top:'#1E0008', bot:'#0D0004', hdr:'๐ด RED', sub:'Physical Examination',col:'#FF4466' }}, | |
| {{ top:'#1A1000', bot:'#0D0800', hdr:'๐ก YELLOW', sub:'Document Check', col:'#FFD700' }}, | |
| {{ top:'#001A09', bot:'#000D04', hdr:'๐ข GREEN', sub:'Facilitated', col:'#44CC88' }}, | |
| {{ top:'#110A1E', bot:'#080512', hdr:'๐๏ธ OFFENCE', sub:'Knowledge Vault', col:'#CC77FF' }}, | |
| ]; | |
| function drawBG() {{ | |
| const lw = laneW(); | |
| LANE_STYLES.forEach((s,i) => {{ | |
| const g = ctx.createLinearGradient(i*lw,0,i*lw,H); | |
| g.addColorStop(0, s.top); | |
| g.addColorStop(1, s.bot); | |
| ctx.fillStyle = g; | |
| ctx.fillRect(i*lw, 0, lw, H); | |
| }}); | |
| // Dividers | |
| ctx.strokeStyle='#1E3A6E'; ctx.lineWidth=1.2; | |
| for(let i=1;i<NUM_LANES;i++) {{ | |
| ctx.beginPath(); ctx.moveTo(i*lw,0); ctx.lineTo(i*lw,H); ctx.stroke(); | |
| }} | |
| // Lane headers | |
| LANE_STYLES.forEach((s,i) => {{ | |
| const cx = laneCX(i); | |
| ctx.fillStyle='rgba(0,0,0,0.55)'; | |
| ctx.beginPath(); ctx.roundRect(i*lw+6,8,lw-12,46,7); ctx.fill(); | |
| ctx.strokeStyle=s.col+'44'; ctx.lineWidth=1; | |
| ctx.beginPath(); ctx.roundRect(i*lw+6,8,lw-12,46,7); ctx.stroke(); | |
| ctx.fillStyle=s.col; | |
| ctx.font='bold 13px Georgia,serif'; | |
| ctx.textAlign='center'; | |
| ctx.fillText(s.hdr, cx, 30); | |
| ctx.fillStyle='#6B85AA'; | |
| ctx.font='10px sans-serif'; | |
| ctx.fillText(s.sub, cx, 46); | |
| }}); | |
| // Gate band | |
| const gy = gateY(); | |
| const gg = ctx.createLinearGradient(0,gy-28,0,gy+28); | |
| gg.addColorStop(0,'rgba(0,48,135,0)'); | |
| gg.addColorStop(0.5,'rgba(0,48,135,0.9)'); | |
| gg.addColorStop(1,'rgba(0,48,135,0)'); | |
| ctx.fillStyle=gg; | |
| ctx.fillRect(0,gy-28,W,56); | |
| ctx.strokeStyle='#C8A951'; ctx.lineWidth=1.4; | |
| ctx.setLineDash([8,5]); | |
| ctx.beginPath(); ctx.moveTo(0,gy-24); ctx.lineTo(W,gy-24); ctx.stroke(); | |
| ctx.beginPath(); ctx.moveTo(0,gy+24); ctx.lineTo(W,gy+24); ctx.stroke(); | |
| ctx.setLineDash([]); | |
| // Gate booths | |
| const boothData = [ | |
| {{label:'DATE SCORING', col:'#C8A951'}}, | |
| {{label:'RED CHANNEL', col:'#C8102E'}}, | |
| {{label:'YELLOW CHANNEL',col:'#F5A800'}}, | |
| {{label:'GREEN CHANNEL', col:'#00843D'}}, | |
| ]; | |
| boothData.forEach((b,i) => {{ | |
| const bx = laneCX(i); | |
| ctx.fillStyle='#0F1C35'; | |
| ctx.strokeStyle=b.col; ctx.lineWidth=1.8; | |
| ctx.beginPath(); ctx.roundRect(bx-40,gy-16,80,32,5); ctx.fill(); ctx.stroke(); | |
| ctx.fillStyle=b.col; | |
| ctx.font='bold 9px IBM Plex Sans,sans-serif'; | |
| ctx.textAlign='center'; | |
| ctx.fillText(b.label, bx, gy+4); | |
| // Dot indicator | |
| ctx.shadowColor=b.col; ctx.shadowBlur=8; | |
| ctx.beginPath(); ctx.arc(bx,gy-20,4,0,Math.PI*2); | |
| ctx.fillStyle=b.col; ctx.fill(); | |
| ctx.shadowBlur=0; | |
| }}); | |
| // Vault (offence DB) | |
| const vx = laneCX(4); | |
| ctx.fillStyle='#1A0A2E'; | |
| ctx.strokeStyle='#CC77FF'; ctx.lineWidth=2; | |
| ctx.beginPath(); ctx.roundRect(vx-46,gy-20,92,40,8); ctx.fill(); ctx.stroke(); | |
| ctx.fillStyle='#CC77FF'; ctx.font='bold 10px Georgia'; | |
| ctx.textAlign='center'; | |
| ctx.fillText('OFFENCE DB', vx, gy-4); | |
| ctx.fillStyle='#8B55CC'; ctx.font='9px sans-serif'; | |
| ctx.fillText('Evidence Vault', vx, gy+12); | |
| // Bezier guide lines (intake โ channels) | |
| [[1,'#C8102E'],[2,'#F5A800'],[3,'#00843D']].forEach(([li,col]) => {{ | |
| const ix = laneCX(0), tx = laneCX(li), topY = gy-100; | |
| ctx.strokeStyle=col+'28'; ctx.lineWidth=1; ctx.setLineDash([4,9]); | |
| ctx.beginPath(); | |
| ctx.moveTo(ix, topY); | |
| ctx.bezierCurveTo(ix,gy-50,tx,gy-50,tx,gy-25); | |
| ctx.stroke(); | |
| ctx.setLineDash([]); | |
| }}); | |
| }} | |
| // โโโโโโโโโโโโโโโโโโโ BILL PARTICLE โโโโโโโโโโโโโโโโโโโ | |
| const bills = []; | |
| let billIdx = 0; | |
| const laneMap = {{ RED:1, YELLOW:2, GREEN:3 }}; | |
| class Bill {{ | |
| constructor(idx) {{ | |
| this.idx = idx; | |
| this.channel = channelSeq[idx] || 'GREEN'; | |
| this.explore = exploreSet.has(idx); | |
| this.fraud = fraudSet.has(idx); | |
| const lw = laneW(); | |
| this.x = laneCX(0) + (Math.random()*24-12); | |
| this.y = -20 - Math.random()*60; | |
| this.vy = 0; // will be set per-frame to hit gate at exact time | |
| this.targetX = laneCX(laneMap[this.channel]) + (Math.random()*16-8); | |
| this.phase = 'intake'; | |
| // colour | |
| const chC = {{ RED:'#C8102E', YELLOW:'#F5A800', GREEN:'#00843D' }}; | |
| this.finalColor = chC[this.channel]; | |
| this.color = '#3A6BB5'; | |
| this.alpha = 1; | |
| // size | |
| this.w = 9; this.h = 12; | |
| // counted flags | |
| this.counted = false; | |
| this.fraudSent = false; | |
| }} | |
| update(elapsed) {{ | |
| const gy = gateY(); | |
| if(this.phase === 'intake') {{ | |
| // Speed to cross gate at correct time proportional to index | |
| const targetArrivalMs = (this.idx / TOTAL) * DUR_MS * 0.75; | |
| const remainMs = Math.max(10, targetArrivalMs - elapsed); | |
| const distToGate = gy - 28 - this.y; | |
| this.vy = (distToGate / remainMs) * 16; // px/frame approx | |
| this.y += Math.max(0.8, this.vy); | |
| if(this.y >= gy - 28) {{ | |
| this.phase = 'routing'; | |
| this.color = this.finalColor; | |
| }} | |
| }} else if(this.phase === 'routing') {{ | |
| const dx = this.targetX - this.x; | |
| this.x += dx * 0.14; | |
| this.y += 2.2; | |
| if(Math.abs(dx) < 2.5 && this.y > gy + 32) {{ | |
| this.phase = 'exiting'; | |
| if(!this.counted) {{ | |
| this.counted = true; | |
| C.total++; | |
| C[this.channel]++; | |
| if(this.explore) C.explore++; | |
| if(this.fraud) C.fraud++; | |
| updateDOM(); | |
| }} | |
| }} | |
| }} else if(this.phase === 'exiting') {{ | |
| this.y += 2.8; | |
| if(this.fraud && !this.fraudSent && this.y > gateY()+90) {{ | |
| this.fraudSent = true; | |
| offParts.push(new OffPart(this.x, this.y, this.finalColor)); | |
| }} | |
| if(this.y > H+20) this.phase='done'; | |
| }} | |
| }} | |
| draw() {{ | |
| if(this.phase==='done') return; | |
| ctx.save(); | |
| ctx.globalAlpha = this.alpha; | |
| ctx.translate(this.x, this.y); | |
| // Document body | |
| ctx.fillStyle = this.color; | |
| ctx.strokeStyle = this.color+'99'; | |
| ctx.lineWidth = 0.7; | |
| ctx.beginPath(); ctx.roundRect(-this.w/2,-this.h/2,this.w,this.h,2); | |
| ctx.fill(); ctx.stroke(); | |
| // Fold corner | |
| ctx.fillStyle='rgba(255,255,255,0.18)'; | |
| ctx.beginPath(); | |
| ctx.moveTo(this.w/2-3,-this.h/2); | |
| ctx.lineTo(this.w/2,-this.h/2+3); | |
| ctx.lineTo(this.w/2-3,-this.h/2+3); | |
| ctx.closePath(); ctx.fill(); | |
| // Lines on doc | |
| ctx.strokeStyle='rgba(255,255,255,0.2)'; ctx.lineWidth=0.8; | |
| [-3,0,3].forEach(dy => {{ | |
| ctx.beginPath(); | |
| ctx.moveTo(-this.w/2+2, dy); | |
| ctx.lineTo(this.w/2-3, dy); | |
| ctx.stroke(); | |
| }}); | |
| // Fraud glow | |
| if(this.fraud) {{ | |
| ctx.shadowColor='#FF4466'; ctx.shadowBlur=10; | |
| ctx.strokeStyle='#FF4466'; ctx.lineWidth=1.5; | |
| ctx.beginPath(); ctx.roundRect(-this.w/2,-this.h/2,this.w,this.h,2); ctx.stroke(); | |
| ctx.shadowBlur=0; | |
| }} | |
| // Explore glow | |
| if(this.explore && !this.fraud) {{ | |
| ctx.shadowColor='#CC77FF'; ctx.shadowBlur=7; | |
| ctx.strokeStyle='#CC77FF'; ctx.lineWidth=1.2; | |
| ctx.beginPath(); ctx.roundRect(-this.w/2,-this.h/2,this.w,this.h,2); ctx.stroke(); | |
| ctx.shadowBlur=0; | |
| }} | |
| ctx.restore(); | |
| }} | |
| }} | |
| // โโโโโโโโโโโโโโโโโโโ OFFENCE PARTICLE โโโโโโโโโโโโโโโโโโโ | |
| const offParts = []; | |
| class OffPart {{ | |
| constructor(x,y,col) {{ | |
| this.x=x; this.y=y; this.col=col; | |
| this.tx=laneCX(4); this.ty=gateY()+5; | |
| this.t=0; this.done=false; | |
| }} | |
| update() {{ | |
| this.t+=0.035; | |
| if(this.t>=1){{ this.done=true; return; }} | |
| this.x += (this.tx-this.x)*0.07; | |
| this.y += (this.ty-this.y)*0.07; | |
| }} | |
| draw() {{ | |
| if(this.done) return; | |
| ctx.save(); | |
| ctx.globalAlpha=Math.max(0,1-this.t); | |
| ctx.fillStyle=this.col; | |
| ctx.shadowColor='#CC77FF'; ctx.shadowBlur=12; | |
| ctx.beginPath(); ctx.arc(this.x,this.y,4,0,Math.PI*2); ctx.fill(); | |
| ctx.shadowBlur=0; | |
| ctx.restore(); | |
| }} | |
| }} | |
| // โโโโโโโโโโโโโโโโโโโ STAGE LABELS โโโโโโโโโโโโโโโโโโโ | |
| const STAGES = [ | |
| {{ pct:0.00, end:0.25, label:'PHASE 1 โ INTAKE & DATE RISK SCORING', col:'#C8A951' }}, | |
| {{ pct:0.25, end:0.60, label:'PHASE 2 โ HYBRID CHANNEL ROUTING (DATE + gATE)', col:'#0066CC' }}, | |
| {{ pct:0.60, end:0.85, label:'PHASE 3 โ INSPECTION & OFFENCE DB FEEDBACK', col:'#C8102E' }}, | |
| {{ pct:0.85, end:1.01, label:'PHASE 4 โ SELF-LEARNING CYCLE COMPLETE โ', col:'#00843D' }}, | |
| ]; | |
| function drawStageLabel(p) {{ | |
| const s = STAGES.find(x => p>=x.pct && p<x.end) || STAGES[3]; | |
| document.getElementById('stage-label').textContent = s.label; | |
| document.getElementById('stage-label').style.color = s.col; | |
| }} | |
| // โโโโโโโโโโโโโโโโโโโ COMPLETION OVERLAY โโโโโโโโโโโโโโโโโโโ | |
| function drawDone() {{ | |
| ctx.save(); | |
| ctx.fillStyle='rgba(7,14,28,0.86)'; | |
| ctx.fillRect(0,0,W,H); | |
| const cw=460,ch=200,cx=(W-cw)/2,cy=(H-ch)/2; | |
| ctx.fillStyle='#0F1C35'; | |
| ctx.strokeStyle='#C8A951'; ctx.lineWidth=2; | |
| ctx.beginPath(); ctx.roundRect(cx,cy,cw,ch,14); ctx.fill(); ctx.stroke(); | |
| // Gold header | |
| ctx.fillStyle='#C8A951'; | |
| ctx.beginPath(); ctx.roundRect(cx,cy,cw,40,{{topLeft:14,topRight:14,bottomLeft:0,bottomRight:0}}); | |
| ctx.fill(); | |
| ctx.fillStyle='#003087'; | |
| ctx.font='bold 13px Georgia'; ctx.textAlign='center'; | |
| ctx.fillText('โ SIMULATION COMPLETE โ WCO RMS CYCLE', W/2, cy+25); | |
| ctx.fillStyle='#44CC88'; ctx.font='bold 22px Georgia'; | |
| ctx.fillText('All '+TOTAL+' Declarations Processed in 30 Seconds', W/2, cy+72); | |
| const cols = [['๐ด RED',C.RED,'#C8102E'],['๐ก YEL',C.YELLOW,'#F5A800'], | |
| ['๐ข GRN',C.GREEN,'#00843D'],['๐จ Fraud',C.fraud,'#FF4466'], | |
| ['๐ Expl',C.explore,'#CC77FF']]; | |
| const bw=74, startX=cx+20; | |
| cols.forEach((col,i) => {{ | |
| const bx=startX+i*(bw+8); | |
| ctx.fillStyle=col[2]+'22'; | |
| ctx.beginPath(); ctx.roundRect(bx,cy+90,bw,52,6); ctx.fill(); | |
| ctx.strokeStyle=col[2]; ctx.lineWidth=1; | |
| ctx.beginPath(); ctx.roundRect(bx,cy+90,bw,52,6); ctx.stroke(); | |
| ctx.fillStyle=col[2]; ctx.font='bold 22px Georgia'; | |
| ctx.fillText(col[1], bx+bw/2, cy+120); | |
| ctx.fillStyle='#8BAAD4'; ctx.font='9px sans-serif'; | |
| ctx.fillText(col[0], bx+bw/2, cy+136); | |
| }}); | |
| ctx.fillStyle='#C8A951'; ctx.font='11px monospace'; | |
| ctx.fillText('Navigate to Page 4 โ for detailed analysis tables & charts', W/2, cy+168); | |
| ctx.restore(); | |
| }} | |
| // โโโโโโโโโโโโโโโโโโโ SPAWN RATE โโโโโโโโโโโโโโโโโโโ | |
| // Bills spawn steadily across 80% of duration so all finish by end | |
| const SPAWN_END_MS = DUR_MS * 0.80; | |
| const SPAWN_INTV = SPAWN_END_MS / TOTAL; // ms per bill | |
| let lastSpawnT = 0; | |
| let startTime = null; | |
| let simComplete = false; | |
| // โโโโโโโโโโโโโโโโโโโ MAIN LOOP โโโโโโโโโโโโโโโโโโโ | |
| function loop(ts) {{ | |
| if(!startTime) startTime=ts; | |
| const elapsed = ts - startTime; | |
| const prog = Math.min(elapsed / DUR_MS, 1); | |
| ctx.clearRect(0,0,W,H); | |
| drawBG(); | |
| // Spawn | |
| if(billIdx < TOTAL && elapsed >= lastSpawnT + SPAWN_INTV) {{ | |
| const batchN = Math.min( | |
| TOTAL - billIdx, | |
| Math.floor((elapsed - lastSpawnT) / SPAWN_INTV) | |
| ); | |
| for(let b=0;b<batchN;b++) bills.push(new Bill(billIdx++)); | |
| lastSpawnT += batchN * SPAWN_INTV; | |
| }} | |
| // Update & draw bills | |
| bills.forEach(bl => {{ bl.update(elapsed); bl.draw(); }}); | |
| // Offence particles | |
| for(let i=offParts.length-1;i>=0;i--) {{ | |
| offParts[i].update(); offParts[i].draw(); | |
| if(offParts[i].done) offParts.splice(i,1); | |
| }} | |
| // Vault counter | |
| const vx = laneCX(4), vy = gateY()+58; | |
| ctx.fillStyle='#CC77FF'; ctx.font='bold 22px Georgia'; ctx.textAlign='center'; | |
| ctx.fillText(C.fraud, vx, vy); | |
| ctx.fillStyle='#8B55CC'; ctx.font='9px sans-serif'; | |
| ctx.fillText('offence entries', vx, vy+14); | |
| // Live efficiency | |
| const liveP = C.total > 0 | |
| ? (C.fraud / Math.max(C.RED+C.YELLOW,1)).toFixed(3) | |
| : 'โ'; | |
| ctx.fillStyle='#C8A951'; ctx.font='bold 10px IBM Plex Sans'; | |
| ctx.textAlign='left'; | |
| ctx.fillText('LIVE PRECISION: '+liveP, 8, H-6); | |
| // Timer | |
| const rem = Math.max(0,30-(elapsed/1000)); | |
| ctx.fillStyle='#557796'; ctx.font='10px monospace'; | |
| ctx.textAlign='right'; | |
| ctx.fillText('โฑ '+rem.toFixed(1)+'s', W-8, H-6); | |
| // Progress bar | |
| document.getElementById('prog-fill').style.width=(prog*100).toFixed(1)+'%'; | |
| document.getElementById('prog-pct').textContent= | |
| (prog*100).toFixed(0)+'% ('+C.total+' / '+TOTAL+' processed)'; | |
| drawStageLabel(prog); | |
| // Check completion | |
| const allDone = prog>=1 && bills.every(b=>b.phase==='done'||b.phase==='exiting'); | |
| if(allDone && !simComplete) {{ | |
| simComplete=true; | |
| // ensure final counts | |
| bills.forEach(b=>{{ if(!b.counted && b.phase!=='done') {{ | |
| b.counted=true; C.total++; C[b.channel]++; | |
| if(b.explore) C.explore++; | |
| if(b.fraud) C.fraud++; | |
| }} }}); | |
| updateDOM(); | |
| setTimeout(()=>{{ drawDone(); }}, 400); | |
| return; | |
| }} | |
| requestAnimationFrame(loop); | |
| }} | |
| drawBG(); | |
| requestAnimationFrame(loop); | |
| </script> | |
| </body> | |
| </html>""" | |
| # โโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโ | |
| def risk_score_distribution(df): | |
| fig = go.Figure() | |
| for ch, color, label in [ | |
| ("GREEN","#00843D","๐ข GREEN"), | |
| ("YELLOW","#F5A800","๐ก YELLOW"), | |
| ("RED","#C8102E","๐ด RED"), | |
| ]: | |
| cdf = df[df["channel"]==ch] | |
| fig.add_trace(go.Violin( | |
| y=cdf["fraud_score"], name=label, | |
| fillcolor=color, opacity=0.7, | |
| line_color=color, box_visible=True, | |
| meanline_visible=True, points=False, | |
| )) | |
| fig.update_layout( | |
| paper_bgcolor="#070E1C", plot_bgcolor="#0B1220", | |
| font=dict(family="IBM Plex Sans", color="#D0DCF0", size=12), | |
| height=300, | |
| title=dict(text="<b>Risk Score Distribution by Channel</b>", | |
| font=dict(color=WCO_GOLD, size=13, family="Playfair Display"), x=0.5), | |
| yaxis=dict(title="Fraud Score", gridcolor="#1E3A6E", range=[0,1]), | |
| xaxis=dict(gridcolor="#1E3A6E"), | |
| legend=dict(bgcolor="#0F1C35", bordercolor=WCO_BORDER), | |
| margin=dict(l=50,r=20,t=50,b=40), violingap=0.3, | |
| ) | |
| return fig | |
| def area_heatmap(df): | |
| pivot = pd.crosstab(df["risk_area"], df["channel"]) | |
| for col in ["RED","YELLOW","GREEN"]: | |
| if col not in pivot.columns: pivot[col]=0 | |
| pivot = pivot[["RED","YELLOW","GREEN"]] | |
| fig = go.Figure(go.Heatmap( | |
| z=pivot.values, x=pivot.columns, y=pivot.index, | |
| colorscale=[[0,"#0B1220"],[0.5,"#003087"],[1,"#C8A951"]], | |
| text=pivot.values, texttemplate="%{text}", textfont={"size":13}, | |
| hovertemplate="<b>%{y}</b><br>Channel: %{x}<br>Bills: %{z}<extra></extra>", | |
| showscale=True, | |
| colorbar=dict(tickfont=dict(color="#D0DCF0"), | |
| title="Bills", titlefont=dict(color=WCO_GOLD)), | |
| )) | |
| fig.update_layout( | |
| paper_bgcolor="#070E1C", plot_bgcolor="#0B1220", | |
| font=dict(family="IBM Plex Sans", color="#D0DCF0", size=11), | |
| height=280, | |
| title=dict(text="<b>Risk Area ร Channel Heatmap</b>", | |
| font=dict(color=WCO_GOLD, size=13, family="Playfair Display"), x=0.5), | |
| margin=dict(l=165,r=30,t=50,b=40), | |
| ) | |
| return fig | |
| # โโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโ | |
| def show(): | |
| inject_global_css() | |
| page_header("๐", "Self-Learning Simulation Engine", | |
| "DATE EXPLOITATION ยท gATE EXPLORATION ยท 30-SECOND LIVE CANVAS ANIMATION") | |
| # โโ Controls โโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโ | |
| st.markdown('<div class="section-title">๐๏ธ Simulation Control Panel</div>', | |
| unsafe_allow_html=True) | |
| c1,c2,c3,c4 = st.columns(4) | |
| with c1: n_bills = st.slider("๐ฆ Declarations", 200,1000,1000,step=100) | |
| with c2: bandwidth = st.slider("๐ก Bandwidth (%)", 5,30,10,step=1)/100 | |
| with c3: exp_ratio = st.slider("๐ Exploration ฮต (%)",1,30,10,step=1)/100 | |
| with c4: seed = st.slider("๐ฒ Seed",1,99,42,step=1) | |
| st.markdown(f""" | |
| <div class="alert-blue"> | |
| <b>Strategy:</b> DATE {100-int(exp_ratio*100)}% + gATE {int(exp_ratio*100)}% | |
| | <b>Bandwidth:</b> {int(bandwidth*100)}% โ {int(n_bills*bandwidth)} bills inspected | |
| | <b>Facilitated:</b> {n_bills-int(n_bills*bandwidth)} bills (GREEN) | |
| | <b>โฑ Animation duration:</b> exactly 30 seconds for full batch | |
| </div>""", unsafe_allow_html=True) | |
| col1,col2,_ = st.columns([1.2,1,4]) | |
| with col1: run_btn = st.button("โถ Run Simulation", type="primary", use_container_width=True) | |
| with col2: reset_btn = st.button("๐ Reset All", use_container_width=True) | |
| if reset_btn: | |
| for k in ["sim_df","sim_weights","sim_efficiency"]: | |
| st.session_state.pop(k,None) | |
| st.rerun() | |
| # โโ Run โโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโ | |
| if run_btn or "sim_df" in st.session_state: | |
| if run_btn or "sim_df" not in st.session_state: | |
| weights = st.session_state.get("rule_weights", get_default_weights()) | |
| prog = st.progress(0, text="โ๏ธ Generating declarations...") | |
| df = generate_declarations(n_bills, seed=seed) | |
| prog.progress(25, text="๐ง DATE scoring...") | |
| df = compute_risk_scores(df, weights) | |
| prog.progress(50, text="โ๏ธ Hybrid channel routing...") | |
| df = assign_channels(df, bandwidth=bandwidth, exploration_ratio=exp_ratio) | |
| prog.progress(75, text="๐ฎ Inspection outcomes...") | |
| df = simulate_inspection_outcomes(df, seed=seed) | |
| prog.progress(90, text="๐ Weight update & efficiency...") | |
| uw = compute_updated_weights(df, weights) | |
| eff = compute_efficiency_metrics(df) | |
| prog.progress(100, text="โ Launching animation!") | |
| st.session_state.sim_df = df | |
| st.session_state.sim_weights = uw | |
| st.session_state.sim_efficiency = eff | |
| df = st.session_state.sim_df | |
| eff = st.session_state.sim_efficiency | |
| hybrid = eff.get("hybrid", {}) | |
| ch = df["channel"].value_counts() | |
| detected = int((df["inspection_outcome"]=="FRAUD_DETECTED").sum()) | |
| revenue = float(df["detected_revenue"].sum()) | |
| eff_idx = float(hybrid.get("efficiency_index", 0.0)) | |
| channel_counts = { | |
| "RED": int(ch.get("RED",0)), | |
| "YELLOW": int(ch.get("YELLOW",0)), | |
| "GREEN": int(ch.get("GREEN",0)), | |
| } | |
| # โโ Legend โโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโ | |
| st.markdown('<div class="section-title">๐ฌ Live RMS Canvas Animation</div>', | |
| unsafe_allow_html=True) | |
| st.markdown("""<div class="alert-gold"> | |
| ๐ <b>30-second real-time simulation.</b> | |
| Each icon = one customs declaration flowing through the RMS pipeline.<br/> | |
| <span style="color:#FF4466;">โ Red glow</span> = Fraud detected | | |
| <span style="color:#CC77FF;">โ Purple glow</span> = gATE exploration pick | | |
| <span style="color:#3A6BB5;">โ Blue</span> = Standard routing | | |
| Fraud bills arc โ Offence DB vault (right lane) | |
| </div>""", unsafe_allow_html=True) | |
| # โโ CANVAS ANIMATION โโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโ | |
| html_src = build_animation_html( | |
| n_bills=n_bills, bandwidth=bandwidth, exp_ratio=exp_ratio, | |
| channel_counts=channel_counts, detected=detected, | |
| revenue=revenue, efficiency_idx=eff_idx, | |
| ) | |
| components.html(html_src, height=720, scrolling=False) | |
| # โโ Supporting charts โโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโ | |
| st.markdown("<br/>", unsafe_allow_html=True) | |
| st.markdown('<div class="section-title">๐ Channel Analytics</div>', | |
| unsafe_allow_html=True) | |
| ca,cb = st.columns([3,2]) | |
| with ca: st.plotly_chart(area_heatmap(df), use_container_width=True) | |
| with cb: st.plotly_chart(risk_score_distribution(df), use_container_width=True) | |
| # โโ Risk area cards โโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโ | |
| st.markdown('<div class="section-title">๐๏ธ Risk Area Breakdown</div>', | |
| unsafe_allow_html=True) | |
| acols = st.columns(5) | |
| for i,(aname,acfg) in enumerate(RISK_AREAS.items()): | |
| a_df = df[df["risk_area"]==aname] | |
| color = acfg["color"] | |
| ch_a = a_df["channel"].value_counts() | |
| fraud_a= (a_df["inspection_outcome"]=="FRAUD_DETECTED").sum() | |
| exp_a = a_df["is_exploration"].sum() | |
| with acols[i]: | |
| st.markdown(f""" | |
| <div style="background:#0F1C35;border:1px solid {color}; | |
| border-radius:10px;padding:14px 12px;"> | |
| <div style="color:{color};font-weight:700;font-size:12px; | |
| margin-bottom:10px;font-family:'Playfair Display',serif;"> | |
| {acfg['icon']} {aname} | |
| </div> | |
| <div style="color:#D0DCF0;font-size:12px;line-height:2.1;"> | |
| ๐ด RED: <b>{ch_a.get('RED',0)}</b><br/> | |
| ๐ก YEL: <b>{ch_a.get('YELLOW',0)}</b><br/> | |
| ๐ข GRN: <b>{ch_a.get('GREEN',0)}</b><br/> | |
| ๐จ Fraud: <b style="color:#FF4466;">{fraud_a}</b><br/> | |
| ๐ Expl: <b style="color:#CC77FF;">{exp_a}</b> | |
| </div> | |
| </div>""", unsafe_allow_html=True) | |
| st.markdown("""<div class="alert-gold" style="margin-top:18px;"> | |
| โ Simulation complete. Go to <b>Page 4</b> for full tables, | |
| offence DB growth, and weight-evolution analysis. | |
| </div>""", unsafe_allow_html=True) | |
| else: | |
| st.markdown(""" | |
| <div style="background:#0F1C35;border:2px dashed #1E3A6E;border-radius:14px; | |
| padding:70px;text-align:center;margin-top:20px;"> | |
| <div style="font-size:52px;margin-bottom:16px;">๐ฌ</div> | |
| <div style="color:#C8A951;font-family:'Playfair Display',serif; | |
| font-size:22px;margin-bottom:12px;">Ready to Simulate</div> | |
| <div style="color:#6B85AA;font-size:14px;line-height:1.8;"> | |
| Configure parameters above and click | |
| <b style="color:#C8A951;">โถ Run Simulation</b><br/> | |
| A 30-second canvas animation will show all declarations flowing | |
| through RED / YELLOW / GREEN channels in real-time. | |
| </div> | |
| </div>""", unsafe_allow_html=True) | |