OPENENV_RL_01 / frontend /react /src /hooks /useStorySimulation.js
Siddharaj Shirke
deploy: fresh snapshot to Hugging Face Space
3eae4cc
import { useState, useRef, useCallback, useEffect } from "react";
import { api } from "../api/client";
// ─────────────────────────────────────────────────────────────────────────────
// Narrative translator: maps raw action → human-readable cause→effect story
// ─────────────────────────────────────────────────────────────────────────────
function mapActionToStory(actionType, payload, reward, backlogDelta, slaDelta, fairnessDelta) {
let title = "Standard Processing Cycle";
let desc = "The system advanced one cycle and continued normal queue processing.";
let reason = "No override was required, so routine processing continued.";
let icon = "schedule";
let type = reward > 0 ? "success" : "info";
const changes = [];
if (backlogDelta < 0) changes.push(`backlog improved by ${Math.abs(backlogDelta)} case(s)`);
else if (backlogDelta > 0) changes.push(`backlog increased by ${backlogDelta} case(s)`);
else changes.push("backlog stayed stable");
if (slaDelta > 0) changes.push(`${slaDelta} new SLA breach(es) occurred`);
else if (slaDelta < 0) changes.push(`${Math.abs(slaDelta)} SLA breach(es) recovered`);
if (Number.isFinite(Number(fairnessDelta)) && Number(fairnessDelta) !== 0) {
const v = Number(fairnessDelta);
changes.push(`fairness gap ${v > 0 ? "worsened" : "improved"} by ${Math.abs(v).toFixed(3)}`);
}
const effectClause = `${changes.join(", ")}.`;
if (slaDelta > 0) type = "error";
switch (actionType) {
case "assign_capacity":
title = "Capacity Assigned";
desc = `Officers were assigned to '${payload.service_target ?? payload.service ?? "target queue"}'; ${effectClause}`;
reason = "The agent detected staffing pressure and increased capacity where it could reduce delay.";
icon = "group_add";
break;
case "reallocate_officers":
title = "Staff Reallocated";
desc = `Officers were reallocated toward higher-pressure services; ${effectClause}`;
reason = `The agent shifted staffing to reduce bottlenecks in '${payload.service_target ?? "priority"}' services.`;
icon = "compare_arrows";
break;
case "request_missing_documents":
title = "Documents Requested";
desc = `Missing documents were requested to unblock pending files; ${effectClause}`;
reason = "The agent prioritized document blockers to avoid queue stagnation.";
icon = "rule_folder";
type = type !== "error" ? "success" : type;
break;
case "escalate_service":
title = "Service Escalated";
desc = `At-risk services were escalated for faster handling; ${effectClause}`;
reason = "Escalation was used to protect SLA-critical cases.";
icon = "warning";
type = "warning";
break;
case "set_priority_mode":
title = "Priority Mode Updated";
desc = `Priority mode switched to '${payload.priority_mode ?? "balanced"}'; ${effectClause}`;
reason = "The agent changed queue strategy to better match current workload pressure.";
icon = "model_training";
break;
default:
desc = `Routine processing executed; ${effectClause}`;
break;
}
if (reward < 0 && type === "info") type = "warning";
const isHighReward = reward >= 1.0;
const isHugeImpact = backlogDelta <= -5;
return { title, desc, reason, icon, type, isHighReward, isHugeImpact };
}
// Determines the simulation phase label from step index and total
function getPhase(step, maxSteps) {
const pct = step / Math.max(maxSteps, 1);
if (pct < 0.33) return "early";
if (pct < 0.67) return "middle";
return "late";
}
// Detect if a step is a "key decision" turning point
function isKeyDecision(s, backlogDelta) {
return (
Math.abs(Number(s.reward)) >= 1.0 || // high reward magnitude
(backlogDelta !== 0 && Math.abs(backlogDelta) >= 5) || // large backlog swing
Boolean(s.invalid_action) // failed action = notable event
);
}
// ─────────────────────────────────────────────────────────────────────────────
// Hook
// ─────────────────────────────────────────────────────────────────────────────
export function useStorySimulation({ defaultTask }) {
const [taskId, setTaskId] = useState(defaultTask || "district_backlog_easy");
const [maxSteps, setMaxSteps] = useState(40);
const [agentMode, setAgentMode] = useState("trained_rl");
const [policyName, setPolicyName] = useState("backlog_clearance");
const [modelPath, setModelPath] = useState("");
const [modelType, setModelType] = useState("maskable");
const [availablePolicies, setAvailablePolicies] = useState([]);
const [availableModels, setAvailableModels] = useState([]);
const [configError, setConfigError] = useState("");
const [running, setRunning] = useState(false);
const [starting, setStarting] = useState(false);
const [runId, setRunId] = useState("");
const [kpis, setKpis] = useState({
backlog: 0, backlogDelta: 0,
slaBreaches: 0, slaDelta: 0,
fairness: 0, fairnessDelta: 0,
});
const [timeline, setTimeline] = useState([]);
const [resources, setResources] = useState([]);
// Progress tracking
const [currentStep, setCurrentStep] = useState(0);
// Before vs after journey stats
const [journeyStats, setJourneyStats] = useState(null); // null = not yet done
// Internal refs
const lastState = useRef({ backlog: 0, sla: 0, fairness: 0 });
const initialSnapshot = useRef(null); // captured on first real step
const stepCount = useRef(0);
const maxStepsRef = useRef(40);
useEffect(() => {
let mounted = true;
(async () => {
try {
const [policiesRes, modelsV1Res, modelsV2Res] = await Promise.allSettled([
api("/agents"),
api("/rl_models"),
api("/rl/models"),
]);
if (!mounted) return;
const policyRows = policiesRes.status === "fulfilled" && Array.isArray(policiesRes.value) ? policiesRes.value : [];
setAvailablePolicies(policyRows);
if (policyRows.length > 0 && !policyRows.includes(policyName)) {
setPolicyName(policyRows[0]);
}
const modelRowsV1 = modelsV1Res.status === "fulfilled" && Array.isArray(modelsV1Res.value?.models)
? modelsV1Res.value.models
: [];
const modelRowsV2 = modelsV2Res.status === "fulfilled" && Array.isArray(modelsV2Res.value)
? modelsV2Res.value.map((row) => ({
label: row?.model_path ? String(row.model_path).split(/[\\/]/).pop() : "model",
path: row?.model_path ? (String(row.model_path).toLowerCase().endsWith(".zip") ? row.model_path : `${row.model_path}.zip`) : "",
exists: Boolean(row?.exists),
model_type: "maskable",
}))
: [];
const dedupe = new Map();
for (const m of [...modelRowsV1, ...modelRowsV2]) {
const key = String(m?.path || "").replace(/\\/g, "/").toLowerCase();
if (!key || dedupe.has(key)) continue;
dedupe.set(key, m);
}
const existingModels = Array.from(dedupe.values()).filter((m) => Boolean(m?.exists));
setAvailableModels(existingModels);
const preferred =
existingModels.find((m) => String(m.path || "").toLowerCase().includes("phase2_final")) ||
existingModels[0];
if (preferred?.path) {
setModelPath(preferred.path);
setModelType(preferred.model_type || "maskable");
setAgentMode((prev) => (prev === "baseline_policy" ? "trained_rl" : prev));
}
} catch (err) {
if (!mounted) return;
setConfigError(err?.message || "Failed to load simulation options.");
}
})();
return () => {
mounted = false;
};
}, []);
const startSimulation = async () => {
setStarting(true);
setConfigError("");
setJourneyStats(null);
setCurrentStep(0);
initialSnapshot.current = null;
stepCount.current = 0;
maxStepsRef.current = maxSteps;
try {
const payload = {
task_id: taskId,
agent_mode: agentMode,
max_steps: maxSteps,
policy_name: policyName,
model_path: modelPath || null,
model_type: modelType,
};
const started = await api("/simulation/live/start", {
method: "POST",
body: JSON.stringify(payload),
});
setRunId(started.run_id);
setTimeline([{
id: "start",
time: "Step 0",
title: "Simulation Initialized",
desc: `Scenario locked: ${taskId.replace(/_/g, " ")}. Agent mode '${agentMode}' engaged — agent begins resolving backlog.`,
impact: 0,
type: "info",
icon: "rocket_launch",
phase: "early",
key: false,
}]);
setResources([]);
lastState.current = { backlog: 0, sla: 0, fairness: 0 };
setRunning(true);
} catch (err) {
console.error("Start failed:", err);
setTimeline([{
id: "error",
time: "—",
title: "Initialization Failed",
desc: `Backend error: ${err.message || "Cannot start simulation."}`,
impact: 0,
type: "error",
icon: "error",
phase: "early",
key: false,
}]);
setConfigError(err?.message || "Cannot start simulation.");
} finally {
setStarting(false);
}
};
const stopSimulation = async () => {
if (!runId) return;
try {
await api(`/simulation/live/${runId}/stop`, { method: "POST" });
} catch (err) {
console.error(err);
} finally {
setRunning(false);
}
};
// Polling loop — runs while running=true
const runLoop = useCallback(async (rid, cancelled) => {
if (cancelled.v) return;
try {
const res = await api("/simulation/live/step", {
method: "POST",
body: JSON.stringify({ run_id: rid }),
});
if (cancelled.v) return;
if (res.step) {
const s = res.step;
stepCount.current += 1;
const stepNum = Number(s.step ?? stepCount.current);
setCurrentStep(stepNum);
const currentBacklog = Number(s.backlog ?? 0);
const currentSla = Number(s.sla_breaches ?? 0);
const currentFairness = Number(s.fairness_gap ?? 0);
// Capture initial snapshot from step 1
if (initialSnapshot.current === null) {
initialSnapshot.current = {
backlog: currentBacklog,
sla: currentSla,
fairness: currentFairness,
};
}
const backlogDelta = currentBacklog - lastState.current.backlog;
const slaDelta = currentSla - lastState.current.sla;
const fairnessDelta = currentFairness - lastState.current.fairness;
setKpis({
backlog: currentBacklog,
backlogDelta,
slaBreaches: currentSla,
slaDelta,
fairness: currentFairness,
fairnessDelta,
});
lastState.current = { backlog: currentBacklog, sla: currentSla, fairness: currentFairness };
const payload = typeof s.action_payload === "string"
? (() => { try { return JSON.parse(s.action_payload); } catch { return {}; } })()
: (s.action_payload || {});
const story = mapActionToStory(
s.action_type || "advance_time",
payload,
Number(s.reward),
backlogDelta,
slaDelta,
fairnessDelta
);
const phase = getPhase(stepNum, maxStepsRef.current);
const key = isKeyDecision(s, backlogDelta);
const improvesBacklog = backlogDelta < 0;
const worsensBacklog = backlogDelta > 0;
const worsensSla = slaDelta > 0;
const improvesSla = slaDelta < 0;
const outcomeLabel = improvesBacklog || improvesSla
? "Improvement"
: worsensBacklog || worsensSla
? "Degradation"
: "Stable";
const outcomeType = outcomeLabel === "Improvement" ? "success" : outcomeLabel === "Degradation" ? "warning" : "info";
const newEvent = {
id: `step-${stepNum}`,
time: `Step ${stepNum}`,
title: s.invalid_action ? "Action Blocked" : story.title,
desc: s.invalid_action
? "This action was blocked by environment constraints; the agent adapts on the next step."
: story.desc,
reason: s.invalid_action ? "The attempted operation violated environment constraints (e.g. over-assignment)." : story.reason,
impact: Number(s.reward),
type: s.invalid_action ? "error" : story.type,
icon: s.invalid_action ? "block" : story.icon,
isHighReward: story.isHighReward && !s.invalid_action,
isHugeImpact: story.isHugeImpact && !s.invalid_action,
phase,
key,
outcomeLabel,
outcomeType,
backlogDelta, // Used for phase summary
};
// Collapse consecutive identical titles (deduplication for repeated events)
setTimeline((prev) => {
const [top, ...rest] = prev;
if (
top &&
top.title === newEvent.title &&
top.phase === newEvent.phase &&
!top.key &&
!newEvent.key
) {
// Merge: bump count, accumulate reward and backlog diff
const merged = {
...top,
id: newEvent.id,
time: `${top.time?.split("–")[0]?.trim()}${newEvent.time}`,
desc: top.desc,
impact: Number(top.impact) + Number(newEvent.impact),
backlogDelta: (top.backlogDelta || 0) + backlogDelta,
_count: (top._count || 1) + 1,
};
return [merged, ...rest].slice(0, 30);
}
return [newEvent, ...prev].slice(0, 30);
});
// Update queue monitors
if (Array.isArray(s.queue_rows) && s.queue_rows.length > 0) {
const maxCases = Math.max(...s.queue_rows.map((q) => q.active_cases ?? 0), 1);
setResources(s.queue_rows.map((q) => ({
name: (q.service ?? q.service_type ?? "unknown").replace(/_/g, " ").toUpperCase(),
activeCases: q.active_cases ?? 0,
percentage: Math.min(100, Math.floor(((q.active_cases ?? 0) / maxCases) * 100)),
})));
}
}
// Episode done
if (res.done || res.step?.done) {
const finalBacklog = lastState.current.backlog;
const initSnap = initialSnapshot.current ?? { backlog: finalBacklog, sla: 0, fairness: 0 };
const backlogImprovement = initSnap.backlog > 0
? Math.round(((initSnap.backlog - finalBacklog) / initSnap.backlog) * 100)
: 0;
setJourneyStats({
initialBacklog: initSnap.backlog,
finalBacklog,
backlogImprovement,
initialSla: initSnap.sla,
finalSla: lastState.current.sla,
totalSteps: stepCount.current,
finalScore: res.score ?? null,
totalReward: res.total_reward ?? null,
});
setTimeline((prev) => [{
id: "end",
time: "Final",
title: "Episode Complete",
desc: `Resolution finished in ${stepCount.current} steps. Final score: ${res.score != null ? (res.score * 100).toFixed(1) + "%" : "N/A"}. Backlog ${finalBacklog < initSnap.backlog ? "reduced" : "unchanged"} — SLAs verified.`,
impact: res.total_reward ?? 0,
type: "success",
icon: "verified",
phase: "late",
key: true,
}, ...prev]);
setRunning(false);
return;
}
setTimeout(() => runLoop(rid, cancelled), 1000);
} catch (err) {
if (!cancelled.v) {
setRunning(false);
setTimeline((prev) => [{
id: `error-${Date.now()}`,
time: "Halted",
title: "System Error Detected",
desc: `Backend synchronization failed: ${err.message}`,
impact: 0,
type: "error",
icon: "warning",
phase: "late",
key: false,
}, ...prev]);
}
}
}, []);
// Start/stop the polling loop reactively
const cancelRef = useRef({ v: false });
useEffect(() => {
if (!running || !runId) {
cancelRef.current.v = true;
return undefined;
}
cancelRef.current = { v: false };
const boot = setTimeout(() => {
if (!cancelRef.current.v) {
runLoop(runId, cancelRef.current);
}
}, 100);
return () => {
clearTimeout(boot);
cancelRef.current.v = true;
};
}, [running, runId, runLoop]);
return {
taskId, setTaskId,
maxSteps, setMaxSteps,
agentMode, setAgentMode,
policyName, setPolicyName,
modelPath, setModelPath,
modelType, setModelType,
availablePolicies,
availableModels,
configError,
running, starting,
currentStep,
kpis, timeline, resources,
journeyStats,
startSimulation, stopSimulation,
};
}