Add filterable artifact browser
This commit is contained in:
@@ -57,8 +57,9 @@ written under `var/custom_yolo_runs` and are scanned by the results dashboard.
|
|||||||
When a dataset is selected, the dataset panel shows its custom YOLO `best.pt`,
|
When a dataset is selected, the dataset panel shows its custom YOLO `best.pt`,
|
||||||
prediction previews, heatmap previews, and inline training curve previews.
|
prediction previews, heatmap previews, and inline training curve previews.
|
||||||
Segmentation previews, YOLO heatmaps, and loss/metric artifacts are grouped on
|
Segmentation previews, YOLO heatmaps, and loss/metric artifacts are grouped on
|
||||||
the results dashboard, and YOLO-style `results.csv` files are parsed into
|
the results dashboard. The artifact browser can filter by model family and
|
||||||
lightweight training curves.
|
artifact role, and YOLO-style `results.csv` files are parsed into lightweight
|
||||||
|
training curves.
|
||||||
Job APIs and the SSE log stream also expose structured progress parsed from
|
Job APIs and the SSE log stream also expose structured progress parsed from
|
||||||
YOLO, MMSeg/MMEngine, SegModel-style epoch logs, and generic tqdm percentages,
|
YOLO, MMSeg/MMEngine, SegModel-style epoch logs, and generic tqdm percentages,
|
||||||
so the queue and live log panel can show stage, epoch/iteration, and percent
|
so the queue and live log panel can show stage, epoch/iteration, and percent
|
||||||
|
|||||||
@@ -50,6 +50,11 @@ def evaluate_project() -> dict:
|
|||||||
and "datasetOutputCurve" in frontend_text
|
and "datasetOutputCurve" in frontend_text
|
||||||
and "MiniCurvePlot" in frontend_text,
|
and "MiniCurvePlot" in frontend_text,
|
||||||
"loss_result_ui": "loss" in frontend_text.lower() and "heatmap" in frontend_text.lower() and "CurvePanel" in frontend_text,
|
"loss_result_ui": "loss" in frontend_text.lower() and "heatmap" in frontend_text.lower() and "CurvePanel" in frontend_text,
|
||||||
|
"result_browser_filter_ui": "ResultBrowser" in frontend_text
|
||||||
|
and "resultFamilyFilter" in frontend_text
|
||||||
|
and "resultRoleFilter" in frontend_text
|
||||||
|
and "familyOptions" in frontend_text
|
||||||
|
and "roleOptions" in frontend_text,
|
||||||
"job_progress_ui": "JobProgressBar" in frontend_text and "progressTrack" in frontend_text,
|
"job_progress_ui": "JobProgressBar" in frontend_text and "progressTrack" in frontend_text,
|
||||||
"runtime_readiness_ui": "runtimeReadiness" in frontend_text and "环境就绪" in frontend_text,
|
"runtime_readiness_ui": "runtimeReadiness" in frontend_text and "环境就绪" in frontend_text,
|
||||||
"capability_matrix_ui": "capabilities" in frontend_text and "全功能矩阵" in frontend_text,
|
"capability_matrix_ui": "capabilities" in frontend_text and "全功能矩阵" in frontend_text,
|
||||||
|
|||||||
@@ -424,6 +424,8 @@ function App() {
|
|||||||
const [datasetDescription, setDatasetDescription] = useState("");
|
const [datasetDescription, setDatasetDescription] = useState("");
|
||||||
const [selectedDatasetName, setSelectedDatasetName] = useState("");
|
const [selectedDatasetName, setSelectedDatasetName] = useState("");
|
||||||
const [selectedCurvePath, setSelectedCurvePath] = useState("");
|
const [selectedCurvePath, setSelectedCurvePath] = useState("");
|
||||||
|
const [resultFamilyFilter, setResultFamilyFilter] = useState("all");
|
||||||
|
const [resultRoleFilter, setResultRoleFilter] = useState("all");
|
||||||
const [uploadKind, setUploadKind] = useState<"images" | "labels" | "masks">("images");
|
const [uploadKind, setUploadKind] = useState<"images" | "labels" | "masks">("images");
|
||||||
const [uploadFiles, setUploadFiles] = useState<FileList | null>(null);
|
const [uploadFiles, setUploadFiles] = useState<FileList | null>(null);
|
||||||
const [agentValidation, setAgentValidation] = useState<ValidationAgentPayload | null>(null);
|
const [agentValidation, setAgentValidation] = useState<ValidationAgentPayload | null>(null);
|
||||||
@@ -472,6 +474,15 @@ function App() {
|
|||||||
};
|
};
|
||||||
}, [curves, results, selectedDataset]);
|
}, [curves, results, selectedDataset]);
|
||||||
const selectedYoloWeightReady = Boolean(selectedYoloOutputs.bestWeight);
|
const selectedYoloWeightReady = Boolean(selectedYoloOutputs.bestWeight);
|
||||||
|
const resultFamilyOptions = useMemo(() => ["all", ...Array.from(new Set(results.map((item) => item.family ?? "artifact"))).sort()], [results]);
|
||||||
|
const resultRoleOptions = useMemo(() => ["all", ...Array.from(new Set(results.map((item) => item.role ?? "artifact"))).sort()], [results]);
|
||||||
|
const filteredResults = useMemo(
|
||||||
|
() => results.filter((item) =>
|
||||||
|
(resultFamilyFilter === "all" || (item.family ?? "artifact") === resultFamilyFilter) &&
|
||||||
|
(resultRoleFilter === "all" || (item.role ?? "artifact") === resultRoleFilter)
|
||||||
|
),
|
||||||
|
[resultFamilyFilter, resultRoleFilter, results]
|
||||||
|
);
|
||||||
|
|
||||||
function pickTask(next: string) {
|
function pickTask(next: string) {
|
||||||
setTaskType(next);
|
setTaskType(next);
|
||||||
@@ -1215,14 +1226,16 @@ function App() {
|
|||||||
</div>
|
</div>
|
||||||
<BarChart3 size={22} />
|
<BarChart3 size={22} />
|
||||||
</div>
|
</div>
|
||||||
<div className="resultList">
|
<ResultBrowser
|
||||||
{results.map((item) => (
|
results={filteredResults}
|
||||||
<a key={item.path} href={`${API_BASE}/api/artifacts/${item.relative_path}`} target="_blank" rel="noreferrer">
|
total={results.length}
|
||||||
<span>{item.name}</span>
|
familyOptions={resultFamilyOptions}
|
||||||
<small>{formatBytes(item.size)}</small>
|
roleOptions={resultRoleOptions}
|
||||||
</a>
|
familyFilter={resultFamilyFilter}
|
||||||
))}
|
roleFilter={resultRoleFilter}
|
||||||
</div>
|
onFamilyFilter={setResultFamilyFilter}
|
||||||
|
onRoleFilter={setResultRoleFilter}
|
||||||
|
/>
|
||||||
</div>
|
</div>
|
||||||
</section>
|
</section>
|
||||||
</section>
|
</section>
|
||||||
@@ -1262,6 +1275,61 @@ function ResultPreview({ results }: { results: ResultItem[] }) {
|
|||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
function ResultBrowser({
|
||||||
|
results,
|
||||||
|
total,
|
||||||
|
familyOptions,
|
||||||
|
roleOptions,
|
||||||
|
familyFilter,
|
||||||
|
roleFilter,
|
||||||
|
onFamilyFilter,
|
||||||
|
onRoleFilter
|
||||||
|
}: {
|
||||||
|
results: ResultItem[];
|
||||||
|
total: number;
|
||||||
|
familyOptions: string[];
|
||||||
|
roleOptions: string[];
|
||||||
|
familyFilter: string;
|
||||||
|
roleFilter: string;
|
||||||
|
onFamilyFilter: (value: string) => void;
|
||||||
|
onRoleFilter: (value: string) => void;
|
||||||
|
}) {
|
||||||
|
return (
|
||||||
|
<div className="resultBrowser">
|
||||||
|
<div className="resultFilters">
|
||||||
|
<label>
|
||||||
|
<span>Family</span>
|
||||||
|
<select value={familyFilter} onChange={(event) => onFamilyFilter(event.target.value)} aria-label="result family">
|
||||||
|
{familyOptions.map((option) => (
|
||||||
|
<option key={option} value={option}>{resultFilterLabel(option)}</option>
|
||||||
|
))}
|
||||||
|
</select>
|
||||||
|
</label>
|
||||||
|
<label>
|
||||||
|
<span>Role</span>
|
||||||
|
<select value={roleFilter} onChange={(event) => onRoleFilter(event.target.value)} aria-label="result role">
|
||||||
|
{roleOptions.map((option) => (
|
||||||
|
<option key={option} value={option}>{resultFilterLabel(option)}</option>
|
||||||
|
))}
|
||||||
|
</select>
|
||||||
|
</label>
|
||||||
|
</div>
|
||||||
|
<div className="resultSummary">
|
||||||
|
<strong>{results.length}</strong>
|
||||||
|
<span>/ {total} artifacts</span>
|
||||||
|
</div>
|
||||||
|
<div className="resultList">
|
||||||
|
{results.map((item) => (
|
||||||
|
<a key={item.path} href={`${API_BASE}/api/artifacts/${item.relative_path}`} target="_blank" rel="noreferrer">
|
||||||
|
<span>{item.name}</span>
|
||||||
|
<small>{item.family ?? "artifact"} · {item.role ?? "artifact"} · {formatBytes(item.size)}</small>
|
||||||
|
</a>
|
||||||
|
))}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
function DatasetQuality({ validation }: { validation: DatasetValidation }) {
|
function DatasetQuality({ validation }: { validation: DatasetValidation }) {
|
||||||
return (
|
return (
|
||||||
<div className="qualityBox">
|
<div className="qualityBox">
|
||||||
@@ -1416,6 +1484,10 @@ function curveColor(index: number) {
|
|||||||
return ["#9de26f", "#73d2de", "#d3b35b", "#7aa2ff", "#f07167"][index % 5];
|
return ["#9de26f", "#73d2de", "#d3b35b", "#7aa2ff", "#f07167"][index % 5];
|
||||||
}
|
}
|
||||||
|
|
||||||
|
function resultFilterLabel(value: string) {
|
||||||
|
return value === "all" ? "All" : value;
|
||||||
|
}
|
||||||
|
|
||||||
createRoot(document.getElementById("root")!).render(
|
createRoot(document.getElementById("root")!).render(
|
||||||
<React.StrictMode>
|
<React.StrictMode>
|
||||||
<App />
|
<App />
|
||||||
|
|||||||
@@ -936,8 +936,9 @@ textarea {
|
|||||||
}
|
}
|
||||||
|
|
||||||
.jobRow, .resultList a {
|
.jobRow, .resultList a {
|
||||||
|
min-width: 0;
|
||||||
display: grid;
|
display: grid;
|
||||||
grid-template-columns: 1fr auto;
|
grid-template-columns: minmax(0, 1fr) minmax(0, auto);
|
||||||
gap: 8px;
|
gap: 8px;
|
||||||
text-align: left;
|
text-align: left;
|
||||||
padding: 10px;
|
padding: 10px;
|
||||||
@@ -952,6 +953,64 @@ textarea {
|
|||||||
grid-template-columns: 1fr;
|
grid-template-columns: 1fr;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
.resultList span,
|
||||||
|
.resultList small {
|
||||||
|
min-width: 0;
|
||||||
|
overflow: hidden;
|
||||||
|
text-overflow: ellipsis;
|
||||||
|
white-space: nowrap;
|
||||||
|
}
|
||||||
|
|
||||||
|
.resultList small {
|
||||||
|
color: var(--muted);
|
||||||
|
}
|
||||||
|
|
||||||
|
.resultBrowser {
|
||||||
|
display: grid;
|
||||||
|
gap: 10px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.resultFilters {
|
||||||
|
display: grid;
|
||||||
|
grid-template-columns: repeat(2, minmax(0, 1fr));
|
||||||
|
gap: 8px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.resultFilters label {
|
||||||
|
min-width: 0;
|
||||||
|
display: grid;
|
||||||
|
gap: 6px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.resultFilters span {
|
||||||
|
color: var(--muted);
|
||||||
|
font-size: 11px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.resultFilters select {
|
||||||
|
width: 100%;
|
||||||
|
height: 36px;
|
||||||
|
padding: 0 9px;
|
||||||
|
border-radius: 6px;
|
||||||
|
border: 1px solid var(--line);
|
||||||
|
background: var(--field);
|
||||||
|
color: var(--ink);
|
||||||
|
}
|
||||||
|
|
||||||
|
.resultSummary {
|
||||||
|
display: flex;
|
||||||
|
align-items: baseline;
|
||||||
|
gap: 5px;
|
||||||
|
color: var(--muted);
|
||||||
|
font-size: 12px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.resultSummary strong {
|
||||||
|
color: var(--green);
|
||||||
|
font-size: 24px;
|
||||||
|
line-height: 1;
|
||||||
|
}
|
||||||
|
|
||||||
.jobRowTop {
|
.jobRowTop {
|
||||||
min-width: 0;
|
min-width: 0;
|
||||||
display: grid;
|
display: grid;
|
||||||
|
|||||||
Reference in New Issue
Block a user