feat(visionA-frontend): B4 model 詳細頁顯示 metadata(input_shape/classes/framework)
對應 backend B4:model 詳細頁顯示轉檔帶來的 metadata。 - Model 型別加 inputShape/classes/framework(optional);normalize 雙吃 snake/camel - input_shape 顯示成 1 × 3 × 224 × 224;classes 前 8 個 + N 折疊;framework 直接顯示 - 有值才顯示(沒值不渲染空白);中英 i18n - tsc / lint / next build 綠 Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
This commit is contained in:
parent
cabbdde495
commit
dc1c0dbee4
@ -37,6 +37,14 @@ function formatFileSize(bytes: number): string {
|
|||||||
return `${(bytes / (1024 * 1024 * 1024)).toFixed(2)} GB`;
|
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 {
|
interface ModelDetailClientProps {
|
||||||
id: string;
|
id: string;
|
||||||
}
|
}
|
||||||
@ -156,6 +164,23 @@ export function ModelDetailClient({ id }: ModelDetailClientProps) {
|
|||||||
label={t("models.createdAt")}
|
label={t("models.createdAt")}
|
||||||
value={selectedModel.createdAt ? new Date(selectedModel.createdAt).toLocaleString() : "—"}
|
value={selectedModel.createdAt ? new Date(selectedModel.createdAt).toLocaleString() : "—"}
|
||||||
/>
|
/>
|
||||||
|
{/* B4 轉檔 metadata:有值才顯示(沒值時 backend omitempty,normalize 後為 undefined)。 */}
|
||||||
|
{selectedModel.framework && (
|
||||||
|
<InfoRow
|
||||||
|
label={t("models.detail.framework")}
|
||||||
|
value={<span className="font-mono text-xs">{selectedModel.framework}</span>}
|
||||||
|
/>
|
||||||
|
)}
|
||||||
|
{selectedModel.inputShape && selectedModel.inputShape.length > 0 && (
|
||||||
|
<InfoRow
|
||||||
|
label={t("models.detail.inputShape")}
|
||||||
|
value={
|
||||||
|
<span className="font-mono text-xs">
|
||||||
|
{formatInputShape(selectedModel.inputShape)}
|
||||||
|
</span>
|
||||||
|
}
|
||||||
|
/>
|
||||||
|
)}
|
||||||
{selectedModel.version && (
|
{selectedModel.version && (
|
||||||
<InfoRow label={t("models.detail.version")} value={selectedModel.version} />
|
<InfoRow label={t("models.detail.version")} value={selectedModel.version} />
|
||||||
)}
|
)}
|
||||||
@ -166,6 +191,30 @@ export function ModelDetailClient({ id }: ModelDetailClientProps) {
|
|||||||
/>
|
/>
|
||||||
)}
|
)}
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
|
{/* classes 可能很長,獨立成 full-width 區塊,用 Badge 列出(超量只顯示前 N 個 + 餘量)。 */}
|
||||||
|
{selectedModel.classes && selectedModel.classes.length > 0 && (
|
||||||
|
<div className="space-y-2 border-t pt-3 text-sm">
|
||||||
|
<div className="flex items-center justify-between gap-2">
|
||||||
|
<span className="text-muted-foreground">{t("models.detail.classes")}</span>
|
||||||
|
<span className="text-muted-foreground text-xs">
|
||||||
|
{selectedModel.classes.length} {t("models.detail.classesCountSuffix")}
|
||||||
|
</span>
|
||||||
|
</div>
|
||||||
|
<div className="flex flex-wrap gap-1.5">
|
||||||
|
{selectedModel.classes.slice(0, CLASSES_PREVIEW_LIMIT).map((c, i) => (
|
||||||
|
<Badge key={`${i}-${c}`} variant="secondary" className="font-normal">
|
||||||
|
{c}
|
||||||
|
</Badge>
|
||||||
|
))}
|
||||||
|
{selectedModel.classes.length > CLASSES_PREVIEW_LIMIT && (
|
||||||
|
<Badge variant="outline" className="font-normal">
|
||||||
|
+{selectedModel.classes.length - CLASSES_PREVIEW_LIMIT}
|
||||||
|
</Badge>
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
</CardContent>
|
</CardContent>
|
||||||
</Card>
|
</Card>
|
||||||
</div>
|
</div>
|
||||||
|
|||||||
@ -195,6 +195,10 @@ export const en: Dictionary = {
|
|||||||
"models.detail.checksum": "Checksum",
|
"models.detail.checksum": "Checksum",
|
||||||
"models.detail.supportedChips": "Supported chips",
|
"models.detail.supportedChips": "Supported chips",
|
||||||
"models.detail.deployToDevice": "Deploy to device",
|
"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 ──
|
// ── Model Upload Dialog ──
|
||||||
"models.upload.button": "Upload model",
|
"models.upload.button": "Upload model",
|
||||||
|
|||||||
@ -196,6 +196,10 @@ export const zhHant: Dictionary = {
|
|||||||
"models.detail.checksum": "校驗碼",
|
"models.detail.checksum": "校驗碼",
|
||||||
"models.detail.supportedChips": "支援晶片",
|
"models.detail.supportedChips": "支援晶片",
|
||||||
"models.detail.deployToDevice": "部署至裝置",
|
"models.detail.deployToDevice": "部署至裝置",
|
||||||
|
"models.detail.inputShape": "輸入形狀",
|
||||||
|
"models.detail.framework": "來源框架",
|
||||||
|
"models.detail.classes": "類別",
|
||||||
|
"models.detail.classesCountSuffix": "個類別",
|
||||||
|
|
||||||
// ── Model Upload Dialog(flow-model-upload) ──
|
// ── Model Upload Dialog(flow-model-upload) ──
|
||||||
"models.upload.button": "上傳模型",
|
"models.upload.button": "上傳模型",
|
||||||
|
|||||||
@ -58,6 +58,13 @@ export interface Model extends ModelSummary {
|
|||||||
version?: string;
|
version?: string;
|
||||||
checksum?: string;
|
checksum?: string;
|
||||||
downloadUrl?: string; // presigned GET
|
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) */
|
/** `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 {
|
function normalizeModel(raw: unknown): Model {
|
||||||
const summary = normalizeModelSummary(raw);
|
const summary = normalizeModelSummary(raw);
|
||||||
const r = (raw ?? {}) as Record<string, unknown>;
|
const r = (raw ?? {}) as Record<string, unknown>;
|
||||||
@ -106,6 +129,9 @@ function normalizeModel(raw: unknown): Model {
|
|||||||
(r.download_url as string | undefined) ??
|
(r.download_url as string | undefined) ??
|
||||||
(r.downloadUrl as string | undefined) ??
|
(r.downloadUrl as string | undefined) ??
|
||||||
undefined,
|
undefined,
|
||||||
|
inputShape: normalizeNumberArray(r.input_shape ?? r.inputShape),
|
||||||
|
classes: normalizeStringArray(r.classes),
|
||||||
|
framework: (r.framework as string | undefined) ?? undefined,
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
Loading…
x
Reference in New Issue
Block a user