/** * Model Sharing — Mock Fixtures(平行開發用) * * 對 API 契約 mock(api-model-sharing.md 的 response 形狀),不依賴後端跑起來。 * 形狀為契約定義的 snake_case,經 model-sharing.ts 的 normalize 後才成前端型別。 * * 用途: * 1. store 在 `NEXT_PUBLIC_USE_MODEL_SHARING_MOCK=1` 時走 mock(見 model-sharing-store) * 2. 元件測試 fixture 來源 * * ⚠️ mock owner 資料刻意不含 email(對齊契約 §4:非擁有者不該看到他人 email)。 * shares 的 email 是 owner 授權目標,才有 email。 */ /** 契約 §1 library item 的原始(snake_case)形狀。 */ export interface RawLibraryItem { id: string; name: string; description?: string; target_chip: string; file_size: number; source: string; status: string; visibility: string; owner: { id: string; name: string; is_me: boolean }; shared_with_me: boolean; my_access: string; created_at: string; updated_at: string; } /** 契約 §1 分頁 response 原始形狀。 */ export interface RawLibraryPage { items: RawLibraryItem[]; next_cursor: string | null; has_more: boolean; } const NOW = "2026-08-01T00:00:00Z"; /** * 一個較大的 mock 資料集(30 筆),用來驗證 cursor 無限捲動分頁。 * 混合三種可見性、owner/非 owner、shared_with_me 各態,覆蓋 UI 分支。 */ export const MOCK_LIBRARY_ITEMS: RawLibraryItem[] = Array.from({ length: 30 }).map( (_, i) => { const isMine = i % 3 === 0; const visibility = isMine ? "private" : i % 3 === 1 ? "public" : "tenant"; const sharedWithMe = !isMine && i % 4 === 0; const chips = ["kl520", "kl720", "kl630", "kl730"]; const sources = ["converted", "uploaded", "preset"]; return { id: `mock-model-${String(i + 1).padStart(2, "0")}`, name: `mock-model-${i + 1} ${["yolov5s", "resnet50", "mobilenet", "ssd"][i % 4]}`, description: i % 2 === 0 ? `Mock 模型 ${i + 1} 的描述` : undefined, target_chip: chips[i % chips.length], file_size: (i + 1) * 1024 * 1024, source: isMine ? "converted" : sources[i % sources.length], status: "ready", visibility, owner: isMine ? { id: "me", name: "我", is_me: true } : { id: `owner-${i}`, name: `Owner ${(i % 5) + 1}`, is_me: false }, shared_with_me: sharedWithMe, my_access: isMine ? "owner" : sharedWithMe ? "viewer" : "viewer", created_at: `2026-07-${String((i % 28) + 1).padStart(2, "0")}T00:00:00Z`, updated_at: NOW, }; }, ); /** * 對 MOCK_LIBRARY_ITEMS 套用 query(搜尋 / filter / 排序 / cursor 分頁)產生一頁。 * cursor = 已消費筆數的 base64(不透明;mock 只需能往下切)。 */ export function mockLibraryPage(query: { cursor?: string; limit?: number; q?: string; targetChip?: string; source?: string; visibility?: string; owned?: boolean; sort?: string; order?: string; }): RawLibraryPage { let items = [...MOCK_LIBRARY_ITEMS]; // filter if (query.q) { const q = query.q.toLowerCase(); items = items.filter( (m) => m.name.toLowerCase().includes(q) || (m.description ?? "").toLowerCase().includes(q), ); } if (query.targetChip) { items = items.filter((m) => m.target_chip === query.targetChip); } if (query.source) { items = items.filter((m) => m.source === query.source); } if (query.visibility) { items = items.filter((m) => m.visibility === query.visibility); } if (query.owned === true) { items = items.filter((m) => m.owner.is_me); } else if (query.owned === false) { items = items.filter((m) => !m.owner.is_me); } // sort const order = query.order === "asc" ? 1 : -1; const sortKey = query.sort ?? "created_at"; items.sort((a, b) => { if (sortKey === "name") return a.name.localeCompare(b.name) * order; if (sortKey === "file_size") return (a.file_size - b.file_size) * order; return a.created_at.localeCompare(b.created_at) * order; }); // cursor 分頁 const limit = Math.min(Math.max(query.limit ?? 20, 1), 100); const start = query.cursor ? Number(atob(query.cursor)) : 0; const slice = items.slice(start, start + limit); const nextStart = start + slice.length; const hasMore = nextStart < items.length; return { items: slice, next_cursor: hasMore ? btoa(String(nextStart)) : null, has_more: hasMore, }; } /** 依 id 產生一筆 profile 原始形狀(找不到回 null,模擬 404)。 */ export function mockProfile(id: string): Record | null { const item = MOCK_LIBRARY_ITEMS.find((m) => m.id === id); if (!item) return null; return { ...item, input_shape: [1, 3, 224, 224], classes: ["person", "car", "dog", "cat"], framework: "onnx", can_download: item.my_access !== "none", uploaded_at: item.created_at, }; } /** mock shares 清單(owner 檢視自己模型的授權對象)。 */ export const MOCK_SHARES: Record> = { "mock-model-01": [ { user_id: "u-alice", email: "alice@corp.com", role: "viewer", created_at: NOW }, { user_id: "u-bob", email: "bob@corp.com", role: "viewer", created_at: NOW }, ], };