NeerajCodz commited on
Commit
de797a5
·
1 Parent(s): f659d59

feat: redesign Dashboard with 2-view split and improved UX

Browse files

Dashboard Input View (ChatGPT-like):
- Multi-URL input with add/remove functionality
- Structured instruction and output format fields
- Model/Vision/Agents/Plugins/Type selection buttons
- Clean centered layout with visual hierarchy

Dashboard View:
- Left sidebar shows only INSTALLED/ENABLED items
- Info icons on each item with popup details
- Session-based stats (start at 0, not fake data)
- Assets section replaces Recent Actions
- Memory management with add/view functionality
- Input summary in right sidebar

Additional changes:
- Move Swagger docs from /docs to /swagger
- Add vision model selection option
- Add info popup component for item details
- Fix system status display

backend/app/main.py CHANGED
@@ -97,7 +97,7 @@ def create_app() -> FastAPI:
97
  version=settings.app_version,
98
  debug=settings.debug,
99
  lifespan=lifespan,
100
- docs_url="/docs",
101
  redoc_url="/redoc",
102
  openapi_url="/openapi.json",
103
  )
 
97
  version=settings.app_version,
98
  debug=settings.debug,
99
  lifespan=lifespan,
100
+ docs_url="/swagger",
101
  redoc_url="/redoc",
102
  openapi_url="/openapi.json",
103
  )
frontend/src/components/Dashboard.tsx CHANGED
@@ -4,7 +4,6 @@ import {
4
  Activity,
5
  Zap,
6
  Target,
7
- Clock,
8
  TrendingUp,
9
  Database,
10
  Cpu,
@@ -13,9 +12,7 @@ import {
13
  Pause,
14
  ChevronDown,
15
  ChevronRight,
16
- MoreHorizontal,
17
  Terminal,
18
- Settings,
19
  Wrench,
20
  Plug,
21
  Eye,
@@ -24,7 +21,14 @@ import {
24
  Check,
25
  Layers,
26
  FileText,
27
- List,
 
 
 
 
 
 
 
28
  } from 'lucide-react';
29
  import { Badge } from '@/components/ui/Badge';
30
  import { classNames } from '@/utils/helpers';
@@ -32,10 +36,12 @@ import { apiClient } from '@/api/client';
32
 
33
  // Types
34
  interface TaskInput {
35
- url: string;
36
  instruction: string;
 
37
  taskType: 'low' | 'medium' | 'high';
38
  selectedModel: string;
 
39
  selectedAgents: string[];
40
  enabledPlugins: string[];
41
  }
@@ -48,81 +54,117 @@ interface LogEntry {
48
  source?: string;
49
  }
50
 
51
- interface EpisodeStats {
52
- total: number;
53
- min: number;
54
- max: number;
55
- avg: number;
 
 
 
 
 
 
 
 
 
56
  }
57
 
58
- interface AgentOption {
59
  id: string;
60
  name: string;
61
  description: string;
62
- active?: boolean;
 
 
 
 
 
 
 
63
  }
64
 
65
- interface ModelOption {
66
  provider: string;
67
  model: string;
68
  name: string;
 
69
  }
70
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
71
  // Popup Components
72
  interface PopupProps {
73
  title: string;
74
  isOpen: boolean;
75
  onClose: () => void;
76
  children: React.ReactNode;
 
77
  }
78
 
79
- const Popup: React.FC<PopupProps> = ({ title, isOpen, onClose, children }) => {
80
  if (!isOpen) return null;
 
 
 
 
 
81
  return (
82
  <div className="fixed inset-0 z-50 flex items-center justify-center bg-black/60 backdrop-blur-sm">
83
- <div className="bg-gray-800 border border-gray-700 rounded-xl shadow-2xl w-full max-w-lg max-h-[80vh] overflow-hidden">
84
  <div className="flex items-center justify-between px-4 py-3 border-b border-gray-700">
85
  <h3 className="font-semibold text-white">{title}</h3>
86
  <button onClick={onClose} className="p-1 text-gray-400 hover:text-white transition-colors">
87
  <X className="w-5 h-5" />
88
  </button>
89
  </div>
90
- <div className="p-4 overflow-y-auto max-h-[60vh]">{children}</div>
91
  </div>
92
  </div>
93
  );
94
  };
95
 
96
- // Stats Popup
97
- const StatsPopup: React.FC<{ isOpen: boolean; onClose: () => void; stats: EpisodeStats; title: string }> = ({
98
- isOpen,
99
- onClose,
100
- stats,
101
- title,
102
- }) => (
103
- <Popup title={title} isOpen={isOpen} onClose={onClose}>
104
- <div className="grid grid-cols-2 gap-4">
105
- <div className="p-4 bg-gray-900/50 rounded-lg text-center">
106
- <p className="text-2xl font-bold text-emerald-400">{stats.total}</p>
107
- <p className="text-xs text-gray-400">Total</p>
108
- </div>
109
- <div className="p-4 bg-gray-900/50 rounded-lg text-center">
110
- <p className="text-2xl font-bold text-cyan-400">{stats.avg.toFixed(2)}</p>
111
- <p className="text-xs text-gray-400">Average</p>
112
- </div>
113
- <div className="p-4 bg-gray-900/50 rounded-lg text-center">
114
- <p className="text-2xl font-bold text-amber-400">{stats.min.toFixed(2)}</p>
115
- <p className="text-xs text-gray-400">Minimum</p>
116
- </div>
117
- <div className="p-4 bg-gray-900/50 rounded-lg text-center">
118
- <p className="text-2xl font-bold text-purple-400">{stats.max.toFixed(2)}</p>
119
- <p className="text-xs text-gray-400">Maximum</p>
120
- </div>
121
- </div>
122
- </Popup>
123
- );
124
-
125
- // Accordion Component
126
  interface AccordionProps {
127
  title: string;
128
  icon: React.ElementType;
@@ -139,43 +181,73 @@ const Accordion: React.FC<AccordionProps> = ({ title, icon: Icon, badge, color,
139
  <div className="border border-gray-700/50 rounded-lg overflow-hidden">
140
  <button
141
  onClick={() => setIsOpen(!isOpen)}
142
- className="w-full flex items-center justify-between px-3 py-2.5 bg-gray-800/50 hover:bg-gray-800 transition-colors"
143
  >
144
  <div className="flex items-center gap-2">
145
  <Icon className={`w-4 h-4 ${color}`} />
146
- <span className="text-sm font-medium text-white">{title}</span>
147
- {badge !== undefined && (
148
  <Badge variant="neutral" size="sm">{badge}</Badge>
149
  )}
150
  </div>
151
- {isOpen ? <ChevronDown className="w-4 h-4 text-gray-400" /> : <ChevronRight className="w-4 h-4 text-gray-400" />}
152
  </button>
153
- {isOpen && <div className="p-3 bg-gray-900/30 border-t border-gray-700/50">{children}</div>}
154
  </div>
155
  );
156
  };
157
 
158
  // Main Dashboard Component
159
  export const Dashboard: React.FC = () => {
160
- // State
 
 
 
161
  const [taskInput, setTaskInput] = useState<TaskInput>({
162
- url: '',
163
  instruction: '',
 
164
  taskType: 'medium',
165
  selectedModel: 'groq/gpt-oss-120b',
166
- selectedAgents: ['coordinator', 'scraper'],
167
- enabledPlugins: ['browser-use', 'firecrawl'],
 
168
  });
169
- const [logs, setLogs] = useState<LogEntry[]>([
170
- { id: '1', timestamp: new Date().toISOString(), level: 'info', message: 'System initialized', source: 'system' },
171
- { id: '2', timestamp: new Date().toISOString(), level: 'info', message: 'Ready to start episode', source: 'coordinator' },
172
- ]);
 
 
 
 
173
  const [isRunning, setIsRunning] = useState(false);
 
 
 
 
 
 
 
 
 
174
  const [showModelPopup, setShowModelPopup] = useState(false);
 
175
  const [showAgentPopup, setShowAgentPopup] = useState(false);
176
  const [showPluginPopup, setShowPluginPopup] = useState(false);
177
  const [showTaskTypePopup, setShowTaskTypePopup] = useState(false);
178
- const [showStatsPopup, setShowStatsPopup] = useState<'episodes' | 'steps' | 'reward' | null>(null);
 
 
 
 
 
 
 
 
 
 
 
179
 
180
  // API Queries
181
  const { data: health } = useQuery({
@@ -187,15 +259,17 @@ export const Dashboard: React.FC = () => {
187
  const { data: agentsData } = useQuery({
188
  queryKey: ['agents'],
189
  queryFn: async () => {
190
- const res = await fetch('/api/agents/');
 
191
  return res.json();
192
  },
193
  });
194
 
195
- useQuery({
196
  queryKey: ['plugins'],
197
  queryFn: async () => {
198
- const res = await fetch('/api/plugins/');
 
199
  return res.json();
200
  },
201
  });
@@ -204,6 +278,7 @@ export const Dashboard: React.FC = () => {
204
  queryKey: ['memory-stats'],
205
  queryFn: async () => {
206
  const res = await fetch('/api/memory/stats/overview');
 
207
  return res.json();
208
  },
209
  refetchInterval: 3000,
@@ -212,384 +287,786 @@ export const Dashboard: React.FC = () => {
212
  const { data: settingsData } = useQuery({
213
  queryKey: ['client-settings'],
214
  queryFn: async () => {
215
- const res = await fetch('/api/settings/');
 
216
  return res.json();
217
  },
218
  });
219
 
220
- // Episode Stats (mock data for now)
221
- const episodeStats: EpisodeStats = { total: 12, min: 3, max: 47, avg: 18.5 };
222
- const stepStats: EpisodeStats = { total: 156, min: 5, max: 89, avg: 23.4 };
223
- const rewardStats: EpisodeStats = { total: 847.5, min: -12.3, max: 98.7, avg: 70.6 };
224
-
225
- // Available options
226
- const availableModels: ModelOption[] = settingsData?.available_models ?? [
227
- { provider: 'groq', model: 'gpt-oss-120b', name: 'GPT-OSS 120B (Groq)' },
228
- { provider: 'google', model: 'gemini-2.5-flash', name: 'Gemini 2.5 Flash' },
229
- { provider: 'openai', model: 'gpt-4-turbo', name: 'GPT-4 Turbo' },
230
- { provider: 'anthropic', model: 'claude-3-opus', name: 'Claude 3 Opus' },
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
231
  ];
232
 
233
- const availableAgents: AgentOption[] = agentsData?.agents ?? [
234
- { id: 'coordinator', name: 'Coordinator', description: 'Orchestrates all agents', active: true },
235
- { id: 'scraper', name: 'Scraper', description: 'Extracts data from pages', active: true },
236
- { id: 'navigator', name: 'Navigator', description: 'Handles page navigation', active: false },
237
- { id: 'analyzer', name: 'Analyzer', description: 'Analyzes extracted data', active: false },
238
- { id: 'validator', name: 'Validator', description: 'Validates data quality', active: false },
239
  ];
240
 
241
- const pluginCategories = {
242
- mcps: [
243
- { id: 'browser-use', name: 'Browser Use', enabled: true, status: 'active' },
244
- { id: 'puppeteer-mcp', name: 'Puppeteer MCP', enabled: false, status: 'idle' },
245
- { id: 'playwright-mcp', name: 'Playwright MCP', enabled: false, status: 'idle' },
246
- ],
247
- skills: [
248
- { id: 'web-scraping', name: 'Web Scraping', enabled: true, status: 'active' },
249
- { id: 'data-extraction', name: 'Data Extraction', enabled: true, status: 'active' },
250
- { id: 'form-filling', name: 'Form Filling', enabled: false, status: 'idle' },
251
- ],
252
- apis: [
253
- { id: 'firecrawl', name: 'Firecrawl', enabled: true, status: 'active' },
254
- { id: 'jina-reader', name: 'Jina Reader', enabled: false, status: 'idle' },
255
- { id: 'serper', name: 'Serper API', enabled: false, status: 'idle' },
256
- ],
257
- vision: [
258
- { id: 'gpt4-vision', name: 'GPT-4 Vision', enabled: false, status: 'idle' },
259
- { id: 'gemini-vision', name: 'Gemini Vision', enabled: false, status: 'idle' },
260
- { id: 'claude-vision', name: 'Claude Vision', enabled: false, status: 'idle' },
261
- ],
262
  };
263
 
264
- const taskTypes = [
265
- { id: 'low', name: 'Low Complexity', description: 'Simple single-page scraping', color: 'emerald' },
266
- { id: 'medium', name: 'Medium Complexity', description: 'Multi-page with navigation', color: 'amber' },
267
- { id: 'high', name: 'High Complexity', description: 'Complex interactive tasks', color: 'red' },
268
- ];
 
 
 
 
 
 
 
 
 
 
 
 
 
269
 
 
270
  const handleStart = () => {
 
 
271
  setIsRunning(true);
272
- const newLog: LogEntry = {
 
 
 
273
  id: Date.now().toString(),
274
  timestamp: new Date().toISOString(),
275
  level: 'info',
276
- message: `Starting episode with URL: ${taskInput.url}`,
277
- source: 'coordinator',
278
- };
279
- setLogs((prev) => [...prev, newLog]);
 
 
280
  };
281
 
 
282
  const handleStop = () => {
283
  setIsRunning(false);
284
- const newLog: LogEntry = {
285
  id: Date.now().toString(),
286
  timestamp: new Date().toISOString(),
287
  level: 'warn',
288
  message: 'Episode stopped by user',
289
  source: 'system',
290
- };
291
- setLogs((prev) => [...prev, newLog]);
292
  };
293
 
 
294
  const formatTime = (isoString: string) => {
295
  return new Date(isoString).toLocaleTimeString('en-US', { hour12: false });
296
  };
297
 
 
298
  const getLogLevelColor = (level: LogEntry['level']) => {
299
- const colors = {
300
- info: 'text-cyan-400',
301
- warn: 'text-amber-400',
302
- error: 'text-red-400',
303
- debug: 'text-gray-400',
304
- };
305
  return colors[level];
306
  };
307
 
308
- return (
309
- <div className="h-[calc(100vh-80px)] flex flex-col">
310
- {/* Input Section */}
311
- <div className="flex-shrink-0 p-4 bg-gray-800/50 border-b border-gray-700/50">
312
- <div className="flex flex-wrap items-end gap-4">
313
- {/* URL Input */}
314
- <div className="flex-1 min-w-[300px]">
315
- <label className="text-xs text-gray-400 mb-1 block">Target URL</label>
316
- <input
317
- type="url"
318
- placeholder="https://example.com/page-to-scrape"
319
- value={taskInput.url}
320
- onChange={(e) => setTaskInput((p) => ({ ...p, url: e.target.value }))}
321
- className="w-full px-3 py-2 bg-gray-900/50 border border-gray-700/50 rounded-lg text-white placeholder-gray-500 focus:outline-none focus:ring-2 focus:ring-emerald-500/50 focus:border-emerald-500/50"
322
- />
 
 
323
  </div>
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
324
 
325
- {/* Instruction */}
326
- <div className="flex-1 min-w-[300px]">
327
- <label className="text-xs text-gray-400 mb-1 block">Instruction</label>
328
- <input
329
- type="text"
330
- placeholder="Extract all product prices and names..."
331
- value={taskInput.instruction}
332
- onChange={(e) => setTaskInput((p) => ({ ...p, instruction: e.target.value }))}
333
- className="w-full px-3 py-2 bg-gray-900/50 border border-gray-700/50 rounded-lg text-white placeholder-gray-500 focus:outline-none focus:ring-2 focus:ring-emerald-500/50 focus:border-emerald-500/50"
334
- />
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
335
  </div>
 
336
 
337
- {/* Selection Buttons */}
338
- <div className="flex items-center gap-2">
339
- <button
340
- onClick={() => setShowModelPopup(true)}
341
- className="px-3 py-2 bg-cyan-500/10 hover:bg-cyan-500/20 border border-cyan-500/30 text-cyan-400 rounded-lg text-sm font-medium transition-colors flex items-center gap-2"
342
- >
343
- <Cpu className="w-4 h-4" />
344
- Model
345
- </button>
346
- <button
347
- onClick={() => setShowAgentPopup(true)}
348
- className="px-3 py-2 bg-purple-500/10 hover:bg-purple-500/20 border border-purple-500/30 text-purple-400 rounded-lg text-sm font-medium transition-colors flex items-center gap-2"
349
- >
350
- <Bot className="w-4 h-4" />
351
- Agents
352
- </button>
353
- <button
354
- onClick={() => setShowPluginPopup(true)}
355
- className="px-3 py-2 bg-amber-500/10 hover:bg-amber-500/20 border border-amber-500/30 text-amber-400 rounded-lg text-sm font-medium transition-colors flex items-center gap-2"
356
- >
357
- <Plug className="w-4 h-4" />
358
- Plugins
359
- </button>
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
360
  <button
361
- onClick={() => setShowTaskTypePopup(true)}
 
 
 
362
  className={classNames(
363
- 'px-3 py-2 border rounded-lg text-sm font-medium transition-colors flex items-center gap-2',
364
- taskInput.taskType === 'low' && 'bg-emerald-500/10 border-emerald-500/30 text-emerald-400',
365
- taskInput.taskType === 'medium' && 'bg-amber-500/10 border-amber-500/30 text-amber-400',
366
- taskInput.taskType === 'high' && 'bg-red-500/10 border-red-500/30 text-red-400'
367
  )}
368
  >
369
- <Target className="w-4 h-4" />
370
- {taskInput.taskType.charAt(0).toUpperCase() + taskInput.taskType.slice(1)}
371
  </button>
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
372
  </div>
 
373
 
374
- {/* Start/Stop Button */}
375
- {isRunning ? (
376
- <button
377
- onClick={handleStop}
378
- className="px-5 py-2 bg-red-500 hover:bg-red-600 text-white rounded-lg font-medium transition-colors flex items-center gap-2 shadow-lg shadow-red-500/20"
379
- >
380
- <Pause className="w-4 h-4" />
381
- Stop
382
- </button>
383
- ) : (
384
- <button
385
- onClick={handleStart}
386
- disabled={!taskInput.url}
387
- className="px-5 py-2 bg-emerald-500 hover:bg-emerald-600 disabled:bg-gray-600 disabled:cursor-not-allowed text-white rounded-lg font-medium transition-colors flex items-center gap-2 shadow-lg shadow-emerald-500/20"
388
- >
389
- <Play className="w-4 h-4" />
390
- Start
391
- </button>
392
- )}
393
- </div>
394
- </div>
395
-
396
- {/* Main Content - 3 Column Layout */}
397
- <div className="flex-1 flex overflow-hidden">
398
- {/* Left Sidebar - Accordions */}
399
- <div className="w-64 flex-shrink-0 bg-gray-800/30 border-r border-gray-700/50 overflow-y-auto p-3 space-y-2">
400
- {/* Agents Accordion */}
401
- <Accordion title="Agents" icon={Bot} badge={taskInput.selectedAgents.length} color="text-purple-400" defaultOpen>
402
- <div className="space-y-2">
403
- {availableAgents.map((agent) => {
404
- const isActive = taskInput.selectedAgents.includes(agent.id);
405
- return (
406
- <div
407
- key={agent.id}
408
  className={classNames(
409
- 'flex items-center justify-between p-2 rounded-lg transition-colors',
410
- isActive ? 'bg-purple-500/10 border border-purple-500/30' : 'bg-gray-800/50'
411
  )}
412
  >
413
- <div className="flex items-center gap-2">
414
- <div className={classNames('w-2 h-2 rounded-full', isActive ? 'bg-emerald-400' : 'bg-gray-500')} />
415
- <span className="text-xs text-white">{agent.name}</span>
416
  </div>
417
- {isActive && <Clock className="w-3 h-3 text-gray-400" />}
418
- </div>
419
- );
420
- })}
421
- </div>
422
- </Accordion>
 
 
 
 
 
 
 
423
 
424
- {/* MCPs Accordion */}
425
- <Accordion title="MCPs" icon={Wrench} badge={pluginCategories.mcps.filter(p => p.enabled).length} color="text-amber-400">
426
- <div className="space-y-2">
427
- {pluginCategories.mcps.map((plugin) => (
428
- <div
429
- key={plugin.id}
430
- className={classNames(
431
- 'flex items-center justify-between p-2 rounded-lg',
432
- plugin.enabled ? 'bg-amber-500/10 border border-amber-500/30' : 'bg-gray-800/50'
433
- )}
434
- >
435
- <span className="text-xs text-white">{plugin.name}</span>
436
- <Badge variant={plugin.status === 'active' ? 'success' : 'neutral'} size="sm">
437
- {plugin.status}
438
- </Badge>
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
439
  </div>
440
- ))}
441
- </div>
442
- </Accordion>
 
443
 
444
- {/* Skills Accordion */}
445
- <Accordion title="Skills" icon={Zap} badge={pluginCategories.skills.filter(p => p.enabled).length} color="text-cyan-400">
446
- <div className="space-y-2">
447
- {pluginCategories.skills.map((plugin) => (
448
- <div
449
- key={plugin.id}
450
- className={classNames(
451
- 'flex items-center justify-between p-2 rounded-lg',
452
- plugin.enabled ? 'bg-cyan-500/10 border border-cyan-500/30' : 'bg-gray-800/50'
453
- )}
454
- >
455
- <span className="text-xs text-white">{plugin.name}</span>
456
- <Badge variant={plugin.status === 'active' ? 'success' : 'neutral'} size="sm">
457
- {plugin.status}
458
- </Badge>
 
 
 
 
 
 
 
 
 
 
459
  </div>
460
- ))}
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
461
  </div>
462
- </Accordion>
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
463
 
464
- {/* APIs Accordion */}
465
- <Accordion title="APIs" icon={Plug} badge={pluginCategories.apis.filter(p => p.enabled).length} color="text-emerald-400">
466
- <div className="space-y-2">
467
- {pluginCategories.apis.map((plugin) => (
468
- <div
469
- key={plugin.id}
470
- className={classNames(
471
- 'flex items-center justify-between p-2 rounded-lg',
472
- plugin.enabled ? 'bg-emerald-500/10 border border-emerald-500/30' : 'bg-gray-800/50'
473
- )}
474
- >
475
- <span className="text-xs text-white">{plugin.name}</span>
476
- <Badge variant={plugin.status === 'active' ? 'success' : 'neutral'} size="sm">
477
- {plugin.status}
478
- </Badge>
479
- </div>
480
- ))}
 
 
 
 
 
 
 
 
 
 
 
 
 
481
  </div>
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
482
  </Accordion>
483
 
484
- {/* Vision Accordion */}
485
- <Accordion title="Vision" icon={Eye} badge={pluginCategories.vision.filter(p => p.enabled).length} color="text-pink-400">
486
- <div className="space-y-2">
487
- {pluginCategories.vision.map((plugin) => (
488
- <div
489
- key={plugin.id}
490
- className={classNames(
491
- 'flex items-center justify-between p-2 rounded-lg',
492
- plugin.enabled ? 'bg-pink-500/10 border border-pink-500/30' : 'bg-gray-800/50'
493
- )}
494
- >
495
- <span className="text-xs text-white">{plugin.name}</span>
496
- <Badge variant={plugin.status === 'active' ? 'success' : 'neutral'} size="sm">
497
- {plugin.status}
498
- </Badge>
499
- </div>
500
- ))}
501
- </div>
502
  </Accordion>
503
 
504
- {/* Tools Accordion */}
505
- <Accordion title="Tools" icon={Wrench} color="text-blue-400">
506
- <div className="space-y-2 text-xs text-gray-400">
507
- <div className="flex items-center justify-between p-2 bg-gray-800/50 rounded-lg">
508
- <span>HTTP Client</span>
509
- <Badge variant="success" size="sm">ready</Badge>
 
 
510
  </div>
511
- <div className="flex items-center justify-between p-2 bg-gray-800/50 rounded-lg">
512
- <span>HTML Parser</span>
513
- <Badge variant="success" size="sm">ready</Badge>
 
 
 
 
 
 
 
 
 
 
 
514
  </div>
515
- <div className="flex items-center justify-between p-2 bg-gray-800/50 rounded-lg">
516
- <span>JSON Extractor</span>
517
- <Badge variant="success" size="sm">ready</Badge>
 
 
 
 
 
 
 
 
518
  </div>
519
- </div>
 
 
520
  </Accordion>
 
 
 
 
 
 
 
 
 
 
 
 
 
 
521
  </div>
522
 
523
  {/* Center Content */}
524
  <div className="flex-1 flex flex-col overflow-hidden">
525
- {/* Stats Header */}
526
  <div className="flex-shrink-0 p-3 bg-gray-800/30 border-b border-gray-700/50">
527
  <div className="flex items-center justify-between">
528
- <div className="flex items-center gap-4">
529
- {/* Episodes */}
530
  <div className="flex items-center gap-2">
531
  <div className="p-1.5 bg-emerald-500/20 rounded">
532
  <Layers className="w-4 h-4 text-emerald-400" />
533
  </div>
534
  <div>
535
- <p className="text-lg font-bold text-white">{episodeStats.total}</p>
536
  <p className="text-[10px] text-gray-500">Episodes</p>
537
  </div>
538
- <button
539
- onClick={() => setShowStatsPopup('episodes')}
540
- className="p-1 text-gray-500 hover:text-gray-300"
541
- >
542
- <MoreHorizontal className="w-4 h-4" />
543
- </button>
544
  </div>
545
 
546
- {/* Steps */}
547
  <div className="flex items-center gap-2">
548
  <div className="p-1.5 bg-cyan-500/20 rounded">
549
  <Target className="w-4 h-4 text-cyan-400" />
550
  </div>
551
  <div>
552
- <p className="text-lg font-bold text-white">{stepStats.total}</p>
553
  <p className="text-[10px] text-gray-500">Steps</p>
554
  </div>
555
- <button
556
- onClick={() => setShowStatsPopup('steps')}
557
- className="p-1 text-gray-500 hover:text-gray-300"
558
- >
559
- <MoreHorizontal className="w-4 h-4" />
560
- </button>
561
  </div>
562
 
563
- {/* Reward */}
564
  <div className="flex items-center gap-2">
565
  <div className="p-1.5 bg-purple-500/20 rounded">
566
  <TrendingUp className="w-4 h-4 text-purple-400" />
567
  </div>
568
  <div>
569
- <p className="text-lg font-bold text-white">{rewardStats.avg.toFixed(1)}</p>
570
  <p className="text-[10px] text-gray-500">Avg Reward</p>
571
  </div>
572
- <button
573
- onClick={() => setShowStatsPopup('reward')}
574
- className="p-1 text-gray-500 hover:text-gray-300"
575
- >
576
- <MoreHorizontal className="w-4 h-4" />
577
- </button>
578
  </div>
579
  </div>
580
 
581
- {/* Time & Status */}
582
  <div className="flex items-center gap-4">
583
  <div className="text-right">
584
  <p className="text-sm font-mono text-white">{new Date().toLocaleTimeString()}</p>
585
  <p className="text-[10px] text-gray-500">Current Time</p>
586
  </div>
587
- <div className={classNames('px-3 py-1.5 rounded-lg flex items-center gap-2', isRunning ? 'bg-emerald-500/20' : 'bg-gray-700/50')}>
588
- <div className={classNames('w-2 h-2 rounded-full', isRunning ? 'bg-emerald-400 animate-pulse' : 'bg-gray-500')} />
589
- <span className={classNames('text-sm font-medium', isRunning ? 'text-emerald-400' : 'text-gray-400')}>
590
- {isRunning ? 'Running' : 'Idle'}
591
- </span>
592
- </div>
 
 
 
 
 
 
 
 
 
 
 
 
 
 
593
  </div>
594
  </div>
595
  </div>
@@ -606,8 +1083,8 @@ export const Dashboard: React.FC = () => {
606
  <span className="text-sm font-medium text-white">Current Action</span>
607
  </div>
608
  <div className="p-3 bg-gray-800/50 rounded-lg">
609
- <p className="text-sm text-gray-300">Navigating to: {taskInput.url}</p>
610
- <p className="text-xs text-gray-500 mt-1">Agent: Scraper | Step: 1/10</p>
611
  </div>
612
  </div>
613
 
@@ -620,11 +1097,11 @@ export const Dashboard: React.FC = () => {
620
  <div className="p-3 bg-gray-800/50 rounded-lg min-h-[200px]">
621
  <pre className="text-xs text-gray-400 font-mono whitespace-pre-wrap">
622
  {`{
623
- "url": "${taskInput.url || 'N/A'}",
624
- "title": "Loading...",
 
625
  "elements": [],
626
- "links": [],
627
- "text_content": "..."
628
  }`}
629
  </pre>
630
  </div>
@@ -637,7 +1114,7 @@ export const Dashboard: React.FC = () => {
637
  </div>
638
  <h3 className="text-lg font-medium text-gray-300 mb-2">Ready to Start</h3>
639
  <p className="text-sm text-gray-500 max-w-md">
640
- Enter a URL and instruction above, configure your agents and plugins, then click Start to begin scraping.
641
  </p>
642
  </div>
643
  )}
@@ -645,272 +1122,151 @@ export const Dashboard: React.FC = () => {
645
  </div>
646
 
647
  {/* Logs Terminal */}
648
- <div className="flex-shrink-0 h-36 bg-gray-900 border-t border-gray-700/50">
649
  <div className="flex items-center justify-between px-3 py-1.5 border-b border-gray-800">
650
  <div className="flex items-center gap-2">
651
  <Terminal className="w-4 h-4 text-gray-500" />
652
  <span className="text-xs font-medium text-gray-400">Logs</span>
653
  </div>
654
- <button
655
- onClick={() => setLogs([])}
656
- className="text-xs text-gray-500 hover:text-gray-300"
657
- >
658
  Clear
659
  </button>
660
  </div>
661
  <div className="h-[calc(100%-28px)] overflow-y-auto p-2 font-mono text-xs">
662
- {logs.map((log) => (
663
- <div key={log.id} className="flex items-start gap-2 py-0.5">
664
- <span className="text-gray-600">[{formatTime(log.timestamp)}]</span>
665
- <span className={getLogLevelColor(log.level)}>[{log.level.toUpperCase()}]</span>
666
- {log.source && <span className="text-purple-400">[{log.source}]</span>}
667
- <span className="text-gray-300">{log.message}</span>
668
- </div>
669
- ))}
 
 
 
 
670
  </div>
671
  </div>
672
  </div>
673
 
674
- {/* Right Sidebar - Memory & Data */}
675
- <div className="w-72 flex-shrink-0 bg-gray-800/30 border-l border-gray-700/50 overflow-y-auto p-3 space-y-3">
676
- {/* Memory Stats */}
677
  <div className="bg-gray-900/50 border border-gray-700/50 rounded-lg p-3">
678
- <div className="flex items-center gap-2 mb-3">
679
- <Database className="w-4 h-4 text-pink-400" />
680
- <span className="text-sm font-medium text-white">Memory</span>
681
- </div>
682
- <div className="grid grid-cols-2 gap-2">
683
- <div className="p-2 bg-gray-800/50 rounded text-center">
684
- <p className="text-lg font-bold text-emerald-400">{memoryData?.working?.count || 0}</p>
685
- <p className="text-[10px] text-gray-500">Working</p>
686
- </div>
687
- <div className="p-2 bg-gray-800/50 rounded text-center">
688
- <p className="text-lg font-bold text-cyan-400">{memoryData?.episodic?.count || 0}</p>
689
- <p className="text-[10px] text-gray-500">Episodic</p>
690
  </div>
691
- <div className="p-2 bg-gray-800/50 rounded text-center">
692
- <p className="text-lg font-bold text-purple-400">{memoryData?.semantic?.count || 0}</p>
693
- <p className="text-[10px] text-gray-500">Semantic</p>
 
 
 
 
 
 
 
 
694
  </div>
695
- <div className="p-2 bg-gray-800/50 rounded text-center">
696
- <p className="text-lg font-bold text-amber-400">{memoryData?.procedural?.count || 0}</p>
697
- <p className="text-[10px] text-gray-500">Procedural</p>
698
  </div>
699
  </div>
700
  </div>
701
 
702
- {/* Extracted Data */}
703
  <div className="bg-gray-900/50 border border-gray-700/50 rounded-lg p-3">
704
  <div className="flex items-center justify-between mb-3">
705
  <div className="flex items-center gap-2">
706
- <FileText className="w-4 h-4 text-cyan-400" />
707
- <span className="text-sm font-medium text-white">Extracted Data</span>
708
  </div>
709
- <Badge variant="neutral" size="sm">0 items</Badge>
 
 
710
  </div>
711
- <div className="text-center py-6 text-gray-500 text-xs">
712
- No data extracted yet.<br />Start an episode to begin.
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
713
  </div>
 
 
 
 
 
 
714
  </div>
715
 
716
- {/* Recent Actions */}
717
  <div className="bg-gray-900/50 border border-gray-700/50 rounded-lg p-3">
718
- <div className="flex items-center gap-2 mb-3">
719
- <List className="w-4 h-4 text-amber-400" />
720
- <span className="text-sm font-medium text-white">Recent Actions</span>
 
 
 
721
  </div>
722
- <div className="space-y-2">
723
- {isRunning ? (
724
- <>
725
- <div className="flex items-center gap-2 p-2 bg-emerald-500/10 rounded">
726
- <Check className="w-3 h-3 text-emerald-400" />
727
- <span className="text-xs text-gray-300">Navigate to URL</span>
728
- </div>
729
- <div className="flex items-center gap-2 p-2 bg-gray-800/50 rounded">
730
- <Activity className="w-3 h-3 text-cyan-400 animate-pulse" />
731
- <span className="text-xs text-gray-300">Loading page...</span>
 
 
 
732
  </div>
733
- </>
734
- ) : (
735
- <div className="text-center py-4 text-gray-500 text-xs">
736
- No recent actions
737
- </div>
738
- )}
739
- </div>
 
 
 
740
  </div>
741
 
742
- {/* System Info */}
743
  <div className="bg-gray-900/50 border border-gray-700/50 rounded-lg p-3">
744
- <div className="flex items-center gap-2 mb-3">
745
- <Settings className="w-4 h-4 text-gray-400" />
746
- <span className="text-sm font-medium text-white">System</span>
747
- </div>
748
- <div className="space-y-2 text-xs">
749
- <div className="flex items-center justify-between">
750
- <span className="text-gray-500">Status</span>
751
- <Badge variant={health?.status === 'ok' ? 'success' : 'error'} size="sm">
752
- {health?.status === 'ok' ? 'Online' : 'Offline'}
753
- </Badge>
754
- </div>
755
- <div className="flex items-center justify-between">
756
- <span className="text-gray-500">Model</span>
757
- <span className="text-gray-300">{taskInput.selectedModel.split('/')[1]}</span>
758
- </div>
759
- <div className="flex items-center justify-between">
760
- <span className="text-gray-500">Version</span>
761
- <span className="text-gray-300">{health?.version || 'v0.1.0'}</span>
762
  </div>
 
 
 
 
763
  </div>
764
  </div>
765
  </div>
766
  </div>
767
 
768
  {/* Popups */}
769
- {/* Model Selection Popup */}
770
- <Popup title="Select Model" isOpen={showModelPopup} onClose={() => setShowModelPopup(false)}>
771
- <div className="space-y-2">
772
- {availableModels.map((model: { provider: string; model: string; name: string }) => (
773
- <button
774
- key={`${model.provider}/${model.model}`}
775
- onClick={() => {
776
- setTaskInput((p) => ({ ...p, selectedModel: `${model.provider}/${model.model}` }));
777
- setShowModelPopup(false);
778
- }}
779
- className={classNames(
780
- 'w-full flex items-center justify-between p-3 rounded-lg transition-colors text-left',
781
- taskInput.selectedModel === `${model.provider}/${model.model}`
782
- ? 'bg-emerald-500/20 border border-emerald-500/30'
783
- : 'bg-gray-900/50 hover:bg-gray-800'
784
- )}
785
- >
786
- <div>
787
- <p className="text-sm font-medium text-white">{model.name}</p>
788
- <p className="text-xs text-gray-500">{model.provider}</p>
789
- </div>
790
- {taskInput.selectedModel === `${model.provider}/${model.model}` && (
791
- <Check className="w-5 h-5 text-emerald-400" />
792
- )}
793
- </button>
794
- ))}
795
- </div>
796
- </Popup>
797
-
798
- {/* Agent Selection Popup */}
799
- <Popup title="Select Agents" isOpen={showAgentPopup} onClose={() => setShowAgentPopup(false)}>
800
- <div className="space-y-2">
801
- {availableAgents.map((agent: { id: string; name: string; description: string }) => {
802
- const isSelected = taskInput.selectedAgents.includes(agent.id);
803
- return (
804
- <button
805
- key={agent.id}
806
- onClick={() => {
807
- setTaskInput((p) => ({
808
- ...p,
809
- selectedAgents: isSelected
810
- ? p.selectedAgents.filter((a) => a !== agent.id)
811
- : [...p.selectedAgents, agent.id],
812
- }));
813
- }}
814
- className={classNames(
815
- 'w-full flex items-center justify-between p-3 rounded-lg transition-colors text-left',
816
- isSelected ? 'bg-purple-500/20 border border-purple-500/30' : 'bg-gray-900/50 hover:bg-gray-800'
817
- )}
818
- >
819
- <div>
820
- <p className="text-sm font-medium text-white">{agent.name}</p>
821
- <p className="text-xs text-gray-500">{agent.description}</p>
822
- </div>
823
- {isSelected && <Check className="w-5 h-5 text-purple-400" />}
824
- </button>
825
- );
826
- })}
827
- </div>
828
- </Popup>
829
-
830
- {/* Plugin Selection Popup */}
831
- <Popup title="Enable Plugins" isOpen={showPluginPopup} onClose={() => setShowPluginPopup(false)}>
832
- <div className="space-y-4">
833
- {Object.entries(pluginCategories).map(([category, plugins]) => (
834
- <div key={category}>
835
- <h4 className="text-xs font-medium text-gray-400 uppercase mb-2">{category}</h4>
836
- <div className="space-y-1">
837
- {plugins.map((plugin) => {
838
- const isEnabled = taskInput.enabledPlugins.includes(plugin.id);
839
- return (
840
- <button
841
- key={plugin.id}
842
- onClick={() => {
843
- setTaskInput((p) => ({
844
- ...p,
845
- enabledPlugins: isEnabled
846
- ? p.enabledPlugins.filter((a) => a !== plugin.id)
847
- : [...p.enabledPlugins, plugin.id],
848
- }));
849
- }}
850
- className={classNames(
851
- 'w-full flex items-center justify-between p-2 rounded-lg transition-colors text-left',
852
- isEnabled ? 'bg-amber-500/20 border border-amber-500/30' : 'bg-gray-900/50 hover:bg-gray-800'
853
- )}
854
- >
855
- <span className="text-sm text-white">{plugin.name}</span>
856
- {isEnabled && <Check className="w-4 h-4 text-amber-400" />}
857
- </button>
858
- );
859
- })}
860
- </div>
861
- </div>
862
- ))}
863
- </div>
864
- </Popup>
865
-
866
- {/* Task Type Popup */}
867
- <Popup title="Select Task Complexity" isOpen={showTaskTypePopup} onClose={() => setShowTaskTypePopup(false)}>
868
- <div className="space-y-2">
869
- {taskTypes.map((type) => (
870
- <button
871
- key={type.id}
872
- onClick={() => {
873
- setTaskInput((p) => ({ ...p, taskType: type.id as 'low' | 'medium' | 'high' }));
874
- setShowTaskTypePopup(false);
875
- }}
876
- className={classNames(
877
- 'w-full flex items-center justify-between p-3 rounded-lg transition-colors text-left',
878
- taskInput.taskType === type.id
879
- ? `bg-${type.color}-500/20 border border-${type.color}-500/30`
880
- : 'bg-gray-900/50 hover:bg-gray-800'
881
- )}
882
- >
883
- <div>
884
- <p className="text-sm font-medium text-white">{type.name}</p>
885
- <p className="text-xs text-gray-500">{type.description}</p>
886
- </div>
887
- {taskInput.taskType === type.id && (
888
- <Check className={`w-5 h-5 text-${type.color}-400`} />
889
- )}
890
- </button>
891
- ))}
892
- </div>
893
- </Popup>
894
-
895
- {/* Stats Popups */}
896
- <StatsPopup
897
- isOpen={showStatsPopup === 'episodes'}
898
- onClose={() => setShowStatsPopup(null)}
899
- stats={episodeStats}
900
- title="Episode Statistics"
901
- />
902
- <StatsPopup
903
- isOpen={showStatsPopup === 'steps'}
904
- onClose={() => setShowStatsPopup(null)}
905
- stats={stepStats}
906
- title="Step Statistics"
907
- />
908
- <StatsPopup
909
- isOpen={showStatsPopup === 'reward'}
910
- onClose={() => setShowStatsPopup(null)}
911
- stats={rewardStats}
912
- title="Reward Statistics"
913
- />
914
  </div>
915
  );
916
  };
 
4
  Activity,
5
  Zap,
6
  Target,
 
7
  TrendingUp,
8
  Database,
9
  Cpu,
 
12
  Pause,
13
  ChevronDown,
14
  ChevronRight,
 
15
  Terminal,
 
16
  Wrench,
17
  Plug,
18
  Eye,
 
21
  Check,
22
  Layers,
23
  FileText,
24
+ Plus,
25
+ Info,
26
+ Link,
27
+ MessageSquare,
28
+ Image,
29
+ FolderOpen,
30
+ Trash2,
31
+ AlertCircle,
32
  } from 'lucide-react';
33
  import { Badge } from '@/components/ui/Badge';
34
  import { classNames } from '@/utils/helpers';
 
36
 
37
  // Types
38
  interface TaskInput {
39
+ urls: string[];
40
  instruction: string;
41
+ outputInstruction: string;
42
  taskType: 'low' | 'medium' | 'high';
43
  selectedModel: string;
44
+ selectedVisionModel: string;
45
  selectedAgents: string[];
46
  enabledPlugins: string[];
47
  }
 
54
  source?: string;
55
  }
56
 
57
+ interface Asset {
58
+ id: string;
59
+ type: 'url' | 'image' | 'file' | 'data';
60
+ name: string;
61
+ source: 'user' | 'ai';
62
+ content: string;
63
+ timestamp: string;
64
+ }
65
+
66
+ interface MemoryEntry {
67
+ id: string;
68
+ type: 'short_term' | 'working' | 'long_term' | 'shared';
69
+ content: string;
70
+ timestamp: string;
71
  }
72
 
73
+ interface PluginInfo {
74
  id: string;
75
  name: string;
76
  description: string;
77
+ category: string;
78
+ installed: boolean;
79
+ }
80
+
81
+ interface AgentInfo {
82
+ type: string;
83
+ name: string;
84
+ description: string;
85
  }
86
 
87
+ interface ModelInfo {
88
  provider: string;
89
  model: string;
90
  name: string;
91
+ description?: string;
92
  }
93
 
94
+ // View type
95
+ type ViewType = 'input' | 'dashboard';
96
+
97
+ // Info Popup Component
98
+ const InfoPopup: React.FC<{
99
+ isOpen: boolean;
100
+ onClose: () => void;
101
+ title: string;
102
+ description: string;
103
+ details?: Record<string, string>;
104
+ }> = ({ isOpen, onClose, title, description, details }) => {
105
+ if (!isOpen) return null;
106
+ return (
107
+ <div className="fixed inset-0 z-[60] flex items-center justify-center bg-black/70 backdrop-blur-sm">
108
+ <div className="bg-gray-800 border border-gray-600 rounded-xl shadow-2xl w-full max-w-md p-5">
109
+ <div className="flex items-start justify-between mb-4">
110
+ <div className="flex items-center gap-3">
111
+ <div className="p-2 bg-cyan-500/20 rounded-lg">
112
+ <Info className="w-5 h-5 text-cyan-400" />
113
+ </div>
114
+ <h3 className="font-semibold text-white text-lg">{title}</h3>
115
+ </div>
116
+ <button onClick={onClose} className="p-1 text-gray-400 hover:text-white transition-colors">
117
+ <X className="w-5 h-5" />
118
+ </button>
119
+ </div>
120
+ <p className="text-gray-300 text-sm mb-4">{description}</p>
121
+ {details && (
122
+ <div className="space-y-2 pt-3 border-t border-gray-700">
123
+ {Object.entries(details).map(([key, value]) => (
124
+ <div key={key} className="flex justify-between text-sm">
125
+ <span className="text-gray-500">{key}</span>
126
+ <span className="text-gray-300">{value}</span>
127
+ </div>
128
+ ))}
129
+ </div>
130
+ )}
131
+ </div>
132
+ </div>
133
+ );
134
+ };
135
+
136
  // Popup Components
137
  interface PopupProps {
138
  title: string;
139
  isOpen: boolean;
140
  onClose: () => void;
141
  children: React.ReactNode;
142
+ size?: 'sm' | 'md' | 'lg';
143
  }
144
 
145
+ const Popup: React.FC<PopupProps> = ({ title, isOpen, onClose, children, size = 'md' }) => {
146
  if (!isOpen) return null;
147
+ const sizeClasses = {
148
+ sm: 'max-w-sm',
149
+ md: 'max-w-lg',
150
+ lg: 'max-w-2xl',
151
+ };
152
  return (
153
  <div className="fixed inset-0 z-50 flex items-center justify-center bg-black/60 backdrop-blur-sm">
154
+ <div className={`bg-gray-800 border border-gray-700 rounded-xl shadow-2xl w-full ${sizeClasses[size]} max-h-[80vh] overflow-hidden`}>
155
  <div className="flex items-center justify-between px-4 py-3 border-b border-gray-700">
156
  <h3 className="font-semibold text-white">{title}</h3>
157
  <button onClick={onClose} className="p-1 text-gray-400 hover:text-white transition-colors">
158
  <X className="w-5 h-5" />
159
  </button>
160
  </div>
161
+ <div className="p-4 overflow-y-auto max-h-[65vh]">{children}</div>
162
  </div>
163
  </div>
164
  );
165
  };
166
 
167
+ // Accordion Component for sidebar
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
168
  interface AccordionProps {
169
  title: string;
170
  icon: React.ElementType;
 
181
  <div className="border border-gray-700/50 rounded-lg overflow-hidden">
182
  <button
183
  onClick={() => setIsOpen(!isOpen)}
184
+ className="w-full flex items-center justify-between px-3 py-2 bg-gray-800/50 hover:bg-gray-800 transition-colors"
185
  >
186
  <div className="flex items-center gap-2">
187
  <Icon className={`w-4 h-4 ${color}`} />
188
+ <span className="text-xs font-medium text-white">{title}</span>
189
+ {badge !== undefined && Number(badge) > 0 && (
190
  <Badge variant="neutral" size="sm">{badge}</Badge>
191
  )}
192
  </div>
193
+ {isOpen ? <ChevronDown className="w-3 h-3 text-gray-400" /> : <ChevronRight className="w-3 h-3 text-gray-400" />}
194
  </button>
195
+ {isOpen && <div className="p-2 bg-gray-900/30 border-t border-gray-700/50 space-y-1">{children}</div>}
196
  </div>
197
  );
198
  };
199
 
200
  // Main Dashboard Component
201
  export const Dashboard: React.FC = () => {
202
+ // View state - 'input' or 'dashboard'
203
+ const [currentView, setCurrentView] = useState<ViewType>('input');
204
+
205
+ // Task input state
206
  const [taskInput, setTaskInput] = useState<TaskInput>({
207
+ urls: [],
208
  instruction: '',
209
+ outputInstruction: '',
210
  taskType: 'medium',
211
  selectedModel: 'groq/gpt-oss-120b',
212
+ selectedVisionModel: '',
213
+ selectedAgents: [],
214
+ enabledPlugins: [],
215
  });
216
+
217
+ // URL input for adding
218
+ const [newUrl, setNewUrl] = useState('');
219
+
220
+ // Logs
221
+ const [logs, setLogs] = useState<LogEntry[]>([]);
222
+
223
+ // Running state
224
  const [isRunning, setIsRunning] = useState(false);
225
+
226
+ // Assets
227
+ const [assets, setAssets] = useState<Asset[]>([]);
228
+
229
+ // Memories
230
+ const [memories, setMemories] = useState<MemoryEntry[]>([]);
231
+ const [newMemory, setNewMemory] = useState('');
232
+
233
+ // Popup states
234
  const [showModelPopup, setShowModelPopup] = useState(false);
235
+ const [showVisionPopup, setShowVisionPopup] = useState(false);
236
  const [showAgentPopup, setShowAgentPopup] = useState(false);
237
  const [showPluginPopup, setShowPluginPopup] = useState(false);
238
  const [showTaskTypePopup, setShowTaskTypePopup] = useState(false);
239
+ const [showMemoriesPopup, setShowMemoriesPopup] = useState(false);
240
+ const [showAssetsPopup, setShowAssetsPopup] = useState(false);
241
+
242
+ // Info popup
243
+ const [infoPopup, setInfoPopup] = useState<{ isOpen: boolean; title: string; description: string; details?: Record<string, string> }>({
244
+ isOpen: false,
245
+ title: '',
246
+ description: '',
247
+ });
248
+
249
+ // Episode stats - session-based, start at 0
250
+ const [stats, setStats] = useState({ episodes: 0, steps: 0, totalReward: 0, avgReward: 0 });
251
 
252
  // API Queries
253
  const { data: health } = useQuery({
 
259
  const { data: agentsData } = useQuery({
260
  queryKey: ['agents'],
261
  queryFn: async () => {
262
+ const res = await fetch('/api/agents/list');
263
+ if (!res.ok) return { agent_types: [] };
264
  return res.json();
265
  },
266
  });
267
 
268
+ const { data: pluginsData } = useQuery({
269
  queryKey: ['plugins'],
270
  queryFn: async () => {
271
+ const res = await fetch('/api/plugins');
272
+ if (!res.ok) return { plugins: {} };
273
  return res.json();
274
  },
275
  });
 
278
  queryKey: ['memory-stats'],
279
  queryFn: async () => {
280
  const res = await fetch('/api/memory/stats/overview');
281
+ if (!res.ok) return { total_count: 0 };
282
  return res.json();
283
  },
284
  refetchInterval: 3000,
 
287
  const { data: settingsData } = useQuery({
288
  queryKey: ['client-settings'],
289
  queryFn: async () => {
290
+ const res = await fetch('/api/settings');
291
+ if (!res.ok) return { available_models: [], api_keys_configured: {} };
292
  return res.json();
293
  },
294
  });
295
 
296
+ // Get installed plugins only
297
+ const getInstalledPlugins = () => {
298
+ if (!pluginsData?.plugins) return { mcps: [], skills: [], apis: [], processors: [] };
299
+ const result: Record<string, PluginInfo[]> = {};
300
+ for (const [category, plugins] of Object.entries(pluginsData.plugins)) {
301
+ result[category] = (plugins as PluginInfo[]).filter(p => p.installed);
302
+ }
303
+ return result;
304
+ };
305
+
306
+ const installedPlugins = getInstalledPlugins();
307
+
308
+ // Get agents
309
+ const agents: AgentInfo[] = agentsData?.agent_types || [];
310
+
311
+ // Get models grouped by provider
312
+ const modelsByProvider = (): Record<string, ModelInfo[]> => {
313
+ const models = settingsData?.available_models || [];
314
+ const grouped: Record<string, ModelInfo[]> = {};
315
+ models.forEach((m: ModelInfo) => {
316
+ if (!grouped[m.provider]) grouped[m.provider] = [];
317
+ grouped[m.provider].push(m);
318
+ });
319
+ return grouped;
320
+ };
321
+
322
+ // Vision models
323
+ const visionModels: ModelInfo[] = [
324
+ { provider: 'openai', model: 'gpt-4-vision-preview', name: 'GPT-4 Vision', description: 'OpenAI vision model' },
325
+ { provider: 'google', model: 'gemini-pro-vision', name: 'Gemini Pro Vision', description: 'Google vision model' },
326
+ { provider: 'anthropic', model: 'claude-3-opus-vision', name: 'Claude 3 Vision', description: 'Anthropic vision model' },
327
  ];
328
 
329
+ // Task types
330
+ const taskTypes = [
331
+ { id: 'low', name: 'Low', description: 'Simple single-page extraction', color: 'emerald', icon: '🟢' },
332
+ { id: 'medium', name: 'Medium', description: 'Multi-page navigation', color: 'amber', icon: '🟡' },
333
+ { id: 'high', name: 'High', description: 'Complex interactive tasks', color: 'red', icon: '🔴' },
 
334
  ];
335
 
336
+ // Add URL to list
337
+ const handleAddUrl = () => {
338
+ if (newUrl.trim() && !taskInput.urls.includes(newUrl.trim())) {
339
+ const url = newUrl.trim();
340
+ setTaskInput(p => ({ ...p, urls: [...p.urls, url] }));
341
+ // Also add to assets
342
+ setAssets(prev => [...prev, {
343
+ id: Date.now().toString(),
344
+ type: 'url',
345
+ name: url,
346
+ source: 'user',
347
+ content: url,
348
+ timestamp: new Date().toISOString(),
349
+ }]);
350
+ setNewUrl('');
351
+ }
 
 
 
 
 
352
  };
353
 
354
+ // Remove URL
355
+ const handleRemoveUrl = (url: string) => {
356
+ setTaskInput(p => ({ ...p, urls: p.urls.filter(u => u !== url) }));
357
+ setAssets(prev => prev.filter(a => a.content !== url));
358
+ };
359
+
360
+ // Add memory
361
+ const handleAddMemory = () => {
362
+ if (newMemory.trim()) {
363
+ setMemories(prev => [...prev, {
364
+ id: Date.now().toString(),
365
+ type: 'working',
366
+ content: newMemory.trim(),
367
+ timestamp: new Date().toISOString(),
368
+ }]);
369
+ setNewMemory('');
370
+ }
371
+ };
372
 
373
+ // Start task
374
  const handleStart = () => {
375
+ if (taskInput.urls.length === 0 && !taskInput.instruction) return;
376
+
377
  setIsRunning(true);
378
+ setCurrentView('dashboard');
379
+
380
+ // Add initial log
381
+ setLogs(prev => [...prev, {
382
  id: Date.now().toString(),
383
  timestamp: new Date().toISOString(),
384
  level: 'info',
385
+ message: `Starting episode with ${taskInput.urls.length} URLs`,
386
+ source: 'system',
387
+ }]);
388
+
389
+ // Update stats
390
+ setStats(prev => ({ ...prev, episodes: prev.episodes + 1 }));
391
  };
392
 
393
+ // Stop task
394
  const handleStop = () => {
395
  setIsRunning(false);
396
+ setLogs(prev => [...prev, {
397
  id: Date.now().toString(),
398
  timestamp: new Date().toISOString(),
399
  level: 'warn',
400
  message: 'Episode stopped by user',
401
  source: 'system',
402
+ }]);
 
403
  };
404
 
405
+ // Format time
406
  const formatTime = (isoString: string) => {
407
  return new Date(isoString).toLocaleTimeString('en-US', { hour12: false });
408
  };
409
 
410
+ // Log level colors
411
  const getLogLevelColor = (level: LogEntry['level']) => {
412
+ const colors = { info: 'text-cyan-400', warn: 'text-amber-400', error: 'text-red-400', debug: 'text-gray-400' };
 
 
 
 
 
413
  return colors[level];
414
  };
415
 
416
+ // Check system status
417
+ const isSystemOnline = health?.status === 'healthy';
418
+
419
+ // Show info popup
420
+ const showInfo = (title: string, description: string, details?: Record<string, string>) => {
421
+ setInfoPopup({ isOpen: true, title, description, details });
422
+ };
423
+
424
+ // ========== INPUT VIEW ==========
425
+ if (currentView === 'input') {
426
+ return (
427
+ <div className="h-[calc(100vh-64px)] flex flex-col bg-gray-900">
428
+ {/* System Status Banner */}
429
+ {!isSystemOnline && (
430
+ <div className="flex-shrink-0 px-4 py-2 bg-red-500/20 border-b border-red-500/30 flex items-center justify-center gap-2">
431
+ <AlertCircle className="w-4 h-4 text-red-400" />
432
+ <span className="text-sm text-red-400">System is offline. Please check your connection.</span>
433
  </div>
434
+ )}
435
+
436
+ {/* Main Content - ChatGPT-like interface */}
437
+ <div className="flex-1 flex flex-col items-center justify-center p-6 overflow-auto">
438
+ <div className="w-full max-w-3xl space-y-6">
439
+ {/* Header */}
440
+ <div className="text-center mb-8">
441
+ <h1 className="text-3xl font-bold text-white mb-2">ScrapeRL</h1>
442
+ <p className="text-gray-400">Enter your scraping task below</p>
443
+ </div>
444
+
445
+ {/* URLs Section */}
446
+ <div className="bg-gray-800/50 border border-gray-700/50 rounded-xl p-4">
447
+ <div className="flex items-center gap-2 mb-3">
448
+ <Link className="w-4 h-4 text-cyan-400" />
449
+ <span className="text-sm font-medium text-white">Target URLs</span>
450
+ </div>
451
+
452
+ {/* URL Input */}
453
+ <div className="flex gap-2 mb-3">
454
+ <input
455
+ type="url"
456
+ placeholder="https://example.com/page-to-scrape"
457
+ value={newUrl}
458
+ onChange={(e) => setNewUrl(e.target.value)}
459
+ onKeyDown={(e) => e.key === 'Enter' && handleAddUrl()}
460
+ className="flex-1 px-4 py-2.5 bg-gray-900/50 border border-gray-700 rounded-lg text-white placeholder-gray-500 focus:outline-none focus:ring-2 focus:ring-cyan-500/50"
461
+ />
462
+ <button
463
+ onClick={handleAddUrl}
464
+ className="px-4 py-2.5 bg-cyan-500/20 hover:bg-cyan-500/30 border border-cyan-500/30 text-cyan-400 rounded-lg transition-colors"
465
+ >
466
+ <Plus className="w-5 h-5" />
467
+ </button>
468
+ </div>
469
+
470
+ {/* URL List */}
471
+ {taskInput.urls.length > 0 && (
472
+ <div className="space-y-2 max-h-32 overflow-y-auto">
473
+ {taskInput.urls.map((url, idx) => (
474
+ <div key={idx} className="flex items-center justify-between px-3 py-2 bg-gray-900/50 rounded-lg">
475
+ <div className="flex items-center gap-2 flex-1 min-w-0">
476
+ <Globe className="w-4 h-4 text-gray-500 flex-shrink-0" />
477
+ <span className="text-sm text-gray-300 truncate">{url}</span>
478
+ </div>
479
+ <button onClick={() => handleRemoveUrl(url)} className="p-1 text-gray-500 hover:text-red-400">
480
+ <X className="w-4 h-4" />
481
+ </button>
482
+ </div>
483
+ ))}
484
+ </div>
485
+ )}
486
+ </div>
487
+
488
+ {/* Instructions */}
489
+ <div className="bg-gray-800/50 border border-gray-700/50 rounded-xl p-4">
490
+ <div className="flex items-center gap-2 mb-3">
491
+ <MessageSquare className="w-4 h-4 text-purple-400" />
492
+ <span className="text-sm font-medium text-white">Instructions</span>
493
+ </div>
494
+ <textarea
495
+ placeholder="What data do you want to extract? Be specific about the fields and structure..."
496
+ value={taskInput.instruction}
497
+ onChange={(e) => setTaskInput(p => ({ ...p, instruction: e.target.value }))}
498
+ rows={3}
499
+ className="w-full px-4 py-3 bg-gray-900/50 border border-gray-700 rounded-lg text-white placeholder-gray-500 focus:outline-none focus:ring-2 focus:ring-purple-500/50 resize-none"
500
+ />
501
+ </div>
502
+
503
+ {/* Output Instructions */}
504
+ <div className="bg-gray-800/50 border border-gray-700/50 rounded-xl p-4">
505
+ <div className="flex items-center gap-2 mb-3">
506
+ <FileText className="w-4 h-4 text-emerald-400" />
507
+ <span className="text-sm font-medium text-white">Output Format</span>
508
+ </div>
509
+ <textarea
510
+ placeholder="How should the output be formatted? (e.g., JSON with fields: name, price, description)"
511
+ value={taskInput.outputInstruction}
512
+ onChange={(e) => setTaskInput(p => ({ ...p, outputInstruction: e.target.value }))}
513
+ rows={2}
514
+ className="w-full px-4 py-3 bg-gray-900/50 border border-gray-700 rounded-lg text-white placeholder-gray-500 focus:outline-none focus:ring-2 focus:ring-emerald-500/50 resize-none"
515
+ />
516
+ </div>
517
 
518
+ {/* Configuration Options */}
519
+ <div className="flex flex-wrap items-center justify-center gap-3">
520
+ {/* Model */}
521
+ <button
522
+ onClick={() => setShowModelPopup(true)}
523
+ className="px-4 py-2 bg-cyan-500/10 hover:bg-cyan-500/20 border border-cyan-500/30 text-cyan-400 rounded-lg text-sm font-medium transition-colors flex items-center gap-2"
524
+ >
525
+ <Cpu className="w-4 h-4" />
526
+ {taskInput.selectedModel ? taskInput.selectedModel.split('/')[1] : 'Model'}
527
+ </button>
528
+
529
+ {/* Vision */}
530
+ <button
531
+ onClick={() => setShowVisionPopup(true)}
532
+ className={classNames(
533
+ 'px-4 py-2 border rounded-lg text-sm font-medium transition-colors flex items-center gap-2',
534
+ taskInput.selectedVisionModel
535
+ ? 'bg-pink-500/10 border-pink-500/30 text-pink-400'
536
+ : 'bg-gray-700/50 border-gray-600 text-gray-400 hover:border-pink-500/30 hover:text-pink-400'
537
+ )}
538
+ >
539
+ <Eye className="w-4 h-4" />
540
+ {taskInput.selectedVisionModel ? 'Vision ✓' : 'Vision'}
541
+ </button>
542
+
543
+ {/* Agents */}
544
+ <button
545
+ onClick={() => setShowAgentPopup(true)}
546
+ className="px-4 py-2 bg-purple-500/10 hover:bg-purple-500/20 border border-purple-500/30 text-purple-400 rounded-lg text-sm font-medium transition-colors flex items-center gap-2"
547
+ >
548
+ <Bot className="w-4 h-4" />
549
+ Agents {taskInput.selectedAgents.length > 0 && `(${taskInput.selectedAgents.length})`}
550
+ </button>
551
+
552
+ {/* Plugins */}
553
+ <button
554
+ onClick={() => setShowPluginPopup(true)}
555
+ className="px-4 py-2 bg-amber-500/10 hover:bg-amber-500/20 border border-amber-500/30 text-amber-400 rounded-lg text-sm font-medium transition-colors flex items-center gap-2"
556
+ >
557
+ <Plug className="w-4 h-4" />
558
+ Plugins {taskInput.enabledPlugins.length > 0 && `(${taskInput.enabledPlugins.length})`}
559
+ </button>
560
+
561
+ {/* Task Type */}
562
+ <button
563
+ onClick={() => setShowTaskTypePopup(true)}
564
+ className={classNames(
565
+ 'px-4 py-2 border rounded-lg text-sm font-medium transition-colors flex items-center gap-2',
566
+ taskInput.taskType === 'low' && 'bg-emerald-500/10 border-emerald-500/30 text-emerald-400',
567
+ taskInput.taskType === 'medium' && 'bg-amber-500/10 border-amber-500/30 text-amber-400',
568
+ taskInput.taskType === 'high' && 'bg-red-500/10 border-red-500/30 text-red-400'
569
+ )}
570
+ >
571
+ <Target className="w-4 h-4" />
572
+ {taskTypes.find(t => t.id === taskInput.taskType)?.icon} {taskInput.taskType.charAt(0).toUpperCase() + taskInput.taskType.slice(1)}
573
+ </button>
574
+ </div>
575
+
576
+ {/* Start Button */}
577
+ <div className="flex justify-center pt-4">
578
+ <button
579
+ onClick={handleStart}
580
+ disabled={taskInput.urls.length === 0 || !isSystemOnline}
581
+ className="px-8 py-3 bg-emerald-500 hover:bg-emerald-600 disabled:bg-gray-600 disabled:cursor-not-allowed text-white rounded-xl font-medium transition-colors flex items-center gap-3 shadow-lg shadow-emerald-500/20"
582
+ >
583
+ <Play className="w-5 h-5" />
584
+ Start Scraping
585
+ </button>
586
+ </div>
587
  </div>
588
+ </div>
589
 
590
+ {/* Popups */}
591
+ {renderPopups()}
592
+ </div>
593
+ );
594
+ }
595
+
596
+ // ========== DASHBOARD VIEW ==========
597
+ // Helper function to render popups (used in both views)
598
+ function renderPopups() {
599
+ return (
600
+ <>
601
+ {/* Model Selection Popup */}
602
+ <Popup title="Select Model" isOpen={showModelPopup} onClose={() => setShowModelPopup(false)} size="lg">
603
+ <div className="space-y-4">
604
+ {Object.entries(modelsByProvider()).map(([provider, models]) => (
605
+ <div key={provider}>
606
+ <h4 className="text-xs font-semibold text-gray-400 uppercase mb-2 flex items-center gap-2">
607
+ <div className="w-2 h-2 rounded-full bg-cyan-400"></div>
608
+ {provider}
609
+ </h4>
610
+ <div className="space-y-1 pl-4">
611
+ {models.map((model) => (
612
+ <button
613
+ key={`${model.provider}/${model.model}`}
614
+ onClick={() => {
615
+ setTaskInput(p => ({ ...p, selectedModel: `${model.provider}/${model.model}` }));
616
+ setShowModelPopup(false);
617
+ }}
618
+ className={classNames(
619
+ 'w-full flex items-center justify-between p-3 rounded-lg transition-colors text-left',
620
+ taskInput.selectedModel === `${model.provider}/${model.model}`
621
+ ? 'bg-cyan-500/20 border border-cyan-500/30'
622
+ : 'bg-gray-900/50 hover:bg-gray-800'
623
+ )}
624
+ >
625
+ <div>
626
+ <p className="text-sm font-medium text-white">{model.name}</p>
627
+ <p className="text-xs text-gray-500">{model.description || model.model}</p>
628
+ </div>
629
+ {taskInput.selectedModel === `${model.provider}/${model.model}` && (
630
+ <Check className="w-5 h-5 text-cyan-400" />
631
+ )}
632
+ </button>
633
+ ))}
634
+ </div>
635
+ </div>
636
+ ))}
637
+ </div>
638
+ </Popup>
639
+
640
+ {/* Vision Model Popup */}
641
+ <Popup title="Select Vision Model" isOpen={showVisionPopup} onClose={() => setShowVisionPopup(false)}>
642
+ <div className="space-y-2">
643
  <button
644
+ onClick={() => {
645
+ setTaskInput(p => ({ ...p, selectedVisionModel: '' }));
646
+ setShowVisionPopup(false);
647
+ }}
648
  className={classNames(
649
+ 'w-full flex items-center justify-between p-3 rounded-lg transition-colors text-left',
650
+ !taskInput.selectedVisionModel ? 'bg-gray-700/50 border border-gray-600' : 'bg-gray-900/50 hover:bg-gray-800'
 
 
651
  )}
652
  >
653
+ <span className="text-sm text-gray-400">None (No vision)</span>
654
+ {!taskInput.selectedVisionModel && <Check className="w-5 h-5 text-gray-400" />}
655
  </button>
656
+ {visionModels.map((model) => (
657
+ <div key={model.model} className="flex items-center gap-2">
658
+ <button
659
+ onClick={() => {
660
+ setTaskInput(p => ({ ...p, selectedVisionModel: model.model }));
661
+ setShowVisionPopup(false);
662
+ }}
663
+ className={classNames(
664
+ 'flex-1 flex items-center justify-between p-3 rounded-lg transition-colors text-left',
665
+ taskInput.selectedVisionModel === model.model
666
+ ? 'bg-pink-500/20 border border-pink-500/30'
667
+ : 'bg-gray-900/50 hover:bg-gray-800'
668
+ )}
669
+ >
670
+ <div>
671
+ <p className="text-sm font-medium text-white">{model.name}</p>
672
+ <p className="text-xs text-gray-500">{model.provider}</p>
673
+ </div>
674
+ {taskInput.selectedVisionModel === model.model && <Check className="w-5 h-5 text-pink-400" />}
675
+ </button>
676
+ <button
677
+ onClick={() => showInfo(model.name, model.description || 'Vision model for image understanding', { Provider: model.provider, Model: model.model })}
678
+ className="p-2 text-gray-500 hover:text-gray-300"
679
+ >
680
+ <Info className="w-4 h-4" />
681
+ </button>
682
+ </div>
683
+ ))}
684
  </div>
685
+ </Popup>
686
 
687
+ {/* Agent Selection Popup */}
688
+ <Popup title="Select Agents" isOpen={showAgentPopup} onClose={() => setShowAgentPopup(false)}>
689
+ <div className="space-y-2">
690
+ {agents.map((agent) => {
691
+ const isSelected = taskInput.selectedAgents.includes(agent.type);
692
+ return (
693
+ <div key={agent.type} className="flex items-center gap-2">
694
+ <button
695
+ onClick={() => {
696
+ setTaskInput(p => ({
697
+ ...p,
698
+ selectedAgents: isSelected
699
+ ? p.selectedAgents.filter(a => a !== agent.type)
700
+ : [...p.selectedAgents, agent.type],
701
+ }));
702
+ }}
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
703
  className={classNames(
704
+ 'flex-1 flex items-center justify-between p-3 rounded-lg transition-colors text-left',
705
+ isSelected ? 'bg-purple-500/20 border border-purple-500/30' : 'bg-gray-900/50 hover:bg-gray-800'
706
  )}
707
  >
708
+ <div>
709
+ <p className="text-sm font-medium text-white">{agent.name}</p>
710
+ <p className="text-xs text-gray-500">{agent.description}</p>
711
  </div>
712
+ {isSelected && <Check className="w-5 h-5 text-purple-400" />}
713
+ </button>
714
+ <button
715
+ onClick={() => showInfo(agent.name, agent.description, { Type: agent.type })}
716
+ className="p-2 text-gray-500 hover:text-gray-300"
717
+ >
718
+ <Info className="w-4 h-4" />
719
+ </button>
720
+ </div>
721
+ );
722
+ })}
723
+ </div>
724
+ </Popup>
725
 
726
+ {/* Plugin Selection Popup */}
727
+ <Popup title="Enable Plugins" isOpen={showPluginPopup} onClose={() => setShowPluginPopup(false)} size="lg">
728
+ <div className="space-y-4">
729
+ {Object.entries(installedPlugins).map(([category, plugins]) => {
730
+ if (plugins.length === 0) return null;
731
+ return (
732
+ <div key={category}>
733
+ <h4 className="text-xs font-semibold text-gray-400 uppercase mb-2">{category}</h4>
734
+ <div className="space-y-1">
735
+ {plugins.map((plugin: PluginInfo) => {
736
+ const isEnabled = taskInput.enabledPlugins.includes(plugin.id);
737
+ return (
738
+ <div key={plugin.id} className="flex items-center gap-2">
739
+ <button
740
+ onClick={() => {
741
+ setTaskInput(p => ({
742
+ ...p,
743
+ enabledPlugins: isEnabled
744
+ ? p.enabledPlugins.filter(a => a !== plugin.id)
745
+ : [...p.enabledPlugins, plugin.id],
746
+ }));
747
+ }}
748
+ className={classNames(
749
+ 'flex-1 flex items-center justify-between p-2 rounded-lg transition-colors text-left',
750
+ isEnabled ? 'bg-amber-500/20 border border-amber-500/30' : 'bg-gray-900/50 hover:bg-gray-800'
751
+ )}
752
+ >
753
+ <span className="text-sm text-white">{plugin.name}</span>
754
+ {isEnabled && <Check className="w-4 h-4 text-amber-400" />}
755
+ </button>
756
+ <button
757
+ onClick={() => showInfo(plugin.name, plugin.description, { Category: plugin.category, ID: plugin.id })}
758
+ className="p-2 text-gray-500 hover:text-gray-300"
759
+ >
760
+ <Info className="w-4 h-4" />
761
+ </button>
762
+ </div>
763
+ );
764
+ })}
765
+ </div>
766
  </div>
767
+ );
768
+ })}
769
+ </div>
770
+ </Popup>
771
 
772
+ {/* Task Type Popup */}
773
+ <Popup title="Select Task Complexity" isOpen={showTaskTypePopup} onClose={() => setShowTaskTypePopup(false)}>
774
+ <div className="space-y-2">
775
+ {taskTypes.map((type) => (
776
+ <button
777
+ key={type.id}
778
+ onClick={() => {
779
+ setTaskInput(p => ({ ...p, taskType: type.id as 'low' | 'medium' | 'high' }));
780
+ setShowTaskTypePopup(false);
781
+ }}
782
+ className={classNames(
783
+ 'w-full flex items-center justify-between p-3 rounded-lg transition-colors text-left',
784
+ taskInput.taskType === type.id
785
+ ? type.id === 'low' ? 'bg-emerald-500/20 border border-emerald-500/30'
786
+ : type.id === 'medium' ? 'bg-amber-500/20 border border-amber-500/30'
787
+ : 'bg-red-500/20 border border-red-500/30'
788
+ : 'bg-gray-900/50 hover:bg-gray-800'
789
+ )}
790
+ >
791
+ <div className="flex items-center gap-3">
792
+ <span className="text-xl">{type.icon}</span>
793
+ <div>
794
+ <p className="text-sm font-medium text-white">{type.name}</p>
795
+ <p className="text-xs text-gray-500">{type.description}</p>
796
+ </div>
797
  </div>
798
+ {taskInput.taskType === type.id && (
799
+ <Check className={classNames(
800
+ 'w-5 h-5',
801
+ type.id === 'low' ? 'text-emerald-400' : type.id === 'medium' ? 'text-amber-400' : 'text-red-400'
802
+ )} />
803
+ )}
804
+ </button>
805
+ ))}
806
+ </div>
807
+ </Popup>
808
+
809
+ {/* Memories Popup */}
810
+ <Popup title="Memories" isOpen={showMemoriesPopup} onClose={() => setShowMemoriesPopup(false)} size="lg">
811
+ <div className="space-y-3">
812
+ {/* Add Memory */}
813
+ <div className="flex gap-2">
814
+ <input
815
+ type="text"
816
+ placeholder="Add a new memory..."
817
+ value={newMemory}
818
+ onChange={(e) => setNewMemory(e.target.value)}
819
+ onKeyDown={(e) => e.key === 'Enter' && handleAddMemory()}
820
+ className="flex-1 px-3 py-2 bg-gray-900/50 border border-gray-700 rounded-lg text-white text-sm"
821
+ />
822
+ <button onClick={handleAddMemory} className="px-3 py-2 bg-purple-500/20 border border-purple-500/30 text-purple-400 rounded-lg">
823
+ <Plus className="w-4 h-4" />
824
+ </button>
825
  </div>
826
+
827
+ {/* Memory List */}
828
+ <div className="space-y-2 max-h-80 overflow-y-auto">
829
+ {memories.length === 0 ? (
830
+ <div className="text-center py-8 text-gray-500 text-sm">No memories yet</div>
831
+ ) : (
832
+ memories.map((mem) => (
833
+ <div key={mem.id} className="p-3 bg-gray-900/50 rounded-lg">
834
+ <div className="flex items-start justify-between">
835
+ <p className="text-sm text-gray-300 flex-1">{mem.content}</p>
836
+ <button
837
+ onClick={() => setMemories(prev => prev.filter(m => m.id !== mem.id))}
838
+ className="p-1 text-gray-500 hover:text-red-400 ml-2"
839
+ >
840
+ <Trash2 className="w-3 h-3" />
841
+ </button>
842
+ </div>
843
+ <div className="flex items-center gap-2 mt-2">
844
+ <Badge variant="neutral" size="sm">{mem.type}</Badge>
845
+ <span className="text-[10px] text-gray-500">{formatTime(mem.timestamp)}</span>
846
+ </div>
847
+ </div>
848
+ ))
849
+ )}
850
+ </div>
851
+ </div>
852
+ </Popup>
853
 
854
+ {/* Assets Popup */}
855
+ <Popup title="Assets" isOpen={showAssetsPopup} onClose={() => setShowAssetsPopup(false)} size="lg">
856
+ <div className="space-y-3">
857
+ <div className="space-y-2 max-h-80 overflow-y-auto">
858
+ {assets.length === 0 ? (
859
+ <div className="text-center py-8 text-gray-500 text-sm">No assets yet. URLs and fetched data will appear here.</div>
860
+ ) : (
861
+ assets.map((asset) => (
862
+ <div key={asset.id} className="p-3 bg-gray-900/50 rounded-lg">
863
+ <div className="flex items-center justify-between">
864
+ <div className="flex items-center gap-2 flex-1 min-w-0">
865
+ {asset.type === 'url' && <Link className="w-4 h-4 text-cyan-400 flex-shrink-0" />}
866
+ {asset.type === 'image' && <Image className="w-4 h-4 text-pink-400 flex-shrink-0" />}
867
+ {asset.type === 'file' && <FileText className="w-4 h-4 text-amber-400 flex-shrink-0" />}
868
+ {asset.type === 'data' && <Database className="w-4 h-4 text-emerald-400 flex-shrink-0" />}
869
+ <span className="text-sm text-gray-300 truncate">{asset.name}</span>
870
+ </div>
871
+ <div className="flex items-center gap-2">
872
+ <Badge variant={asset.source === 'ai' ? 'info' : 'neutral'} size="sm">{asset.source}</Badge>
873
+ <button
874
+ onClick={() => setAssets(prev => prev.filter(a => a.id !== asset.id))}
875
+ className="p-1 text-gray-500 hover:text-red-400"
876
+ >
877
+ <Trash2 className="w-3 h-3" />
878
+ </button>
879
+ </div>
880
+ </div>
881
+ </div>
882
+ ))
883
+ )}
884
  </div>
885
+ </div>
886
+ </Popup>
887
+
888
+ {/* Info Popup */}
889
+ <InfoPopup
890
+ isOpen={infoPopup.isOpen}
891
+ onClose={() => setInfoPopup({ ...infoPopup, isOpen: false })}
892
+ title={infoPopup.title}
893
+ description={infoPopup.description}
894
+ details={infoPopup.details}
895
+ />
896
+ </>
897
+ );
898
+ }
899
+
900
+ return (
901
+ <div className="h-[calc(100vh-64px)] flex flex-col">
902
+ {/* Main 3-Column Layout */}
903
+ <div className="flex-1 flex overflow-hidden">
904
+ {/* Left Sidebar - Active Components */}
905
+ <div className="w-56 flex-shrink-0 bg-gray-800/30 border-r border-gray-700/50 overflow-y-auto p-2 space-y-2">
906
+ {/* Back to Input */}
907
+ <button
908
+ onClick={() => setCurrentView('input')}
909
+ className="w-full flex items-center gap-2 px-3 py-2 bg-gray-700/50 hover:bg-gray-700 rounded-lg text-sm text-gray-300 transition-colors"
910
+ >
911
+ <ChevronRight className="w-4 h-4 rotate-180" />
912
+ New Task
913
+ </button>
914
+
915
+ {/* Agents */}
916
+ <Accordion title="Agents" icon={Bot} badge={taskInput.selectedAgents.length} color="text-purple-400" defaultOpen>
917
+ {taskInput.selectedAgents.length === 0 ? (
918
+ <p className="text-xs text-gray-500 p-2">No agents selected</p>
919
+ ) : (
920
+ taskInput.selectedAgents.map((agentId) => {
921
+ const agent = agents.find(a => a.type === agentId);
922
+ return (
923
+ <div key={agentId} className="flex items-center justify-between p-2 bg-purple-500/10 border border-purple-500/30 rounded-lg">
924
+ <div className="flex items-center gap-2">
925
+ <div className="w-2 h-2 rounded-full bg-emerald-400"></div>
926
+ <span className="text-xs text-white">{agent?.name || agentId}</span>
927
+ </div>
928
+ <button onClick={() => showInfo(agent?.name || agentId, agent?.description || '', { Type: agentId })} className="text-gray-500 hover:text-gray-300">
929
+ <Info className="w-3 h-3" />
930
+ </button>
931
+ </div>
932
+ );
933
+ })
934
+ )}
935
  </Accordion>
936
 
937
+ {/* MCPs */}
938
+ <Accordion title="MCPs" icon={Wrench} badge={taskInput.enabledPlugins.filter(p => installedPlugins.mcps?.some((m: PluginInfo) => m.id === p)).length} color="text-amber-400">
939
+ {installedPlugins.mcps?.filter((p: PluginInfo) => taskInput.enabledPlugins.includes(p.id)).map((plugin: PluginInfo) => (
940
+ <div key={plugin.id} className="flex items-center justify-between p-2 bg-amber-500/10 border border-amber-500/30 rounded-lg">
941
+ <span className="text-xs text-white">{plugin.name}</span>
942
+ <button onClick={() => showInfo(plugin.name, plugin.description)} className="text-gray-500 hover:text-gray-300">
943
+ <Info className="w-3 h-3" />
944
+ </button>
945
+ </div>
946
+ ))}
947
+ {!installedPlugins.mcps?.some((p: PluginInfo) => taskInput.enabledPlugins.includes(p.id)) && (
948
+ <p className="text-xs text-gray-500 p-2">No MCPs enabled</p>
949
+ )}
 
 
 
 
 
950
  </Accordion>
951
 
952
+ {/* Skills */}
953
+ <Accordion title="Skills" icon={Zap} badge={taskInput.enabledPlugins.filter(p => installedPlugins.skills?.some((s: PluginInfo) => s.id === p)).length} color="text-cyan-400">
954
+ {installedPlugins.skills?.filter((p: PluginInfo) => taskInput.enabledPlugins.includes(p.id)).map((plugin: PluginInfo) => (
955
+ <div key={plugin.id} className="flex items-center justify-between p-2 bg-cyan-500/10 border border-cyan-500/30 rounded-lg">
956
+ <span className="text-xs text-white">{plugin.name}</span>
957
+ <button onClick={() => showInfo(plugin.name, plugin.description)} className="text-gray-500 hover:text-gray-300">
958
+ <Info className="w-3 h-3" />
959
+ </button>
960
  </div>
961
+ ))}
962
+ {!installedPlugins.skills?.some((p: PluginInfo) => taskInput.enabledPlugins.includes(p.id)) && (
963
+ <p className="text-xs text-gray-500 p-2">No skills enabled</p>
964
+ )}
965
+ </Accordion>
966
+
967
+ {/* APIs */}
968
+ <Accordion title="APIs" icon={Plug} badge={taskInput.enabledPlugins.filter(p => installedPlugins.apis?.some((a: PluginInfo) => a.id === p)).length} color="text-emerald-400">
969
+ {installedPlugins.apis?.filter((p: PluginInfo) => taskInput.enabledPlugins.includes(p.id)).map((plugin: PluginInfo) => (
970
+ <div key={plugin.id} className="flex items-center justify-between p-2 bg-emerald-500/10 border border-emerald-500/30 rounded-lg">
971
+ <span className="text-xs text-white">{plugin.name}</span>
972
+ <button onClick={() => showInfo(plugin.name, plugin.description)} className="text-gray-500 hover:text-gray-300">
973
+ <Info className="w-3 h-3" />
974
+ </button>
975
  </div>
976
+ ))}
977
+ {!installedPlugins.apis?.some((p: PluginInfo) => taskInput.enabledPlugins.includes(p.id)) && (
978
+ <p className="text-xs text-gray-500 p-2">No APIs enabled</p>
979
+ )}
980
+ </Accordion>
981
+
982
+ {/* Vision */}
983
+ <Accordion title="Vision" icon={Eye} badge={taskInput.selectedVisionModel ? 1 : 0} color="text-pink-400">
984
+ {taskInput.selectedVisionModel ? (
985
+ <div className="p-2 bg-pink-500/10 border border-pink-500/30 rounded-lg">
986
+ <span className="text-xs text-white">{taskInput.selectedVisionModel}</span>
987
  </div>
988
+ ) : (
989
+ <p className="text-xs text-gray-500 p-2">No vision model</p>
990
+ )}
991
  </Accordion>
992
+
993
+ {/* System Status */}
994
+ <div className="mt-4 p-3 bg-gray-900/50 border border-gray-700/50 rounded-lg">
995
+ <div className="flex items-center justify-between mb-2">
996
+ <span className="text-xs text-gray-400">Status</span>
997
+ <Badge variant={isSystemOnline ? 'success' : 'error'} size="sm">
998
+ {isSystemOnline ? 'Online' : 'Offline'}
999
+ </Badge>
1000
+ </div>
1001
+ <div className="flex items-center justify-between">
1002
+ <span className="text-xs text-gray-400">Model</span>
1003
+ <span className="text-xs text-gray-300">{taskInput.selectedModel.split('/')[1]}</span>
1004
+ </div>
1005
+ </div>
1006
  </div>
1007
 
1008
  {/* Center Content */}
1009
  <div className="flex-1 flex flex-col overflow-hidden">
1010
+ {/* Stats Header - Session-based, start at 0 */}
1011
  <div className="flex-shrink-0 p-3 bg-gray-800/30 border-b border-gray-700/50">
1012
  <div className="flex items-center justify-between">
1013
+ <div className="flex items-center gap-6">
 
1014
  <div className="flex items-center gap-2">
1015
  <div className="p-1.5 bg-emerald-500/20 rounded">
1016
  <Layers className="w-4 h-4 text-emerald-400" />
1017
  </div>
1018
  <div>
1019
+ <p className="text-lg font-bold text-white">{stats.episodes}</p>
1020
  <p className="text-[10px] text-gray-500">Episodes</p>
1021
  </div>
 
 
 
 
 
 
1022
  </div>
1023
 
 
1024
  <div className="flex items-center gap-2">
1025
  <div className="p-1.5 bg-cyan-500/20 rounded">
1026
  <Target className="w-4 h-4 text-cyan-400" />
1027
  </div>
1028
  <div>
1029
+ <p className="text-lg font-bold text-white">{stats.steps}</p>
1030
  <p className="text-[10px] text-gray-500">Steps</p>
1031
  </div>
 
 
 
 
 
 
1032
  </div>
1033
 
 
1034
  <div className="flex items-center gap-2">
1035
  <div className="p-1.5 bg-purple-500/20 rounded">
1036
  <TrendingUp className="w-4 h-4 text-purple-400" />
1037
  </div>
1038
  <div>
1039
+ <p className="text-lg font-bold text-white">{stats.avgReward.toFixed(1)}</p>
1040
  <p className="text-[10px] text-gray-500">Avg Reward</p>
1041
  </div>
 
 
 
 
 
 
1042
  </div>
1043
  </div>
1044
 
 
1045
  <div className="flex items-center gap-4">
1046
  <div className="text-right">
1047
  <p className="text-sm font-mono text-white">{new Date().toLocaleTimeString()}</p>
1048
  <p className="text-[10px] text-gray-500">Current Time</p>
1049
  </div>
1050
+
1051
+ {/* Control Buttons */}
1052
+ {isRunning ? (
1053
+ <button
1054
+ onClick={handleStop}
1055
+ className="px-4 py-2 bg-red-500 hover:bg-red-600 text-white rounded-lg font-medium transition-colors flex items-center gap-2"
1056
+ >
1057
+ <Pause className="w-4 h-4" />
1058
+ Stop
1059
+ </button>
1060
+ ) : (
1061
+ <button
1062
+ onClick={handleStart}
1063
+ disabled={taskInput.urls.length === 0}
1064
+ className="px-4 py-2 bg-emerald-500 hover:bg-emerald-600 disabled:bg-gray-600 text-white rounded-lg font-medium transition-colors flex items-center gap-2"
1065
+ >
1066
+ <Play className="w-4 h-4" />
1067
+ Start
1068
+ </button>
1069
+ )}
1070
  </div>
1071
  </div>
1072
  </div>
 
1083
  <span className="text-sm font-medium text-white">Current Action</span>
1084
  </div>
1085
  <div className="p-3 bg-gray-800/50 rounded-lg">
1086
+ <p className="text-sm text-gray-300">Processing URLs...</p>
1087
+ <p className="text-xs text-gray-500 mt-1">Agent: {taskInput.selectedAgents[0] || 'None'} | URLs: {taskInput.urls.length}</p>
1088
  </div>
1089
  </div>
1090
 
 
1097
  <div className="p-3 bg-gray-800/50 rounded-lg min-h-[200px]">
1098
  <pre className="text-xs text-gray-400 font-mono whitespace-pre-wrap">
1099
  {`{
1100
+ "urls": ${JSON.stringify(taskInput.urls.slice(0, 3))},
1101
+ "instruction": "${taskInput.instruction.slice(0, 50)}...",
1102
+ "status": "processing",
1103
  "elements": [],
1104
+ "extracted_data": []
 
1105
  }`}
1106
  </pre>
1107
  </div>
 
1114
  </div>
1115
  <h3 className="text-lg font-medium text-gray-300 mb-2">Ready to Start</h3>
1116
  <p className="text-sm text-gray-500 max-w-md">
1117
+ {taskInput.urls.length} URLs loaded. Click Start to begin scraping.
1118
  </p>
1119
  </div>
1120
  )}
 
1122
  </div>
1123
 
1124
  {/* Logs Terminal */}
1125
+ <div className="flex-shrink-0 h-32 bg-gray-900 border-t border-gray-700/50">
1126
  <div className="flex items-center justify-between px-3 py-1.5 border-b border-gray-800">
1127
  <div className="flex items-center gap-2">
1128
  <Terminal className="w-4 h-4 text-gray-500" />
1129
  <span className="text-xs font-medium text-gray-400">Logs</span>
1130
  </div>
1131
+ <button onClick={() => setLogs([])} className="text-xs text-gray-500 hover:text-gray-300">
 
 
 
1132
  Clear
1133
  </button>
1134
  </div>
1135
  <div className="h-[calc(100%-28px)] overflow-y-auto p-2 font-mono text-xs">
1136
+ {logs.length === 0 ? (
1137
+ <p className="text-gray-600 p-2">No logs yet...</p>
1138
+ ) : (
1139
+ logs.map((log) => (
1140
+ <div key={log.id} className="flex items-start gap-2 py-0.5">
1141
+ <span className="text-gray-600">[{formatTime(log.timestamp)}]</span>
1142
+ <span className={getLogLevelColor(log.level)}>[{log.level.toUpperCase()}]</span>
1143
+ {log.source && <span className="text-purple-400">[{log.source}]</span>}
1144
+ <span className="text-gray-300">{log.message}</span>
1145
+ </div>
1146
+ ))
1147
+ )}
1148
  </div>
1149
  </div>
1150
  </div>
1151
 
1152
+ {/* Right Sidebar */}
1153
+ <div className="w-64 flex-shrink-0 bg-gray-800/30 border-l border-gray-700/50 overflow-y-auto p-3 space-y-3">
1154
+ {/* Input Summary */}
1155
  <div className="bg-gray-900/50 border border-gray-700/50 rounded-lg p-3">
1156
+ <div className="flex items-center justify-between mb-3">
1157
+ <div className="flex items-center gap-2">
1158
+ <FileText className="w-4 h-4 text-cyan-400" />
1159
+ <span className="text-sm font-medium text-white">Input</span>
 
 
 
 
 
 
 
 
1160
  </div>
1161
+ <button
1162
+ onClick={() => setCurrentView('input')}
1163
+ className="text-xs text-cyan-400 hover:text-cyan-300"
1164
+ >
1165
+ Edit
1166
+ </button>
1167
+ </div>
1168
+ <div className="space-y-2 text-xs">
1169
+ <div>
1170
+ <p className="text-gray-500">URLs ({taskInput.urls.length})</p>
1171
+ <p className="text-gray-300 truncate">{taskInput.urls[0] || 'None'}</p>
1172
  </div>
1173
+ <div>
1174
+ <p className="text-gray-500">Instruction</p>
1175
+ <p className="text-gray-300 truncate">{taskInput.instruction || 'None'}</p>
1176
  </div>
1177
  </div>
1178
  </div>
1179
 
1180
+ {/* Memories */}
1181
  <div className="bg-gray-900/50 border border-gray-700/50 rounded-lg p-3">
1182
  <div className="flex items-center justify-between mb-3">
1183
  <div className="flex items-center gap-2">
1184
+ <Database className="w-4 h-4 text-purple-400" />
1185
+ <span className="text-sm font-medium text-white">Memories</span>
1186
  </div>
1187
+ <button onClick={() => setShowMemoriesPopup(true)} className="text-xs text-purple-400 hover:text-purple-300">
1188
+ View All
1189
+ </button>
1190
  </div>
1191
+ <div className="grid grid-cols-2 gap-2 text-center">
1192
+ <div className="p-2 bg-gray-800/50 rounded">
1193
+ <p className="text-lg font-bold text-emerald-400">{memoryData?.short_term_count || 0}</p>
1194
+ <p className="text-[10px] text-gray-500">Short</p>
1195
+ </div>
1196
+ <div className="p-2 bg-gray-800/50 rounded">
1197
+ <p className="text-lg font-bold text-cyan-400">{memoryData?.working_count || 0}</p>
1198
+ <p className="text-[10px] text-gray-500">Working</p>
1199
+ </div>
1200
+ <div className="p-2 bg-gray-800/50 rounded">
1201
+ <p className="text-lg font-bold text-purple-400">{memoryData?.long_term_count || 0}</p>
1202
+ <p className="text-[10px] text-gray-500">Long</p>
1203
+ </div>
1204
+ <div className="p-2 bg-gray-800/50 rounded">
1205
+ <p className="text-lg font-bold text-amber-400">{memoryData?.shared_count || 0}</p>
1206
+ <p className="text-[10px] text-gray-500">Shared</p>
1207
+ </div>
1208
  </div>
1209
+ <button
1210
+ onClick={() => setShowMemoriesPopup(true)}
1211
+ className="w-full mt-2 px-2 py-1.5 bg-purple-500/10 hover:bg-purple-500/20 border border-purple-500/30 text-purple-400 rounded text-xs flex items-center justify-center gap-1"
1212
+ >
1213
+ <Plus className="w-3 h-3" /> Add Memory
1214
+ </button>
1215
  </div>
1216
 
1217
+ {/* Assets */}
1218
  <div className="bg-gray-900/50 border border-gray-700/50 rounded-lg p-3">
1219
+ <div className="flex items-center justify-between mb-3">
1220
+ <div className="flex items-center gap-2">
1221
+ <FolderOpen className="w-4 h-4 text-amber-400" />
1222
+ <span className="text-sm font-medium text-white">Assets</span>
1223
+ </div>
1224
+ <Badge variant="neutral" size="sm">{assets.length}</Badge>
1225
  </div>
1226
+
1227
+ {assets.length === 0 ? (
1228
+ <p className="text-center py-4 text-gray-500 text-xs">No assets yet</p>
1229
+ ) : (
1230
+ <div className="space-y-1.5 max-h-40 overflow-y-auto">
1231
+ {assets.slice(0, 5).map((asset) => (
1232
+ <div key={asset.id} className="flex items-center justify-between p-2 bg-gray-800/50 rounded text-xs">
1233
+ <div className="flex items-center gap-2 min-w-0">
1234
+ {asset.type === 'url' && <Link className="w-3 h-3 text-cyan-400 flex-shrink-0" />}
1235
+ {asset.type === 'data' && <Database className="w-3 h-3 text-emerald-400 flex-shrink-0" />}
1236
+ <span className="text-gray-300 truncate">{asset.name.slice(0, 30)}</span>
1237
+ </div>
1238
+ <Badge variant={asset.source === 'ai' ? 'info' : 'neutral'} size="sm">{asset.source}</Badge>
1239
  </div>
1240
+ ))}
1241
+ </div>
1242
+ )}
1243
+
1244
+ <button
1245
+ onClick={() => setShowAssetsPopup(true)}
1246
+ className="w-full mt-2 px-2 py-1.5 bg-amber-500/10 hover:bg-amber-500/20 border border-amber-500/30 text-amber-400 rounded text-xs"
1247
+ >
1248
+ View All Assets
1249
+ </button>
1250
  </div>
1251
 
1252
+ {/* Extracted Data */}
1253
  <div className="bg-gray-900/50 border border-gray-700/50 rounded-lg p-3">
1254
+ <div className="flex items-center justify-between mb-3">
1255
+ <div className="flex items-center gap-2">
1256
+ <FileText className="w-4 h-4 text-emerald-400" />
1257
+ <span className="text-sm font-medium text-white">Extracted Data</span>
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1258
  </div>
1259
+ <Badge variant="neutral" size="sm">0 items</Badge>
1260
+ </div>
1261
+ <div className="text-center py-4 text-gray-500 text-xs">
1262
+ No data extracted yet.
1263
  </div>
1264
  </div>
1265
  </div>
1266
  </div>
1267
 
1268
  {/* Popups */}
1269
+ {renderPopups()}
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1270
  </div>
1271
  );
1272
  };