Add filterable artifact browser

This commit is contained in:
2026-06-30 15:41:58 +08:00
parent 90dccbea0e
commit 41cb0235cd
4 changed files with 148 additions and 11 deletions

View File

@@ -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 />

View File

@@ -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;