synapse-gateway 2.0.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (135) hide show
  1. package/README.md +385 -0
  2. package/bin/synapse.js +242 -0
  3. package/docs/PLAN.md +1723 -0
  4. package/docs/PRD.md +1799 -0
  5. package/drizzle.config.ts +12 -0
  6. package/next.config.ts +8 -0
  7. package/package.json +82 -0
  8. package/postcss.config.mjs +7 -0
  9. package/public/file.svg +1 -0
  10. package/public/globe.svg +1 -0
  11. package/public/next.svg +1 -0
  12. package/public/vercel.svg +1 -0
  13. package/public/window.svg +1 -0
  14. package/src/app/api/analytics/cost/route.ts +13 -0
  15. package/src/app/api/analytics/usage/route.ts +16 -0
  16. package/src/app/api/auth/login/route.ts +42 -0
  17. package/src/app/api/cache/route.ts +19 -0
  18. package/src/app/api/dashboard/route.ts +35 -0
  19. package/src/app/api/distill/route.ts +10 -0
  20. package/src/app/api/events/route.ts +54 -0
  21. package/src/app/api/health/route.ts +10 -0
  22. package/src/app/api/intelligence/forensics/route.ts +23 -0
  23. package/src/app/api/intelligence/neural-router/route.ts +23 -0
  24. package/src/app/api/keys/route.ts +34 -0
  25. package/src/app/api/mcp/route.ts +49 -0
  26. package/src/app/api/memory/route.ts +10 -0
  27. package/src/app/api/models/benchmark/route.ts +13 -0
  28. package/src/app/api/models/route.ts +39 -0
  29. package/src/app/api/namespace/route.ts +25 -0
  30. package/src/app/api/plugins/route.ts +41 -0
  31. package/src/app/api/providers/accounts/route.ts +91 -0
  32. package/src/app/api/providers/fetch-models/route.ts +52 -0
  33. package/src/app/api/providers/health/route.ts +10 -0
  34. package/src/app/api/providers/route.ts +46 -0
  35. package/src/app/api/routes/pipeline/route.ts +20 -0
  36. package/src/app/api/settings/route.ts +33 -0
  37. package/src/app/api/skills/route.ts +39 -0
  38. package/src/app/api/v1/chat/completions/route.ts +156 -0
  39. package/src/app/api/v1/models/route.ts +44 -0
  40. package/src/app/dashboard/intelligence/loading.tsx +14 -0
  41. package/src/app/dashboard/intelligence/page.tsx +125 -0
  42. package/src/app/dashboard/layout.tsx +143 -0
  43. package/src/app/dashboard/loading.tsx +17 -0
  44. package/src/app/dashboard/memory/loading.tsx +15 -0
  45. package/src/app/dashboard/memory/page.tsx +71 -0
  46. package/src/app/dashboard/models/loading.tsx +13 -0
  47. package/src/app/dashboard/models/page.tsx +107 -0
  48. package/src/app/dashboard/page.tsx +183 -0
  49. package/src/app/dashboard/playground/loading.tsx +17 -0
  50. package/src/app/dashboard/playground/page.tsx +212 -0
  51. package/src/app/dashboard/providers/loading.tsx +15 -0
  52. package/src/app/dashboard/providers/page.tsx +248 -0
  53. package/src/app/dashboard/routes/loading.tsx +15 -0
  54. package/src/app/dashboard/routes/page.tsx +72 -0
  55. package/src/app/dashboard/settings/loading.tsx +20 -0
  56. package/src/app/dashboard/settings/page.tsx +208 -0
  57. package/src/app/dashboard/skills/loading.tsx +26 -0
  58. package/src/app/dashboard/skills/page.tsx +137 -0
  59. package/src/app/dashboard/vault/loading.tsx +18 -0
  60. package/src/app/dashboard/vault/page.tsx +139 -0
  61. package/src/app/favicon.ico +0 -0
  62. package/src/app/globals.css +59 -0
  63. package/src/app/layout.tsx +32 -0
  64. package/src/app/login/page.tsx +87 -0
  65. package/src/app/page.tsx +5 -0
  66. package/src/components/ui/badge.tsx +32 -0
  67. package/src/components/ui/button.tsx +38 -0
  68. package/src/components/ui/card.tsx +50 -0
  69. package/src/components/ui/error-boundary.tsx +47 -0
  70. package/src/components/ui/index.ts +11 -0
  71. package/src/components/ui/input.tsx +26 -0
  72. package/src/components/ui/select.tsx +24 -0
  73. package/src/components/ui/skeleton.tsx +53 -0
  74. package/src/components/ui/toast.tsx +51 -0
  75. package/src/instrumentation.ts +6 -0
  76. package/src/lib/__tests__/auth.test.ts +42 -0
  77. package/src/lib/__tests__/format.test.ts +94 -0
  78. package/src/lib/__tests__/namespace.test.ts +102 -0
  79. package/src/lib/__tests__/squeezer.test.ts +93 -0
  80. package/src/lib/__tests__/utils.test.ts +28 -0
  81. package/src/lib/analytics/index.ts +187 -0
  82. package/src/lib/auth/guard.tsx +71 -0
  83. package/src/lib/auth/index.ts +105 -0
  84. package/src/lib/auth/middleware.ts +64 -0
  85. package/src/lib/benchmark/index.ts +137 -0
  86. package/src/lib/bootstrap.ts +122 -0
  87. package/src/lib/cache/index.ts +1 -0
  88. package/src/lib/cache/semantic.ts +211 -0
  89. package/src/lib/config/defaults.ts +61 -0
  90. package/src/lib/config/index.ts +72 -0
  91. package/src/lib/config/schema.ts +63 -0
  92. package/src/lib/db/index.ts +22 -0
  93. package/src/lib/db/migrate.ts +327 -0
  94. package/src/lib/db/schema.ts +303 -0
  95. package/src/lib/distiller/index.ts +331 -0
  96. package/src/lib/fallback/index.ts +153 -0
  97. package/src/lib/forensics/index.ts +188 -0
  98. package/src/lib/format/anthropic.ts +139 -0
  99. package/src/lib/format/gemini.ts +130 -0
  100. package/src/lib/format/index.ts +3 -0
  101. package/src/lib/format/openai.ts +78 -0
  102. package/src/lib/health/index.ts +158 -0
  103. package/src/lib/mcp/builtin.ts +83 -0
  104. package/src/lib/mcp/index.ts +1 -0
  105. package/src/lib/mcp/registry.ts +49 -0
  106. package/src/lib/memory/index.ts +3 -0
  107. package/src/lib/memory/store.ts +215 -0
  108. package/src/lib/memory/types.ts +56 -0
  109. package/src/lib/namespace/index.ts +89 -0
  110. package/src/lib/neural/features.ts +74 -0
  111. package/src/lib/neural/index.ts +85 -0
  112. package/src/lib/neural/strategies.ts +124 -0
  113. package/src/lib/pipeline/index.ts +84 -0
  114. package/src/lib/pipeline/types.ts +77 -0
  115. package/src/lib/plugins/builtin.ts +79 -0
  116. package/src/lib/plugins/index.ts +65 -0
  117. package/src/lib/prediction/index.ts +113 -0
  118. package/src/lib/providers/api-key/anthropic.ts +96 -0
  119. package/src/lib/providers/api-key/deepseek.ts +108 -0
  120. package/src/lib/providers/api-key/gemini.ts +112 -0
  121. package/src/lib/providers/api-key/openai.ts +122 -0
  122. package/src/lib/providers/api-key/openrouter.ts +112 -0
  123. package/src/lib/providers/base-adapter.ts +122 -0
  124. package/src/lib/providers/registry.ts +46 -0
  125. package/src/lib/providers/types.ts +121 -0
  126. package/src/lib/router/index.ts +82 -0
  127. package/src/lib/skills/forge.ts +57 -0
  128. package/src/lib/skills/index.ts +3 -0
  129. package/src/lib/skills/registry.ts +195 -0
  130. package/src/lib/skills/types.ts +44 -0
  131. package/src/lib/squeezer/index.ts +158 -0
  132. package/src/lib/utils/cn.ts +6 -0
  133. package/src/lib/utils/logger.ts +16 -0
  134. package/src/middleware.ts +60 -0
  135. package/tsconfig.json +34 -0
@@ -0,0 +1,212 @@
1
+ 'use client'
2
+
3
+ import { useEffect, useState, useRef } from 'react'
4
+ import { Send, Trash2 } from 'lucide-react'
5
+
6
+ interface Message {
7
+ role: 'user' | 'assistant' | 'system'
8
+ content: string
9
+ model?: string
10
+ tokens?: number
11
+ streaming?: boolean
12
+ }
13
+
14
+ interface ModelOption {
15
+ id: string
16
+ name: string
17
+ providerId: string
18
+ available: boolean
19
+ }
20
+
21
+ export default function PlaygroundPage() {
22
+ const [messages, setMessages] = useState<Message[]>([])
23
+ const [input, setInput] = useState('')
24
+ const [model, setModel] = useState('best')
25
+ const [models, setModels] = useState<ModelOption[]>([])
26
+ const [loading, setLoading] = useState(false)
27
+ const scrollRef = useRef<HTMLDivElement>(null)
28
+
29
+ useEffect(() => {
30
+ fetch('/api/models').then((r) => r.json()).then((data) => {
31
+ const all = (data as { models: ModelOption[] }).models || []
32
+ setModels(all)
33
+ if (all.length > 0 && model === 'best') setModel(all[0].name)
34
+ }).catch(() => {})
35
+ }, [])
36
+
37
+ useEffect(() => {
38
+ if (scrollRef.current) {
39
+ scrollRef.current.scrollTop = scrollRef.current.scrollHeight
40
+ }
41
+ }, [messages])
42
+
43
+ async function handleSend() {
44
+ if (!input.trim() || loading) return
45
+
46
+ const userMsg: Message = { role: 'user', content: input.trim() }
47
+ const allMessages = [...messages, userMsg]
48
+ setMessages(allMessages)
49
+ setInput('')
50
+ setLoading(true)
51
+
52
+ const assistantIdx = allMessages.length
53
+ setMessages((prev) => [...prev, { role: 'assistant', content: '', streaming: true }])
54
+
55
+ try {
56
+ const res = await fetch('/api/v1/chat/completions', {
57
+ method: 'POST',
58
+ headers: { 'Content-Type': 'application/json' },
59
+ body: JSON.stringify({
60
+ model,
61
+ messages: allMessages.map((m) => ({ role: m.role, content: m.content })),
62
+ stream: true,
63
+ }),
64
+ })
65
+
66
+ if (!res.ok || !res.body) {
67
+ const errData = await res.json().catch(() => ({ error: { message: 'Request failed' } }))
68
+ setMessages((prev) => {
69
+ const updated = [...prev]
70
+ updated[assistantIdx] = { role: 'assistant', content: errData.error?.message || 'Request failed' }
71
+ return updated
72
+ })
73
+ setLoading(false)
74
+ return
75
+ }
76
+
77
+ const reader = res.body.getReader()
78
+ const decoder = new TextDecoder()
79
+ let buffer = ''
80
+ let fullContent = ''
81
+ let modelUsed = model
82
+
83
+ while (true) {
84
+ const { done, value } = await reader.read()
85
+ if (done) break
86
+
87
+ buffer += decoder.decode(value, { stream: true })
88
+ const lines = buffer.split('\n')
89
+ buffer = lines.pop() || ''
90
+
91
+ for (const line of lines) {
92
+ const trimmed = line.trim()
93
+ if (!trimmed.startsWith('data: ')) continue
94
+ const data = trimmed.slice(6)
95
+ if (data === '[DONE]') continue
96
+
97
+ try {
98
+ const parsed = JSON.parse(data)
99
+ if (parsed.error) {
100
+ fullContent += parsed.error.message || JSON.stringify(parsed.error)
101
+ continue
102
+ }
103
+ const delta = parsed.choices?.[0]?.delta?.content
104
+ if (delta) fullContent += delta
105
+ if (parsed.model) modelUsed = parsed.model
106
+ } catch { continue }
107
+ }
108
+
109
+ setMessages((prev) => {
110
+ const updated = [...prev]
111
+ updated[assistantIdx] = { role: 'assistant', content: fullContent, model: modelUsed, streaming: true }
112
+ return updated
113
+ })
114
+ }
115
+
116
+ setMessages((prev) => {
117
+ const updated = [...prev]
118
+ updated[assistantIdx] = { role: 'assistant', content: fullContent, model: modelUsed, streaming: false }
119
+ return updated
120
+ })
121
+ } catch {
122
+ setMessages((prev) => {
123
+ const updated = [...prev]
124
+ updated[assistantIdx] = { role: 'assistant', content: 'Network error: Failed to connect' }
125
+ return updated
126
+ })
127
+ } finally {
128
+ setLoading(false)
129
+ }
130
+ }
131
+
132
+ const namespaceAliases = [
133
+ { value: 'best', label: 'Best (auto)' },
134
+ { value: 'fast', label: 'Fast (auto)' },
135
+ { value: 'cheap', label: 'Cheap (auto)' },
136
+ { value: 'reasoning', label: 'Reasoning (auto)' },
137
+ { value: 'code', label: 'Code (auto)' },
138
+ ]
139
+
140
+ return (
141
+ <div className="flex flex-col h-[calc(100vh-8rem)]">
142
+ <div className="flex items-center justify-between mb-4">
143
+ <div className="flex items-center gap-3">
144
+ <select
145
+ value={model}
146
+ onChange={(e) => setModel(e.target.value)}
147
+ className="bg-muted border border-border rounded-md text-sm px-3 py-1.5 focus:outline-none focus:ring-1 focus:ring-ring max-w-xs"
148
+ >
149
+ <optgroup label="Namespace Aliases">
150
+ {namespaceAliases.map((a) => (
151
+ <option key={a.value} value={a.value}>{a.label}</option>
152
+ ))}
153
+ </optgroup>
154
+ <optgroup label="Models">
155
+ {models.map((m) => (
156
+ <option key={m.id} value={m.name} disabled={!m.available}>
157
+ {m.name} ({m.providerId})
158
+ </option>
159
+ ))}
160
+ </optgroup>
161
+ </select>
162
+ </div>
163
+ <button
164
+ onClick={() => setMessages([])}
165
+ className="flex items-center gap-1.5 px-3 py-1.5 text-sm text-muted-foreground hover:text-foreground border border-border rounded-md hover:bg-muted transition-colors"
166
+ >
167
+ <Trash2 className="h-3.5 w-3.5" /> Clear
168
+ </button>
169
+ </div>
170
+
171
+ <div ref={scrollRef} className="flex-1 overflow-y-auto bg-card border border-border rounded-lg p-4 space-y-4 mb-4">
172
+ {messages.length === 0 && (
173
+ <div className="flex items-center justify-center h-full text-sm text-muted-foreground">
174
+ Send a message to start testing
175
+ </div>
176
+ )}
177
+ {messages.map((m, i) => (
178
+ <div key={i} className={`flex ${m.role === 'user' ? 'justify-end' : 'justify-start'}`}>
179
+ <div className={`max-w-[80%] rounded-lg px-4 py-2.5 text-sm ${m.role === 'user' ? 'bg-primary/10 text-foreground' : 'bg-muted text-foreground'}`}>
180
+ <p className="whitespace-pre-wrap">{m.content || '\u00A0'}{m.streaming && <span className="animate-pulse">|</span>}</p>
181
+ {m.model && !m.streaming && (
182
+ <div className="text-xs text-muted-foreground mt-1.5 flex items-center gap-2">
183
+ <code className="font-mono">{m.model}</code>
184
+ {m.tokens && <span>{m.tokens} tokens</span>}
185
+ </div>
186
+ )}
187
+ </div>
188
+ </div>
189
+ ))}
190
+ </div>
191
+
192
+ <div className="flex gap-2">
193
+ <input
194
+ type="text"
195
+ value={input}
196
+ onChange={(e) => setInput(e.target.value)}
197
+ onKeyDown={(e) => e.key === 'Enter' && !e.shiftKey && handleSend()}
198
+ placeholder="Type a message..."
199
+ className="flex-1 bg-muted border border-border rounded-md px-4 py-2.5 text-sm focus:outline-none focus:ring-1 focus:ring-ring"
200
+ disabled={loading}
201
+ />
202
+ <button
203
+ onClick={handleSend}
204
+ disabled={loading || !input.trim()}
205
+ className="flex items-center gap-2 px-4 py-2.5 bg-primary text-primary-foreground rounded-md text-sm font-medium hover:bg-primary/90 transition-colors disabled:opacity-50 disabled:cursor-not-allowed"
206
+ >
207
+ <Send className="h-4 w-4" /> Send
208
+ </button>
209
+ </div>
210
+ </div>
211
+ )
212
+ }
@@ -0,0 +1,15 @@
1
+ import { CardSkeleton } from '@/components/ui/skeleton'
2
+
3
+ export default function ProvidersLoading() {
4
+ return (
5
+ <div className="space-y-6">
6
+ <div className="flex items-center justify-between">
7
+ <div className="h-4 w-64 animate-pulse rounded bg-muted" />
8
+ <div className="h-8 w-32 animate-pulse rounded bg-muted" />
9
+ </div>
10
+ {Array.from({ length: 5 }).map((_, i) => (
11
+ <CardSkeleton key={i} />
12
+ ))}
13
+ </div>
14
+ )
15
+ }
@@ -0,0 +1,248 @@
1
+ 'use client'
2
+
3
+ import { useEffect, useState } from 'react'
4
+ import { Server, Plus, Eye, EyeOff, Trash2, RefreshCw, Key } from 'lucide-react'
5
+
6
+ interface ProviderData {
7
+ providers: Array<{
8
+ id: string
9
+ name: string
10
+ prefix: string
11
+ type: string
12
+ enabled: boolean
13
+ config: string
14
+ createdAt: string
15
+ health: {
16
+ status: string
17
+ avgLatencyMs: number | null
18
+ successRate: number | null
19
+ errorRate: number | null
20
+ } | null
21
+ }>
22
+ }
23
+
24
+ interface AccountData {
25
+ id: string
26
+ providerId: string
27
+ label: string | null
28
+ enabled: boolean
29
+ priority: number
30
+ hasApiKey: boolean
31
+ lastUsedAt: string | null
32
+ createdAt: string
33
+ }
34
+
35
+ export default function ProvidersPage() {
36
+ const [providers, setProviders] = useState<ProviderData['providers']>([])
37
+ const [accounts, setAccounts] = useState<AccountData[]>([])
38
+ const [expanded, setExpanded] = useState<string | null>(null)
39
+ const [showAddAccount, setShowAddAccount] = useState<string | null>(null)
40
+ const [accountForm, setAccountForm] = useState({ label: '', apiKey: '' })
41
+ const [showApiKey, setShowApiKey] = useState<Record<string, boolean>>({})
42
+ const [fetching, setFetching] = useState<Record<string, boolean>>({})
43
+
44
+ function loadData() {
45
+ Promise.all([
46
+ fetch('/api/providers').then((r) => r.json()).catch(() => ({ providers: [] })),
47
+ fetch('/api/providers/accounts').then((r) => r.json()).catch(() => ({ accounts: [] })),
48
+ ]).then(([pData, aData]) => {
49
+ setProviders((pData as ProviderData).providers || [])
50
+ setAccounts((aData as { accounts: AccountData[] }).accounts || [])
51
+ })
52
+ }
53
+
54
+ useEffect(() => { loadData() }, [])
55
+
56
+ async function handleAddAccount(providerId: string) {
57
+ if (!accountForm.apiKey) return
58
+ await fetch('/api/providers/accounts', {
59
+ method: 'POST',
60
+ headers: { 'Content-Type': 'application/json' },
61
+ body: JSON.stringify({ providerId, label: accountForm.label, apiKey: accountForm.apiKey }),
62
+ })
63
+ setShowAddAccount(null)
64
+ setAccountForm({ label: '', apiKey: '' })
65
+ loadData()
66
+ }
67
+
68
+ async function handleDeleteAccount(id: string) {
69
+ await fetch('/api/providers/accounts', {
70
+ method: 'DELETE',
71
+ headers: { 'Content-Type': 'application/json' },
72
+ body: JSON.stringify({ id }),
73
+ })
74
+ loadData()
75
+ }
76
+
77
+ async function handleFetchModels(providerId: string) {
78
+ setFetching((prev) => ({ ...prev, [providerId]: true }))
79
+ try {
80
+ await fetch('/api/providers/fetch-models', {
81
+ method: 'POST',
82
+ headers: { 'Content-Type': 'application/json' },
83
+ body: JSON.stringify({ providerId }),
84
+ })
85
+ } finally {
86
+ setFetching((prev) => ({ ...prev, [providerId]: false }))
87
+ }
88
+ }
89
+
90
+ async function handleToggleAccount(id: string, enabled: boolean) {
91
+ await fetch('/api/providers/accounts', {
92
+ method: 'PUT',
93
+ headers: { 'Content-Type': 'application/json' },
94
+ body: JSON.stringify({ id, enabled }),
95
+ })
96
+ loadData()
97
+ }
98
+
99
+ return (
100
+ <div className="space-y-6">
101
+ <p className="text-sm text-muted-foreground">Configure AI providers, manage API keys, and fetch available models</p>
102
+
103
+ {providers.length === 0 ? (
104
+ <div className="bg-card border border-border rounded-lg p-8 text-center text-sm text-muted-foreground">
105
+ No providers registered. Bootstrap runs on startup.
106
+ </div>
107
+ ) : (
108
+ <div className="space-y-3">
109
+ {providers.map((p) => {
110
+ const providerAccounts = accounts.filter((a) => a.providerId === p.id)
111
+ const isExpanded = expanded === p.id
112
+
113
+ return (
114
+ <div key={p.id} className="bg-card border border-border rounded-lg">
115
+ <div className="p-4">
116
+ <div className="flex items-start justify-between">
117
+ <div className="flex items-center gap-3">
118
+ <Server className="h-4 w-4 text-primary shrink-0" />
119
+ <div>
120
+ <div className="flex items-center gap-2">
121
+ <h3 className="font-medium text-sm">{p.name}</h3>
122
+ <span className="text-xs font-mono bg-muted px-1.5 py-0.5 rounded">{p.prefix}</span>
123
+ </div>
124
+ <div className="flex items-center gap-3 mt-1 text-xs text-muted-foreground">
125
+ <span>Type: {p.type}</span>
126
+ <span>{providerAccounts.length} account(s)</span>
127
+ {p.health && (
128
+ <>
129
+ <span className={p.health.status === 'healthy' ? 'text-primary' : 'text-destructive'}>
130
+ {p.health.status}
131
+ </span>
132
+ {p.health.avgLatencyMs != null && <span>{p.health.avgLatencyMs}ms</span>}
133
+ </>
134
+ )}
135
+ </div>
136
+ </div>
137
+ </div>
138
+
139
+ <div className="flex items-center gap-2">
140
+ <span className={`text-xs px-2 py-0.5 rounded-full ${p.enabled ? 'bg-primary/10 text-primary' : 'bg-muted text-muted-foreground'}`}>
141
+ {p.enabled ? 'Enabled' : 'Disabled'}
142
+ </span>
143
+ <button
144
+ onClick={() => setExpanded(isExpanded ? null : p.id)}
145
+ className="px-2 py-1 text-xs border border-border rounded hover:bg-muted transition-colors"
146
+ >
147
+ {isExpanded ? 'Collapse' : 'Manage'}
148
+ </button>
149
+ </div>
150
+ </div>
151
+ </div>
152
+
153
+ {isExpanded && (
154
+ <div className="border-t border-border p-4 space-y-3">
155
+ <div className="flex items-center justify-between">
156
+ <h4 className="text-xs font-medium uppercase tracking-wider text-muted-foreground">Accounts</h4>
157
+ <div className="flex items-center gap-2">
158
+ <button
159
+ onClick={() => handleFetchModels(p.id)}
160
+ disabled={fetching[p.id]}
161
+ className="flex items-center gap-1.5 px-2 py-1 text-xs border border-border rounded hover:bg-muted transition-colors disabled:opacity-50"
162
+ >
163
+ <RefreshCw className={`h-3 w-3 ${fetching[p.id] ? 'animate-spin' : ''}`} />
164
+ Fetch Models
165
+ </button>
166
+ <button
167
+ onClick={() => setShowAddAccount(showAddAccount === p.id ? null : p.id)}
168
+ className="flex items-center gap-1.5 px-2 py-1 text-xs bg-primary text-primary-foreground rounded hover:bg-primary/90 transition-colors"
169
+ >
170
+ <Plus className="h-3 w-3" /> Add Account
171
+ </button>
172
+ </div>
173
+ </div>
174
+
175
+ {showAddAccount === p.id && (
176
+ <div className="bg-muted/50 border border-border rounded-md p-3 space-y-2">
177
+ <input
178
+ placeholder="Label (e.g. Production)"
179
+ value={accountForm.label}
180
+ onChange={(e) => setAccountForm({ ...accountForm, label: e.target.value })}
181
+ className="w-full bg-background border border-border rounded px-3 py-1.5 text-sm focus:outline-none focus:ring-1 focus:ring-ring"
182
+ />
183
+ <div className="relative">
184
+ <input
185
+ type={showApiKey[`new-${p.id}`] ? 'text' : 'password'}
186
+ placeholder="API Key"
187
+ value={accountForm.apiKey}
188
+ onChange={(e) => setAccountForm({ ...accountForm, apiKey: e.target.value })}
189
+ className="w-full bg-background border border-border rounded px-3 py-1.5 text-sm focus:outline-none focus:ring-1 focus:ring-ring pr-10"
190
+ />
191
+ <button
192
+ onClick={() => setShowApiKey((prev) => ({ ...prev, [`new-${p.id}`]: !prev[`new-${p.id}`] }))}
193
+ className="absolute right-2 top-1/2 -translate-y-1/2 text-muted-foreground hover:text-foreground"
194
+ >
195
+ {showApiKey[`new-${p.id}`] ? <EyeOff className="h-3.5 w-3.5" /> : <Eye className="h-3.5 w-3.5" />}
196
+ </button>
197
+ </div>
198
+ <div className="flex gap-2">
199
+ <button onClick={() => handleAddAccount(p.id)} disabled={!accountForm.apiKey} className="px-3 py-1 bg-primary text-primary-foreground rounded text-xs hover:bg-primary/90 disabled:opacity-50">Save</button>
200
+ <button onClick={() => { setShowAddAccount(null); setAccountForm({ label: '', apiKey: '' }) }} className="px-3 py-1 border border-border rounded text-xs hover:bg-muted">Cancel</button>
201
+ </div>
202
+ </div>
203
+ )}
204
+
205
+ {providerAccounts.length === 0 ? (
206
+ <div className="text-xs text-muted-foreground py-2">No accounts configured. Add an API key to start using this provider.</div>
207
+ ) : (
208
+ providerAccounts.map((a) => (
209
+ <div key={a.id} className="flex items-center justify-between bg-muted/30 border border-border rounded-md px-3 py-2">
210
+ <div className="flex items-center gap-3">
211
+ <Key className="h-3.5 w-3.5 text-muted-foreground" />
212
+ <div>
213
+ <span className="text-sm">{a.label || 'Unnamed'}</span>
214
+ <span className="ml-2 text-xs text-muted-foreground">
215
+ {a.hasApiKey ? 'Key configured' : 'No key'}
216
+ </span>
217
+ </div>
218
+ </div>
219
+ <div className="flex items-center gap-2">
220
+ <span className="text-xs text-muted-foreground">
221
+ {a.lastUsedAt ? `Used ${new Date(a.lastUsedAt).toLocaleDateString()}` : 'Never used'}
222
+ </span>
223
+ <button
224
+ onClick={() => handleToggleAccount(a.id, !a.enabled)}
225
+ className={`text-xs px-2 py-0.5 rounded-full ${a.enabled ? 'bg-primary/10 text-primary' : 'bg-muted text-muted-foreground'}`}
226
+ >
227
+ {a.enabled ? 'Active' : 'Disabled'}
228
+ </button>
229
+ <button
230
+ onClick={() => handleDeleteAccount(a.id)}
231
+ className="p-1 hover:bg-destructive/10 rounded text-muted-foreground hover:text-destructive transition-colors"
232
+ >
233
+ <Trash2 className="h-3.5 w-3.5" />
234
+ </button>
235
+ </div>
236
+ </div>
237
+ ))
238
+ )}
239
+ </div>
240
+ )}
241
+ </div>
242
+ )
243
+ })}
244
+ </div>
245
+ )}
246
+ </div>
247
+ )
248
+ }
@@ -0,0 +1,15 @@
1
+ import { CardSkeleton } from '@/components/ui/skeleton'
2
+
3
+ export default function RoutesLoading() {
4
+ return (
5
+ <div className="space-y-6">
6
+ <div className="flex items-center justify-between">
7
+ <div className="h-4 w-64 animate-pulse rounded bg-muted" />
8
+ <div className="h-8 w-32 animate-pulse rounded bg-muted" />
9
+ </div>
10
+ {Array.from({ length: 3 }).map((_, i) => (
11
+ <CardSkeleton key={i} />
12
+ ))}
13
+ </div>
14
+ )
15
+ }
@@ -0,0 +1,72 @@
1
+ 'use client'
2
+
3
+ import { useEffect, useState } from 'react'
4
+ import { Route, Plus, ArrowRight } from 'lucide-react'
5
+
6
+ interface Pipeline {
7
+ id: string
8
+ name: string
9
+ description: string | null
10
+ nodes: Array<{ id: string; type: string; config: Record<string, unknown> }>
11
+ connections: Array<{ from: string; to: string }>
12
+ enabled: boolean
13
+ }
14
+
15
+ export default function RoutesPage() {
16
+ const [pipelines, setPipelines] = useState<Pipeline[]>([])
17
+
18
+ useEffect(() => {
19
+ fetch('/api/routes/pipeline').then((r) => r.json()).then((data) => setPipelines((data as { pipelines: Pipeline[] }).pipelines || [])).catch(() => {})
20
+ }, [])
21
+
22
+ return (
23
+ <div className="space-y-6">
24
+ <div className="flex items-center justify-between">
25
+ <p className="text-sm text-muted-foreground">Configure routing pipelines and fallback chains</p>
26
+ <button className="flex items-center gap-2 px-3 py-1.5 bg-primary text-primary-foreground rounded-md text-sm font-medium hover:bg-primary/90 transition-colors">
27
+ <Plus className="h-4 w-4" /> Add Pipeline
28
+ </button>
29
+ </div>
30
+
31
+ {pipelines.length === 0 ? (
32
+ <div className="bg-card border border-border rounded-lg p-8 text-center text-sm text-muted-foreground">
33
+ No pipelines configured. The default pipeline processes requests through provider routing with automatic fallback.
34
+ </div>
35
+ ) : (
36
+ <div className="space-y-3">
37
+ {pipelines.map((p) => (
38
+ <div key={p.id} className="bg-card border border-border rounded-lg p-4">
39
+ <div className="flex items-center gap-3 mb-3">
40
+ <Route className="h-4 w-4 text-primary shrink-0" />
41
+ <h3 className="font-medium text-sm flex-1">{p.name}</h3>
42
+ <span className="text-xs text-muted-foreground">
43
+ {p.nodes?.length || 0} nodes
44
+ </span>
45
+ <span className={`text-xs px-2 py-0.5 rounded-full ${p.enabled ? 'bg-primary/10 text-primary' : 'bg-muted text-muted-foreground'}`}>
46
+ {p.enabled ? 'Active' : 'Disabled'}
47
+ </span>
48
+ </div>
49
+
50
+ {p.description && (
51
+ <p className="text-xs text-muted-foreground mb-3">{p.description}</p>
52
+ )}
53
+
54
+ {p.nodes && p.nodes.length > 0 && (
55
+ <div className="flex items-center gap-2 flex-wrap">
56
+ {p.nodes.map((n, i) => (
57
+ <span key={n.id} className="flex items-center gap-1">
58
+ {i > 0 && <ArrowRight className="h-3 w-3 text-muted-foreground" />}
59
+ <code className="text-xs font-mono bg-muted px-2 py-1 rounded">
60
+ {n.type}
61
+ </code>
62
+ </span>
63
+ ))}
64
+ </div>
65
+ )}
66
+ </div>
67
+ ))}
68
+ </div>
69
+ )}
70
+ </div>
71
+ )
72
+ }
@@ -0,0 +1,20 @@
1
+ export default function SettingsLoading() {
2
+ return (
3
+ <div className="flex gap-6 h-[calc(100vh-8rem)]">
4
+ <div className="w-48 shrink-0 space-y-2">
5
+ {Array.from({ length: 6 }).map((_, i) => (
6
+ <div key={i} className="h-9 w-full animate-pulse rounded bg-muted" />
7
+ ))}
8
+ </div>
9
+ <div className="flex-1 bg-card border border-border rounded-lg p-6 space-y-6">
10
+ <div className="h-6 w-48 animate-pulse rounded bg-muted" />
11
+ {Array.from({ length: 4 }).map((_, i) => (
12
+ <div key={i} className="space-y-2">
13
+ <div className="h-4 w-32 animate-pulse rounded bg-muted" />
14
+ <div className="h-9 w-40 animate-pulse rounded bg-muted" />
15
+ </div>
16
+ ))}
17
+ </div>
18
+ </div>
19
+ )
20
+ }