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`,
|
||||
prediction previews, heatmap previews, and inline training curve previews.
|
||||
Segmentation previews, YOLO heatmaps, and loss/metric artifacts are grouped on
|
||||
the results dashboard, and YOLO-style `results.csv` files are parsed into
|
||||
lightweight training curves.
|
||||
the results dashboard. The artifact browser can filter by model family and
|
||||
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
|
||||
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
|
||||
|
||||
@@ -50,6 +50,11 @@ def evaluate_project() -> dict:
|
||||
and "datasetOutputCurve" 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,
|
||||
"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,
|
||||
"runtime_readiness_ui": "runtimeReadiness" 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 [selectedDatasetName, setSelectedDatasetName] = useState("");
|
||||
const [selectedCurvePath, setSelectedCurvePath] = useState("");
|
||||
const [resultFamilyFilter, setResultFamilyFilter] = useState("all");
|
||||
const [resultRoleFilter, setResultRoleFilter] = useState("all");
|
||||
const [uploadKind, setUploadKind] = useState<"images" | "labels" | "masks">("images");
|
||||
const [uploadFiles, setUploadFiles] = useState<FileList | null>(null);
|
||||
const [agentValidation, setAgentValidation] = useState<ValidationAgentPayload | null>(null);
|
||||
@@ -472,6 +474,15 @@ function App() {
|
||||
};
|
||||
}, [curves, results, selectedDataset]);
|
||||
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) {
|
||||
setTaskType(next);
|
||||
@@ -1215,14 +1226,16 @@ function App() {
|
||||
</div>
|
||||
<BarChart3 size={22} />
|
||||
</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>{formatBytes(item.size)}</small>
|
||||
</a>
|
||||
))}
|
||||
</div>
|
||||
<ResultBrowser
|
||||
results={filteredResults}
|
||||
total={results.length}
|
||||
familyOptions={resultFamilyOptions}
|
||||
roleOptions={resultRoleOptions}
|
||||
familyFilter={resultFamilyFilter}
|
||||
roleFilter={resultRoleFilter}
|
||||
onFamilyFilter={setResultFamilyFilter}
|
||||
onRoleFilter={setResultRoleFilter}
|
||||
/>
|
||||
</div>
|
||||
</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 }) {
|
||||
return (
|
||||
<div className="qualityBox">
|
||||
@@ -1416,6 +1484,10 @@ function curveColor(index: number) {
|
||||
return ["#9de26f", "#73d2de", "#d3b35b", "#7aa2ff", "#f07167"][index % 5];
|
||||
}
|
||||
|
||||
function resultFilterLabel(value: string) {
|
||||
return value === "all" ? "All" : value;
|
||||
}
|
||||
|
||||
createRoot(document.getElementById("root")!).render(
|
||||
<React.StrictMode>
|
||||
<App />
|
||||
|
||||
@@ -936,8 +936,9 @@ textarea {
|
||||
}
|
||||
|
||||
.jobRow, .resultList a {
|
||||
min-width: 0;
|
||||
display: grid;
|
||||
grid-template-columns: 1fr auto;
|
||||
grid-template-columns: minmax(0, 1fr) minmax(0, auto);
|
||||
gap: 8px;
|
||||
text-align: left;
|
||||
padding: 10px;
|
||||
@@ -952,6 +953,64 @@ textarea {
|
||||
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 {
|
||||
min-width: 0;
|
||||
display: grid;
|
||||
|
||||
Reference in New Issue
Block a user