Spaces:
Running
Running
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 ( | |
<div className="w-full h-full bg-gray-100 flex items-center justify-center p-2"> | |
<div ref={wrapRef} className="w-full h-full bg-white shadow-xl rounded-lg overflow-hidden"> | |
<iframe | |
ref={iframeRef} | |
srcDoc={code} | |
className="w-full h-full border-0" | |
style={{ | |
transform: 'scale(1.0)', | |
transformOrigin: 'center', | |
imageRendering: '-webkit-optimize-contrast' | |
}} | |
/> | |
</div> | |
</div> | |
); | |
} | |
// 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 ( | |
<div className="w-full h-full bg-gray-100 flex items-center justify-center p-2"> | |
<div ref={wrapRef} className="w-full h-full bg-white shadow-xl rounded-lg overflow-hidden"> | |
<iframe | |
ref={iframeRef} | |
srcDoc={code} | |
className="w-full h-full border-0" | |
style={{ | |
transform: 'scale(1.0)', | |
transformOrigin: 'top left', | |
imageRendering: '-webkit-optimize-contrast' | |
}} | |
/> | |
</div> | |
</div> | |
); | |
} | |
// 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 ( | |
<div className="w-full h-full bg-gray-100 flex items-center justify-center p-2"> | |
<div ref={wrapRef} className="w-full h-full bg-white shadow-xl rounded-lg overflow-hidden"> | |
<iframe | |
ref={iframeRef} | |
srcDoc={code} | |
className="w-full h-full border-0" | |
style={{ | |
transform: 'scale(1.01)', | |
transformOrigin: 'center', | |
// imageRendering: '-webkit-optimize-contrast' | |
}} | |
/> | |
</div> | |
</div> | |
); | |
} | |
// Scaled preview component | |
function ScaledPreview({ code, demoId }) { | |
const iframeRef = useRef(null); | |
const wrapRef = useRef(null); | |
useAutoScale(iframeRef, wrapRef); | |
if (demoId === 'instagram') { | |
return <InstagramPreview code={code} />; | |
} | |
if (demoId === 'design') { | |
return <DesignPreview code={code} />; | |
} | |
if (demoId === 'linkedin') { | |
return <LinkedInPreview code={code} />; | |
} | |
// Default preview for all other demos | |
return ( | |
<div ref={wrapRef} className="w-full h-full overflow-hidden bg-gray-50"> | |
<iframe | |
ref={iframeRef} | |
srcDoc={code} | |
className="w-full h-full border-0 bg-white" | |
style={{ | |
imageRendering: '-webkit-optimize-contrast', | |
minHeight: '800px', | |
transform: 'scale(1.1)', | |
transformOrigin: 'top left' | |
}} | |
/> | |
</div> | |
); | |
} | |
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(` | |
<!DOCTYPE html> | |
<html lang="en"> | |
<head> | |
<meta charset="UTF-8"> | |
<meta name="viewport" content="width=device-width, initial-scale=1.0"> | |
<title>Screenshot to Code</title> | |
<script src="https://cdn.tailwindcss.com"></script> | |
</head> | |
<body class="bg-gray-50 flex items-center justify-center min-h-screen"> | |
<div class="text-center"> | |
<div class="text-6xl mb-4">🎨</div> | |
<h1 class="text-2xl font-bold text-gray-800 mb-2">UI2Code Demo</h1> | |
<p class="text-gray-600 mb-4">Choose a demo to see how code generation works</p> | |
<div class="bg-white rounded-lg p-6 shadow-lg max-w-md mx-auto"> | |
<p class="text-sm text-gray-500"> | |
Select a demo from the list to start the interactive code generation experience. | |
</p> | |
</div> | |
</div> | |
</body> | |
</html> | |
`); | |
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(` | |
<!DOCTYPE html> | |
<html lang="en"> | |
<head> | |
<meta charset="UTF-8"> | |
<meta name="viewport" content="width=device-width, initial-scale=1.0"> | |
<title>Error</title> | |
<script src="https://cdn.tailwindcss.com"></script> | |
</head> | |
<body class="bg-gray-100 flex items-center justify-center min-h-screen"> | |
<div class="text-center"> | |
<div class="text-6xl mb-4">⚠️</div> | |
<h1 class="text-2xl font-bold text-gray-800 mb-2">Final HTML Not Found</h1> | |
<p class="text-gray-600 mb-4">Could not load final HTML</p> | |
<div class="bg-white rounded-lg p-6 shadow-lg max-w-md mx-auto"> | |
<p class="text-sm text-gray-500"> | |
The final HTML file might be missing or the path is incorrect. | |
</p> | |
</div> | |
</div> | |
</body> | |
</html> | |
`); | |
}); | |
} 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(` | |
<!DOCTYPE html> | |
<html lang="en"> | |
<head> | |
<meta charset="UTF-8"> | |
<meta name="viewport" content="width=device-width, initial-scale=1.0"> | |
<title>Error</title> | |
<script src="https://cdn.tailwindcss.com"></script> | |
</head> | |
<body class="bg-gray-100 flex items-center justify-center min-h-screen"> | |
<div class="text-center"> | |
<div class="text-6xl mb-4">⚠️</div> | |
<h1 class="text-2xl font-bold text-gray-800 mb-2">File Not Found</h1> | |
<p class="text-gray-600 mb-4">Could not load: ${step.file}</p> | |
<div class="bg-white rounded-lg p-6 shadow-lg max-w-md mx-auto"> | |
<p class="text-sm text-gray-500"> | |
This might be because the build steps haven't been generated yet, | |
or the file path is incorrect. | |
</p> | |
</div> | |
</div> | |
</body> | |
</html> | |
`); | |
}); | |
} | |
}, [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(` | |
<!DOCTYPE html> | |
<html lang="en"> | |
<head> | |
<meta charset="UTF-8"> | |
<meta name="viewport" content="width=device-width, initial-scale=1.0"> | |
<title>Ready to Generate</title> | |
<script src="https://cdn.tailwindcss.com"></script> | |
</head> | |
<body class="bg-gradient-to-br from-blue-50 to-indigo-100 flex items-center justify-center min-h-screen"> | |
<div class="text-center"> | |
<div class="text-6xl mb-4">🚀</div> | |
<h1 class="text-3xl font-bold text-gray-800 mb-2">Ready to Generate!</h1> | |
<p class="text-gray-600 mb-4">Demo "${demo.name}" is loaded and ready</p> | |
<div class="bg-white rounded-lg p-6 shadow-lg max-w-md mx-auto"> | |
<p class="text-sm text-gray-500 mb-4"> | |
Click the "▶️ Play" button to start the step-by-step code generation. | |
</p> | |
<div class="flex items-center justify-center space-x-2"> | |
<div class="w-3 h-3 bg-green-500 rounded-full animate-pulse"></div> | |
<span class="text-sm font-medium text-green-600">Ready to start</span> | |
</div> | |
</div> | |
</div> | |
</body> | |
</html> | |
`); | |
}; | |
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(` | |
<!DOCTYPE html> | |
<html lang="en"> | |
<head> | |
<meta charset="UTF-8"> | |
<meta name="viewport" content="width=device-width, initial-scale=1.0"> | |
<title>Ready to Generate</title> | |
<script src="https://cdn.tailwindcss.com"></script> | |
</head> | |
<body class="bg-gradient-to-br from-blue-50 to-indigo-100 flex items-center justify-center min-h-screen"> | |
<div class="text-center"> | |
<div class="text-6xl mb-4">🚀</div> | |
<h1 class="text-3xl font-bold text-gray-800 mb-2">Ready to Generate!</h1> | |
<p class="text-gray-600 mb-4">Your screenshot has been uploaded successfully</p> | |
<div class="bg-white rounded-lg p-6 shadow-lg max-w-md mx-auto"> | |
<p class="text-sm text-gray-500 mb-4"> | |
Click the "▶️ Play" button to start generating code from your design. | |
</p> | |
<div class="flex items-center justify-center space-x-2"> | |
<div class="w-3 h-3 bg-green-500 rounded-full animate-pulse"></div> | |
<span class="text-sm font-medium text-green-600">Ready to start</span> | |
</div> | |
</div> | |
</div> | |
</body> | |
</html> | |
`); | |
}; | |
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(` | |
<!DOCTYPE html> | |
<html lang="en"> | |
<head> | |
<meta charset="UTF-8"> | |
<meta name="viewport" content="width=device-width, initial-scale=1.0"> | |
<title>Ready to Generate</title> | |
<script src="https://cdn.tailwindcss.com"></script> | |
</head> | |
<body class="bg-gradient-to-br from-blue-50 to-indigo-100 flex items-center justify-center min-h-screen"> | |
<div class="text-center"> | |
<div class="text-6xl mb-4">🚀</div> | |
<h1 class="text-3xl font-bold text-gray-800 mb-2">Ready to Generate!</h1> | |
<p class="text-gray-600 mb-4">Your screenshot has been uploaded successfully</p> | |
<div class="bg-white rounded-lg p-6 shadow-lg max-w-md mx-auto"> | |
<p class="text-sm text-gray-500 mb-4"> | |
Click the "▶️ Play" button to start generating code from your design. | |
</p> | |
<div class="flex items-center justify-center space-x-2"> | |
<div class="w-3 h-3 bg-green-500 rounded-full animate-pulse"></div> | |
<span class="text-sm font-medium text-green-600">Ready to start</span> | |
</div> | |
</div> | |
</div> | |
</body> | |
</html> | |
`); | |
} | |
}; | |
const step = steps[idx] || {}; | |
const isDesignDemo = currentDemo?.id === 'design'; | |
// If showing demo selector | |
if (showDemoSelector) { | |
return ( | |
<div className="min-h-screen bg-gray-50"> | |
<DemoSelector onDemoSelect={handleDemoSelect} currentDemo={currentDemo} /> | |
</div> | |
); | |
} | |
return ( | |
<div className="flex h-screen bg-gray-50"> | |
{/* Left side - Screenshot to Code area */} | |
<div className="w-2/5 bg-white border-r border-gray-200 flex flex-col"> | |
{/* Title area */} | |
<div className="p-4 border-b border-gray-200"> | |
<div className="flex items-center justify-between mb-1"> | |
<h1 className="text-xl font-bold text-gray-900">Screenshot to Code</h1> | |
<button | |
onClick={() => setShowDemoSelector(true)} | |
className="text-xs text-blue-600 hover:text-blue-800 underline" | |
> | |
← Back to Demos | |
</button> | |
</div> | |
<p className="text-sm text-gray-600"> | |
{currentDemo ? `Demo: ${currentDemo.name}` : 'Drag & drop a screenshot to get started.'} | |
</p> | |
</div> | |
{/* Upload area */} | |
<div className="p-4 border-b border-gray-100"> | |
{!uploadedImage ? ( | |
<div | |
className={`border-2 border-dashed rounded-lg p-4 text-center cursor-pointer transition-colors ${ | |
dragOver ? 'border-blue-500 bg-blue-50' : 'border-gray-300 hover:border-gray-400' | |
}`} | |
onDragOver={handleDragOver} | |
onDragLeave={handleDragLeave} | |
onDrop={handleDrop} | |
onClick={() => fileInputRef.current?.click()} | |
> | |
<div className="text-2xl mb-2">📸</div> | |
<p className="text-sm text-gray-600 mb-1">Drop an image here</p> | |
<p className="text-xs text-gray-500">or click to browse</p> | |
</div> | |
) : ( | |
<div className="text-center"> | |
<div className="text-sm text-green-700 font-medium mb-2">✅ Image uploaded successfully!</div> | |
<button | |
className="text-xs text-blue-600 hover:text-blue-800 underline" | |
onClick={() => { | |
setUploadedImage(null); | |
setImageReady(false); | |
setPlaying(false); | |
setIdx(0); | |
setProgress(0); // Reset progress | |
// Clear file input | |
if (fileInputRef.current) { | |
fileInputRef.current.value = ''; | |
} | |
// Reset code to initial state | |
setCode(` | |
<html lang="en"> | |
<head> | |
<meta charset="UTF-8"> | |
<meta name="viewport" content="width=device-width, initial-scale=1.0"> | |
<title>Screenshot to Code</title> | |
<script src="https://cdn.tailwindcss.com"></script> | |
</head> | |
<body class="bg-gray-50 flex items-center justify-center min-h-screen"> | |
<div class="text-center"> | |
<div class="text-6xl mb-4">📸</div> | |
<h1 class="text-2xl font-bold text-gray-800 mb-2">Upload a Screenshot</h1> | |
<p class="text-gray-600 mb-4">Upload an image to start generating code</p> | |
<div class="bg-white rounded-lg p-6 shadow-lg max-w-md mx-auto"> | |
<p class="text-sm text-gray-500"> | |
Click the "▶️ Play" button after uploading your screenshot to begin the code generation process. | |
</p> | |
</div> | |
</div> | |
</body> | |
</html> | |
`); | |
}} | |
> | |
Upload different image | |
</button> | |
</div> | |
)} | |
<input | |
ref={fileInputRef} | |
type="file" | |
accept="image/*" | |
multiple | |
onChange={handleFileSelect} | |
className="hidden" | |
/> | |
</div> | |
{/* Upload image preview area */} | |
{uploadedImage && ( | |
<div className="p-4 border-b border-gray-100"> | |
<h3 className="text-sm font-semibold text-gray-800 mb-2">📋 Target Design</h3> | |
<div className="bg-gray-50 rounded-lg p-2"> | |
<img | |
src={uploadedImage} | |
alt="Uploaded screenshot" | |
className="w-full h-auto rounded-md shadow-sm border border-gray-200" | |
style={{ maxHeight: '300px', objectFit: 'contain' }} | |
onLoad={() => console.log('Image rendered successfully')} | |
onError={(e) => { | |
console.error('Image render error:', e); | |
setLoadingError('Failed to display image'); | |
}} | |
/> | |
</div> | |
</div> | |
)} | |
{/* 仅在design demo时显示 Design Prompt,在Target Design和Code Generation之间 */} | |
{isDesignDemo && ( | |
<div className="p-4 border-b border-blue-200 bg-blue-50"> | |
<h3 className="text-sm font-semibold text-blue-800 mb-2">📝 Design Prompt</h3> | |
<textarea | |
className="w-full min-h-[80px] border border-blue-300 rounded p-2 text-sm focus:outline-none focus:ring-2 focus:ring-blue-400 bg-white" | |
placeholder="Describe your design prompt here..." | |
value={designPrompt} | |
onChange={e => setDesignPrompt(e.target.value)} | |
/> | |
</div> | |
)} | |
{/* Generation progress and status info */} | |
<div className="flex-1 p-4 flex flex-col"> | |
<div className="bg-gradient-to-br from-blue-50 to-indigo-50 rounded-lg p-4 border border-blue-100"> | |
<div className="text-center mb-4"> | |
<div className="text-3xl mb-2">⚡</div> | |
<h3 className="text-lg font-semibold text-gray-800 mb-1">Code Generation</h3> | |
<p className="text-xs text-gray-600"> | |
{!imageReady ? 'Please upload an image to start' : | |
playing ? 'Generating code from your design...' : | |
'Ready to generate code'} | |
</p> | |
</div> | |
<div className="space-y-3"> | |
<div className="flex items-center justify-between"> | |
<span className="text-xs text-gray-600">Progress:</span> | |
<div className="w-full bg-gray-200 rounded-full h-2 ml-3"> | |
<div | |
className="bg-gradient-to-r from-blue-500 to-indigo-500 h-2 rounded-full transition-all duration-100" | |
style={{ width: `${Math.round(progress)}%` }} | |
></div> | |
</div> | |
<span className="text-xs font-medium text-gray-800 ml-2"> | |
{Math.round(progress)}% | |
</span> | |
</div> | |
<div className="flex items-center justify-between"> | |
<span className="text-xs text-gray-600">Current Step:</span> | |
<span className="text-xs font-medium text-gray-800">{step.caption || 'Loading images... Done!'}</span> | |
</div> | |
<div className="flex items-center justify-between"> | |
<span className="text-xs text-gray-600">Status:</span> | |
<span className={`text-xs font-medium flex items-center gap-1 ${ | |
playing ? 'text-green-600' : imageReady ? 'text-blue-600' : 'text-gray-600' | |
}`}> | |
{playing ? ( | |
<> | |
<span className="w-2 h-2 bg-green-500 rounded-full animate-pulse"></span> | |
Generating | |
</> | |
) : imageReady ? ( | |
<> | |
<span className="w-2 h-2 bg-blue-500 rounded-full"></span> | |
Ready | |
</> | |
) : ( | |
<> | |
<span className="w-2 h-2 bg-gray-400 rounded-full"></span> | |
Waiting | |
</> | |
)} | |
</span> | |
</div> | |
</div> | |
{loadingError && ( | |
<div className="mt-3 text-xs text-red-600 bg-red-50 p-2 rounded border border-red-200"> | |
{loadingError} | |
</div> | |
)} | |
</div> | |
</div> | |
</div> | |
{/* Right side - Preview area */} | |
<div className="w-3/5 bg-white flex flex-col"> | |
{/* Preview header */} | |
<div className="border-b border-gray-200 p-4"> | |
<div className="flex items-center justify-between"> | |
<h2 className="text-lg font-semibold text-gray-900">Live Preview</h2> | |
<div className="flex items-center space-x-4"> | |
<button | |
className={`px-3 py-1 rounded text-sm transition-colors ${ | |
!imageReady | |
? 'bg-gray-300 text-gray-500 cursor-not-allowed' | |
: playing | |
? 'bg-red-500 text-white hover:bg-red-600' | |
: 'bg-green-500 text-white hover:bg-green-600' | |
}`} | |
onClick={handlePlayToggle} | |
disabled={!imageReady} | |
title={!imageReady ? 'Please upload an image first' : ''} | |
> | |
{playing ? '⏸️ Pause' : '▶️ Play'} | |
</button> | |
<button | |
className={`px-3 py-1 rounded text-sm transition-colors ${ | |
!imageReady | |
? 'bg-gray-200 text-gray-400 cursor-not-allowed' | |
: 'bg-gray-200 text-gray-700 hover:bg-gray-300' | |
}`} | |
onClick={handleReset} | |
disabled={!imageReady} | |
> | |
🔄 Reset | |
</button> | |
<div className="flex items-center space-x-2"> | |
<div className="w-24 bg-gray-200 rounded-full h-2"> | |
<div | |
className="bg-blue-600 h-2 rounded-full transition-all duration-100" | |
style={{ width: `${Math.round(progress)}%` }} | |
></div> | |
</div> | |
<span className="text-xs text-gray-500 min-w-0"> | |
{Math.round(progress)}% | |
</span> | |
</div> | |
</div> | |
</div> | |
</div> | |
{/* Preview content - using ScaledPreview component */} | |
<div className="flex-1 p-0"> | |
<div className="w-full h-full bg-white overflow-hidden"> | |
<ScaledPreview code={code} demoId={currentDemo?.id} /> | |
</div> | |
</div> | |
{/* Bottom control bar */} | |
<div className="border-t border-gray-200 p-4"> | |
<div className="flex items-center space-x-4"> | |
<input | |
type="range" | |
min={0} | |
max={100} | |
value={Math.round(progress)} | |
onChange={e => { | |
const newProgress = Number(e.target.value); | |
setProgress(newProgress); | |
// Calculate corresponding step based on progress | |
const targetStepIndex = Math.floor((newProgress / 100) * steps.length); | |
if (targetStepIndex < steps.length) { | |
setIdx(targetStepIndex); | |
} | |
}} | |
className="flex-1" | |
disabled={!imageReady} | |
/> | |
<div className="text-sm text-gray-500 min-w-0"> | |
{step.caption || step.description || 'Loading images... Done!'} | |
</div> | |
</div> | |
</div> | |
</div> | |
</div> | |
); | |
} |