import { useEffect, useRef, useState, useLayoutEffect } from 'react';
import Editor from '@monaco-editor/react';
import DemoSelector from './DemoSelector';
// Auto-scale Hook - simplified back to its original purpose
function useAutoScale(iframeRef, wrapRef) {
useLayoutEffect(() => {
if (!iframeRef.current || !wrapRef.current) return;
const calc = () => {
const doc = iframeRef.current.contentDocument;
if (!doc || !doc.body) return;
// Inject base styles for responsive scaling, if not present
if (!doc.querySelector('style[data-responsive-scale]')) {
const style = doc.createElement('style');
style.setAttribute('data-responsive-scale', '');
style.innerHTML = `
html, body {
margin: 0;
padding: 0;
width: 100%;
height: 100%;
overflow: hidden;
font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, sans-serif;
}
.container {
position: relative;
width: 100%;
height: 100vh;
box-sizing: border-box;
}
.box {
position: absolute;
box-sizing: border-box;
overflow: hidden;
}
.box img { max-width: 100%; height: auto; }
.box p, .box span:not(.sidebar-text) { font-size: max(16px, 1.2vw); line-height: 1.4; }
.box button { font-size: max(14px, 1.0vw); padding: max(6px, 0.4vw) max(12px, 0.8vw); }
.box input { font-size: max(16px, 1.2vw); padding: max(6px, 0.4vw) max(12px, 0.8vw); }
.box svg { width: max(20px, 1.5vw); height: max(20px, 1.5vw); }
`;
doc.head.appendChild(style);
}
};
const iframe = iframeRef.current;
iframe.addEventListener('load', calc);
calc();
window.addEventListener('resize', calc);
return () => {
iframe.removeEventListener('load', calc);
window.removeEventListener('resize', calc);
};
}, [iframeRef, wrapRef]);
}
// Instagram-specific preview component
function InstagramPreview({ code }) {
const iframeRef = useRef(null);
const wrapRef = useRef(null);
useLayoutEffect(() => {
if (!iframeRef.current || !wrapRef.current) return;
const calc = () => {
const doc = iframeRef.current.contentDocument;
if (!doc || !doc.body) return;
// Inject Instagram-specific responsive styles
if (!doc.querySelector('style[data-instagram-responsive]')) {
const style = doc.createElement('style');
style.setAttribute('data-instagram-responsive', '');
style.innerHTML = `
html, body {
margin: 0;
padding: 0;
width: 100%;
height: 100%;
overflow: hidden;
font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, sans-serif;
}
.container {
position: relative;
width: 100%;
height: 100vh;
box-sizing: border-box;
overflow: hidden;
}
.box {
position: absolute;
box-sizing: border-box;
overflow: hidden;
}
// .box img { max-width: 100%; height: auto; }
// .box p, .box span { font-size: max(14px, 1.0vw); line-height: 1.4; }
// .box button { font-size: max(12px, 0.9vw); padding: max(4px, 0.3vw) max(8px, 0.6vw); }
// .box input { font-size: max(14px, 1.0vw); padding: max(4px, 0.3vw) max(8px, 0.6vw); }
// .box svg { width: max(16px, 1.2vw); height: max(16px, 1.2vw); }
`;
doc.head.appendChild(style);
}
};
const iframe = iframeRef.current;
iframe.addEventListener('load', calc);
calc();
window.addEventListener('resize', calc);
return () => {
iframe.removeEventListener('load', calc);
window.removeEventListener('resize', calc);
};
}, [iframeRef, wrapRef]);
return (
);
}
// Design-specific preview component
function DesignPreview({ code }) {
const iframeRef = useRef(null);
const wrapRef = useRef(null);
useLayoutEffect(() => {
if (!iframeRef.current || !wrapRef.current) return;
const calc = () => {
const doc = iframeRef.current.contentDocument;
if (!doc || !doc.body) return;
if (!doc.querySelector('style[data-design-responsive]')) {
const style = doc.createElement('style');
style.setAttribute('data-design-responsive', '');
style.innerHTML = `
html, body {
margin: 0;
padding: 0;
width: 100%;
height: 100%;
overflow: hidden;
font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, sans-serif;
}
.container {
position: relative;
width: 100%;
height: 100vh;
box-sizing: border-box;
overflow: hidden;
}
.box {
position: absolute;
box-sizing: border-box;
overflow: hidden;
}
`;
doc.head.appendChild(style);
}
};
const iframe = iframeRef.current;
iframe.addEventListener('load', calc);
calc();
window.addEventListener('resize', calc);
return () => {
iframe.removeEventListener('load', calc);
window.removeEventListener('resize', calc);
};
}, [iframeRef, wrapRef]);
return (
);
}
// LinkedIn-specific preview component
function LinkedInPreview({ code }) {
const iframeRef = useRef(null);
const wrapRef = useRef(null);
useLayoutEffect(() => {
if (!iframeRef.current || !wrapRef.current) return;
const calc = () => {
const doc = iframeRef.current.contentDocument;
if (!doc || !doc.body) return;
if (!doc.querySelector('style[data-linkedin-responsive]')) {
const style = doc.createElement('style');
style.setAttribute('data-linkedin-responsive', '');
style.innerHTML = `
html, body {
margin: 0;
padding: 0;
width: 100%;
height: 100%;
overflow: hidden;
font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, sans-serif;
}
.container {
position: relative;
width: 100%;
height: 100vh;
box-sizing: border-box;
overflow: hidden;
}
.box {
position: absolute;
box-sizing: border-box;
overflow: hidden;
}
`;
doc.head.appendChild(style);
}
};
const iframe = iframeRef.current;
iframe.addEventListener('load', calc);
calc();
window.addEventListener('resize', calc);
return () => {
iframe.removeEventListener('load', calc);
window.removeEventListener('resize', calc);
};
}, [iframeRef, wrapRef]);
return (
);
}
// Scaled preview component
function ScaledPreview({ code, demoId }) {
const iframeRef = useRef(null);
const wrapRef = useRef(null);
useAutoScale(iframeRef, wrapRef);
if (demoId === 'instagram') {
return ;
}
if (demoId === 'design') {
return ;
}
if (demoId === 'linkedin') {
return ;
}
// Default preview for all other demos
return (
);
}
export default function App() {
const [currentDemo, setCurrentDemo] = useState(null);
const [showDemoSelector, setShowDemoSelector] = useState(true);
const [steps, setSteps] = useState([]);
const [idx, setIdx] = useState(0);
const [progress, setProgress] = useState(0); // Continuous progress percentage
const [code, setCode] = useState(`
Screenshot to Code
🎨
UI2Code Demo
Choose a demo to see how code generation works
Select a demo from the list to start the interactive code generation experience.
`);
const [playing, setPlaying] = useState(false);
const [isGenerating, setIsGenerating] = useState(false);
const [dragOver, setDragOver] = useState(false);
const [loadingError, setLoadingError] = useState(null);
const [uploadedImage, setUploadedImage] = useState(null);
const [imageReady, setImageReady] = useState(false);
const intervalRef = useRef(null);
const progressIntervalRef = useRef(null);
const fileInputRef = useRef(null);
const [designPrompt, setDesignPrompt] = useState('');
// load manifest when demo is selected
useEffect(() => {
if (!currentDemo) return;
fetch(currentDemo.manifest)
.then(res => {
if (!res.ok) {
throw new Error(`HTTP error! status: ${res.status}`);
}
return res.json();
})
.then(data => {
setSteps(data);
setLoadingError(null);
})
.catch(err => {
console.error('Failed to load manifest:', err);
setLoadingError('Failed to load build steps manifest');
// Create a simple default step
setSteps([
{ file: 'demo.html', caption: 'Demo Interface', description: 'Demo Interface' }
]);
});
}, [currentDemo]);
// load code whenever idx changes - but only if image is ready AND playing
useEffect(() => {
if (!steps.length || !imageReady || !playing || !currentDemo) return;
// if finalHtml is available, show finalHtml when idx equals steps.length
if (
currentDemo.finalHtml &&
idx === steps.length
) {
// load finalHtml
fetch(currentDemo.finalHtml)
.then(res => {
if (!res.ok) {
throw new Error(`HTTP error! status: ${res.status}`);
}
return res.text();
})
.then(html => {
setCode(html);
setLoadingError(null);
})
.catch(err => {
console.error('Failed to load final HTML:', err);
setLoadingError('Failed to load final HTML');
setCode(`
Error
⚠️
Final HTML Not Found
Could not load final HTML
The final HTML file might be missing or the path is incorrect.
`);
});
} else {
// normal steps
const step = steps[idx];
if (!step) return;
const demoBasePath = currentDemo.manifest.replace('/manifest.json', '');
fetch(demoBasePath + '/' + step.file)
.then(res => {
if (!res.ok) {
throw new Error(`HTTP error! status: ${res.status}`);
}
return res.text();
})
.then(html => {
setCode(html);
setLoadingError(null);
})
.catch(err => {
console.error('Failed to load step file:', err);
setLoadingError(`Failed to load step: ${step.file}`);
// Create a simple error page
setCode(`
Error
⚠️
File Not Found
Could not load: ${step.file}
This might be because the build steps haven't been generated yet,
or the file path is incorrect.
`);
});
}
}, [idx, steps, imageReady, playing, currentDemo]);
// autoplay with smooth progress and auto-stop at the end
useEffect(() => {
if (!playing) {
clearInterval(progressIntervalRef.current);
return;
}
// Check if the current demo has a specific interval time
const customIntervalTime = currentDemo?.intervalTime;
// Define total duration based on whether a custom interval is set
const totalDuration = customIntervalTime
? customIntervalTime * (steps.length || 1) // Use custom time if available
: (steps.length > 100 ? 100 : 200) * (steps.length || 1); // Default dynamic duration
const updateInterval = 50; // Update UI every 50ms
const progressStep = 100 / (totalDuration / updateInterval);
progressIntervalRef.current = setInterval(() => {
setProgress(currentProgress => {
const newProgress = currentProgress + progressStep;
// Calculate which step we should be at
// if finalHtml is available, total steps include finalHtml
const totalSteps = currentDemo?.finalHtml ? steps.length + 1 : steps.length;
const targetStepIndex = Math.min(totalSteps, Math.floor((newProgress / 100) * totalSteps));
// Directly set the index without causing re-trigger of this effect
setIdx(targetStepIndex);
// If reaching 100%, stop playing
if (newProgress >= 100) {
setPlaying(false);
// Ensure we are at the very end (finalHtml if available, otherwise last step)
setIdx(totalSteps - 1);
return 100;
}
return newProgress;
});
}, updateInterval);
return () => {
clearInterval(progressIntervalRef.current);
};
}, [playing, steps.length, currentDemo]);
const handleDragOver = (e) => {
e.preventDefault();
setDragOver(true);
};
const handleDragLeave = (e) => {
e.preventDefault();
setDragOver(false);
};
const handleDrop = (e) => {
e.preventDefault();
setDragOver(false);
const files = Array.from(e.dataTransfer.files);
handleFiles(files);
};
const handleFileSelect = (e) => {
const files = Array.from(e.target.files);
handleFiles(files);
};
const handleDemoSelect = (demo) => {
setCurrentDemo(demo);
setShowDemoSelector(false);
setUploadedImage(demo.thumbnail);
setImageReady(true);
setIdx(0);
setProgress(0); // Reset progress
setPlaying(false);
// Set ready page
setCode(`
Ready to Generate
🚀
Ready to Generate!
Demo "${demo.name}" is loaded and ready
Click the "▶️ Play" button to start the step-by-step code generation.
`);
};
const handleFiles = (files) => {
const imageFiles = files.filter(file => file.type.startsWith('image/'));
if (imageFiles.length > 0) {
const file = imageFiles[0];
const reader = new FileReader();
reader.onload = (e) => {
console.log('Image loaded:', e.target.result ? 'Success' : 'Failed');
setUploadedImage(e.target.result);
setImageReady(true);
setIdx(0); // Reset to first step
setProgress(0); // Reset progress
setPlaying(false); // Don't auto-play, wait for user to click play
// Set ready page
setCode(`
Ready to Generate
🚀
Ready to Generate!
Your screenshot has been uploaded successfully
Click the "▶️ Play" button to start generating code from your design.
`);
};
reader.onerror = (e) => {
console.error('Error reading file:', e);
setLoadingError('Failed to read image file');
};
reader.readAsDataURL(file);
}
};
const handlePlayToggle = () => {
if (!imageReady && !playing) {
// If no image uploaded, prompt user
return;
}
setPlaying(p => {
const newPlaying = !p;
// If starting to play, reset progress
if (newPlaying) {
setProgress(0);
setIdx(0);
}
// If starting to play, immediately load first step code
if (newPlaying && steps.length > 0 && currentDemo) {
const step = steps[0];
if (step) {
const demoBasePath = currentDemo.manifest.replace('/manifest.json', '');
fetch(demoBasePath + '/' + step.file)
.then(res => {
if (!res.ok) {
throw new Error(`HTTP error! status: ${res.status}`);
}
return res.text();
})
.then(html => {
setCode(html);
setLoadingError(null);
})
.catch(err => {
console.error('Failed to load step file:', err);
setLoadingError(`Failed to load step: ${step.file}`);
});
}
}
return newPlaying;
});
};
const handleReset = () => {
setIdx(0);
setProgress(0); // Reset progress
setPlaying(false);
// If image uploaded, return to Ready state, otherwise return to initial state
if (imageReady) {
setCode(`
Ready to Generate
🚀
Ready to Generate!
Your screenshot has been uploaded successfully
Click the "▶️ Play" button to start generating code from your design.
`);
}
};
const step = steps[idx] || {};
const isDesignDemo = currentDemo?.id === 'design';
// If showing demo selector
if (showDemoSelector) {
return (
);
}
return (
{/* Left side - Screenshot to Code area */}
{/* Title area */}
Screenshot to Code
{currentDemo ? `Demo: ${currentDemo.name}` : 'Drag & drop a screenshot to get started.'}
{/* Upload area */}
{!uploadedImage ? (
fileInputRef.current?.click()}
>
📸
Drop an image here
or click to browse
) : (
✅ Image uploaded successfully!
)}
{/* Upload image preview area */}
{uploadedImage && (
📋 Target Design

console.log('Image rendered successfully')}
onError={(e) => {
console.error('Image render error:', e);
setLoadingError('Failed to display image');
}}
/>
)}
{/* 仅在design demo时显示 Design Prompt,在Target Design和Code Generation之间 */}
{isDesignDemo && (
📝 Design Prompt
)}
{/* Generation progress and status info */}
⚡
Code Generation
{!imageReady ? 'Please upload an image to start' :
playing ? 'Generating code from your design...' :
'Ready to generate code'}
Progress:
{Math.round(progress)}%
Current Step:
{step.caption || 'Loading images... Done!'}
Status:
{playing ? (
<>
Generating
>
) : imageReady ? (
<>
Ready
>
) : (
<>
Waiting
>
)}
{loadingError && (
{loadingError}
)}
{/* Right side - Preview area */}
{/* Preview header */}
Live Preview
{/* Preview content - using ScaledPreview component */}
{/* Bottom control bar */}
);
}