| """ |
| 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") |
|
|
| |
| 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() |
|
|
| |
| 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)), |
| } |
|
|
| |
| 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) |
|
|
| |
| 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) |
|
|
| |
| 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) |
|
|
| |
| 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) |
|
|