waypoi 0.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 (260) hide show
  1. package/.github/instructions/ui.instructions.md +42 -0
  2. package/.github/workflows/ci.yml +35 -0
  3. package/.github/workflows/publish.yml +71 -0
  4. package/.github/workflows/release.yml +48 -0
  5. package/.playwright-mcp/console-2026-04-04T01-41-10-746Z.log +2 -0
  6. package/.playwright-mcp/console-2026-04-04T01-41-28-799Z.log +3 -0
  7. package/.playwright-mcp/console-2026-04-05T02-26-51-909Z.log +76 -0
  8. package/.playwright-mcp/page-2026-04-04T01-41-10-816Z.yml +1 -0
  9. package/.playwright-mcp/page-2026-04-04T01-41-29-141Z.yml +77 -0
  10. package/.playwright-mcp/page-2026-04-04T01-41-42-633Z.yml +190 -0
  11. package/.playwright-mcp/page-2026-04-04T01-42-03-929Z.yml +262 -0
  12. package/.playwright-mcp/page-2026-04-04T02-12-54-813Z.yml +6 -0
  13. package/.playwright-mcp/page-2026-04-04T02-14-58-600Z.yml +190 -0
  14. package/.playwright-mcp/page-2026-04-04T02-15-03-923Z.yml +190 -0
  15. package/.playwright-mcp/page-2026-04-04T02-15-07-426Z.yml +190 -0
  16. package/.playwright-mcp/page-2026-04-04T02-15-25-729Z.yml +262 -0
  17. package/.playwright-mcp/page-2026-04-04T02-16-22-984Z.yml +262 -0
  18. package/.playwright-mcp/page-2026-04-04T02-17-00-599Z.yml +190 -0
  19. package/.playwright-mcp/page-2026-04-04T02-17-50-874Z.yml +190 -0
  20. package/.playwright-mcp/page-2026-04-05T02-26-55-570Z.yml +6 -0
  21. package/AGENTS.md +48 -0
  22. package/CHANGELOG.md +131 -0
  23. package/README.md +552 -0
  24. package/assets/agent-mode.png +0 -0
  25. package/assets/categorize.png +0 -0
  26. package/assets/dashboard.png +0 -0
  27. package/assets/endpoint-proxy.png +0 -0
  28. package/assets/icon.png +0 -0
  29. package/assets/mcp-generate-image.png +0 -0
  30. package/assets/mcp-understand-image.png +0 -0
  31. package/assets/peek-token-flow.png +0 -0
  32. package/assets/playground.png +0 -0
  33. package/assets/sankey.png +0 -0
  34. package/cli/index.ts +2805 -0
  35. package/cli/legacyRewrite.ts +108 -0
  36. package/cli/modelRef.ts +24 -0
  37. package/dist/cli/index.js +2536 -0
  38. package/dist/cli/legacyRewrite.js +92 -0
  39. package/dist/cli/modelRef.js +20 -0
  40. package/dist/src/benchmark/artifacts.js +131 -0
  41. package/dist/src/benchmark/capabilityClassifier.js +81 -0
  42. package/dist/src/benchmark/capabilityStore.js +144 -0
  43. package/dist/src/benchmark/config.js +238 -0
  44. package/dist/src/benchmark/gates.js +118 -0
  45. package/dist/src/benchmark/jobs.js +252 -0
  46. package/dist/src/benchmark/runner.js +1847 -0
  47. package/dist/src/benchmark/schema.js +353 -0
  48. package/dist/src/benchmark/suites.js +314 -0
  49. package/dist/src/benchmark/tinyQaDataset.js +422 -0
  50. package/dist/src/benchmark/types.js +25 -0
  51. package/dist/src/config.js +47 -0
  52. package/dist/src/index.js +178 -0
  53. package/dist/src/mcp/client.js +215 -0
  54. package/dist/src/mcp/discovery.js +226 -0
  55. package/dist/src/mcp/policy.js +65 -0
  56. package/dist/src/mcp/registry.js +129 -0
  57. package/dist/src/mcp/service.js +460 -0
  58. package/dist/src/middleware/auth.js +179 -0
  59. package/dist/src/middleware/requestCapture.js +192 -0
  60. package/dist/src/middleware/requestStats.js +118 -0
  61. package/dist/src/pools/builder.js +132 -0
  62. package/dist/src/pools/repository.js +69 -0
  63. package/dist/src/pools/scheduler.js +360 -0
  64. package/dist/src/pools/types.js +2 -0
  65. package/dist/src/protocols/adapters/dashscope.js +267 -0
  66. package/dist/src/protocols/adapters/inferenceV2.js +346 -0
  67. package/dist/src/protocols/adapters/openai.js +27 -0
  68. package/dist/src/protocols/registry.js +99 -0
  69. package/dist/src/protocols/types.js +2 -0
  70. package/dist/src/providers/health.js +153 -0
  71. package/dist/src/providers/importer.js +289 -0
  72. package/dist/src/providers/modelRegistry.js +313 -0
  73. package/dist/src/providers/repository.js +361 -0
  74. package/dist/src/providers/types.js +2 -0
  75. package/dist/src/routes/admin.js +531 -0
  76. package/dist/src/routes/audio.js +295 -0
  77. package/dist/src/routes/chat.js +240 -0
  78. package/dist/src/routes/embeddings.js +157 -0
  79. package/dist/src/routes/images.js +288 -0
  80. package/dist/src/routes/mcp.js +256 -0
  81. package/dist/src/routes/mcpService.js +100 -0
  82. package/dist/src/routes/models.js +48 -0
  83. package/dist/src/routes/responses.js +711 -0
  84. package/dist/src/routes/sessions.js +450 -0
  85. package/dist/src/routes/stats.js +270 -0
  86. package/dist/src/routes/ui.js +97 -0
  87. package/dist/src/routes/videos.js +107 -0
  88. package/dist/src/routing/router.js +338 -0
  89. package/dist/src/services/imageGeneration.js +280 -0
  90. package/dist/src/services/imageUnderstanding.js +352 -0
  91. package/dist/src/services/videoGeneration.js +79 -0
  92. package/dist/src/storage/captureRepository.js +1591 -0
  93. package/dist/src/storage/files.js +157 -0
  94. package/dist/src/storage/imageCache.js +346 -0
  95. package/dist/src/storage/repositories.js +388 -0
  96. package/dist/src/storage/sessionRepository.js +370 -0
  97. package/dist/src/storage/statsRepository.js +204 -0
  98. package/dist/src/transport/httpClient.js +126 -0
  99. package/dist/src/types.js +2 -0
  100. package/dist/src/utils/messageMedia.js +285 -0
  101. package/dist/src/utils/modelCapabilities.js +108 -0
  102. package/dist/src/utils/modelDiscovery.js +170 -0
  103. package/dist/src/version.js +5 -0
  104. package/dist/src/workers/captureRetention.js +25 -0
  105. package/dist/src/workers/configWatcher.js +91 -0
  106. package/dist/src/workers/healthChecker.js +21 -0
  107. package/dist/src/workers/statsRotation.js +41 -0
  108. package/docs/LLM/output_schema.md +312 -0
  109. package/docs/benchmark.md +208 -0
  110. package/docs/mcp-guidelines.md +125 -0
  111. package/docs/mcp-service.md +178 -0
  112. package/docs/opencode.md +86 -0
  113. package/docs/providers.md +79 -0
  114. package/examples/benchmark.config.yaml +28 -0
  115. package/examples/providers/alibaba-dashscope.yaml +88 -0
  116. package/examples/providers/alibaba-llm.yaml +64 -0
  117. package/examples/providers/alibaba-registry.yaml +7 -0
  118. package/examples/providers/inference-v2-ray.yaml +29 -0
  119. package/examples/scenarios/assets/omni-call-sample.wav +0 -0
  120. package/examples/scenarios/custom.jsonl +5 -0
  121. package/examples/scenarios/custom.yaml +40 -0
  122. package/model-form-v2.png +0 -0
  123. package/package.json +66 -0
  124. package/provider-form-v2.png +0 -0
  125. package/provider-form.png +0 -0
  126. package/scripts/manual-test.sh +11 -0
  127. package/scripts/version-from-git.js +23 -0
  128. package/src/benchmark/artifacts.ts +149 -0
  129. package/src/benchmark/capabilityClassifier.ts +99 -0
  130. package/src/benchmark/capabilityStore.ts +174 -0
  131. package/src/benchmark/config.ts +337 -0
  132. package/src/benchmark/gates.ts +164 -0
  133. package/src/benchmark/jobs.ts +312 -0
  134. package/src/benchmark/runner.ts +2519 -0
  135. package/src/benchmark/schema.ts +443 -0
  136. package/src/benchmark/suites.ts +323 -0
  137. package/src/benchmark/tinyQaDataset.ts +428 -0
  138. package/src/benchmark/types.ts +442 -0
  139. package/src/config.ts +44 -0
  140. package/src/index.ts +195 -0
  141. package/src/mcp/client.ts +305 -0
  142. package/src/mcp/discovery.ts +266 -0
  143. package/src/mcp/policy.ts +105 -0
  144. package/src/mcp/registry.ts +164 -0
  145. package/src/mcp/service.ts +611 -0
  146. package/src/middleware/auth.ts +251 -0
  147. package/src/middleware/requestCapture.ts +245 -0
  148. package/src/middleware/requestStats.ts +163 -0
  149. package/src/pools/builder.ts +159 -0
  150. package/src/pools/repository.ts +71 -0
  151. package/src/pools/scheduler.ts +425 -0
  152. package/src/pools/types.ts +117 -0
  153. package/src/protocols/adapters/dashscope.ts +335 -0
  154. package/src/protocols/adapters/inferenceV2.ts +428 -0
  155. package/src/protocols/adapters/openai.ts +32 -0
  156. package/src/protocols/registry.ts +117 -0
  157. package/src/protocols/types.ts +81 -0
  158. package/src/providers/health.ts +207 -0
  159. package/src/providers/importer.ts +402 -0
  160. package/src/providers/modelRegistry.ts +415 -0
  161. package/src/providers/repository.ts +439 -0
  162. package/src/providers/types.ts +113 -0
  163. package/src/routes/admin.ts +666 -0
  164. package/src/routes/audio.ts +372 -0
  165. package/src/routes/chat.ts +301 -0
  166. package/src/routes/embeddings.ts +197 -0
  167. package/src/routes/images.ts +356 -0
  168. package/src/routes/mcp.ts +320 -0
  169. package/src/routes/mcpService.ts +114 -0
  170. package/src/routes/models.ts +50 -0
  171. package/src/routes/responses.ts +872 -0
  172. package/src/routes/sessions.ts +558 -0
  173. package/src/routes/stats.ts +312 -0
  174. package/src/routes/ui.ts +96 -0
  175. package/src/routes/videos.ts +132 -0
  176. package/src/routing/router.ts +501 -0
  177. package/src/services/imageGeneration.ts +396 -0
  178. package/src/services/imageUnderstanding.ts +449 -0
  179. package/src/services/videoGeneration.ts +127 -0
  180. package/src/storage/captureRepository.ts +1835 -0
  181. package/src/storage/files.ts +178 -0
  182. package/src/storage/imageCache.ts +405 -0
  183. package/src/storage/repositories.ts +494 -0
  184. package/src/storage/sessionRepository.ts +419 -0
  185. package/src/storage/statsRepository.ts +238 -0
  186. package/src/transport/httpClient.ts +145 -0
  187. package/src/types.ts +322 -0
  188. package/src/utils/messageMedia.ts +293 -0
  189. package/src/utils/modelCapabilities.ts +161 -0
  190. package/src/utils/modelDiscovery.ts +203 -0
  191. package/src/workers/captureRetention.ts +25 -0
  192. package/src/workers/configWatcher.ts +115 -0
  193. package/src/workers/healthChecker.ts +22 -0
  194. package/src/workers/statsRotation.ts +49 -0
  195. package/tests/benchmarkAdminRoutes.test.ts +82 -0
  196. package/tests/benchmarkBasics.test.ts +116 -0
  197. package/tests/captureAdminRoutes.test.ts +420 -0
  198. package/tests/captureRepository.test.ts +797 -0
  199. package/tests/cliLegacyRewrite.test.ts +45 -0
  200. package/tests/imageGeneration.service.test.ts +107 -0
  201. package/tests/imageUnderstanding.service.test.ts +123 -0
  202. package/tests/mcpPolicy.test.ts +105 -0
  203. package/tests/mcpService.test.ts +1245 -0
  204. package/tests/modelRef.test.ts +23 -0
  205. package/tests/modelsRoutes.test.ts +154 -0
  206. package/tests/sessionMediaCache.test.ts +167 -0
  207. package/tests/statsRoutes.test.ts +323 -0
  208. package/tsconfig.json +15 -0
  209. package/ui/index.html +16 -0
  210. package/ui/package-lock.json +8521 -0
  211. package/ui/package.json +52 -0
  212. package/ui/postcss.config.js +6 -0
  213. package/ui/public/assets/apple-touch-icon.png +0 -0
  214. package/ui/public/assets/favicon-16.png +0 -0
  215. package/ui/public/assets/favicon-32.png +0 -0
  216. package/ui/public/assets/icon-192.png +0 -0
  217. package/ui/public/assets/icon-512.png +0 -0
  218. package/ui/src/App.tsx +27 -0
  219. package/ui/src/api/client.ts +1503 -0
  220. package/ui/src/components/EndpointUsageGuide.tsx +361 -0
  221. package/ui/src/components/Layout.tsx +124 -0
  222. package/ui/src/components/MessageContent.tsx +365 -0
  223. package/ui/src/components/ToolCallMessage.tsx +179 -0
  224. package/ui/src/components/ToolPicker.tsx +442 -0
  225. package/ui/src/components/messageContentParser.test.ts +41 -0
  226. package/ui/src/components/messageContentParser.ts +73 -0
  227. package/ui/src/components/thinkingPreview.test.ts +27 -0
  228. package/ui/src/components/thinkingPreview.ts +15 -0
  229. package/ui/src/components/toMermaidSankey.test.ts +78 -0
  230. package/ui/src/components/toMermaidSankey.ts +56 -0
  231. package/ui/src/components/ui/button.tsx +58 -0
  232. package/ui/src/components/ui/input.tsx +21 -0
  233. package/ui/src/components/ui/textarea.tsx +21 -0
  234. package/ui/src/lib/utils.ts +6 -0
  235. package/ui/src/main.tsx +9 -0
  236. package/ui/src/pages/AgentPlayground.tsx +2010 -0
  237. package/ui/src/pages/Benchmark.tsx +988 -0
  238. package/ui/src/pages/Dashboard.tsx +581 -0
  239. package/ui/src/pages/Peek.tsx +962 -0
  240. package/ui/src/pages/Settings.tsx +2013 -0
  241. package/ui/src/pages/agentPlaygroundPayload.test.ts +109 -0
  242. package/ui/src/pages/agentPlaygroundPayload.ts +97 -0
  243. package/ui/src/pages/agentThinkingContent.test.ts +50 -0
  244. package/ui/src/pages/agentThinkingContent.ts +57 -0
  245. package/ui/src/pages/dashboardTokenUsage.test.ts +66 -0
  246. package/ui/src/pages/dashboardTokenUsage.ts +36 -0
  247. package/ui/src/pages/imageUpload.test.ts +39 -0
  248. package/ui/src/pages/imageUpload.ts +71 -0
  249. package/ui/src/pages/peekFilters.test.ts +29 -0
  250. package/ui/src/pages/peekFilters.ts +13 -0
  251. package/ui/src/pages/peekMedia.test.ts +58 -0
  252. package/ui/src/pages/peekMedia.ts +148 -0
  253. package/ui/src/pages/sessionAutoTitle.test.ts +128 -0
  254. package/ui/src/pages/sessionAutoTitle.ts +106 -0
  255. package/ui/src/stores/settings.ts +58 -0
  256. package/ui/src/styles/globals.css +223 -0
  257. package/ui/src/vite-env.d.ts +8 -0
  258. package/ui/tailwind.config.js +106 -0
  259. package/ui/tsconfig.json +32 -0
  260. package/ui/vite.config.ts +37 -0
@@ -0,0 +1,581 @@
1
+ import { useEffect, useState } from 'react'
2
+ import {
3
+ Activity,
4
+ Clock,
5
+ Zap,
6
+ AlertTriangle,
7
+ TrendingUp,
8
+ RefreshCw,
9
+ Server,
10
+ } from 'lucide-react'
11
+ import { Button } from '@/components/ui/button'
12
+ import { cn } from '@/lib/utils'
13
+ import {
14
+ getStats,
15
+ getLatencyDistribution,
16
+ getTokenUsage,
17
+ listProviders as listProviderCatalog,
18
+ type StatsAggregation,
19
+ type LatencyDistribution,
20
+ type TokenUsage,
21
+ type Provider,
22
+ } from '@/api/client'
23
+ import { buildDashboardTokenChartData, buildDashboardTokenMetadata } from './dashboardTokenUsage'
24
+ import {
25
+ BarChart,
26
+ Bar,
27
+ XAxis,
28
+ YAxis,
29
+ CartesianGrid,
30
+ Tooltip,
31
+ ResponsiveContainer,
32
+ ComposedChart,
33
+ Line,
34
+ } from 'recharts'
35
+
36
+ const MODEL_ROW_DEFAULT_LIMIT = 10
37
+
38
+ export function Dashboard() {
39
+ const [stats, setStats] = useState<StatsAggregation | null>(null)
40
+ const [latency, setLatency] = useState<LatencyDistribution | null>(null)
41
+ const [tokenUsage, setTokenUsage] = useState<TokenUsage | null>(null)
42
+ const [providers, setProviders] = useState<Provider[]>([])
43
+ const [isLoading, setIsLoading] = useState(true)
44
+ const [loadError, setLoadError] = useState<string | null>(null)
45
+ const [timeWindow, setTimeWindow] = useState('24h')
46
+ const [showAllModels, setShowAllModels] = useState(false)
47
+ const browserTimeZone = getBrowserTimeZone()
48
+
49
+ const loadData = async () => {
50
+ setIsLoading(true)
51
+ setLoadError(null)
52
+ try {
53
+ const [statsData, latencyData, tokenData, providersData] = await Promise.all([
54
+ getStats(timeWindow, { timeZone: browserTimeZone }),
55
+ getLatencyDistribution(timeWindow, { timeZone: browserTimeZone }),
56
+ getTokenUsage(timeWindow, { timeZone: browserTimeZone }),
57
+ listProviderCatalog(),
58
+ ])
59
+ setStats(statsData)
60
+ setLatency(latencyData)
61
+ setTokenUsage(tokenData)
62
+ setProviders(providersData)
63
+ } catch (error) {
64
+ console.error('Failed to load dashboard data:', error)
65
+ setLoadError('Failed to load dashboard data. Please retry.')
66
+ } finally {
67
+ setIsLoading(false)
68
+ }
69
+ }
70
+
71
+ useEffect(() => {
72
+ setShowAllModels(false)
73
+ }, [timeWindow])
74
+
75
+ useEffect(() => {
76
+ loadData()
77
+ const interval = setInterval(loadData, 30000)
78
+ return () => clearInterval(interval)
79
+ }, [timeWindow])
80
+
81
+ const errorRate = stats && stats.total > 0 ? ((stats.errors / stats.total) * 100).toFixed(1) : '0.0'
82
+
83
+ const histogramData = latency?.histogram
84
+ ? Object.entries(latency.histogram).map(([bucket, count]) => ({
85
+ bucket,
86
+ count,
87
+ percentage: latency.count > 0 ? Math.round((count / latency.count) * 100) : 0,
88
+ }))
89
+ : []
90
+
91
+ const tokenChartData = buildDashboardTokenChartData(tokenUsage)
92
+ const totalProviderModels = providers.reduce((sum, provider) => sum + provider.models.length, 0)
93
+ const enabledProviderModels = providers.reduce(
94
+ (sum, provider) => sum + provider.models.filter((model) => model.enabled !== false).length,
95
+ 0
96
+ )
97
+
98
+ const modelRows = Object.entries(stats?.byModel ?? {})
99
+ .map(([model, data]) => ({
100
+ model,
101
+ requests: data.count,
102
+ latencyMs: Math.round(data.avgLatencyMs),
103
+ tokens: data.tokens,
104
+ }))
105
+ .sort((a, b) => b.tokens - a.tokens)
106
+
107
+ const visibleModelRows = showAllModels ? modelRows : modelRows.slice(0, MODEL_ROW_DEFAULT_LIMIT)
108
+ const maxModelRequests = Math.max(1, ...modelRows.map((row) => row.requests))
109
+ const maxModelLatency = Math.max(1, ...modelRows.map((row) => row.latencyMs))
110
+ const maxModelTokens = Math.max(1, ...modelRows.map((row) => row.tokens))
111
+
112
+ const endpointRows = Object.entries(stats?.byEndpoint ?? {})
113
+ .map(([endpoint, data]) => ({
114
+ endpoint,
115
+ requests: data.count,
116
+ errors: data.errors,
117
+ errorRate: data.count > 0 ? (data.errors / data.count) * 100 : 0,
118
+ avgLatencyMs: Math.round(data.avgLatencyMs),
119
+ tokens: data.tokens,
120
+ }))
121
+ .sort((a, b) => {
122
+ if (b.errorRate !== a.errorRate) return b.errorRate - a.errorRate
123
+ return b.requests - a.requests
124
+ })
125
+
126
+ const {
127
+ estimatedCount: tokenEstimatedCount,
128
+ estimatedRate: tokenEstimatedRate,
129
+ granularityLabel: tokenGranularityLabel,
130
+ timeZoneLabel: tokenTimeZoneLabel,
131
+ } = buildDashboardTokenMetadata(tokenUsage, browserTimeZone)
132
+
133
+ return (
134
+ <div className="flex-1 flex flex-col h-full min-h-0">
135
+ <header className="sticky top-0 z-20 h-14 border-b border-border bg-background/95 backdrop-blur flex items-center px-6 gap-4 shrink-0">
136
+ <div className="flex items-center gap-2">
137
+ <Activity className="w-4 h-4 text-primary" />
138
+ <h2 className="font-mono font-semibold text-sm uppercase tracking-wider">Dashboard</h2>
139
+ </div>
140
+ <div className="flex-1" />
141
+
142
+ <div className="flex items-center gap-1 bg-secondary rounded-md p-1">
143
+ {['1h', '24h', '7d'].map((window) => (
144
+ <button
145
+ key={window}
146
+ onClick={() => setTimeWindow(window)}
147
+ className={cn(
148
+ 'px-3 py-1 text-xs font-mono rounded transition-colors',
149
+ timeWindow === window
150
+ ? 'bg-primary text-primary-foreground'
151
+ : 'text-muted-foreground hover:text-foreground'
152
+ )}
153
+ >
154
+ {window}
155
+ </button>
156
+ ))}
157
+ </div>
158
+
159
+ <Button variant="outline" size="sm" onClick={loadData} disabled={isLoading}>
160
+ <RefreshCw className={cn('w-3 h-3 mr-2', isLoading && 'animate-spin')} />
161
+ Refresh
162
+ </Button>
163
+ </header>
164
+
165
+ <div className="flex-1 min-h-0 p-6 space-y-6 overflow-auto">
166
+ {loadError && (
167
+ <div className="rounded-md border border-destructive/40 bg-destructive/10 px-3 py-2 text-sm text-destructive">
168
+ {loadError}
169
+ </div>
170
+ )}
171
+
172
+ <div className="grid grid-cols-2 lg:grid-cols-4 gap-4">
173
+ <MetricCard label="Total Requests" value={stats?.total ?? 0} icon={Zap} loading={isLoading} />
174
+ <MetricCard
175
+ label="Avg Latency"
176
+ value={stats?.avgLatencyMs ?? 0}
177
+ unit="ms"
178
+ icon={Clock}
179
+ loading={isLoading}
180
+ />
181
+ <MetricCard
182
+ label="Error Rate"
183
+ value={errorRate}
184
+ unit="%"
185
+ icon={AlertTriangle}
186
+ loading={isLoading}
187
+ variant={Number(errorRate) > 5 ? 'warning' : 'default'}
188
+ />
189
+ <MetricCard
190
+ label="Tokens/Hour"
191
+ value={stats?.tokensPerHour ?? 0}
192
+ icon={TrendingUp}
193
+ loading={isLoading}
194
+ />
195
+ </div>
196
+
197
+ <div className="grid grid-cols-2 gap-6">
198
+ <div className="panel">
199
+ <div className="panel-header">
200
+ <Clock className="w-4 h-4 text-muted-foreground" />
201
+ <span className="panel-title">Latency Distribution</span>
202
+ </div>
203
+ <div className="p-4">
204
+ <div className="grid grid-cols-4 gap-4 mb-6">
205
+ <PercentileCard label="P50" value={latency?.p50} />
206
+ <PercentileCard label="P95" value={latency?.p95} />
207
+ <PercentileCard label="P99" value={latency?.p99} />
208
+ <PercentileCard label="Max" value={latency?.max} />
209
+ </div>
210
+
211
+ {histogramData.length > 0 ? (
212
+ <div className="h-48">
213
+ <ResponsiveContainer width="100%" height="100%">
214
+ <BarChart data={histogramData} margin={{ top: 0, right: 0, left: -20, bottom: 0 }}>
215
+ <CartesianGrid strokeDasharray="3 3" stroke="hsl(var(--border))" />
216
+ <XAxis
217
+ dataKey="bucket"
218
+ tick={{ fontSize: 10, fill: 'hsl(var(--muted-foreground))' }}
219
+ axisLine={{ stroke: 'hsl(var(--border))' }}
220
+ />
221
+ <YAxis
222
+ tick={{ fontSize: 10, fill: 'hsl(var(--muted-foreground))' }}
223
+ axisLine={{ stroke: 'hsl(var(--border))' }}
224
+ />
225
+ <Tooltip
226
+ contentStyle={{
227
+ background: 'hsl(var(--background))',
228
+ border: '1px solid hsl(var(--border))',
229
+ borderRadius: '4px',
230
+ fontSize: '12px',
231
+ }}
232
+ labelStyle={{ color: 'hsl(var(--foreground))' }}
233
+ formatter={(value: number, _name, entry: { payload?: { percentage?: number } }) => {
234
+ const pct = entry?.payload?.percentage ?? 0
235
+ return [`${value.toLocaleString()} (${pct}%)`, 'Count']
236
+ }}
237
+ />
238
+ <Bar dataKey="count" fill="hsl(var(--primary))" radius={[2, 2, 0, 0]} />
239
+ </BarChart>
240
+ </ResponsiveContainer>
241
+ </div>
242
+ ) : (
243
+ <div className="h-48 flex items-center justify-center text-muted-foreground text-sm">
244
+ No latency samples in selected window
245
+ </div>
246
+ )}
247
+ </div>
248
+ </div>
249
+
250
+ <div className="panel">
251
+ <div className="panel-header">
252
+ <TrendingUp className="w-4 h-4 text-muted-foreground" />
253
+ <span className="panel-title">Token Usage Over Time</span>
254
+ </div>
255
+ <div className="p-4">
256
+ <div className="grid grid-cols-3 gap-4 mb-3">
257
+ <div className="text-center">
258
+ <p className="text-2xs font-mono uppercase text-muted-foreground">Total</p>
259
+ <p className="text-xl font-mono font-semibold tabular-nums">
260
+ {(tokenUsage?.totalTokens ?? 0).toLocaleString()}
261
+ </p>
262
+ </div>
263
+ <div className="text-center">
264
+ <p className="text-2xs font-mono uppercase text-muted-foreground">Requests</p>
265
+ <p className="text-xl font-mono font-semibold tabular-nums">
266
+ {(tokenUsage?.totalRequests ?? 0).toLocaleString()}
267
+ </p>
268
+ </div>
269
+ <div className="text-center">
270
+ <p className="text-2xs font-mono uppercase text-muted-foreground">Avg/Req</p>
271
+ <p className="text-xl font-mono font-semibold tabular-nums">
272
+ {Math.round(tokenUsage?.avgTokensPerRequest ?? 0).toLocaleString()}
273
+ </p>
274
+ </div>
275
+ </div>
276
+
277
+ <p className="text-2xs text-muted-foreground mb-4">
278
+ Estimated token entries: {tokenEstimatedCount.toLocaleString()} ({(tokenEstimatedRate * 100).toFixed(1)}%)
279
+ {' '}<span className="font-mono uppercase">{tokenGranularityLabel}</span>
280
+ {' '}<span className="font-mono uppercase">({tokenTimeZoneLabel})</span>
281
+ </p>
282
+
283
+ {tokenChartData.length > 0 ? (
284
+ <div className="h-48">
285
+ <ResponsiveContainer width="100%" height="100%">
286
+ <ComposedChart data={tokenChartData} margin={{ top: 0, right: 6, left: -20, bottom: 0 }}>
287
+ <CartesianGrid strokeDasharray="3 3" stroke="hsl(var(--border))" />
288
+ <XAxis
289
+ dataKey="date"
290
+ tick={{ fontSize: 10, fill: 'hsl(var(--muted-foreground))' }}
291
+ axisLine={{ stroke: 'hsl(var(--border))' }}
292
+ tickFormatter={(value: string) => formatTokenBucketLabel(value)}
293
+ />
294
+ <YAxis
295
+ yAxisId="requests"
296
+ tick={{ fontSize: 10, fill: 'hsl(var(--muted-foreground))' }}
297
+ axisLine={{ stroke: 'hsl(var(--border))' }}
298
+ allowDecimals={false}
299
+ />
300
+ <YAxis
301
+ yAxisId="tokens"
302
+ orientation="right"
303
+ tick={{ fontSize: 10, fill: 'hsl(var(--muted-foreground))' }}
304
+ axisLine={{ stroke: 'hsl(var(--border))' }}
305
+ />
306
+ <Tooltip
307
+ labelFormatter={(value) => formatTokenBucketTooltip(String(value), tokenTimeZoneLabel)}
308
+ content={({ active, label, payload }) => {
309
+ if (!active || !payload || payload.length === 0) return null
310
+ const row = payload[0]?.payload as
311
+ | { count?: number; tokens?: number; estimated?: number }
312
+ | undefined
313
+ return (
314
+ <div
315
+ style={{
316
+ background: 'hsl(var(--background))',
317
+ border: '1px solid hsl(var(--border))',
318
+ borderRadius: '4px',
319
+ fontSize: '12px',
320
+ padding: '8px',
321
+ }}
322
+ >
323
+ <div className="mb-1 font-mono">{formatTokenBucketTooltip(String(label), tokenTimeZoneLabel)}</div>
324
+ <div>Requests: {(row?.count ?? 0).toLocaleString()}</div>
325
+ <div>Total tokens: {(row?.tokens ?? 0).toLocaleString()}</div>
326
+ <div>Estimated entries: {(row?.estimated ?? 0).toLocaleString()}</div>
327
+ </div>
328
+ )
329
+ }}
330
+ />
331
+ <Bar yAxisId="requests" dataKey="count" name="Requests" fill="hsl(var(--muted))" radius={[2, 2, 0, 0]} />
332
+ <Line yAxisId="tokens" type="monotone" dataKey="tokens" name="Total tokens" stroke="hsl(var(--primary))" dot={false} strokeWidth={2} />
333
+ </ComposedChart>
334
+ </ResponsiveContainer>
335
+ </div>
336
+ ) : (
337
+ <div className="h-48 flex items-center justify-center text-muted-foreground text-sm">
338
+ No token usage data available
339
+ </div>
340
+ )}
341
+ </div>
342
+ </div>
343
+ </div>
344
+
345
+ <div className="panel">
346
+ <div className="panel-header">
347
+ <Zap className="w-4 h-4 text-muted-foreground" />
348
+ <span className="panel-title">Performance by Model</span>
349
+ <span className="text-2xs text-muted-foreground ml-auto">Sorted by tokens</span>
350
+ </div>
351
+ <div className="p-4">
352
+ {modelRows.length === 0 ? (
353
+ <div className="text-muted-foreground text-sm">No model-level request data available</div>
354
+ ) : (
355
+ <>
356
+ <div className="overflow-x-auto">
357
+ <table className="w-full text-sm">
358
+ <thead>
359
+ <tr className="border-b border-border text-2xs font-mono uppercase text-muted-foreground">
360
+ <th className="py-2 text-left">Model</th>
361
+ <th className="py-2 text-right">Requests</th>
362
+ <th className="py-2 text-right">Avg Latency (ms)</th>
363
+ <th className="py-2 text-right">Tokens</th>
364
+ </tr>
365
+ </thead>
366
+ <tbody>
367
+ {visibleModelRows.map((row) => (
368
+ <tr key={row.model} className="border-b border-border/50 last:border-0 align-top">
369
+ <td className="py-2 pr-3" title={row.model}>
370
+ <div className="max-w-[340px] truncate font-mono text-xs">{row.model}</div>
371
+ </td>
372
+ <td className="py-2 pl-3">
373
+ <div className="flex justify-end">
374
+ <MiniBar value={row.requests} max={maxModelRequests} text={row.requests.toLocaleString()} />
375
+ </div>
376
+ </td>
377
+ <td className="py-2 pl-3">
378
+ <div className="flex justify-end">
379
+ <MiniBar
380
+ value={row.latencyMs}
381
+ max={maxModelLatency}
382
+ text={row.latencyMs.toLocaleString()}
383
+ tone="warning"
384
+ />
385
+ </div>
386
+ </td>
387
+ <td className="py-2 pl-3">
388
+ <div className="flex justify-end">
389
+ <MiniBar value={row.tokens} max={maxModelTokens} text={row.tokens.toLocaleString()} tone="accent" />
390
+ </div>
391
+ </td>
392
+ </tr>
393
+ ))}
394
+ </tbody>
395
+ </table>
396
+ </div>
397
+
398
+ {modelRows.length > MODEL_ROW_DEFAULT_LIMIT && (
399
+ <div className="mt-3 flex justify-end">
400
+ <Button variant="outline" size="sm" onClick={() => setShowAllModels((prev) => !prev)}>
401
+ {showAllModels ? 'Show less' : `Show more (${modelRows.length - MODEL_ROW_DEFAULT_LIMIT})`}
402
+ </Button>
403
+ </div>
404
+ )}
405
+ </>
406
+ )}
407
+ </div>
408
+ </div>
409
+
410
+ <div className="panel">
411
+ <div className="panel-header">
412
+ <AlertTriangle className="w-4 h-4 text-muted-foreground" />
413
+ <span className="panel-title">Endpoint Quality</span>
414
+ <span className="text-2xs text-muted-foreground ml-auto">Sorted by error rate</span>
415
+ </div>
416
+ <div className="p-4">
417
+ {endpointRows.length === 0 ? (
418
+ <div className="text-muted-foreground text-sm">No endpoint-level request data available</div>
419
+ ) : (
420
+ <div className="overflow-x-auto">
421
+ <table className="w-full text-sm">
422
+ <thead>
423
+ <tr className="border-b border-border text-2xs font-mono uppercase text-muted-foreground">
424
+ <th className="py-2 text-left">Endpoint</th>
425
+ <th className="py-2 text-right">Requests</th>
426
+ <th className="py-2 text-right">Errors</th>
427
+ <th className="py-2 text-right">Error Rate</th>
428
+ <th className="py-2 text-right">Avg Latency</th>
429
+ <th className="py-2 text-right">Tokens</th>
430
+ </tr>
431
+ </thead>
432
+ <tbody>
433
+ {endpointRows.map((row) => (
434
+ <tr key={row.endpoint} className="border-b border-border/50 last:border-0">
435
+ <td className="py-2 pr-3 font-mono text-xs">{row.endpoint}</td>
436
+ <td className="py-2 text-right font-mono tabular-nums">{row.requests.toLocaleString()}</td>
437
+ <td className="py-2 text-right font-mono tabular-nums">{row.errors.toLocaleString()}</td>
438
+ <td className="py-2 text-right font-mono tabular-nums">{row.errorRate.toFixed(1)}%</td>
439
+ <td className="py-2 text-right font-mono tabular-nums">{row.avgLatencyMs.toLocaleString()}ms</td>
440
+ <td className="py-2 text-right font-mono tabular-nums">{row.tokens.toLocaleString()}</td>
441
+ </tr>
442
+ ))}
443
+ </tbody>
444
+ </table>
445
+ </div>
446
+ )}
447
+ </div>
448
+ </div>
449
+
450
+ <div className="panel">
451
+ <div className="panel-header">
452
+ <Server className="w-4 h-4 text-muted-foreground" />
453
+ <span className="panel-title">Providers & Models</span>
454
+ <span className="text-2xs text-muted-foreground ml-auto">
455
+ {providers.length} providers / {totalProviderModels} models
456
+ </span>
457
+ </div>
458
+ <div className="divide-y divide-border">
459
+ {providers.length === 0 && (
460
+ <div className="p-8 text-center text-muted-foreground">
461
+ <p>No providers configured</p>
462
+ <p className="text-xs mt-1">Import providers via CLI: waypoi provider import</p>
463
+ </div>
464
+ )}
465
+ {providers.map((provider) => (
466
+ <div key={provider.id} className="p-4">
467
+ <div className="flex items-center gap-4">
468
+ <div className={cn('status-dot', provider.enabled ? 'status-dot-live' : 'status-dot-down')} />
469
+ <div className="flex-1 min-w-0">
470
+ <p className="font-medium text-sm truncate">{provider.id}</p>
471
+ <p className="text-xs text-muted-foreground truncate font-mono">{provider.baseUrl}</p>
472
+ </div>
473
+ <div className="text-right">
474
+ <p className="text-sm font-mono tabular-nums">
475
+ {provider.models.filter((model) => model.enabled !== false).length}/{provider.models.length}
476
+ </p>
477
+ <p className="text-2xs text-muted-foreground">enabled models</p>
478
+ </div>
479
+ <div className="text-right">
480
+ <p className="text-xs font-mono uppercase px-2 py-0.5 rounded bg-secondary">
481
+ {provider.protocolRaw ?? provider.protocol}
482
+ </p>
483
+ </div>
484
+ </div>
485
+ </div>
486
+ ))}
487
+ </div>
488
+ <div className="px-4 pb-4 pt-2 text-xs text-muted-foreground">
489
+ Enabled models: {enabledProviderModels} / {totalProviderModels}
490
+ </div>
491
+ </div>
492
+ </div>
493
+ </div>
494
+ )
495
+ }
496
+
497
+ function formatTokenBucketLabel(value: string): string {
498
+ return value.includes('T') ? value.slice(11, 16) : value.slice(5)
499
+ }
500
+
501
+ function formatTokenBucketTooltip(value: string, timeZone: string): string {
502
+ const label = value.includes('T') ? `${value.replace('T', ' ')}` : value
503
+ return `${label} (${timeZone})`
504
+ }
505
+
506
+ function getBrowserTimeZone(): string {
507
+ try {
508
+ return Intl.DateTimeFormat().resolvedOptions().timeZone || 'UTC'
509
+ } catch {
510
+ return 'UTC'
511
+ }
512
+ }
513
+
514
+ interface MetricCardProps {
515
+ label: string
516
+ value: number | string
517
+ unit?: string
518
+ icon: React.ElementType
519
+ loading?: boolean
520
+ variant?: 'default' | 'warning'
521
+ }
522
+
523
+ function MetricCard({ label, value, unit, icon: Icon, loading, variant = 'default' }: MetricCardProps) {
524
+ return (
525
+ <div className={cn('metric-card', variant === 'warning' && 'border-warning/50')}>
526
+ <div className="flex items-center gap-2">
527
+ <Icon className={cn('w-4 h-4', variant === 'warning' ? 'text-warning' : 'text-muted-foreground')} />
528
+ <span className="metric-label">{label}</span>
529
+ </div>
530
+ {loading ? (
531
+ <div className="skeleton h-8 w-24 mt-1" />
532
+ ) : (
533
+ <p className="metric-value">
534
+ {typeof value === 'number' ? value.toLocaleString() : value}
535
+ {unit && <span className="metric-unit">{unit}</span>}
536
+ </p>
537
+ )}
538
+ </div>
539
+ )
540
+ }
541
+
542
+ function PercentileCard({ label, value }: { label: string; value: number | null | undefined }) {
543
+ return (
544
+ <div className="text-center">
545
+ <p className="text-2xs font-mono uppercase text-muted-foreground">{label}</p>
546
+ <p className="text-2xl font-mono font-semibold tabular-nums">
547
+ {value ?? '-'}
548
+ {value !== null && value !== undefined && <span className="text-sm text-muted-foreground ml-1">ms</span>}
549
+ </p>
550
+ </div>
551
+ )
552
+ }
553
+
554
+ function MiniBar({
555
+ value,
556
+ max,
557
+ text,
558
+ tone = 'default',
559
+ }: {
560
+ value: number
561
+ max: number
562
+ text: string
563
+ tone?: 'default' | 'warning' | 'accent'
564
+ }) {
565
+ const width = `${Math.max(8, Math.round((value / max) * 100))}%`
566
+ const barClass =
567
+ tone === 'warning'
568
+ ? 'bg-warning/30'
569
+ : tone === 'accent'
570
+ ? 'bg-primary/40'
571
+ : 'bg-secondary-foreground/20'
572
+
573
+ return (
574
+ <div className="w-[170px]">
575
+ <div className="relative h-6 rounded bg-secondary/40 overflow-hidden">
576
+ <div className={cn('absolute inset-y-0 left-0 rounded', barClass)} style={{ width }} />
577
+ <div className="relative h-full px-2 flex items-center justify-end font-mono text-xs tabular-nums">{text}</div>
578
+ </div>
579
+ </div>
580
+ )
581
+ }