import { useState, useEffect } from "react"; import { Dialog, DialogContent, DialogHeader, DialogTitle, DialogDescription } from "@/components/ui/dialog"; import { Button } from "@/components/ui/button"; import { ScrollArea } from "@/components/ui/scroll-area"; import { Loader2, Download, Check, AlertCircle, Brain, RefreshCw } from "lucide-react"; import { zipSync, strToU8 } from "fflate"; import type { NormalizedPost } from "@/lib/grabber/sites"; interface ImageDescriberModalProps { open: boolean; onOpenChange: (open: boolean) => void; posts: NormalizedPost[]; } export function ImageDescriberModal({ open, onOpenChange, posts }: ImageDescriberModalProps) { const [descriptions, setDescriptionState] = useState< Record >({}); const [downloadingZip, setDownloadingZip] = useState>({}); useEffect(() => { if (!open) return; let active = true; const runDescriptions = async () => { for (const post of posts) { if (!active) break; // Skip if already generating or has description const currentState = descriptions[post.id]; if (currentState && (currentState.text || currentState.loading)) { continue; } setDescriptionState((prev) => ({ ...prev, [post.id]: { text: "", loading: true }, })); try { const res = await fetch("/api/describe-image", { method: "POST", headers: { "Content-Type": "application/json" }, body: JSON.stringify({ imageUrl: post.fileUrl }), }); const data = await res.json(); if (!active) break; if (data.success && data.description) { setDescriptionState((prev) => ({ ...prev, [post.id]: { text: data.description, loading: false }, })); } else { setDescriptionState((prev) => ({ ...prev, [post.id]: { text: "", loading: false, error: data.error || "Failed to generate description", }, })); } } catch (err) { if (!active) break; setDescriptionState((prev) => ({ ...prev, [post.id]: { text: "", loading: false, error: err instanceof Error ? err.message : "Inference error", }, })); } } }; runDescriptions(); return () => { active = false; }; }, [open, posts]); const handleDescribeSingle = async (post: NormalizedPost) => { setDescriptionState((prev) => ({ ...prev, [post.id]: { text: "", loading: true, error: undefined }, })); try { const res = await fetch("/api/describe-image", { method: "POST", headers: { "Content-Type": "application/json" }, body: JSON.stringify({ imageUrl: post.fileUrl }), }); const data = await res.json(); if (data.success && data.description) { setDescriptionState((prev) => ({ ...prev, [post.id]: { text: data.description, loading: false }, })); } else { setDescriptionState((prev) => ({ ...prev, [post.id]: { text: "", loading: false, error: data.error || "Failed to generate description", }, })); } } catch (err) { setDescriptionState((prev) => ({ ...prev, [post.id]: { text: "", loading: false, error: err instanceof Error ? err.message : "Inference error", }, })); } }; const downloadSingleDescZip = async (post: NormalizedPost, description: string) => { setDownloadingZip((prev) => ({ ...prev, [post.id]: true })); try { const response = await fetch(post.fileUrl); if (!response.ok) throw new Error("Failed to fetch image file"); const buffer = await response.arrayBuffer(); const imgBytes = new Uint8Array(buffer); const zipFiles: Record = {}; const ext = post.ext || "jpg"; zipFiles[`image.${ext}`] = imgBytes; zipFiles[`desc.txt`] = strToU8(description); const zipped = zipSync(zipFiles, { level: 0 }); const blob = new Blob([zipped], { type: "application/zip" }); const url = URL.createObjectURL(blob); const a = document.createElement("a"); a.href = url; a.download = `image_${post.id}_with_description.zip`; document.body.appendChild(a); a.click(); a.remove(); URL.revokeObjectURL(url); } catch (err) { alert(`Download failed: ${err instanceof Error ? err.message : "unknown error"}`); } finally { setDownloadingZip((prev) => ({ ...prev, [post.id]: false })); } }; // Progress stats const describedCount = posts.filter((p) => descriptions[p.id]?.text).length; const loadingCount = posts.filter((p) => descriptions[p.id]?.loading).length; return (
AI Image Describer (GGUF LLaVA) Described {describedCount} of {posts.length} selected images{" "} {loadingCount > 0 && "(Processing sequentially...)"}
{posts.map((post) => { const state = descriptions[post.id] || { text: "", loading: false }; const isZipping = downloadingZip[post.id]; return (
{`Post
ID: {post.id}
Description {state.loading && ( Analyzing image... )} {state.text && ( Completed )} {state.error && ( Error: {state.error} )}
{state.loading && (

Running GGUF multimodal inference...

)} {!state.loading && !state.text && !state.error && ( Waiting in sequence queue... )} {state.error && (
{state.error}
)} {state.text &&

{state.text}

}
{state.text && (
)}
); })}
); }