ademarteau commited on
Commit
9a68962
Β·
1 Parent(s): cb82053

simulator jsx from claude

Browse files
Files changed (1) hide show
  1. 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
+ }