Customs_Selflearning_RMS / page3_simulation.py
rameshmoorthy's picture
Upload 9 files
67bf425 verified
"""
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)}%
&nbsp;|&nbsp; <b>Bandwidth:</b> {int(bandwidth*100)}% โ†’ {int(n_bills*bandwidth)} bills inspected
&nbsp;|&nbsp; <b>Facilitated:</b> {n_bills-int(n_bills*bandwidth)} bills (GREEN)
&nbsp;|&nbsp; <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> &nbsp;
Each icon = one customs declaration flowing through the RMS pipeline.<br/>
<span style="color:#FF4466;">โ–  Red glow</span> = Fraud detected &nbsp;|&nbsp;
<span style="color:#CC77FF;">โ–  Purple glow</span> = gATE exploration pick &nbsp;|&nbsp;
<span style="color:#3A6BB5;">โ–  Blue</span> = Standard routing &nbsp;|&nbsp;
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)