import { useState, useEffect } from "react"; import { useGetImageHistory, useDeleteImage, getGetImageHistoryQueryKey } from "@workspace/api-client-react"; import { useQueryClient } from "@tanstack/react-query"; import { motion, AnimatePresence } from "framer-motion"; import { Download, Trash2, ImageIcon, Loader2, Lock, Globe, Film, Play, Clapperboard, } from "lucide-react"; import { Button } from "@/components/ui/button"; import { useToast } from "@/hooks/use-toast"; import { useAuth } from "@/contexts/AuthContext"; import { useLang } from "@/contexts/LanguageContext"; import { AlertDialog, AlertDialogAction, AlertDialogCancel, AlertDialogContent, AlertDialogDescription, AlertDialogFooter, AlertDialogHeader, AlertDialogTitle, AlertDialogTrigger, } from "@/components/ui/alert-dialog"; // ── Types ───────────────────────────────────────────────────────────────────── interface VideoRecord { id: number; videoUrl: string; thumbnailUrl: string | null; prompt: string; negativePrompt: string | null; model: string; aspectRatio: string; resolution: string; duration: number; hasRefImage: boolean; isPrivate: boolean; userId: number | null; createdAt: string; } // ── Constants ───────────────────────────────────────────────────────────────── const BASE = import.meta.env.BASE_URL.replace(/\/$/, ""); const styleLabels: Record = { none: "無風格", realistic: "寫實", anime: "動漫", artistic: "藝術", cartoon: "卡通", sketch: "素描", oil_painting: "油畫", watercolor: "水彩", digital_art: "數位藝術", }; const modelLabels: Record = { grok: "Grok", meta: "Meta", "imagen-pro": "Imagen Pro", "imagen-4": "Imagen 4", "imagen-flash": "Imagen Flash", "nano-banana-pro": "Nano Banana Pro", "nano-banana-2": "Nano Banana 2", }; const videoModelLabels: Record = { "grok-3": "Grok-3", "veo-3-fast": "Veo 3.1 Fast", }; type FilterTab = "all" | "public" | "private"; type MediaTab = "images" | "videos"; // ── useVideoHistory hook ─────────────────────────────────────────────────────── function useVideoHistory() { const [videos, setVideos] = useState([]); const [isLoading, setIsLoading] = useState(true); const [error, setError] = useState(null); const fetch_ = async () => { setIsLoading(true); setError(null); try { const resp = await fetch(`${BASE}/api/videos/history?limit=50`, { credentials: "include" }); if (!resp.ok) throw new Error(`HTTP ${resp.status}`); const data = await resp.json() as { videos: VideoRecord[] }; setVideos(data.videos || []); } catch (e) { setError(e instanceof Error ? e.message : "Failed to load"); } finally { setIsLoading(false); } }; useEffect(() => { fetch_(); }, []); return { videos, isLoading, error, refetch: fetch_ }; } // ── VideoCard ───────────────────────────────────────────────────────────────── function VideoCard({ video, onDelete }: { video: VideoRecord; onDelete: (id: number) => void }) { const { t } = useLang(); const [playing, setPlaying] = useState(false); const resolveUrl = (url: string) => url.startsWith("/") ? `${BASE}${url}` : url; return ( {/* Media area */}
{playing ? (
{/* Info + actions */}

{video.prompt}

{video.aspectRatio} {video.resolution} {video.duration}s
{t.historyDeleteVidTitle} {t.historyDeleteVidDesc} {t.poolCancel} onDelete(video.id)} className="bg-destructive text-destructive-foreground hover:bg-destructive/90" > {t.historyConfirmDelete}
); } // ── ImageCard ───────────────────────────────────────────────────────────────── function ImageCard({ img, onDelete, onDownload }: { img: any; onDelete: (id: number) => void; onDownload: (url: string) => void; }) { const { t } = useLang(); const imgPrivate = (img as any).isPrivate === true; return (
{img.prompt}
{imgPrivate ? ( {t.historyTabPrivate} ) : ( {t.historyTabPublic} )}
{t.historyDeleteImgTitle} {t.historyDeleteImgDesc} {t.poolCancel} onDelete(img.id)} className="bg-destructive text-destructive-foreground hover:bg-destructive/90" > {t.historyConfirmDelete}

{img.prompt}

{styleLabels[img.style] || img.style} {img.aspectRatio} {modelLabels[img.model] || img.model}
); } // ── History page ─────────────────────────────────────────────────────────────── export function History() { const { toast } = useToast(); const { t } = useLang(); const queryClient = useQueryClient(); const { isSignedIn } = useAuth(); const [mediaTab, setMediaTab] = useState("images"); const [filter, setFilter] = useState("all"); // ── Image history ────────────────────────────────────────────────────────── const { data: imgData, isLoading: imgLoading } = useGetImageHistory(undefined, { query: { enabled: true, queryKey: getGetImageHistoryQueryKey() }, }); const { mutate: deleteImage } = useDeleteImage(); const handleDeleteImage = (id: number) => { deleteImage({ id }, { onSuccess: () => { toast({ title: t.historyDeletedImg }); queryClient.invalidateQueries({ queryKey: getGetImageHistoryQueryKey() }); }, onError: () => toast({ variant: "destructive", title: t.historyDeleteFailed }), }); }; const handleDownloadImage = (url: string) => { const a = document.createElement("a"); a.href = url; a.download = "generated-image.png"; document.body.appendChild(a); a.click(); document.body.removeChild(a); }; // ── Video history ────────────────────────────────────────────────────────── const { videos, isLoading: vidLoading, refetch: refetchVideos } = useVideoHistory(); const handleDeleteVideo = async (id: number) => { try { const resp = await fetch(`${BASE}/api/videos/${id}`, { method: "DELETE", credentials: "include" }); if (!resp.ok) throw new Error(`HTTP ${resp.status}`); toast({ title: t.historyDeletedVid }); refetchVideos(); } catch { toast({ variant: "destructive", title: t.historyDeleteFailed }); } }; // ── Derived lists with filter ────────────────────────────────────────────── const allImages = imgData?.images || []; const filteredImages = allImages.filter((img) => { if (filter === "private") return (img as any).isPrivate === true; if (filter === "public") return !(img as any).isPrivate; return true; }); const filteredVideos = videos.filter((vid) => { if (!isSignedIn) return !vid.isPrivate; if (filter === "private") return vid.isPrivate === true; if (filter === "public") return !vid.isPrivate; return true; }); // ── Filter tabs ──────────────────────────────────────────────────────────── const filterTabs: { key: FilterTab; label: string }[] = [ { key: "all", label: t.historyTabAll }, { key: "public", label: t.historyTabPublic }, { key: "private", label: t.historyTabPrivate }, ]; const isLoading = mediaTab === "images" ? imgLoading : vidLoading; const isEmpty = mediaTab === "images" ? allImages.length === 0 : videos.length === 0; return (
{/* Page header */}

{t.navHistory}

回顧您過往的靈感創作

{/* ── Media type tabs ── */}
{/* ── Visibility filter tabs (only when signed in & not empty) ── */} {isSignedIn && !isEmpty && (
{filterTabs.map((tab) => ( ))}
)} {/* ── Content ── */} {isLoading ? (

{mediaTab === "images" ? "載入圖片記錄中..." : t.historyLoadingVideos}

) : isEmpty ? ( /* Global empty state */
{mediaTab === "images" ? : }

{mediaTab === "images" ? t.historyEmptyImages : t.historyEmptyVideos}

{mediaTab === "images" ? t.historyEmptyImagesDesc : t.historyEmptyVideosDesc}

) : mediaTab === "images" ? ( /* ── Images grid ── */ {filteredImages.length === 0 ? (

{t.historyNoItemsInFilter}

) : (
{filteredImages.map((img) => ( ))}
)}
) : ( /* ── Videos grid ── */ {filteredVideos.length === 0 ? (

{t.historyNoItemsInFilter}

) : (
{filteredVideos.map((vid) => ( ))}
)}
)}
); }