Spaces:
Runtime error
Runtime error
ademarteau commited on
Commit Β·
9a68962
1
Parent(s): cb82053
simulator jsx from claude
Browse files- rl_simulator.jsx +826 -0
rl_simulator.jsx
ADDED
|
@@ -0,0 +1,826 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
import { useState, useRef, useCallback, useEffect } from "react";
|
| 2 |
+
import { LineChart, Line, XAxis, YAxis, Tooltip, ResponsiveContainer, ReferenceLine, AreaChart, Area, BarChart, Bar, Legend } from "recharts";
|
| 3 |
+
|
| 4 |
+
// βββ DESIGN TOKENS ββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββ
|
| 5 |
+
const C = {
|
| 6 |
+
bg: "#07090f",
|
| 7 |
+
panel: "#0d1117",
|
| 8 |
+
border: "#161d2a",
|
| 9 |
+
border2: "#1e2d40",
|
| 10 |
+
text: "#c9d5e0",
|
| 11 |
+
muted: "#3a5060",
|
| 12 |
+
dim: "#1a2535",
|
| 13 |
+
green: "#34d399",
|
| 14 |
+
blue: "#38bdf8",
|
| 15 |
+
amber: "#fbbf24",
|
| 16 |
+
red: "#f87171",
|
| 17 |
+
purple: "#a78bfa",
|
| 18 |
+
teal: "#2dd4bf",
|
| 19 |
+
};
|
| 20 |
+
|
| 21 |
+
// βββ CONFIG (mirrors config.py exactly) βββββββββββββββββββββββββββββββββββββββ
|
| 22 |
+
const CFG = {
|
| 23 |
+
LEAD_TIME: 3,
|
| 24 |
+
BASE_STOCK: 0,
|
| 25 |
+
DEFAULT_SL: 0.95,
|
| 26 |
+
WRITE_OFF_RATE: 0.01,
|
| 27 |
+
WRITE_OFF_FREQ: 7,
|
| 28 |
+
HISTO_DAYS: 30,
|
| 29 |
+
SIM_DAYS: 120,
|
| 30 |
+
};
|
| 31 |
+
|
| 32 |
+
// βββ MATH HELPERS βββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββ
|
| 33 |
+
function normalRandom() {
|
| 34 |
+
let u=0,v=0; while(!u)u=Math.random(); while(!v)v=Math.random();
|
| 35 |
+
return Math.sqrt(-2*Math.log(u))*Math.cos(2*Math.PI*v);
|
| 36 |
+
}
|
| 37 |
+
function gammaRandom(shape,scale){
|
| 38 |
+
if(shape<1)return gammaRandom(1+shape,scale)*Math.pow(Math.random(),1/shape);
|
| 39 |
+
const d=shape-1/3,c=1/Math.sqrt(9*d);
|
| 40 |
+
while(true){let x,v;do{x=normalRandom();v=1+c*x;}while(v<=0);v=v*v*v;const u=Math.random();
|
| 41 |
+
if(u<1-0.0331*x*x*x*x)return d*v*scale;
|
| 42 |
+
if(Math.log(u)<0.5*x*x+d*(1-v+Math.log(v)))return d*v*scale;}
|
| 43 |
+
}
|
| 44 |
+
function poissonRandom(lambda){let L=Math.exp(-lambda),k=0,p=1;do{k++;p*=Math.random();}while(p>L);return k-1;}
|
| 45 |
+
function expRandom(rate){return-Math.log(Math.random())/rate;}
|
| 46 |
+
function arr_mean(a){return a.length?a.reduce((s,x)=>s+x,0)/a.length:0;}
|
| 47 |
+
function arr_std(a){if(a.length<2)return 0;const m=arr_mean(a);return Math.sqrt(a.reduce((s,x)=>s+(x-m)**2,0)/(a.length-1));}
|
| 48 |
+
function quantile(sorted,q){return sorted[Math.floor(sorted.length*q)];}
|
| 49 |
+
|
| 50 |
+
// βββ DEMAND ENVIRONMENTS (mirrors demand_environment.py) ββββββββββββββββββββββ
|
| 51 |
+
const ENVS = {
|
| 52 |
+
gamma_poisson:{
|
| 53 |
+
label:"GammaβPoisson",tag:"MODERATE",color:C.green,
|
| 54 |
+
desc:"90% Gamma(7,16) + 10% Poisson(80). Stable with rare spikes.",
|
| 55 |
+
sample:()=>Math.random()<0.9?Math.max(0,Math.round(gammaRandom(7,16))):poissonRandom(80),
|
| 56 |
+
demMean:112,demStd:38,
|
| 57 |
+
},
|
| 58 |
+
bimodal_hv:{
|
| 59 |
+
label:"Bimodal High-Var",tag:"HARD",color:C.amber,
|
| 60 |
+
desc:"50% Gamma(low mean) + 50% Gamma(high mean). Extremely unpredictable.",
|
| 61 |
+
sample:()=>Math.random()<0.5?Math.max(0,Math.round(gammaRandom(7,3))):Math.max(0,Math.round(gammaRandom(7,29))),
|
| 62 |
+
demMean:112,demStd:95,
|
| 63 |
+
},
|
| 64 |
+
spiking:{
|
| 65 |
+
label:"Sporadic Spiking",tag:"EXTREME",color:C.red,
|
| 66 |
+
desc:"95% zero demand, 5% large Exponential bursts. Hardest to plan.",
|
| 67 |
+
sample:()=>Math.random()<0.95?0:Math.max(0,Math.round(expRandom(0.05))),
|
| 68 |
+
demMean:20,demStd:55,
|
| 69 |
+
},
|
| 70 |
+
gamma_stable:{
|
| 71 |
+
label:"Stable Gamma",tag:"EASY",color:C.blue,
|
| 72 |
+
desc:"Single Gamma(7,16), low variance. Baseline environment.",
|
| 73 |
+
sample:()=>Math.max(0,Math.round(gammaRandom(7,16))),
|
| 74 |
+
demMean:112,demStd:35,
|
| 75 |
+
},
|
| 76 |
+
};
|
| 77 |
+
|
| 78 |
+
// βββ BASELINE AGENTS (mirrors agent_environment.py) βββββββββββββββββββββββββββ
|
| 79 |
+
const BASELINES = {
|
| 80 |
+
base:{
|
| 81 |
+
label:"Base",color:C.muted,
|
| 82 |
+
compute:(hist)=>arr_mean(hist)*CFG.LEAD_TIME,
|
| 83 |
+
},
|
| 84 |
+
safety_stock:{
|
| 85 |
+
label:"Safety Stock",color:C.blue,
|
| 86 |
+
compute:(hist)=>{
|
| 87 |
+
const m=arr_mean(hist),s=arr_std(hist);
|
| 88 |
+
return m*CFG.LEAD_TIME+1.645*s*Math.sqrt(CFG.LEAD_TIME);
|
| 89 |
+
},
|
| 90 |
+
},
|
| 91 |
+
forecast:{
|
| 92 |
+
label:"Oracle Forecast",color:C.green,
|
| 93 |
+
compute:(hist,dMean,dStd)=>dMean*CFG.LEAD_TIME+1.645*dStd*Math.sqrt(CFG.LEAD_TIME),
|
| 94 |
+
},
|
| 95 |
+
monte_carlo:{
|
| 96 |
+
label:"Monte Carlo",color:C.purple,
|
| 97 |
+
compute:(hist)=>{
|
| 98 |
+
const s=[];
|
| 99 |
+
for(let i=0;i<500;i++){
|
| 100 |
+
let t=0;for(let j=0;j<CFG.LEAD_TIME;j++)t+=hist[Math.floor(Math.random()*hist.length)]*(0.8+Math.random()*0.4);
|
| 101 |
+
s.push(t);
|
| 102 |
+
}
|
| 103 |
+
s.sort((a,b)=>a-b);return quantile(s,0.95);
|
| 104 |
+
},
|
| 105 |
+
},
|
| 106 |
+
};
|
| 107 |
+
|
| 108 |
+
// βββ SIMULATION ENGINE ββββββββββββββββββββββββββββββββββββββββββββββββββββββββ
|
| 109 |
+
function buildDemandSeries(envKey, n){
|
| 110 |
+
return Array.from({length:n},()=>ENVS[envKey].sample());
|
| 111 |
+
}
|
| 112 |
+
|
| 113 |
+
function runOneSimulation(computeROP, demandSeries, envKey){
|
| 114 |
+
const env=ENVS[envKey];
|
| 115 |
+
const n=demandSeries.length;
|
| 116 |
+
let inventory=0;
|
| 117 |
+
const orders=[];
|
| 118 |
+
let totDemand=0,totFulfilled=0,totWriteOff=0,stockOuts=0,lostSales=0;
|
| 119 |
+
const timeline=[];
|
| 120 |
+
|
| 121 |
+
for(let day=0;day<n;day++){
|
| 122 |
+
const demand=demandSeries[day];
|
| 123 |
+
const hist=demandSeries.slice(Math.max(0,day-CFG.HISTO_DAYS),day);
|
| 124 |
+
|
| 125 |
+
// Deliver orders
|
| 126 |
+
const arrivals=orders.filter(o=>o.arr===day);
|
| 127 |
+
const delivered=arrivals.reduce((s,o)=>s+o.qty,0);
|
| 128 |
+
inventory+=delivered;
|
| 129 |
+
orders.splice(0,orders.length,...orders.filter(o=>o.arr>day));
|
| 130 |
+
|
| 131 |
+
const preInv=inventory;
|
| 132 |
+
|
| 133 |
+
// Fulfill demand
|
| 134 |
+
const fulfilled=Math.min(demand,inventory);
|
| 135 |
+
inventory=Math.max(0,inventory-demand);
|
| 136 |
+
const lost=Math.max(0,demand-fulfilled);
|
| 137 |
+
if(lost>0)stockOuts++;
|
| 138 |
+
lostSales+=lost;
|
| 139 |
+
|
| 140 |
+
// Reorder
|
| 141 |
+
let rop=0,ordered=0;
|
| 142 |
+
if(hist.length>=5&&day<n-CFG.LEAD_TIME){
|
| 143 |
+
rop=Math.max(0,computeROP(hist,env.demMean,env.demStd));
|
| 144 |
+
if(inventory<=rop){
|
| 145 |
+
const qty=Math.ceil(rop-inventory+arr_mean(hist)*CFG.LEAD_TIME);
|
| 146 |
+
orders.push({arr:day+CFG.LEAD_TIME,qty});
|
| 147 |
+
ordered=qty;
|
| 148 |
+
}
|
| 149 |
+
}
|
| 150 |
+
|
| 151 |
+
// Write-off
|
| 152 |
+
let wo=0;
|
| 153 |
+
if(day%CFG.WRITE_OFF_FREQ===0){wo=Math.floor(inventory*CFG.WRITE_OFF_RATE);inventory-=wo;totWriteOff+=wo;}
|
| 154 |
+
|
| 155 |
+
totDemand+=demand;totFulfilled+=fulfilled;
|
| 156 |
+
const fillRateCum=totDemand>0?totFulfilled/totDemand:0;
|
| 157 |
+
timeline.push({day,demand,inventory:preInv,inventoryAfter:inventory,fulfilled,lost,rop:Math.round(rop),ordered,wo,delivered,fillRateCum});
|
| 158 |
+
}
|
| 159 |
+
return{timeline,metrics:{fillRate:totDemand>0?totFulfilled/totDemand:0,stockOuts,lostSales,totWriteOff,totDemand,totFulfilled}};
|
| 160 |
+
}
|
| 161 |
+
|
| 162 |
+
// βββ BUILD ENVIRONMENT SNAPSHOT FOR LLM βββββββββββββββββββββββββββββββββββββββ
|
| 163 |
+
function buildEnvSnapshot(demandSeries, timeline, day){
|
| 164 |
+
const recent=demandSeries.slice(Math.max(0,day-10),day);
|
| 165 |
+
const hist=demandSeries.slice(Math.max(0,day-CFG.HISTO_DAYS),day);
|
| 166 |
+
const last5=timeline.slice(Math.max(0,day-5),day);
|
| 167 |
+
const curInv=timeline[day-1]?.inventoryAfter??0;
|
| 168 |
+
const pendingOrders=[];
|
| 169 |
+
// Reconstruct pending from timeline (simplified)
|
| 170 |
+
const fillSoFar=timeline[day-1]?.fillRateCum??null;
|
| 171 |
+
|
| 172 |
+
return {
|
| 173 |
+
day,
|
| 174 |
+
current_inventory: curInv,
|
| 175 |
+
lead_time: CFG.LEAD_TIME,
|
| 176 |
+
write_off_rate: CFG.WRITE_OFF_RATE,
|
| 177 |
+
service_level_target: CFG.DEFAULT_SL,
|
| 178 |
+
sim_days_total: CFG.SIM_DAYS,
|
| 179 |
+
days_remaining: CFG.SIM_DAYS-day,
|
| 180 |
+
recent_demand_10d: recent,
|
| 181 |
+
demand_mean_30d: Math.round(arr_mean(hist)*10)/10,
|
| 182 |
+
demand_std_30d: Math.round(arr_std(hist)*10)/10,
|
| 183 |
+
fill_rate_so_far: fillSoFar ? Math.round(fillSoFar*1000)/10+"%" : "N/A",
|
| 184 |
+
last_5_days: last5.map(d=>({day:d.day,demand:d.demand,inv:d.inventoryAfter,lost:d.lost,rop:d.rop,ordered:d.ordered})),
|
| 185 |
+
recent_stockouts: last5.filter(d=>d.lost>0).length,
|
| 186 |
+
recent_lost_sales: last5.reduce((s,d)=>s+d.lost,0),
|
| 187 |
+
};
|
| 188 |
+
}
|
| 189 |
+
|
| 190 |
+
// βββ LLM CALL βββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββ
|
| 191 |
+
async function callClaude(messages, systemPrompt){
|
| 192 |
+
const resp=await fetch("https://api.anthropic.com/v1/messages",{
|
| 193 |
+
method:"POST",
|
| 194 |
+
headers:{"Content-Type":"application/json"},
|
| 195 |
+
body:JSON.stringify({
|
| 196 |
+
model:"claude-sonnet-4-20250514",
|
| 197 |
+
max_tokens:1000,
|
| 198 |
+
system:systemPrompt,
|
| 199 |
+
messages,
|
| 200 |
+
}),
|
| 201 |
+
});
|
| 202 |
+
const data=await resp.json();
|
| 203 |
+
const text=data.content?.find(b=>b.type==="text")?.text||"";
|
| 204 |
+
return text;
|
| 205 |
+
}
|
| 206 |
+
|
| 207 |
+
// βββ SYSTEM PROMPT ββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββ
|
| 208 |
+
const SYSTEM_PROMPT = `You are an expert inventory optimization agent embedded in a stochastic simulation environment.
|
| 209 |
+
|
| 210 |
+
YOUR ROLE:
|
| 211 |
+
You receive a JSON snapshot of the current simulation state and must decide the REORDER POINT (ROP) β the inventory threshold that triggers a new order.
|
| 212 |
+
|
| 213 |
+
ENVIRONMENT RULES:
|
| 214 |
+
- Orders arrive exactly LEAD_TIME=3 days after placement
|
| 215 |
+
- You place an order whenever inventory <= your ROP
|
| 216 |
+
- Order quantity = ROP - current_inventory + mean_demand * LEAD_TIME (already handled)
|
| 217 |
+
- Every 7 days, 1% of inventory is written off (waste/expiry)
|
| 218 |
+
- Reward = fill_rate at end of simulation (target: >=95%)
|
| 219 |
+
- Reward is SPARSE: fill rate only stabilizes after ~50 days
|
| 220 |
+
|
| 221 |
+
REASONING REQUIREMENTS - you MUST do all 4:
|
| 222 |
+
1. SUBGOAL DECOMPOSITION: Break the problem into explicit subgoals (e.g., "build buffer", "survive spike risk", "minimize waste")
|
| 223 |
+
2. STATE ANALYSIS: Interpret current inventory, demand trend, stockout risk, fill rate trajectory
|
| 224 |
+
3. DECISION: Output a specific numeric ROP with clear justification
|
| 225 |
+
4. RECOVERY PLAN: If fill rate < 95% or recent stockouts occurred, state your recovery strategy
|
| 226 |
+
|
| 227 |
+
CRITICAL: You must reason BEYOND the next step. Consider that your ROP today affects inventory 3+ days from now.
|
| 228 |
+
For spiking demand: ROP must account for rare but catastrophic spikes.
|
| 229 |
+
For high-variance: wider safety buffers needed.
|
| 230 |
+
For stable demand: tighter ROP to avoid write-offs.
|
| 231 |
+
|
| 232 |
+
OUTPUT FORMAT β respond with this exact JSON (no markdown fences):
|
| 233 |
+
{
|
| 234 |
+
"subgoals": ["subgoal 1", "subgoal 2", "subgoal 3"],
|
| 235 |
+
"state_analysis": "2-3 sentence analysis of current state and risks",
|
| 236 |
+
"recovery_plan": "what you're doing to recover or maintain performance",
|
| 237 |
+
"reorder_point": <number>,
|
| 238 |
+
"confidence": "high|medium|low",
|
| 239 |
+
"reasoning_depth": "brief note on what makes this decision non-trivial"
|
| 240 |
+
}`;
|
| 241 |
+
|
| 242 |
+
// βββ MAIN COMPONENT βββββββββββββββββββββββββββββββββββββββββββββββββββββββββββ
|
| 243 |
+
export default function StockOracleAgent() {
|
| 244 |
+
const [envKey, setEnvKey] = useState("gamma_poisson");
|
| 245 |
+
const [phase, setPhase] = useState("config"); // config | running | done
|
| 246 |
+
const [agentLog, setAgentLog] = useState([]); // [{day, snapshot, decision, rop}]
|
| 247 |
+
const [simTimeline, setSimTimeline] = useState([]);
|
| 248 |
+
const [baselineResults, setBaselineResults] = useState({});
|
| 249 |
+
const [agentMetrics, setAgentMetrics] = useState(null);
|
| 250 |
+
const [runningDay, setRunningDay] = useState(0);
|
| 251 |
+
const [statusMsg, setStatusMsg] = useState("");
|
| 252 |
+
const [memoryBank, setMemoryBank] = useState([]); // persistent cross-turn memory
|
| 253 |
+
const [conversationHistory, setConversationHistory] = useState([]);
|
| 254 |
+
const [activeTab, setActiveTab] = useState("live"); // live | reasoning | compare | memory
|
| 255 |
+
const abortRef = useRef(false);
|
| 256 |
+
const logEndRef = useRef(null);
|
| 257 |
+
|
| 258 |
+
useEffect(()=>{if(logEndRef.current)logEndRef.current.scrollIntoView({behavior:"smooth"});},[agentLog]);
|
| 259 |
+
|
| 260 |
+
// ββ Run baselines (instant, no API) ββ
|
| 261 |
+
const runBaselines = useCallback((demandSeries) => {
|
| 262 |
+
const results = {};
|
| 263 |
+
Object.entries(BASELINES).forEach(([k,ag])=>{
|
| 264 |
+
results[k]=runOneSimulation((h,dm,ds)=>ag.compute(h,dm,ds), demandSeries, envKey);
|
| 265 |
+
});
|
| 266 |
+
setBaselineResults(results);
|
| 267 |
+
return results;
|
| 268 |
+
},[envKey]);
|
| 269 |
+
|
| 270 |
+
// ββ Build persistent memory summary ββ
|
| 271 |
+
function updateMemory(prevMemory, decision, day, metrics){
|
| 272 |
+
const entry = {
|
| 273 |
+
day,
|
| 274 |
+
rop: decision.reorder_point,
|
| 275 |
+
confidence: decision.confidence,
|
| 276 |
+
fill_rate: metrics?.fillRate ? Math.round(metrics.fillRate*1000)/10 : null,
|
| 277 |
+
stockouts_in_window: metrics?.stockOuts??0,
|
| 278 |
+
key_insight: decision.state_analysis?.slice(0,80)+"...",
|
| 279 |
+
};
|
| 280 |
+
// Keep last 15 memory entries as compressed state
|
| 281 |
+
const newMem = [...prevMemory.slice(-14), entry];
|
| 282 |
+
return newMem;
|
| 283 |
+
}
|
| 284 |
+
|
| 285 |
+
// ββ Main simulation loop ββ
|
| 286 |
+
const runAgentSimulation = useCallback(async () => {
|
| 287 |
+
abortRef.current = false;
|
| 288 |
+
setPhase("running");
|
| 289 |
+
setAgentLog([]);
|
| 290 |
+
setSimTimeline([]);
|
| 291 |
+
setAgentMetrics(null);
|
| 292 |
+
setMemoryBank([]);
|
| 293 |
+
setConversationHistory([]);
|
| 294 |
+
setRunningDay(0);
|
| 295 |
+
|
| 296 |
+
const demandSeries = buildDemandSeries(envKey, CFG.SIM_DAYS);
|
| 297 |
+
|
| 298 |
+
// Run baselines in background
|
| 299 |
+
setStatusMsg("Computing baseline agents...");
|
| 300 |
+
runBaselines(demandSeries);
|
| 301 |
+
|
| 302 |
+
// Agent-driven simulation
|
| 303 |
+
// We step through the sim, calling Claude every DECISION_INTERVAL days
|
| 304 |
+
const DECISION_INTERVAL = 5; // Claude decides ROP every 5 days
|
| 305 |
+
let inventory = 0;
|
| 306 |
+
const orders = [];
|
| 307 |
+
let totDemand=0, totFulfilled=0, totWriteOff=0, stockOuts=0, lostSales=0;
|
| 308 |
+
const timeline = [];
|
| 309 |
+
let currentROP = arr_mean(demandSeries.slice(0,CFG.HISTO_DAYS)) * CFG.LEAD_TIME; // initial ROP
|
| 310 |
+
let localMemory = [];
|
| 311 |
+
let localConvo = [];
|
| 312 |
+
let localLog = [];
|
| 313 |
+
|
| 314 |
+
for(let day=0; day<CFG.SIM_DAYS; day++){
|
| 315 |
+
if(abortRef.current) break;
|
| 316 |
+
|
| 317 |
+
const demand = demandSeries[day];
|
| 318 |
+
const hist = demandSeries.slice(Math.max(0,day-CFG.HISTO_DAYS), day);
|
| 319 |
+
|
| 320 |
+
// Deliver orders
|
| 321 |
+
const arrivals = orders.filter(o=>o.arr===day);
|
| 322 |
+
const delivered = arrivals.reduce((s,o)=>s+o.qty,0);
|
| 323 |
+
inventory += delivered;
|
| 324 |
+
orders.splice(0,orders.length,...orders.filter(o=>o.arr>day));
|
| 325 |
+
|
| 326 |
+
const preInv = inventory;
|
| 327 |
+
|
| 328 |
+
// Fulfill demand
|
| 329 |
+
const fulfilled = Math.min(demand, inventory);
|
| 330 |
+
inventory = Math.max(0, inventory-demand);
|
| 331 |
+
const lost = Math.max(0, demand-fulfilled);
|
| 332 |
+
if(lost>0) stockOuts++;
|
| 333 |
+
lostSales += lost;
|
| 334 |
+
|
| 335 |
+
// Reorder check using current ROP
|
| 336 |
+
let ordered=0;
|
| 337 |
+
if(hist.length>=5 && day<CFG.SIM_DAYS-CFG.LEAD_TIME){
|
| 338 |
+
if(inventory<=currentROP){
|
| 339 |
+
const qty=Math.ceil(currentROP-inventory+arr_mean(hist)*CFG.LEAD_TIME);
|
| 340 |
+
orders.push({arr:day+CFG.LEAD_TIME,qty});
|
| 341 |
+
ordered=qty;
|
| 342 |
+
}
|
| 343 |
+
}
|
| 344 |
+
|
| 345 |
+
// Write-off
|
| 346 |
+
let wo=0;
|
| 347 |
+
if(day%CFG.WRITE_OFF_FREQ===0){wo=Math.floor(inventory*CFG.WRITE_OFF_RATE);inventory-=wo;totWriteOff+=wo;}
|
| 348 |
+
|
| 349 |
+
totDemand+=demand; totFulfilled+=fulfilled;
|
| 350 |
+
const fillRateCum = totDemand>0?totFulfilled/totDemand:0;
|
| 351 |
+
const tEntry = {day,demand,inventory:preInv,inventoryAfter:inventory,fulfilled,lost,rop:Math.round(currentROP),ordered,wo,delivered,fillRateCum};
|
| 352 |
+
timeline.push(tEntry);
|
| 353 |
+
|
| 354 |
+
setSimTimeline([...timeline]);
|
| 355 |
+
setRunningDay(day);
|
| 356 |
+
|
| 357 |
+
// ββ LLM Decision every DECISION_INTERVAL days ββ
|
| 358 |
+
if(day>=CFG.HISTO_DAYS && day%DECISION_INTERVAL===0 && day<CFG.SIM_DAYS-CFG.LEAD_TIME){
|
| 359 |
+
setStatusMsg(`Day ${day}: Agent reasoning...`);
|
| 360 |
+
|
| 361 |
+
const snapshot = buildEnvSnapshot(demandSeries, timeline, day);
|
| 362 |
+
|
| 363 |
+
// Build memory context
|
| 364 |
+
const memoryContext = localMemory.length>0
|
| 365 |
+
? `\nYOUR MEMORY FROM PREVIOUS DECISIONS:\n${JSON.stringify(localMemory.slice(-8),null,2)}`
|
| 366 |
+
: "";
|
| 367 |
+
|
| 368 |
+
const userMsg = {
|
| 369 |
+
role:"user",
|
| 370 |
+
content: `ENVIRONMENT SNAPSHOT β Day ${day}/${CFG.SIM_DAYS}\n${JSON.stringify(snapshot,null,2)}${memoryContext}\n\nDecide your reorder_point for the next ${DECISION_INTERVAL} days.`
|
| 371 |
+
};
|
| 372 |
+
|
| 373 |
+
// Maintain rolling conversation (last 6 turns to stay in context)
|
| 374 |
+
const trimmedConvo = localConvo.slice(-6);
|
| 375 |
+
const fullMessages = [...trimmedConvo, userMsg];
|
| 376 |
+
|
| 377 |
+
try {
|
| 378 |
+
const rawResp = await callClaude(fullMessages, SYSTEM_PROMPT);
|
| 379 |
+
let decision;
|
| 380 |
+
try {
|
| 381 |
+
const cleaned = rawResp.replace(/```json|```/g,"").trim();
|
| 382 |
+
decision = JSON.parse(cleaned);
|
| 383 |
+
} catch {
|
| 384 |
+
// Fallback: extract reorder_point with regex
|
| 385 |
+
const match = rawResp.match(/"reorder_point"\s*:\s*(\d+\.?\d*)/);
|
| 386 |
+
decision = {
|
| 387 |
+
subgoals:["parse error β fallback"],
|
| 388 |
+
state_analysis: rawResp.slice(0,200),
|
| 389 |
+
recovery_plan:"N/A",
|
| 390 |
+
reorder_point: match ? parseFloat(match[1]) : currentROP,
|
| 391 |
+
confidence:"low",
|
| 392 |
+
reasoning_depth:"parse failed",
|
| 393 |
+
};
|
| 394 |
+
}
|
| 395 |
+
|
| 396 |
+
currentROP = Math.max(0, decision.reorder_point||currentROP);
|
| 397 |
+
|
| 398 |
+
// Update conversation history
|
| 399 |
+
const assistantMsg = {role:"assistant", content:rawResp};
|
| 400 |
+
localConvo = [...localConvo, userMsg, assistantMsg];
|
| 401 |
+
setConversationHistory([...localConvo]);
|
| 402 |
+
|
| 403 |
+
// Update memory bank
|
| 404 |
+
localMemory = updateMemory(localMemory, decision, day, {fillRate:fillRateCum, stockOuts});
|
| 405 |
+
setMemoryBank([...localMemory]);
|
| 406 |
+
|
| 407 |
+
// Add to agent log
|
| 408 |
+
const logEntry = {day, snapshot, decision, rop:currentROP, fillRateCum};
|
| 409 |
+
localLog = [...localLog, logEntry];
|
| 410 |
+
setAgentLog([...localLog]);
|
| 411 |
+
|
| 412 |
+
} catch(e) {
|
| 413 |
+
setStatusMsg(`Day ${day}: API error β ${e.message}`);
|
| 414 |
+
}
|
| 415 |
+
|
| 416 |
+
// Small pause to not slam API
|
| 417 |
+
await new Promise(r=>setTimeout(r,200));
|
| 418 |
+
}
|
| 419 |
+
}
|
| 420 |
+
|
| 421 |
+
// Final metrics
|
| 422 |
+
const finalMetrics = {
|
| 423 |
+
fillRate:totDemand>0?totFulfilled/totDemand:0,
|
| 424 |
+
stockOuts, lostSales, totWriteOff, totDemand, totFulfilled
|
| 425 |
+
};
|
| 426 |
+
setAgentMetrics(finalMetrics);
|
| 427 |
+
setSimTimeline([...timeline]);
|
| 428 |
+
setPhase("done");
|
| 429 |
+
setStatusMsg("Simulation complete.");
|
| 430 |
+
setActiveTab("compare");
|
| 431 |
+
}, [envKey, runBaselines]);
|
| 432 |
+
|
| 433 |
+
const stopSim = () => { abortRef.current=true; setStatusMsg("Stopped by user."); setPhase("done"); };
|
| 434 |
+
|
| 435 |
+
// ββ Render helpers ββ
|
| 436 |
+
const env = ENVS[envKey];
|
| 437 |
+
const latestLog = agentLog[agentLog.length-1];
|
| 438 |
+
|
| 439 |
+
function FillBadge({rate}){
|
| 440 |
+
const c=rate>=0.95?C.green:rate>=0.85?C.amber:C.red;
|
| 441 |
+
return <span style={{color:c,fontWeight:700}}>{rate?(rate*100).toFixed(1)+"%":"β"}</span>;
|
| 442 |
+
}
|
| 443 |
+
|
| 444 |
+
function Panel({title,children,style={}}){
|
| 445 |
+
return(
|
| 446 |
+
<div style={{background:C.panel,border:`1px solid ${C.border}`,borderRadius:10,padding:"16px 18px",...style}}>
|
| 447 |
+
{title&&<div style={{fontSize:9,letterSpacing:4,color:C.muted,marginBottom:12,textTransform:"uppercase"}}>{title}</div>}
|
| 448 |
+
{children}
|
| 449 |
+
</div>
|
| 450 |
+
);
|
| 451 |
+
}
|
| 452 |
+
|
| 453 |
+
function Tab({id,label}){
|
| 454 |
+
const active=activeTab===id;
|
| 455 |
+
return(
|
| 456 |
+
<button onClick={()=>setActiveTab(id)} style={{
|
| 457 |
+
background:active?C.border2:"transparent",
|
| 458 |
+
border:`1px solid ${active?C.border2:"transparent"}`,
|
| 459 |
+
borderRadius:6,padding:"7px 14px",
|
| 460 |
+
color:active?C.text:C.muted,fontFamily:"inherit",
|
| 461 |
+
fontSize:11,cursor:"pointer",letterSpacing:1,transition:"all 0.15s",
|
| 462 |
+
}}>{label}</button>
|
| 463 |
+
);
|
| 464 |
+
}
|
| 465 |
+
|
| 466 |
+
const agentTimelineFillRates = simTimeline.map(t=>({day:t.day,agent:t.fillRateCum}));
|
| 467 |
+
|
| 468 |
+
return(
|
| 469 |
+
<div style={{minHeight:"100vh",background:C.bg,fontFamily:"'JetBrains Mono',monospace",color:C.text,padding:"24px 16px"}}>
|
| 470 |
+
<link href="https://fonts.googleapis.com/css2?family=JetBrains+Mono:wght@300;400;500;600&family=Clash+Display:wght@600;700&display=swap" rel="stylesheet"/>
|
| 471 |
+
|
| 472 |
+
{/* ββ HEADER ββ */}
|
| 473 |
+
<div style={{maxWidth:1200,margin:"0 auto"}}>
|
| 474 |
+
<div style={{display:"flex",justifyContent:"space-between",alignItems:"flex-start",marginBottom:28,flexWrap:"wrap",gap:12}}>
|
| 475 |
+
<div>
|
| 476 |
+
<div style={{fontSize:9,letterSpacing:5,color:C.muted,marginBottom:6}}>HACKATHON Β· LONG-HORIZON REASONING ENVIRONMENT</div>
|
| 477 |
+
<h1 style={{margin:0,fontSize:"clamp(32px,5vw,52px)",fontWeight:700,letterSpacing:-1,
|
| 478 |
+
background:`linear-gradient(120deg,${C.teal},${C.blue},${C.purple})`,
|
| 479 |
+
WebkitBackgroundClip:"text",WebkitTextFillColor:"transparent",lineHeight:1.1,
|
| 480 |
+
fontFamily:"'JetBrains Mono',monospace",
|
| 481 |
+
}}>STOCK ORACLE</h1>
|
| 482 |
+
<div style={{fontSize:10,color:C.muted,marginTop:5,letterSpacing:2}}>
|
| 483 |
+
LLM AGENT Β· INVENTORY OPTIMIZATION Β· SPARSE REWARD Β· MULTI-STEP PLANNING
|
| 484 |
+
</div>
|
| 485 |
+
</div>
|
| 486 |
+
{phase==="done"&&agentMetrics&&(
|
| 487 |
+
<div style={{display:"flex",gap:10,flexWrap:"wrap"}}>
|
| 488 |
+
{[
|
| 489 |
+
{label:"AGENT FILL RATE",val:<FillBadge rate={agentMetrics.fillRate}/>,highlight:true},
|
| 490 |
+
{label:"STOCKOUTS",val:agentMetrics.stockOuts},
|
| 491 |
+
{label:"LOST SALES",val:agentMetrics.lostSales.toLocaleString()},
|
| 492 |
+
{label:"LLM DECISIONS",val:agentLog.length},
|
| 493 |
+
].map(({label,val,highlight})=>(
|
| 494 |
+
<div key={label} style={{background:highlight?"#0d1f18":C.panel,border:`1px solid ${highlight?C.green+"30":C.border}`,borderRadius:8,padding:"10px 16px",textAlign:"center"}}>
|
| 495 |
+
<div style={{fontSize:9,letterSpacing:3,color:C.muted,marginBottom:3}}>{label}</div>
|
| 496 |
+
<div style={{fontSize:22,fontWeight:600,letterSpacing:1}}>{val}</div>
|
| 497 |
+
</div>
|
| 498 |
+
))}
|
| 499 |
+
</div>
|
| 500 |
+
)}
|
| 501 |
+
</div>
|
| 502 |
+
|
| 503 |
+
{/* ββ CONFIG ββ */}
|
| 504 |
+
{phase==="config"&&(
|
| 505 |
+
<div style={{display:"grid",gridTemplateColumns:"1fr 1fr",gap:16,marginBottom:20,maxWidth:800}}>
|
| 506 |
+
<Panel title="Demand Environment">
|
| 507 |
+
{Object.entries(ENVS).map(([k,e])=>(
|
| 508 |
+
<button key={k} onClick={()=>setEnvKey(k)} style={{
|
| 509 |
+
display:"block",width:"100%",textAlign:"left",
|
| 510 |
+
background:envKey===k?"#0f1e2e":"transparent",
|
| 511 |
+
border:`1px solid ${envKey===k?e.color+"50":C.border}`,
|
| 512 |
+
borderRadius:6,padding:"10px 12px",marginBottom:6,cursor:"pointer",fontFamily:"inherit",
|
| 513 |
+
transition:"all 0.15s",
|
| 514 |
+
}}>
|
| 515 |
+
<div style={{display:"flex",justifyContent:"space-between",alignItems:"center"}}>
|
| 516 |
+
<span style={{fontSize:12,color:envKey===k?e.color:C.muted,fontWeight:500}}>{e.label}</span>
|
| 517 |
+
<span style={{fontSize:9,color:e.color,border:`1px solid ${e.color}40`,borderRadius:3,padding:"2px 6px"}}>{e.tag}</span>
|
| 518 |
+
</div>
|
| 519 |
+
<div style={{fontSize:10,color:C.dim,marginTop:4,lineHeight:1.5}}>{e.desc}</div>
|
| 520 |
+
</button>
|
| 521 |
+
))}
|
| 522 |
+
</Panel>
|
| 523 |
+
<Panel title="About This Environment">
|
| 524 |
+
<div style={{fontSize:11,color:C.muted,lineHeight:1.8}}>
|
| 525 |
+
{[
|
| 526 |
+
["Sparse Reward","Fill rate only converges after 50+ days. No reward signal per individual decision."],
|
| 527 |
+
["Multi-Step Planning","Each ROP decision affects inventory 3 days forward (lead time). Cascading errors are common."],
|
| 528 |
+
["State Tracking","Agent maintains memory across 120 days: inventory levels, order pipeline, demand patterns."],
|
| 529 |
+
["Error Recovery","Post-stockout, agent must over-order to rebuild buffer without triggering write-off waste."],
|
| 530 |
+
["Extended Horizon","120 decisions Γ 5-day intervals. LLM conversation history managed via rolling window + memory bank."],
|
| 531 |
+
].map(([t,d])=>(
|
| 532 |
+
<div key={t} style={{marginBottom:10}}>
|
| 533 |
+
<span style={{color:C.teal,fontWeight:600}}>{t}: </span>
|
| 534 |
+
<span style={{color:C.muted}}>{d}</span>
|
| 535 |
+
</div>
|
| 536 |
+
))}
|
| 537 |
+
</div>
|
| 538 |
+
<button onClick={runAgentSimulation} style={{
|
| 539 |
+
width:"100%",marginTop:16,
|
| 540 |
+
background:"#0d1f18",border:`1px solid ${C.green}60`,
|
| 541 |
+
borderRadius:7,padding:"14px",color:C.green,
|
| 542 |
+
fontFamily:"inherit",fontSize:13,cursor:"pointer",
|
| 543 |
+
letterSpacing:2,fontWeight:600,transition:"all 0.2s",
|
| 544 |
+
}}>
|
| 545 |
+
βΆ LAUNCH AGENT SIMULATION
|
| 546 |
+
</button>
|
| 547 |
+
</Panel>
|
| 548 |
+
</div>
|
| 549 |
+
)}
|
| 550 |
+
|
| 551 |
+
{/* ββ RUNNING / DONE ββ */}
|
| 552 |
+
{(phase==="running"||phase==="done")&&(
|
| 553 |
+
<>
|
| 554 |
+
{/* Status bar */}
|
| 555 |
+
<div style={{display:"flex",justifyContent:"space-between",alignItems:"center",marginBottom:16,flexWrap:"wrap",gap:8}}>
|
| 556 |
+
<div style={{display:"flex",gap:8,alignItems:"center",fontSize:11}}>
|
| 557 |
+
{phase==="running"&&<span style={{color:C.amber,animation:"pulse 1s infinite"}}
|
| 558 |
+
>β</span>}
|
| 559 |
+
<span style={{color:C.muted}}>{statusMsg}</span>
|
| 560 |
+
{phase==="running"&&(
|
| 561 |
+
<div style={{width:200,height:4,background:C.border,borderRadius:2,overflow:"hidden"}}>
|
| 562 |
+
<div style={{height:"100%",width:`${(runningDay/CFG.SIM_DAYS)*100}%`,background:C.teal,transition:"width 0.3s",borderRadius:2}}/>
|
| 563 |
+
</div>
|
| 564 |
+
)}
|
| 565 |
+
</div>
|
| 566 |
+
<div style={{display:"flex",gap:8}}>
|
| 567 |
+
{phase==="running"&&<button onClick={stopSim} style={{background:"#2a0f0f",border:`1px solid ${C.red}40`,borderRadius:6,padding:"6px 14px",color:C.red,fontFamily:"inherit",fontSize:11,cursor:"pointer"}}>β STOP</button>}
|
| 568 |
+
<button onClick={()=>{setPhase("config");setAgentLog([]);setSimTimeline([]);setBaselineResults({});setAgentMetrics(null);}} style={{background:C.panel,border:`1px solid ${C.border}`,borderRadius:6,padding:"6px 14px",color:C.muted,fontFamily:"inherit",fontSize:11,cursor:"pointer"}}>βΊ RESET</button>
|
| 569 |
+
</div>
|
| 570 |
+
</div>
|
| 571 |
+
|
| 572 |
+
{/* Tabs */}
|
| 573 |
+
<div style={{display:"flex",gap:6,marginBottom:14,flexWrap:"wrap"}}>
|
| 574 |
+
<Tab id="live" label="LIVE SIM"/>
|
| 575 |
+
<Tab id="reasoning" label={`AGENT REASONING (${agentLog.length})`}/>
|
| 576 |
+
<Tab id="compare" label="COMPARE AGENTS"/>
|
| 577 |
+
<Tab id="memory" label={`MEMORY BANK (${memoryBank.length})`}/>
|
| 578 |
+
</div>
|
| 579 |
+
|
| 580 |
+
{/* ββ TAB: LIVE SIM ββ */}
|
| 581 |
+
{activeTab==="live"&&(
|
| 582 |
+
<div style={{display:"flex",flexDirection:"column",gap:14}}>
|
| 583 |
+
<Panel title="Inventory Β· Demand Β· Reorder Point">
|
| 584 |
+
<ResponsiveContainer width="100%" height={200}>
|
| 585 |
+
<AreaChart data={simTimeline} margin={{top:4,right:4,bottom:0,left:0}}>
|
| 586 |
+
<defs>
|
| 587 |
+
<linearGradient id="ig" x1="0" y1="0" x2="0" y2="1">
|
| 588 |
+
<stop offset="5%" stopColor={C.blue} stopOpacity={0.25}/>
|
| 589 |
+
<stop offset="95%" stopColor={C.blue} stopOpacity={0}/>
|
| 590 |
+
</linearGradient>
|
| 591 |
+
</defs>
|
| 592 |
+
<XAxis dataKey="day" tick={{fontSize:9,fill:C.muted}}/>
|
| 593 |
+
<YAxis tick={{fontSize:9,fill:C.muted}} width={45}/>
|
| 594 |
+
<Tooltip contentStyle={{background:"#0a0f18",border:`1px solid ${C.border2}`,fontSize:10,borderRadius:6}} labelFormatter={d=>`Day ${d}`}/>
|
| 595 |
+
<Area type="monotone" dataKey="inventory" stroke={C.blue} strokeWidth={1.5} fill="url(#ig)" dot={false} name="Inventory"/>
|
| 596 |
+
<Line type="monotone" dataKey="demand" stroke={C.red} strokeWidth={1} dot={false} name="Demand"/>
|
| 597 |
+
<Line type="monotone" dataKey="rop" stroke={C.amber} strokeWidth={1} strokeDasharray="5 3" dot={false} name="Agent ROP"/>
|
| 598 |
+
</AreaChart>
|
| 599 |
+
</ResponsiveContainer>
|
| 600 |
+
</Panel>
|
| 601 |
+
<div style={{display:"grid",gridTemplateColumns:"1fr 1fr",gap:14}}>
|
| 602 |
+
<Panel title="Cumulative Fill Rate">
|
| 603 |
+
<ResponsiveContainer width="100%" height={130}>
|
| 604 |
+
<LineChart data={simTimeline} margin={{top:4,right:4,bottom:0,left:0}}>
|
| 605 |
+
<XAxis dataKey="day" tick={{fontSize:9,fill:C.muted}}/>
|
| 606 |
+
<YAxis domain={[0,1]} tickFormatter={v=>`${(v*100).toFixed(0)}%`} tick={{fontSize:9,fill:C.muted}} width={38}/>
|
| 607 |
+
<ReferenceLine y={0.95} stroke={C.amber} strokeDasharray="4 3"/>
|
| 608 |
+
<Tooltip contentStyle={{background:"#0a0f18",border:`1px solid ${C.border2}`,fontSize:10}} formatter={v=>`${(v*100).toFixed(1)}%`}/>
|
| 609 |
+
<Line type="monotone" dataKey="fillRateCum" stroke={C.teal} strokeWidth={2} dot={false} name="Fill Rate"/>
|
| 610 |
+
</LineChart>
|
| 611 |
+
</ResponsiveContainer>
|
| 612 |
+
</Panel>
|
| 613 |
+
<Panel title="Lost Sales Per Day">
|
| 614 |
+
<ResponsiveContainer width="100%" height={130}>
|
| 615 |
+
<BarChart data={simTimeline} barSize={2} margin={{top:4,right:4,bottom:0,left:0}}>
|
| 616 |
+
<XAxis dataKey="day" tick={{fontSize:9,fill:C.muted}}/>
|
| 617 |
+
<YAxis tick={{fontSize:9,fill:C.muted}} width={38}/>
|
| 618 |
+
<Tooltip contentStyle={{background:"#0a0f18",border:`1px solid ${C.border2}`,fontSize:10}}/>
|
| 619 |
+
<Bar dataKey="lost" fill={C.red} opacity={0.8} name="Lost Sales"/>
|
| 620 |
+
</BarChart>
|
| 621 |
+
</ResponsiveContainer>
|
| 622 |
+
</Panel>
|
| 623 |
+
</div>
|
| 624 |
+
</div>
|
| 625 |
+
)}
|
| 626 |
+
|
| 627 |
+
{/* ββ TAB: AGENT REASONING ββ */}
|
| 628 |
+
{activeTab==="reasoning"&&(
|
| 629 |
+
<div style={{display:"flex",flexDirection:"column",gap:10,maxHeight:"72vh",overflowY:"auto",paddingRight:4}}>
|
| 630 |
+
{agentLog.length===0&&<div style={{color:C.muted,fontSize:12,padding:20,textAlign:"center"}}>Waiting for first LLM decision (after day {CFG.HISTO_DAYS})...</div>}
|
| 631 |
+
{agentLog.map((entry,i)=>{
|
| 632 |
+
const d=entry.decision;
|
| 633 |
+
const isLatest=i===agentLog.length-1;
|
| 634 |
+
return(
|
| 635 |
+
<div key={i} style={{
|
| 636 |
+
background:isLatest?"#0c1a24":C.panel,
|
| 637 |
+
border:`1px solid ${isLatest?C.teal+"40":C.border}`,
|
| 638 |
+
borderRadius:10,padding:"14px 16px",
|
| 639 |
+
borderLeft:`3px solid ${isLatest?C.teal:C.border2}`,
|
| 640 |
+
}}>
|
| 641 |
+
<div style={{display:"flex",justifyContent:"space-between",alignItems:"center",marginBottom:10,flexWrap:"wrap",gap:6}}>
|
| 642 |
+
<div style={{fontSize:11,color:C.teal,fontWeight:600}}>Day {entry.day} β Decision #{i+1}</div>
|
| 643 |
+
<div style={{display:"flex",gap:8,flexWrap:"wrap"}}>
|
| 644 |
+
<span style={{fontSize:10,color:C.muted}}>ROP: <span style={{color:C.amber,fontWeight:600}}>{Math.round(entry.rop)}</span></span>
|
| 645 |
+
<span style={{fontSize:10,color:C.muted}}>Fill: <FillBadge rate={entry.fillRateCum}/></span>
|
| 646 |
+
<span style={{fontSize:9,padding:"2px 7px",borderRadius:3,
|
| 647 |
+
background:d.confidence==="high"?"#0d1f18":d.confidence==="medium"?"#1f1a0d":"#1f0d0d",
|
| 648 |
+
color:d.confidence==="high"?C.green:d.confidence==="medium"?C.amber:C.red,
|
| 649 |
+
border:`1px solid currentColor`,opacity:0.8,
|
| 650 |
+
}}>{d.confidence?.toUpperCase()||"?"}</span>
|
| 651 |
+
</div>
|
| 652 |
+
</div>
|
| 653 |
+
|
| 654 |
+
{/* Subgoals */}
|
| 655 |
+
{d.subgoals?.length>0&&(
|
| 656 |
+
<div style={{marginBottom:10}}>
|
| 657 |
+
<div style={{fontSize:9,letterSpacing:3,color:C.muted,marginBottom:6}}>SUBGOAL DECOMPOSITION</div>
|
| 658 |
+
<div style={{display:"flex",gap:6,flexWrap:"wrap"}}>
|
| 659 |
+
{d.subgoals.map((sg,j)=>(
|
| 660 |
+
<div key={j} style={{fontSize:10,background:C.dim,border:`1px solid ${C.border2}`,borderRadius:4,padding:"4px 9px",color:C.blue}}>
|
| 661 |
+
{j+1}. {sg}
|
| 662 |
+
</div>
|
| 663 |
+
))}
|
| 664 |
+
</div>
|
| 665 |
+
</div>
|
| 666 |
+
)}
|
| 667 |
+
|
| 668 |
+
{/* State analysis */}
|
| 669 |
+
<div style={{marginBottom:8}}>
|
| 670 |
+
<div style={{fontSize:9,letterSpacing:3,color:C.muted,marginBottom:5}}>STATE ANALYSIS</div>
|
| 671 |
+
<div style={{fontSize:11,color:C.text,lineHeight:1.7,background:C.dim,borderRadius:6,padding:"8px 10px"}}>{d.state_analysis}</div>
|
| 672 |
+
</div>
|
| 673 |
+
|
| 674 |
+
{/* Recovery */}
|
| 675 |
+
{d.recovery_plan&&d.recovery_plan!=="N/A"&&(
|
| 676 |
+
<div style={{marginBottom:8}}>
|
| 677 |
+
<div style={{fontSize:9,letterSpacing:3,color:C.muted,marginBottom:5}}>RECOVERY PLAN</div>
|
| 678 |
+
<div style={{fontSize:11,color:C.amber,lineHeight:1.6,background:"#1a1400",borderRadius:6,padding:"8px 10px",border:`1px solid ${C.amber}20`}}>{d.recovery_plan}</div>
|
| 679 |
+
</div>
|
| 680 |
+
)}
|
| 681 |
+
|
| 682 |
+
{/* Reasoning depth */}
|
| 683 |
+
{d.reasoning_depth&&(
|
| 684 |
+
<div style={{fontSize:10,color:C.muted,marginTop:6}}>
|
| 685 |
+
<span style={{color:C.purple}}>Reasoning: </span>{d.reasoning_depth}
|
| 686 |
+
</div>
|
| 687 |
+
)}
|
| 688 |
+
</div>
|
| 689 |
+
);
|
| 690 |
+
})}
|
| 691 |
+
<div ref={logEndRef}/>
|
| 692 |
+
</div>
|
| 693 |
+
)}
|
| 694 |
+
|
| 695 |
+
{/* ββ TAB: COMPARE ββ */}
|
| 696 |
+
{activeTab==="compare"&&(
|
| 697 |
+
<div style={{display:"flex",flexDirection:"column",gap:14}}>
|
| 698 |
+
{/* Scorecard */}
|
| 699 |
+
<div style={{display:"grid",gridTemplateColumns:"repeat(5,1fr)",gap:10}}>
|
| 700 |
+
{/* Agent */}
|
| 701 |
+
{agentMetrics&&(
|
| 702 |
+
<div style={{background:"#0a1e18",border:`2px solid ${C.teal}40`,borderRadius:10,padding:"14px",gridColumn:"1"}}>
|
| 703 |
+
<div style={{fontSize:9,color:C.teal,letterSpacing:3,marginBottom:8}}>π€ LLM AGENT</div>
|
| 704 |
+
{[["Fill Rate",<FillBadge rate={agentMetrics.fillRate}/>],["Stockouts",agentMetrics.stockOuts],["Lost Sales",agentMetrics.lostSales.toLocaleString()],["Write-Offs",agentMetrics.totWriteOff.toLocaleString()]].map(([l,v])=>(
|
| 705 |
+
<div key={l} style={{display:"flex",justifyContent:"space-between",fontSize:11,marginBottom:5}}>
|
| 706 |
+
<span style={{color:C.muted}}>{l}</span><span style={{fontWeight:600}}>{v}</span>
|
| 707 |
+
</div>
|
| 708 |
+
))}
|
| 709 |
+
</div>
|
| 710 |
+
)}
|
| 711 |
+
{/* Baselines */}
|
| 712 |
+
{Object.entries(baselineResults).map(([bk,br])=>(
|
| 713 |
+
<div key={bk} style={{background:C.panel,border:`1px solid ${BASELINES[bk].color}30`,borderRadius:10,padding:"14px"}}>
|
| 714 |
+
<div style={{fontSize:9,color:BASELINES[bk].color,letterSpacing:3,marginBottom:8}}>{BASELINES[bk].label.toUpperCase()}</div>
|
| 715 |
+
{[["Fill Rate",<FillBadge rate={br.metrics.fillRate}/>],["Stockouts",br.metrics.stockOuts],["Lost Sales",br.metrics.lostSales.toLocaleString()],["Write-Offs",br.metrics.totWriteOff.toLocaleString()]].map(([l,v])=>(
|
| 716 |
+
<div key={l} style={{display:"flex",justifyContent:"space-between",fontSize:11,marginBottom:5}}>
|
| 717 |
+
<span style={{color:C.muted}}>{l}</span><span style={{fontWeight:600}}>{v}</span>
|
| 718 |
+
</div>
|
| 719 |
+
))}
|
| 720 |
+
</div>
|
| 721 |
+
))}
|
| 722 |
+
</div>
|
| 723 |
+
|
| 724 |
+
{/* Fill rate comparison chart */}
|
| 725 |
+
{Object.keys(baselineResults).length>0&&(
|
| 726 |
+
<Panel title="Fill Rate Convergence β Agent vs All Baselines">
|
| 727 |
+
<div style={{fontSize:10,color:C.muted,marginBottom:10}}>
|
| 728 |
+
Dashed line = 95% target. The LLM agent ({C.teal}) must beat baselines through structured reasoning, not hard-coded rules.
|
| 729 |
+
</div>
|
| 730 |
+
<ResponsiveContainer width="100%" height={220}>
|
| 731 |
+
<LineChart margin={{top:4,right:8,bottom:0,left:0}}>
|
| 732 |
+
<XAxis dataKey="day" type="number" domain={[0,CFG.SIM_DAYS]} tick={{fontSize:9,fill:C.muted}}/>
|
| 733 |
+
<YAxis domain={[0,1]} tickFormatter={v=>`${(v*100).toFixed(0)}%`} tick={{fontSize:9,fill:C.muted}} width={40}/>
|
| 734 |
+
<ReferenceLine y={0.95} stroke={C.amber} strokeDasharray="5 3" label={{value:"95% target",fontSize:9,fill:C.amber}}/>
|
| 735 |
+
<Tooltip contentStyle={{background:"#0a0f18",border:`1px solid ${C.border2}`,fontSize:10}} formatter={v=>`${(v*100).toFixed(1)}%`}/>
|
| 736 |
+
<Legend wrapperStyle={{fontSize:10}}/>
|
| 737 |
+
{/* Agent line */}
|
| 738 |
+
<Line data={agentTimelineFillRates} type="monotone" dataKey="agent" stroke={C.teal} strokeWidth={2.5} dot={false} name="LLM Agent"/>
|
| 739 |
+
{/* Baselines */}
|
| 740 |
+
{Object.entries(baselineResults).map(([bk,br])=>(
|
| 741 |
+
<Line key={bk} data={br.timeline.map(t=>({day:t.day,fillRate:t.fillRateCum}))}
|
| 742 |
+
type="monotone" dataKey="fillRate" stroke={BASELINES[bk].color} strokeWidth={1}
|
| 743 |
+
strokeDasharray="3 2" dot={false} name={BASELINES[bk].label}/>
|
| 744 |
+
))}
|
| 745 |
+
</LineChart>
|
| 746 |
+
</ResponsiveContainer>
|
| 747 |
+
</Panel>
|
| 748 |
+
)}
|
| 749 |
+
|
| 750 |
+
{/* ROP decisions overlay */}
|
| 751 |
+
{agentLog.length>0&&(
|
| 752 |
+
<Panel title="Agent Reorder Point Over Time vs Demand Distribution">
|
| 753 |
+
<ResponsiveContainer width="100%" height={160}>
|
| 754 |
+
<AreaChart data={simTimeline} margin={{top:4,right:4,bottom:0,left:0}}>
|
| 755 |
+
<defs>
|
| 756 |
+
<linearGradient id="dg" x1="0" y1="0" x2="0" y2="1">
|
| 757 |
+
<stop offset="5%" stopColor={C.red} stopOpacity={0.15}/>
|
| 758 |
+
<stop offset="95%" stopColor={C.red} stopOpacity={0}/>
|
| 759 |
+
</linearGradient>
|
| 760 |
+
</defs>
|
| 761 |
+
<XAxis dataKey="day" tick={{fontSize:9,fill:C.muted}}/>
|
| 762 |
+
<YAxis tick={{fontSize:9,fill:C.muted}} width={45}/>
|
| 763 |
+
<Tooltip contentStyle={{background:"#0a0f18",border:`1px solid ${C.border2}`,fontSize:10}}/>
|
| 764 |
+
<Area type="monotone" dataKey="demand" stroke={C.red} strokeWidth={1} fill="url(#dg)" dot={false} name="Demand"/>
|
| 765 |
+
<Line type="monotone" dataKey="rop" stroke={C.amber} strokeWidth={2} dot={false} name="Agent ROP"/>
|
| 766 |
+
</AreaChart>
|
| 767 |
+
</ResponsiveContainer>
|
| 768 |
+
</Panel>
|
| 769 |
+
)}
|
| 770 |
+
</div>
|
| 771 |
+
)}
|
| 772 |
+
|
| 773 |
+
{/* ββ TAB: MEMORY BANK ββ */}
|
| 774 |
+
{activeTab==="memory"&&(
|
| 775 |
+
<div style={{display:"flex",flexDirection:"column",gap:10}}>
|
| 776 |
+
<Panel>
|
| 777 |
+
<div style={{fontSize:11,color:C.muted,lineHeight:1.8,marginBottom:12}}>
|
| 778 |
+
The memory bank is a compressed rolling state passed to the LLM on every decision turn. It enables the agent to reason beyond its context window β tracking performance trends, past ROP decisions, and emerging patterns across the full 120-day horizon.
|
| 779 |
+
</div>
|
| 780 |
+
<div style={{display:"grid",gridTemplateColumns:"repeat(auto-fill,minmax(200px,1fr))",gap:8}}>
|
| 781 |
+
{memoryBank.map((m,i)=>(
|
| 782 |
+
<div key={i} style={{background:C.dim,border:`1px solid ${C.border}`,borderRadius:7,padding:"10px 12px"}}>
|
| 783 |
+
<div style={{fontSize:10,color:C.teal,fontWeight:600,marginBottom:6}}>Day {m.day}</div>
|
| 784 |
+
{[
|
| 785 |
+
["ROP Set",m.rop],
|
| 786 |
+
["Confidence",m.confidence],
|
| 787 |
+
["Fill Rate",m.fill_rate?(m.fill_rate+"%"):"β"],
|
| 788 |
+
["Stockouts",m.stockouts_in_window],
|
| 789 |
+
].map(([l,v])=>(
|
| 790 |
+
<div key={l} style={{display:"flex",justifyContent:"space-between",fontSize:10,marginBottom:4}}>
|
| 791 |
+
<span style={{color:C.muted}}>{l}</span>
|
| 792 |
+
<span style={{color:C.text}}>{v}</span>
|
| 793 |
+
</div>
|
| 794 |
+
))}
|
| 795 |
+
<div style={{fontSize:9,color:C.muted,marginTop:6,lineHeight:1.5,borderTop:`1px solid ${C.border}`,paddingTop:5}}>
|
| 796 |
+
{m.key_insight}
|
| 797 |
+
</div>
|
| 798 |
+
</div>
|
| 799 |
+
))}
|
| 800 |
+
{memoryBank.length===0&&<div style={{color:C.muted,fontSize:11}}>Memory builds as agent makes decisions...</div>}
|
| 801 |
+
</div>
|
| 802 |
+
</Panel>
|
| 803 |
+
</div>
|
| 804 |
+
)}
|
| 805 |
+
</>
|
| 806 |
+
)}
|
| 807 |
+
|
| 808 |
+
{/* ββ FOOTER ββ */}
|
| 809 |
+
<div style={{marginTop:28,paddingTop:16,borderTop:`1px solid ${C.border}`,display:"grid",gridTemplateColumns:"repeat(4,1fr)",gap:12,fontSize:10,color:C.dim}}>
|
| 810 |
+
{[
|
| 811 |
+
["Environment","Stochastic inventory simulation with 4 demand regimes (Gamma-Poisson, Bimodal HV, Spiking, Stable Gamma). Mirrors real supply-chain uncertainty."],
|
| 812 |
+
["Agent Architecture","Claude Sonnet 4 called every 5 simulation days. Rolling 6-turn conversation + compressed memory bank enables reasoning beyond context window."],
|
| 813 |
+
["Reward Structure","Sparse: fill rate signal only meaningful after 50+ days. Agent must plan across 120-day horizon with no per-step guidance."],
|
| 814 |
+
["Benchmarking","LLM agent compared against 4 rule-based baselines: Base, Safety Stock, Oracle Forecast, Monte Carlo β all from the original Python codebase."],
|
| 815 |
+
].map(([t,d])=>(
|
| 816 |
+
<div key={t}>
|
| 817 |
+
<div style={{color:C.muted,fontWeight:600,marginBottom:4,fontSize:9,letterSpacing:2}}>{t.toUpperCase()}</div>
|
| 818 |
+
<div style={{lineHeight:1.7}}>{d}</div>
|
| 819 |
+
</div>
|
| 820 |
+
))}
|
| 821 |
+
</div>
|
| 822 |
+
</div>
|
| 823 |
+
<style>{`@keyframes pulse{0%,100%{opacity:1}50%{opacity:0.3}}`}</style>
|
| 824 |
+
</div>
|
| 825 |
+
);
|
| 826 |
+
}
|