import { useEffect, useRef, useState, type ChangeEvent, type CSSProperties } from 'react'; import { motion, AnimatePresence } from 'framer-motion'; import { AlertTriangle, Check, Download, ExternalLink, Home, Image as ImageIcon, Loader2, RefreshCw, Server, Sparkles, Upload, WandSparkles, } from 'lucide-react'; import { useMarketingStore } from '@/store/useMarketingStore'; import { DREAM_WEAVER_URL, checkDreamWeaverHealth, fetchDreamWeaverResult, getDreamWeaverStatus, submitDreamWeaverJob, type DreamWeaverHealth, type DreamWeaverJobResponse, type DreamWeaverStatusResponse, } from '@/lib/dreamWeaverApi'; const GLASS: CSSProperties = { background: 'rgba(8, 10, 18, 0.82)', border: '1px solid rgba(59,130,246,0.14)', backdropFilter: 'blur(24px)', WebkitBackdropFilter: 'blur(24px)', boxShadow: '0 0 0 1px rgba(255,255,255,0.04), 0 4px 32px rgba(0,0,0,0.55)', }; const INNER: CSSProperties = { background: 'rgba(255,255,255,0.04)', border: '1px solid rgba(255,255,255,0.07)', }; const ROOM_TYPES = [ { id: 'bedroom', label: 'Bedroom' }, { id: 'living_room', label: 'Living Room' }, { id: 'bathroom', label: 'Bathroom' }, { id: 'kitchen', label: 'Kitchen' }, { id: 'dining_room', label: 'Dining Room' }, { id: 'home_office', label: 'Office' }, { id: 'hallway', label: 'Hallway' }, { id: 'balcony', label: 'Balcony' }, ] as const; type ProcessingState = 'idle' | 'checking' | 'submitting' | 'rendering' | 'downloading'; interface DreamWeaverOutput { id: string; roomLabel: string; keywords: string; imageUrl: string; createdAt: Date; } function sleep(ms: number) { return new Promise((resolve) => window.setTimeout(resolve, ms)); } function isReadyStatus(status: DreamWeaverStatusResponse) { const normalized = status.status?.toLowerCase() ?? ''; return Boolean(status.ready) || ['ready', 'completed', 'complete', 'succeeded', 'success', 'finished'].includes(normalized); } function isFailedStatus(status: DreamWeaverStatusResponse) { const normalized = status.status?.toLowerCase() ?? ''; return ['failed', 'error', 'cancelled', 'canceled'].includes(normalized); } function statusLabel(state: ProcessingState, health: DreamWeaverHealth | null) { if (state === 'checking') return 'Checking gateway'; if (state === 'submitting') return 'Submitting render'; if (state === 'rendering') return 'ComfyUI rendering'; if (state === 'downloading') return 'Fetching result'; if (!health) return 'Gateway unknown'; if (health.online && health.routeMounted && health.comfyuiOnline === false) return 'Gateway online · ComfyUI offline'; if (health.online && health.routeMounted && health.comfyuiOnline && health.checkpointReady === false) return 'Gateway online · Model missing'; return health.online && health.routeMounted ? 'Gateway online' : 'Gateway offline'; } function ResultActions({ output }: { output: DreamWeaverOutput }) { function downloadResult() { const anchor = document.createElement('a'); anchor.href = output.imageUrl; anchor.download = `dream-weaver-${output.id}.png`; anchor.click(); } return (
); } export function CatalystDreamWeaverTab() { const { pushLiveEvent } = useMarketingStore(); const fileInputRef = useRef(null); const objectUrlsRef = useRef>(new Set()); const [sourceFile, setSourceFile] = useState(null); const [sourcePreview, setSourcePreview] = useState(null); const [selectedRoomType, setSelectedRoomType] = useState<(typeof ROOM_TYPES)[number]['id']>('bedroom'); const [keywords, setKeywords] = useState(''); const [health, setHealth] = useState(null); const [processingState, setProcessingState] = useState('checking'); const [progress, setProgress] = useState('Checking Dream Weaver gateway...'); const [error, setError] = useState(null); const [currentOutput, setCurrentOutput] = useState(null); const [history, setHistory] = useState([]); const isProcessing = processingState !== 'idle' && processingState !== 'checking'; const roomLabel = ROOM_TYPES.find((room) => room.id === selectedRoomType)?.label ?? 'Bedroom'; useEffect(() => { void refreshHealth(); return () => { objectUrlsRef.current.forEach((url) => URL.revokeObjectURL(url)); objectUrlsRef.current.clear(); }; }, []); function rememberObjectUrl(blobOrFile: Blob) { const url = URL.createObjectURL(blobOrFile); objectUrlsRef.current.add(url); return url; } function setSourceFromFile(file: File) { if (!file.type.startsWith('image/')) { setError('Dream Weaver needs a source room image.'); return; } const previewUrl = rememberObjectUrl(file); setSourceFile(file); setSourcePreview(previewUrl); setCurrentOutput(null); setError(null); } function handleFileChange(event: ChangeEvent) { const file = event.target.files?.[0]; if (file) { setSourceFromFile(file); } event.target.value = ''; } async function refreshHealth() { setProcessingState('checking'); setProgress('Checking Dream Weaver gateway...'); const nextHealth = await checkDreamWeaverHealth(); setHealth(nextHealth); setProcessingState('idle'); setProgress(nextHealth.online && nextHealth.routeMounted ? nextHealth.comfyuiOnline === false ? `Gateway is online and the Dream Weaver route is mounted. ComfyUI is offline${nextHealth.comfyuiUrl ? ` at ${nextHealth.comfyuiUrl}` : ''}.` : nextHealth.checkpointReady === false ? `ComfyUI is online${nextHealth.comfyuiUrl ? ` at ${nextHealth.comfyuiUrl}` : ''}, but no checkpoint model is installed. Hydrate RealVisXL into ComfyUI/models/checkpoints.` : 'Gateway is online and the Dream Weaver route is mounted.' : nextHealth.detail ?? 'Dream Weaver gateway is not reachable.'); } async function pollUntilReady(job: DreamWeaverJobResponse) { let latestResultUrl = job.result_url; for (let attempt = 1; attempt <= 150; attempt += 1) { const status = await getDreamWeaverStatus(job); latestResultUrl = status.result_url ?? latestResultUrl; setProgress(status.status ? `Render ${status.status} · poll ${attempt}/150` : `Render queued · poll ${attempt}/150`); if (isReadyStatus(status)) { return latestResultUrl; } if (isFailedStatus(status) || status.error) { throw new Error(status.error ?? `Dream Weaver render ${status.status ?? 'failed'}.`); } await sleep(2000); } throw new Error('Dream Weaver timed out after 5 minutes.'); } async function generate() { if (!sourceFile || isProcessing) return; setError(null); setCurrentOutput(null); try { setProcessingState('submitting'); setProgress(`Submitting ${roomLabel.toLowerCase()} staging request...`); const job = await submitDreamWeaverJob({ image: sourceFile, roomType: selectedRoomType, keywords, }); setProcessingState('rendering'); setProgress(`Job ${job.job_id} accepted. Waiting for ComfyUI output...`); const resultUrl = await pollUntilReady(job); setProcessingState('downloading'); setProgress('Fetching generated image...'); const resultBlob = await fetchDreamWeaverResult(job.job_id, resultUrl); const imageUrl = rememberObjectUrl(resultBlob); const output: DreamWeaverOutput = { id: job.job_id, roomLabel, keywords: keywords.trim(), imageUrl, createdAt: new Date(), }; setCurrentOutput(output); setHistory((items) => [output, ...items].slice(0, 6)); setProgress('Dream Weaver render complete.'); pushLiveEvent({ id: `dw-${job.job_id}-${Date.now()}`, type: 'create', campaignName: 'Dream Weaver', message: `${roomLabel} staging render completed.`, timestamp: new Date(), }); } catch (err) { const message = err instanceof Error ? err.message : 'Dream Weaver render failed.'; setError(message); setProgress(message); pushLiveEvent({ id: `dw-error-${Date.now()}`, type: 'alert', campaignName: 'Dream Weaver', message, timestamp: new Date(), }); } finally { setProcessingState('idle'); } } return (

Dream Weaver

Room image transformation pipeline using the same Dream Weaver gateway as the iPad app.

{statusLabel(processingState, health)}

Source Room

Upload a ground-truth room photograph, choose the target room type, then add optional styling keywords.

Room type

{ROOM_TYPES.map((room) => { const selected = selectedRoomType === room.id; return ( ); })}