Add full capability readiness matrix
This commit is contained in:
20
README.md
20
README.md
@@ -67,6 +67,10 @@ The runtime panel calls `GET /api/system/readiness` and verifies the conda
|
||||
imports required for the backend/task environment and the full MMSeg/mmcv
|
||||
environment. Command-line verification is available with
|
||||
`PYTHONPATH=backend conda run -n seg_smp python scripts/verify_runtime_envs.py --refresh`.
|
||||
The capability matrix calls `GET /api/capabilities` and summarizes readiness
|
||||
for Dataset, SegModel, YOLO, MMSeg, visual tools, analysis, and system
|
||||
operations, including task coverage, runtime status, uploaded datasets,
|
||||
heatmap/segmentation artifacts, training curves, and weight manifest status.
|
||||
|
||||
The same panel can run `POST /api/acceptance/smoke`, a lightweight live smoke
|
||||
that creates an upload dataset, uploads a label, downloads it through the
|
||||
@@ -144,6 +148,8 @@ Use `GET /api/catalog` to inspect supported models, algorithms, datasets, and
|
||||
task types discovered from the existing `Seg/` workspace.
|
||||
Use `GET /api/coverage` to inspect script-to-task coverage and task
|
||||
buildability.
|
||||
Use `GET /api/capabilities` to inspect the grouped full-function readiness
|
||||
matrix used by the web dashboard and agents.
|
||||
Use `GET /api/results/curves` to inspect parsed training curves discovered
|
||||
from YOLO, SegModel, MMSeg, visual-tool, and analysis output directories.
|
||||
|
||||
@@ -155,10 +161,10 @@ Run the local evaluation and validation agents before publishing changes:
|
||||
PYTHONPATH=backend conda run -n seg_smp python scripts/run_agents.py --build
|
||||
```
|
||||
|
||||
The validation agent checks catalog coverage, the `seg_smp` task env, the
|
||||
`seg_mmcv` MMSeg env, runtime import readiness, GPU visibility, no-weight Git
|
||||
safety, backend tests, frontend build, and live backend/frontend endpoints
|
||||
when the services are running. With live validation enabled it also runs the
|
||||
lightweight acceptance smoke above. By default it also runs the deep training
|
||||
acceptance; set `SEG_VALIDATE_DEEP=0` when a quick non-training validation
|
||||
pass is needed.
|
||||
The validation agent checks catalog coverage, the grouped capability matrix,
|
||||
the `seg_smp` task env, the `seg_mmcv` MMSeg env, runtime import readiness,
|
||||
GPU visibility, no-weight Git safety, backend tests, frontend build, and live
|
||||
backend/frontend endpoints when the services are running. With live validation
|
||||
enabled it also runs the lightweight acceptance smoke above. By default it
|
||||
also runs the deep training acceptance; set `SEG_VALIDATE_DEEP=0` when a quick
|
||||
non-training validation pass is needed.
|
||||
|
||||
@@ -44,10 +44,12 @@ def evaluate_project() -> dict:
|
||||
"loss_result_ui": "loss" in frontend_text.lower() and "heatmap" in frontend_text.lower() and "CurvePanel" 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,
|
||||
"dataset_api": "/api/datasets" in backend_text and "api_upload_dataset_files" in backend_text,
|
||||
"dataset_quality_api": "/api/datasets/{dataset_name}/validate" in backend_text and "/api/datasets/{dataset_name}/yolo-yaml" in backend_text,
|
||||
"job_progress_api": "progress_from_log_path" in backend_text and '"progress"' in backend_text,
|
||||
"runtime_readiness_api": "/api/system/readiness" in backend_text,
|
||||
"capability_matrix_api": "/api/capabilities" in backend_text,
|
||||
"runtime_bootstrap_scripts": (settings.project_root / "scripts" / "bootstrap_conda_envs.sh").exists()
|
||||
and (settings.project_root / "scripts" / "verify_runtime_envs.py").exists(),
|
||||
"curve_api": "/api/results/curves" in backend_text,
|
||||
|
||||
@@ -9,6 +9,7 @@ import urllib.request
|
||||
from pathlib import Path
|
||||
|
||||
from ..acceptance import run_deep_acceptance, run_live_acceptance
|
||||
from ..capabilities import get_capability_matrix
|
||||
from ..catalog import get_catalog
|
||||
from ..config import settings
|
||||
from ..coverage import get_coverage_report
|
||||
@@ -66,6 +67,12 @@ def validate_project(run_build: bool = False) -> dict:
|
||||
checks.append({"name": "mmseg_env_exists", "passed": settings.mmseg_conda_env in env_names, "detail": {"env": settings.mmseg_conda_env}})
|
||||
runtime_readiness = get_runtime_readiness(force=True)
|
||||
checks.append({"name": "runtime_env_readiness", "passed": runtime_readiness["passed"], "detail": runtime_readiness})
|
||||
capability_matrix = get_capability_matrix()
|
||||
checks.append({
|
||||
"name": "capability_matrix_ready",
|
||||
"passed": capability_matrix["passed"] and capability_matrix["summary"]["ready_domains"] == capability_matrix["summary"]["total_domains"],
|
||||
"detail": capability_matrix,
|
||||
})
|
||||
|
||||
smoke = _run(
|
||||
[
|
||||
@@ -105,6 +112,7 @@ def validate_project(run_build: bool = False) -> dict:
|
||||
datasets = _fetch(f"{backend_url}/api/datasets")
|
||||
live_jobs = _fetch(f"{backend_url}/api/jobs")
|
||||
live_readiness = _fetch(f"{backend_url}/api/system/readiness")
|
||||
live_capabilities = _fetch(f"{backend_url}/api/capabilities")
|
||||
live_coverage = _fetch(f"{backend_url}/api/coverage")
|
||||
live_curves = _fetch(f"{backend_url}/api/results/curves")
|
||||
frontend = _fetch(frontend_url)
|
||||
@@ -124,6 +132,11 @@ def validate_project(run_build: bool = False) -> dict:
|
||||
"passed": live_readiness["passed"] and '"passed":true' in live_readiness.get("body", "").replace(" ", ""),
|
||||
"detail": live_readiness,
|
||||
})
|
||||
checks.append({
|
||||
"name": "live_capability_matrix_api",
|
||||
"passed": live_capabilities["passed"] and '"passed":true' in live_capabilities.get("body", "").replace(" ", ""),
|
||||
"detail": live_capabilities,
|
||||
})
|
||||
checks.append({"name": "live_coverage_api", "passed": live_coverage["passed"] and '"task_build_passed":true' in live_coverage.get("body", "").replace(" ", ""), "detail": live_coverage})
|
||||
checks.append({"name": "live_training_curves_api", "passed": live_curves["passed"] and live_curves.get("body", "").lstrip().startswith("["), "detail": live_curves})
|
||||
checks.append({"name": "live_frontend_index", "passed": frontend["passed"] and "Seg Data Server" in frontend.get("body", ""), "detail": frontend})
|
||||
|
||||
299
backend/app/capabilities.py
Normal file
299
backend/app/capabilities.py
Normal file
@@ -0,0 +1,299 @@
|
||||
from __future__ import annotations
|
||||
|
||||
import time
|
||||
from typing import Any
|
||||
|
||||
from .acceptance import latest_acceptance_report, latest_deep_acceptance_report
|
||||
from .catalog import get_catalog
|
||||
from .coverage import get_coverage_report
|
||||
from .modules.dataset.service import list_uploaded_datasets
|
||||
from .modules.results.service import scan_results, scan_training_curves
|
||||
from .modules.system.service import get_gpus, get_runtime_readiness
|
||||
from .modules.weights.service import load_manifest
|
||||
|
||||
|
||||
CAPABILITY_GROUPS = [
|
||||
{
|
||||
"id": "dataset",
|
||||
"label": "Dataset",
|
||||
"description": "上传图片、label、mask,并执行预处理、配对、叠加、拼接、视频抽帧和 YOLO 数据构建。",
|
||||
"required_tasks": [
|
||||
"dataset.rename",
|
||||
"dataset.to_png",
|
||||
"dataset.resize",
|
||||
"dataset.pair",
|
||||
"dataset.rebuild_labels",
|
||||
"dataset.stack",
|
||||
"dataset.stitch",
|
||||
"dataset.video_frames",
|
||||
"dataset.yolo_txt_sort",
|
||||
"dataset.yolo_convert_png",
|
||||
"dataset.yolo_resize",
|
||||
],
|
||||
"task_prefixes": ["dataset."],
|
||||
"evidence_roles": ["segmentation"],
|
||||
},
|
||||
{
|
||||
"id": "segmodel",
|
||||
"label": "SegModel",
|
||||
"description": "SMP/SegModel 单模型和批量训练、预测、raw mask、指标、FLOPs/FPS。",
|
||||
"required_tasks": [
|
||||
"segmodel.train",
|
||||
"segmodel.batch_train",
|
||||
"segmodel.predict",
|
||||
"segmodel.batch_predict",
|
||||
"segmodel.flops",
|
||||
"segmodel.benchmark",
|
||||
"segmodel.raw_mask_check",
|
||||
"segmodel.metrics",
|
||||
],
|
||||
"task_prefixes": ["segmodel."],
|
||||
"runtime_roles": ["backend_task"],
|
||||
"families": ["segmodel"],
|
||||
},
|
||||
{
|
||||
"id": "yolo",
|
||||
"label": "YOLO",
|
||||
"description": "YOLOv8/v9/v11/v12 分割训练、上传数据集训练、预测、热度图、对比、视频预测和 raw mask。",
|
||||
"required_tasks": [
|
||||
"yolo.train",
|
||||
"yolo.train_custom",
|
||||
"yolo.predict",
|
||||
"yolo.batch_predict",
|
||||
"yolo.heatmap",
|
||||
"yolo.compare",
|
||||
"yolo.raw_mask_check",
|
||||
"yolo.video_visible",
|
||||
"yolo.video_unvisible",
|
||||
],
|
||||
"task_prefixes": ["yolo."],
|
||||
"runtime_roles": ["backend_task"],
|
||||
"families": ["yolo"],
|
||||
"evidence_roles": ["heatmap", "segmentation", "curve", "video"],
|
||||
},
|
||||
{
|
||||
"id": "mmseg",
|
||||
"label": "MMSeg",
|
||||
"description": "MMSeg 数据/算法配置生成、31 个算法、训练、预测、指标、FLOPs/FPS、绘图和 loss/mIoU 提取。",
|
||||
"required_tasks": [
|
||||
"mmseg.generate_data",
|
||||
"mmseg.generate_alg",
|
||||
"mmseg.train",
|
||||
"mmseg.predict_v1",
|
||||
"mmseg.predict_v2",
|
||||
"mmseg.metrics",
|
||||
"mmseg.flops_fps",
|
||||
"mmseg.draw",
|
||||
"mmseg.extract_loss_miou",
|
||||
],
|
||||
"task_prefixes": ["mmseg."],
|
||||
"runtime_roles": ["mmseg_full"],
|
||||
"families": ["mmseg"],
|
||||
},
|
||||
{
|
||||
"id": "visual",
|
||||
"label": "Visual Tools",
|
||||
"description": "可视化工具训练、推理、FPS、YOLO11 热度图、label 转换和 8-bit PNG 生成。",
|
||||
"required_tasks": [
|
||||
"visual.train",
|
||||
"visual.inference",
|
||||
"visual.fps",
|
||||
"visual.yolo11_heatmap_v1",
|
||||
"visual.yolo11_heatmap_v2",
|
||||
"visual.deal_labels",
|
||||
],
|
||||
"task_prefixes": ["visual."],
|
||||
"runtime_roles": ["backend_task"],
|
||||
"families": ["tool"],
|
||||
"evidence_roles": ["heatmap", "segmentation"],
|
||||
},
|
||||
{
|
||||
"id": "analysis",
|
||||
"label": "Analysis",
|
||||
"description": "合并 SegModel/MMSeg/YOLO 指标,生成 CSV、表格、图表和性能摘要。",
|
||||
"required_tasks": ["analysis.all"],
|
||||
"task_prefixes": ["analysis."],
|
||||
"families": ["analysis"],
|
||||
},
|
||||
{
|
||||
"id": "system",
|
||||
"label": "System",
|
||||
"description": "GPU 查询、环境检查、权重同步、任务日志/进度、取消和备份入口。",
|
||||
"required_tasks": ["system.backup", "system.check_graph_card"],
|
||||
"task_prefixes": ["system.", "mock."],
|
||||
"runtime_roles": ["backend_task", "mmseg_full"],
|
||||
},
|
||||
]
|
||||
|
||||
|
||||
def _tasks_for_group(all_tasks: list[str], prefixes: list[str]) -> list[str]:
|
||||
return sorted(task for task in all_tasks if any(task.startswith(prefix) for prefix in prefixes))
|
||||
|
||||
|
||||
def _artifact_matches(item: dict[str, Any], group: dict[str, Any]) -> bool:
|
||||
families = set(group.get("families", []))
|
||||
roles = set(group.get("evidence_roles", []))
|
||||
return (not families or item.get("family") in families) and (not roles or item.get("role") in roles)
|
||||
|
||||
|
||||
def _coverage_build_task_set(coverage: dict[str, Any]) -> set[str]:
|
||||
return {item["task"] for item in coverage.get("task_build_checks", []) if item.get("passed")}
|
||||
|
||||
|
||||
def _runtime_map(readiness: dict[str, Any]) -> dict[str, dict[str, Any]]:
|
||||
return {item["role"]: item for item in readiness.get("envs", [])}
|
||||
|
||||
|
||||
def get_capability_matrix() -> dict[str, Any]:
|
||||
catalog = get_catalog()
|
||||
coverage = get_coverage_report()
|
||||
readiness = get_runtime_readiness(force=False)
|
||||
results = scan_results(limit=1000)
|
||||
curves = scan_training_curves(limit=50)
|
||||
datasets = list_uploaded_datasets()
|
||||
manifest = load_manifest()
|
||||
gpus = get_gpus()
|
||||
acceptance = latest_acceptance_report()
|
||||
deep_acceptance = latest_deep_acceptance_report()
|
||||
|
||||
all_tasks = catalog["task_types"]
|
||||
buildable_tasks = _coverage_build_task_set(coverage)
|
||||
runtime_by_role = _runtime_map(readiness)
|
||||
domains = []
|
||||
|
||||
for group in CAPABILITY_GROUPS:
|
||||
group_tasks = _tasks_for_group(all_tasks, group["task_prefixes"])
|
||||
required_tasks = group["required_tasks"]
|
||||
missing_required = [task for task in required_tasks if task not in all_tasks]
|
||||
unbuildable_required = [task for task in required_tasks if task in all_tasks and task not in buildable_tasks]
|
||||
runtime_roles = group.get("runtime_roles", [])
|
||||
runtime_reports = [runtime_by_role.get(role) for role in runtime_roles if runtime_by_role.get(role)]
|
||||
runtime_ready = all(item.get("passed") for item in runtime_reports) if runtime_roles else True
|
||||
artifacts = [item for item in results if _artifact_matches(item, group)]
|
||||
group_curves = [item for item in curves if not group.get("families") or item.get("family") in group["families"]]
|
||||
evidence_count = len(artifacts) + len(group_curves)
|
||||
|
||||
if group["id"] == "dataset":
|
||||
evidence_count += sum(dataset["counts"]["images"] + dataset["counts"]["labels"] + dataset["counts"]["masks"] for dataset in datasets)
|
||||
if group["id"] == "mmseg":
|
||||
evidence_count += len(catalog.get("mmseg_algorithms", []))
|
||||
if group["id"] == "system":
|
||||
evidence_count += int(bool(gpus.get("available"))) + int(manifest.get("count", 0) > 0)
|
||||
|
||||
gaps = []
|
||||
if missing_required:
|
||||
gaps.append(f"missing tasks: {', '.join(missing_required[:4])}")
|
||||
if unbuildable_required:
|
||||
gaps.append(f"unbuildable tasks: {', '.join(unbuildable_required[:4])}")
|
||||
if runtime_roles and not runtime_ready:
|
||||
gaps.append("runtime env check failed")
|
||||
|
||||
domains.append(
|
||||
{
|
||||
"id": group["id"],
|
||||
"label": group["label"],
|
||||
"description": group["description"],
|
||||
"ready": not missing_required and not unbuildable_required and runtime_ready,
|
||||
"tasks": {
|
||||
"total": len(group_tasks),
|
||||
"required": len(required_tasks),
|
||||
"required_ready": len(required_tasks) - len(missing_required) - len(unbuildable_required),
|
||||
"examples": group_tasks[:10],
|
||||
"missing_required": missing_required,
|
||||
"unbuildable_required": unbuildable_required,
|
||||
},
|
||||
"runtime": [
|
||||
{"role": item["role"], "name": item["name"], "passed": item["passed"]}
|
||||
for item in runtime_reports
|
||||
],
|
||||
"evidence": {
|
||||
"count": evidence_count,
|
||||
"artifacts": [
|
||||
{
|
||||
"name": item["name"],
|
||||
"relative_path": item["relative_path"],
|
||||
"role": item.get("role"),
|
||||
"family": item.get("family"),
|
||||
}
|
||||
for item in artifacts[:6]
|
||||
],
|
||||
"curves": [
|
||||
{
|
||||
"name": item["name"],
|
||||
"relative_path": item["relative_path"],
|
||||
"family": item.get("family"),
|
||||
"row_count": item.get("row_count"),
|
||||
}
|
||||
for item in group_curves[:4]
|
||||
],
|
||||
},
|
||||
"gaps": gaps,
|
||||
}
|
||||
)
|
||||
|
||||
requirements = [
|
||||
{
|
||||
"id": "user_script_mapping",
|
||||
"label": "用户侧脚本映射",
|
||||
"passed": coverage["mapped_user_scripts"] == coverage["user_scripts_total"] and not coverage["unmapped_user_scripts"],
|
||||
"detail": f"{coverage['mapped_user_scripts']}/{coverage['user_scripts_total']}",
|
||||
},
|
||||
{
|
||||
"id": "runtime_readiness",
|
||||
"label": "运行环境就绪",
|
||||
"passed": readiness.get("passed", False),
|
||||
"detail": ", ".join(f"{item['name']}={item['passed']}" for item in readiness.get("envs", [])),
|
||||
},
|
||||
{
|
||||
"id": "dataset_upload",
|
||||
"label": "数据集上传/Label/Mask",
|
||||
"passed": len(datasets) >= 1,
|
||||
"detail": f"{len(datasets)} uploaded dataset(s)",
|
||||
},
|
||||
{
|
||||
"id": "yolo_heatmap",
|
||||
"label": "YOLO 热度图证据",
|
||||
"passed": any(item.get("family") == "yolo" and item.get("role") == "heatmap" for item in results),
|
||||
"detail": "heatmap artifact discovered",
|
||||
},
|
||||
{
|
||||
"id": "training_curves",
|
||||
"label": "训练 loss/指标曲线",
|
||||
"passed": len(curves) >= 1,
|
||||
"detail": f"{len(curves)} curve file(s)",
|
||||
},
|
||||
{
|
||||
"id": "deep_acceptance",
|
||||
"label": "深度训练验收",
|
||||
"passed": bool(deep_acceptance.get("passed")),
|
||||
"detail": deep_acceptance.get("run_id", "not run"),
|
||||
},
|
||||
{
|
||||
"id": "weights_manifest",
|
||||
"label": "权重清单",
|
||||
"passed": manifest.get("count", 0) >= 1,
|
||||
"detail": f"{manifest.get('count', 0)} weights indexed",
|
||||
},
|
||||
]
|
||||
|
||||
ready_domains = sum(1 for item in domains if item["ready"])
|
||||
return {
|
||||
"generated_at": time.strftime("%Y-%m-%dT%H:%M:%SZ", time.gmtime()),
|
||||
"passed": ready_domains == len(domains) and all(item["passed"] for item in requirements),
|
||||
"summary": {
|
||||
"ready_domains": ready_domains,
|
||||
"total_domains": len(domains),
|
||||
"mapped_user_scripts": coverage["mapped_user_scripts"],
|
||||
"user_scripts_total": coverage["user_scripts_total"],
|
||||
"task_build_passed": coverage["task_build_passed"],
|
||||
"uploaded_datasets": len(datasets),
|
||||
"artifacts": len(results),
|
||||
"curves": len(curves),
|
||||
"weights": manifest.get("count", 0),
|
||||
"gpus_available": bool(gpus.get("available")),
|
||||
"acceptance_passed": bool(acceptance.get("passed")),
|
||||
"deep_acceptance_passed": bool(deep_acceptance.get("passed")),
|
||||
},
|
||||
"requirements": requirements,
|
||||
"domains": domains,
|
||||
}
|
||||
@@ -10,6 +10,7 @@ from fastapi.responses import FileResponse, StreamingResponse
|
||||
|
||||
from . import db
|
||||
from .acceptance import latest_acceptance_report, latest_deep_acceptance_report, run_deep_acceptance, run_live_acceptance
|
||||
from .capabilities import get_capability_matrix
|
||||
from .catalog import get_catalog
|
||||
from .config import settings
|
||||
from .coverage import get_coverage_report
|
||||
@@ -86,6 +87,11 @@ def api_coverage() -> dict:
|
||||
return get_coverage_report()
|
||||
|
||||
|
||||
@app.get("/api/capabilities")
|
||||
def api_capabilities() -> dict:
|
||||
return get_capability_matrix()
|
||||
|
||||
|
||||
@app.get("/api/acceptance/latest")
|
||||
def api_acceptance_latest() -> dict:
|
||||
return latest_acceptance_report()
|
||||
|
||||
21
backend/tests/test_capabilities.py
Normal file
21
backend/tests/test_capabilities.py
Normal file
@@ -0,0 +1,21 @@
|
||||
from app.capabilities import get_capability_matrix
|
||||
|
||||
|
||||
def test_capability_matrix_covers_core_domains():
|
||||
matrix = get_capability_matrix()
|
||||
domains = {item["id"]: item for item in matrix["domains"]}
|
||||
|
||||
assert {"dataset", "segmodel", "yolo", "mmseg", "visual", "analysis", "system"} <= set(domains)
|
||||
assert domains["dataset"]["tasks"]["required_ready"] == domains["dataset"]["tasks"]["required"]
|
||||
assert domains["yolo"]["ready"] is True
|
||||
assert domains["mmseg"]["ready"] is True
|
||||
|
||||
|
||||
def test_capability_matrix_tracks_user_requirements():
|
||||
matrix = get_capability_matrix()
|
||||
requirements = {item["id"]: item for item in matrix["requirements"]}
|
||||
|
||||
assert requirements["user_script_mapping"]["passed"] is True
|
||||
assert requirements["runtime_readiness"]["passed"] is True
|
||||
assert requirements["yolo_heatmap"]["passed"] is True
|
||||
assert requirements["training_curves"]["passed"] is True
|
||||
@@ -193,6 +193,44 @@ type RuntimeReadinessPayload = {
|
||||
};
|
||||
};
|
||||
|
||||
type CapabilityDomain = {
|
||||
id: string;
|
||||
label: string;
|
||||
ready: boolean;
|
||||
tasks: {
|
||||
total: number;
|
||||
required: number;
|
||||
required_ready: number;
|
||||
examples: string[];
|
||||
missing_required: string[];
|
||||
unbuildable_required: string[];
|
||||
};
|
||||
runtime: Array<{ role: string; name: string; passed: boolean }>;
|
||||
evidence: {
|
||||
count: number;
|
||||
artifacts: Array<{ name: string; relative_path: string; role?: string; family?: string }>;
|
||||
curves: Array<{ name: string; relative_path: string; family?: string; row_count?: number }>;
|
||||
};
|
||||
gaps: string[];
|
||||
};
|
||||
|
||||
type CapabilityPayload = {
|
||||
passed: boolean;
|
||||
generated_at: string;
|
||||
summary: {
|
||||
ready_domains: number;
|
||||
total_domains: number;
|
||||
mapped_user_scripts: number;
|
||||
user_scripts_total: number;
|
||||
uploaded_datasets: number;
|
||||
artifacts: number;
|
||||
curves: number;
|
||||
weights: number;
|
||||
};
|
||||
requirements: Array<{ id: string; label: string; passed: boolean; detail: string }>;
|
||||
domains: CapabilityDomain[];
|
||||
};
|
||||
|
||||
async function api<T>(path: string, init?: RequestInit): Promise<T> {
|
||||
const res = await fetch(`${API_BASE}${path}`, {
|
||||
headers: { "Content-Type": "application/json" },
|
||||
@@ -266,14 +304,16 @@ function useData() {
|
||||
const [acceptance, setAcceptance] = useState<AcceptancePayload | null>(null);
|
||||
const [deepAcceptance, setDeepAcceptance] = useState<DeepAcceptancePayload | null>(null);
|
||||
const [runtimeReadiness, setRuntimeReadiness] = useState<RuntimeReadinessPayload | null>(null);
|
||||
const [capabilities, setCapabilities] = useState<CapabilityPayload | null>(null);
|
||||
const [error, setError] = useState<string>("");
|
||||
|
||||
async function refresh() {
|
||||
try {
|
||||
const [catalogNext, gpusNext, readinessNext, jobsNext, resultsNext, curvesNext, datasetsNext, coverageNext, acceptanceNext, deepAcceptanceNext] = await Promise.all([
|
||||
const [catalogNext, gpusNext, readinessNext, capabilitiesNext, jobsNext, resultsNext, curvesNext, datasetsNext, coverageNext, acceptanceNext, deepAcceptanceNext] = await Promise.all([
|
||||
api<Catalog>("/api/catalog"),
|
||||
api<GpuPayload>("/api/system/gpus"),
|
||||
api<RuntimeReadinessPayload>("/api/system/readiness"),
|
||||
api<CapabilityPayload>("/api/capabilities"),
|
||||
api<Job[]>("/api/jobs"),
|
||||
api<ResultItem[]>("/api/results"),
|
||||
api<TrainingCurve[]>("/api/results/curves"),
|
||||
@@ -285,6 +325,7 @@ function useData() {
|
||||
setCatalog(catalogNext);
|
||||
setGpus(gpusNext);
|
||||
setRuntimeReadiness(readinessNext);
|
||||
setCapabilities(capabilitiesNext);
|
||||
setJobs(jobsNext);
|
||||
setResults(resultsNext.slice(0, 80));
|
||||
setCurves(curvesNext.slice(0, 12));
|
||||
@@ -316,7 +357,7 @@ function useData() {
|
||||
return () => window.clearInterval(timer);
|
||||
}, []);
|
||||
|
||||
return { catalog, gpus, runtimeReadiness, jobs, results, curves, datasets, datasetValidations, coverage, acceptance, deepAcceptance, error, refresh };
|
||||
return { catalog, gpus, runtimeReadiness, capabilities, jobs, results, curves, datasets, datasetValidations, coverage, acceptance, deepAcceptance, error, refresh };
|
||||
}
|
||||
|
||||
function StatusPill({ status }: { status: string }) {
|
||||
@@ -339,7 +380,7 @@ function JobProgressBar({ progress }: { progress?: JobProgress }) {
|
||||
}
|
||||
|
||||
function App() {
|
||||
const { catalog, gpus, runtimeReadiness, jobs, results, curves, datasets, datasetValidations, coverage, acceptance, deepAcceptance, error, refresh } = useData();
|
||||
const { catalog, gpus, runtimeReadiness, capabilities, jobs, results, curves, datasets, datasetValidations, coverage, acceptance, deepAcceptance, error, refresh } = useData();
|
||||
const [taskType, setTaskType] = useState("mock.echo");
|
||||
const [params, setParams] = useState(JSON.stringify(defaultParams["mock.echo"], null, 2));
|
||||
const [selectedJob, setSelectedJob] = useState<Job | null>(null);
|
||||
@@ -529,6 +570,7 @@ function App() {
|
||||
</div>
|
||||
<nav>
|
||||
<a href="#jobs"><Terminal size={18} />任务</a>
|
||||
<a href="#capabilities"><Gauge size={18} />能力</a>
|
||||
<a href="#datasets"><Boxes size={18} />数据集</a>
|
||||
<a href="#gpu"><Cpu size={18} />GPU</a>
|
||||
<a href="#runtime"><ShieldCheck size={18} />环境</a>
|
||||
@@ -579,6 +621,48 @@ function App() {
|
||||
</div>
|
||||
</section>
|
||||
|
||||
<section className="panel" id="capabilities">
|
||||
<div className="panelHead">
|
||||
<div>
|
||||
<p className="eyebrow">Capability Matrix</p>
|
||||
<h2>全功能矩阵</h2>
|
||||
</div>
|
||||
<StatusPill status={capabilities?.passed ? "success" : "queued"} />
|
||||
</div>
|
||||
<div className="capSummary">
|
||||
<div><span>域</span><strong>{capabilities?.summary.ready_domains ?? 0}/{capabilities?.summary.total_domains ?? 0}</strong></div>
|
||||
<div><span>脚本</span><strong>{capabilities?.summary.mapped_user_scripts ?? 0}/{capabilities?.summary.user_scripts_total ?? 0}</strong></div>
|
||||
<div><span>数据集</span><strong>{capabilities?.summary.uploaded_datasets ?? 0}</strong></div>
|
||||
<div><span>结果</span><strong>{capabilities?.summary.artifacts ?? 0}</strong></div>
|
||||
<div><span>曲线</span><strong>{capabilities?.summary.curves ?? 0}</strong></div>
|
||||
<div><span>权重</span><strong>{capabilities?.summary.weights ?? 0}</strong></div>
|
||||
</div>
|
||||
<div className="capabilityGrid">
|
||||
{(capabilities?.domains ?? []).map((domain) => (
|
||||
<div key={domain.id} className={`capCard ${domain.ready ? "ok" : "bad"}`}>
|
||||
<div className="capHead">
|
||||
<strong>{domain.label}</strong>
|
||||
<span>{domain.ready ? "READY" : "CHECK"}</span>
|
||||
</div>
|
||||
<div className="capNumbers">
|
||||
<span>{domain.tasks.required_ready}/{domain.tasks.required} required</span>
|
||||
<span>{domain.evidence.count} evidence</span>
|
||||
</div>
|
||||
<div className="capTags">
|
||||
{domain.tasks.examples.slice(0, 4).map((task) => <small key={task}>{task}</small>)}
|
||||
</div>
|
||||
</div>
|
||||
))}
|
||||
</div>
|
||||
<div className="requirementStrip">
|
||||
{(capabilities?.requirements ?? []).map((item) => (
|
||||
<span key={item.id} className={item.passed ? "ok" : "bad"} title={item.detail}>
|
||||
{item.label}
|
||||
</span>
|
||||
))}
|
||||
</div>
|
||||
</section>
|
||||
|
||||
<section className="grid two" id="jobs">
|
||||
<div className="panel taskPanel">
|
||||
<div className="panelHead">
|
||||
|
||||
@@ -186,6 +186,131 @@ h2 {
|
||||
margin-bottom: 16px;
|
||||
}
|
||||
|
||||
.capSummary {
|
||||
display: grid;
|
||||
grid-template-columns: repeat(6, minmax(0, 1fr));
|
||||
gap: 10px;
|
||||
margin-bottom: 12px;
|
||||
}
|
||||
|
||||
.capSummary div {
|
||||
min-width: 0;
|
||||
padding: 10px;
|
||||
border-radius: 7px;
|
||||
border: 1px solid var(--line);
|
||||
background: #101310;
|
||||
}
|
||||
|
||||
.capSummary span,
|
||||
.capSummary strong {
|
||||
display: block;
|
||||
}
|
||||
|
||||
.capSummary span {
|
||||
color: var(--muted);
|
||||
font-size: 11px;
|
||||
}
|
||||
|
||||
.capSummary strong {
|
||||
margin-top: 3px;
|
||||
font-size: 20px;
|
||||
}
|
||||
|
||||
.capabilityGrid {
|
||||
display: grid;
|
||||
grid-template-columns: repeat(7, minmax(0, 1fr));
|
||||
gap: 9px;
|
||||
}
|
||||
|
||||
.capCard {
|
||||
min-width: 0;
|
||||
display: grid;
|
||||
gap: 8px;
|
||||
padding: 10px;
|
||||
border-radius: 7px;
|
||||
border: 1px solid var(--line);
|
||||
background: #101310;
|
||||
}
|
||||
|
||||
.capCard.ok {
|
||||
border-color: rgba(157, 226, 111, 0.32);
|
||||
}
|
||||
|
||||
.capCard.bad {
|
||||
border-color: rgba(240, 113, 103, 0.55);
|
||||
}
|
||||
|
||||
.capHead {
|
||||
min-width: 0;
|
||||
display: grid;
|
||||
grid-template-columns: minmax(0, 1fr) auto;
|
||||
gap: 8px;
|
||||
}
|
||||
|
||||
.capHead strong {
|
||||
overflow: hidden;
|
||||
text-overflow: ellipsis;
|
||||
white-space: nowrap;
|
||||
}
|
||||
|
||||
.capHead span {
|
||||
color: var(--green);
|
||||
font-size: 11px;
|
||||
font-weight: 760;
|
||||
}
|
||||
|
||||
.capCard.bad .capHead span {
|
||||
color: var(--red);
|
||||
}
|
||||
|
||||
.capNumbers {
|
||||
display: grid;
|
||||
gap: 3px;
|
||||
color: var(--muted);
|
||||
font-size: 11px;
|
||||
}
|
||||
|
||||
.capTags {
|
||||
display: grid;
|
||||
gap: 4px;
|
||||
}
|
||||
|
||||
.capTags small {
|
||||
min-width: 0;
|
||||
padding: 4px 5px;
|
||||
border-radius: 4px;
|
||||
background: #080a08;
|
||||
overflow: hidden;
|
||||
text-overflow: ellipsis;
|
||||
white-space: nowrap;
|
||||
}
|
||||
|
||||
.requirementStrip {
|
||||
display: flex;
|
||||
flex-wrap: wrap;
|
||||
gap: 6px;
|
||||
margin-top: 12px;
|
||||
}
|
||||
|
||||
.requirementStrip span {
|
||||
padding: 5px 7px;
|
||||
border: 1px solid var(--line);
|
||||
border-radius: 999px;
|
||||
color: var(--muted);
|
||||
background: #101310;
|
||||
font-size: 11px;
|
||||
}
|
||||
|
||||
.requirementStrip span.ok {
|
||||
color: var(--green);
|
||||
border-color: rgba(157, 226, 111, 0.32);
|
||||
}
|
||||
|
||||
.requirementStrip span.bad {
|
||||
color: var(--red);
|
||||
border-color: rgba(240, 113, 103, 0.55);
|
||||
}
|
||||
|
||||
.metric, .panel {
|
||||
border: 1px solid var(--line);
|
||||
background: rgba(21, 24, 21, 0.94);
|
||||
@@ -958,7 +1083,7 @@ meter {
|
||||
}
|
||||
|
||||
nav {
|
||||
grid-template-columns: repeat(6, minmax(0, 1fr));
|
||||
grid-template-columns: repeat(4, minmax(0, 1fr));
|
||||
align-items: center;
|
||||
}
|
||||
|
||||
@@ -985,8 +1110,11 @@ meter {
|
||||
}
|
||||
|
||||
.metrics,
|
||||
.capSummary,
|
||||
.capabilityGrid,
|
||||
.grid.two,
|
||||
.grid.three,
|
||||
.grid.four,
|
||||
.taskColumns {
|
||||
grid-template-columns: 1fr;
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user