Spaces:
Running
Running
| import React, { useState, useEffect, useRef } from 'react'; | |
| import { X, Copy, Check, Terminal, HardHat, Search, Wand2, CheckCircle2, ArrowLeft, Play, Send, MessageSquare, Sparkles, Loader2, History, FileCode, Plus, Minus } from 'lucide-react'; | |
| import { generateCodeWithAgents, AgentStatus, generateCodePatches, applyPatches, DEFAULT_MODEL } from '../services/geminiService'; | |
| import { Node, Edge } from 'reactflow'; | |
| interface Message { | |
| role: 'user' | 'assistant'; | |
| content: string; | |
| summary?: string; | |
| files_changed?: string[]; | |
| } | |
| interface PatchHistory { | |
| id: string; | |
| timestamp: Date; | |
| summary: string; | |
| files_changed: string[]; | |
| patches: { search: string, replace: string }[]; | |
| } | |
| interface CodeViewerProps { | |
| nodes: Node[]; | |
| edges: Edge[]; | |
| promptText: string; | |
| isOpen: boolean; | |
| onClose: () => void; | |
| isLoading: boolean; | |
| } | |
| const CodeViewer: React.FC<CodeViewerProps> = ({ nodes, edges, promptText, isOpen, onClose, isLoading }) => { | |
| const [copied, setCopied] = useState(false); | |
| const [viewMode, setViewMode] = useState<'prompt' | 'building' | 'code'>('prompt'); | |
| const [finalCode, setFinalCode] = useState(''); | |
| const [agentStatus, setAgentStatus] = useState<AgentStatus>('idle'); | |
| const [agentMessage, setAgentMessage] = useState(''); | |
| // Chat State | |
| const [messages, setMessages] = useState<Message[]>([]); | |
| const [inputValue, setInputValue] = useState(''); | |
| const [isChatting, setIsChatting] = useState(false); | |
| const chatEndRef = useRef<HTMLDivElement>(null); | |
| // History & Diff State | |
| const [patchHistory, setPatchHistory] = useState<PatchHistory[]>([]); | |
| const [isHistoryOpen, setIsHistoryOpen] = useState(false); | |
| const [selectedHistoryId, setSelectedHistoryId] = useState<string | null>(null); | |
| const scrollToBottom = () => { | |
| chatEndRef.current?.scrollIntoView({ behavior: "smooth" }); | |
| }; | |
| useEffect(() => { | |
| scrollToBottom(); | |
| }, [messages]); | |
| // Reset state when opening/closing or changing input | |
| useEffect(() => { | |
| if (isOpen) { | |
| setViewMode('prompt'); | |
| setFinalCode(''); | |
| setAgentStatus('idle'); | |
| setMessages([]); | |
| setIsChatting(false); | |
| setPatchHistory([]); | |
| setIsHistoryOpen(false); | |
| setSelectedHistoryId(null); | |
| } | |
| }, [isOpen, promptText]); | |
| const handleSendMessage = async () => { | |
| if (!inputValue.trim() || isChatting) return; | |
| const userMsg = inputValue.trim(); | |
| setInputValue(''); | |
| setMessages(prev => [...prev, { role: 'user', content: userMsg }]); | |
| setIsChatting(true); | |
| try { | |
| const patchResult = await generateCodePatches(finalCode, userMsg, DEFAULT_MODEL); | |
| const updatedCode = applyPatches(finalCode, patchResult.patches); | |
| const newPatch: PatchHistory = { | |
| id: Date.now().toString(), | |
| timestamp: new Date(), | |
| summary: patchResult.summary, | |
| files_changed: patchResult.files_changed, | |
| patches: patchResult.patches | |
| }; | |
| setPatchHistory(prev => [newPatch, ...prev]); | |
| setFinalCode(updatedCode); | |
| setMessages(prev => [...prev, { | |
| role: 'assistant', | |
| content: patchResult.reasoning || "I've applied the requested changes to the code.", | |
| summary: patchResult.summary, | |
| files_changed: patchResult.files_changed | |
| }]); | |
| } catch (error) { | |
| setMessages(prev => [...prev, { | |
| role: 'assistant', | |
| content: "Sorry, I encountered an error while trying to apply those changes." | |
| }]); | |
| } finally { | |
| setIsChatting(false); | |
| } | |
| }; | |
| const handleCopy = (text: string) => { | |
| navigator.clipboard.writeText(text); | |
| setCopied(true); | |
| setTimeout(() => setCopied(false), 2000); | |
| }; | |
| const handleBuildWithAgents = async () => { | |
| setViewMode('building'); | |
| setAgentStatus('architect'); | |
| setAgentMessage('Initializing agents...'); | |
| try { | |
| const result = await generateCodeWithAgents(nodes, edges, promptText, (status, msg) => { | |
| setAgentStatus(status); | |
| setAgentMessage(msg); | |
| }); | |
| setFinalCode(result.code); | |
| // Add initial evaluation/patches to chat | |
| if (result.evaluation) { | |
| const initialMsgs: Message[] = [ | |
| { | |
| role: 'assistant', | |
| content: `I've completed the initial build. Here's my self-evaluation: | |
| - Score: ${result.evaluation.score}/100 | |
| - Missing Components: ${result.evaluation.missing_components.length > 0 ? result.evaluation.missing_components.join(', ') : 'None'} | |
| - Potential Bugs: ${result.evaluation.bugs.length > 0 ? result.evaluation.bugs.join(', ') : 'None'}` | |
| } | |
| ]; | |
| if (result.initialPatches) { | |
| const patchHistoryItem: PatchHistory = { | |
| id: 'initial-auto-fix', | |
| timestamp: new Date(), | |
| summary: result.initialPatches.summary, | |
| files_changed: result.initialPatches.files_changed, | |
| patches: result.initialPatches.patches | |
| }; | |
| setPatchHistory([patchHistoryItem]); | |
| initialMsgs.push({ | |
| role: 'assistant', | |
| content: `I've automatically applied some improvements to address the evaluation findings.`, | |
| summary: result.initialPatches.summary, | |
| files_changed: result.initialPatches.files_changed | |
| }); | |
| } else if (result.autoFixError) { | |
| initialMsgs.push({ | |
| role: 'assistant', | |
| content: `I attempted to auto-fix the issues, but the auto-fixer failed: ${result.autoFixError}` | |
| }); | |
| } else if (result.evaluation.score < 90 && result.evaluation.improvement_tasks.length > 0) { | |
| initialMsgs.push({ | |
| role: 'assistant', | |
| content: `I skipped auto-fixing because patching was not successful or no patches were applied.` | |
| }); | |
| } | |
| setMessages(initialMsgs); | |
| } | |
| // Small delay to show completion state | |
| setTimeout(() => { | |
| setViewMode('code'); | |
| }, 1000); | |
| } catch (error) { | |
| setAgentStatus('error'); | |
| setAgentMessage('Generation failed. Please try again.'); | |
| setTimeout(() => setViewMode('prompt'), 2000); | |
| } | |
| }; | |
| const getStepStatus = (step: AgentStatus, current: AgentStatus) => { | |
| const order = ['idle', 'architect', 'critic', 'refiner', 'evaluator', 'patcher', 'complete']; | |
| const stepIdx = order.indexOf(step); | |
| const currentIdx = order.indexOf(current); | |
| if (current === 'error') return 'text-slate-500'; | |
| if (currentIdx > stepIdx) return 'text-emerald-400'; | |
| if (currentIdx === stepIdx) return 'text-blue-400 animate-pulse'; | |
| return 'text-slate-600'; | |
| }; | |
| const renderAgentStep = (step: AgentStatus, icon: React.ReactNode, label: string) => { | |
| const statusColor = getStepStatus(step, agentStatus); | |
| const isCurrent = agentStatus === step; | |
| const order = ['architect', 'critic', 'refiner', 'evaluator', 'patcher', 'complete']; | |
| const isDone = order.indexOf(agentStatus) > order.indexOf(step); | |
| return ( | |
| <div className={`flex items-center gap-3 p-4 rounded-lg border border-slate-800 ${isCurrent ? 'bg-slate-800/50 border-blue-500/30' : 'bg-slate-900'}`}> | |
| <div className={`${statusColor} transition-colors duration-300`}> | |
| {isDone ? <CheckCircle2 size={24} /> : icon} | |
| </div> | |
| <div className="flex-1"> | |
| <div className={`font-semibold text-sm ${isCurrent ? 'text-blue-200' : 'text-slate-400'}`}>{label}</div> | |
| {isCurrent && ( | |
| <div className="text-xs text-slate-500 mt-1">{agentMessage}</div> | |
| )} | |
| </div> | |
| {isCurrent && <div className="w-2 h-2 rounded-full bg-blue-400 animate-ping" />} | |
| </div> | |
| ); | |
| }; | |
| if (!isOpen) return null; | |
| return ( | |
| <div className="absolute inset-0 z-50 flex items-center justify-center bg-black/70 backdrop-blur-sm p-4 md:p-8"> | |
| <div className="bg-slate-900 w-full max-w-5xl h-[85vh] rounded-xl border border-slate-700 shadow-2xl flex flex-col overflow-hidden animate-in fade-in zoom-in duration-200"> | |
| {/* Header */} | |
| <div className="flex items-center justify-between px-6 py-4 border-b border-slate-800 bg-slate-800/50"> | |
| <h2 className="text-lg font-bold text-white flex items-center gap-2"> | |
| {viewMode === 'prompt' && <span className="text-blue-400">Architecture Prompt</span>} | |
| {viewMode === 'building' && <span className="text-purple-400 flex items-center gap-2"><HardHat size={18}/> Agents Building...</span>} | |
| {viewMode === 'code' && <span className="text-emerald-400 flex items-center gap-2"><Terminal size={18}/> Final Code</span>} | |
| {isLoading && <span className="text-xs text-blue-400 animate-pulse ml-2">Building...</span>} | |
| </h2> | |
| <div className="flex items-center gap-3"> | |
| {viewMode === 'code' && ( | |
| <button | |
| onClick={() => setViewMode('prompt')} | |
| className="flex items-center gap-2 px-3 py-1.5 bg-slate-800 hover:bg-slate-700 rounded text-xs font-medium text-slate-300 transition-colors border border-slate-700" | |
| > | |
| <ArrowLeft size={14} /> Back to Prompt | |
| </button> | |
| )} | |
| {viewMode !== 'building' && ( | |
| <div className="flex items-center gap-2"> | |
| {viewMode === 'code' && ( | |
| <button | |
| onClick={() => setIsHistoryOpen(!isHistoryOpen)} | |
| className={`flex items-center gap-2 px-3 py-1.5 rounded text-xs font-medium transition-colors border ${isHistoryOpen ? 'bg-blue-600 border-blue-500 text-white' : 'bg-slate-800 hover:bg-slate-700 text-slate-300 border-slate-700'}`} | |
| > | |
| <History size={14} /> | |
| Change Logs | |
| </button> | |
| )} | |
| <button | |
| onClick={() => handleCopy(viewMode === 'code' ? finalCode : promptText)} | |
| className="flex items-center gap-2 px-3 py-1.5 bg-slate-800 hover:bg-slate-700 rounded text-xs font-medium text-slate-300 transition-colors border border-slate-700" | |
| > | |
| {copied ? <Check size={14} className="text-emerald-400"/> : <Copy size={14} />} | |
| {copied ? 'Copied' : 'Copy'} | |
| </button> | |
| </div> | |
| )} | |
| {viewMode === 'prompt' && !isLoading && ( | |
| <button | |
| onClick={handleBuildWithAgents} | |
| className="flex items-center gap-2 px-3 py-1.5 bg-purple-600 hover:bg-purple-500 rounded text-xs font-medium text-white transition-colors shadow-lg shadow-purple-500/20" | |
| > | |
| <Play size={14} fill="currentColor" /> | |
| Build with Agents (Beta) | |
| </button> | |
| )} | |
| <button onClick={onClose} className="text-slate-400 hover:text-white transition-colors ml-2"> | |
| <X size={20} /> | |
| </button> | |
| </div> | |
| </div> | |
| {/* Content */} | |
| <div className="flex-1 overflow-hidden bg-[#0d1117] relative"> | |
| {/* View: Loading Prompt */} | |
| {isLoading && viewMode === 'prompt' && ( | |
| <div className="h-full flex flex-col items-center justify-center space-y-4"> | |
| <div className="w-12 h-12 border-4 border-blue-500/30 border-t-blue-500 rounded-full animate-spin"></div> | |
| <p className="text-slate-400 text-sm animate-pulse">Refining architecture prompt...</p> | |
| </div> | |
| )} | |
| {/* View: Prompt Text */} | |
| {!isLoading && viewMode === 'prompt' && ( | |
| <div className="h-full overflow-auto p-6 font-mono text-sm text-slate-300 leading-relaxed whitespace-pre-wrap"> | |
| {promptText} | |
| </div> | |
| )} | |
| {/* View: Agents Building */} | |
| {viewMode === 'building' && ( | |
| <div className="h-full flex flex-col items-center justify-center p-8"> | |
| <div className="w-full max-w-md space-y-3"> | |
| {renderAgentStep('architect', <HardHat size={24} />, "Coder Agent: Writing Implementation")} | |
| {renderAgentStep('critic', <Search size={24} />, "Reviewer Agent: Analyzing Logic")} | |
| {renderAgentStep('refiner', <Wand2 size={24} />, "Polisher Agent: Finalizing Code")} | |
| {renderAgentStep('evaluator', <CheckCircle2 size={24} />, "Evaluator Agent: Verifying Requirements")} | |
| {renderAgentStep('patcher', <Terminal size={24} />, "Patcher Agent: Auto-fixing bugs")} | |
| </div> | |
| {agentStatus === 'complete' && ( | |
| <div className="mt-8 text-emerald-400 font-bold animate-bounce"> | |
| Done! Loading code... | |
| </div> | |
| )} | |
| </div> | |
| )} | |
| {/* View: Final Code */} | |
| {viewMode === 'code' && ( | |
| <div className="flex h-full overflow-hidden"> | |
| <div className="flex-1 overflow-auto p-6 font-mono text-sm leading-relaxed whitespace-pre-wrap text-blue-100 selection:bg-blue-500/30"> | |
| {finalCode} | |
| </div> | |
| {/* History/Diff Overlay */} | |
| {isHistoryOpen && ( | |
| <div className="absolute inset-0 z-40 bg-slate-950 flex flex-col animate-in slide-in-from-right duration-300"> | |
| <div className="flex items-center justify-between p-4 border-b border-slate-800 bg-slate-900"> | |
| <div className="flex items-center gap-3"> | |
| <History size={18} className="text-blue-400" /> | |
| <h3 className="font-bold text-slate-200">Change Logs</h3> | |
| </div> | |
| <button onClick={() => setIsHistoryOpen(false)} className="text-slate-400 hover:text-white"> | |
| <X size={20} /> | |
| </button> | |
| </div> | |
| <div className="flex-1 flex overflow-hidden"> | |
| {/* History List */} | |
| <div className="w-64 border-r border-slate-800 overflow-y-auto p-2 space-y-2 bg-slate-900/50"> | |
| {patchHistory.length === 0 ? ( | |
| <div className="text-center py-10 text-slate-500 text-xs">No changes yet.</div> | |
| ) : ( | |
| patchHistory.map((h) => ( | |
| <button | |
| key={h.id} | |
| onClick={() => setSelectedHistoryId(h.id)} | |
| className={`w-full text-left p-3 rounded-lg border transition-all ${selectedHistoryId === h.id ? 'bg-blue-600/20 border-blue-500/50' : 'bg-slate-800/50 border-slate-700 hover:bg-slate-800'}`} | |
| > | |
| <div className="text-xs font-bold text-slate-200 truncate">{h.summary}</div> | |
| <div className="text-[10px] text-slate-500 mt-1">{h.timestamp.toLocaleTimeString()}</div> | |
| </button> | |
| )) | |
| )} | |
| </div> | |
| {/* Diff Viewer */} | |
| <div className="flex-1 overflow-y-auto p-6 bg-[#0d1117]"> | |
| {selectedHistoryId ? ( | |
| <div className="space-y-6"> | |
| {patchHistory.find(h => h.id === selectedHistoryId)?.patches.map((p, i) => ( | |
| <div key={i} className="border border-slate-800 rounded-lg overflow-hidden"> | |
| <div className="bg-slate-800 px-4 py-2 text-xs font-mono text-slate-300 border-b border-slate-700 flex items-center gap-2"> | |
| <FileCode size={14} /> | |
| Patch {i + 1} | |
| </div> | |
| <div className="font-mono text-xs overflow-x-auto"> | |
| <div className="bg-red-900/20 p-4 border-b border-red-900/30 relative"> | |
| <Minus size={12} className="absolute left-1 top-4 text-red-500" /> | |
| <div className="text-red-200 opacity-70 line-through whitespace-pre">{p.search}</div> | |
| </div> | |
| <div className="bg-emerald-900/20 p-4 relative"> | |
| <Plus size={12} className="absolute left-1 top-4 text-emerald-500" /> | |
| <div className="text-emerald-200 whitespace-pre">{p.replace}</div> | |
| </div> | |
| </div> | |
| </div> | |
| ))} | |
| </div> | |
| ) : ( | |
| <div className="h-full flex flex-col items-center justify-center text-slate-500"> | |
| <History size={48} className="opacity-20 mb-4" /> | |
| <p>Select a change log to view the diff.</p> | |
| </div> | |
| )} | |
| </div> | |
| </div> | |
| </div> | |
| )} | |
| {/* Chat Sidebar */} | |
| <div className="w-80 border-l border-slate-800 bg-slate-900/50 flex flex-col"> | |
| <div className="p-4 border-b border-slate-800 flex items-center gap-2 text-slate-300 font-semibold text-sm"> | |
| <MessageSquare size={16} className="text-blue-400" /> | |
| AI Assistant | |
| </div> | |
| <div className="flex-1 overflow-y-auto p-4 space-y-4 scrollbar-thin scrollbar-thumb-slate-800"> | |
| {messages.length === 0 && ( | |
| <div className="text-center py-8"> | |
| <Sparkles className="mx-auto text-slate-700 mb-2" size={32} /> | |
| <p className="text-xs text-slate-500">Ask me to modify the code or add features.</p> | |
| </div> | |
| )} | |
| {messages.map((msg, i) => ( | |
| <div key={i} className={`flex flex-col ${msg.role === 'user' ? 'items-end' : 'items-start'}`}> | |
| <div className={`max-w-[90%] p-3 rounded-lg text-xs leading-relaxed ${ | |
| msg.role === 'user' | |
| ? 'bg-blue-600 text-white rounded-tr-none' | |
| : 'bg-slate-800 text-slate-300 rounded-tl-none border border-slate-700' | |
| }`}> | |
| {msg.content} | |
| {msg.role === 'assistant' && msg.summary && ( | |
| <div className="mt-3 pt-3 border-t border-slate-700 space-y-2"> | |
| <div className="flex items-center gap-2 text-[10px] font-bold text-emerald-400 uppercase tracking-wider"> | |
| <CheckCircle2 size={10} /> | |
| Update Complete | |
| </div> | |
| <div className="text-slate-200 font-medium">{msg.summary}</div> | |
| {msg.files_changed && msg.files_changed.length > 0 && ( | |
| <div className="flex items-center gap-1.5 text-[10px] text-slate-500"> | |
| <FileCode size={10} /> | |
| {msg.files_changed.length} file{msg.files_changed.length > 1 ? 's' : ''} changed | |
| </div> | |
| )} | |
| </div> | |
| )} | |
| </div> | |
| </div> | |
| ))} | |
| {isChatting && ( | |
| <div className="flex items-center gap-2 text-slate-500 text-xs animate-pulse"> | |
| <Loader2 size={12} className="animate-spin" /> | |
| Applying patches... | |
| </div> | |
| )} | |
| <div ref={chatEndRef} /> | |
| </div> | |
| <div className="p-4 border-t border-slate-800"> | |
| <div className="relative"> | |
| <input | |
| type="text" | |
| value={inputValue} | |
| onChange={(e) => setInputValue(e.target.value)} | |
| onKeyDown={(e) => e.key === 'Enter' && handleSendMessage()} | |
| placeholder="Request changes..." | |
| className="w-full bg-slate-950 border border-slate-700 rounded-lg pl-3 pr-10 py-2 text-xs text-slate-200 focus:outline-none focus:border-blue-500 transition-colors" | |
| /> | |
| <button | |
| onClick={handleSendMessage} | |
| disabled={!inputValue.trim() || isChatting} | |
| className="absolute right-2 top-1/2 -translate-y-1/2 text-slate-500 hover:text-blue-400 disabled:opacity-50 disabled:hover:text-slate-500 transition-colors" | |
| > | |
| <Send size={16} /> | |
| </button> | |
| </div> | |
| </div> | |
| </div> | |
| </div> | |
| )} | |
| </div> | |
| </div> | |
| </div> | |
| ); | |
| }; | |
| export default CodeViewer; |