| 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 { |
| |
| } |
| }; |
|
|
| 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> |
| ); |
| } |
|
|