Compare commits

...

5 Commits

Author SHA1 Message Date
Supra4E8C
eadfd7a957 feat(quota): group Gemini CLI buckets and refine Gemini quota groups 2025-12-30 21:48:12 +08:00
Supra4E8C
f739e0b372 style(config): double editor height 2025-12-30 18:29:47 +08:00
Supra4E8C
23fb88e5fd feat(quota): add zustand store for quota state caching 2025-12-30 18:29:28 +08:00
Supra4E8C
49b9259452 feat(quota): add quota page and update i18n 2025-12-30 14:13:04 +08:00
Supra4E8C
4e26b6c92d feat(auth-files): add Gemini CLI quota card and API call 2025-12-30 12:18:20 +08:00
14 changed files with 2503 additions and 1234 deletions

1
.gitignore vendored
View File

@@ -11,6 +11,7 @@ usage.json
CLAUDE.md
AGENTS.md
antigravity_usage.json
codex_usage.json
node_modules
dist

View File

@@ -7,6 +7,7 @@ import { ApiKeysPage } from '@/pages/ApiKeysPage';
import { AiProvidersPage } from '@/pages/AiProvidersPage';
import { AuthFilesPage } from '@/pages/AuthFilesPage';
import { OAuthPage } from '@/pages/OAuthPage';
import { QuotaPage } from '@/pages/QuotaPage';
import { UsagePage } from '@/pages/UsagePage';
import { ConfigPage } from '@/pages/ConfigPage';
import { LogsPage } from '@/pages/LogsPage';
@@ -88,6 +89,7 @@ function App() {
<Route path="ai-providers" element={<AiProvidersPage />} />
<Route path="auth-files" element={<AuthFilesPage />} />
<Route path="oauth" element={<OAuthPage />} />
<Route path="quota" element={<QuotaPage />} />
<Route path="usage" element={<UsagePage />} />
<Route path="config" element={<ConfigPage />} />
<Route path="logs" element={<LogsPage />} />

View File

@@ -23,6 +23,7 @@ import {
IconSettings,
IconShield,
IconSlidersHorizontal,
IconTimer,
} from '@/components/ui/icons';
import { INLINE_LOGO_JPEG } from '@/assets/logoInline';
import {
@@ -41,6 +42,7 @@ const sidebarIcons: Record<string, ReactNode> = {
aiProviders: <IconBot size={18} />,
authFiles: <IconFileText size={18} />,
oauth: <IconShield size={18} />,
quota: <IconTimer size={18} />,
usage: <IconChartLine size={18} />,
config: <IconSettings size={18} />,
logs: <IconScrollText size={18} />,
@@ -355,6 +357,7 @@ export function MainLayout() {
{ path: '/ai-providers', label: t('nav.ai_providers'), icon: sidebarIcons.aiProviders },
{ path: '/auth-files', label: t('nav.auth_files'), icon: sidebarIcons.authFiles },
{ path: '/oauth', label: t('nav.oauth', { defaultValue: 'OAuth' }), icon: sidebarIcons.oauth },
{ path: '/quota', label: t('nav.quota_management'), icon: sidebarIcons.quota },
{ path: '/usage', label: t('nav.usage_stats'), icon: sidebarIcons.usage },
{ path: '/config', label: t('nav.config_management'), icon: sidebarIcons.config },
...(config?.loggingToFile

View File

@@ -90,6 +90,7 @@
"ai_providers": "AI Providers",
"auth_files": "Auth Files",
"oauth": "OAuth Login",
"quota_management": "Quota Management",
"usage_stats": "Usage Statistics",
"config_management": "Config Management",
"logs": "Logs Viewer",
@@ -392,6 +393,20 @@
"plan_team": "Team",
"plan_free": "Free"
},
"gemini_cli_quota": {
"title": "Gemini CLI Quota",
"empty_title": "No Gemini CLI Auth Files",
"empty_desc": "Upload a Gemini CLI credential to view remaining quota.",
"idle": "Not loaded. Click Refresh Button.",
"loading": "Loading quota...",
"load_failed": "Failed to load quota: {{message}}",
"missing_auth_index": "Auth file missing auth_index",
"missing_project_id": "Gemini CLI credential missing project ID",
"empty_buckets": "No quota data available",
"refresh_button": "Refresh Quota",
"fetch_all": "Fetch All",
"remaining_amount": "Remaining {{count}}"
},
"vertex_import": {
"title": "Vertex JSON Login",
"description": "Upload a Google service account JSON to store it as auth-dir/vertex-<project>.json using the same rules as the CLI vertex-import helper.",
@@ -690,6 +705,11 @@
"search_prev": "Previous",
"search_next": "Next"
},
"quota_management": {
"title": "Quota Management",
"description": "Monitor OAuth quota status for Antigravity, Codex, and Gemini CLI credentials.",
"refresh_files": "Refresh auth files"
},
"system_info": {
"title": "Management Center Info",
"connection_status_title": "Connection Status",

View File

@@ -90,6 +90,7 @@
"ai_providers": "AI 提供商",
"auth_files": "认证文件",
"oauth": "OAuth 登录",
"quota_management": "配额管理",
"usage_stats": "使用统计",
"config_management": "配置管理",
"logs": "日志查看",
@@ -392,6 +393,20 @@
"plan_team": "Team",
"plan_free": "Free"
},
"gemini_cli_quota": {
"title": "Gemini CLI 额度",
"empty_title": "暂无 Gemini CLI 认证",
"empty_desc": "上传 Gemini CLI 认证文件后即可查看额度。",
"idle": "尚未加载额度,请点击刷新按钮。",
"loading": "正在加载额度...",
"load_failed": "额度获取失败:{{message}}",
"missing_auth_index": "认证文件缺少 auth_index",
"missing_project_id": "Gemini CLI 凭证缺少 Project ID",
"empty_buckets": "暂无额度数据",
"refresh_button": "刷新额度",
"fetch_all": "获取全部",
"remaining_amount": "剩余 {{count}}"
},
"vertex_import": {
"title": "Vertex JSON 登录",
"description": "上传 Google 服务账号 JSON使用 CLI vertex-import 同步规则写入 auth-dir/vertex-<project>.json。",
@@ -690,6 +705,11 @@
"search_prev": "上一个",
"search_next": "下一个"
},
"quota_management": {
"title": "配额管理",
"description": "集中查看 OAuth 额度与剩余情况",
"refresh_files": "刷新认证文件"
},
"system_info": {
"title": "管理中心信息",
"connection_status_title": "连接状态",

View File

@@ -190,6 +190,20 @@
}
}
.geminiCliGrid {
display: grid;
gap: $spacing-md;
grid-template-columns: repeat(3, minmax(0, 1fr));
@include tablet {
grid-template-columns: repeat(2, minmax(0, 1fr));
}
@include mobile {
grid-template-columns: 1fr;
}
}
.antigravityControls {
display: flex;
gap: $spacing-md;
@@ -232,6 +246,27 @@
}
}
.geminiCliControls {
display: flex;
gap: $spacing-md;
flex-wrap: wrap;
align-items: flex-end;
margin-bottom: $spacing-md;
}
.geminiCliControl {
display: flex;
flex-direction: column;
gap: 4px;
label {
font-size: 12px;
color: var(--text-secondary);
font-weight: 500;
white-space: nowrap;
}
}
.antigravityCard {
background-image: linear-gradient(
180deg,
@@ -248,6 +283,14 @@
);
}
.geminiCliCard {
background-image: linear-gradient(
180deg,
rgba(231, 239, 255, 0.2),
rgba(231, 239, 255, 0)
);
}
.quotaSection {
display: flex;
flex-direction: column;
@@ -338,6 +381,10 @@
color: var(--text-tertiary);
}
.quotaAmount {
color: var(--text-secondary);
}
.quotaMessage {
font-size: 12px;
color: var(--text-tertiary);

File diff suppressed because it is too large Load Diff

View File

@@ -134,7 +134,7 @@
.editorWrapper {
width: 100%;
flex: 1;
min-height: 400px;
min-height: 800px;
border: 1px solid var(--border-color);
border-radius: $radius-lg;
overflow: hidden;
@@ -219,7 +219,7 @@
.configCard {
display: flex;
flex-direction: column;
height: 560px;
height: 1120px;
flex-shrink: 0;
overflow: visible;
}
@@ -253,11 +253,11 @@
}
.configCard {
height: 440px;
height: 880px;
padding: $spacing-md;
}
.editorWrapper {
min-height: 300px;
min-height: 600px;
}
}

View File

@@ -0,0 +1,333 @@
@use '../styles/variables' as *;
@use '../styles/mixins' as *;
.container {
display: flex;
flex-direction: column;
gap: $spacing-lg;
}
.pageHeader {
display: flex;
flex-direction: column;
gap: $spacing-sm;
}
.pageTitle {
font-size: 28px;
font-weight: 700;
color: var(--text-primary);
margin: 0;
}
.description {
font-size: 14px;
color: var(--text-secondary);
margin: 0;
}
.headerActions {
display: flex;
gap: $spacing-sm;
flex-wrap: wrap;
}
.errorBox {
padding: $spacing-md;
background-color: rgba(239, 68, 68, 0.1);
border: 1px solid var(--danger-color);
border-radius: $radius-md;
color: var(--danger-color);
font-size: 14px;
}
.pageSizeSelect {
padding: 8px 12px;
border: 1px solid var(--border-color);
border-radius: $radius-md;
background-color: var(--bg-primary);
color: var(--text-primary);
font-size: 14px;
cursor: pointer;
height: 38px;
box-sizing: border-box;
&:focus {
outline: none;
border-color: var(--primary-color);
}
}
.statsInfo {
padding: 8px 12px;
background-color: var(--bg-secondary);
border-radius: $radius-md;
font-size: 13px;
color: var(--text-secondary);
white-space: nowrap;
height: 38px;
box-sizing: border-box;
display: flex;
align-items: center;
}
.antigravityGrid,
.codexGrid,
.geminiCliGrid {
display: grid;
gap: $spacing-md;
grid-template-columns: repeat(3, minmax(0, 1fr));
@include tablet {
grid-template-columns: repeat(2, minmax(0, 1fr));
}
@include mobile {
grid-template-columns: 1fr;
}
}
.antigravityControls,
.codexControls,
.geminiCliControls {
display: flex;
gap: $spacing-md;
flex-wrap: wrap;
align-items: flex-end;
margin-bottom: $spacing-md;
}
.antigravityControl,
.codexControl,
.geminiCliControl {
display: flex;
flex-direction: column;
gap: 4px;
label {
font-size: 12px;
color: var(--text-secondary);
font-weight: 500;
white-space: nowrap;
}
}
.antigravityCard {
background-image: linear-gradient(
180deg,
rgba(224, 247, 250, 0.12),
rgba(224, 247, 250, 0)
);
}
.codexCard {
background-image: linear-gradient(
180deg,
rgba(255, 243, 224, 0.18),
rgba(255, 243, 224, 0)
);
}
.geminiCliCard {
background-image: linear-gradient(
180deg,
rgba(231, 239, 255, 0.2),
rgba(231, 239, 255, 0)
);
}
.quotaSection {
display: flex;
flex-direction: column;
gap: $spacing-sm;
padding-top: $spacing-sm;
margin-top: $spacing-xs;
border-top: 1px dashed var(--border-color);
}
.quotaRow {
display: flex;
flex-direction: column;
gap: $spacing-xs;
}
.quotaRowHeader {
display: flex;
align-items: center;
justify-content: space-between;
gap: $spacing-sm;
min-width: 0;
@include mobile {
flex-direction: column;
align-items: flex-start;
}
}
.quotaModel {
font-size: 13px;
font-weight: 600;
color: var(--text-primary);
white-space: nowrap;
overflow: hidden;
text-overflow: ellipsis;
flex: 1;
min-width: 0;
@include mobile {
white-space: normal;
}
}
.quotaBar {
height: 8px;
background-color: var(--bg-tertiary);
border-radius: 999px;
overflow: hidden;
}
.quotaBarFill {
height: 100%;
background-color: var(--success-color, #22c55e);
transition: width 0.2s ease;
}
.quotaBarFillHigh {
background-color: var(--success-color, #22c55e);
}
.quotaBarFillMedium {
background-color: var(--warning-color, #f59e0b);
}
.quotaBarFillLow {
background-color: var(--danger-color, #ef4444);
}
.quotaMeta {
display: flex;
align-items: center;
gap: 8px;
font-size: 12px;
color: var(--text-secondary);
white-space: nowrap;
@include mobile {
justify-content: flex-start;
}
}
.quotaPercent {
font-weight: 600;
color: var(--text-primary);
}
.quotaReset {
color: var(--text-tertiary);
}
.quotaAmount {
color: var(--text-secondary);
}
.quotaMessage {
font-size: 12px;
color: var(--text-tertiary);
text-align: center;
padding: $spacing-sm 0;
}
.quotaError {
font-size: 12px;
color: var(--danger-color);
background-color: rgba(239, 68, 68, 0.08);
border: 1px solid var(--danger-color);
border-radius: $radius-sm;
padding: $spacing-xs $spacing-sm;
}
.quotaWarning {
font-size: 12px;
color: var(--warning-color, #f59e0b);
background-color: rgba(245, 158, 11, 0.12);
border: 1px solid var(--warning-color, #f59e0b);
border-radius: $radius-sm;
padding: $spacing-xs $spacing-sm;
}
.codexPlan {
display: flex;
align-items: center;
gap: 6px;
font-size: 12px;
color: var(--text-secondary);
}
.codexPlanLabel {
color: var(--text-tertiary);
}
.codexPlanValue {
font-weight: 600;
color: var(--text-primary);
text-transform: capitalize;
}
.fileCard {
background-color: var(--bg-primary);
border: 1px solid var(--border-color);
border-radius: $radius-lg;
padding: $spacing-md;
display: flex;
flex-direction: column;
gap: $spacing-sm;
transition: transform $transition-fast, box-shadow $transition-fast, border-color $transition-fast;
&:hover {
transform: translateY(-2px);
box-shadow: $shadow-md;
border-color: rgba(37, 99, 235, 0.2);
}
}
.cardHeader {
display: flex;
align-items: center;
gap: $spacing-sm;
min-height: 28px;
}
.typeBadge {
padding: 4px 10px;
border-radius: 12px;
font-size: 12px;
font-weight: 600;
white-space: nowrap;
flex-shrink: 0;
}
.fileName {
font-size: 14px;
font-weight: 600;
color: var(--text-primary);
word-break: break-all;
line-height: 1.4;
}
.pagination {
display: flex;
justify-content: center;
align-items: center;
gap: $spacing-md;
margin-top: $spacing-lg;
padding-top: $spacing-md;
border-top: 1px solid var(--border-color);
}
.pageInfo {
font-size: 13px;
color: var(--text-secondary);
padding: $spacing-xs $spacing-md;
background-color: var(--bg-secondary);
border-radius: $radius-md;
}

1966
src/pages/QuotaPage.tsx Normal file

File diff suppressed because it is too large Load Diff

View File

@@ -8,3 +8,4 @@ export { useLanguageStore } from './useLanguageStore';
export { useAuthStore } from './useAuthStore';
export { useConfigStore } from './useConfigStore';
export { useModelsStore } from './useModelsStore';
export { useQuotaStore } from './useQuotaStore';

View File

@@ -0,0 +1,49 @@
/**
* Quota cache that survives route switches.
*/
import { create } from 'zustand';
import type { AntigravityQuotaState, CodexQuotaState, GeminiCliQuotaState } from '@/types';
type QuotaUpdater<T> = T | ((prev: T) => T);
interface QuotaStoreState {
antigravityQuota: Record<string, AntigravityQuotaState>;
codexQuota: Record<string, CodexQuotaState>;
geminiCliQuota: Record<string, GeminiCliQuotaState>;
setAntigravityQuota: (updater: QuotaUpdater<Record<string, AntigravityQuotaState>>) => void;
setCodexQuota: (updater: QuotaUpdater<Record<string, CodexQuotaState>>) => void;
setGeminiCliQuota: (updater: QuotaUpdater<Record<string, GeminiCliQuotaState>>) => void;
clearQuotaCache: () => void;
}
const resolveUpdater = <T,>(updater: QuotaUpdater<T>, prev: T): T => {
if (typeof updater === 'function') {
return (updater as (value: T) => T)(prev);
}
return updater;
};
export const useQuotaStore = create<QuotaStoreState>((set) => ({
antigravityQuota: {},
codexQuota: {},
geminiCliQuota: {},
setAntigravityQuota: (updater) =>
set((state) => ({
antigravityQuota: resolveUpdater(updater, state.antigravityQuota)
})),
setCodexQuota: (updater) =>
set((state) => ({
codexQuota: resolveUpdater(updater, state.codexQuota)
})),
setGeminiCliQuota: (updater) =>
set((state) => ({
geminiCliQuota: resolveUpdater(updater, state.geminiCliQuota)
})),
clearQuotaCache: () =>
set({
antigravityQuota: {},
codexQuota: {},
geminiCliQuota: {}
})
}));

View File

@@ -12,3 +12,4 @@ export * from './authFile';
export * from './oauth';
export * from './usage';
export * from './log';
export * from './quota';

50
src/types/quota.ts Normal file
View File

@@ -0,0 +1,50 @@
/**
* Quota management types.
*/
export interface AntigravityQuotaGroup {
id: string;
label: string;
models: string[];
remainingFraction: number;
resetTime?: string;
}
export interface AntigravityQuotaState {
status: 'idle' | 'loading' | 'success' | 'error';
groups: AntigravityQuotaGroup[];
error?: string;
errorStatus?: number;
}
export interface GeminiCliQuotaBucketState {
id: string;
label: string;
remainingFraction: number | null;
remainingAmount: number | null;
resetTime: string | undefined;
tokenType: string | null;
modelIds?: string[];
}
export interface GeminiCliQuotaState {
status: 'idle' | 'loading' | 'success' | 'error';
buckets: GeminiCliQuotaBucketState[];
error?: string;
errorStatus?: number;
}
export interface CodexQuotaWindow {
id: string;
label: string;
usedPercent: number | null;
resetLabel: string;
}
export interface CodexQuotaState {
status: 'idle' | 'loading' | 'success' | 'error';
windows: CodexQuotaWindow[];
planType?: string | null;
error?: string;
errorStatus?: number;
}