diff --git a/visionA-frontend/src/app/models/[id]/model-detail-client.tsx b/visionA-frontend/src/app/models/[id]/model-detail-client.tsx index b66332f..433b5f1 100644 --- a/visionA-frontend/src/app/models/[id]/model-detail-client.tsx +++ b/visionA-frontend/src/app/models/[id]/model-detail-client.tsx @@ -37,6 +37,14 @@ function formatFileSize(bytes: number): string { return `${(bytes / (1024 * 1024 * 1024)).toFixed(2)} GB`; } +/** input_shape(NCHW,如 [1,3,224,224])→ 易讀字串 `1 × 3 × 224 × 224`。 */ +function formatInputShape(shape: number[]): string { + return shape.join(" × "); +} + +/** classes 多時只展示前幾個 Badge,其餘以 `+N` 表示,避免塞爆版面。 */ +const CLASSES_PREVIEW_LIMIT = 8; + interface ModelDetailClientProps { id: string; } @@ -156,6 +164,23 @@ export function ModelDetailClient({ id }: ModelDetailClientProps) { label={t("models.createdAt")} value={selectedModel.createdAt ? new Date(selectedModel.createdAt).toLocaleString() : "—"} /> + {/* B4 轉檔 metadata:有值才顯示(沒值時 backend omitempty,normalize 後為 undefined)。 */} + {selectedModel.framework && ( + {selectedModel.framework}} + /> + )} + {selectedModel.inputShape && selectedModel.inputShape.length > 0 && ( + + {formatInputShape(selectedModel.inputShape)} + + } + /> + )} {selectedModel.version && ( )} @@ -166,6 +191,30 @@ export function ModelDetailClient({ id }: ModelDetailClientProps) { /> )} + + {/* classes 可能很長,獨立成 full-width 區塊,用 Badge 列出(超量只顯示前 N 個 + 餘量)。 */} + {selectedModel.classes && selectedModel.classes.length > 0 && ( +
+
+ {t("models.detail.classes")} + + {selectedModel.classes.length} {t("models.detail.classesCountSuffix")} + +
+
+ {selectedModel.classes.slice(0, CLASSES_PREVIEW_LIMIT).map((c, i) => ( + + {c} + + ))} + {selectedModel.classes.length > CLASSES_PREVIEW_LIMIT && ( + + +{selectedModel.classes.length - CLASSES_PREVIEW_LIMIT} + + )} +
+
+ )} diff --git a/visionA-frontend/src/lib/i18n/dictionaries/en.ts b/visionA-frontend/src/lib/i18n/dictionaries/en.ts index 082bc6e..a197a7c 100644 --- a/visionA-frontend/src/lib/i18n/dictionaries/en.ts +++ b/visionA-frontend/src/lib/i18n/dictionaries/en.ts @@ -195,6 +195,10 @@ export const en: Dictionary = { "models.detail.checksum": "Checksum", "models.detail.supportedChips": "Supported chips", "models.detail.deployToDevice": "Deploy to device", + "models.detail.inputShape": "Input shape", + "models.detail.framework": "Framework", + "models.detail.classes": "Classes", + "models.detail.classesCountSuffix": "classes", // ── Model Upload Dialog ── "models.upload.button": "Upload model", diff --git a/visionA-frontend/src/lib/i18n/dictionaries/zh-Hant.ts b/visionA-frontend/src/lib/i18n/dictionaries/zh-Hant.ts index c363ef1..f1f7ef4 100644 --- a/visionA-frontend/src/lib/i18n/dictionaries/zh-Hant.ts +++ b/visionA-frontend/src/lib/i18n/dictionaries/zh-Hant.ts @@ -196,6 +196,10 @@ export const zhHant: Dictionary = { "models.detail.checksum": "校驗碼", "models.detail.supportedChips": "支援晶片", "models.detail.deployToDevice": "部署至裝置", + "models.detail.inputShape": "輸入形狀", + "models.detail.framework": "來源框架", + "models.detail.classes": "類別", + "models.detail.classesCountSuffix": "個類別", // ── Model Upload Dialog(flow-model-upload) ── "models.upload.button": "上傳模型", diff --git a/visionA-frontend/src/stores/model-store.ts b/visionA-frontend/src/stores/model-store.ts index 1d37dce..a5fb303 100644 --- a/visionA-frontend/src/stores/model-store.ts +++ b/visionA-frontend/src/stores/model-store.ts @@ -58,6 +58,13 @@ export interface Model extends ModelSummary { version?: string; checksum?: string; downloadUrl?: string; // presigned GET + /** + * 轉檔 metadata(B4):值由 backend 從轉檔 result 帶來,omitempty——沒值時 JSON 不含該欄。 + * 前端「有值才顯示」,所以全部 optional。 + */ + inputShape?: number[]; // NCHW,如 [1, 3, 224, 224] + classes?: string[]; // 類別清單 + framework?: string; // 來源框架,如 onnx / tflite } /** `POST /api/models/init` 的回傳(對齊 api-spec §4) */ @@ -94,6 +101,22 @@ function normalizeModelSummary(raw: unknown): ModelSummary { }; } +/** 把後端帶來的 metadata 陣列正規化:只接受陣列、過濾掉 null/undefined,空陣列回 undefined。 */ +function normalizeStringArray(value: unknown): string[] | undefined { + if (!Array.isArray(value)) return undefined; + const arr = value + .filter((v) => v !== null && v !== undefined) + .map((v) => String(v)); + return arr.length > 0 ? arr : undefined; +} + +/** input_shape:number[],過濾非有限數值;空陣列回 undefined(讓 UI「有值才顯示」)。 */ +function normalizeNumberArray(value: unknown): number[] | undefined { + if (!Array.isArray(value)) return undefined; + const arr = value.map((v) => Number(v)).filter((n) => Number.isFinite(n)); + return arr.length > 0 ? arr : undefined; +} + function normalizeModel(raw: unknown): Model { const summary = normalizeModelSummary(raw); const r = (raw ?? {}) as Record; @@ -106,6 +129,9 @@ function normalizeModel(raw: unknown): Model { (r.download_url as string | undefined) ?? (r.downloadUrl as string | undefined) ?? undefined, + inputShape: normalizeNumberArray(r.input_shape ?? r.inputShape), + classes: normalizeStringArray(r.classes), + framework: (r.framework as string | undefined) ?? undefined, }; }