import { newId } from "./id"; /** 編輯器內附圖(本地選檔或 mock 產圖) */ export type AttachedImage = { id: string; url: string; name?: string; /** 產圖 prompt;選檔可無 */ prompt?: string; }; export const MAX_ATTACHED_IMAGES = 10; /** 單檔上限(讀成 data URL 前) */ const MAX_FILE_BYTES = 8 * 1024 * 1024; export function withAttachedImageNote(text: string, count: number): string { const body = text.trim(); if (!count || count <= 0) return body; if (/(附圖\s*\d+\s*張)/.test(body)) return body; return `${body}\n\n(附圖 ${count} 張)`; } function readFileAsDataUrl(file: File): Promise { return new Promise((resolve, reject) => { const reader = new FileReader(); reader.onload = () => resolve(String(reader.result || "")); reader.onerror = () => reject(new Error(`讀取失敗:${file.name}`)); reader.readAsDataURL(file); }); } /** * 把本機圖檔讀成可預覽的 data URL。 * mock 階段不真上傳;送出時 repo 只記張數/可選縮圖。 */ export async function filesToAttachedImages( files: FileList | File[], opts?: { max?: number; existingCount?: number }, ): Promise { const max = opts?.max ?? MAX_ATTACHED_IMAGES; const existing = opts?.existingCount ?? 0; const room = Math.max(0, max - existing); const list = Array.from(files).filter((f) => f.type.startsWith("image/")); if (list.length === 0) throw new Error("請選擇圖片檔"); const take = list.slice(0, room); if (take.length === 0) throw new Error(`最多附 ${max} 張圖`); const out: AttachedImage[] = []; for (const file of take) { if (file.size > MAX_FILE_BYTES) { throw new Error(`${file.name} 超過 8MB`); } const url = await readFileAsDataUrl(file); if (!url.startsWith("data:image")) throw new Error(`無法讀取:${file.name}`); out.push({ id: newId("img"), url, name: file.name, }); } return out; } /** 大 data URL 不寫進 localStorage;改成可顯示的 mock 縮圖 seed */ export function toPersistableImageUrl(img: AttachedImage): string { if (img.url.startsWith("data:") && img.url.length > 1800) { const seed = encodeURIComponent((img.name || img.id || "attach").slice(0, 40)); return `https://api.dicebear.com/9.x/shapes/svg?seed=${seed}&backgroundColor=c0aede,ffd5dc,b6e3f4`; } return img.url; }