"use client"; /** * ModelProfileClient — 模型 profile 頁(owner / 公開雙態) * * 對齊設計規格 §6 + API 契約 §2(GET /:id/profile)。取代舊的 owner-only detail: * 用 `GET /api/models/:id/profile` 取詳情(後端依身份裁剪 + 權限檢查),依 `myAccess` * 決定渲染 owner 版或公開 / 共享版。 * * 雙態差異(設計規格 §6.2): * - owner 版(myAccess==='owner'):下載(若可)+ 刪除 + 【新增】公開設定 Dialog * - 公開 / 共享版:僅下載(canDownload 為 true 時);隱藏刪除 / 公開設定;顯示 ModelOwnerBar * * 錯誤(契約 §2):無可見性 → 404(防 enumeration)→ 全頁「找不到 / 無權限」EmptyState。 * * 下載沿用既有 FAA delegated download(lib/api/model-download),走同一 endpoint * (契約 §3:download 已加共享權限檢查)。 */ import { useEffect, useState } from "react"; import Link from "next/link"; import { useRouter } from "next/navigation"; import { ArrowLeft, DownloadIcon, Globe, SearchX, Trash2 } from "lucide-react"; import { toast } from "sonner"; import { ModelOwnerBar } from "@/components/models/model-owner-bar"; import { ModelVisibilityBadge } from "@/components/models/model-visibility-badge"; import { ModelVisibilityDialog } from "@/components/models/model-visibility-dialog"; import { AlertDialog, AlertDialogAction, AlertDialogCancel, AlertDialogContent, AlertDialogDescription, AlertDialogFooter, AlertDialogHeader, AlertDialogTitle, AlertDialogTrigger, } from "@/components/ui/alert-dialog"; import { Badge } from "@/components/ui/badge"; import { Button } from "@/components/ui/button"; import { Card, CardContent, CardHeader, CardTitle } from "@/components/ui/card"; import { EmptyState } from "@/components/ui/empty-state"; import { Skeleton } from "@/components/ui/skeleton"; import { Spinner } from "@/components/ui/spinner"; import { getModelDownload, ModelDownloadError, triggerNavDownload, } from "@/lib/api/model-download"; import { useT } from "@/lib/i18n/context"; import { useModelSharingStore } from "@/stores/model-sharing-store"; import { useModelStore } from "@/stores/model-store"; function formatFileSize(bytes: number): string { if (!bytes) return "—"; if (bytes < 1024 * 1024) return `${(bytes / 1024).toFixed(1)} KB`; if (bytes < 1024 * 1024 * 1024) return `${(bytes / (1024 * 1024)).toFixed(1)} MB`; return `${(bytes / (1024 * 1024 * 1024)).toFixed(2)} GB`; } function formatInputShape(shape: number[]): string { return shape.join(" × "); } const CLASSES_PREVIEW_LIMIT = 8; interface ModelProfileClientProps { id: string; } export function ModelProfileClient({ id }: ModelProfileClientProps) { const t = useT(); const router = useRouter(); const profile = useModelSharingStore((s) => s.profile); const isLoading = useModelSharingStore((s) => s.isProfileLoading); const profileError = useModelSharingStore((s) => s.profileError); const loadProfile = useModelSharingStore((s) => s.loadProfile); const clearProfile = useModelSharingStore((s) => s.clearProfile); // 刪除沿用既有 model-store(owner-only DELETE /api/models/:id)。 const deleteModel = useModelStore((s) => s.deleteModel); const [deleting, setDeleting] = useState(false); const [downloadBusy, setDownloadBusy] = useState(false); const [visibilityDialogOpen, setVisibilityDialogOpen] = useState(false); useEffect(() => { if (id) void loadProfile(id); return () => clearProfile(); }, [id, loadProfile, clearProfile]); const isOwner = profile?.myAccess === "owner"; async function handleDownload() { if (!profile || downloadBusy) return; setDownloadBusy(true); try { const grant = await getModelDownload(profile.id); triggerNavDownload(grant.downloadUrl); toast.success(t("models.download.toast.start"), { description: t("models.download.toast.hint"), }); } catch (err) { const code = err instanceof ModelDownloadError ? err.code : "unknown"; const key = `models.download.error.${code}`; const desc = t(key); toast.error(t("models.download.error.title"), { description: desc === key ? t("models.download.error.unknown") : desc, }); } finally { setDownloadBusy(false); } } async function handleDelete() { setDeleting(true); const ok = await deleteModel(id); setDeleting(false); if (ok) { toast.success(t("common.save")); router.push("/models"); } else { toast.error(t("common.error")); } } const backButton = ( ); // 載入中 if (isLoading && !profile) { return (
); } // 無權限 / 找不到(契約:無可見性回 404,合併「找不到 / 無權限」)。 if (profileError || !profile) { return (
{backButton} router.push("/models/library"), }} />
); } return (
{backButton}

{profile.name}

{profile.targetChip.toUpperCase()} {t(`models.status.${profile.status === "ready" ? "ready" : "scanning"}`)} {profile.source !== "uploaded" && ( {t(`models.source.${profile.source}`)} )} {/* owner 看到自己的 visibility;非 owner 看到共享標示。 */}
{profile.canDownload && ( )} {/* owner-only 操作:公開設定 + 刪除 */} {isOwner && ( <> {t("common.confirm")} {profile.name} {t("common.cancel")} {t("common.delete")} )}
{/* 非 owner:擁有者資訊列。 */} {!isOwner && ( )} {t("models.detail.description")} {profile.description ? (

{profile.description}

) : (

)}
{profile.framework && ( {profile.framework}} /> )} {profile.inputShape && profile.inputShape.length > 0 && ( {formatInputShape(profile.inputShape)} } /> )}
{profile.classes && profile.classes.length > 0 && (
{t("models.detail.classes")} {profile.classes.length} {t("models.detail.classesCountSuffix")}
{profile.classes.slice(0, CLASSES_PREVIEW_LIMIT).map((c, i) => ( {c} ))} {profile.classes.length > CLASSES_PREVIEW_LIMIT && ( +{profile.classes.length - CLASSES_PREVIEW_LIMIT} )}
)}
{isOwner && ( )}
); } function InfoRow({ label, value }: { label: string; value: React.ReactNode }) { return (
{label} {value}
); }