abc1181 commited on
Commit
83c80d0
·
1 Parent(s): e0f9f17

Complete frontend rebuild: 11 pages, professional composio-level design, connections/playground pages, fixed tailwind config

Browse files
backend/routers/playground.py CHANGED
@@ -1,4 +1,4 @@
1
- from fastapi import APIRouter, Depends
2
  from sqlalchemy.orm import Session
3
  from pydantic import BaseModel
4
  from typing import List, Optional, Dict, Any
@@ -26,8 +26,50 @@ class PlaygroundChatRequest(BaseModel):
26
  allowed_tools: List[str] = []
27
 
28
 
29
- @router.post("/run")
30
- async def run_playground(
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
31
  request: PlaygroundRequest,
32
  current_user: User = Depends(get_current_user),
33
  db: Session = Depends(get_db)
 
1
+ from fastapi import APIRouter, Depends, HTTPException
2
  from sqlalchemy.orm import Session
3
  from pydantic import BaseModel
4
  from typing import List, Optional, Dict, Any
 
26
  allowed_tools: List[str] = []
27
 
28
 
29
+ class EvaluateRequest(BaseModel):
30
+ code: str
31
+
32
+
33
+ @router.post("/evaluate")
34
+ async def evaluate_code(
35
+ request: EvaluateRequest,
36
+ current_user: User = Depends(get_current_user),
37
+ db: Session = Depends(get_db)
38
+ ):
39
+ """Evaluate sandboxed code for the playground."""
40
+ import json
41
+ import re
42
+
43
+ code = request.code
44
+
45
+ # Parse composio_execute_tool calls
46
+ matches = re.findall(r'composio_execute_tool\("(\w+)",\s*"(\w+)",\s*({[^}]+})\)', code)
47
+
48
+ results = []
49
+ for integration_id, tool_id, params_str in matches:
50
+ try:
51
+ params = json.loads(params_str)
52
+ except json.JSONDecodeError:
53
+ params = {"raw": params_str}
54
+
55
+ from backend.database import Tool
56
+ tool = db.query(Tool).filter(Tool.id == tool_id).first()
57
+
58
+ results.append({
59
+ "integration_id": integration_id,
60
+ "tool_id": tool_id,
61
+ "tool_name": tool.name if tool else tool_id,
62
+ "params": params,
63
+ "status": "parsed",
64
+ "message": f"Would execute {tool_id} with params: {json.dumps(params)}"
65
+ })
66
+
67
+ return {
68
+ "status": "success",
69
+ "evaluated": True,
70
+ "calls_found": len(matches),
71
+ "results": results
72
+ }
73
  request: PlaygroundRequest,
74
  current_user: User = Depends(get_current_user),
75
  db: Session = Depends(get_db)
frontend/app/analytics/page.tsx CHANGED
@@ -3,7 +3,7 @@
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)
@@ -16,44 +16,111 @@ export default function AnalyticsPage() {
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
- }
 
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, TrendingUp, BarChart3 } from 'lucide-react'
7
 
8
  export default function AnalyticsPage() {
9
  const [stats, setStats] = useState<Stats | null>(null)
 
16
  }, [])
17
 
18
  const metrics = [
19
+ { label: 'Total Executions', value: stats?.total_executions ?? 0, icon: Activity, color: 'text-accent-cyan', bg: 'bg-accent-cyan/10' },
20
+ { label: 'Successful', value: stats?.successful ?? 0, icon: CheckCircle, color: 'text-accent-green', bg: 'bg-accent-green/10' },
21
+ { label: 'Failed', value: stats?.failed ?? 0, icon: XCircle, color: 'text-accent-red', bg: 'bg-accent-red/10' },
22
+ { label: 'Avg Latency', value: `${stats?.avg_latency_ms ?? 0}ms`, icon: Clock, color: 'text-accent-amber', bg: 'bg-accent-amber/10' },
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/80 backdrop-blur-xl border-b border-border flex items-center px-6 sticky top-0 z-10">
30
+ <div className="flex items-center gap-3">
31
+ <h1 className="text-base font-semibold">Analytics</h1>
32
+ </div>
33
  </header>
34
  <div className="flex-1 p-6 space-y-6 overflow-auto">
35
+ <div className="grid grid-cols-1 md:grid-cols-2 xl:grid-cols-4 gap-4">
36
  {metrics.map((m) => (
37
+ <div key={m.label} className="bg-bg-tertiary border border-border rounded-xl p-5 card-hover">
38
  <div className="flex items-center justify-between">
39
  <div>
40
  <p className="text-sm text-text-muted">{m.label}</p>
41
+ <p className="text-2xl font-bold mt-1">{loading ? <span className="bg-bg-hover rounded w-16 h-8 block animate-pulse" /> : m.value}</p>
42
+ </div>
43
+ <div className={`w-10 h-10 ${m.bg} rounded-lg flex items-center justify-center`}>
44
+ <m.icon className={`w-5 h-5 ${m.color}`} />
45
  </div>
 
46
  </div>
47
  </div>
48
  ))}
49
  </div>
50
+
51
+ <div className="grid grid-cols-1 xl:grid-cols-2 gap-6">
52
+ {/* Success Rate */}
53
+ <div className="bg-bg-tertiary border border-border rounded-xl p-6">
54
+ <h3 className="text-sm font-semibold flex items-center gap-2 mb-5">
55
+ <CheckCircle className="w-4 h-4 text-accent-green" />
56
+ Success Rate
57
+ </h3>
58
+ <div className="flex items-center gap-4">
59
+ <div className="flex-1 h-4 bg-bg-secondary rounded-full overflow-hidden">
60
+ <div className="h-full bg-gradient-to-r from-accent-green to-accent-cyan rounded-full transition-all duration-500" style={{ width: `${stats?.success_rate ?? 100}%` }} />
61
+ </div>
62
+ <span className="text-lg font-bold">{stats?.success_rate ?? 100}%</span>
63
+ </div>
64
+ {stats && (
65
+ <div className="flex items-center justify-between mt-4 pt-4 border-t border-border text-xs text-text-muted">
66
+ <span>{stats.successful} successful</span>
67
+ <span>{stats.failed} failed</span>
68
+ </div>
69
+ )}
70
+ </div>
71
+
72
+ {/* Latency */}
73
+ <div className="bg-bg-tertiary border border-border rounded-xl p-6">
74
+ <h3 className="text-sm font-semibold flex items-center gap-2 mb-5">
75
+ <Clock className="w-4 h-4 text-accent-amber" />
76
+ Average Latency
77
+ </h3>
78
+ <div className="flex items-center gap-4">
79
+ <div className="flex-1 h-4 bg-bg-secondary rounded-full overflow-hidden">
80
+ <div className="h-full bg-gradient-to-r from-accent-amber to-accent-cyan rounded-full transition-all duration-500" style={{ width: `${Math.min((stats?.avg_latency_ms ?? 0) / 20, 100)}%` }} />
81
+ </div>
82
+ <span className="text-lg font-bold">{stats?.avg_latency_ms ?? 0}ms</span>
83
+ </div>
84
+ <div className="mt-4 pt-4 border-t border-border">
85
+ <div className="flex items-center gap-2 text-xs text-text-muted">
86
+ <BarChart3 className="w-3.5 h-3.5" />
87
+ Performance metrics are updated in real-time
88
+ </div>
89
  </div>
 
90
  </div>
91
  </div>
92
+
93
+ {/* Quick Stats */}
94
+ <div className="bg-bg-tertiary border border-border rounded-xl p-6">
95
+ <h3 className="text-sm font-semibold flex items-center gap-2 mb-4">
96
+ <TrendingUp className="w-4 h-4 text-accent-cyan" />
97
+ Usage Summary
98
+ </h3>
99
+ {loading ? (
100
+ <div className="animate-pulse space-y-3">
101
+ {Array.from({ length: 3 }).map((_, i) => <div key={i} className="h-4 bg-bg-hover rounded w-full" />)}
102
+ </div>
103
+ ) : stats ? (
104
+ <div className="grid grid-cols-1 md:grid-cols-3 gap-6">
105
+ <div className="bg-bg-secondary border border-border rounded-xl p-4">
106
+ <p className="text-[10px] text-text-muted uppercase tracking-wider font-medium mb-1">Success Rate</p>
107
+ <p className="text-xl font-bold text-accent-green">{stats.success_rate}%</p>
108
+ </div>
109
+ <div className="bg-bg-secondary border border-border rounded-xl p-4">
110
+ <p className="text-[10px] text-text-muted uppercase tracking-wider font-medium mb-1">Total Calls</p>
111
+ <p className="text-xl font-bold text-accent-cyan">{stats.total_executions}</p>
112
+ </div>
113
+ <div className="bg-bg-secondary border border-border rounded-xl p-4">
114
+ <p className="text-[10px] text-text-muted uppercase tracking-wider font-medium mb-1">Avg Latency</p>
115
+ <p className="text-xl font-bold text-accent-amber">{stats.avg_latency_ms}ms</p>
116
+ </div>
117
+ </div>
118
+ ) : (
119
+ <p className="text-text-muted text-sm">No analytics data yet</p>
120
+ )}
121
+ </div>
122
  </div>
123
  </main>
124
  </div>
125
  )
126
+ }
frontend/app/api-keys/page.tsx CHANGED
@@ -1,26 +1,175 @@
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
- }
 
1
  'use client'
2
 
3
+ import { useEffect, useState } from 'react'
4
  import { Sidebar } from '@/components/sidebar'
5
+ import { api, type APIKey } from '@/lib/api'
6
+ import { Key, Plus, Copy, Check, Trash2, Eye, EyeOff, Calendar, Clock } from 'lucide-react'
7
 
8
  export default function APIKeysPage() {
9
+ const [keys, setKeys] = useState<APIKey[]>([])
10
+ const [loading, setLoading] = useState(true)
11
+ const [showCreate, setShowCreate] = useState(false)
12
+ const [newKeyName, setNewKeyName] = useState('')
13
+ const [newKeyValue, setNewKeyValue] = useState('')
14
+ const [copied, setCopied] = useState(false)
15
+ const [showKey, setShowKey] = useState(false)
16
+
17
+ const fetchKeys = () => {
18
+ api.get<APIKey[]>('/api/apikeys')
19
+ .then(setKeys)
20
+ .catch(() => setKeys([]))
21
+ .finally(() => setLoading(false))
22
+ }
23
+
24
+ useEffect(() => { fetchKeys() }, [])
25
+
26
+ const handleCreate = async () => {
27
+ if (!newKeyName.trim()) return
28
+ const data = await api.post<{ id: string; key: string; name: string }>('/api/apikeys', { name: newKeyName })
29
+ setNewKeyValue(data.key)
30
+ setShowCreate(false)
31
+ setNewKeyName('')
32
+ fetchKeys()
33
+ }
34
+
35
+ const handleDelete = async (id: string) => {
36
+ await api.delete(`/api/apikeys/${id}`)
37
+ fetchKeys()
38
+ }
39
+
40
+ const handleCopy = () => {
41
+ navigator.clipboard.writeText(newKeyValue)
42
+ setCopied(true)
43
+ setTimeout(() => setCopied(false), 2000)
44
+ }
45
+
46
  return (
47
  <div className="flex min-h-screen">
48
  <Sidebar />
49
  <main className="flex-1 flex flex-col">
50
+ <header className="h-14 bg-bg-secondary/80 backdrop-blur-xl border-b border-border flex items-center justify-between px-6 sticky top-0 z-10">
51
+ <div className="flex items-center gap-3">
52
+ <h1 className="text-base font-semibold">API Keys</h1>
53
+ <span className="text-xs text-text-muted bg-bg-tertiary border border-border px-2 py-0.5 rounded-md">{keys.length} keys</span>
54
+ </div>
55
+ <button onClick={() => setShowCreate(true)}
56
+ className="px-3 py-1.5 bg-accent-cyan hover:bg-accent-cyanLight text-bg-primary text-xs font-semibold rounded-lg transition-colors flex items-center gap-1.5 shadow-lg shadow-accent-cyan/20"
57
+ >
58
+ <Plus className="w-3.5 h-3.5" /> Create Key
59
+ </button>
60
  </header>
61
+
62
+ <div className="flex-1 p-6 overflow-auto">
63
+ {/* Create Modal */}
64
+ {showCreate && (
65
+ <div className="fixed inset-0 z-50 flex items-center justify-center bg-black/50 backdrop-blur-sm">
66
+ <div className="bg-bg-tertiary border border-border rounded-2xl p-6 w-full max-w-md shadow-2xl animate-fade-in">
67
+ <h3 className="text-base font-semibold mb-4">Create API Key</h3>
68
+ <input
69
+ type="text"
70
+ value={newKeyName}
71
+ onChange={(e) => setNewKeyName(e.target.value)}
72
+ className="w-full px-3.5 py-2.5 bg-bg-secondary border border-border rounded-xl text-sm text-text-primary placeholder:text-text-dim focus:outline-none focus:border-accent-cyan"
73
+ placeholder="Key name (e.g., Production, Dev)"
74
+ autoFocus
75
+ onKeyDown={(e) => e.key === 'Enter' && handleCreate()}
76
+ />
77
+ <div className="flex justify-end gap-2 mt-4">
78
+ <button onClick={() => setShowCreate(false)}
79
+ className="px-4 py-2 bg-bg-secondary border border-border text-text-secondary text-xs font-medium rounded-lg hover:text-text-primary transition-colors">
80
+ Cancel
81
+ </button>
82
+ <button onClick={handleCreate}
83
+ className="px-4 py-2 bg-accent-cyan hover:bg-accent-cyanLight text-bg-primary text-xs font-semibold rounded-lg transition-colors">
84
+ Create
85
+ </button>
86
+ </div>
87
+ </div>
88
  </div>
89
+ )}
90
+
91
+ {/* New Key Display */}
92
+ {newKeyValue && (
93
+ <div className="mb-4 p-4 bg-accent-green/5 border border-accent-green/20 rounded-xl animate-fade-in">
94
+ <div className="flex items-center gap-2 mb-2">
95
+ <Key className="w-4 h-4 text-accent-green" />
96
+ <span className="text-sm font-semibold text-accent-green">Key Created</span>
97
+ </div>
98
+ <p className="text-xs text-text-muted mb-3">Copy this key now. You won&apos;t be able to see it again.</p>
99
+ <div className="flex items-center gap-2">
100
+ <div className="flex-1 bg-bg-secondary border border-border rounded-lg px-3 py-2">
101
+ <span className="text-xs font-mono">{showKey ? newKeyValue : '••••••••••••••••'}</span>
102
+ </div>
103
+ <button onClick={() => setShowKey(!showKey)} className="p-2 text-text-muted hover:text-text-primary rounded-lg hover:bg-bg-hover">
104
+ {showKey ? <EyeOff className="w-4 h-4" /> : <Eye className="w-4 h-4" />}
105
+ </button>
106
+ <button onClick={handleCopy} className="p-2 text-text-muted hover:text-text-primary rounded-lg hover:bg-bg-hover">
107
+ {copied ? <Check className="w-4 h-4 text-accent-green" /> : <Copy className="w-4 h-4" />}
108
+ </button>
109
+ </div>
110
+ </div>
111
+ )}
112
+
113
+ {/* Keys List */}
114
+ {loading ? (
115
+ <div className="space-y-2">
116
+ {Array.from({ length: 3 }).map((_, i) => (
117
+ <div key={i} className="bg-bg-tertiary border border-border rounded-xl p-5 animate-pulse">
118
+ <div className="flex items-center gap-4"><div className="flex-1"><div className="h-3 bg-bg-hover rounded w-32 mb-1" /><div className="h-2 bg-bg-hover rounded w-20" /></div></div>
119
+ </div>
120
+ ))}
121
+ </div>
122
+ ) : keys.length === 0 && !newKeyValue ? (
123
+ <div className="flex flex-col items-center justify-center py-20">
124
+ <div className="w-16 h-16 bg-bg-tertiary border border-border rounded-2xl flex items-center justify-center mb-4">
125
+ <Key className="w-6 h-6 text-text-dim" />
126
+ </div>
127
+ <p className="text-text-muted font-medium">No API keys</p>
128
+ <p className="text-text-dim text-sm mt-1">Create a key for programmatic access</p>
129
+ <button onClick={() => setShowCreate(true)}
130
+ className="mt-4 px-4 py-2 bg-accent-cyan hover:bg-accent-cyanLight text-bg-primary text-xs font-medium rounded-lg transition-colors flex items-center gap-1.5">
131
+ <Plus className="w-3.5 h-3.5" /> Create First Key
132
+ </button>
133
+ </div>
134
+ ) : (
135
+ <div className="space-y-2">
136
+ {keys.map((k) => (
137
+ <div key={k.id} className="bg-bg-tertiary border border-border rounded-xl p-5 card-hover">
138
+ <div className="flex items-center justify-between">
139
+ <div className="flex items-center gap-4">
140
+ <div className="w-10 h-10 bg-accent-cyan/10 rounded-lg flex items-center justify-center">
141
+ <Key className="w-5 h-5 text-accent-cyan" />
142
+ </div>
143
+ <div>
144
+ <p className="text-sm font-semibold">{k.name}</p>
145
+ <div className="flex items-center gap-2 mt-0.5">
146
+ <span className="text-xs font-mono text-text-muted">{k.key_preview}</span>
147
+ <span className="text-text-dim">·</span>
148
+ <span className="text-[10px] text-text-muted flex items-center gap-1">
149
+ <Calendar className="w-3 h-3" /> {new Date(k.created_at).toLocaleDateString()}
150
+ </span>
151
+ {k.last_used && (
152
+ <>
153
+ <span className="text-text-dim">·</span>
154
+ <span className="text-[10px] text-text-muted flex items-center gap-1">
155
+ <Clock className="w-3 h-3" /> Last used {new Date(k.last_used).toLocaleDateString()}
156
+ </span>
157
+ </>
158
+ )}
159
+ </div>
160
+ </div>
161
+ </div>
162
+ <button onClick={() => handleDelete(k.id)}
163
+ className="p-2 text-text-muted hover:text-accent-red hover:bg-accent-red/10 rounded-lg transition-colors">
164
+ <Trash2 className="w-4 h-4" />
165
+ </button>
166
+ </div>
167
+ </div>
168
+ ))}
169
+ </div>
170
+ )}
171
  </div>
172
  </main>
173
  </div>
174
  )
175
+ }
frontend/app/apps/page.tsx CHANGED
@@ -4,15 +4,14 @@ 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')
@@ -20,8 +19,10 @@ export default function AppsPage() {
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
  })
@@ -30,61 +31,126 @@ export default function AppsPage() {
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
  )}
@@ -92,4 +158,4 @@ export default function AppsPage() {
92
  </main>
93
  </div>
94
  )
95
- }
 
4
  import Link from 'next/link'
5
  import { Sidebar } from '@/components/sidebar'
6
  import { api, type Integration } from '@/lib/api'
7
+ import { Search, ArrowUpRight, Grid3X3, List } from 'lucide-react'
 
 
8
 
9
  export default function AppsPage() {
10
  const [integrations, setIntegrations] = useState<Integration[]>([])
11
  const [loading, setLoading] = useState(true)
12
+ const [category, setCategory] = useState('all')
13
  const [search, setSearch] = useState('')
14
+ const [viewMode, setViewMode] = useState<'grid' | 'list'>('grid')
15
 
16
  useEffect(() => {
17
  api.get<Integration[]>('/api/apps')
 
19
  .catch(() => setLoading(false))
20
  }, [])
21
 
22
+ const categories = ['all', ...new Set(integrations.map(i => i.category).filter(Boolean))].slice(0, 12)
23
+
24
  const filtered = integrations.filter((i) => {
25
+ const matchCat = category === 'all' || i.category === category
26
  const matchSearch = !search || i.name.toLowerCase().includes(search.toLowerCase()) || i.description.toLowerCase().includes(search.toLowerCase())
27
  return matchCat && matchSearch
28
  })
 
31
  <div className="flex min-h-screen">
32
  <Sidebar integrationCount={integrations.length} />
33
  <main className="flex-1 flex flex-col">
34
+ <header className="h-14 bg-bg-secondary/80 backdrop-blur-xl border-b border-border flex items-center justify-between px-6 sticky top-0 z-10">
35
+ <div className="flex items-center gap-3">
36
+ <h1 className="text-base font-semibold">Apps</h1>
37
+ {!loading && <span className="text-xs text-text-muted bg-bg-tertiary border border-border px-2 py-0.5 rounded-md">{integrations.length} total</span>}
38
+ </div>
39
+ <div className="flex items-center gap-3">
40
+ <div className="relative">
41
+ <Search className="absolute left-3 top-1/2 -translate-y-1/2 w-3.5 h-3.5 text-text-dim" />
42
+ <input
43
+ className="pl-8 pr-4 py-1.5 bg-bg-tertiary border border-border rounded-lg text-xs text-text-primary placeholder:text-text-dim focus:outline-none focus:border-accent-cyan w-56"
44
+ placeholder="Search apps..."
45
+ value={search}
46
+ onChange={(e) => setSearch(e.target.value)}
47
+ />
48
+ </div>
49
+ <div className="flex border border-border rounded-lg overflow-hidden">
50
+ <button onClick={() => setViewMode('grid')} className={`p-1.5 ${viewMode === 'grid' ? 'bg-accent-cyan/10 text-accent-cyanLight' : 'text-text-muted hover:text-text-primary'}`}>
51
+ <Grid3X3 className="w-3.5 h-3.5" />
52
+ </button>
53
+ <button onClick={() => setViewMode('list')} className={`p-1.5 ${viewMode === 'list' ? 'bg-accent-cyan/10 text-accent-cyanLight' : 'text-text-muted hover:text-text-primary'}`}>
54
+ <List className="w-3.5 h-3.5" />
55
+ </button>
56
+ </div>
57
  </div>
58
  </header>
59
  <div className="flex-1 p-6 overflow-auto">
60
+ {/* Categories */}
61
  <div className="flex gap-2 mb-6 flex-wrap">
62
  {categories.map((c) => (
63
  <button key={c} onClick={() => setCategory(c)}
64
+ className={`px-3 py-1.5 rounded-lg text-xs font-medium transition-all capitalize ${category === c
65
+ ? 'bg-accent-cyan text-bg-primary shadow-lg shadow-accent-cyan/20'
66
+ : 'bg-bg-tertiary border border-border text-text-secondary hover:text-text-primary hover:border-accent-cyan/30'
67
+ }`}
68
+ >
69
+ {c.replace(/_/g, ' ')}
70
+ </button>
71
  ))}
72
  </div>
73
+
74
+ {/* Grid */}
75
  {loading ? (
76
+ <div className={viewMode === 'grid' ? 'grid grid-cols-2 md:grid-cols-3 lg:grid-cols-4 gap-4' : 'space-y-2'}>
77
  {Array.from({ length: 8 }).map((_, i) => (
78
  <div key={i} className="bg-bg-tertiary border border-border rounded-xl p-5 animate-pulse">
79
  <div className="flex items-center gap-3 mb-3">
80
+ <div className="w-10 h-10 bg-bg-hover rounded-lg shrink-0" />
81
+ <div className="flex-1"><div className="h-3 bg-bg-hover rounded w-20 mb-1.5" /><div className="h-2 bg-bg-hover rounded w-14" /></div>
82
  </div>
83
  <div className="h-2 bg-bg-hover rounded w-full mb-2" /><div className="h-2 bg-bg-hover rounded w-3/4" />
84
  </div>
85
  ))}
86
  </div>
87
  ) : filtered.length === 0 ? (
88
+ <div className="flex flex-col items-center justify-center py-20">
89
+ <div className="w-16 h-16 bg-bg-tertiary border border-border rounded-2xl flex items-center justify-center mb-4">
90
+ <Search className="w-6 h-6 text-text-dim" />
91
+ </div>
92
+ <p className="text-text-muted font-medium">No apps found</p>
93
+ <p className="text-text-dim text-sm mt-1">Try adjusting your search or filters</p>
94
+ </div>
95
+ ) : viewMode === 'grid' ? (
96
+ <div className="grid grid-cols-2 md:grid-cols-3 lg:grid-cols-4 gap-4">
97
  {filtered.map((i) => (
98
+ <Link key={i.id} href={`/connect?id=${i.id}`}
99
+ className="bg-bg-tertiary border border-border rounded-xl p-5 card-hover group"
100
+ >
101
  <div className="flex items-center gap-3 mb-3">
102
+ <div className="w-10 h-10 bg-bg-secondary rounded-lg flex items-center justify-center overflow-hidden border border-border shrink-0">
103
  <img src={i.logo_url} alt={i.name} className="w-6 h-6" onError={(e) => (e.currentTarget.style.display = 'none')} />
104
  </div>
105
+ <div className="min-w-0">
106
+ <p className="text-sm font-semibold truncate">{i.name}</p>
107
+ <p className="text-[10px] text-text-muted capitalize truncate">{i.category?.replace(/_/g, ' ')}</p>
108
  </div>
109
  </div>
110
+ <p className="text-xs text-text-secondary leading-relaxed line-clamp-2 mb-4 h-8">{i.description}</p>
111
  <div className="flex items-center justify-between pt-3 border-t border-border">
112
+ <div className="flex items-center gap-2">
113
+ <span className="text-[10px] text-text-dim">{i.tool_count} tools</span>
114
+ <span className={`text-[10px] px-1.5 py-0.5 rounded font-medium ${
115
+ i.auth_type === 'oauth2'
116
+ ? 'bg-accent-cyan/10 text-accent-cyanLight'
117
+ : i.auth_type === 'api_key'
118
+ ? 'bg-accent-green/10 text-accent-green'
119
+ : 'bg-accent-amber/10 text-accent-amber'
120
+ }`}>
121
+ {i.auth_type}
122
+ </span>
123
+ </div>
124
+ <span className="text-[10px] text-accent-cyan opacity-0 group-hover:opacity-100 transition-opacity flex items-center gap-0.5 font-medium">
125
+ Connect <ArrowUpRight className="w-2.5 h-2.5" />
126
+ </span>
127
+ </div>
128
+ </Link>
129
+ ))}
130
+ </div>
131
+ ) : (
132
+ <div className="space-y-1">
133
+ {filtered.map((i) => (
134
+ <Link key={i.id} href={`/connect?id=${i.id}`}
135
+ className="flex items-center gap-4 px-4 py-3 bg-bg-tertiary border border-border rounded-lg card-hover group"
136
+ >
137
+ <div className="w-9 h-9 bg-bg-secondary rounded-lg flex items-center justify-center overflow-hidden border border-border shrink-0">
138
+ <img src={i.logo_url} alt={i.name} className="w-5 h-5" onError={(e) => (e.currentTarget.style.display = 'none')} />
139
+ </div>
140
+ <div className="flex-1 min-w-0">
141
+ <p className="text-sm font-medium truncate">{i.name}</p>
142
+ <p className="text-xs text-text-muted truncate">{i.description}</p>
143
+ </div>
144
+ <div className="flex items-center gap-3 shrink-0">
145
+ <span className="text-xs text-text-dim">{i.tool_count} tools</span>
146
+ <span className={`text-[10px] px-1.5 py-0.5 rounded font-medium ${
147
+ i.auth_type === 'oauth2' ? 'bg-accent-cyan/10 text-accent-cyanLight' : 'bg-accent-green/10 text-accent-green'
148
+ }`}>{i.auth_type}</span>
149
+ <span className="text-xs text-accent-cyan font-medium opacity-0 group-hover:opacity-100 transition-opacity flex items-center gap-0.5">
150
  Connect <ArrowUpRight className="w-3 h-3" />
151
+ </span>
152
  </div>
153
+ </Link>
154
  ))}
155
  </div>
156
  )}
 
158
  </main>
159
  </div>
160
  )
161
+ }
frontend/app/connect/page.tsx CHANGED
@@ -3,49 +3,34 @@
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))
@@ -56,91 +41,143 @@ function ConnectContent() {
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>
@@ -151,10 +188,18 @@ function ConnectContent() {
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
- }
 
3
  import { useEffect, useState, Suspense } from 'react'
4
  import { useRouter, useSearchParams } from 'next/navigation'
5
  import { Sidebar } from '@/components/sidebar'
6
+ import { api, type AuthConfig, type Integration } from '@/lib/api'
7
+ import { ArrowLeft, Loader2, Check, Key, ExternalLink, Shield, Eye, EyeOff } from 'lucide-react'
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
8
 
9
  function ConnectContent() {
10
  const router = useRouter()
11
  const searchParams = useSearchParams()
12
  const id = searchParams.get('id') || ''
13
  const [config, setConfig] = useState<AuthConfig | null>(null)
14
+ const [integration, setIntegration] = useState<Integration | null>(null)
15
  const [values, setValues] = useState<Record<string, string>>({})
16
  const [loading, setLoading] = useState(true)
17
  const [submitting, setSubmitting] = useState(false)
18
  const [error, setError] = useState('')
19
  const [success, setSuccess] = useState(false)
20
+ const [showSecrets, setShowSecrets] = useState<Record<string, boolean>>({})
21
 
22
  useEffect(() => {
23
  if (!id) { router.push('/apps'); return }
24
  Promise.all([
25
+ api.get<Integration>(`/api/apps/${id}`).catch(() => null),
26
+ api.get<AuthConfig>(`/api/auth-configs/integration/${id}`).catch(() => null),
27
  ]).then(([app, cfg]) => {
28
+ setIntegration(app)
29
  setConfig(cfg)
30
  setLoading(false)
31
  if (cfg) {
32
  const init: Record<string, string> = {}
33
+ cfg.fields.forEach((f) => { init[f.name] = f.default || '' })
34
  setValues(init)
35
  }
36
  }).catch(() => setLoading(false))
 
41
  setError('')
42
  setSubmitting(true)
43
  try {
44
+ const data = await api.post<{ message: string; connection_id: string }>(`/api/connections/connect?integration_id=${id}`, {
45
+ credentials: values,
46
+ account_label: `${integration?.name || id}`,
 
47
  })
 
 
48
  setSuccess(true)
49
+ setTimeout(() => router.push('/connections'), 1500)
50
+ } catch (err: unknown) {
51
+ setError(err instanceof Error ? err.message : 'Connection failed')
52
+ } finally { setSubmitting(false) }
53
  }
54
 
55
+ if (!id) return null
56
+
57
  return (
58
  <div className="flex min-h-screen">
59
  <Sidebar />
60
  <main className="flex-1 flex flex-col">
61
+ <header className="h-14 bg-bg-secondary/80 backdrop-blur-xl border-b border-border flex items-center px-6 sticky top-0 z-10">
62
+ <button onClick={() => router.back()} className="flex items-center gap-2 text-text-muted hover:text-text-primary transition-colors mr-4 p-1 -ml-1">
63
  <ArrowLeft className="w-4 h-4" />
64
  </button>
65
+ <h1 className="text-base font-semibold">Connect {integration?.name || id}</h1>
66
  </header>
67
+ <div className="flex-1 flex items-start justify-center p-6 pt-12">
68
  {loading ? (
69
+ <div className="flex flex-col items-center gap-3 text-text-muted">
70
+ <Loader2 className="w-6 h-6 animate-spin" />
71
+ <span className="text-sm">Loading configuration...</span>
72
+ </div>
73
  ) : success ? (
74
+ <div className="bg-bg-tertiary border border-accent-green/20 rounded-2xl p-10 text-center max-w-md animate-fade-in">
75
+ <div className="w-14 h-14 bg-accent-green/10 rounded-full flex items-center justify-center mx-auto mb-5 border border-accent-green/20">
76
+ <Check className="w-7 h-7 text-accent-green" />
77
+ </div>
78
+ <h2 className="text-lg font-semibold mb-2">Connected!</h2>
79
+ <p className="text-sm text-text-muted mb-6">{integration?.name} has been connected successfully</p>
80
+ <div className="flex gap-3 justify-center">
81
+ <button onClick={() => router.push('/connections')} className="px-4 py-2 bg-accent-cyan text-bg-primary text-xs font-medium rounded-lg hover:bg-accent-cyanLight transition-colors">
82
+ View Connections
83
+ </button>
84
+ <button onClick={() => router.push('/apps')} className="px-4 py-2 bg-bg-secondary border border-border text-text-secondary text-xs font-medium rounded-lg hover:text-text-primary transition-colors">
85
+ Browse Apps
86
+ </button>
87
  </div>
 
 
88
  </div>
89
  ) : !config ? (
90
+ <div className="bg-bg-tertiary border border-border rounded-2xl p-10 text-center max-w-md">
91
+ <div className="w-14 h-14 bg-accent-amber/10 rounded-full flex items-center justify-center mx-auto mb-5 border border-accent-amber/20">
92
+ <Key className="w-7 h-7 text-accent-amber" />
93
+ </div>
94
+ <h2 className="text-lg font-semibold mb-2">No Auth Configuration</h2>
95
+ <p className="text-sm text-text-muted mb-2">This integration doesn&apos;t have a credential schema configured.</p>
96
+ <p className="text-xs text-text-dim mb-6">Define what credentials are needed in Settings</p>
97
+ <button onClick={() => router.push('/settings')} className="px-4 py-2 bg-accent-cyan text-bg-primary text-xs font-medium rounded-lg hover:bg-accent-cyanLight transition-colors">
98
+ Configure in Settings
99
+ </button>
100
  </div>
101
  ) : (
102
+ <div className="bg-bg-tertiary border border-border rounded-2xl p-8 w-full max-w-lg animate-fade-in">
103
+ <div className="flex items-center gap-4 mb-6 pb-6 border-b border-border">
104
+ <div className="w-12 h-12 bg-bg-secondary border border-border rounded-xl flex items-center justify-center overflow-hidden shrink-0">
105
+ <img src={integration?.logo_url} alt={integration?.name} className="w-7 h-7" onError={(e) => (e.currentTarget.style.display = 'none')} />
106
  </div>
107
  <div>
108
+ <h2 className="text-base font-semibold">{integration?.name || id}</h2>
109
+ <div className="flex items-center gap-2 mt-0.5">
110
+ <span className={`text-[10px] px-1.5 py-0.5 rounded font-medium ${
111
+ config.auth_type === 'oauth2' ? 'bg-accent-cyan/10 text-accent-cyanLight' : 'bg-accent-green/10 text-accent-green'
112
+ }`}>{config.auth_type}</span>
113
+ <span className="text-xs text-text-muted">{integration?.tool_count || 0} tools</span>
114
+ </div>
115
  </div>
116
  </div>
117
+
118
+ {error && (
119
+ <div className="mb-5 p-3.5 bg-accent-red/10 border border-accent-red/20 rounded-xl text-accent-red text-xs flex items-start gap-2.5">
120
+ <XCircle className="w-4 h-4 mt-0.5 shrink-0" />
121
+ <span>{error}</span>
122
+ </div>
123
+ )}
124
+
125
  <form onSubmit={handleSubmit} className="space-y-4">
126
  {config.fields.map((field) => (
127
  <div key={field.name}>
128
+ <label className="block text-xs text-text-muted mb-1.5 font-medium">
129
+ {field.label}
130
+ {field.required && <span className="text-accent-red ml-0.5">*</span>}
131
  </label>
132
+ <div className="relative">
133
+ <input
134
+ type={field.type === 'password' && !showSecrets[field.name] ? 'password' : 'text'}
135
+ value={values[field.name] || ''}
136
+ onChange={(e) => setValues(prev => ({ ...prev, [field.name]: e.target.value }))}
137
+ className="w-full px-3.5 py-2.5 bg-bg-secondary border border-border rounded-xl text-sm text-text-primary placeholder:text-text-dim focus:outline-none focus:border-accent-cyan transition-colors pr-10"
138
+ placeholder={field.description || field.label}
139
+ required={field.required}
140
+ />
141
+ {field.type === 'password' && (
142
+ <button type="button" onClick={() => setShowSecrets(prev => ({ ...prev, [field.name]: !prev[field.name] }))}
143
+ className="absolute right-3 top-1/2 -translate-y-1/2 text-text-muted hover:text-text-primary"
144
+ >
145
+ {showSecrets[field.name] ? <EyeOff className="w-4 h-4" /> : <Eye className="w-4 h-4" />}
146
+ </button>
147
+ )}
148
+ </div>
149
+ {field.description && values[field.name] && (
150
+ <p className="text-[10px] text-text-dim mt-1">{field.description}</p>
151
+ )}
152
  </div>
153
  ))}
154
+
155
+ {config.auth_type === 'oauth2' && (
156
+ <div className="pt-4">
157
+ <div className="relative">
158
+ <div className="absolute inset-0 flex items-center">
159
+ <div className="w-full border-t border-border" />
160
+ </div>
161
+ <div className="relative flex justify-center text-xs">
162
+ <span className="px-3 bg-bg-tertiary text-text-muted">or connect via OAuth</span>
163
+ </div>
164
+ </div>
165
  <a
166
  href={`${config.oauth_authorize_url}?client_id=${values['client_id'] || ''}&redirect_uri=${typeof window !== 'undefined' ? window.location.origin : ''}/oauth/callback/${id}&response_type=code`}
167
+ className="mt-4 w-full flex items-center justify-center gap-2 py-2.5 bg-bg-secondary border border-border hover:border-accent-cyan/40 text-text-primary text-sm font-medium rounded-xl transition-all hover:bg-bg-hover"
168
  >
169
+ <ExternalLink className="w-4 h-4" />
170
  Continue with OAuth
171
  </a>
172
  </div>
173
  )}
174
+
175
  <button
176
  type="submit"
177
  disabled={submitting}
178
+ className="w-full py-2.5 bg-gradient-to-r from-accent-cyan to-accent-blue hover:from-accent-cyanLight hover:to-accent-blue text-bg-primary font-semibold text-sm rounded-xl transition-all disabled:opacity-50 flex items-center justify-center gap-2 shadow-lg shadow-accent-cyan/20"
179
  >
180
+ {submitting ? <><Loader2 className="w-4 h-4 animate-spin" /> Connecting...</> : <><Shield className="w-4 h-4" /> Connect {integration?.name || id}</>}
181
  </button>
182
  </form>
183
  </div>
 
188
  )
189
  }
190
 
191
+ function XCircle(props: { className?: string }) {
192
+ return (
193
+ <svg xmlns="http://www.w3.org/2000/svg" width="24" height="24" viewBox="0 0 24 24" fill="none" stroke="currentColor" strokeWidth="2" strokeLinecap="round" strokeLinejoin="round" className={props.className}>
194
+ <circle cx="12" cy="12" r="10" /><path d="m15 9-6 6" /><path d="m9 9 6 6" />
195
+ </svg>
196
+ )
197
+ }
198
+
199
  export default function ConnectPage() {
200
  return (
201
+ <Suspense fallback={<div className="flex min-h-screen"><div className="flex-1 flex items-center justify-center"><Loader2 className="w-6 h-6 animate-spin text-text-muted" /></div></div>}>
202
  <ConnectContent />
203
  </Suspense>
204
  )
205
+ }
frontend/app/connections/page.tsx ADDED
@@ -0,0 +1,122 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
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 Connection } from '@/lib/api'
7
+ import { Link2, Search, ExternalLink, Trash2, RefreshCw, CheckCircle, XCircle, Clock } from 'lucide-react'
8
+
9
+ export default function ConnectionsPage() {
10
+ const [connections, setConnections] = useState<Connection[]>([])
11
+ const [loading, setLoading] = useState(true)
12
+ const [search, setSearch] = useState('')
13
+
14
+ const fetchConnections = () => {
15
+ setLoading(true)
16
+ api.get<Connection[]>('/api/connections')
17
+ .then(setConnections)
18
+ .catch(() => setConnections([]))
19
+ .finally(() => setLoading(false))
20
+ }
21
+
22
+ useEffect(() => { fetchConnections() }, [])
23
+
24
+ const handleDisconnect = async (id: string) => {
25
+ await api.delete(`/api/connections/${id}`)
26
+ fetchConnections()
27
+ }
28
+
29
+ const filtered = connections.filter(c =>
30
+ !search || c.integration_name.toLowerCase().includes(search.toLowerCase()) || c.account_label?.toLowerCase().includes(search.toLowerCase())
31
+ )
32
+
33
+ return (
34
+ <div className="flex min-h-screen">
35
+ <Sidebar connectionCount={connections.length} />
36
+ <main className="flex-1 flex flex-col">
37
+ <header className="h-14 bg-bg-secondary/80 backdrop-blur-xl border-b border-border flex items-center justify-between px-6 sticky top-0 z-10">
38
+ <div className="flex items-center gap-3">
39
+ <h1 className="text-base font-semibold">Connections</h1>
40
+ <span className="text-xs text-text-muted bg-bg-tertiary border border-border px-2 py-0.5 rounded-md">{connections.length} active</span>
41
+ </div>
42
+ <div className="flex items-center gap-3">
43
+ <div className="relative">
44
+ <Search className="absolute left-3 top-1/2 -translate-y-1/2 w-3.5 h-3.5 text-text-dim" />
45
+ <input className="pl-8 pr-4 py-1.5 bg-bg-tertiary border border-border rounded-lg text-xs text-text-primary placeholder:text-text-dim focus:outline-none focus:border-accent-cyan w-56" placeholder="Search connections..." value={search} onChange={(e) => setSearch(e.target.value)} />
46
+ </div>
47
+ <button onClick={fetchConnections} className="p-1.5 text-text-muted hover:text-text-primary hover:bg-bg-hover rounded-lg transition-colors" title="Refresh">
48
+ <RefreshCw className="w-4 h-4" />
49
+ </button>
50
+ </div>
51
+ </header>
52
+ <div className="flex-1 p-6 overflow-auto">
53
+ {loading ? (
54
+ <div className="space-y-2">
55
+ {Array.from({ length: 3 }).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-4"><div className="w-10 h-10 bg-bg-hover rounded-lg" /><div className="flex-1"><div className="h-3 bg-bg-hover rounded w-32 mb-1" /><div className="h-2 bg-bg-hover rounded w-20" /></div></div>
58
+ </div>
59
+ ))}
60
+ </div>
61
+ ) : filtered.length === 0 ? (
62
+ <div className="flex flex-col items-center justify-center py-20">
63
+ <div className="w-16 h-16 bg-bg-tertiary border border-border rounded-2xl flex items-center justify-center mb-4">
64
+ <Link2 className="w-6 h-6 text-text-dim" />
65
+ </div>
66
+ <p className="text-text-muted font-medium">{connections.length === 0 ? 'No connections yet' : 'No matches'}</p>
67
+ <p className="text-text-dim text-sm mt-1">
68
+ {connections.length === 0 ? 'Connect your first app to get started' : 'Try adjusting your search'}
69
+ </p>
70
+ {connections.length === 0 && (
71
+ <Link href="/apps" className="mt-4 px-4 py-2 bg-accent-cyan hover:bg-accent-cyanLight text-bg-primary text-xs font-medium rounded-lg transition-colors">
72
+ Browse Apps
73
+ </Link>
74
+ )}
75
+ </div>
76
+ ) : (
77
+ <div className="space-y-2">
78
+ {filtered.map((c) => (
79
+ <div key={c.id} className="bg-bg-tertiary border border-border rounded-xl p-5 card-hover">
80
+ <div className="flex items-center justify-between">
81
+ <div className="flex items-center gap-4">
82
+ <div className="w-10 h-10 bg-bg-secondary border border-border rounded-lg flex items-center justify-center overflow-hidden shrink-0">
83
+ <img src={`https://logos.composio.dev/api/${c.integration_id}`} alt={c.integration_name} className="w-6 h-6" onError={(e) => (e.currentTarget.style.display = 'none')} />
84
+ </div>
85
+ <div>
86
+ <p className="text-sm font-semibold">{c.account_label || c.integration_name}</p>
87
+ <div className="flex items-center gap-2 mt-0.5">
88
+ <span className="text-xs text-text-muted">{c.integration_name}</span>
89
+ <span className="text-text-dim">·</span>
90
+ <span className={`text-[10px] flex items-center gap-1 ${c.status === 'active' ? 'text-accent-green' : 'text-text-muted'}`}>
91
+ {c.status === 'active' ? <CheckCircle className="w-3 h-3" /> : <XCircle className="w-3 h-3" />}
92
+ {c.status}
93
+ </span>
94
+ <span className="text-text-dim">·</span>
95
+ <span className="text-[10px] text-text-muted flex items-center gap-1">
96
+ <Clock className="w-3 h-3" />
97
+ {new Date(c.connected_at).toLocaleDateString()}
98
+ </span>
99
+ </div>
100
+ </div>
101
+ </div>
102
+ <div className="flex items-center gap-2">
103
+ {c.metadata?.auth_type === 'oauth2' && (
104
+ <span className="text-[10px] bg-accent-cyan/10 text-accent-cyanLight px-2 py-0.5 rounded font-medium">OAuth 2.0</span>
105
+ )}
106
+ <Link href={`/connect?id=${c.integration_id}`} className="p-2 text-text-muted hover:text-accent-cyan hover:bg-accent-cyan/10 rounded-lg transition-colors" title="Reconnect">
107
+ <ExternalLink className="w-3.5 h-3.5" />
108
+ </Link>
109
+ <button onClick={() => handleDisconnect(c.id)} className="p-2 text-text-muted hover:text-accent-red hover:bg-accent-red/10 rounded-lg transition-colors" title="Disconnect">
110
+ <Trash2 className="w-3.5 h-3.5" />
111
+ </button>
112
+ </div>
113
+ </div>
114
+ </div>
115
+ ))}
116
+ </div>
117
+ )}
118
+ </div>
119
+ </main>
120
+ </div>
121
+ )
122
+ }
frontend/app/globals.css CHANGED
@@ -2,21 +2,56 @@
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; }
 
 
 
 
 
 
 
 
 
2
  @tailwind components;
3
  @tailwind utilities;
4
 
5
+ @layer base {
6
+ :root {
7
+ --bg-primary: 9 9 11;
8
+ --bg-secondary: 17 17 19;
9
+ --bg-tertiary: 24 24 27;
10
+ --bg-hover: 31 31 35;
11
+ --border: 39 39 42;
12
+ --border-focus: 63 63 70;
13
+ --text-primary: 250 250 250;
14
+ --text-secondary: 161 161 170;
15
+ --text-muted: 113 113 122;
16
+ --text-dim: 82 82 91;
17
+ --accent-cyan: 6 182 212;
18
+ --accent-cyanLight: 34 211 238;
19
+ --accent-green: 16 185 129;
20
+ --accent-amber: 245 158 11;
21
+ --accent-blue: 59 130 246;
22
+ --accent-red: 239 68 68;
23
+ }
24
  }
25
 
26
+ @layer base {
27
+ * { @apply border-border; }
28
+ body {
29
+ @apply bg-bg-primary text-text-primary antialiased;
30
+ font-feature-settings: "cv02", "cv03", "cv04", "cv11";
31
+ }
32
+ }
33
 
34
+ @layer utilities {
35
+ .glass {
36
+ @apply bg-bg-tertiary/80 backdrop-blur-xl border border-border;
37
+ }
38
+ .card-hover {
39
+ @apply transition-all duration-200 hover:border-accent-cyan/30 hover:shadow-lg hover:shadow-accent-cyan/5;
40
+ }
41
+ .text-gradient {
42
+ @apply bg-gradient-to-r from-accent-cyan via-accent-cyanLight to-accent-blue bg-clip-text text-transparent;
43
+ }
44
  }
45
 
46
+ ::-webkit-scrollbar { width: 6px; height: 6px; }
47
+ ::-webkit-scrollbar-track { background: rgb(var(--bg-secondary)); }
48
+ ::-webkit-scrollbar-thumb { background: rgb(var(--border)); border-radius: 3px; }
49
+ ::-webkit-scrollbar-thumb:hover { background: rgb(var(--border-focus)); }
50
+
51
+ @keyframes fadeIn {
52
+ from { opacity: 0; transform: translateY(4px); }
53
+ to { opacity: 1; transform: translateY(0); }
54
+ }
55
+ .animate-fade-in {
56
+ animation: fadeIn 0.3s ease-out;
57
+ }
frontend/app/layout.tsx CHANGED
@@ -5,16 +5,16 @@ import './globals.css'
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
- }
 
5
  const inter = Inter({ subsets: ['latin'], variable: '--font-inter' })
6
 
7
  export const metadata: Metadata = {
8
+ title: 'Compost - AI Tool Integration Platform',
9
+ description: 'Unified platform for AI agent tool integration. Connect 1000+ apps with managed auth, execute tools, and build automations.',
10
  }
11
 
12
  export default function RootLayout({ children }: { children: React.ReactNode }) {
13
  return (
14
+ <html lang="en" className="dark">
15
+ <body className={`${inter.variable} font-sans`}>
16
  {children}
17
  </body>
18
  </html>
19
  )
20
+ }
frontend/app/login/page.tsx CHANGED
@@ -1,12 +1,14 @@
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()
@@ -31,31 +33,56 @@ export default function LoginPage() {
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
- }
 
1
  'use client'
2
 
3
  import { useState } from 'react'
4
+ import { Plug, Eye, EyeOff } from 'lucide-react'
5
 
6
  export default function LoginPage() {
7
+ const [email, setEmail] = useState('admin@platform.local')
8
+ const [password, setPassword] = useState('admin123')
9
  const [error, setError] = useState('')
10
  const [loading, setLoading] = useState(false)
11
+ const [showPwd, setShowPwd] = useState(false)
12
 
13
  const handleSubmit = async (e: React.FormEvent) => {
14
  e.preventDefault()
 
33
  }
34
 
35
  return (
36
+ <div className="min-h-screen flex items-center justify-center bg-bg-primary relative overflow-hidden">
37
+ <div className="absolute inset-0 overflow-hidden">
38
+ <div className="absolute -top-40 -right-40 w-80 h-80 bg-accent-cyan/5 rounded-full blur-3xl" />
39
+ <div className="absolute -bottom-40 -left-40 w-80 h-80 bg-accent-blue/5 rounded-full blur-3xl" />
40
+ </div>
41
+ <div className="w-full max-w-sm relative z-10">
42
  <div className="text-center mb-8">
43
+ <div className="w-16 h-16 bg-gradient-to-br from-accent-cyan to-accent-blue rounded-2xl flex items-center justify-center mx-auto mb-5 shadow-lg shadow-accent-cyan/20">
44
+ <span className="font-bold text-2xl text-bg-primary">C</span>
45
+ </div>
46
+ <h1 className="text-2xl font-bold">Compost</h1>
47
  <p className="text-sm text-text-muted mt-1">AI Tool Integration Platform</p>
48
  </div>
49
+ <div className="bg-bg-tertiary/80 backdrop-blur-xl border border-border rounded-2xl p-6 shadow-xl">
50
+ {error && (
51
+ <div className="mb-4 p-3 bg-accent-red/10 border border-accent-red/20 rounded-xl text-accent-red text-xs flex items-center gap-2.5">
52
+ <svg xmlns="http://www.w3.org/2000/svg" width="16" height="16" viewBox="0 0 24 24" fill="none" stroke="currentColor" strokeWidth="2" strokeLinecap="round" strokeLinejoin="round" className="shrink-0"><circle cx="12" cy="12" r="10"/><path d="m15 9-6 6"/><path d="m9 9 6 6"/></svg>
53
+ {error}
54
+ </div>
55
+ )}
56
  <form onSubmit={handleSubmit} className="space-y-4">
57
  <div>
58
+ <label className="block text-xs text-text-muted mb-1.5 font-medium">Email</label>
59
+ <input type="email" value={email} onChange={(e) => setEmail(e.target.value)}
60
+ className="w-full px-3.5 py-2.5 bg-bg-secondary border border-border rounded-xl text-sm text-text-primary placeholder:text-text-dim focus:outline-none focus:border-accent-cyan transition-colors"
61
+ placeholder="admin@platform.local" required />
62
  </div>
63
  <div>
64
+ <label className="block text-xs text-text-muted mb-1.5 font-medium">Password</label>
65
+ <div className="relative">
66
+ <input type={showPwd ? 'text' : 'password'} value={password} onChange={(e) => setPassword(e.target.value)}
67
+ className="w-full px-3.5 py-2.5 bg-bg-secondary border border-border rounded-xl text-sm text-text-primary placeholder:text-text-dim focus:outline-none focus:border-accent-cyan transition-colors pr-10"
68
+ placeholder="••••••••" required />
69
+ <button type="button" onClick={() => setShowPwd(!showPwd)}
70
+ className="absolute right-3 top-1/2 -translate-y-1/2 text-text-muted hover:text-text-primary">
71
+ {showPwd ? <EyeOff className="w-4 h-4" /> : <Eye className="w-4 h-4" />}
72
+ </button>
73
+ </div>
74
  </div>
75
+ <button type="submit" disabled={loading}
76
+ className="w-full py-2.5 bg-gradient-to-r from-accent-cyan to-accent-blue hover:from-accent-cyanLight hover:to-accent-blue text-bg-primary font-semibold text-sm rounded-xl transition-all disabled:opacity-50 flex items-center justify-center gap-2 shadow-lg shadow-accent-cyan/20"
77
+ >
78
+ {loading ? <><span className="w-4 h-4 border-2 border-bg-primary border-t-transparent rounded-full animate-spin" /> Signing in...</> : <><Plug className="w-4 h-4" /> Sign In</>}
79
  </button>
80
  </form>
81
  </div>
82
+ <p className="text-center text-xs text-text-dim mt-4">
83
+ Default credentials: <span className="text-text-muted font-mono">admin@platform.local</span> / <span className="text-text-muted font-mono">admin123</span>
84
+ </p>
85
  </div>
86
  </div>
87
  )
88
+ }
frontend/app/page.tsx CHANGED
@@ -1,90 +1,247 @@
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
- }
 
 
 
 
 
 
 
 
 
 
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, type Connection, type Stats } from '@/lib/api'
7
+ import { Activity, Plug, CheckCircle, Clock, ArrowUpRight, Zap, Server, TrendingUp, BarChart3 } from 'lucide-react'
8
 
9
  export default function DashboardPage() {
10
  const [integrations, setIntegrations] = useState<Integration[]>([])
11
+ const [connections, setConnections] = useState<Connection[]>([])
12
  const [stats, setStats] = useState<Stats | null>(null)
13
  const [loading, setLoading] = useState(true)
14
 
15
  useEffect(() => {
16
  Promise.all([
17
  api.get<Integration[]>('/api/apps').catch(() => []),
18
+ api.get<Connection[]>('/api/connections').catch(() => []),
19
  api.get<Stats>('/api/analytics/overview').catch(() => null),
20
+ ]).then(([apps, conns, s]) => {
21
  setIntegrations(apps)
22
+ setConnections(conns)
23
  setStats(s)
24
  setLoading(false)
25
  })
26
  }, [])
27
 
28
  const metrics = [
29
+ { label: 'Total Executions', value: stats?.total_executions ?? 0, icon: Activity, color: 'text-accent-cyan', bg: 'bg-accent-cyan/10' },
30
+ { label: 'Integrations', value: integrations.length, icon: Server, color: 'text-accent-blue', bg: 'bg-accent-blue/10' },
31
+ { label: 'Connections', value: connections.length, icon: Plug, color: 'text-accent-green', bg: 'bg-accent-green/10' },
32
+ { label: 'Success Rate', value: stats?.success_rate != null ? `${stats.success_rate}%` : '-', icon: CheckCircle, color: stats?.success_rate != null && stats.success_rate > 90 ? 'text-accent-green' : 'text-accent-amber', bg: 'bg-accent-amber/10' },
33
  ]
34
 
35
+ const recentConnections = connections.slice(0, 5)
36
+
37
  return (
38
  <div className="flex min-h-screen">
39
+ <Sidebar integrationCount={integrations.length} connectionCount={connections.length} />
40
  <main className="flex-1 flex flex-col">
41
+ <header className="h-14 bg-bg-secondary/80 backdrop-blur-xl border-b border-border flex items-center justify-between px-6 sticky top-0 z-10">
42
+ <div className="flex items-center gap-3">
43
+ <h1 className="text-base font-semibold">Dashboard</h1>
44
+ </div>
45
+ <div className="flex items-center gap-3">
46
+ <span className="text-[11px] text-text-muted bg-bg-tertiary border border-border px-3 py-1 rounded-full">
47
+ <span className="inline-block w-1.5 h-1.5 bg-accent-green rounded-full mr-1.5 animate-pulse" />
48
+ All systems operational
49
+ </span>
50
+ </div>
51
  </header>
52
+
53
  <div className="flex-1 p-6 space-y-6 overflow-auto">
54
+ {/* Metrics Grid */}
55
+ <div className="grid grid-cols-1 md:grid-cols-2 xl:grid-cols-4 gap-4">
56
  {metrics.map((m) => (
57
+ <div key={m.label} className="bg-bg-tertiary border border-border rounded-xl p-5 card-hover">
58
  <div className="flex items-center justify-between">
59
  <div>
60
  <p className="text-sm text-text-muted">{m.label}</p>
61
+ <p className={`text-2xl font-bold mt-1 ${loading ? 'animate-pulse' : ''}`}>
62
+ {loading ? <span className="bg-bg-hover rounded w-16 h-8 block" /> : m.value}
63
+ </p>
64
+ </div>
65
+ <div className={`w-10 h-10 ${m.bg} rounded-lg flex items-center justify-center`}>
66
+ <m.icon className={`w-5 h-5 ${m.color}`} />
67
  </div>
 
68
  </div>
69
  </div>
70
  ))}
71
  </div>
72
 
73
+ <div className="grid grid-cols-1 xl:grid-cols-2 gap-6">
74
+ {/* Quick Connect */}
75
+ <div className="bg-bg-tertiary border border-border rounded-xl">
76
+ <div className="px-6 py-4 border-b border-border flex items-center justify-between">
77
+ <h3 className="text-sm font-semibold flex items-center gap-2">
78
+ <Zap className="w-4 h-4 text-accent-cyan" />
79
+ Quick Connect
80
+ </h3>
81
+ <Link href="/apps" className="text-xs text-accent-cyan hover:text-accent-cyanLight flex items-center gap-1">
82
+ View all <ArrowUpRight className="w-3 h-3" />
83
+ </Link>
84
  </div>
85
+ <div className="p-6">
86
+ {loading ? (
87
+ <div className="grid grid-cols-2 sm:grid-cols-4 gap-3">
88
+ {Array.from({ length: 4 }).map((_, i) => (
89
+ <div key={i} className="bg-bg-secondary border border-border rounded-lg p-4 animate-pulse">
90
+ <div className="w-8 h-8 bg-bg-hover rounded-lg mb-2" />
91
+ <div className="h-3 bg-bg-hover rounded w-16" />
92
  </div>
93
+ ))}
94
+ </div>
95
+ ) : integrations.length === 0 ? (
96
+ <div className="text-center py-8">
97
+ <Plug className="w-10 h-10 text-text-dim mx-auto mb-3" />
98
+ <p className="text-text-muted text-sm">No integrations available</p>
99
+ <p className="text-text-dim text-xs mt-1">Set COMPOSIO_API_KEY to sync all 1000+ tools</p>
100
+ </div>
101
+ ) : (
102
+ <div className="grid grid-cols-2 sm:grid-cols-4 gap-3">
103
+ {integrations.slice(0, 8).map((i) => {
104
+ const isConnected = connections.some(c => c.integration_id === i.id)
105
+ return (
106
+ <Link key={i.id} href={`/connect?id=${i.id}`}
107
+ className="bg-bg-secondary border border-border rounded-lg p-3.5 card-hover group"
108
+ >
109
+ <div className="flex items-center gap-2.5 mb-2.5">
110
+ <div className="w-8 h-8 bg-bg-tertiary rounded-lg flex items-center justify-center overflow-hidden border border-border">
111
+ <img src={i.logo_url} alt={i.name} className="w-5 h-5" onError={(e) => (e.currentTarget.style.display = 'none')} />
112
+ </div>
113
+ <div className="min-w-0">
114
+ <p className="text-xs font-medium truncate">{i.name}</p>
115
+ <p className="text-[10px] text-text-muted truncate">{i.category}</p>
116
+ </div>
117
+ </div>
118
+ <div className="flex items-center justify-between">
119
+ <span className="text-[10px] text-text-dim">{i.tool_count} tools</span>
120
+ {isConnected ? (
121
+ <span className="text-[10px] text-accent-green flex items-center gap-1">
122
+ <span className="w-1.5 h-1.5 bg-accent-green rounded-full" />
123
+ Connected
124
+ </span>
125
+ ) : (
126
+ <span className="text-[10px] text-accent-cyan group-hover:text-accent-cyanLight font-medium">
127
+ Connect
128
+ </span>
129
+ )}
130
+ </div>
131
+ </Link>
132
+ )
133
+ })}
134
+ </div>
135
+ )}
136
+ </div>
137
+ </div>
138
+
139
+ {/* Recent Connections */}
140
+ <div className="bg-bg-tertiary border border-border rounded-xl">
141
+ <div className="px-6 py-4 border-b border-border flex items-center justify-between">
142
+ <h3 className="text-sm font-semibold flex items-center gap-2">
143
+ <Plug className="w-4 h-4 text-accent-green" />
144
+ Recent Connections
145
+ </h3>
146
+ <Link href="/connections" className="text-xs text-accent-cyan hover:text-accent-cyanLight flex items-center gap-1">
147
+ View all <ArrowUpRight className="w-3 h-3" />
148
+ </Link>
149
+ </div>
150
+ <div className="p-6">
151
+ {loading ? (
152
+ <div className="space-y-3">
153
+ {Array.from({ length: 3 }).map((_, i) => (
154
+ <div key={i} className="flex items-center gap-3 animate-pulse">
155
+ <div className="w-9 h-9 bg-bg-hover rounded-lg" />
156
+ <div className="flex-1"><div className="h-3 bg-bg-hover rounded w-24 mb-1" /><div className="h-2 bg-bg-hover rounded w-16" /></div>
157
  </div>
158
+ ))}
 
 
 
 
 
 
159
  </div>
160
+ ) : recentConnections.length === 0 ? (
161
+ <div className="text-center py-6">
162
+ <Link2 className="w-8 h-8 text-text-dim mx-auto mb-2" />
163
+ <p className="text-text-muted text-sm">No connections yet</p>
164
+ <p className="text-text-dim text-xs mt-1">Connect your first app</p>
165
+ </div>
166
+ ) : (
167
+ <div className="space-y-2">
168
+ {recentConnections.map((c) => (
169
+ <Link key={c.id} href={`/connections`}
170
+ className="flex items-center gap-3 px-3 py-2.5 rounded-lg hover:bg-bg-hover transition-colors group"
171
+ >
172
+ <div className="w-9 h-9 bg-bg-secondary border border-border rounded-lg flex items-center justify-center">
173
+ <img src={`https://logos.composio.dev/api/${c.integration_id}`} alt={c.integration_name} className="w-5 h-5" onError={(e) => (e.currentTarget.style.display = 'none')} />
174
+ </div>
175
+ <div className="flex-1 min-w-0">
176
+ <p className="text-sm font-medium truncate">{c.account_label || c.integration_name}</p>
177
+ <p className="text-[11px] text-text-muted">
178
+ {c.status === 'active' ? 'Connected' : c.status} · {new Date(c.connected_at).toLocaleDateString()}
179
+ </p>
180
+ </div>
181
+ <span className="text-[10px] text-accent-green flex items-center gap-1.5">
182
+ <span className="w-1.5 h-1.5 bg-accent-green rounded-full" />
183
+ Active
184
+ </span>
185
+ </Link>
186
+ ))}
187
+ </div>
188
+ )}
189
  </div>
190
+ </div>
191
  </div>
192
+
193
+ {/* Success Rate Bar */}
194
+ {stats && (
195
+ <div className="bg-bg-tertiary border border-border rounded-xl p-6">
196
+ <div className="flex items-center justify-between mb-4">
197
+ <h3 className="text-sm font-semibold flex items-center gap-2">
198
+ <TrendingUp className="w-4 h-4 text-accent-cyan" />
199
+ Performance Overview
200
+ </h3>
201
+ <span className="text-xs text-text-muted">Last 24 hours</span>
202
+ </div>
203
+ <div className="grid grid-cols-3 gap-6">
204
+ <div>
205
+ <div className="flex items-center justify-between mb-2">
206
+ <span className="text-xs text-text-muted">Success Rate</span>
207
+ <span className="text-sm font-semibold">{stats.success_rate}%</span>
208
+ </div>
209
+ <div className="h-2 bg-bg-secondary rounded-full overflow-hidden">
210
+ <div className="h-full bg-gradient-to-r from-accent-green to-accent-cyan rounded-full transition-all" style={{ width: `${stats.success_rate}%` }} />
211
+ </div>
212
+ </div>
213
+ <div>
214
+ <div className="flex items-center justify-between mb-2">
215
+ <span className="text-xs text-text-muted">Avg Latency</span>
216
+ <span className="text-sm font-semibold">{stats.avg_latency_ms}ms</span>
217
+ </div>
218
+ <div className="h-2 bg-bg-secondary rounded-full overflow-hidden">
219
+ <div className="h-full bg-accent-amber rounded-full" style={{ width: `${Math.min(stats.avg_latency_ms / 20, 100)}%` }} />
220
+ </div>
221
+ </div>
222
+ <div>
223
+ <div className="flex items-center justify-between mb-2">
224
+ <span className="text-xs text-text-muted">Total Calls</span>
225
+ <span className="text-sm font-semibold">{stats.total_executions}</span>
226
+ </div>
227
+ <div className="h-2 bg-bg-secondary rounded-full overflow-hidden">
228
+ <div className="h-full bg-accent-blue rounded-full" style={{ width: `${Math.min((stats.total_executions / 1000) * 100, 100)}%` }} />
229
+ </div>
230
+ </div>
231
+ </div>
232
+ </div>
233
+ )}
234
  </div>
235
  </main>
236
  </div>
237
  )
238
+ }
239
+
240
+ function Link2(props: { className?: string }) {
241
+ return (
242
+ <svg xmlns="http://www.w3.org/2000/svg" width="24" height="24" viewBox="0 0 24 24" fill="none" stroke="currentColor" strokeWidth="2" strokeLinecap="round" strokeLinejoin="round" className={props.className}>
243
+ <path d="M10 13a5 5 0 0 0 7.54.54l3-3a5 5 0 0 0-7.07-7.07l-1.72 1.71" />
244
+ <path d="M14 11a5 5 0 0 0-7.54-.54l-3 3a5 5 0 0 0 7.07 7.07l1.71-1.71" />
245
+ </svg>
246
+ )
247
+ }
frontend/app/playground/page.tsx ADDED
@@ -0,0 +1,125 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ 'use client'
2
+
3
+ import { useState, useRef } from 'react'
4
+ import { Sidebar } from '@/components/sidebar'
5
+ import { api, type ToolExecution } from '@/lib/api'
6
+ import { Play, Terminal, Upload, Trash2, Copy, Check } from 'lucide-react'
7
+
8
+ export default function PlaygroundPage() {
9
+ const [code, setCode] = useState(`# Try executing a tool
10
+ # Example: call composio_execute_tool("github", "GITHUB_GET_USER", {"username": "octocat"})
11
+
12
+ composio_execute_tool("github", "GITHUB_LIST_REPOS", {"owner": "octocat"})
13
+ `)
14
+ const [output, setOutput] = useState('')
15
+ const [executing, setExecuting] = useState(false)
16
+ const fileInputRef = useRef<HTMLInputElement>(null)
17
+
18
+ const handleExecute = async () => {
19
+ setExecuting(true)
20
+ setOutput('')
21
+ try {
22
+ const match = code.match(/composio_execute_tool\("(\w+)",\s*"(\w+)",\s*({[^}]+})\)/)
23
+ if (match) {
24
+ const [_, integrationId, toolId, paramsStr] = match
25
+ let params = {}
26
+ try { params = JSON.parse(paramsStr) } catch { params = { raw: paramsStr } }
27
+ const result = await api.post<ToolExecution>(`/api/tools/${toolId}/execute`, { params })
28
+ setOutput(JSON.stringify(result, null, 2))
29
+ } else {
30
+ // Try direct tool execution
31
+ const lines = code.split('\n').filter(l => l.trim() && !l.trim().startsWith('#'))
32
+ if (lines.length > 0) {
33
+ try {
34
+ const evalResult = await api.post<Record<string, unknown>>('/api/playground/evaluate', { code })
35
+ setOutput(JSON.stringify(evalResult, null, 2))
36
+ } catch {
37
+ setOutput(`Error: Could not parse tool call.\n\nExpected format:\ncomposio_execute_tool("integration", "TOOL_NAME", {"param": "value"})`)
38
+ }
39
+ }
40
+ }
41
+ } catch (err: unknown) {
42
+ setOutput(`Error: ${err instanceof Error ? err.message : 'Execution failed'}`)
43
+ } finally { setExecuting(false) }
44
+ }
45
+
46
+ const handleFileUpload = (e: React.ChangeEvent<HTMLInputElement>) => {
47
+ const file = e.target.files?.[0]
48
+ if (file) {
49
+ const reader = new FileReader()
50
+ reader.onload = (ev) => setCode(ev.target?.result as string)
51
+ reader.readAsText(file)
52
+ }
53
+ }
54
+
55
+ const [copied, setCopied] = useState(false)
56
+ const handleCopy = () => {
57
+ navigator.clipboard.writeText(output)
58
+ setCopied(true)
59
+ setTimeout(() => setCopied(false), 2000)
60
+ }
61
+
62
+ return (
63
+ <div className="flex min-h-screen">
64
+ <Sidebar />
65
+ <main className="flex-1 flex flex-col">
66
+ <header className="h-14 bg-bg-secondary/80 backdrop-blur-xl border-b border-border flex items-center justify-between px-6 sticky top-0 z-10">
67
+ <div className="flex items-center gap-3">
68
+ <h1 className="text-base font-semibold">Playground</h1>
69
+ </div>
70
+ <div className="flex items-center gap-2">
71
+ <input type="file" ref={fileInputRef} onChange={handleFileUpload} className="hidden" accept=".py,.js,.txt,.json" />
72
+ <button onClick={() => fileInputRef.current?.click()} className="px-3 py-1.5 bg-bg-tertiary border border-border text-text-secondary hover:text-text-primary text-xs font-medium rounded-lg transition-colors flex items-center gap-1.5">
73
+ <Upload className="w-3.5 h-3.5" /> Import
74
+ </button>
75
+ <button onClick={() => setCode('')} className="px-3 py-1.5 bg-bg-tertiary border border-border text-text-secondary hover:text-text-primary text-xs font-medium rounded-lg transition-colors flex items-center gap-1.5">
76
+ <Trash2 className="w-3.5 h-3.5" /> Clear
77
+ </button>
78
+ <button
79
+ onClick={handleExecute}
80
+ disabled={executing}
81
+ className="px-4 py-1.5 bg-gradient-to-r from-accent-cyan to-accent-blue hover:from-accent-cyanLight hover:to-accent-blue text-bg-primary text-xs font-semibold rounded-lg transition-all disabled:opacity-50 flex items-center gap-1.5 shadow-lg shadow-accent-cyan/20"
82
+ >
83
+ {executing ? <><span className="w-3 h-3 border-2 border-bg-primary border-t-transparent rounded-full animate-spin" /> Running...</> : <><Play className="w-3.5 h-3.5" /> Execute</>}
84
+ </button>
85
+ </div>
86
+ </header>
87
+ <div className="flex-1 flex overflow-hidden">
88
+ {/* Code Editor */}
89
+ <div className="w-1/2 border-r border-border flex flex-col">
90
+ <div className="px-4 py-2.5 border-b border-border bg-bg-tertiary flex items-center justify-between">
91
+ <div className="flex items-center gap-2">
92
+ <Terminal className="w-3.5 h-3.5 text-accent-cyan" />
93
+ <span className="text-[10px] font-medium text-text-muted uppercase tracking-wider">Input</span>
94
+ </div>
95
+ <span className="text-[10px] text-text-dim">Python · Sandbox</span>
96
+ </div>
97
+ <textarea
98
+ value={code}
99
+ onChange={(e) => setCode(e.target.value)}
100
+ className="flex-1 w-full p-4 bg-bg-secondary text-xs font-mono text-text-primary resize-none focus:outline-none leading-relaxed"
101
+ spellCheck={false}
102
+ />
103
+ </div>
104
+ {/* Output */}
105
+ <div className="w-1/2 flex flex-col">
106
+ <div className="px-4 py-2.5 border-b border-border bg-bg-tertiary flex items-center justify-between">
107
+ <div className="flex items-center gap-2">
108
+ <Terminal className="w-3.5 h-3.5 text-accent-green" />
109
+ <span className="text-[10px] font-medium text-text-muted uppercase tracking-wider">Output</span>
110
+ </div>
111
+ {output && (
112
+ <button onClick={handleCopy} className="flex items-center gap-1 text-[10px] text-text-muted hover:text-text-primary transition-colors">
113
+ {copied ? <><Check className="w-3 h-3" /> Copied</> : <><Copy className="w-3 h-3" /> Copy</>}
114
+ </button>
115
+ )}
116
+ </div>
117
+ <pre className="flex-1 p-4 bg-bg-secondary text-xs font-mono text-text-primary overflow-auto whitespace-pre-wrap leading-relaxed">
118
+ {output || 'Output will appear here...'}
119
+ </pre>
120
+ </div>
121
+ </div>
122
+ </main>
123
+ </div>
124
+ )
125
+ }
frontend/app/settings/page.tsx CHANGED
@@ -1,40 +1,249 @@
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
- }
 
1
  'use client'
2
 
3
+ import { useEffect, useState } from 'react'
4
  import { Sidebar } from '@/components/sidebar'
5
+ import { api, type AuthConfig, type Integration } from '@/lib/api'
6
+ import { Server, Database, Shield, Key, Edit3, Plus, X, Check, ExternalLink, Save } 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 ConfigWithIntegration {
18
+ id: string
19
+ integration_id: string
20
+ integration_name: string
21
+ auth_type: string
22
+ fields: AuthField[]
23
+ oauth_authorize_url: string | null
24
+ oauth_token_url: string | null
25
+ is_active: boolean
26
+ }
27
 
28
  export default function SettingsPage() {
29
+ const [configs, setConfigs] = useState<ConfigWithIntegration[]>([])
30
+ const [loading, setLoading] = useState(true)
31
+ const [editing, setEditing] = useState<string | null>(null)
32
+ const [editForm, setEditForm] = useState<Partial<ConfigWithIntegration>>({})
33
+
34
+ useEffect(() => {
35
+ api.get<ConfigWithIntegration[]>('/api/auth-configs').then(setConfigs).catch(() => setConfigs([])).finally(() => setLoading(false))
36
+ }, [])
37
+
38
+ const startEdit = (c: ConfigWithIntegration) => {
39
+ setEditing(c.id)
40
+ setEditForm({ ...c, fields: c.fields.map(f => ({ ...f })) })
41
+ }
42
+
43
+ const saveEdit = async () => {
44
+ if (!editForm.integration_id) return
45
+ await api.post(`/api/auth-configs`, editForm)
46
+ setEditing(null)
47
+ api.get<ConfigWithIntegration[]>('/api/auth-configs').then(setConfigs)
48
+ }
49
+
50
+ const addField = () => {
51
+ setEditForm(prev => ({
52
+ ...prev,
53
+ fields: [...(prev.fields || []), { name: '', label: '', type: 'text', required: false, description: '', default: '' }]
54
+ }))
55
+ }
56
+
57
+ const removeField = (idx: number) => {
58
+ setEditForm(prev => ({
59
+ ...prev,
60
+ fields: prev.fields?.filter((_, i) => i !== idx) || []
61
+ }))
62
+ }
63
+
64
+ const updateField = (idx: number, key: keyof AuthField, value: string | boolean) => {
65
+ setEditForm(prev => ({
66
+ ...prev,
67
+ fields: prev.fields?.map((f, i) => i === idx ? { ...f, [key]: value } : f) || []
68
+ }))
69
+ }
70
+
71
  return (
72
  <div className="flex min-h-screen">
73
  <Sidebar />
74
  <main className="flex-1 flex flex-col">
75
+ <header className="h-14 bg-bg-secondary/80 backdrop-blur-xl border-b border-border flex items-center px-6 sticky top-0 z-10">
76
+ <h1 className="text-base font-semibold">Settings</h1>
77
  </header>
78
+
79
+ <div className="flex-1 p-6 space-y-6 overflow-auto">
80
+ {/* System Info */}
81
+ <div className="grid grid-cols-1 md:grid-cols-3 gap-4">
82
+ <div className="bg-bg-tertiary border border-border rounded-xl p-5">
83
+ <div className="flex items-center gap-3 mb-4">
84
+ <div className="w-9 h-9 bg-accent-cyan/10 rounded-lg flex items-center justify-center"><Server className="w-5 h-5 text-accent-cyan" /></div>
85
+ <h3 className="text-sm font-semibold">System</h3>
86
+ </div>
87
+ <div className="space-y-2.5 text-sm">
88
+ <div className="flex justify-between"><span className="text-text-muted text-xs">Version</span><span className="text-xs font-mono">1.0.0</span></div>
89
+ <div className="flex justify-between"><span className="text-text-muted text-xs">Data Dir</span><span className="text-xs font-mono text-accent-cyan">/data</span></div>
90
+ <div className="flex justify-between"><span className="text-text-muted text-xs">Status</span><span className="text-xs text-accent-green flex items-center gap-1"><span className="w-1.5 h-1.5 bg-accent-green rounded-full" /> Healthy</span></div>
91
+ </div>
92
  </div>
93
+ <div className="bg-bg-tertiary border border-border rounded-xl p-5">
94
+ <div className="flex items-center gap-3 mb-4">
95
+ <div className="w-9 h-9 bg-accent-green/10 rounded-lg flex items-center justify-center"><Database className="w-5 h-5 text-accent-green" /></div>
96
+ <h3 className="text-sm font-semibold">Database</h3>
97
+ </div>
98
+ <div className="space-y-2.5 text-sm">
99
+ <div className="flex justify-between"><span className="text-text-muted text-xs">Engine</span><span className="text-xs font-mono">SQLite</span></div>
100
+ <div className="flex justify-between"><span className="text-text-muted text-xs">Path</span><span className="text-xs font-mono text-accent-cyan">/data/platform.db</span></div>
101
+ <div className="flex justify-between"><span className="text-text-muted text-xs">Cache</span><span className="text-xs text-accent-green">Active</span></div>
102
+ </div>
103
+ </div>
104
+ <div className="bg-bg-tertiary border border-border rounded-xl p-5">
105
+ <div className="flex items-center gap-3 mb-4">
106
+ <div className="w-9 h-9 bg-accent-amber/10 rounded-lg flex items-center justify-center"><Shield className="w-5 h-5 text-accent-amber" /></div>
107
+ <h3 className="text-sm font-semibold">Security</h3>
108
+ </div>
109
+ <div className="space-y-2.5 text-sm">
110
+ <div className="flex justify-between"><span className="text-text-muted text-xs">Auth</span><span className="text-xs font-mono">JWT (HS256)</span></div>
111
+ <div className="flex justify-between"><span className="text-text-muted text-xs">Vault</span><span className="text-xs font-mono">AES-256-GCM</span></div>
112
+ <div className="flex justify-between"><span className="text-text-muted text-xs">Encryption</span><span className="text-xs text-accent-green">End-to-end</span></div>
113
+ </div>
114
  </div>
115
  </div>
116
+
117
+ {/* Auth Configs */}
118
+ <div className="bg-bg-tertiary border border-border rounded-xl">
119
+ <div className="px-6 py-4 border-b border-border flex items-center justify-between">
120
+ <div className="flex items-center gap-3">
121
+ <Key className="w-4 h-4 text-accent-cyan" />
122
+ <h3 className="text-sm font-semibold">Credential Schemas</h3>
123
+ <span className="text-xs text-text-muted bg-bg-secondary border border-border px-2 py-0.5 rounded-md">{configs.length} integrations</span>
124
+ </div>
125
+ <p className="text-[10px] text-text-dim">Define what credentials each integration needs</p>
126
+ </div>
127
+ <div className="p-4">
128
+ {loading ? (
129
+ <div className="space-y-2">
130
+ {Array.from({ length: 4 }).map((_, i) => (
131
+ <div key={i} className="h-16 bg-bg-secondary border border-border rounded-xl animate-pulse" />
132
+ ))}
133
+ </div>
134
+ ) : configs.length === 0 ? (
135
+ <div className="text-center py-10 text-text-muted text-sm">No credential schemas configured</div>
136
+ ) : (
137
+ <div className="space-y-2">
138
+ {configs.map((c) => (
139
+ <div key={c.id}>
140
+ {editing === c.id ? (
141
+ <div className="bg-bg-secondary border border-accent-cyan/30 rounded-xl p-5 space-y-4">
142
+ <div className="flex items-center justify-between">
143
+ <div className="flex items-center gap-3">
144
+ <div className="w-8 h-8 bg-accent-cyan/10 rounded-lg flex items-center justify-center"><Key className="w-4 h-4 text-accent-cyan" /></div>
145
+ <span className="text-sm font-semibold">{c.integration_name} ({c.integration_id})</span>
146
+ </div>
147
+ <div className="flex items-center gap-2">
148
+ <button onClick={saveEdit} className="p-1.5 text-accent-green hover:bg-accent-green/10 rounded-lg transition-colors"><Check className="w-4 h-4" /></button>
149
+ <button onClick={() => setEditing(null)} className="p-1.5 text-text-muted hover:text-text-primary hover:bg-bg-hover rounded-lg transition-colors"><X className="w-4 h-4" /></button>
150
+ </div>
151
+ </div>
152
+
153
+ <div className="grid grid-cols-2 gap-3">
154
+ <div>
155
+ <label className="text-[10px] text-text-muted font-medium">Auth Type</label>
156
+ <select value={editForm.auth_type || 'api_key'} onChange={(e) => setEditForm(prev => ({ ...prev, auth_type: e.target.value }))}
157
+ className="w-full mt-1 px-3 py-1.5 bg-bg-tertiary border border-border rounded-lg text-xs text-text-primary focus:outline-none focus:border-accent-cyan">
158
+ <option value="api_key">API Key</option>
159
+ <option value="oauth2">OAuth 2.0</option>
160
+ <option value="bearer">Bearer Token</option>
161
+ <option value="basic">Basic Auth</option>
162
+ </select>
163
+ </div>
164
+ {editForm.auth_type === 'oauth2' && (
165
+ <>
166
+ <div>
167
+ <label className="text-[10px] text-text-muted font-medium">Authorize URL</label>
168
+ <input value={editForm.oauth_authorize_url || ''} onChange={(e) => setEditForm(prev => ({ ...prev, oauth_authorize_url: e.target.value }))}
169
+ className="w-full mt-1 px-3 py-1.5 bg-bg-tertiary border border-border rounded-lg text-xs text-text-primary focus:outline-none focus:border-accent-cyan font-mono" />
170
+ </div>
171
+ <div>
172
+ <label className="text-[10px] text-text-muted font-medium">Token URL</label>
173
+ <input value={editForm.oauth_token_url || ''} onChange={(e) => setEditForm(prev => ({ ...prev, oauth_token_url: e.target.value }))}
174
+ className="w-full mt-1 px-3 py-1.5 bg-bg-tertiary border border-border rounded-lg text-xs text-text-primary focus:outline-none focus:border-accent-cyan font-mono" />
175
+ </div>
176
+ </>
177
+ )}
178
+ </div>
179
+
180
+ <div>
181
+ <div className="flex items-center justify-between mb-2">
182
+ <span className="text-[10px] text-text-muted font-medium">Fields</span>
183
+ <button onClick={addField} className="text-[10px] text-accent-cyan hover:text-accent-cyanLight flex items-center gap-0.5">
184
+ <Plus className="w-3 h-3" /> Add Field
185
+ </button>
186
+ </div>
187
+ <div className="space-y-2">
188
+ {editForm.fields?.map((field, idx) => (
189
+ <div key={idx} className="flex items-center gap-2 bg-bg-tertiary border border-border rounded-lg p-3">
190
+ <input value={field.name} onChange={(e) => updateField(idx, 'name', e.target.value)} placeholder="name"
191
+ className="w-24 px-2 py-1 bg-bg-secondary border border-border rounded text-[10px] font-mono text-text-primary focus:outline-none focus:border-accent-cyan" />
192
+ <input value={field.label} onChange={(e) => updateField(idx, 'label', e.target.value)} placeholder="Label"
193
+ className="w-28 px-2 py-1 bg-bg-secondary border border-border rounded text-[10px] text-text-primary focus:outline-none focus:border-accent-cyan" />
194
+ <select value={field.type} onChange={(e) => updateField(idx, 'type', e.target.value)}
195
+ className="w-20 px-2 py-1 bg-bg-secondary border border-border rounded text-[10px] text-text-primary focus:outline-none focus:border-accent-cyan">
196
+ <option value="text">text</option>
197
+ <option value="password">password</option>
198
+ <option value="url">url</option>
199
+ </select>
200
+ <label className="flex items-center gap-1 text-[10px] text-text-muted cursor-pointer">
201
+ <input type="checkbox" checked={field.required} onChange={(e) => updateField(idx, 'required', e.target.checked)}
202
+ className="rounded border-border bg-bg-secondary text-accent-cyan focus:ring-accent-cyan" />
203
+ Required
204
+ </label>
205
+ <input value={field.description} onChange={(e) => updateField(idx, 'description', e.target.value)} placeholder="Description"
206
+ className="flex-1 px-2 py-1 bg-bg-secondary border border-border rounded text-[10px] text-text-primary focus:outline-none focus:border-accent-cyan" />
207
+ <button onClick={() => removeField(idx)} className="p-1 text-text-muted hover:text-accent-red">
208
+ <X className="w-3 h-3" />
209
+ </button>
210
+ </div>
211
+ ))}
212
+ </div>
213
+ </div>
214
+ </div>
215
+ ) : (
216
+ <div className="flex items-center justify-between px-4 py-3 bg-bg-secondary border border-border rounded-xl hover:border-border-focus transition-colors group">
217
+ <div className="flex items-center gap-3">
218
+ <div className="w-8 h-8 rounded-lg flex items-center justify-center overflow-hidden bg-bg-tertiary border border-border">
219
+ <img src={`https://logos.composio.dev/api/${c.integration_id}`} alt={c.integration_name} className="w-5 h-5" onError={(e) => (e.currentTarget.style.display = 'none')} />
220
+ </div>
221
+ <div>
222
+ <p className="text-sm font-medium">{c.integration_name}</p>
223
+ <div className="flex items-center gap-2 mt-0.5">
224
+ <span className="text-[10px] text-text-muted font-mono">{c.integration_id}</span>
225
+ <span className="text-text-dim">·</span>
226
+ <span className={`text-[10px] px-1.5 py-0.5 rounded font-medium ${
227
+ c.auth_type === 'oauth2' ? 'bg-accent-cyan/10 text-accent-cyanLight' : 'bg-accent-green/10 text-accent-green'
228
+ }`}>{c.auth_type}</span>
229
+ <span className="text-[10px] text-text-muted">{c.fields.length} fields</span>
230
+ </div>
231
+ </div>
232
+ </div>
233
+ <button onClick={() => startEdit(c)}
234
+ className="p-1.5 text-text-muted opacity-0 group-hover:opacity-100 hover:text-accent-cyan hover:bg-accent-cyan/10 rounded-lg transition-all">
235
+ <Edit3 className="w-3.5 h-3.5" />
236
+ </button>
237
+ </div>
238
+ )}
239
+ </div>
240
+ ))}
241
+ </div>
242
+ )}
243
  </div>
244
  </div>
245
  </div>
246
  </main>
247
  </div>
248
  )
249
+ }
frontend/app/tools/page.tsx CHANGED
@@ -2,63 +2,186 @@
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
- }
 
2
 
3
  import { useEffect, useState } from 'react'
4
  import { Sidebar } from '@/components/sidebar'
5
+ import { api, type Tool, type ToolExecution } from '@/lib/api'
6
+ import { Search, Play, Terminal, ChevronDown, ChevronRight, Clock, CheckCircle, XCircle } 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
+ const [selectedTool, setSelectedTool] = useState<Tool | null>(null)
13
+ const [executing, setExecuting] = useState(false)
14
+ const [executionResult, setExecutionResult] = useState<ToolExecution | null>(null)
15
+ const [params, setParams] = useState<string>('{}')
16
+ const [expandedTool, setExpandedTool] = useState<string | null>(null)
17
+ const [integrationFilter, setIntegrationFilter] = useState<string>('all')
18
 
19
  useEffect(() => {
20
+ Promise.all([
21
+ api.get<Tool[]>('/api/tools').catch(() => []),
22
+ ]).then(([t]) => { setTools(t); setLoading(false) })
23
  }, [])
24
 
25
+ const integrations = [...new Set(tools.map(t => t.integration_id))].sort()
26
+
27
+ const filtered = tools.filter((t) => {
28
+ const matchSearch = !search || t.name.toLowerCase().includes(search.toLowerCase()) || t.description.toLowerCase().includes(search.toLowerCase())
29
+ const matchInt = integrationFilter === 'all' || t.integration_id === integrationFilter
30
+ return matchSearch && matchInt
31
+ })
32
+
33
+ const handleExecute = async (tool: Tool) => {
34
+ setExecuting(true)
35
+ setExecutionResult(null)
36
+ try {
37
+ let parsedParams = {}
38
+ try { parsedParams = JSON.parse(params) } catch { /* use empty */ }
39
+ const result = await api.post<ToolExecution>(`/api/tools/${tool.id}/execute`, {
40
+ params: parsedParams,
41
+ dry_run: false,
42
+ })
43
+ setExecutionResult(result)
44
+ } catch (err: unknown) {
45
+ setExecutionResult({
46
+ execution_id: 'error',
47
+ status: 'error',
48
+ result: null,
49
+ latency_ms: 0,
50
+ executed_at: new Date().toISOString(),
51
+ error: err instanceof Error ? err.message : 'Execution failed',
52
+ })
53
+ } finally { setExecuting(false) }
54
+ }
55
 
56
  return (
57
  <div className="flex min-h-screen">
58
  <Sidebar />
59
  <main className="flex-1 flex flex-col">
60
+ <header className="h-14 bg-bg-secondary/80 backdrop-blur-xl border-b border-border flex items-center justify-between px-6 sticky top-0 z-10">
61
+ <div className="flex items-center gap-3">
62
+ <h1 className="text-base font-semibold">Tools</h1>
63
+ {!loading && <span className="text-xs text-text-muted bg-bg-tertiary border border-border px-2 py-0.5 rounded-md">{tools.length} total</span>}
64
+ </div>
65
+ <div className="flex items-center gap-3">
66
+ <select value={integrationFilter} onChange={(e) => setIntegrationFilter(e.target.value)}
67
+ className="px-3 py-1.5 bg-bg-tertiary border border-border rounded-lg text-xs text-text-primary focus:outline-none focus:border-accent-cyan"
68
+ >
69
+ <option value="all">All Integrations</option>
70
+ {integrations.map(int => <option key={int} value={int}>{int}</option>)}
71
+ </select>
72
+ <div className="relative">
73
+ <Search className="absolute left-3 top-1/2 -translate-y-1/2 w-3.5 h-3.5 text-text-dim" />
74
+ <input className="pl-8 pr-4 py-1.5 bg-bg-tertiary border border-border rounded-lg text-xs text-text-primary placeholder:text-text-dim focus:outline-none focus:border-accent-cyan w-56" placeholder="Search tools..." value={search} onChange={(e) => setSearch(e.target.value)} />
75
+ </div>
76
  </div>
77
  </header>
78
+
79
+ <div className="flex-1 flex overflow-hidden">
80
+ {/* Tool List */}
81
+ <div className="w-1/2 border-r border-border overflow-y-auto p-4">
82
+ {loading ? (
83
+ <div className="space-y-2">
84
+ {Array.from({ length: 8 }).map((_, i) => (
85
+ <div key={i} className="bg-bg-tertiary border border-border rounded-xl p-4 animate-pulse">
86
+ <div className="flex items-center gap-3"><div className="w-8 h-8 bg-bg-hover rounded-lg" /><div className="flex-1"><div className="h-3 bg-bg-hover rounded w-32 mb-1" /><div className="h-2 bg-bg-hover rounded w-full" /></div></div>
87
+ </div>
88
+ ))}
89
+ </div>
90
+ ) : filtered.length === 0 ? (
91
+ <div className="flex flex-col items-center justify-center py-16">
92
+ <Terminal className="w-10 h-10 text-text-dim mb-3" />
93
+ <p className="text-text-muted text-sm">No tools found</p>
94
+ </div>
95
+ ) : (
96
+ <div className="space-y-1.5">
97
+ {filtered.map((t) => (
98
+ <div key={t.id}>
99
+ <button
100
+ onClick={() => setExpandedTool(expandedTool === t.id ? null : t.id)}
101
+ className={`w-full flex items-center gap-3 px-4 py-3 rounded-xl text-left transition-all ${
102
+ expandedTool === t.id ? 'bg-accent-cyan/5 border border-accent-cyan/20' : 'bg-bg-tertiary border border-border hover:border-border-focus'
103
+ }`}
104
+ >
105
+ <div className="w-8 h-8 bg-bg-secondary border border-border rounded-lg flex items-center justify-center shrink-0">
106
+ <Play className="w-3.5 h-3.5 text-text-muted" />
107
+ </div>
108
+ <div className="flex-1 min-w-0">
109
+ <p className="text-sm font-medium truncate">{t.name}</p>
110
+ <p className="text-[11px] text-text-muted truncate">{t.description}</p>
111
+ </div>
112
+ <span className="text-[10px] bg-bg-secondary border border-border px-1.5 py-0.5 rounded text-text-muted shrink-0">{t.integration_id}</span>
113
+ {expandedTool === t.id ? <ChevronDown className="w-3.5 h-3.5 text-text-muted shrink-0" /> : <ChevronRight className="w-3.5 h-3.5 text-text-muted shrink-0" />}
114
+ </button>
115
+ {expandedTool === t.id && (
116
+ <div className="ml-12 mt-1.5 p-4 bg-bg-secondary border border-border rounded-xl space-y-3 animate-fade-in mb-1.5">
117
+ <div>
118
+ <label className="text-[10px] text-text-muted font-medium uppercase tracking-wider">Parameters (JSON)</label>
119
+ <textarea
120
+ value={params}
121
+ onChange={(e) => setParams(e.target.value)}
122
+ className="mt-1.5 w-full h-24 px-3 py-2 bg-bg-tertiary border border-border rounded-lg text-xs font-mono text-text-primary focus:outline-none focus:border-accent-cyan resize-none"
123
+ placeholder='{"key": "value"}'
124
+ />
125
+ </div>
126
+ <button
127
+ onClick={() => handleExecute(t)}
128
+ disabled={executing}
129
+ className="w-full py-2 bg-accent-cyan hover:bg-accent-cyanLight text-bg-primary text-xs font-semibold rounded-lg transition-all disabled:opacity-50 flex items-center justify-center gap-2"
130
+ >
131
+ {executing ? <><span className="w-3 h-3 border-2 border-bg-primary border-t-transparent rounded-full animate-spin" /> Executing...</> : <><Play className="w-3.5 h-3.5" /> Execute {t.name}</>}
132
+ </button>
133
+ </div>
134
+ )}
135
  </div>
136
+ ))}
137
+ </div>
138
+ )}
139
+ </div>
140
+
141
+ {/* Result Panel */}
142
+ <div className="w-1/2 overflow-y-auto p-4">
143
+ {!executionResult && !executing && (
144
+ <div className="flex flex-col items-center justify-center h-full text-text-dim">
145
+ <Terminal className="w-12 h-12 mb-4" />
146
+ <p className="text-sm">Select a tool and execute it</p>
147
+ <p className="text-xs mt-1">Results will appear here</p>
148
+ </div>
149
+ )}
150
+ {executing && (
151
+ <div className="flex flex-col items-center justify-center h-full">
152
+ <div className="w-8 h-8 border-2 border-accent-cyan border-t-transparent rounded-full animate-spin mb-3" />
153
+ <p className="text-sm text-text-muted">Executing...</p>
154
+ </div>
155
+ )}
156
+ {executionResult && !executing && (
157
+ <div className="space-y-3 animate-fade-in">
158
+ <div className="flex items-center gap-3 mb-4">
159
+ <div className={`w-8 h-8 rounded-lg flex items-center justify-center ${
160
+ executionResult.status === 'success' ? 'bg-accent-green/10' : 'bg-accent-red/10'
161
+ }`}>
162
+ {executionResult.status === 'success' ? <CheckCircle className="w-4 h-4 text-accent-green" /> : <XCircle className="w-4 h-4 text-accent-red" />}
163
+ </div>
164
+ <div>
165
+ <p className="text-sm font-semibold capitalize">{executionResult.status}</p>
166
+ <p className="text-[10px] text-text-muted flex items-center gap-1">
167
+ <Clock className="w-3 h-3" /> {executionResult.latency_ms}ms · {new Date(executionResult.executed_at).toLocaleTimeString()}
168
+ </p>
169
  </div>
170
  </div>
171
+ <div className="bg-bg-secondary border border-border rounded-xl overflow-hidden">
172
+ <div className="px-4 py-2.5 border-b border-border bg-bg-tertiary flex items-center gap-2">
173
+ <Terminal className="w-3.5 h-3.5 text-accent-cyan" />
174
+ <span className="text-[10px] font-medium text-text-muted uppercase tracking-wider">Output</span>
175
+ </div>
176
+ <pre className="p-4 text-xs font-mono text-text-primary overflow-x-auto max-h-96 overflow-y-auto whitespace-pre-wrap">
177
+ {JSON.stringify(executionResult.result || executionResult.error, null, 2)}
178
+ </pre>
179
+ </div>
180
+ </div>
181
+ )}
182
+ </div>
183
  </div>
184
  </main>
185
  </div>
186
  )
187
+ }
frontend/components/sidebar.tsx CHANGED
@@ -2,87 +2,115 @@
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>
@@ -90,4 +118,4 @@ export function Sidebar({ integrationCount = 0 }: { integrationCount?: number })
90
  </div>
91
  </aside>
92
  )
93
- }
 
2
 
3
  import Link from 'next/link'
4
  import { usePathname } from 'next/navigation'
5
+ import {
6
+ LayoutDashboard, Plug, Wrench, Link2, Key, BarChart3, Settings,
7
+ LogOut, ExternalLink, Play, ChevronRight
8
+ } from 'lucide-react'
9
  import { cn } from '@/lib/utils'
10
 
11
+ const mainNav = [
12
  { href: '/', icon: LayoutDashboard, label: 'Dashboard' },
13
  { href: '/apps', icon: Plug, label: 'Apps' },
14
+ { href: '/connections', icon: Link2, label: 'Connections' },
15
  { href: '/tools', icon: Wrench, label: 'Tools' },
16
+ { href: '/playground', icon: Play, label: 'Playground' },
17
  ]
18
 
19
+ const configNav = [
20
  { href: '/api-keys', icon: Key, label: 'API Keys' },
21
  { href: '/analytics', icon: BarChart3, label: 'Analytics' },
22
  { href: '/settings', icon: Settings, label: 'Settings' },
23
  ]
24
 
25
+ export function Sidebar({ integrationCount = 0, connectionCount = 0 }: { integrationCount?: number; connectionCount?: number }) {
26
  const pathname = usePathname()
27
 
28
  return (
29
+ <aside className="w-60 bg-bg-secondary border-r border-border flex flex-col shrink-0 h-screen sticky top-0">
30
  <div className="px-5 py-4 border-b border-border">
31
+ <Link href="/" className="flex items-center gap-3 group">
32
+ <div className="w-8 h-8 bg-gradient-to-br from-accent-cyan to-accent-blue rounded-lg flex items-center justify-center font-bold text-bg-primary text-sm group-hover:scale-105 transition-transform">
33
+ C
34
+ </div>
35
+ <div>
36
+ <div className="text-base font-semibold tracking-tight">Compost</div>
37
+ <div className="text-[10px] text-text-dim font-medium">Platform v1.0</div>
38
+ </div>
39
+ </Link>
40
  </div>
41
 
42
+ <nav className="flex-1 px-3 py-4 space-y-1 overflow-y-auto">
43
+ <div className="px-3 mb-2 text-[10px] font-semibold uppercase tracking-widest text-text-dim">Navigation</div>
44
+ {mainNav.map((item) => {
45
+ const isActive = pathname === item.href || (item.href !== '/' && pathname.startsWith(item.href))
46
+ return (
47
+ <Link key={item.href} href={item.href}
48
+ className={cn(
49
+ 'flex items-center gap-3 px-3 py-2 rounded-lg text-sm transition-all duration-150 group',
50
+ isActive
51
+ ? 'bg-accent-cyan/10 text-accent-cyanLight font-medium'
52
+ : 'text-text-secondary hover:bg-bg-hover hover:text-text-primary'
53
+ )}
54
+ >
55
+ <item.icon className={cn('w-4 h-4 shrink-0', isActive ? 'text-accent-cyanLight' : 'text-text-muted group-hover:text-text-primary')} />
56
+ <span className="truncate">{item.label}</span>
57
+ {item.href === '/apps' && integrationCount > 0 && (
58
+ <span className="ml-auto text-[10px] bg-bg-tertiary border border-border text-text-muted px-1.5 py-0.5 rounded-md font-medium">{integrationCount}</span>
59
+ )}
60
+ {item.href === '/connections' && connectionCount > 0 && (
61
+ <span className="ml-auto text-[10px] bg-accent-green/10 border border-accent-green/20 text-accent-green px-1.5 py-0.5 rounded-md font-medium">{connectionCount}</span>
62
+ )}
63
+ </Link>
64
+ )
65
+ })}
66
 
67
+ <div className="pt-6 pb-1">
68
+ <div className="px-3 mb-2 text-[10px] font-semibold uppercase tracking-widest text-text-dim">Configuration</div>
69
+ {configNav.map((item) => {
70
+ const isActive = pathname === item.href
71
+ return (
72
+ <Link key={item.href} href={item.href}
73
+ className={cn(
74
+ 'flex items-center gap-3 px-3 py-2 rounded-lg text-sm transition-all duration-150 group',
75
+ isActive
76
+ ? 'bg-accent-cyan/10 text-accent-cyanLight font-medium'
77
+ : 'text-text-secondary hover:bg-bg-hover hover:text-text-primary'
78
+ )}
79
+ >
80
+ <item.icon className={cn('w-4 h-4 shrink-0', isActive ? 'text-accent-cyanLight' : 'text-text-muted group-hover:text-text-primary')} />
81
+ <span className="truncate">{item.label}</span>
82
+ </Link>
83
+ )
84
+ })}
85
+ </div>
86
 
87
+ <div className="pt-6">
88
+ <a href="/api/docs" target="_blank" rel="noopener noreferrer"
89
+ 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 group"
90
+ >
91
+ <ExternalLink className="w-4 h-4 shrink-0 text-text-muted group-hover:text-text-primary" />
92
+ <span>API Docs</span>
93
+ <ChevronRight className="w-3 h-3 ml-auto text-text-dim" />
94
+ </a>
95
+ </div>
96
  </nav>
97
 
98
  <div className="px-3 py-4 border-t border-border">
99
+ <div className="flex items-center justify-between px-1">
100
+ <div className="flex items-center gap-2.5 min-w-0">
101
+ <div className="w-7 h-7 bg-gradient-to-br from-accent-cyan/20 to-accent-blue/20 rounded-full flex items-center justify-center text-xs font-semibold shrink-0 border border-border">
102
  {typeof window !== 'undefined' ? (localStorage.getItem('email')?.[0]?.toUpperCase() || 'U') : 'U'}
103
  </div>
104
+ <div className="min-w-0">
105
+ <div className="text-xs text-text-secondary truncate">
106
+ {typeof window !== 'undefined' ? localStorage.getItem('email') || 'User' : 'User'}
107
+ </div>
108
+ <div className="text-[10px] text-text-dim">Online</div>
109
+ </div>
110
  </div>
111
  <button onClick={() => { localStorage.removeItem('token'); localStorage.removeItem('email'); window.location.href = '/login' }}
112
  className="p-1.5 text-text-muted hover:text-accent-red hover:bg-accent-red/10 rounded-lg transition-colors"
113
+ title="Sign out"
114
  >
115
  <LogOut className="w-3.5 h-3.5" />
116
  </button>
 
118
  </div>
119
  </aside>
120
  )
121
+ }
frontend/lib/api.ts CHANGED
@@ -5,6 +5,7 @@ export interface Integration {
5
  logo_url: string
6
  category: string
7
  auth_type: string
 
8
  tool_count: number
9
  trigger_count: number
10
  is_active: boolean
@@ -17,10 +18,21 @@ export interface Tool {
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
@@ -29,9 +41,44 @@ export interface Stats {
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}`
@@ -40,16 +87,23 @@ async function fetchApi(path: string, opts: RequestInit = {}) {
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
- }
 
 
 
5
  logo_url: string
6
  category: string
7
  auth_type: string
8
+ oauth_scopes?: string[] | null
9
  tool_count: number
10
  trigger_count: number
11
  is_active: boolean
 
18
  description: string
19
  input_schema: Record<string, unknown>
20
  output_schema?: Record<string, unknown>
21
+ category: string | null
22
  is_active: boolean
23
  }
24
 
25
+ export interface Connection {
26
+ id: string
27
+ integration_id: string
28
+ integration_name: string
29
+ account_label: string | null
30
+ status: string
31
+ connected_at: string
32
+ last_used: string | null
33
+ metadata: Record<string, unknown> | null
34
+ }
35
+
36
  export interface Stats {
37
  total_executions: number
38
  successful: number
 
41
  avg_latency_ms: number
42
  }
43
 
44
+ export interface AuthField {
45
+ name: string
46
+ label: string
47
+ type: string
48
+ required: boolean
49
+ description: string
50
+ default: string
51
+ }
52
+
53
+ export interface AuthConfig {
54
+ integration_id: string
55
+ auth_type: string
56
+ fields: AuthField[]
57
+ oauth_authorize_url: string | null
58
+ oauth_token_url: string | null
59
+ }
60
+
61
+ export interface ToolExecution {
62
+ execution_id: string
63
+ status: string
64
+ result: Record<string, unknown> | null
65
+ latency_ms: number
66
+ executed_at: string
67
+ error: string | null
68
+ }
69
 
70
+ export interface APIKey {
71
+ id: string
72
+ name: string
73
+ key_preview: string
74
+ created_at: string
75
+ last_used: string | null
76
+ is_active: boolean
77
+ }
78
+
79
+ const API_BASE = ''
80
+
81
+ async function request<T>(path: string, opts: RequestInit = {}): Promise<T> {
82
  const token = typeof window !== 'undefined' ? localStorage.getItem('token') : null
83
  const headers: Record<string, string> = { 'Content-Type': 'application/json' }
84
  if (token) headers['Authorization'] = `Bearer ${token}`
 
87
  if (res.status === 401) {
88
  if (typeof window !== 'undefined') {
89
  localStorage.removeItem('token')
90
+ localStorage.removeItem('email')
91
  window.location.href = '/login'
92
  }
93
  throw new Error('Unauthorized')
94
  }
95
+ if (!res.ok) {
96
+ const data = await res.json().catch(() => ({ detail: 'Request failed' }))
97
+ throw new Error(data.detail || `HTTP ${res.status}`)
98
+ }
99
  return res.json()
100
  }
101
 
102
  export const api = {
103
+ get: <T>(path: string) => request<T>(path),
104
  post: <T>(path: string, body?: unknown) =>
105
+ request<T>(path, { method: 'POST', body: body ? JSON.stringify(body) : undefined }),
106
+ put: <T>(path: string, body?: unknown) =>
107
+ request<T>(path, { method: 'PUT', body: body ? JSON.stringify(body) : undefined }),
108
+ delete: <T>(path: string) => request<T>(path, { method: 'DELETE' }),
109
+ }
frontend/tailwind.config.ts CHANGED
@@ -2,7 +2,8 @@ import type { Config } from 'tailwindcss'
2
 
3
  const config: Config = {
4
  content: [
5
- './src/**/*.{js,ts,jsx,tsx,mdx}',
 
6
  ],
7
  theme: {
8
  extend: {
 
2
 
3
  const config: Config = {
4
  content: [
5
+ './app/**/*.{js,ts,jsx,tsx,mdx}',
6
+ './components/**/*.{js,ts,jsx,tsx,mdx}',
7
  ],
8
  theme: {
9
  extend: {