Spaces:
Running
Running
Upload 7 files
Browse files- components/AIBuilderModal.tsx +184 -0
- components/ApiKeyModal.tsx +84 -0
- components/CodeViewer.tsx +188 -0
- components/CustomNode.tsx +183 -0
- components/PropertiesPanel.tsx +112 -0
- components/Sidebar.tsx +89 -0
- components/SuggestionsModal.tsx +78 -0
components/AIBuilderModal.tsx
ADDED
|
@@ -0,0 +1,184 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
import React, { useState, useEffect, useRef } from 'react';
|
| 2 |
+
import { Sparkles, X, MessageSquare, Play, RefreshCw, AlertCircle, HardHat, Search, Wand2, CheckCircle2 } from 'lucide-react';
|
| 3 |
+
import { generateGraphWithAgents, AgentStatus } from '../services/geminiService';
|
| 4 |
+
import { Node } from 'reactflow';
|
| 5 |
+
import { NodeData } from '../types';
|
| 6 |
+
|
| 7 |
+
interface AIBuilderModalProps {
|
| 8 |
+
isOpen: boolean;
|
| 9 |
+
onClose: () => void;
|
| 10 |
+
onApply: (nodes: any[], edges: any[]) => void;
|
| 11 |
+
currentNodes: Node<NodeData>[];
|
| 12 |
+
}
|
| 13 |
+
|
| 14 |
+
const AIBuilderModal: React.FC<AIBuilderModalProps> = ({ isOpen, onClose, onApply, currentNodes }) => {
|
| 15 |
+
const [prompt, setPrompt] = useState('');
|
| 16 |
+
const [isBuilding, setIsBuilding] = useState(false);
|
| 17 |
+
const [error, setError] = useState<string | null>(null);
|
| 18 |
+
|
| 19 |
+
const [agentStatus, setAgentStatus] = useState<AgentStatus>('idle');
|
| 20 |
+
const [agentMessage, setAgentMessage] = useState('');
|
| 21 |
+
|
| 22 |
+
// Reset state when opened
|
| 23 |
+
useEffect(() => {
|
| 24 |
+
if (isOpen) {
|
| 25 |
+
setAgentStatus('idle');
|
| 26 |
+
setAgentMessage('');
|
| 27 |
+
setError(null);
|
| 28 |
+
}
|
| 29 |
+
}, [isOpen]);
|
| 30 |
+
|
| 31 |
+
if (!isOpen) return null;
|
| 32 |
+
|
| 33 |
+
const handleBuild = async () => {
|
| 34 |
+
if (!prompt.trim()) return;
|
| 35 |
+
setIsBuilding(true);
|
| 36 |
+
setError(null);
|
| 37 |
+
|
| 38 |
+
try {
|
| 39 |
+
const result = await generateGraphWithAgents(prompt, currentNodes, (status, msg) => {
|
| 40 |
+
setAgentStatus(status);
|
| 41 |
+
setAgentMessage(msg);
|
| 42 |
+
});
|
| 43 |
+
|
| 44 |
+
if (result && result.nodes && result.edges) {
|
| 45 |
+
// Ensure nodes have correct style defaults if missing
|
| 46 |
+
const processedNodes = result.nodes.map((n: any) => ({
|
| 47 |
+
...n,
|
| 48 |
+
type: 'custom', // enforce custom type
|
| 49 |
+
data: {
|
| 50 |
+
...n.data,
|
| 51 |
+
// ensure label exists if AI forgot it
|
| 52 |
+
label: n.data.label || n.data.type
|
| 53 |
+
}
|
| 54 |
+
}));
|
| 55 |
+
|
| 56 |
+
// Ensure edges have styling
|
| 57 |
+
const processedEdges = result.edges.map((e: any) => ({
|
| 58 |
+
...e,
|
| 59 |
+
animated: true,
|
| 60 |
+
style: { stroke: '#94a3b8' }
|
| 61 |
+
}));
|
| 62 |
+
|
| 63 |
+
// Small delay to let user see "Complete" state
|
| 64 |
+
setTimeout(() => {
|
| 65 |
+
onApply(processedNodes, processedEdges);
|
| 66 |
+
onClose();
|
| 67 |
+
setIsBuilding(false);
|
| 68 |
+
}, 1000);
|
| 69 |
+
} else {
|
| 70 |
+
throw new Error("Invalid response format from AI");
|
| 71 |
+
}
|
| 72 |
+
} catch (err) {
|
| 73 |
+
setError(err instanceof Error ? err.message : "Failed to generate architecture");
|
| 74 |
+
setAgentStatus('error');
|
| 75 |
+
setIsBuilding(false);
|
| 76 |
+
}
|
| 77 |
+
};
|
| 78 |
+
|
| 79 |
+
const getStepStatus = (step: AgentStatus, current: AgentStatus) => {
|
| 80 |
+
const order = ['idle', 'architect', 'critic', 'refiner', 'complete'];
|
| 81 |
+
const stepIdx = order.indexOf(step);
|
| 82 |
+
const currentIdx = order.indexOf(current);
|
| 83 |
+
|
| 84 |
+
if (current === 'error') return 'text-slate-500';
|
| 85 |
+
if (currentIdx > stepIdx) return 'text-emerald-400';
|
| 86 |
+
if (currentIdx === stepIdx) return 'text-blue-400 animate-pulse';
|
| 87 |
+
return 'text-slate-600';
|
| 88 |
+
};
|
| 89 |
+
|
| 90 |
+
const renderAgentStep = (step: AgentStatus, icon: React.ReactNode, label: string) => {
|
| 91 |
+
const statusColor = getStepStatus(step, agentStatus);
|
| 92 |
+
const isCurrent = agentStatus === step;
|
| 93 |
+
const isDone = ['architect', 'critic', 'refiner', 'complete'].indexOf(agentStatus) > ['architect', 'critic', 'refiner'].indexOf(step);
|
| 94 |
+
|
| 95 |
+
return (
|
| 96 |
+
<div className={`flex items-center gap-3 p-3 rounded-lg border border-slate-800 ${isCurrent ? 'bg-slate-800/50 border-blue-500/30' : 'bg-slate-900'}`}>
|
| 97 |
+
<div className={`${statusColor} transition-colors duration-300`}>
|
| 98 |
+
{isDone ? <CheckCircle2 size={24} /> : icon}
|
| 99 |
+
</div>
|
| 100 |
+
<div className="flex-1">
|
| 101 |
+
<div className={`font-semibold text-sm ${isCurrent ? 'text-blue-200' : 'text-slate-400'}`}>{label}</div>
|
| 102 |
+
{isCurrent && (
|
| 103 |
+
<div className="text-xs text-slate-500 mt-1">{agentMessage}</div>
|
| 104 |
+
)}
|
| 105 |
+
</div>
|
| 106 |
+
{isCurrent && <div className="w-2 h-2 rounded-full bg-blue-400 animate-ping" />}
|
| 107 |
+
</div>
|
| 108 |
+
);
|
| 109 |
+
}
|
| 110 |
+
|
| 111 |
+
return (
|
| 112 |
+
<div className="absolute inset-0 z-50 flex items-center justify-center bg-black/70 backdrop-blur-sm p-4">
|
| 113 |
+
<div className="bg-slate-950 w-full max-w-lg rounded-xl border border-slate-700 shadow-2xl flex flex-col overflow-hidden animate-in fade-in zoom-in duration-200">
|
| 114 |
+
|
| 115 |
+
<div className="flex items-center justify-between px-6 py-4 border-b border-slate-800 bg-gradient-to-r from-slate-900 to-slate-800">
|
| 116 |
+
<h2 className="text-lg font-bold text-white flex items-center gap-2">
|
| 117 |
+
<Sparkles className="text-purple-400" size={20} />
|
| 118 |
+
AI Architect Team
|
| 119 |
+
</h2>
|
| 120 |
+
<button onClick={onClose} disabled={isBuilding} className="text-slate-400 hover:text-white transition-colors disabled:opacity-50">
|
| 121 |
+
<X size={20} />
|
| 122 |
+
</button>
|
| 123 |
+
</div>
|
| 124 |
+
|
| 125 |
+
<div className="p-6 space-y-4">
|
| 126 |
+
{!isBuilding && agentStatus !== 'complete' ? (
|
| 127 |
+
<>
|
| 128 |
+
<div className="bg-purple-500/10 border border-purple-500/20 rounded-lg p-3 text-sm text-purple-200 flex gap-3">
|
| 129 |
+
<MessageSquare size={18} className="shrink-0 mt-0.5" />
|
| 130 |
+
<p>
|
| 131 |
+
Describe your model. Our AI Agents (Architect, Critic, and Refiner) will collaborate to build the best architecture for you.
|
| 132 |
+
</p>
|
| 133 |
+
</div>
|
| 134 |
+
|
| 135 |
+
<textarea
|
| 136 |
+
className="w-full h-32 bg-slate-900 border border-slate-700 rounded-lg p-3 text-slate-200 focus:ring-2 focus:ring-purple-500 outline-none resize-none placeholder-slate-600"
|
| 137 |
+
placeholder="e.g. Build a robust CNN for medical image segmentation with attention gates..."
|
| 138 |
+
value={prompt}
|
| 139 |
+
onChange={(e) => setPrompt(e.target.value)}
|
| 140 |
+
/>
|
| 141 |
+
|
| 142 |
+
{error && (
|
| 143 |
+
<div className="flex items-center gap-2 text-red-400 text-sm bg-red-500/10 p-2 rounded border border-red-500/20">
|
| 144 |
+
<AlertCircle size={14} />
|
| 145 |
+
{error}
|
| 146 |
+
</div>
|
| 147 |
+
)}
|
| 148 |
+
|
| 149 |
+
<div className="flex justify-end pt-2">
|
| 150 |
+
<button
|
| 151 |
+
onClick={handleBuild}
|
| 152 |
+
disabled={!prompt.trim()}
|
| 153 |
+
className={`
|
| 154 |
+
flex items-center gap-2 px-6 py-2 rounded-lg font-medium text-white transition-all
|
| 155 |
+
${!prompt.trim()
|
| 156 |
+
? 'bg-slate-800 cursor-not-allowed opacity-50'
|
| 157 |
+
: 'bg-gradient-to-r from-purple-600 to-blue-600 hover:from-purple-500 hover:to-blue-500 shadow-lg shadow-purple-500/20'}
|
| 158 |
+
`}
|
| 159 |
+
>
|
| 160 |
+
<Play size={18} fill="currentColor" />
|
| 161 |
+
Start Agents
|
| 162 |
+
</button>
|
| 163 |
+
</div>
|
| 164 |
+
</>
|
| 165 |
+
) : (
|
| 166 |
+
<div className="space-y-3 py-2">
|
| 167 |
+
{renderAgentStep('architect', <HardHat size={24} />, "Architect Agent: Drafting Layout")}
|
| 168 |
+
{renderAgentStep('critic', <Search size={24} />, "Critic Agent: Reviewing Design")}
|
| 169 |
+
{renderAgentStep('refiner', <Wand2 size={24} />, "Refiner Agent: Finalizing Architecture")}
|
| 170 |
+
|
| 171 |
+
{agentStatus === 'complete' && (
|
| 172 |
+
<div className="text-center text-emerald-400 font-bold mt-4 animate-pulse">
|
| 173 |
+
Architecture Generation Complete!
|
| 174 |
+
</div>
|
| 175 |
+
)}
|
| 176 |
+
</div>
|
| 177 |
+
)}
|
| 178 |
+
</div>
|
| 179 |
+
</div>
|
| 180 |
+
</div>
|
| 181 |
+
);
|
| 182 |
+
};
|
| 183 |
+
|
| 184 |
+
export default AIBuilderModal;
|
components/ApiKeyModal.tsx
ADDED
|
@@ -0,0 +1,84 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
import React, { useState } from 'react';
|
| 2 |
+
import { Key, X, Check, Lock } from 'lucide-react';
|
| 3 |
+
import { setUserApiKey } from '../services/geminiService';
|
| 4 |
+
|
| 5 |
+
interface ApiKeyModalProps {
|
| 6 |
+
isOpen: boolean;
|
| 7 |
+
onClose: () => void;
|
| 8 |
+
onSuccess: () => void;
|
| 9 |
+
}
|
| 10 |
+
|
| 11 |
+
const ApiKeyModal: React.FC<ApiKeyModalProps> = ({ isOpen, onClose, onSuccess }) => {
|
| 12 |
+
const [key, setKey] = useState('');
|
| 13 |
+
const [saved, setSaved] = useState(false);
|
| 14 |
+
|
| 15 |
+
if (!isOpen) return null;
|
| 16 |
+
|
| 17 |
+
const handleSave = () => {
|
| 18 |
+
if (key.trim()) {
|
| 19 |
+
setUserApiKey(key.trim());
|
| 20 |
+
setSaved(true);
|
| 21 |
+
setTimeout(() => {
|
| 22 |
+
onSuccess();
|
| 23 |
+
onClose();
|
| 24 |
+
setSaved(false);
|
| 25 |
+
}, 1000);
|
| 26 |
+
}
|
| 27 |
+
};
|
| 28 |
+
|
| 29 |
+
return (
|
| 30 |
+
<div className="absolute inset-0 z-50 flex items-center justify-center bg-black/70 backdrop-blur-sm p-4">
|
| 31 |
+
<div className="bg-slate-900 w-full max-w-md rounded-xl border border-slate-700 shadow-2xl overflow-hidden animate-in fade-in zoom-in duration-200">
|
| 32 |
+
<div className="flex items-center justify-between px-6 py-4 border-b border-slate-800 bg-slate-800/50">
|
| 33 |
+
<h2 className="text-lg font-bold text-white flex items-center gap-2">
|
| 34 |
+
<Key className="text-emerald-400" size={20} />
|
| 35 |
+
API Access
|
| 36 |
+
</h2>
|
| 37 |
+
<button onClick={onClose} className="text-slate-400 hover:text-white transition-colors">
|
| 38 |
+
<X size={20} />
|
| 39 |
+
</button>
|
| 40 |
+
</div>
|
| 41 |
+
|
| 42 |
+
<div className="p-6 space-y-4">
|
| 43 |
+
<p className="text-sm text-slate-400">
|
| 44 |
+
Enter your Google Gemini API Key to enable AI features like Agents, Suggestions, and Code Generation.
|
| 45 |
+
</p>
|
| 46 |
+
|
| 47 |
+
<div className="relative">
|
| 48 |
+
<Lock className="absolute left-3 top-2.5 text-slate-500" size={16} />
|
| 49 |
+
<input
|
| 50 |
+
type="password"
|
| 51 |
+
className="w-full bg-slate-950 border border-slate-700 rounded-lg pl-10 pr-4 py-2 text-slate-200 focus:ring-2 focus:ring-emerald-500 outline-none placeholder-slate-600 font-mono text-sm"
|
| 52 |
+
placeholder="Paste your API key here..."
|
| 53 |
+
value={key}
|
| 54 |
+
onChange={(e) => setKey(e.target.value)}
|
| 55 |
+
/>
|
| 56 |
+
</div>
|
| 57 |
+
|
| 58 |
+
<div className="bg-emerald-500/10 border border-emerald-500/20 rounded-lg p-3">
|
| 59 |
+
<p className="text-xs text-emerald-200">
|
| 60 |
+
Your key is stored locally in your browser and used directly with Google's API.
|
| 61 |
+
</p>
|
| 62 |
+
</div>
|
| 63 |
+
|
| 64 |
+
<div className="flex justify-end pt-2">
|
| 65 |
+
<button
|
| 66 |
+
onClick={handleSave}
|
| 67 |
+
disabled={!key.trim()}
|
| 68 |
+
className={`
|
| 69 |
+
flex items-center gap-2 px-6 py-2 rounded-lg font-medium text-white transition-all
|
| 70 |
+
${saved ? 'bg-emerald-600' : 'bg-emerald-600 hover:bg-emerald-500'}
|
| 71 |
+
${!key.trim() && !saved ? 'opacity-50 cursor-not-allowed bg-slate-700' : ''}
|
| 72 |
+
`}
|
| 73 |
+
>
|
| 74 |
+
{saved ? <Check size={18} /> : <Key size={18} />}
|
| 75 |
+
{saved ? 'Saved!' : 'Save Key'}
|
| 76 |
+
</button>
|
| 77 |
+
</div>
|
| 78 |
+
</div>
|
| 79 |
+
</div>
|
| 80 |
+
</div>
|
| 81 |
+
);
|
| 82 |
+
};
|
| 83 |
+
|
| 84 |
+
export default ApiKeyModal;
|
components/CodeViewer.tsx
ADDED
|
@@ -0,0 +1,188 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
import React, { useState, useEffect } from 'react';
|
| 2 |
+
import { X, Copy, Check, Terminal, HardHat, Search, Wand2, CheckCircle2, ArrowLeft, Play } from 'lucide-react';
|
| 3 |
+
import { generateCodeWithAgents, AgentStatus } from '../services/geminiService';
|
| 4 |
+
|
| 5 |
+
interface CodeViewerProps {
|
| 6 |
+
code: string; // This represents the PROMPT text initially
|
| 7 |
+
isOpen: boolean;
|
| 8 |
+
onClose: () => void;
|
| 9 |
+
isLoading: boolean;
|
| 10 |
+
}
|
| 11 |
+
|
| 12 |
+
const CodeViewer: React.FC<CodeViewerProps> = ({ code, isOpen, onClose, isLoading }) => {
|
| 13 |
+
const [copied, setCopied] = useState(false);
|
| 14 |
+
const [viewMode, setViewMode] = useState<'prompt' | 'building' | 'code'>('prompt');
|
| 15 |
+
|
| 16 |
+
const [finalCode, setFinalCode] = useState('');
|
| 17 |
+
const [agentStatus, setAgentStatus] = useState<AgentStatus>('idle');
|
| 18 |
+
const [agentMessage, setAgentMessage] = useState('');
|
| 19 |
+
|
| 20 |
+
// Reset state when opening/closing or changing input
|
| 21 |
+
useEffect(() => {
|
| 22 |
+
if (isOpen) {
|
| 23 |
+
setViewMode('prompt');
|
| 24 |
+
setFinalCode('');
|
| 25 |
+
setAgentStatus('idle');
|
| 26 |
+
}
|
| 27 |
+
}, [isOpen, code]);
|
| 28 |
+
|
| 29 |
+
const handleCopy = (text: string) => {
|
| 30 |
+
navigator.clipboard.writeText(text);
|
| 31 |
+
setCopied(true);
|
| 32 |
+
setTimeout(() => setCopied(false), 2000);
|
| 33 |
+
};
|
| 34 |
+
|
| 35 |
+
const handleBuildWithAgents = async () => {
|
| 36 |
+
setViewMode('building');
|
| 37 |
+
setAgentStatus('architect');
|
| 38 |
+
setAgentMessage('Initializing agents...');
|
| 39 |
+
|
| 40 |
+
try {
|
| 41 |
+
const result = await generateCodeWithAgents(code, (status, msg) => {
|
| 42 |
+
setAgentStatus(status);
|
| 43 |
+
setAgentMessage(msg);
|
| 44 |
+
});
|
| 45 |
+
setFinalCode(result);
|
| 46 |
+
// Small delay to show completion state
|
| 47 |
+
setTimeout(() => {
|
| 48 |
+
setViewMode('code');
|
| 49 |
+
}, 1000);
|
| 50 |
+
} catch (error) {
|
| 51 |
+
setAgentStatus('error');
|
| 52 |
+
setAgentMessage('Generation failed. Please try again.');
|
| 53 |
+
setTimeout(() => setViewMode('prompt'), 2000);
|
| 54 |
+
}
|
| 55 |
+
};
|
| 56 |
+
|
| 57 |
+
const getStepStatus = (step: AgentStatus, current: AgentStatus) => {
|
| 58 |
+
const order = ['idle', 'architect', 'critic', 'refiner', 'complete'];
|
| 59 |
+
const stepIdx = order.indexOf(step);
|
| 60 |
+
const currentIdx = order.indexOf(current);
|
| 61 |
+
|
| 62 |
+
if (current === 'error') return 'text-slate-500';
|
| 63 |
+
if (currentIdx > stepIdx) return 'text-emerald-400';
|
| 64 |
+
if (currentIdx === stepIdx) return 'text-blue-400 animate-pulse';
|
| 65 |
+
return 'text-slate-600';
|
| 66 |
+
};
|
| 67 |
+
|
| 68 |
+
const renderAgentStep = (step: AgentStatus, icon: React.ReactNode, label: string) => {
|
| 69 |
+
const statusColor = getStepStatus(step, agentStatus);
|
| 70 |
+
const isCurrent = agentStatus === step;
|
| 71 |
+
const isDone = ['architect', 'critic', 'refiner', 'complete'].indexOf(agentStatus) > ['architect', 'critic', 'refiner'].indexOf(step);
|
| 72 |
+
|
| 73 |
+
return (
|
| 74 |
+
<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'}`}>
|
| 75 |
+
<div className={`${statusColor} transition-colors duration-300`}>
|
| 76 |
+
{isDone ? <CheckCircle2 size={24} /> : icon}
|
| 77 |
+
</div>
|
| 78 |
+
<div className="flex-1">
|
| 79 |
+
<div className={`font-semibold text-sm ${isCurrent ? 'text-blue-200' : 'text-slate-400'}`}>{label}</div>
|
| 80 |
+
{isCurrent && (
|
| 81 |
+
<div className="text-xs text-slate-500 mt-1">{agentMessage}</div>
|
| 82 |
+
)}
|
| 83 |
+
</div>
|
| 84 |
+
{isCurrent && <div className="w-2 h-2 rounded-full bg-blue-400 animate-ping" />}
|
| 85 |
+
</div>
|
| 86 |
+
);
|
| 87 |
+
};
|
| 88 |
+
|
| 89 |
+
if (!isOpen) return null;
|
| 90 |
+
|
| 91 |
+
return (
|
| 92 |
+
<div className="absolute inset-0 z-50 flex items-center justify-center bg-black/70 backdrop-blur-sm p-4 md:p-8">
|
| 93 |
+
<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">
|
| 94 |
+
|
| 95 |
+
{/* Header */}
|
| 96 |
+
<div className="flex items-center justify-between px-6 py-4 border-b border-slate-800 bg-slate-800/50">
|
| 97 |
+
<h2 className="text-lg font-bold text-white flex items-center gap-2">
|
| 98 |
+
{viewMode === 'prompt' && <span className="text-blue-400">Architecture Prompt</span>}
|
| 99 |
+
{viewMode === 'building' && <span className="text-purple-400 flex items-center gap-2"><HardHat size={18}/> Agents Building...</span>}
|
| 100 |
+
{viewMode === 'code' && <span className="text-emerald-400 flex items-center gap-2"><Terminal size={18}/> Final Code</span>}
|
| 101 |
+
|
| 102 |
+
{isLoading && <span className="text-xs text-blue-400 animate-pulse ml-2">Building...</span>}
|
| 103 |
+
</h2>
|
| 104 |
+
|
| 105 |
+
<div className="flex items-center gap-3">
|
| 106 |
+
{viewMode === 'code' && (
|
| 107 |
+
<button
|
| 108 |
+
onClick={() => setViewMode('prompt')}
|
| 109 |
+
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"
|
| 110 |
+
>
|
| 111 |
+
<ArrowLeft size={14} /> Back to Prompt
|
| 112 |
+
</button>
|
| 113 |
+
)}
|
| 114 |
+
|
| 115 |
+
{viewMode !== 'building' && (
|
| 116 |
+
<button
|
| 117 |
+
onClick={() => handleCopy(viewMode === 'code' ? finalCode : code)}
|
| 118 |
+
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"
|
| 119 |
+
>
|
| 120 |
+
{copied ? <Check size={14} className="text-emerald-400"/> : <Copy size={14} />}
|
| 121 |
+
{copied ? 'Copied' : 'Copy'}
|
| 122 |
+
</button>
|
| 123 |
+
)}
|
| 124 |
+
|
| 125 |
+
{viewMode === 'prompt' && !isLoading && (
|
| 126 |
+
<button
|
| 127 |
+
onClick={handleBuildWithAgents}
|
| 128 |
+
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"
|
| 129 |
+
>
|
| 130 |
+
<Play size={14} fill="currentColor" />
|
| 131 |
+
Build with Agents (Beta)
|
| 132 |
+
</button>
|
| 133 |
+
)}
|
| 134 |
+
|
| 135 |
+
<button onClick={onClose} className="text-slate-400 hover:text-white transition-colors ml-2">
|
| 136 |
+
<X size={20} />
|
| 137 |
+
</button>
|
| 138 |
+
</div>
|
| 139 |
+
</div>
|
| 140 |
+
|
| 141 |
+
{/* Content */}
|
| 142 |
+
<div className="flex-1 overflow-hidden bg-[#0d1117] relative">
|
| 143 |
+
|
| 144 |
+
{/* View: Loading Prompt */}
|
| 145 |
+
{isLoading && viewMode === 'prompt' && (
|
| 146 |
+
<div className="h-full flex flex-col items-center justify-center space-y-4">
|
| 147 |
+
<div className="w-12 h-12 border-4 border-blue-500/30 border-t-blue-500 rounded-full animate-spin"></div>
|
| 148 |
+
<p className="text-slate-400 text-sm animate-pulse">Refining architecture prompt...</p>
|
| 149 |
+
</div>
|
| 150 |
+
)}
|
| 151 |
+
|
| 152 |
+
{/* View: Prompt Text */}
|
| 153 |
+
{!isLoading && viewMode === 'prompt' && (
|
| 154 |
+
<div className="h-full overflow-auto p-6 font-mono text-sm text-slate-300 leading-relaxed whitespace-pre-wrap">
|
| 155 |
+
{code}
|
| 156 |
+
</div>
|
| 157 |
+
)}
|
| 158 |
+
|
| 159 |
+
{/* View: Agents Building */}
|
| 160 |
+
{viewMode === 'building' && (
|
| 161 |
+
<div className="h-full flex flex-col items-center justify-center p-8">
|
| 162 |
+
<div className="w-full max-w-md space-y-3">
|
| 163 |
+
{renderAgentStep('architect', <HardHat size={24} />, "Coder Agent: Writing Implementation")}
|
| 164 |
+
{renderAgentStep('critic', <Search size={24} />, "Reviewer Agent: Analyzing Logic")}
|
| 165 |
+
{renderAgentStep('refiner', <Wand2 size={24} />, "Polisher Agent: Finalizing Code")}
|
| 166 |
+
</div>
|
| 167 |
+
{agentStatus === 'complete' && (
|
| 168 |
+
<div className="mt-8 text-emerald-400 font-bold animate-bounce">
|
| 169 |
+
Done! Loading code...
|
| 170 |
+
</div>
|
| 171 |
+
)}
|
| 172 |
+
</div>
|
| 173 |
+
)}
|
| 174 |
+
|
| 175 |
+
{/* View: Final Code */}
|
| 176 |
+
{viewMode === 'code' && (
|
| 177 |
+
<div className="h-full overflow-auto p-6 font-mono text-sm leading-relaxed whitespace-pre-wrap text-blue-100 selection:bg-blue-500/30">
|
| 178 |
+
{finalCode}
|
| 179 |
+
</div>
|
| 180 |
+
)}
|
| 181 |
+
|
| 182 |
+
</div>
|
| 183 |
+
</div>
|
| 184 |
+
</div>
|
| 185 |
+
);
|
| 186 |
+
};
|
| 187 |
+
|
| 188 |
+
export default CodeViewer;
|
components/CustomNode.tsx
ADDED
|
@@ -0,0 +1,183 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
import React, { memo } from 'react';
|
| 2 |
+
import { Handle, Position, NodeProps } from 'reactflow';
|
| 3 |
+
import {
|
| 4 |
+
Layers, Box, Activity, Zap, ArrowRight, Grid, Minimize,
|
| 5 |
+
Database, GitBranch, AlignJustify, Type, Combine, Maximize,
|
| 6 |
+
ArrowUpCircle, Sliders, RefreshCcw, Brain, Crosshair, Network, Clock, Eye, Workflow,
|
| 7 |
+
Terminal, MinusCircle
|
| 8 |
+
} from 'lucide-react';
|
| 9 |
+
import { NodeData, LayerType } from '../types';
|
| 10 |
+
|
| 11 |
+
const getIcon = (type: LayerType) => {
|
| 12 |
+
switch (type) {
|
| 13 |
+
case LayerType.INPUT: return <ArrowRight className="w-4 h-4" />;
|
| 14 |
+
case LayerType.CONV1D: return <Activity className="w-4 h-4" />;
|
| 15 |
+
case LayerType.CONV2D: return <Layers className="w-4 h-4" />;
|
| 16 |
+
case LayerType.CONV_TRANSPOSE2D: return <ArrowUpCircle className="w-4 h-4" />;
|
| 17 |
+
case LayerType.LINEAR: return <Grid className="w-4 h-4" />;
|
| 18 |
+
case LayerType.RELU:
|
| 19 |
+
case LayerType.LEAKYRELU:
|
| 20 |
+
case LayerType.GELU:
|
| 21 |
+
case LayerType.SILU:
|
| 22 |
+
case LayerType.SWIGLU:
|
| 23 |
+
case LayerType.SIGMOID:
|
| 24 |
+
case LayerType.TANH: return <Zap className="w-4 h-4" />;
|
| 25 |
+
case LayerType.MAXPOOL:
|
| 26 |
+
case LayerType.AVGPOOL:
|
| 27 |
+
case LayerType.ADAPTIVEAVGPOOL: return <Minimize className="w-4 h-4" />;
|
| 28 |
+
case LayerType.UPSAMPLE: return <Maximize className="w-4 h-4" />;
|
| 29 |
+
|
| 30 |
+
// Transformer / GenAI
|
| 31 |
+
case LayerType.ATTENTION:
|
| 32 |
+
case LayerType.CROSS_ATTENTION: return <RefreshCcw className="w-4 h-4" />;
|
| 33 |
+
case LayerType.TRANSFORMER_BLOCK:
|
| 34 |
+
case LayerType.TRANSFORMER_ENCODER:
|
| 35 |
+
case LayerType.TRANSFORMER_DECODER: return <Brain className="w-4 h-4" />;
|
| 36 |
+
case LayerType.MOE_BLOCK: return <Network className="w-4 h-4" />;
|
| 37 |
+
case LayerType.ACTION_HEAD: return <Crosshair className="w-4 h-4" />;
|
| 38 |
+
case LayerType.PATCH_EMBED:
|
| 39 |
+
case LayerType.SAM_PROMPT_ENCODER: return <Eye className="w-4 h-4" />;
|
| 40 |
+
case LayerType.SAM_MASK_DECODER: return <Workflow className="w-4 h-4" />;
|
| 41 |
+
case LayerType.TIME_EMBEDDING:
|
| 42 |
+
case LayerType.ROPE: return <Clock className="w-4 h-4" />;
|
| 43 |
+
|
| 44 |
+
case LayerType.LSTM:
|
| 45 |
+
case LayerType.GRU: return <GitBranch className="w-4 h-4" />;
|
| 46 |
+
case LayerType.EMBEDDING: return <Database className="w-4 h-4" />;
|
| 47 |
+
case LayerType.LAYERNORM:
|
| 48 |
+
case LayerType.BATCHNORM:
|
| 49 |
+
case LayerType.INSTANCENORM:
|
| 50 |
+
case LayerType.RMSNORM: return <Sliders className="w-4 h-4" />;
|
| 51 |
+
case LayerType.FLATTEN: return <Type className="w-4 h-4" />;
|
| 52 |
+
case LayerType.CONCAT:
|
| 53 |
+
case LayerType.ADD: return <Combine className="w-4 h-4" />;
|
| 54 |
+
|
| 55 |
+
case LayerType.CUSTOM: return <Terminal className="w-4 h-4" />;
|
| 56 |
+
case LayerType.IDENTITY: return <MinusCircle className="w-4 h-4" />;
|
| 57 |
+
|
| 58 |
+
default: return <Box className="w-4 h-4" />;
|
| 59 |
+
}
|
| 60 |
+
};
|
| 61 |
+
|
| 62 |
+
const getColor = (type: LayerType) => {
|
| 63 |
+
switch (type) {
|
| 64 |
+
case LayerType.INPUT: return 'border-emerald-500 shadow-emerald-500/20';
|
| 65 |
+
case LayerType.OUTPUT: return 'border-red-500 shadow-red-500/20';
|
| 66 |
+
|
| 67 |
+
case LayerType.CONV2D:
|
| 68 |
+
case LayerType.CONV1D:
|
| 69 |
+
case LayerType.CONV_TRANSPOSE2D:
|
| 70 |
+
case LayerType.MAXPOOL:
|
| 71 |
+
case LayerType.AVGPOOL:
|
| 72 |
+
case LayerType.ADAPTIVEAVGPOOL:
|
| 73 |
+
case LayerType.UPSAMPLE: return 'border-blue-500 shadow-blue-500/20';
|
| 74 |
+
|
| 75 |
+
case LayerType.LINEAR:
|
| 76 |
+
case LayerType.ACTION_HEAD: return 'border-violet-500 shadow-violet-500/20';
|
| 77 |
+
|
| 78 |
+
// GenAI / Transformer - Gold/Amber theme
|
| 79 |
+
case LayerType.ATTENTION:
|
| 80 |
+
case LayerType.CROSS_ATTENTION:
|
| 81 |
+
case LayerType.TRANSFORMER_BLOCK:
|
| 82 |
+
case LayerType.TRANSFORMER_ENCODER:
|
| 83 |
+
case LayerType.TRANSFORMER_DECODER:
|
| 84 |
+
case LayerType.MOE_BLOCK:
|
| 85 |
+
case LayerType.PATCH_EMBED:
|
| 86 |
+
case LayerType.SAM_PROMPT_ENCODER:
|
| 87 |
+
case LayerType.SAM_MASK_DECODER: return 'border-amber-500 shadow-amber-500/20';
|
| 88 |
+
|
| 89 |
+
case LayerType.LSTM:
|
| 90 |
+
case LayerType.GRU:
|
| 91 |
+
case LayerType.EMBEDDING:
|
| 92 |
+
case LayerType.TIME_EMBEDDING:
|
| 93 |
+
case LayerType.ROPE: return 'border-orange-500 shadow-orange-500/20';
|
| 94 |
+
|
| 95 |
+
case LayerType.BATCHNORM:
|
| 96 |
+
case LayerType.LAYERNORM:
|
| 97 |
+
case LayerType.INSTANCENORM:
|
| 98 |
+
case LayerType.RMSNORM: return 'border-cyan-500 shadow-cyan-500/20';
|
| 99 |
+
|
| 100 |
+
case LayerType.CONCAT:
|
| 101 |
+
case LayerType.ADD: return 'border-pink-500 shadow-pink-500/20';
|
| 102 |
+
|
| 103 |
+
case LayerType.CUSTOM: return 'border-lime-500 shadow-lime-500/20';
|
| 104 |
+
case LayerType.IDENTITY: return 'border-slate-600 shadow-slate-600/20';
|
| 105 |
+
|
| 106 |
+
default: return 'border-slate-500 shadow-slate-500/20';
|
| 107 |
+
}
|
| 108 |
+
};
|
| 109 |
+
|
| 110 |
+
const CustomNode = ({ data, selected }: NodeProps<NodeData>) => {
|
| 111 |
+
const isInput = data.type === LayerType.INPUT;
|
| 112 |
+
const isOutput = data.type === LayerType.OUTPUT;
|
| 113 |
+
|
| 114 |
+
// Render input modality badge
|
| 115 |
+
const inputBadge = isInput && data.params.modality ? (
|
| 116 |
+
<span className="ml-auto text-[9px] px-1.5 py-0.5 rounded-full bg-slate-700 text-emerald-400 font-mono tracking-tighter uppercase">
|
| 117 |
+
{data.params.modality}
|
| 118 |
+
</span>
|
| 119 |
+
) : null;
|
| 120 |
+
|
| 121 |
+
// Custom layer name badge
|
| 122 |
+
const customBadge = data.type === LayerType.CUSTOM && data.params.class_name ? (
|
| 123 |
+
<span className="ml-auto text-[8px] px-1.5 py-0.5 rounded-full bg-slate-700 text-lime-400 font-mono truncate max-w-[80px]">
|
| 124 |
+
{data.params.class_name.split('.').pop()}
|
| 125 |
+
</span>
|
| 126 |
+
) : null;
|
| 127 |
+
|
| 128 |
+
return (
|
| 129 |
+
<div className={`
|
| 130 |
+
relative min-w-[150px] bg-slate-900 rounded-lg border-2
|
| 131 |
+
transition-all duration-200 shadow-lg
|
| 132 |
+
${getColor(data.type)}
|
| 133 |
+
${selected ? 'ring-2 ring-white ring-offset-2 ring-offset-slate-900 scale-105' : ''}
|
| 134 |
+
`}>
|
| 135 |
+
{/* Input Handle */}
|
| 136 |
+
{!isInput && (
|
| 137 |
+
<Handle
|
| 138 |
+
type="target"
|
| 139 |
+
position={Position.Top}
|
| 140 |
+
className="!bg-slate-400 !w-3 !h-3 !-top-2 !border-slate-900"
|
| 141 |
+
/>
|
| 142 |
+
)}
|
| 143 |
+
|
| 144 |
+
{/* Header */}
|
| 145 |
+
<div className="flex items-center gap-2 px-3 py-2 border-b border-slate-800 bg-slate-800/50 rounded-t-md">
|
| 146 |
+
<span className="text-slate-300">
|
| 147 |
+
{getIcon(data.type)}
|
| 148 |
+
</span>
|
| 149 |
+
<span className="text-xs font-bold uppercase tracking-wider text-slate-200 truncate max-w-[100px]">
|
| 150 |
+
{data.label}
|
| 151 |
+
</span>
|
| 152 |
+
{inputBadge}
|
| 153 |
+
{customBadge}
|
| 154 |
+
</div>
|
| 155 |
+
|
| 156 |
+
{/* Body - Parameters Summary */}
|
| 157 |
+
<div className="p-3">
|
| 158 |
+
<div className="text-[10px] text-slate-400 font-mono space-y-1">
|
| 159 |
+
{Object.entries(data.params).slice(0, 3).map(([key, value]) => (
|
| 160 |
+
<div key={key} className="flex justify-between gap-4">
|
| 161 |
+
<span className="opacity-70 truncate max-w-[80px]">{key}:</span>
|
| 162 |
+
<span className="text-slate-200 truncate max-w-[80px]">{String(value)}</span>
|
| 163 |
+
</div>
|
| 164 |
+
))}
|
| 165 |
+
{Object.keys(data.params).length === 0 && (
|
| 166 |
+
<span className="opacity-50 italic">No parameters</span>
|
| 167 |
+
)}
|
| 168 |
+
</div>
|
| 169 |
+
</div>
|
| 170 |
+
|
| 171 |
+
{/* Output Handle */}
|
| 172 |
+
{!isOutput && (
|
| 173 |
+
<Handle
|
| 174 |
+
type="source"
|
| 175 |
+
position={Position.Bottom}
|
| 176 |
+
className="!bg-slate-400 !w-3 !h-3 !-bottom-2 !border-slate-900"
|
| 177 |
+
/>
|
| 178 |
+
)}
|
| 179 |
+
</div>
|
| 180 |
+
);
|
| 181 |
+
};
|
| 182 |
+
|
| 183 |
+
export default memo(CustomNode);
|
components/PropertiesPanel.tsx
ADDED
|
@@ -0,0 +1,112 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
import React from 'react';
|
| 2 |
+
import { Node } from 'reactflow';
|
| 3 |
+
import { NodeData, LayerDefinition } from '../types';
|
| 4 |
+
import { LAYER_DEFINITIONS } from '../constants';
|
| 5 |
+
import { X, Trash2 } from 'lucide-react';
|
| 6 |
+
|
| 7 |
+
interface PropertiesPanelProps {
|
| 8 |
+
selectedNode: Node<NodeData> | null;
|
| 9 |
+
onChange: (id: string, newParams: Record<string, any>) => void;
|
| 10 |
+
onDelete: (id: string) => void;
|
| 11 |
+
onClose: () => void;
|
| 12 |
+
}
|
| 13 |
+
|
| 14 |
+
const PropertiesPanel: React.FC<PropertiesPanelProps> = ({ selectedNode, onChange, onDelete, onClose }) => {
|
| 15 |
+
if (!selectedNode) {
|
| 16 |
+
return (
|
| 17 |
+
<div className="w-80 bg-slate-900 border-l border-slate-800 p-6 flex flex-col items-center justify-center text-slate-500 h-full">
|
| 18 |
+
<div className="w-16 h-16 rounded-full bg-slate-800 mb-4 flex items-center justify-center">
|
| 19 |
+
<span className="text-2xl opacity-50">⚡</span>
|
| 20 |
+
</div>
|
| 21 |
+
<p className="text-sm text-center">Select a node on the canvas to configure parameters.</p>
|
| 22 |
+
</div>
|
| 23 |
+
);
|
| 24 |
+
}
|
| 25 |
+
|
| 26 |
+
const definition: LayerDefinition = LAYER_DEFINITIONS[selectedNode.data.type];
|
| 27 |
+
|
| 28 |
+
const handleParamChange = (name: string, value: any, type: string) => {
|
| 29 |
+
let parsedValue = value;
|
| 30 |
+
if (type === 'number') parsedValue = Number(value);
|
| 31 |
+
if (type === 'boolean') parsedValue = value === 'true';
|
| 32 |
+
|
| 33 |
+
onChange(selectedNode.id, {
|
| 34 |
+
...selectedNode.data.params,
|
| 35 |
+
[name]: parsedValue
|
| 36 |
+
});
|
| 37 |
+
};
|
| 38 |
+
|
| 39 |
+
return (
|
| 40 |
+
<div className="w-80 bg-slate-900 border-l border-slate-800 flex flex-col h-full">
|
| 41 |
+
<div className="p-4 border-b border-slate-800 flex justify-between items-center">
|
| 42 |
+
<div>
|
| 43 |
+
<h2 className="text-lg font-bold text-slate-100">{definition.label}</h2>
|
| 44 |
+
<p className="text-xs text-slate-500 font-mono">{selectedNode.id}</p>
|
| 45 |
+
</div>
|
| 46 |
+
<button onClick={onClose} className="text-slate-500 hover:text-slate-300">
|
| 47 |
+
<X size={18} />
|
| 48 |
+
</button>
|
| 49 |
+
</div>
|
| 50 |
+
|
| 51 |
+
<div className="flex-1 overflow-y-auto p-4 space-y-5">
|
| 52 |
+
<div className="text-sm text-slate-400 italic bg-slate-800/50 p-3 rounded border border-slate-800">
|
| 53 |
+
{definition.description}
|
| 54 |
+
</div>
|
| 55 |
+
|
| 56 |
+
<div className="space-y-4">
|
| 57 |
+
{definition.parameters.map((param) => (
|
| 58 |
+
<div key={param.name} className="space-y-1.5">
|
| 59 |
+
<label className="block text-xs font-semibold text-slate-300 uppercase tracking-wide">
|
| 60 |
+
{param.label}
|
| 61 |
+
</label>
|
| 62 |
+
|
| 63 |
+
{param.type === 'select' ? (
|
| 64 |
+
<select
|
| 65 |
+
className="w-full bg-slate-950 border border-slate-700 rounded px-3 py-2 text-sm text-slate-200 focus:ring-1 focus:ring-blue-500 outline-none"
|
| 66 |
+
value={selectedNode.data.params[param.name] || param.default}
|
| 67 |
+
onChange={(e) => handleParamChange(param.name, e.target.value, param.type)}
|
| 68 |
+
>
|
| 69 |
+
{param.options?.map(opt => (
|
| 70 |
+
<option key={opt} value={opt}>{opt}</option>
|
| 71 |
+
))}
|
| 72 |
+
</select>
|
| 73 |
+
) : param.type === 'boolean' ? (
|
| 74 |
+
<select
|
| 75 |
+
className="w-full bg-slate-950 border border-slate-700 rounded px-3 py-2 text-sm text-slate-200 focus:ring-1 focus:ring-blue-500 outline-none"
|
| 76 |
+
value={String(selectedNode.data.params[param.name] ?? param.default)}
|
| 77 |
+
onChange={(e) => handleParamChange(param.name, e.target.value === 'true', param.type)}
|
| 78 |
+
>
|
| 79 |
+
<option value="true">True</option>
|
| 80 |
+
<option value="false">False</option>
|
| 81 |
+
</select>
|
| 82 |
+
) : (
|
| 83 |
+
<input
|
| 84 |
+
type={param.type === 'number' ? 'number' : 'text'}
|
| 85 |
+
className="w-full bg-slate-950 border border-slate-700 rounded px-3 py-2 text-sm text-slate-200 focus:ring-1 focus:ring-blue-500 outline-none placeholder-slate-700"
|
| 86 |
+
value={selectedNode.data.params[param.name] ?? param.default}
|
| 87 |
+
onChange={(e) => handleParamChange(param.name, e.target.value, param.type)}
|
| 88 |
+
/>
|
| 89 |
+
)}
|
| 90 |
+
</div>
|
| 91 |
+
))}
|
| 92 |
+
|
| 93 |
+
{definition.parameters.length === 0 && (
|
| 94 |
+
<p className="text-sm text-slate-500 text-center py-4">This layer has no configurable parameters.</p>
|
| 95 |
+
)}
|
| 96 |
+
</div>
|
| 97 |
+
</div>
|
| 98 |
+
|
| 99 |
+
<div className="p-4 border-t border-slate-800">
|
| 100 |
+
<button
|
| 101 |
+
onClick={() => onDelete(selectedNode.id)}
|
| 102 |
+
className="w-full flex items-center justify-center gap-2 bg-red-500/10 hover:bg-red-500/20 text-red-500 py-2 rounded transition-colors text-sm font-medium border border-red-500/20"
|
| 103 |
+
>
|
| 104 |
+
<Trash2 size={16} />
|
| 105 |
+
Delete Node
|
| 106 |
+
</button>
|
| 107 |
+
</div>
|
| 108 |
+
</div>
|
| 109 |
+
);
|
| 110 |
+
};
|
| 111 |
+
|
| 112 |
+
export default PropertiesPanel;
|
components/Sidebar.tsx
ADDED
|
@@ -0,0 +1,89 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
import React from 'react';
|
| 2 |
+
import { LAYER_DEFINITIONS } from '../constants';
|
| 3 |
+
import { LayerType } from '../types';
|
| 4 |
+
import { Box, Sparkles, LayoutTemplate, Circle } from 'lucide-react';
|
| 5 |
+
|
| 6 |
+
interface SidebarProps {
|
| 7 |
+
onOpenAIBuilder: () => void;
|
| 8 |
+
onSelectTemplate: (templateId: string) => void;
|
| 9 |
+
isConnected: boolean;
|
| 10 |
+
}
|
| 11 |
+
|
| 12 |
+
const Sidebar: React.FC<SidebarProps> = ({ onOpenAIBuilder, onSelectTemplate, isConnected }) => {
|
| 13 |
+
const onDragStart = (event: React.DragEvent, layerType: LayerType) => {
|
| 14 |
+
event.dataTransfer.setData('application/reactflow', layerType);
|
| 15 |
+
event.dataTransfer.effectAllowed = 'move';
|
| 16 |
+
};
|
| 17 |
+
|
| 18 |
+
const categories = Array.from(new Set(Object.values(LAYER_DEFINITIONS).map(l => l.category)));
|
| 19 |
+
const categoryOrder = ['Core', 'Convolution', 'Recurrent', 'Transformer', 'Normalization', 'Utility'];
|
| 20 |
+
categories.sort((a, b) => categoryOrder.indexOf(a) - categoryOrder.indexOf(b));
|
| 21 |
+
|
| 22 |
+
return (
|
| 23 |
+
<aside className="w-64 bg-slate-900 border-r border-slate-800 flex flex-col h-full z-10">
|
| 24 |
+
<div className="p-4 border-b border-slate-800 bg-slate-900">
|
| 25 |
+
<h1 className="text-xl font-bold bg-gradient-to-r from-blue-400 to-violet-400 bg-clip-text text-transparent">
|
| 26 |
+
Architecture Agents
|
| 27 |
+
</h1>
|
| 28 |
+
<p className="text-xs text-slate-500 mt-1">AI-Powered Model Builder</p>
|
| 29 |
+
|
| 30 |
+
<div className="mt-4 grid grid-cols-2 gap-2">
|
| 31 |
+
<button
|
| 32 |
+
onClick={onOpenAIBuilder}
|
| 33 |
+
className="flex flex-col items-center justify-center p-2 bg-purple-500/10 hover:bg-purple-500/20 border border-purple-500/30 rounded-lg text-purple-300 transition-colors group"
|
| 34 |
+
>
|
| 35 |
+
<Sparkles size={18} className="mb-1 group-hover:scale-110 transition-transform" />
|
| 36 |
+
<span className="text-[10px] font-bold">AI Builder</span>
|
| 37 |
+
</button>
|
| 38 |
+
<button
|
| 39 |
+
onClick={() => onSelectTemplate('menu')}
|
| 40 |
+
className="flex flex-col items-center justify-center p-2 bg-blue-500/10 hover:bg-blue-500/20 border border-blue-500/30 rounded-lg text-blue-300 transition-colors group"
|
| 41 |
+
>
|
| 42 |
+
<LayoutTemplate size={18} className="mb-1 group-hover:scale-110 transition-transform" />
|
| 43 |
+
<span className="text-[10px] font-bold">Templates</span>
|
| 44 |
+
</button>
|
| 45 |
+
</div>
|
| 46 |
+
</div>
|
| 47 |
+
|
| 48 |
+
<div className="flex-1 overflow-y-auto p-4 space-y-6 scrollbar-thin scrollbar-thumb-slate-700">
|
| 49 |
+
{categories.map(category => (
|
| 50 |
+
<div key={category}>
|
| 51 |
+
<h3 className="text-xs font-semibold text-slate-500 uppercase tracking-widest mb-3 flex items-center gap-2">
|
| 52 |
+
{category}
|
| 53 |
+
<div className="h-px flex-1 bg-slate-800"></div>
|
| 54 |
+
</h3>
|
| 55 |
+
<div className="grid grid-cols-1 gap-2">
|
| 56 |
+
{Object.values(LAYER_DEFINITIONS)
|
| 57 |
+
.filter(l => l.category === category)
|
| 58 |
+
.map(layer => (
|
| 59 |
+
<div
|
| 60 |
+
key={layer.type}
|
| 61 |
+
className="bg-slate-800 hover:bg-slate-750 p-3 rounded border border-slate-700 cursor-grab active:cursor-grabbing transition-colors group relative overflow-hidden"
|
| 62 |
+
onDragStart={(event) => onDragStart(event, layer.type)}
|
| 63 |
+
draggable
|
| 64 |
+
>
|
| 65 |
+
<div className="flex items-center gap-3 relative z-10">
|
| 66 |
+
<div className={`p-1.5 rounded transition-colors group-hover:bg-slate-900 bg-slate-900/50`}>
|
| 67 |
+
<Box size={14} className="text-slate-400 group-hover:text-blue-400" />
|
| 68 |
+
</div>
|
| 69 |
+
<div>
|
| 70 |
+
<div className="text-sm font-medium text-slate-200 group-hover:text-white">{layer.label}</div>
|
| 71 |
+
<div className="text-xs text-slate-500 leading-tight group-hover:text-slate-400">{layer.description}</div>
|
| 72 |
+
</div>
|
| 73 |
+
</div>
|
| 74 |
+
</div>
|
| 75 |
+
))}
|
| 76 |
+
</div>
|
| 77 |
+
</div>
|
| 78 |
+
))}
|
| 79 |
+
</div>
|
| 80 |
+
|
| 81 |
+
<div className="p-4 border-t border-slate-800 text-[10px] text-slate-500 text-center flex items-center justify-center gap-2">
|
| 82 |
+
<span>v1.2.0 • Powered by Gemini 2.5</span>
|
| 83 |
+
<Circle size={8} className={isConnected ? "fill-emerald-500 text-emerald-500" : "fill-red-500 text-red-500"} />
|
| 84 |
+
</div>
|
| 85 |
+
</aside>
|
| 86 |
+
);
|
| 87 |
+
};
|
| 88 |
+
|
| 89 |
+
export default Sidebar;
|
components/SuggestionsModal.tsx
ADDED
|
@@ -0,0 +1,78 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
import React from 'react';
|
| 2 |
+
import { Lightbulb, X, Sparkles, Wand2 } from 'lucide-react';
|
| 3 |
+
|
| 4 |
+
interface SuggestionsModalProps {
|
| 5 |
+
isOpen: boolean;
|
| 6 |
+
onClose: () => void;
|
| 7 |
+
suggestions: string;
|
| 8 |
+
isLoading: boolean;
|
| 9 |
+
onImplement: () => void;
|
| 10 |
+
isImplementing: boolean;
|
| 11 |
+
}
|
| 12 |
+
|
| 13 |
+
const SuggestionsModal: React.FC<SuggestionsModalProps> = ({
|
| 14 |
+
isOpen,
|
| 15 |
+
onClose,
|
| 16 |
+
suggestions,
|
| 17 |
+
isLoading,
|
| 18 |
+
onImplement,
|
| 19 |
+
isImplementing
|
| 20 |
+
}) => {
|
| 21 |
+
if (!isOpen) return null;
|
| 22 |
+
|
| 23 |
+
return (
|
| 24 |
+
<div className="absolute inset-0 z-50 flex items-center justify-center bg-black/70 backdrop-blur-sm p-4">
|
| 25 |
+
<div className="bg-slate-900 w-full max-w-lg rounded-xl border border-slate-700 shadow-2xl flex flex-col overflow-hidden animate-in fade-in zoom-in duration-200">
|
| 26 |
+
<div className="flex items-center justify-between px-6 py-4 border-b border-slate-800 bg-gradient-to-r from-slate-900 to-slate-800">
|
| 27 |
+
<h2 className="text-lg font-bold text-white flex items-center gap-2">
|
| 28 |
+
<Lightbulb className="text-amber-400" size={20} />
|
| 29 |
+
AI Suggestions
|
| 30 |
+
</h2>
|
| 31 |
+
<button onClick={onClose} className="text-slate-400 hover:text-white transition-colors">
|
| 32 |
+
<X size={20} />
|
| 33 |
+
</button>
|
| 34 |
+
</div>
|
| 35 |
+
<div className="p-6 overflow-y-auto max-h-[60vh]">
|
| 36 |
+
{isLoading ? (
|
| 37 |
+
<div className="flex flex-col items-center justify-center space-y-4 py-8">
|
| 38 |
+
<Sparkles className="text-amber-400 animate-spin" size={32} />
|
| 39 |
+
<p className="text-slate-400 text-sm animate-pulse">Analyzing architecture...</p>
|
| 40 |
+
</div>
|
| 41 |
+
) : isImplementing ? (
|
| 42 |
+
<div className="flex flex-col items-center justify-center space-y-4 py-8">
|
| 43 |
+
<Wand2 className="text-purple-400 animate-pulse" size={32} />
|
| 44 |
+
<p className="text-slate-400 text-sm animate-pulse">Agents are applying fixes...</p>
|
| 45 |
+
</div>
|
| 46 |
+
) : (
|
| 47 |
+
<div className="text-slate-200 text-sm leading-relaxed whitespace-pre-wrap font-medium">
|
| 48 |
+
{suggestions || "No suggestions available."}
|
| 49 |
+
</div>
|
| 50 |
+
)}
|
| 51 |
+
</div>
|
| 52 |
+
<div className="p-4 border-t border-slate-800 bg-slate-900/50 flex justify-between items-center">
|
| 53 |
+
<div className="text-xs text-slate-500">
|
| 54 |
+
Review carefully before applying.
|
| 55 |
+
</div>
|
| 56 |
+
<div className="flex gap-3">
|
| 57 |
+
<button
|
| 58 |
+
onClick={onClose}
|
| 59 |
+
className="px-4 py-2 bg-slate-800 hover:bg-slate-700 text-slate-200 rounded text-sm transition-colors"
|
| 60 |
+
>
|
| 61 |
+
Close
|
| 62 |
+
</button>
|
| 63 |
+
{!isLoading && !isImplementing && suggestions && !suggestions.includes("Error") && (
|
| 64 |
+
<button
|
| 65 |
+
onClick={onImplement}
|
| 66 |
+
className="flex items-center gap-2 px-4 py-2 bg-purple-600 hover:bg-purple-500 text-white rounded text-sm font-medium transition-colors shadow-lg shadow-purple-900/20"
|
| 67 |
+
>
|
| 68 |
+
<Wand2 size={16} />
|
| 69 |
+
Implement Fixes (Auto)
|
| 70 |
+
</button>
|
| 71 |
+
)}
|
| 72 |
+
</div>
|
| 73 |
+
</div>
|
| 74 |
+
</div>
|
| 75 |
+
</div>
|
| 76 |
+
);
|
| 77 |
+
};
|
| 78 |
+
export default SuggestionsModal;
|