Download App.tsx from RosticFACE/UI-Generator-from-Image: direct link, hf CLI and curl.
- Browser
- Download file 14 kB
-
https://huggingface.co/spaces/RosticFACE/UI-Generator-from-Image/resolve/main/App.tsx
- Command line
-
hf download hf://spaces/RosticFACE/UI-Generator-from-Image/App.tsx
-
curl -L -o App.tsx https://huggingface.co/spaces/RosticFACE/UI-Generator-from-Image/resolve/main/App.tsx
14 kB
| import React, { useState, useCallback, useEffect } from 'react'; | |
| import { AppStatus, Provider, AppType } from './types'; | |
| import { generateCode } from './services/geminiService'; | |
| import ImageUploader from './components/ImageUploader'; | |
| import CodeDisplay from './components/CodeDisplay'; | |
| import Loader from './components/Loader'; | |
| import PythonIcon from './components/icons/PythonIcon'; | |
| import CppIcon from './components/icons/CppIcon'; | |
| import SparklesIcon from './components/icons/SparklesIcon'; | |
| import ApiConfig from './components/ApiConfig'; | |
| import AppTypeSelector from './components/AppTypeSelector'; | |
| const App: React.FC = () => { | |
| const [status, setStatus] = useState<AppStatus>(AppStatus.IDLE); | |
| const [appType, setAppType] = useState<AppType | null>(null); | |
| const [generatedCode, setGeneratedCode] = useState<string>(''); | |
| const [generatedFiles, setGeneratedFiles] = useState<Record<string, string> | null>(null); | |
| const [errorMessage, setErrorMessage] = useState<string>(''); | |
| const [imageFiles, setImageFiles] = useState<File[]>([]); | |
| const [imagePreviewUrls, setImagePreviewUrls] = useState<string[]>([]); | |
| const [instructions, setInstructions] = useState<string>(''); | |
| const [loadingMessage, setLoadingMessage] = useState<string>(''); | |
| const [provider, setProvider] = useState<Provider>('gemini'); | |
| const [geminiApiKey, setGeminiApiKey] = useState<string>(''); | |
| const [openaiApiKey, setOpenaiApiKey] = useState<string>(''); | |
| const [anthropicApiKey, setAnthropicApiKey] = useState<string>(''); | |
| useEffect(() => { | |
| // Load keys and provider from localStorage on initial render. | |
| const savedGeminiKey = localStorage.getItem('geminiApiKey'); | |
| const savedOpenaiKey = localStorage.getItem('openaiApiKey'); | |
| const savedAnthropicKey = localStorage.getItem('anthropicApiKey'); | |
| const savedProvider = localStorage.getItem('provider') as Provider; | |
| if (savedGeminiKey) setGeminiApiKey(savedGeminiKey); | |
| if (savedOpenaiKey) setOpenaiApiKey(savedOpenaiKey); | |
| if (savedAnthropicKey) setAnthropicApiKey(savedAnthropicKey); | |
| if (savedProvider) setProvider(savedProvider); | |
| // Effect to clean up object URLs | |
| return () => { | |
| imagePreviewUrls.forEach(url => URL.revokeObjectURL(url)); | |
| }; | |
| }, []); // Run only once on mount | |
| const resetStateForNewRun = () => { | |
| setGeneratedCode(''); | |
| setGeneratedFiles(null); | |
| setErrorMessage(''); | |
| setStatus(AppStatus.IDLE); | |
| } | |
| const handleProviderChange = (newProvider: Provider) => { | |
| setProvider(newProvider); | |
| localStorage.setItem('provider', newProvider); | |
| }; | |
| const handleApiKeyChange = (keyType: Provider, key: string) => { | |
| switch (keyType) { | |
| case 'gemini': | |
| setGeminiApiKey(key); | |
| localStorage.setItem('geminiApiKey', key); | |
| break; | |
| case 'openai': | |
| setOpenaiApiKey(key); | |
| localStorage.setItem('openaiApiKey', key); | |
| break; | |
| case 'anthropic': | |
| setAnthropicApiKey(key); | |
| localStorage.setItem('anthropicApiKey', key); | |
| break; | |
| } | |
| }; | |
| const fileToBase64 = (file: File): Promise<{mimeType: string, data: string}> => { | |
| return new Promise((resolve, reject) => { | |
| const reader = new FileReader(); | |
| reader.readAsDataURL(file); | |
| reader.onload = () => { | |
| const result = reader.result as string; | |
| const [header, data] = result.split(','); | |
| const mimeType = header.match(/:(.*?);/)?.[1] || 'application/octet-stream'; | |
| resolve({ mimeType, data }); | |
| }; | |
| reader.onerror = error => reject(error); | |
| }); | |
| }; | |
| const handleImageUpload = (newFiles: File[]) => { | |
| const filesToUpload = newFiles.slice(0, 10); | |
| setImageFiles(filesToUpload); | |
| // Revoke old URLs before creating new ones to prevent memory leaks | |
| imagePreviewUrls.forEach(url => URL.revokeObjectURL(url)); | |
| setImagePreviewUrls(filesToUpload.map(file => URL.createObjectURL(file))); | |
| resetStateForNewRun(); | |
| }; | |
| const handleRemoveImage = (indexToRemove: number) => { | |
| const updatedFiles = imageFiles.filter((_, index) => index !== indexToRemove); | |
| URL.revokeObjectURL(imagePreviewUrls[indexToRemove]); | |
| const updatedUrls = imagePreviewUrls.filter((_, index) => index !== indexToRemove); | |
| setImageFiles(updatedFiles); | |
| setImagePreviewUrls(updatedUrls); | |
| } | |
| const handleGenerateClick = useCallback(async () => { | |
| if (!appType) return; | |
| const apiKeys = { gemini: geminiApiKey, openai: openaiApiKey, anthropic: anthropicApiKey }; | |
| const apiKey = apiKeys[provider]; | |
| if (!apiKey) { | |
| setErrorMessage(`Please provide an API key for ${provider.charAt(0).toUpperCase() + provider.slice(1)}.`); | |
| setStatus(AppStatus.ERROR); | |
| return; | |
| } | |
| if (imageFiles.length === 0) { | |
| setErrorMessage('Please upload at least one image first.'); | |
| setStatus(AppStatus.ERROR); | |
| return; | |
| } | |
| setStatus(AppStatus.PROCESSING); | |
| setErrorMessage(''); | |
| setGeneratedCode(''); | |
| setGeneratedFiles(null); | |
| setLoadingMessage('Preparing to generate...'); | |
| try { | |
| const imageParts = await Promise.all(imageFiles.map(file => fileToBase64(file))); | |
| const result = await generateCode( | |
| appType, | |
| provider, | |
| apiKey, | |
| imageParts, | |
| instructions, | |
| (statusMsg) => setLoadingMessage(statusMsg), | |
| (chunk) => { | |
| if (appType === 'python') { | |
| setGeneratedCode(prev => prev + chunk); | |
| } else { | |
| // For C++, we accumulate the JSON string representation | |
| setGeneratedCode(prev => prev + chunk); | |
| } | |
| } | |
| ); | |
| if(appType === 'c++') { | |
| try { | |
| const finalJsonString = result as string; | |
| // Clean potential markdown fences | |
| const fenceRegex = /^```(json)?\s*\n?(.*?)\n?\s*```$/s; | |
| const match = finalJsonString.match(fenceRegex); | |
| const cleanedJson = match ? match[2] : finalJsonString; | |
| const files = JSON.parse(cleanedJson); | |
| setGeneratedFiles(files); | |
| setGeneratedCode(''); // Clear the raw string | |
| } catch(e) { | |
| console.error("Failed to parse C++ file JSON:", e); | |
| throw new Error("AI returned an invalid format for C++ files. Please try again."); | |
| } | |
| } | |
| setStatus(AppStatus.SUCCESS); | |
| } catch (error) { | |
| console.error(error); | |
| setErrorMessage(error instanceof Error ? error.message : 'An unknown error occurred.'); | |
| setStatus(AppStatus.ERROR); | |
| } | |
| }, [appType, imageFiles, instructions, provider, geminiApiKey, openaiApiKey, anthropicApiKey]); | |
| if (!appType) { | |
| return <AppTypeSelector onSelect={setAppType} />; | |
| } | |
| const OutputArea: React.FC = () => { | |
| switch (status) { | |
| case AppStatus.PROCESSING: | |
| return ( | |
| <div className="flex flex-col items-center justify-center h-full text-center p-4"> | |
| <Loader /> | |
| <p className="mt-4 text-lg text-gray-300">{loadingMessage || 'Initializing...'}</p> | |
| <p className="text-sm text-gray-500 capitalize">Using {provider} to analyze UI and generate code.</p> | |
| { appType === 'python' && generatedCode && | |
| <pre className="mt-4 w-full max-h-64 overflow-y-auto bg-gray-900/50 p-4 rounded-lg text-left"> | |
| <code className="language-python text-xs font-code whitespace-pre-wrap text-gray-400"> | |
| {generatedCode} | |
| </code> | |
| </pre> | |
| } | |
| </div> | |
| ); | |
| case AppStatus.SUCCESS: | |
| return <CodeDisplay | |
| appType={appType} | |
| pythonCode={generatedCode} | |
| cppFiles={generatedFiles} | |
| status={status} />; | |
| case AppStatus.ERROR: | |
| return ( | |
| <div className="flex items-center justify-center h-full"> | |
| <div className="text-center bg-red-900/20 border border-red-600 p-6 rounded-lg"> | |
| <h3 className="text-2xl font-bold text-red-400">Generation Failed</h3> | |
| <p className="mt-2 text-red-300">{errorMessage}</p> | |
| </div> | |
| </div> | |
| ); | |
| case AppStatus.IDLE: | |
| default: | |
| return ( | |
| <div className="flex flex-col items-center justify-center h-full text-center text-gray-500 p-8"> | |
| {appType === 'python' ? <PythonIcon className="w-24 h-24 mb-4 opacity-20" /> : <CppIcon className="w-24 h-24 mb-4 opacity-20" />} | |
| <h3 className="text-2xl font-semibold text-gray-400">Functional Code Output</h3> | |
| <p className="mt-2 max-w-sm">Configure your API, upload UI images, and provide instructions to generate a functional {appType === 'python' ? 'Python' : 'C++'} script.</p> | |
| </div> | |
| ); | |
| } | |
| }; | |
| const isApiKeyMissing = !( (provider === 'gemini' && geminiApiKey) || (provider === 'openai' && openaiApiKey) || (provider === 'anthropic' && anthropicApiKey) ); | |
| return ( | |
| <div className="min-h-screen bg-gray-900 text-white flex flex-col p-4 sm:p-6 lg:p-8"> | |
| <header className="text-center mb-8"> | |
| <button onClick={() => setAppType(null)} className="text-gray-400 hover:text-white mb-4 text-sm">← Back to selection</button> | |
| <h1 className="text-4xl sm:text-5xl font-bold bg-gradient-to-r from-green-300 via-blue-400 to-purple-500 text-transparent bg-clip-text"> | |
| Functional {appType === 'python' ? 'PyQt' : 'C++ Qt'} App Generator | |
| </h1> | |
| <p className="text-gray-400 mt-2 text-lg">Turn UI Images into Functional {appType === 'python' ? 'Python' : 'C++'} Apps with AI</p> | |
| </header> | |
| <main className="flex-grow grid grid-cols-1 lg:grid-cols-2 gap-8"> | |
| <div className="bg-gray-800/50 border border-gray-700 rounded-2xl p-6 flex flex-col shadow-2xl shadow-black/20"> | |
| <div className="flex-grow"> | |
| <ApiConfig | |
| provider={provider} | |
| setProvider={handleProviderChange} | |
| apiKeys={{ gemini: geminiApiKey, openai: openaiApiKey, anthropic: anthropicApiKey }} | |
| setApiKey={handleApiKeyChange} | |
| isProcessing={status === AppStatus.PROCESSING} | |
| /> | |
| <div> | |
| <h2 className="text-2xl font-semibold mb-4 text-gray-200">1. Upload UI Images</h2> | |
| <ImageUploader onImageUpload={handleImageUpload} isProcessing={status === AppStatus.PROCESSING} /> | |
| {imagePreviewUrls.length > 0 && ( | |
| <div className="mt-6 w-full"> | |
| <p className="text-center mb-3 font-semibold text-gray-300">Image Previews ({imagePreviewUrls.length}/10)</p> | |
| <div className="grid grid-cols-2 sm:grid-cols-3 md:grid-cols-4 lg:grid-cols-3 xl:grid-cols-4 gap-4 p-4 border-2 border-dashed border-gray-600 rounded-xl bg-gray-900/50 max-h-80 overflow-y-auto"> | |
| {imagePreviewUrls.map((url, index) => ( | |
| <div key={url} className="relative group aspect-square"> | |
| <img src={url} alt={`UI Preview ${index + 1}`} className="w-full h-full object-contain rounded-lg bg-gray-800" /> | |
| <button | |
| onClick={() => handleRemoveImage(index)} | |
| disabled={status === AppStatus.PROCESSING} | |
| className="absolute -top-2 -right-2 bg-red-600 text-white rounded-full w-6 h-6 flex items-center justify-center text-sm font-bold opacity-0 group-hover:opacity-100 transition-opacity duration-200 hover:bg-red-700 disabled:opacity-50 disabled:cursor-not-allowed" | |
| aria-label={`Remove image ${index + 1}`} | |
| > | |
| × | |
| </button> | |
| </div> | |
| ))} | |
| </div> | |
| </div> | |
| )} | |
| </div> | |
| <div className="mt-8"> | |
| <h2 className="text-2xl font-semibold mb-4 text-gray-200">2. Add Instructions (Optional)</h2> | |
| <textarea | |
| value={instructions} | |
| onChange={(e) => setInstructions(e.target.value)} | |
| disabled={status === AppStatus.PROCESSING} | |
| placeholder="e.g., 'Add a dark mode toggle', 'Make the primary button green'" | |
| className="w-full h-28 p-3 bg-gray-900/70 border-2 border-gray-600 rounded-xl text-gray-300 placeholder-gray-500 focus:ring-2 focus:ring-blue-500 focus:border-blue-500 transition-all duration-200 resize-none" | |
| aria-label="Refinement instructions" | |
| /> | |
| </div> | |
| </div> | |
| <button | |
| onClick={handleGenerateClick} | |
| disabled={imageFiles.length === 0 || status === AppStatus.PROCESSING || isApiKeyMissing} | |
| className="w-full mt-6 py-4 px-6 rounded-xl bg-gradient-to-r from-blue-500 to-purple-600 text-white font-bold text-lg shadow-lg hover:shadow-xl hover:from-blue-600 hover:to-purple-700 transition-all duration-300 disabled:opacity-50 disabled:cursor-not-allowed disabled:shadow-none flex items-center justify-center gap-3" | |
| > | |
| <SparklesIcon className="w-6 h-6"/> | |
| {status === AppStatus.PROCESSING ? 'Generating...' : '3. Generate Code'} | |
| </button> | |
| </div> | |
| <div className="bg-gray-800/50 border border-gray-700 rounded-2xl flex flex-col shadow-2xl shadow-black/20 overflow-hidden"> | |
| <h2 className="text-2xl font-semibold p-6 text-gray-200 border-b border-gray-700">Get Your {appType === 'python' ? 'Python' : 'C++'} Code</h2> | |
| <div className="flex-grow bg-gray-900/70 overflow-y-auto"> | |
| <OutputArea /> | |
| </div> | |
| </div> | |
| </main> | |
| <footer className="text-center mt-8 text-gray-500 text-sm"> | |
| <p>Powered by AI. Generated code may require adjustments.</p> | |
| </footer> | |
| </div> | |
| ); | |
| }; | |
| export default App; |