import { useCallback, useEffect, useRef, useState } from 'react'; import { ImagePlus, Import, LoaderCircle } from 'lucide-react'; import useWebSocket from 'react-use-websocket'; import { useGenerationActions } from '../hooks/use-generation-actions'; import { useStore } from '../store/useStore'; import type { PreviewMessage } from '../types'; import { Button } from './ui/button'; import { cn } from '../lib/utils'; import { useShallow } from 'zustand/react/shallow'; type FeedbackState = { tone: 'success' | 'warning' | 'error'; text: string; }; export function ImagePreview() { const { importSettingsFromBase64 } = useGenerationActions(); const { currentImage, preview, setPreview, setServerStatus, status } = useStore(useShallow((state) => ({ currentImage: state.currentImage, preview: state.preview, setPreview: state.setPreview, setServerStatus: state.setServerStatus, status: state.status, }))); const [activePreviewImage, setActivePreviewImage] = useState(null); const [feedback, setFeedback] = useState(null); const currentGenerationIdRef = useRef(null); const lastStepRef = useRef(-1); const protocol = window.location.protocol === 'https:' ? 'wss' : 'ws'; const wsUrl = `${protocol}://${window.location.host}/ws/preview`; const handleMessage = useCallback( (event: MessageEvent) => { try { const message = JSON.parse(event.data) as PreviewMessage; if (message.type === 'generation_start' && message.generation_id) { currentGenerationIdRef.current = message.generation_id; lastStepRef.current = -1; setActivePreviewImage(null); setPreview(null); return; } if ( message.generation_id && currentGenerationIdRef.current && message.generation_id !== currentGenerationIdRef.current ) { return; } if (message.step !== undefined) { if (message.step < lastStepRef.current && message.step !== 0) { return; } lastStepRef.current = message.step; } if (message.images && message.images.length > 0) { setActivePreviewImage(message.images[0]); } setPreview(message); } catch (error) { console.error('Failed to parse websocket message', error); } }, [setPreview], ); useWebSocket(wsUrl, { shouldReconnect: () => true, reconnectInterval: 3000, onOpen: () => setServerStatus(true), onClose: () => setServerStatus(false), onError: () => setServerStatus(false), onMessage: handleMessage, }); useEffect(() => { lastStepRef.current = -1; }, [status]); useEffect(() => { if (status === 'idle') { currentGenerationIdRef.current = null; } }, [status]); const isGenerating = status === 'generating'; const displayImage = isGenerating ? (preview ? activePreviewImage : null) : currentImage; const progressValue = isGenerating && preview?.step !== undefined && preview.total_steps ? (preview.step / preview.total_steps) * 100 : 0; const stepText = isGenerating && preview?.step !== undefined && preview.total_steps ? `Step ${preview.step} / ${preview.total_steps}` : isGenerating ? 'Generating...' : 'Idle'; const handleImportFromPreview = async () => { if (!displayImage) return; const result = await importSettingsFromBase64(displayImage); setFeedback({ tone: result.ok ? (result.warning ? 'warning' : 'success') : 'error', text: result.warning ? `${result.message} ${result.warning}` : result.message, }); }; return (
{isGenerating ? (
Generating
{stepText}
) : null}
{displayImage ? ( Generated preview ) : (
{isGenerating ? : }

{isGenerating ? 'Preparing the next frame' : 'Ready to generate'}

{isGenerating ? 'Live previews appear here as the run progresses.' : 'Choose a model, write a prompt, then generate your first frame.'}

)}
{isGenerating ? (
Progress {Math.round(progressValue)}%
) : null}
{displayImage ? (
) : null} {feedback ? (

{feedback.text}

) : null}
); }