abc1181 commited on
Commit
7657e9f
·
1 Parent(s): 272cb6f

Add dynamic auth configs, connect page, Next.js frontend

Browse files
This view is limited to 50 files because it contains too many changes.   See raw diff
Files changed (50) hide show
  1. .gitignore +9 -5
  2. BUILD.md +142 -0
  3. backend/database.py +13 -0
  4. backend/integrations/registry.py +8 -0
  5. backend/main.py +8 -4
  6. backend/routers/auth_configs.py +293 -0
  7. backend/routers/connections.py +43 -11
  8. backend/static/assets/index-CIpcBIY5.js +0 -0
  9. backend/static/assets/index-DVvq7QG0.css +0 -1
  10. backend/static/index.html +1 -17
  11. frontend/app/analytics/page.tsx +59 -0
  12. frontend/app/api-keys/page.tsx +26 -0
  13. frontend/app/apps/page.tsx +95 -0
  14. frontend/app/connect/page.tsx +160 -0
  15. frontend/app/globals.css +22 -0
  16. frontend/app/layout.tsx +20 -0
  17. frontend/app/login/page.tsx +61 -0
  18. frontend/app/page.tsx +90 -0
  19. frontend/app/settings/page.tsx +40 -0
  20. frontend/app/tools/page.tsx +64 -0
  21. frontend/components/sidebar.tsx +93 -0
  22. frontend/index.html +0 -16
  23. frontend/lib/api.ts +55 -0
  24. frontend/lib/utils.ts +6 -0
  25. frontend/next-env.d.ts +5 -0
  26. frontend/next.config.js +9 -0
  27. frontend/package-lock.json +0 -0
  28. frontend/package.json +19 -23
  29. frontend/postcss.config.js +1 -1
  30. frontend/src/App.tsx +0 -44
  31. frontend/src/components/layout/Layout.tsx +0 -17
  32. frontend/src/components/layout/Sidebar.tsx +0 -109
  33. frontend/src/components/layout/TopBar.tsx +0 -27
  34. frontend/src/index.css +0 -76
  35. frontend/src/lib/api.ts +0 -30
  36. frontend/src/main.tsx +0 -10
  37. frontend/src/pages/APIKeys.tsx +0 -163
  38. frontend/src/pages/Analytics.tsx +0 -117
  39. frontend/src/pages/AppDetail.tsx +0 -217
  40. frontend/src/pages/Apps.tsx +0 -122
  41. frontend/src/pages/Connections.tsx +0 -132
  42. frontend/src/pages/Dashboard.tsx +0 -188
  43. frontend/src/pages/Login.tsx +0 -119
  44. frontend/src/pages/Playground.tsx +0 -107
  45. frontend/src/pages/Settings.tsx +0 -137
  46. frontend/src/pages/Tools.tsx +0 -100
  47. frontend/src/stores/authStore.ts +0 -54
  48. frontend/tailwind.config.ts +9 -12
  49. frontend/tsconfig.json +34 -16
  50. frontend/tsconfig.node.json +0 -10
.gitignore CHANGED
@@ -1,7 +1,11 @@
1
- node_modules
2
- __pycache__
 
 
 
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="Platform API",
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("/assets", StaticFiles(directory=f"{STATIC_DIR}/assets"), name="assets")
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 ConnectAPIKeyRequest(BaseModel):
25
- api_key: str
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("/apikey")
81
- async def connect_with_api_key(
82
- request: ConnectAPIKeyRequest,
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
- encrypted = vault.store_credentials({"api_key": request.api_key})
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} API",
99
  status="active",
100
  encrypted_credentials=encrypted,
101
- conn_metadata={"type": "api_key"}
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&apos;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": "platform-frontend",
3
- "private": true,
4
  "version": "1.0.0",
5
- "type": "module",
6
  "scripts": {
7
- "dev": "vite",
8
- "build": "tsc && vite build",
9
- "preview": "vite preview"
 
10
  },
11
  "dependencies": {
12
- "react": "^18.2.0",
13
- "react-dom": "^18.2.0",
14
- "react-router-dom": "^6.22.0",
15
- "zustand": "^4.5.0",
16
- "axios": "^1.6.7",
17
- "lucide-react": "^0.330.0",
18
- "recharts": "^2.12.0",
19
- "clsx": "^2.1.0",
20
- "date-fns": "^3.3.1"
21
  },
22
  "devDependencies": {
23
- "@types/react": "^18.2.55",
24
- "@types/react-dom": "^18.2.19",
25
- "@vitejs/plugin-react": "^4.2.1",
26
- "autoprefixer": "^10.4.17",
27
- "postcss": "^8.4.35",
28
- "tailwindcss": "^3.4.1",
29
- "typescript": "^5.3.3",
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
- export default {
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
- export default {
4
  content: [
5
- "./index.html",
6
- "./src/**/*.{js,ts,jsx,tsx}",
7
  ],
8
  theme: {
9
  extend: {
10
  colors: {
11
- slate: {
12
- 850: '#1e293b',
13
- 900: '#0f172a',
14
- 950: '#020617',
15
- }
16
- },
17
- fontFamily: {
18
- sans: ['Inter', 'system-ui', 'sans-serif'],
19
  },
 
20
  },
21
  },
22
  plugins: [],
23
- } satisfies Config
 
 
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": "ES2020",
4
- "useDefineForClassFields": true,
5
- "lib": ["ES2020", "DOM", "DOM.Iterable"],
6
- "module": "ESNext",
 
 
 
7
  "skipLibCheck": true,
 
 
 
 
8
  "moduleResolution": "bundler",
9
- "allowImportingTsExtensions": true,
10
  "resolveJsonModule": true,
11
  "isolatedModules": true,
12
- "noEmit": true,
13
- "jsx": "react-jsx",
14
- "strict": true,
15
- "noUnusedLocals": false,
16
- "noUnusedParameters": false,
17
- "noFallthroughCasesInSwitch": true,
18
- "allowSyntheticDefaultImports": true,
19
- "esModuleInterop": true
 
 
 
 
20
  },
21
- "include": ["src"],
22
- "references": [{ "path": "./tsconfig.node.json" }]
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
- }