From dc58a0701f88a687334086cd845fd8aaeda03990 Mon Sep 17 00:00:00 2001 From: LTbinglingfeng Date: Fri, 2 Jan 2026 20:11:16 +0800 Subject: [PATCH 1/4] fix(logs): parse latency durations with minutes --- src/pages/LogsPage.tsx | 19 +++++++++++++------ 1 file changed, 13 insertions(+), 6 deletions(-) diff --git a/src/pages/LogsPage.tsx b/src/pages/LogsPage.tsx index 21331c7..c86b70f 100644 --- a/src/pages/LogsPage.tsx +++ b/src/pages/LogsPage.tsx @@ -50,7 +50,8 @@ const HTTP_METHOD_REGEX = new RegExp(`\\b(${HTTP_METHODS.join('|')})\\b`); const LOG_TIMESTAMP_REGEX = /^\[?(\d{4}-\d{2}-\d{2}[ T]\d{2}:\d{2}:\d{2}(?:\.\d{1,3})?)\]?/; const LOG_LEVEL_REGEX = /^\[?(trace|debug|info|warn|warning|error|fatal)\s*\]?(?=\s|\[|$)\s*/i; const LOG_SOURCE_REGEX = /^\[([^\]]+)\]/; -const LOG_LATENCY_REGEX = /\b(\d+(?:\.\d+)?)(?:\s*)(µs|us|ms|s)\b/i; +const LOG_LATENCY_REGEX = + /\b(?:\d+(?:\.\d+)?\s*(?:µs|us|ms|s|m))(?:\s*\d+(?:\.\d+)?\s*(?:µs|us|ms|s|m))*\b/i; const LOG_IPV4_REGEX = /\b(?:\d{1,3}\.){3}\d{1,3}\b/; const LOG_IPV6_REGEX = /\b(?:[a-f0-9]{0,4}:){2,7}[a-f0-9]{0,4}\b/i; const LOG_REQUEST_ID_REGEX = /^([a-f0-9]{8}|--------)$/i; @@ -102,6 +103,12 @@ const normalizeTimestampToSeconds = (value: string): string => { return `${match[1]} ${match[2]}`; }; +const extractLatency = (text: string): string | undefined => { + const match = text.match(LOG_LATENCY_REGEX); + if (!match) return undefined; + return match[0].replace(/\s+/g, ''); +}; + type ParsedLogLine = { raw: string; timestamp?: string; @@ -244,9 +251,9 @@ const parseLogLine = (raw: string): ParsedLogLine => { // latency const latencyIndex = segments.findIndex((segment) => LOG_LATENCY_REGEX.test(segment)); if (latencyIndex >= 0) { - const match = segments[latencyIndex].match(LOG_LATENCY_REGEX); - if (match) { - latency = `${match[1]}${match[2]}`; + const extracted = extractLatency(segments[latencyIndex]); + if (extracted) { + latency = extracted; consumed.add(latencyIndex); } } @@ -287,8 +294,8 @@ const parseLogLine = (raw: string): ParsedLogLine => { } else { statusCode = detectHttpStatusCode(remaining); - const latencyMatch = remaining.match(LOG_LATENCY_REGEX); - if (latencyMatch) latency = `${latencyMatch[1]}${latencyMatch[2]}`; + const extracted = extractLatency(remaining); + if (extracted) latency = extracted; ip = extractIp(remaining); From 2ae06a886098ed852c10f1c1621f817182d8cf13 Mon Sep 17 00:00:00 2001 From: LTbinglingfeng Date: Fri, 2 Jan 2026 20:26:41 +0800 Subject: [PATCH 2/4] perf(ui): smooth gsap page transitions --- src/components/common/PageTransition.scss | 2 ++ src/components/common/PageTransition.tsx | 31 +++++++++++++++-------- 2 files changed, 23 insertions(+), 10 deletions(-) diff --git a/src/components/common/PageTransition.scss b/src/components/common/PageTransition.scss index 2b867b4..6b918cd 100644 --- a/src/components/common/PageTransition.scss +++ b/src/components/common/PageTransition.scss @@ -27,6 +27,8 @@ &--animating &__layer { will-change: transform, opacity; + backface-visibility: hidden; + transform-style: preserve-3d; } // When both layers exist, current layer also needs positioning diff --git a/src/components/common/PageTransition.tsx b/src/components/common/PageTransition.tsx index ab15477..e237fc2 100644 --- a/src/components/common/PageTransition.tsx +++ b/src/components/common/PageTransition.tsx @@ -9,7 +9,9 @@ interface PageTransitionProps { scrollContainerRef?: React.RefObject; } -const TRANSITION_DURATION = 0.65; +const TRANSITION_DURATION = 0.5; +const EXIT_DURATION = 0.45; +const ENTER_DELAY = 0.08; type LayerStatus = 'current' | 'exiting'; @@ -99,6 +101,13 @@ export function PageTransition({ scrollContainer.scrollTo({ top: 0, left: 0, behavior: 'auto' }); } + const containerHeight = scrollContainer?.clientHeight ?? 0; + const viewportHeight = typeof window === 'undefined' ? 0 : window.innerHeight; + const travelDistance = Math.max(containerHeight, viewportHeight, 1); + const enterFromY = transitionDirection === 'forward' ? travelDistance : -travelDistance; + const exitToY = transitionDirection === 'forward' ? -travelDistance : travelDistance; + const exitBaseY = scrollOffset ? -scrollOffset : 0; + const tl = gsap.timeline({ onComplete: () => { setLayers((prev) => prev.filter((layer) => layer.status !== 'exiting')); @@ -108,15 +117,16 @@ export function PageTransition({ // Exit animation: fly out to top (slow-to-fast) if (exitingLayerRef.current) { - gsap.set(exitingLayerRef.current, { y: scrollOffset ? -scrollOffset : 0 }); + gsap.set(exitingLayerRef.current, { y: exitBaseY }); tl.fromTo( exitingLayerRef.current, - { yPercent: 0, opacity: 1 }, + { y: exitBaseY, opacity: 1 }, { - yPercent: transitionDirection === 'forward' ? -100 : 100, + y: exitBaseY + exitToY, opacity: 0, - duration: TRANSITION_DURATION, - ease: 'power3.in', // slow start, fast end + duration: EXIT_DURATION, + ease: 'power2.in', // fast finish to clear screen + force3D: true, }, 0 ); @@ -125,15 +135,16 @@ export function PageTransition({ // Enter animation: slide in from bottom (slow-to-fast) tl.fromTo( currentLayerRef.current, - { yPercent: transitionDirection === 'forward' ? 100 : -100, opacity: 0 }, + { y: enterFromY, opacity: 0 }, { - yPercent: 0, + y: 0, opacity: 1, duration: TRANSITION_DURATION, - ease: 'power2.in', // slow start, fast end + ease: 'power2.out', // smooth settle clearProps: 'transform,opacity', + force3D: true, }, - 0 + ENTER_DELAY ); return () => { From 8232812ac27477f68f11a69f1c31318eb5072f94 Mon Sep 17 00:00:00 2001 From: LTbinglingfeng Date: Fri, 2 Jan 2026 22:42:20 +0800 Subject: [PATCH 3/4] feat(ui): show AIStudio models for virtual auth files and adjust Gemini OAuth spacing --- src/pages/AuthFilesPage.tsx | 69 ++++++++++++++++++--------------- src/pages/OAuthPage.module.scss | 7 ++++ src/pages/OAuthPage.tsx | 28 ++++++------- 3 files changed, 59 insertions(+), 45 deletions(-) diff --git a/src/pages/AuthFilesPage.tsx b/src/pages/AuthFilesPage.tsx index 3371543..fdabaa3 100644 --- a/src/pages/AuthFilesPage.tsx +++ b/src/pages/AuthFilesPage.tsx @@ -719,9 +719,11 @@ export function AuthFilesPage() { const renderFileCard = (item: AuthFileItem) => { const fileStats = resolveAuthFileStats(item, keyStats); const isRuntimeOnly = isRuntimeOnlyAuthFile(item); + const isAistudio = (item.type || '').toLowerCase() === 'aistudio'; + const showModelsButton = !isRuntimeOnly || isAistudio; const typeColor = getTypeColor(item.type || 'unknown'); - - return ( + + return (
- {isRuntimeOnly ? ( -
{t('auth_files.type_virtual') || '虚拟认证文件'}
- ) : ( - <> - - + )} + {!isRuntimeOnly && ( + <> + @@ -799,13 +801,16 @@ export function AuthFilesPage() { > {deleting === item.name ? ( - ) : ( - - )} - - - )} -
+ ) : ( + + )} + + + )} + {isRuntimeOnly && ( +
{t('auth_files.type_virtual') || '虚拟认证文件'}
+ )} +
); }; diff --git a/src/pages/OAuthPage.module.scss b/src/pages/OAuthPage.module.scss index 777ecd1..199c024 100644 --- a/src/pages/OAuthPage.module.scss +++ b/src/pages/OAuthPage.module.scss @@ -115,6 +115,13 @@ margin-top: $spacing-sm; } +.geminiProjectField { + :global(.form-group) { + margin-top: $spacing-sm; + gap: $spacing-sm; + } +} + .filePicker { display: flex; align-items: center; diff --git a/src/pages/OAuthPage.tsx b/src/pages/OAuthPage.tsx index f93234d..a21beae 100644 --- a/src/pages/OAuthPage.tsx +++ b/src/pages/OAuthPage.tsx @@ -327,19 +327,21 @@ export function OAuthPage() { >
{t(provider.hintKey)}
{provider.id === 'gemini-cli' && ( - - updateProviderState(provider.id, { - projectId: e.target.value, - projectIdError: undefined - }) - } - placeholder={t('auth_login.gemini_cli_project_id_placeholder')} - /> +
+ + updateProviderState(provider.id, { + projectId: e.target.value, + projectIdError: undefined + }) + } + placeholder={t('auth_login.gemini_cli_project_id_placeholder')} + /> +
)} {state.url && (
From db376c75040b4488f947d9b90787f91cfd652ec9 Mon Sep 17 00:00:00 2001 From: LTbinglingfeng Date: Sat, 3 Jan 2026 01:40:54 +0800 Subject: [PATCH 4/4] fix(layout): wire header refresh to page loaders and quota config refresh --- src/components/layout/MainLayout.tsx | 21 +++++++++++++----- src/hooks/index.ts | 1 + src/hooks/useHeaderRefresh.ts | 24 +++++++++++++++++++++ src/pages/AuthFilesPage.tsx | 32 +++++++++++++++++++--------- src/pages/LogsPage.tsx | 3 +++ src/pages/QuotaPage.tsx | 27 ++++++++++++++++------- src/pages/UsagePage.tsx | 3 +++ 7 files changed, 88 insertions(+), 23 deletions(-) create mode 100644 src/hooks/useHeaderRefresh.ts diff --git a/src/components/layout/MainLayout.tsx b/src/components/layout/MainLayout.tsx index 1a3b8bf..4226607 100644 --- a/src/components/layout/MainLayout.tsx +++ b/src/components/layout/MainLayout.tsx @@ -36,6 +36,7 @@ import { useThemeStore, } from '@/stores'; import { configApi, versionApi } from '@/services/api'; +import { triggerHeaderRefresh } from '@/hooks/useHeaderRefresh'; const sidebarIcons: Record = { dashboard: , @@ -384,12 +385,22 @@ export function MainLayout() { const handleRefreshAll = async () => { clearCache(); - try { - await fetchConfig(undefined, true); - showNotification(t('notification.data_refreshed'), 'success'); - } catch (error: any) { - showNotification(`${t('notification.refresh_failed')}: ${error?.message || ''}`, 'error'); + const results = await Promise.allSettled([ + fetchConfig(undefined, true), + triggerHeaderRefresh() + ]); + const rejected = results.find((result) => result.status === 'rejected'); + if (rejected && rejected.status === 'rejected') { + const reason = rejected.reason; + const message = + typeof reason === 'string' ? reason : reason instanceof Error ? reason.message : ''; + showNotification( + `${t('notification.refresh_failed')}${message ? `: ${message}` : ''}`, + 'error' + ); + return; } + showNotification(t('notification.data_refreshed'), 'success'); }; const handleVersionCheck = async () => { diff --git a/src/hooks/index.ts b/src/hooks/index.ts index 6dc876f..ee756c7 100644 --- a/src/hooks/index.ts +++ b/src/hooks/index.ts @@ -8,3 +8,4 @@ export { useLocalStorage } from './useLocalStorage'; export { useInterval } from './useInterval'; export { useMediaQuery } from './useMediaQuery'; export { usePagination } from './usePagination'; +export { useHeaderRefresh } from './useHeaderRefresh'; diff --git a/src/hooks/useHeaderRefresh.ts b/src/hooks/useHeaderRefresh.ts new file mode 100644 index 0000000..88f60e4 --- /dev/null +++ b/src/hooks/useHeaderRefresh.ts @@ -0,0 +1,24 @@ +import { useEffect } from 'react'; + +export type HeaderRefreshHandler = () => void | Promise; + +let activeHeaderRefreshHandler: HeaderRefreshHandler | null = null; + +export const triggerHeaderRefresh = async () => { + if (!activeHeaderRefreshHandler) return; + await activeHeaderRefreshHandler(); +}; + +export const useHeaderRefresh = (handler?: HeaderRefreshHandler | null) => { + useEffect(() => { + if (!handler) return; + + activeHeaderRefreshHandler = handler; + + return () => { + if (activeHeaderRefreshHandler === handler) { + activeHeaderRefreshHandler = null; + } + }; + }, [handler]); +}; diff --git a/src/pages/AuthFilesPage.tsx b/src/pages/AuthFilesPage.tsx index fdabaa3..35aa4e9 100644 --- a/src/pages/AuthFilesPage.tsx +++ b/src/pages/AuthFilesPage.tsx @@ -1,8 +1,9 @@ -import { useEffect, useMemo, useRef, useState, useCallback } from 'react'; -import { useTranslation } from 'react-i18next'; -import { useInterval } from '@/hooks/useInterval'; -import { Card } from '@/components/ui/Card'; -import { Button } from '@/components/ui/Button'; +import { useEffect, useMemo, useRef, useState, useCallback } from 'react'; +import { useTranslation } from 'react-i18next'; +import { useInterval } from '@/hooks/useInterval'; +import { useHeaderRefresh } from '@/hooks/useHeaderRefresh'; +import { Card } from '@/components/ui/Card'; +import { Button } from '@/components/ui/Button'; import { LoadingSpinner } from '@/components/ui/LoadingSpinner'; import { Input } from '@/components/ui/Input'; import { Modal } from '@/components/ui/Modal'; @@ -270,6 +271,12 @@ export function AuthFilesPage() { } }, [showNotification, t]); + const handleHeaderRefresh = useCallback(async () => { + await Promise.all([loadFiles(), loadKeyStats(), loadExcluded()]); + }, [loadFiles, loadKeyStats, loadExcluded]); + + useHeaderRefresh(handleHeaderRefresh); + useEffect(() => { loadFiles(); loadKeyStats(); @@ -824,11 +831,16 @@ export function AuthFilesPage() { - + extra={ +
+ -
{error &&
{error}
} diff --git a/src/pages/UsagePage.tsx b/src/pages/UsagePage.tsx index ba25420..14e61f0 100644 --- a/src/pages/UsagePage.tsx +++ b/src/pages/UsagePage.tsx @@ -14,6 +14,7 @@ import { import { Button } from '@/components/ui/Button'; import { LoadingSpinner } from '@/components/ui/LoadingSpinner'; import { useMediaQuery } from '@/hooks/useMediaQuery'; +import { useHeaderRefresh } from '@/hooks/useHeaderRefresh'; import { useThemeStore } from '@/stores'; import { StatCards, @@ -63,6 +64,8 @@ export function UsagePage() { importing } = useUsageData(); + useHeaderRefresh(loadUsage); + // Chart lines state const [chartLines, setChartLines] = useState(['all']); const MAX_CHART_LINES = 9;