fix: token tracking pipeline + premium usage dashboard redesign

Backend fixes:
- Fix totalTokens defaulting to 0 when API omits totalTokenCount (now computes prompt+completion)
- Fix streaming metadata extraction: unwrap response wrapper in Anthropic/Gemini handlers
- Fix Zod schemas: handle SQL SUM(null) with nullable().transform()
- Add is_estimated column migration to token_usage table

Frontend redesign:
- Rewrite Usage page with area charts (nivo/line), horizontal bar model ranking
- StatCard: add trend indicators (% vs previous period) and inline SVG sparklines
- Premium color palette, prompt/completion ratio bar, top models mini-list
- Live indicator with 15s auto-refresh

Design system migration:
- Standardize all components on Gemini Nexus tokens (rounded-xl, border-white/[0.06])
- Refine UI components, proxy tabs, cloud account list, provider groups
This commit is contained in:
Gemini Nexus Bot committed 2026-04-29 01:43:48 -03:00
1 parent b1c76f3b69
commit a648f2a281
44 files changed
+8920 -14566

No files matched your search

+6148 -12308
View File
File diff suppressed because it is too large. Load diff
+1
View File
@@ -107,6 +107,7 @@
"@nestjs/websockets": "^11.1.19", "@nestjs/websockets": "^11.1.19",
"@nivo/bar": "^0.99.0", "@nivo/bar": "^0.99.0",
"@nivo/core": "^0.99.0", "@nivo/core": "^0.99.0",
"@nivo/line": "^0.99.0",
"@nivo/pie": "^0.99.0", "@nivo/pie": "^0.99.0",
"@orpc/client": "^1.13.14", "@orpc/client": "^1.13.14",
"@orpc/server": "^1.13.14", "@orpc/server": "^1.13.14",
+8
View File
@@ -0,0 +1,8 @@
const fs = require('fs');
let file = fs.readFileSync('src/routes/proxy.tsx', 'utf8');
// The Proxy layout also uses heavy cards
file = file.replace(/<Card>/g, '<Card className="bg-background border border-white/5 shadow-none">');
fs.writeFileSync('src/routes/proxy.tsx', file);
+11
View File
@@ -0,0 +1,11 @@
const fs = require('fs');
let file = fs.readFileSync('src/routes/settings.tsx', 'utf8');
// The settings page uses standard cards that look bulky in minimal dark mode
file = file.replace(/<div className="flex flex-col gap-6">/g, '<div className="flex flex-col gap-2">');
file = file.replace(/<Card>/g, '<Card className="bg-transparent border-0 shadow-none rounded-none border-b border-white/5 last:border-0 pb-6">');
file = file.replace(/<CardHeader>/g, '<CardHeader className="px-0 pt-4">');
file = file.replace(/<CardContent>/g, '<CardContent className="px-0">');
fs.writeFileSync('src/routes/settings.tsx', file);
+69
View File
@@ -46,6 +46,9 @@ importers:
'@nivo/core': '@nivo/core':
specifier: ^0.99.0 specifier: ^0.99.0
version: 0.99.0([email protected]([email protected]))([email protected]) version: 0.99.0([email protected]([email protected]))([email protected])
'@nivo/line':
specifier: ^0.99.0
version: 0.99.0([email protected]([email protected]))([email protected])
'@nivo/pie': '@nivo/pie':
specifier: ^0.99.0 specifier: ^0.99.0
version: 0.99.0([email protected]([email protected]))([email protected]) version: 0.99.0([email protected]([email protected]))([email protected])
@@ -1641,6 +1644,11 @@ packages:
peerDependencies: peerDependencies:
react: ^16.14 || ^17.0 || ^18.0 || ^19.0 react: ^16.14 || ^17.0 || ^18.0 || ^19.0
'@nivo/[email protected]':
resolution: {integrity: sha512-bAqTXSjpnpcGMs341qWFUi7hJTqQiNoSeJHsYPuPS3icuXPcp3WETQH+zRZACeEF79ZigeOWCW+dzODgne1y9w==}
peerDependencies:
react: ^16.14 || ^17.0 || ^18.0 || ^19.0
'@nivo/[email protected]': '@nivo/[email protected]':
resolution: {integrity: sha512-zUbo8UdLndp2RMljrOqitAKKEnl7YypkJrOzjKLk8jQGU7qqUKtgFoJIPhiBsvNPs3xtX2KwgtS1+JKNTNns7A==} resolution: {integrity: sha512-zUbo8UdLndp2RMljrOqitAKKEnl7YypkJrOzjKLk8jQGU7qqUKtgFoJIPhiBsvNPs3xtX2KwgtS1+JKNTNns7A==}
peerDependencies: peerDependencies:
@@ -1664,6 +1672,11 @@ packages:
peerDependencies: peerDependencies:
react: ^16.14 || ^17.0 || ^18.0 || ^19.0 react: ^16.14 || ^17.0 || ^18.0 || ^19.0
'@nivo/[email protected]':
resolution: {integrity: sha512-KfmMdidbYzhiUCki1FG4X4nHEFT4loK8G5bMBnmCl9U+S78W+gvkfrgD2Aoqp/Q9yKQvr3Y8UcZKSFZnn3HgjQ==}
peerDependencies:
react: ^16.14 || ^17.0 || ^18.0 || ^19.0
'@nodelib/[email protected]': '@nodelib/[email protected]':
resolution: {integrity: sha512-vq24Bq3ym5HEQm2NKCr3yXDwjc7vTsEThRDnkp2DK9p1uqLR+DHurm/NOTo0KG7HYHU7eppKZj3MyqYuMBf62g==} resolution: {integrity: sha512-vq24Bq3ym5HEQm2NKCr3yXDwjc7vTsEThRDnkp2DK9p1uqLR+DHurm/NOTo0KG7HYHU7eppKZj3MyqYuMBf62g==}
engines: {node: '>= 8'} engines: {node: '>= 8'}
@@ -3505,6 +3518,9 @@ packages:
'@types/[email protected]': '@types/[email protected]':
resolution: {integrity: sha512-iO90scth9WAbmgv7ogoq57O9YpKmFBbmoEoCHDB2xMBY0+/KVrqAaCDyCE16dUspeOvIxFFRI+0sEtqDqy2b4A==} resolution: {integrity: sha512-iO90scth9WAbmgv7ogoq57O9YpKmFBbmoEoCHDB2xMBY0+/KVrqAaCDyCE16dUspeOvIxFFRI+0sEtqDqy2b4A==}
'@types/[email protected]':
resolution: {integrity: sha512-ZMaSKu4THYCU6sV64Lhg6qjf1orxBthaC161plr5KuPHo3CNm8DTHiLw/5Eq2b6TsNP0W0iJrUOFscY6Q450Hw==}
'@types/[email protected]': '@types/[email protected]':
resolution: {integrity: sha512-mLxrC1MSWupOSncXN/HOlWUAAIffAEBaI4+PKy2uMPsKe4FNZlk7qrbTjmzJXITQQqBHivaks4Td18azgqnotA==} resolution: {integrity: sha512-mLxrC1MSWupOSncXN/HOlWUAAIffAEBaI4+PKy2uMPsKe4FNZlk7qrbTjmzJXITQQqBHivaks4Td18azgqnotA==}
@@ -4593,6 +4609,10 @@ packages:
resolution: {integrity: sha512-zg/chbXyeBtMQ1LbD/WSoW2DpC3I0mpmPdW+ynRTj/x2DAWYrIY7qeZIHidozwV24m4iavr15lNwIwLxRmOxhA==} resolution: {integrity: sha512-zg/chbXyeBtMQ1LbD/WSoW2DpC3I0mpmPdW+ynRTj/x2DAWYrIY7qeZIHidozwV24m4iavr15lNwIwLxRmOxhA==}
engines: {node: '>=12'} engines: {node: '>=12'}
[email protected]:
resolution: {integrity: sha512-mdjtIZ1XLAM8bm/hx3WwjfHt6Sggek7qH043O8KEjDXN40xi3vx/6pYSVTwLjEgiXQTbvaouWKynLBiUZ6SK6A==}
engines: {node: '>=12'}
[email protected]: [email protected]:
resolution: {integrity: sha512-J0piedu6Z8iB6TbIGfZgDzfXxUFN3qQRMofy2oPdXzQibYGqPB/9iMcxr/TGalU+2RsyDO+U4f33id8tbnSRMQ==} resolution: {integrity: sha512-J0piedu6Z8iB6TbIGfZgDzfXxUFN3qQRMofy2oPdXzQibYGqPB/9iMcxr/TGalU+2RsyDO+U4f33id8tbnSRMQ==}
@@ -4702,6 +4722,9 @@ packages:
resolution: {integrity: sha512-8QmQKqEASLd5nx0U1B1okLElbUuuttJ/AnYmRXbbbGDWh6uS208EjD4Xqq/I9wK7u0v6O08XhTWnt5XtEbR6Dg==} resolution: {integrity: sha512-8QmQKqEASLd5nx0U1B1okLElbUuuttJ/AnYmRXbbbGDWh6uS208EjD4Xqq/I9wK7u0v6O08XhTWnt5XtEbR6Dg==}
engines: {node: '>= 0.4'} engines: {node: '>= 0.4'}
[email protected]:
resolution: {integrity: sha512-AGrQ4QSgssa1NGmWmLPqN5NY2KajF5MqxetNEO+o0n3ZwZZeTmt7bBnvzHWrmkZFxGgr4HdyFgelzgi06otLuQ==}
[email protected]: [email protected]:
resolution: {integrity: sha512-ZySD7Nf91aLB0RxL4KGrKHBXl7Eds1DAmEdcoVawXnLD7SDhpNgtuII2aAkg7a7QS41jxPSZ17p4VdGnMHk3MQ==} resolution: {integrity: sha512-ZySD7Nf91aLB0RxL4KGrKHBXl7Eds1DAmEdcoVawXnLD7SDhpNgtuII2aAkg7a7QS41jxPSZ17p4VdGnMHk3MQ==}
engines: {node: '>=0.4.0'} engines: {node: '>=0.4.0'}
@@ -7618,6 +7641,9 @@ packages:
resolution: {integrity: sha512-CHhPh+UNHD2GTXNYhPWLnU8ONHdI+5DI+4EYIAOaiD63rHeYlZvyh8P+in5999TTSFgUYuKUAjzRI4mdh/p+2A==} resolution: {integrity: sha512-CHhPh+UNHD2GTXNYhPWLnU8ONHdI+5DI+4EYIAOaiD63rHeYlZvyh8P+in5999TTSFgUYuKUAjzRI4mdh/p+2A==}
engines: {node: '>=8.0'} engines: {node: '>=8.0'}
[email protected]:
resolution: {integrity: sha512-NS3levdsRIUOmiJ8FZWCP7LG3QpJyrs/TE0Zpf1yvZu8cAJJ6QMW92H1c7kWpdIHo8RvmLxN/o2JXTKHp74lUA==}
[email protected]: [email protected]:
resolution: {integrity: sha512-ZrT53oAKrtA4+YtBWPQbtPOxIbVDbxT0orcYERKd63VJTF13zPcgXTvD4843L8pcsI7M6MErt8QtON6lrB9tyA==} resolution: {integrity: sha512-ZrT53oAKrtA4+YtBWPQbtPOxIbVDbxT0orcYERKd63VJTF13zPcgXTvD4843L8pcsI7M6MErt8QtON6lrB9tyA==}
engines: {node: ^20.19.0 || >=22.12.0} engines: {node: ^20.19.0 || >=22.12.0}
@@ -10389,6 +10415,24 @@ snapshots:
transitivePeerDependencies: transitivePeerDependencies:
- react-dom - react-dom
'@nivo/[email protected]([email protected]([email protected]))([email protected])':
dependencies:
'@nivo/annotations': 0.99.0([email protected]([email protected]))([email protected])
'@nivo/axes': 0.99.0([email protected]([email protected]))([email protected])
'@nivo/colors': 0.99.0([email protected]([email protected]))([email protected])
'@nivo/core': 0.99.0([email protected]([email protected]))([email protected])
'@nivo/legends': 0.99.0([email protected]([email protected]))([email protected])
'@nivo/scales': 0.99.0
'@nivo/theming': 0.99.0([email protected])
'@nivo/tooltip': 0.99.0([email protected]([email protected]))([email protected])
'@nivo/voronoi': 0.99.0([email protected]([email protected]))([email protected])
'@react-spring/web': 10.0.3([email protected]([email protected]))([email protected])
'@types/d3-shape': 3.1.8
d3-shape: 3.2.0
react: 19.2.5
transitivePeerDependencies:
- react-dom
'@nivo/[email protected]([email protected]([email protected]))([email protected])': '@nivo/[email protected]([email protected]([email protected]))([email protected])':
dependencies: dependencies:
'@nivo/arcs': 0.99.0([email protected]([email protected]))([email protected]) '@nivo/arcs': 0.99.0([email protected]([email protected]))([email protected])
@@ -10438,6 +10482,19 @@ snapshots:
transitivePeerDependencies: transitivePeerDependencies:
- react-dom - react-dom
'@nivo/[email protected]([email protected]([email protected]))([email protected])':
dependencies:
'@nivo/core': 0.99.0([email protected]([email protected]))([email protected])
'@nivo/theming': 0.99.0([email protected])
'@nivo/tooltip': 0.99.0([email protected]([email protected]))([email protected])
'@types/d3-delaunay': 6.0.4
'@types/d3-scale': 4.0.9
d3-delaunay: 6.0.4
d3-scale: 4.0.2
react: 19.2.5
transitivePeerDependencies:
- react-dom
'@nodelib/[email protected]': '@nodelib/[email protected]':
dependencies: dependencies:
'@nodelib/fs.stat': 2.0.5 '@nodelib/fs.stat': 2.0.5
@@ -12378,6 +12435,8 @@ snapshots:
'@types/[email protected]': {} '@types/[email protected]': {}
'@types/[email protected]': {}
'@types/[email protected]': {} '@types/[email protected]': {}
'@types/[email protected]': '@types/[email protected]':
@@ -13684,6 +13743,10 @@ snapshots:
[email protected]: {} [email protected]: {}
[email protected]:
dependencies:
delaunator: 5.1.0
[email protected]: {} [email protected]: {}
[email protected]: {} [email protected]: {}
@@ -13790,6 +13853,10 @@ snapshots:
has-property-descriptors: 1.0.2 has-property-descriptors: 1.0.2
object-keys: 1.1.1 object-keys: 1.1.1
[email protected]:
dependencies:
robust-predicates: 3.0.3
[email protected]: {} [email protected]: {}
[email protected]: {} [email protected]: {}
@@ -16781,6 +16848,8 @@ snapshots:
sprintf-js: 1.1.3 sprintf-js: 1.1.3
optional: true optional: true
[email protected]: {}
[email protected]: [email protected]:
dependencies: dependencies:
'@oxc-project/types': 0.127.0 '@oxc-project/types': 0.127.0
+1 -1
View File
@@ -67,7 +67,7 @@ function AccountProxyItem({ account }: { account: CloudAccount }) {
/> />
<div className="w-16"> <div className="w-16">
{proxySaved && ( {proxySaved && (
<span className="text-[10px] whitespace-nowrap text-green-500"> <span className="text-[10px] whitespace-nowrap text-emerald-400">
{t('cloud.card.proxySaved')} {t('cloud.card.proxySaved')}
</span> </span>
)} )}
+16 -16
View File
@@ -58,9 +58,9 @@ const MODEL_DISPLAY_REPLACEMENTS: Array<[string, string]> = [
]; ];
const QUOTA_TEXT_COLOR_CLASS_BY_STATUS: Record<QuotaStatus, string> = { const QUOTA_TEXT_COLOR_CLASS_BY_STATUS: Record<QuotaStatus, string> = {
high: 'text-green-500', high: 'text-emerald-400',
medium: 'text-yellow-500', medium: 'text-amber-400',
low: 'text-red-500', low: 'text-rose-400',
}; };
const QUOTA_BAR_COLOR_CLASS_BY_STATUS: Record<QuotaStatus, string> = { const QUOTA_BAR_COLOR_CLASS_BY_STATUS: Record<QuotaStatus, string> = {
@@ -240,12 +240,12 @@ export function CloudAccountCard({
<span className="text-muted-foreground/70 text-[10px] font-bold tracking-wider uppercase"> <span className="text-muted-foreground/70 text-[10px] font-bold tracking-wider uppercase">
{title} {title}
</span> </span>
<div className="bg-border/50 h-[1px] flex-1" /> <div className="h-[1px] flex-1 bg-white/[0.04]" />
</div> </div>
{models.map(([modelName, info]) => ( {models.map(([modelName, info]) => (
<div <div
key={modelName} key={modelName}
className="group/item hover:bg-muted/60 hover:border-border/60 grid grid-cols-[minmax(0,1fr)_auto] items-start gap-3 rounded-lg border border-transparent px-2 py-1.5 text-sm transition-all" className="group/item grid grid-cols-[minmax(0,1fr)_auto] items-start gap-3 rounded-lg border border-transparent px-2 py-1.5 text-[13px] transition-all hover:bg-white/[0.03]"
> >
<span <span
className="text-muted-foreground group-hover/item:text-foreground min-w-0 truncate font-semibold" className="text-muted-foreground group-hover/item:text-foreground min-w-0 truncate font-semibold"
@@ -291,7 +291,7 @@ export function CloudAccountCard({
const providerGroupedQuotaSection = const providerGroupedQuotaSection =
providerStats && providerStats.visibleModels > 0 ? ( providerStats && providerStats.visibleModels > 0 ? (
<> <>
<div className="bg-muted/40 flex items-center justify-between rounded-lg px-3 py-1.5 text-xs"> <div className="flex items-center justify-between rounded-lg bg-white/[0.03] px-3 py-1.5 text-xs">
<span className="font-medium">{t('settings.providerGroupings.overall')}</span> <span className="font-medium">{t('settings.providerGroupings.overall')}</span>
<div className="flex items-center gap-2"> <div className="flex items-center gap-2">
<span <span
@@ -343,7 +343,7 @@ export function CloudAccountCard({
return ( return (
<Card <Card
className={`group bg-card hover:border-primary/40 flex h-full flex-col overflow-hidden border transition-all duration-200 hover:shadow-sm ${isSelected ? 'ring-primary border-primary/50 ring-2' : ''}`} className={`group flex h-full flex-col overflow-hidden rounded-xl border border-white/[0.06] bg-card transition-all duration-200 hover:border-white/[0.12] ${isSelected ? 'ring-accent/30 border-accent/40 ring-2' : ''}`}
> >
<CardHeader className="relative flex flex-row items-center gap-4 space-y-0 pb-2"> <CardHeader className="relative flex flex-row items-center gap-4 space-y-0 pb-2">
{onToggleSelection && ( {onToggleSelection && (
@@ -365,7 +365,7 @@ export function CloudAccountCard({
className="bg-muted h-10 w-10 rounded-full border" className="bg-muted h-10 w-10 rounded-full border"
/> />
) : ( ) : (
<div className="bg-primary/10 text-primary flex h-10 w-10 items-center justify-center rounded-full border"> <div className="flex h-10 w-10 items-center justify-center rounded-full bg-white/[0.06] text-muted-foreground">
{account.name?.[0]?.toUpperCase() || 'A'} {account.name?.[0]?.toUpperCase() || 'A'}
</div> </div>
)} )}
@@ -493,7 +493,7 @@ export function CloudAccountCard({
{account.provider.toUpperCase()} {account.provider.toUpperCase()}
</Badge> </Badge>
{account.is_active && ( {account.is_active && (
<Badge variant="default" className="bg-green-500 text-xs hover:bg-green-600"> <Badge variant="default" className="bg-emerald-500/15 text-emerald-400 border-emerald-500/20 text-xs hover:bg-emerald-500/20">
{t('cloud.card.active')} {t('cloud.card.active')}
</Badge> </Badge>
)} )}
@@ -514,7 +514,7 @@ export function CloudAccountCard({
)} )}
{account.is_active ? ( {account.is_active ? (
<Button variant="ghost" size="sm" disabled className="text-green-600 opacity-100"> <Button variant="ghost" size="sm" disabled className="text-emerald-400 opacity-100">
<Power className="mr-1 h-3 w-3" /> <Power className="mr-1 h-3 w-3" />
{t('cloud.card.active')} {t('cloud.card.active')}
</Button> </Button>
@@ -551,7 +551,7 @@ export function CloudAccountCard({
</div> </div>
</CardContent> </CardContent>
<CardFooter className="bg-muted/20 text-muted-foreground justify-center border-t p-2 px-4 text-xs"> <CardFooter className="justify-center border-t border-white/[0.04] p-2 px-4 text-xs text-muted-foreground">
<span> <span>
{t('cloud.card.used')}{' '} {t('cloud.card.used')}{' '}
{formatDistanceToNow(account.last_used * 1000, { addSuffix: true })} {formatDistanceToNow(account.last_used * 1000, { addSuffix: true })}
@@ -613,7 +613,7 @@ export function CompactCloudAccountCard({
); );
return ( return (
<div className="group bg-card hover:border-primary/40 flex items-center gap-3 rounded-lg border px-3 py-2 transition-all duration-200"> <div className="group flex items-center gap-3 rounded-xl border border-white/[0.06] bg-card px-3 py-2 transition-all duration-200 hover:border-white/[0.12]">
{account.avatar_url ? ( {account.avatar_url ? (
<img <img
src={account.avatar_url} src={account.avatar_url}
@@ -621,14 +621,14 @@ export function CompactCloudAccountCard({
className="bg-muted h-7 w-7 rounded-full border" className="bg-muted h-7 w-7 rounded-full border"
/> />
) : ( ) : (
<div className="bg-primary/10 text-primary flex h-7 w-7 items-center justify-center rounded-full border text-xs font-semibold"> <div className="flex h-7 w-7 items-center justify-center rounded-full bg-white/[0.06] text-xs font-semibold text-muted-foreground">
{account.name?.[0]?.toUpperCase() || 'A'} {account.name?.[0]?.toUpperCase() || 'A'}
</div> </div>
)} )}
<div className="min-w-0 flex-1"> <div className="min-w-0 flex-1">
<div className="flex items-center gap-2"> <div className="flex items-center gap-2">
<span className="truncate text-sm font-semibold"> <span className="truncate text-[13px] font-semibold">
{account.name || t('cloud.card.unknown')} {account.name || t('cloud.card.unknown')}
</span> </span>
<Badge <Badge
@@ -644,7 +644,7 @@ export function CompactCloudAccountCard({
{account.is_active && ( {account.is_active && (
<Badge <Badge
variant="default" variant="default"
className="shrink-0 bg-green-500 text-[10px] hover:bg-green-600" className="shrink-0 bg-emerald-500/15 text-emerald-400 border-emerald-500/20 text-[10px] hover:bg-emerald-500/20"
> >
{t('cloud.card.active')} {t('cloud.card.active')}
</Badge> </Badge>
@@ -756,7 +756,7 @@ export function CompactCloudAccountCard({
variant="ghost" variant="ghost"
size="sm" size="sm"
disabled disabled
className="h-7 text-xs text-green-600 opacity-100" className="h-7 text-xs text-emerald-400 opacity-100"
> >
<Power className="mr-1 h-3 w-3" /> <Power className="mr-1 h-3 w-3" />
{t('cloud.card.active')} {t('cloud.card.active')}
+22 -22
View File
@@ -85,10 +85,10 @@ import { shouldAutoSubmitGoogleAuthCode } from '@/utils/googleAuthSubmission';
export type GridLayout = 'auto' | '2-col' | '3-col' | 'list' | 'compact'; export type GridLayout = 'auto' | '2-col' | '3-col' | 'list' | 'compact';
const GRID_LAYOUT_CLASSES: Record<GridLayout, string> = { const GRID_LAYOUT_CLASSES: Record<GridLayout, string> = {
auto: 'grid gap-4 md:grid-cols-2 xl:grid-cols-3', auto: 'grid gap-3 md:grid-cols-2 xl:grid-cols-3',
'2-col': 'grid gap-4 grid-cols-2', '2-col': 'grid gap-3 grid-cols-2',
'3-col': 'grid gap-4 grid-cols-3', '3-col': 'grid gap-3 grid-cols-3',
list: 'grid gap-4 grid-cols-1', list: 'grid gap-3 grid-cols-1',
compact: 'flex flex-col gap-2', compact: 'flex flex-col gap-2',
}; };
@@ -99,9 +99,9 @@ const GLOBAL_QUOTA_BAR_COLOR_CLASS_BY_STATUS: Record<QuotaStatus, string> = {
}; };
const GLOBAL_QUOTA_TEXT_COLOR_CLASS_BY_STATUS: Record<QuotaStatus, string> = { const GLOBAL_QUOTA_TEXT_COLOR_CLASS_BY_STATUS: Record<QuotaStatus, string> = {
high: 'text-emerald-600 dark:text-emerald-400', high: 'text-emerald-400',
medium: 'text-amber-600 dark:text-amber-400', medium: 'text-amber-400',
low: 'text-rose-600 dark:text-rose-400', low: 'text-rose-400',
}; };
export function CloudAccountList() { export function CloudAccountList() {
@@ -663,34 +663,34 @@ export function CloudAccountList() {
return ( return (
<div className="space-y-5 pb-20"> <div className="space-y-5 pb-20">
<div className="rounded-lg border border-white/5 p-5"> <div className="rounded-xl border border-white/[0.06] p-5">
<div className="flex flex-wrap items-start justify-between gap-4"> <div className="flex flex-wrap items-start justify-between gap-4">
<div className="flex shrink-0 flex-col gap-1"> <div className="flex shrink-0 flex-col gap-1">
<h2 className="text-2xl font-bold tracking-tight">{t('cloud.title')}</h2> <h2 className="text-lg font-semibold tracking-tight">{t('cloud.title')}</h2>
<p className="text-muted-foreground max-w-2xl">{t('cloud.description')}</p> <p className="text-muted-foreground max-w-2xl text-[13px]">{t('cloud.description')}</p>
</div> </div>
<div className="flex flex-wrap gap-2"> <div className="flex flex-wrap gap-2">
<div className="bg-muted/10 rounded-md border border-white/5 px-3 py-2"> <div className="rounded-lg border border-white/[0.06] bg-white/[0.02] px-3 py-2">
<div className="text-muted-foreground text-[11px] uppercase"> <div className="text-muted-foreground text-[11px] uppercase">
{t('cloud.card.actions')} {t('cloud.card.actions')}
</div> </div>
<div className="text-base font-semibold">{totalAccounts}</div> <div className="text-sm font-semibold tabular-nums">{totalAccounts}</div>
</div> </div>
<div className="bg-muted/10 rounded-md border border-white/5 px-3 py-2"> <div className="rounded-lg border border-white/[0.06] bg-white/[0.02] px-3 py-2">
<div className="text-muted-foreground text-[11px] uppercase"> <div className="text-muted-foreground text-[11px] uppercase">
{t('cloud.card.active')} {t('cloud.card.active')}
</div> </div>
<div className="text-base font-semibold text-emerald-600">{activeAccounts}</div> <div className="text-sm font-semibold text-emerald-400">{activeAccounts}</div>
</div> </div>
<div className="bg-muted/10 rounded-md border border-white/5 px-3 py-2"> <div className="rounded-lg border border-white/[0.06] bg-white/[0.02] px-3 py-2">
<div className="text-muted-foreground text-[11px] uppercase"> <div className="text-muted-foreground text-[11px] uppercase">
{t('cloud.card.rateLimited')} {t('cloud.card.rateLimited')}
</div> </div>
<div className="text-base font-semibold text-rose-600">{rateLimitedAccounts}</div> <div className="text-sm font-semibold text-rose-400">{rateLimitedAccounts}</div>
</div> </div>
{/* Global Quota */} {/* Global Quota */}
{overallQuotaPercentage !== null && ( {overallQuotaPercentage !== null && (
<div className="bg-muted/10 rounded-md border border-white/5 px-3 py-2"> <div className="rounded-lg border border-white/[0.06] bg-white/[0.02] px-3 py-2">
<div className="text-muted-foreground text-[11px] uppercase"> <div className="text-muted-foreground text-[11px] uppercase">
{t('cloud.globalQuota')} {t('cloud.globalQuota')}
</div> </div>
@@ -713,13 +713,13 @@ export function CloudAccountList() {
</div> </div>
</div> </div>
<div className="bg-card flex flex-wrap items-center gap-2 rounded-lg border p-3"> <div className="flex flex-wrap items-center gap-2 rounded-xl border border-white/[0.06] bg-card p-2.5">
<div className="bg-muted/50 flex items-center gap-2 rounded-md border px-3 py-2"> <div className="flex items-center gap-2 rounded-lg border border-white/[0.06] px-3 py-1.5">
<div className="flex items-center gap-2"> <div className="flex items-center gap-2">
<Zap <Zap
className={`h-4 w-4 ${autoSwitchEnabled ? 'fill-yellow-500 text-yellow-500' : 'text-muted-foreground'}`} className={`h-4 w-4 ${autoSwitchEnabled ? 'fill-yellow-500 text-yellow-500' : 'text-muted-foreground'}`}
/> />
<Label htmlFor="auto-switch" className="cursor-pointer text-sm font-medium"> <Label htmlFor="auto-switch" className="cursor-pointer text-[13px] font-medium">
{t('cloud.autoSwitch')} {t('cloud.autoSwitch')}
</Label> </Label>
</div> </div>
@@ -1108,7 +1108,7 @@ export function CloudAccountList() {
)} )}
{sortedAccounts.length === 0 && ( {sortedAccounts.length === 0 && (
<div className="text-muted-foreground bg-muted/20 col-span-full rounded-lg border border-dashed py-14 text-center"> <div className="text-muted-foreground col-span-full rounded-xl border border-dashed border-white/[0.1] py-14 text-center">
<Cloud className="mx-auto mb-3 h-10 w-10 opacity-40" /> <Cloud className="mx-auto mb-3 h-10 w-10 opacity-40" />
<div className="text-sm">{t('cloud.list.noAccounts')}</div> <div className="text-sm">{t('cloud.list.noAccounts')}</div>
</div> </div>
@@ -1117,7 +1117,7 @@ export function CloudAccountList() {
{/* Batch Action Bar */} {/* Batch Action Bar */}
{selectedIds.size > 0 && ( {selectedIds.size > 0 && (
<div className="bg-card animate-in fade-in slide-in-from-bottom-4 fixed bottom-6 left-1/2 z-50 flex -translate-x-1/2 items-center gap-4 rounded-full border px-6 py-2 shadow-lg"> <div className="bg-card animate-in fade-in slide-in-from-bottom-4 fixed bottom-6 left-1/2 z-50 flex -translate-x-1/2 items-center gap-4 rounded-full border border-white/[0.1] px-6 py-2 shadow-2xl shadow-black/30">
<div className="flex items-center gap-2 border-r pr-4"> <div className="flex items-center gap-2 border-r pr-4">
<span className="text-sm font-semibold"> <span className="text-sm font-semibold">
{t('cloud.batch.selected', { count: selectedIds.size })} {t('cloud.batch.selected', { count: selectedIds.size })}
+1 -1
View File
@@ -343,7 +343,7 @@ export function IdentityProfileDialog({ account, open, onOpenChange }: IdentityP
{!showLoadingPlaceholder {!showLoadingPlaceholder
? history.map((version) => ( ? history.map((version) => (
<div key={version.id} className="bg-muted/20 rounded-xl border p-3"> <div key={version.id} className="rounded-xl border border-white/[0.06] bg-white/[0.02] p-3">
<div className="mb-2 flex items-center justify-between gap-3"> <div className="mb-2 flex items-center justify-between gap-3">
<div className="min-w-0"> <div className="min-w-0">
<div className="truncate text-sm font-medium">{version.label}</div> <div className="truncate text-sm font-medium">{version.label}</div>
+2 -2
View File
@@ -184,7 +184,7 @@ export function ModelVisibilitySettings() {
return ( return (
<div <div
key={modelId} key={modelId}
className="hover:bg-muted/50 flex items-center space-x-3 rounded p-2" className="flex items-center space-x-3 rounded-lg p-2 hover:bg-white/[0.03] transition-colors"
> >
<Checkbox <Checkbox
id={`model-${modelId}`} id={`model-${modelId}`}
@@ -206,7 +206,7 @@ export function ModelVisibilitySettings() {
title={t('proxy.copy')} title={t('proxy.copy')}
> >
{copiedModel === modelId ? ( {copiedModel === modelId ? (
<CheckCircle className="h-4 w-4 text-green-500" /> <CheckCircle className="h-4 w-4 text-emerald-400" />
) : ( ) : (
<Copy className="h-4 w-4" /> <Copy className="h-4 w-4" />
)} )}
+7 -7
View File
@@ -37,12 +37,12 @@ export const ProviderGroup: React.FC<ProviderGroupProps> = ({
} }
return ( return (
<div className="border-border/60 overflow-hidden rounded-lg border"> <div className="overflow-hidden rounded-xl border border-white/[0.06]">
{/* Provider Header */} {/* Provider Header */}
<button <button
type="button" type="button"
onClick={onToggleCollapse} onClick={onToggleCollapse}
className="hover:bg-muted/60 flex w-full cursor-pointer items-center gap-2 px-3 py-2 text-sm transition-colors" className="flex w-full cursor-pointer items-center gap-2 px-3 py-2 text-[13px] transition-colors hover:bg-white/[0.03]"
> >
{isCollapsed ? ( {isCollapsed ? (
<ChevronRight className="text-muted-foreground h-3.5 w-3.5 shrink-0" /> <ChevronRight className="text-muted-foreground h-3.5 w-3.5 shrink-0" />
@@ -87,7 +87,7 @@ export const ProviderGroup: React.FC<ProviderGroupProps> = ({
</span> </span>
)} )}
</div> </div>
<div className="bg-muted h-1.5 w-16 overflow-hidden rounded-full"> <div className="h-1.5 w-16 overflow-hidden rounded-full bg-white/[0.06]">
<div <div
className={`h-full rounded-full transition-all duration-300 ${getQuotaBarColorClass(avgPercentage)}`} className={`h-full rounded-full transition-all duration-300 ${getQuotaBarColorClass(avgPercentage)}`}
style={{ width: `${clampQuotaPercentage(avgPercentage)}%` }} style={{ width: `${clampQuotaPercentage(avgPercentage)}%` }}
@@ -98,12 +98,12 @@ export const ProviderGroup: React.FC<ProviderGroupProps> = ({
{/* Individual model rows (shown when expanded) */} {/* Individual model rows (shown when expanded) */}
{!isCollapsed && ( {!isCollapsed && (
<div className="border-border/40 border-t"> <div className="border-t border-white/[0.06]">
{visibleModels.map((model, index) => ( {visibleModels.map((model, index) => (
<div <div
key={model.id} key={model.id}
className={`hover:bg-muted/60 grid grid-cols-[minmax(0,1fr)_auto] items-start gap-3 px-3 py-2 pl-9 text-sm transition-colors ${ className={`grid grid-cols-[minmax(0,1fr)_auto] items-start gap-3 px-3 py-2 pl-9 text-[13px] transition-colors hover:bg-white/[0.03] ${
index < visibleModels.length - 1 ? 'border-border/20 border-b' : '' index < visibleModels.length - 1 ? 'border-b border-white/[0.04]' : ''
}`} }`}
> >
<span className="text-muted-foreground min-w-0 truncate" title={model.id}> <span className="text-muted-foreground min-w-0 truncate" title={model.id}>
@@ -126,7 +126,7 @@ export const ProviderGroup: React.FC<ProviderGroupProps> = ({
<span className="text-muted-foreground text-[10px]">{leftLabel}</span> <span className="text-muted-foreground text-[10px]">{leftLabel}</span>
)} )}
</div> </div>
<div className="bg-muted h-1.5 w-24 overflow-hidden rounded-full"> <div className="h-1.5 w-24 overflow-hidden rounded-full bg-white/[0.06]">
<div <div
className={`h-full rounded-full transition-all duration-300 ${getQuotaBarColorClass(model.percentage)}`} className={`h-full rounded-full transition-all duration-300 ${getQuotaBarColorClass(model.percentage)}`}
style={{ width: `${clampQuotaPercentage(model.percentage)}%` }} style={{ width: `${clampQuotaPercentage(model.percentage)}%` }}
+156 -146
View File
@@ -1,146 +1,156 @@
import React from 'react'; import React from 'react';
import { useQuery, useMutation, useQueryClient } from '@tanstack/react-query'; import { useQuery, useMutation, useQueryClient } from '@tanstack/react-query';
import { isProcessRunning, startGeminiNexus, closeGeminiNexus } from '@/actions/process'; import { isProcessRunning, startGeminiNexus, closeGeminiNexus } from '@/actions/process';
import { Button } from '@/components/ui/button'; import { Button } from '@/components/ui/button';
import { cn } from '@/lib/utils'; import { cn } from '@/lib/utils';
import { Play, Square, Loader2, Power } from 'lucide-react'; import { Play, Square, Loader2, Power } from 'lucide-react';
import { useTranslation } from 'react-i18next'; import { useTranslation } from 'react-i18next';
import { Tooltip, TooltipContent, TooltipProvider, TooltipTrigger } from '@/components/ui/tooltip'; import { Tooltip, TooltipContent, TooltipProvider, TooltipTrigger } from '@/components/ui/tooltip';
interface StatusBarProps { interface StatusBarProps {
isCollapsed?: boolean; isCollapsed?: boolean;
} }
export const StatusBar: React.FC<StatusBarProps> = ({ isCollapsed = false }) => { export const StatusBar: React.FC<StatusBarProps> = ({ isCollapsed = false }) => {
const { t } = useTranslation(); const { t } = useTranslation();
const queryClient = useQueryClient(); const queryClient = useQueryClient();
const { data: isRunning, isLoading } = useQuery({ const { data: isRunning, isLoading } = useQuery({
queryKey: ['process', 'status'], queryKey: ['process', 'status'],
queryFn: isProcessRunning, queryFn: isProcessRunning,
refetchInterval: 30000, refetchInterval: 30000,
}); });
const startMutation = useMutation({ const startMutation = useMutation({
mutationFn: startGeminiNexus, mutationFn: startGeminiNexus,
onSuccess: () => { onSuccess: () => {
queryClient.invalidateQueries({ queryKey: ['process', 'status'] }); queryClient.invalidateQueries({ queryKey: ['process', 'status'] });
}, },
}); });
const stopMutation = useMutation({ const stopMutation = useMutation({
mutationFn: closeGeminiNexus, mutationFn: closeGeminiNexus,
onSuccess: () => { onSuccess: () => {
queryClient.invalidateQueries({ queryKey: ['process', 'status'] }); queryClient.invalidateQueries({ queryKey: ['process', 'status'] });
}, },
}); });
const handleToggle = () => { const handleToggle = () => {
if (isRunning) { if (isRunning) {
stopMutation.mutate(); stopMutation.mutate();
} else { } else {
startMutation.mutate(); startMutation.mutate();
} }
}; };
const isPending = startMutation.isPending || stopMutation.isPending; const isPending = startMutation.isPending || stopMutation.isPending;
// Collapsed View // Collapsed View
if (isCollapsed) { if (isCollapsed) {
return ( return (
<div className="flex justify-center py-2"> <div className="flex justify-center py-2">
<TooltipProvider> <TooltipProvider>
<Tooltip delayDuration={0}> <Tooltip delayDuration={0}>
<TooltipTrigger asChild> <TooltipTrigger asChild>
<Button <button
variant="ghost" onClick={handleToggle}
size="icon" disabled={isLoading || isPending}
onClick={handleToggle} className={cn(
disabled={isLoading || isPending} 'flex h-8 w-8 items-center justify-center rounded-lg transition-all duration-200',
className={cn( 'disabled:opacity-50 disabled:cursor-not-allowed',
'h-10 w-10 rounded-lg transition-all duration-300', isRunning
isRunning ? 'bg-emerald-500/10 text-emerald-400 hover:bg-emerald-500/20'
? 'bg-green-100/50 text-green-700 hover:bg-green-100 hover:text-green-800 dark:bg-green-900/30 dark:text-green-400 dark:hover:bg-green-900/50' : 'bg-white/[0.04] text-muted-foreground hover:bg-white/[0.08] hover:text-foreground',
: 'bg-red-100/50 text-red-700 hover:bg-red-100 hover:text-red-800 dark:bg-red-900/30 dark:text-red-400 dark:hover:bg-red-900/50', )}
)} >
> {isPending ? (
{isPending ? ( <Loader2 className="h-4 w-4 animate-spin" />
<Loader2 className="h-5 w-5 animate-spin" /> ) : (
) : ( <Power className="h-4 w-4" />
<Power className="h-5 w-5" /> )}
)} </button>
</Button> </TooltipTrigger>
</TooltipTrigger> <TooltipContent side="right" className="flex items-center gap-2 text-xs">
<TooltipContent side="right" className="flex items-center gap-2"> <div
<div className={cn(
className={cn('h-2 w-2 rounded-full', isRunning ? 'bg-green-500' : 'bg-red-500')} 'h-1.5 w-1.5 rounded-full',
/> isRunning ? 'bg-emerald-400' : 'bg-muted-foreground',
<p> )}
{isLoading />
? t('status.checking') <span>
: isRunning {isLoading
? t('status.running') ? t('status.checking')
: t('status.stopped')} : isRunning
</p> ? t('status.running')
</TooltipContent> : t('status.stopped')}
</Tooltip> </span>
</TooltipProvider> </TooltipContent>
</div> </Tooltip>
); </TooltipProvider>
} </div>
);
// Expanded View }
return (
<div // Expanded View
className={cn( return (
'flex items-center justify-between overflow-hidden rounded-lg px-3 py-2.5 text-sm font-medium ring-1 transition-colors ring-inset', <div
isRunning className={cn(
? 'bg-green-100 text-green-900 ring-green-200 dark:bg-green-900/20 dark:text-green-100 dark:ring-green-900/50' 'flex items-center justify-between rounded-lg px-3 py-2 transition-all duration-200',
: 'bg-red-100 text-red-900 ring-red-200 dark:bg-red-900/20 dark:text-red-100 dark:ring-red-900/50', isRunning
)} ? 'bg-emerald-500/[0.08]'
> : 'bg-white/[0.02]',
<div className="flex flex-1 items-start gap-3"> )}
{isLoading ? ( >
<Loader2 className="mt-0.5 h-3.5 w-3.5 shrink-0 animate-spin" /> <div className="flex items-center gap-2.5">
) : isRunning ? ( {isLoading ? (
<div className="mt-1.5 h-2.5 w-2.5 shrink-0 rounded-full bg-green-500 shadow-[0_0_8px_rgba(34,197,94,0.5)]" /> <Loader2 className="h-3 w-3 shrink-0 animate-spin text-muted-foreground" />
) : ( ) : (
<div className="mt-1.5 h-2.5 w-2.5 shrink-0 rounded-full bg-red-500 shadow-[0_0_8px_rgba(239,68,68,0.5)]" /> <div
)} className={cn(
<div className="flex flex-col gap-1 leading-none"> 'h-2 w-2 shrink-0 rounded-full transition-colors duration-300',
<span className="text-xs font-semibold tracking-wider uppercase opacity-80">Status</span> isRunning
<span className="text-sm leading-tight font-medium"> ? 'bg-emerald-400 shadow-[0_0_6px_rgba(16,185,129,0.4)]'
{isLoading : 'bg-muted-foreground/50',
? t('status.checking') )}
: isRunning />
? t('status.running') )}
: t('status.stopped')} <div className="flex flex-col">
</span> <span className="text-[11px] font-medium uppercase tracking-wider text-muted-foreground">
</div> Status
</div> </span>
<Button <span className={cn(
variant="ghost" 'text-xs font-medium leading-tight',
size="sm" isRunning ? 'text-emerald-400' : 'text-muted-foreground',
onClick={handleToggle} )}>
disabled={isLoading || isPending} {isLoading
className={cn( ? t('status.checking')
'ml-2 h-8 shrink-0 rounded-md border px-3 transition-all', : isRunning
isRunning ? t('status.running')
? 'border-green-200 bg-green-200/50 text-green-900 hover:bg-green-200 dark:border-green-800 dark:bg-green-800/30 dark:text-green-100 dark:hover:bg-green-800/50' : t('status.stopped')}
: 'border-red-200 bg-red-200/50 text-red-900 hover:bg-red-200 dark:border-red-800 dark:bg-red-800/30 dark:text-red-100 dark:hover:bg-red-800/50', </span>
)} </div>
> </div>
{isPending ? ( <Button
<Loader2 className="h-3.5 w-3.5 animate-spin" /> variant="ghost"
) : isRunning ? ( size="sm"
<Square className="h-3.5 w-3.5 fill-current" /> onClick={handleToggle}
) : ( disabled={isLoading || isPending}
<Play className="h-3.5 w-3.5 fill-current" /> className={cn(
)} 'h-7 gap-1.5 rounded-md px-2.5 text-xs',
<span className="ml-2 font-semibold"> isRunning
{isRunning ? t('action.stop') : t('action.start')} ? 'text-emerald-400 hover:bg-emerald-500/10'
</span> : 'text-muted-foreground hover:bg-white/[0.06]',
</Button> )}
</div> >
); {isPending ? (
}; <Loader2 className="h-3 w-3 animate-spin" />
) : isRunning ? (
<Square className="h-3 w-3 fill-current" />
) : (
<Play className="h-3 w-3 fill-current" />
)}
<span>{isRunning ? t('action.stop') : t('action.start')}</span>
</Button>
</div>
);
};
+1 -1
View File
@@ -35,7 +35,7 @@ export default function DragWindowRegion({ title }: DragWindowRegionProps) {
<div className="flex w-screen items-stretch justify-between"> <div className="flex w-screen items-stretch justify-between">
<div className="draglayer w-full"> <div className="draglayer w-full">
{title && !isMacOS && ( {title && !isMacOS && (
<div className="flex flex-1 p-2 text-xs whitespace-nowrap text-gray-400 select-none"> <div className="flex flex-1 p-2 text-xs whitespace-nowrap text-muted-foreground select-none">
{title} {title}
</div> </div>
)} )}
+231 -188
View File
@@ -1,188 +1,231 @@
import React, { useMemo } from 'react'; import React, { useMemo } from 'react';
import { useTranslation } from 'react-i18next'; import { useTranslation } from 'react-i18next';
import { useQuery } from '@tanstack/react-query'; import { useQuery } from '@tanstack/react-query';
import { groupBy } from 'lodash-es'; import { groupBy } from 'lodash-es';
import { ipc } from '@/ipc/manager'; import { ipc } from '@/ipc/manager';
import { Card, CardContent, CardHeader, CardTitle, CardDescription } from '@/components/ui/card'; import { Badge } from '@/components/ui/badge';
import { Badge } from '@/components/ui/badge'; import { Loader2, Eye, Zap, Code, Brain, Star, Layers } from 'lucide-react';
import { Loader2, Eye, Zap, Code, Brain, Star } from 'lucide-react';
interface ModelCapability {
interface ModelCapability { id: string;
id: string; object: string;
object: string; displayName?: string;
displayName?: string; capabilities: {
capabilities: { vision: boolean;
vision: boolean; thinking: boolean;
thinking: boolean; streaming: boolean;
streaming: boolean; jsonMode: boolean;
jsonMode: boolean; recommended: boolean;
recommended: boolean; };
}; limits: {
limits: { maxTokens?: number;
maxTokens?: number; maxOutputTokens?: number;
maxOutputTokens?: number; thinkingBudget?: number;
thinkingBudget?: number; };
}; }
}
const CAPABILITY_ICONS = {
const CAPABILITY_ICONS = { vision: Eye,
vision: Eye, thinking: Brain,
thinking: Brain, streaming: Zap,
streaming: Zap, jsonMode: Code,
jsonMode: Code, recommended: Star,
recommended: Star, } as const;
} as const;
const CAPABILITY_LABELS = {
const CAPABILITY_LABELS = { vision: 'Vision',
vision: 'Vision', thinking: 'Thinking',
thinking: 'Thinking', streaming: 'Streaming',
streaming: 'Streaming', jsonMode: 'JSON Mode',
jsonMode: 'JSON Mode', recommended: 'Recommended',
recommended: 'Recommended', } as const;
} as const;
function getProvider(modelId: string): string {
function getProvider(modelId: string): string { const id = modelId.toLowerCase();
const id = modelId.toLowerCase(); if (id.startsWith('gemini')) return 'Google';
if (id.startsWith('gemini')) return 'Google'; if (id.startsWith('claude')) return 'Anthropic';
if (id.startsWith('claude')) return 'Anthropic'; if (id.startsWith('gpt') || id.startsWith('o1') || id.startsWith('o3') || id.startsWith('text-'))
if (id.startsWith('gpt') || id.startsWith('o1') || id.startsWith('o3') || id.startsWith('text-')) return 'OpenAI'; return 'OpenAI';
if (id.startsWith('deepseek')) return 'DeepSeek'; if (id.startsWith('deepseek')) return 'DeepSeek';
if (id.startsWith('meta-llama') || id.startsWith('llama')) return 'Meta'; if (id.startsWith('meta-llama') || id.startsWith('llama')) return 'Meta';
if (id.startsWith('mistral') || id.startsWith('mixtral')) return 'Mistral'; if (id.startsWith('mistral') || id.startsWith('mixtral')) return 'Mistral';
if (id.startsWith('qwen')) return 'Alibaba'; if (id.startsWith('qwen')) return 'Alibaba';
if (id.startsWith('x-ai') || id.startsWith('grok')) return 'xAI'; if (id.startsWith('x-ai') || id.startsWith('grok')) return 'xAI';
if (id.startsWith('sonar')) return 'Perplexity'; if (id.startsWith('sonar')) return 'Perplexity';
if (id.startsWith('command')) return 'Cohere'; if (id.startsWith('command')) return 'Cohere';
return 'Other'; return 'Other';
} }
export const CapabilitiesTab: React.FC = () => { /** Format large token counts with K/M suffixes */
const { t } = useTranslation(); function formatTokenCount(value: number): string {
if (value >= 1_000_000) {
const { data: capabilitiesData, isLoading } = useQuery({ return `${(value / 1_000_000).toFixed(1)}M`;
queryKey: ['proxyAdvanced', 'modelCapabilities'], }
queryFn: () => ipc.client.proxyAdvanced.getModelCapabilities(), if (value >= 1_000) {
refetchInterval: 60000, return `${(value / 1_000).toFixed(0)}K`;
}); }
return value.toLocaleString();
const models: ModelCapability[] = capabilitiesData?.success }
? (capabilitiesData.data as ModelCapability[])
: []; export const CapabilitiesTab: React.FC = () => {
const { t } = useTranslation();
const groupedModels = useMemo(() => {
return groupBy(models, (m) => getProvider(m.id)); const { data: capabilitiesData, isLoading } = useQuery({
}, [models]); queryKey: ['proxyAdvanced', 'modelCapabilities'],
queryFn: () => ipc.client.proxyAdvanced.getModelCapabilities(),
// Sort providers alphabetically, but keep 'Other' at the bottom refetchInterval: 60000,
const sortedProviders = Object.keys(groupedModels).sort((a, b) => { });
if (a === 'Other') return 1;
if (b === 'Other') return -1; const models: ModelCapability[] = capabilitiesData?.success
return a.localeCompare(b); ? (capabilitiesData.data as ModelCapability[])
}); : [];
return ( const groupedModels = useMemo(() => {
<div className="space-y-5"> return groupBy(models, (m) => getProvider(m.id));
{/* Header */} }, [models]);
<div>
<h3 className="text-lg font-medium">{t('proxy.advanced.capabilities.title', 'Model Capabilities')}</h3> const sortedProviders = Object.keys(groupedModels).sort((a, b) => {
<p className="text-sm text-muted-foreground">{t('proxy.advanced.capabilities.description', 'Discover what each model can do')}</p> if (a === 'Other') return 1;
</div> if (b === 'Other') return -1;
return a.localeCompare(b);
{/* Capabilities Grid */} });
{isLoading ? (
<div className="flex h-32 items-center justify-center"> return (
<Loader2 className="h-6 w-6 animate-spin" /> <div className="space-y-6">
</div> {/* Header */}
) : ( <div className="flex items-center justify-between">
<div className="space-y-8"> <div>
{sortedProviders.map((provider) => ( <h3 className="text-base font-semibold">
<div key={provider} className="space-y-4"> {t('proxy.advanced.capabilities.title', 'Model Capabilities')}
<h4 className="text-md font-semibold text-foreground border-b pb-2"> </h3>
{provider} <p className="text-[13px] text-muted-foreground">
</h4> {t('proxy.advanced.capabilities.description', 'Discover what each model can do')}
<div className="grid grid-cols-1 gap-4 md:grid-cols-2 lg:grid-cols-3"> </p>
{groupedModels[provider].map((model) => ( </div>
<Card key={model.id} className="flex flex-col transition-shadow hover:shadow-md"> {models.length > 0 && (
<CardHeader className="pb-3"> <Badge variant="outline" className="text-xs tabular-nums">
<div className="flex items-center justify-between"> <Layers className="mr-1.5 h-3 w-3" />
<CardTitle className="text-base truncate pr-2" title={model.displayName || model.id}> {models.length} models
{model.displayName || model.id} </Badge>
</CardTitle> )}
<Badge variant="outline" className="shrink-0">{model.object}</Badge> </div>
</div>
<CardDescription className="text-xs font-mono text-muted-foreground truncate" title={model.id}> {/* Content */}
{model.id} {isLoading ? (
</CardDescription> <div className="flex h-40 items-center justify-center">
</CardHeader> <Loader2 className="h-5 w-5 animate-spin text-muted-foreground" />
<CardContent className="space-y-4 flex-1 flex flex-col justify-between"> </div>
<div className="grid grid-cols-2 gap-2"> ) : (
{Object.entries(model.capabilities).map(([key, supported]) => { <div className="space-y-8">
const iconKey = key as keyof typeof CAPABILITY_ICONS; {sortedProviders.map((provider) => (
const Icon = CAPABILITY_ICONS[iconKey]; <div key={provider} className="space-y-3">
const label = CAPABILITY_LABELS[iconKey]; {/* Provider header */}
return ( <div className="flex items-center gap-3">
<div <h4 className="text-[13px] font-semibold tracking-wide uppercase text-muted-foreground">
key={key} {provider}
className={`flex items-center gap-2 rounded-md border p-2 ${ </h4>
supported <div className="h-px flex-1 bg-white/[0.06]" />
? 'border-green-200 bg-green-50 dark:border-green-900 dark:bg-green-950/30' <span className="text-[11px] text-muted-foreground/60 tabular-nums">
: 'border-gray-200 bg-gray-50 dark:border-gray-800 dark:bg-gray-900/50' {groupedModels[provider].length}
}`} </span>
> </div>
<Icon
className={`h-4 w-4 shrink-0 ${ {/* Model cards */}
supported ? 'text-green-600 dark:text-green-400' : 'text-gray-400' <div className="grid grid-cols-1 gap-3 md:grid-cols-2 lg:grid-cols-3">
}`} {groupedModels[provider].map((model) => {
/> const enabledCount = Object.values(model.capabilities).filter(Boolean).length;
<span const totalCount = Object.values(model.capabilities).length;
className={`text-[11px] font-medium truncate ${ const hasLimits = Object.values(model.limits).some((v) => v != null);
supported
? 'text-green-700 dark:text-green-300' return (
: 'text-gray-500 dark:text-gray-400' <div
}`} key={model.id}
title={label} className="group flex flex-col rounded-xl border border-white/[0.06] bg-card p-4 transition-all hover:border-white/[0.12] hover:bg-white/[0.02]"
> >
{label} {/* Model header */}
</span> <div className="mb-3 space-y-1">
</div> <div className="flex items-start justify-between gap-2">
); <h5
})} className="text-[13px] font-semibold leading-tight truncate"
</div> title={model.displayName || model.id}
{Object.values(model.limits).some((v) => v != null) && ( >
<div className="space-y-2 pt-2 border-t mt-auto"> {model.displayName || model.id}
<p className="text-xs font-medium text-muted-foreground">Limits</p> </h5>
<div className="flex flex-wrap gap-1.5"> <span className="shrink-0 text-[10px] font-medium text-muted-foreground/60 rounded-md bg-white/[0.04] px-1.5 py-0.5">
{model.limits.maxTokens != null && ( {enabledCount}/{totalCount}
<Badge variant="secondary" className="text-[10px] font-normal px-1.5"> </span>
Context: {model.limits.maxTokens.toLocaleString()} </div>
</Badge> <p
)} className="font-mono text-[11px] text-muted-foreground truncate"
{model.limits.maxOutputTokens != null && ( title={model.id}
<Badge variant="secondary" className="text-[10px] font-normal px-1.5"> >
Output: {model.limits.maxOutputTokens.toLocaleString()} {model.id}
</Badge> </p>
)} </div>
{model.limits.thinkingBudget != null && (
<Badge variant="secondary" className="text-[10px] font-normal px-1.5 border-blue-200 bg-blue-50 text-blue-700 dark:bg-blue-900/30 dark:border-blue-800 dark:text-blue-300"> {/* Capability pills */}
Think: {model.limits.thinkingBudget.toLocaleString()} <div className="flex flex-wrap gap-1.5 mb-3">
</Badge> {Object.entries(model.capabilities).map(([key, supported]) => {
)} const iconKey = key as keyof typeof CAPABILITY_ICONS;
</div> const Icon = CAPABILITY_ICONS[iconKey];
</div> const label = CAPABILITY_LABELS[iconKey];
)} return (
</CardContent> <div
</Card> key={key}
))} className={`inline-flex items-center gap-1 rounded-md px-2 py-1 text-[10px] font-medium transition-colors ${
</div> supported
</div> ? 'bg-emerald-500/10 text-emerald-400 border border-emerald-500/15'
))} : 'bg-white/[0.02] text-muted-foreground/50 border border-white/[0.04]'
{models.length === 0 && ( }`}
<div className="text-center py-10 text-muted-foreground border rounded-lg border-dashed"> title={label}
No models found. Check your provider configurations. >
</div> <Icon className="h-3 w-3 shrink-0" />
)} {label}
</div> </div>
)} );
</div> })}
); </div>
};
{/* Limits section */}
{hasLimits && (
<div className="mt-auto flex flex-wrap gap-1.5 border-t border-white/[0.04] pt-3">
{model.limits.maxTokens != null && (
<span className="inline-flex items-center rounded-md bg-white/[0.04] px-1.5 py-0.5 font-mono text-[10px] text-muted-foreground">
CTX {formatTokenCount(model.limits.maxTokens)}
</span>
)}
{model.limits.maxOutputTokens != null && (
<span className="inline-flex items-center rounded-md bg-white/[0.04] px-1.5 py-0.5 font-mono text-[10px] text-muted-foreground">
OUT {formatTokenCount(model.limits.maxOutputTokens)}
</span>
)}
{model.limits.thinkingBudget != null && (
<span className="inline-flex items-center rounded-md bg-blue-500/10 border border-blue-500/15 px-1.5 py-0.5 font-mono text-[10px] text-blue-300">
THINK {formatTokenCount(model.limits.thinkingBudget)}
</span>
)}
</div>
)}
</div>
);
})}
</div>
</div>
))}
{models.length === 0 && (
<div className="flex flex-col items-center justify-center gap-3 rounded-xl border border-dashed border-white/[0.1] py-16 text-center">
<Layers className="h-8 w-8 text-muted-foreground/30" />
<div className="space-y-1">
<p className="text-[13px] font-medium">No models found</p>
<p className="text-xs text-muted-foreground">
Check your provider configurations
</p>
</div>
</div>
)}
</div>
)}
</div>
);
};
+13 -16
View File
@@ -2,7 +2,6 @@ import React, { useState } from 'react';
import { useTranslation } from 'react-i18next'; import { useTranslation } from 'react-i18next';
import { useQuery } from '@tanstack/react-query'; import { useQuery } from '@tanstack/react-query';
import { ipc } from '@/ipc/manager'; import { ipc } from '@/ipc/manager';
import { Card, CardContent, CardHeader, CardTitle } from '@/components/ui/card';
import { Button } from '@/components/ui/button'; import { Button } from '@/components/ui/button';
import { Badge } from '@/components/ui/badge'; import { Badge } from '@/components/ui/badge';
import { import {
@@ -113,26 +112,25 @@ export const MonitorTab: React.FC = () => {
</div> </div>
{/* Circuit Breakers */} {/* Circuit Breakers */}
<Card> <div className="overflow-hidden rounded-xl border border-white/[0.06] bg-card">
<CardHeader> <div className="border-b border-white/[0.06] px-5 py-4">
<CardTitle className="text-sm font-semibold"> <h4 className="text-[13px] font-semibold">
{t('proxy.advanced.circuitBreaker.title')} {t('proxy.advanced.circuitBreaker.title')}
</CardTitle> </h4>
</CardHeader> </div>
<CardContent> <div className="p-5">
{cbLoading ? ( {cbLoading ? (
<div className="flex h-20 items-center justify-center"> <div className="flex h-20 items-center justify-center">
<Loader2 className="h-5 w-5 animate-spin" /> <Loader2 className="h-5 w-5 animate-spin" />
</div> </div>
) : !circuitBreakers ? ( ) : !circuitBreakers ? (
<p className="text-sm text-muted-foreground">No data</p> <p className="text-[13px] text-muted-foreground">No data</p>
) : ( ) : (
<div className="grid grid-cols-1 gap-3 sm:grid-cols-2 lg:grid-cols-3"> <div className="grid grid-cols-1 gap-3 sm:grid-cols-2 lg:grid-cols-3">
{Object.entries(circuitBreakers.states).map(([api, state]) => ( {Object.entries(circuitBreakers.states).map(([api, state]) => (
<Card key={api} className="bg-muted/30"> <div key={api} className="rounded-xl border border-white/[0.06] bg-white/[0.02] p-4">
<CardContent className="p-4">
<div className="flex items-center justify-between"> <div className="flex items-center justify-between">
<span className="text-sm font-medium">{api}</span> <span className="text-[13px] font-medium">{api}</span>
<Badge variant={getCircuitBadgeVariant(state.state)}> <Badge variant={getCircuitBadgeVariant(state.state)}>
{state.state} {state.state}
</Badge> </Badge>
@@ -146,19 +144,18 @@ export const MonitorTab: React.FC = () => {
{new Date(state.lastFailure).toLocaleString()} {new Date(state.lastFailure).toLocaleString()}
</div> </div>
)} )}
</CardContent> </div>
</Card>
))} ))}
</div> </div>
)} )}
</CardContent> </div>
</Card> </div>
{/* Traffic Logs */} {/* Traffic Logs */}
<div className="space-y-3"> <div className="space-y-3">
<div className="flex flex-wrap items-center justify-between gap-3"> <div className="flex flex-wrap items-center justify-between gap-3">
<div className="flex items-center gap-3"> <div className="flex items-center gap-3">
<h3 className="text-sm font-semibold"> <h3 className="text-[13px] font-semibold">
{t('proxy.advanced.trafficLog.title')} {t('proxy.advanced.trafficLog.title')}
</h3> </h3>
{stats && ( {stats && (
+18 -18
View File
@@ -2,7 +2,7 @@ import React, { useState } from 'react';
import { useTranslation } from 'react-i18next'; import { useTranslation } from 'react-i18next';
import { useQuery, useMutation, useQueryClient } from '@tanstack/react-query'; import { useQuery, useMutation, useQueryClient } from '@tanstack/react-query';
import { ipc } from '@/ipc/manager'; import { ipc } from '@/ipc/manager';
import { Card, CardContent, CardHeader, CardTitle, CardDescription } from '@/components/ui/card';
import { Button } from '@/components/ui/button'; import { Button } from '@/components/ui/button';
import { Badge } from '@/components/ui/badge'; import { Badge } from '@/components/ui/badge';
import { useToast } from '@/components/ui/use-toast'; import { useToast } from '@/components/ui/use-toast';
@@ -76,7 +76,7 @@ export const ReplayTab: React.FC = () => {
if (!status) return <Badge variant="outline">{t('proxy.advanced.replay.pending')}</Badge>; if (!status) return <Badge variant="outline">{t('proxy.advanced.replay.pending')}</Badge>;
if (status >= 200 && status < 300) { if (status >= 200 && status < 300) {
return ( return (
<Badge variant="default" className="bg-green-600 hover:bg-green-700"> <Badge variant="default" className="bg-emerald-500/15 text-emerald-400 border-emerald-500/20 hover:bg-emerald-500/20">
<CheckCircle className="mr-1 h-3 w-3" /> <CheckCircle className="mr-1 h-3 w-3" />
{status} {status}
</Badge> </Badge>
@@ -98,8 +98,8 @@ export const ReplayTab: React.FC = () => {
{/* Header */} {/* Header */}
<div className="flex items-center justify-between"> <div className="flex items-center justify-between">
<div> <div>
<h3 className="text-lg font-medium">{t('proxy.advanced.replay.title')}</h3> <h3 className="text-base font-semibold">{t('proxy.advanced.replay.title')}</h3>
<p className="text-sm text-muted-foreground">{t('proxy.advanced.replay.description')}</p> <p className="text-[13px] text-muted-foreground">{t('proxy.advanced.replay.description')}</p>
</div> </div>
<Button <Button
variant="outline" variant="outline"
@@ -107,26 +107,26 @@ export const ReplayTab: React.FC = () => {
onClick={() => refetchRequests()} onClick={() => refetchRequests()}
disabled={requestsLoading} disabled={requestsLoading}
> >
<RefreshCw className={`mr-2 h-4 w-4 ${requestsLoading ? 'animate-spin' : ''}`} /> <RefreshCw className={`mr-1.5 h-3.5 w-3.5 ${requestsLoading ? 'animate-spin' : ''}`} />
{t('proxy.advanced.replay.refresh')} {t('proxy.advanced.replay.refresh')}
</Button> </Button>
</div> </div>
{/* Requests List */} {/* Requests List */}
<Card> <div className="overflow-hidden rounded-xl border border-white/[0.06] bg-card">
<CardHeader> <div className="border-b border-white/[0.06] px-5 py-4 space-y-1">
<CardTitle>{t('proxy.advanced.replay.recentRequests')}</CardTitle> <h4 className="text-[13px] font-semibold">{t('proxy.advanced.replay.recentRequests')}</h4>
<CardDescription>{t('proxy.advanced.replay.clickToReplay')}</CardDescription> <p className="text-xs text-muted-foreground">{t('proxy.advanced.replay.clickToReplay')}</p>
</CardHeader> </div>
<CardContent> <div className="p-5">
{requestsLoading ? ( {requestsLoading ? (
<div className="flex h-32 items-center justify-center"> <div className="flex h-32 items-center justify-center">
<Loader2 className="h-6 w-6 animate-spin" /> <Loader2 className="h-6 w-6 animate-spin" />
</div> </div>
) : requests.length === 0 ? ( ) : requests.length === 0 ? (
<div className="flex h-32 flex-col items-center justify-center gap-2 rounded-lg border border-dashed"> <div className="flex h-32 flex-col items-center justify-center gap-2 rounded-xl border border-dashed border-white/[0.1]">
<Clock className="h-8 w-8 text-muted-foreground" /> <Clock className="h-6 w-6 text-muted-foreground/30" />
<p className="text-sm text-muted-foreground"> <p className="text-[13px] text-muted-foreground">
{t('proxy.advanced.replay.noRequests')} {t('proxy.advanced.replay.noRequests')}
</p> </p>
</div> </div>
@@ -135,12 +135,12 @@ export const ReplayTab: React.FC = () => {
{requests.map((request) => ( {requests.map((request) => (
<div <div
key={request.id} key={request.id}
className="flex items-center justify-between rounded-lg border p-3 transition-colors hover:bg-muted/50" className="flex items-center justify-between rounded-xl border border-white/[0.06] p-3 transition-colors hover:bg-white/[0.02]"
> >
<div className="flex items-center gap-3"> <div className="flex items-center gap-3">
{getStatusBadge(request.status)} {getStatusBadge(request.status)}
<div className="flex flex-col"> <div className="flex flex-col">
<span className="text-sm font-medium"> <span className="text-[13px] font-medium">
{request.method} {request.endpoint} {request.method} {request.endpoint}
</span> </span>
<span className="text-xs text-muted-foreground"> <span className="text-xs text-muted-foreground">
@@ -166,8 +166,8 @@ export const ReplayTab: React.FC = () => {
))} ))}
</div> </div>
)} )}
</CardContent> </div>
</Card> </div>
</div> </div>
); );
}; };
+12 -12
View File
@@ -2,7 +2,7 @@ import React from 'react';
import { useTranslation } from 'react-i18next'; import { useTranslation } from 'react-i18next';
import { useQuery, useQueries } from '@tanstack/react-query'; import { useQuery, useQueries } from '@tanstack/react-query';
import { ipc } from '@/ipc/manager'; import { ipc } from '@/ipc/manager';
import { Card, CardContent, CardHeader, CardTitle, CardDescription } from '@/components/ui/card';
import { Button } from '@/components/ui/button'; import { Button } from '@/components/ui/button';
import { useToast } from '@/components/ui/use-toast'; import { useToast } from '@/components/ui/use-toast';
import { StatCard } from '@/components/usage/StatCard'; import { StatCard } from '@/components/usage/StatCard';
@@ -87,12 +87,12 @@ export const ToolsTab: React.FC = () => {
</div> </div>
{/* IDE Quick Setup */} {/* IDE Quick Setup */}
<Card> <div className="overflow-hidden rounded-xl border border-white/[0.06] bg-card">
<CardHeader> <div className="border-b border-white/[0.06] px-5 py-4 space-y-1">
<CardTitle>{t('proxy.advanced.tools.ideSetup')}</CardTitle> <h4 className="text-[13px] font-semibold">{t('proxy.advanced.tools.ideSetup')}</h4>
<CardDescription>Quick configuration for popular IDEs</CardDescription> <p className="text-xs text-muted-foreground">Quick configuration for popular IDEs</p>
</CardHeader> </div>
<CardContent className="space-y-3"> <div className="p-5 space-y-3">
{isIdeLoading ? ( {isIdeLoading ? (
<div className="flex h-20 items-center justify-center"> <div className="flex h-20 items-center justify-center">
<Loader2 className="h-5 w-5 animate-spin" /> <Loader2 className="h-5 w-5 animate-spin" />
@@ -101,11 +101,11 @@ export const ToolsTab: React.FC = () => {
IDE_CONFIGS.map(({ key, label, icon: Icon }) => { IDE_CONFIGS.map(({ key, label, icon: Icon }) => {
const instructions = getIdeInstructions(key); const instructions = getIdeInstructions(key);
return ( return (
<div key={key} className="rounded-lg border"> <div key={key} className="overflow-hidden rounded-xl border border-white/[0.06]">
<div className="flex items-center justify-between p-3"> <div className="flex items-center justify-between p-3">
<div className="flex items-center gap-2"> <div className="flex items-center gap-2">
<Icon className="h-4 w-4 text-muted-foreground" /> <Icon className="h-4 w-4 text-muted-foreground" />
<span className="text-sm font-medium">{label}</span> <span className="text-[13px] font-medium">{label}</span>
</div> </div>
<Button <Button
variant="ghost" variant="ghost"
@@ -118,7 +118,7 @@ export const ToolsTab: React.FC = () => {
</Button> </Button>
</div> </div>
{instructions && ( {instructions && (
<pre className="overflow-x-auto border-t bg-gray-900 p-3 font-mono text-xs whitespace-pre-wrap text-gray-100"> <pre className="overflow-x-auto border-t border-white/[0.06] bg-card p-3 font-mono text-xs whitespace-pre-wrap text-foreground/90">
{instructions} {instructions}
</pre> </pre>
)} )}
@@ -126,8 +126,8 @@ export const ToolsTab: React.FC = () => {
); );
}) })
)} )}
</CardContent> </div>
</Card> </div>
</div> </div>
); );
@@ -1,7 +1,7 @@
import React, { useState, useMemo } from 'react'; import React, { useState, useMemo } from 'react';
import { useTranslation } from 'react-i18next'; import { useTranslation } from 'react-i18next';
import { Loader2, ArrowUp, ArrowDown } from 'lucide-react'; import { Loader2, ArrowUp, ArrowDown } from 'lucide-react';
import { Card, CardContent, CardHeader, CardTitle } from '@/components/ui/card';
import { Badge } from '@/components/ui/badge'; import { Badge } from '@/components/ui/badge';
interface TrafficLogEntry { interface TrafficLogEntry {
@@ -74,26 +74,26 @@ export const TrafficLogTable: React.FC<TrafficLogTableProps> = ({ logs, isLoadin
}, [logs, sortKey, sortDir]); }, [logs, sortKey, sortDir]);
return ( return (
<Card> <div className="overflow-hidden rounded-xl border border-white/[0.06] bg-card">
<CardHeader> <div className="border-b border-white/[0.06] px-5 py-4">
<CardTitle className="text-sm font-semibold"> <h4 className="text-[13px] font-semibold">
{t('proxy.advanced.trafficLog.title')} {t('proxy.advanced.trafficLog.title')}
</CardTitle> </h4>
</CardHeader> </div>
<CardContent> <div className="p-5">
{isLoading ? ( {isLoading ? (
<div className="flex h-32 items-center justify-center"> <div className="flex h-32 items-center justify-center">
<Loader2 className="h-5 w-5 animate-spin" /> <Loader2 className="h-5 w-5 animate-spin" />
</div> </div>
) : logs.length === 0 ? ( ) : logs.length === 0 ? (
<div className="py-8 text-center text-sm text-muted-foreground"> <div className="py-8 text-center text-[13px] text-muted-foreground">
{t('proxy.advanced.trafficLog.noLogs')} {t('proxy.advanced.trafficLog.noLogs')}
</div> </div>
) : ( ) : (
<div className="overflow-x-auto"> <div className="overflow-x-auto">
<table className="w-full text-sm"> <table className="w-full text-sm">
<thead> <thead>
<tr className="border-b text-left text-xs text-muted-foreground"> <tr className="border-b border-white/[0.06] text-left text-[10px] font-medium tracking-wider uppercase text-muted-foreground">
<th <th
className="cursor-pointer pb-2 pr-4 font-medium select-none" className="cursor-pointer pb-2 pr-4 font-medium select-none"
onClick={() => toggleSort('timestamp')} onClick={() => toggleSort('timestamp')}
@@ -142,7 +142,7 @@ export const TrafficLogTable: React.FC<TrafficLogTableProps> = ({ logs, isLoadin
{sortedLogs.map((log, i) => ( {sortedLogs.map((log, i) => (
<tr <tr
key={`${log.requestId}-${i}`} key={`${log.requestId}-${i}`}
className="border-b border-border/40 last:border-0" className="border-b border-white/[0.04] last:border-0"
> >
<td className="py-2 pr-4 font-mono text-xs text-muted-foreground"> <td className="py-2 pr-4 font-mono text-xs text-muted-foreground">
{new Date(log.timestamp).toLocaleTimeString()} {new Date(log.timestamp).toLocaleTimeString()}
@@ -184,7 +184,7 @@ export const TrafficLogTable: React.FC<TrafficLogTableProps> = ({ logs, isLoadin
</table> </table>
</div> </div>
)} )}
</CardContent> </div>
</Card> </div>
); );
}; };
+30 -31
View File
@@ -1,31 +1,30 @@
import * as React from 'react'; import * as React from 'react';
import { cva, type VariantProps } from 'class-variance-authority'; import { cn } from '@/lib/utils';
import { cn } from '@/lib/utils'; import { cva, type VariantProps } from 'class-variance-authority';
const badgeVariants = cva( const badgeVariants = cva(
'inline-flex items-center rounded-md border px-2.5 py-0.5 text-xs font-semibold transition-colors focus:outline-none focus:ring-2 focus:ring-ring focus:ring-offset-2', 'inline-flex items-center rounded-md px-2 py-0.5 text-[11px] font-medium transition-colors focus:outline-none focus:ring-2 focus:ring-ring/30',
{ {
variants: { variants: {
variant: { variant: {
default: 'border-transparent bg-primary text-primary-foreground shadow hover:bg-primary/80', default: 'bg-accent/15 text-accent border border-accent/20',
secondary: secondary: 'bg-secondary text-secondary-foreground border border-white/[0.06]',
'border-transparent bg-secondary text-secondary-foreground hover:bg-secondary/80', destructive: 'bg-destructive/15 text-red-400 border border-destructive/20',
destructive: outline: 'border border-white/[0.1] text-muted-foreground',
'border-transparent bg-destructive text-destructive-foreground shadow hover:bg-destructive/80', },
outline: 'text-foreground', },
}, defaultVariants: {
}, variant: 'default',
defaultVariants: { },
variant: 'default', },
}, );
},
); function Badge({
className,
export interface BadgeProps variant,
extends React.HTMLAttributes<HTMLDivElement>, VariantProps<typeof badgeVariants> {} ...props
}: React.HTMLAttributes<HTMLDivElement> & VariantProps<typeof badgeVariants>) {
function Badge({ className, variant, ...props }: BadgeProps) { return <div className={cn(badgeVariants({ variant }), className)} {...props} />;
return <div className={cn(badgeVariants({ variant }), className)} {...props} />; }
}
export { Badge, badgeVariants };
export { Badge, badgeVariants };
+59 -56
View File
@@ -1,56 +1,59 @@
import * as React from 'react'; import * as React from 'react';
import { Slot } from '@radix-ui/react-slot'; import { Slot } from '@radix-ui/react-slot';
import { cva, type VariantProps } from 'class-variance-authority'; import { cva, type VariantProps } from 'class-variance-authority';
import { cn } from '@/lib/utils'; import { cn } from '@/lib/utils';
const buttonVariants = cva( const buttonVariants = cva(
"inline-flex items-center justify-center gap-2 whitespace-nowrap rounded-md text-sm font-medium transition-all disabled:pointer-events-none disabled:opacity-50 [&_svg]:pointer-events-none [&_svg:not([class*='size-'])]:size-4 shrink-0 [&_svg]:shrink-0 outline-none focus-visible:border-ring focus-visible:ring-ring/50 focus-visible:ring-[3px] aria-invalid:ring-destructive/20 dark:aria-invalid:ring-destructive/40 aria-invalid:border-destructive", "inline-flex items-center justify-center gap-2 whitespace-nowrap rounded-lg text-[13px] font-medium transition-all duration-200 disabled:pointer-events-none disabled:opacity-50 [&_svg]:pointer-events-none [&_svg:not([class*='size-'])]:size-4 shrink-0 [&_svg]:shrink-0 outline-none focus-visible:ring-2 focus-visible:ring-ring/30 focus-visible:ring-offset-1 focus-visible:ring-offset-background",
{ {
variants: { variants: {
variant: { variant: {
default: 'bg-primary text-primary-foreground shadow-none hover:bg-primary/90', default:
destructive: 'bg-primary text-primary-foreground hover:bg-primary/90 active:scale-[0.98]',
'bg-destructive text-white shadow-none hover:bg-destructive/90 focus-visible:ring-destructive/20 dark:focus-visible:ring-destructive/40 dark:bg-destructive/60', destructive:
outline: 'bg-destructive/90 text-white hover:bg-destructive active:scale-[0.98] focus-visible:ring-destructive/30',
'border bg-background shadow-none hover:bg-accent hover:text-accent-foreground dark:bg-input/30 dark:border-input dark:hover:bg-input/50', outline:
secondary: 'bg-secondary text-secondary-foreground shadow-none hover:bg-secondary/80', 'border border-white/[0.08] bg-transparent hover:bg-white/[0.04] hover:border-white/[0.12] text-foreground',
ghost: 'hover:bg-accent hover:text-accent-foreground dark:hover:bg-accent/50', secondary:
link: 'text-primary underline-offset-4 hover:underline', 'bg-secondary text-secondary-foreground hover:bg-secondary/80 active:scale-[0.98]',
}, ghost:
size: { 'hover:bg-white/[0.06] text-muted-foreground hover:text-foreground',
default: 'h-9 px-4 py-2 has-[>svg]:px-3', link: 'text-accent underline-offset-4 hover:underline',
sm: 'h-8 rounded-md gap-1.5 px-3 has-[>svg]:px-2.5', },
lg: 'h-10 rounded-md px-6 has-[>svg]:px-4', size: {
icon: 'size-9', default: 'h-9 px-4 py-2',
}, sm: 'h-8 rounded-md gap-1.5 px-3 text-xs',
}, lg: 'h-10 rounded-lg px-6',
defaultVariants: { icon: 'size-8 rounded-lg',
variant: 'default', },
size: 'default', },
}, defaultVariants: {
}, variant: 'default',
); size: 'default',
},
function Button({ },
className, );
variant,
size, function Button({
asChild = false, className,
...props variant,
}: React.ComponentProps<'button'> & size,
VariantProps<typeof buttonVariants> & { asChild = false,
asChild?: boolean; ...props
}) { }: React.ComponentProps<'button'> &
const Comp = asChild ? Slot : 'button'; VariantProps<typeof buttonVariants> & {
asChild?: boolean;
return ( }) {
<Comp const Comp = asChild ? Slot : 'button';
data-slot="button"
className={cn(buttonVariants({ variant, size, className }))} return (
{...props} <Comp
/> data-slot="button"
); className={cn(buttonVariants({ variant, size, className }))}
} {...props}
/>
export { Button, buttonVariants }; );
}
export { Button, buttonVariants };
+58 -55
View File
@@ -1,55 +1,58 @@
import * as React from 'react'; import * as React from 'react';
import { cn } from '@/lib/utils'; import { cn } from '@/lib/utils';
const Card = React.forwardRef<HTMLDivElement, React.HTMLAttributes<HTMLDivElement>>( const Card = React.forwardRef<HTMLDivElement, React.HTMLAttributes<HTMLDivElement>>(
({ className, ...props }, ref) => ( ({ className, ...props }, ref) => (
<div <div
ref={ref} ref={ref}
className={cn('bg-card text-card-foreground rounded-xl border border-white/5 shadow-sm', className)} className={cn(
{...props} 'bg-card text-card-foreground rounded-xl border border-white/[0.06] shadow-sm transition-colors duration-200',
/> className,
), )}
); {...props}
Card.displayName = 'Card'; />
),
const CardHeader = React.forwardRef<HTMLDivElement, React.HTMLAttributes<HTMLDivElement>>( );
({ className, ...props }, ref) => ( Card.displayName = 'Card';
<div ref={ref} className={cn('flex flex-col space-y-1.5 p-6', className)} {...props} />
), const CardHeader = React.forwardRef<HTMLDivElement, React.HTMLAttributes<HTMLDivElement>>(
); ({ className, ...props }, ref) => (
CardHeader.displayName = 'CardHeader'; <div ref={ref} className={cn('flex flex-col space-y-1.5 p-6', className)} {...props} />
),
const CardTitle = React.forwardRef<HTMLParagraphElement, React.HTMLAttributes<HTMLHeadingElement>>( );
({ className, ...props }, ref) => ( CardHeader.displayName = 'CardHeader';
<h3
ref={ref} const CardTitle = React.forwardRef<HTMLParagraphElement, React.HTMLAttributes<HTMLHeadingElement>>(
className={cn('leading-none font-semibold tracking-tight', className)} ({ className, ...props }, ref) => (
{...props} <h3
/> ref={ref}
), className={cn('text-sm font-semibold leading-none tracking-tight', className)}
); {...props}
CardTitle.displayName = 'CardTitle'; />
),
const CardDescription = React.forwardRef< );
HTMLParagraphElement, CardTitle.displayName = 'CardTitle';
React.HTMLAttributes<HTMLParagraphElement>
>(({ className, ...props }, ref) => ( const CardDescription = React.forwardRef<
<p ref={ref} className={cn('text-muted-foreground text-sm', className)} {...props} /> HTMLParagraphElement,
)); React.HTMLAttributes<HTMLParagraphElement>
CardDescription.displayName = 'CardDescription'; >(({ className, ...props }, ref) => (
<p ref={ref} className={cn('text-muted-foreground text-[13px]', className)} {...props} />
const CardContent = React.forwardRef<HTMLDivElement, React.HTMLAttributes<HTMLDivElement>>( ));
({ className, ...props }, ref) => ( CardDescription.displayName = 'CardDescription';
<div ref={ref} className={cn('p-6 pt-0', className)} {...props} />
), const CardContent = React.forwardRef<HTMLDivElement, React.HTMLAttributes<HTMLDivElement>>(
); ({ className, ...props }, ref) => (
CardContent.displayName = 'CardContent'; <div ref={ref} className={cn('p-6 pt-0', className)} {...props} />
),
const CardFooter = React.forwardRef<HTMLDivElement, React.HTMLAttributes<HTMLDivElement>>( );
({ className, ...props }, ref) => ( CardContent.displayName = 'CardContent';
<div ref={ref} className={cn('flex items-center p-6 pt-0', className)} {...props} />
), const CardFooter = React.forwardRef<HTMLDivElement, React.HTMLAttributes<HTMLDivElement>>(
); ({ className, ...props }, ref) => (
CardFooter.displayName = 'CardFooter'; <div ref={ref} className={cn('flex items-center p-6 pt-0', className)} {...props} />
),
export { Card, CardHeader, CardFooter, CardTitle, CardDescription, CardContent }; );
CardFooter.displayName = 'CardFooter';
export { Card, CardHeader, CardFooter, CardTitle, CardDescription, CardContent };
+1 -1
View File
@@ -13,7 +13,7 @@ const Checkbox = React.forwardRef<
<CheckboxPrimitive.Root <CheckboxPrimitive.Root
ref={ref} ref={ref}
className={cn( className={cn(
'peer border-primary ring-offset-background focus-visible:ring-ring data-[state=checked]:bg-primary data-[state=checked]:text-primary-foreground h-4 w-4 shrink-0 rounded-sm border focus-visible:ring-2 focus-visible:ring-offset-2 focus-visible:outline-none disabled:cursor-not-allowed disabled:opacity-50', 'peer h-4 w-4 shrink-0 rounded-[4px] border border-white/[0.15] bg-transparent ring-offset-background transition-colors focus-visible:ring-2 focus-visible:ring-accent/30 focus-visible:ring-offset-2 focus-visible:outline-none disabled:cursor-not-allowed disabled:opacity-50 data-[state=checked]:border-accent data-[state=checked]:bg-accent data-[state=checked]:text-background',
className, className,
)} )}
{...props} {...props}
+107 -102
View File
@@ -1,102 +1,107 @@
import * as React from 'react'; import * as React from 'react';
import * as DialogPrimitive from '@radix-ui/react-dialog'; import * as DialogPrimitive from '@radix-ui/react-dialog';
import { Cross2Icon } from '@radix-ui/react-icons'; import { X } from 'lucide-react';
import { cn } from '@/lib/utils'; import { cn } from '@/lib/utils';
const Dialog = DialogPrimitive.Root; const Dialog = DialogPrimitive.Root;
const DialogTrigger = DialogPrimitive.Trigger; const DialogTrigger = DialogPrimitive.Trigger;
const DialogPortal = DialogPrimitive.Portal; const DialogPortal = DialogPrimitive.Portal;
const DialogClose = DialogPrimitive.Close; const DialogClose = DialogPrimitive.Close;
const DialogOverlay = React.forwardRef< const DialogOverlay = React.forwardRef<
React.ElementRef<typeof DialogPrimitive.Overlay>, React.ElementRef<typeof DialogPrimitive.Overlay>,
React.ComponentPropsWithoutRef<typeof DialogPrimitive.Overlay> React.ComponentPropsWithoutRef<typeof DialogPrimitive.Overlay>
>(({ className, ...props }, ref) => ( >(({ className, ...props }, ref) => (
<DialogPrimitive.Overlay <DialogPrimitive.Overlay
ref={ref} ref={ref}
className={cn( className={cn(
'data-[state=open]:animate-in data-[state=closed]:animate-out data-[state=closed]:fade-out-0 data-[state=open]:fade-in-0 fixed inset-0 z-50 bg-black/80', 'fixed inset-0 z-50 bg-black/60 backdrop-blur-sm',
className, 'data-[state=open]:animate-in data-[state=closed]:animate-out data-[state=closed]:fade-out-0 data-[state=open]:fade-in-0',
)} className,
{...props} )}
/> {...props}
)); />
DialogOverlay.displayName = DialogPrimitive.Overlay.displayName; ));
DialogOverlay.displayName = DialogPrimitive.Overlay.displayName;
const DialogContent = React.forwardRef<
React.ElementRef<typeof DialogPrimitive.Content>, const DialogContent = React.forwardRef<
React.ComponentPropsWithoutRef<typeof DialogPrimitive.Content> React.ElementRef<typeof DialogPrimitive.Content>,
>(({ className, children, ...props }, ref) => ( React.ComponentPropsWithoutRef<typeof DialogPrimitive.Content>
<DialogPortal> >(({ className, children, ...props }, ref) => (
<DialogOverlay /> <DialogPortal>
<DialogPrimitive.Content <DialogOverlay />
ref={ref} <DialogPrimitive.Content
className={cn( ref={ref}
'bg-background data-[state=open]:animate-in data-[state=closed]:animate-out data-[state=closed]:fade-out-0 data-[state=open]:fade-in-0 data-[state=closed]:zoom-out-95 data-[state=open]:zoom-in-95 data-[state=closed]:slide-out-to-left-1/2 data-[state=closed]:slide-out-to-top-[48%] data-[state=open]:slide-in-from-left-1/2 data-[state=open]:slide-in-from-top-[48%] fixed top-[50%] left-[50%] z-50 grid w-full max-w-lg translate-x-[-50%] translate-y-[-50%] gap-4 border p-6 shadow-lg duration-200 sm:rounded-lg', className={cn(
className, 'fixed top-[50%] left-[50%] z-50 grid w-full max-w-lg translate-x-[-50%] translate-y-[-50%]',
)} 'gap-4 rounded-xl border border-white/[0.08] bg-card p-6 shadow-2xl shadow-black/30',
{...props} 'duration-200',
> 'data-[state=open]:animate-in data-[state=closed]:animate-out data-[state=closed]:fade-out-0 data-[state=open]:fade-in-0 data-[state=closed]:zoom-out-95 data-[state=open]:zoom-in-95',
{children} 'data-[state=closed]:slide-out-to-left-1/2 data-[state=closed]:slide-out-to-top-[48%] data-[state=open]:slide-in-from-left-1/2 data-[state=open]:slide-in-from-top-[48%]',
<DialogPrimitive.Close className="ring-offset-background focus:ring-ring data-[state=open]:bg-accent data-[state=open]:text-muted-foreground absolute top-4 right-4 rounded-sm opacity-70 transition-opacity hover:opacity-100 focus:ring-2 focus:ring-offset-2 focus:outline-none disabled:pointer-events-none"> className,
<Cross2Icon className="h-4 w-4" /> )}
<span className="sr-only">Close</span> {...props}
</DialogPrimitive.Close> >
</DialogPrimitive.Content> {children}
</DialogPortal> <DialogPrimitive.Close className="absolute top-4 right-4 flex h-6 w-6 items-center justify-center rounded-md opacity-50 transition-all hover:bg-white/[0.06] hover:opacity-100 focus:outline-none focus:ring-2 focus:ring-ring/30 disabled:pointer-events-none">
)); <X className="h-4 w-4" />
DialogContent.displayName = DialogPrimitive.Content.displayName; <span className="sr-only">Close</span>
</DialogPrimitive.Close>
const DialogHeader = ({ className, ...props }: React.HTMLAttributes<HTMLDivElement>) => ( </DialogPrimitive.Content>
<div className={cn('flex flex-col space-y-1.5 text-center sm:text-left', className)} {...props} /> </DialogPortal>
); ));
DialogHeader.displayName = 'DialogHeader'; DialogContent.displayName = DialogPrimitive.Content.displayName;
const DialogFooter = ({ className, ...props }: React.HTMLAttributes<HTMLDivElement>) => ( const DialogHeader = ({ className, ...props }: React.HTMLAttributes<HTMLDivElement>) => (
<div <div className={cn('flex flex-col space-y-1.5 text-center sm:text-left', className)} {...props} />
className={cn('flex flex-col-reverse sm:flex-row sm:justify-end sm:space-x-2', className)} );
{...props} DialogHeader.displayName = 'DialogHeader';
/>
); const DialogFooter = ({ className, ...props }: React.HTMLAttributes<HTMLDivElement>) => (
DialogFooter.displayName = 'DialogFooter'; <div
className={cn('flex flex-col-reverse sm:flex-row sm:justify-end sm:space-x-2', className)}
const DialogTitle = React.forwardRef< {...props}
React.ElementRef<typeof DialogPrimitive.Title>, />
React.ComponentPropsWithoutRef<typeof DialogPrimitive.Title> );
>(({ className, ...props }, ref) => ( DialogFooter.displayName = 'DialogFooter';
<DialogPrimitive.Title
ref={ref} const DialogTitle = React.forwardRef<
className={cn('text-lg leading-none font-semibold tracking-tight', className)} React.ElementRef<typeof DialogPrimitive.Title>,
{...props} React.ComponentPropsWithoutRef<typeof DialogPrimitive.Title>
/> >(({ className, ...props }, ref) => (
)); <DialogPrimitive.Title
DialogTitle.displayName = DialogPrimitive.Title.displayName; ref={ref}
className={cn('text-base leading-none font-semibold tracking-tight', className)}
const DialogDescription = React.forwardRef< {...props}
React.ElementRef<typeof DialogPrimitive.Description>, />
React.ComponentPropsWithoutRef<typeof DialogPrimitive.Description> ));
>(({ className, ...props }, ref) => ( DialogTitle.displayName = DialogPrimitive.Title.displayName;
<DialogPrimitive.Description
ref={ref} const DialogDescription = React.forwardRef<
className={cn('text-muted-foreground text-sm', className)} React.ElementRef<typeof DialogPrimitive.Description>,
{...props} React.ComponentPropsWithoutRef<typeof DialogPrimitive.Description>
/> >(({ className, ...props }, ref) => (
)); <DialogPrimitive.Description
DialogDescription.displayName = DialogPrimitive.Description.displayName; ref={ref}
className={cn('text-muted-foreground text-[13px]', className)}
export { {...props}
Dialog, />
DialogPortal, ));
DialogOverlay, DialogDescription.displayName = DialogPrimitive.Description.displayName;
DialogTrigger,
DialogClose, export {
DialogContent, Dialog,
DialogHeader, DialogPortal,
DialogFooter, DialogOverlay,
DialogTitle, DialogTrigger,
DialogDescription, DialogClose,
}; DialogContent,
DialogHeader,
DialogFooter,
DialogTitle,
DialogDescription,
};
+8 -8
View File
@@ -20,7 +20,7 @@ const DropdownMenuSubTrigger = React.forwardRef<
<DropdownMenuPrimitive.SubTrigger <DropdownMenuPrimitive.SubTrigger
ref={ref} ref={ref}
className={cn( className={cn(
'focus:bg-accent data-[state=open]:bg-accent flex cursor-default items-center rounded-sm px-2 py-1.5 text-sm outline-none select-none', 'flex cursor-default items-center rounded-md px-2 py-1.5 text-[13px] outline-none select-none transition-colors focus:bg-white/[0.06] data-[state=open]:bg-white/[0.06]',
inset && 'pl-8', inset && 'pl-8',
className, className,
)} )}
@@ -39,7 +39,7 @@ const DropdownMenuSubContent = React.forwardRef<
<DropdownMenuPrimitive.SubContent <DropdownMenuPrimitive.SubContent
ref={ref} ref={ref}
className={cn( className={cn(
'bg-popover text-popover-foreground data-[state=open]:animate-in data-[state=closed]:animate-out data-[state=closed]:fade-out-0 data-[state=open]:fade-in-0 data-[state=closed]:zoom-out-95 data-[state=open]:zoom-in-95 data-[side=bottom]:slide-in-from-top-2 data-[side=left]:slide-in-from-right-2 data-[side=right]:slide-in-from-left-2 data-[side=top]:slide-in-from-bottom-2 z-50 min-w-[8rem] overflow-hidden rounded-md border p-1 shadow-lg', 'bg-popover text-popover-foreground data-[state=open]:animate-in data-[state=closed]:animate-out data-[state=closed]:fade-out-0 data-[state=open]:fade-in-0 data-[state=closed]:zoom-out-95 data-[state=open]:zoom-in-95 data-[side=bottom]:slide-in-from-top-2 data-[side=left]:slide-in-from-right-2 data-[side=right]:slide-in-from-left-2 data-[side=top]:slide-in-from-bottom-2 z-50 min-w-[8rem] overflow-hidden rounded-lg border border-white/[0.08] p-1 shadow-xl shadow-black/20',
className, className,
)} )}
{...props} {...props}
@@ -72,7 +72,7 @@ const DropdownMenuContent = React.forwardRef<
sideOffset={sideOffset} sideOffset={sideOffset}
collisionPadding={collisionPadding} collisionPadding={collisionPadding}
className={cn( className={cn(
'bg-popover text-popover-foreground z-50 max-h-[var(--radix-dropdown-menu-content-available-height)] min-w-[8rem] overflow-hidden overflow-y-auto rounded-md border p-1 shadow-md', 'bg-popover text-popover-foreground z-50 max-h-[var(--radix-dropdown-menu-content-available-height)] min-w-[8rem] overflow-hidden overflow-y-auto rounded-lg border border-white/[0.08] p-1 shadow-xl shadow-black/20',
'data-[state=open]:animate-in data-[state=closed]:animate-out data-[state=closed]:fade-out-0 data-[state=open]:fade-in-0 data-[state=closed]:zoom-out-95 data-[state=open]:zoom-in-95 data-[side=bottom]:slide-in-from-top-2 data-[side=left]:slide-in-from-right-2 data-[side=right]:slide-in-from-left-2 data-[side=top]:slide-in-from-bottom-2', 'data-[state=open]:animate-in data-[state=closed]:animate-out data-[state=closed]:fade-out-0 data-[state=open]:fade-in-0 data-[state=closed]:zoom-out-95 data-[state=open]:zoom-in-95 data-[side=bottom]:slide-in-from-top-2 data-[side=left]:slide-in-from-right-2 data-[side=right]:slide-in-from-left-2 data-[side=top]:slide-in-from-bottom-2',
className, className,
)} )}
@@ -92,7 +92,7 @@ const DropdownMenuItem = React.forwardRef<
<DropdownMenuPrimitive.Item <DropdownMenuPrimitive.Item
ref={ref} ref={ref}
className={cn( className={cn(
'focus:bg-accent focus:text-accent-foreground relative flex cursor-default items-center rounded-sm px-2 py-1.5 text-sm transition-colors outline-none select-none data-[disabled]:pointer-events-none data-[disabled]:opacity-50', 'relative flex cursor-default items-center rounded-md px-2 py-1.5 text-[13px] transition-colors outline-none select-none focus:bg-white/[0.06] focus:text-foreground data-[disabled]:pointer-events-none data-[disabled]:opacity-50',
inset && 'pl-8', inset && 'pl-8',
className, className,
)} )}
@@ -108,7 +108,7 @@ const DropdownMenuCheckboxItem = React.forwardRef<
<DropdownMenuPrimitive.CheckboxItem <DropdownMenuPrimitive.CheckboxItem
ref={ref} ref={ref}
className={cn( className={cn(
'focus:bg-accent focus:text-accent-foreground relative flex cursor-default items-center rounded-sm py-1.5 pr-2 pl-8 text-sm transition-colors outline-none select-none data-[disabled]:pointer-events-none data-[disabled]:opacity-50', 'relative flex cursor-default items-center rounded-md py-1.5 pr-2 pl-8 text-[13px] transition-colors outline-none select-none focus:bg-white/[0.06] focus:text-foreground data-[disabled]:pointer-events-none data-[disabled]:opacity-50',
className, className,
)} )}
checked={checked} checked={checked}
@@ -131,7 +131,7 @@ const DropdownMenuRadioItem = React.forwardRef<
<DropdownMenuPrimitive.RadioItem <DropdownMenuPrimitive.RadioItem
ref={ref} ref={ref}
className={cn( className={cn(
'focus:bg-accent focus:text-accent-foreground relative flex cursor-default items-center rounded-sm py-1.5 pr-2 pl-8 text-sm transition-colors outline-none select-none data-[disabled]:pointer-events-none data-[disabled]:opacity-50', 'relative flex cursor-default items-center rounded-md py-1.5 pr-2 pl-8 text-[13px] transition-colors outline-none select-none focus:bg-white/[0.06] focus:text-foreground data-[disabled]:pointer-events-none data-[disabled]:opacity-50',
className, className,
)} )}
{...props} {...props}
@@ -154,7 +154,7 @@ const DropdownMenuLabel = React.forwardRef<
>(({ className, inset, ...props }, ref) => ( >(({ className, inset, ...props }, ref) => (
<DropdownMenuPrimitive.Label <DropdownMenuPrimitive.Label
ref={ref} ref={ref}
className={cn('px-2 py-1.5 text-sm font-semibold', inset && 'pl-8', className)} className={cn('px-2 py-1.5 text-xs font-medium text-muted-foreground', inset && 'pl-8', className)}
{...props} {...props}
/> />
)); ));
@@ -166,7 +166,7 @@ const DropdownMenuSeparator = React.forwardRef<
>(({ className, ...props }, ref) => ( >(({ className, ...props }, ref) => (
<DropdownMenuPrimitive.Separator <DropdownMenuPrimitive.Separator
ref={ref} ref={ref}
className={cn('bg-muted -mx-1 my-1 h-px', className)} className={cn('-mx-1 my-1 h-px bg-white/[0.06]', className)}
{...props} {...props}
/> />
)); ));
+26 -23
View File
@@ -1,23 +1,26 @@
import * as React from 'react'; import * as React from 'react';
import { cn } from '@/lib/utils';
import { cn } from '@/lib/utils';
export interface InputProps extends React.InputHTMLAttributes<HTMLInputElement> {}
const Input = React.forwardRef<HTMLInputElement, React.ComponentProps<'input'>>(
const Input = React.forwardRef<HTMLInputElement, InputProps>( ({ className, type, ...props }, ref) => {
({ className, type, ...props }, ref) => { return (
return ( <input
<input type={type}
type={type} className={cn(
className={cn( 'flex h-9 w-full rounded-lg border border-white/[0.08] bg-muted/50 px-3 py-1 text-[13px] text-foreground shadow-none transition-colors duration-200',
'border-input placeholder:text-muted-foreground focus-visible:ring-ring focus-visible:border-primary flex h-10 w-full rounded-md border bg-background px-3 py-2 text-sm transition-colors file:border-0 file:bg-transparent file:text-sm file:font-medium focus-visible:outline-none disabled:cursor-not-allowed disabled:opacity-50', 'file:border-0 file:bg-transparent file:text-sm file:font-medium file:text-foreground',
className, 'placeholder:text-muted-foreground',
)} 'focus-visible:outline-none focus-visible:border-accent/50 focus-visible:ring-1 focus-visible:ring-accent/20',
ref={ref} 'disabled:cursor-not-allowed disabled:opacity-50',
{...props} className,
/> )}
); ref={ref}
}, {...props}
); />
Input.displayName = 'Input'; );
},
export { Input }; );
Input.displayName = 'Input';
export { Input };
+18 -18
View File
@@ -1,18 +1,18 @@
import * as React from 'react'; import * as React from 'react';
import * as LabelPrimitive from '@radix-ui/react-label'; import * as LabelPrimitive from '@radix-ui/react-label';
import { cva, type VariantProps } from 'class-variance-authority'; import { cva, type VariantProps } from 'class-variance-authority';
import { cn } from '@/lib/utils'; import { cn } from '@/lib/utils';
const labelVariants = cva( const labelVariants = cva(
'text-sm font-medium leading-none peer-disabled:cursor-not-allowed peer-disabled:opacity-70', 'text-[13px] font-medium leading-none peer-disabled:cursor-not-allowed peer-disabled:opacity-70',
); );
const Label = React.forwardRef< const Label = React.forwardRef<
React.ElementRef<typeof LabelPrimitive.Root>, React.ElementRef<typeof LabelPrimitive.Root>,
React.ComponentPropsWithoutRef<typeof LabelPrimitive.Root> & VariantProps<typeof labelVariants> React.ComponentPropsWithoutRef<typeof LabelPrimitive.Root> & VariantProps<typeof labelVariants>
>(({ className, ...props }, ref) => ( >(({ className, ...props }, ref) => (
<LabelPrimitive.Root ref={ref} className={cn(labelVariants(), className)} {...props} /> <LabelPrimitive.Root ref={ref} className={cn(labelVariants(), className)} {...props} />
)); ));
Label.displayName = LabelPrimitive.Root.displayName; Label.displayName = LabelPrimitive.Root.displayName;
export { Label }; export { Label };
+161 -153
View File
@@ -1,153 +1,161 @@
'use client'; 'use client';
import * as React from 'react'; import * as React from 'react';
import * as SelectPrimitive from '@radix-ui/react-select'; import * as SelectPrimitive from '@radix-ui/react-select';
import { Check, ChevronDown, ChevronUp } from 'lucide-react'; import { Check, ChevronDown, ChevronUp } from 'lucide-react';
import { cn } from '@/lib/utils'; import { cn } from '@/lib/utils';
const Select = SelectPrimitive.Root; const Select = SelectPrimitive.Root;
const SelectGroup = SelectPrimitive.Group; const SelectGroup = SelectPrimitive.Group;
const SelectValue = SelectPrimitive.Value; const SelectValue = SelectPrimitive.Value;
const SelectTrigger = React.forwardRef< const SelectTrigger = React.forwardRef<
React.ElementRef<typeof SelectPrimitive.Trigger>, React.ElementRef<typeof SelectPrimitive.Trigger>,
React.ComponentPropsWithoutRef<typeof SelectPrimitive.Trigger> React.ComponentPropsWithoutRef<typeof SelectPrimitive.Trigger>
>(({ className, children, ...props }, ref) => ( >(({ className, children, ...props }, ref) => (
<SelectPrimitive.Trigger <SelectPrimitive.Trigger
ref={ref} ref={ref}
className={cn( className={cn(
'border-input bg-background ring-offset-background placeholder:text-muted-foreground focus:ring-ring flex h-10 w-full items-center justify-between rounded-md border px-3 py-2 text-sm focus:ring-2 focus:ring-offset-2 focus:outline-none disabled:cursor-not-allowed disabled:opacity-50 [&>span]:line-clamp-1', 'flex h-9 w-full items-center justify-between rounded-lg border border-white/[0.08] bg-muted/50 px-3 py-2 text-[13px] text-foreground transition-colors duration-200',
className, 'placeholder:text-muted-foreground',
)} 'focus:outline-none focus:border-accent/50 focus:ring-1 focus:ring-accent/20',
{...props} 'disabled:cursor-not-allowed disabled:opacity-50',
> '[&>span]:line-clamp-1',
{children} className,
<SelectPrimitive.Icon asChild> )}
<ChevronDown className="h-4 w-4 opacity-50" /> {...props}
</SelectPrimitive.Icon> >
</SelectPrimitive.Trigger> {children}
)); <SelectPrimitive.Icon asChild>
SelectTrigger.displayName = SelectPrimitive.Trigger.displayName; <ChevronDown className="h-3.5 w-3.5 opacity-40" />
</SelectPrimitive.Icon>
const SelectScrollUpButton = React.forwardRef< </SelectPrimitive.Trigger>
React.ElementRef<typeof SelectPrimitive.ScrollUpButton>, ));
React.ComponentPropsWithoutRef<typeof SelectPrimitive.ScrollUpButton> SelectTrigger.displayName = SelectPrimitive.Trigger.displayName;
>(({ className, ...props }, ref) => (
<SelectPrimitive.ScrollUpButton const SelectScrollUpButton = React.forwardRef<
ref={ref} React.ElementRef<typeof SelectPrimitive.ScrollUpButton>,
className={cn('flex cursor-default items-center justify-center py-1', className)} React.ComponentPropsWithoutRef<typeof SelectPrimitive.ScrollUpButton>
{...props} >(({ className, ...props }, ref) => (
> <SelectPrimitive.ScrollUpButton
<ChevronUp className="h-4 w-4" /> ref={ref}
</SelectPrimitive.ScrollUpButton> className={cn('flex cursor-default items-center justify-center py-1', className)}
)); {...props}
SelectScrollUpButton.displayName = SelectPrimitive.ScrollUpButton.displayName; >
<ChevronUp className="h-3.5 w-3.5" />
const SelectScrollDownButton = React.forwardRef< </SelectPrimitive.ScrollUpButton>
React.ElementRef<typeof SelectPrimitive.ScrollDownButton>, ));
React.ComponentPropsWithoutRef<typeof SelectPrimitive.ScrollDownButton> SelectScrollUpButton.displayName = SelectPrimitive.ScrollUpButton.displayName;
>(({ className, ...props }, ref) => (
<SelectPrimitive.ScrollDownButton const SelectScrollDownButton = React.forwardRef<
ref={ref} React.ElementRef<typeof SelectPrimitive.ScrollDownButton>,
className={cn('flex cursor-default items-center justify-center py-1', className)} React.ComponentPropsWithoutRef<typeof SelectPrimitive.ScrollDownButton>
{...props} >(({ className, ...props }, ref) => (
> <SelectPrimitive.ScrollDownButton
<ChevronDown className="h-4 w-4" /> ref={ref}
</SelectPrimitive.ScrollDownButton> className={cn('flex cursor-default items-center justify-center py-1', className)}
)); {...props}
SelectScrollDownButton.displayName = SelectPrimitive.ScrollDownButton.displayName; >
<ChevronDown className="h-3.5 w-3.5" />
const SelectContent = React.forwardRef< </SelectPrimitive.ScrollDownButton>
React.ElementRef<typeof SelectPrimitive.Content>, ));
React.ComponentPropsWithoutRef<typeof SelectPrimitive.Content> SelectScrollDownButton.displayName = SelectPrimitive.ScrollDownButton.displayName;
>(({ className, children, position = 'popper', ...props }, ref) => (
<SelectPrimitive.Portal> const SelectContent = React.forwardRef<
<SelectPrimitive.Content React.ElementRef<typeof SelectPrimitive.Content>,
ref={ref} React.ComponentPropsWithoutRef<typeof SelectPrimitive.Content>
className={cn( >(({ className, children, position = 'popper', ...props }, ref) => (
'bg-popover text-popover-foreground data-[state=open]:animate-in data-[state=closed]:animate-out data-[state=closed]:fade-out-0 data-[state=open]:fade-in-0 data-[state=closed]:zoom-out-95 data-[state=open]:zoom-in-95 data-[side=bottom]:slide-in-from-top-2 data-[side=left]:slide-in-from-right-2 data-[side=right]:slide-in-from-left-2 data-[side=top]:slide-in-from-bottom-2 relative z-50 max-h-96 min-w-[8rem] overflow-hidden rounded-md border shadow-md', <SelectPrimitive.Portal>
position === 'popper' && <SelectPrimitive.Content
'data-[side=bottom]:translate-y-1 data-[side=left]:-translate-x-1 data-[side=right]:translate-x-1 data-[side=top]:-translate-y-1', ref={ref}
className, className={cn(
)} 'relative z-50 max-h-96 min-w-[8rem] overflow-hidden rounded-lg border border-white/[0.08] bg-popover text-popover-foreground shadow-xl shadow-black/20',
position={position} 'data-[state=open]:animate-in data-[state=closed]:animate-out data-[state=closed]:fade-out-0 data-[state=open]:fade-in-0 data-[state=closed]:zoom-out-95 data-[state=open]:zoom-in-95',
{...props} 'data-[side=bottom]:slide-in-from-top-2 data-[side=left]:slide-in-from-right-2 data-[side=right]:slide-in-from-left-2 data-[side=top]:slide-in-from-bottom-2',
> position === 'popper' &&
<SelectScrollUpButton /> 'data-[side=bottom]:translate-y-1 data-[side=left]:-translate-x-1 data-[side=right]:translate-x-1 data-[side=top]:-translate-y-1',
<SelectPrimitive.Viewport className,
className={cn( )}
'p-1', position={position}
position === 'popper' && {...props}
'h-[var(--radix-select-trigger-height)] w-full min-w-[var(--radix-select-trigger-width)]', >
)} <SelectScrollUpButton />
> <SelectPrimitive.Viewport
{children} className={cn(
</SelectPrimitive.Viewport> 'p-1',
<SelectScrollDownButton /> position === 'popper' &&
</SelectPrimitive.Content> 'h-[var(--radix-select-trigger-height)] w-full min-w-[var(--radix-select-trigger-width)]',
</SelectPrimitive.Portal> )}
)); >
SelectContent.displayName = SelectPrimitive.Content.displayName; {children}
</SelectPrimitive.Viewport>
const SelectLabel = React.forwardRef< <SelectScrollDownButton />
React.ElementRef<typeof SelectPrimitive.Label>, </SelectPrimitive.Content>
React.ComponentPropsWithoutRef<typeof SelectPrimitive.Label> </SelectPrimitive.Portal>
>(({ className, ...props }, ref) => ( ));
<SelectPrimitive.Label SelectContent.displayName = SelectPrimitive.Content.displayName;
ref={ref}
className={cn('py-1.5 pr-2 pl-8 text-sm font-semibold', className)} const SelectLabel = React.forwardRef<
{...props} React.ElementRef<typeof SelectPrimitive.Label>,
/> React.ComponentPropsWithoutRef<typeof SelectPrimitive.Label>
)); >(({ className, ...props }, ref) => (
SelectLabel.displayName = SelectPrimitive.Label.displayName; <SelectPrimitive.Label
ref={ref}
const SelectItem = React.forwardRef< className={cn('py-1.5 pr-2 pl-8 text-xs font-medium text-muted-foreground', className)}
React.ElementRef<typeof SelectPrimitive.Item>, {...props}
React.ComponentPropsWithoutRef<typeof SelectPrimitive.Item> />
>(({ className, children, ...props }, ref) => ( ));
<SelectPrimitive.Item SelectLabel.displayName = SelectPrimitive.Label.displayName;
ref={ref}
className={cn( const SelectItem = React.forwardRef<
'focus:bg-accent focus:text-accent-foreground relative flex w-full cursor-default items-center rounded-sm py-1.5 pr-2 pl-8 text-sm outline-none select-none data-[disabled]:pointer-events-none data-[disabled]:opacity-50', React.ElementRef<typeof SelectPrimitive.Item>,
className, React.ComponentPropsWithoutRef<typeof SelectPrimitive.Item>
)} >(({ className, children, ...props }, ref) => (
{...props} <SelectPrimitive.Item
> ref={ref}
<span className="absolute left-2 flex h-3.5 w-3.5 items-center justify-center"> className={cn(
<SelectPrimitive.ItemIndicator> 'relative flex w-full cursor-default items-center rounded-md py-1.5 pr-2 pl-8 text-[13px] outline-none select-none transition-colors',
<Check className="h-4 w-4" /> 'focus:bg-white/[0.06] focus:text-foreground',
</SelectPrimitive.ItemIndicator> 'data-[disabled]:pointer-events-none data-[disabled]:opacity-50',
</span> className,
)}
<SelectPrimitive.ItemText>{children}</SelectPrimitive.ItemText> {...props}
</SelectPrimitive.Item> >
)); <span className="absolute left-2 flex h-3.5 w-3.5 items-center justify-center">
SelectItem.displayName = SelectPrimitive.Item.displayName; <SelectPrimitive.ItemIndicator>
<Check className="h-3.5 w-3.5 text-accent" />
const SelectSeparator = React.forwardRef< </SelectPrimitive.ItemIndicator>
React.ElementRef<typeof SelectPrimitive.Separator>, </span>
React.ComponentPropsWithoutRef<typeof SelectPrimitive.Separator>
>(({ className, ...props }, ref) => ( <SelectPrimitive.ItemText>{children}</SelectPrimitive.ItemText>
<SelectPrimitive.Separator </SelectPrimitive.Item>
ref={ref} ));
className={cn('bg-muted -mx-1 my-1 h-px', className)} SelectItem.displayName = SelectPrimitive.Item.displayName;
{...props}
/> const SelectSeparator = React.forwardRef<
)); React.ElementRef<typeof SelectPrimitive.Separator>,
SelectSeparator.displayName = SelectPrimitive.Separator.displayName; React.ComponentPropsWithoutRef<typeof SelectPrimitive.Separator>
>(({ className, ...props }, ref) => (
export { <SelectPrimitive.Separator
Select, ref={ref}
SelectGroup, className={cn('-mx-1 my-1 h-px bg-white/[0.06]', className)}
SelectValue, {...props}
SelectTrigger, />
SelectContent, ));
SelectLabel, SelectSeparator.displayName = SelectPrimitive.Separator.displayName;
SelectItem,
SelectSeparator, export {
SelectScrollUpButton, Select,
SelectScrollDownButton, SelectGroup,
}; SelectValue,
SelectTrigger,
SelectContent,
SelectLabel,
SelectItem,
SelectSeparator,
SelectScrollUpButton,
SelectScrollDownButton,
};
+12 -7
View File
@@ -1,7 +1,12 @@
import { cn } from '@/lib/utils'; import { cn } from '@/lib/utils';
function Skeleton({ className, ...props }: React.HTMLAttributes<HTMLDivElement>) { function Skeleton({ className, ...props }: React.HTMLAttributes<HTMLDivElement>) {
return <div className={cn('bg-muted animate-pulse rounded-md', className)} {...props} />; return (
} <div
className={cn('animate-pulse rounded-lg bg-white/[0.04]', className)}
export { Skeleton }; {...props}
/>
);
}
export { Skeleton };
+31 -26
View File
@@ -1,26 +1,31 @@
import * as React from 'react'; import * as React from 'react';
import * as SwitchPrimitive from '@radix-ui/react-switch'; import * as SwitchPrimitives from '@radix-ui/react-switch';
import { cn } from '@/lib/utils';
import { cn } from '@/lib/utils';
const Switch = React.forwardRef<
React.ElementRef<typeof SwitchPrimitive.Root>, const Switch = React.forwardRef<
React.ComponentPropsWithoutRef<typeof SwitchPrimitive.Root> React.ElementRef<typeof SwitchPrimitives.Root>,
>(({ className, ...props }, ref) => ( React.ComponentPropsWithoutRef<typeof SwitchPrimitives.Root>
<SwitchPrimitive.Root >(({ className, ...props }, ref) => (
className={cn( <SwitchPrimitives.Root
'peer focus-visible:ring-ring focus-visible:ring-offset-background data-[state=checked]:bg-primary data-[state=unchecked]:bg-input inline-flex h-5 w-9 shrink-0 cursor-pointer items-center rounded-full border-2 border-transparent shadow-sm transition-colors focus-visible:ring-2 focus-visible:ring-offset-2 focus-visible:outline-none disabled:cursor-not-allowed disabled:opacity-50', className={cn(
className, 'peer inline-flex h-5 w-9 shrink-0 cursor-pointer items-center rounded-full border border-transparent shadow-none transition-colors duration-200',
)} 'focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-ring/30 focus-visible:ring-offset-2 focus-visible:ring-offset-background',
{...props} 'disabled:cursor-not-allowed disabled:opacity-50',
ref={ref} 'data-[state=checked]:bg-accent data-[state=unchecked]:bg-white/[0.1]',
> className,
<SwitchPrimitive.Thumb )}
className={cn( {...props}
'bg-background pointer-events-none block h-4 w-4 rounded-full shadow-lg ring-0 transition-transform data-[state=checked]:translate-x-4 data-[state=unchecked]:translate-x-0', ref={ref}
)} >
/> <SwitchPrimitives.Thumb
</SwitchPrimitive.Root> className={cn(
)); 'pointer-events-none block h-4 w-4 rounded-full bg-white shadow-sm ring-0 transition-transform duration-200',
Switch.displayName = SwitchPrimitive.Root.displayName; 'data-[state=checked]:translate-x-[18px] data-[state=unchecked]:translate-x-0.5',
)}
export { Switch }; />
</SwitchPrimitives.Root>
));
Switch.displayName = SwitchPrimitives.Root.displayName;
export { Switch };
+56 -52
View File
@@ -1,52 +1,56 @@
import * as React from 'react'; import * as React from 'react';
import * as TabsPrimitive from '@radix-ui/react-tabs'; import * as TabsPrimitive from '@radix-ui/react-tabs';
import { cn } from '@/lib/utils'; import { cn } from '@/lib/utils';
const Tabs = TabsPrimitive.Root; const Tabs = TabsPrimitive.Root;
const TabsList = React.forwardRef< const TabsList = React.forwardRef<
React.ElementRef<typeof TabsPrimitive.List>, React.ElementRef<typeof TabsPrimitive.List>,
React.ComponentPropsWithoutRef<typeof TabsPrimitive.List> React.ComponentPropsWithoutRef<typeof TabsPrimitive.List>
>(({ className, ...props }, ref) => ( >(({ className, ...props }, ref) => (
<TabsPrimitive.List <TabsPrimitive.List
ref={ref} ref={ref}
className={cn( className={cn(
'bg-muted/30 text-muted-foreground inline-flex h-10 items-center justify-center rounded-md p-1 border border-white/5', 'bg-muted/40 text-muted-foreground inline-flex h-9 items-center justify-center rounded-lg p-1 border border-white/[0.04]',
className, className,
)} )}
{...props} {...props}
/> />
)); ));
TabsList.displayName = TabsPrimitive.List.displayName; TabsList.displayName = TabsPrimitive.List.displayName;
const TabsTrigger = React.forwardRef< const TabsTrigger = React.forwardRef<
React.ElementRef<typeof TabsPrimitive.Trigger>, React.ElementRef<typeof TabsPrimitive.Trigger>,
React.ComponentPropsWithoutRef<typeof TabsPrimitive.Trigger> React.ComponentPropsWithoutRef<typeof TabsPrimitive.Trigger>
>(({ className, ...props }, ref) => ( >(({ className, ...props }, ref) => (
<TabsPrimitive.Trigger <TabsPrimitive.Trigger
ref={ref} ref={ref}
className={cn( className={cn(
'ring-offset-background focus-visible:ring-ring data-[state=active]:bg-background data-[state=active]:text-foreground inline-flex items-center justify-center rounded-sm px-3 py-1.5 text-sm font-medium whitespace-nowrap transition-all focus-visible:ring-2 focus-visible:ring-offset-2 focus-visible:outline-none disabled:pointer-events-none disabled:opacity-50 data-[state=active]:shadow-none data-[state=active]:border data-[state=active]:border-white/10', 'inline-flex items-center justify-center rounded-md px-3 py-1 text-[13px] font-medium whitespace-nowrap transition-all duration-200',
className, 'text-muted-foreground hover:text-foreground',
)} 'data-[state=active]:bg-background data-[state=active]:text-foreground data-[state=active]:shadow-sm data-[state=active]:border data-[state=active]:border-white/[0.08]',
{...props} 'focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-ring/30',
/> 'disabled:pointer-events-none disabled:opacity-50',
)); className,
TabsTrigger.displayName = TabsPrimitive.Trigger.displayName; )}
{...props}
const TabsContent = React.forwardRef< />
React.ElementRef<typeof TabsPrimitive.Content>, ));
React.ComponentPropsWithoutRef<typeof TabsPrimitive.Content> TabsTrigger.displayName = TabsPrimitive.Trigger.displayName;
>(({ className, ...props }, ref) => (
<TabsPrimitive.Content const TabsContent = React.forwardRef<
ref={ref} React.ElementRef<typeof TabsPrimitive.Content>,
className={cn( React.ComponentPropsWithoutRef<typeof TabsPrimitive.Content>
'ring-offset-background focus-visible:ring-ring mt-2 focus-visible:ring-2 focus-visible:ring-offset-2 focus-visible:outline-none', >(({ className, ...props }, ref) => (
className, <TabsPrimitive.Content
)} ref={ref}
{...props} className={cn(
/> 'mt-3 animate-in focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-ring/30',
)); className,
TabsContent.displayName = TabsPrimitive.Content.displayName; )}
{...props}
export { Tabs, TabsList, TabsTrigger, TabsContent }; />
));
TabsContent.displayName = TabsPrimitive.Content.displayName;
export { Tabs, TabsList, TabsTrigger, TabsContent };
+31 -28
View File
@@ -1,28 +1,31 @@
import * as React from 'react'; import * as React from 'react';
import * as TooltipPrimitive from '@radix-ui/react-tooltip'; import * as TooltipPrimitive from '@radix-ui/react-tooltip';
import { cn } from '@/lib/utils'; import { cn } from '@/lib/utils';
const TooltipProvider = TooltipPrimitive.Provider; const TooltipProvider = TooltipPrimitive.Provider;
const Tooltip = TooltipPrimitive.Root; const Tooltip = TooltipPrimitive.Root;
const TooltipTrigger = TooltipPrimitive.Trigger; const TooltipTrigger = TooltipPrimitive.Trigger;
const TooltipContent = React.forwardRef< const TooltipContent = React.forwardRef<
React.ElementRef<typeof TooltipPrimitive.Content>, React.ElementRef<typeof TooltipPrimitive.Content>,
React.ComponentPropsWithoutRef<typeof TooltipPrimitive.Content> React.ComponentPropsWithoutRef<typeof TooltipPrimitive.Content>
>(({ className, sideOffset = 4, ...props }, ref) => ( >(({ className, sideOffset = 4, ...props }, ref) => (
<TooltipPrimitive.Content <TooltipPrimitive.Content
ref={ref} ref={ref}
sideOffset={sideOffset} sideOffset={sideOffset}
className={cn( className={cn(
'bg-popover text-popover-foreground animate-in fade-in-0 zoom-in-95 data-[state=closed]:animate-out data-[state=closed]:fade-out-0 data-[state=closed]:zoom-out-95 data-[side=bottom]:slide-in-from-top-2 data-[side=left]:slide-in-from-right-2 data-[side=right]:slide-in-from-left-2 data-[side=top]:slide-in-from-bottom-2 z-50 origin-[--radix-tooltip-content-transform-origin] overflow-hidden rounded-md border px-3 py-1.5 text-sm shadow-md', 'z-50 overflow-hidden rounded-lg border border-white/[0.08] bg-popover px-3 py-1.5 text-xs text-popover-foreground shadow-lg shadow-black/20',
className, 'animate-in fade-in-0 zoom-in-95',
)} 'data-[state=closed]:animate-out data-[state=closed]:fade-out-0 data-[state=closed]:zoom-out-95',
{...props} 'data-[side=bottom]:slide-in-from-top-2 data-[side=left]:slide-in-from-right-2 data-[side=right]:slide-in-from-left-2 data-[side=top]:slide-in-from-bottom-2',
/> className,
)); )}
TooltipContent.displayName = TooltipPrimitive.Content.displayName; {...props}
/>
export { Tooltip, TooltipTrigger, TooltipContent, TooltipProvider }; ));
TooltipContent.displayName = TooltipPrimitive.Content.displayName;
export { Tooltip, TooltipTrigger, TooltipContent, TooltipProvider };
+70 -67
View File
@@ -1,67 +1,70 @@
import React from 'react'; import React from 'react';
import { Card, CardContent } from '@/components/ui/card'; import { Skeleton } from '@/components/ui/skeleton';
import { Skeleton } from '@/components/ui/skeleton'; import { Button } from '@/components/ui/button';
import { Button } from '@/components/ui/button'; import { AlertTriangle, BarChart3, RotateCcw } from 'lucide-react';
import { AlertTriangle, BarChart3, RotateCcw } from 'lucide-react';
interface ChartStatesProps {
interface ChartStatesProps { isLoading: boolean;
isLoading: boolean; error: Error | null;
error: Error | null; isEmpty: boolean;
isEmpty: boolean; onRetry: () => void;
onRetry: () => void; title: string;
title: string; emptyTitle: string;
emptyTitle: string; emptyDescription: string;
emptyDescription: string; errorMessage: string;
errorMessage: string; retryLabel: string;
retryLabel: string; children: React.ReactNode;
children: React.ReactNode; }
}
export const ChartCard: React.FC<ChartStatesProps> = ({
export const ChartCard: React.FC<ChartStatesProps> = ({ isLoading,
isLoading, error,
error, isEmpty,
isEmpty, onRetry,
onRetry, title,
title, emptyTitle,
emptyTitle, emptyDescription,
emptyDescription, errorMessage,
errorMessage, retryLabel,
retryLabel, children,
children, }) => {
}) => { return (
return ( <div className="flex flex-col rounded-xl border border-white/[0.06] bg-card">
<Card className="border border-white/5 bg-background flex flex-col"> <div className="flex flex-col p-5">
<CardContent className="flex flex-1 flex-col p-4"> <div className="mb-4 flex items-center justify-between">
<div className="mb-3 flex items-center justify-between"> <h3 className="text-[13px] font-semibold tracking-tight text-foreground">{title}</h3>
<h3 className="text-foreground text-sm font-semibold tracking-tight">{title}</h3> </div>
</div> <div className="flex-1">
<div className="flex-1"> {isLoading ? (
{isLoading ? ( <div className="flex h-[280px] flex-col gap-3">
<div className="flex h-[280px] flex-col gap-3"> <Skeleton className="h-full w-full rounded-lg" />
<Skeleton className="h-full w-full" /> </div>
</div> ) : error ? (
) : error ? ( <div className="flex h-[280px] flex-col items-center justify-center gap-3">
<div className="flex h-[280px] flex-col items-center justify-center gap-3"> <div className="flex h-10 w-10 items-center justify-center rounded-lg bg-destructive/10">
<AlertTriangle className="text-destructive h-5 w-5" /> <AlertTriangle className="h-5 w-5 text-destructive" />
<p className="text-muted-foreground text-sm">{errorMessage}</p> </div>
<Button variant="outline" size="sm" onClick={onRetry}> <p className="text-muted-foreground text-[13px]">{errorMessage}</p>
<RotateCcw className="mr-1.5 h-3.5 w-3.5" /> <Button variant="outline" size="sm" onClick={onRetry}>
{retryLabel} <RotateCcw className="mr-1.5 h-3.5 w-3.5" />
</Button> {retryLabel}
</div> </Button>
) : isEmpty ? ( </div>
<div className="flex h-[280px] flex-col items-center justify-center gap-2"> ) : isEmpty ? (
<BarChart3 className="text-muted-foreground/50 h-6 w-6" /> <div className="flex h-[280px] flex-col items-center justify-center gap-2">
<p className="text-foreground text-sm font-medium">{emptyTitle}</p> <div className="flex h-10 w-10 items-center justify-center rounded-lg bg-white/[0.04]">
<p className="text-muted-foreground max-w-[240px] text-center text-xs"> <BarChart3 className="h-5 w-5 text-muted-foreground/60" />
{emptyDescription} </div>
</p> <p className="text-[13px] font-medium text-foreground">{emptyTitle}</p>
</div> <p className="text-muted-foreground max-w-[240px] text-center text-xs">
) : ( {emptyDescription}
children </p>
)} </div>
</div> ) : (
</CardContent> children
</Card> )}
); </div>
}; </div>
</div>
);
};
+35 -13
View File
@@ -1,17 +1,21 @@
export const COLOR_PROMPT = 'var(--chart-2)'; /** Premium chart palette — refined for dark mode contrast */
export const COLOR_COMPLETION = 'var(--chart-1)'; export const COLOR_PROMPT = '#60a5fa'; // blue-400
export const COLOR_TOTAL = 'var(--chart-3)'; export const COLOR_COMPLETION = '#34d399'; // emerald-400
export const COLOR_TOTAL = '#fbbf24'; // amber-400
export const PIE_COLORS = [ export const PIE_COLORS = [
'var(--chart-1)', '#60a5fa', '#34d399', '#fbbf24', '#a78bfa', '#f472b6',
'var(--chart-2)', '#38bdf8', '#4ade80', '#facc15', '#c084fc',
'var(--chart-3)',
'var(--chart-4)',
'var(--chart-5)',
'var(--chart-1)',
'var(--chart-2)',
'var(--chart-3)',
]; ];
export const MODEL_COLORS: Record<string, string> = {
'gemini-3-flash': '#60a5fa',
'gemini-3.1-pro-low': '#818cf8',
'gemini-3.1-pro-high': '#a78bfa',
'claude-sonnet-4-6-thinking': '#f472b6',
'claude-opus-4-6-thinking': '#fb7185',
};
const colorMap: Record<string, string> = { const colorMap: Record<string, string> = {
Prompt: COLOR_PROMPT, Prompt: COLOR_PROMPT,
Completion: COLOR_COMPLETION, Completion: COLOR_COMPLETION,
@@ -22,7 +26,7 @@ export function BarTooltip({ indexValue, data }: any) {
const keys = Object.keys(data).filter((k: string) => k !== 'name' && colorMap[k]); const keys = Object.keys(data).filter((k: string) => k !== 'name' && colorMap[k]);
return ( return (
<div className="bg-popover border-border rounded-md border px-3 py-2 shadow-md"> <div className="bg-popover rounded-lg border border-white/[0.06] px-3 py-2 shadow-md">
<p className="text-foreground mb-1.5 text-xs font-medium">{indexValue}</p> <p className="text-foreground mb-1.5 text-xs font-medium">{indexValue}</p>
<div className="flex flex-col gap-1"> <div className="flex flex-col gap-1">
{keys.map((key: string) => ( {keys.map((key: string) => (
@@ -44,7 +48,7 @@ export function BarTooltip({ indexValue, data }: any) {
export function PieTooltip({ datum }: any) { export function PieTooltip({ datum }: any) {
return ( return (
<div className="bg-popover border-border rounded-md border px-3 py-2 shadow-md"> <div className="bg-popover rounded-lg border border-white/[0.06] px-3 py-2 shadow-md">
<div className="flex items-center gap-2 text-xs"> <div className="flex items-center gap-2 text-xs">
<span <span
className="h-2 w-2 rounded-full" className="h-2 w-2 rounded-full"
@@ -58,3 +62,21 @@ export function PieTooltip({ datum }: any) {
</div> </div>
); );
} }
export function LineTooltip({ point }: any) {
return (
<div className="bg-popover rounded-lg border border-white/[0.06] px-3 py-2 shadow-md">
<p className="text-foreground mb-1 text-xs font-medium">{point.data.xFormatted ?? point.data.x}</p>
<div className="flex items-center gap-2 text-xs">
<span
className="h-2 w-2 rounded-full"
style={{ backgroundColor: point.serieColor }}
/>
<span className="text-muted-foreground">{point.serieId}:</span>
<span className="text-foreground font-mono font-medium">
{typeof point.data.y === 'number' ? point.data.y.toLocaleString() : point.data.y}
</span>
</div>
</div>
);
}
+170 -71
View File
@@ -1,71 +1,170 @@
import React from 'react'; import React from 'react';
import { Card, CardContent } from '@/components/ui/card'; import { Skeleton } from '@/components/ui/skeleton';
import { Skeleton } from '@/components/ui/skeleton'; import { cn } from '@/lib/utils';
import { cn } from '@/lib/utils'; import { ArrowUpRight, ArrowDownRight, Minus } from 'lucide-react';
import type { LucideIcon } from 'lucide-react'; import type { LucideIcon } from 'lucide-react';
interface StatCardProps { interface TrendInfo {
label: string; /** Percentage change vs previous period */
value: number | string; value: number;
icon: LucideIcon; direction: 'up' | 'down' | 'neutral';
isLoading?: boolean; }
className?: string;
accent?: 'blue' | 'green' | 'amber' | 'slate' | 'purple'; interface StatCardProps {
} label: string;
value: number | string;
const ACCENT_MAP = { icon: LucideIcon;
blue: 'text-blue-400', isLoading?: boolean;
green: 'text-emerald-400', className?: string;
amber: 'text-amber-400', accent?: 'blue' | 'green' | 'amber' | 'slate' | 'purple';
slate: 'text-slate-400', trend?: TrendInfo;
purple: 'text-purple-400', sparkline?: number[];
}; }
const BG_MAP = { const ACCENT_ICON = {
blue: 'bg-blue-500/5', blue: 'text-blue-400',
green: 'bg-emerald-500/5', green: 'text-emerald-400',
amber: 'bg-amber-500/5', amber: 'text-amber-400',
slate: 'bg-slate-500/5', slate: 'text-muted-foreground',
purple: 'bg-purple-500/5', purple: 'text-violet-400',
}; };
export const StatCard: React.FC<StatCardProps> = ({ const ACCENT_BG = {
label, blue: 'bg-blue-500/8',
value, green: 'bg-emerald-500/8',
icon: Icon, amber: 'bg-amber-500/8',
isLoading, slate: 'bg-white/[0.04]',
className, purple: 'bg-violet-500/8',
accent = 'slate', };
}) => {
return ( /**
<Card * Render a tiny inline SVG sparkline from an array of values.
className={cn( * Normalized to fit within a 60x20 viewBox.
'border border-white/5 bg-background hover:bg-muted/10 backdrop-blur-none transition-colors', */
className, function Sparkline({ data, accent }: { data: number[]; accent: string }) {
)} if (data.length < 2) {
> return null;
<CardContent className="flex items-center justify-between p-4"> }
<div className="flex flex-col gap-1"> const max = Math.max(...data);
<span className="text-muted-foreground text-xs font-medium tracking-wide uppercase"> const min = Math.min(...data);
{label} const range = max - min || 1;
</span> const w = 60;
{isLoading ? ( const h = 20;
<Skeleton className="h-7 w-24" /> const pad = 1;
) : (
<span className="text-foreground font-mono text-2xl font-semibold tracking-tight"> const points = data.map((v, i) => {
{typeof value === 'number' ? value.toLocaleString() : value} const x = (i / (data.length - 1)) * w;
</span> const y = h - pad - ((v - min) / range) * (h - pad * 2);
)} return `${x},${y}`;
</div> });
<div
className={cn( const strokeColor =
'flex h-9 w-9 shrink-0 items-center justify-center rounded-md', accent === 'blue'
BG_MAP[accent], ? '#60a5fa'
)} : accent === 'green'
> ? '#34d399'
<Icon className={cn('h-4 w-4', ACCENT_MAP[accent])} /> : accent === 'amber'
</div> ? '#fbbf24'
</CardContent> : accent === 'purple'
</Card> ? '#a78bfa'
); : '#6b7280';
};
return (
<svg
viewBox={`0 0 ${w} ${h}`}
className="h-5 w-[60px] overflow-visible"
preserveAspectRatio="none"
>
<defs>
<linearGradient id={`sparkGrad-${accent}`} x1="0" y1="0" x2="0" y2="1">
<stop offset="0%" stopColor={strokeColor} stopOpacity="0.3" />
<stop offset="100%" stopColor={strokeColor} stopOpacity="0" />
</linearGradient>
</defs>
{/* Area fill */}
<polygon
points={`0,${h} ${points.join(' ')} ${w},${h}`}
fill={`url(#sparkGrad-${accent})`}
/>
{/* Line */}
<polyline
points={points.join(' ')}
fill="none"
stroke={strokeColor}
strokeWidth="1.5"
strokeLinecap="round"
strokeLinejoin="round"
/>
</svg>
);
}
function TrendBadge({ trend }: { trend: TrendInfo }) {
const isUp = trend.direction === 'up';
const isNeutral = trend.direction === 'neutral' || trend.value === 0;
const Icon = isNeutral ? Minus : isUp ? ArrowUpRight : ArrowDownRight;
const color = isNeutral
? 'text-muted-foreground'
: isUp
? 'text-emerald-400'
: 'text-rose-400';
return (
<div className={cn('flex items-center gap-0.5 text-[10px] font-medium', color)}>
<Icon className="h-3 w-3" />
<span className="tabular-nums">{Math.abs(trend.value).toFixed(1)}%</span>
</div>
);
}
export const StatCard: React.FC<StatCardProps> = ({
label,
value,
icon: Icon,
isLoading,
className,
accent = 'slate',
trend,
sparkline,
}) => {
return (
<div
className={cn(
'group relative rounded-xl border border-white/[0.06] bg-card p-4 transition-all duration-200',
'hover:border-white/[0.1] hover:bg-white/[0.02]',
className,
)}
>
<div className="flex items-start justify-between">
<div className="flex flex-col gap-1.5">
<span className="text-muted-foreground text-[11px] font-medium tracking-wider uppercase">
{label}
</span>
{isLoading ? (
<Skeleton className="h-7 w-20" />
) : (
<div className="flex items-baseline gap-2">
<span className="text-foreground font-mono text-xl font-semibold tracking-tight tabular-nums">
{typeof value === 'number' ? value.toLocaleString() : value}
</span>
{trend && <TrendBadge trend={trend} />}
</div>
)}
</div>
<div className="flex flex-col items-end gap-2">
<div
className={cn(
'flex h-8 w-8 shrink-0 items-center justify-center rounded-lg',
ACCENT_BG[accent],
)}
>
<Icon className={cn('h-4 w-4', ACCENT_ICON[accent])} />
</div>
{sparkline && sparkline.length >= 2 && !isLoading && (
<Sparkline data={sparkline} accent={accent} />
)}
</div>
</div>
</div>
);
};
+4
View File
@@ -153,6 +153,10 @@ export function ensureDatabaseInitialized(dbPath: string): void {
if (!hasRequestType) { if (!hasRequestType) {
db.exec('ALTER TABLE token_usage ADD COLUMN request_type TEXT'); db.exec('ALTER TABLE token_usage ADD COLUMN request_type TEXT');
} }
const hasIsEstimated = tokenUsageTableInfo.some((col) => col.name === 'is_estimated');
if (!hasIsEstimated) {
db.exec('ALTER TABLE token_usage ADD COLUMN is_estimated INTEGER');
}
// Create indexes for token_usage // Create indexes for token_usage
db.exec(`CREATE INDEX IF NOT EXISTS idx_token_usage_account ON token_usage(account_id);`); db.exec(`CREATE INDEX IF NOT EXISTS idx_token_usage_account ON token_usage(account_id);`);
+206 -194
View File
@@ -1,194 +1,206 @@
import React, { useState, useEffect, useRef } from 'react'; import React, { useState, useEffect, useRef } from 'react';
import { Link, Outlet, useLocation } from '@tanstack/react-router'; import { Link, Outlet, useLocation } from '@tanstack/react-router';
import { cn } from '@/lib/utils'; import { cn } from '@/lib/utils';
import { StatusBar } from '@/components/StatusBar'; import { StatusBar } from '@/components/StatusBar';
import { import {
LayoutDashboard, LayoutDashboard,
Settings, Settings,
Network, Network,
ChevronLeft, ChevronLeft,
ChevronRight, ChevronRight,
RefreshCw, RefreshCw,
BarChart3, BarChart3,
} from 'lucide-react'; } from 'lucide-react';
import { useTranslation } from 'react-i18next'; import { useTranslation } from 'react-i18next';
import { Button } from '@/components/ui/button'; import { Button } from '@/components/ui/button';
import { Tooltip, TooltipContent, TooltipProvider, TooltipTrigger } from '@/components/ui/tooltip'; import { Tooltip, TooltipContent, TooltipProvider, TooltipTrigger } from '@/components/ui/tooltip';
import { ErrorBoundary } from 'react-error-boundary'; import { ErrorBoundary } from 'react-error-boundary';
import { useToast } from '@/components/ui/use-toast'; import { useToast } from '@/components/ui/use-toast';
import { getLocalizedErrorMessage } from '@/utils/errorMessages'; import { getLocalizedErrorMessage } from '@/utils/errorMessages';
import iconPng from '@/assets/icon.png'; import iconPng from '@/assets/icon.png';
export const MainLayout: React.FC = () => { export const MainLayout: React.FC = () => {
const location = useLocation(); const location = useLocation();
const { t } = useTranslation(); const { t } = useTranslation();
const { toast } = useToast(); const { toast } = useToast();
const hasShownRouteErrorToastRef = useRef(false); const hasShownRouteErrorToastRef = useRef(false);
// Initialize state from localStorage if available, default to false (expanded) // Initialize state from localStorage if available, default to false (expanded)
const [isCollapsed, setIsCollapsed] = useState(() => { const [isCollapsed, setIsCollapsed] = useState(() => {
const saved = localStorage.getItem('sidebar-collapsed'); const saved = localStorage.getItem('sidebar-collapsed');
return saved ? JSON.parse(saved) : false; return saved ? JSON.parse(saved) : false;
}); });
// Persist state changes // Persist state changes
useEffect(() => { useEffect(() => {
localStorage.setItem('sidebar-collapsed', JSON.stringify(isCollapsed)); localStorage.setItem('sidebar-collapsed', JSON.stringify(isCollapsed));
}, [isCollapsed]); }, [isCollapsed]);
const navItems = [ const navItems = [
{ {
to: '/', to: '/',
icon: LayoutDashboard, icon: LayoutDashboard,
label: t('nav.accounts'), label: t('nav.accounts'),
}, },
{ {
to: '/usage', to: '/usage',
icon: BarChart3, icon: BarChart3,
label: t('nav.usage'), label: t('nav.usage'),
}, },
{ {
to: '/proxy', to: '/proxy',
icon: Network, icon: Network,
label: t('nav.proxy'), label: t('nav.proxy'),
}, },
{ {
to: '/settings', to: '/settings',
icon: Settings, icon: Settings,
label: t('nav.settings'), label: t('nav.settings'),
}, },
]; ];
return ( return (
<div className="bg-background text-foreground flex h-screen flex-col overflow-hidden"> <div className="bg-background text-foreground flex h-screen flex-col overflow-hidden">
<div className="flex flex-1 overflow-hidden"> <div className="flex flex-1 overflow-hidden">
{/* Sidebar */} {/* Sidebar */}
<aside <aside
className={cn( className={cn(
'bg-background group relative flex flex-col border-r border-white/5 transition-all duration-300 ease-in-out', 'relative flex flex-col border-r border-white/[0.06] transition-all duration-300 ease-[cubic-bezier(0.4,0,0.2,1)]',
isCollapsed ? 'w-[70px]' : 'w-64', isCollapsed ? 'w-[60px]' : 'w-56',
)} )}
> >
<Button {/* Collapse toggle */}
variant="ghost" <button
size="icon" className={cn(
className="bg-background hover:bg-accent hover:text-accent-foreground absolute top-6 -right-3 z-10 h-6 w-6 rounded-full border opacity-0 shadow-md transition-opacity group-hover:opacity-100" 'absolute -right-3 top-7 z-10 flex h-6 w-6 items-center justify-center rounded-full',
onClick={() => setIsCollapsed(!isCollapsed)} 'bg-muted border border-white/[0.1] text-muted-foreground',
> 'opacity-0 transition-all duration-200 hover:bg-white/[0.1] hover:text-foreground',
{isCollapsed ? ( 'group-hover:opacity-100',
<ChevronRight className="h-3 w-3" /> )}
) : ( style={{ opacity: 0 }}
<ChevronLeft className="h-3 w-3" /> onMouseEnter={(e) => (e.currentTarget.style.opacity = '1')}
)} onMouseLeave={(e) => (e.currentTarget.style.opacity = '0')}
</Button> onClick={() => setIsCollapsed(!isCollapsed)}
>
<div className={cn('flex flex-col', isCollapsed ? 'items-center p-4' : 'p-6')}> {isCollapsed ? (
<div className="flex items-center gap-2 overflow-hidden whitespace-nowrap"> <ChevronRight className="h-3 w-3" />
<div className="flex h-6 w-6 shrink-0 items-center justify-center overflow-hidden rounded bg-transparent"> ) : (
<img <ChevronLeft className="h-3 w-3" />
src={iconPng} )}
alt="Gemini Nexus Logo" </button>
className="h-full w-full object-contain"
/> {/* Brand */}
</div> <div className={cn('flex flex-col', isCollapsed ? 'items-center p-3' : 'px-4 py-5')}>
<div <div className="flex items-center gap-2.5 overflow-hidden whitespace-nowrap">
className={cn( <div className="flex h-7 w-7 shrink-0 items-center justify-center overflow-hidden rounded-lg bg-transparent">
'overflow-hidden transition-all duration-300', <img
isCollapsed ? 'w-0 opacity-0' : 'w-auto opacity-100', src={iconPng}
)} alt="Gemini Nexus Logo"
> className="h-full w-full object-contain"
<h1 className="text-xl font-bold tracking-tight">Gemini Nexus</h1> />
</div> </div>
</div> <div
</div> className={cn(
'overflow-hidden transition-all duration-300',
<nav className="flex-1 space-y-2 px-2"> isCollapsed ? 'w-0 opacity-0' : 'w-auto opacity-100',
<TooltipProvider> )}
{navItems.map((item) => { >
const isActive = location.pathname === item.to; <h1 className="text-[15px] font-semibold tracking-tight">Gemini Nexus</h1>
</div>
if (isCollapsed) { </div>
return ( </div>
<Tooltip key={item.to} delayDuration={0}>
<TooltipTrigger asChild> {/* Navigation */}
<Link <nav className="flex-1 space-y-1 px-2 pt-2">
to={item.to} <TooltipProvider>
className={cn( {navItems.map((item) => {
'mx-auto flex h-10 w-10 items-center justify-center rounded-md transition-colors', const isActive = location.pathname === item.to;
isActive
? 'bg-primary text-primary-foreground' if (isCollapsed) {
: 'hover:bg-muted text-muted-foreground hover:text-foreground', return (
)} <Tooltip key={item.to} delayDuration={0}>
> <TooltipTrigger asChild>
<item.icon className="h-5 w-5" /> <Link
<span className="sr-only">{item.label}</span> to={item.to}
</Link> className={cn(
</TooltipTrigger> 'mx-auto flex h-9 w-9 items-center justify-center rounded-lg transition-all duration-200',
<TooltipContent side="right">{item.label}</TooltipContent> isActive
</Tooltip> ? 'bg-white/[0.08] text-foreground'
); : 'text-muted-foreground hover:bg-white/[0.04] hover:text-foreground',
} )}
>
return ( <item.icon className="h-[18px] w-[18px]" />
<Link <span className="sr-only">{item.label}</span>
key={item.to} </Link>
to={item.to} </TooltipTrigger>
className={cn( <TooltipContent side="right" className="text-xs">
'flex items-center gap-3 rounded-md px-3 py-2 text-sm font-medium transition-colors', {item.label}
isActive </TooltipContent>
? 'bg-primary text-primary-foreground' </Tooltip>
: 'hover:bg-muted text-muted-foreground hover:text-foreground', );
)} }
>
<item.icon className="h-4 w-4" /> return (
{item.label} <Link
</Link> key={item.to}
); to={item.to}
})} className={cn(
</TooltipProvider> 'flex items-center gap-3 rounded-lg px-3 py-2 text-[13px] font-medium transition-all duration-200',
</nav> isActive
? 'bg-white/[0.08] text-foreground'
<div className="border-t p-2"> : 'text-muted-foreground hover:bg-white/[0.04] hover:text-foreground',
<StatusBar isCollapsed={isCollapsed} /> )}
</div> >
</aside> <item.icon className="h-[18px] w-[18px]" />
{item.label}
{/* Content Area */} </Link>
<main className="flex-1 overflow-auto transition-all duration-300"> );
<ErrorBoundary })}
resetKeys={[location.pathname]} </TooltipProvider>
onReset={() => { </nav>
hasShownRouteErrorToastRef.current = false;
}} {/* Status bar */}
onError={(error) => { <div className="border-t border-white/[0.06] p-2">
if (hasShownRouteErrorToastRef.current) { <StatusBar isCollapsed={isCollapsed} />
return; </div>
} </aside>
toast({ {/* Content Area */}
title: t('error.generic'), <main className="flex-1 overflow-auto transition-all duration-300">
description: getLocalizedErrorMessage(error, t), <ErrorBoundary
variant: 'destructive', resetKeys={[location.pathname]}
}); onReset={() => {
hasShownRouteErrorToastRef.current = true; hasShownRouteErrorToastRef.current = false;
}} }}
fallbackRender={({ resetErrorBoundary }) => ( onError={(error) => {
<div className="mx-auto max-w-3xl p-6"> if (hasShownRouteErrorToastRef.current) {
<div className="rounded-lg border border-dashed p-8 text-center"> return;
<div className="text-lg font-semibold">{t('error.generic')}</div> }
<div className="text-muted-foreground mt-2 text-sm">{t('action.retry')}</div>
<Button className="mt-4" variant="outline" onClick={resetErrorBoundary}> toast({
<RefreshCw className="mr-2 h-4 w-4" /> title: t('error.generic'),
{t('action.retry')} description: getLocalizedErrorMessage(error, t),
</Button> variant: 'destructive',
</div> });
</div> hasShownRouteErrorToastRef.current = true;
)} }}
> fallbackRender={({ resetErrorBoundary }) => (
<Outlet /> <div className="mx-auto max-w-3xl p-6">
</ErrorBoundary> <div className="rounded-xl border border-white/[0.06] border-dashed p-8 text-center">
</main> <div className="text-sm font-semibold">{t('error.generic')}</div>
</div> <div className="text-muted-foreground mt-2 text-xs">{t('action.retry')}</div>
</div> <Button className="mt-4" variant="outline" onClick={resetErrorBoundary}>
); <RefreshCw className="mr-2 h-4 w-4" />
}; {t('action.retry')}
</Button>
</div>
</div>
)}
>
<Outlet />
</ErrorBoundary>
</main>
</div>
</div>
);
};
+65 -61
View File
@@ -1,61 +1,65 @@
import type { PartialTheme } from '@nivo/theming'; import type { PartialTheme } from '@nivo/theming';
export const nivoTheme: PartialTheme = { /**
background: 'transparent', * Nivo chart theme aligned with Gemini Nexus design system.
text: { * Uses Inter font family and refined dark palette.
fontFamily: "Geist, 'Microsoft YaHei', 'Heiti SC', 'WenQuanYi Micro Hei', sans-serif", */
fontSize: 11, export const nivoTheme: PartialTheme = {
fill: '#b8b8b8', background: 'transparent',
}, text: {
axis: { fontFamily: "Inter, -apple-system, BlinkMacSystemFont, 'Segoe UI', sans-serif",
domain: { fontSize: 11,
line: { fill: '#6b7280',
stroke: 'rgba(255,255,255,0.1)', },
strokeWidth: 1, axis: {
}, domain: {
}, line: {
ticks: { stroke: 'rgba(255,255,255,0.06)',
line: { strokeWidth: 1,
stroke: 'transparent', },
strokeWidth: 0, },
}, ticks: {
text: { line: {
fontFamily: "Geist, 'Microsoft YaHei', 'Heiti SC', 'WenQuanYi Micro Hei', sans-serif", stroke: 'transparent',
fontSize: 11, strokeWidth: 0,
fill: '#b8b8b8', },
}, text: {
}, fontFamily: "'JetBrains Mono', monospace",
legend: { fontSize: 10,
text: { fill: '#6b7280',
fontFamily: "Geist, 'Microsoft YaHei', 'Heiti SC', 'WenQuanYi Micro Hei', sans-serif", },
fontSize: 12, },
fill: '#b8b8b8', legend: {
}, text: {
}, fontFamily: "Inter, sans-serif",
}, fontSize: 11,
grid: { fill: '#6b7280',
line: { },
stroke: 'rgba(255,255,255,0.06)', },
strokeWidth: 1, },
strokeDasharray: '4 4', grid: {
}, line: {
}, stroke: 'rgba(255,255,255,0.04)',
legends: { strokeWidth: 1,
text: { },
fontFamily: "Geist, 'Microsoft YaHei', 'Heiti SC', 'WenQuanYi Micro Hei', sans-serif", },
fontSize: 12, legends: {
fill: '#b8b8b8', text: {
}, fontFamily: "Inter, sans-serif",
}, fontSize: 11,
tooltip: { fill: '#6b7280',
container: { },
background: '#2e2e2e', },
color: '#fff', tooltip: {
fontFamily: "Geist, 'Microsoft YaHei', 'Heiti SC', 'WenQuanYi Micro Hei', sans-serif", container: {
fontSize: 12, background: '#16161e',
borderRadius: 6, color: '#e8eaed',
boxShadow: '0 4px 6px -1px rgba(0, 0, 0, 0.1), 0 2px 4px -1px rgba(0, 0, 0, 0.06)', fontFamily: "Inter, sans-serif",
padding: '8px 12px', fontSize: 12,
}, borderRadius: 8,
}, border: '1px solid rgba(255,255,255,0.06)',
}; boxShadow: '0 8px 24px -4px rgba(0, 0, 0, 0.3)',
padding: '8px 12px',
},
},
};
+3 -3
View File
@@ -35,9 +35,9 @@ function HomePageErrorBoundary({ error, reset }: { error: unknown; reset: () =>
return ( return (
<div className="container mx-auto max-w-4xl p-6"> <div className="container mx-auto max-w-4xl p-6">
<div className="rounded-lg border border-dashed p-8 text-center"> <div className="rounded-xl border border-dashed border-white/[0.1] p-8 text-center">
<div className="text-lg font-semibold">{t('error.generic')}</div> <div className="text-base font-semibold">{t('error.generic')}</div>
<div className="text-muted-foreground mt-2 text-sm">{t('action.retry')}</div> <div className="text-muted-foreground mt-2 text-[13px]">{t('action.retry')}</div>
<Button className="mt-4" variant="outline" onClick={() => reset()}> <Button className="mt-4" variant="outline" onClick={() => reset()}>
<RefreshCw className="mr-2 h-4 w-4" /> <RefreshCw className="mr-2 h-4 w-4" />
{t('action.retry')} {t('action.retry')}
+55 -55
View File
@@ -294,10 +294,10 @@ print(response.choices[0].message.content)`;
} }
return ( return (
<div className="container mx-auto max-w-4xl space-y-5 p-6"> <div className="container mx-auto max-w-4xl space-y-5 overflow-auto p-6">
<div> <div>
<h2 className="text-3xl font-bold tracking-tight">{t('proxy.title')}</h2> <h2 className="text-lg font-semibold tracking-tight">{t('proxy.title')}</h2>
<p className="text-muted-foreground mt-1">{t('proxy.description')}</p> <p className="text-muted-foreground mt-1 text-[13px]">{t('proxy.description')}</p>
</div> </div>
<Tabs defaultValue="status"> <Tabs defaultValue="status">
@@ -311,7 +311,7 @@ print(response.choices[0].message.content)`;
{/* Status Tab */} {/* Status Tab */}
<TabsContent value="status" className="space-y-5"> <TabsContent value="status" className="space-y-5">
{/* Service Control Card */} {/* Service Control Card */}
<Card> <Card className="border border-white/[0.06] bg-card shadow-none">
<CardHeader> <CardHeader>
<div className="flex items-center justify-between"> <div className="flex items-center justify-between">
<div> <div>
@@ -320,15 +320,15 @@ print(response.choices[0].message.content)`;
</div> </div>
<div className="flex items-center gap-2"> <div className="flex items-center gap-2">
<div <div
className={`h-3 w-3 rounded-full ${proxyConfig.enabled ? 'animate-pulse bg-green-500' : 'bg-gray-400'}`} className={`h-2.5 w-2.5 rounded-full ${proxyConfig.enabled ? 'animate-pulse bg-emerald-400' : 'bg-muted-foreground/30'}`}
></div> ></div>
<span className="text-sm font-medium"> <span className="text-[13px] font-medium">
{proxyConfig.enabled ? t('proxy.service.running') : t('proxy.service.stopped')} {proxyConfig.enabled ? t('proxy.service.running') : t('proxy.service.stopped')}
</span> </span>
</div> </div>
</div> </div>
</CardHeader> </CardHeader>
<CardContent className="space-y-6"> <CardContent className="space-y-5">
{/* Start/Stop Button */} {/* Start/Stop Button */}
<div className="flex items-center gap-4"> <div className="flex items-center gap-4">
<Button <Button
@@ -387,7 +387,7 @@ print(response.choices[0].message.content)`;
value={proxyConfig.api_key || ''} value={proxyConfig.api_key || ''}
readOnly readOnly
type={showKey ? 'text' : 'password'} type={showKey ? 'text' : 'password'}
className="pr-10 font-mono text-sm" className="pr-10 font-mono text-[13px]"
/> />
<Button <Button
variant="ghost" variant="ghost"
@@ -454,10 +454,10 @@ print(response.choices[0].message.content)`;
</div> </div>
{/* Auto Start Toggle */} {/* Auto Start Toggle */}
<div className="flex items-center justify-between rounded-lg border p-4"> <div className="flex items-center justify-between rounded-xl border border-white/[0.06] p-4">
<div className="space-y-1"> <div className="space-y-1">
<Label>{t('proxy.config.auto_start')}</Label> <Label>{t('proxy.config.auto_start')}</Label>
<p className="text-xs text-gray-500">{t('proxy.config.auto_start_desc')}</p> <p className="text-xs text-muted-foreground">{t('proxy.config.auto_start_desc')}</p>
</div> </div>
<Switch <Switch
checked={proxyConfig.auto_start} checked={proxyConfig.auto_start}
@@ -470,7 +470,7 @@ print(response.choices[0].message.content)`;
</Card> </Card>
{/* Account Quota Context */} {/* Account Quota Context */}
<Card> <Card className="border border-white/[0.06] bg-card shadow-none">
<CardHeader> <CardHeader>
<CardTitle>Account Pool</CardTitle> <CardTitle>Account Pool</CardTitle>
<CardDescription>Quota and routing context</CardDescription> <CardDescription>Quota and routing context</CardDescription>
@@ -480,21 +480,21 @@ print(response.choices[0].message.content)`;
<div className="space-y-3"> <div className="space-y-3">
<div className="flex items-center justify-between"> <div className="flex items-center justify-between">
<div> <div>
<p className="text-sm font-medium">Primary Account</p> <p className="text-[13px] font-medium">Primary Account</p>
<p className="text-muted-foreground text-sm">{accounts[0].email}</p> <p className="text-muted-foreground font-mono text-[13px]">{accounts[0].email}</p>
</div> </div>
{accounts.length > 1 && ( {accounts.length > 1 && (
<Badge variant="outline">Pool Mode: Active ({accounts.length} accounts)</Badge> <Badge variant="outline">Pool Mode: Active ({accounts.length} accounts)</Badge>
)} )}
</div> </div>
<Button variant="outline" size="sm" asChild> <Button variant="outline" size="sm" className="text-xs" asChild>
<Link to={'/accounts' as any}>Manage Accounts</Link> <Link to={'/accounts' as any}>Manage Accounts</Link>
</Button> </Button>
</div> </div>
) : ( ) : (
<div className="flex flex-col items-center gap-3 py-6 text-center"> <div className="flex flex-col items-center gap-3 py-6 text-center">
<p className="text-muted-foreground text-sm">No accounts configured</p> <p className="text-muted-foreground text-[13px]">No accounts configured</p>
<Button size="sm" asChild> <Button size="sm" className="text-xs" asChild>
<Link to={'/accounts' as any}>Add Account</Link> <Link to={'/accounts' as any}>Add Account</Link>
</Button> </Button>
</div> </div>
@@ -505,16 +505,16 @@ print(response.choices[0].message.content)`;
{proxyConfig.enabled ? ( {proxyConfig.enabled ? (
<> <>
{/* Local Access Info Banner */} {/* Local Access Info Banner */}
<div className="flex flex-col gap-2 rounded-md border border-blue-100 bg-blue-50 p-3 text-sm text-blue-900 dark:border-blue-900/50 dark:bg-blue-950/30 dark:text-blue-200"> <div className="flex flex-col gap-2 rounded-xl border border-blue-500/20 bg-blue-500/5 p-3 text-[13px] text-blue-200">
<div className="flex items-center gap-2"> <div className="flex items-center gap-2">
<div className="font-semibold">{t('proxy.config.local_access')}</div> <div className="font-semibold">{t('proxy.config.local_access')}</div>
<code className="rounded bg-blue-100 px-1.5 py-0.5 font-mono select-all dark:bg-blue-900/50"> <code className="rounded-md bg-blue-500/10 px-1.5 py-0.5 font-mono text-xs select-all">
http://{selectedIp || 'localhost'}:{proxyConfig.port}/v1 http://{selectedIp || 'localhost'}:{proxyConfig.port}/v1
</code> </code>
<Button <Button
variant="ghost" variant="ghost"
size="icon" size="icon"
className="h-7 w-7 text-blue-700 hover:bg-blue-100 dark:text-blue-300 dark:hover:bg-blue-900/50" className="h-7 w-7 text-blue-300 hover:bg-blue-500/10"
onClick={() => onClick={() =>
copyToClipboard( copyToClipboard(
`http://${selectedIp || 'localhost'}:${proxyConfig.port}/v1`, `http://${selectedIp || 'localhost'}:${proxyConfig.port}/v1`,
@@ -539,7 +539,7 @@ print(response.choices[0].message.content)`;
)} )}
</div> </div>
{!proxyConfig.api_key && ( {!proxyConfig.api_key && (
<div className="flex items-center gap-2 text-xs font-medium text-amber-600 dark:text-amber-400"> <div className="flex items-center gap-2 text-xs font-medium text-amber-400">
{t('proxy.config.no_token_warning')} {t('proxy.config.no_token_warning')}
</div> </div>
)} )}
@@ -572,11 +572,11 @@ print(response.choices[0].message.content)`;
}, },
]; ];
return items.map((metric) => ( return items.map((metric) => (
<Card key={metric.label}> <Card key={metric.label} className="border border-white/[0.06] bg-card shadow-none">
<CardContent className="pt-6"> <CardContent className="pt-6">
<p className="text-muted-foreground text-xs">{metric.label}</p> <p className="text-muted-foreground text-[11px] font-medium tracking-wider uppercase">{metric.label}</p>
<div className="mt-1 flex items-center gap-2"> <div className="mt-1 flex items-center gap-2">
<span className="text-2xl font-bold"> <span className="font-mono text-xl font-semibold tabular-nums">
{metric.value} {metric.value}
{metric.suffix} {metric.suffix}
</span> </span>
@@ -588,11 +588,11 @@ print(response.choices[0].message.content)`;
</div> </div>
</> </>
) : ( ) : (
<div className="flex flex-col items-center justify-center gap-4 rounded-lg border border-dashed p-12 text-center"> <div className="flex flex-col items-center justify-center gap-4 rounded-xl border border-dashed border-white/[0.1] p-12 text-center">
<ServerOff className="text-muted-foreground h-12 w-12" /> <ServerOff className="text-muted-foreground/50 h-10 w-10" />
<div className="space-y-1"> <div className="space-y-1">
<p className="text-lg font-medium">Proxy is not running</p> <p className="text-base font-semibold">Proxy is not running</p>
<p className="text-muted-foreground text-sm">Start the proxy to enable local API access</p> <p className="text-muted-foreground text-[13px]">Start the proxy to enable local API access</p>
</div> </div>
<Button <Button
onClick={async () => { onClick={async () => {
@@ -609,20 +609,20 @@ print(response.choices[0].message.content)`;
{/* Routing Tab */} {/* Routing Tab */}
<TabsContent value="routing" className="space-y-5"> <TabsContent value="routing" className="space-y-5">
<Card> <Card className="border border-white/[0.06] bg-card shadow-none">
<CardHeader> <CardHeader>
<CardTitle>{t('proxy.mapping.title')}</CardTitle> <CardTitle>{t('proxy.mapping.title')}</CardTitle>
<CardDescription>{t('proxy.mapping.description')}</CardDescription> <CardDescription>{t('proxy.mapping.description')}</CardDescription>
</CardHeader> </CardHeader>
<CardContent className="space-y-4"> <CardContent className="space-y-4">
<div className="rounded-lg border"> <div className="overflow-hidden rounded-xl border border-white/[0.06]">
<div className="grid grid-cols-3 gap-4 border-b bg-muted/50 p-3 text-xs font-medium text-muted-foreground"> <div className="grid grid-cols-3 gap-4 border-b border-white/[0.06] bg-white/[0.02] p-3 text-[11px] font-medium tracking-wider uppercase text-muted-foreground">
<div>Requested Model</div> <div>Requested Model</div>
<div>Resolved Model</div> <div>Resolved Model</div>
<div>Provider</div> <div>Provider</div>
</div> </div>
<div className="grid grid-cols-3 gap-4 items-center p-3"> <div className="grid grid-cols-3 gap-4 items-center p-3 text-[13px]">
<div className="text-sm font-medium">Claude Sonnet 4.6 (Thinking)</div> <div className="font-medium">Claude Sonnet 4.6 (Thinking)</div>
<Select <Select
value={resolveAnthropicMappingValue( value={resolveAnthropicMappingValue(
proxyConfig.anthropic_mapping, proxyConfig.anthropic_mapping,
@@ -649,8 +649,8 @@ print(response.choices[0].message.content)`;
</Select> </Select>
<Badge variant="secondary">Anthropic</Badge> <Badge variant="secondary">Anthropic</Badge>
</div> </div>
<div className="grid grid-cols-3 gap-4 items-center border-t p-3"> <div className="grid grid-cols-3 gap-4 items-center border-t border-white/[0.06] p-3 text-[13px]">
<div className="text-sm font-medium">Claude Opus 4.6 (Thinking)</div> <div className="font-medium">Claude Opus 4.6 (Thinking)</div>
<Select <Select
value={resolveAnthropicMappingValue( value={resolveAnthropicMappingValue(
proxyConfig.anthropic_mapping, proxyConfig.anthropic_mapping,
@@ -696,7 +696,7 @@ print(response.choices[0].message.content)`;
</Card> </Card>
{/* Current Mappings */} {/* Current Mappings */}
<Card> <Card className="border border-white/[0.06] bg-card shadow-none">
<CardHeader> <CardHeader>
<CardTitle>{t('proxy.routing.currentMappings')}</CardTitle> <CardTitle>{t('proxy.routing.currentMappings')}</CardTitle>
<CardDescription>{t('proxy.routing.modelMapping')}</CardDescription> <CardDescription>{t('proxy.routing.modelMapping')}</CardDescription>
@@ -706,10 +706,10 @@ print(response.choices[0].message.content)`;
const mappingEntries = Object.entries(proxyConfig.anthropic_mapping || {}); const mappingEntries = Object.entries(proxyConfig.anthropic_mapping || {});
const customEntries = Object.entries(proxyConfig.custom_mapping || {}); const customEntries = Object.entries(proxyConfig.custom_mapping || {});
return mappingEntries.length === 0 && customEntries.length === 0 ? ( return mappingEntries.length === 0 && customEntries.length === 0 ? (
<p className="text-sm text-muted-foreground">{t('proxy.routing.noMapping')}</p> <p className="text-[13px] text-muted-foreground">{t('proxy.routing.noMapping')}</p>
) : ( ) : (
<div className="rounded-lg border"> <div className="overflow-hidden rounded-xl border border-white/[0.06]">
<div className="grid grid-cols-3 gap-4 border-b bg-muted/50 p-3 text-xs font-medium text-muted-foreground"> <div className="grid grid-cols-3 gap-4 border-b border-white/[0.06] bg-white/[0.02] p-3 text-[11px] font-medium tracking-wider uppercase text-muted-foreground">
<div>{t('proxy.mapping.maps_to')}</div> <div>{t('proxy.mapping.maps_to')}</div>
<div>{t('settings.modelMapping.targetGemini')}</div> <div>{t('settings.modelMapping.targetGemini')}</div>
<div>Provider</div> <div>Provider</div>
@@ -717,7 +717,7 @@ print(response.choices[0].message.content)`;
{mappingEntries.map(([key, value]) => ( {mappingEntries.map(([key, value]) => (
<div <div
key={key} key={key}
className="grid grid-cols-3 gap-4 items-center border-b p-3 last:border-0" className="grid grid-cols-3 gap-4 items-center border-b border-white/[0.06] p-3 last:border-0"
> >
<div className="font-mono text-xs">{key}</div> <div className="font-mono text-xs">{key}</div>
<div className="font-mono text-xs">{value}</div> <div className="font-mono text-xs">{value}</div>
@@ -727,7 +727,7 @@ print(response.choices[0].message.content)`;
{customEntries.map(([key, value]) => ( {customEntries.map(([key, value]) => (
<div <div
key={key} key={key}
className="grid grid-cols-3 gap-4 items-center border-b p-3 last:border-0" className="grid grid-cols-3 gap-4 items-center border-b border-white/[0.06] p-3 last:border-0"
> >
<div className="font-mono text-xs">{key}</div> <div className="font-mono text-xs">{key}</div>
<div className="font-mono text-xs">{value}</div> <div className="font-mono text-xs">{value}</div>
@@ -741,21 +741,21 @@ print(response.choices[0].message.content)`;
</Card> </Card>
{/* Upstream Proxy Status */} {/* Upstream Proxy Status */}
<Card> <Card className="border border-white/[0.06] bg-card shadow-none">
<CardHeader> <CardHeader>
<CardTitle>{t('proxy.routing.upstreamProxy')}</CardTitle> <CardTitle>{t('proxy.routing.upstreamProxy')}</CardTitle>
<CardDescription>{t('proxy.routing.upstreamProxy')}</CardDescription> <CardDescription>{t('proxy.routing.upstreamProxy')}</CardDescription>
</CardHeader> </CardHeader>
<CardContent className="space-y-2"> <CardContent className="space-y-2">
<div className="flex items-center justify-between rounded-lg border p-3"> <div className="flex items-center justify-between rounded-xl border border-white/[0.06] p-3">
<span className="text-sm text-muted-foreground">{t('proxy.routing.status')}</span> <span className="text-[13px] text-muted-foreground">{t('proxy.routing.status')}</span>
<Badge variant={proxyConfig.upstream_proxy?.enabled ? 'default' : 'secondary'}> <Badge variant={proxyConfig.upstream_proxy?.enabled ? 'default' : 'secondary'}>
{proxyConfig.upstream_proxy?.enabled ? t('proxy.routing.enabled') : t('proxy.routing.disabled')} {proxyConfig.upstream_proxy?.enabled ? t('proxy.routing.enabled') : t('proxy.routing.disabled')}
</Badge> </Badge>
</div> </div>
{proxyConfig.upstream_proxy?.enabled && ( {proxyConfig.upstream_proxy?.enabled && (
<div className="flex items-center justify-between rounded-lg border p-3"> <div className="flex items-center justify-between rounded-xl border border-white/[0.06] p-3">
<span className="text-sm text-muted-foreground">{t('proxy.routing.url')}</span> <span className="text-[13px] text-muted-foreground">{t('proxy.routing.url')}</span>
<span className="font-mono text-xs">{proxyConfig.upstream_proxy?.url || '—'}</span> <span className="font-mono text-xs">{proxyConfig.upstream_proxy?.url || '—'}</span>
</div> </div>
)} )}
@@ -765,7 +765,7 @@ print(response.choices[0].message.content)`;
{/* API Docs Tab */} {/* API Docs Tab */}
<TabsContent value="api-docs" className="space-y-5"> <TabsContent value="api-docs" className="space-y-5">
<Card> <Card className="border border-white/[0.06] bg-card shadow-none">
<CardHeader> <CardHeader>
<CardTitle className="flex items-center gap-2"> <CardTitle className="flex items-center gap-2">
<Code size={20} /> <Code size={20} />
@@ -791,9 +791,9 @@ print(response.choices[0].message.content)`;
</div> </div>
{/* Model Selector & Copy API ID */} {/* Model Selector & Copy API ID */}
<div className="flex flex-col gap-4 border-b border-gray-200 pb-4 sm:flex-row sm:items-center sm:justify-between dark:border-gray-700"> <div className="flex flex-col gap-4 border-b border-white/[0.06] pb-4 sm:flex-row sm:items-center sm:justify-between">
<div className="flex items-center gap-3"> <div className="flex items-center gap-3">
<Label className="text-sm font-medium"> <Label className="text-[13px] font-medium">
Model{' '} Model{' '}
<span className="text-muted-foreground ml-1 font-normal"> <span className="text-muted-foreground ml-1 font-normal">
({availableModelIds.length} available) ({availableModelIds.length} available)
@@ -836,7 +836,7 @@ print(response.choices[0].message.content)`;
{/* Collapsible Code Examples */} {/* Collapsible Code Examples */}
<div className="space-y-3"> <div className="space-y-3">
<div className="flex items-center justify-between"> <div className="flex items-center justify-between">
<Label className="text-sm font-medium">Code Examples</Label> <Label className="text-[13px] font-medium">Code Examples</Label>
<Button <Button
variant="ghost" variant="ghost"
size="sm" size="sm"
@@ -851,9 +851,9 @@ print(response.choices[0].message.content)`;
</div> </div>
{/* cURL */} {/* cURL */}
<div className="rounded-lg border"> <div className="overflow-hidden rounded-xl border border-white/[0.06]">
<div <div
className="flex w-full cursor-pointer items-center justify-between p-3 text-sm font-medium hover:bg-muted/50" className="flex w-full cursor-pointer items-center justify-between p-3 text-[13px] font-medium hover:bg-white/[0.02] transition-colors"
onClick={() => setShowCurl(!showCurl)} onClick={() => setShowCurl(!showCurl)}
> >
<span className="flex items-center gap-2"> <span className="flex items-center gap-2">
@@ -876,16 +876,16 @@ print(response.choices[0].message.content)`;
</div> </div>
</div> </div>
{showCurl && ( {showCurl && (
<pre className="overflow-x-auto border-t bg-gray-900 p-3 font-mono text-xs whitespace-pre-wrap text-gray-100"> <pre className="overflow-x-auto border-t border-white/[0.06] bg-card p-3 font-mono text-xs whitespace-pre-wrap text-foreground/90">
{getCurlExample(activeModelTab)} {getCurlExample(activeModelTab)}
</pre> </pre>
)} )}
</div> </div>
{/* Python */} {/* Python */}
<div className="rounded-lg border"> <div className="overflow-hidden rounded-xl border border-white/[0.06]">
<div <div
className="flex w-full cursor-pointer items-center justify-between p-3 text-sm font-medium hover:bg-muted/50" className="flex w-full cursor-pointer items-center justify-between p-3 text-[13px] font-medium hover:bg-white/[0.02] transition-colors"
onClick={() => setShowPython(!showPython)} onClick={() => setShowPython(!showPython)}
> >
<span className="flex items-center gap-2"> <span className="flex items-center gap-2">
@@ -908,7 +908,7 @@ print(response.choices[0].message.content)`;
</div> </div>
</div> </div>
{showPython && ( {showPython && (
<pre className="overflow-x-auto border-t bg-gray-900 p-3 font-mono text-xs whitespace-pre-wrap text-gray-100"> <pre className="overflow-x-auto border-t border-white/[0.06] bg-card p-3 font-mono text-xs whitespace-pre-wrap text-foreground/90">
{getPythonExample(activeModelTab)} {getPythonExample(activeModelTab)}
</pre> </pre>
)} )}
+27 -27
View File
@@ -79,8 +79,8 @@ function SettingsPage() {
return ( return (
<div className="container mx-auto max-w-4xl space-y-5 p-6"> <div className="container mx-auto max-w-4xl space-y-5 p-6">
<div> <div>
<h2 className="text-3xl font-bold tracking-tight">{t('settings.title')}</h2> <h2 className="text-lg font-semibold tracking-tight">{t('settings.title')}</h2>
<p className="text-muted-foreground mt-1">{t('settings.description')}</p> <p className="text-muted-foreground mt-1 text-[13px]">{t('settings.description')}</p>
</div> </div>
<Tabs defaultValue="general" className="w-full"> <Tabs defaultValue="general" className="w-full">
@@ -92,16 +92,16 @@ function SettingsPage() {
{/* --- GENERAL TAB --- */} {/* --- GENERAL TAB --- */}
<TabsContent value="general" className="space-y-5"> <TabsContent value="general" className="space-y-5">
<Card> <Card className="bg-transparent border-0 shadow-none rounded-none border-b border-white/5 last:border-0 pb-6">
<CardHeader> <CardHeader className="px-0 pt-4">
<CardTitle>{t('settings.appearance.title')}</CardTitle> <CardTitle>{t('settings.appearance.title')}</CardTitle>
<CardDescription>{t('settings.appearance.description')}</CardDescription> <CardDescription>{t('settings.appearance.description')}</CardDescription>
</CardHeader> </CardHeader>
<CardContent className="space-y-6"> <CardContent className="space-y-5">
<div className="flex items-center justify-between space-x-2"> <div className="flex items-center justify-between space-x-2">
<div className="space-y-1"> <div className="space-y-1">
<Label htmlFor="dark-mode">{t('settings.darkMode')}</Label> <Label htmlFor="dark-mode">{t('settings.darkMode')}</Label>
<p className="text-muted-foreground text-sm"> <p className="text-muted-foreground text-xs">
{t('settings.darkModeDescription')} {t('settings.darkModeDescription')}
</p> </p>
</div> </div>
@@ -139,8 +139,8 @@ function SettingsPage() {
</Card> </Card>
{/* Account Settings Card */} {/* Account Settings Card */}
<Card> <Card className="bg-transparent border-0 shadow-none rounded-none border-b border-white/5 last:border-0 pb-6">
<CardHeader> <CardHeader className="px-0 pt-4">
<CardTitle>{t('settings.account.title')}</CardTitle> <CardTitle>{t('settings.account.title')}</CardTitle>
<CardDescription>{t('settings.account.description')}</CardDescription> <CardDescription>{t('settings.account.description')}</CardDescription>
</CardHeader> </CardHeader>
@@ -149,7 +149,7 @@ function SettingsPage() {
<div className="flex items-center justify-between rounded-lg border p-4"> <div className="flex items-center justify-between rounded-lg border p-4">
<div className="space-y-1"> <div className="space-y-1">
<Label>{t('settings.account.auto_refresh')}</Label> <Label>{t('settings.account.auto_refresh')}</Label>
<p className="text-xs text-gray-500">{t('settings.account.auto_refresh_desc')}</p> <p className="text-muted-foreground text-xs">{t('settings.account.auto_refresh_desc')}</p>
</div> </div>
<Switch <Switch
checked={config?.auto_refresh || false} checked={config?.auto_refresh || false}
@@ -165,7 +165,7 @@ function SettingsPage() {
<div className="flex items-center justify-between rounded-lg border p-4"> <div className="flex items-center justify-between rounded-lg border p-4">
<div className="space-y-1"> <div className="space-y-1">
<Label>{t('settings.account.auto_sync')}</Label> <Label>{t('settings.account.auto_sync')}</Label>
<p className="text-xs text-gray-500">{t('settings.account.auto_sync_desc')}</p> <p className="text-muted-foreground text-xs">{t('settings.account.auto_sync_desc')}</p>
</div> </div>
<Switch <Switch
checked={config?.auto_sync || false} checked={config?.auto_sync || false}
@@ -180,8 +180,8 @@ function SettingsPage() {
</Card> </Card>
{isAutoStartSupported && ( {isAutoStartSupported && (
<Card> <Card className="bg-transparent border-0 shadow-none rounded-none border-b border-white/5 last:border-0 pb-6">
<CardHeader> <CardHeader className="px-0 pt-4">
<CardTitle>{t('settings.startup.title')}</CardTitle> <CardTitle>{t('settings.startup.title')}</CardTitle>
<CardDescription>{t('settings.startup.description')}</CardDescription> <CardDescription>{t('settings.startup.description')}</CardDescription>
</CardHeader> </CardHeader>
@@ -189,7 +189,7 @@ function SettingsPage() {
<div className="flex items-center justify-between rounded-lg border p-4"> <div className="flex items-center justify-between rounded-lg border p-4">
<div className="space-y-1"> <div className="space-y-1">
<Label>{t('settings.startup.auto_startup')}</Label> <Label>{t('settings.startup.auto_startup')}</Label>
<p className="text-xs text-gray-500"> <p className="text-muted-foreground text-xs">
{t('settings.startup.auto_startup_desc')} {t('settings.startup.auto_startup_desc')}
</p> </p>
</div> </div>
@@ -211,13 +211,13 @@ function SettingsPage() {
</Card> </Card>
)} )}
<Card> <Card className="bg-transparent border-0 shadow-none rounded-none border-b border-white/5 last:border-0 pb-6">
<CardHeader> <CardHeader className="px-0 pt-4">
<CardTitle>{t('settings.about.title')}</CardTitle> <CardTitle>{t('settings.about.title')}</CardTitle>
<CardDescription>{t('settings.about.description')}</CardDescription> <CardDescription>{t('settings.about.description')}</CardDescription>
</CardHeader> </CardHeader>
<CardContent className="space-y-4"> <CardContent className="space-y-4">
<div className="grid grid-cols-2 gap-4 text-sm"> <div className="grid grid-cols-2 gap-4 text-[13px]">
<div className="text-muted-foreground">{t('settings.version')}</div> <div className="text-muted-foreground">{t('settings.version')}</div>
<div className="font-medium">{appVersion || t('common.unknown')}</div> <div className="font-medium">{appVersion || t('common.unknown')}</div>
@@ -230,7 +230,7 @@ function SettingsPage() {
<div className="text-muted-foreground">{t('action.openLogs')}</div> <div className="text-muted-foreground">{t('action.openLogs')}</div>
<button <button
onClick={() => openLogDirectory()} onClick={() => openLogDirectory()}
className="flex items-center gap-2 font-medium text-blue-600 hover:text-blue-700 dark:text-blue-400 dark:hover:text-blue-300" className="flex items-center gap-2 font-medium text-accent hover:text-accent/80 transition-colors"
> >
<FolderOpen className="h-4 w-4" /> <FolderOpen className="h-4 w-4" />
<span>{t('settings.openLogDir')}</span> <span>{t('settings.openLogDir')}</span>
@@ -240,8 +240,8 @@ function SettingsPage() {
</Card> </Card>
{/* Privacy & Error Reporting Card */} {/* Privacy & Error Reporting Card */}
<Card> <Card className="bg-transparent border-0 shadow-none rounded-none border-b border-white/5 last:border-0 pb-6">
<CardHeader> <CardHeader className="px-0 pt-4">
<CardTitle>{t('settings.privacy.title')}</CardTitle> <CardTitle>{t('settings.privacy.title')}</CardTitle>
<CardDescription>{t('settings.privacy.description')}</CardDescription> <CardDescription>{t('settings.privacy.description')}</CardDescription>
</CardHeader> </CardHeader>
@@ -249,7 +249,7 @@ function SettingsPage() {
<div className="flex items-center justify-between rounded-lg border p-4"> <div className="flex items-center justify-between rounded-lg border p-4">
<div className="space-y-1"> <div className="space-y-1">
<Label>{t('settings.privacy.error_reporting')}</Label> <Label>{t('settings.privacy.error_reporting')}</Label>
<p className="text-xs text-gray-500"> <p className="text-muted-foreground text-xs">
{t('settings.privacy.error_reporting_desc')} {t('settings.privacy.error_reporting_desc')}
</p> </p>
</div> </div>
@@ -267,8 +267,8 @@ function SettingsPage() {
</Card> </Card>
{/* Notifications Card */} {/* Notifications Card */}
<Card> <Card className="bg-transparent border-0 shadow-none rounded-none border-b border-white/5 last:border-0 pb-6">
<CardHeader> <CardHeader className="px-0 pt-4">
<CardTitle>{t('settings.notifications.title')}</CardTitle> <CardTitle>{t('settings.notifications.title')}</CardTitle>
<CardDescription>{t('settings.notifications.description')}</CardDescription> <CardDescription>{t('settings.notifications.description')}</CardDescription>
</CardHeader> </CardHeader>
@@ -276,7 +276,7 @@ function SettingsPage() {
<div className="flex items-center justify-between rounded-lg border p-4"> <div className="flex items-center justify-between rounded-lg border p-4">
<div className="space-y-1"> <div className="space-y-1">
<Label>{t('settings.notifications.quotaAlert')}</Label> <Label>{t('settings.notifications.quotaAlert')}</Label>
<p className="text-xs text-gray-500"> <p className="text-muted-foreground text-xs">
{t('settings.notifications.quotaAlertDesc')} {t('settings.notifications.quotaAlertDesc')}
</p> </p>
</div> </div>
@@ -300,7 +300,7 @@ function SettingsPage() {
<div className="flex items-center justify-between rounded-lg border p-4"> <div className="flex items-center justify-between rounded-lg border p-4">
<div className="space-y-1"> <div className="space-y-1">
<Label>{t('settings.notifications.quotaThreshold')}</Label> <Label>{t('settings.notifications.quotaThreshold')}</Label>
<p className="text-xs text-gray-500"> <p className="text-muted-foreground text-xs">
{t('settings.notifications.quotaThresholdDesc')} {t('settings.notifications.quotaThresholdDesc')}
</p> </p>
</div> </div>
@@ -327,7 +327,7 @@ function SettingsPage() {
} }
} }
}} }}
className="w-16 rounded-md border bg-transparent px-2 py-1 text-center text-sm" className="w-16 rounded-lg border border-white/[0.08] bg-muted/50 px-2 py-1 text-center text-[13px] text-foreground focus:outline-none focus:border-accent/50"
/> />
<span className="text-muted-foreground text-sm">%</span> <span className="text-muted-foreground text-sm">%</span>
</div> </div>
@@ -343,8 +343,8 @@ function SettingsPage() {
{/* --- PROXY TAB (Upstream Proxy Config Only) --- */} {/* --- PROXY TAB (Upstream Proxy Config Only) --- */}
<TabsContent value="proxy" className="space-y-5"> <TabsContent value="proxy" className="space-y-5">
<Card> <Card className="bg-transparent border-0 shadow-none rounded-none border-b border-white/5 last:border-0 pb-6">
<CardHeader> <CardHeader className="px-0 pt-4">
<CardTitle>{t('settings.proxy.title')}</CardTitle> <CardTitle>{t('settings.proxy.title')}</CardTitle>
<CardDescription>{t('settings.proxy.description')}</CardDescription> <CardDescription>{t('settings.proxy.description')}</CardDescription>
</CardHeader> </CardHeader>
+667 -497
View File
File diff suppressed because it is too large. Load diff
+14 -9
View File
@@ -77,12 +77,15 @@ export class ProxyService {
if (!usage) { if (!usage) {
return; return;
} }
const prompt = usage.promptTokenCount ?? 0;
const completion = usage.candidatesTokenCount ?? 0;
const total = usage.totalTokenCount ?? (prompt + completion);
this.tokenUsageService.recordUsage({ this.tokenUsageService.recordUsage({
accountId, accountId,
model, model,
promptTokens: usage.promptTokenCount ?? 0, promptTokens: prompt,
completionTokens: usage.candidatesTokenCount ?? 0, completionTokens: completion,
totalTokens: usage.totalTokenCount ?? 0, totalTokens: total,
timestamp: Date.now(), timestamp: Date.now(),
requestType, requestType,
isEstimated, isEstimated,
@@ -319,14 +322,15 @@ export class ProxyService {
if (startMsg) subscriber.next(startMsg); if (startMsg) subscriber.next(startMsg);
} }
const candidate = json.candidates?.[0]; const responsePayload = json.response ?? json;
const candidate = responsePayload.candidates?.[0];
const part = candidate?.content?.parts?.[0]; const part = candidate?.content?.parts?.[0];
if (candidate?.finishReason) { if (candidate?.finishReason) {
lastFinishReason = candidate.finishReason; lastFinishReason = candidate.finishReason;
} }
if (json.usageMetadata) { if (responsePayload.usageMetadata) {
lastUsageMetadata = json.usageMetadata; lastUsageMetadata = responsePayload.usageMetadata;
} }
if (this.isGeminiPart(part)) { if (this.isGeminiPart(part)) {
@@ -605,10 +609,11 @@ export class ProxyService {
if (dataStr === '[DONE]') continue; if (dataStr === '[DONE]') continue;
try { try {
const parsed = JSON.parse(dataStr); const parsed = JSON.parse(dataStr);
if (parsed?.usageMetadata) { const responsePayload = parsed?.response ?? parsed;
lastUsageMetadata = parsed.usageMetadata; if (responsePayload?.usageMetadata) {
lastUsageMetadata = responsePayload.usageMetadata;
} }
const text = parsed?.candidates?.[0]?.content?.parts?.[0]?.text; const text = responsePayload?.candidates?.[0]?.content?.parts?.[0]?.text;
if (typeof text === 'string') { if (typeof text === 'string') {
accumulatedText += text; accumulatedText += text;
} }
+257 -239
View File
@@ -1,245 +1,263 @@
@import url('https://fonts.googleapis.com/css2?family=Plus+Jakarta+Sans:ital,wght@0,400;0,500;0,700;1,400&display=swap'); @import url('https://fonts.googleapis.com/css2?family=Inter:wght@300;400;500;600;700&family=JetBrains+Mono:wght@400;500;600;700&display=swap');
@import 'tailwindcss'; @import 'tailwindcss';
@plugin 'tailwindcss-animate';
@custom-variant dark (&:is(.dark *));
@theme {
--color-border: hsl(var(--border));
--color-input: hsl(var(--input));
--color-ring: hsl(var(--ring));
--color-background: hsl(var(--background));
--color-foreground: hsl(var(--foreground));
--color-primary: hsl(var(--primary));
--color-primary-foreground: hsl(var(--primary-foreground));
--color-secondary: hsl(var(--secondary));
--color-secondary-foreground: hsl(var(--secondary-foreground));
--color-destructive: hsl(var(--destructive));
--color-destructive-foreground: hsl(var(--destructive-foreground));
--color-muted: hsl(var(--muted));
--color-muted-foreground: hsl(var(--muted-foreground));
--color-accent: hsl(var(--accent));
--color-accent-foreground: hsl(var(--accent-foreground));
--color-popover: hsl(var(--popover));
--color-popover-foreground: hsl(var(--popover-foreground));
--color-card: hsl(var(--card));
--color-card-foreground: hsl(var(--card-foreground));
--font-sans: Plus Jakarta Sans, 'Microsoft YaHei', 'Heiti SC', 'WenQuanYi Micro Hei', sans-serif;
--font-mono: Geist Mono, monospace;
--font-tomorrow: Tomorrow, sans-serif;
--radius-lg: var(--radius);
--radius-md: calc(var(--radius) - 2px);
--radius-sm: calc(var(--radius) - 4px);
--animate-accordion-down: accordion-down 0.2s ease-out;
--animate-accordion-up: accordion-up 0.2s ease-out;
@keyframes accordion-down {
from {
height: 0;
}
to {
height: var(--radix-accordion-content-height);
}
}
@keyframes accordion-up {
from {
height: var(--radix-accordion-content-height);
}
to {
height: 0;
}
}
}
@utility container {
margin-inline: auto;
padding-inline: 2rem;
@media (width >= --theme(--breakpoint-sm)) {
max-width: none;
}
@media (width >= 1400px) {
max-width: 1400px;
}
}
@layer base {
@font-face {
}
@font-face {
font-family: 'Geist Mono';
font-display: swap;
src: url('../assets/fonts/geist-mono/geist-mono.ttf') format('truetype');
}
@font-face {
font-family: 'Tomorrow';
font-weight: 400;
font-style: normal;
src: url('../assets/fonts/tomorrow/tomorrow-regular.ttf') format('truetype');
}
@font-face {
font-family: 'Tomorrow';
font-weight: 400;
font-style: italic;
src: url('../assets/fonts/tomorrow/tomorrow-italic.ttf') format('truetype');
}
@font-face {
font-family: 'Tomorrow';
font-weight: 700;
font-style: normal;
src: url('../assets/fonts/tomorrow/tomorrow-bold.ttf') format('truetype');
}
@font-face {
font-family: 'Tomorrow';
font-weight: 700;
font-style: italic;
src: url('../assets/fonts/tomorrow/tomorrow-bold-italic.ttf') format('truetype');
}
body {
letter-spacing: var(--tracking-normal);
}
}
@layer base {
}
@layer base {
body {
@apply overflow-hidden;
}
.draglayer {
@apply bg-background;
-webkit-app-region: drag;
}
button {
@apply cursor-pointer;
}
}
@layer base {
* {
@apply border-border outline-ring/50;
}
body {
@apply bg-background text-foreground;
}
}
@theme inline {
--font-sans: Plus Jakarta Sans, sans-serif;
--font-mono: Geist Mono, monospace;
--font-serif: Georgia, serif;
--radius: 0.5rem;
--tracking-tighter: calc(var(--tracking-normal) - 0.05em);
--tracking-tight: calc(var(--tracking-normal) - 0.025em);
--tracking-wide: calc(var(--tracking-normal) + 0.025em);
--tracking-wider: calc(var(--tracking-normal) + 0.05em);
--tracking-widest: calc(var(--tracking-normal) + 0.1em);
--tracking-normal: var(--tracking-normal);
--shadow-2xl: var(--shadow-2xl);
--shadow-xl: var(--shadow-xl);
--shadow-lg: var(--shadow-lg);
--shadow-md: var(--shadow-md);
--shadow: var(--shadow);
--shadow-sm: var(--shadow-sm);
--shadow-xs: var(--shadow-xs);
--shadow-2xs: var(--shadow-2xs);
--spacing: var(--spacing);
--letter-spacing: var(--letter-spacing);
--shadow-offset-y: var(--shadow-offset-y);
--shadow-offset-x: var(--shadow-offset-x);
--shadow-spread: var(--shadow-spread);
--shadow-blur: var(--shadow-blur);
--shadow-opacity: var(--shadow-opacity);
--color-shadow-color: var(--shadow-color);
--color-sidebar-ring: var(--sidebar-ring);
--color-sidebar-border: var(--sidebar-border);
--color-sidebar-accent-foreground: var(--sidebar-accent-foreground);
--color-sidebar-accent: var(--sidebar-accent);
--color-sidebar-primary-foreground: var(--sidebar-primary-foreground);
--color-sidebar-primary: var(--sidebar-primary);
--color-sidebar-foreground: var(--sidebar-foreground);
--color-sidebar: var(--sidebar);
--color-chart-5: var(--chart-5);
--color-chart-4: var(--chart-4);
--color-chart-3: var(--chart-3);
--color-chart-2: var(--chart-2);
--color-chart-1: var(--chart-1);
--color-ring: var(--ring);
--color-input: var(--input);
--color-border: var(--border);
--color-destructive-foreground: var(--destructive-foreground);
--color-destructive: var(--destructive);
--color-accent-foreground: var(--accent-foreground);
--color-accent: var(--accent);
--color-muted-foreground: var(--muted-foreground);
--color-muted: var(--muted);
--color-secondary-foreground: var(--secondary-foreground);
--color-secondary: var(--secondary);
--color-primary-foreground: var(--primary-foreground);
--color-primary: var(--primary);
--color-popover-foreground: var(--popover-foreground);
--color-popover: var(--popover);
--color-card-foreground: var(--card-foreground);
--color-card: var(--card);
--color-foreground: var(--foreground);
--color-background: var(--background);
--radius-sm: calc(var(--radius) - 4px);
--radius-md: calc(var(--radius) - 2px);
--radius-lg: var(--radius);
--radius-xl: calc(var(--radius) + 4px);
--radius-2xl: calc(var(--radius) + 8px);
--radius-3xl: calc(var(--radius) + 12px);
--radius-4xl: calc(var(--radius) + 16px);
}
@plugin 'tailwindcss-animate';
@custom-variant dark (&:is(.dark *));
/*
* Gemini Nexus Design System
* Style: Minimal Dark Professional
* Fonts: Inter (UI) + JetBrains Mono (code)
* Palette: Refined Slate + Emerald accents
*/
@theme inline {
--font-sans: Inter, -apple-system, BlinkMacSystemFont, 'Segoe UI', sans-serif;
--font-mono: 'JetBrains Mono', 'Fira Code', 'Cascadia Code', monospace;
--radius: 0.625rem;
--radius-sm: calc(var(--radius) - 4px);
--radius-md: calc(var(--radius) - 2px);
--radius-lg: var(--radius);
--radius-xl: calc(var(--radius) + 4px);
--radius-2xl: calc(var(--radius) + 8px);
--color-background: var(--background);
--color-foreground: var(--foreground);
--color-card: var(--card);
--color-card-foreground: var(--card-foreground);
--color-popover: var(--popover);
--color-popover-foreground: var(--popover-foreground);
--color-primary: var(--primary);
--color-primary-foreground: var(--primary-foreground);
--color-secondary: var(--secondary);
--color-secondary-foreground: var(--secondary-foreground);
--color-muted: var(--muted);
--color-muted-foreground: var(--muted-foreground);
--color-accent: var(--accent);
--color-accent-foreground: var(--accent-foreground);
--color-destructive: var(--destructive);
--color-destructive-foreground: var(--destructive-foreground);
--color-border: var(--border);
--color-input: var(--input);
--color-ring: var(--ring);
--color-chart-1: var(--chart-1);
--color-chart-2: var(--chart-2);
--color-chart-3: var(--chart-3);
--color-chart-4: var(--chart-4);
--color-chart-5: var(--chart-5);
--color-sidebar: var(--sidebar);
--color-sidebar-foreground: var(--sidebar-foreground);
--color-sidebar-primary: var(--sidebar-primary);
--color-sidebar-primary-foreground: var(--sidebar-primary-foreground);
--color-sidebar-accent: var(--sidebar-accent);
--color-sidebar-accent-foreground: var(--sidebar-accent-foreground);
--color-sidebar-border: var(--sidebar-border);
--color-sidebar-ring: var(--sidebar-ring);
--animate-accordion-down: accordion-down 0.2s ease-out;
--animate-accordion-up: accordion-up 0.2s ease-out;
--animate-fade-in: fade-in 0.2s ease-out;
--animate-slide-up: slide-up 0.25s ease-out;
--animate-scale-in: scale-in 0.2s ease-out;
@keyframes accordion-down {
from { height: 0; }
to { height: var(--radix-accordion-content-height); }
}
@keyframes accordion-up {
from { height: var(--radix-accordion-content-height); }
to { height: 0; }
}
@keyframes fade-in {
from { opacity: 0; }
to { opacity: 1; }
}
@keyframes slide-up {
from { opacity: 0; transform: translateY(8px); }
to { opacity: 1; transform: translateY(0); }
}
@keyframes scale-in {
from { opacity: 0; transform: scale(0.97); }
to { opacity: 1; transform: scale(1); }
}
}
/* Container utility */
@utility container {
margin-inline: auto;
padding-inline: 2rem;
@media (width >= 1400px) {
max-width: 1400px;
}
}
/* Font faces — local fallbacks */
@layer base { @layer base {
:root, .dark { @font-face {
--background: #0F172A; font-family: 'Geist Mono';
--foreground: #F8FAFC; font-display: swap;
--card: #0F172A; src: url('../assets/fonts/geist-mono/geist-mono.ttf') format('truetype');
--card-foreground: #F8FAFC; }
--popover: #0F172A;
--popover-foreground: #F8FAFC; @font-face {
--primary: #1E293B; font-family: 'Tomorrow';
--primary-foreground: #FFFFFF; font-weight: 400;
--secondary: #334155; font-style: normal;
--secondary-foreground: #FFFFFF; src: url('../assets/fonts/tomorrow/tomorrow-regular.ttf') format('truetype');
--muted: #272F42; }
--muted-foreground: #94A3B8; @font-face {
--accent: #22C55E; font-family: 'Tomorrow';
--accent-foreground: #FFFFFF; font-weight: 400;
--destructive: #EF4444; font-style: italic;
--destructive-foreground: #FFFFFF; src: url('../assets/fonts/tomorrow/tomorrow-italic.ttf') format('truetype');
--border: #475569; }
--input: #475569; @font-face {
--ring: #1E293B; font-family: 'Tomorrow';
--radius: 0.5rem; font-weight: 700;
font-style: normal;
src: url('../assets/fonts/tomorrow/tomorrow-bold.ttf') format('truetype');
}
@font-face {
font-family: 'Tomorrow';
font-weight: 700;
font-style: italic;
src: url('../assets/fonts/tomorrow/tomorrow-bold-italic.ttf') format('truetype');
}
}
/*
* Refined dark color palette
* Inspired by: Linear, Vercel, Raycast
* Deep neutral darks with subtle warm undertones
*/
@layer base {
:root,
.dark {
/* Core surfaces */
--background: #0a0a0f;
--foreground: #e8eaed;
--card: #111118;
--card-foreground: #e8eaed;
--popover: #16161e;
--popover-foreground: #e8eaed;
/* Interactive elements */
--primary: #e8eaed;
--primary-foreground: #0a0a0f;
--secondary: #1c1c26;
--secondary-foreground: #c4c7cc;
--muted: #16161e;
--muted-foreground: #6b7280;
--accent: #10b981;
--accent-foreground: #ffffff;
/* State colors */
--destructive: #ef4444;
--destructive-foreground: #ffffff;
/* Boundaries */
--border: #ffffff0d;
--input: #1c1c26;
--ring: #10b981;
--radius: 0.625rem;
/* Chart palette — vibrant on dark */
--chart-1: #6366f1;
--chart-2: #10b981;
--chart-3: #f59e0b;
--chart-4: #ec4899;
--chart-5: #8b5cf6;
/* Sidebar */
--sidebar: #0a0a0f;
--sidebar-foreground: #e8eaed;
--sidebar-primary: #e8eaed;
--sidebar-primary-foreground: #0a0a0f;
--sidebar-accent: #10b981;
--sidebar-accent-foreground: #ffffff;
--sidebar-border: #ffffff0d;
--sidebar-ring: #10b981;
}
}
/* Global base styles */
@layer base {
* {
@apply border-border;
}
body {
@apply bg-background text-foreground overflow-hidden antialiased;
font-feature-settings: 'cv02', 'cv03', 'cv04', 'cv11';
-webkit-font-smoothing: antialiased;
-moz-osx-font-smoothing: grayscale;
}
.draglayer {
@apply bg-background;
-webkit-app-region: drag;
}
button {
@apply cursor-pointer;
}
/* Smooth scrollbar for dark mode */
::-webkit-scrollbar {
width: 6px;
height: 6px;
}
::-webkit-scrollbar-track {
background: transparent;
}
::-webkit-scrollbar-thumb {
background: #ffffff1a;
border-radius: 3px;
}
::-webkit-scrollbar-thumb:hover {
background: #ffffff33;
}
/* Selection color */
::selection {
background: #10b98133;
color: #e8eaed;
}
}
/* Utility classes for the design system */
@layer utilities {
.glass {
background: rgba(17, 17, 24, 0.6);
backdrop-filter: blur(12px);
-webkit-backdrop-filter: blur(12px);
}
.glass-border {
border: 1px solid rgba(255, 255, 255, 0.06);
}
.glow-accent {
box-shadow: 0 0 20px rgba(16, 185, 129, 0.1);
}
.glow-accent-sm {
box-shadow: 0 0 10px rgba(16, 185, 129, 0.08);
}
.text-gradient {
background: linear-gradient(135deg, #e8eaed 0%, #6b7280 100%);
-webkit-background-clip: text;
-webkit-text-fill-color: transparent;
background-clip: text;
}
.animate-in {
animation: fade-in 0.2s ease-out, slide-up 0.25s ease-out;
} }
} }
+20 -20
View File
@@ -87,50 +87,50 @@ export type TokenUsageRow = z.infer<typeof TokenUsageRowSchema>;
export const HourlyUsageSchema = z.object({ export const HourlyUsageSchema = z.object({
bucket: z.string(), bucket: z.string(),
promptTokens: z.number(), promptTokens: z.number().nullable().transform((v) => v ?? 0),
completionTokens: z.number(), completionTokens: z.number().nullable().transform((v) => v ?? 0),
totalTokens: z.number(), totalTokens: z.number().nullable().transform((v) => v ?? 0),
requests: z.number(), requests: z.number().nullable().transform((v) => v ?? 0),
}); });
export type HourlyUsage = z.infer<typeof HourlyUsageSchema>; export type HourlyUsage = z.infer<typeof HourlyUsageSchema>;
export const DailyUsageSchema = z.object({ export const DailyUsageSchema = z.object({
bucket: z.string(), bucket: z.string(),
promptTokens: z.number(), promptTokens: z.number().nullable().transform((v) => v ?? 0),
completionTokens: z.number(), completionTokens: z.number().nullable().transform((v) => v ?? 0),
totalTokens: z.number(), totalTokens: z.number().nullable().transform((v) => v ?? 0),
requests: z.number(), requests: z.number().nullable().transform((v) => v ?? 0),
}); });
export type DailyUsage = z.infer<typeof DailyUsageSchema>; export type DailyUsage = z.infer<typeof DailyUsageSchema>;
export const WeeklyUsageSchema = z.object({ export const WeeklyUsageSchema = z.object({
bucket: z.string(), bucket: z.string(),
promptTokens: z.number(), promptTokens: z.number().nullable().transform((v) => v ?? 0),
completionTokens: z.number(), completionTokens: z.number().nullable().transform((v) => v ?? 0),
totalTokens: z.number(), totalTokens: z.number().nullable().transform((v) => v ?? 0),
requests: z.number(), requests: z.number().nullable().transform((v) => v ?? 0),
}); });
export type WeeklyUsage = z.infer<typeof WeeklyUsageSchema>; export type WeeklyUsage = z.infer<typeof WeeklyUsageSchema>;
export const MonthlyUsageSchema = z.object({ export const MonthlyUsageSchema = z.object({
bucket: z.string(), bucket: z.string(),
promptTokens: z.number(), promptTokens: z.number().nullable().transform((v) => v ?? 0),
completionTokens: z.number(), completionTokens: z.number().nullable().transform((v) => v ?? 0),
totalTokens: z.number(), totalTokens: z.number().nullable().transform((v) => v ?? 0),
requests: z.number(), requests: z.number().nullable().transform((v) => v ?? 0),
}); });
export type MonthlyUsage = z.infer<typeof MonthlyUsageSchema>; export type MonthlyUsage = z.infer<typeof MonthlyUsageSchema>;
export const ModelUsageSchema = z.object({ export const ModelUsageSchema = z.object({
model: z.string(), model: z.string(),
promptTokens: z.number(), promptTokens: z.number().nullable().transform((v) => v ?? 0),
completionTokens: z.number(), completionTokens: z.number().nullable().transform((v) => v ?? 0),
totalTokens: z.number(), totalTokens: z.number().nullable().transform((v) => v ?? 0),
requests: z.number(), requests: z.number().nullable().transform((v) => v ?? 0),
}); });
export type ModelUsage = z.infer<typeof ModelUsageSchema>; export type ModelUsage = z.infer<typeof ModelUsageSchema>;