import { useState } from "preact/hooks"; import { ImageFile } from "../utils/types.ts"; import { decodeData } from "../utils/urlEncoder.ts"; import PhotoCropperModal from "./PhotoCropperModal.tsx"; interface ImageGeneraterProps { text_1: string; image_data: string; role: string; recordingCallback: (data: ImageFile | null) => void; } export default function ImageGenerater( { text_1, image_data, role, recordingCallback }: ImageGeneraterProps, ) { const [isVisible, setIsVisible] = useState(true); const [displayImage, setDisplayImage] = useState(image_data); // User-editable description shown in the input field const [nexAIDescription, setNextAiDescription] = useState( removeDashAndNumbers( removePngExtension(text_1), ), ); // Actual saved filename from AI generation (with timestamp hash) // Falls back to the initial text_1 if no AI generation occurred const [actualFileName, setActualFileName] = useState(text_1); const [ai_nameError, setAiGenerateNameError] = useState(false); const [showCropperModal, setShowCropperModal] = useState(false); const [isAiGenerating, setIsAiGenerating] = useState(false); const handleSave = () => { setIsVisible(false); // image_file: the actual saved filename (with timestamp hash for AI-generated) // image_text: the user-editable description (preserves spaces, casing) // image_data: the URL or base64 data recordingCallback({ image_text: nexAIDescription, image_file: actualFileName, image_data: displayImage, }); }; const handleCancel = () => { setIsVisible(false); recordingCallback(null); }; if (!isVisible) return null; const handleAiGenerate = async () => { if (isAiGenerating) return; const preloadImage = (src: string) => new Promise((resolve) => { const img = new Image(); img.onload = () => resolve(); img.onerror = () => resolve(); img.src = src; }); setIsAiGenerating(true); try { const id = nexAIDescription.trim().slice(0, 120); const apiResponse = await fetch( `/api/ai-images/${encodeURIComponent(id)}`, { method: "GET", headers: { "Content-Type": "application/json", }, }, ); if (!apiResponse.ok) { throw new Error( `Failed to fetch image from local API: ${apiResponse.statusText}`, ); } const newImageData = await apiResponse.json() as ImageFile; const rawImageData = newImageData.image_data ?? ""; const nextSrc = rawImageData && (rawImageData.startsWith("http") || rawImageData.startsWith("/")) ? decodeData(rawImageData) : `data:image/png;base64,${rawImageData}`; await preloadImage(nextSrc); // Update display image with the URL/data from API setDisplayImage(newImageData.image_data); // Preserve the actual saved filename (with timestamp) from the API response // This is the real file on disk, e.g., "a_person_running-20251212123456.png" if (newImageData.image_file) { setActualFileName(newImageData.image_file); } } catch (e) { console.error("Error generating AI image:", e); } finally { setIsAiGenerating(false); } }; return (

{role === "admin" && ( setNextAiDescription(e.currentTarget.value)} onKeyDown={() => setAiGenerateNameError(false)} placeholder="Quiz Name" className={`theme-image-input ${ ai_nameError ? "border-red-500" : "" }`} /> )}

{role === "admin" && ( )} {role === "admin" && ( )} {role === "admin" && ( )}
{showCropperModal && ( { setDisplayImage(imageData); // For uploaded images (base64), use the description as filename // This will be hashed when uploaded via /api/image-blob/ setActualFileName(nexAIDescription); setShowCropperModal(false); }} onCancel={() => setShowCropperModal(false)} /> )}
); } export const removePngExtension = (str: string) => { const pngExtension = ".png"; if (str.toLowerCase().endsWith(pngExtension)) { return str.slice(0, -pngExtension.length); } return str; }; export const removeDashAndNumbers = (str: string) => { const pattern = /-\d{14}$/; if (pattern.test(str)) { return str.slice(0, -15); // Remove the dash and 14 numeric characters } return str; };