| import { useState, useCallback, useRef, useEffect } from 'react'; |
| import Sidebar from './components/Sidebar'; |
| import Editor from './components/Editor'; |
| import Preview from './components/Preview'; |
| import PromptBar from './components/PromptBar'; |
| import PlanModal from './components/PlanModal'; |
| import SettingsModal from './components/SettingsModal'; |
| import Toasts from './components/Toasts'; |
| import { callMINDI, generateDemo, isQuotaError, isQuotaException, pingAPI } from './services/api'; |
| import { analyzePrompt, enhancePrompt, getQuickEnhancement } from './services/promptEnhancer'; |
| import { parseFiles, buildPreviewHTML } from './services/fileParser'; |
| import './App.css'; |
|
|
| const STORAGE_KEY = 'mindi.builder.v1'; |
|
|
| function loadSettings() { |
| try { |
| return JSON.parse(localStorage.getItem(STORAGE_KEY)) || {}; |
| } catch { return {}; } |
| } |
|
|
| export default function App() { |
| const saved = loadSettings(); |
| const [settings, setSettings] = useState({ |
| apiUrl: saved.apiUrl || 'https://mindigenous-mindi-chat.hf.space', |
| hfToken: saved.hfToken || '', |
| temperature: saved.temperature ?? 0.7, |
| maxTokens: saved.maxTokens ?? 2048, |
| }); |
| const [settingsOpen, setSettingsOpen] = useState(false); |
| const [planModal, setPlanModal] = useState(null); |
| const [toasts, setToasts] = useState([]); |
| const [status, setStatus] = useState('connecting'); |
| const [files, setFiles] = useState([]); |
| const [activeFile, setActiveFile] = useState(null); |
| const [previewHTML, setPreviewHTML] = useState(null); |
| const [isGenerating, setIsGenerating] = useState(false); |
| const [generationProgress, setGenerationProgress] = useState(''); |
| const [agentSteps, setAgentSteps] = useState([]); |
| const [codeLines, setCodeLines] = useState([]); |
| const [consoleOutput, setConsoleOutput] = useState([]); |
| const [history, setHistory] = useState([]); |
| const abortRef = useRef(null); |
|
|
| const addToast = useCallback((msg, type = 'info', ms = 3000) => { |
| const id = Date.now() + Math.random(); |
| setToasts(t => [...t, { id, msg, type }]); |
| setTimeout(() => setToasts(t => t.filter(x => x.id !== id)), ms); |
| }, []); |
|
|
| const saveSettings = useCallback((s) => { |
| setSettings(s); |
| try { localStorage.setItem(STORAGE_KEY, JSON.stringify(s)); } catch {} |
| }, []); |
|
|
| |
| useEffect(() => { |
| const check = async () => { |
| const ok = await pingAPI(settings.apiUrl, settings.hfToken); |
| setStatus(ok ? 'online' : 'demo'); |
| }; |
| check(); |
| const iv = setInterval(check, 60000); |
| return () => clearInterval(iv); |
| }, [settings.apiUrl, settings.hfToken]); |
|
|
| const addAgentStep = useCallback((type, detail, status = 'running') => { |
| const step = { id: Date.now(), type, detail, status, time: new Date() }; |
| setAgentSteps(prev => [...prev, step]); |
| return step.id; |
| }, []); |
|
|
| const updateAgentStep = useCallback((id, updates) => { |
| setAgentSteps(prev => prev.map(s => s.id === id ? { ...s, ...updates } : s)); |
| }, []); |
|
|
| |
| const animateCode = useCallback((code, fileList) => { |
| const lines = code.split('\n'); |
| setCodeLines([]); |
| let i = 0; |
| const interval = setInterval(() => { |
| if (i < lines.length) { |
| setCodeLines(prev => [...prev, { text: lines[i], id: i, visible: true }]); |
| i++; |
| } else { |
| clearInterval(interval); |
| } |
| }, 15); |
| return () => clearInterval(interval); |
| }, []); |
|
|
| |
| const handleGenerate = useCallback(async (userPrompt, skipPlan = false) => { |
| if (!userPrompt.trim() || isGenerating) return; |
|
|
| |
| if (!skipPlan) { |
| const analysis = analyzePrompt(userPrompt); |
| if (analysis.questions.length > 0) { |
| setPlanModal({ userPrompt, questions: analysis.questions }); |
| return; |
| } |
| } |
|
|
| setIsGenerating(true); |
| setAgentSteps([]); |
| setConsoleOutput([]); |
| setCodeLines([]); |
| setFiles([]); |
| setPreviewHTML(null); |
| abortRef.current = new AbortController(); |
|
|
| |
| const planId = addAgentStep('plan', 'Analyzing your request...'); |
| setGenerationProgress('Planning...'); |
| await new Promise(r => setTimeout(r, 400)); |
| updateAgentStep(planId, { status: 'success', detail: 'Requirements analyzed' }); |
|
|
| |
| const enhanceId = addAgentStep('enhance', 'Enhancing prompt for best output...'); |
| const enhanced = getQuickEnhancement(userPrompt); |
| await new Promise(r => setTimeout(r, 300)); |
| updateAgentStep(enhanceId, { status: 'success', detail: 'Prompt optimized' }); |
|
|
| |
| const genId = addAgentStep('generate', 'Generating code with MINDI 1.5...'); |
| setGenerationProgress('Generating code...'); |
|
|
| let result; |
| try { |
| if (status === 'demo' || !settings.apiUrl) { |
| result = await generateDemo(userPrompt); |
| } else { |
| result = await callMINDI({ |
| prompt: enhanced, |
| temperature: settings.temperature, |
| maxTokens: settings.maxTokens, |
| history, |
| hfToken: settings.hfToken, |
| apiUrl: settings.apiUrl, |
| signal: abortRef.current.signal, |
| }); |
| } |
|
|
| if (isQuotaError(result)) { |
| updateAgentStep(genId, { status: 'failed', detail: 'GPU quota — using demo fallback' }); |
| addToast('GPU quota exceeded — showing demo. Add HF token in Settings for real generation.', 'error', 5000); |
| result = await generateDemo(userPrompt); |
| } |
|
|
| updateAgentStep(genId, { status: 'success', detail: `Response received (${(result.response || '').length} chars)` }); |
|
|
| |
| const parseId = addAgentStep('parse', 'Extracting files...'); |
| const parsedFiles = parseFiles(result.response); |
| setFiles(parsedFiles); |
| if (parsedFiles.length > 0) { |
| setActiveFile(parsedFiles[0].id); |
| |
| animateCode(parsedFiles[0].content, parsedFiles); |
| } |
| updateAgentStep(parseId, { status: 'success', detail: `${parsedFiles.length} file(s) extracted` }); |
|
|
| |
| const previewId = addAgentStep('preview', 'Rendering preview...'); |
| const html = buildPreviewHTML(parsedFiles); |
| if (html) { |
| setPreviewHTML(html); |
| updateAgentStep(previewId, { status: 'success', detail: 'Preview rendered' }); |
| setConsoleOutput(prev => [...prev, { type: 'log', text: '✓ Page rendered successfully' }]); |
| } else { |
| updateAgentStep(previewId, { status: 'success', detail: 'No HTML to preview' }); |
| } |
|
|
| |
| setHistory(prev => [ |
| ...prev.slice(-18), |
| { role: 'user', content: userPrompt }, |
| { role: 'assistant', content: result.response }, |
| ]); |
|
|
| |
| addAgentStep('done', 'Generation complete!', 'success'); |
| setGenerationProgress(''); |
|
|
| } catch (err) { |
| updateAgentStep(genId, { status: 'failed', detail: err.message }); |
| addToast(`Error: ${err.message}`, 'error'); |
|
|
| |
| try { |
| result = await generateDemo(userPrompt); |
| const parsedFiles = parseFiles(result.response); |
| setFiles(parsedFiles); |
| if (parsedFiles.length > 0) { |
| setActiveFile(parsedFiles[0].id); |
| animateCode(parsedFiles[0].content, parsedFiles); |
| } |
| const html = buildPreviewHTML(parsedFiles); |
| if (html) setPreviewHTML(html); |
| addAgentStep('done', 'Demo response used as fallback', 'success'); |
| } catch {} |
| } |
|
|
| setIsGenerating(false); |
| setGenerationProgress(''); |
| }, [isGenerating, settings, status, history, addToast, addAgentStep, updateAgentStep, animateCode]); |
|
|
| const handlePlanSubmit = useCallback((userPrompt, answers) => { |
| setPlanModal(null); |
| const enhanced = enhancePrompt(userPrompt, answers); |
| |
| setIsGenerating(true); |
| setAgentSteps([]); |
| setConsoleOutput([]); |
| setCodeLines([]); |
| setFiles([]); |
| setPreviewHTML(null); |
|
|
| (async () => { |
| const genId = addAgentStep('generate', 'Generating with your preferences...'); |
| setGenerationProgress('Generating...'); |
| let result; |
| try { |
| if (status === 'demo') { |
| result = await generateDemo(userPrompt); |
| } else { |
| result = await callMINDI({ prompt: enhanced, temperature: settings.temperature, maxTokens: settings.maxTokens, history, hfToken: settings.hfToken, apiUrl: settings.apiUrl }); |
| } |
| if (isQuotaError(result)) { |
| updateAgentStep(genId, { status: 'failed', detail: 'GPU quota — using demo' }); |
| addToast('GPU quota exceeded — showing demo.', 'error', 4000); |
| result = await generateDemo(userPrompt); |
| } |
| updateAgentStep(genId, { status: 'success', detail: 'Code generated' }); |
| const parsedFiles = parseFiles(result.response); |
| setFiles(parsedFiles); |
| if (parsedFiles.length > 0) { setActiveFile(parsedFiles[0].id); animateCode(parsedFiles[0].content, parsedFiles); } |
| const html = buildPreviewHTML(parsedFiles); |
| if (html) setPreviewHTML(html); |
| setHistory(prev => [...prev.slice(-18), { role: 'user', content: userPrompt }, { role: 'assistant', content: result.response }]); |
| addAgentStep('done', 'Complete!', 'success'); |
| } catch (err) { |
| updateAgentStep(genId, { status: 'failed', detail: err.message }); |
| |
| try { |
| if (isQuotaException(err.message)) { |
| addToast('GPU quota exceeded — showing demo.', 'error', 4000); |
| } |
| result = await generateDemo(userPrompt); |
| const parsedFiles = parseFiles(result.response); |
| setFiles(parsedFiles); |
| if (parsedFiles.length > 0) { setActiveFile(parsedFiles[0].id); animateCode(parsedFiles[0].content, parsedFiles); } |
| const html = buildPreviewHTML(parsedFiles); |
| if (html) { setPreviewHTML(html); setConsoleOutput(prev => [...prev, { type: 'log', text: '✓ Demo preview rendered' }]); } |
| addAgentStep('done', 'Demo fallback used', 'success'); |
| } catch {} |
| } |
| setIsGenerating(false); |
| setGenerationProgress(''); |
| })(); |
| }, [settings, status, history, addToast, addAgentStep, updateAgentStep, animateCode]); |
|
|
| const handleFileSelect = useCallback((fileId) => { |
| setActiveFile(fileId); |
| const file = files.find(f => f.id === fileId); |
| if (file) { |
| setCodeLines(file.content.split('\n').map((text, i) => ({ text, id: i, visible: true }))); |
| } |
| }, [files]); |
|
|
| const handleStop = useCallback(() => { |
| abortRef.current?.abort(); |
| setIsGenerating(false); |
| setGenerationProgress(''); |
| addAgentStep('stop', 'Generation stopped by user', 'failed'); |
| }, [addAgentStep]); |
|
|
| const activeFileData = files.find(f => f.id === activeFile); |
|
|
| return ( |
| <div className="app-shell"> |
| <div className="ambient"> |
| <div className="grid-pattern" /> |
| <div className="blob blob--purple" /> |
| <div className="blob blob--blue" /> |
| </div> |
| |
| <Sidebar |
| files={files} |
| activeFile={activeFile} |
| onFileSelect={handleFileSelect} |
| agentSteps={agentSteps} |
| status={status} |
| isGenerating={isGenerating} |
| onSettingsOpen={() => setSettingsOpen(true)} |
| /> |
| |
| <main className="main-area"> |
| <Editor |
| file={activeFileData} |
| codeLines={codeLines} |
| isGenerating={isGenerating} |
| generationProgress={generationProgress} |
| files={files} |
| activeFile={activeFile} |
| onFileSelect={handleFileSelect} |
| /> |
| |
| <PromptBar |
| onSubmit={handleGenerate} |
| onStop={handleStop} |
| isGenerating={isGenerating} |
| generationProgress={generationProgress} |
| status={status} |
| /> |
| </main> |
| |
| <Preview |
| html={previewHTML} |
| consoleOutput={consoleOutput} |
| isGenerating={isGenerating} |
| /> |
| |
| {planModal && ( |
| <PlanModal |
| userPrompt={planModal.userPrompt} |
| questions={planModal.questions} |
| onSubmit={handlePlanSubmit} |
| onClose={() => setPlanModal(null)} |
| /> |
| )} |
| |
| {settingsOpen && ( |
| <SettingsModal |
| settings={settings} |
| onSave={(s) => { saveSettings(s); setSettingsOpen(false); addToast('Settings saved', 'success'); }} |
| onClose={() => setSettingsOpen(false)} |
| /> |
| )} |
| |
| <Toasts toasts={toasts} /> |
| </div> |
| ); |
| } |
|
|