Files
gemini-nexus/src/routes/settings.tsx
T
Gemini Nexus Bot a905877c76 feat: bump version to 0.14.0
Includes all changes from this release cycle:
- Top header navigation (sidebar removed)
- Accounts screen decomposition and visual redesign
- Dev/prod environment isolation
- Usage Analytics with accurate averages, cost reporting, OpenRouter pricing
- Monitor moved to Usage with per-request token/cost tracking
- Clickable traffic logs with expandable JSON body viewer
- OpenCode IDE config in Quick Setup
- API Proxy reorganization (flattened tabs)
- CI/CD pipeline fixes (release config, Node version, pnpm alignment)
- Repository cleanup (~80MB of binaries removed)
2026-05-02 00:32:21 -03:00

399 lines
17 KiB
TypeScript

import { createFileRoute } from '@tanstack/react-router';
import { useTheme } from '@/components/theme-provider';
import { Card, CardContent, CardDescription, CardHeader, CardTitle } from '@/components/ui/card';
import { Label } from '@/components/ui/label';
import { Switch } from '@/components/ui/switch';
import { Input } from '@/components/ui/input';
import { Tabs, TabsContent, TabsList, TabsTrigger } from '@/components/ui/tabs';
import {
Select,
SelectContent,
SelectItem,
SelectTrigger,
SelectValue,
} from '@/components/ui/select';
import { useQuery } from '@tanstack/react-query';
import { getAppVersion, getPlatform } from '@/actions/app';
import { useTranslation } from 'react-i18next';
import { setAppLanguage } from '@/actions/language';
import { useAppConfig } from '@/hooks/useAppConfig';
import { useToast } from '@/components/ui/use-toast';
import { Loader2, FolderOpen } from 'lucide-react';
import { ModelVisibilitySettings } from '@/components/ModelVisibilitySettings';
import { useEffect, useState } from 'react';
import { ProxyConfig } from '@/types/config';
import { openLogDirectory } from '@/actions/system';
import { AccountProxySettings } from '@/components/AccountProxySettings';
function SettingsPage() {
const { theme, setTheme } = useTheme();
const { t, i18n } = useTranslation();
const { config, isLoading, saveConfig } = useAppConfig();
const { toast } = useToast();
// Local state for configuration editing
const [proxyConfig, setProxyConfig] = useState<ProxyConfig | undefined>(undefined);
// Sync config to local state when loaded
useEffect(() => {
if (config) {
// eslint-disable-next-line
setProxyConfig(config.proxy);
}
}, [config]);
const { data: appVersion } = useQuery({
queryKey: ['app', 'version'],
queryFn: getAppVersion,
});
const { data: platform } = useQuery({
queryKey: ['app', 'platform'],
queryFn: getPlatform,
});
const isAutoStartSupported =
platform === 'win32' || platform === 'darwin' || platform === 'linux';
const isMac = platform === 'darwin';
const handleLanguageChange = (value: string) => {
setAppLanguage(value, i18n);
};
// Helper to update proxyConfig and auto-save
const updateProxyConfig = async (newProxyConfig: ProxyConfig) => {
setProxyConfig(newProxyConfig);
if (config) {
await saveConfig({ ...config, proxy: newProxyConfig });
}
};
if (isLoading || !proxyConfig) {
return (
<div className="flex h-full items-center justify-center">
<Loader2 className="animate-spin" />
</div>
);
}
return (
<div className="container mx-auto max-w-4xl space-y-5 p-6">
<div>
<h2 className="text-lg font-semibold tracking-tight">{t('settings.title')}</h2>
<p className="text-muted-foreground mt-1 text-[13px]">{t('settings.description')}</p>
</div>
<Tabs defaultValue="general" className="w-full">
<TabsList className="grid w-full grid-cols-3">
<TabsTrigger value="general">{t('settings.general')}</TabsTrigger>
<TabsTrigger value="models">{t('settings.models')}</TabsTrigger>
<TabsTrigger value="proxy">{t('settings.proxy_tab')}</TabsTrigger>
</TabsList>
{/* --- GENERAL TAB --- */}
<TabsContent value="general" className="space-y-5">
<Card className="rounded-none border-0 border-b border-white/5 bg-transparent pb-6 shadow-none last:border-0">
<CardHeader className="px-0 pt-4">
<CardTitle>{t('settings.appearance.title')}</CardTitle>
<CardDescription>{t('settings.appearance.description')}</CardDescription>
</CardHeader>
<CardContent className="space-y-5">
<div className="flex items-center justify-between space-x-2">
<div className="space-y-1">
<Label htmlFor="dark-mode">{t('settings.darkMode')}</Label>
<p className="text-muted-foreground text-xs">
{t('settings.darkModeDescription')}
</p>
</div>
<Switch
id="dark-mode"
checked={theme === 'dark'}
onCheckedChange={(checked) => setTheme(checked ? 'dark' : 'light')}
/>
</div>
<div className="flex items-center justify-between space-x-2">
<div className="space-y-1">
<Label htmlFor="language">{t('settings.language.title')}</Label>
<p className="text-muted-foreground text-sm">
{t('settings.language.description')}
</p>
</div>
<Select
value={i18n.language}
onValueChange={handleLanguageChange}
key={i18n.language}
>
<SelectTrigger className="w-[180px]">
<SelectValue placeholder={t('settings.language.title')} />
</SelectTrigger>
<SelectContent>
<SelectItem value="en">{t('settings.language.english')}</SelectItem>
<SelectItem value="zh-CN">{t('settings.language.chinese')}</SelectItem>
<SelectItem value="ru">{t('settings.language.russian')}</SelectItem>
<SelectItem value="vi">{t('settings.language.vietnamese')}</SelectItem>
</SelectContent>
</Select>
</div>
</CardContent>
</Card>
{/* Account Settings Card */}
<Card className="rounded-none border-0 border-b border-white/5 bg-transparent pb-6 shadow-none last:border-0">
<CardHeader className="px-0 pt-4">
<CardTitle>{t('settings.account.title')}</CardTitle>
<CardDescription>{t('settings.account.description')}</CardDescription>
</CardHeader>
<CardContent className="space-y-4">
{/* Auto Refresh Quota */}
<div className="flex items-center justify-between rounded-lg border p-4">
<div className="space-y-1">
<Label>{t('settings.account.auto_refresh')}</Label>
<p className="text-muted-foreground text-xs">
{t('settings.account.auto_refresh_desc')}
</p>
</div>
<Switch
checked={config?.auto_refresh || false}
onCheckedChange={async (checked) => {
if (config) {
await saveConfig({ ...config, auto_refresh: checked });
}
}}
/>
</div>
{/* Auto Sync Account */}
<div className="flex items-center justify-between rounded-lg border p-4">
<div className="space-y-1">
<Label>{t('settings.account.auto_sync')}</Label>
<p className="text-muted-foreground text-xs">
{t('settings.account.auto_sync_desc')}
</p>
</div>
<Switch
checked={config?.auto_sync || false}
onCheckedChange={async (checked) => {
if (config) {
await saveConfig({ ...config, auto_sync: checked });
}
}}
/>
</div>
</CardContent>
</Card>
{isAutoStartSupported && (
<Card className="rounded-none border-0 border-b border-white/5 bg-transparent pb-6 shadow-none last:border-0">
<CardHeader className="px-0 pt-4">
<CardTitle>{t('settings.startup.title')}</CardTitle>
<CardDescription>{t('settings.startup.description')}</CardDescription>
</CardHeader>
<CardContent className="space-y-4">
<div className="flex items-center justify-between rounded-lg border p-4">
<div className="space-y-1">
<Label>{t('settings.startup.auto_startup')}</Label>
<p className="text-muted-foreground text-xs">
{t('settings.startup.auto_startup_desc')}
</p>
</div>
<Switch
checked={config?.auto_startup || false}
onCheckedChange={async (checked) => {
if (config) {
await saveConfig({ ...config, auto_startup: checked });
}
}}
/>
</div>
{isMac && (
<p className="text-muted-foreground text-xs">
{t('settings.startup.macos_hint')}
</p>
)}
</CardContent>
</Card>
)}
<Card className="rounded-none border-0 border-b border-white/5 bg-transparent pb-6 shadow-none last:border-0">
<CardHeader className="px-0 pt-4">
<CardTitle>{t('settings.about.title')}</CardTitle>
<CardDescription>{t('settings.about.description')}</CardDescription>
</CardHeader>
<CardContent className="space-y-4">
<div className="grid grid-cols-2 gap-4 text-[13px]">
<div className="text-muted-foreground">{t('settings.version')}</div>
<div className="font-medium">{appVersion || t('common.unknown')}</div>
<div className="text-muted-foreground">{t('settings.platform')}</div>
<div className="font-medium capitalize">{platform || t('common.unknown')}</div>
<div className="text-muted-foreground">{t('settings.license')}</div>
<div className="font-medium">CC BY-NC-SA 4.0</div>
<div className="text-muted-foreground">{t('action.openLogs')}</div>
<button
onClick={() => openLogDirectory()}
className="text-accent hover:text-accent/80 flex items-center gap-2 font-medium transition-colors"
>
<FolderOpen className="h-4 w-4" />
<span>{t('settings.openLogDir')}</span>
</button>
</div>
</CardContent>
</Card>
{/* Privacy & Error Reporting Card */}
<Card className="rounded-none border-0 border-b border-white/5 bg-transparent pb-6 shadow-none last:border-0">
<CardHeader className="px-0 pt-4">
<CardTitle>{t('settings.privacy.title')}</CardTitle>
<CardDescription>{t('settings.privacy.description')}</CardDescription>
</CardHeader>
<CardContent className="space-y-4">
<div className="flex items-center justify-between rounded-lg border p-4">
<div className="space-y-1">
<Label>{t('settings.privacy.error_reporting')}</Label>
<p className="text-muted-foreground text-xs">
{t('settings.privacy.error_reporting_desc')}
</p>
</div>
<Switch
checked={config?.error_reporting_enabled || false}
onCheckedChange={async (checked) => {
if (config) {
await saveConfig({ ...config, error_reporting_enabled: checked });
}
}}
/>
</div>
<p className="text-muted-foreground text-xs">{t('settings.privacy.restart_note')}</p>
</CardContent>
</Card>
{/* Notifications Card */}
<Card className="rounded-none border-0 border-b border-white/5 bg-transparent pb-6 shadow-none last:border-0">
<CardHeader className="px-0 pt-4">
<CardTitle>{t('settings.notifications.title')}</CardTitle>
<CardDescription>{t('settings.notifications.description')}</CardDescription>
</CardHeader>
<CardContent className="space-y-4">
<div className="flex items-center justify-between rounded-lg border p-4">
<div className="space-y-1">
<Label>{t('settings.notifications.quotaAlert')}</Label>
<p className="text-muted-foreground text-xs">
{t('settings.notifications.quotaAlertDesc')}
</p>
</div>
<Switch
checked={config?.quota_alert_enabled || false}
onCheckedChange={async (checked) => {
if (config) {
try {
await saveConfig({ ...config, quota_alert_enabled: checked });
} catch {
toast({
title: t('common.error'),
description: t('settings.notifications.saveFailed'),
variant: 'destructive',
});
}
}
}}
/>
</div>
<div className="flex items-center justify-between rounded-lg border p-4">
<div className="space-y-1">
<Label>{t('settings.notifications.quotaThreshold')}</Label>
<p className="text-muted-foreground text-xs">
{t('settings.notifications.quotaThresholdDesc')}
</p>
</div>
<div className="flex items-center gap-2">
<input
type="number"
min={0}
max={100}
value={config?.quota_alert_threshold ?? 20}
onChange={async (e) => {
const rawValue = e.target.value;
const parsed = parseInt(rawValue, 10);
if (isNaN(parsed) || parsed < 0 || parsed > 100) return;
if (config) {
try {
await saveConfig({ ...config, quota_alert_threshold: parsed });
} catch {
toast({
title: t('common.error'),
description: t('settings.notifications.thresholdSaveFailed'),
variant: 'destructive',
});
}
}
}}
className="bg-muted/50 text-foreground focus:border-accent/50 w-16 rounded-lg border border-white/[0.08] px-2 py-1 text-center text-[13px] focus:outline-none"
/>
<span className="text-muted-foreground text-sm">%</span>
</div>
</div>
</CardContent>
</Card>
</TabsContent>
{/* --- MODELS TAB --- */}
<TabsContent value="models" className="space-y-5">
<ModelVisibilitySettings />
</TabsContent>
{/* --- PROXY TAB (Upstream Proxy Config Only) --- */}
<TabsContent value="proxy" className="space-y-5">
<Card className="rounded-none border-0 border-b border-white/5 bg-transparent pb-6 shadow-none last:border-0">
<CardHeader className="px-0 pt-4">
<CardTitle>{t('settings.proxy.title')}</CardTitle>
<CardDescription>{t('settings.proxy.description')}</CardDescription>
</CardHeader>
<CardContent className="space-y-4">
<div className="flex items-center justify-between space-x-2">
<div className="space-y-1">
<Label htmlFor="upstream-proxy-enabled">{t('settings.proxy.enable')}</Label>
</div>
<Switch
id="upstream-proxy-enabled"
checked={proxyConfig.upstream_proxy.enabled}
onCheckedChange={(checked) =>
updateProxyConfig({
...proxyConfig,
upstream_proxy: { ...proxyConfig.upstream_proxy, enabled: checked },
})
}
/>
</div>
<div className="space-y-2">
<Label htmlFor="upstream-proxy-url">{t('settings.proxy.url')}</Label>
<Input
id="upstream-proxy-url"
placeholder="http://127.0.0.1:7890"
value={proxyConfig.upstream_proxy.url}
onChange={(e) =>
updateProxyConfig({
...proxyConfig,
upstream_proxy: { ...proxyConfig.upstream_proxy, url: e.target.value },
})
}
disabled={!proxyConfig.upstream_proxy.enabled}
/>
</div>
</CardContent>
</Card>
<AccountProxySettings />
</TabsContent>
</Tabs>
</div>
);
}
export const Route = createFileRoute('/settings')({
component: SettingsPage,
});