NeerajCodz commited on
Commit
039e1ea
·
1 Parent(s): 5e3d245

feat: add static file serving and improve UI/UX design

Browse files

- Add static file serving in FastAPI for frontend assets
- Add SPA routing support for React Router
- Redesign App.tsx with modern gradient navbar and footer
- Improve Dashboard with stat cards and cleaner layout
- Enhance Settings page with 2-column standalone design
- Redesign PluginsPage with category colors and better cards
- Fix TypeScript lint errors (unused imports)

backend/app/main.py CHANGED
@@ -2,11 +2,14 @@
2
 
3
  import logging
4
  from contextlib import asynccontextmanager
 
5
  from typing import AsyncGenerator
6
 
7
  import uvicorn
8
- from fastapi import FastAPI
9
  from fastapi.middleware.cors import CORSMiddleware
 
 
10
 
11
  from app.api.routes import agents, episode, health, memory, plugins, tasks, tools
12
  from app.api.routes import settings as settings_routes
@@ -119,6 +122,37 @@ def create_app() -> FastAPI:
119
  app.include_router(settings_routes.router, prefix=api_prefix, tags=["Settings"])
120
  app.include_router(plugins.router, prefix=api_prefix, tags=["Plugins"])
121
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
122
  return app
123
 
124
 
 
2
 
3
  import logging
4
  from contextlib import asynccontextmanager
5
+ from pathlib import Path
6
  from typing import AsyncGenerator
7
 
8
  import uvicorn
9
+ from fastapi import FastAPI, Request
10
  from fastapi.middleware.cors import CORSMiddleware
11
+ from fastapi.responses import FileResponse, HTMLResponse
12
+ from fastapi.staticfiles import StaticFiles
13
 
14
  from app.api.routes import agents, episode, health, memory, plugins, tasks, tools
15
  from app.api.routes import settings as settings_routes
 
122
  app.include_router(settings_routes.router, prefix=api_prefix, tags=["Settings"])
123
  app.include_router(plugins.router, prefix=api_prefix, tags=["Plugins"])
124
 
125
+ # Serve static files (frontend build)
126
+ static_dir = Path(__file__).parent.parent / "static"
127
+ if static_dir.exists():
128
+ app.mount("/assets", StaticFiles(directory=static_dir / "assets"), name="assets")
129
+
130
+ @app.get("/", response_class=HTMLResponse)
131
+ async def serve_spa():
132
+ """Serve the main SPA index.html."""
133
+ index_file = static_dir / "index.html"
134
+ if index_file.exists():
135
+ return FileResponse(index_file)
136
+ return HTMLResponse("<h1>ScrapeRL</h1><p>Frontend not built.</p>")
137
+
138
+ @app.get("/{full_path:path}")
139
+ async def serve_spa_routes(request: Request, full_path: str):
140
+ """Serve SPA routes - return index.html for client-side routing."""
141
+ # Don't serve index.html for API routes
142
+ if full_path.startswith("api/"):
143
+ return {"detail": "Not Found"}
144
+
145
+ # Check if it's a static file
146
+ static_file = static_dir / full_path
147
+ if static_file.exists() and static_file.is_file():
148
+ return FileResponse(static_file)
149
+
150
+ # Return index.html for SPA routing
151
+ index_file = static_dir / "index.html"
152
+ if index_file.exists():
153
+ return FileResponse(index_file)
154
+ return HTMLResponse("<h1>ScrapeRL</h1><p>Frontend not built.</p>")
155
+
156
  return app
157
 
158
 
frontend/src/App.tsx CHANGED
@@ -1,6 +1,6 @@
1
  import { QueryClient, QueryClientProvider } from '@tanstack/react-query';
2
  import { BrowserRouter, Routes, Route, Link, useLocation } from 'react-router-dom';
3
- import { Home, Settings as SettingsIcon, Package } from 'lucide-react';
4
  import Dashboard from './components/Dashboard';
5
  import Settings from './components/Settings';
6
  import PluginsPage from './components/PluginsPage';
@@ -25,29 +25,61 @@ function NavBar() {
25
  ];
26
 
27
  return (
28
- <nav className="bg-dark-900 border-b border-dark-700">
29
- <div className="max-w-7xl mx-auto px-4">
30
- <div className="flex items-center justify-between h-14">
31
- <div className="flex items-center gap-2">
32
- <span className="text-xl font-bold text-primary-400">🕷️ ScrapeRL</span>
 
 
 
 
 
 
 
 
 
 
 
 
 
 
33
  </div>
34
- <div className="flex items-center gap-1">
 
 
35
  {navItems.map(({ path, label, icon: Icon }) => (
36
  <Link
37
  key={path}
38
  to={path}
39
  className={classNames(
40
- 'flex items-center gap-2 px-4 py-2 rounded-lg text-sm font-medium transition-colors',
41
  location.pathname === path
42
- ? 'bg-primary-500/20 text-primary-400'
43
- : 'text-dark-300 hover:text-dark-100 hover:bg-dark-800'
44
  )}
45
  >
46
  <Icon className="w-4 h-4" />
47
- {label}
48
  </Link>
49
  ))}
50
  </div>
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
51
  </div>
52
  </div>
53
  </nav>
@@ -58,15 +90,38 @@ function App() {
58
  return (
59
  <QueryClientProvider client={queryClient}>
60
  <BrowserRouter>
61
- <div className="min-h-screen bg-gray-950 text-gray-100">
62
  <NavBar />
63
- <main className="max-w-7xl mx-auto px-4 py-6">
64
  <Routes>
65
  <Route path="/" element={<Dashboard />} />
66
  <Route path="/plugins" element={<PluginsPage />} />
67
  <Route path="/settings" element={<Settings />} />
68
  </Routes>
69
  </main>
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
70
  </div>
71
  </BrowserRouter>
72
  </QueryClientProvider>
 
1
  import { QueryClient, QueryClientProvider } from '@tanstack/react-query';
2
  import { BrowserRouter, Routes, Route, Link, useLocation } from 'react-router-dom';
3
+ import { Home, Settings as SettingsIcon, Package, Activity, Zap, Brain, Github } from 'lucide-react';
4
  import Dashboard from './components/Dashboard';
5
  import Settings from './components/Settings';
6
  import PluginsPage from './components/PluginsPage';
 
25
  ];
26
 
27
  return (
28
+ <nav className="bg-gradient-to-r from-gray-900 via-gray-900 to-gray-800 border-b border-gray-700/50 shadow-lg">
29
+ <div className="max-w-7xl mx-auto px-4 sm:px-6 lg:px-8">
30
+ <div className="flex items-center justify-between h-16">
31
+ {/* Logo */}
32
+ <div className="flex items-center gap-3">
33
+ <div className="relative">
34
+ <div className="w-10 h-10 bg-gradient-to-br from-emerald-500 via-cyan-500 to-blue-500 rounded-xl flex items-center justify-center shadow-lg shadow-emerald-500/20">
35
+ <Brain className="w-6 h-6 text-white" />
36
+ </div>
37
+ <div className="absolute -top-1 -right-1 w-3 h-3 bg-emerald-400 rounded-full animate-pulse" />
38
+ </div>
39
+ <div className="flex flex-col">
40
+ <span className="text-xl font-bold bg-gradient-to-r from-emerald-400 via-cyan-400 to-blue-400 bg-clip-text text-transparent">
41
+ ScrapeRL
42
+ </span>
43
+ <span className="text-[10px] text-gray-500 font-medium tracking-wider">
44
+ RL-POWERED SCRAPING
45
+ </span>
46
+ </div>
47
  </div>
48
+
49
+ {/* Navigation */}
50
+ <div className="flex items-center gap-2">
51
  {navItems.map(({ path, label, icon: Icon }) => (
52
  <Link
53
  key={path}
54
  to={path}
55
  className={classNames(
56
+ 'flex items-center gap-2 px-4 py-2.5 rounded-xl text-sm font-medium transition-all duration-200',
57
  location.pathname === path
58
+ ? 'bg-gradient-to-r from-emerald-500/20 to-cyan-500/20 text-emerald-400 shadow-lg shadow-emerald-500/10 border border-emerald-500/30'
59
+ : 'text-gray-400 hover:text-gray-200 hover:bg-gray-800/50'
60
  )}
61
  >
62
  <Icon className="w-4 h-4" />
63
+ <span className="hidden sm:inline">{label}</span>
64
  </Link>
65
  ))}
66
  </div>
67
+
68
+ {/* Status Badge */}
69
+ <div className="hidden md:flex items-center gap-3">
70
+ <div className="flex items-center gap-2 px-3 py-1.5 bg-emerald-500/10 border border-emerald-500/30 rounded-full">
71
+ <Zap className="w-3.5 h-3.5 text-emerald-400" />
72
+ <span className="text-xs text-emerald-400 font-medium">Online</span>
73
+ </div>
74
+ <a
75
+ href="https://github.com/NeerajCodz/scrapeRL"
76
+ target="_blank"
77
+ rel="noopener noreferrer"
78
+ className="p-2 text-gray-500 hover:text-gray-300 transition-colors"
79
+ >
80
+ <Github className="w-5 h-5" />
81
+ </a>
82
+ </div>
83
  </div>
84
  </div>
85
  </nav>
 
90
  return (
91
  <QueryClientProvider client={queryClient}>
92
  <BrowserRouter>
93
+ <div className="min-h-screen bg-gradient-to-br from-gray-950 via-gray-900 to-gray-950 text-gray-100">
94
  <NavBar />
95
+ <main className="max-w-7xl mx-auto px-4 sm:px-6 lg:px-8 py-6">
96
  <Routes>
97
  <Route path="/" element={<Dashboard />} />
98
  <Route path="/plugins" element={<PluginsPage />} />
99
  <Route path="/settings" element={<Settings />} />
100
  </Routes>
101
  </main>
102
+
103
+ {/* Footer */}
104
+ <footer className="border-t border-gray-800/50 bg-gray-900/30">
105
+ <div className="max-w-7xl mx-auto px-4 sm:px-6 lg:px-8 py-4">
106
+ <div className="flex flex-col sm:flex-row items-center justify-between gap-2 text-xs text-gray-500">
107
+ <div className="flex items-center gap-2">
108
+ <Activity className="w-3.5 h-3.5 text-emerald-500" />
109
+ <span>ScrapeRL v0.1.0 • Reinforcement Learning Web Scraping</span>
110
+ </div>
111
+ <div className="flex items-center gap-4">
112
+ <span>Built with FastAPI + React</span>
113
+ <a
114
+ href="https://huggingface.co/spaces/NeerajCodz/scrapeRL"
115
+ target="_blank"
116
+ rel="noopener noreferrer"
117
+ className="text-cyan-500 hover:text-cyan-400 transition-colors"
118
+ >
119
+ 🤗 HuggingFace
120
+ </a>
121
+ </div>
122
+ </div>
123
+ </div>
124
+ </footer>
125
  </div>
126
  </BrowserRouter>
127
  </QueryClientProvider>
frontend/src/components/Dashboard.tsx CHANGED
@@ -1,14 +1,16 @@
1
- import React, { useState } from 'react';
2
  import {
3
- LayoutDashboard,
4
- Settings as SettingsIcon,
5
  Activity,
6
- Menu,
7
- X,
8
- Wifi,
9
- WifiOff,
10
- ChevronLeft,
11
- ChevronRight,
 
 
 
 
12
  } from 'lucide-react';
13
  import { EpisodePanel } from './EpisodePanel';
14
  import { AgentView } from './AgentView';
@@ -17,200 +19,172 @@ import { ToolRegistry } from './ToolRegistry';
17
  import { RewardChart } from './RewardChart';
18
  import { ObservationView } from './ObservationView';
19
  import { ActionPanel } from './ActionPanel';
20
- import { Settings } from './Settings';
21
- import { Badge } from '@/components/ui/Badge';
22
- import { useWebSocket } from '@/hooks/useWebSocket';
23
  import { useCurrentEpisode } from '@/hooks/useEpisode';
24
 
25
- type ViewMode = 'dashboard' | 'settings';
 
 
 
 
 
 
26
 
27
- export const Dashboard: React.FC = () => {
28
- const [viewMode, setViewMode] = useState<ViewMode>('dashboard');
29
- const [sidebarCollapsed, setSidebarCollapsed] = useState(false);
30
- const [mobileMenuOpen, setMobileMenuOpen] = useState(false);
 
 
 
31
 
32
- const { isConnected, isConnecting } = useWebSocket('/ws', {
33
- onMessage: (message) => {
34
- console.log('WebSocket message:', message);
35
- },
36
- });
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
37
 
 
38
  const { data: episode } = useCurrentEpisode();
39
 
40
  return (
41
- <div className="min-h-screen bg-dark-900 flex">
42
- {/* Sidebar */}
43
- <aside
44
- className={`fixed lg:relative inset-y-0 left-0 z-40 bg-dark-800 border-r border-dark-700
45
- transition-all duration-300 ${
46
- sidebarCollapsed ? 'w-16' : 'w-64'
47
- } ${mobileMenuOpen ? 'translate-x-0' : '-translate-x-full lg:translate-x-0'}`}
48
- >
49
- {/* Logo */}
50
- <div className="h-16 flex items-center justify-between px-4 border-b border-dark-700">
51
- {!sidebarCollapsed && (
52
- <div className="flex items-center gap-2">
53
- <div className="w-8 h-8 bg-gradient-to-br from-accent-primary to-accent-tertiary rounded-lg flex items-center justify-center">
54
- <Activity className="w-5 h-5 text-white" />
55
- </div>
56
- <span className="font-bold text-lg gradient-text">ScrapeRL</span>
57
  </div>
58
- )}
59
- <button
60
- onClick={() => setSidebarCollapsed(!sidebarCollapsed)}
61
- className="hidden lg:flex p-1.5 text-dark-400 hover:text-dark-200 hover:bg-dark-700 rounded transition-colors"
62
- >
63
- {sidebarCollapsed ? (
64
- <ChevronRight className="w-4 h-4" />
65
- ) : (
66
- <ChevronLeft className="w-4 h-4" />
67
- )}
68
- </button>
69
- <button
70
- onClick={() => setMobileMenuOpen(false)}
71
- className="lg:hidden p-1.5 text-dark-400 hover:text-dark-200"
72
- >
73
- <X className="w-5 h-5" />
74
- </button>
75
  </div>
76
-
77
- {/* Navigation */}
78
- <nav className="p-2 space-y-1">
79
- <button
80
- onClick={() => {
81
- setViewMode('dashboard');
82
- setMobileMenuOpen(false);
83
- }}
84
- className={`w-full flex items-center gap-3 px-3 py-2.5 rounded-lg transition-colors ${
85
- viewMode === 'dashboard'
86
- ? 'bg-accent-primary/10 text-accent-primary'
87
- : 'text-dark-400 hover:text-dark-200 hover:bg-dark-700'
88
- }`}
89
- >
90
- <LayoutDashboard className="w-5 h-5 flex-shrink-0" />
91
- {!sidebarCollapsed && <span>Dashboard</span>}
92
  </button>
93
- <button
94
- onClick={() => {
95
- setViewMode('settings');
96
- setMobileMenuOpen(false);
97
- }}
98
- className={`w-full flex items-center gap-3 px-3 py-2.5 rounded-lg transition-colors ${
99
- viewMode === 'settings'
100
- ? 'bg-accent-primary/10 text-accent-primary'
101
- : 'text-dark-400 hover:text-dark-200 hover:bg-dark-700'
102
- }`}
103
- >
104
- <SettingsIcon className="w-5 h-5 flex-shrink-0" />
105
- {!sidebarCollapsed && <span>Settings</span>}
106
  </button>
107
- </nav>
108
-
109
- {/* Status */}
110
- <div className="absolute bottom-0 left-0 right-0 p-4 border-t border-dark-700">
111
- <div className="flex items-center gap-2">
112
- {isConnected ? (
113
- <Wifi className="w-4 h-4 text-green-400" />
114
- ) : isConnecting ? (
115
- <Wifi className="w-4 h-4 text-yellow-400 animate-pulse" />
116
- ) : (
117
- <WifiOff className="w-4 h-4 text-red-400" />
118
- )}
119
- {!sidebarCollapsed && (
120
- <span className="text-xs text-dark-400">
121
- {isConnected
122
- ? 'Connected'
123
- : isConnecting
124
- ? 'Connecting...'
125
- : 'Disconnected'}
126
- </span>
127
- )}
128
- </div>
129
  </div>
130
- </aside>
131
 
132
- {/* Mobile Overlay */}
133
- {mobileMenuOpen && (
134
- <div
135
- className="fixed inset-0 bg-black/50 z-30 lg:hidden"
136
- onClick={() => setMobileMenuOpen(false)}
 
 
 
 
 
 
 
 
137
  />
138
- )}
 
 
 
 
 
 
 
 
 
 
 
 
 
139
 
140
- {/* Main Content */}
141
- <main className="flex-1 overflow-hidden">
142
- {/* Header */}
143
- <header className="h-16 bg-dark-800 border-b border-dark-700 flex items-center justify-between px-4 lg:px-6">
144
- <div className="flex items-center gap-4">
145
- <button
146
- onClick={() => setMobileMenuOpen(true)}
147
- className="lg:hidden p-2 text-dark-400 hover:text-dark-200"
148
- >
149
- <Menu className="w-5 h-5" />
150
- </button>
151
- <div>
152
- <h1 className="text-lg font-semibold text-dark-100">
153
- {viewMode === 'dashboard' ? 'Dashboard' : 'Settings'}
154
- </h1>
155
- {episode && (
156
- <p className="text-xs text-dark-400">
157
- Episode: {episode.id.slice(0, 8)}...
158
- </p>
159
- )}
160
- </div>
161
  </div>
162
-
163
- <div className="flex items-center gap-3">
164
- {episode && (
165
- <Badge
166
- variant={
167
- episode.status === 'running'
168
- ? 'success'
169
- : episode.status === 'failed'
170
- ? 'error'
171
- : 'neutral'
172
- }
173
- dot
174
- pulse={episode.status === 'running'}
175
- >
176
- {episode.status}
177
- </Badge>
178
- )}
179
  </div>
180
- </header>
181
-
182
- {/* Content Area */}
183
- <div className="h-[calc(100vh-4rem)] overflow-auto p-4 lg:p-6">
184
- {viewMode === 'dashboard' ? (
185
- <div className="grid grid-cols-1 lg:grid-cols-12 gap-4 lg:gap-6">
186
- {/* Left Column */}
187
- <div className="lg:col-span-3 space-y-4 lg:space-y-6">
188
- <EpisodePanel />
189
- <AgentView />
190
- </div>
191
-
192
- {/* Center Column */}
193
- <div className="lg:col-span-6 space-y-4 lg:space-y-6">
194
- <ObservationView />
195
- <div className="grid grid-cols-1 md:grid-cols-2 gap-4 lg:gap-6">
196
- <RewardChart />
197
- <ActionPanel />
198
- </div>
199
- </div>
200
 
201
- {/* Right Column */}
202
- <div className="lg:col-span-3 space-y-4 lg:space-y-6">
203
- <MemoryPanel />
204
- <ToolRegistry />
205
- </div>
 
 
 
 
 
 
 
 
 
 
 
 
206
  </div>
207
- ) : (
208
- <div className="max-w-2xl mx-auto">
209
- <Settings />
 
 
 
 
210
  </div>
211
- )}
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
212
  </div>
213
- </main>
214
  </div>
215
  );
216
  };
 
1
+ import React from 'react';
2
  import {
 
 
3
  Activity,
4
+ Zap,
5
+ Brain,
6
+ Target,
7
+ Clock,
8
+ TrendingUp,
9
+ Database,
10
+ Cpu,
11
+ Globe,
12
+ Play,
13
+ RotateCcw,
14
  } from 'lucide-react';
15
  import { EpisodePanel } from './EpisodePanel';
16
  import { AgentView } from './AgentView';
 
19
  import { RewardChart } from './RewardChart';
20
  import { ObservationView } from './ObservationView';
21
  import { ActionPanel } from './ActionPanel';
 
 
 
22
  import { useCurrentEpisode } from '@/hooks/useEpisode';
23
 
24
+ interface StatCardProps {
25
+ icon: React.ElementType;
26
+ label: string;
27
+ value: string | number;
28
+ change?: string;
29
+ color: 'emerald' | 'cyan' | 'purple' | 'amber';
30
+ }
31
 
32
+ const StatCard: React.FC<StatCardProps> = ({ icon: Icon, label, value, change, color }) => {
33
+ const colorClasses = {
34
+ emerald: 'from-emerald-500/20 to-emerald-600/10 border-emerald-500/30 text-emerald-400',
35
+ cyan: 'from-cyan-500/20 to-cyan-600/10 border-cyan-500/30 text-cyan-400',
36
+ purple: 'from-purple-500/20 to-purple-600/10 border-purple-500/30 text-purple-400',
37
+ amber: 'from-amber-500/20 to-amber-600/10 border-amber-500/30 text-amber-400',
38
+ };
39
 
40
+ return (
41
+ <div className={`bg-gradient-to-br ${colorClasses[color]} border rounded-xl p-4 backdrop-blur-sm`}>
42
+ <div className="flex items-center justify-between">
43
+ <div className={`p-2 rounded-lg bg-${color}-500/20`}>
44
+ <Icon className={`w-5 h-5 ${colorClasses[color].split(' ').pop()}`} />
45
+ </div>
46
+ {change && (
47
+ <span className="text-xs text-emerald-400 flex items-center gap-1">
48
+ <TrendingUp className="w-3 h-3" />
49
+ {change}
50
+ </span>
51
+ )}
52
+ </div>
53
+ <div className="mt-3">
54
+ <p className="text-2xl font-bold text-white">{value}</p>
55
+ <p className="text-xs text-gray-400 mt-1">{label}</p>
56
+ </div>
57
+ </div>
58
+ );
59
+ };
60
 
61
+ export const Dashboard: React.FC = () => {
62
  const { data: episode } = useCurrentEpisode();
63
 
64
  return (
65
+ <div className="space-y-6">
66
+ {/* Header Section */}
67
+ <div className="flex flex-col lg:flex-row lg:items-center lg:justify-between gap-4">
68
+ <div>
69
+ <h1 className="text-2xl font-bold text-white flex items-center gap-3">
70
+ <div className="p-2 bg-gradient-to-br from-emerald-500/20 to-cyan-500/20 rounded-lg">
71
+ <Activity className="w-6 h-6 text-emerald-400" />
 
 
 
 
 
 
 
 
 
72
  </div>
73
+ Dashboard
74
+ </h1>
75
+ <p className="text-gray-400 mt-1">Monitor your RL scraping agents in real-time</p>
 
 
 
 
 
 
 
 
 
 
 
 
 
 
76
  </div>
77
+
78
+ <div className="flex items-center gap-3">
79
+ <button className="flex items-center gap-2 px-4 py-2 bg-emerald-500 hover:bg-emerald-600 text-white rounded-lg font-medium transition-colors shadow-lg shadow-emerald-500/20">
80
+ <Play className="w-4 h-4" />
81
+ Start Episode
 
 
 
 
 
 
 
 
 
 
 
82
  </button>
83
+ <button className="flex items-center gap-2 px-4 py-2 bg-gray-700 hover:bg-gray-600 text-white rounded-lg font-medium transition-colors">
84
+ <RotateCcw className="w-4 h-4" />
85
+ Reset
 
 
 
 
 
 
 
 
 
 
86
  </button>
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
87
  </div>
88
+ </div>
89
 
90
+ {/* Stats Grid */}
91
+ <div className="grid grid-cols-2 lg:grid-cols-4 gap-4">
92
+ <StatCard
93
+ icon={Zap}
94
+ label="Total Episodes"
95
+ value={episode?.id ? '1' : '0'}
96
+ color="emerald"
97
+ />
98
+ <StatCard
99
+ icon={Target}
100
+ label="Current Step"
101
+ value={episode?.currentStep || 0}
102
+ color="cyan"
103
  />
104
+ <StatCard
105
+ icon={TrendingUp}
106
+ label="Total Reward"
107
+ value={episode?.totalReward?.toFixed(2) || '0.00'}
108
+ change="+12%"
109
+ color="purple"
110
+ />
111
+ <StatCard
112
+ icon={Clock}
113
+ label="Avg Time/Step"
114
+ value="1.2s"
115
+ color="amber"
116
+ />
117
+ </div>
118
 
119
+ {/* Main Content Grid */}
120
+ <div className="grid grid-cols-1 lg:grid-cols-12 gap-6">
121
+ {/* Left Column - Episode & Agents */}
122
+ <div className="lg:col-span-3 space-y-6">
123
+ <div className="bg-gray-800/50 backdrop-blur-sm border border-gray-700/50 rounded-xl p-4">
124
+ <h3 className="text-sm font-semibold text-white flex items-center gap-2 mb-4">
125
+ <Brain className="w-4 h-4 text-emerald-400" />
126
+ Episode Status
127
+ </h3>
128
+ <EpisodePanel />
 
 
 
 
 
 
 
 
 
 
 
129
  </div>
130
+
131
+ <div className="bg-gray-800/50 backdrop-blur-sm border border-gray-700/50 rounded-xl p-4">
132
+ <h3 className="text-sm font-semibold text-white flex items-center gap-2 mb-4">
133
+ <Cpu className="w-4 h-4 text-cyan-400" />
134
+ Active Agents
135
+ </h3>
136
+ <AgentView />
 
 
 
 
 
 
 
 
 
 
137
  </div>
138
+ </div>
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
139
 
140
+ {/* Center Column - Observation & Charts */}
141
+ <div className="lg:col-span-6 space-y-6">
142
+ <div className="bg-gray-800/50 backdrop-blur-sm border border-gray-700/50 rounded-xl p-4">
143
+ <h3 className="text-sm font-semibold text-white flex items-center gap-2 mb-4">
144
+ <Globe className="w-4 h-4 text-purple-400" />
145
+ Current Observation
146
+ </h3>
147
+ <ObservationView />
148
+ </div>
149
+
150
+ <div className="grid grid-cols-1 md:grid-cols-2 gap-6">
151
+ <div className="bg-gray-800/50 backdrop-blur-sm border border-gray-700/50 rounded-xl p-4">
152
+ <h3 className="text-sm font-semibold text-white flex items-center gap-2 mb-4">
153
+ <TrendingUp className="w-4 h-4 text-emerald-400" />
154
+ Reward History
155
+ </h3>
156
+ <RewardChart />
157
  </div>
158
+
159
+ <div className="bg-gray-800/50 backdrop-blur-sm border border-gray-700/50 rounded-xl p-4">
160
+ <h3 className="text-sm font-semibold text-white flex items-center gap-2 mb-4">
161
+ <Zap className="w-4 h-4 text-amber-400" />
162
+ Actions
163
+ </h3>
164
+ <ActionPanel />
165
  </div>
166
+ </div>
167
+ </div>
168
+
169
+ {/* Right Column - Memory & Tools */}
170
+ <div className="lg:col-span-3 space-y-6">
171
+ <div className="bg-gray-800/50 backdrop-blur-sm border border-gray-700/50 rounded-xl p-4">
172
+ <h3 className="text-sm font-semibold text-white flex items-center gap-2 mb-4">
173
+ <Database className="w-4 h-4 text-pink-400" />
174
+ Memory Layers
175
+ </h3>
176
+ <MemoryPanel />
177
+ </div>
178
+
179
+ <div className="bg-gray-800/50 backdrop-blur-sm border border-gray-700/50 rounded-xl p-4">
180
+ <h3 className="text-sm font-semibold text-white flex items-center gap-2 mb-4">
181
+ <Cpu className="w-4 h-4 text-blue-400" />
182
+ Tool Registry
183
+ </h3>
184
+ <ToolRegistry />
185
+ </div>
186
  </div>
187
+ </div>
188
  </div>
189
  );
190
  };
frontend/src/components/PluginsPage.tsx CHANGED
@@ -13,10 +13,8 @@ import {
13
  Cpu,
14
  Wrench,
15
  Database,
 
16
  } from 'lucide-react';
17
- import { Card, CardContent } from '@/components/ui/Card';
18
- import { Button } from '@/components/ui/Button';
19
- import { Input } from '@/components/ui/Input';
20
  import { Badge } from '@/components/ui/Badge';
21
  import { classNames } from '@/utils/helpers';
22
 
@@ -31,13 +29,6 @@ interface Plugin {
31
  requires_key: boolean;
32
  }
33
 
34
- interface Category {
35
- id: string;
36
- name: string;
37
- description: string;
38
- icon: string;
39
- }
40
-
41
  interface PluginsResponse {
42
  plugins: Record<string, Plugin[]>;
43
  categories: string[];
@@ -55,15 +46,15 @@ interface PluginsPageProps {
55
  const getCategoryIcon = (category: string) => {
56
  switch (category) {
57
  case 'apis':
58
- return <Plug className="w-5 h-5" />;
59
  case 'mcps':
60
- return <Wrench className="w-5 h-5" />;
61
  case 'skills':
62
- return <Cpu className="w-5 h-5" />;
63
  case 'processors':
64
- return <Database className="w-5 h-5" />;
65
  default:
66
- return <Package className="w-5 h-5" />;
67
  }
68
  };
69
 
@@ -71,12 +62,22 @@ const getCategoryLabel = (category: string) => {
71
  const labels: Record<string, string> = {
72
  apis: 'API Providers',
73
  mcps: 'MCP Tools',
74
- skills: 'Skills/Agents',
75
  processors: 'Data Processors',
76
  };
77
  return labels[category] || category;
78
  };
79
 
 
 
 
 
 
 
 
 
 
 
80
  export const PluginsPage: React.FC<PluginsPageProps> = ({ className }) => {
81
  const queryClient = useQueryClient();
82
  const [searchQuery, setSearchQuery] = useState('');
@@ -92,15 +93,6 @@ export const PluginsPage: React.FC<PluginsPageProps> = ({ className }) => {
92
  },
93
  });
94
 
95
- // Fetch categories
96
- const { data: categoriesData } = useQuery<{ categories: Category[] }>({
97
- queryKey: ['plugin-categories'],
98
- queryFn: async () => {
99
- const res = await fetch('/api/plugins/categories');
100
- return res.json();
101
- },
102
- });
103
-
104
  // Install plugin mutation
105
  const installMutation = useMutation({
106
  mutationFn: async (pluginId: string) => {
@@ -168,92 +160,122 @@ export const PluginsPage: React.FC<PluginsPageProps> = ({ className }) => {
168
  return (
169
  <div className={classNames('space-y-6', className)}>
170
  {/* Header */}
171
- <div className="flex items-center justify-between">
172
  <div>
173
- <h1 className="text-2xl font-bold text-dark-100">Plugins</h1>
174
- <p className="text-dark-400 mt-1">
 
 
 
 
 
175
  Extend ScrapeRL with APIs, tools, skills, and processors
176
  </p>
177
  </div>
 
 
178
  {pluginsData?.stats && (
179
- <div className="flex gap-4 text-sm">
180
- <div className="text-center">
181
- <div className="text-2xl font-bold text-primary-400">
182
  {pluginsData.stats.installed}
183
  </div>
184
- <div className="text-dark-400">Installed</div>
185
  </div>
186
- <div className="text-center">
187
- <div className="text-2xl font-bold text-dark-300">
188
  {pluginsData.stats.available}
189
  </div>
190
- <div className="text-dark-400">Available</div>
 
 
 
 
 
 
191
  </div>
192
  </div>
193
  )}
194
  </div>
195
 
196
  {/* Filters */}
197
- <Card>
198
- <CardContent className="py-4">
199
- <div className="flex flex-wrap gap-4 items-center">
200
- {/* Search */}
201
- <div className="flex-1 min-w-[200px]">
202
- <Input
 
 
203
  placeholder="Search plugins..."
204
  value={searchQuery}
205
  onChange={(e) => setSearchQuery(e.target.value)}
206
- leftIcon={<Search className="w-4 h-4" />}
207
  />
208
  </div>
 
209
 
210
- {/* Category Filter */}
211
- <div className="flex gap-2">
212
- <Button
213
- size="sm"
214
- variant={selectedCategory === null ? 'primary' : 'ghost'}
215
- onClick={() => setSelectedCategory(null)}
216
- >
217
- All
218
- </Button>
219
- {categoriesData?.categories.map((cat) => (
220
- <Button
221
- key={cat.id}
222
- size="sm"
223
- variant={selectedCategory === cat.id ? 'primary' : 'ghost'}
224
- onClick={() => setSelectedCategory(cat.id)}
225
- leftIcon={<span>{cat.icon}</span>}
226
- >
227
- {cat.name}
228
- </Button>
229
- ))}
230
- </div>
231
-
232
- {/* Show Installed Toggle */}
233
- <Button
234
- size="sm"
235
- variant={showInstalled ? 'primary' : 'ghost'}
236
- onClick={() => setShowInstalled(!showInstalled)}
237
- leftIcon={<Filter className="w-4 h-4" />}
238
  >
239
- Installed Only
240
- </Button>
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
241
  </div>
242
- </CardContent>
243
- </Card>
 
 
 
 
 
 
 
 
 
 
 
 
 
 
244
 
245
  {/* Plugin List */}
246
  {isLoading ? (
247
- <div className="flex items-center justify-center py-12">
248
- <Loader2 className="w-8 h-8 text-primary-400 animate-spin" />
 
249
  </div>
250
  ) : (
251
- <div className="space-y-6">
252
  {Object.entries(filteredPlugins).map(([category, plugins]) => (
253
  <div key={category}>
254
- <div className="flex items-center gap-2 mb-4">
255
- {getCategoryIcon(category)}
256
- <h2 className="text-lg font-semibold text-dark-100">
 
 
257
  {getCategoryLabel(category)}
258
  </h2>
259
  <Badge variant="neutral" size="sm">
@@ -263,74 +285,72 @@ export const PluginsPage: React.FC<PluginsPageProps> = ({ className }) => {
263
 
264
  <div className="grid grid-cols-1 md:grid-cols-2 lg:grid-cols-3 gap-4">
265
  {plugins.map((plugin) => (
266
- <Card key={plugin.id} className="relative">
267
- <CardContent className="py-4">
268
- <div className="flex items-start justify-between">
269
- <div className="flex-1">
270
- <div className="flex items-center gap-2">
271
- <h3 className="font-medium text-dark-100">
272
- {plugin.name}
273
- </h3>
274
- {plugin.installed && (
275
- <CheckCircle className="w-4 h-4 text-green-400" />
276
- )}
277
- </div>
278
- <p className="text-sm text-dark-400 mt-1">
279
- {plugin.description}
280
- </p>
281
- <div className="flex items-center gap-3 mt-3 text-xs text-dark-500">
282
- <span>v{plugin.version}</span>
283
- <span>•</span>
284
- <span>{plugin.size}</span>
285
- {plugin.requires_key && (
286
- <>
287
- <span>•</span>
288
- <span className="text-yellow-400">
289
- Requires API Key
290
- </span>
291
- </>
292
- )}
293
- </div>
294
- </div>
295
- </div>
296
-
297
- <div className="flex gap-2 mt-4">
298
- {plugin.installed ? (
299
- <Button
300
- size="sm"
301
- variant="ghost"
302
- className="flex-1 text-red-400 hover:text-red-300"
303
- onClick={() => uninstallMutation.mutate(plugin.id)}
304
- disabled={uninstallMutation.isPending}
305
- leftIcon={<Trash2 className="w-4 h-4" />}
306
- >
307
- Uninstall
308
- </Button>
309
- ) : (
310
- <Button
311
- size="sm"
312
- variant="primary"
313
- className="flex-1"
314
- onClick={() => installMutation.mutate(plugin.id)}
315
- disabled={installMutation.isPending}
316
- leftIcon={<Download className="w-4 h-4" />}
317
- >
318
- Install
319
- </Button>
320
  )}
321
  </div>
322
- </CardContent>
323
- </Card>
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
324
  ))}
325
  </div>
326
  </div>
327
  ))}
328
 
329
  {Object.keys(filteredPlugins).length === 0 && (
330
- <div className="text-center py-12">
331
- <Package className="w-12 h-12 text-dark-500 mx-auto mb-4" />
332
- <h3 className="text-lg font-medium text-dark-300">No plugins found</h3>
333
- <p className="text-dark-400 mt-1">
 
 
334
  Try adjusting your search or filter criteria
335
  </p>
336
  </div>
@@ -340,7 +360,7 @@ export const PluginsPage: React.FC<PluginsPageProps> = ({ className }) => {
340
 
341
  {/* Error Messages */}
342
  {uninstallMutation.isError && (
343
- <div className="fixed bottom-4 right-4 flex items-center gap-2 p-4 bg-red-500/10 border border-red-500/30 rounded-lg">
344
  <AlertCircle className="w-5 h-5 text-red-400" />
345
  <span className="text-sm text-red-400">
346
  {(uninstallMutation.error as Error).message}
 
13
  Cpu,
14
  Wrench,
15
  Database,
16
+ Sparkles,
17
  } from 'lucide-react';
 
 
 
18
  import { Badge } from '@/components/ui/Badge';
19
  import { classNames } from '@/utils/helpers';
20
 
 
29
  requires_key: boolean;
30
  }
31
 
 
 
 
 
 
 
 
32
  interface PluginsResponse {
33
  plugins: Record<string, Plugin[]>;
34
  categories: string[];
 
46
  const getCategoryIcon = (category: string) => {
47
  switch (category) {
48
  case 'apis':
49
+ return <Plug className="w-5 h-5 text-cyan-400" />;
50
  case 'mcps':
51
+ return <Wrench className="w-5 h-5 text-amber-400" />;
52
  case 'skills':
53
+ return <Cpu className="w-5 h-5 text-purple-400" />;
54
  case 'processors':
55
+ return <Database className="w-5 h-5 text-pink-400" />;
56
  default:
57
+ return <Package className="w-5 h-5 text-gray-400" />;
58
  }
59
  };
60
 
 
62
  const labels: Record<string, string> = {
63
  apis: 'API Providers',
64
  mcps: 'MCP Tools',
65
+ skills: 'Skills & Agents',
66
  processors: 'Data Processors',
67
  };
68
  return labels[category] || category;
69
  };
70
 
71
+ const getCategoryColor = (category: string) => {
72
+ const colors: Record<string, string> = {
73
+ apis: 'from-cyan-500/20 to-blue-500/10 border-cyan-500/30',
74
+ mcps: 'from-amber-500/20 to-orange-500/10 border-amber-500/30',
75
+ skills: 'from-purple-500/20 to-pink-500/10 border-purple-500/30',
76
+ processors: 'from-pink-500/20 to-rose-500/10 border-pink-500/30',
77
+ };
78
+ return colors[category] || 'from-gray-500/20 to-gray-500/10 border-gray-500/30';
79
+ };
80
+
81
  export const PluginsPage: React.FC<PluginsPageProps> = ({ className }) => {
82
  const queryClient = useQueryClient();
83
  const [searchQuery, setSearchQuery] = useState('');
 
93
  },
94
  });
95
 
 
 
 
 
 
 
 
 
 
96
  // Install plugin mutation
97
  const installMutation = useMutation({
98
  mutationFn: async (pluginId: string) => {
 
160
  return (
161
  <div className={classNames('space-y-6', className)}>
162
  {/* Header */}
163
+ <div className="flex flex-col lg:flex-row lg:items-center lg:justify-between gap-4">
164
  <div>
165
+ <h1 className="text-2xl font-bold text-white flex items-center gap-3">
166
+ <div className="p-2 bg-gradient-to-br from-cyan-500/20 to-blue-500/20 rounded-lg">
167
+ <Package className="w-6 h-6 text-cyan-400" />
168
+ </div>
169
+ Plugins
170
+ </h1>
171
+ <p className="text-gray-400 mt-1">
172
  Extend ScrapeRL with APIs, tools, skills, and processors
173
  </p>
174
  </div>
175
+
176
+ {/* Stats */}
177
  {pluginsData?.stats && (
178
+ <div className="flex gap-4">
179
+ <div className="px-4 py-2 bg-emerald-500/10 border border-emerald-500/30 rounded-xl text-center">
180
+ <div className="text-xl font-bold text-emerald-400">
181
  {pluginsData.stats.installed}
182
  </div>
183
+ <div className="text-xs text-emerald-400/70">Installed</div>
184
  </div>
185
+ <div className="px-4 py-2 bg-gray-700/30 border border-gray-600/30 rounded-xl text-center">
186
+ <div className="text-xl font-bold text-gray-300">
187
  {pluginsData.stats.available}
188
  </div>
189
+ <div className="text-xs text-gray-500">Available</div>
190
+ </div>
191
+ <div className="px-4 py-2 bg-purple-500/10 border border-purple-500/30 rounded-xl text-center">
192
+ <div className="text-xl font-bold text-purple-400">
193
+ {pluginsData.stats.total}
194
+ </div>
195
+ <div className="text-xs text-purple-400/70">Total</div>
196
  </div>
197
  </div>
198
  )}
199
  </div>
200
 
201
  {/* Filters */}
202
+ <div className="bg-gray-800/50 backdrop-blur-sm border border-gray-700/50 rounded-xl p-4">
203
+ <div className="flex flex-wrap gap-4 items-center">
204
+ {/* Search */}
205
+ <div className="flex-1 min-w-[200px]">
206
+ <div className="relative">
207
+ <Search className="absolute left-3 top-1/2 -translate-y-1/2 w-4 h-4 text-gray-500" />
208
+ <input
209
+ type="text"
210
  placeholder="Search plugins..."
211
  value={searchQuery}
212
  onChange={(e) => setSearchQuery(e.target.value)}
213
+ className="w-full pl-10 pr-4 py-2.5 bg-gray-900/50 border border-gray-700/50 rounded-lg text-gray-200 placeholder-gray-500 focus:outline-none focus:ring-2 focus:ring-cyan-500/50 focus:border-cyan-500/50 transition-all"
214
  />
215
  </div>
216
+ </div>
217
 
218
+ {/* Category Filter */}
219
+ <div className="flex gap-2 flex-wrap">
220
+ <button
221
+ onClick={() => setSelectedCategory(null)}
222
+ className={classNames(
223
+ 'px-4 py-2 rounded-lg text-sm font-medium transition-all',
224
+ selectedCategory === null
225
+ ? 'bg-emerald-500 text-white shadow-lg shadow-emerald-500/20'
226
+ : 'bg-gray-700/50 text-gray-400 hover:text-gray-200 hover:bg-gray-700'
227
+ )}
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
228
  >
229
+ All
230
+ </button>
231
+ {['apis', 'mcps', 'skills', 'processors'].map((cat) => (
232
+ <button
233
+ key={cat}
234
+ onClick={() => setSelectedCategory(cat)}
235
+ className={classNames(
236
+ 'flex items-center gap-2 px-4 py-2 rounded-lg text-sm font-medium transition-all',
237
+ selectedCategory === cat
238
+ ? 'bg-emerald-500 text-white shadow-lg shadow-emerald-500/20'
239
+ : 'bg-gray-700/50 text-gray-400 hover:text-gray-200 hover:bg-gray-700'
240
+ )}
241
+ >
242
+ {getCategoryIcon(cat)}
243
+ {getCategoryLabel(cat)}
244
+ </button>
245
+ ))}
246
  </div>
247
+
248
+ {/* Show Installed Toggle */}
249
+ <button
250
+ onClick={() => setShowInstalled(!showInstalled)}
251
+ className={classNames(
252
+ 'flex items-center gap-2 px-4 py-2 rounded-lg text-sm font-medium transition-all',
253
+ showInstalled
254
+ ? 'bg-purple-500 text-white shadow-lg shadow-purple-500/20'
255
+ : 'bg-gray-700/50 text-gray-400 hover:text-gray-200 hover:bg-gray-700'
256
+ )}
257
+ >
258
+ <Filter className="w-4 h-4" />
259
+ Installed Only
260
+ </button>
261
+ </div>
262
+ </div>
263
 
264
  {/* Plugin List */}
265
  {isLoading ? (
266
+ <div className="flex flex-col items-center justify-center py-16">
267
+ <Loader2 className="w-10 h-10 text-cyan-400 animate-spin mb-4" />
268
+ <p className="text-gray-400">Loading plugins...</p>
269
  </div>
270
  ) : (
271
+ <div className="space-y-8">
272
  {Object.entries(filteredPlugins).map(([category, plugins]) => (
273
  <div key={category}>
274
+ <div className="flex items-center gap-3 mb-4">
275
+ <div className={`p-2 rounded-lg bg-gradient-to-br ${getCategoryColor(category)}`}>
276
+ {getCategoryIcon(category)}
277
+ </div>
278
+ <h2 className="text-lg font-semibold text-white">
279
  {getCategoryLabel(category)}
280
  </h2>
281
  <Badge variant="neutral" size="sm">
 
285
 
286
  <div className="grid grid-cols-1 md:grid-cols-2 lg:grid-cols-3 gap-4">
287
  {plugins.map((plugin) => (
288
+ <div
289
+ key={plugin.id}
290
+ className={`relative bg-gradient-to-br ${getCategoryColor(category)} border rounded-xl p-5 backdrop-blur-sm transition-all hover:scale-[1.02] hover:shadow-xl`}
291
+ >
292
+ <div className="flex items-start justify-between mb-3">
293
+ <div className="flex items-center gap-2">
294
+ <h3 className="font-semibold text-white">{plugin.name}</h3>
295
+ {plugin.installed && (
296
+ <CheckCircle className="w-4 h-4 text-emerald-400" />
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
297
  )}
298
  </div>
299
+ <Badge
300
+ variant={plugin.installed ? 'success' : 'neutral'}
301
+ size="sm"
302
+ >
303
+ {plugin.installed ? 'Installed' : 'Available'}
304
+ </Badge>
305
+ </div>
306
+
307
+ <p className="text-sm text-gray-400 mb-4 line-clamp-2">
308
+ {plugin.description}
309
+ </p>
310
+
311
+ <div className="flex items-center gap-3 text-xs text-gray-500 mb-4">
312
+ <span className="px-2 py-0.5 bg-gray-800/50 rounded">v{plugin.version}</span>
313
+ <span>{plugin.size}</span>
314
+ {plugin.requires_key && (
315
+ <span className="flex items-center gap-1 text-amber-400">
316
+ <Sparkles className="w-3 h-3" />
317
+ API Key
318
+ </span>
319
+ )}
320
+ </div>
321
+
322
+ {plugin.installed ? (
323
+ <button
324
+ onClick={() => uninstallMutation.mutate(plugin.id)}
325
+ disabled={uninstallMutation.isPending}
326
+ className="w-full flex items-center justify-center gap-2 px-4 py-2.5 bg-red-500/10 hover:bg-red-500/20 border border-red-500/30 text-red-400 rounded-lg font-medium transition-all disabled:opacity-50"
327
+ >
328
+ <Trash2 className="w-4 h-4" />
329
+ Uninstall
330
+ </button>
331
+ ) : (
332
+ <button
333
+ onClick={() => installMutation.mutate(plugin.id)}
334
+ disabled={installMutation.isPending}
335
+ className="w-full flex items-center justify-center gap-2 px-4 py-2.5 bg-emerald-500 hover:bg-emerald-600 text-white rounded-lg font-medium transition-all shadow-lg shadow-emerald-500/20 disabled:opacity-50"
336
+ >
337
+ <Download className="w-4 h-4" />
338
+ Install
339
+ </button>
340
+ )}
341
+ </div>
342
  ))}
343
  </div>
344
  </div>
345
  ))}
346
 
347
  {Object.keys(filteredPlugins).length === 0 && (
348
+ <div className="text-center py-16">
349
+ <div className="w-16 h-16 bg-gray-800/50 rounded-full flex items-center justify-center mx-auto mb-4">
350
+ <Package className="w-8 h-8 text-gray-500" />
351
+ </div>
352
+ <h3 className="text-lg font-medium text-gray-300">No plugins found</h3>
353
+ <p className="text-gray-500 mt-1">
354
  Try adjusting your search or filter criteria
355
  </p>
356
  </div>
 
360
 
361
  {/* Error Messages */}
362
  {uninstallMutation.isError && (
363
+ <div className="fixed bottom-4 right-4 flex items-center gap-3 p-4 bg-red-500/10 border border-red-500/30 rounded-xl backdrop-blur-sm shadow-xl">
364
  <AlertCircle className="w-5 h-5 text-red-400" />
365
  <span className="text-sm text-red-400">
366
  {(uninstallMutation.error as Error).message}
frontend/src/components/Settings.tsx CHANGED
@@ -3,16 +3,13 @@ import { useQuery, useMutation, useQueryClient } from '@tanstack/react-query';
3
  import {
4
  Settings as SettingsIcon,
5
  Key,
6
- Wifi,
7
- Database,
8
- Image,
9
  AlertCircle,
10
  CheckCircle,
11
  Eye,
12
  EyeOff,
13
  Zap,
 
14
  } from 'lucide-react';
15
- import { Card, CardHeader, CardContent } from '@/components/ui/Card';
16
  import { Button } from '@/components/ui/Button';
17
  import { Input } from '@/components/ui/Input';
18
  import { Select, Toggle } from '@/components/ui/Select';
@@ -147,39 +144,51 @@ export const Settings: React.FC<SettingsProps> = ({ className }) => {
147
  : 'groq/gpt-oss-120b';
148
 
149
  return (
150
- <Card className={className}>
151
- <CardHeader
152
- title="Settings"
153
- icon={<SettingsIcon className="w-4 h-4" />}
154
- action={
155
- health && (
156
- <Badge variant={health.status === 'ok' ? 'success' : 'error'} dot>
157
- {health.status === 'ok' ? 'Connected' : 'Disconnected'}
158
- </Badge>
159
- )
160
- }
161
- />
162
- <CardContent>
163
- {settingsLoading ? (
164
- <div className="flex items-center justify-center py-8">
165
- <SettingsIcon className="w-6 h-6 text-dark-500 animate-spin" />
 
 
 
 
 
 
 
 
166
  </div>
167
- ) : (
 
 
 
168
  <div className="space-y-6">
169
  {/* API Key Required Warning */}
170
  {keyRequired?.required && (
171
- <div className="flex items-center gap-2 p-3 bg-yellow-500/10 border border-yellow-500/30 rounded-lg">
172
- <AlertCircle className="w-4 h-4 text-yellow-400" />
173
- <span className="text-sm text-yellow-400">
174
- {keyRequired.message}
175
- </span>
 
176
  </div>
177
  )}
178
 
179
  {/* Model Selection */}
180
- <div>
181
- <div className="flex items-center gap-2 text-sm font-medium text-dark-200 mb-3">
182
- <Zap className="w-4 h-4" />
183
  Active Model
184
  </div>
185
  <Select
@@ -190,86 +199,20 @@ export const Settings: React.FC<SettingsProps> = ({ className }) => {
190
  placeholder="Select model"
191
  />
192
  {selectModelMutation.isPending && (
193
- <p className="text-xs text-dark-400 mt-1">Switching model...</p>
194
  )}
195
- </div>
196
-
197
- {/* API Keys Section */}
198
- <div>
199
- <div className="flex items-center gap-2 text-sm font-medium text-dark-200 mb-3">
200
- <Key className="w-4 h-4" />
201
- API Keys
202
- </div>
203
- <p className="text-xs text-dark-400 mb-4">
204
- Enter your API keys to use the corresponding models. Keys are stored in your browser session.
205
  </p>
206
- <div className="space-y-4">
207
- {providers.map((provider) => {
208
- const isConfigured = settingsData?.api_keys_configured?.[provider.id] ?? false;
209
- return (
210
- <div key={provider.id} className="p-3 bg-dark-800/50 rounded-lg">
211
- <div className="flex items-center justify-between mb-2">
212
- <div className="flex items-center gap-2">
213
- <span className="text-lg">{provider.icon}</span>
214
- <div>
215
- <span className="text-sm font-medium text-dark-100">
216
- {provider.name}
217
- </span>
218
- <p className="text-xs text-dark-400">{provider.description}</p>
219
- </div>
220
- </div>
221
- <Badge variant={isConfigured ? 'success' : 'warning'} size="sm">
222
- {isConfigured ? 'Configured' : 'Not Set'}
223
- </Badge>
224
- </div>
225
- <div className="flex gap-2">
226
- <div className="flex-1 relative">
227
- <Input
228
- type={showKeys[provider.id] ? 'text' : 'password'}
229
- placeholder={`Enter ${provider.name} API key...`}
230
- value={apiKeys[provider.id as keyof ApiKeyState]}
231
- onChange={(e) =>
232
- setApiKeys((prev) => ({
233
- ...prev,
234
- [provider.id]: e.target.value,
235
- }))
236
- }
237
- className="pr-10"
238
- />
239
- <button
240
- type="button"
241
- onClick={() => toggleShowKey(provider.id)}
242
- className="absolute right-2 top-1/2 -translate-y-1/2 text-dark-400 hover:text-dark-200"
243
- >
244
- {showKeys[provider.id] ? (
245
- <EyeOff className="w-4 h-4" />
246
- ) : (
247
- <Eye className="w-4 h-4" />
248
- )}
249
- </button>
250
- </div>
251
- <Button
252
- size="sm"
253
- variant="primary"
254
- onClick={() => handleSaveApiKey(provider.id)}
255
- disabled={!apiKeys[provider.id as keyof ApiKeyState]}
256
- >
257
- Save
258
- </Button>
259
- </div>
260
- </div>
261
- );
262
- })}
263
- </div>
264
  </div>
265
 
266
  {/* Connection Settings */}
267
- <div>
268
- <div className="flex items-center gap-2 text-sm font-medium text-dark-200 mb-3">
269
- <Wifi className="w-4 h-4" />
270
- Connection
271
  </div>
272
- <div className="space-y-3">
273
  <Toggle
274
  label="WebSocket Updates"
275
  description="Enable real-time episode updates"
@@ -278,16 +221,6 @@ export const Settings: React.FC<SettingsProps> = ({ className }) => {
278
  setLocalSettings((prev) => ({ ...prev, enableWebSocket: checked }));
279
  }}
280
  />
281
- </div>
282
- </div>
283
-
284
- {/* Storage Settings */}
285
- <div>
286
- <div className="flex items-center gap-2 text-sm font-medium text-dark-200 mb-3">
287
- <Database className="w-4 h-4" />
288
- Storage
289
- </div>
290
- <div className="space-y-3">
291
  <Toggle
292
  label="Memory Persistence"
293
  description="Persist memory across episodes"
@@ -298,42 +231,88 @@ export const Settings: React.FC<SettingsProps> = ({ className }) => {
298
  />
299
  </div>
300
  </div>
 
301
 
302
- {/* Screenshot Settings */}
303
- <div>
304
- <div className="flex items-center gap-2 text-sm font-medium text-dark-200 mb-3">
305
- <Image className="w-4 h-4" />
306
- Screenshots
307
- </div>
308
- <div className="space-y-3">
309
- <Input
310
- label="Screenshot Quality"
311
- type="range"
312
- min={10}
313
- max={100}
314
- value={localSettings.screenshotQuality ?? 80}
315
- onChange={(e) => {
316
- setLocalSettings((prev) => ({
317
- ...prev,
318
- screenshotQuality: parseInt(e.target.value),
319
- }));
320
- }}
321
- hint={`Quality: ${localSettings.screenshotQuality ?? 80}%`}
322
- />
323
- </div>
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
324
  </div>
325
 
326
  {/* Status Messages */}
327
  {updateApiKeyMutation.isSuccess && (
328
- <div className="flex items-center gap-2 p-3 bg-green-500/10 border border-green-500/30 rounded-lg">
329
- <CheckCircle className="w-4 h-4 text-green-400" />
330
- <span className="text-sm text-green-400">API key saved successfully</span>
331
  </div>
332
  )}
333
  </div>
334
- )}
335
- </CardContent>
336
- </Card>
337
  );
338
  };
339
 
 
3
  import {
4
  Settings as SettingsIcon,
5
  Key,
 
 
 
6
  AlertCircle,
7
  CheckCircle,
8
  Eye,
9
  EyeOff,
10
  Zap,
11
+ Server,
12
  } from 'lucide-react';
 
13
  import { Button } from '@/components/ui/Button';
14
  import { Input } from '@/components/ui/Input';
15
  import { Select, Toggle } from '@/components/ui/Select';
 
144
  : 'groq/gpt-oss-120b';
145
 
146
  return (
147
+ <div className={`space-y-6 ${className}`}>
148
+ {/* Header */}
149
+ <div className="flex items-center justify-between">
150
+ <div>
151
+ <h1 className="text-2xl font-bold text-white flex items-center gap-3">
152
+ <div className="p-2 bg-gradient-to-br from-purple-500/20 to-pink-500/20 rounded-lg">
153
+ <SettingsIcon className="w-6 h-6 text-purple-400" />
154
+ </div>
155
+ Settings
156
+ </h1>
157
+ <p className="text-gray-400 mt-1">Configure your ScrapeRL environment</p>
158
+ </div>
159
+ {health && (
160
+ <Badge variant={health.status === 'ok' ? 'success' : 'error'} dot>
161
+ {health.status === 'ok' ? 'Connected' : 'Disconnected'}
162
+ </Badge>
163
+ )}
164
+ </div>
165
+
166
+ {settingsLoading ? (
167
+ <div className="flex items-center justify-center py-16">
168
+ <div className="flex flex-col items-center gap-3">
169
+ <SettingsIcon className="w-8 h-8 text-gray-500 animate-spin" />
170
+ <p className="text-gray-400">Loading settings...</p>
171
  </div>
172
+ </div>
173
+ ) : (
174
+ <div className="grid grid-cols-1 lg:grid-cols-2 gap-6">
175
+ {/* Left Column */}
176
  <div className="space-y-6">
177
  {/* API Key Required Warning */}
178
  {keyRequired?.required && (
179
+ <div className="flex items-center gap-3 p-4 bg-amber-500/10 border border-amber-500/30 rounded-xl">
180
+ <AlertCircle className="w-5 h-5 text-amber-400 flex-shrink-0" />
181
+ <div>
182
+ <p className="text-sm font-medium text-amber-400">API Key Required</p>
183
+ <p className="text-xs text-amber-400/70">{keyRequired.message}</p>
184
+ </div>
185
  </div>
186
  )}
187
 
188
  {/* Model Selection */}
189
+ <div className="bg-gray-800/50 backdrop-blur-sm border border-gray-700/50 rounded-xl p-5">
190
+ <div className="flex items-center gap-2 text-sm font-semibold text-white mb-4">
191
+ <Zap className="w-4 h-4 text-emerald-400" />
192
  Active Model
193
  </div>
194
  <Select
 
199
  placeholder="Select model"
200
  />
201
  {selectModelMutation.isPending && (
202
+ <p className="text-xs text-gray-400 mt-2">Switching model...</p>
203
  )}
204
+ <p className="text-xs text-gray-500 mt-3">
205
+ Select the AI model to use for scraping tasks. Different models have different capabilities and costs.
 
 
 
 
 
 
 
 
206
  </p>
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
207
  </div>
208
 
209
  {/* Connection Settings */}
210
+ <div className="bg-gray-800/50 backdrop-blur-sm border border-gray-700/50 rounded-xl p-5">
211
+ <div className="flex items-center gap-2 text-sm font-semibold text-white mb-4">
212
+ <Server className="w-4 h-4 text-cyan-400" />
213
+ Connection Settings
214
  </div>
215
+ <div className="space-y-4">
216
  <Toggle
217
  label="WebSocket Updates"
218
  description="Enable real-time episode updates"
 
221
  setLocalSettings((prev) => ({ ...prev, enableWebSocket: checked }));
222
  }}
223
  />
 
 
 
 
 
 
 
 
 
 
224
  <Toggle
225
  label="Memory Persistence"
226
  description="Persist memory across episodes"
 
231
  />
232
  </div>
233
  </div>
234
+ </div>
235
 
236
+ {/* Right Column - API Keys */}
237
+ <div className="bg-gray-800/50 backdrop-blur-sm border border-gray-700/50 rounded-xl p-5">
238
+ <div className="flex items-center gap-2 text-sm font-semibold text-white mb-2">
239
+ <Key className="w-4 h-4 text-amber-400" />
240
+ API Keys
241
+ </div>
242
+ <p className="text-xs text-gray-400 mb-5">
243
+ Configure your API keys. Server keys are used by default, but you can override them here.
244
+ </p>
245
+
246
+ <div className="space-y-4">
247
+ {providers.map((provider) => {
248
+ const isConfigured = settingsData?.api_keys_configured?.[provider.id] ?? false;
249
+ return (
250
+ <div key={provider.id} className="p-4 bg-gray-900/50 border border-gray-700/30 rounded-xl">
251
+ <div className="flex items-center justify-between mb-3">
252
+ <div className="flex items-center gap-3">
253
+ <span className="text-2xl">{provider.icon}</span>
254
+ <div>
255
+ <span className="text-sm font-medium text-white">
256
+ {provider.name}
257
+ </span>
258
+ <p className="text-xs text-gray-500">{provider.description}</p>
259
+ </div>
260
+ </div>
261
+ <Badge variant={isConfigured ? 'success' : 'warning'} size="sm">
262
+ {isConfigured ? 'Active' : 'Not Set'}
263
+ </Badge>
264
+ </div>
265
+ <div className="flex gap-2">
266
+ <div className="flex-1 relative">
267
+ <Input
268
+ type={showKeys[provider.id] ? 'text' : 'password'}
269
+ placeholder={`Enter ${provider.name} API key...`}
270
+ value={apiKeys[provider.id as keyof ApiKeyState]}
271
+ onChange={(e) =>
272
+ setApiKeys((prev) => ({
273
+ ...prev,
274
+ [provider.id]: e.target.value,
275
+ }))
276
+ }
277
+ className="pr-10"
278
+ />
279
+ <button
280
+ type="button"
281
+ onClick={() => toggleShowKey(provider.id)}
282
+ className="absolute right-3 top-1/2 -translate-y-1/2 text-gray-500 hover:text-gray-300 transition-colors"
283
+ >
284
+ {showKeys[provider.id] ? (
285
+ <EyeOff className="w-4 h-4" />
286
+ ) : (
287
+ <Eye className="w-4 h-4" />
288
+ )}
289
+ </button>
290
+ </div>
291
+ <Button
292
+ size="sm"
293
+ variant="primary"
294
+ onClick={() => handleSaveApiKey(provider.id)}
295
+ disabled={!apiKeys[provider.id as keyof ApiKeyState]}
296
+ >
297
+ Save
298
+ </Button>
299
+ </div>
300
+ </div>
301
+ );
302
+ })}
303
  </div>
304
 
305
  {/* Status Messages */}
306
  {updateApiKeyMutation.isSuccess && (
307
+ <div className="flex items-center gap-2 mt-4 p-3 bg-emerald-500/10 border border-emerald-500/30 rounded-lg">
308
+ <CheckCircle className="w-4 h-4 text-emerald-400" />
309
+ <span className="text-sm text-emerald-400">API key saved successfully</span>
310
  </div>
311
  )}
312
  </div>
313
+ </div>
314
+ )}
315
+ </div>
316
  );
317
  };
318
 
frontend/tsconfig.tsbuildinfo CHANGED
@@ -1 +1 @@
1
- {"root":["./src/app.tsx","./src/main.tsx","./src/vite-env.d.ts","./src/api/client.ts","./src/components/actionpanel.tsx","./src/components/agentview.tsx","./src/components/dashboard.tsx","./src/components/episodepanel.tsx","./src/components/memorypanel.tsx","./src/components/observationview.tsx","./src/components/rewardchart.tsx","./src/components/settings.tsx","./src/components/toolregistry.tsx","./src/components/ui/badge.tsx","./src/components/ui/button.tsx","./src/components/ui/card.tsx","./src/components/ui/input.tsx","./src/components/ui/select.tsx","./src/hooks/useagents.ts","./src/hooks/useepisode.ts","./src/hooks/usememory.ts","./src/hooks/usewebsocket.ts","./src/types/index.ts","./src/utils/helpers.ts"],"version":"5.6.3"}
 
1
+ {"root":["./src/app.tsx","./src/main.tsx","./src/vite-env.d.ts","./src/api/client.ts","./src/components/actionpanel.tsx","./src/components/agentview.tsx","./src/components/dashboard.tsx","./src/components/episodepanel.tsx","./src/components/memorypanel.tsx","./src/components/observationview.tsx","./src/components/pluginspage.tsx","./src/components/rewardchart.tsx","./src/components/settings.tsx","./src/components/toolregistry.tsx","./src/components/ui/badge.tsx","./src/components/ui/button.tsx","./src/components/ui/card.tsx","./src/components/ui/input.tsx","./src/components/ui/select.tsx","./src/hooks/useagents.ts","./src/hooks/useepisode.ts","./src/hooks/usememory.ts","./src/hooks/usewebsocket.ts","./src/test/components.test.tsx","./src/test/helpers.test.ts","./src/test/setup.ts","./src/types/index.ts","./src/utils/helpers.ts"],"version":"5.6.3"}