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
39 changes: 39 additions & 0 deletions PanTS-Demo/src/helpers/recentUploads.ts
Original file line number Diff line number Diff line change
Expand Up @@ -5,7 +5,12 @@ export type RecentUploadStatus = "Processing" | "Completed" | "Failed" | "Cancel

export type RecentUpload = {
sessionId: string;
// User-facing name. Defaults to a friendly "<model> · <date>" (see
// friendlyScanName) rather than the raw upload filename, and is renameable.
label: string;
// The original filename, kept for reference (shown as a tooltip) even after
// the label is renamed. Optional so older localStorage entries still parse.
sourceName?: string;
model: string;
status: RecentUploadStatus;
timestamp: number;
Expand Down Expand Up @@ -128,6 +133,40 @@ export const updateRecentUploadStatus = (
return list;
};

// Rename a scan. Empty/whitespace input falls back to a sensible default so a
// scan is never left nameless.
export const renameRecentUpload = (
sessionId: string,
label: string
): RecentUpload[] => {
const list = loadRecentUploads().map((u) => {
if (u.sessionId !== sessionId) return u;
const next = label.trim();
return { ...u, label: next || friendlyScanName(u.model, u.timestamp) };
});
persistRecentUploads(list);
return list;
};

// A meaningful default name for a scan: the model it was run with plus the date,
// e.g. "ePAI · Aug 13, 2026". Far more useful in the history list than the raw
// upload filename (often "ct.nii.gz" or a cryptic export name). The user can
// rename it afterwards.
export const friendlyScanName = (model: string, timestamp: number): string => {
const who = model && model !== "None" ? model : "Scan";
let date: string;
try {
date = new Date(timestamp).toLocaleDateString(undefined, {
year: "numeric",
month: "short",
day: "numeric",
});
} catch {
date = new Date(timestamp).toISOString().slice(0, 10);
}
return `${who} · ${date}`;
};

export const formatRelativeTime = (ts: number): string => {
const mins = Math.floor((Date.now() - ts) / 60000);
if (mins < 1) return "Just now";
Expand Down
49 changes: 46 additions & 3 deletions PanTS-Demo/src/routes/UploadPage.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -67,6 +67,8 @@ const DicomPreview = lazy(() => import("../components/CtPreview/DicomPreview"));
import { API_BASE } from "../helpers/constants";
import {
addRecentUpload,
friendlyScanName,
renameRecentUpload,
formatRelativeTime,
groupUploads,
isGroupInFlight,
Expand Down Expand Up @@ -212,6 +214,18 @@ const UploadPage: React.FC = () => {
const [recentUploads, setRecentUploads] = useState<RecentUpload[]>(() =>
loadRecentUploads(),
);
// Inline rename of a scan in the history list: which one is being edited and
// the working text.
const [renamingId, setRenamingId] = useState<string | null>(null);
const [renameValue, setRenameValue] = useState("");
const startRename = (u: RecentUpload) => {
setRenamingId(u.sessionId);
setRenameValue(u.label);
};
const commitRename = () => {
if (renamingId) setRecentUploads(renameRecentUpload(renamingId, renameValue));
setRenamingId(null);
};
// Which batch's "View details" popup is open (null = none).
const [detailsBatchId, setDetailsBatchId] = useState<string | null>(null);
// Sub-state of each Active card: "waiting" | "uploading" | "queued" | "running".
Expand Down Expand Up @@ -962,15 +976,20 @@ const UploadPage: React.FC = () => {
batch?: { batchId: string; batchLabel: string },
) => {
const sid = crypto.randomUUID();
const label = (item.kind === "dicom" ? item.label : item.file.name) || sid;
const ts = Date.now();
// Keep the raw filename for reference, but name the scan meaningfully by
// default (model + date); the user can rename it later.
const sourceName = (item.kind === "dicom" ? item.label : item.file.name) || undefined;
const label = friendlyScanName(model, ts);

setRecentUploads(
addRecentUpload({
sessionId: sid,
label,
sourceName,
model,
status: "Processing",
timestamp: Date.now(),
timestamp: ts,
isReconstruction: model === "OpenVAE",
batchId: batch?.batchId,
batchLabel: batch?.batchLabel,
Expand Down Expand Up @@ -1895,7 +1914,31 @@ const UploadPage: React.FC = () => {
<div style={{ display: "flex", alignItems: "center", gap: "16px", minWidth: 0 }}>
<FileIcon />
<div style={{ minWidth: 0 }}>
<div style={{ fontFamily: "'Space Grotesk', sans-serif", fontSize: "14px", fontWeight: 600, color: "#111111" }}>{u.label}</div>
{renamingId === u.sessionId ? (
<input
autoFocus
value={renameValue}
onClick={(e) => e.stopPropagation()}
onChange={(e) => setRenameValue(e.target.value)}
onBlur={commitRename}
onKeyDown={(e) => {
if (e.key === "Enter") commitRename();
else if (e.key === "Escape") setRenamingId(null);
}}
style={{ fontFamily: "'Space Grotesk', sans-serif", fontSize: "14px", fontWeight: 600, color: "#111111", border: "1px solid rgba(0,45,114,0.3)", borderRadius: "6px", padding: "2px 6px", width: "100%", maxWidth: "260px" }}
/>
) : (
<div style={{ display: "flex", alignItems: "center", gap: "6px", minWidth: 0 }}>
<span title={u.sourceName || undefined} style={{ fontFamily: "'Space Grotesk', sans-serif", fontSize: "14px", fontWeight: 600, color: "#111111", overflow: "hidden", textOverflow: "ellipsis", whiteSpace: "nowrap" }}>{u.label}</span>
<button
title="Rename scan"
onClick={(e) => { e.stopPropagation(); startRename(u); }}
style={{ background: "none", border: "none", cursor: "pointer", padding: "2px", color: "#6a6a6a", flexShrink: 0, lineHeight: 0 }}
>
<svg width="13" height="13" viewBox="0 0 24 24" fill="none" stroke="currentColor" strokeWidth="2" strokeLinecap="round" strokeLinejoin="round"><path d="M12 20h9" /><path d="M16.5 3.5a2.121 2.121 0 0 1 3 3L7 19l-4 1 1-4Z" /></svg>
</button>
</div>
)}
<div style={{ fontFamily: "'JetBrains Mono', monospace", fontSize: "11px", color: "#6a6a6a", marginTop: "2px" }}>
{u.model ? `${u.model} · ` : ""}{formatRelativeTime(u.timestamp)}
</div>
Expand Down
Loading