"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 (
{profile.description}
) : (—
)}