Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
69 changes: 69 additions & 0 deletions PanTS-Demo/src/routes/UploadPage.css
Original file line number Diff line number Diff line change
Expand Up @@ -1110,3 +1110,72 @@
border-top-color: #002D72;
animation: upload-spin 0.75s linear infinite;
}

/* ── Simplified config: Auto hint, speed/quality toggle, result note ── */
.simple-config {
display: flex;
flex-direction: column;
gap: 12px;
margin-top: 16px;
}

.auto-hint-input {
width: 100%;
padding: 11px 14px;
border: 1px solid rgba(0, 45, 114, 0.16);
border-radius: 10px;
font-size: 13.5px;
font-family: inherit;
color: #1a2b47;
background: #fbfcfe;
outline: none;
transition: border-color 0.15s ease, box-shadow 0.15s ease;
}
.auto-hint-input:focus {
border-color: #002D72;
box-shadow: 0 0 0 3px rgba(0, 45, 114, 0.08);
}

.precision-toggle {
display: grid;
grid-template-columns: 1fr 1fr;
gap: 10px;
}
.precision-opt {
display: flex;
flex-direction: column;
align-items: flex-start;
gap: 3px;
padding: 12px 14px;
border: 1px solid rgba(0, 45, 114, 0.16);
border-radius: 10px;
background: #fff;
cursor: pointer;
text-align: left;
transition: border-color 0.15s ease, background 0.15s ease, box-shadow 0.15s ease;
}
.precision-opt:hover { border-color: rgba(0, 45, 114, 0.4); }
.precision-opt.active {
border-color: #002D72;
background: rgba(0, 45, 114, 0.05);
box-shadow: 0 0 0 2px rgba(0, 45, 114, 0.12);
}
.precision-opt-title {
font-size: 13.5px;
font-weight: 600;
color: #002D72;
}
.precision-opt-sub {
font-size: 11.5px;
color: #6b7793;
}

.auto-note {
padding: 10px 13px;
border-radius: 10px;
background: rgba(45, 130, 90, 0.08);
border: 1px solid rgba(45, 130, 90, 0.2);
color: #1f5b3f;
font-size: 12.5px;
line-height: 1.45;
}
81 changes: 77 additions & 4 deletions PanTS-Demo/src/routes/UploadPage.tsx
Original file line number Diff line number Diff line change
@@ -1,6 +1,7 @@
import React, { lazy, Suspense, useCallback, useEffect, useRef, useState } from 'react';

const MODEL_OPTIONS: { id: string; label: string; desc: string }[] = [
{ id: "Auto", label: "Auto — AI picks the best model", desc: "Recommended · a local AI reads your request and chooses for you" },
{ id: "None", label: "None", desc: "View only — files never leave your browser" },
{ id: "ePAI", label: "ePAI", desc: "For detailed pancreas and tumor analysis" },
{ id: "SuPreM", label: "SuPreM", desc: "For whole-body scans from lungs to legs" },
Expand Down Expand Up @@ -79,7 +80,13 @@ const UploadPage: React.FC = () => {
const [uploadProgress, setUploadProgress] = useState<number>(0);
const [isUploading, setIsUploading] = useState<boolean>(false);
const [inferenceCompleted, setInferenceCompleted] = useState<boolean>(false);
const [selectedModel, setSelectedModel] = useState<"None" | "ePAI" | "SuPreM" | "OpenVAE" | "MedFormer" | "R-Super" | "Atlas-Net" | "">("None");
const [selectedModel, setSelectedModel] = useState<"Auto" | "None" | "ePAI" | "SuPreM" | "OpenVAE" | "MedFormer" | "R-Super" | "Atlas-Net" | "">("Auto");
// FP16 = quantized (faster, half the memory, identical masks); the default.
const [selectedPrecision, setSelectedPrecision] = useState<"fp16" | "fp32">("fp16");
// Optional plain-language hint for the Auto picker, and the note it returns.
const [autoHint, setAutoHint] = useState("");
const [autoNote, setAutoNote] = useState("");
const [autoBusy, setAutoBusy] = useState(false);
const [modelDropOpen, setModelDropOpen] = useState(false);
const modelDropRef = useRef<HTMLDivElement>(null);
const [preDropOpen, setPreDropOpen] = useState(false);
Expand Down Expand Up @@ -412,6 +419,7 @@ const UploadPage: React.FC = () => {
const inferFd = new FormData();
inferFd.append("session_id", sid);
inferFd.append("model_name", model);
inferFd.append("precision", selectedPrecision);
inferFd.append("uploaded_filename", uploadedName);
const res = await fetch(`${API_BASE}/api/run-epai-inference`, {
method: "POST", body: inferFd, signal: controller.signal,
Expand Down Expand Up @@ -489,6 +497,7 @@ const UploadPage: React.FC = () => {
const inferFd = new FormData();
inferFd.append("session_id", sid);
inferFd.append("model_name", model);
inferFd.append("precision", selectedPrecision);
inferFd.append("uploaded_filename", uploadedName);
const res = await fetch(`${API_BASE}/api/run-epai-inference`, {
method: "POST", body: inferFd, signal: controller.signal,
Expand Down Expand Up @@ -541,7 +550,37 @@ const UploadPage: React.FC = () => {
return;
}

const model = selectedModel;
// Auto mode: ask the backend (local LLM, with a heuristic fallback) to choose
// the model + precision, then run with that choice. Resolves to a concrete
// model name before anything is uploaded.
let model: string = selectedModel;
let precision = selectedPrecision;
if (selectedModel === "Auto") {
try {
setAutoBusy(true);
setMessage("Auto-selecting the best model for your scan…");
const filename = item ? (item.kind === 'dicom' ? item.label : item.file.name) : (path ? path.split("/").pop() : "");
const res = await fetch(`${API_BASE}/api/suggest-model`, {
method: "POST",
headers: { "Content-Type": "application/json" },
body: JSON.stringify({ hint: autoHint, filename }),
});
const data = await parseApiResponse(res);
model = data.model || "ePAI";
precision = (data.precision === "fp32" ? "fp32" : "fp16");
setSelectedModel(model as typeof selectedModel);
setSelectedPrecision(precision);
setAutoNote(`Auto-selected ${model} · ${precision.toUpperCase()} — ${data.reason || "best fit for this scan."}`);
} catch {
model = "ePAI";
precision = "fp16";
setSelectedModel("ePAI");
setAutoNote("Auto-select was unavailable, so it defaulted to ePAI · FP16.");
} finally {
setAutoBusy(false);
}
}

const sid = crypto.randomUUID();
const itemName = item ? (item.kind === 'dicom' ? item.label : item.file.name) : undefined;
const label = bdmapId.trim() || (path ? path.split("/").pop() : itemName) || sid;
Expand All @@ -567,6 +606,7 @@ const UploadPage: React.FC = () => {
const fd = new FormData();
fd.append("session_id", sid);
fd.append("model_name", model);
fd.append("precision", selectedPrecision);
fd.append("INPUT_SERVER_PATH", path);
const res = await fetch(`${API_BASE}/api/run-epai-inference`, { method: "POST", body: fd });
const data = await parseApiResponse(res);
Expand Down Expand Up @@ -682,6 +722,7 @@ const UploadPage: React.FC = () => {
const formData = new FormData();
formData.append("session_id", newSessionId);
formData.append("model_name", "ePAI");
formData.append("precision", selectedPrecision);
formData.append("source_reconstruction_session_id", sessionId);

try {
Expand Down Expand Up @@ -969,12 +1010,44 @@ const UploadPage: React.FC = () => {
<button
className="run-btn"
onClick={handleRunEpaiInference}
disabled={!selectedModel || isUploading}
disabled={!selectedModel || isUploading || autoBusy}
>
{selectedModel === "None" ? "View" : "Run"}
{autoBusy ? "Selecting…" : isUploading ? "Working…" : selectedModel === "None" ? "View" : selectedModel === "Auto" ? "Analyze" : "Run"}
</button>
</div>

{/* ── Simple controls: Auto hint, speed/quality, result note ── */}
<div className="simple-config">
{selectedModel === "Auto" && (
<input
type="text"
className="auto-hint-input"
placeholder="Optional — what are you looking for? (e.g. pancreatic tumor, whole abdomen)"
value={autoHint}
onChange={(e) => setAutoHint(e.target.value)}
/>
)}
<div className="precision-toggle" role="group" aria-label="Speed versus quality">
<button
type="button"
className={`precision-opt${selectedPrecision === 'fp16' ? ' active' : ''}`}
onClick={() => setSelectedPrecision('fp16')}
>
<span className="precision-opt-title">Faster · recommended</span>
<span className="precision-opt-sub">FP16 · half the memory, same result</span>
</button>
<button
type="button"
className={`precision-opt${selectedPrecision === 'fp32' ? ' active' : ''}`}
onClick={() => setSelectedPrecision('fp32')}
>
<span className="precision-opt-title">Highest precision</span>
<span className="precision-opt-sub">FP32 · original full weights</span>
</button>
</div>
{autoNote && <div className="auto-note">{autoNote}</div>}
</div>

{/* ── Advanced options ── */}
<div className="advanced-section">
<button
Expand Down
Loading