Spaces:
Running
Running
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 +35 -1
- frontend/src/App.tsx +68 -13
- frontend/src/components/Dashboard.tsx +155 -181
- frontend/src/components/PluginsPage.tsx +161 -141
- frontend/src/components/Settings.tsx +120 -141
- frontend/tsconfig.tsbuildinfo +1 -1
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-
|
| 29 |
-
<div className="max-w-7xl mx-auto px-4">
|
| 30 |
-
<div className="flex items-center justify-between h-
|
| 31 |
-
|
| 32 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 33 |
</div>
|
| 34 |
-
|
|
|
|
|
|
|
| 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-
|
| 41 |
location.pathname === path
|
| 42 |
-
? 'bg-
|
| 43 |
-
: 'text-
|
| 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
|
| 2 |
import {
|
| 3 |
-
LayoutDashboard,
|
| 4 |
-
Settings as SettingsIcon,
|
| 5 |
Activity,
|
| 6 |
-
|
| 7 |
-
|
| 8 |
-
|
| 9 |
-
|
| 10 |
-
|
| 11 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
| 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 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 26 |
|
| 27 |
-
|
| 28 |
-
const
|
| 29 |
-
|
| 30 |
-
|
|
|
|
|
|
|
|
|
|
| 31 |
|
| 32 |
-
|
| 33 |
-
|
| 34 |
-
|
| 35 |
-
|
| 36 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 37 |
|
|
|
|
| 38 |
const { data: episode } = useCurrentEpisode();
|
| 39 |
|
| 40 |
return (
|
| 41 |
-
<div className="
|
| 42 |
-
{/*
|
| 43 |
-
<
|
| 44 |
-
|
| 45 |
-
|
| 46 |
-
|
| 47 |
-
|
| 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 |
-
<
|
| 60 |
-
|
| 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 |
-
|
| 78 |
-
|
| 79 |
-
|
| 80 |
-
|
| 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 |
-
|
| 95 |
-
|
| 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 |
-
</
|
| 131 |
|
| 132 |
-
{/*
|
| 133 |
-
|
| 134 |
-
<
|
| 135 |
-
|
| 136 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 137 |
/>
|
| 138 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 139 |
|
| 140 |
-
{/* Main Content */}
|
| 141 |
-
<
|
| 142 |
-
{/*
|
| 143 |
-
<
|
| 144 |
-
<div className="
|
| 145 |
-
<
|
| 146 |
-
|
| 147 |
-
|
| 148 |
-
>
|
| 149 |
-
|
| 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="
|
| 164 |
-
|
| 165 |
-
<
|
| 166 |
-
|
| 167 |
-
|
| 168 |
-
|
| 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 |
-
</
|
| 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 |
-
|
| 202 |
-
|
| 203 |
-
|
| 204 |
-
|
| 205 |
-
</
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 206 |
</div>
|
| 207 |
-
|
| 208 |
-
<div className="
|
| 209 |
-
<
|
|
|
|
|
|
|
|
|
|
|
|
|
| 210 |
</div>
|
| 211 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 212 |
</div>
|
| 213 |
-
</
|
| 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
|
| 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-
|
| 174 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 175 |
Extend ScrapeRL with APIs, tools, skills, and processors
|
| 176 |
</p>
|
| 177 |
</div>
|
|
|
|
|
|
|
| 178 |
{pluginsData?.stats && (
|
| 179 |
-
<div className="flex gap-4
|
| 180 |
-
<div className="text-center">
|
| 181 |
-
<div className="text-
|
| 182 |
{pluginsData.stats.installed}
|
| 183 |
</div>
|
| 184 |
-
<div className="text-
|
| 185 |
</div>
|
| 186 |
-
<div className="text-center">
|
| 187 |
-
<div className="text-
|
| 188 |
{pluginsData.stats.available}
|
| 189 |
</div>
|
| 190 |
-
<div className="text-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 191 |
</div>
|
| 192 |
</div>
|
| 193 |
)}
|
| 194 |
</div>
|
| 195 |
|
| 196 |
{/* Filters */}
|
| 197 |
-
<
|
| 198 |
-
<
|
| 199 |
-
|
| 200 |
-
|
| 201 |
-
<div className="
|
| 202 |
-
<
|
|
|
|
|
|
|
| 203 |
placeholder="Search plugins..."
|
| 204 |
value={searchQuery}
|
| 205 |
onChange={(e) => setSearchQuery(e.target.value)}
|
| 206 |
-
|
| 207 |
/>
|
| 208 |
</div>
|
|
|
|
| 209 |
|
| 210 |
-
|
| 211 |
-
|
| 212 |
-
|
| 213 |
-
|
| 214 |
-
|
| 215 |
-
|
| 216 |
-
|
| 217 |
-
|
| 218 |
-
|
| 219 |
-
|
| 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 |
-
|
| 240 |
-
</
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 241 |
</div>
|
| 242 |
-
|
| 243 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 244 |
|
| 245 |
{/* Plugin List */}
|
| 246 |
{isLoading ? (
|
| 247 |
-
<div className="flex items-center justify-center py-
|
| 248 |
-
<Loader2 className="w-
|
|
|
|
| 249 |
</div>
|
| 250 |
) : (
|
| 251 |
-
<div className="space-y-
|
| 252 |
{Object.entries(filteredPlugins).map(([category, plugins]) => (
|
| 253 |
<div key={category}>
|
| 254 |
-
<div className="flex items-center gap-
|
| 255 |
-
{
|
| 256 |
-
|
|
|
|
|
|
|
| 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 |
-
<
|
| 267 |
-
|
| 268 |
-
|
| 269 |
-
|
| 270 |
-
|
| 271 |
-
|
| 272 |
-
|
| 273 |
-
|
| 274 |
-
|
| 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 |
-
|
| 323 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 324 |
))}
|
| 325 |
</div>
|
| 326 |
</div>
|
| 327 |
))}
|
| 328 |
|
| 329 |
{Object.keys(filteredPlugins).length === 0 && (
|
| 330 |
-
<div className="text-center py-
|
| 331 |
-
<
|
| 332 |
-
|
| 333 |
-
<
|
|
|
|
|
|
|
| 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-
|
| 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 |
-
<
|
| 151 |
-
|
| 152 |
-
|
| 153 |
-
|
| 154 |
-
|
| 155 |
-
|
| 156 |
-
|
| 157 |
-
|
| 158 |
-
|
| 159 |
-
|
| 160 |
-
|
| 161 |
-
|
| 162 |
-
|
| 163 |
-
|
| 164 |
-
|
| 165 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 166 |
</div>
|
| 167 |
-
|
|
|
|
|
|
|
|
|
|
| 168 |
<div className="space-y-6">
|
| 169 |
{/* API Key Required Warning */}
|
| 170 |
{keyRequired?.required && (
|
| 171 |
-
<div className="flex items-center gap-
|
| 172 |
-
<AlertCircle className="w-
|
| 173 |
-
<
|
| 174 |
-
|
| 175 |
-
|
|
|
|
| 176 |
</div>
|
| 177 |
)}
|
| 178 |
|
| 179 |
{/* Model Selection */}
|
| 180 |
-
<div>
|
| 181 |
-
<div className="flex items-center gap-2 text-sm font-
|
| 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-
|
| 194 |
)}
|
| 195 |
-
|
| 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-
|
| 269 |
-
<
|
| 270 |
-
Connection
|
| 271 |
</div>
|
| 272 |
-
<div className="space-y-
|
| 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 |
-
|
| 303 |
-
|
| 304 |
-
|
| 305 |
-
|
| 306 |
-
|
| 307 |
-
|
| 308 |
-
|
| 309 |
-
|
| 310 |
-
|
| 311 |
-
|
| 312 |
-
|
| 313 |
-
|
| 314 |
-
|
| 315 |
-
|
| 316 |
-
|
| 317 |
-
|
| 318 |
-
|
| 319 |
-
|
| 320 |
-
|
| 321 |
-
|
| 322 |
-
|
| 323 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 324 |
</div>
|
| 325 |
|
| 326 |
{/* Status Messages */}
|
| 327 |
{updateApiKeyMutation.isSuccess && (
|
| 328 |
-
<div className="flex items-center gap-2 p-3 bg-
|
| 329 |
-
<CheckCircle className="w-4 h-4 text-
|
| 330 |
-
<span className="text-sm text-
|
| 331 |
</div>
|
| 332 |
)}
|
| 333 |
</div>
|
| 334 |
-
|
| 335 |
-
|
| 336 |
-
</
|
| 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"}
|