Skip to content
Merged
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
9 changes: 7 additions & 2 deletions PanTS-Demo/src/components/Preview.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -3,10 +3,11 @@ import { useNavigate } from "react-router-dom";
import { API_BASE } from "../helpers/constants";
import { prefetchViewer } from "../helpers/prefetchViewer";
import { prefetchVolume } from "../helpers/prefetchVolume";
import type { CaseId } from "../helpers/search";
import type { PreviewType } from "../types";

type Props = {
id: number;
id: CaseId;
previewMetadata: PreviewType;
saved?: boolean;
onToggleSave?: () => void;
Expand Down Expand Up @@ -39,7 +40,11 @@ export default function Preview({

if (!previewMetadata) return null;

const caseIdStr = `PanTS_${id.toString().padStart(8, "0")}`;
// CancerVerse ids arrive as full strings ("CV_00000001"); PanTS as bare numbers.
const caseIdStr =
typeof id === "string" && id.toUpperCase().startsWith("CV")
? id
: `PanTS_${id.toString().padStart(8, "0")}`;
// HuggingFace fallback, routed through the backend's same-origin proxy. A *direct*
// cross-origin image is blocked by the viewer's COEP: require-corp header (which is
// why thumbnails went missing); the proxy keeps it same-origin, matching home.html.
Expand Down
12 changes: 7 additions & 5 deletions PanTS-Demo/src/helpers/prefetchVolume.ts
Original file line number Diff line number Diff line change
Expand Up @@ -11,10 +11,11 @@
// Callers should debounce with a short hover dwell (see Preview.tsx) so a quick pass-over
// doesn't trigger a fetch.
import { API_BASE } from "./constants";
import type { CaseId } from "./search";

const MAX_CONCURRENT = 2;
const requested = new Set<number>();
const queue: number[] = [];
const requested = new Set<CaseId>();
const queue: CaseId[] = [];
let inFlight = 0;

function pump(): void {
Expand All @@ -33,9 +34,10 @@ function pump(): void {
}
}

/** Queue a low-res CT prefetch for a dataset case (idempotent, bandwidth-bounded). */
export function prefetchVolume(id: number): void {
if (!Number.isFinite(id) || requested.has(id)) return;
/** Queue a low-res CT prefetch for a dataset case (idempotent, bandwidth-bounded).
* Accepts a PanTS number or a CancerVerse string id (e.g. "CV_00000001"). */
export function prefetchVolume(id: CaseId): void {
if (!id || requested.has(id)) return; // 0 / "" = no usable id
requested.add(id);
queue.push(id);
pump();
Expand Down
9 changes: 6 additions & 3 deletions PanTS-Demo/src/helpers/savedCases.ts
Original file line number Diff line number Diff line change
Expand Up @@ -5,7 +5,7 @@
// (cross-tab) to stay in sync.

export type SavedCase = {
id: number;
id: number | string; // PanTS number (8854) or CancerVerse string ("CV_00000001")
sex: string;
age: number;
tumor: number;
Expand All @@ -18,7 +18,9 @@ export const SAVED_CASES_EVENT = "savedcaseschange";
export const loadSavedCases = (): SavedCase[] => {
try {
const arr = JSON.parse(localStorage.getItem(SAVED_CASES_KEY) || "[]");
return Array.isArray(arr) ? arr.filter((c) => c && typeof c.id === "number") : [];
return Array.isArray(arr)
? arr.filter((c) => c && (typeof c.id === "number" || typeof c.id === "string"))
: [];
} catch {
return [];
}
Expand All @@ -38,7 +40,8 @@ const persistSavedCases = (list: SavedCase[]) => {
}
};

export const isSavedCase = (id: number): boolean => loadSavedCases().some((c) => c.id === id);
export const isSavedCase = (id: number | string): boolean =>
loadSavedCases().some((c) => c.id === id);

// Add the case if it isn't saved, otherwise remove it. Returns the updated list (most
// recently saved first).
Expand Down
5 changes: 5 additions & 0 deletions PanTS-Demo/src/helpers/search.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -23,6 +23,11 @@ describe("itemToId", () => {
expect(itemToId({})).toBe(0);
expect(itemToId({ case_id: "no-digits-here" })).toBe(0);
});

it("keeps CancerVerse ids as the full prefixed string (not a stripped number)", () => {
expect(itemToId({ case_id: "CV_00000001" })).toBe("CV_00000001");
expect(itemToId({ "PanTS ID": "CV_00012345" })).toBe("CV_00012345");
});
});

describe("buildSearchParams", () => {
Expand Down
19 changes: 14 additions & 5 deletions PanTS-Demo/src/helpers/search.ts
Original file line number Diff line number Diff line change
Expand Up @@ -28,7 +28,13 @@ export const EMPTY_FILTERS: SearchFilters = {
// The multi-select array keys (everything except `tumor`).
export type MultiFilterKey = "dataset" | "sex" | "age" | "manufacturer" | "ctPhase" | "siteNat" | "year";

// Minimal shape of an item returned by /api/search and /api/random.
// A case id as used across the UI: a bare number for PanTS (e.g. 8854) or the full
// prefixed string for CancerVerse (e.g. "CV_00000001"). CancerVerse ids MUST keep
// their prefix so they route to the CV endpoints instead of being mistaken for PanTS.
export type CaseId = number | string;

// Minimal shape of an item returned by /api/search and /api/random. The id fields can
// be a PanTS id ("PanTS_00008854") or a CancerVerse id ("CV_00000001").
export type SearchItem = {
case_id?: string | number;
"PanTS ID"?: string | number;
Expand All @@ -38,10 +44,13 @@ export type SearchItem = {
age?: number | string | null;
};

// Parse the numeric case id out of any of the id-ish fields, e.g.
// "PanTS_00008854" -> 8854. Returns 0 when nothing usable is present.
export const itemToId = (it: SearchItem): number => {
const raw = String(it.case_id ?? it["PanTS ID"] ?? it.id ?? "");
// Resolve a card id from any of the id-ish fields. PanTS → the bare number
// ("PanTS_00008854" → 8854); CancerVerse → the full string kept as-is
// ("CV_00000001") so it hits the CV endpoints. Returns 0 when nothing usable.
export const itemToId = (it: SearchItem): CaseId => {
const raw = String(it.case_id ?? it["PanTS ID"] ?? it.id ?? "").trim();
if (!raw) return 0;
if (raw.toUpperCase().startsWith("CV")) return raw; // keep "CV_00000001" as-is
const m = raw.match(/\d+/);
return m ? Number(m[0]) : 0;
};
Expand Down
22 changes: 14 additions & 8 deletions PanTS-Demo/src/routes/Homepage.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -13,6 +13,7 @@ import Preview from "../components/Preview";
import { API_BASE } from "../helpers/constants";
import {
buildSearchParams,
type CaseId,
countActiveFilters,
EMPTY_FILTERS,
itemToId,
Expand Down Expand Up @@ -137,7 +138,7 @@ const filterLabelStyle: React.CSSProperties = {
};

export default function Homepage() {
const [PREVIEW_IDS, SET_PREVIEW_IDS] = useState<number[]>([]);
const [PREVIEW_IDS, SET_PREVIEW_IDS] = useState<CaseId[]>([]);
const navigation = useNavigate();
const [previewMetadata, setPreviewMetadata] = useState<{
[key: string]: PreviewType;
Expand Down Expand Up @@ -170,33 +171,38 @@ export default function Homepage() {
};
}, []);

const handleToggleSave = (id: number, meta?: PreviewType) => {
const handleToggleSave = (id: CaseId, meta?: PreviewType) => {
const m = meta ?? previewMetadata[id];
toggleSavedCase({ id, sex: m?.sex ?? "", age: m?.age ?? 0, tumor: m?.tumor ?? 0 });
};

// Cases picked for side-by-side comparison (max 2). Adding a third drops the oldest,
// so the two most recent picks are always what get compared.
const [compareIds, setCompareIds] = useState<number[]>([]);
const [compareIds, setCompareIds] = useState<CaseId[]>([]);
const [compareTyped, setCompareTyped] = useState("");
const toggleCompare = (id: number) => {
const toggleCompare = (id: CaseId) => {
setCompareIds((prev) =>
prev.includes(id) ? prev.filter((x) => x !== id) : [...prev, id].slice(-2)
);
};
// Add a case by id typed into the tray (idempotent; caps at the two most recent).
const addCompareId = (id: number) => {
const addCompareId = (id: CaseId) => {
setCompareIds((prev) => (prev.includes(id) ? prev : [...prev, id].slice(-2)));
};
const submitTypedCompare = () => {
const n = parseInt(compareTyped.trim(), 10);
if (Number.isFinite(n) && n > 0) addCompareId(n);
const raw = compareTyped.trim();
if (raw.toUpperCase().startsWith("CV")) {
addCompareId(raw); // CancerVerse id typed in full
} else {
const n = parseInt(raw, 10);
if (Number.isFinite(n) && n > 0) addCompareId(n);
}
setCompareTyped("");
};

// Turn /api/search (or /api/random) items into the ids + metadata the grid needs.
const ingestItems = (items: SearchItem[]) => {
const ids: number[] = [];
const ids: CaseId[] = [];
const meta: { [key: string]: PreviewType } = {};
for (const it of items) {
const id = itemToId(it);
Expand Down
Loading