"use client"; import { useEffect, useMemo, useState } from "react"; import type { GenerateMeta, GeneratedImage, PublicUser, QuotaSnapshot, } from "@/lib/types"; const QUICK_PROMPTS = [ "一款银白色耳机悬浮在纯白背景中,柔和边缘光,苹果官网产品摄影质感。", "安静的极简客厅,浅木地板,大窗户自然光,室内设计杂志封面风格。", "未来感城市夜景中的电动车,低饱和电影光影,写实概念海报。", ]; const PROCESSING_STAGES = [ "验证账号和额度", "发送 Prompt 到 gpt-image-2", "等待模型渲染图像", "整理图片并准备下载", ]; type StudioStatus = "idle" | "pending" | "success" | "error"; type StudioResult = GeneratedImage & { previewUrl: string; }; type NotificationPermissionState = NotificationPermission | "unsupported"; type ServiceWorkerStatus = "checking" | "ready" | "error"; interface BeforeInstallPromptEvent extends Event { prompt(): Promise; userChoice: Promise<{ outcome: "accepted" | "dismissed"; platform: string; }>; } interface GenerateResponse { ok?: boolean; error?: string; images?: GeneratedImage[]; meta?: GenerateMeta; quota?: QuotaSnapshot; } interface StudioClientProps { currentUser: PublicUser; initialQuota: QuotaSnapshot; } interface ProcessingCardProps { status: StudioStatus; startedAt: number; } interface PwaFeedback { tone: "success" | "error"; message: string; } function getErrorMessage(error: unknown, fallbackMessage: string): string { return error instanceof Error ? error.message : fallbackMessage; } function truncateText(text: string, maxLength: number): string { if (text.length <= maxLength) { return text; } return `${text.slice(0, maxLength - 1)}...`; } function getIsStandaloneMode(): boolean { if (typeof window === "undefined") { return false; } const iosStandalone = Boolean( (window.navigator as Navigator & { standalone?: boolean }).standalone, ); return window.matchMedia("(display-mode: standalone)").matches || iosStandalone; } async function showGenerationNotification({ prompt, image, meta, }: { prompt: string; image: GeneratedImage; meta: GenerateMeta | null; }): Promise { if (typeof window === "undefined" || !("Notification" in window)) { return; } if (Notification.permission !== "granted") { return; } const details = [ meta?.model, meta?.size, meta?.durationMs ? `${meta.durationMs} ms` : null, ].filter(Boolean); const body = [truncateText(prompt, 42), details.join(" · ")] .filter(Boolean) .join(" | "); const options: NotificationOptions = { body, icon: "/pwa-icon.svg", badge: "/pwa-badge.svg", tag: `generation-${image.id}`, data: { url: "/studio", }, }; if ("serviceWorker" in navigator) { const registration = await navigator.serviceWorker.ready; await registration.showNotification("图片生成完成", options); return; } const notification = new Notification("图片生成完成", options); notification.onclick = () => { window.focus(); window.location.href = "/studio"; }; } export default function StudioClient({ currentUser, initialQuota, }: StudioClientProps) { const [prompt, setPrompt] = useState(""); const [status, setStatus] = useState("idle"); const [errorMessage, setErrorMessage] = useState(""); const [requestMeta, setRequestMeta] = useState(null); const [result, setResult] = useState(null); const [quota, setQuota] = useState(initialQuota); const [startedAt, setStartedAt] = useState(0); const [serviceWorkerStatus, setServiceWorkerStatus] = useState("checking"); const [notificationPermission, setNotificationPermission] = useState("default"); const [installPromptEvent, setInstallPromptEvent] = useState(null); const [isStandalone, setIsStandalone] = useState(false); const [pwaFeedback, setPwaFeedback] = useState(null); useEffect(() => { if (typeof window === "undefined") { return undefined; } setNotificationPermission( "Notification" in window ? Notification.permission : "unsupported", ); setIsStandalone(getIsStandaloneMode()); const handleBeforeInstallPrompt = (event: Event) => { event.preventDefault(); setInstallPromptEvent(event as BeforeInstallPromptEvent); }; const handleAppInstalled = () => { setInstallPromptEvent(null); setIsStandalone(true); setPwaFeedback({ tone: "success", message: "应用已安装完成,之后可以直接从桌面或启动器打开。", }); }; window.addEventListener("beforeinstallprompt", handleBeforeInstallPrompt); window.addEventListener("appinstalled", handleAppInstalled); if (!("serviceWorker" in navigator)) { setServiceWorkerStatus("error"); return () => { window.removeEventListener( "beforeinstallprompt", handleBeforeInstallPrompt, ); window.removeEventListener("appinstalled", handleAppInstalled); }; } navigator.serviceWorker .register("/sw.js") .then(async () => { await navigator.serviceWorker.ready; setServiceWorkerStatus("ready"); }) .catch(() => { setServiceWorkerStatus("error"); }); return () => { window.removeEventListener("beforeinstallprompt", handleBeforeInstallPrompt); window.removeEventListener("appinstalled", handleAppInstalled); }; }, []); const promptStats = useMemo(() => { const cleanPrompt = prompt.trim(); if (!cleanPrompt) { return "输入一句尽可能具体的画面描述即可。"; } return `${cleanPrompt.length} 个字符`; }, [prompt]); const serviceWorkerLabel = useMemo(() => { if (serviceWorkerStatus === "ready") { return "已就绪"; } if (serviceWorkerStatus === "error") { return "不可用"; } return "注册中"; }, [serviceWorkerStatus]); const notificationLabel = useMemo(() => { if (notificationPermission === "granted") { return "已开启"; } if (notificationPermission === "denied") { return "已拒绝"; } if (notificationPermission === "unsupported") { return "浏览器不支持"; } return "等待授权"; }, [notificationPermission]); const installLabel = useMemo(() => { if (isStandalone) { return "已作为应用运行"; } if (installPromptEvent) { return "可直接安装"; } return "可通过浏览器菜单安装"; }, [installPromptEvent, isStandalone]); async function handleInstallApp(): Promise { if (isStandalone) { setPwaFeedback({ tone: "success", message: "当前已经在 PWA 模式下运行。", }); return; } if (!installPromptEvent) { setPwaFeedback({ tone: "error", message: "当前浏览器没有暴露安装弹窗,可以使用地址栏或浏览器菜单中的“安装应用/添加到主屏幕”。", }); return; } await installPromptEvent.prompt(); const choice = await installPromptEvent.userChoice; setPwaFeedback({ tone: choice.outcome === "accepted" ? "success" : "error", message: choice.outcome === "accepted" ? "浏览器已接受安装请求,应用会加入桌面或启动器。" : "这次没有完成安装,你之后仍然可以再次安装。", }); setInstallPromptEvent(null); } async function handleEnableNotifications(): Promise { if (!("Notification" in window)) { setNotificationPermission("unsupported"); setPwaFeedback({ tone: "error", message: "当前浏览器不支持系统通知。", }); return; } if (Notification.permission === "denied") { setNotificationPermission("denied"); setPwaFeedback({ tone: "error", message: "通知权限已被拒绝,请在浏览器的站点设置里手动重新开启。", }); return; } const permission = await Notification.requestPermission(); setNotificationPermission(permission); setPwaFeedback({ tone: permission === "granted" ? "success" : "error", message: permission === "granted" ? "系统通知已开启,图片生成成功后会立刻提醒你。" : "你还没有授予通知权限,本次不会发送系统通知。", }); } async function handleGenerate(): Promise { const cleanPrompt = prompt.trim(); if (!cleanPrompt) { setErrorMessage("请输入 Prompt 后再开始生成。"); return; } setStatus("pending"); setErrorMessage(""); setResult(null); setRequestMeta(null); setStartedAt(Date.now()); try { const response = await fetch("/api/generate", { method: "POST", headers: { "Content-Type": "application/json", }, body: JSON.stringify({ prompt: cleanPrompt, size: "1024x1024", quality: "high", outputFormat: "png", background: "auto", count: 1, moderation: "auto", }), }); const payload = (await response.json()) as GenerateResponse; if (!response.ok || payload.ok !== true) { throw new Error(payload.error || "生成失败,请稍后重试。"); } const image = payload.images?.[0]; if (!image) { throw new Error("没有拿到可用图片。"); } setResult({ ...image, previewUrl: `data:${image.mimeType};base64,${image.base64}`, }); setRequestMeta(payload.meta ?? null); setQuota(payload.quota ?? initialQuota); setStatus("success"); try { await showGenerationNotification({ prompt: cleanPrompt, image, meta: payload.meta ?? null, }); } catch { setPwaFeedback({ tone: "error", message: "图片已生成成功,但系统通知发送失败,请刷新后重试。", }); } } catch (error) { setErrorMessage(getErrorMessage(error, "生成失败,请稍后重试。")); setStatus("error"); } } function handleDownload() { if (!result) { return; } const blob = base64ToBlob(result.base64, result.mimeType); const blobUrl = URL.createObjectURL(blob); const anchor = document.createElement("a"); anchor.href = blobUrl; anchor.download = result.fileName; anchor.click(); window.setTimeout(() => URL.revokeObjectURL(blobUrl), 2000); } return (

Prompt

Describe your image

{quota.isUnlimited ? "Unlimited" : `剩余 ${quota.remaining} / ${quota.limit}`}

PWA

安装应用并接收生成完成通知

允许通知后,当前页面调用 /api/generate{" "} 成功返回图片时,会通过 Service Worker 发送系统提醒。

应用状态 {installLabel}
通知权限 {notificationLabel}
Service Worker {serviceWorkerLabel}
{pwaFeedback ? (
{pwaFeedback.message}
) : null}