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,143 @@
1
+ 'use client'
2
+
3
+ import Link from 'next/link'
4
+ import { usePathname } from 'next/navigation'
5
+ import { useState } from 'react'
6
+ import { cn } from '@/lib/utils/cn'
7
+ import { AuthGuard, useAuth } from '@/lib/auth/guard'
8
+ import { ToastProvider } from '@/components/ui/toast'
9
+ import { ErrorBoundary } from '@/components/ui/error-boundary'
10
+ import {
11
+ LayoutDashboard,
12
+ Server,
13
+ Brain,
14
+ Route,
15
+ Wrench,
16
+ Sparkles,
17
+ MessageSquare,
18
+ Shield,
19
+ Settings,
20
+ Database,
21
+ Menu,
22
+ X,
23
+ Zap,
24
+ LogOut,
25
+ } from 'lucide-react'
26
+
27
+ const navItems = [
28
+ { href: '/dashboard', label: 'Command Center', icon: LayoutDashboard },
29
+ { href: '/dashboard/providers', label: 'Providers', icon: Server },
30
+ { href: '/dashboard/models', label: 'Models', icon: Brain },
31
+ { href: '/dashboard/routes', label: 'Routes', icon: Route },
32
+ { href: '/dashboard/skills', label: 'Skills', icon: Wrench },
33
+ { href: '/dashboard/intelligence', label: 'Intelligence', icon: Sparkles },
34
+ { href: '/dashboard/playground', label: 'Playground', icon: MessageSquare },
35
+ { href: '/dashboard/memory', label: 'Memory', icon: Database },
36
+ { href: '/dashboard/vault', label: 'Vault', icon: Shield },
37
+ { href: '/dashboard/settings', label: 'Settings', icon: Settings },
38
+ ]
39
+
40
+ export default function DashboardLayout({ children }: { children: React.ReactNode }) {
41
+ return (
42
+ <AuthGuard>
43
+ <ToastProvider>
44
+ <ErrorBoundary>
45
+ <DashboardShell>{children}</DashboardShell>
46
+ </ErrorBoundary>
47
+ </ToastProvider>
48
+ </AuthGuard>
49
+ )
50
+ }
51
+
52
+ function DashboardShell({ children }: { children: React.ReactNode }) {
53
+ const pathname = usePathname()
54
+ const [mobileOpen, setMobileOpen] = useState(false)
55
+ const { user, logout } = useAuth()
56
+
57
+ return (
58
+ <div className="flex h-screen overflow-hidden">
59
+ <aside
60
+ className={cn(
61
+ 'hidden lg:flex flex-col w-64 border-r border-border bg-sidebar shrink-0',
62
+ mobileOpen && 'fixed inset-y-0 left-0 z-50 flex lg:hidden'
63
+ )}
64
+ >
65
+ <div className="flex items-center gap-2 px-4 h-14 border-b border-border">
66
+ <Zap className="h-5 w-5 text-primary" />
67
+ <span className="font-semibold text-foreground tracking-tight">Synapse</span>
68
+ <span className="text-xs text-muted-foreground ml-auto font-mono">v2.0</span>
69
+ </div>
70
+
71
+ <nav className="flex-1 overflow-y-auto py-2 px-2 space-y-0.5">
72
+ {navItems.map((item) => {
73
+ const isActive = pathname === item.href || (item.href !== '/dashboard' && pathname.startsWith(item.href))
74
+ return (
75
+ <Link
76
+ key={item.href}
77
+ href={item.href}
78
+ onClick={() => setMobileOpen(false)}
79
+ className={cn(
80
+ 'flex items-center gap-3 px-3 py-2 rounded-md text-sm transition-colors',
81
+ isActive
82
+ ? 'bg-primary/10 text-sidebar-active font-medium'
83
+ : 'text-sidebar-foreground hover:bg-muted hover:text-foreground'
84
+ )}
85
+ >
86
+ <item.icon className="h-4 w-4 shrink-0" />
87
+ {item.label}
88
+ </Link>
89
+ )
90
+ })}
91
+ </nav>
92
+
93
+ <div className="border-t border-border p-4 space-y-3">
94
+ <div className="flex items-center gap-2 text-xs text-muted-foreground">
95
+ <div className="h-2 w-2 rounded-full bg-primary animate-pulse" />
96
+ System Online
97
+ </div>
98
+ <div className="flex items-center justify-between">
99
+ <span className="text-xs text-muted-foreground">{user?.name || 'Admin'}</span>
100
+ <button onClick={logout} className="flex items-center gap-1 text-xs text-muted-foreground hover:text-foreground transition-colors">
101
+ <LogOut className="h-3 w-3" /> Sign Out
102
+ </button>
103
+ </div>
104
+ </div>
105
+ </aside>
106
+
107
+ {mobileOpen && (
108
+ <div className="fixed inset-0 z-40 bg-black/50 lg:hidden" onClick={() => setMobileOpen(false)} />
109
+ )}
110
+
111
+ <div className="flex flex-col flex-1 overflow-hidden">
112
+ <header className="flex items-center h-14 px-4 border-b border-border bg-card shrink-0">
113
+ <button
114
+ className="lg:hidden mr-3 p-1 rounded-md hover:bg-muted"
115
+ onClick={() => setMobileOpen(!mobileOpen)}
116
+ >
117
+ {mobileOpen ? <X className="h-5 w-5" /> : <Menu className="h-5 w-5" />}
118
+ </button>
119
+
120
+ <div className="flex items-center gap-2 flex-1">
121
+ <h1 className="text-sm font-medium">
122
+ {navItems.find((i) => pathname === i.href || (i.href !== '/dashboard' && pathname.startsWith(i.href)))?.label || 'Dashboard'}
123
+ </h1>
124
+ </div>
125
+
126
+ <div className="flex items-center gap-4">
127
+ <div className="hidden sm:flex items-center gap-2 text-xs text-muted-foreground font-mono">
128
+ <span>localhost:3333</span>
129
+ <div className="h-2 w-2 rounded-full bg-primary" />
130
+ </div>
131
+ <button onClick={logout} className="lg:hidden flex items-center gap-1 text-xs text-muted-foreground hover:text-foreground">
132
+ <LogOut className="h-3.5 w-3.5" />
133
+ </button>
134
+ </div>
135
+ </header>
136
+
137
+ <main className="flex-1 overflow-y-auto p-4 lg:p-6">
138
+ {children}
139
+ </main>
140
+ </div>
141
+ </div>
142
+ )
143
+ }
@@ -0,0 +1,17 @@
1
+ import { StatsSkeleton } from '@/components/ui/skeleton'
2
+
3
+ export default function DashboardLoading() {
4
+ return (
5
+ <div className="space-y-6">
6
+ <div className="flex items-center justify-between">
7
+ <div className="h-4 w-48 animate-pulse rounded bg-muted" />
8
+ <div className="h-8 w-32 animate-pulse rounded bg-muted" />
9
+ </div>
10
+ <StatsSkeleton />
11
+ <div className="grid grid-cols-1 lg:grid-cols-2 gap-4">
12
+ <div className="bg-card border border-border rounded-lg p-4 h-64 animate-pulse" />
13
+ <div className="bg-card border border-border rounded-lg p-4 h-64 animate-pulse" />
14
+ </div>
15
+ </div>
16
+ )
17
+ }
@@ -0,0 +1,15 @@
1
+ import { CardSkeleton } from '@/components/ui/skeleton'
2
+
3
+ export default function MemoryLoading() {
4
+ return (
5
+ <div className="space-y-6">
6
+ <div className="h-4 w-64 animate-pulse rounded bg-muted" />
7
+ <div className="grid grid-cols-1 md:grid-cols-3 gap-4">
8
+ <CardSkeleton />
9
+ <CardSkeleton />
10
+ <CardSkeleton />
11
+ </div>
12
+ <div className="bg-card border border-border rounded-lg p-4 h-40 animate-pulse" />
13
+ </div>
14
+ )
15
+ }
@@ -0,0 +1,71 @@
1
+ 'use client'
2
+
3
+ import { useEffect, useState } from 'react'
4
+ import { Database, Clock, BookOpen, Wrench } from 'lucide-react'
5
+
6
+ interface MemoryStats {
7
+ stats: { episodes: number; knowledge: number; rules: number; totalSize: string }
8
+ }
9
+
10
+ export default function MemoryPage() {
11
+ const [stats, setStats] = useState<MemoryStats['stats'] | null>(null)
12
+
13
+ useEffect(() => {
14
+ fetch('/api/memory').then((r) => r.json()).then((data) => setStats((data as MemoryStats).stats)).catch(() => {})
15
+ }, [])
16
+
17
+ const memoryTypes = [
18
+ { type: 'Episodic', icon: Clock, description: 'Conversational experiences with 90-day rolling window', count: stats?.episodes || 0, color: 'text-primary', bgColor: 'bg-primary/10' },
19
+ { type: 'Semantic', icon: BookOpen, description: 'Permanent knowledge facts extracted from episodes', count: stats?.knowledge || 0, color: 'text-accent', bgColor: 'bg-accent/10' },
20
+ { type: 'Procedural', icon: Wrench, description: 'Compiled rules and decision procedures', count: stats?.rules || 0, color: 'text-yellow-400', bgColor: 'bg-yellow-400/10' },
21
+ ]
22
+
23
+ return (
24
+ <div className="space-y-6">
25
+ <p className="text-sm text-muted-foreground">Persistent memory system with episodic, semantic, and procedural layers</p>
26
+
27
+ <div className="grid grid-cols-1 md:grid-cols-3 gap-4">
28
+ {memoryTypes.map((m) => (
29
+ <div key={m.type} className="bg-card border border-border rounded-lg p-4 space-y-3">
30
+ <div className="flex items-center gap-2">
31
+ <div className={`p-1.5 rounded ${m.bgColor}`}>
32
+ <m.icon className={`h-4 w-4 ${m.color}`} />
33
+ </div>
34
+ <h3 className="font-medium text-sm">{m.type} Memory</h3>
35
+ </div>
36
+ <p className="text-xs text-muted-foreground">{m.description}</p>
37
+ <div className="pt-2 border-t border-border">
38
+ <div className="text-lg font-semibold">{m.count.toLocaleString()}</div>
39
+ <div className="text-xs text-muted-foreground">entries</div>
40
+ </div>
41
+ </div>
42
+ ))}
43
+ </div>
44
+
45
+ <div className="bg-card border border-border rounded-lg p-4">
46
+ <div className="flex items-center gap-2 mb-3">
47
+ <Database className="h-4 w-4 text-muted-foreground" />
48
+ <h2 className="text-sm font-medium">Memory Operations</h2>
49
+ </div>
50
+ <div className="grid grid-cols-2 lg:grid-cols-4 gap-3">
51
+ <button onClick={async () => { await fetch('/api/distill', { method: 'POST' }) }} className="p-3 border border-border rounded-md text-sm hover:bg-muted transition-colors text-left">
52
+ <div className="font-medium">Distill Now</div>
53
+ <div className="text-xs text-muted-foreground mt-1">Run experience distillation</div>
54
+ </button>
55
+ <div className="p-3 border border-border rounded-md text-sm">
56
+ <div className="font-medium">Search</div>
57
+ <div className="text-xs text-muted-foreground mt-1">Query across all memory types</div>
58
+ </div>
59
+ <div className="p-3 border border-border rounded-md text-sm">
60
+ <div className="font-medium">Export</div>
61
+ <div className="text-xs text-muted-foreground mt-1">Export all memory data</div>
62
+ </div>
63
+ <div className="p-3 border border-border rounded-md text-sm">
64
+ <div className="font-medium">Cleanup</div>
65
+ <div className="text-xs text-muted-foreground mt-1">Remove expired episodes</div>
66
+ </div>
67
+ </div>
68
+ </div>
69
+ </div>
70
+ )
71
+ }
@@ -0,0 +1,13 @@
1
+ import { TableSkeleton } from '@/components/ui/skeleton'
2
+
3
+ export default function ModelsLoading() {
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-56 animate-pulse rounded bg-muted" />
9
+ </div>
10
+ <TableSkeleton rows={8} />
11
+ </div>
12
+ )
13
+ }
@@ -0,0 +1,107 @@
1
+ 'use client'
2
+
3
+ import { useEffect, useState } from 'react'
4
+ import { Brain, Search } from 'lucide-react'
5
+
6
+ interface ModelData {
7
+ models: Array<{
8
+ id: string
9
+ providerId: string
10
+ name: string
11
+ displayName: string | null
12
+ pricingTier: string | null
13
+ costPer1mInput: number | null
14
+ costPer1mOutput: number | null
15
+ contextWindow: number | null
16
+ available: boolean
17
+ }>
18
+ }
19
+
20
+ const categoryColors: Record<string, string> = {
21
+ free: 'bg-primary/10 text-primary',
22
+ cheap: 'bg-blue-500/10 text-blue-400',
23
+ pay_per_use: 'bg-accent/10 text-accent',
24
+ subscription: 'bg-yellow-500/10 text-yellow-400',
25
+ }
26
+
27
+ export default function ModelsPage() {
28
+ const [models, setModels] = useState<ModelData['models']>([])
29
+ const [search, setSearch] = useState('')
30
+
31
+ useEffect(() => {
32
+ fetch('/api/models').then((r) => r.json()).then((data) => setModels((data as ModelData).models || [])).catch(() => {})
33
+ }, [])
34
+
35
+ const filtered = models.filter((m) =>
36
+ !search || m.name.toLowerCase().includes(search.toLowerCase()) || m.id.toLowerCase().includes(search.toLowerCase()) || m.providerId.toLowerCase().includes(search.toLowerCase())
37
+ )
38
+
39
+ return (
40
+ <div className="space-y-6">
41
+ <div className="flex items-center justify-between">
42
+ <p className="text-sm text-muted-foreground">Browse and manage all available AI models</p>
43
+ <div className="relative">
44
+ <Search className="absolute left-2.5 top-1/2 -translate-y-1/2 h-3.5 w-3.5 text-muted-foreground" />
45
+ <input
46
+ type="text"
47
+ placeholder="Search models..."
48
+ value={search}
49
+ onChange={(e) => setSearch(e.target.value)}
50
+ className="pl-8 pr-3 py-1.5 bg-muted border border-border rounded-md text-sm w-56 focus:outline-none focus:ring-1 focus:ring-ring"
51
+ />
52
+ </div>
53
+ </div>
54
+
55
+ {filtered.length === 0 ? (
56
+ <div className="bg-card border border-border rounded-lg p-8 text-center text-sm text-muted-foreground">
57
+ {models.length === 0 ? 'No models registered yet. Fetch models from providers.' : 'No models match your search.'}
58
+ </div>
59
+ ) : (
60
+ <div className="bg-card border border-border rounded-lg overflow-hidden">
61
+ <table className="w-full text-sm">
62
+ <thead>
63
+ <tr className="border-b border-border bg-muted/50">
64
+ <th className="text-left px-4 py-2.5 text-xs font-medium text-muted-foreground uppercase tracking-wider">Model</th>
65
+ <th className="text-left px-4 py-2.5 text-xs font-medium text-muted-foreground uppercase tracking-wider">Provider</th>
66
+ <th className="text-left px-4 py-2.5 text-xs font-medium text-muted-foreground uppercase tracking-wider">Tier</th>
67
+ <th className="text-left px-4 py-2.5 text-xs font-medium text-muted-foreground uppercase tracking-wider">Context</th>
68
+ <th className="text-left px-4 py-2.5 text-xs font-medium text-muted-foreground uppercase tracking-wider">Input $/1M</th>
69
+ <th className="text-left px-4 py-2.5 text-xs font-medium text-muted-foreground uppercase tracking-wider">Output $/1M</th>
70
+ <th className="text-left px-4 py-2.5 text-xs font-medium text-muted-foreground uppercase tracking-wider">Status</th>
71
+ </tr>
72
+ </thead>
73
+ <tbody className="divide-y divide-border">
74
+ {filtered.map((m) => (
75
+ <tr key={m.id} className="hover:bg-muted/30 transition-colors">
76
+ <td className="px-4 py-3">
77
+ <div className="flex items-center gap-2">
78
+ <Brain className="h-3.5 w-3.5 text-muted-foreground" />
79
+ <code className="font-mono text-xs">{m.name}</code>
80
+ </div>
81
+ </td>
82
+ <td className="px-4 py-3 text-xs text-muted-foreground">{m.providerId}</td>
83
+ <td className="px-4 py-3">
84
+ {m.pricingTier && (
85
+ <span className={`text-xs px-2 py-0.5 rounded-full ${categoryColors[m.pricingTier] || 'bg-muted text-muted-foreground'}`}>
86
+ {m.pricingTier}
87
+ </span>
88
+ )}
89
+ </td>
90
+ <td className="px-4 py-3 text-xs font-mono">{m.contextWindow ? `${m.contextWindow / 1000}K` : '—'}</td>
91
+ <td className="px-4 py-3 text-xs font-mono">{m.costPer1mInput != null ? `$${m.costPer1mInput}` : '—'}</td>
92
+ <td className="px-4 py-3 text-xs font-mono">{m.costPer1mOutput != null ? `$${m.costPer1mOutput}` : '—'}</td>
93
+ <td className="px-4 py-3">
94
+ <span className="flex items-center gap-1.5 text-xs text-primary">
95
+ <span className={`h-1.5 w-1.5 rounded-full ${m.available ? 'bg-primary' : 'bg-muted-foreground'}`} />
96
+ {m.available ? 'active' : 'inactive'}
97
+ </span>
98
+ </td>
99
+ </tr>
100
+ ))}
101
+ </tbody>
102
+ </table>
103
+ </div>
104
+ )}
105
+ </div>
106
+ )
107
+ }
@@ -0,0 +1,183 @@
1
+ 'use client'
2
+
3
+ import { useEffect, useState } from 'react'
4
+ import {
5
+ Activity,
6
+ DollarSign,
7
+ Clock,
8
+ ArrowUpRight,
9
+ ArrowDownRight,
10
+ Zap,
11
+ Server,
12
+ } from 'lucide-react'
13
+
14
+ interface DashboardData {
15
+ stats: {
16
+ totalRequests: number
17
+ todayTokens: number
18
+ todayCost: number
19
+ cacheHits: number
20
+ }
21
+ recentRequests: Array<{
22
+ id: string
23
+ requestId: string
24
+ model: string
25
+ providerId: string | null
26
+ inputTokens: number | null
27
+ outputTokens: number | null
28
+ tokensSaved: number | null
29
+ cost: number | null
30
+ latencyMs: number | null
31
+ statusCode: number | null
32
+ fallbackUsed: number | null
33
+ cached: number | null
34
+ createdAt: string
35
+ }>
36
+ providers: Array<{
37
+ id: string
38
+ name: string
39
+ prefix: string
40
+ enabled: boolean
41
+ health: {
42
+ status: string
43
+ avgLatencyMs: number | null
44
+ successRate: number | null
45
+ } | null
46
+ }>
47
+ }
48
+
49
+ function formatNumber(n: number): string {
50
+ if (n >= 1_000_000) return (n / 1_000_000).toFixed(1) + 'M'
51
+ if (n >= 1_000) return (n / 1_000).toFixed(1) + 'K'
52
+ return n.toLocaleString()
53
+ }
54
+
55
+ function StatusDot({ status }: { status: string }) {
56
+ const color = status === 'healthy' ? 'bg-primary' : status === 'degraded' ? 'bg-yellow-500' : 'bg-destructive'
57
+ return <span className={`inline-block h-2 w-2 rounded-full ${color}`} />
58
+ }
59
+
60
+ export default function DashboardPage() {
61
+ const [data, setData] = useState<DashboardData | null>(null)
62
+ const [loading, setLoading] = useState(true)
63
+
64
+ useEffect(() => {
65
+ async function load() {
66
+ try {
67
+ const res = await fetch('/api/dashboard')
68
+ if (res.ok) setData(await res.json())
69
+ } catch { /* ignore */ }
70
+ setLoading(false)
71
+ }
72
+ load()
73
+ const interval = setInterval(load, 10000)
74
+ return () => clearInterval(interval)
75
+ }, [])
76
+
77
+ if (loading) {
78
+ return (
79
+ <div className="flex items-center justify-center h-64 text-sm text-muted-foreground">
80
+ Loading dashboard...
81
+ </div>
82
+ )
83
+ }
84
+
85
+ if (!data) {
86
+ return (
87
+ <div className="flex items-center justify-center h-64 text-sm text-muted-foreground">
88
+ Unable to load dashboard data. Make sure the database is initialized.
89
+ </div>
90
+ )
91
+ }
92
+
93
+ const stats = [
94
+ { label: 'Total Requests', value: formatNumber(data.stats.totalRequests), change: '', up: true, icon: Activity },
95
+ { label: 'Tokens Used', value: formatNumber(data.stats.todayTokens), change: '', up: true, icon: Zap },
96
+ { label: 'Total Cost', value: `$${Number(data.stats.todayCost).toFixed(2)}`, change: '', up: false, icon: DollarSign },
97
+ { label: 'Cache Hits', value: formatNumber(data.stats.cacheHits), change: '', up: true, icon: Clock },
98
+ ]
99
+
100
+ return (
101
+ <div className="space-y-6">
102
+ <div className="grid grid-cols-1 sm:grid-cols-2 lg:grid-cols-4 gap-4">
103
+ {stats.map((s) => (
104
+ <div key={s.label} className="bg-card border border-border rounded-lg p-4">
105
+ <div className="flex items-center justify-between mb-2">
106
+ <span className="text-xs text-muted-foreground uppercase tracking-wider">{s.label}</span>
107
+ <s.icon className="h-4 w-4 text-muted-foreground" />
108
+ </div>
109
+ <div className="text-2xl font-semibold">{s.value}</div>
110
+ </div>
111
+ ))}
112
+ </div>
113
+
114
+ <div className="grid grid-cols-1 lg:grid-cols-3 gap-4">
115
+ <div className="lg:col-span-2 bg-card border border-border rounded-lg">
116
+ <div className="px-4 py-3 border-b border-border">
117
+ <h2 className="text-sm font-medium">Recent Requests</h2>
118
+ </div>
119
+ {data.recentRequests.length === 0 ? (
120
+ <div className="px-4 py-8 text-sm text-muted-foreground text-center">
121
+ No requests yet. Send a request to <code className="font-mono text-xs">/api/v1/chat/completions</code> to get started.
122
+ </div>
123
+ ) : (
124
+ <div className="divide-y divide-border">
125
+ {data.recentRequests.slice(0, 10).map((r) => (
126
+ <div key={r.id} className="px-4 py-3 flex items-center gap-4 text-sm">
127
+ <span className="text-xs text-muted-foreground w-16 shrink-0">
128
+ {new Date(r.createdAt).toLocaleTimeString()}
129
+ </span>
130
+ <span className="font-mono text-xs">{r.model}</span>
131
+ <span className="text-muted-foreground text-xs">{r.providerId || '—'}</span>
132
+ <span className="ml-auto text-muted-foreground text-xs">
133
+ {(r.inputTokens || 0) + (r.outputTokens || 0)} tok
134
+ </span>
135
+ <span className="text-muted-foreground text-xs w-16 text-right">
136
+ {r.latencyMs ? `${r.latencyMs}ms` : '—'}
137
+ </span>
138
+ <span className={`text-xs px-2 py-0.5 rounded-full ${
139
+ r.cached ? 'bg-accent/10 text-accent'
140
+ : r.fallbackUsed ? 'bg-yellow-500/10 text-yellow-500'
141
+ : r.statusCode && r.statusCode >= 400 ? 'bg-destructive/10 text-destructive'
142
+ : 'bg-primary/10 text-primary'
143
+ }`}>
144
+ {r.cached ? 'cached' : r.fallbackUsed ? 'fallback' : r.statusCode && r.statusCode >= 400 ? 'error' : 'success'}
145
+ </span>
146
+ </div>
147
+ ))}
148
+ </div>
149
+ )}
150
+ </div>
151
+
152
+ <div className="bg-card border border-border rounded-lg">
153
+ <div className="px-4 py-3 border-b border-border flex items-center gap-2">
154
+ <Server className="h-4 w-4 text-muted-foreground" />
155
+ <h2 className="text-sm font-medium">Providers</h2>
156
+ </div>
157
+ {data.providers.length === 0 ? (
158
+ <div className="px-4 py-8 text-sm text-muted-foreground text-center">
159
+ No providers configured yet.
160
+ </div>
161
+ ) : (
162
+ <div className="divide-y divide-border">
163
+ {data.providers.map((p) => (
164
+ <div key={p.id} className="px-4 py-3 flex items-center gap-3">
165
+ <StatusDot status={p.health?.status || 'unknown'} />
166
+ <div className="flex-1 min-w-0">
167
+ <div className="text-sm font-medium">{p.name}</div>
168
+ <div className="text-xs text-muted-foreground font-mono">{p.prefix}</div>
169
+ </div>
170
+ {p.health && (
171
+ <div className="text-xs text-muted-foreground">
172
+ {p.health.avgLatencyMs ? `${p.health.avgLatencyMs}ms` : '—'}
173
+ </div>
174
+ )}
175
+ </div>
176
+ ))}
177
+ </div>
178
+ )}
179
+ </div>
180
+ </div>
181
+ </div>
182
+ )
183
+ }
@@ -0,0 +1,17 @@
1
+ export default function PlaygroundLoading() {
2
+ return (
3
+ <div className="flex flex-col h-[calc(100vh-8rem)]">
4
+ <div className="flex items-center justify-between mb-4">
5
+ <div className="h-9 w-48 animate-pulse rounded bg-muted" />
6
+ <div className="h-8 w-20 animate-pulse rounded bg-muted" />
7
+ </div>
8
+ <div className="flex-1 bg-card border border-border rounded-lg p-4 mb-4 flex items-center justify-center">
9
+ <div className="text-sm text-muted-foreground animate-pulse">Loading playground...</div>
10
+ </div>
11
+ <div className="flex gap-2">
12
+ <div className="flex-1 h-10 animate-pulse rounded bg-muted" />
13
+ <div className="h-10 w-20 animate-pulse rounded bg-muted" />
14
+ </div>
15
+ </div>
16
+ )
17
+ }