studio: humanize ETA display for long training runs (#4608)

* studio: humanize ETA display for long training runs

When training takes hours or days, the ETA displayed raw minutes
(e.g. '560m 50s'). This changes the format to:
- Under 1 hour: Xm Ys (unchanged)
- 1-24 hours: Xh Ym Zs
- Over 24 hours: Xd Xh Xm

* Fix formatDuration edge cases and consolidate duplicate for PR #4608

- Guard NaN/Infinity inputs with Number.isFinite() (matches formatNumber in same file)
- Add sub-minute branch so 30s displays as "30s" instead of "0m 30s"
- Accept undefined in type signature to match formatNumber pattern
- Remove duplicate formatDuration from history-card-grid.tsx and import the shared one

---------

Co-authored-by: Daniel Han <danielhanchen@gmail.com>
This commit is contained in:
Radouane Elhajali 2026-03-26 14:55:54 +01:00 committed by GitHub
commit a6fe743ebe
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
2 changed files with 11 additions and 15 deletions

View file

@ -14,6 +14,7 @@ import {
import { Button } from "@/components/ui/button";
import type { TrainingRunSummary } from "@/features/training";
import { deleteTrainingRun, listTrainingRuns } from "@/features/training";
import { formatDuration } from "@/features/studio/sections/progress-section-lib";
import { cn } from "@/lib/utils";
import { Delete02Icon } from "@hugeicons/core-free-icons";
import { HugeiconsIcon } from "@hugeicons/react";
@ -129,16 +130,6 @@ function formatRelativeTime(isoDate: string): string {
return `${days}d ago`;
}
function formatDuration(seconds: number | null): string {
if (seconds == null) return "--";
const total = Math.floor(seconds);
if (total < 60) return `${total}s`;
const min = Math.floor(total / 60);
const sec = total % 60;
if (min < 60) return `${min}m ${sec}s`;
const hrs = Math.floor(min / 60);
return `${hrs}h ${min % 60}m`;
}
interface HistoryCardGridProps {
onSelectRun: (runId: string) => void;

View file

@ -35,12 +35,17 @@ export const phaseColors: Record<TrainingPhase, string> = {
stopped: "bg-muted text-muted-foreground",
};
export function formatDuration(seconds: number | null): string {
if (seconds == null || seconds < 0) return "--";
export function formatDuration(seconds: number | null | undefined): string {
if (seconds == null || !Number.isFinite(seconds) || seconds < 0) return "--";
const total = Math.floor(seconds);
const min = Math.floor(total / 60);
const sec = total % 60;
return `${min}m ${sec}s`;
const d = Math.floor(total / 86400);
const h = Math.floor((total % 86400) / 3600);
const m = Math.floor((total % 3600) / 60);
const s = total % 60;
if (d > 0) return `${d}d ${h}h ${m}m`;
if (h > 0) return `${h}h ${m}m ${s}s`;
if (m > 0) return `${m}m ${s}s`;
return `${s}s`;
}
export function formatNumber(value: number | null | undefined, digits: number): string {