feat: complete rebranding to Gemini Nexus with improved UI, proxies and documentation

This commit is contained in:
Gemini Nexus Bot committed 2026-04-22 00:00:56 -03:00
1 parent e524a340cb
commit 433dccbaa9
108 files changed
+1004 -1139

No files matched your search

+113
View File
@@ -0,0 +1,113 @@
import React, { useState } from 'react';
import { useTranslation } from 'react-i18next';
import { useCloudAccounts, useSetAccountProxy } from '@/hooks/useCloudAccounts';
import { Card, CardContent, CardDescription, CardHeader, CardTitle } from '@/components/ui/card';
import { Input } from '@/components/ui/input';
import { isValidProxyUrl } from '@/utils/url';
import { Loader2 } from 'lucide-react';
import { CloudAccount } from '@/types/cloudAccount';
function AccountProxyItem({ account }: { account: CloudAccount }) {
const { t } = useTranslation();
const setAccountProxy = useSetAccountProxy();
const [proxyUrl, setProxyUrl] = useState(account.proxy_url || '');
const [proxySaved, setProxySaved] = useState(false);
return (
<div className="flex items-center justify-between gap-4 py-3 border-b last:border-0">
<div className="flex items-center gap-3 overflow-hidden">
{account.avatar_url ? (
<img
src={account.avatar_url}
alt={account.name || ''}
className="bg-muted h-8 w-8 rounded-full border shrink-0"
/>
) : (
<div className="bg-primary/10 text-primary flex h-8 w-8 items-center justify-center rounded-full border shrink-0">
{account.name?.[0]?.toUpperCase() || 'A'}
</div>
)}
<div className="flex flex-col overflow-hidden">
<span className="truncate text-sm font-semibold">
{account.name || t('cloud.card.unknown')}
</span>
<span className="truncate text-xs text-muted-foreground">{account.email}</span>
</div>
</div>
<div className="flex items-center gap-2">
<Input
value={proxyUrl}
onChange={(e) => {
setProxyUrl(e.target.value);
setProxySaved(false);
}}
onBlur={() => {
const trimmed = proxyUrl.trim();
if (trimmed && !isValidProxyUrl(trimmed)) {
setProxyUrl(account.proxy_url || '');
return;
}
if (trimmed !== (account.proxy_url || '')) {
setAccountProxy.mutate({
accountId: account.id,
proxyUrl: trimmed || null,
});
setProxySaved(true);
setTimeout(() => setProxySaved(false), 2000);
}
}}
placeholder={t('cloud.card.proxyPlaceholder') || 'e.g. http://127.0.0.1:7890'}
className="h-8 w-48 text-sm"
onKeyDown={(e) => {
if (e.key === 'Enter') {
e.currentTarget.blur();
}
}}
/>
<div className="w-16">
{proxySaved && (
<span className="text-[10px] text-green-500 whitespace-nowrap">{t('cloud.card.proxySaved')}</span>
)}
</div>
</div>
</div>
);
}
export function AccountProxySettings() {
const { t } = useTranslation();
const { data: accounts, isLoading } = useCloudAccounts();
if (isLoading) {
return (
<Card>
<CardHeader>
<CardTitle>{t('settings.proxy.accountProxies.title')}</CardTitle>
<CardDescription>{t('settings.proxy.accountProxies.description')}</CardDescription>
</CardHeader>
<CardContent className="flex justify-center p-6">
<Loader2 className="animate-spin text-muted-foreground" />
</CardContent>
</Card>
);
}
if (!accounts || accounts.length === 0) {
return null;
}
return (
<Card>
<CardHeader>
<CardTitle>{t('settings.proxy.accountProxies.title')}</CardTitle>
<CardDescription>{t('settings.proxy.accountProxies.description')}</CardDescription>
</CardHeader>
<CardContent className="flex flex-col">
{accounts.map((account) => (
<AccountProxyItem key={account.id} account={account} />
))}
</CardContent>
</Card>
);
}
+1 -41
View File
@@ -35,9 +35,6 @@ import {
getQuotaStatus,
type QuotaStatus,
} from '@/utils/quota-display';
import { useState } from 'react';
import { useSetAccountProxy } from '@/hooks/useCloudAccounts';
import { isValidProxyUrl } from '@/utils/url';
import { getValidationBlockedStatusLabel } from '@/components/accountValidationStatus';
type ModelQuotaEntry = [string, CloudQuotaModelInfo];
@@ -186,9 +183,6 @@ export function CloudAccountCard({
isProviderCollapsed,
toggleProviderCollapse,
} = useProviderGrouping();
const setAccountProxy = useSetAccountProxy();
const [proxyUrl, setProxyUrl] = useState(account.proxy_url || '');
const [proxySaved, setProxySaved] = useState(false);
const getQuotaTextColorClass = (percentage: number) => {
const quotaStatus = getQuotaStatus(percentage);
@@ -558,45 +552,11 @@ export function CloudAccountCard({
</div>
</CardContent>
<CardFooter className="bg-muted/20 text-muted-foreground justify-between border-t p-2 px-4 text-xs">
<CardFooter className="bg-muted/20 text-muted-foreground justify-center border-t p-2 px-4 text-xs">
<span>
{t('cloud.card.used')}{' '}
{formatDistanceToNow(account.last_used * 1000, { addSuffix: true })}
</span>
<div className="flex items-center gap-2">
<Input
value={proxyUrl}
onChange={(e) => {
setProxyUrl(e.target.value);
setProxySaved(false);
}}
onBlur={() => {
const trimmed = proxyUrl.trim();
if (trimmed && !isValidProxyUrl(trimmed)) {
setProxyUrl(account.proxy_url || '');
return;
}
if (trimmed !== (account.proxy_url || '')) {
setAccountProxy.mutate({
accountId: account.id,
proxyUrl: trimmed || null,
});
setProxySaved(true);
setTimeout(() => setProxySaved(false), 2000);
}
}}
placeholder={t('cloud.card.proxyPlaceholder')}
className="h-6 w-40 text-xs"
onKeyDown={(e) => {
if (e.key === 'Enter') {
e.currentTarget.blur();
}
}}
/>
{proxySaved && (
<span className="text-[10px] text-green-500">{t('cloud.card.proxySaved')}</span>
)}
</div>
</CardFooter>
</Card>
);
+29 -7
View File
@@ -9,7 +9,7 @@ import { Checkbox } from '@/components/ui/checkbox';
import { Badge } from '@/components/ui/badge';
import { Switch } from '@/components/ui/switch';
import { Label } from '@/components/ui/label';
import { Loader2, Search, RotateCcw, Save } from 'lucide-react';
import { Loader2, Search, RotateCcw, Save, Copy, CheckCircle } from 'lucide-react';
import { filter, flatMap, includes, size, sortBy, sumBy, uniq, values } from 'lodash-es';
import type { CloudAccount } from '@/types/cloudAccount';
@@ -44,6 +44,7 @@ export function ModelVisibilitySettings() {
const [modelVisibilityMap, setModelVisibilityMap] = useState<Record<string, boolean>>({});
const [providerGroupingEnabled, setProviderGroupingEnabled] = useState(false);
const [isSavingSettings, setIsSavingSettings] = useState(false);
const [copiedModel, setCopiedModel] = useState<string | null>(null);
// Initialize model visibility and provider groupings from config
useEffect(() => {
@@ -114,6 +115,14 @@ export function ModelVisibilitySettings() {
}));
};
const copyModelId = (modelId: string, e: React.MouseEvent) => {
e.preventDefault();
e.stopPropagation();
navigator.clipboard.writeText(modelId);
setCopiedModel(modelId);
setTimeout(() => setCopiedModel(null), 2000);
};
if (accountsLoading) {
return (
<Card>
@@ -188,12 +197,25 @@ export function ModelVisibilitySettings() {
handleModelVisibilityChange(modelId, checked as boolean)
}
/>
<label
htmlFor={`model-${modelId}`}
className="flex-1 cursor-pointer text-sm font-medium"
>
{modelId}
</label>
<div className="flex flex-1 items-center gap-2">
<label
htmlFor={`model-${modelId}`}
className="cursor-pointer text-sm font-medium"
>
{modelId}
</label>
<button
onClick={(e) => copyModelId(modelId, e)}
className="text-muted-foreground hover:text-foreground transition-colors"
title={t('proxy.copy')}
>
{copiedModel === modelId ? (
<CheckCircle className="h-4 w-4 text-green-500" />
) : (
<Copy className="h-4 w-4" />
)}
</button>
</div>
{!isVisible && (
<Badge variant="secondary" className="text-xs">
{t('settings.modelVisibility.hidden')}
+3 -3
View File
@@ -1,6 +1,6 @@
import React from 'react';
import { useQuery, useMutation, useQueryClient } from '@tanstack/react-query';
import { isProcessRunning, startAntigravity, closeAntigravity } from '@/actions/process';
import { isProcessRunning, startGeminiNexus, closeGeminiNexus } from '@/actions/process';
import { Button } from '@/components/ui/button';
import { cn } from '@/lib/utils';
import { Play, Square, Loader2, Power } from 'lucide-react';
@@ -22,14 +22,14 @@ export const StatusBar: React.FC<StatusBarProps> = ({ isCollapsed = false }) =>
});
const startMutation = useMutation({
mutationFn: startAntigravity,
mutationFn: startGeminiNexus,
onSuccess: () => {
queryClient.invalidateQueries({ queryKey: ['process', 'status'] });
},
});
const stopMutation = useMutation({
mutationFn: closeAntigravity,
mutationFn: closeGeminiNexus,
onSuccess: () => {
queryClient.invalidateQueries({ queryKey: ['process', 'status'] });
},