wuhp commited on
Commit
93320b8
·
verified ·
1 Parent(s): e90c524

Create Builder.tsx

Browse files
Files changed (1) hide show
  1. components/Builder.tsx +560 -0
components/Builder.tsx ADDED
@@ -0,0 +1,560 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ import React, { useCallback, useRef, useState, useMemo, useEffect } from 'react';
2
+ import ReactFlow, {
3
+ addEdge,
4
+ useNodesState,
5
+ useEdgesState,
6
+ Controls,
7
+ Background,
8
+ Connection,
9
+ Edge,
10
+ Node,
11
+ BackgroundVariant,
12
+ Panel,
13
+ ConnectionMode
14
+ } from 'reactflow';
15
+ import { FileText, CheckCircle, AlertTriangle, X, Copy, Check, Lightbulb, Key, Wrench, Menu, Activity } from 'lucide-react';
16
+ import Sidebar from './Sidebar';
17
+ import PropertiesPanel from './PropertiesPanel';
18
+ import CustomNode from './CustomNode';
19
+ import CodeViewer from './CodeViewer';
20
+ import AIBuilderModal from './AIBuilderModal';
21
+ import SuggestionsModal from './SuggestionsModal';
22
+ import ApiKeyModal from './ApiKeyModal';
23
+ import FixerModal from './FixerModal';
24
+ import { INITIAL_NODES, INITIAL_EDGES, LAYER_DEFINITIONS, TEMPLATES } from '../constants';
25
+ import { generateRefinedPrompt, validateArchitecture, getArchitectureSuggestions, getUserApiKey, implementArchitectureSuggestions } from '../services/geminiService';
26
+ import { NodeData, LayerType, LogEntry } from '../types';
27
+
28
+ let id = 1000;
29
+ const getId = () => `${id++}`;
30
+
31
+ // --- MAIN BUILDER CONTENT ---
32
+ interface BuilderProps {
33
+ onBackToHome: () => void;
34
+ }
35
+
36
+ const Builder: React.FC<BuilderProps> = ({ onBackToHome }) => {
37
+ const reactFlowWrapper = useRef<HTMLDivElement>(null);
38
+ const [nodes, setNodes, onNodesChange] = useNodesState(INITIAL_NODES);
39
+ const [edges, setEdges, onEdgesChange] = useEdgesState(INITIAL_EDGES);
40
+ const [reactFlowInstance, setReactFlowInstance] = useState<any>(null);
41
+ const [selectedNodeId, setSelectedNodeId] = useState<string | null>(null);
42
+
43
+ const [generatedPrompt, setGeneratedPrompt] = useState('');
44
+ const [isPromptViewerOpen, setIsPromptViewerOpen] = useState(false);
45
+ const [isGeneratingPrompt, setIsGeneratingPrompt] = useState(false);
46
+
47
+ const [validationMsg, setValidationMsg] = useState<string | null>(null);
48
+ const [layoutCopied, setLayoutCopied] = useState(false);
49
+
50
+ // Responsive Layout State
51
+ const [isSidebarOpen, setIsSidebarOpen] = useState(true);
52
+ const [isPropertiesOpen, setIsPropertiesOpen] = useState(false);
53
+
54
+ // Suggestions State
55
+ const [isSuggestionsOpen, setIsSuggestionsOpen] = useState(false);
56
+ const [suggestionsText, setSuggestionsText] = useState('');
57
+ const [isSuggestionsLoading, setIsSuggestionsLoading] = useState(false);
58
+ const [isImplementingSuggestions, setIsImplementingSuggestions] = useState(false);
59
+
60
+ // Fixer State
61
+ const [isFixerOpen, setIsFixerOpen] = useState(false);
62
+
63
+ // AI Builder and Template State
64
+ const [isAIBuilderOpen, setIsAIBuilderOpen] = useState(false);
65
+ const [isTemplateMenuOpen, setIsTemplateMenuOpen] = useState(false);
66
+
67
+ // API Key State
68
+ const [isApiKeyModalOpen, setIsApiKeyModalOpen] = useState(false);
69
+ const [isConnected, setIsConnected] = useState(false);
70
+
71
+ // Activity Log State
72
+ const [logs, setLogs] = useState<LogEntry[]>([]);
73
+
74
+ const addLog = useCallback((message: string, type: 'info' | 'success' | 'warning' | 'error' = 'info') => {
75
+ setLogs(prev => [{
76
+ id: Date.now().toString() + Math.random(),
77
+ timestamp: new Date(),
78
+ message,
79
+ type
80
+ }, ...prev]);
81
+ }, []);
82
+
83
+ // Initialize responsiveness
84
+ useEffect(() => {
85
+ const handleResize = () => {
86
+ if (window.innerWidth < 768) {
87
+ setIsSidebarOpen(false);
88
+ } else {
89
+ setIsSidebarOpen(true);
90
+ }
91
+ };
92
+
93
+ // Set initial state
94
+ handleResize();
95
+
96
+ window.addEventListener('resize', handleResize);
97
+ return () => window.removeEventListener('resize', handleResize);
98
+ }, []);
99
+
100
+ // Load connection status on mount
101
+ useEffect(() => {
102
+ setIsConnected(!!getUserApiKey());
103
+ addLog('System initialized. Ready to build.', 'info');
104
+ }, [addLog]);
105
+
106
+ // Define custom node types
107
+ const nodeTypes = useMemo(() => ({ custom: CustomNode }), []);
108
+
109
+ const onConnect = useCallback(
110
+ (params: Connection) => setEdges((eds) => addEdge({ ...params, animated: true, style: { stroke: '#94a3b8' } }, eds)),
111
+ [setEdges]
112
+ );
113
+
114
+ const onDragOver = useCallback((event: React.DragEvent) => {
115
+ event.preventDefault();
116
+ event.dataTransfer.dropEffect = 'move';
117
+ }, []);
118
+
119
+ const onDrop = useCallback(
120
+ (event: React.DragEvent) => {
121
+ event.preventDefault();
122
+
123
+ if (!reactFlowWrapper.current || !reactFlowInstance) return;
124
+
125
+ const type = event.dataTransfer.getData('application/reactflow') as LayerType;
126
+
127
+ if (!type) return;
128
+
129
+ const position = reactFlowInstance.screenToFlowPosition({
130
+ x: event.clientX,
131
+ y: event.clientY,
132
+ });
133
+
134
+ const label = LAYER_DEFINITIONS[type].label;
135
+ const newNode: Node<NodeData> = {
136
+ id: getId(),
137
+ type: 'custom',
138
+ position,
139
+ data: {
140
+ label: label,
141
+ type: type,
142
+ params: LAYER_DEFINITIONS[type].parameters.reduce((acc, p) => ({...acc, [p.name]: p.default}), {})
143
+ },
144
+ };
145
+
146
+ setNodes((nds) => nds.concat(newNode));
147
+ setSelectedNodeId(newNode.id);
148
+ setIsPropertiesOpen(true); // Open properties panel on drop
149
+
150
+ // On mobile, close sidebar after dropping
151
+ if (window.innerWidth < 768) {
152
+ setIsSidebarOpen(false);
153
+ }
154
+
155
+ addLog(`Added layer: ${label}`, 'info');
156
+ },
157
+ [reactFlowInstance, setNodes, addLog]
158
+ );
159
+
160
+ const onNodeClick = useCallback((_: React.MouseEvent, node: Node) => {
161
+ setSelectedNodeId(node.id);
162
+ setIsPropertiesOpen(true);
163
+ }, []);
164
+
165
+ const onPaneClick = useCallback(() => {
166
+ setSelectedNodeId(null);
167
+ setIsPropertiesOpen(false);
168
+ }, []);
169
+
170
+ // Update node data (params or label) from Properties Panel
171
+ const updateNodeData = (id: string, newData: Partial<NodeData>) => {
172
+ setNodes((nds) =>
173
+ nds.map((node) => {
174
+ if (node.id === id) {
175
+ return {
176
+ ...node,
177
+ data: { ...node.data, ...newData }
178
+ };
179
+ }
180
+ return node;
181
+ })
182
+ );
183
+ };
184
+
185
+ const deleteNode = (id: string) => {
186
+ const node = nodes.find(n => n.id === id);
187
+ setNodes((nds) => nds.filter((node) => node.id !== id));
188
+ setEdges((eds) => eds.filter((edge) => edge.source !== id && edge.target !== id));
189
+ setSelectedNodeId(null);
190
+ setIsPropertiesOpen(false);
191
+ if (node) addLog(`Deleted layer: ${node.data.label}`, 'info');
192
+ };
193
+
194
+ const handleAPIError = (error: any) => {
195
+ const msg = error.message || String(error);
196
+ addLog(`API Error: ${msg}`, 'error');
197
+
198
+ // Force disconnect if authentication failed to allow user to re-enter key
199
+ if (msg.includes('API Key') || msg.includes('403') || msg.includes('401') || msg.includes('400')) {
200
+ setIsConnected(false);
201
+ setIsApiKeyModalOpen(true);
202
+ addLog("Disconnected due to invalid API key. Please check your key.", 'warning');
203
+ }
204
+ }
205
+
206
+ const handleGeneratePrompt = async () => {
207
+ if (!isConnected) { setIsApiKeyModalOpen(true); return; }
208
+ setIsGeneratingPrompt(true);
209
+ setIsPromptViewerOpen(true);
210
+ addLog("Generating PyTorch code prompt...", 'info');
211
+ try {
212
+ const promptText = await generateRefinedPrompt(nodes, edges);
213
+ setGeneratedPrompt(promptText);
214
+ addLog("Code prompt generated successfully.", 'success');
215
+ } catch (e) {
216
+ setGeneratedPrompt("Failed to generate prompt. See logs for details.");
217
+ handleAPIError(e);
218
+ } finally {
219
+ setIsGeneratingPrompt(false);
220
+ }
221
+ };
222
+
223
+ const handleValidate = async () => {
224
+ if (!isConnected) { setIsApiKeyModalOpen(true); return; }
225
+ setValidationMsg("Validating...");
226
+ addLog("Running architecture validation...", 'info');
227
+ try {
228
+ const result = await validateArchitecture(nodes, edges);
229
+ setValidationMsg(result);
230
+ if (result.toLowerCase().includes("valid") && !result.toLowerCase().includes("invalid") && result.length < 50) {
231
+ setTimeout(() => setValidationMsg(null), 5000);
232
+ addLog("Validation passed: Architecture is valid.", 'success');
233
+ } else {
234
+ addLog("Validation issues found.", 'warning');
235
+ }
236
+ } catch (e) {
237
+ setValidationMsg("Validation failed due to API error.");
238
+ handleAPIError(e);
239
+ }
240
+ };
241
+
242
+ const handleGetSuggestions = async () => {
243
+ if (!isConnected) { setIsApiKeyModalOpen(true); return; }
244
+ setIsSuggestionsOpen(true);
245
+ setIsSuggestionsLoading(true);
246
+ try {
247
+ const suggestions = await getArchitectureSuggestions(nodes, edges);
248
+ setSuggestionsText(suggestions);
249
+ } catch (error) {
250
+ setSuggestionsText("Failed to get suggestions. API Error.");
251
+ handleAPIError(error);
252
+ } finally {
253
+ setIsSuggestionsLoading(false);
254
+ }
255
+ };
256
+
257
+ const handleImplementSuggestions = async () => {
258
+ if (!suggestionsText || isImplementingSuggestions) return;
259
+ setIsImplementingSuggestions(true);
260
+ addLog("AI Agents implementing suggestions...", 'info');
261
+ try {
262
+ const result = await implementArchitectureSuggestions(nodes, edges, suggestionsText);
263
+ if (result && result.nodes) {
264
+ // Fix types and defaults safely
265
+ const processedNodes = result.nodes.map((n: any) => {
266
+ const data = n.data || {};
267
+ const type = data.type || n.type || 'Identity'; // Fallback type
268
+ return {
269
+ ...n,
270
+ type: 'custom',
271
+ data: {
272
+ ...data,
273
+ type: type,
274
+ label: data.label || n.label || type, // Safe fallback for label
275
+ params: data.params || {}
276
+ }
277
+ };
278
+ });
279
+
280
+ const processedEdges = result.edges.map((e: any) => ({
281
+ ...e, animated: true, style: { stroke: '#94a3b8' }
282
+ }));
283
+
284
+ setNodes(processedNodes);
285
+ setEdges(processedEdges);
286
+ setIsSuggestionsOpen(false); // Close modal on success
287
+ addLog("Implemented AI improvements.", 'success');
288
+ }
289
+ } catch (e) {
290
+ handleAPIError(e);
291
+ addLog("Failed to implement suggestions.", 'error');
292
+ } finally {
293
+ setIsImplementingSuggestions(false);
294
+ }
295
+ };
296
+
297
+ const handleCopyLayout = () => {
298
+ const layout = {
299
+ nodes,
300
+ edges
301
+ };
302
+ navigator.clipboard.writeText(JSON.stringify(layout, null, 2));
303
+ setLayoutCopied(true);
304
+ setTimeout(() => setLayoutCopied(false), 2000);
305
+ addLog("Layout JSON copied to clipboard.", 'info');
306
+ };
307
+
308
+ const handleApplyAIBuild = (newNodes: any[], newEdges: any[], logMsg?: string) => {
309
+ setNodes(newNodes);
310
+ setEdges(newEdges);
311
+ if (logMsg) {
312
+ addLog(logMsg, 'success');
313
+ }
314
+ };
315
+
316
+ const loadTemplate = (templateKey: string) => {
317
+ const template = TEMPLATES[templateKey];
318
+ if (template) {
319
+ // Need to deep clone to avoid reference issues if loaded multiple times
320
+ const clonedNodes = JSON.parse(JSON.stringify(template.nodes));
321
+ const clonedEdges = JSON.parse(JSON.stringify(template.edges));
322
+
323
+ // Update styling and functional props that aren't in JSON
324
+ const hydratedNodes = clonedNodes.map((n: any) => ({
325
+ ...n,
326
+ data: {
327
+ ...n.data,
328
+ label: LAYER_DEFINITIONS[n.data.type as LayerType]?.label || n.data.label
329
+ }
330
+ }));
331
+
332
+ const hydratedEdges = clonedEdges.map((e: any) => ({
333
+ ...e,
334
+ animated: true,
335
+ style: { stroke: '#94a3b8' }
336
+ }));
337
+
338
+ setNodes(hydratedNodes);
339
+ setEdges(hydratedEdges);
340
+ setIsTemplateMenuOpen(false);
341
+
342
+ // Auto open properties to see details of first node
343
+ if (hydratedNodes.length > 0) {
344
+ setSelectedNodeId(hydratedNodes[0].id);
345
+ setIsPropertiesOpen(true);
346
+ }
347
+ addLog(`Loaded template: ${template.name}`, 'info');
348
+ }
349
+ };
350
+
351
+ const selectedNode = useMemo(() =>
352
+ nodes.find((n) => n.id === selectedNodeId) || null,
353
+ [nodes, selectedNodeId]
354
+ );
355
+
356
+ return (
357
+ <div className="flex h-screen w-screen bg-slate-950 overflow-hidden">
358
+ <Sidebar
359
+ onOpenAIBuilder={() => isConnected ? setIsAIBuilderOpen(true) : setIsApiKeyModalOpen(true)}
360
+ onSelectTemplate={(id) => id === 'menu' ? setIsTemplateMenuOpen(true) : loadTemplate(id)}
361
+ onBackToHome={onBackToHome}
362
+ isConnected={isConnected}
363
+ isOpen={isSidebarOpen}
364
+ onToggle={() => setIsSidebarOpen(!isSidebarOpen)}
365
+ />
366
+
367
+ <div className="flex-1 h-full relative" ref={reactFlowWrapper}>
368
+ <ReactFlow
369
+ nodes={nodes}
370
+ edges={edges}
371
+ onNodesChange={onNodesChange}
372
+ onEdgesChange={onEdgesChange}
373
+ onConnect={onConnect}
374
+ onInit={setReactFlowInstance}
375
+ onDrop={onDrop}
376
+ onDragOver={onDragOver}
377
+ onNodeClick={onNodeClick}
378
+ onPaneClick={onPaneClick}
379
+ nodeTypes={nodeTypes}
380
+ connectionMode={ConnectionMode.Loose}
381
+ fitView
382
+ className="bg-slate-950"
383
+ >
384
+ <Background color="#1e293b" variant={BackgroundVariant.Dots} gap={20} size={1} />
385
+ <Controls className="!bg-slate-800 !border-slate-700 [&>button]:!fill-slate-300 [&>button:hover]:!bg-slate-700 md:bottom-4 md:left-4 bottom-24 left-4" />
386
+
387
+ {/* Mobile Menu Trigger */}
388
+ <Panel position="top-left" className="md:hidden">
389
+ <button
390
+ onClick={() => setIsSidebarOpen(true)}
391
+ className="p-2 bg-slate-800/80 backdrop-blur border border-slate-700 rounded-lg text-slate-300 shadow-lg"
392
+ >
393
+ <Menu size={20} />
394
+ </button>
395
+ </Panel>
396
+
397
+ {/* Action Toolbar */}
398
+ <Panel position="top-right" className="flex flex-wrap justify-end gap-2 max-w-[80vw]">
399
+ <button
400
+ onClick={handleCopyLayout}
401
+ className="flex items-center gap-2 px-3 py-2 bg-slate-800/80 backdrop-blur hover:bg-slate-700 border border-slate-700 rounded-lg text-slate-200 text-xs md:text-sm font-medium transition-colors shadow-lg"
402
+ title="Copy Layout JSON"
403
+ >
404
+ {layoutCopied ? <Check size={16} className="text-emerald-500" /> : <Copy size={16} className="text-slate-400" />}
405
+ <span className="hidden md:inline">{layoutCopied ? 'Copied' : 'Copy'}</span>
406
+ </button>
407
+ <button
408
+ onClick={() => setIsApiKeyModalOpen(true)}
409
+ className="flex items-center gap-2 px-3 py-2 bg-slate-800/80 backdrop-blur hover:bg-slate-700 border border-slate-700 rounded-lg text-slate-200 text-xs md:text-sm font-medium transition-colors shadow-lg"
410
+ title="API Key Settings"
411
+ >
412
+ <Key size={16} className={isConnected ? "text-emerald-500" : "text-slate-400"} />
413
+ <span className="hidden md:inline">{isConnected ? 'Key Active' : 'Add Key'}</span>
414
+ </button>
415
+ <button
416
+ onClick={handleGetSuggestions}
417
+ className="flex items-center gap-2 px-3 py-2 bg-slate-800/80 backdrop-blur hover:bg-slate-700 border border-slate-700 rounded-lg text-slate-200 text-xs md:text-sm font-medium transition-colors shadow-lg"
418
+ title="Get AI Suggestions"
419
+ >
420
+ <Lightbulb size={16} className="text-amber-400" />
421
+ <span className="hidden md:inline">Tips</span>
422
+ </button>
423
+ <button
424
+ onClick={handleValidate}
425
+ className="flex items-center gap-2 px-3 py-2 bg-slate-800/80 backdrop-blur hover:bg-slate-700 border border-slate-700 rounded-lg text-slate-200 text-xs md:text-sm font-medium transition-colors shadow-lg"
426
+ title="Validate Architecture"
427
+ >
428
+ <CheckCircle size={16} className="text-emerald-500" />
429
+ <span className="hidden md:inline">Validate</span>
430
+ </button>
431
+ <button
432
+ onClick={handleGeneratePrompt}
433
+ className="flex items-center gap-2 px-3 py-2 bg-blue-600/90 hover:bg-blue-500 backdrop-blur rounded-lg text-white text-xs md:text-sm font-medium transition-colors shadow-lg shadow-blue-900/20"
434
+ title="Generate Code Prompt"
435
+ >
436
+ <FileText size={16} />
437
+ <span className="hidden md:inline">Code</span>
438
+ </button>
439
+ {/* Mobile Activity Toggle */}
440
+ <button
441
+ onClick={() => { setSelectedNodeId(null); setIsPropertiesOpen(true); }}
442
+ className="md:hidden flex items-center gap-2 px-3 py-2 bg-slate-800/80 backdrop-blur hover:bg-slate-700 border border-slate-700 rounded-lg text-slate-200 transition-colors shadow-lg"
443
+ title="System Logs"
444
+ >
445
+ <Activity size={16} className="text-blue-400" />
446
+ </button>
447
+ </Panel>
448
+
449
+ {validationMsg && (
450
+ <Panel position="bottom-center" className="mb-20 md:mb-8 w-full flex justify-center px-4">
451
+ <div className="bg-slate-900/95 backdrop-blur border border-slate-700 text-slate-200 px-6 py-4 rounded-xl shadow-2xl max-w-2xl w-full flex flex-col gap-3 animate-in slide-in-from-bottom-5">
452
+ <div className="flex items-start gap-3">
453
+ <AlertTriangle className="text-amber-500 shrink-0 mt-0.5" size={20} />
454
+ <div className="flex-1 min-w-0">
455
+ <h4 className="font-bold text-sm text-slate-300 mb-1">Validation Report</h4>
456
+ <p className="text-sm whitespace-pre-wrap leading-relaxed text-slate-400 max-h-[30vh] overflow-y-auto scrollbar-thin scrollbar-thumb-slate-700 pr-2">{validationMsg}</p>
457
+ </div>
458
+ </div>
459
+
460
+ <div className="flex justify-end gap-3 mt-2">
461
+ <button
462
+ onClick={() => setValidationMsg(null)}
463
+ className="px-4 py-1.5 text-xs font-medium text-slate-400 hover:text-slate-200 bg-slate-800 hover:bg-slate-700 rounded transition-colors"
464
+ >
465
+ Dismiss
466
+ </button>
467
+ {!validationMsg.toLowerCase().includes("architecture is valid") && (
468
+ <button
469
+ onClick={() => setIsFixerOpen(true)}
470
+ className="flex items-center gap-2 px-4 py-1.5 bg-red-600 hover:bg-red-500 text-white rounded text-xs font-medium transition-colors shadow-lg shadow-red-900/20"
471
+ >
472
+ <Wrench size={14} />
473
+ Fix Errors
474
+ </button>
475
+ )}
476
+ </div>
477
+ </div>
478
+ </Panel>
479
+ )}
480
+
481
+ {/* Template Selection Modal Overlay */}
482
+ {isTemplateMenuOpen && (
483
+ <div className="absolute inset-0 z-50 flex items-center justify-center bg-black/60 backdrop-blur-sm p-4">
484
+ <div className="bg-slate-900 border border-slate-700 rounded-xl shadow-2xl w-96 max-w-full flex flex-col max-h-[80vh] overflow-hidden animate-in zoom-in-95 duration-200">
485
+ <div className="flex justify-between items-center p-6 border-b border-slate-800 shrink-0 bg-slate-900">
486
+ <h3 className="text-lg font-bold text-white">Select Template</h3>
487
+ <button onClick={() => setIsTemplateMenuOpen(false)}><X size={20} className="text-slate-500 hover:text-white"/></button>
488
+ </div>
489
+ <div className="overflow-y-auto p-6 scrollbar-thin scrollbar-thumb-slate-700">
490
+ <div className="grid gap-3">
491
+ {Object.entries(TEMPLATES).map(([key, t]) => (
492
+ <button
493
+ key={key}
494
+ onClick={() => loadTemplate(key)}
495
+ className="text-left p-3 rounded bg-slate-800 hover:bg-slate-750 border border-slate-700 hover:border-blue-500/50 transition-all group"
496
+ >
497
+ <div className="font-semibold text-slate-200 group-hover:text-blue-400">{t.name}</div>
498
+ <div className="text-xs text-slate-500">{t.description}</div>
499
+ </button>
500
+ ))}
501
+ </div>
502
+ </div>
503
+ </div>
504
+ </div>
505
+ )}
506
+
507
+ </ReactFlow>
508
+ </div>
509
+
510
+ <PropertiesPanel
511
+ selectedNode={selectedNode}
512
+ onChange={updateNodeData}
513
+ onDelete={deleteNode}
514
+ onClose={() => { setSelectedNodeId(null); setIsPropertiesOpen(false); }}
515
+ logs={logs}
516
+ isOpen={isPropertiesOpen}
517
+ />
518
+
519
+ <CodeViewer
520
+ isOpen={isPromptViewerOpen}
521
+ onClose={() => setIsPromptViewerOpen(false)}
522
+ code={generatedPrompt}
523
+ isLoading={isGeneratingPrompt}
524
+ />
525
+
526
+ <SuggestionsModal
527
+ isOpen={isSuggestionsOpen}
528
+ onClose={() => setIsSuggestionsOpen(false)}
529
+ suggestions={suggestionsText}
530
+ isLoading={isSuggestionsLoading}
531
+ onImplement={handleImplementSuggestions}
532
+ isImplementing={isImplementingSuggestions}
533
+ />
534
+
535
+ <AIBuilderModal
536
+ isOpen={isAIBuilderOpen}
537
+ onClose={() => setIsAIBuilderOpen(false)}
538
+ onApply={handleApplyAIBuild}
539
+ currentNodes={nodes}
540
+ />
541
+
542
+ <FixerModal
543
+ isOpen={isFixerOpen}
544
+ onClose={() => setIsFixerOpen(false)}
545
+ onApply={handleApplyAIBuild}
546
+ errorMsg={validationMsg || ''}
547
+ nodes={nodes}
548
+ edges={edges}
549
+ />
550
+
551
+ <ApiKeyModal
552
+ isOpen={isApiKeyModalOpen}
553
+ onClose={() => setIsApiKeyModalOpen(false)}
554
+ onSuccess={() => setIsConnected(true)}
555
+ />
556
+ </div>
557
+ );
558
+ };
559
+
560
+ export default Builder;