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 (
Polypharmacy Control Center
Metaverse Clinical Ops Console
{hasValidEpisode ? "Session Live" : "Waiting for reset"}
Episode
{hasValidEpisode ? (
Episode{obs.episode_id}
Task{obs.task_id}
Age / Sex{obs.age} / {obs.sex}
Step{obs.step_index}
Query budget{obs.remaining_query_budget}
Intervention budget{obs.remaining_intervention_budget}
) : (
Start with Reset Episode. Until then, step actions are blocked.
)}
{noBudgetsLeft && (
Query and intervention budgets are exhausted. Finish review to get final score.
)}
{isDone && (
Episode complete
{finalScore !== null ? ` • final score: ${finalScore.toFixed(3)}` : ""}.
Click Reset Episode to start a new case.
)}
Current Medications
{(obs?.current_medications || []).map((m) => (
{m.drug_id}
{m.generic_name}
{m.dose_mg} mg • {m.atc_class}
))}
Event Log
{log.map((line, idx) => (
{line}
))}
);
}