Add dynamic auth configs, connect page, Next.js frontend
Browse filesThis view is limited to 50 files because it contains too many changes. See raw diff
- .gitignore +9 -5
- BUILD.md +142 -0
- backend/database.py +13 -0
- backend/integrations/registry.py +8 -0
- backend/main.py +8 -4
- backend/routers/auth_configs.py +293 -0
- backend/routers/connections.py +43 -11
- backend/static/assets/index-CIpcBIY5.js +0 -0
- backend/static/assets/index-DVvq7QG0.css +0 -1
- backend/static/index.html +1 -17
- frontend/app/analytics/page.tsx +59 -0
- frontend/app/api-keys/page.tsx +26 -0
- frontend/app/apps/page.tsx +95 -0
- frontend/app/connect/page.tsx +160 -0
- frontend/app/globals.css +22 -0
- frontend/app/layout.tsx +20 -0
- frontend/app/login/page.tsx +61 -0
- frontend/app/page.tsx +90 -0
- frontend/app/settings/page.tsx +40 -0
- frontend/app/tools/page.tsx +64 -0
- frontend/components/sidebar.tsx +93 -0
- frontend/index.html +0 -16
- frontend/lib/api.ts +55 -0
- frontend/lib/utils.ts +6 -0
- frontend/next-env.d.ts +5 -0
- frontend/next.config.js +9 -0
- frontend/package-lock.json +0 -0
- frontend/package.json +19 -23
- frontend/postcss.config.js +1 -1
- frontend/src/App.tsx +0 -44
- frontend/src/components/layout/Layout.tsx +0 -17
- frontend/src/components/layout/Sidebar.tsx +0 -109
- frontend/src/components/layout/TopBar.tsx +0 -27
- frontend/src/index.css +0 -76
- frontend/src/lib/api.ts +0 -30
- frontend/src/main.tsx +0 -10
- frontend/src/pages/APIKeys.tsx +0 -163
- frontend/src/pages/Analytics.tsx +0 -117
- frontend/src/pages/AppDetail.tsx +0 -217
- frontend/src/pages/Apps.tsx +0 -122
- frontend/src/pages/Connections.tsx +0 -132
- frontend/src/pages/Dashboard.tsx +0 -188
- frontend/src/pages/Login.tsx +0 -119
- frontend/src/pages/Playground.tsx +0 -107
- frontend/src/pages/Settings.tsx +0 -137
- frontend/src/pages/Tools.tsx +0 -100
- frontend/src/stores/authStore.ts +0 -54
- frontend/tailwind.config.ts +9 -12
- frontend/tsconfig.json +34 -16
- frontend/tsconfig.node.json +0 -10
.gitignore
CHANGED
|
@@ -1,7 +1,11 @@
|
|
| 1 |
-
node_modules
|
| 2 |
-
|
|
|
|
|
|
|
|
|
|
| 3 |
*.pyc
|
|
|
|
|
|
|
|
|
|
|
|
|
| 4 |
.env
|
| 5 |
-
dist
|
| 6 |
-
data
|
| 7 |
-
*.db
|
|
|
|
| 1 |
+
node_modules/
|
| 2 |
+
.next/
|
| 3 |
+
out/
|
| 4 |
+
dist/
|
| 5 |
+
__pycache__/
|
| 6 |
*.pyc
|
| 7 |
+
.venv/
|
| 8 |
+
*.egg-info/
|
| 9 |
+
backend/static/
|
| 10 |
+
data/
|
| 11 |
.env
|
|
|
|
|
|
|
|
|
BUILD.md
ADDED
|
@@ -0,0 +1,142 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
# Platform - Build & Test Guide
|
| 2 |
+
|
| 3 |
+
## Architecture
|
| 4 |
+
|
| 5 |
+
```
|
| 6 |
+
┌─────────────────────────────────────────────────────────┐
|
| 7 |
+
│ Platform Platform │
|
| 8 |
+
│ │
|
| 9 |
+
│ ┌─────────────┐ ┌──────────────┐ ┌────────────┐ │
|
| 10 |
+
│ │ React UI │◄──►│ FastAPI │◄──►│ SQLite │ │
|
| 11 |
+
│ │ (Vite) │ │ Backend │ │ Database │ │
|
| 12 |
+
│ └─────────────┘ └──────┬───────┘ └────────────┘ │
|
| 13 |
+
│ │ │
|
| 14 |
+
│ ▼ │
|
| 15 |
+
│ ┌─────────────────────────┐ │
|
| 16 |
+
│ │ Composio API │ │
|
| 17 |
+
│ │ backend.composio.dev │ │
|
| 18 |
+
│ │ /api/v3.1/toolkits │ │
|
| 19 |
+
│ │ /api/v3.1/tools │ │
|
| 20 |
+
│ └─────────────────────────┘ │
|
| 21 |
+
│ │ │
|
| 22 |
+
│ ▼ │
|
| 23 |
+
│ ┌─────────────────────────┐ │
|
| 24 |
+
│ │ Local Cache │ │
|
| 25 |
+
│ │ SQLite + JSON backup │ │
|
| 26 |
+
│ │ (/data/) │ │
|
| 27 |
+
│ └─────────────────────────┘ │
|
| 28 |
+
└─────────────────────────────────────────────────────────┘
|
| 29 |
+
```
|
| 30 |
+
|
| 31 |
+
## Local DB System
|
| 32 |
+
|
| 33 |
+
The platform uses a **dual-layer caching system**:
|
| 34 |
+
|
| 35 |
+
1. **SQLite Database** (`/data/platform.db`) - Primary store for integrations and tools
|
| 36 |
+
2. **JSON Backup** (`/data/composio_cache.json`) - Raw Composio API response backup
|
| 37 |
+
|
| 38 |
+
### How it works:
|
| 39 |
+
|
| 40 |
+
1. **Startup**: Tries to fetch from Composio API
|
| 41 |
+
2. **If API succeeds**: Saves to SQLite + JSON backup
|
| 42 |
+
3. **If API fails**: Loads from JSON backup → seeds SQLite
|
| 43 |
+
4. **If no cache**: Uses minimal fallback definitions (5 integrations)
|
| 44 |
+
|
| 45 |
+
### Cache TTL: 24 hours (configurable via `CACHE_TTL`)
|
| 46 |
+
|
| 47 |
+
## Tool Calls Quota
|
| 48 |
+
|
| 49 |
+
**Good news**: Fetching tools via `GET /api/v3.1/toolkits` and `GET /api/v3.1/tools` **does NOT consume your 20,000 free tool calls**. Only `POST /api/v3.1/tools/execute/{slug}` counts as a tool call.
|
| 50 |
+
|
| 51 |
+
## Build & Run
|
| 52 |
+
|
| 53 |
+
### Option 1: Docker (Recommended for HF Spaces)
|
| 54 |
+
|
| 55 |
+
```bash
|
| 56 |
+
cd /home/runner/workspace/platform
|
| 57 |
+
|
| 58 |
+
# Set your Composio API key
|
| 59 |
+
export COMPOSIO_API_KEY=sk_your_key_here
|
| 60 |
+
|
| 61 |
+
# Build
|
| 62 |
+
docker build -t platform .
|
| 63 |
+
|
| 64 |
+
# Run
|
| 65 |
+
docker run -p 7860:7860 \
|
| 66 |
+
-v $(pwd)/data:/data \
|
| 67 |
+
-e COMPOSIO_API_KEY=$COMPOSIO_API_KEY \
|
| 68 |
+
platform
|
| 69 |
+
```
|
| 70 |
+
|
| 71 |
+
### Option 2: Direct (Development)
|
| 72 |
+
|
| 73 |
+
```bash
|
| 74 |
+
cd /home/runner/workspace/platform
|
| 75 |
+
|
| 76 |
+
# Build frontend
|
| 77 |
+
cd frontend && npm install && npm run build && cd ..
|
| 78 |
+
|
| 79 |
+
# Install backend deps
|
| 80 |
+
cd backend && pip install -r requirements.txt && cd ..
|
| 81 |
+
|
| 82 |
+
# Set env vars
|
| 83 |
+
export COMPOSIO_API_KEY=sk_your_key_here
|
| 84 |
+
export DATA_DIR=$(pwd)/data
|
| 85 |
+
|
| 86 |
+
# Initialize and run
|
| 87 |
+
python -c "from backend.database import init_db; init_db()"
|
| 88 |
+
python backend/integrations/registry.py # Syncs tools
|
| 89 |
+
cd backend && uvicorn main:app --reload --port 7860
|
| 90 |
+
```
|
| 91 |
+
|
| 92 |
+
## Test
|
| 93 |
+
|
| 94 |
+
```bash
|
| 95 |
+
# After starting the server:
|
| 96 |
+
cd /home/runner/workspace/platform
|
| 97 |
+
chmod +x test.sh
|
| 98 |
+
./test.sh
|
| 99 |
+
```
|
| 100 |
+
|
| 101 |
+
## Hugging Face Spaces Deployment
|
| 102 |
+
|
| 103 |
+
1. Create a new Docker Space on HF
|
| 104 |
+
2. Push these files to the repo:
|
| 105 |
+
```
|
| 106 |
+
Dockerfile
|
| 107 |
+
entrypoint.sh
|
| 108 |
+
backend/
|
| 109 |
+
frontend/
|
| 110 |
+
.env.example
|
| 111 |
+
```
|
| 112 |
+
3. Add `COMPOSIO_API_KEY` as a Space secret
|
| 113 |
+
4. The Space will auto-build and start on port 7860
|
| 114 |
+
|
| 115 |
+
## API Endpoints
|
| 116 |
+
|
| 117 |
+
| Method | Path | Description |
|
| 118 |
+
|--------|------|-------------|
|
| 119 |
+
| GET | `/health` | Health check |
|
| 120 |
+
| GET | `/api/docs` | Swagger UI |
|
| 121 |
+
| POST | `/api/auth/register` | Register user |
|
| 122 |
+
| POST | `/api/auth/login` | Login |
|
| 123 |
+
| GET | `/api/apps` | List integrations |
|
| 124 |
+
| POST | `/api/apps/sync` | Sync from Composio API |
|
| 125 |
+
| GET | `/api/tools` | List tools |
|
| 126 |
+
| POST | `/api/tools/{id}/execute` | Execute tool |
|
| 127 |
+
| POST | `/api/tools/sync` | Sync tools from Composio |
|
| 128 |
+
| GET | `/api/connections` | List connections |
|
| 129 |
+
| GET | `/api/apikeys` | List API keys |
|
| 130 |
+
| GET | `/api/analytics/overview` | Usage stats |
|
| 131 |
+
| GET | `/mcp/v1/discovery` | MCP discovery |
|
| 132 |
+
| GET | `/mcp/v1/tools` | MCP tools list |
|
| 133 |
+
|
| 134 |
+
## Environment Variables
|
| 135 |
+
|
| 136 |
+
| Variable | Required | Default | Description |
|
| 137 |
+
|----------|----------|---------|-------------|
|
| 138 |
+
| `COMPOSIO_API_KEY` | No | - | Composio API key for dynamic sync |
|
| 139 |
+
| `COMPOSIO_API_BASE` | No | `https://backend.composio.dev/api/v3.1` | Composio API base URL |
|
| 140 |
+
| `SECRET_KEY` | No | (default) | JWT signing key |
|
| 141 |
+
| `DATA_DIR` | No | `/data` | Data directory |
|
| 142 |
+
| `CACHE_TTL` | No | `86400` | Cache TTL in seconds (24h) |
|
backend/database.py
CHANGED
|
@@ -51,6 +51,19 @@ class Integration(Base):
|
|
| 51 |
tools = relationship("Tool", back_populates="integration", cascade="all, delete-orphan")
|
| 52 |
|
| 53 |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 54 |
class Connection(Base):
|
| 55 |
__tablename__ = "connections"
|
| 56 |
id = Column(String, primary_key=True, default=lambda: str(uuid4()))
|
|
|
|
| 51 |
tools = relationship("Tool", back_populates="integration", cascade="all, delete-orphan")
|
| 52 |
|
| 53 |
|
| 54 |
+
class AuthConfig(Base):
|
| 55 |
+
"""Dynamic credential schema per integration - defines what fields are needed."""
|
| 56 |
+
__tablename__ = "auth_configs"
|
| 57 |
+
id = Column(String, primary_key=True, default=lambda: str(uuid4()))
|
| 58 |
+
integration_id = Column(String, ForeignKey("integrations.id"), nullable=False)
|
| 59 |
+
auth_type = Column(String, nullable=False) # oauth2, api_key, bearer, basic
|
| 60 |
+
fields = Column(JSON, nullable=False) # [{name, label, type, required, description, default}]
|
| 61 |
+
oauth_authorize_url = Column(String, nullable=True)
|
| 62 |
+
oauth_token_url = Column(String, nullable=True)
|
| 63 |
+
is_active = Column(Boolean, default=True)
|
| 64 |
+
integration = relationship("Integration")
|
| 65 |
+
|
| 66 |
+
|
| 67 |
class Connection(Base):
|
| 68 |
__tablename__ = "connections"
|
| 69 |
id = Column(String, primary_key=True, default=lambda: str(uuid4()))
|
backend/integrations/registry.py
CHANGED
|
@@ -249,6 +249,10 @@ class LocalDB:
|
|
| 249 |
|
| 250 |
db.commit()
|
| 251 |
|
|
|
|
|
|
|
|
|
|
|
|
|
| 252 |
# Save JSON backup
|
| 253 |
self.save_to_json(toolkits, tools)
|
| 254 |
|
|
@@ -339,6 +343,10 @@ def _seed_fallback():
|
|
| 339 |
|
| 340 |
db.commit()
|
| 341 |
print(f"[Fallback] Seeded {len(fallback_integrations)} integrations")
|
|
|
|
|
|
|
|
|
|
|
|
|
| 342 |
finally:
|
| 343 |
db.close()
|
| 344 |
|
|
|
|
| 249 |
|
| 250 |
db.commit()
|
| 251 |
|
| 252 |
+
# Seed default auth configs for all integrations
|
| 253 |
+
from backend.routers.auth_configs import seed_default_auth_configs
|
| 254 |
+
seed_default_auth_configs(db)
|
| 255 |
+
|
| 256 |
# Save JSON backup
|
| 257 |
self.save_to_json(toolkits, tools)
|
| 258 |
|
|
|
|
| 343 |
|
| 344 |
db.commit()
|
| 345 |
print(f"[Fallback] Seeded {len(fallback_integrations)} integrations")
|
| 346 |
+
|
| 347 |
+
# Also seed auth configs
|
| 348 |
+
from backend.routers.auth_configs import seed_default_auth_configs
|
| 349 |
+
seed_default_auth_configs(db)
|
| 350 |
finally:
|
| 351 |
db.close()
|
| 352 |
|
backend/main.py
CHANGED
|
@@ -16,8 +16,8 @@ async def lifespan(app: FastAPI):
|
|
| 16 |
|
| 17 |
|
| 18 |
app = FastAPI(
|
| 19 |
-
title="
|
| 20 |
-
description="Unified Tool Integration Platform",
|
| 21 |
version="1.0.0",
|
| 22 |
docs_url="/api/docs",
|
| 23 |
redoc_url="/api/redoc",
|
|
@@ -33,7 +33,7 @@ app.add_middleware(
|
|
| 33 |
)
|
| 34 |
app.add_middleware(GZipMiddleware, minimum_size=1000)
|
| 35 |
|
| 36 |
-
from backend.routers import auth, apps, tools, connections, apikeys, analytics, playground, settings, mcp
|
| 37 |
|
| 38 |
app.include_router(auth.router, prefix="/api/auth", tags=["auth"])
|
| 39 |
app.include_router(apps.router, prefix="/api/apps", tags=["apps"])
|
|
@@ -44,16 +44,20 @@ app.include_router(analytics.router, prefix="/api/analytics", tags=["analytics"]
|
|
| 44 |
app.include_router(playground.router, prefix="/api/playground", tags=["playground"])
|
| 45 |
app.include_router(settings.router, prefix="/api/settings", tags=["settings"])
|
| 46 |
app.include_router(mcp.router, prefix="/mcp", tags=["mcp"])
|
|
|
|
| 47 |
|
| 48 |
STATIC_DIR = os.path.join(os.path.dirname(__file__), "static")
|
| 49 |
|
| 50 |
if os.path.exists(STATIC_DIR):
|
| 51 |
-
app.mount("/
|
| 52 |
|
| 53 |
@app.get("/{full_path:path}")
|
| 54 |
async def serve_spa(full_path: str):
|
| 55 |
if full_path.startswith("api/") or full_path.startswith("mcp/"):
|
| 56 |
return JSONResponse({"detail": "Not found"}, status_code=404)
|
|
|
|
|
|
|
|
|
|
| 57 |
return FileResponse(os.path.join(STATIC_DIR, "index.html"))
|
| 58 |
|
| 59 |
@app.get("/")
|
|
|
|
| 16 |
|
| 17 |
|
| 18 |
app = FastAPI(
|
| 19 |
+
title="Compost API",
|
| 20 |
+
description="Unified AI Tool Integration Platform",
|
| 21 |
version="1.0.0",
|
| 22 |
docs_url="/api/docs",
|
| 23 |
redoc_url="/api/redoc",
|
|
|
|
| 33 |
)
|
| 34 |
app.add_middleware(GZipMiddleware, minimum_size=1000)
|
| 35 |
|
| 36 |
+
from backend.routers import auth, apps, tools, connections, apikeys, analytics, playground, settings, mcp, auth_configs
|
| 37 |
|
| 38 |
app.include_router(auth.router, prefix="/api/auth", tags=["auth"])
|
| 39 |
app.include_router(apps.router, prefix="/api/apps", tags=["apps"])
|
|
|
|
| 44 |
app.include_router(playground.router, prefix="/api/playground", tags=["playground"])
|
| 45 |
app.include_router(settings.router, prefix="/api/settings", tags=["settings"])
|
| 46 |
app.include_router(mcp.router, prefix="/mcp", tags=["mcp"])
|
| 47 |
+
app.include_router(auth_configs.router, prefix="/api/auth-configs", tags=["auth-configs"])
|
| 48 |
|
| 49 |
STATIC_DIR = os.path.join(os.path.dirname(__file__), "static")
|
| 50 |
|
| 51 |
if os.path.exists(STATIC_DIR):
|
| 52 |
+
app.mount("/_next", StaticFiles(directory=f"{STATIC_DIR}/_next"), name="next-assets")
|
| 53 |
|
| 54 |
@app.get("/{full_path:path}")
|
| 55 |
async def serve_spa(full_path: str):
|
| 56 |
if full_path.startswith("api/") or full_path.startswith("mcp/"):
|
| 57 |
return JSONResponse({"detail": "Not found"}, status_code=404)
|
| 58 |
+
static_file = os.path.join(STATIC_DIR, full_path)
|
| 59 |
+
if os.path.isfile(static_file):
|
| 60 |
+
return FileResponse(static_file)
|
| 61 |
return FileResponse(os.path.join(STATIC_DIR, "index.html"))
|
| 62 |
|
| 63 |
@app.get("/")
|
backend/routers/auth_configs.py
ADDED
|
@@ -0,0 +1,293 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
from typing import List, Optional
|
| 2 |
+
from fastapi import APIRouter, Depends, HTTPException
|
| 3 |
+
from sqlalchemy.orm import Session
|
| 4 |
+
from pydantic import BaseModel
|
| 5 |
+
from backend.database import get_db, AuthConfig, Integration
|
| 6 |
+
from backend.auth import get_current_user, User
|
| 7 |
+
|
| 8 |
+
router = APIRouter()
|
| 9 |
+
|
| 10 |
+
|
| 11 |
+
class AuthField(BaseModel):
|
| 12 |
+
name: str
|
| 13 |
+
label: str
|
| 14 |
+
type: str # text, password, url
|
| 15 |
+
required: bool = True
|
| 16 |
+
description: str = ""
|
| 17 |
+
default: str = ""
|
| 18 |
+
|
| 19 |
+
|
| 20 |
+
class AuthConfigCreate(BaseModel):
|
| 21 |
+
integration_id: str
|
| 22 |
+
auth_type: str
|
| 23 |
+
fields: List[AuthField]
|
| 24 |
+
oauth_authorize_url: Optional[str] = None
|
| 25 |
+
oauth_token_url: Optional[str] = None
|
| 26 |
+
|
| 27 |
+
|
| 28 |
+
class AuthConfigResponse(BaseModel):
|
| 29 |
+
id: str
|
| 30 |
+
integration_id: str
|
| 31 |
+
integration_name: str
|
| 32 |
+
auth_type: str
|
| 33 |
+
fields: list
|
| 34 |
+
oauth_authorize_url: Optional[str]
|
| 35 |
+
oauth_token_url: Optional[str]
|
| 36 |
+
is_active: bool
|
| 37 |
+
|
| 38 |
+
|
| 39 |
+
@router.get("/integration/{integration_id}", response_model=Optional[AuthConfigResponse])
|
| 40 |
+
async def get_auth_config(integration_id: str, db: Session = Depends(get_db)):
|
| 41 |
+
config = db.query(AuthConfig).filter(
|
| 42 |
+
AuthConfig.integration_id == integration_id,
|
| 43 |
+
AuthConfig.is_active == True
|
| 44 |
+
).first()
|
| 45 |
+
if not config:
|
| 46 |
+
return None
|
| 47 |
+
integration = db.query(Integration).filter(Integration.id == integration_id).first()
|
| 48 |
+
return AuthConfigResponse(
|
| 49 |
+
id=config.id,
|
| 50 |
+
integration_id=config.integration_id,
|
| 51 |
+
integration_name=integration.name if integration else integration_id,
|
| 52 |
+
auth_type=config.auth_type,
|
| 53 |
+
fields=config.fields,
|
| 54 |
+
oauth_authorize_url=config.oauth_authorize_url,
|
| 55 |
+
oauth_token_url=config.oauth_token_url,
|
| 56 |
+
is_active=config.is_active
|
| 57 |
+
)
|
| 58 |
+
|
| 59 |
+
|
| 60 |
+
@router.post("", response_model=AuthConfigResponse)
|
| 61 |
+
async def create_auth_config(
|
| 62 |
+
data: AuthConfigCreate,
|
| 63 |
+
current_user: User = Depends(get_current_user),
|
| 64 |
+
db: Session = Depends(get_db)
|
| 65 |
+
):
|
| 66 |
+
integration = db.query(Integration).filter(Integration.id == data.integration_id).first()
|
| 67 |
+
if not integration:
|
| 68 |
+
raise HTTPException(status_code=404, detail="Integration not found")
|
| 69 |
+
|
| 70 |
+
existing = db.query(AuthConfig).filter(
|
| 71 |
+
AuthConfig.integration_id == data.integration_id
|
| 72 |
+
).first()
|
| 73 |
+
if existing:
|
| 74 |
+
existing.auth_type = data.auth_type
|
| 75 |
+
existing.fields = [f.model_dump() for f in data.fields]
|
| 76 |
+
existing.oauth_authorize_url = data.oauth_authorize_url
|
| 77 |
+
existing.oauth_token_url = data.oauth_token_url
|
| 78 |
+
config = existing
|
| 79 |
+
else:
|
| 80 |
+
config = AuthConfig(
|
| 81 |
+
integration_id=data.integration_id,
|
| 82 |
+
auth_type=data.auth_type,
|
| 83 |
+
fields=[f.model_dump() for f in data.fields],
|
| 84 |
+
oauth_authorize_url=data.oauth_authorize_url,
|
| 85 |
+
oauth_token_url=data.oauth_token_url,
|
| 86 |
+
)
|
| 87 |
+
db.add(config)
|
| 88 |
+
|
| 89 |
+
db.commit()
|
| 90 |
+
db.refresh(config)
|
| 91 |
+
|
| 92 |
+
return AuthConfigResponse(
|
| 93 |
+
id=config.id,
|
| 94 |
+
integration_id=config.integration_id,
|
| 95 |
+
integration_name=integration.name,
|
| 96 |
+
auth_type=config.auth_type,
|
| 97 |
+
fields=config.fields,
|
| 98 |
+
oauth_authorize_url=config.oauth_authorize_url,
|
| 99 |
+
oauth_token_url=config.oauth_token_url,
|
| 100 |
+
is_active=config.is_active
|
| 101 |
+
)
|
| 102 |
+
|
| 103 |
+
|
| 104 |
+
@router.get("", response_model=List[AuthConfigResponse])
|
| 105 |
+
async def list_auth_configs(db: Session = Depends(get_db)):
|
| 106 |
+
configs = db.query(AuthConfig).filter(AuthConfig.is_active == True).all()
|
| 107 |
+
result = []
|
| 108 |
+
for c in configs:
|
| 109 |
+
integration = db.query(Integration).filter(Integration.id == c.integration_id).first()
|
| 110 |
+
result.append(AuthConfigResponse(
|
| 111 |
+
id=c.id,
|
| 112 |
+
integration_id=c.integration_id,
|
| 113 |
+
integration_name=integration.name if integration else c.integration_id,
|
| 114 |
+
auth_type=c.auth_type,
|
| 115 |
+
fields=c.fields,
|
| 116 |
+
oauth_authorize_url=c.oauth_authorize_url,
|
| 117 |
+
oauth_token_url=c.oauth_token_url,
|
| 118 |
+
is_active=c.is_active
|
| 119 |
+
))
|
| 120 |
+
return result
|
| 121 |
+
|
| 122 |
+
|
| 123 |
+
def seed_default_auth_configs(db: Session):
|
| 124 |
+
"""Seed default auth configs for all integrations."""
|
| 125 |
+
defaults = {
|
| 126 |
+
"github": {
|
| 127 |
+
"auth_type": "oauth2",
|
| 128 |
+
"fields": [
|
| 129 |
+
{"name": "client_id", "label": "Client ID", "type": "text", "required": True, "description": "GitHub OAuth App Client ID"},
|
| 130 |
+
{"name": "client_secret", "label": "Client Secret", "type": "password", "required": True, "description": "GitHub OAuth App Client Secret"},
|
| 131 |
+
],
|
| 132 |
+
"oauth_authorize_url": "https://github.com/login/oauth/authorize",
|
| 133 |
+
"oauth_token_url": "https://github.com/login/oauth/access_token",
|
| 134 |
+
},
|
| 135 |
+
"gmail": {
|
| 136 |
+
"auth_type": "oauth2",
|
| 137 |
+
"fields": [
|
| 138 |
+
{"name": "client_id", "label": "Client ID", "type": "text", "required": True, "description": "Google OAuth Client ID"},
|
| 139 |
+
{"name": "client_secret", "label": "Client Secret", "type": "password", "required": True, "description": "Google OAuth Client Secret"},
|
| 140 |
+
],
|
| 141 |
+
"oauth_authorize_url": "https://accounts.google.com/o/oauth2/v2/auth",
|
| 142 |
+
"oauth_token_url": "https://oauth2.googleapis.com/token",
|
| 143 |
+
},
|
| 144 |
+
"google_calendar": {
|
| 145 |
+
"auth_type": "oauth2",
|
| 146 |
+
"fields": [
|
| 147 |
+
{"name": "client_id", "label": "Client ID", "type": "text", "required": True, "description": "Google OAuth Client ID"},
|
| 148 |
+
{"name": "client_secret", "label": "Client Secret", "type": "password", "required": True, "description": "Google OAuth Client Secret"},
|
| 149 |
+
],
|
| 150 |
+
"oauth_authorize_url": "https://accounts.google.com/o/oauth2/v2/auth",
|
| 151 |
+
"oauth_token_url": "https://oauth2.googleapis.com/token",
|
| 152 |
+
},
|
| 153 |
+
"google_drive": {
|
| 154 |
+
"auth_type": "oauth2",
|
| 155 |
+
"fields": [
|
| 156 |
+
{"name": "client_id", "label": "Client ID", "type": "text", "required": True, "description": "Google OAuth Client ID"},
|
| 157 |
+
{"name": "client_secret", "label": "Client Secret", "type": "password", "required": True, "description": "Google OAuth Client Secret"},
|
| 158 |
+
],
|
| 159 |
+
"oauth_authorize_url": "https://accounts.google.com/o/oauth2/v2/auth",
|
| 160 |
+
"oauth_token_url": "https://oauth2.googleapis.com/token",
|
| 161 |
+
},
|
| 162 |
+
"slack": {
|
| 163 |
+
"auth_type": "oauth2",
|
| 164 |
+
"fields": [
|
| 165 |
+
{"name": "client_id", "label": "Client ID", "type": "text", "required": True, "description": "Slack App Client ID"},
|
| 166 |
+
{"name": "client_secret", "label": "Client Secret", "type": "password", "required": True, "description": "Slack App Client Secret"},
|
| 167 |
+
],
|
| 168 |
+
"oauth_authorize_url": "https://slack.com/oauth/v2/authorize",
|
| 169 |
+
"oauth_token_url": "https://slack.com/api/oauth.v2.access",
|
| 170 |
+
},
|
| 171 |
+
"notion": {
|
| 172 |
+
"auth_type": "oauth2",
|
| 173 |
+
"fields": [
|
| 174 |
+
{"name": "client_id", "label": "Client ID", "type": "text", "required": True, "description": "Notion Integration Client ID"},
|
| 175 |
+
{"name": "client_secret", "label": "Client Secret", "type": "password", "required": True, "description": "Notion Integration Client Secret"},
|
| 176 |
+
],
|
| 177 |
+
"oauth_authorize_url": "https://api.notion.com/v1/oauth/authorize",
|
| 178 |
+
"oauth_token_url": "https://api.notion.com/v1/oauth/token",
|
| 179 |
+
},
|
| 180 |
+
"linear": {
|
| 181 |
+
"auth_type": "oauth2",
|
| 182 |
+
"fields": [
|
| 183 |
+
{"name": "client_id", "label": "Client ID", "type": "text", "required": True, "description": "Linear OAuth Client ID"},
|
| 184 |
+
{"name": "client_secret", "label": "Client Secret", "type": "password", "required": True, "description": "Linear OAuth Client Secret"},
|
| 185 |
+
],
|
| 186 |
+
"oauth_authorize_url": "https://linear.app/oauth/authorize",
|
| 187 |
+
"oauth_token_url": "https://api.linear.app/oauth/token",
|
| 188 |
+
},
|
| 189 |
+
"discord": {
|
| 190 |
+
"auth_type": "oauth2",
|
| 191 |
+
"fields": [
|
| 192 |
+
{"name": "client_id", "label": "Client ID", "type": "text", "required": True, "description": "Discord App Client ID"},
|
| 193 |
+
{"name": "client_secret", "label": "Client Secret", "type": "password", "required": True, "description": "Discord App Client Secret"},
|
| 194 |
+
],
|
| 195 |
+
"oauth_authorize_url": "https://discord.com/api/oauth2/authorize",
|
| 196 |
+
"oauth_token_url": "https://discord.com/api/oauth2/token",
|
| 197 |
+
},
|
| 198 |
+
"hubspot": {
|
| 199 |
+
"auth_type": "oauth2",
|
| 200 |
+
"fields": [
|
| 201 |
+
{"name": "client_id", "label": "Client ID", "type": "text", "required": True, "description": "HubSpot App Client ID"},
|
| 202 |
+
{"name": "client_secret", "label": "Client Secret", "type": "password", "required": True, "description": "HubSpot App Client Secret"},
|
| 203 |
+
],
|
| 204 |
+
"oauth_authorize_url": "https://app.hubspot.com/oauth/authorize",
|
| 205 |
+
"oauth_token_url": "https://api.hubapi.com/oauth/v1/token",
|
| 206 |
+
},
|
| 207 |
+
"stripe": {
|
| 208 |
+
"auth_type": "api_key",
|
| 209 |
+
"fields": [
|
| 210 |
+
{"name": "api_key", "label": "Secret API Key", "type": "password", "required": True, "description": "Stripe Secret API Key (sk_...)"},
|
| 211 |
+
],
|
| 212 |
+
},
|
| 213 |
+
"salesforce": {
|
| 214 |
+
"auth_type": "oauth2",
|
| 215 |
+
"fields": [
|
| 216 |
+
{"name": "client_id", "label": "Consumer Key", "type": "text", "required": True, "description": "Salesforce Connected App Consumer Key"},
|
| 217 |
+
{"name": "client_secret", "label": "Consumer Secret", "type": "password", "required": True, "description": "Salesforce Connected App Consumer Secret"},
|
| 218 |
+
],
|
| 219 |
+
"oauth_authorize_url": "https://login.salesforce.com/services/oauth2/authorize",
|
| 220 |
+
"oauth_token_url": "https://login.salesforce.com/services/oauth2/token",
|
| 221 |
+
},
|
| 222 |
+
"airtable": {
|
| 223 |
+
"auth_type": "api_key",
|
| 224 |
+
"fields": [
|
| 225 |
+
{"name": "api_key", "label": "Personal Access Token", "type": "password", "required": True, "description": "Airtable Personal Access Token"},
|
| 226 |
+
],
|
| 227 |
+
},
|
| 228 |
+
"trello": {
|
| 229 |
+
"auth_type": "api_key",
|
| 230 |
+
"fields": [
|
| 231 |
+
{"name": "api_key", "label": "API Key", "type": "password", "required": True, "description": "Trello API Key"},
|
| 232 |
+
{"name": "token", "label": "Token", "type": "password", "required": True, "description": "Trello Token"},
|
| 233 |
+
],
|
| 234 |
+
},
|
| 235 |
+
"asana": {
|
| 236 |
+
"auth_type": "oauth2",
|
| 237 |
+
"fields": [
|
| 238 |
+
{"name": "client_id", "label": "Client ID", "type": "text", "required": True, "description": "Asana App Client ID"},
|
| 239 |
+
{"name": "client_secret", "label": "Client Secret", "type": "password", "required": True, "description": "Asana App Client Secret"},
|
| 240 |
+
],
|
| 241 |
+
"oauth_authorize_url": "https://app.asana.com/-/oauth_authorize",
|
| 242 |
+
"oauth_token_url": "https://app.asana.com/-/oauth_token",
|
| 243 |
+
},
|
| 244 |
+
"zendesk": {
|
| 245 |
+
"auth_type": "api_key",
|
| 246 |
+
"fields": [
|
| 247 |
+
{"name": "api_key", "label": "API Token", "type": "password", "required": True, "description": "Zendesk API Token"},
|
| 248 |
+
{"name": "subdomain", "label": "Subdomain", "type": "text", "required": True, "description": "Your Zendesk subdomain"},
|
| 249 |
+
],
|
| 250 |
+
},
|
| 251 |
+
"twilio": {
|
| 252 |
+
"auth_type": "api_key",
|
| 253 |
+
"fields": [
|
| 254 |
+
{"name": "account_sid", "label": "Account SID", "type": "text", "required": True, "description": "Twilio Account SID"},
|
| 255 |
+
{"name": "auth_token", "label": "Auth Token", "type": "password", "required": True, "description": "Twilio Auth Token"},
|
| 256 |
+
],
|
| 257 |
+
},
|
| 258 |
+
"sendgrid": {
|
| 259 |
+
"auth_type": "api_key",
|
| 260 |
+
"fields": [
|
| 261 |
+
{"name": "api_key", "label": "API Key", "type": "password", "required": True, "description": "SendGrid API Key"},
|
| 262 |
+
],
|
| 263 |
+
},
|
| 264 |
+
"twitter": {
|
| 265 |
+
"auth_type": "oauth2",
|
| 266 |
+
"fields": [
|
| 267 |
+
{"name": "client_id", "label": "Client ID", "type": "text", "required": True, "description": "Twitter App Client ID"},
|
| 268 |
+
{"name": "client_secret", "label": "Client Secret", "type": "password", "required": True, "description": "Twitter App Client Secret"},
|
| 269 |
+
],
|
| 270 |
+
"oauth_authorize_url": "https://twitter.com/i/oauth2/authorize",
|
| 271 |
+
"oauth_token_url": "https://api.twitter.com/2/oauth2/token",
|
| 272 |
+
},
|
| 273 |
+
"jira": {
|
| 274 |
+
"auth_type": "api_key",
|
| 275 |
+
"fields": [
|
| 276 |
+
{"name": "email", "label": "Email", "type": "text", "required": True, "description": "Atlassian Account Email"},
|
| 277 |
+
{"name": "api_token", "label": "API Token", "type": "password", "required": True, "description": "Jira API Token"},
|
| 278 |
+
{"name": "domain", "label": "Domain", "type": "text", "required": True, "description": "Your Jira domain (e.g. yourcompany.atlassian.net)"},
|
| 279 |
+
],
|
| 280 |
+
},
|
| 281 |
+
}
|
| 282 |
+
|
| 283 |
+
for int_id, cfg in defaults.items():
|
| 284 |
+
existing = db.query(AuthConfig).filter(AuthConfig.integration_id == int_id).first()
|
| 285 |
+
if not existing:
|
| 286 |
+
db.add(AuthConfig(
|
| 287 |
+
integration_id=int_id,
|
| 288 |
+
auth_type=cfg["auth_type"],
|
| 289 |
+
fields=cfg["fields"],
|
| 290 |
+
oauth_authorize_url=cfg.get("oauth_authorize_url"),
|
| 291 |
+
oauth_token_url=cfg.get("oauth_token_url"),
|
| 292 |
+
))
|
| 293 |
+
db.commit()
|
backend/routers/connections.py
CHANGED
|
@@ -2,7 +2,7 @@ from typing import Optional, List
|
|
| 2 |
from fastapi import APIRouter, Depends, HTTPException, Query
|
| 3 |
from sqlalchemy.orm import Session
|
| 4 |
from pydantic import BaseModel
|
| 5 |
-
from backend.database import get_db, Connection, Integration
|
| 6 |
from backend.auth import get_current_user, User
|
| 7 |
from backend.vault import vault
|
| 8 |
from datetime import datetime
|
|
@@ -21,11 +21,29 @@ class ConnectionResponse(BaseModel):
|
|
| 21 |
metadata: Optional[dict]
|
| 22 |
|
| 23 |
|
| 24 |
-
class
|
| 25 |
-
|
| 26 |
account_label: Optional[str] = None
|
| 27 |
|
| 28 |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 29 |
@router.get("", response_model=List[ConnectionResponse])
|
| 30 |
async def list_connections(
|
| 31 |
current_user: User = Depends(get_current_user),
|
|
@@ -77,28 +95,42 @@ async def get_connection(
|
|
| 77 |
)
|
| 78 |
|
| 79 |
|
| 80 |
-
@router.post("/
|
| 81 |
-
async def
|
| 82 |
-
request:
|
| 83 |
integration_id: str = Query(...),
|
| 84 |
current_user: User = Depends(get_current_user),
|
| 85 |
db: Session = Depends(get_db)
|
| 86 |
):
|
|
|
|
| 87 |
integration = db.query(Integration).filter(Integration.id == integration_id).first()
|
| 88 |
if not integration:
|
| 89 |
raise HTTPException(status_code=404, detail="Integration not found")
|
| 90 |
-
|
| 91 |
-
|
| 92 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 93 |
import uuid
|
| 94 |
connection = Connection(
|
| 95 |
id=str(uuid.uuid4()),
|
| 96 |
user_id=current_user.id,
|
| 97 |
integration_id=integration_id,
|
| 98 |
-
account_label=request.account_label or f"{integration.name}
|
| 99 |
status="active",
|
| 100 |
encrypted_credentials=encrypted,
|
| 101 |
-
conn_metadata={"
|
| 102 |
)
|
| 103 |
db.add(connection)
|
| 104 |
db.commit()
|
|
|
|
| 2 |
from fastapi import APIRouter, Depends, HTTPException, Query
|
| 3 |
from sqlalchemy.orm import Session
|
| 4 |
from pydantic import BaseModel
|
| 5 |
+
from backend.database import get_db, Connection, Integration, AuthConfig
|
| 6 |
from backend.auth import get_current_user, User
|
| 7 |
from backend.vault import vault
|
| 8 |
from datetime import datetime
|
|
|
|
| 21 |
metadata: Optional[dict]
|
| 22 |
|
| 23 |
|
| 24 |
+
class ConnectRequest(BaseModel):
|
| 25 |
+
credentials: dict # Dynamic - matches auth config fields
|
| 26 |
account_label: Optional[str] = None
|
| 27 |
|
| 28 |
|
| 29 |
+
@router.get("/auth-config/{integration_id}")
|
| 30 |
+
async def get_connection_auth_config(integration_id: str, db: Session = Depends(get_db)):
|
| 31 |
+
"""Get the auth config for connecting to an integration."""
|
| 32 |
+
config = db.query(AuthConfig).filter(
|
| 33 |
+
AuthConfig.integration_id == integration_id,
|
| 34 |
+
AuthConfig.is_active == True
|
| 35 |
+
).first()
|
| 36 |
+
if not config:
|
| 37 |
+
raise HTTPException(status_code=404, detail="No auth config found for this integration")
|
| 38 |
+
return {
|
| 39 |
+
"integration_id": integration_id,
|
| 40 |
+
"auth_type": config.auth_type,
|
| 41 |
+
"fields": config.fields,
|
| 42 |
+
"oauth_authorize_url": config.oauth_authorize_url,
|
| 43 |
+
"oauth_token_url": config.oauth_token_url,
|
| 44 |
+
}
|
| 45 |
+
|
| 46 |
+
|
| 47 |
@router.get("", response_model=List[ConnectionResponse])
|
| 48 |
async def list_connections(
|
| 49 |
current_user: User = Depends(get_current_user),
|
|
|
|
| 95 |
)
|
| 96 |
|
| 97 |
|
| 98 |
+
@router.post("/connect")
|
| 99 |
+
async def connect_integration(
|
| 100 |
+
request: ConnectRequest,
|
| 101 |
integration_id: str = Query(...),
|
| 102 |
current_user: User = Depends(get_current_user),
|
| 103 |
db: Session = Depends(get_db)
|
| 104 |
):
|
| 105 |
+
"""Connect to an integration with dynamic credentials."""
|
| 106 |
integration = db.query(Integration).filter(Integration.id == integration_id).first()
|
| 107 |
if not integration:
|
| 108 |
raise HTTPException(status_code=404, detail="Integration not found")
|
| 109 |
+
|
| 110 |
+
# Validate auth config exists
|
| 111 |
+
config = db.query(AuthConfig).filter(
|
| 112 |
+
AuthConfig.integration_id == integration_id,
|
| 113 |
+
AuthConfig.is_active == True
|
| 114 |
+
).first()
|
| 115 |
+
if not config:
|
| 116 |
+
raise HTTPException(status_code=400, detail="No auth config found. Configure credentials first.")
|
| 117 |
+
|
| 118 |
+
# Validate required fields
|
| 119 |
+
for field in config.fields:
|
| 120 |
+
if field.get("required") and field["name"] not in request.credentials:
|
| 121 |
+
raise HTTPException(status_code=400, detail=f"Missing required field: {field['label']}")
|
| 122 |
+
|
| 123 |
+
encrypted = vault.store_credentials(request.credentials)
|
| 124 |
+
|
| 125 |
import uuid
|
| 126 |
connection = Connection(
|
| 127 |
id=str(uuid.uuid4()),
|
| 128 |
user_id=current_user.id,
|
| 129 |
integration_id=integration_id,
|
| 130 |
+
account_label=request.account_label or f"{integration.name}",
|
| 131 |
status="active",
|
| 132 |
encrypted_credentials=encrypted,
|
| 133 |
+
conn_metadata={"auth_type": config.auth_type}
|
| 134 |
)
|
| 135 |
db.add(connection)
|
| 136 |
db.commit()
|
backend/static/assets/index-CIpcBIY5.js
DELETED
|
The diff for this file is too large to render.
See raw diff
|
|
|
backend/static/assets/index-DVvq7QG0.css
DELETED
|
@@ -1 +0,0 @@
|
|
| 1 |
-
*,:before,:after{--tw-border-spacing-x: 0;--tw-border-spacing-y: 0;--tw-translate-x: 0;--tw-translate-y: 0;--tw-rotate: 0;--tw-skew-x: 0;--tw-skew-y: 0;--tw-scale-x: 1;--tw-scale-y: 1;--tw-pan-x: ;--tw-pan-y: ;--tw-pinch-zoom: ;--tw-scroll-snap-strictness: proximity;--tw-gradient-from-position: ;--tw-gradient-via-position: ;--tw-gradient-to-position: ;--tw-ordinal: ;--tw-slashed-zero: ;--tw-numeric-figure: ;--tw-numeric-spacing: ;--tw-numeric-fraction: ;--tw-ring-inset: ;--tw-ring-offset-width: 0px;--tw-ring-offset-color: #fff;--tw-ring-color: rgb(59 130 246 / .5);--tw-ring-offset-shadow: 0 0 #0000;--tw-ring-shadow: 0 0 #0000;--tw-shadow: 0 0 #0000;--tw-shadow-colored: 0 0 #0000;--tw-blur: ;--tw-brightness: ;--tw-contrast: ;--tw-grayscale: ;--tw-hue-rotate: ;--tw-invert: ;--tw-saturate: ;--tw-sepia: ;--tw-drop-shadow: ;--tw-backdrop-blur: ;--tw-backdrop-brightness: ;--tw-backdrop-contrast: ;--tw-backdrop-grayscale: ;--tw-backdrop-hue-rotate: ;--tw-backdrop-invert: ;--tw-backdrop-opacity: ;--tw-backdrop-saturate: ;--tw-backdrop-sepia: ;--tw-contain-size: ;--tw-contain-layout: ;--tw-contain-paint: ;--tw-contain-style: }::backdrop{--tw-border-spacing-x: 0;--tw-border-spacing-y: 0;--tw-translate-x: 0;--tw-translate-y: 0;--tw-rotate: 0;--tw-skew-x: 0;--tw-skew-y: 0;--tw-scale-x: 1;--tw-scale-y: 1;--tw-pan-x: ;--tw-pan-y: ;--tw-pinch-zoom: ;--tw-scroll-snap-strictness: proximity;--tw-gradient-from-position: ;--tw-gradient-via-position: ;--tw-gradient-to-position: ;--tw-ordinal: ;--tw-slashed-zero: ;--tw-numeric-figure: ;--tw-numeric-spacing: ;--tw-numeric-fraction: ;--tw-ring-inset: ;--tw-ring-offset-width: 0px;--tw-ring-offset-color: #fff;--tw-ring-color: rgb(59 130 246 / .5);--tw-ring-offset-shadow: 0 0 #0000;--tw-ring-shadow: 0 0 #0000;--tw-shadow: 0 0 #0000;--tw-shadow-colored: 0 0 #0000;--tw-blur: ;--tw-brightness: ;--tw-contrast: ;--tw-grayscale: ;--tw-hue-rotate: ;--tw-invert: ;--tw-saturate: ;--tw-sepia: ;--tw-drop-shadow: ;--tw-backdrop-blur: ;--tw-backdrop-brightness: ;--tw-backdrop-contrast: ;--tw-backdrop-grayscale: ;--tw-backdrop-hue-rotate: ;--tw-backdrop-invert: ;--tw-backdrop-opacity: ;--tw-backdrop-saturate: ;--tw-backdrop-sepia: ;--tw-contain-size: ;--tw-contain-layout: ;--tw-contain-paint: ;--tw-contain-style: }*,:before,:after{box-sizing:border-box;border-width:0;border-style:solid;border-color:#e5e7eb}:before,:after{--tw-content: ""}html,:host{line-height:1.5;-webkit-text-size-adjust:100%;-moz-tab-size:4;-o-tab-size:4;tab-size:4;font-family:Inter,system-ui,sans-serif;font-feature-settings:normal;font-variation-settings:normal;-webkit-tap-highlight-color:transparent}body{margin:0;line-height:inherit}hr{height:0;color:inherit;border-top-width:1px}abbr:where([title]){-webkit-text-decoration:underline dotted;text-decoration:underline dotted}h1,h2,h3,h4,h5,h6{font-size:inherit;font-weight:inherit}a{color:inherit;text-decoration:inherit}b,strong{font-weight:bolder}code,kbd,samp,pre{font-family:ui-monospace,SFMono-Regular,Menlo,Monaco,Consolas,Liberation Mono,Courier New,monospace;font-feature-settings:normal;font-variation-settings:normal;font-size:1em}small{font-size:80%}sub,sup{font-size:75%;line-height:0;position:relative;vertical-align:baseline}sub{bottom:-.25em}sup{top:-.5em}table{text-indent:0;border-color:inherit;border-collapse:collapse}button,input,optgroup,select,textarea{font-family:inherit;font-feature-settings:inherit;font-variation-settings:inherit;font-size:100%;font-weight:inherit;line-height:inherit;letter-spacing:inherit;color:inherit;margin:0;padding:0}button,select{text-transform:none}button,input:where([type=button]),input:where([type=reset]),input:where([type=submit]){-webkit-appearance:button;background-color:transparent;background-image:none}:-moz-focusring{outline:auto}:-moz-ui-invalid{box-shadow:none}progress{vertical-align:baseline}::-webkit-inner-spin-button,::-webkit-outer-spin-button{height:auto}[type=search]{-webkit-appearance:textfield;outline-offset:-2px}::-webkit-search-decoration{-webkit-appearance:none}::-webkit-file-upload-button{-webkit-appearance:button;font:inherit}summary{display:list-item}blockquote,dl,dd,h1,h2,h3,h4,h5,h6,hr,figure,p,pre{margin:0}fieldset{margin:0;padding:0}legend{padding:0}ol,ul,menu{list-style:none;margin:0;padding:0}dialog{padding:0}textarea{resize:vertical}input::-moz-placeholder,textarea::-moz-placeholder{opacity:1;color:#9ca3af}input::placeholder,textarea::placeholder{opacity:1;color:#9ca3af}button,[role=button]{cursor:pointer}:disabled{cursor:default}img,svg,video,canvas,audio,iframe,embed,object{display:block;vertical-align:middle}img,video{max-width:100%;height:auto}[hidden]:where(:not([hidden=until-found])){display:none}.btn{display:flex;align-items:center;gap:.5rem;border-radius:.5rem;padding:.5rem 1rem;font-weight:500;transition-property:all;transition-timing-function:cubic-bezier(.4,0,.2,1);transition-duration:.2s}.btn-primary{--tw-bg-opacity: 1;background-color:rgb(6 182 212 / var(--tw-bg-opacity, 1));--tw-text-opacity: 1;color:rgb(15 23 42 / var(--tw-text-opacity, 1))}.btn-primary:hover{--tw-bg-opacity: 1;background-color:rgb(34 211 238 / var(--tw-bg-opacity, 1))}.btn-secondary{--tw-bg-opacity: 1;background-color:rgb(39 39 42 / var(--tw-bg-opacity, 1));--tw-text-opacity: 1;color:rgb(244 244 245 / var(--tw-text-opacity, 1))}.btn-secondary:hover{--tw-bg-opacity: 1;background-color:rgb(63 63 70 / var(--tw-bg-opacity, 1))}.btn-ghost{background-color:transparent;--tw-text-opacity: 1;color:rgb(212 212 216 / var(--tw-text-opacity, 1))}.btn-ghost:hover{--tw-bg-opacity: 1;background-color:rgb(39 39 42 / var(--tw-bg-opacity, 1))}.card{border-radius:.75rem;border-width:1px;--tw-border-opacity: 1;border-color:rgb(39 39 42 / var(--tw-border-opacity, 1));background-color:#18181b80;padding:1rem}.input{border-radius:.5rem;border-width:1px;--tw-border-opacity: 1;border-color:rgb(39 39 42 / var(--tw-border-opacity, 1));--tw-bg-opacity: 1;background-color:rgb(24 24 27 / var(--tw-bg-opacity, 1));padding:.5rem .75rem;--tw-text-opacity: 1;color:rgb(244 244 245 / var(--tw-text-opacity, 1))}.input::-moz-placeholder{--tw-placeholder-opacity: 1;color:rgb(113 113 122 / var(--tw-placeholder-opacity, 1))}.input::placeholder{--tw-placeholder-opacity: 1;color:rgb(113 113 122 / var(--tw-placeholder-opacity, 1))}.input{transition-property:color,background-color,border-color,text-decoration-color,fill,stroke;transition-timing-function:cubic-bezier(.4,0,.2,1);transition-duration:.15s}.input:focus{--tw-border-opacity: 1;border-color:rgb(6 182 212 / var(--tw-border-opacity, 1));outline:2px solid transparent;outline-offset:2px}.badge{border-radius:9999px;padding:.125rem .5rem;font-size:.75rem;line-height:1rem;font-weight:500}.absolute{position:absolute}.relative{position:relative}.left-3{left:.75rem}.top-1\/2{top:50%}.mx-auto{margin-left:auto;margin-right:auto}.-mb-px{margin-bottom:-1px}.mb-1{margin-bottom:.25rem}.mb-2{margin-bottom:.5rem}.mb-3{margin-bottom:.75rem}.mb-4{margin-bottom:1rem}.mb-6{margin-bottom:1.5rem}.mb-8{margin-bottom:2rem}.mt-0\.5{margin-top:.125rem}.mt-1{margin-top:.25rem}.mt-2{margin-top:.5rem}.mt-3{margin-top:.75rem}.mt-4{margin-top:1rem}.mt-6{margin-top:1.5rem}.line-clamp-2{overflow:hidden;display:-webkit-box;-webkit-box-orient:vertical;-webkit-line-clamp:2}.block{display:block}.flex{display:flex}.table{display:table}.grid{display:grid}.h-10{height:2.5rem}.h-14{height:3.5rem}.h-16{height:4rem}.h-2{height:.5rem}.h-3{height:.75rem}.h-4{height:1rem}.h-48{height:12rem}.h-5{height:1.25rem}.h-6{height:1.5rem}.h-64{height:16rem}.h-8{height:2rem}.h-\[400px\]{height:400px}.h-full{height:100%}.h-screen{height:100vh}.min-h-screen{min-height:100vh}.w-10{width:2.5rem}.w-16{width:4rem}.w-3{width:.75rem}.w-4{width:1rem}.w-5{width:1.25rem}.w-6{width:1.5rem}.w-64{width:16rem}.w-8{width:2rem}.w-full{width:100%}.min-w-0{min-width:0px}.max-w-\[120px\]{max-width:120px}.max-w-md{max-width:28rem}.flex-1{flex:1 1 0%}.-translate-y-1\/2{--tw-translate-y: -50%;transform:translate(var(--tw-translate-x),var(--tw-translate-y)) rotate(var(--tw-rotate)) skew(var(--tw-skew-x)) skewY(var(--tw-skew-y)) scaleX(var(--tw-scale-x)) scaleY(var(--tw-scale-y))}.cursor-pointer{cursor:pointer}.grid-cols-1{grid-template-columns:repeat(1,minmax(0,1fr))}.flex-col{flex-direction:column}.flex-wrap{flex-wrap:wrap}.items-start{align-items:flex-start}.items-center{align-items:center}.justify-end{justify-content:flex-end}.justify-center{justify-content:center}.justify-between{justify-content:space-between}.gap-1{gap:.25rem}.gap-2{gap:.5rem}.gap-3{gap:.75rem}.gap-4{gap:1rem}.gap-6{gap:1.5rem}.space-y-1>:not([hidden])~:not([hidden]){--tw-space-y-reverse: 0;margin-top:calc(.25rem * calc(1 - var(--tw-space-y-reverse)));margin-bottom:calc(.25rem * var(--tw-space-y-reverse))}.space-y-3>:not([hidden])~:not([hidden]){--tw-space-y-reverse: 0;margin-top:calc(.75rem * calc(1 - var(--tw-space-y-reverse)));margin-bottom:calc(.75rem * var(--tw-space-y-reverse))}.space-y-4>:not([hidden])~:not([hidden]){--tw-space-y-reverse: 0;margin-top:calc(1rem * calc(1 - var(--tw-space-y-reverse)));margin-bottom:calc(1rem * var(--tw-space-y-reverse))}.space-y-6>:not([hidden])~:not([hidden]){--tw-space-y-reverse: 0;margin-top:calc(1.5rem * calc(1 - var(--tw-space-y-reverse)));margin-bottom:calc(1.5rem * var(--tw-space-y-reverse))}.divide-y>:not([hidden])~:not([hidden]){--tw-divide-y-reverse: 0;border-top-width:calc(1px * calc(1 - var(--tw-divide-y-reverse)));border-bottom-width:calc(1px * var(--tw-divide-y-reverse))}.divide-\[var\(--border\)\]>:not([hidden])~:not([hidden]){border-color:var(--border)}.overflow-auto{overflow:auto}.overflow-hidden{overflow:hidden}.truncate{overflow:hidden;text-overflow:ellipsis;white-space:nowrap}.break-all{word-break:break-all}.rounded{border-radius:.25rem}.rounded-2xl{border-radius:1rem}.rounded-full{border-radius:9999px}.rounded-lg{border-radius:.5rem}.rounded-xl{border-radius:.75rem}.border{border-width:1px}.border-b{border-bottom-width:1px}.border-b-2{border-bottom-width:2px}.border-r{border-right-width:1px}.border-t{border-top-width:1px}.border-\[var\(--border\)\]{border-color:var(--border)}.border-amber-500\/50{border-color:#f59e0b80}.border-cyan-500{--tw-border-opacity: 1;border-color:rgb(6 182 212 / var(--tw-border-opacity, 1))}.border-red-500\/30{border-color:#ef44444d}.border-transparent{border-color:transparent}.bg-\[var\(--bg-primary\)\]{background-color:var(--bg-primary)}.bg-\[var\(--bg-secondary\)\]{background-color:var(--bg-secondary)}.bg-amber-500{--tw-bg-opacity: 1;background-color:rgb(245 158 11 / var(--tw-bg-opacity, 1))}.bg-amber-500\/10{background-color:#f59e0b1a}.bg-amber-500\/5{background-color:#f59e0b0d}.bg-blue-500\/10{background-color:#3b82f61a}.bg-cyan-500{--tw-bg-opacity: 1;background-color:rgb(6 182 212 / var(--tw-bg-opacity, 1))}.bg-cyan-500\/10{background-color:#06b6d41a}.bg-emerald-500{--tw-bg-opacity: 1;background-color:rgb(16 185 129 / var(--tw-bg-opacity, 1))}.bg-emerald-500\/10{background-color:#10b9811a}.bg-red-500\/10{background-color:#ef44441a}.bg-zinc-700{--tw-bg-opacity: 1;background-color:rgb(63 63 70 / var(--tw-bg-opacity, 1))}.bg-zinc-800{--tw-bg-opacity: 1;background-color:rgb(39 39 42 / var(--tw-bg-opacity, 1))}.bg-zinc-800\/50{background-color:#27272a80}.bg-zinc-950{--tw-bg-opacity: 1;background-color:rgb(9 9 11 / var(--tw-bg-opacity, 1))}.p-0{padding:0}.p-2{padding:.5rem}.p-3{padding:.75rem}.p-4{padding:1rem}.p-6{padding:1.5rem}.px-2{padding-left:.5rem;padding-right:.5rem}.px-3{padding-left:.75rem;padding-right:.75rem}.px-4{padding-left:1rem;padding-right:1rem}.px-6{padding-left:1.5rem;padding-right:1.5rem}.py-1{padding-top:.25rem;padding-bottom:.25rem}.py-1\.5{padding-top:.375rem;padding-bottom:.375rem}.py-12{padding-top:3rem;padding-bottom:3rem}.py-2{padding-top:.5rem;padding-bottom:.5rem}.py-3{padding-top:.75rem;padding-bottom:.75rem}.py-4{padding-top:1rem;padding-bottom:1rem}.py-8{padding-top:2rem;padding-bottom:2rem}.pl-10{padding-left:2.5rem}.pr-4{padding-right:1rem}.pt-3{padding-top:.75rem}.text-left{text-align:left}.text-center{text-align:center}.text-right{text-align:right}.font-mono{font-family:ui-monospace,SFMono-Regular,Menlo,Monaco,Consolas,Liberation Mono,Courier New,monospace}.text-2xl{font-size:1.5rem;line-height:2rem}.text-lg{font-size:1.125rem;line-height:1.75rem}.text-sm{font-size:.875rem;line-height:1.25rem}.text-xs{font-size:.75rem;line-height:1rem}.font-medium{font-weight:500}.font-semibold{font-weight:600}.uppercase{text-transform:uppercase}.capitalize{text-transform:capitalize}.tracking-wider{letter-spacing:.05em}.text-\[var\(--text-muted\)\]{color:var(--text-muted)}.text-\[var\(--text-secondary\)\]{color:var(--text-secondary)}.text-amber-400{--tw-text-opacity: 1;color:rgb(251 191 36 / var(--tw-text-opacity, 1))}.text-blue-400{--tw-text-opacity: 1;color:rgb(96 165 250 / var(--tw-text-opacity, 1))}.text-cyan-400{--tw-text-opacity: 1;color:rgb(34 211 238 / var(--tw-text-opacity, 1))}.text-emerald-400{--tw-text-opacity: 1;color:rgb(52 211 153 / var(--tw-text-opacity, 1))}.text-red-400{--tw-text-opacity: 1;color:rgb(248 113 113 / var(--tw-text-opacity, 1))}.text-slate-900{--tw-text-opacity: 1;color:rgb(15 23 42 / var(--tw-text-opacity, 1))}.text-white{--tw-text-opacity: 1;color:rgb(255 255 255 / var(--tw-text-opacity, 1))}.placeholder-\[var\(--text-muted\)\]::-moz-placeholder{color:var(--text-muted)}.placeholder-\[var\(--text-muted\)\]::placeholder{color:var(--text-muted)}.filter{filter:var(--tw-blur) var(--tw-brightness) var(--tw-contrast) var(--tw-grayscale) var(--tw-hue-rotate) var(--tw-invert) var(--tw-saturate) var(--tw-sepia) var(--tw-drop-shadow)}.transition-all{transition-property:all;transition-timing-function:cubic-bezier(.4,0,.2,1);transition-duration:.15s}.transition-colors{transition-property:color,background-color,border-color,text-decoration-color,fill,stroke;transition-timing-function:cubic-bezier(.4,0,.2,1);transition-duration:.15s}:root{--bg-primary: #0a0a0b;--bg-secondary: #111113;--bg-tertiary: #18181b;--bg-hover: #1f1f23;--border: #27272a;--border-hover: #3f3f46;--text-primary: #fafafa;--text-secondary: #a1a1aa;--text-muted: #71717a;--accent: #22d3ee;--accent-hover: #06b6d4;--accent-green: #10b981;--accent-red: #ef4444;--accent-yellow: #f59e0b;--accent-blue: #3b82f6}*{box-sizing:border-box}body{margin:0;font-family:Inter,system-ui,sans-serif;background-color:var(--bg-primary);color:var(--text-primary);-webkit-font-smoothing:antialiased;-moz-osx-font-smoothing:grayscale}::-webkit-scrollbar{width:8px;height:8px}::-webkit-scrollbar-track{background:var(--bg-secondary)}::-webkit-scrollbar-thumb{background:var(--border);border-radius:4px}::-webkit-scrollbar-thumb:hover{background:var(--border-hover)}.last\:border-0:last-child{border-width:0px}.hover\:-translate-y-1:hover{--tw-translate-y: -.25rem;transform:translate(var(--tw-translate-x),var(--tw-translate-y)) rotate(var(--tw-rotate)) skew(var(--tw-skew-x)) skewY(var(--tw-skew-y)) scaleX(var(--tw-scale-x)) scaleY(var(--tw-scale-y))}.hover\:transform:hover{transform:translate(var(--tw-translate-x),var(--tw-translate-y)) rotate(var(--tw-rotate)) skew(var(--tw-skew-x)) skewY(var(--tw-skew-y)) scaleX(var(--tw-scale-x)) scaleY(var(--tw-scale-y))}.hover\:border-cyan-500\/30:hover{border-color:#06b6d44d}.hover\:border-cyan-500\/50:hover{border-color:#06b6d480}.hover\:bg-\[var\(--bg-hover\)\]:hover{background-color:var(--bg-hover)}.hover\:bg-cyan-500\/10:hover{background-color:#06b6d41a}.hover\:bg-red-500\/10:hover{background-color:#ef44441a}.hover\:bg-zinc-700:hover{--tw-bg-opacity: 1;background-color:rgb(63 63 70 / var(--tw-bg-opacity, 1))}.hover\:text-cyan-400:hover{--tw-text-opacity: 1;color:rgb(34 211 238 / var(--tw-text-opacity, 1))}.hover\:text-red-400:hover{--tw-text-opacity: 1;color:rgb(248 113 113 / var(--tw-text-opacity, 1))}.hover\:text-white:hover{--tw-text-opacity: 1;color:rgb(255 255 255 / var(--tw-text-opacity, 1))}.focus\:border-cyan-500:focus{--tw-border-opacity: 1;border-color:rgb(6 182 212 / var(--tw-border-opacity, 1))}.focus\:outline-none:focus{outline:2px solid transparent;outline-offset:2px}@media (min-width: 768px){.md\:grid-cols-2{grid-template-columns:repeat(2,minmax(0,1fr))}.md\:grid-cols-4{grid-template-columns:repeat(4,minmax(0,1fr))}}@media (min-width: 1024px){.lg\:grid-cols-2{grid-template-columns:repeat(2,minmax(0,1fr))}.lg\:grid-cols-3{grid-template-columns:repeat(3,minmax(0,1fr))}.lg\:grid-cols-4{grid-template-columns:repeat(4,minmax(0,1fr))}}@media (min-width: 1280px){.xl\:grid-cols-4{grid-template-columns:repeat(4,minmax(0,1fr))}}
|
|
|
|
|
|
backend/static/index.html
CHANGED
|
@@ -1,17 +1 @@
|
|
| 1 |
-
<!DOCTYPE html>
|
| 2 |
-
<html lang="en">
|
| 3 |
-
<head>
|
| 4 |
-
<meta charset="UTF-8" />
|
| 5 |
-
<link rel="icon" type="image/svg+xml" href="/vite.svg" />
|
| 6 |
-
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
|
| 7 |
-
<title>Platform - AI Tool Integration</title>
|
| 8 |
-
<link rel="preconnect" href="https://fonts.googleapis.com">
|
| 9 |
-
<link rel="preconnect" href="https://fonts.gstatic.com" crossorigin>
|
| 10 |
-
<link href="https://fonts.googleapis.com/css2?family=Inter:wght@300;400;500;600;700&display=swap" rel="stylesheet">
|
| 11 |
-
<script type="module" crossorigin src="/assets/index-CIpcBIY5.js"></script>
|
| 12 |
-
<link rel="stylesheet" crossorigin href="/assets/index-DVvq7QG0.css">
|
| 13 |
-
</head>
|
| 14 |
-
<body>
|
| 15 |
-
<div id="root"></div>
|
| 16 |
-
</body>
|
| 17 |
-
</html>
|
|
|
|
| 1 |
+
<!DOCTYPE html><html lang="en"><head><meta charSet="utf-8"/><meta name="viewport" content="width=device-width, initial-scale=1"/><link rel="preload" href="/_next/static/media/e4af272ccee01ff0-s.p.woff2" as="font" crossorigin="" type="font/woff2"/><link rel="stylesheet" href="/_next/static/css/b8fe60b397de5bf4.css" data-precedence="next"/><link rel="preload" as="script" fetchPriority="low" href="/_next/static/chunks/webpack-16fd57b891a459c7.js"/><script src="/_next/static/chunks/fd9d1056-9e075b64004453d8.js" async=""></script><script src="/_next/static/chunks/117-4e7593576fd7f596.js" async=""></script><script src="/_next/static/chunks/main-app-c4d682a1ca6a9af7.js" async=""></script><script src="/_next/static/chunks/876-6377c47dc51ad221.js" async=""></script><script src="/_next/static/chunks/app/page-b6939147ba61bda4.js" async=""></script><title>Compost - AI Tool Platform</title><meta name="description" content="Unified AI tool integration platform"/><meta name="next-size-adjust"/><script src="/_next/static/chunks/polyfills-42372ed130431b0a.js" noModule=""></script></head><body class="__variable_f367f3 font-sans bg-bg-primary text-text-primary"><div class="flex min-h-screen"><aside class="w-60 bg-bg-secondary border-r border-border flex flex-col shrink-0"><div class="px-5 py-4 border-b border-border"><div class="flex items-center gap-3"><div class="w-8 h-8 bg-accent-cyan rounded-lg flex items-center justify-center font-bold text-bg-primary text-sm">C</div><span class="text-base font-semibold tracking-tight">Compost</span></div></div><nav class="flex-1 px-3 py-4 space-y-1"><div class="px-3 mb-2 text-[11px] font-semibold uppercase tracking-wider text-text-dim">Main</div><a class="flex items-center gap-3 px-3 py-2 rounded-lg text-sm transition-colors bg-accent-cyan/10 text-accent-cyanLight font-medium" href="/"><svg xmlns="http://www.w3.org/2000/svg" width="24" height="24" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" class="lucide lucide-layout-dashboard w-4 h-4"><rect width="7" height="9" x="3" y="3" rx="1"></rect><rect width="7" height="5" x="14" y="3" rx="1"></rect><rect width="7" height="9" x="14" y="12" rx="1"></rect><rect width="7" height="5" x="3" y="16" rx="1"></rect></svg>Dashboard</a><a class="flex items-center gap-3 px-3 py-2 rounded-lg text-sm transition-colors text-text-secondary hover:bg-bg-hover hover:text-text-primary" href="/apps/"><svg xmlns="http://www.w3.org/2000/svg" width="24" height="24" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" class="lucide lucide-plug w-4 h-4"><path d="M12 22v-5"></path><path d="M9 8V2"></path><path d="M15 8V2"></path><path d="M18 8v5a4 4 0 0 1-4 4h-4a4 4 0 0 1-4-4V8Z"></path></svg>Apps</a><a class="flex items-center gap-3 px-3 py-2 rounded-lg text-sm transition-colors text-text-secondary hover:bg-bg-hover hover:text-text-primary" href="/tools/"><svg xmlns="http://www.w3.org/2000/svg" width="24" height="24" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" class="lucide lucide-wrench w-4 h-4"><path d="M14.7 6.3a1 1 0 0 0 0 1.4l1.6 1.6a1 1 0 0 0 1.4 0l3.77-3.77a6 6 0 0 1-7.94 7.94l-6.91 6.91a2.12 2.12 0 0 1-3-3l6.91-6.91a6 6 0 0 1 7.94-7.94l-3.76 3.76z"></path></svg>Tools</a><div class="px-3 mt-6 mb-2 text-[11px] font-semibold uppercase tracking-wider text-text-dim">Configuration</div><a class="flex items-center gap-3 px-3 py-2 rounded-lg text-sm transition-colors text-text-secondary hover:bg-bg-hover hover:text-text-primary" href="/api-keys/"><svg xmlns="http://www.w3.org/2000/svg" width="24" height="24" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" class="lucide lucide-key w-4 h-4"><circle cx="7.5" cy="15.5" r="5.5"></circle><path d="m21 2-9.6 9.6"></path><path d="m15.5 7.5 3 3L22 7l-3-3"></path></svg>API Keys</a><a class="flex items-center gap-3 px-3 py-2 rounded-lg text-sm transition-colors text-text-secondary hover:bg-bg-hover hover:text-text-primary" href="/analytics/"><svg xmlns="http://www.w3.org/2000/svg" width="24" height="24" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" class="lucide lucide-bar-chart3 w-4 h-4"><path d="M3 3v18h18"></path><path d="M18 17V9"></path><path d="M13 17V5"></path><path d="M8 17v-3"></path></svg>Analytics</a><a class="flex items-center gap-3 px-3 py-2 rounded-lg text-sm transition-colors text-text-secondary hover:bg-bg-hover hover:text-text-primary" href="/settings/"><svg xmlns="http://www.w3.org/2000/svg" width="24" height="24" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" class="lucide lucide-settings w-4 h-4"><path d="M12.22 2h-.44a2 2 0 0 0-2 2v.18a2 2 0 0 1-1 1.73l-.43.25a2 2 0 0 1-2 0l-.15-.08a2 2 0 0 0-2.73.73l-.22.38a2 2 0 0 0 .73 2.73l.15.1a2 2 0 0 1 1 1.72v.51a2 2 0 0 1-1 1.74l-.15.09a2 2 0 0 0-.73 2.73l.22.38a2 2 0 0 0 2.73.73l.15-.08a2 2 0 0 1 2 0l.43.25a2 2 0 0 1 1 1.73V20a2 2 0 0 0 2 2h.44a2 2 0 0 0 2-2v-.18a2 2 0 0 1 1-1.73l.43-.25a2 2 0 0 1 2 0l.15.08a2 2 0 0 0 2.73-.73l.22-.39a2 2 0 0 0-.73-2.73l-.15-.08a2 2 0 0 1-1-1.74v-.5a2 2 0 0 1 1-1.74l.15-.09a2 2 0 0 0 .73-2.73l-.22-.38a2 2 0 0 0-2.73-.73l-.15.08a2 2 0 0 1-2 0l-.43-.25a2 2 0 0 1-1-1.73V4a2 2 0 0 0-2-2z"></path><circle cx="12" cy="12" r="3"></circle></svg>Settings</a><a href="/api/docs" target="_blank" rel="noopener noreferrer" class="flex items-center gap-3 px-3 py-2 rounded-lg text-sm text-text-secondary hover:bg-bg-hover hover:text-text-primary transition-colors"><svg xmlns="http://www.w3.org/2000/svg" width="24" height="24" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" class="lucide lucide-external-link w-4 h-4"><path d="M15 3h6v6"></path><path d="M10 14 21 3"></path><path d="M18 13v6a2 2 0 0 1-2 2H5a2 2 0 0 1-2-2V8a2 2 0 0 1 2-2h6"></path></svg>API Docs</a></nav><div class="px-3 py-4 border-t border-border"><div class="flex items-center justify-between"><div class="flex items-center gap-2 min-w-0"><div class="w-7 h-7 bg-bg-tertiary rounded-full flex items-center justify-center text-xs font-semibold shrink-0">U</div><span class="text-xs text-text-secondary truncate">User</span></div><button class="p-1.5 text-text-muted hover:text-accent-red hover:bg-accent-red/10 rounded-lg transition-colors"><svg xmlns="http://www.w3.org/2000/svg" width="24" height="24" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" class="lucide lucide-log-out w-3.5 h-3.5"><path d="M9 21H5a2 2 0 0 1-2-2V5a2 2 0 0 1 2-2h4"></path><polyline points="16 17 21 12 16 7"></polyline><line x1="21" x2="9" y1="12" y2="12"></line></svg></button></div></div></aside><main class="flex-1 flex flex-col"><header class="h-14 bg-bg-secondary border-b border-border flex items-center px-6"><h1 class="text-base font-medium">Dashboard</h1></header><div class="flex-1 p-6 space-y-6 overflow-auto"><div class="grid grid-cols-4 gap-4"><div class="bg-bg-tertiary border border-border rounded-xl p-5"><div class="flex items-center justify-between"><div><p class="text-sm text-text-muted">Total Executions</p><p class="text-2xl font-bold mt-1">...</p></div><svg xmlns="http://www.w3.org/2000/svg" width="24" height="24" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" class="lucide lucide-activity w-5 h-5 text-accent-cyan"><path d="M22 12h-2.48a2 2 0 0 0-1.93 1.46l-2.35 8.36a.25.25 0 0 1-.48 0L9.24 2.18a.25.25 0 0 0-.48 0l-2.35 8.36A2 2 0 0 1 4.49 12H2"></path></svg></div></div><div class="bg-bg-tertiary border border-border rounded-xl p-5"><div class="flex items-center justify-between"><div><p class="text-sm text-text-muted">Integrations</p><p class="text-2xl font-bold mt-1">...</p></div><svg xmlns="http://www.w3.org/2000/svg" width="24" height="24" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" class="lucide lucide-plug w-5 h-5 text-accent-green"><path d="M12 22v-5"></path><path d="M9 8V2"></path><path d="M15 8V2"></path><path d="M18 8v5a4 4 0 0 1-4 4h-4a4 4 0 0 1-4-4V8Z"></path></svg></div></div><div class="bg-bg-tertiary border border-border rounded-xl p-5"><div class="flex items-center justify-between"><div><p class="text-sm text-text-muted">Success Rate</p><p class="text-2xl font-bold mt-1">...</p></div><svg xmlns="http://www.w3.org/2000/svg" width="24" height="24" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" class="lucide lucide-circle-check-big w-5 h-5 text-accent-blue"><path d="M22 11.08V12a10 10 0 1 1-5.93-9.14"></path><path d="m9 11 3 3L22 4"></path></svg></div></div><div class="bg-bg-tertiary border border-border rounded-xl p-5"><div class="flex items-center justify-between"><div><p class="text-sm text-text-muted">Avg Latency</p><p class="text-2xl font-bold mt-1">...</p></div><svg xmlns="http://www.w3.org/2000/svg" width="24" height="24" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" class="lucide lucide-clock w-5 h-5 text-accent-amber"><circle cx="12" cy="12" r="10"></circle><polyline points="12 6 12 12 16 14"></polyline></svg></div></div></div><div class="bg-bg-tertiary border border-border rounded-xl p-6"><h3 class="text-sm font-medium mb-4">Quick Connect</h3><p class="text-text-muted text-sm">Loading...</p></div></div></main></div><script src="/_next/static/chunks/webpack-16fd57b891a459c7.js" async=""></script><script>(self.__next_f=self.__next_f||[]).push([0]);self.__next_f.push([2,null])</script><script>self.__next_f.push([1,"1:HL[\"/_next/static/media/e4af272ccee01ff0-s.p.woff2\",\"font\",{\"crossOrigin\":\"\",\"type\":\"font/woff2\"}]\n2:HL[\"/_next/static/css/b8fe60b397de5bf4.css\",\"style\"]\n"])</script><script>self.__next_f.push([1,"3:I[2846,[],\"\"]\n5:I[9107,[],\"ClientPageRoot\"]\n6:I[7340,[\"876\",\"static/chunks/876-6377c47dc51ad221.js\",\"931\",\"static/chunks/app/page-b6939147ba61bda4.js\"],\"default\",1]\n7:I[4707,[],\"\"]\n8:I[6423,[],\"\"]\na:I[1060,[],\"\"]\nb:[]\n"])</script><script>self.__next_f.push([1,"0:[\"$\",\"$L3\",null,{\"buildId\":\"Kf4VGOEAsTnd5eMvxpmU3\",\"assetPrefix\":\"\",\"urlParts\":[\"\",\"\"],\"initialTree\":[\"\",{\"children\":[\"__PAGE__\",{}]},\"$undefined\",\"$undefined\",true],\"initialSeedData\":[\"\",{\"children\":[\"__PAGE__\",{},[[\"$L4\",[\"$\",\"$L5\",null,{\"props\":{\"params\":{},\"searchParams\":{}},\"Component\":\"$6\"}],null],null],null]},[[[[\"$\",\"link\",\"0\",{\"rel\":\"stylesheet\",\"href\":\"/_next/static/css/b8fe60b397de5bf4.css\",\"precedence\":\"next\",\"crossOrigin\":\"$undefined\"}]],[\"$\",\"html\",null,{\"lang\":\"en\",\"children\":[\"$\",\"body\",null,{\"className\":\"__variable_f367f3 font-sans bg-bg-primary text-text-primary\",\"children\":[\"$\",\"$L7\",null,{\"parallelRouterKey\":\"children\",\"segmentPath\":[\"children\"],\"error\":\"$undefined\",\"errorStyles\":\"$undefined\",\"errorScripts\":\"$undefined\",\"template\":[\"$\",\"$L8\",null,{}],\"templateStyles\":\"$undefined\",\"templateScripts\":\"$undefined\",\"notFound\":[[\"$\",\"title\",null,{\"children\":\"404: This page could not be found.\"}],[\"$\",\"div\",null,{\"style\":{\"fontFamily\":\"system-ui,\\\"Segoe UI\\\",Roboto,Helvetica,Arial,sans-serif,\\\"Apple Color Emoji\\\",\\\"Segoe UI Emoji\\\"\",\"height\":\"100vh\",\"textAlign\":\"center\",\"display\":\"flex\",\"flexDirection\":\"column\",\"alignItems\":\"center\",\"justifyContent\":\"center\"},\"children\":[\"$\",\"div\",null,{\"children\":[[\"$\",\"style\",null,{\"dangerouslySetInnerHTML\":{\"__html\":\"body{color:#000;background:#fff;margin:0}.next-error-h1{border-right:1px solid rgba(0,0,0,.3)}@media (prefers-color-scheme:dark){body{color:#fff;background:#000}.next-error-h1{border-right:1px solid rgba(255,255,255,.3)}}\"}}],[\"$\",\"h1\",null,{\"className\":\"next-error-h1\",\"style\":{\"display\":\"inline-block\",\"margin\":\"0 20px 0 0\",\"padding\":\"0 23px 0 0\",\"fontSize\":24,\"fontWeight\":500,\"verticalAlign\":\"top\",\"lineHeight\":\"49px\"},\"children\":\"404\"}],[\"$\",\"div\",null,{\"style\":{\"display\":\"inline-block\"},\"children\":[\"$\",\"h2\",null,{\"style\":{\"fontSize\":14,\"fontWeight\":400,\"lineHeight\":\"49px\",\"margin\":0},\"children\":\"This page could not be found.\"}]}]]}]}]],\"notFoundStyles\":[]}]}]}]],null],null],\"couldBeIntercepted\":false,\"initialHead\":[null,\"$L9\"],\"globalErrorComponent\":\"$a\",\"missingSlots\":\"$Wb\"}]\n"])</script><script>self.__next_f.push([1,"9:[[\"$\",\"meta\",\"0\",{\"name\":\"viewport\",\"content\":\"width=device-width, initial-scale=1\"}],[\"$\",\"meta\",\"1\",{\"charSet\":\"utf-8\"}],[\"$\",\"title\",\"2\",{\"children\":\"Compost - AI Tool Platform\"}],[\"$\",\"meta\",\"3\",{\"name\":\"description\",\"content\":\"Unified AI tool integration platform\"}],[\"$\",\"meta\",\"4\",{\"name\":\"next-size-adjust\"}]]\n4:null\n"])</script></body></html>
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
frontend/app/analytics/page.tsx
ADDED
|
@@ -0,0 +1,59 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
'use client'
|
| 2 |
+
|
| 3 |
+
import { useEffect, useState } from 'react'
|
| 4 |
+
import { Sidebar } from '@/components/sidebar'
|
| 5 |
+
import { api, type Stats } from '@/lib/api'
|
| 6 |
+
import { Activity, CheckCircle, XCircle, Clock } from 'lucide-react'
|
| 7 |
+
|
| 8 |
+
export default function AnalyticsPage() {
|
| 9 |
+
const [stats, setStats] = useState<Stats | null>(null)
|
| 10 |
+
const [loading, setLoading] = useState(true)
|
| 11 |
+
|
| 12 |
+
useEffect(() => {
|
| 13 |
+
api.get<Stats>('/api/analytics/overview')
|
| 14 |
+
.then((data) => { setStats(data); setLoading(false) })
|
| 15 |
+
.catch(() => setLoading(false))
|
| 16 |
+
}, [])
|
| 17 |
+
|
| 18 |
+
const metrics = [
|
| 19 |
+
{ label: 'Executions', value: stats?.total_executions ?? 0, icon: Activity, color: 'text-accent-cyan' },
|
| 20 |
+
{ label: 'Successful', value: stats?.successful ?? 0, icon: CheckCircle, color: 'text-accent-green' },
|
| 21 |
+
{ label: 'Failed', value: stats?.failed ?? 0, icon: XCircle, color: 'text-accent-red' },
|
| 22 |
+
{ label: 'Avg Latency', value: `${stats?.avg_latency_ms ?? 0}ms`, icon: Clock, color: 'text-accent-amber' },
|
| 23 |
+
]
|
| 24 |
+
|
| 25 |
+
return (
|
| 26 |
+
<div className="flex min-h-screen">
|
| 27 |
+
<Sidebar />
|
| 28 |
+
<main className="flex-1 flex flex-col">
|
| 29 |
+
<header className="h-14 bg-bg-secondary border-b border-border flex items-center px-6">
|
| 30 |
+
<h1 className="text-base font-medium">Analytics</h1>
|
| 31 |
+
</header>
|
| 32 |
+
<div className="flex-1 p-6 space-y-6 overflow-auto">
|
| 33 |
+
<div className="grid grid-cols-4 gap-4">
|
| 34 |
+
{metrics.map((m) => (
|
| 35 |
+
<div key={m.label} className="bg-bg-tertiary border border-border rounded-xl p-5">
|
| 36 |
+
<div className="flex items-center justify-between">
|
| 37 |
+
<div>
|
| 38 |
+
<p className="text-sm text-text-muted">{m.label}</p>
|
| 39 |
+
<p className={`text-2xl font-bold mt-1 ${m.color}`}>{loading ? '...' : m.value}</p>
|
| 40 |
+
</div>
|
| 41 |
+
<m.icon className={`w-5 h-5 ${m.color}`} />
|
| 42 |
+
</div>
|
| 43 |
+
</div>
|
| 44 |
+
))}
|
| 45 |
+
</div>
|
| 46 |
+
<div className="bg-bg-tertiary border border-border rounded-xl p-6">
|
| 47 |
+
<h3 className="text-sm font-medium mb-4">Success Rate</h3>
|
| 48 |
+
<div className="flex items-center gap-4">
|
| 49 |
+
<div className="flex-1 h-3 bg-bg-secondary rounded-full overflow-hidden">
|
| 50 |
+
<div className="h-full bg-accent-green rounded-full transition-all" style={{ width: `${stats?.success_rate ?? 100}%` }} />
|
| 51 |
+
</div>
|
| 52 |
+
<span className="text-sm font-medium">{stats?.success_rate ?? 100}%</span>
|
| 53 |
+
</div>
|
| 54 |
+
</div>
|
| 55 |
+
</div>
|
| 56 |
+
</main>
|
| 57 |
+
</div>
|
| 58 |
+
)
|
| 59 |
+
}
|
frontend/app/api-keys/page.tsx
ADDED
|
@@ -0,0 +1,26 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
'use client'
|
| 2 |
+
|
| 3 |
+
import { Sidebar } from '@/components/sidebar'
|
| 4 |
+
import { Key, Server } from 'lucide-react'
|
| 5 |
+
|
| 6 |
+
export default function APIKeysPage() {
|
| 7 |
+
return (
|
| 8 |
+
<div className="flex min-h-screen">
|
| 9 |
+
<Sidebar />
|
| 10 |
+
<main className="flex-1 flex flex-col">
|
| 11 |
+
<header className="h-14 bg-bg-secondary border-b border-border flex items-center px-6 justify-between">
|
| 12 |
+
<h1 className="text-base font-medium">API Keys</h1>
|
| 13 |
+
</header>
|
| 14 |
+
<div className="flex-1 p-6">
|
| 15 |
+
<div className="bg-bg-tertiary border border-border rounded-xl p-6">
|
| 16 |
+
<div className="flex items-center gap-3 mb-4">
|
| 17 |
+
<Key className="w-5 h-5 text-accent-cyan" />
|
| 18 |
+
<h3 className="text-sm font-medium">Manage API Keys</h3>
|
| 19 |
+
</div>
|
| 20 |
+
<p className="text-sm text-text-muted">Generate API keys for programmatic access to the platform.</p>
|
| 21 |
+
</div>
|
| 22 |
+
</div>
|
| 23 |
+
</main>
|
| 24 |
+
</div>
|
| 25 |
+
)
|
| 26 |
+
}
|
frontend/app/apps/page.tsx
ADDED
|
@@ -0,0 +1,95 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
'use client'
|
| 2 |
+
|
| 3 |
+
import { useEffect, useState } from 'react'
|
| 4 |
+
import Link from 'next/link'
|
| 5 |
+
import { Sidebar } from '@/components/sidebar'
|
| 6 |
+
import { api, type Integration } from '@/lib/api'
|
| 7 |
+
import { Search, ArrowUpRight } from 'lucide-react'
|
| 8 |
+
|
| 9 |
+
const categories = ['All', 'Developer Tools', 'Communication', 'Productivity', 'CRM', 'Finance', 'Social', 'Other']
|
| 10 |
+
|
| 11 |
+
export default function AppsPage() {
|
| 12 |
+
const [integrations, setIntegrations] = useState<Integration[]>([])
|
| 13 |
+
const [loading, setLoading] = useState(true)
|
| 14 |
+
const [category, setCategory] = useState('All')
|
| 15 |
+
const [search, setSearch] = useState('')
|
| 16 |
+
|
| 17 |
+
useEffect(() => {
|
| 18 |
+
api.get<Integration[]>('/api/apps')
|
| 19 |
+
.then((data) => { setIntegrations(data); setLoading(false) })
|
| 20 |
+
.catch(() => setLoading(false))
|
| 21 |
+
}, [])
|
| 22 |
+
|
| 23 |
+
const filtered = integrations.filter((i) => {
|
| 24 |
+
const matchCat = category === 'All' || i.category === category
|
| 25 |
+
const matchSearch = !search || i.name.toLowerCase().includes(search.toLowerCase()) || i.description.toLowerCase().includes(search.toLowerCase())
|
| 26 |
+
return matchCat && matchSearch
|
| 27 |
+
})
|
| 28 |
+
|
| 29 |
+
return (
|
| 30 |
+
<div className="flex min-h-screen">
|
| 31 |
+
<Sidebar integrationCount={integrations.length} />
|
| 32 |
+
<main className="flex-1 flex flex-col">
|
| 33 |
+
<header className="h-14 bg-bg-secondary border-b border-border flex items-center px-6 justify-between">
|
| 34 |
+
<h1 className="text-base font-medium">Apps</h1>
|
| 35 |
+
<div className="relative">
|
| 36 |
+
<Search className="absolute left-3 top-1/2 -translate-y-1/2 w-4 h-4 text-text-dim" />
|
| 37 |
+
<input
|
| 38 |
+
className="pl-9 pr-4 py-1.5 bg-bg-tertiary border border-border rounded-lg text-sm text-text-primary placeholder:text-text-dim focus:outline-none focus:border-accent-cyan w-64"
|
| 39 |
+
placeholder="Search apps..."
|
| 40 |
+
value={search}
|
| 41 |
+
onChange={(e) => setSearch(e.target.value)}
|
| 42 |
+
/>
|
| 43 |
+
</div>
|
| 44 |
+
</header>
|
| 45 |
+
<div className="flex-1 p-6 overflow-auto">
|
| 46 |
+
<div className="flex gap-2 mb-6 flex-wrap">
|
| 47 |
+
{categories.map((c) => (
|
| 48 |
+
<button key={c} onClick={() => setCategory(c)}
|
| 49 |
+
className={`px-3 py-1.5 rounded-lg text-xs font-medium transition-colors ${category === c ? 'bg-accent-cyan text-bg-primary' : 'bg-bg-tertiary border border-border text-text-secondary hover:text-text-primary'}`}
|
| 50 |
+
>{c}</button>
|
| 51 |
+
))}
|
| 52 |
+
</div>
|
| 53 |
+
{loading ? (
|
| 54 |
+
<div className="grid grid-cols-4 gap-4">
|
| 55 |
+
{Array.from({ length: 8 }).map((_, i) => (
|
| 56 |
+
<div key={i} className="bg-bg-tertiary border border-border rounded-xl p-5 animate-pulse">
|
| 57 |
+
<div className="flex items-center gap-3 mb-3">
|
| 58 |
+
<div className="w-9 h-9 bg-bg-hover rounded-lg" />
|
| 59 |
+
<div className="flex-1"><div className="h-3 bg-bg-hover rounded w-20 mb-1" /><div className="h-2 bg-bg-hover rounded w-14" /></div>
|
| 60 |
+
</div>
|
| 61 |
+
<div className="h-2 bg-bg-hover rounded w-full mb-2" /><div className="h-2 bg-bg-hover rounded w-3/4" />
|
| 62 |
+
</div>
|
| 63 |
+
))}
|
| 64 |
+
</div>
|
| 65 |
+
) : filtered.length === 0 ? (
|
| 66 |
+
<div className="text-center py-20"><p className="text-text-muted">No apps found</p></div>
|
| 67 |
+
) : (
|
| 68 |
+
<div className="grid grid-cols-4 gap-4">
|
| 69 |
+
{filtered.map((i) => (
|
| 70 |
+
<div key={i.id} className="bg-bg-tertiary border border-border rounded-xl p-5 hover:border-accent-cyan/40 transition-colors group">
|
| 71 |
+
<div className="flex items-center gap-3 mb-3">
|
| 72 |
+
<div className="w-10 h-10 bg-bg-secondary rounded-lg flex items-center justify-center overflow-hidden border border-border">
|
| 73 |
+
<img src={i.logo_url} alt={i.name} className="w-6 h-6" onError={(e) => (e.currentTarget.style.display = 'none')} />
|
| 74 |
+
</div>
|
| 75 |
+
<div>
|
| 76 |
+
<p className="text-sm font-semibold">{i.name}</p>
|
| 77 |
+
<p className="text-[11px] text-text-muted">{i.category}</p>
|
| 78 |
+
</div>
|
| 79 |
+
</div>
|
| 80 |
+
<p className="text-xs text-text-secondary leading-relaxed line-clamp-2 mb-4">{i.description}</p>
|
| 81 |
+
<div className="flex items-center justify-between pt-3 border-t border-border">
|
| 82 |
+
<span className="text-[11px] text-text-muted">{i.tool_count} tools</span>
|
| 83 |
+
<Link href={`/connect?id=${i.id}`} className="text-[11px] text-accent-cyan hover:text-accent-cyanLight flex items-center gap-1 font-medium">
|
| 84 |
+
Connect <ArrowUpRight className="w-3 h-3" />
|
| 85 |
+
</Link>
|
| 86 |
+
</div>
|
| 87 |
+
</div>
|
| 88 |
+
))}
|
| 89 |
+
</div>
|
| 90 |
+
)}
|
| 91 |
+
</div>
|
| 92 |
+
</main>
|
| 93 |
+
</div>
|
| 94 |
+
)
|
| 95 |
+
}
|
frontend/app/connect/page.tsx
ADDED
|
@@ -0,0 +1,160 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
'use client'
|
| 2 |
+
|
| 3 |
+
import { useEffect, useState, Suspense } from 'react'
|
| 4 |
+
import { useRouter, useSearchParams } from 'next/navigation'
|
| 5 |
+
import { Sidebar } from '@/components/sidebar'
|
| 6 |
+
import { ArrowLeft, Loader2, Check, Key } from 'lucide-react'
|
| 7 |
+
|
| 8 |
+
interface AuthField {
|
| 9 |
+
name: string
|
| 10 |
+
label: string
|
| 11 |
+
type: string
|
| 12 |
+
required: boolean
|
| 13 |
+
description: string
|
| 14 |
+
default: string
|
| 15 |
+
}
|
| 16 |
+
|
| 17 |
+
interface AuthConfig {
|
| 18 |
+
integration_id: string
|
| 19 |
+
auth_type: string
|
| 20 |
+
fields: AuthField[]
|
| 21 |
+
oauth_authorize_url: string | null
|
| 22 |
+
oauth_token_url: string | null
|
| 23 |
+
}
|
| 24 |
+
|
| 25 |
+
function ConnectContent() {
|
| 26 |
+
const router = useRouter()
|
| 27 |
+
const searchParams = useSearchParams()
|
| 28 |
+
const id = searchParams.get('id') || ''
|
| 29 |
+
const [config, setConfig] = useState<AuthConfig | null>(null)
|
| 30 |
+
const [integrationName, setIntegrationName] = useState('')
|
| 31 |
+
const [values, setValues] = useState<Record<string, string>>({})
|
| 32 |
+
const [loading, setLoading] = useState(true)
|
| 33 |
+
const [submitting, setSubmitting] = useState(false)
|
| 34 |
+
const [error, setError] = useState('')
|
| 35 |
+
const [success, setSuccess] = useState(false)
|
| 36 |
+
|
| 37 |
+
useEffect(() => {
|
| 38 |
+
if (!id) { router.push('/apps'); return }
|
| 39 |
+
Promise.all([
|
| 40 |
+
fetch(`/api/apps/${id}`).then(r => r.json()),
|
| 41 |
+
fetch(`/api/connections/auth-config/${id}`).then(r => r.ok ? r.json() : null),
|
| 42 |
+
]).then(([app, cfg]) => {
|
| 43 |
+
setIntegrationName(app.name || id)
|
| 44 |
+
setConfig(cfg)
|
| 45 |
+
setLoading(false)
|
| 46 |
+
if (cfg) {
|
| 47 |
+
const init: Record<string, string> = {}
|
| 48 |
+
cfg.fields.forEach((f: AuthField) => { init[f.name] = f.default || '' })
|
| 49 |
+
setValues(init)
|
| 50 |
+
}
|
| 51 |
+
}).catch(() => setLoading(false))
|
| 52 |
+
}, [id, router])
|
| 53 |
+
|
| 54 |
+
const handleSubmit = async (e: React.FormEvent) => {
|
| 55 |
+
e.preventDefault()
|
| 56 |
+
setError('')
|
| 57 |
+
setSubmitting(true)
|
| 58 |
+
try {
|
| 59 |
+
const res = await fetch(`/api/connections/connect?integration_id=${id}`, {
|
| 60 |
+
method: 'POST',
|
| 61 |
+
headers: { 'Content-Type': 'application/json', 'Authorization': `Bearer ${localStorage.getItem('token')}` },
|
| 62 |
+
body: JSON.stringify({ credentials: values }),
|
| 63 |
+
})
|
| 64 |
+
const data = await res.json()
|
| 65 |
+
if (!res.ok) { setError(data.detail || 'Connection failed'); return }
|
| 66 |
+
setSuccess(true)
|
| 67 |
+
setTimeout(() => router.push('/apps'), 1500)
|
| 68 |
+
} catch { setError('Network error') }
|
| 69 |
+
finally { setSubmitting(false) }
|
| 70 |
+
}
|
| 71 |
+
|
| 72 |
+
return (
|
| 73 |
+
<div className="flex min-h-screen">
|
| 74 |
+
<Sidebar />
|
| 75 |
+
<main className="flex-1 flex flex-col">
|
| 76 |
+
<header className="h-14 bg-bg-secondary border-b border-border flex items-center px-6">
|
| 77 |
+
<button onClick={() => router.back()} className="flex items-center gap-2 text-text-muted hover:text-text-primary transition-colors mr-4">
|
| 78 |
+
<ArrowLeft className="w-4 h-4" />
|
| 79 |
+
</button>
|
| 80 |
+
<h1 className="text-base font-medium">Connect {integrationName}</h1>
|
| 81 |
+
</header>
|
| 82 |
+
<div className="flex-1 flex items-center justify-center p-6">
|
| 83 |
+
{loading ? (
|
| 84 |
+
<div className="text-text-muted">Loading...</div>
|
| 85 |
+
) : success ? (
|
| 86 |
+
<div className="bg-bg-tertiary border border-accent-green/30 rounded-2xl p-8 text-center max-w-md">
|
| 87 |
+
<div className="w-12 h-12 bg-accent-green/10 rounded-full flex items-center justify-center mx-auto mb-4">
|
| 88 |
+
<Check className="w-6 h-6 text-accent-green" />
|
| 89 |
+
</div>
|
| 90 |
+
<h2 className="text-lg font-semibold mb-2">Connected Successfully</h2>
|
| 91 |
+
<p className="text-sm text-text-muted">{integrationName} is now connected</p>
|
| 92 |
+
</div>
|
| 93 |
+
) : !config ? (
|
| 94 |
+
<div className="bg-bg-tertiary border border-border rounded-2xl p-8 text-center max-w-md">
|
| 95 |
+
<h2 className="text-lg font-semibold mb-2">No Auth Config</h2>
|
| 96 |
+
<p className="text-sm text-text-muted mb-4">This integration hasn't been configured yet.</p>
|
| 97 |
+
<button onClick={() => router.push('/settings')} className="px-4 py-2 bg-accent-cyan text-bg-primary text-sm font-medium rounded-lg">Configure in Settings</button>
|
| 98 |
+
</div>
|
| 99 |
+
) : (
|
| 100 |
+
<div className="bg-bg-tertiary border border-border rounded-2xl p-8 w-full max-w-md">
|
| 101 |
+
<div className="flex items-center gap-3 mb-6">
|
| 102 |
+
<div className="w-10 h-10 bg-bg-secondary border border-border rounded-lg flex items-center justify-center">
|
| 103 |
+
<Key className="w-5 h-5 text-accent-cyan" />
|
| 104 |
+
</div>
|
| 105 |
+
<div>
|
| 106 |
+
<h2 className="text-base font-semibold">{integrationName}</h2>
|
| 107 |
+
<p className="text-xs text-text-muted capitalize">{config.auth_type} authentication</p>
|
| 108 |
+
</div>
|
| 109 |
+
</div>
|
| 110 |
+
{error && <div className="mb-4 p-3 bg-accent-red/10 border border-accent-red/30 rounded-lg text-accent-red text-sm">{error}</div>}
|
| 111 |
+
<form onSubmit={handleSubmit} className="space-y-4">
|
| 112 |
+
{config.fields.map((field) => (
|
| 113 |
+
<div key={field.name}>
|
| 114 |
+
<label className="block text-xs text-text-muted mb-1.5">
|
| 115 |
+
{field.label} {field.required && <span className="text-accent-red">*</span>}
|
| 116 |
+
</label>
|
| 117 |
+
<input
|
| 118 |
+
type={field.type === 'password' ? 'password' : 'text'}
|
| 119 |
+
value={values[field.name] || ''}
|
| 120 |
+
onChange={(e) => setValues(prev => ({ ...prev, [field.name]: e.target.value }))}
|
| 121 |
+
className="w-full px-3 py-2 bg-bg-secondary border border-border rounded-lg text-sm text-text-primary placeholder:text-text-dim focus:outline-none focus:border-accent-cyan"
|
| 122 |
+
placeholder={field.description}
|
| 123 |
+
required={field.required}
|
| 124 |
+
/>
|
| 125 |
+
</div>
|
| 126 |
+
))}
|
| 127 |
+
{config.auth_type === 'oauth2' && config.oauth_authorize_url && (
|
| 128 |
+
<div className="pt-2 border-t border-border">
|
| 129 |
+
<p className="text-xs text-text-muted mb-3">Or connect via OAuth:</p>
|
| 130 |
+
<a
|
| 131 |
+
href={`${config.oauth_authorize_url}?client_id=${values['client_id'] || ''}&redirect_uri=${typeof window !== 'undefined' ? window.location.origin : ''}/oauth/callback/${id}&response_type=code`}
|
| 132 |
+
className="w-full flex items-center justify-center gap-2 py-2.5 bg-bg-secondary border border-border hover:border-accent-cyan/50 text-text-primary text-sm font-medium rounded-lg transition-colors"
|
| 133 |
+
>
|
| 134 |
+
Continue with OAuth
|
| 135 |
+
</a>
|
| 136 |
+
</div>
|
| 137 |
+
)}
|
| 138 |
+
<button
|
| 139 |
+
type="submit"
|
| 140 |
+
disabled={submitting}
|
| 141 |
+
className="w-full py-2.5 bg-accent-cyan hover:bg-accent-cyanLight text-bg-primary font-medium text-sm rounded-lg transition-colors disabled:opacity-50 flex items-center justify-center gap-2"
|
| 142 |
+
>
|
| 143 |
+
{submitting ? <><Loader2 className="w-4 h-4 animate-spin" /> Connecting...</> : 'Connect'}
|
| 144 |
+
</button>
|
| 145 |
+
</form>
|
| 146 |
+
</div>
|
| 147 |
+
)}
|
| 148 |
+
</div>
|
| 149 |
+
</main>
|
| 150 |
+
</div>
|
| 151 |
+
)
|
| 152 |
+
}
|
| 153 |
+
|
| 154 |
+
export default function ConnectPage() {
|
| 155 |
+
return (
|
| 156 |
+
<Suspense fallback={<div className="flex min-h-screen"><div className="flex-1 flex items-center justify-center text-text-muted">Loading...</div></div>}>
|
| 157 |
+
<ConnectContent />
|
| 158 |
+
</Suspense>
|
| 159 |
+
)
|
| 160 |
+
}
|
frontend/app/globals.css
ADDED
|
@@ -0,0 +1,22 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
@tailwind base;
|
| 2 |
+
@tailwind components;
|
| 3 |
+
@tailwind utilities;
|
| 4 |
+
|
| 5 |
+
:root {
|
| 6 |
+
--background: #09090b;
|
| 7 |
+
--foreground: #fafafa;
|
| 8 |
+
}
|
| 9 |
+
|
| 10 |
+
* { box-sizing: border-box; }
|
| 11 |
+
|
| 12 |
+
body {
|
| 13 |
+
background: var(--background);
|
| 14 |
+
color: var(--foreground);
|
| 15 |
+
font-family: 'Inter', system-ui, sans-serif;
|
| 16 |
+
-webkit-font-smoothing: antialiased;
|
| 17 |
+
}
|
| 18 |
+
|
| 19 |
+
::-webkit-scrollbar { width: 6px; }
|
| 20 |
+
::-webkit-scrollbar-track { background: #111113; }
|
| 21 |
+
::-webkit-scrollbar-thumb { background: #27272a; border-radius: 3px; }
|
| 22 |
+
::-webkit-scrollbar-thumb:hover { background: #3f3f46; }
|
frontend/app/layout.tsx
ADDED
|
@@ -0,0 +1,20 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
import type { Metadata } from 'next'
|
| 2 |
+
import { Inter } from 'next/font/google'
|
| 3 |
+
import './globals.css'
|
| 4 |
+
|
| 5 |
+
const inter = Inter({ subsets: ['latin'], variable: '--font-inter' })
|
| 6 |
+
|
| 7 |
+
export const metadata: Metadata = {
|
| 8 |
+
title: 'Compost - AI Tool Platform',
|
| 9 |
+
description: 'Unified AI tool integration platform',
|
| 10 |
+
}
|
| 11 |
+
|
| 12 |
+
export default function RootLayout({ children }: { children: React.ReactNode }) {
|
| 13 |
+
return (
|
| 14 |
+
<html lang="en">
|
| 15 |
+
<body className={`${inter.variable} font-sans bg-bg-primary text-text-primary`}>
|
| 16 |
+
{children}
|
| 17 |
+
</body>
|
| 18 |
+
</html>
|
| 19 |
+
)
|
| 20 |
+
}
|
frontend/app/login/page.tsx
ADDED
|
@@ -0,0 +1,61 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
'use client'
|
| 2 |
+
|
| 3 |
+
import { useState } from 'react'
|
| 4 |
+
|
| 5 |
+
export default function LoginPage() {
|
| 6 |
+
const [email, setEmail] = useState('')
|
| 7 |
+
const [password, setPassword] = useState('')
|
| 8 |
+
const [error, setError] = useState('')
|
| 9 |
+
const [loading, setLoading] = useState(false)
|
| 10 |
+
|
| 11 |
+
const handleSubmit = async (e: React.FormEvent) => {
|
| 12 |
+
e.preventDefault()
|
| 13 |
+
setError('')
|
| 14 |
+
setLoading(true)
|
| 15 |
+
try {
|
| 16 |
+
const formData = new URLSearchParams()
|
| 17 |
+
formData.append('username', email)
|
| 18 |
+
formData.append('password', password)
|
| 19 |
+
const res = await fetch('/api/auth/login', {
|
| 20 |
+
method: 'POST',
|
| 21 |
+
headers: { 'Content-Type': 'application/x-www-form-urlencoded' },
|
| 22 |
+
body: formData,
|
| 23 |
+
})
|
| 24 |
+
const data = await res.json()
|
| 25 |
+
if (!res.ok) { setError(data.detail || 'Login failed'); return }
|
| 26 |
+
localStorage.setItem('token', data.access_token)
|
| 27 |
+
localStorage.setItem('email', data.user?.email || email)
|
| 28 |
+
window.location.href = '/'
|
| 29 |
+
} catch { setError('Network error') }
|
| 30 |
+
finally { setLoading(false) }
|
| 31 |
+
}
|
| 32 |
+
|
| 33 |
+
return (
|
| 34 |
+
<div className="min-h-screen flex items-center justify-center bg-bg-primary">
|
| 35 |
+
<div className="w-full max-w-sm">
|
| 36 |
+
<div className="text-center mb-8">
|
| 37 |
+
<div className="w-14 h-14 bg-accent-cyan rounded-2xl flex items-center justify-center mx-auto mb-4 font-bold text-xl text-bg-primary">C</div>
|
| 38 |
+
<h1 className="text-xl font-semibold">Compost</h1>
|
| 39 |
+
<p className="text-sm text-text-muted mt-1">AI Tool Integration Platform</p>
|
| 40 |
+
</div>
|
| 41 |
+
<div className="bg-bg-tertiary border border-border rounded-2xl p-6">
|
| 42 |
+
{error && <div className="mb-4 p-3 bg-accent-red/10 border border-accent-red/30 rounded-lg text-accent-red text-sm">{error}</div>}
|
| 43 |
+
<form onSubmit={handleSubmit} className="space-y-4">
|
| 44 |
+
<div>
|
| 45 |
+
<label className="block text-xs text-text-muted mb-1.5">Email</label>
|
| 46 |
+
<input type="email" value={email} onChange={(e) => setEmail(e.target.value)} className="w-full px-3 py-2 bg-bg-secondary border border-border rounded-lg text-sm text-text-primary placeholder:text-text-dim focus:outline-none focus:border-accent-cyan" placeholder="admin@platform.local" required />
|
| 47 |
+
</div>
|
| 48 |
+
<div>
|
| 49 |
+
<label className="block text-xs text-text-muted mb-1.5">Password</label>
|
| 50 |
+
<input type="password" value={password} onChange={(e) => setPassword(e.target.value)} className="w-full px-3 py-2 bg-bg-secondary border border-border rounded-lg text-sm text-text-primary placeholder:text-text-dim focus:outline-none focus:border-accent-cyan" placeholder="••••••••" required />
|
| 51 |
+
</div>
|
| 52 |
+
<button type="submit" disabled={loading} className="w-full py-2.5 bg-accent-cyan hover:bg-accent-cyanLight text-bg-primary font-medium text-sm rounded-lg transition-colors disabled:opacity-50">
|
| 53 |
+
{loading ? 'Signing in...' : 'Sign In'}
|
| 54 |
+
</button>
|
| 55 |
+
</form>
|
| 56 |
+
</div>
|
| 57 |
+
<p className="text-center text-xs text-text-dim mt-4">Default: admin@platform.local / admin123</p>
|
| 58 |
+
</div>
|
| 59 |
+
</div>
|
| 60 |
+
)
|
| 61 |
+
}
|
frontend/app/page.tsx
ADDED
|
@@ -0,0 +1,90 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
'use client'
|
| 2 |
+
|
| 3 |
+
import { useEffect, useState } from 'react'
|
| 4 |
+
import { Sidebar } from '@/components/sidebar'
|
| 5 |
+
import { api, type Integration, type Stats } from '@/lib/api'
|
| 6 |
+
import { Activity, Plug, Zap, CheckCircle, Clock } from 'lucide-react'
|
| 7 |
+
|
| 8 |
+
export default function DashboardPage() {
|
| 9 |
+
const [integrations, setIntegrations] = useState<Integration[]>([])
|
| 10 |
+
const [stats, setStats] = useState<Stats | null>(null)
|
| 11 |
+
const [loading, setLoading] = useState(true)
|
| 12 |
+
|
| 13 |
+
useEffect(() => {
|
| 14 |
+
Promise.all([
|
| 15 |
+
api.get<Integration[]>('/api/apps').catch(() => []),
|
| 16 |
+
api.get<Stats>('/api/analytics/overview').catch(() => null),
|
| 17 |
+
]).then(([apps, s]) => {
|
| 18 |
+
setIntegrations(apps)
|
| 19 |
+
setStats(s)
|
| 20 |
+
setLoading(false)
|
| 21 |
+
})
|
| 22 |
+
}, [])
|
| 23 |
+
|
| 24 |
+
const metrics = [
|
| 25 |
+
{ label: 'Total Executions', value: stats?.total_executions ?? 0, icon: Activity, color: 'text-accent-cyan' },
|
| 26 |
+
{ label: 'Integrations', value: integrations.length, icon: Plug, color: 'text-accent-green' },
|
| 27 |
+
{ label: 'Success Rate', value: `${stats?.success_rate ?? 100}%`, icon: CheckCircle, color: 'text-accent-blue' },
|
| 28 |
+
{ label: 'Avg Latency', value: `${stats?.avg_latency_ms ?? 0}ms`, icon: Clock, color: 'text-accent-amber' },
|
| 29 |
+
]
|
| 30 |
+
|
| 31 |
+
return (
|
| 32 |
+
<div className="flex min-h-screen">
|
| 33 |
+
<Sidebar integrationCount={integrations.length} />
|
| 34 |
+
<main className="flex-1 flex flex-col">
|
| 35 |
+
<header className="h-14 bg-bg-secondary border-b border-border flex items-center px-6">
|
| 36 |
+
<h1 className="text-base font-medium">Dashboard</h1>
|
| 37 |
+
</header>
|
| 38 |
+
<div className="flex-1 p-6 space-y-6 overflow-auto">
|
| 39 |
+
<div className="grid grid-cols-4 gap-4">
|
| 40 |
+
{metrics.map((m) => (
|
| 41 |
+
<div key={m.label} className="bg-bg-tertiary border border-border rounded-xl p-5">
|
| 42 |
+
<div className="flex items-center justify-between">
|
| 43 |
+
<div>
|
| 44 |
+
<p className="text-sm text-text-muted">{m.label}</p>
|
| 45 |
+
<p className="text-2xl font-bold mt-1">{loading ? '...' : m.value}</p>
|
| 46 |
+
</div>
|
| 47 |
+
<m.icon className={`w-5 h-5 ${m.color}`} />
|
| 48 |
+
</div>
|
| 49 |
+
</div>
|
| 50 |
+
))}
|
| 51 |
+
</div>
|
| 52 |
+
|
| 53 |
+
<div className="bg-bg-tertiary border border-border rounded-xl p-6">
|
| 54 |
+
<h3 className="text-sm font-medium mb-4">Quick Connect</h3>
|
| 55 |
+
{loading ? (
|
| 56 |
+
<p className="text-text-muted text-sm">Loading...</p>
|
| 57 |
+
) : integrations.length === 0 ? (
|
| 58 |
+
<div className="text-center py-12">
|
| 59 |
+
<p className="text-text-muted">No integrations yet</p>
|
| 60 |
+
<p className="text-text-dim text-sm mt-1">Set COMPOSIO_API_KEY to sync all 1000+ tools</p>
|
| 61 |
+
</div>
|
| 62 |
+
) : (
|
| 63 |
+
<div className="grid grid-cols-4 gap-4">
|
| 64 |
+
{integrations.slice(0, 8).map((i) => (
|
| 65 |
+
<div key={i.id} className="bg-bg-secondary border border-border rounded-lg p-4 hover:border-accent-cyan/40 transition-colors">
|
| 66 |
+
<div className="flex items-center gap-3 mb-3">
|
| 67 |
+
<div className="w-9 h-9 bg-bg-tertiary rounded-lg flex items-center justify-center overflow-hidden">
|
| 68 |
+
<img src={i.logo_url} alt={i.name} className="w-5 h-5" onError={(e) => (e.currentTarget.style.display = 'none')} />
|
| 69 |
+
</div>
|
| 70 |
+
<div>
|
| 71 |
+
<p className="text-sm font-medium">{i.name}</p>
|
| 72 |
+
<p className="text-[11px] text-text-muted">{i.category}</p>
|
| 73 |
+
</div>
|
| 74 |
+
</div>
|
| 75 |
+
<div className="flex items-center justify-between pt-3 border-t border-border">
|
| 76 |
+
<span className="text-[11px] text-text-muted">{i.tool_count} tools</span>
|
| 77 |
+
<span className={`text-[11px] px-2 py-0.5 rounded ${i.auth_type === 'oauth2' ? 'bg-accent-cyan/10 text-accent-cyanLight' : 'bg-accent-green/10 text-accent-green'}`}>
|
| 78 |
+
{i.auth_type}
|
| 79 |
+
</span>
|
| 80 |
+
</div>
|
| 81 |
+
</div>
|
| 82 |
+
))}
|
| 83 |
+
</div>
|
| 84 |
+
)}
|
| 85 |
+
</div>
|
| 86 |
+
</div>
|
| 87 |
+
</main>
|
| 88 |
+
</div>
|
| 89 |
+
)
|
| 90 |
+
}
|
frontend/app/settings/page.tsx
ADDED
|
@@ -0,0 +1,40 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
'use client'
|
| 2 |
+
|
| 3 |
+
import { Sidebar } from '@/components/sidebar'
|
| 4 |
+
import { Server, Database, Shield } from 'lucide-react'
|
| 5 |
+
|
| 6 |
+
export default function SettingsPage() {
|
| 7 |
+
return (
|
| 8 |
+
<div className="flex min-h-screen">
|
| 9 |
+
<Sidebar />
|
| 10 |
+
<main className="flex-1 flex flex-col">
|
| 11 |
+
<header className="h-14 bg-bg-secondary border-b border-border flex items-center px-6">
|
| 12 |
+
<h1 className="text-base font-medium">Settings</h1>
|
| 13 |
+
</header>
|
| 14 |
+
<div className="flex-1 p-6 space-y-4 overflow-auto">
|
| 15 |
+
<div className="bg-bg-tertiary border border-border rounded-xl p-5">
|
| 16 |
+
<div className="flex items-center gap-3 mb-4"><Server className="w-5 h-5 text-accent-cyan" /><h3 className="text-sm font-medium">System</h3></div>
|
| 17 |
+
<div className="space-y-3 text-sm">
|
| 18 |
+
<div className="flex justify-between"><span className="text-text-muted">Version</span><span>1.0.0</span></div>
|
| 19 |
+
<div className="flex justify-between"><span className="text-text-muted">Data Dir</span><span className="font-mono">/data</span></div>
|
| 20 |
+
</div>
|
| 21 |
+
</div>
|
| 22 |
+
<div className="bg-bg-tertiary border border-border rounded-xl p-5">
|
| 23 |
+
<div className="flex items-center gap-3 mb-4"><Database className="w-5 h-5 text-accent-green" /><h3 className="text-sm font-medium">Database</h3></div>
|
| 24 |
+
<div className="space-y-3 text-sm">
|
| 25 |
+
<div className="flex justify-between"><span className="text-text-muted">Engine</span><span>SQLite</span></div>
|
| 26 |
+
<div className="flex justify-between"><span className="text-text-muted">Path</span><span className="font-mono">/data/platform.db</span></div>
|
| 27 |
+
</div>
|
| 28 |
+
</div>
|
| 29 |
+
<div className="bg-bg-tertiary border border-border rounded-xl p-5">
|
| 30 |
+
<div className="flex items-center gap-3 mb-4"><Shield className="w-5 h-5 text-accent-amber" /><h3 className="text-sm font-medium">Security</h3></div>
|
| 31 |
+
<div className="space-y-3 text-sm">
|
| 32 |
+
<div className="flex justify-between"><span className="text-text-muted">Auth</span><span>JWT (HS256)</span></div>
|
| 33 |
+
<div className="flex justify-between"><span className="text-text-muted">Vault</span><span>AES-256-GCM</span></div>
|
| 34 |
+
</div>
|
| 35 |
+
</div>
|
| 36 |
+
</div>
|
| 37 |
+
</main>
|
| 38 |
+
</div>
|
| 39 |
+
)
|
| 40 |
+
}
|
frontend/app/tools/page.tsx
ADDED
|
@@ -0,0 +1,64 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
'use client'
|
| 2 |
+
|
| 3 |
+
import { useEffect, useState } from 'react'
|
| 4 |
+
import { Sidebar } from '@/components/sidebar'
|
| 5 |
+
import { api, type Tool } from '@/lib/api'
|
| 6 |
+
import { Search, Play } from 'lucide-react'
|
| 7 |
+
|
| 8 |
+
export default function ToolsPage() {
|
| 9 |
+
const [tools, setTools] = useState<Tool[]>([])
|
| 10 |
+
const [loading, setLoading] = useState(true)
|
| 11 |
+
const [search, setSearch] = useState('')
|
| 12 |
+
|
| 13 |
+
useEffect(() => {
|
| 14 |
+
api.get<Tool[]>('/api/tools')
|
| 15 |
+
.then((data) => { setTools(data); setLoading(false) })
|
| 16 |
+
.catch(() => setLoading(false))
|
| 17 |
+
}, [])
|
| 18 |
+
|
| 19 |
+
const filtered = tools.filter((t) =>
|
| 20 |
+
!search || t.name.toLowerCase().includes(search.toLowerCase()) || t.description.toLowerCase().includes(search.toLowerCase())
|
| 21 |
+
)
|
| 22 |
+
|
| 23 |
+
return (
|
| 24 |
+
<div className="flex min-h-screen">
|
| 25 |
+
<Sidebar />
|
| 26 |
+
<main className="flex-1 flex flex-col">
|
| 27 |
+
<header className="h-14 bg-bg-secondary border-b border-border flex items-center px-6 justify-between">
|
| 28 |
+
<h1 className="text-base font-medium">Tools</h1>
|
| 29 |
+
<div className="relative">
|
| 30 |
+
<Search className="absolute left-3 top-1/2 -translate-y-1/2 w-4 h-4 text-text-dim" />
|
| 31 |
+
<input className="pl-9 pr-4 py-1.5 bg-bg-tertiary border border-border rounded-lg text-sm text-text-primary placeholder:text-text-dim focus:outline-none focus:border-accent-cyan w-64" placeholder="Search tools..." value={search} onChange={(e) => setSearch(e.target.value)} />
|
| 32 |
+
</div>
|
| 33 |
+
</header>
|
| 34 |
+
<div className="flex-1 p-6 overflow-auto">
|
| 35 |
+
{loading ? (
|
| 36 |
+
<div className="space-y-2">{Array.from({ length: 10 }).map((_, i) => <div key={i} className="bg-bg-tertiary border border-border rounded-lg p-4 animate-pulse"><div className="h-3 bg-bg-hover rounded w-32 mb-2" /><div className="h-2 bg-bg-hover rounded w-full" /></div>)}</div>
|
| 37 |
+
) : filtered.length === 0 ? (
|
| 38 |
+
<div className="text-center py-20"><p className="text-text-muted">No tools found</p></div>
|
| 39 |
+
) : (
|
| 40 |
+
<div className="space-y-2">
|
| 41 |
+
{filtered.map((t) => (
|
| 42 |
+
<div key={t.id} className="bg-bg-tertiary border border-border rounded-lg p-4 flex items-center justify-between hover:border-border-focus transition-colors">
|
| 43 |
+
<div className="flex items-center gap-4">
|
| 44 |
+
<div className="w-8 h-8 bg-bg-secondary border border-border rounded-lg flex items-center justify-center">
|
| 45 |
+
<Play className="w-3.5 h-3.5 text-text-muted" />
|
| 46 |
+
</div>
|
| 47 |
+
<div>
|
| 48 |
+
<p className="text-sm font-medium">{t.name}</p>
|
| 49 |
+
<p className="text-xs text-text-muted">{t.description}</p>
|
| 50 |
+
</div>
|
| 51 |
+
</div>
|
| 52 |
+
<div className="flex items-center gap-3">
|
| 53 |
+
<span className="text-[11px] bg-bg-secondary border border-border px-2 py-0.5 rounded text-text-muted">{t.integration_id}</span>
|
| 54 |
+
<span className="text-[11px] text-text-dim">{t.category}</span>
|
| 55 |
+
</div>
|
| 56 |
+
</div>
|
| 57 |
+
))}
|
| 58 |
+
</div>
|
| 59 |
+
)}
|
| 60 |
+
</div>
|
| 61 |
+
</main>
|
| 62 |
+
</div>
|
| 63 |
+
)
|
| 64 |
+
}
|
frontend/components/sidebar.tsx
ADDED
|
@@ -0,0 +1,93 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
'use client'
|
| 2 |
+
|
| 3 |
+
import Link from 'next/link'
|
| 4 |
+
import { usePathname } from 'next/navigation'
|
| 5 |
+
import { LayoutDashboard, Plug, Wrench, Key, BarChart3, Settings, LogOut, ExternalLink } from 'lucide-react'
|
| 6 |
+
import { cn } from '@/lib/utils'
|
| 7 |
+
|
| 8 |
+
const navItems = [
|
| 9 |
+
{ href: '/', icon: LayoutDashboard, label: 'Dashboard' },
|
| 10 |
+
{ href: '/apps', icon: Plug, label: 'Apps' },
|
| 11 |
+
{ href: '/tools', icon: Wrench, label: 'Tools' },
|
| 12 |
+
]
|
| 13 |
+
|
| 14 |
+
const configItems = [
|
| 15 |
+
{ href: '/api-keys', icon: Key, label: 'API Keys' },
|
| 16 |
+
{ href: '/analytics', icon: BarChart3, label: 'Analytics' },
|
| 17 |
+
{ href: '/settings', icon: Settings, label: 'Settings' },
|
| 18 |
+
]
|
| 19 |
+
|
| 20 |
+
export function Sidebar({ integrationCount = 0 }: { integrationCount?: number }) {
|
| 21 |
+
const pathname = usePathname()
|
| 22 |
+
|
| 23 |
+
return (
|
| 24 |
+
<aside className="w-60 bg-bg-secondary border-r border-border flex flex-col shrink-0">
|
| 25 |
+
<div className="px-5 py-4 border-b border-border">
|
| 26 |
+
<div className="flex items-center gap-3">
|
| 27 |
+
<div className="w-8 h-8 bg-accent-cyan rounded-lg flex items-center justify-center font-bold text-bg-primary text-sm">C</div>
|
| 28 |
+
<span className="text-base font-semibold tracking-tight">Compost</span>
|
| 29 |
+
</div>
|
| 30 |
+
</div>
|
| 31 |
+
|
| 32 |
+
<nav className="flex-1 px-3 py-4 space-y-1">
|
| 33 |
+
<div className="px-3 mb-2 text-[11px] font-semibold uppercase tracking-wider text-text-dim">Main</div>
|
| 34 |
+
{navItems.map((item) => (
|
| 35 |
+
<Link key={item.href} href={item.href}
|
| 36 |
+
className={cn(
|
| 37 |
+
'flex items-center gap-3 px-3 py-2 rounded-lg text-sm transition-colors',
|
| 38 |
+
pathname === item.href
|
| 39 |
+
? 'bg-accent-cyan/10 text-accent-cyanLight font-medium'
|
| 40 |
+
: 'text-text-secondary hover:bg-bg-hover hover:text-text-primary'
|
| 41 |
+
)}
|
| 42 |
+
>
|
| 43 |
+
<item.icon className="w-4 h-4" />
|
| 44 |
+
{item.label}
|
| 45 |
+
{item.href === '/apps' && integrationCount > 0 && (
|
| 46 |
+
<span className="ml-auto text-[11px] bg-bg-tertiary text-text-muted px-1.5 py-0.5 rounded">{integrationCount}</span>
|
| 47 |
+
)}
|
| 48 |
+
</Link>
|
| 49 |
+
))}
|
| 50 |
+
|
| 51 |
+
<div className="px-3 mt-6 mb-2 text-[11px] font-semibold uppercase tracking-wider text-text-dim">Configuration</div>
|
| 52 |
+
{configItems.map((item) => (
|
| 53 |
+
<Link key={item.href} href={item.href}
|
| 54 |
+
className={cn(
|
| 55 |
+
'flex items-center gap-3 px-3 py-2 rounded-lg text-sm transition-colors',
|
| 56 |
+
pathname === item.href
|
| 57 |
+
? 'bg-accent-cyan/10 text-accent-cyanLight font-medium'
|
| 58 |
+
: 'text-text-secondary hover:bg-bg-hover hover:text-text-primary'
|
| 59 |
+
)}
|
| 60 |
+
>
|
| 61 |
+
<item.icon className="w-4 h-4" />
|
| 62 |
+
{item.label}
|
| 63 |
+
</Link>
|
| 64 |
+
))}
|
| 65 |
+
|
| 66 |
+
<a href="/api/docs" target="_blank" rel="noopener noreferrer"
|
| 67 |
+
className="flex items-center gap-3 px-3 py-2 rounded-lg text-sm text-text-secondary hover:bg-bg-hover hover:text-text-primary transition-colors"
|
| 68 |
+
>
|
| 69 |
+
<ExternalLink className="w-4 h-4" />
|
| 70 |
+
API Docs
|
| 71 |
+
</a>
|
| 72 |
+
</nav>
|
| 73 |
+
|
| 74 |
+
<div className="px-3 py-4 border-t border-border">
|
| 75 |
+
<div className="flex items-center justify-between">
|
| 76 |
+
<div className="flex items-center gap-2 min-w-0">
|
| 77 |
+
<div className="w-7 h-7 bg-bg-tertiary rounded-full flex items-center justify-center text-xs font-semibold shrink-0">
|
| 78 |
+
{typeof window !== 'undefined' ? (localStorage.getItem('email')?.[0]?.toUpperCase() || 'U') : 'U'}
|
| 79 |
+
</div>
|
| 80 |
+
<span className="text-xs text-text-secondary truncate">
|
| 81 |
+
{typeof window !== 'undefined' ? localStorage.getItem('email') || 'User' : 'User'}
|
| 82 |
+
</span>
|
| 83 |
+
</div>
|
| 84 |
+
<button onClick={() => { localStorage.removeItem('token'); localStorage.removeItem('email'); window.location.href = '/login' }}
|
| 85 |
+
className="p-1.5 text-text-muted hover:text-accent-red hover:bg-accent-red/10 rounded-lg transition-colors"
|
| 86 |
+
>
|
| 87 |
+
<LogOut className="w-3.5 h-3.5" />
|
| 88 |
+
</button>
|
| 89 |
+
</div>
|
| 90 |
+
</div>
|
| 91 |
+
</aside>
|
| 92 |
+
)
|
| 93 |
+
}
|
frontend/index.html
DELETED
|
@@ -1,16 +0,0 @@
|
|
| 1 |
-
<!DOCTYPE html>
|
| 2 |
-
<html lang="en">
|
| 3 |
-
<head>
|
| 4 |
-
<meta charset="UTF-8" />
|
| 5 |
-
<link rel="icon" type="image/svg+xml" href="/vite.svg" />
|
| 6 |
-
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
|
| 7 |
-
<title>Platform - AI Tool Integration</title>
|
| 8 |
-
<link rel="preconnect" href="https://fonts.googleapis.com">
|
| 9 |
-
<link rel="preconnect" href="https://fonts.gstatic.com" crossorigin>
|
| 10 |
-
<link href="https://fonts.googleapis.com/css2?family=Inter:wght@300;400;500;600;700&display=swap" rel="stylesheet">
|
| 11 |
-
</head>
|
| 12 |
-
<body>
|
| 13 |
-
<div id="root"></div>
|
| 14 |
-
<script type="module" src="/src/main.tsx"></script>
|
| 15 |
-
</body>
|
| 16 |
-
</html>
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
frontend/lib/api.ts
ADDED
|
@@ -0,0 +1,55 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
export interface Integration {
|
| 2 |
+
id: string
|
| 3 |
+
name: string
|
| 4 |
+
description: string
|
| 5 |
+
logo_url: string
|
| 6 |
+
category: string
|
| 7 |
+
auth_type: string
|
| 8 |
+
tool_count: number
|
| 9 |
+
trigger_count: number
|
| 10 |
+
is_active: boolean
|
| 11 |
+
}
|
| 12 |
+
|
| 13 |
+
export interface Tool {
|
| 14 |
+
id: string
|
| 15 |
+
integration_id: string
|
| 16 |
+
name: string
|
| 17 |
+
description: string
|
| 18 |
+
input_schema: Record<string, unknown>
|
| 19 |
+
output_schema?: Record<string, unknown>
|
| 20 |
+
category: string
|
| 21 |
+
is_active: boolean
|
| 22 |
+
}
|
| 23 |
+
|
| 24 |
+
export interface Stats {
|
| 25 |
+
total_executions: number
|
| 26 |
+
successful: number
|
| 27 |
+
failed: number
|
| 28 |
+
success_rate: number
|
| 29 |
+
avg_latency_ms: number
|
| 30 |
+
}
|
| 31 |
+
|
| 32 |
+
const API_BASE = typeof window !== 'undefined' ? '' : process.env.NEXT_PUBLIC_API_URL || ''
|
| 33 |
+
|
| 34 |
+
async function fetchApi(path: string, opts: RequestInit = {}) {
|
| 35 |
+
const token = typeof window !== 'undefined' ? localStorage.getItem('token') : null
|
| 36 |
+
const headers: Record<string, string> = { 'Content-Type': 'application/json' }
|
| 37 |
+
if (token) headers['Authorization'] = `Bearer ${token}`
|
| 38 |
+
|
| 39 |
+
const res = await fetch(`${API_BASE}${path}`, { ...opts, headers })
|
| 40 |
+
if (res.status === 401) {
|
| 41 |
+
if (typeof window !== 'undefined') {
|
| 42 |
+
localStorage.removeItem('token')
|
| 43 |
+
window.location.href = '/login'
|
| 44 |
+
}
|
| 45 |
+
throw new Error('Unauthorized')
|
| 46 |
+
}
|
| 47 |
+
return res.json()
|
| 48 |
+
}
|
| 49 |
+
|
| 50 |
+
export const api = {
|
| 51 |
+
get: <T>(path: string) => fetchApi(path) as Promise<T>,
|
| 52 |
+
post: <T>(path: string, body?: unknown) =>
|
| 53 |
+
fetchApi(path, { method: 'POST', body: body ? JSON.stringify(body) : undefined }) as Promise<T>,
|
| 54 |
+
delete: <T>(path: string) => fetchApi(path, { method: 'DELETE' }) as Promise<T>,
|
| 55 |
+
}
|
frontend/lib/utils.ts
ADDED
|
@@ -0,0 +1,6 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
import { type ClassValue, clsx } from 'clsx'
|
| 2 |
+
import { twMerge } from 'tailwind-merge'
|
| 3 |
+
|
| 4 |
+
export function cn(...inputs: ClassValue[]) {
|
| 5 |
+
return twMerge(clsx(inputs))
|
| 6 |
+
}
|
frontend/next-env.d.ts
ADDED
|
@@ -0,0 +1,5 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
/// <reference types="next" />
|
| 2 |
+
/// <reference types="next/image-types/global" />
|
| 3 |
+
|
| 4 |
+
// NOTE: This file should not be edited
|
| 5 |
+
// see https://nextjs.org/docs/app/building-your-application/configuring/typescript for more information.
|
frontend/next.config.js
ADDED
|
@@ -0,0 +1,9 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
/** @type {import('next').NextConfig} */
|
| 2 |
+
const nextConfig = {
|
| 3 |
+
output: 'export',
|
| 4 |
+
distDir: 'dist',
|
| 5 |
+
images: { unoptimized: true },
|
| 6 |
+
trailingSlash: true,
|
| 7 |
+
}
|
| 8 |
+
|
| 9 |
+
module.exports = nextConfig
|
frontend/package-lock.json
CHANGED
|
The diff for this file is too large to render.
See raw diff
|
|
|
frontend/package.json
CHANGED
|
@@ -1,32 +1,28 @@
|
|
| 1 |
{
|
| 2 |
-
"name": "
|
| 3 |
-
"private": true,
|
| 4 |
"version": "1.0.0",
|
| 5 |
-
"
|
| 6 |
"scripts": {
|
| 7 |
-
"dev": "
|
| 8 |
-
"build": "
|
| 9 |
-
"
|
|
|
|
| 10 |
},
|
| 11 |
"dependencies": {
|
| 12 |
-
"
|
| 13 |
-
"react
|
| 14 |
-
"react-
|
| 15 |
-
"
|
| 16 |
-
"
|
| 17 |
-
"
|
| 18 |
-
"recharts": "^2.12.0",
|
| 19 |
-
"clsx": "^2.1.0",
|
| 20 |
-
"date-fns": "^3.3.1"
|
| 21 |
},
|
| 22 |
"devDependencies": {
|
| 23 |
-
"@types/
|
| 24 |
-
"@types/react
|
| 25 |
-
"@
|
| 26 |
-
"autoprefixer": "^10.4.
|
| 27 |
-
"postcss": "^8.4.
|
| 28 |
-
"tailwindcss": "^3.4.
|
| 29 |
-
"typescript": "^5.
|
| 30 |
-
"vite": "^5.1.0"
|
| 31 |
}
|
| 32 |
}
|
|
|
|
| 1 |
{
|
| 2 |
+
"name": "compost-frontend",
|
|
|
|
| 3 |
"version": "1.0.0",
|
| 4 |
+
"private": true,
|
| 5 |
"scripts": {
|
| 6 |
+
"dev": "next dev",
|
| 7 |
+
"build": "next build",
|
| 8 |
+
"start": "next start",
|
| 9 |
+
"lint": "next lint"
|
| 10 |
},
|
| 11 |
"dependencies": {
|
| 12 |
+
"next": "^14.2.0",
|
| 13 |
+
"react": "^18.3.0",
|
| 14 |
+
"react-dom": "^18.3.0",
|
| 15 |
+
"lucide-react": "^0.378.0",
|
| 16 |
+
"clsx": "^2.1.1",
|
| 17 |
+
"tailwind-merge": "^2.3.0"
|
|
|
|
|
|
|
|
|
|
| 18 |
},
|
| 19 |
"devDependencies": {
|
| 20 |
+
"@types/node": "^20.12.0",
|
| 21 |
+
"@types/react": "^18.3.0",
|
| 22 |
+
"@types/react-dom": "^18.3.0",
|
| 23 |
+
"autoprefixer": "^10.4.19",
|
| 24 |
+
"postcss": "^8.4.38",
|
| 25 |
+
"tailwindcss": "^3.4.3",
|
| 26 |
+
"typescript": "^5.4.5"
|
|
|
|
| 27 |
}
|
| 28 |
}
|
frontend/postcss.config.js
CHANGED
|
@@ -1,4 +1,4 @@
|
|
| 1 |
-
|
| 2 |
plugins: {
|
| 3 |
tailwindcss: {},
|
| 4 |
autoprefixer: {},
|
|
|
|
| 1 |
+
module.exports = {
|
| 2 |
plugins: {
|
| 3 |
tailwindcss: {},
|
| 4 |
autoprefixer: {},
|
frontend/src/App.tsx
DELETED
|
@@ -1,44 +0,0 @@
|
|
| 1 |
-
import { BrowserRouter, Routes, Route, Navigate } from 'react-router-dom'
|
| 2 |
-
import { useAuthStore } from './stores/authStore'
|
| 3 |
-
import Layout from './components/layout/Layout'
|
| 4 |
-
import Dashboard from './pages/Dashboard'
|
| 5 |
-
import Apps from './pages/Apps'
|
| 6 |
-
import AppDetail from './pages/AppDetail'
|
| 7 |
-
import Connections from './pages/Connections'
|
| 8 |
-
import Tools from './pages/Tools'
|
| 9 |
-
import Playground from './pages/Playground'
|
| 10 |
-
import APIKeys from './pages/APIKeys'
|
| 11 |
-
import Analytics from './pages/Analytics'
|
| 12 |
-
import Settings from './pages/Settings'
|
| 13 |
-
import Login from './pages/Login'
|
| 14 |
-
|
| 15 |
-
function ProtectedRoute({ children }: { children: React.ReactNode }) {
|
| 16 |
-
const { token } = useAuthStore()
|
| 17 |
-
if (!token) return <Navigate to="/login" />
|
| 18 |
-
return <>{children}</>
|
| 19 |
-
}
|
| 20 |
-
|
| 21 |
-
export default function App() {
|
| 22 |
-
return (
|
| 23 |
-
<BrowserRouter>
|
| 24 |
-
<Routes>
|
| 25 |
-
<Route path="/login" element={<Login />} />
|
| 26 |
-
<Route path="/" element={
|
| 27 |
-
<ProtectedRoute>
|
| 28 |
-
<Layout />
|
| 29 |
-
</ProtectedRoute>
|
| 30 |
-
}>
|
| 31 |
-
<Route index element={<Dashboard />} />
|
| 32 |
-
<Route path="apps" element={<Apps />} />
|
| 33 |
-
<Route path="apps/:id" element={<AppDetail />} />
|
| 34 |
-
<Route path="connections" element={<Connections />} />
|
| 35 |
-
<Route path="tools" element={<Tools />} />
|
| 36 |
-
<Route path="playground" element={<Playground />} />
|
| 37 |
-
<Route path="api-keys" element={<APIKeys />} />
|
| 38 |
-
<Route path="analytics" element={<Analytics />} />
|
| 39 |
-
<Route path="settings" element={<Settings />} />
|
| 40 |
-
</Route>
|
| 41 |
-
</Routes>
|
| 42 |
-
</BrowserRouter>
|
| 43 |
-
)
|
| 44 |
-
}
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
frontend/src/components/layout/Layout.tsx
DELETED
|
@@ -1,17 +0,0 @@
|
|
| 1 |
-
import { Outlet } from 'react-router-dom'
|
| 2 |
-
import Sidebar from './Sidebar'
|
| 3 |
-
import TopBar from './TopBar'
|
| 4 |
-
|
| 5 |
-
export default function Layout() {
|
| 6 |
-
return (
|
| 7 |
-
<div className="flex h-screen bg-[var(--bg-primary)]">
|
| 8 |
-
<Sidebar />
|
| 9 |
-
<div className="flex-1 flex flex-col overflow-hidden">
|
| 10 |
-
<TopBar />
|
| 11 |
-
<main className="flex-1 overflow-auto p-6">
|
| 12 |
-
<Outlet />
|
| 13 |
-
</main>
|
| 14 |
-
</div>
|
| 15 |
-
</div>
|
| 16 |
-
)
|
| 17 |
-
}
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
frontend/src/components/layout/Sidebar.tsx
DELETED
|
@@ -1,109 +0,0 @@
|
|
| 1 |
-
import { NavLink } from 'react-router-dom'
|
| 2 |
-
import { useAuthStore } from '../../stores/authStore'
|
| 3 |
-
import {
|
| 4 |
-
LayoutDashboard,
|
| 5 |
-
Plug,
|
| 6 |
-
Link2,
|
| 7 |
-
Wrench,
|
| 8 |
-
PlayCircle,
|
| 9 |
-
Key,
|
| 10 |
-
BarChart3,
|
| 11 |
-
Settings,
|
| 12 |
-
LogOut,
|
| 13 |
-
Terminal
|
| 14 |
-
} from 'lucide-react'
|
| 15 |
-
import clsx from 'clsx'
|
| 16 |
-
|
| 17 |
-
const navItems = [
|
| 18 |
-
{ path: '/', icon: LayoutDashboard, label: 'Dashboard' },
|
| 19 |
-
{ path: '/apps', icon: Plug, label: 'Apps' },
|
| 20 |
-
{ path: '/connections', icon: Link2, label: 'Connections' },
|
| 21 |
-
{ path: '/tools', icon: Wrench, label: 'Tools' },
|
| 22 |
-
{ path: '/playground', icon: PlayCircle, label: 'Playground' },
|
| 23 |
-
]
|
| 24 |
-
|
| 25 |
-
const configItems = [
|
| 26 |
-
{ path: '/api-keys', icon: Key, label: 'API Keys' },
|
| 27 |
-
{ path: '/analytics', icon: BarChart3, label: 'Analytics' },
|
| 28 |
-
{ path: '/settings', icon: Settings, label: 'Settings' },
|
| 29 |
-
]
|
| 30 |
-
|
| 31 |
-
export default function Sidebar() {
|
| 32 |
-
const { user, logout } = useAuthStore()
|
| 33 |
-
|
| 34 |
-
return (
|
| 35 |
-
<aside className="w-64 bg-[var(--bg-secondary)] border-r border-[var(--border)] flex flex-col">
|
| 36 |
-
<div className="p-4 border-b border-[var(--border)]">
|
| 37 |
-
<div className="flex items-center gap-3">
|
| 38 |
-
<div className="w-8 h-8 bg-cyan-500 rounded-lg flex items-center justify-center">
|
| 39 |
-
<Terminal className="w-5 h-5 text-slate-900" />
|
| 40 |
-
</div>
|
| 41 |
-
<span className="text-lg font-semibold text-white">Platform</span>
|
| 42 |
-
</div>
|
| 43 |
-
</div>
|
| 44 |
-
|
| 45 |
-
<nav className="flex-1 p-3 space-y-1">
|
| 46 |
-
<div className="text-xs font-medium text-[var(--text-muted)] uppercase tracking-wider px-3 mb-2">
|
| 47 |
-
Main
|
| 48 |
-
</div>
|
| 49 |
-
{navItems.map((item) => (
|
| 50 |
-
<NavLink
|
| 51 |
-
key={item.path}
|
| 52 |
-
to={item.path}
|
| 53 |
-
className={({ isActive }) => clsx(
|
| 54 |
-
'flex items-center gap-3 px-3 py-2 rounded-lg text-sm font-medium transition-colors',
|
| 55 |
-
isActive
|
| 56 |
-
? 'bg-cyan-500/10 text-cyan-400'
|
| 57 |
-
: 'text-[var(--text-secondary)] hover:bg-[var(--bg-hover)] hover:text-white'
|
| 58 |
-
)}
|
| 59 |
-
>
|
| 60 |
-
<item.icon className="w-4 h-4" />
|
| 61 |
-
{item.label}
|
| 62 |
-
</NavLink>
|
| 63 |
-
))}
|
| 64 |
-
|
| 65 |
-
<div className="text-xs font-medium text-[var(--text-muted)] uppercase tracking-wider px-3 mt-6 mb-2">
|
| 66 |
-
Configuration
|
| 67 |
-
</div>
|
| 68 |
-
{configItems.map((item) => (
|
| 69 |
-
<NavLink
|
| 70 |
-
key={item.path}
|
| 71 |
-
to={item.path}
|
| 72 |
-
className={({ isActive }) => clsx(
|
| 73 |
-
'flex items-center gap-3 px-3 py-2 rounded-lg text-sm font-medium transition-colors',
|
| 74 |
-
isActive
|
| 75 |
-
? 'bg-cyan-500/10 text-cyan-400'
|
| 76 |
-
: 'text-[var(--text-secondary)] hover:bg-[var(--bg-hover)] hover:text-white'
|
| 77 |
-
)}
|
| 78 |
-
>
|
| 79 |
-
<item.icon className="w-4 h-4" />
|
| 80 |
-
{item.label}
|
| 81 |
-
</NavLink>
|
| 82 |
-
))}
|
| 83 |
-
</nav>
|
| 84 |
-
|
| 85 |
-
<div className="p-3 border-t border-[var(--border)]">
|
| 86 |
-
<div className="flex items-center justify-between">
|
| 87 |
-
<div className="flex items-center gap-2">
|
| 88 |
-
<div className="w-8 h-8 bg-zinc-700 rounded-full flex items-center justify-center text-sm font-medium">
|
| 89 |
-
{user?.email?.[0]?.toUpperCase() || 'U'}
|
| 90 |
-
</div>
|
| 91 |
-
<span className="text-sm text-[var(--text-secondary)] truncate max-w-[120px]">
|
| 92 |
-
{user?.email || 'User'}
|
| 93 |
-
</span>
|
| 94 |
-
</div>
|
| 95 |
-
<button
|
| 96 |
-
onClick={logout}
|
| 97 |
-
className="p-2 text-[var(--text-muted)] hover:text-red-400 hover:bg-red-500/10 rounded-lg transition-colors"
|
| 98 |
-
title="Logout"
|
| 99 |
-
>
|
| 100 |
-
<LogOut className="w-4 h-4" />
|
| 101 |
-
</button>
|
| 102 |
-
</div>
|
| 103 |
-
<div className="text-xs text-[var(--text-muted)] mt-2 text-center">
|
| 104 |
-
Version 1.0.0
|
| 105 |
-
</div>
|
| 106 |
-
</div>
|
| 107 |
-
</aside>
|
| 108 |
-
)
|
| 109 |
-
}
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
frontend/src/components/layout/TopBar.tsx
DELETED
|
@@ -1,27 +0,0 @@
|
|
| 1 |
-
import { Search, Bell, HelpCircle } from 'lucide-react'
|
| 2 |
-
|
| 3 |
-
export default function TopBar() {
|
| 4 |
-
return (
|
| 5 |
-
<header className="h-14 bg-[var(--bg-secondary)] border-b border-[var(--border)] flex items-center justify-between px-6">
|
| 6 |
-
<div className="flex items-center gap-4 flex-1">
|
| 7 |
-
<div className="relative max-w-md flex-1">
|
| 8 |
-
<Search className="absolute left-3 top-1/2 -translate-y-1/2 w-4 h-4 text-[var(--text-muted)]" />
|
| 9 |
-
<input
|
| 10 |
-
type="text"
|
| 11 |
-
placeholder="Search integrations, tools..."
|
| 12 |
-
className="w-full bg-[var(--bg-primary)] border border-[var(--border)] rounded-lg pl-10 pr-4 py-2 text-sm text-white placeholder-[var(--text-muted)] focus:outline-none focus:border-cyan-500"
|
| 13 |
-
/>
|
| 14 |
-
</div>
|
| 15 |
-
</div>
|
| 16 |
-
|
| 17 |
-
<div className="flex items-center gap-2">
|
| 18 |
-
<button className="p-2 text-[var(--text-muted)] hover:text-white hover:bg-[var(--bg-hover)] rounded-lg transition-colors">
|
| 19 |
-
<HelpCircle className="w-4 h-4" />
|
| 20 |
-
</button>
|
| 21 |
-
<button className="p-2 text-[var(--text-muted)] hover:text-white hover:bg-[var(--bg-hover)] rounded-lg transition-colors">
|
| 22 |
-
<Bell className="w-4 h-4" />
|
| 23 |
-
</button>
|
| 24 |
-
</div>
|
| 25 |
-
</header>
|
| 26 |
-
)
|
| 27 |
-
}
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
frontend/src/index.css
DELETED
|
@@ -1,76 +0,0 @@
|
|
| 1 |
-
@tailwind base;
|
| 2 |
-
@tailwind components;
|
| 3 |
-
@tailwind utilities;
|
| 4 |
-
|
| 5 |
-
:root {
|
| 6 |
-
--bg-primary: #0a0a0b;
|
| 7 |
-
--bg-secondary: #111113;
|
| 8 |
-
--bg-tertiary: #18181b;
|
| 9 |
-
--bg-hover: #1f1f23;
|
| 10 |
-
--border: #27272a;
|
| 11 |
-
--border-hover: #3f3f46;
|
| 12 |
-
--text-primary: #fafafa;
|
| 13 |
-
--text-secondary: #a1a1aa;
|
| 14 |
-
--text-muted: #71717a;
|
| 15 |
-
--accent: #22d3ee;
|
| 16 |
-
--accent-hover: #06b6d4;
|
| 17 |
-
--accent-green: #10b981;
|
| 18 |
-
--accent-red: #ef4444;
|
| 19 |
-
--accent-yellow: #f59e0b;
|
| 20 |
-
--accent-blue: #3b82f6;
|
| 21 |
-
}
|
| 22 |
-
|
| 23 |
-
* {
|
| 24 |
-
box-sizing: border-box;
|
| 25 |
-
}
|
| 26 |
-
|
| 27 |
-
body {
|
| 28 |
-
margin: 0;
|
| 29 |
-
font-family: 'Inter', system-ui, sans-serif;
|
| 30 |
-
background-color: var(--bg-primary);
|
| 31 |
-
color: var(--text-primary);
|
| 32 |
-
-webkit-font-smoothing: antialiased;
|
| 33 |
-
-moz-osx-font-smoothing: grayscale;
|
| 34 |
-
}
|
| 35 |
-
|
| 36 |
-
::-webkit-scrollbar {
|
| 37 |
-
width: 8px;
|
| 38 |
-
height: 8px;
|
| 39 |
-
}
|
| 40 |
-
|
| 41 |
-
::-webkit-scrollbar-track {
|
| 42 |
-
background: var(--bg-secondary);
|
| 43 |
-
}
|
| 44 |
-
|
| 45 |
-
::-webkit-scrollbar-thumb {
|
| 46 |
-
background: var(--border);
|
| 47 |
-
border-radius: 4px;
|
| 48 |
-
}
|
| 49 |
-
|
| 50 |
-
::-webkit-scrollbar-thumb:hover {
|
| 51 |
-
background: var(--border-hover);
|
| 52 |
-
}
|
| 53 |
-
|
| 54 |
-
@layer components {
|
| 55 |
-
.btn {
|
| 56 |
-
@apply px-4 py-2 rounded-lg font-medium transition-all duration-200 flex items-center gap-2;
|
| 57 |
-
}
|
| 58 |
-
.btn-primary {
|
| 59 |
-
@apply bg-cyan-500 hover:bg-cyan-400 text-slate-900;
|
| 60 |
-
}
|
| 61 |
-
.btn-secondary {
|
| 62 |
-
@apply bg-zinc-800 hover:bg-zinc-700 text-zinc-100;
|
| 63 |
-
}
|
| 64 |
-
.btn-ghost {
|
| 65 |
-
@apply bg-transparent hover:bg-zinc-800 text-zinc-300;
|
| 66 |
-
}
|
| 67 |
-
.card {
|
| 68 |
-
@apply bg-zinc-900/50 border border-zinc-800 rounded-xl p-4;
|
| 69 |
-
}
|
| 70 |
-
.input {
|
| 71 |
-
@apply bg-zinc-900 border border-zinc-800 rounded-lg px-3 py-2 text-zinc-100 placeholder-zinc-500 focus:outline-none focus:border-cyan-500 transition-colors;
|
| 72 |
-
}
|
| 73 |
-
.badge {
|
| 74 |
-
@apply px-2 py-0.5 text-xs font-medium rounded-full;
|
| 75 |
-
}
|
| 76 |
-
}
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
frontend/src/lib/api.ts
DELETED
|
@@ -1,30 +0,0 @@
|
|
| 1 |
-
import axios from 'axios'
|
| 2 |
-
|
| 3 |
-
const api = axios.create({
|
| 4 |
-
baseURL: '',
|
| 5 |
-
headers: { 'Content-Type': 'application/json' }
|
| 6 |
-
})
|
| 7 |
-
|
| 8 |
-
api.interceptors.request.use((config) => {
|
| 9 |
-
const stored = localStorage.getItem('auth-storage')
|
| 10 |
-
if (stored) {
|
| 11 |
-
const { state } = JSON.parse(stored)
|
| 12 |
-
if (state?.token) {
|
| 13 |
-
config.headers.Authorization = `Bearer ${state.token}`
|
| 14 |
-
}
|
| 15 |
-
}
|
| 16 |
-
return config
|
| 17 |
-
})
|
| 18 |
-
|
| 19 |
-
api.interceptors.response.use(
|
| 20 |
-
(response) => response,
|
| 21 |
-
async (error) => {
|
| 22 |
-
if (error.response?.status === 401) {
|
| 23 |
-
localStorage.removeItem('auth-storage')
|
| 24 |
-
window.location.href = '/login'
|
| 25 |
-
}
|
| 26 |
-
return Promise.reject(error)
|
| 27 |
-
}
|
| 28 |
-
)
|
| 29 |
-
|
| 30 |
-
export default api
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
frontend/src/main.tsx
DELETED
|
@@ -1,10 +0,0 @@
|
|
| 1 |
-
import React from 'react'
|
| 2 |
-
import ReactDOM from 'react-dom/client'
|
| 3 |
-
import App from './App.tsx'
|
| 4 |
-
import './index.css'
|
| 5 |
-
|
| 6 |
-
ReactDOM.createRoot(document.getElementById('root')!).render(
|
| 7 |
-
<React.StrictMode>
|
| 8 |
-
<App />
|
| 9 |
-
</React.StrictMode>,
|
| 10 |
-
)
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
frontend/src/pages/APIKeys.tsx
DELETED
|
@@ -1,163 +0,0 @@
|
|
| 1 |
-
import { useEffect, useState } from 'react'
|
| 2 |
-
import { Plus, Trash2, Copy, Check } from 'lucide-react'
|
| 3 |
-
import api from '../lib/api'
|
| 4 |
-
|
| 5 |
-
interface APIKey {
|
| 6 |
-
id: string
|
| 7 |
-
name: string
|
| 8 |
-
key_prefix: string
|
| 9 |
-
created_at: string
|
| 10 |
-
last_used: string | null
|
| 11 |
-
}
|
| 12 |
-
|
| 13 |
-
export default function APIKeys() {
|
| 14 |
-
const [keys, setKeys] = useState<APIKey[]>([])
|
| 15 |
-
const [loading, setLoading] = useState(true)
|
| 16 |
-
const [showCreate, setShowCreate] = useState(false)
|
| 17 |
-
const [newKeyName, setNewKeyName] = useState('')
|
| 18 |
-
const [newKey, setNewKey] = useState('')
|
| 19 |
-
|
| 20 |
-
useEffect(() => {
|
| 21 |
-
fetchKeys()
|
| 22 |
-
}, [])
|
| 23 |
-
|
| 24 |
-
const fetchKeys = async () => {
|
| 25 |
-
try {
|
| 26 |
-
const response = await api.get('/api/apikeys')
|
| 27 |
-
setKeys(response.data)
|
| 28 |
-
} catch (e) {
|
| 29 |
-
console.error(e)
|
| 30 |
-
} finally {
|
| 31 |
-
setLoading(false)
|
| 32 |
-
}
|
| 33 |
-
}
|
| 34 |
-
|
| 35 |
-
const handleCreate = async () => {
|
| 36 |
-
if (!newKeyName.trim()) return
|
| 37 |
-
try {
|
| 38 |
-
const response = await api.post('/api/apikeys', { name: newKeyName })
|
| 39 |
-
setNewKey(response.data.api_key)
|
| 40 |
-
setNewKeyName('')
|
| 41 |
-
fetchKeys()
|
| 42 |
-
} catch (e) {
|
| 43 |
-
console.error(e)
|
| 44 |
-
}
|
| 45 |
-
}
|
| 46 |
-
|
| 47 |
-
const handleDelete = async (id: string) => {
|
| 48 |
-
if (!confirm('Are you sure you want to delete this API key?')) return
|
| 49 |
-
try {
|
| 50 |
-
await api.delete(`/api/apikeys/${id}`)
|
| 51 |
-
fetchKeys()
|
| 52 |
-
} catch (e) {
|
| 53 |
-
console.error(e)
|
| 54 |
-
}
|
| 55 |
-
}
|
| 56 |
-
|
| 57 |
-
const copyKey = (key: string) => {
|
| 58 |
-
navigator.clipboard.writeText(key)
|
| 59 |
-
}
|
| 60 |
-
|
| 61 |
-
return (
|
| 62 |
-
<div className="space-y-6">
|
| 63 |
-
<div className="flex items-center justify-between">
|
| 64 |
-
<div>
|
| 65 |
-
<h1 className="text-2xl font-semibold text-white">API Keys</h1>
|
| 66 |
-
<p className="text-[var(--text-muted)] mt-1">Manage API keys for programmatic access</p>
|
| 67 |
-
</div>
|
| 68 |
-
<button onClick={() => setShowCreate(true)} className="btn btn-primary">
|
| 69 |
-
<Plus className="w-4 h-4" />
|
| 70 |
-
Create Key
|
| 71 |
-
</button>
|
| 72 |
-
</div>
|
| 73 |
-
|
| 74 |
-
{newKey && (
|
| 75 |
-
<div className="card border-amber-500/50 bg-amber-500/5">
|
| 76 |
-
<h3 className="font-medium text-amber-400 mb-2">API Key Created</h3>
|
| 77 |
-
<p className="text-sm text-[var(--text-muted)] mb-3">
|
| 78 |
-
Copy this key now. You won't be able to see it again!
|
| 79 |
-
</p>
|
| 80 |
-
<div className="flex items-center gap-2">
|
| 81 |
-
<code className="flex-1 bg-zinc-950 p-3 rounded-lg text-sm text-white font-mono break-all">
|
| 82 |
-
{newKey}
|
| 83 |
-
</code>
|
| 84 |
-
<button
|
| 85 |
-
onClick={() => copyKey(newKey)}
|
| 86 |
-
className="btn btn-secondary"
|
| 87 |
-
>
|
| 88 |
-
<Copy className="w-4 h-4" />
|
| 89 |
-
</button>
|
| 90 |
-
</div>
|
| 91 |
-
<button onClick={() => setNewKey('')} className="text-sm text-[var(--text-muted)] mt-2 hover:text-white">
|
| 92 |
-
I've copied the key
|
| 93 |
-
</button>
|
| 94 |
-
</div>
|
| 95 |
-
)}
|
| 96 |
-
|
| 97 |
-
{showCreate && !newKey && (
|
| 98 |
-
<div className="card">
|
| 99 |
-
<h3 className="font-medium text-white mb-3">Create New API Key</h3>
|
| 100 |
-
<div className="flex gap-3">
|
| 101 |
-
<input
|
| 102 |
-
type="text"
|
| 103 |
-
placeholder="Key name (e.g., 'Production')"
|
| 104 |
-
value={newKeyName}
|
| 105 |
-
onChange={(e) => setNewKeyName(e.target.value)}
|
| 106 |
-
className="input flex-1"
|
| 107 |
-
/>
|
| 108 |
-
<button onClick={handleCreate} className="btn btn-primary">
|
| 109 |
-
Create
|
| 110 |
-
</button>
|
| 111 |
-
<button onClick={() => setShowCreate(false)} className="btn btn-secondary">
|
| 112 |
-
Cancel
|
| 113 |
-
</button>
|
| 114 |
-
</div>
|
| 115 |
-
</div>
|
| 116 |
-
)}
|
| 117 |
-
|
| 118 |
-
{loading ? (
|
| 119 |
-
<div className="text-[var(--text-muted)]">Loading...</div>
|
| 120 |
-
) : keys.length === 0 ? (
|
| 121 |
-
<div className="card text-center py-12">
|
| 122 |
-
<p className="text-[var(--text-muted)]">No API keys yet</p>
|
| 123 |
-
</div>
|
| 124 |
-
) : (
|
| 125 |
-
<div className="card overflow-hidden p-0">
|
| 126 |
-
<table className="w-full">
|
| 127 |
-
<thead className="bg-zinc-800/50">
|
| 128 |
-
<tr>
|
| 129 |
-
<th className="px-4 py-3 text-left text-xs font-medium text-[var(--text-muted)] uppercase">Name</th>
|
| 130 |
-
<th className="px-4 py-3 text-left text-xs font-medium text-[var(--text-muted)] uppercase">Key</th>
|
| 131 |
-
<th className="px-4 py-3 text-left text-xs font-medium text-[var(--text-muted)] uppercase">Created</th>
|
| 132 |
-
<th className="px-4 py-3 text-left text-xs font-medium text-[var(--text-muted)] uppercase">Last Used</th>
|
| 133 |
-
<th className="px-4 py-3 text-right text-xs font-medium text-[var(--text-muted)] uppercase">Actions</th>
|
| 134 |
-
</tr>
|
| 135 |
-
</thead>
|
| 136 |
-
<tbody className="divide-y divide-[var(--border)]">
|
| 137 |
-
{keys.map((key) => (
|
| 138 |
-
<tr key={key.id} className="hover:bg-[var(--bg-hover)]">
|
| 139 |
-
<td className="px-4 py-3 font-medium text-white">{key.name}</td>
|
| 140 |
-
<td className="px-4 py-3 font-mono text-sm text-[var(--text-muted)]">{key.key_prefix}</td>
|
| 141 |
-
<td className="px-4 py-3 text-[var(--text-muted)] text-sm">
|
| 142 |
-
{new Date(key.created_at).toLocaleDateString()}
|
| 143 |
-
</td>
|
| 144 |
-
<td className="px-4 py-3 text-[var(--text-muted)] text-sm">
|
| 145 |
-
{key.last_used ? new Date(key.last_used).toLocaleDateString() : 'Never'}
|
| 146 |
-
</td>
|
| 147 |
-
<td className="px-4 py-3 text-right">
|
| 148 |
-
<button
|
| 149 |
-
onClick={() => handleDelete(key.id)}
|
| 150 |
-
className="p-2 text-[var(--text-muted)] hover:text-red-400 hover:bg-red-500/10 rounded-lg transition-colors"
|
| 151 |
-
>
|
| 152 |
-
<Trash2 className="w-4 h-4" />
|
| 153 |
-
</button>
|
| 154 |
-
</td>
|
| 155 |
-
</tr>
|
| 156 |
-
))}
|
| 157 |
-
</tbody>
|
| 158 |
-
</table>
|
| 159 |
-
</div>
|
| 160 |
-
)}
|
| 161 |
-
</div>
|
| 162 |
-
)
|
| 163 |
-
}
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
frontend/src/pages/Analytics.tsx
DELETED
|
@@ -1,117 +0,0 @@
|
|
| 1 |
-
import { useEffect, useState } from 'react'
|
| 2 |
-
import { BarChart, Bar, XAxis, YAxis, Tooltip, ResponsiveContainer } from 'recharts'
|
| 3 |
-
import api from '../lib/api'
|
| 4 |
-
|
| 5 |
-
interface Stats {
|
| 6 |
-
total_executions: number
|
| 7 |
-
successful: number
|
| 8 |
-
failed: number
|
| 9 |
-
success_rate: number
|
| 10 |
-
avg_latency_ms: number
|
| 11 |
-
}
|
| 12 |
-
|
| 13 |
-
interface ToolStats {
|
| 14 |
-
tool_id: string
|
| 15 |
-
count: number
|
| 16 |
-
}
|
| 17 |
-
|
| 18 |
-
export default function Analytics() {
|
| 19 |
-
const [stats, setStats] = useState<Stats | null>(null)
|
| 20 |
-
const [topTools, setTopTools] = useState<ToolStats[]>([])
|
| 21 |
-
const [executions, setExecutions] = useState<any[]>([])
|
| 22 |
-
const [loading, setLoading] = useState(true)
|
| 23 |
-
|
| 24 |
-
useEffect(() => {
|
| 25 |
-
const fetchData = async () => {
|
| 26 |
-
try {
|
| 27 |
-
const [overview, tools, execData] = await Promise.all([
|
| 28 |
-
api.get('/api/analytics/overview'),
|
| 29 |
-
api.get('/api/analytics/top-tools'),
|
| 30 |
-
api.get('/api/analytics/executions?days=7')
|
| 31 |
-
])
|
| 32 |
-
setStats(overview.data)
|
| 33 |
-
setTopTools(tools.data.tools || [])
|
| 34 |
-
setExecutions(execData.data.executions || [])
|
| 35 |
-
} catch (e) {
|
| 36 |
-
console.error(e)
|
| 37 |
-
} finally {
|
| 38 |
-
setLoading(false)
|
| 39 |
-
}
|
| 40 |
-
}
|
| 41 |
-
fetchData()
|
| 42 |
-
}, [])
|
| 43 |
-
|
| 44 |
-
if (loading) {
|
| 45 |
-
return <div className="text-[var(--text-muted)]">Loading...</div>
|
| 46 |
-
}
|
| 47 |
-
|
| 48 |
-
return (
|
| 49 |
-
<div className="space-y-6">
|
| 50 |
-
<div>
|
| 51 |
-
<h1 className="text-2xl font-semibold text-white">Analytics</h1>
|
| 52 |
-
<p className="text-[var(--text-muted)] mt-1">Usage metrics and performance insights</p>
|
| 53 |
-
</div>
|
| 54 |
-
|
| 55 |
-
<div className="grid grid-cols-1 md:grid-cols-4 gap-4">
|
| 56 |
-
<div className="card">
|
| 57 |
-
<p className="text-sm text-[var(--text-muted)]">Total Executions</p>
|
| 58 |
-
<p className="text-2xl font-semibold text-white mt-1">{stats?.total_executions || 0}</p>
|
| 59 |
-
</div>
|
| 60 |
-
<div className="card">
|
| 61 |
-
<p className="text-sm text-[var(--text-muted)]">Successful</p>
|
| 62 |
-
<p className="text-2xl font-semibold text-emerald-400 mt-1">{stats?.successful || 0}</p>
|
| 63 |
-
</div>
|
| 64 |
-
<div className="card">
|
| 65 |
-
<p className="text-sm text-[var(--text-muted)]">Failed</p>
|
| 66 |
-
<p className="text-2xl font-semibold text-red-400 mt-1">{stats?.failed || 0}</p>
|
| 67 |
-
</div>
|
| 68 |
-
<div className="card">
|
| 69 |
-
<p className="text-sm text-[var(--text-muted)]">Avg Latency</p>
|
| 70 |
-
<p className="text-2xl font-semibold text-cyan-400 mt-1">{stats?.avg_latency_ms || 0}ms</p>
|
| 71 |
-
</div>
|
| 72 |
-
</div>
|
| 73 |
-
|
| 74 |
-
<div className="grid grid-cols-1 lg:grid-cols-2 gap-6">
|
| 75 |
-
<div className="card">
|
| 76 |
-
<h3 className="font-medium text-white mb-4">Execution Trend (7 days)</h3>
|
| 77 |
-
<div className="h-64">
|
| 78 |
-
<ResponsiveContainer width="100%" height="100%">
|
| 79 |
-
<BarChart data={executions}>
|
| 80 |
-
<XAxis dataKey="date" stroke="#71717a" fontSize={12} />
|
| 81 |
-
<YAxis stroke="#71717a" fontSize={12} />
|
| 82 |
-
<Tooltip
|
| 83 |
-
contentStyle={{
|
| 84 |
-
backgroundColor: '#18181b',
|
| 85 |
-
border: '1px solid #27272a',
|
| 86 |
-
borderRadius: '8px'
|
| 87 |
-
}}
|
| 88 |
-
/>
|
| 89 |
-
<Bar dataKey="count" fill="#22d3ee" radius={[4, 4, 0, 0]} />
|
| 90 |
-
</BarChart>
|
| 91 |
-
</ResponsiveContainer>
|
| 92 |
-
</div>
|
| 93 |
-
</div>
|
| 94 |
-
|
| 95 |
-
<div className="card">
|
| 96 |
-
<h3 className="font-medium text-white mb-4">Top Tools</h3>
|
| 97 |
-
<div className="space-y-3">
|
| 98 |
-
{topTools.map((tool, idx) => (
|
| 99 |
-
<div key={tool.tool_id} className="flex items-center justify-between">
|
| 100 |
-
<div className="flex items-center gap-3">
|
| 101 |
-
<span className="w-6 h-6 bg-zinc-800 rounded flex items-center justify-center text-xs text-[var(--text-muted)]">
|
| 102 |
-
{idx + 1}
|
| 103 |
-
</span>
|
| 104 |
-
<span className="font-mono text-sm text-white">{tool.tool_id}</span>
|
| 105 |
-
</div>
|
| 106 |
-
<span className="text-sm text-[var(--text-muted)]">{tool.count} calls</span>
|
| 107 |
-
</div>
|
| 108 |
-
))}
|
| 109 |
-
{topTools.length === 0 && (
|
| 110 |
-
<p className="text-[var(--text-muted)] text-center py-4">No tool usage data yet</p>
|
| 111 |
-
)}
|
| 112 |
-
</div>
|
| 113 |
-
</div>
|
| 114 |
-
</div>
|
| 115 |
-
</div>
|
| 116 |
-
)
|
| 117 |
-
}
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
frontend/src/pages/AppDetail.tsx
DELETED
|
@@ -1,217 +0,0 @@
|
|
| 1 |
-
import { useEffect, useState } from 'react'
|
| 2 |
-
import { useParams, useNavigate } from 'react-router-dom'
|
| 3 |
-
import { ArrowLeft, Check, ExternalLink, Key, Zap } from 'lucide-react'
|
| 4 |
-
import api from '../lib/api'
|
| 5 |
-
import { useAuthStore } from '../stores/authStore'
|
| 6 |
-
|
| 7 |
-
interface Integration {
|
| 8 |
-
id: string
|
| 9 |
-
name: string
|
| 10 |
-
description: string
|
| 11 |
-
logo_url: string
|
| 12 |
-
category: string
|
| 13 |
-
auth_type: string
|
| 14 |
-
oauth_scopes: string[]
|
| 15 |
-
tool_count: number
|
| 16 |
-
trigger_count: number
|
| 17 |
-
}
|
| 18 |
-
|
| 19 |
-
interface Tool {
|
| 20 |
-
id: string
|
| 21 |
-
name: string
|
| 22 |
-
description: string
|
| 23 |
-
category: string
|
| 24 |
-
}
|
| 25 |
-
|
| 26 |
-
export default function AppDetail() {
|
| 27 |
-
const { id } = useParams<{ id: string }>()
|
| 28 |
-
const navigate = useNavigate()
|
| 29 |
-
const { token } = useAuthStore()
|
| 30 |
-
const [integration, setIntegration] = useState<Integration | null>(null)
|
| 31 |
-
const [tools, setTools] = useState<Tool[]>([])
|
| 32 |
-
const [activeTab, setActiveTab] = useState<'overview' | 'tools' | 'setup'>('overview')
|
| 33 |
-
const [loading, setLoading] = useState(true)
|
| 34 |
-
|
| 35 |
-
useEffect(() => {
|
| 36 |
-
const fetchData = async () => {
|
| 37 |
-
try {
|
| 38 |
-
const [intRes, toolsRes] = await Promise.all([
|
| 39 |
-
api.get(`/api/apps/${id}`),
|
| 40 |
-
api.get(`/api/tools?integration_id=${id}`)
|
| 41 |
-
])
|
| 42 |
-
setIntegration(intRes.data)
|
| 43 |
-
setTools(toolsRes.data)
|
| 44 |
-
} catch (e) {
|
| 45 |
-
console.error(e)
|
| 46 |
-
} finally {
|
| 47 |
-
setLoading(false)
|
| 48 |
-
}
|
| 49 |
-
}
|
| 50 |
-
if (id) fetchData()
|
| 51 |
-
}, [id])
|
| 52 |
-
|
| 53 |
-
const handleConnect = async () => {
|
| 54 |
-
if (integration?.auth_type === 'oauth2') {
|
| 55 |
-
try {
|
| 56 |
-
const response = await api.post(`/api/apps/${id}/connect`)
|
| 57 |
-
if (response.data.auth_url) {
|
| 58 |
-
window.location.href = response.data.auth_url
|
| 59 |
-
}
|
| 60 |
-
} catch (e) {
|
| 61 |
-
console.error(e)
|
| 62 |
-
}
|
| 63 |
-
}
|
| 64 |
-
}
|
| 65 |
-
|
| 66 |
-
if (loading) {
|
| 67 |
-
return <div className="text-[var(--text-muted)]">Loading...</div>
|
| 68 |
-
}
|
| 69 |
-
|
| 70 |
-
if (!integration) {
|
| 71 |
-
return <div className="text-[var(--text-muted)]">Integration not found</div>
|
| 72 |
-
}
|
| 73 |
-
|
| 74 |
-
return (
|
| 75 |
-
<div className="space-y-6">
|
| 76 |
-
<button
|
| 77 |
-
onClick={() => navigate('/apps')}
|
| 78 |
-
className="flex items-center gap-2 text-[var(--text-muted)] hover:text-white transition-colors"
|
| 79 |
-
>
|
| 80 |
-
<ArrowLeft className="w-4 h-4" />
|
| 81 |
-
Back to Integrations
|
| 82 |
-
</button>
|
| 83 |
-
|
| 84 |
-
<div className="flex items-start gap-4">
|
| 85 |
-
<img
|
| 86 |
-
src={integration.logo_url}
|
| 87 |
-
alt={integration.name}
|
| 88 |
-
className="w-16 h-16 rounded-xl bg-zinc-800"
|
| 89 |
-
/>
|
| 90 |
-
<div className="flex-1">
|
| 91 |
-
<h1 className="text-2xl font-semibold text-white">{integration.name}</h1>
|
| 92 |
-
<p className="text-[var(--text-secondary)] mt-1">{integration.description}</p>
|
| 93 |
-
<div className="flex items-center gap-3 mt-3">
|
| 94 |
-
<span className="badge bg-zinc-800 text-[var(--text-muted)]">{integration.category}</span>
|
| 95 |
-
<span className="badge bg-cyan-500/10 text-cyan-400 capitalize">{integration.auth_type}</span>
|
| 96 |
-
<span className="text-sm text-[var(--text-muted)]">{integration.tool_count} tools</span>
|
| 97 |
-
</div>
|
| 98 |
-
</div>
|
| 99 |
-
<button
|
| 100 |
-
onClick={handleConnect}
|
| 101 |
-
className="btn btn-primary"
|
| 102 |
-
>
|
| 103 |
-
<Key className="w-4 h-4" />
|
| 104 |
-
Connect
|
| 105 |
-
</button>
|
| 106 |
-
</div>
|
| 107 |
-
|
| 108 |
-
<div className="flex gap-2 border-b border-[var(--border)]">
|
| 109 |
-
{(['overview', 'tools', 'setup'] as const).map((tab) => (
|
| 110 |
-
<button
|
| 111 |
-
key={tab}
|
| 112 |
-
onClick={() => setActiveTab(tab)}
|
| 113 |
-
className={`px-4 py-2 text-sm font-medium border-b-2 -mb-px transition-colors ${
|
| 114 |
-
activeTab === tab
|
| 115 |
-
? 'border-cyan-500 text-cyan-400'
|
| 116 |
-
: 'border-transparent text-[var(--text-muted)] hover:text-white'
|
| 117 |
-
}`}
|
| 118 |
-
>
|
| 119 |
-
{tab.charAt(0).toUpperCase() + tab.slice(1)}
|
| 120 |
-
</button>
|
| 121 |
-
))}
|
| 122 |
-
</div>
|
| 123 |
-
|
| 124 |
-
{activeTab === 'overview' && (
|
| 125 |
-
<div className="grid grid-cols-1 md:grid-cols-2 gap-6">
|
| 126 |
-
<div className="card">
|
| 127 |
-
<h3 className="font-medium text-white mb-4">About</h3>
|
| 128 |
-
<p className="text-[var(--text-secondary)] text-sm">{integration.description}</p>
|
| 129 |
-
{integration.oauth_scopes && integration.oauth_scopes.length > 0 && (
|
| 130 |
-
<div className="mt-4">
|
| 131 |
-
<h4 className="text-sm font-medium text-[var(--text-muted)] mb-2">Required Permissions</h4>
|
| 132 |
-
<div className="flex flex-wrap gap-2">
|
| 133 |
-
{integration.oauth_scopes.map((scope) => (
|
| 134 |
-
<span key={scope} className="px-2 py-1 bg-zinc-800 rounded text-xs text-[var(--text-secondary)]">
|
| 135 |
-
{scope}
|
| 136 |
-
</span>
|
| 137 |
-
))}
|
| 138 |
-
</div>
|
| 139 |
-
</div>
|
| 140 |
-
)}
|
| 141 |
-
</div>
|
| 142 |
-
<div className="card">
|
| 143 |
-
<h3 className="font-medium text-white mb-4">Capabilities</h3>
|
| 144 |
-
<div className="space-y-3">
|
| 145 |
-
<div className="flex items-center gap-3">
|
| 146 |
-
<div className="p-2 bg-cyan-500/10 rounded-lg">
|
| 147 |
-
<Key className="w-4 h-4 text-cyan-400" />
|
| 148 |
-
</div>
|
| 149 |
-
<div>
|
| 150 |
-
<p className="text-sm font-medium text-white">{integration.tool_count} Tools</p>
|
| 151 |
-
<p className="text-xs text-[var(--text-muted)]">Actions you can perform</p>
|
| 152 |
-
</div>
|
| 153 |
-
</div>
|
| 154 |
-
<div className="flex items-center gap-3">
|
| 155 |
-
<div className="p-2 bg-emerald-500/10 rounded-lg">
|
| 156 |
-
<Zap className="w-4 h-4 text-emerald-400" />
|
| 157 |
-
</div>
|
| 158 |
-
<div>
|
| 159 |
-
<p className="text-sm font-medium text-white">{integration.trigger_count} Triggers</p>
|
| 160 |
-
<p className="text-xs text-[var(--text-muted)]">Event-based automation</p>
|
| 161 |
-
</div>
|
| 162 |
-
</div>
|
| 163 |
-
</div>
|
| 164 |
-
</div>
|
| 165 |
-
</div>
|
| 166 |
-
)}
|
| 167 |
-
|
| 168 |
-
{activeTab === 'tools' && (
|
| 169 |
-
<div className="space-y-3">
|
| 170 |
-
{tools.map((tool) => (
|
| 171 |
-
<div key={tool.id} className="card">
|
| 172 |
-
<div className="flex items-start justify-between">
|
| 173 |
-
<div>
|
| 174 |
-
<h4 className="font-medium text-white">{tool.name}</h4>
|
| 175 |
-
<p className="text-sm text-[var(--text-muted)] mt-1">{tool.description}</p>
|
| 176 |
-
</div>
|
| 177 |
-
<span className="badge bg-zinc-800 text-[var(--text-muted)]">{tool.category}</span>
|
| 178 |
-
</div>
|
| 179 |
-
</div>
|
| 180 |
-
))}
|
| 181 |
-
{tools.length === 0 && (
|
| 182 |
-
<div className="text-center py-8 text-[var(--text-muted)]">No tools available</div>
|
| 183 |
-
)}
|
| 184 |
-
</div>
|
| 185 |
-
)}
|
| 186 |
-
|
| 187 |
-
{activeTab === 'setup' && (
|
| 188 |
-
<div className="card">
|
| 189 |
-
<h3 className="font-medium text-white mb-4">Setup Instructions</h3>
|
| 190 |
-
<div className="space-y-4 text-sm text-[var(--text-secondary)]">
|
| 191 |
-
<div className="flex gap-3">
|
| 192 |
-
<span className="w-6 h-6 bg-cyan-500 rounded-full flex items-center justify-center text-xs font-medium text-slate-900">1</span>
|
| 193 |
-
<div>
|
| 194 |
-
<p className="text-white font-medium">Click "Connect" button above</p>
|
| 195 |
-
<p>You'll be redirected to {integration.name} to authorize access</p>
|
| 196 |
-
</div>
|
| 197 |
-
</div>
|
| 198 |
-
<div className="flex gap-3">
|
| 199 |
-
<span className="w-6 h-6 bg-zinc-700 rounded-full flex items-center justify-center text-xs font-medium text-white">2</span>
|
| 200 |
-
<div>
|
| 201 |
-
<p className="text-white font-medium">Grant permissions</p>
|
| 202 |
-
<p>Review and allow the requested scopes</p>
|
| 203 |
-
</div>
|
| 204 |
-
</div>
|
| 205 |
-
<div className="flex gap-3">
|
| 206 |
-
<span className="w-6 h-6 bg-zinc-700 rounded-full flex items-center justify-center text-xs font-medium text-white">3</span>
|
| 207 |
-
<div>
|
| 208 |
-
<p className="text-white font-medium">Start using the integration</p>
|
| 209 |
-
<p>Your connection will be saved automatically</p>
|
| 210 |
-
</div>
|
| 211 |
-
</div>
|
| 212 |
-
</div>
|
| 213 |
-
</div>
|
| 214 |
-
)}
|
| 215 |
-
</div>
|
| 216 |
-
)
|
| 217 |
-
}
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
frontend/src/pages/Apps.tsx
DELETED
|
@@ -1,122 +0,0 @@
|
|
| 1 |
-
import { useEffect, useState } from 'react'
|
| 2 |
-
import { useNavigate } from 'react-router-dom'
|
| 3 |
-
import { Search, ExternalLink } from 'lucide-react'
|
| 4 |
-
import api from '../lib/api'
|
| 5 |
-
|
| 6 |
-
interface Integration {
|
| 7 |
-
id: string
|
| 8 |
-
name: string
|
| 9 |
-
description: string
|
| 10 |
-
logo_url: string
|
| 11 |
-
category: string
|
| 12 |
-
auth_type: string
|
| 13 |
-
tool_count: number
|
| 14 |
-
trigger_count: number
|
| 15 |
-
}
|
| 16 |
-
|
| 17 |
-
const categories = ['All', 'Developer Tools', 'Communication', 'Productivity', 'CRM', 'Finance', 'Social']
|
| 18 |
-
|
| 19 |
-
export default function Apps() {
|
| 20 |
-
const [integrations, setIntegrations] = useState<Integration[]>([])
|
| 21 |
-
const [loading, setLoading] = useState(true)
|
| 22 |
-
const [search, setSearch] = useState('')
|
| 23 |
-
const [category, setCategory] = useState('All')
|
| 24 |
-
const navigate = useNavigate()
|
| 25 |
-
|
| 26 |
-
useEffect(() => {
|
| 27 |
-
const fetchIntegrations = async () => {
|
| 28 |
-
try {
|
| 29 |
-
const params = new URLSearchParams()
|
| 30 |
-
if (search) params.append('search', search)
|
| 31 |
-
if (category !== 'All') params.append('category', category)
|
| 32 |
-
|
| 33 |
-
const response = await api.get(`/api/apps?${params}`)
|
| 34 |
-
setIntegrations(response.data)
|
| 35 |
-
} catch (e) {
|
| 36 |
-
console.error(e)
|
| 37 |
-
} finally {
|
| 38 |
-
setLoading(false)
|
| 39 |
-
}
|
| 40 |
-
}
|
| 41 |
-
fetchIntegrations()
|
| 42 |
-
}, [search, category])
|
| 43 |
-
|
| 44 |
-
return (
|
| 45 |
-
<div className="space-y-6">
|
| 46 |
-
<div>
|
| 47 |
-
<h1 className="text-2xl font-semibold text-white">Integrations</h1>
|
| 48 |
-
<p className="text-[var(--text-muted)] mt-1">Browse and connect to 250+ external services</p>
|
| 49 |
-
</div>
|
| 50 |
-
|
| 51 |
-
<div className="flex items-center gap-4 flex-wrap">
|
| 52 |
-
<div className="relative flex-1 max-w-md">
|
| 53 |
-
<Search className="absolute left-3 top-1/2 -translate-y-1/2 w-4 h-4 text-[var(--text-muted)]" />
|
| 54 |
-
<input
|
| 55 |
-
type="text"
|
| 56 |
-
placeholder="Search integrations..."
|
| 57 |
-
value={search}
|
| 58 |
-
onChange={(e) => setSearch(e.target.value)}
|
| 59 |
-
className="input w-full pl-10"
|
| 60 |
-
/>
|
| 61 |
-
</div>
|
| 62 |
-
<div className="flex gap-2 flex-wrap">
|
| 63 |
-
{categories.map((cat) => (
|
| 64 |
-
<button
|
| 65 |
-
key={cat}
|
| 66 |
-
onClick={() => setCategory(cat)}
|
| 67 |
-
className={`px-3 py-1.5 rounded-lg text-sm font-medium transition-colors ${
|
| 68 |
-
category === cat
|
| 69 |
-
? 'bg-cyan-500 text-slate-900'
|
| 70 |
-
: 'bg-zinc-800 text-[var(--text-secondary)] hover:bg-zinc-700'
|
| 71 |
-
}`}
|
| 72 |
-
>
|
| 73 |
-
{cat}
|
| 74 |
-
</button>
|
| 75 |
-
))}
|
| 76 |
-
</div>
|
| 77 |
-
</div>
|
| 78 |
-
|
| 79 |
-
{loading ? (
|
| 80 |
-
<div className="text-center py-12 text-[var(--text-muted)]">Loading integrations...</div>
|
| 81 |
-
) : (
|
| 82 |
-
<div className="grid grid-cols-1 md:grid-cols-2 lg:grid-cols-3 xl:grid-cols-4 gap-4">
|
| 83 |
-
{integrations.map((integration) => (
|
| 84 |
-
<div
|
| 85 |
-
key={integration.id}
|
| 86 |
-
onClick={() => navigate(`/apps/${integration.id}`)}
|
| 87 |
-
className="card hover:border-cyan-500/50 cursor-pointer transition-all hover:transform hover:-translate-y-1"
|
| 88 |
-
>
|
| 89 |
-
<div className="flex items-start gap-3">
|
| 90 |
-
<img
|
| 91 |
-
src={integration.logo_url}
|
| 92 |
-
alt={integration.name}
|
| 93 |
-
className="w-10 h-10 rounded-lg bg-zinc-800"
|
| 94 |
-
/>
|
| 95 |
-
<div className="flex-1 min-w-0">
|
| 96 |
-
<h3 className="font-medium text-white">{integration.name}</h3>
|
| 97 |
-
<p className="text-xs text-[var(--text-muted)] mt-0.5">{integration.category}</p>
|
| 98 |
-
</div>
|
| 99 |
-
</div>
|
| 100 |
-
<p className="text-sm text-[var(--text-secondary)] mt-3 line-clamp-2">
|
| 101 |
-
{integration.description}
|
| 102 |
-
</p>
|
| 103 |
-
<div className="flex items-center justify-between mt-4 pt-3 border-t border-[var(--border)]">
|
| 104 |
-
<div className="flex gap-3 text-xs text-[var(--text-muted)]">
|
| 105 |
-
<span>{integration.tool_count} tools</span>
|
| 106 |
-
<span>{integration.trigger_count} triggers</span>
|
| 107 |
-
</div>
|
| 108 |
-
<ExternalLink className="w-4 h-4 text-[var(--text-muted)]" />
|
| 109 |
-
</div>
|
| 110 |
-
</div>
|
| 111 |
-
))}
|
| 112 |
-
</div>
|
| 113 |
-
)}
|
| 114 |
-
|
| 115 |
-
{!loading && integrations.length === 0 && (
|
| 116 |
-
<div className="text-center py-12 text-[var(--text-muted)]">
|
| 117 |
-
No integrations found matching your criteria
|
| 118 |
-
</div>
|
| 119 |
-
)}
|
| 120 |
-
</div>
|
| 121 |
-
)
|
| 122 |
-
}
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
frontend/src/pages/Connections.tsx
DELETED
|
@@ -1,132 +0,0 @@
|
|
| 1 |
-
import { useEffect, useState } from 'react'
|
| 2 |
-
import { RefreshCw, Trash2, CheckCircle, AlertCircle } from 'lucide-react'
|
| 3 |
-
import api from '../lib/api'
|
| 4 |
-
|
| 5 |
-
interface Connection {
|
| 6 |
-
id: string
|
| 7 |
-
integration_id: string
|
| 8 |
-
integration_name: string
|
| 9 |
-
account_label: string
|
| 10 |
-
status: string
|
| 11 |
-
connected_at: string
|
| 12 |
-
last_used: string | null
|
| 13 |
-
}
|
| 14 |
-
|
| 15 |
-
export default function Connections() {
|
| 16 |
-
const [connections, setConnections] = useState<Connection[]>([])
|
| 17 |
-
const [loading, setLoading] = useState(true)
|
| 18 |
-
|
| 19 |
-
useEffect(() => {
|
| 20 |
-
const fetchConnections = async () => {
|
| 21 |
-
try {
|
| 22 |
-
const response = await api.get('/api/connections')
|
| 23 |
-
setConnections(response.data)
|
| 24 |
-
} catch (e) {
|
| 25 |
-
console.error(e)
|
| 26 |
-
} finally {
|
| 27 |
-
setLoading(false)
|
| 28 |
-
}
|
| 29 |
-
}
|
| 30 |
-
fetchConnections()
|
| 31 |
-
}, [])
|
| 32 |
-
|
| 33 |
-
const handleDisconnect = async (id: string) => {
|
| 34 |
-
if (!confirm('Are you sure you want to disconnect this account?')) return
|
| 35 |
-
try {
|
| 36 |
-
await api.delete(`/api/connections/${id}`)
|
| 37 |
-
setConnections(connections.filter(c => c.id !== id))
|
| 38 |
-
} catch (e) {
|
| 39 |
-
console.error(e)
|
| 40 |
-
}
|
| 41 |
-
}
|
| 42 |
-
|
| 43 |
-
const handleRefresh = async (id: string) => {
|
| 44 |
-
try {
|
| 45 |
-
await api.post(`/api/connections/${id}/refresh`)
|
| 46 |
-
const response = await api.get('/api/connections')
|
| 47 |
-
setConnections(response.data)
|
| 48 |
-
} catch (e) {
|
| 49 |
-
console.error(e)
|
| 50 |
-
}
|
| 51 |
-
}
|
| 52 |
-
|
| 53 |
-
return (
|
| 54 |
-
<div className="space-y-6">
|
| 55 |
-
<div>
|
| 56 |
-
<h1 className="text-2xl font-semibold text-white">Connections</h1>
|
| 57 |
-
<p className="text-[var(--text-muted)] mt-1">Manage your connected accounts</p>
|
| 58 |
-
</div>
|
| 59 |
-
|
| 60 |
-
{loading ? (
|
| 61 |
-
<div className="text-[var(--text-muted)]">Loading...</div>
|
| 62 |
-
) : connections.length === 0 ? (
|
| 63 |
-
<div className="card text-center py-12">
|
| 64 |
-
<p className="text-[var(--text-muted)]">No connections yet</p>
|
| 65 |
-
<p className="text-sm text-[var(--text-muted)] mt-1">Connect an app to get started</p>
|
| 66 |
-
</div>
|
| 67 |
-
) : (
|
| 68 |
-
<div className="card overflow-hidden p-0">
|
| 69 |
-
<table className="w-full">
|
| 70 |
-
<thead className="bg-zinc-800/50">
|
| 71 |
-
<tr>
|
| 72 |
-
<th className="px-4 py-3 text-left text-xs font-medium text-[var(--text-muted)] uppercase">App</th>
|
| 73 |
-
<th className="px-4 py-3 text-left text-xs font-medium text-[var(--text-muted)] uppercase">Account</th>
|
| 74 |
-
<th className="px-4 py-3 text-left text-xs font-medium text-[var(--text-muted)] uppercase">Status</th>
|
| 75 |
-
<th className="px-4 py-3 text-left text-xs font-medium text-[var(--text-muted)] uppercase">Connected</th>
|
| 76 |
-
<th className="px-4 py-3 text-left text-xs font-medium text-[var(--text-muted)] uppercase">Last Used</th>
|
| 77 |
-
<th className="px-4 py-3 text-right text-xs font-medium text-[var(--text-muted)] uppercase">Actions</th>
|
| 78 |
-
</tr>
|
| 79 |
-
</thead>
|
| 80 |
-
<tbody className="divide-y divide-[var(--border)]">
|
| 81 |
-
{connections.map((conn) => (
|
| 82 |
-
<tr key={conn.id} className="hover:bg-[var(--bg-hover)]">
|
| 83 |
-
<td className="px-4 py-3">
|
| 84 |
-
<span className="font-medium text-white">{conn.integration_name}</span>
|
| 85 |
-
</td>
|
| 86 |
-
<td className="px-4 py-3 text-[var(--text-secondary)]">
|
| 87 |
-
{conn.account_label || '-'}
|
| 88 |
-
</td>
|
| 89 |
-
<td className="px-4 py-3">
|
| 90 |
-
{conn.status === 'active' ? (
|
| 91 |
-
<span className="flex items-center gap-1 text-emerald-400 text-sm">
|
| 92 |
-
<CheckCircle className="w-4 h-4" /> Active
|
| 93 |
-
</span>
|
| 94 |
-
) : (
|
| 95 |
-
<span className="flex items-center gap-1 text-amber-400 text-sm">
|
| 96 |
-
<AlertCircle className="w-4 h-4" /> {conn.status}
|
| 97 |
-
</span>
|
| 98 |
-
)}
|
| 99 |
-
</td>
|
| 100 |
-
<td className="px-4 py-3 text-[var(--text-muted)] text-sm">
|
| 101 |
-
{new Date(conn.connected_at).toLocaleDateString()}
|
| 102 |
-
</td>
|
| 103 |
-
<td className="px-4 py-3 text-[var(--text-muted)] text-sm">
|
| 104 |
-
{conn.last_used ? new Date(conn.last_used).toLocaleDateString() : 'Never'}
|
| 105 |
-
</td>
|
| 106 |
-
<td className="px-4 py-3">
|
| 107 |
-
<div className="flex items-center justify-end gap-2">
|
| 108 |
-
<button
|
| 109 |
-
onClick={() => handleRefresh(conn.id)}
|
| 110 |
-
className="p-2 text-[var(--text-muted)] hover:text-cyan-400 hover:bg-cyan-500/10 rounded-lg transition-colors"
|
| 111 |
-
title="Refresh"
|
| 112 |
-
>
|
| 113 |
-
<RefreshCw className="w-4 h-4" />
|
| 114 |
-
</button>
|
| 115 |
-
<button
|
| 116 |
-
onClick={() => handleDisconnect(conn.id)}
|
| 117 |
-
className="p-2 text-[var(--text-muted)] hover:text-red-400 hover:bg-red-500/10 rounded-lg transition-colors"
|
| 118 |
-
title="Disconnect"
|
| 119 |
-
>
|
| 120 |
-
<Trash2 className="w-4 h-4" />
|
| 121 |
-
</button>
|
| 122 |
-
</div>
|
| 123 |
-
</td>
|
| 124 |
-
</tr>
|
| 125 |
-
))}
|
| 126 |
-
</tbody>
|
| 127 |
-
</table>
|
| 128 |
-
</div>
|
| 129 |
-
)}
|
| 130 |
-
</div>
|
| 131 |
-
)
|
| 132 |
-
}
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
frontend/src/pages/Dashboard.tsx
DELETED
|
@@ -1,188 +0,0 @@
|
|
| 1 |
-
import { useEffect, useState } from 'react'
|
| 2 |
-
import { Activity, Plug, Zap, CheckCircle, AlertCircle, Clock } from 'lucide-react'
|
| 3 |
-
import { LineChart, Line, XAxis, YAxis, Tooltip, ResponsiveContainer } from 'recharts'
|
| 4 |
-
import api from '../lib/api'
|
| 5 |
-
|
| 6 |
-
interface Stats {
|
| 7 |
-
total_executions: number
|
| 8 |
-
successful: number
|
| 9 |
-
failed: number
|
| 10 |
-
success_rate: number
|
| 11 |
-
avg_latency_ms: number
|
| 12 |
-
}
|
| 13 |
-
|
| 14 |
-
interface Execution {
|
| 15 |
-
id: string
|
| 16 |
-
tool_id: string
|
| 17 |
-
status: string
|
| 18 |
-
latency_ms: number
|
| 19 |
-
executed_at: string
|
| 20 |
-
}
|
| 21 |
-
|
| 22 |
-
export default function Dashboard() {
|
| 23 |
-
const [stats, setStats] = useState<Stats | null>(null)
|
| 24 |
-
const [executions, setExecutions] = useState<Execution[]>([])
|
| 25 |
-
const [loading, setLoading] = useState(true)
|
| 26 |
-
|
| 27 |
-
useEffect(() => {
|
| 28 |
-
const fetchData = async () => {
|
| 29 |
-
try {
|
| 30 |
-
const [statsRes, execRes] = await Promise.all([
|
| 31 |
-
api.get('/api/analytics/overview'),
|
| 32 |
-
api.get('/api/tools/executions?limit=10')
|
| 33 |
-
])
|
| 34 |
-
setStats(statsRes.data)
|
| 35 |
-
setExecutions(execRes.data)
|
| 36 |
-
} catch (e) {
|
| 37 |
-
console.error(e)
|
| 38 |
-
} finally {
|
| 39 |
-
setLoading(false)
|
| 40 |
-
}
|
| 41 |
-
}
|
| 42 |
-
fetchData()
|
| 43 |
-
}, [])
|
| 44 |
-
|
| 45 |
-
const metricCards = [
|
| 46 |
-
{
|
| 47 |
-
label: 'Total Executions',
|
| 48 |
-
value: stats?.total_executions || 0,
|
| 49 |
-
icon: Activity,
|
| 50 |
-
color: 'text-cyan-400',
|
| 51 |
-
bg: 'bg-cyan-500/10'
|
| 52 |
-
},
|
| 53 |
-
{
|
| 54 |
-
label: 'Connected Apps',
|
| 55 |
-
value: '12 / 18',
|
| 56 |
-
icon: Plug,
|
| 57 |
-
color: 'text-emerald-400',
|
| 58 |
-
bg: 'bg-emerald-500/10'
|
| 59 |
-
},
|
| 60 |
-
{
|
| 61 |
-
label: 'Active Triggers',
|
| 62 |
-
value: '5',
|
| 63 |
-
icon: Zap,
|
| 64 |
-
color: 'text-amber-400',
|
| 65 |
-
bg: 'bg-amber-500/10'
|
| 66 |
-
},
|
| 67 |
-
{
|
| 68 |
-
label: 'Success Rate',
|
| 69 |
-
value: `${stats?.success_rate || 100}%`,
|
| 70 |
-
icon: CheckCircle,
|
| 71 |
-
color: 'text-blue-400',
|
| 72 |
-
bg: 'bg-blue-500/10'
|
| 73 |
-
},
|
| 74 |
-
]
|
| 75 |
-
|
| 76 |
-
return (
|
| 77 |
-
<div className="space-y-6">
|
| 78 |
-
<div>
|
| 79 |
-
<h1 className="text-2xl font-semibold text-white">Dashboard</h1>
|
| 80 |
-
<p className="text-[var(--text-muted)] mt-1">Overview of your integration platform</p>
|
| 81 |
-
</div>
|
| 82 |
-
|
| 83 |
-
<div className="grid grid-cols-1 md:grid-cols-2 lg:grid-cols-4 gap-4">
|
| 84 |
-
{metricCards.map((card) => (
|
| 85 |
-
<div key={card.label} className="card">
|
| 86 |
-
<div className="flex items-center justify-between">
|
| 87 |
-
<div>
|
| 88 |
-
<p className="text-sm text-[var(--text-muted)]">{card.label}</p>
|
| 89 |
-
<p className="text-2xl font-semibold text-white mt-1">{card.value}</p>
|
| 90 |
-
</div>
|
| 91 |
-
<div className={`p-3 rounded-lg ${card.bg}`}>
|
| 92 |
-
<card.icon className={`w-5 h-5 ${card.color}`} />
|
| 93 |
-
</div>
|
| 94 |
-
</div>
|
| 95 |
-
</div>
|
| 96 |
-
))}
|
| 97 |
-
</div>
|
| 98 |
-
|
| 99 |
-
<div className="grid grid-cols-1 lg:grid-cols-2 gap-6">
|
| 100 |
-
<div className="card">
|
| 101 |
-
<h3 className="text-lg font-medium text-white mb-4">Recent Executions</h3>
|
| 102 |
-
{loading ? (
|
| 103 |
-
<div className="text-[var(--text-muted)]">Loading...</div>
|
| 104 |
-
) : executions.length === 0 ? (
|
| 105 |
-
<div className="text-[var(--text-muted)] text-center py-8">
|
| 106 |
-
No executions yet. Start by connecting an app!
|
| 107 |
-
</div>
|
| 108 |
-
) : (
|
| 109 |
-
<div className="space-y-3">
|
| 110 |
-
{executions.slice(0, 5).map((exec) => (
|
| 111 |
-
<div key={exec.id} className="flex items-center justify-between py-2 border-b border-[var(--border)] last:border-0">
|
| 112 |
-
<div className="flex items-center gap-3">
|
| 113 |
-
{exec.status === 'success' ? (
|
| 114 |
-
<CheckCircle className="w-4 h-4 text-emerald-400" />
|
| 115 |
-
) : (
|
| 116 |
-
<AlertCircle className="w-4 h-4 text-red-400" />
|
| 117 |
-
)}
|
| 118 |
-
<span className="text-sm font-mono text-white">{exec.tool_id}</span>
|
| 119 |
-
</div>
|
| 120 |
-
<div className="flex items-center gap-4 text-sm text-[var(--text-muted)]">
|
| 121 |
-
<span className="flex items-center gap-1">
|
| 122 |
-
<Clock className="w-3 h-3" />
|
| 123 |
-
{exec.latency_ms}ms
|
| 124 |
-
</span>
|
| 125 |
-
</div>
|
| 126 |
-
</div>
|
| 127 |
-
))}
|
| 128 |
-
</div>
|
| 129 |
-
)}
|
| 130 |
-
</div>
|
| 131 |
-
|
| 132 |
-
<div className="card">
|
| 133 |
-
<h3 className="text-lg font-medium text-white mb-4">System Health</h3>
|
| 134 |
-
<div className="space-y-4">
|
| 135 |
-
<div>
|
| 136 |
-
<div className="flex justify-between text-sm mb-1">
|
| 137 |
-
<span className="text-[var(--text-muted)]">Disk Usage</span>
|
| 138 |
-
<span className="text-white">45%</span>
|
| 139 |
-
</div>
|
| 140 |
-
<div className="h-2 bg-[var(--bg-primary)] rounded-full overflow-hidden">
|
| 141 |
-
<div className="h-full bg-cyan-500 rounded-full" style={{ width: '45%' }} />
|
| 142 |
-
</div>
|
| 143 |
-
</div>
|
| 144 |
-
<div>
|
| 145 |
-
<div className="flex justify-between text-sm mb-1">
|
| 146 |
-
<span className="text-[var(--text-muted)]">Memory</span>
|
| 147 |
-
<span className="text-white">32%</span>
|
| 148 |
-
</div>
|
| 149 |
-
<div className="h-2 bg-[var(--bg-primary)] rounded-full overflow-hidden">
|
| 150 |
-
<div className="h-full bg-emerald-500 rounded-full" style={{ width: '32%' }} />
|
| 151 |
-
</div>
|
| 152 |
-
</div>
|
| 153 |
-
<div>
|
| 154 |
-
<div className="flex justify-between text-sm mb-1">
|
| 155 |
-
<span className="text-[var(--text-muted)]">CPU</span>
|
| 156 |
-
<span className="text-white">12%</span>
|
| 157 |
-
</div>
|
| 158 |
-
<div className="h-2 bg-[var(--bg-primary)] rounded-full overflow-hidden">
|
| 159 |
-
<div className="h-full bg-amber-500 rounded-full" style={{ width: '12%' }} />
|
| 160 |
-
</div>
|
| 161 |
-
</div>
|
| 162 |
-
</div>
|
| 163 |
-
</div>
|
| 164 |
-
</div>
|
| 165 |
-
|
| 166 |
-
<div className="card">
|
| 167 |
-
<h3 className="text-lg font-medium text-white mb-4">Execution Trends (7 days)</h3>
|
| 168 |
-
<div className="h-64">
|
| 169 |
-
<ResponsiveContainer width="100%" height="100%">
|
| 170 |
-
<LineChart data={[]}>
|
| 171 |
-
<XAxis dataKey="date" stroke="#71717a" fontSize={12} />
|
| 172 |
-
<YAxis stroke="#71717a" fontSize={12} />
|
| 173 |
-
<Tooltip
|
| 174 |
-
contentStyle={{
|
| 175 |
-
backgroundColor: '#18181b',
|
| 176 |
-
border: '1px solid #27272a',
|
| 177 |
-
borderRadius: '8px'
|
| 178 |
-
}}
|
| 179 |
-
/>
|
| 180 |
-
<Line type="monotone" dataKey="count" stroke="#22d3ee" strokeWidth={2} dot={false} />
|
| 181 |
-
</LineChart>
|
| 182 |
-
</ResponsiveContainer>
|
| 183 |
-
</div>
|
| 184 |
-
<p className="text-sm text-[var(--text-muted)] text-center mt-2">No data available yet</p>
|
| 185 |
-
</div>
|
| 186 |
-
</div>
|
| 187 |
-
)
|
| 188 |
-
}
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
frontend/src/pages/Login.tsx
DELETED
|
@@ -1,119 +0,0 @@
|
|
| 1 |
-
import { useState } from 'react'
|
| 2 |
-
import { useNavigate } from 'react-router-dom'
|
| 3 |
-
import { Terminal, LogIn, UserPlus } from 'lucide-react'
|
| 4 |
-
import { useAuthStore } from '../stores/authStore'
|
| 5 |
-
|
| 6 |
-
export default function Login() {
|
| 7 |
-
const [isLogin, setIsLogin] = useState(true)
|
| 8 |
-
const [email, setEmail] = useState('')
|
| 9 |
-
const [password, setPassword] = useState('')
|
| 10 |
-
const [error, setError] = useState('')
|
| 11 |
-
const [loading, setLoading] = useState(false)
|
| 12 |
-
const { login, register } = useAuthStore()
|
| 13 |
-
const navigate = useNavigate()
|
| 14 |
-
|
| 15 |
-
const handleSubmit = async (e: React.FormEvent) => {
|
| 16 |
-
e.preventDefault()
|
| 17 |
-
setError('')
|
| 18 |
-
setLoading(true)
|
| 19 |
-
|
| 20 |
-
try {
|
| 21 |
-
if (isLogin) {
|
| 22 |
-
await login(email, password)
|
| 23 |
-
} else {
|
| 24 |
-
await register(email, password)
|
| 25 |
-
await login(email, password)
|
| 26 |
-
}
|
| 27 |
-
navigate('/')
|
| 28 |
-
} catch (err: any) {
|
| 29 |
-
setError(err.response?.data?.detail || 'An error occurred')
|
| 30 |
-
} finally {
|
| 31 |
-
setLoading(false)
|
| 32 |
-
}
|
| 33 |
-
}
|
| 34 |
-
|
| 35 |
-
return (
|
| 36 |
-
<div className="min-h-screen bg-[var(--bg-primary)] flex items-center justify-center p-4">
|
| 37 |
-
<div className="w-full max-w-md">
|
| 38 |
-
<div className="text-center mb-8">
|
| 39 |
-
<div className="w-16 h-16 bg-cyan-500 rounded-2xl flex items-center justify-center mx-auto mb-4">
|
| 40 |
-
<Terminal className="w-8 h-8 text-slate-900" />
|
| 41 |
-
</div>
|
| 42 |
-
<h1 className="text-2xl font-semibold text-white">Platform</h1>
|
| 43 |
-
<p className="text-[var(--text-muted)] mt-1">AI Tool Integration Platform</p>
|
| 44 |
-
</div>
|
| 45 |
-
|
| 46 |
-
<div className="card">
|
| 47 |
-
<h2 className="text-lg font-medium text-white mb-6 text-center">
|
| 48 |
-
{isLogin ? 'Sign in to your account' : 'Create an account'}
|
| 49 |
-
</h2>
|
| 50 |
-
|
| 51 |
-
<form onSubmit={handleSubmit} className="space-y-4">
|
| 52 |
-
{error && (
|
| 53 |
-
<div className="p-3 bg-red-500/10 border border-red-500/30 rounded-lg text-red-400 text-sm">
|
| 54 |
-
{error}
|
| 55 |
-
</div>
|
| 56 |
-
)}
|
| 57 |
-
|
| 58 |
-
<div>
|
| 59 |
-
<label className="block text-sm text-[var(--text-muted)] mb-1">Email</label>
|
| 60 |
-
<input
|
| 61 |
-
type="email"
|
| 62 |
-
value={email}
|
| 63 |
-
onChange={(e) => setEmail(e.target.value)}
|
| 64 |
-
className="input w-full"
|
| 65 |
-
placeholder="you@example.com"
|
| 66 |
-
required
|
| 67 |
-
/>
|
| 68 |
-
</div>
|
| 69 |
-
|
| 70 |
-
<div>
|
| 71 |
-
<label className="block text-sm text-[var(--text-muted)] mb-1">Password</label>
|
| 72 |
-
<input
|
| 73 |
-
type="password"
|
| 74 |
-
value={password}
|
| 75 |
-
onChange={(e) => setPassword(e.target.value)}
|
| 76 |
-
className="input w-full"
|
| 77 |
-
placeholder="••••••••"
|
| 78 |
-
required
|
| 79 |
-
/>
|
| 80 |
-
</div>
|
| 81 |
-
|
| 82 |
-
<button
|
| 83 |
-
type="submit"
|
| 84 |
-
disabled={loading}
|
| 85 |
-
className="btn btn-primary w-full justify-center"
|
| 86 |
-
>
|
| 87 |
-
{loading ? (
|
| 88 |
-
'Loading...'
|
| 89 |
-
) : isLogin ? (
|
| 90 |
-
<>
|
| 91 |
-
<LogIn className="w-4 h-4" />
|
| 92 |
-
Sign In
|
| 93 |
-
</>
|
| 94 |
-
) : (
|
| 95 |
-
<>
|
| 96 |
-
<UserPlus className="w-4 h-4" />
|
| 97 |
-
Create Account
|
| 98 |
-
</>
|
| 99 |
-
)}
|
| 100 |
-
</button>
|
| 101 |
-
</form>
|
| 102 |
-
|
| 103 |
-
<div className="mt-6 text-center">
|
| 104 |
-
<button
|
| 105 |
-
onClick={() => setIsLogin(!isLogin)}
|
| 106 |
-
className="text-sm text-[var(--text-muted)] hover:text-cyan-400 transition-colors"
|
| 107 |
-
>
|
| 108 |
-
{isLogin ? "Don't have an account? Sign up" : 'Already have an account? Sign in'}
|
| 109 |
-
</button>
|
| 110 |
-
</div>
|
| 111 |
-
</div>
|
| 112 |
-
|
| 113 |
-
<p className="text-xs text-[var(--text-muted)] text-center mt-6">
|
| 114 |
-
Default credentials: admin@platform.local / admin123
|
| 115 |
-
</p>
|
| 116 |
-
</div>
|
| 117 |
-
</div>
|
| 118 |
-
)
|
| 119 |
-
}
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
frontend/src/pages/Playground.tsx
DELETED
|
@@ -1,107 +0,0 @@
|
|
| 1 |
-
import { useEffect, useState } from 'react'
|
| 2 |
-
import { Play, Send } from 'lucide-react'
|
| 3 |
-
import api from '../lib/api'
|
| 4 |
-
|
| 5 |
-
interface Tool {
|
| 6 |
-
id: string
|
| 7 |
-
name: string
|
| 8 |
-
description: string
|
| 9 |
-
}
|
| 10 |
-
|
| 11 |
-
export default function Playground() {
|
| 12 |
-
const [tools, setTools] = useState<Tool[]>([])
|
| 13 |
-
const [selectedTool, setSelectedTool] = useState<string>('')
|
| 14 |
-
const [params, setParams] = useState('')
|
| 15 |
-
const [result, setResult] = useState<string>('')
|
| 16 |
-
const [loading, setLoading] = useState(false)
|
| 17 |
-
|
| 18 |
-
useEffect(() => {
|
| 19 |
-
const fetchTools = async () => {
|
| 20 |
-
try {
|
| 21 |
-
const response = await api.get('/api/playground/tools')
|
| 22 |
-
setTools(response.data.tools || [])
|
| 23 |
-
} catch (e) {
|
| 24 |
-
console.error(e)
|
| 25 |
-
}
|
| 26 |
-
}
|
| 27 |
-
fetchTools()
|
| 28 |
-
}, [])
|
| 29 |
-
|
| 30 |
-
const handleRun = async () => {
|
| 31 |
-
setLoading(true)
|
| 32 |
-
setResult('')
|
| 33 |
-
try {
|
| 34 |
-
const parsedParams = params ? JSON.parse(params) : {}
|
| 35 |
-
const response = await api.post('/api/playground/run', {
|
| 36 |
-
tool_id: selectedTool,
|
| 37 |
-
params: parsedParams
|
| 38 |
-
})
|
| 39 |
-
setResult(JSON.stringify(response.data, null, 2))
|
| 40 |
-
} catch (e: any) {
|
| 41 |
-
setResult(`Error: ${e.response?.data?.detail || e.message}`)
|
| 42 |
-
} finally {
|
| 43 |
-
setLoading(false)
|
| 44 |
-
}
|
| 45 |
-
}
|
| 46 |
-
|
| 47 |
-
return (
|
| 48 |
-
<div className="space-y-6">
|
| 49 |
-
<div>
|
| 50 |
-
<h1 className="text-2xl font-semibold text-white">Playground</h1>
|
| 51 |
-
<p className="text-[var(--text-muted)] mt-1">Test tools with your connected accounts</p>
|
| 52 |
-
</div>
|
| 53 |
-
|
| 54 |
-
<div className="grid grid-cols-1 lg:grid-cols-2 gap-6">
|
| 55 |
-
<div className="space-y-4">
|
| 56 |
-
<div className="card">
|
| 57 |
-
<h3 className="font-medium text-white mb-3">Select Tool</h3>
|
| 58 |
-
<select
|
| 59 |
-
value={selectedTool}
|
| 60 |
-
onChange={(e) => setSelectedTool(e.target.value)}
|
| 61 |
-
className="input w-full"
|
| 62 |
-
>
|
| 63 |
-
<option value="">Choose a tool...</option>
|
| 64 |
-
{tools.map((tool) => (
|
| 65 |
-
<option key={tool.id} value={tool.id}>
|
| 66 |
-
{tool.name}
|
| 67 |
-
</option>
|
| 68 |
-
))}
|
| 69 |
-
</select>
|
| 70 |
-
</div>
|
| 71 |
-
|
| 72 |
-
<div className="card">
|
| 73 |
-
<h3 className="font-medium text-white mb-3">Parameters (JSON)</h3>
|
| 74 |
-
<textarea
|
| 75 |
-
value={params}
|
| 76 |
-
onChange={(e) => setParams(e.target.value)}
|
| 77 |
-
placeholder='{"key": "value"}'
|
| 78 |
-
className="input w-full h-48 font-mono text-sm"
|
| 79 |
-
/>
|
| 80 |
-
</div>
|
| 81 |
-
|
| 82 |
-
<button
|
| 83 |
-
onClick={handleRun}
|
| 84 |
-
disabled={!selectedTool || loading}
|
| 85 |
-
className="btn btn-primary w-full justify-center"
|
| 86 |
-
>
|
| 87 |
-
{loading ? (
|
| 88 |
-
'Running...'
|
| 89 |
-
) : (
|
| 90 |
-
<>
|
| 91 |
-
<Play className="w-4 h-4" />
|
| 92 |
-
Execute Tool
|
| 93 |
-
</>
|
| 94 |
-
)}
|
| 95 |
-
</button>
|
| 96 |
-
</div>
|
| 97 |
-
|
| 98 |
-
<div className="card">
|
| 99 |
-
<h3 className="font-medium text-white mb-3">Output</h3>
|
| 100 |
-
<pre className="bg-zinc-950 p-4 rounded-lg text-sm text-[var(--text-secondary)] overflow-auto h-[400px]">
|
| 101 |
-
{result || 'Execute a tool to see results here...'}
|
| 102 |
-
</pre>
|
| 103 |
-
</div>
|
| 104 |
-
</div>
|
| 105 |
-
</div>
|
| 106 |
-
)
|
| 107 |
-
}
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
frontend/src/pages/Settings.tsx
DELETED
|
@@ -1,137 +0,0 @@
|
|
| 1 |
-
import { useEffect, useState } from 'react'
|
| 2 |
-
import { Save, Server } from 'lucide-react'
|
| 3 |
-
import api from '../lib/api'
|
| 4 |
-
|
| 5 |
-
interface Health {
|
| 6 |
-
status: string
|
| 7 |
-
disk?: { total_gb: number; used_gb: number; percent: number }
|
| 8 |
-
memory?: { total_gb: number; used_gb: number; percent: number }
|
| 9 |
-
cpu_percent?: number
|
| 10 |
-
}
|
| 11 |
-
|
| 12 |
-
interface Settings {
|
| 13 |
-
data_dir: string
|
| 14 |
-
platform_version: string
|
| 15 |
-
integrations_count: number
|
| 16 |
-
features: Record<string, boolean>
|
| 17 |
-
}
|
| 18 |
-
|
| 19 |
-
export default function Settings() {
|
| 20 |
-
const [settings, setSettings] = useState<Settings | null>(null)
|
| 21 |
-
const [health, setHealth] = useState<Health | null>(null)
|
| 22 |
-
const [loading, setLoading] = useState(true)
|
| 23 |
-
|
| 24 |
-
useEffect(() => {
|
| 25 |
-
const fetchData = async () => {
|
| 26 |
-
try {
|
| 27 |
-
const [settingsRes, healthRes] = await Promise.all([
|
| 28 |
-
api.get('/api/settings'),
|
| 29 |
-
api.get('/api/settings/health')
|
| 30 |
-
])
|
| 31 |
-
setSettings(settingsRes.data)
|
| 32 |
-
setHealth(healthRes.data)
|
| 33 |
-
} catch (e) {
|
| 34 |
-
console.error(e)
|
| 35 |
-
} finally {
|
| 36 |
-
setLoading(false)
|
| 37 |
-
}
|
| 38 |
-
}
|
| 39 |
-
fetchData()
|
| 40 |
-
}, [])
|
| 41 |
-
|
| 42 |
-
if (loading) {
|
| 43 |
-
return <div className="text-[var(--text-muted)]">Loading...</div>
|
| 44 |
-
}
|
| 45 |
-
|
| 46 |
-
return (
|
| 47 |
-
<div className="space-y-6">
|
| 48 |
-
<div>
|
| 49 |
-
<h1 className="text-2xl font-semibold text-white">Settings</h1>
|
| 50 |
-
<p className="text-[var(--text-muted)] mt-1">Platform configuration and system status</p>
|
| 51 |
-
</div>
|
| 52 |
-
|
| 53 |
-
<div className="grid grid-cols-1 lg:grid-cols-2 gap-6">
|
| 54 |
-
<div className="card">
|
| 55 |
-
<div className="flex items-center gap-2 mb-4">
|
| 56 |
-
<Server className="w-5 h-5 text-cyan-400" />
|
| 57 |
-
<h3 className="font-medium text-white">System Health</h3>
|
| 58 |
-
</div>
|
| 59 |
-
|
| 60 |
-
{health && (
|
| 61 |
-
<div className="space-y-4">
|
| 62 |
-
<div>
|
| 63 |
-
<div className="flex justify-between text-sm mb-1">
|
| 64 |
-
<span className="text-[var(--text-muted)]">Disk</span>
|
| 65 |
-
<span className="text-white">{health.disk?.percent}%</span>
|
| 66 |
-
</div>
|
| 67 |
-
<div className="h-2 bg-zinc-800 rounded-full overflow-hidden">
|
| 68 |
-
<div className="h-full bg-cyan-500 rounded-full" style={{ width: `${health.disk?.percent}%` }} />
|
| 69 |
-
</div>
|
| 70 |
-
<p className="text-xs text-[var(--text-muted)] mt-1">
|
| 71 |
-
{health.disk?.used_gb}GB / {health.disk?.total_gb}GB
|
| 72 |
-
</p>
|
| 73 |
-
</div>
|
| 74 |
-
|
| 75 |
-
<div>
|
| 76 |
-
<div className="flex justify-between text-sm mb-1">
|
| 77 |
-
<span className="text-[var(--text-muted)]">Memory</span>
|
| 78 |
-
<span className="text-white">{health.memory?.percent}%</span>
|
| 79 |
-
</div>
|
| 80 |
-
<div className="h-2 bg-zinc-800 rounded-full overflow-hidden">
|
| 81 |
-
<div className="h-full bg-emerald-500 rounded-full" style={{ width: `${health.memory?.percent}%` }} />
|
| 82 |
-
</div>
|
| 83 |
-
<p className="text-xs text-[var(--text-muted)] mt-1">
|
| 84 |
-
{health.memory?.used_gb}GB / {health.memory?.total_gb}GB
|
| 85 |
-
</p>
|
| 86 |
-
</div>
|
| 87 |
-
|
| 88 |
-
<div>
|
| 89 |
-
<div className="flex justify-between text-sm mb-1">
|
| 90 |
-
<span className="text-[var(--text-muted)]">CPU</span>
|
| 91 |
-
<span className="text-white">{health.cpu_percent}%</span>
|
| 92 |
-
</div>
|
| 93 |
-
<div className="h-2 bg-zinc-800 rounded-full overflow-hidden">
|
| 94 |
-
<div className="h-full bg-amber-500 rounded-full" style={{ width: `${health.cpu_percent}%` }} />
|
| 95 |
-
</div>
|
| 96 |
-
</div>
|
| 97 |
-
</div>
|
| 98 |
-
)}
|
| 99 |
-
</div>
|
| 100 |
-
|
| 101 |
-
<div className="card">
|
| 102 |
-
<h3 className="font-medium text-white mb-4">Platform Info</h3>
|
| 103 |
-
<div className="space-y-3">
|
| 104 |
-
<div className="flex justify-between">
|
| 105 |
-
<span className="text-[var(--text-muted)]">Version</span>
|
| 106 |
-
<span className="text-white">{settings?.platform_version}</span>
|
| 107 |
-
</div>
|
| 108 |
-
<div className="flex justify-between">
|
| 109 |
-
<span className="text-[var(--text-muted)]">Data Directory</span>
|
| 110 |
-
<span className="text-white font-mono text-sm">{settings?.data_dir}</span>
|
| 111 |
-
</div>
|
| 112 |
-
<div className="flex justify-between">
|
| 113 |
-
<span className="text-[var(--text-muted)]">Integrations</span>
|
| 114 |
-
<span className="text-white">{settings?.integrations_count}</span>
|
| 115 |
-
</div>
|
| 116 |
-
</div>
|
| 117 |
-
|
| 118 |
-
<h4 className="font-medium text-white mt-6 mb-3">Features</h4>
|
| 119 |
-
<div className="flex flex-wrap gap-2">
|
| 120 |
-
{settings?.features && Object.entries(settings.features).map(([key, enabled]) => (
|
| 121 |
-
<span
|
| 122 |
-
key={key}
|
| 123 |
-
className={`px-2 py-1 rounded text-xs font-medium ${
|
| 124 |
-
enabled
|
| 125 |
-
? 'bg-emerald-500/10 text-emerald-400'
|
| 126 |
-
: 'bg-zinc-800 text-[var(--text-muted)]'
|
| 127 |
-
}`}
|
| 128 |
-
>
|
| 129 |
-
{key}
|
| 130 |
-
</span>
|
| 131 |
-
))}
|
| 132 |
-
</div>
|
| 133 |
-
</div>
|
| 134 |
-
</div>
|
| 135 |
-
</div>
|
| 136 |
-
)
|
| 137 |
-
}
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
frontend/src/pages/Tools.tsx
DELETED
|
@@ -1,100 +0,0 @@
|
|
| 1 |
-
import { useEffect, useState } from 'react'
|
| 2 |
-
import { Search, Play } from 'lucide-react'
|
| 3 |
-
import api from '../lib/api'
|
| 4 |
-
|
| 5 |
-
interface Tool {
|
| 6 |
-
id: string
|
| 7 |
-
integration_id: string
|
| 8 |
-
name: string
|
| 9 |
-
description: string
|
| 10 |
-
category: string
|
| 11 |
-
}
|
| 12 |
-
|
| 13 |
-
export default function Tools() {
|
| 14 |
-
const [tools, setTools] = useState<Tool[]>([])
|
| 15 |
-
const [loading, setLoading] = useState(true)
|
| 16 |
-
const [search, setSearch] = useState('')
|
| 17 |
-
const [selectedTool, setSelectedTool] = useState<Tool | null>(null)
|
| 18 |
-
|
| 19 |
-
useEffect(() => {
|
| 20 |
-
const fetchTools = async () => {
|
| 21 |
-
try {
|
| 22 |
-
const params = search ? `?search=${search}` : ''
|
| 23 |
-
const response = await api.get(`/api/tools${params}`)
|
| 24 |
-
setTools(response.data)
|
| 25 |
-
} catch (e) {
|
| 26 |
-
console.error(e)
|
| 27 |
-
} finally {
|
| 28 |
-
setLoading(false)
|
| 29 |
-
}
|
| 30 |
-
}
|
| 31 |
-
fetchTools()
|
| 32 |
-
}, [search])
|
| 33 |
-
|
| 34 |
-
const handleExecute = async (toolId: string) => {
|
| 35 |
-
try {
|
| 36 |
-
const response = await api.post(`/api/tools/${toolId}/execute`, {
|
| 37 |
-
params: {}
|
| 38 |
-
})
|
| 39 |
-
alert(`Executed: ${response.data.status}`)
|
| 40 |
-
} catch (e) {
|
| 41 |
-
console.error(e)
|
| 42 |
-
}
|
| 43 |
-
}
|
| 44 |
-
|
| 45 |
-
return (
|
| 46 |
-
<div className="space-y-6">
|
| 47 |
-
<div>
|
| 48 |
-
<h1 className="text-2xl font-semibold text-white">Tools</h1>
|
| 49 |
-
<p className="text-[var(--text-muted)] mt-1">Browse all available tools across integrations</p>
|
| 50 |
-
</div>
|
| 51 |
-
|
| 52 |
-
<div className="relative max-w-md">
|
| 53 |
-
<Search className="absolute left-3 top-1/2 -translate-y-1/2 w-4 h-4 text-[var(--text-muted)]" />
|
| 54 |
-
<input
|
| 55 |
-
type="text"
|
| 56 |
-
placeholder="Search tools..."
|
| 57 |
-
value={search}
|
| 58 |
-
onChange={(e) => setSearch(e.target.value)}
|
| 59 |
-
className="input w-full pl-10"
|
| 60 |
-
/>
|
| 61 |
-
</div>
|
| 62 |
-
|
| 63 |
-
{loading ? (
|
| 64 |
-
<div className="text-[var(--text-muted)]">Loading...</div>
|
| 65 |
-
) : (
|
| 66 |
-
<div className="grid grid-cols-1 md:grid-cols-2 lg:grid-cols-3 gap-4">
|
| 67 |
-
{tools.map((tool) => (
|
| 68 |
-
<div
|
| 69 |
-
key={tool.id}
|
| 70 |
-
className="card hover:border-cyan-500/30 transition-all"
|
| 71 |
-
>
|
| 72 |
-
<div className="flex items-start justify-between">
|
| 73 |
-
<div>
|
| 74 |
-
<h3 className="font-medium text-white">{tool.name}</h3>
|
| 75 |
-
<p className="text-sm text-[var(--text-muted)] mt-1">{tool.description}</p>
|
| 76 |
-
</div>
|
| 77 |
-
</div>
|
| 78 |
-
<div className="flex items-center justify-between mt-4">
|
| 79 |
-
<span className="text-xs text-[var(--text-muted)] bg-zinc-800 px-2 py-1 rounded">
|
| 80 |
-
{tool.integration_id}
|
| 81 |
-
</span>
|
| 82 |
-
<button
|
| 83 |
-
onClick={() => handleExecute(tool.id)}
|
| 84 |
-
className="btn btn-ghost text-sm py-1 px-2"
|
| 85 |
-
>
|
| 86 |
-
<Play className="w-3 h-3" />
|
| 87 |
-
Run
|
| 88 |
-
</button>
|
| 89 |
-
</div>
|
| 90 |
-
</div>
|
| 91 |
-
))}
|
| 92 |
-
</div>
|
| 93 |
-
)}
|
| 94 |
-
|
| 95 |
-
{!loading && tools.length === 0 && (
|
| 96 |
-
<div className="text-center py-12 text-[var(--text-muted)]">No tools found</div>
|
| 97 |
-
)}
|
| 98 |
-
</div>
|
| 99 |
-
)
|
| 100 |
-
}
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
frontend/src/stores/authStore.ts
DELETED
|
@@ -1,54 +0,0 @@
|
|
| 1 |
-
import { create } from 'zustand'
|
| 2 |
-
import { persist } from 'zustand/middleware'
|
| 3 |
-
import api from '../lib/api'
|
| 4 |
-
|
| 5 |
-
interface User {
|
| 6 |
-
id: string
|
| 7 |
-
email: string
|
| 8 |
-
}
|
| 9 |
-
|
| 10 |
-
interface AuthState {
|
| 11 |
-
token: string | null
|
| 12 |
-
refreshToken: string | null
|
| 13 |
-
user: User | null
|
| 14 |
-
login: (email: string, password: string) => Promise<void>
|
| 15 |
-
logout: () => void
|
| 16 |
-
register: (email: string, password: string) => Promise<void>
|
| 17 |
-
}
|
| 18 |
-
|
| 19 |
-
export const useAuthStore = create<AuthState>()(
|
| 20 |
-
persist(
|
| 21 |
-
(set) => ({
|
| 22 |
-
token: null,
|
| 23 |
-
refreshToken: null,
|
| 24 |
-
user: null,
|
| 25 |
-
|
| 26 |
-
login: async (email: string, password: string) => {
|
| 27 |
-
const formData = new URLSearchParams()
|
| 28 |
-
formData.append('username', email)
|
| 29 |
-
formData.append('password', password)
|
| 30 |
-
|
| 31 |
-
const response = await api.post('/api/auth/login', formData, {
|
| 32 |
-
headers: { 'Content-Type': 'application/x-www-form-urlencoded' }
|
| 33 |
-
})
|
| 34 |
-
|
| 35 |
-
set({
|
| 36 |
-
token: response.data.access_token,
|
| 37 |
-
refreshToken: response.data.refresh_token,
|
| 38 |
-
user: response.data.user
|
| 39 |
-
})
|
| 40 |
-
},
|
| 41 |
-
|
| 42 |
-
logout: () => {
|
| 43 |
-
set({ token: null, refreshToken: null, user: null })
|
| 44 |
-
},
|
| 45 |
-
|
| 46 |
-
register: async (email: string, password: string) => {
|
| 47 |
-
await api.post('/api/auth/register', { email, password })
|
| 48 |
-
}
|
| 49 |
-
}),
|
| 50 |
-
{
|
| 51 |
-
name: 'auth-storage'
|
| 52 |
-
}
|
| 53 |
-
)
|
| 54 |
-
)
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
frontend/tailwind.config.ts
CHANGED
|
@@ -1,23 +1,20 @@
|
|
| 1 |
import type { Config } from 'tailwindcss'
|
| 2 |
|
| 3 |
-
|
| 4 |
content: [
|
| 5 |
-
|
| 6 |
-
"./src/**/*.{js,ts,jsx,tsx}",
|
| 7 |
],
|
| 8 |
theme: {
|
| 9 |
extend: {
|
| 10 |
colors: {
|
| 11 |
-
|
| 12 |
-
|
| 13 |
-
|
| 14 |
-
|
| 15 |
-
}
|
| 16 |
-
},
|
| 17 |
-
fontFamily: {
|
| 18 |
-
sans: ['Inter', 'system-ui', 'sans-serif'],
|
| 19 |
},
|
|
|
|
| 20 |
},
|
| 21 |
},
|
| 22 |
plugins: [],
|
| 23 |
-
}
|
|
|
|
|
|
| 1 |
import type { Config } from 'tailwindcss'
|
| 2 |
|
| 3 |
+
const config: Config = {
|
| 4 |
content: [
|
| 5 |
+
'./src/**/*.{js,ts,jsx,tsx,mdx}',
|
|
|
|
| 6 |
],
|
| 7 |
theme: {
|
| 8 |
extend: {
|
| 9 |
colors: {
|
| 10 |
+
bg: { primary: '#09090b', secondary: '#111113', tertiary: '#18181b', hover: '#1f1f23' },
|
| 11 |
+
border: { DEFAULT: '#27272a', focus: '#3f3f46' },
|
| 12 |
+
text: { primary: '#fafafa', secondary: '#a1a1aa', muted: '#71717a', dim: '#52525b' },
|
| 13 |
+
accent: { cyan: '#06b6d4', cyanLight: '#22d3ee', green: '#10b981', amber: '#f59e0b', blue: '#3b82f6', red: '#ef4444' },
|
|
|
|
|
|
|
|
|
|
|
|
|
| 14 |
},
|
| 15 |
+
fontFamily: { sans: ['Inter', 'system-ui', 'sans-serif'] },
|
| 16 |
},
|
| 17 |
},
|
| 18 |
plugins: [],
|
| 19 |
+
}
|
| 20 |
+
export default config
|
frontend/tsconfig.json
CHANGED
|
@@ -1,23 +1,41 @@
|
|
| 1 |
{
|
| 2 |
"compilerOptions": {
|
| 3 |
-
"target": "
|
| 4 |
-
"
|
| 5 |
-
|
| 6 |
-
|
|
|
|
|
|
|
|
|
|
| 7 |
"skipLibCheck": true,
|
|
|
|
|
|
|
|
|
|
|
|
|
| 8 |
"moduleResolution": "bundler",
|
| 9 |
-
"allowImportingTsExtensions": true,
|
| 10 |
"resolveJsonModule": true,
|
| 11 |
"isolatedModules": true,
|
| 12 |
-
"
|
| 13 |
-
"
|
| 14 |
-
"
|
| 15 |
-
|
| 16 |
-
|
| 17 |
-
|
| 18 |
-
|
| 19 |
-
"
|
|
|
|
|
|
|
|
|
|
|
|
|
| 20 |
},
|
| 21 |
-
"include": [
|
| 22 |
-
|
| 23 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
{
|
| 2 |
"compilerOptions": {
|
| 3 |
+
"target": "ES2017",
|
| 4 |
+
"lib": [
|
| 5 |
+
"dom",
|
| 6 |
+
"dom.iterable",
|
| 7 |
+
"esnext"
|
| 8 |
+
],
|
| 9 |
+
"allowJs": true,
|
| 10 |
"skipLibCheck": true,
|
| 11 |
+
"strict": true,
|
| 12 |
+
"noEmit": true,
|
| 13 |
+
"esModuleInterop": true,
|
| 14 |
+
"module": "esnext",
|
| 15 |
"moduleResolution": "bundler",
|
|
|
|
| 16 |
"resolveJsonModule": true,
|
| 17 |
"isolatedModules": true,
|
| 18 |
+
"jsx": "preserve",
|
| 19 |
+
"incremental": true,
|
| 20 |
+
"plugins": [
|
| 21 |
+
{
|
| 22 |
+
"name": "next"
|
| 23 |
+
}
|
| 24 |
+
],
|
| 25 |
+
"paths": {
|
| 26 |
+
"@/*": [
|
| 27 |
+
"./*"
|
| 28 |
+
]
|
| 29 |
+
}
|
| 30 |
},
|
| 31 |
+
"include": [
|
| 32 |
+
"next-env.d.ts",
|
| 33 |
+
"**/*.ts",
|
| 34 |
+
"**/*.tsx",
|
| 35 |
+
".next/types/**/*.ts"
|
| 36 |
+
],
|
| 37 |
+
"exclude": [
|
| 38 |
+
"node_modules",
|
| 39 |
+
"_src"
|
| 40 |
+
]
|
| 41 |
+
}
|
frontend/tsconfig.node.json
DELETED
|
@@ -1,10 +0,0 @@
|
|
| 1 |
-
{
|
| 2 |
-
"compilerOptions": {
|
| 3 |
-
"composite": true,
|
| 4 |
-
"skipLibCheck": true,
|
| 5 |
-
"module": "ESNext",
|
| 6 |
-
"moduleResolution": "bundler",
|
| 7 |
-
"allowSyntheticDefaultImports": true
|
| 8 |
-
},
|
| 9 |
-
"include": ["vite.config.ts"]
|
| 10 |
-
}
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|