TheJackBright's picture
Deploy GitHub root master to Space
c296d62
import { useCallback, useEffect, useMemo, useState } from "react";
import type { CSSProperties, Dispatch, SetStateAction } from "react";
import {
closeEnvSocket,
envWsSend,
fetchCatalog,
fetchModelStatus,
fetchRewardBreakdown,
orchestrateStep,
resetEnv,
stepCandidate,
} from "./lib/api";
import type {
CandidateAction,
EnvCatalog,
EnvObservation,
EnvStepPacket,
ModelStatus,
PolyGuardActionPayload,
StepResponse,
TaskPreset,
} from "./lib/types";
import AlternativeMedicineSearch from "./components/AlternativeMedicineSearch";
import MetaverseBackdrop from "./components/MetaverseBackdrop";
type WorkbenchMode = "agent" | "env";
type GuideTarget =
| "topbar"
| "mode"
| "task"
| "model"
| "overview"
| "candidates"
| "console"
| "rewards"
| "medications"
| "history"
| "event-log";
type GuideStep = {
target: GuideTarget;
title: string;
body: string;
};
const FALLBACK_CATALOG: EnvCatalog = {
reward_range: [0.001, 0.999],
reward_precision: 3,
task_presets: [
{ id: "easy_screening", label: "Easy Screening", difficulty: "easy", sub_environment: "DDI" },
{ id: "budgeted_screening", label: "Budgeted Screening", difficulty: "medium", sub_environment: "REGIMEN_RISK" },
{ id: "complex_tradeoff", label: "Complex Tradeoff", difficulty: "hard", sub_environment: "REGIMEN_RISK" },
{ id: "bandit_mining", label: "Bandit Mining", difficulty: "hard", sub_environment: "BANDIT_MINING" },
],
sub_environments: [
"DDI",
"BANDIT_MINING",
"REGIMEN_RISK",
"PRECISION_DOSING",
"LONGITUDINAL_DEPRESCRIBING",
"WEB_SEARCH_MISSING_DATA",
"ALTERNATIVE_SUGGESTION",
"NEW_DRUG_DECOMPOSITION",
],
};
const REWARD_KEYS = [
"total_reward",
"primary_safety_legality",
"primary_clinical_improvement",
"primary_dosing_quality",
"primary_process_integrity",
"legality_score",
"safety_delta_score",
"burden_improvement_score",
"disease_stability_score",
"dosing_quality_score",
"process_fidelity_score",
"explanation_grounding_score",
"anti_cheat_score",
"uncertainty_calibration_score",
];
const QTIPS_SEEN_KEY = "polyguard.qtips.v2.seen";
const GUIDE_STEPS: GuideStep[] = [
{
target: "topbar",
title: "Start here",
body: "PolyGuard is an interactive OpenEnv workbench. Use this top bar to choose the runtime, pick a clinical scenario, and reset into a real environment episode.",
},
{
target: "mode",
title: "Choose the runtime",
body: "Agent Workbench uses the local REST API, candidate selector, reward breakdown, and Qwen-backed policy path. Env Explorer talks directly to the OpenEnv WebSocket service.",
},
{
target: "task",
title: "Pick a scenario",
body: "Choose Easy Screening, Budgeted Screening, Complex Tradeoff, or Bandit Mining. Reset Episode then loads a real patient/regimen state from the backend.",
},
{
target: "model",
title: "Check the model truth",
body: "This panel reports the live model-status endpoint. It only calls Qwen active when the API says Qwen/Qwen2.5-0.5B-Instruct artifacts are enabled and available.",
},
{
target: "overview",
title: "Read the episode state",
body: "After reset, this shows the active task, patient, remaining step budget, latest reward, and risk delta. These values come from the current environment response.",
},
{
target: "candidates",
title: "Review legal actions",
body: "Candidate Actions are the currently legal moves emitted by the environment. Select one to inspect its safety, uncertainty, target drug, and mode.",
},
{
target: "console",
title: "Submit or ask the agent",
body: "Submit Candidate executes the selected legal action. Run Agent lets the policy stack choose a step, so check the model panel first if you require Qwen-backed output.",
},
{
target: "rewards",
title: "Inspect reward channels",
body: "Reward Channels show real scorer output after each step. Empty values mean no step has produced that channel yet, not placeholder scoring.",
},
{
target: "medications",
title: "Track regimen changes",
body: "Medication cards update from the environment observation. High-risk tags and dose/class details help explain why actions are legal or useful.",
},
{
target: "history",
title: "Audit actions and warnings",
body: "Action History and Warnings give a running trace of what happened in the episode. Use this to verify that the workflow is not canned.",
},
{
target: "event-log",
title: "Follow the run",
body: "The Event Log records resets, steps, rewards, and API errors. If Qwen or an env service is unavailable, this is where the UI tells you plainly.",
},
];
function isRecord(value: unknown): value is Record<string, unknown> {
return typeof value === "object" && value !== null && !Array.isArray(value);
}
function toNumber(value: unknown): number | null {
return typeof value === "number" && Number.isFinite(value) ? value : null;
}
function formatReward(value: unknown): string {
const num = toNumber(value);
return num === null ? "-" : num.toFixed(3);
}
function humanize(value: string): string {
return value
.replace(/^primary_/, "")
.replace(/_/g, " ")
.replace(/\b\w/g, (char) => char.toUpperCase());
}
function shortValue(value: unknown): string {
if (value === null || value === undefined || value === "") return "-";
if (typeof value === "number") return Number.isFinite(value) ? value.toFixed(value > 10 ? 0 : 3) : "-";
if (typeof value === "boolean") return value ? "Yes" : "No";
if (Array.isArray(value)) return value.length ? value.map(shortValue).join(", ") : "-";
if (isRecord(value)) return JSON.stringify(value);
return String(value);
}
function taskLabel(taskId: string, presets: TaskPreset[]): string {
return presets.find((item) => item.id === taskId)?.label ?? humanize(taskId);
}
function taskResetOptions(taskId: string, difficulty: string, subEnvironment: string, presets: TaskPreset[]) {
const preset = presets.find((item) => item.id === taskId);
if (preset) {
return {
agent: { task_id: preset.id },
env: { difficulty: preset.difficulty, sub_environment: preset.sub_environment },
};
}
return {
agent: { difficulty, sub_environment: subEnvironment },
env: { difficulty, sub_environment: subEnvironment },
};
}
function defaultCandidateForMode(candidates: CandidateAction[], mode: WorkbenchMode): CandidateAction | null {
if (mode !== "env") return candidates[0] ?? null;
return (
candidates.find(
(candidate) =>
candidate.legality_precheck !== false &&
candidate.action_type !== "KEEP_REGIMEN" &&
!candidate.action_type.startsWith("REQUEST_"),
) ??
candidates.find((candidate) => candidate.legality_precheck !== false && candidate.action_type !== "KEEP_REGIMEN") ??
candidates[0] ??
null
);
}
function modelSignal(status: ModelStatus | null): {
label: string;
detail: string;
isQwen: boolean;
isLive: boolean;
} {
if (!status) {
return {
label: "Model status unavailable",
detail: "The API did not return /policy/model_status. Results can still run, but Qwen cannot be verified here.",
isQwen: false,
isLive: false,
};
}
if (status.ollama?.enabled && status.ollama.available) {
return {
label: "Ollama Qwen active",
detail: `${status.ollama.model || "Ollama model"} is enabled locally; provider order=${(status.provider_preference ?? []).join(" > ") || "ollama > transformers"}.`,
isQwen: /qwen/i.test(status.ollama.model || ""),
isLive: true,
};
}
const modelName = status.model_id || status.base_model || status.runtime_model_name || "";
const isQwen = /Qwen\/Qwen2\.5-0\.5B-Instruct/i.test(modelName);
const available = Object.values(status.availability ?? {}).some(Boolean);
const isLive = Boolean(status.enabled && status.active && available && isQwen);
const artifact = status.loaded_source || status.preferred_artifact || "artifact";
const loadError = status.load_error ? ` Load error: ${status.load_error}` : "";
return {
label: isLive ? "Qwen 0.5B active" : "Qwen not verified",
detail: isLive
? `${modelName} is enabled with ${artifact}; run ${status.run_id || "active manifest"}.${loadError}`
: `${modelName || "No model"}; enabled=${String(status.enabled)} active=${String(status.active)} available=${String(available)}.${loadError}`,
isQwen,
isLive,
};
}
function normalizeStepPacket(packet: EnvStepPacket | StepResponse | Record<string, unknown>): {
observation: EnvObservation | null;
reward: number | null;
done: boolean;
info: Record<string, unknown>;
} {
const observation = isRecord(packet.observation) ? (packet.observation as EnvObservation) : null;
const info = isRecord(packet.info) ? packet.info : {};
return {
observation,
reward: toNumber(packet.reward),
done: Boolean(packet.done),
info,
};
}
function buildActionPayload(
candidate: CandidateAction,
confidence: number,
rationale: string,
): PolyGuardActionPayload {
return {
mode: candidate.mode || "REVIEW",
action_type: candidate.action_type,
target_drug: candidate.target_drug ?? null,
replacement_drug: candidate.replacement_drug ?? null,
dose_bucket: candidate.dose_bucket ?? "NA",
taper_days: candidate.taper_days ?? null,
monitoring_plan: candidate.monitoring_plan ?? null,
evidence_query: candidate.evidence_query ?? null,
new_drug_name: candidate.new_drug_name ?? null,
candidate_components: candidate.candidate_components ?? [],
candidate_id: candidate.candidate_id,
confidence,
rationale_brief: rationale,
};
}
function appendEvent(setter: Dispatch<SetStateAction<string[]>>, message: string) {
setter((prev) => [`${new Date().toLocaleTimeString()} ${message}`, ...prev].slice(0, 24));
}
function QTips({
open,
step,
steps,
onNext,
onPrev,
onClose,
}: {
open: boolean;
step: number;
steps: GuideStep[];
onNext: () => void;
onPrev: () => void;
onClose: () => void;
}) {
const [rect, setRect] = useState<DOMRect | null>(null);
const current = steps[step];
const updateRect = useCallback(() => {
if (!open || !current) return;
const target = document.querySelector(`[data-guide="${current.target}"]`);
if (!target) {
setRect(null);
return;
}
target.scrollIntoView({ block: "nearest", inline: "nearest", behavior: "smooth" });
setRect(target.getBoundingClientRect());
}, [current, open]);
useEffect(() => {
updateRect();
window.addEventListener("resize", updateRect);
window.addEventListener("scroll", updateRect, true);
return () => {
window.removeEventListener("resize", updateRect);
window.removeEventListener("scroll", updateRect, true);
};
}, [updateRect]);
if (!open || !current) return null;
const tooltipStyle = rect
? ({
"--tip-top": `${Math.max(14, Math.min(window.innerHeight - 260, rect.bottom + 12))}px`,
"--tip-left": `${Math.max(14, Math.min(window.innerWidth - 390, rect.left))}px`,
} as CSSProperties)
: undefined;
return (
<div className="qtip-overlay" role="dialog" aria-modal="true" aria-label="Q Tips walkthrough">
<div className="qtip-dim" onClick={onClose} />
{rect && (
<div
className="qtip-ring"
style={{
top: rect.top - 6,
left: rect.left - 6,
width: rect.width + 12,
height: rect.height + 12,
}}
/>
)}
<section className="qtip-card panel-surface" style={tooltipStyle}>
<div className="qtip-header">
<span>Q Tips</span>
<strong>
{step + 1} / {steps.length}
</strong>
</div>
<h2>{current.title}</h2>
<p>{current.body}</p>
<div className="qtip-actions">
<button className="secondary" onClick={onPrev} disabled={step === 0}>
Back
</button>
<button className="secondary" onClick={onClose}>
Skip
</button>
<button onClick={step === steps.length - 1 ? onClose : onNext}>
{step === steps.length - 1 ? "Done" : "Next"}
</button>
</div>
</section>
</div>
);
}
function TopBar({
mode,
setMode,
taskId,
onTaskChange,
catalog,
statusText,
modelStatus,
loading,
onReset,
onOpenTips,
}: {
mode: WorkbenchMode;
setMode: (mode: WorkbenchMode) => void;
taskId: string;
onTaskChange: (taskId: string) => void;
catalog: EnvCatalog;
statusText: string;
modelStatus: ModelStatus | null;
loading: boolean;
onReset: () => void;
onOpenTips: () => void;
}) {
const signal = modelSignal(modelStatus);
return (
<header className="topbar panel-surface" data-guide="topbar">
<div className="title-wrap">
<h1>PolyGuard</h1>
<p>OpenEnv medication safety workbench</p>
</div>
<div className="mode-toggle" aria-label="Runtime mode" data-guide="mode">
<button className={mode === "agent" ? "active" : ""} onClick={() => setMode("agent")}>
Agent Workbench
</button>
<button className={mode === "env" ? "active" : ""} onClick={() => setMode("env")}>
Env Explorer
</button>
</div>
<div className="topbar-status">
<span className={`status-chip ${statusText === "Live" ? "live" : "idle"}`}>{statusText}</span>
<span className={`status-chip ${signal.isLive ? "live" : "idle"}`}>
{mode === "agent" ? signal.label : "ws env"}
</span>
<button className="qtip-trigger secondary" onClick={onOpenTips}>
Q Tips
</button>
</div>
<div className="topbar-actions" data-guide="task">
<select aria-label="Task" value={taskId} onChange={(event) => onTaskChange(event.target.value)}>
{catalog.task_presets.map((item) => (
<option key={item.id} value={item.id}>
{item.label}
</option>
))}
<option value="advanced">Advanced</option>
</select>
<button onClick={onReset} disabled={loading}>
Reset Episode
</button>
</div>
</header>
);
}
function EpisodeOverview({
mode,
observation,
reward,
done,
taskId,
catalog,
}: {
mode: WorkbenchMode;
observation: EnvObservation | null;
reward: number | null;
done: boolean;
taskId: string;
catalog: EnvCatalog;
}) {
const contract = observation?.deterministic_contract ?? {};
const summary = observation?.patient_summary ?? {};
const burden = observation?.burden_score_summary ?? {};
const kpis: Array<[string, unknown]> = [
["Mode", mode === "agent" ? "Agent Workbench" : "Env Explorer"],
["Task", taskLabel(taskId, catalog.task_presets)],
["Difficulty", contract.difficulty ?? "-"],
["Environment", contract.sub_environment ?? observation?.sub_environment ?? "-"],
["Step Budget", observation?.step_budget_remaining ?? "-"],
["Last Reward", formatReward(reward)],
["Patient", summary.patient_id ?? summary.id ?? "-"],
["Status", done ? "Complete" : observation ? "Live" : "Ready"],
];
return (
<section className="panel-surface panel-wide" data-guide="overview">
<div className="panel-heading">
<h2>Episode Overview</h2>
<span>{observation ? "Live" : "Ready"}</span>
</div>
<div className="kpi-grid">
{kpis.map(([label, value]) => (
<div key={String(label)}>
<span>{label}</span>
<strong>{shortValue(value)}</strong>
</div>
))}
</div>
<div className="overview-lower">
<div>
<h3>Patient Summary</h3>
<dl className="compact-defs">
{Object.entries(summary).slice(0, 8).map(([key, value]) => (
<div key={key}>
<dt>{humanize(key)}</dt>
<dd>{shortValue(value)}</dd>
</div>
))}
{Object.keys(summary).length === 0 && <p className="muted">No patient loaded.</p>}
</dl>
</div>
<div>
<h3>Risk Delta</h3>
<dl className="compact-defs">
{Object.entries(burden).slice(0, 8).map(([key, value]) => (
<div key={key}>
<dt>{humanize(key)}</dt>
<dd>{shortValue(value)}</dd>
</div>
))}
{Object.keys(burden).length === 0 && <p className="muted">No risk data.</p>}
</dl>
</div>
</div>
</section>
);
}
function CandidatePanel({
candidates,
selected,
onSelect,
}: {
candidates: CandidateAction[];
selected: CandidateAction | null;
onSelect: (candidateId: string) => void;
}) {
return (
<section className="panel-surface panel-scroll" data-guide="candidates">
<div className="panel-heading">
<h2>Candidate Actions</h2>
<span>{candidates.length}</span>
</div>
<div className="candidate-list">
{candidates.map((candidate) => {
const active = candidate.candidate_id === selected?.candidate_id;
const legal = candidate.legality_precheck !== false;
return (
<button
key={candidate.candidate_id}
className={`candidate-row ${active ? "selected" : ""} ${legal ? "" : "illegal"}`}
onClick={() => {
if (legal) onSelect(candidate.candidate_id);
}}
disabled={!legal}
>
<span>
<strong>{candidate.candidate_id}</strong>
{humanize(candidate.action_type)}
</span>
<span>{shortValue(candidate.target_drug ?? candidate.replacement_drug ?? candidate.mode)}</span>
<span>{legal ? formatReward(candidate.estimated_safety_delta) : "Blocked"}</span>
</button>
);
})}
{candidates.length === 0 && <p className="muted">Reset an episode to load legal candidates.</p>}
</div>
</section>
);
}
function ActionConsole({
mode,
selected,
confidence,
rationale,
loading,
canSubmit,
canRunAgent,
done,
terminationReason,
onConfidence,
onRationale,
onSubmit,
onAgent,
onReset,
}: {
mode: WorkbenchMode;
selected: CandidateAction | null;
confidence: number;
rationale: string;
loading: boolean;
canSubmit: boolean;
canRunAgent: boolean;
done: boolean;
terminationReason: string | null;
onConfidence: (value: number) => void;
onRationale: (value: string) => void;
onSubmit: () => void;
onAgent: () => void;
onReset: () => void;
}) {
const details = [
["Type", selected?.action_type],
["Mode", selected?.mode],
["Target", selected?.target_drug],
["Replacement", selected?.replacement_drug],
["Dose", selected?.dose_bucket],
["Uncertainty", selected?.uncertainty_score],
];
return (
<section className="panel-surface action-console" data-guide="console">
<div className="panel-heading">
<h2>Action Console</h2>
<span>{selected?.candidate_id ?? "-"}</span>
</div>
<div className="action-detail-grid">
{details.map(([label, value]) => (
<div key={String(label)}>
<span>{label}</span>
<strong>{shortValue(value)}</strong>
</div>
))}
</div>
<label className="field">
<span>Confidence</span>
<input
type="number"
min="0.001"
max="0.999"
step="0.001"
value={confidence.toFixed(3)}
onChange={(event) => onConfidence(Number(event.target.value))}
/>
</label>
<label className="field">
<span>Rationale</span>
<input value={rationale} onChange={(event) => onRationale(event.target.value)} />
</label>
{done && (
<div className="console-notice">
{mode === "env" ? "Env Explorer" : "Agent Workbench"} returned <strong>done</strong>
{terminationReason ? ` (${humanize(terminationReason)})` : ""}. Reset the episode before submitting another
step.
</div>
)}
<div className="button-row">
<button onClick={done ? onReset : onSubmit} disabled={loading || (!canSubmit && !done)}>
{done ? "Reset Episode" : mode === "env" ? "Submit Env Step" : "Submit Candidate"}
</button>
<button className="secondary" onClick={onAgent} disabled={mode !== "agent" || loading || done || !canRunAgent}>
Run Agent
</button>
</div>
</section>
);
}
function MedicationCards({ meds }: { meds: Array<Record<string, unknown>> }) {
return (
<section className="panel-surface panel-wide" data-guide="medications">
<div className="panel-heading">
<h2>Current Medications</h2>
<span>{meds.length}</span>
</div>
<div className="med-grid">
{meds.map((med, index) => {
const flags = [med.beers_flag, med.flag, med.warning].filter(Boolean);
const highRisk = Boolean(med.high_risk ?? med.is_high_risk_elderly ?? flags.length);
return (
<article className={`med-card ${highRisk ? "high-risk" : ""}`} key={`${shortValue(med.drug)}-${index}`}>
<div className="med-card-header">
<strong>{shortValue(med.drug ?? med.drug_id ?? med.name)}</strong>
{highRisk && <span>High Risk</span>}
</div>
<p>{shortValue(med.indication ?? med.class_name ?? med.atc_class)}</p>
<div className="med-meta">
<span>{shortValue(med.dose_bucket ?? med.dose_mg ?? med.dose)}</span>
<span>{shortValue(med.requires_taper ? "taper" : med.monitoring ?? med.route)}</span>
</div>
</article>
);
})}
{meds.length === 0 && <p className="muted">No medications loaded.</p>}
</div>
</section>
);
}
function RewardBars({ rewardBreakdown, reward }: { rewardBreakdown: Record<string, unknown> | null; reward: number | null }) {
const source = rewardBreakdown ?? { total_reward: reward };
return (
<section className="panel-surface panel-scroll" data-guide="rewards">
<div className="panel-heading">
<h2>Reward Channels</h2>
<span>{formatReward(source.total_reward ?? reward)}</span>
</div>
<div className="reward-bars">
{REWARD_KEYS.map((key) => {
const value = toNumber(source[key]);
const width = Math.max(0.5, Math.min(value ?? 0, 0.999) * 100);
return (
<div className="reward-row" key={key}>
<span>{humanize(key)}</span>
<div className="reward-track">
<div className="reward-fill" style={{ width: `${width}%` }} />
</div>
<strong>{formatReward(value)}</strong>
</div>
);
})}
</div>
</section>
);
}
function ModelTruthPanel({ status }: { status: ModelStatus | null }) {
const signal = modelSignal(status);
const availability = status?.availability ?? {};
const availabilityRows = Object.entries(availability);
return (
<section className={`model-truth panel-surface ${signal.isLive ? "verified" : "unverified"}`} data-guide="model">
<div className="panel-heading">
<h2>Model Truth</h2>
<span>{signal.label}</span>
</div>
<p>{signal.detail}</p>
<div className="model-truth-grid">
<div>
<span>Model</span>
<strong>{shortValue(status?.model_id ?? status?.base_model ?? "unavailable")}</strong>
</div>
<div>
<span>Run</span>
<strong>{shortValue(status?.run_id)}</strong>
</div>
<div>
<span>Artifact</span>
<strong>{shortValue(status?.loaded_source || status?.preferred_artifact)}</strong>
</div>
<div>
<span>Availability</span>
<strong>
{availabilityRows.length
? availabilityRows.map(([key, value]) => `${humanize(key)}:${value ? "yes" : "no"}`).join(" | ")
: "-"}
</strong>
</div>
</div>
</section>
);
}
function HistoryPanel({ observation }: { observation: EnvObservation | null }) {
const history = observation?.action_history ?? [];
const warnings = observation?.warning_summary ?? [];
return (
<section className="panel-surface panel-wide" data-guide="history">
<div className="history-grid">
<div>
<div className="panel-heading inline-heading">
<h2>Action History</h2>
<span>{history.length}</span>
</div>
<div className="history-list">
{history.map((item, index) => {
const action = isRecord(item.action) ? item.action : item;
return (
<div className="history-item" key={`${index}-${shortValue(item.step ?? index)}`}>
<strong>
Step {shortValue(item.step ?? index)} - {humanize(shortValue(action.action_type ?? "action"))}
</strong>
<span>{shortValue(action.candidate_id ?? action.target_drug ?? item.reward)}</span>
</div>
);
})}
{history.length === 0 && <p className="muted">No actions yet.</p>}
</div>
</div>
<div>
<div className="panel-heading inline-heading">
<h2>Warnings</h2>
<span>{warnings.length}</span>
</div>
<div className="history-list">
{warnings.map((warning, index) => (
<div className="history-item warning" key={`${warning}-${index}`}>
{warning}
</div>
))}
{warnings.length === 0 && <p className="muted">No active warnings.</p>}
</div>
</div>
</div>
</section>
);
}
function DetailPanel({
title,
data,
}: {
title: string;
data: Record<string, unknown> | unknown[] | null | undefined;
}) {
const hasData = Array.isArray(data) ? data.length > 0 : isRecord(data) && Object.keys(data).length > 0;
return (
<section className="panel-surface detail-panel">
<div className="panel-heading">
<h2>{title}</h2>
</div>
{hasData ? <pre>{JSON.stringify(data, null, 2)}</pre> : <p className="muted">No data.</p>}
</section>
);
}
function EventLog({ events, error }: { events: string[]; error: string | null }) {
return (
<section className="panel-surface panel-wide event-panel" data-guide="event-log">
<div className="panel-heading">
<h2>Event Log</h2>
<span>{events.length}</span>
</div>
{error && <div className="error-banner">{error}</div>}
<div className="event-log">
{events.map((line, index) => (
<div key={`${line}-${index}`}>{line}</div>
))}
{events.length === 0 && <p className="muted">Events will appear here.</p>}
</div>
</section>
);
}
export default function App() {
const [mode, setMode] = useState<WorkbenchMode>("agent");
const [catalog, setCatalog] = useState<EnvCatalog>(FALLBACK_CATALOG);
const [taskId, setTaskId] = useState("budgeted_screening");
const [difficulty, setDifficulty] = useState("medium");
const [subEnvironment, setSubEnvironment] = useState("REGIMEN_RISK");
const [agentObservation, setAgentObservation] = useState<EnvObservation | null>(null);
const [envObservation, setEnvObservation] = useState<EnvObservation | null>(null);
const [agentReward, setAgentReward] = useState<number | null>(null);
const [envReward, setEnvReward] = useState<number | null>(null);
const [agentDone, setAgentDone] = useState(false);
const [envDone, setEnvDone] = useState(false);
const [selectedId, setSelectedId] = useState<string | null>(null);
const [confidence, setConfidence] = useState(0.75);
const [rationale, setRationale] = useState("Selected from the interactive workbench.");
const [rewardBreakdown, setRewardBreakdown] = useState<Record<string, unknown> | null>(null);
const [agentInfo, setAgentInfo] = useState<Record<string, unknown> | null>(null);
const [envInfo, setEnvInfo] = useState<Record<string, unknown> | null>(null);
const [modelStatus, setModelStatus] = useState<ModelStatus | null>(null);
const [decision, setDecision] = useState<Record<string, unknown> | null>(null);
const [explanation, setExplanation] = useState<Record<string, unknown> | null>(null);
const [evidence, setEvidence] = useState<unknown>(null);
const [events, setEvents] = useState<string[]>([]);
const [loading, setLoading] = useState(false);
const [error, setError] = useState<string | null>(null);
const [tipsOpen, setTipsOpen] = useState(() => {
try {
return window.localStorage.getItem(QTIPS_SEEN_KEY) !== "true";
} catch {
return true;
}
});
const [tipStep, setTipStep] = useState(0);
const refreshModelStatus = useCallback(async () => {
try {
const status = await fetchModelStatus();
setModelStatus(status);
return status;
} catch {
setModelStatus(null);
return null;
}
}, []);
useEffect(() => {
fetchCatalog().then(setCatalog).catch(() => setCatalog(FALLBACK_CATALOG));
refreshModelStatus().then((status) => {
if (!status) appendEvent(setEvents, "Model status endpoint unavailable; Qwen cannot be verified yet.");
});
return () => closeEnvSocket();
}, [refreshModelStatus]);
const activeObservation = mode === "agent" ? agentObservation : envObservation;
const activeReward = mode === "agent" ? agentReward : envReward;
const activeDone = mode === "agent" ? agentDone : envDone;
const candidates = activeObservation?.candidate_action_set ?? [];
const selected = useMemo(
() => candidates.find((candidate) => candidate.candidate_id === selectedId) ?? defaultCandidateForMode(candidates, mode),
[candidates, mode, selectedId],
);
const statusText = activeDone ? "Complete" : activeObservation ? "Live" : "Ready";
const activeInfo = mode === "agent" ? agentInfo : envInfo;
const activeTerminationReason = shortValue(activeInfo?.termination_reason);
const terminationReason = activeTerminationReason !== "-" ? activeTerminationReason : null;
const regimenForAltTool = useMemo(() => {
const meds = activeObservation?.medication_table ?? [];
const names: string[] = [];
for (const row of meds) {
const v = row.drug ?? row.drug_id ?? row.name;
if (typeof v === "string" && v.trim()) {
names.push(v.trim());
}
}
return names;
}, [activeObservation]);
const heroStats: Array<[string, string]> = [
["Runtime", mode === "agent" ? "Agent Workbench" : "Env Explorer"],
["Scenario", taskLabel(taskId, catalog.task_presets)],
["Candidates", String(candidates.length)],
["Reward", formatReward(activeReward)],
];
const closeTips = () => {
setTipsOpen(false);
try {
window.localStorage.setItem(QTIPS_SEEN_KEY, "true");
} catch {
// Ignore localStorage failures in private browser contexts.
}
};
const handleTaskChange = (nextTaskId: string) => {
setTaskId(nextTaskId);
const preset = catalog.task_presets.find((item) => item.id === nextTaskId);
if (preset) {
setDifficulty(preset.difficulty);
setSubEnvironment(preset.sub_environment);
}
};
const handleModeChange = (nextMode: WorkbenchMode) => {
if (nextMode === mode) return;
setMode(nextMode);
setEvents([]);
setError(null);
setSelectedId(null);
if (nextMode === "agent") {
setAgentObservation(null);
setAgentReward(null);
setAgentDone(false);
setAgentInfo(null);
setRewardBreakdown(null);
setDecision(null);
setExplanation(null);
setEvidence(null);
} else {
setEnvObservation(null);
setEnvReward(null);
setEnvDone(false);
setEnvInfo(null);
setRewardBreakdown(null);
}
};
const updateAgentResult = useCallback(async (packet: StepResponse | Record<string, unknown>, source: string) => {
const normalized = normalizeStepPacket(packet);
setAgentObservation(normalized.observation);
setAgentReward(normalized.reward);
setAgentDone(normalized.done);
setAgentInfo(normalized.info);
setDecision((packet.final_action as Record<string, unknown> | undefined) ?? null);
setExplanation((packet.explanation as Record<string, unknown> | undefined) ?? null);
setEvidence(packet.evidence);
const finalAction = isRecord(packet.final_action) ? packet.final_action : null;
const finalCandidateId = typeof finalAction?.candidate_id === "string" ? finalAction.candidate_id : null;
const candidatesAfterStep = normalized.observation?.candidate_action_set ?? [];
setSelectedId(
finalCandidateId && candidatesAfterStep.some((candidate) => candidate.candidate_id === finalCandidateId)
? finalCandidateId
: defaultCandidateForMode(candidatesAfterStep, "agent")?.candidate_id ?? null,
);
const breakdown =
(normalized.info.reward_breakdown as Record<string, unknown> | undefined) ??
((await fetchRewardBreakdown().catch(() => null)) as Record<string, unknown> | null);
setRewardBreakdown(breakdown ?? null);
const reason = shortValue(normalized.info.termination_reason);
appendEvent(
setEvents,
`${source} reward ${formatReward(normalized.reward)}${normalized.done && reason !== "-" ? ` - complete: ${reason}` : ""}`,
);
}, []);
const updateEnvResult = useCallback((packet: EnvStepPacket, source: string, submittedCandidateId?: string) => {
const normalized = normalizeStepPacket(packet);
const candidatesAfterStep = normalized.observation?.candidate_action_set ?? [];
setEnvObservation(normalized.observation);
setEnvReward(normalized.reward);
setEnvDone(normalized.done);
setEnvInfo(normalized.info);
setSelectedId(
submittedCandidateId && candidatesAfterStep.some((candidate) => candidate.candidate_id === submittedCandidateId)
? submittedCandidateId
: defaultCandidateForMode(candidatesAfterStep, "env")?.candidate_id ?? null,
);
const rawBreakdown = normalized.info.reward_breakdown;
if (isRecord(rawBreakdown) && Object.keys(rawBreakdown).length > 0) {
setRewardBreakdown(rawBreakdown);
} else {
setRewardBreakdown(null);
}
const reason = shortValue(normalized.info.termination_reason);
appendEvent(
setEvents,
`${source} reward ${formatReward(normalized.reward)}${normalized.done && reason !== "-" ? ` - complete: ${reason}` : ""}`,
);
}, []);
const handleReset = async () => {
setLoading(true);
setError(null);
setEvents([]);
try {
const options = taskResetOptions(taskId, difficulty, subEnvironment, catalog.task_presets);
if (mode === "agent") {
await refreshModelStatus();
const obs = await resetEnv(options.agent);
setAgentObservation(obs);
setAgentReward(null);
setAgentDone(false);
setAgentInfo(null);
setRewardBreakdown(null);
setDecision(null);
setExplanation(null);
setEvidence(null);
setSelectedId(defaultCandidateForMode(obs.candidate_action_set, "agent")?.candidate_id ?? null);
} else {
const packet = await envWsSend<EnvStepPacket>("reset", options.env);
updateEnvResult(packet, "Env reset");
}
appendEvent(setEvents, `Reset ${taskLabel(taskId, catalog.task_presets)} in ${mode}`);
} catch (err) {
const message = err instanceof Error ? err.message : "Reset failed";
setError(message);
appendEvent(setEvents, message);
} finally {
setLoading(false);
}
};
const submitSelected = async () => {
if (!selected) return;
setLoading(true);
setError(null);
try {
if (mode === "agent") {
const result = await stepCandidate({
candidate_id: selected.candidate_id,
confidence,
rationale_brief: rationale,
});
await updateAgentResult(result, humanize(selected.action_type));
await refreshModelStatus();
} else {
const payload = buildActionPayload(selected, confidence, rationale);
const packet = await envWsSend<EnvStepPacket>("step", payload);
updateEnvResult(packet, humanize(selected.action_type), selected.candidate_id);
}
} catch (err) {
const message = err instanceof Error ? err.message : "Step failed";
setError(message);
appendEvent(setEvents, message);
} finally {
setLoading(false);
}
};
const runAgent = async () => {
setLoading(true);
setError(null);
try {
const result = await orchestrateStep();
await updateAgentResult(result, "Agent");
await refreshModelStatus();
} catch (err) {
const message = err instanceof Error ? err.message : "Agent run failed";
setError(message);
appendEvent(setEvents, message);
} finally {
setLoading(false);
}
};
return (
<div className="workbench-shell">
<MetaverseBackdrop />
<div className="workbench-container">
<section className="metaverse-hero panel-surface">
<div className="hero-copy">
<div className="welcome-box">
<span className="spark-glyph">*</span>
<span className="welcome-text">PolyGuard neural safety cockpit</span>
</div>
<h2>
Clinical medication safety, guided by
<span> constrained RL decisions.</span>
</h2>
<p>
PolyGuard coordinates live OpenEnv episodes, candidate actions, reward channels, and evidence-grounded
policy traces for safer polypharmacy review.
</p>
</div>
<div className="hero-stat-grid" aria-label="Current workbench state">
{heroStats.map(([label, value]) => (
<div key={label}>
<span>{label}</span>
<strong>{value}</strong>
</div>
))}
</div>
</section>
<TopBar
mode={mode}
setMode={handleModeChange}
taskId={taskId}
onTaskChange={handleTaskChange}
catalog={catalog}
statusText={statusText}
modelStatus={modelStatus}
loading={loading}
onReset={handleReset}
onOpenTips={() => {
setTipStep(0);
setTipsOpen(true);
}}
/>
<ModelTruthPanel status={modelStatus} />
{taskId === "advanced" && (
<section className="advanced-strip panel-surface">
<label className="field">
<span>Difficulty</span>
<select value={difficulty} onChange={(event) => setDifficulty(event.target.value)}>
<option value="easy">easy</option>
<option value="medium">medium</option>
<option value="hard">hard</option>
</select>
</label>
<label className="field">
<span>Environment</span>
<select value={subEnvironment} onChange={(event) => setSubEnvironment(event.target.value)}>
{catalog.sub_environments.map((item) => (
<option key={item} value={item}>
{item}
</option>
))}
</select>
</label>
</section>
)}
<main className="workbench-layout">
<EpisodeOverview
mode={mode}
observation={activeObservation}
reward={activeReward}
done={activeDone}
taskId={taskId}
catalog={catalog}
/>
<CandidatePanel candidates={candidates} selected={selected} onSelect={setSelectedId} />
<ActionConsole
mode={mode}
selected={selected}
confidence={confidence}
rationale={rationale}
loading={loading}
canSubmit={Boolean(selected && selected.legality_precheck !== false && activeObservation && !activeDone)}
canRunAgent={Boolean(mode === "agent" && activeObservation && !activeDone)}
done={activeDone}
terminationReason={terminationReason}
onConfidence={setConfidence}
onRationale={setRationale}
onSubmit={submitSelected}
onAgent={runAgent}
onReset={handleReset}
/>
<RewardBars rewardBreakdown={rewardBreakdown} reward={activeReward} />
<MedicationCards meds={activeObservation?.medication_table ?? []} />
<HistoryPanel observation={activeObservation} />
<DetailPanel title="Decision" data={mode === "agent" ? decision : null} />
<DetailPanel title="Explanation" data={mode === "agent" ? explanation : null} />
<DetailPanel title="Evidence" data={mode === "agent" ? (isRecord(evidence) || Array.isArray(evidence) ? evidence : null) : null} />
<EventLog events={events} error={error} />
<AlternativeMedicineSearch regimenDrugNames={regimenForAltTool} />
</main>
<QTips
open={tipsOpen}
step={tipStep}
steps={GUIDE_STEPS}
onNext={() => setTipStep((step) => Math.min(step + 1, GUIDE_STEPS.length - 1))}
onPrev={() => setTipStep((step) => Math.max(step - 1, 0))}
onClose={closeTips}
/>
</div>
</div>
);
}