adithya9903's picture
Flatten project to root for OpenEnv submission readiness.
fa51dd9
import { useEffect, useMemo, useRef, useState } from "react";
function resolveApiBase() {
const explicitBase = import.meta.env.VITE_API_BASE;
if (explicitBase) return explicitBase.replace(/\/$/, "");
const host = window.location.hostname;
const isLocal =
host === "localhost" || host === "127.0.0.1" || host === "0.0.0.0";
// In local Vite dev, backend runs on :7860. In Spaces/prod, serve same-origin.
if (isLocal && window.location.port === "5173") {
return "http://localhost:7860";
}
return window.location.origin.replace(/\/$/, "");
}
const API_BASE = resolveApiBase();
const WS_URL = `${API_BASE.replace(/^http/, "ws")}/ws`;
const TASKS = ["easy_screening", "budgeted_screening", "complex_tradeoff"];
async function apiPost(path, body) {
const res = await fetch(`${API_BASE}${path}`, {
method: "POST",
headers: { "Content-Type": "application/json" },
body: JSON.stringify(body),
});
if (!res.ok) {
const msg = await res.text();
throw new Error(msg || `HTTP ${res.status}`);
}
return res.json();
}
export default function App() {
const [taskId, setTaskId] = useState("budgeted_screening");
const [obs, setObs] = useState(null);
const [log, setLog] = useState([]);
const [loading, setLoading] = useState(false);
const [action, setAction] = useState({
action_type: "query_ddi",
drug_id_1: "",
drug_id_2: "",
target_drug_id: "",
intervention_type: "stop",
proposed_new_drug_id: "",
rationale: "",
});
const medIds = useMemo(
() => (obs?.current_medications || []).map((m) => m.drug_id),
[obs]
);
const hasValidEpisode = Boolean(obs?.episode_id) && (obs?.current_medications?.length || 0) > 0;
const isDone = Boolean(obs?.done);
const finalScore =
typeof obs?.metadata?.grader_score === "number" ? obs.metadata.grader_score : null;
const noBudgetsLeft =
hasValidEpisode &&
(obs?.remaining_query_budget ?? 0) <= 0 &&
(obs?.remaining_intervention_budget ?? 0) <= 0;
const wsRef = useRef(null);
const pendingRef = useRef([]);
const wsEnsure = async () => {
if (wsRef.current && wsRef.current.readyState === WebSocket.OPEN) return wsRef.current;
if (wsRef.current && wsRef.current.readyState === WebSocket.CONNECTING) {
await new Promise((r) => setTimeout(r, 80));
return wsEnsure();
}
const ws = new WebSocket(WS_URL);
wsRef.current = ws;
ws.onmessage = (evt) => {
try {
const msg = JSON.parse(evt.data);
const pending = pendingRef.current.shift();
if (pending) pending.resolve(msg);
} catch (e) {
const pending = pendingRef.current.shift();
if (pending) pending.reject(e);
}
};
ws.onerror = (err) => {
const pending = pendingRef.current.shift();
if (pending) pending.reject(err);
};
ws.onclose = () => {
wsRef.current = null;
};
await new Promise((resolve, reject) => {
const t = setTimeout(() => reject(new Error("WebSocket connect timeout")), 2500);
ws.onopen = () => {
clearTimeout(t);
resolve();
};
});
return ws;
};
const wsSend = async (type, data) => {
const ws = await wsEnsure();
return await new Promise((resolve, reject) => {
pendingRef.current.push({ resolve, reject });
ws.send(JSON.stringify({ type, data }));
});
};
useEffect(() => {
return () => {
try {
wsRef.current?.close();
} catch {
// ignore
}
};
}, []);
const appendLog = (text) => {
setLog((prev) => [`${new Date().toLocaleTimeString()} ${text}`, ...prev].slice(0, 20));
};
const normalizeObsFromWs = (packetData) => {
const observation = packetData?.observation || {};
const mergedMetadata = {
...(observation?.metadata || {}),
...(packetData?.info || {}),
};
return {
...observation,
done: Boolean(packetData?.done ?? observation?.done ?? false),
reward: packetData?.reward ?? observation?.reward ?? null,
metadata: mergedMetadata,
};
};
const handleReset = async () => {
setLoading(true);
try {
const msg = await wsSend("reset", { task_id: taskId });
const data = msg?.data || {};
const normalized = normalizeObsFromWs(data);
setObs(normalized);
const ids = (normalized?.current_medications || []).map((m) => m.drug_id);
setAction((prev) => ({
...prev,
drug_id_1: ids[0] || "",
drug_id_2: ids[1] || "",
target_drug_id: ids[0] || "",
}));
appendLog(`Reset task=${taskId}`);
} catch (err) {
appendLog(`Reset failed: ${err.message}`);
} finally {
setLoading(false);
}
};
const buildActionPayload = () => {
if (noBudgetsLeft) {
return { action_type: "finish_review" };
}
if (action.action_type === "query_ddi") {
return {
action_type: "query_ddi",
drug_id_1: action.drug_id_1,
drug_id_2: action.drug_id_2,
};
}
if (action.action_type === "propose_intervention") {
return {
action_type: "propose_intervention",
target_drug_id: action.target_drug_id,
intervention_type: action.intervention_type,
proposed_new_drug_id: action.proposed_new_drug_id || undefined,
rationale: action.rationale || undefined,
};
}
return { action_type: "finish_review" };
};
const isActionValid = () => {
if (!hasValidEpisode) return false;
if (isDone) return false;
if (noBudgetsLeft) return true;
if (action.action_type === "query_ddi") {
return Boolean(action.drug_id_1 && action.drug_id_2);
}
if (action.action_type === "propose_intervention") {
return Boolean(action.target_drug_id && action.intervention_type);
}
return true;
};
const handleStep = async (overrideAction = null) => {
if (!hasValidEpisode) {
appendLog("Run Reset Episode before stepping.");
return;
}
setLoading(true);
try {
const payload = overrideAction || buildActionPayload();
const msg = await wsSend("step", payload);
const data = msg?.data || {};
const normalized = normalizeObsFromWs(data);
setObs(normalized);
appendLog(`Step: ${payload.action_type} -> reward=${data.reward ?? 0}`);
} catch (err) {
appendLog(`Step failed: ${err.message}`);
} finally {
setLoading(false);
}
};
const askAi = async () => {
if (!hasValidEpisode) {
appendLog("Run Reset Episode before asking AI.");
return;
}
setLoading(true);
try {
const data = await apiPost("/agent/suggest", { observation: obs });
appendLog(`AI suggestion: ${data.action.action_type}`);
await handleStep(data.action);
} catch (err) {
appendLog(`AI suggestion failed: ${err.message}`);
} finally {
setLoading(false);
}
};
return (
<div className="shell">
<div className="bg-orb orb-a" />
<div className="bg-orb orb-b" />
<div className="container">
<header className="topbar glass">
<div className="title-wrap">
<h1>Polypharmacy Control Center</h1>
<p>Metaverse Clinical Ops Console</p>
</div>
<div className={`status-chip ${hasValidEpisode ? "live" : "idle"}`}>
{hasValidEpisode ? "Session Live" : "Waiting for reset"}
</div>
<div className="actions">
<select value={taskId} onChange={(e) => setTaskId(e.target.value)}>
{TASKS.map((t) => (
<option key={t} value={t}>
{t}
</option>
))}
</select>
<button onClick={handleReset} disabled={loading}>
Reset Episode
</button>
<button className="secondary" onClick={askAi} disabled={!hasValidEpisode || isDone || loading}>
Ask AI + Auto Step
</button>
</div>
</header>
<main className="layout">
<section className="panel glass panel-wide">
<h2>Episode</h2>
{hasValidEpisode ? (
<div className="kpi-grid">
<div><span>Episode</span><strong>{obs.episode_id}</strong></div>
<div><span>Task</span><strong>{obs.task_id}</strong></div>
<div><span>Age / Sex</span><strong>{obs.age} / {obs.sex}</strong></div>
<div><span>Step</span><strong>{obs.step_index}</strong></div>
<div><span>Query budget</span><strong>{obs.remaining_query_budget}</strong></div>
<div><span>Intervention budget</span><strong>{obs.remaining_intervention_budget}</strong></div>
</div>
) : (
<p className="muted">Start with Reset Episode. Until then, step actions are blocked.</p>
)}
{noBudgetsLeft && (
<p className="muted budget-note">Query and intervention budgets are exhausted. Finish review to get final score.</p>
)}
{isDone && (
<p className="muted budget-note">
Episode complete
{finalScore !== null ? ` • final score: ${finalScore.toFixed(3)}` : ""}.
Click Reset Episode to start a new case.
</p>
)}
</section>
<section className="panel glass">
<h2>Action Console</h2>
<div className="action-row">
<label>Action type</label>
<select
value={action.action_type}
onChange={(e) => setAction((a) => ({ ...a, action_type: e.target.value }))}
>
<option value="query_ddi">query_ddi</option>
<option value="propose_intervention">propose_intervention</option>
<option value="finish_review">finish_review</option>
</select>
</div>
{action.action_type === "query_ddi" && (
<div className="stack stack-two">
<input
placeholder="drug_id_1"
value={action.drug_id_1}
onChange={(e) => setAction((a) => ({ ...a, drug_id_1: e.target.value }))}
/>
<input
placeholder="drug_id_2"
value={action.drug_id_2}
onChange={(e) => setAction((a) => ({ ...a, drug_id_2: e.target.value }))}
/>
</div>
)}
{action.action_type === "propose_intervention" && (
<div className="stack">
<select
value={action.target_drug_id}
onChange={(e) => setAction((a) => ({ ...a, target_drug_id: e.target.value }))}
>
<option value="">Select target drug</option>
{medIds.map((id) => (
<option key={id} value={id}>
{id}
</option>
))}
</select>
<select
value={action.intervention_type}
onChange={(e) => setAction((a) => ({ ...a, intervention_type: e.target.value }))}
>
<option value="stop">stop</option>
<option value="dose_reduce">dose_reduce</option>
<option value="substitute">substitute</option>
<option value="add_monitoring">add_monitoring</option>
</select>
<input
placeholder="proposed_new_drug_id (optional)"
value={action.proposed_new_drug_id}
onChange={(e) =>
setAction((a) => ({ ...a, proposed_new_drug_id: e.target.value }))
}
/>
<input
placeholder="rationale (optional)"
value={action.rationale}
onChange={(e) => setAction((a) => ({ ...a, rationale: e.target.value }))}
/>
</div>
)}
<button onClick={() => handleStep()} disabled={!isActionValid() || loading}>
{noBudgetsLeft ? "Finish Review" : "Submit Step"}
</button>
</section>
<section className="panel glass">
<h2>Current Medications</h2>
<div className="med-grid">
{(obs?.current_medications || []).map((m) => (
<div key={m.drug_id} className="med-card">
<strong>{m.drug_id}</strong>
<p>{m.generic_name}</p>
<small>{m.dose_mg} mg • {m.atc_class}</small>
</div>
))}
</div>
</section>
<section className="panel glass">
<h2>Event Log</h2>
<div className="logs">
{log.map((line, idx) => (
<div key={idx}>{line}</div>
))}
</div>
</section>
</main>
</div>
</div>
);
}