33 * Used by both global routes and project routes
44 */
55
6- import { useState , useMemo } from 'react' ;
6+ import { useState , useMemo , useRef } from 'react' ;
77import { useTranslation } from 'react-i18next' ;
88import { Plus , RefreshCw , Zap } from 'lucide-react' ;
99import {
@@ -38,7 +38,7 @@ import { useQueryClient } from '@tanstack/react-query';
3838import { useStreamingRequests } from '@/hooks/use-streaming' ;
3939import { getClientName , getClientColor } from '@/components/icons/client-icons' ;
4040import { getProviderColor , type ProviderType } from '@/lib/theme' ;
41- import type { ClientType , Provider } from '@/lib/transport' ;
41+ import type { ClientType , Provider , ProviderStats } from '@/lib/transport' ;
4242import {
4343 SortableProviderRow ,
4444 ProviderRowContent ,
@@ -58,6 +58,44 @@ const PROVIDER_TYPE_LABELS: Record<Exclude<ProviderTypeKey, 'custom'>, string> =
5858 codex : 'Codex' ,
5959} ;
6060
61+ function isSameProviderStats ( a : ProviderStats , b : ProviderStats ) : boolean {
62+ return (
63+ a . providerID === b . providerID &&
64+ a . totalRequests === b . totalRequests &&
65+ a . successfulRequests === b . successfulRequests &&
66+ a . failedRequests === b . failedRequests &&
67+ a . successRate === b . successRate &&
68+ a . activeRequests === b . activeRequests &&
69+ a . totalInputTokens === b . totalInputTokens &&
70+ a . totalOutputTokens === b . totalOutputTokens &&
71+ a . totalCacheRead === b . totalCacheRead &&
72+ a . totalCacheWrite === b . totalCacheWrite &&
73+ a . totalCost === b . totalCost
74+ ) ;
75+ }
76+
77+ function useStableProviderStats ( stats : Record < number , ProviderStats > ) {
78+ const prevRef = useRef < Record < number , ProviderStats > > ( { } ) ;
79+
80+ return useMemo ( ( ) => {
81+ const prev = prevRef . current ;
82+ const next : Record < number , ProviderStats > = { } ;
83+
84+ for ( const [ key , value ] of Object . entries ( stats ) ) {
85+ const id = Number ( key ) ;
86+ const prevValue = prev [ id ] ;
87+ if ( prevValue && isSameProviderStats ( prevValue , value ) ) {
88+ next [ id ] = prevValue ;
89+ } else {
90+ next [ id ] = value ;
91+ }
92+ }
93+
94+ prevRef . current = next ;
95+ return next ;
96+ } , [ stats ] ) ;
97+ }
98+
6199interface ClientTypeRoutesContentProps {
62100 clientType : ClientType ;
63101 projectID : number ; // 0 for global routes
@@ -84,6 +122,7 @@ function ClientTypeRoutesContentInner({
84122 const { t } = useTranslation ( ) ;
85123 const [ activeId , setActiveId ] = useState < string | null > ( null ) ;
86124 const { data : providerStats = { } } = useProviderStats ( clientType , projectID || undefined ) ;
125+ const stableProviderStats = useStableProviderStats ( providerStats ) ;
87126 const queryClient = useQueryClient ( ) ;
88127
89128 // 订阅请求更新事件,确保 providerStats 实时刷新
@@ -102,7 +141,6 @@ function ClientTypeRoutesContentInner({
102141
103142 const { data : allRoutes , isLoading : routesLoading } = useRoutes ( ) ;
104143 const { data : providers = [ ] , isLoading : providersLoading } = useProviders ( ) ;
105- const { countsByProviderAndClient } = useStreamingRequests ( ) ;
106144
107145 const createRoute = useCreateRoute ( ) ;
108146 const toggleRoute = useToggleRoute ( ) ;
@@ -116,42 +154,62 @@ function ClientTypeRoutesContentInner({
116154 return allRoutes ?. filter ( ( r ) => r . clientType === clientType && r . projectID === projectID ) || [ ] ;
117155 } , [ allRoutes , clientType , projectID ] ) ;
118156
157+ const normalizedQuery = useMemo ( ( ) => searchQuery . trim ( ) . toLowerCase ( ) , [ searchQuery ] ) ;
158+
159+ const providerById = useMemo ( ( ) => {
160+ const map = new Map < number , Provider > ( ) ;
161+ for ( const provider of providers ) {
162+ map . set ( Number ( provider . id ) , provider ) ;
163+ }
164+ return map ;
165+ } , [ providers ] ) ;
166+
167+ const routeByProviderId = useMemo ( ( ) => {
168+ const map = new Map < number , ( typeof clientRoutes ) [ number ] > ( ) ;
169+ for ( const route of clientRoutes ) {
170+ map . set ( Number ( route . providerID ) , route ) ;
171+ }
172+ return map ;
173+ } , [ clientRoutes ] ) ;
174+
119175 // Build provider config items
120176 const items = useMemo ( ( ) : ProviderConfigItem [ ] => {
121- const allItems = providers . map ( ( provider ) => {
122- const route = clientRoutes . find ( ( r ) => Number ( r . providerID ) === Number ( provider . id ) ) || null ;
177+ const allItems : ProviderConfigItem [ ] = [ ] ;
178+
179+ for ( const route of clientRoutes ) {
180+ const provider = providerById . get ( Number ( route . providerID ) ) ;
181+ if ( ! provider ) continue ;
123182 const isNative = ( provider . supportedClientTypes || [ ] ) . includes ( clientType ) ;
124- return {
183+ allItems . push ( {
125184 id : `${ clientType } -provider-${ provider . id } ` ,
126185 provider,
127186 route,
128- enabled : route ? .isEnabled ?? false ,
187+ enabled : route . isEnabled ?? false ,
129188 isNative,
130- } ;
131- } ) ;
189+ } ) ;
190+ }
132191
133- // Only show providers that have routes
134- let filteredItems = allItems . filter ( ( item ) => item . route ) ;
192+ let filteredItems = allItems ;
135193
136194 // Apply search filter
137- if ( searchQuery . trim ( ) ) {
138- const query = searchQuery . toLowerCase ( ) ;
195+ if ( normalizedQuery ) {
139196 filteredItems = filteredItems . filter (
140197 ( item ) =>
141- item . provider . name . toLowerCase ( ) . includes ( query ) ||
142- item . provider . type . toLowerCase ( ) . includes ( query ) ,
198+ item . provider . name . toLowerCase ( ) . includes ( normalizedQuery ) ||
199+ item . provider . type . toLowerCase ( ) . includes ( normalizedQuery ) ,
143200 ) ;
144201 }
145202
146203 return filteredItems . sort ( ( a , b ) => {
147- if ( a . route && b . route ) return a . route . position - b . route . position ;
148- if ( a . route && ! b . route ) return - 1 ;
149- if ( ! a . route && b . route ) return 1 ;
150- if ( a . isNative && ! b . isNative ) return - 1 ;
151- if ( ! a . isNative && b . isNative ) return 1 ;
204+ const posDiff = ( a . route ?. position ?? 0 ) - ( b . route ?. position ?? 0 ) ;
205+ if ( posDiff !== 0 ) return posDiff ;
206+ if ( a . isNative !== b . isNative ) return a . isNative ? - 1 : 1 ;
152207 return a . provider . name . localeCompare ( b . provider . name ) ;
153208 } ) ;
154- } , [ providers , clientRoutes , clientType , searchQuery ] ) ;
209+ } , [ clientRoutes , clientType , normalizedQuery , providerById ] ) ;
210+
211+ const streamingThrottleMs = items . length > 200 ? 1000 : 0 ;
212+ const { countsByProviderAndClient } = useStreamingRequests ( { throttleMs : streamingThrottleMs } ) ;
155213
156214 // Get available providers (without routes yet), grouped by type and sorted alphabetically
157215 const groupedAvailableProviders = useMemo ( ( ) : Record < ProviderTypeKey , Provider [ ] > => {
@@ -162,16 +220,14 @@ function ClientTypeRoutesContentInner({
162220 custom : [ ] ,
163221 } ;
164222
165- let available = providers . filter ( ( p ) => {
166- const hasRoute = clientRoutes . some ( ( r ) => Number ( r . providerID ) === Number ( p . id ) ) ;
167- return ! hasRoute ;
168- } ) ;
223+ let available = providers . filter ( ( p ) => ! routeByProviderId . has ( Number ( p . id ) ) ) ;
169224
170225 // Apply search filter
171- if ( searchQuery . trim ( ) ) {
172- const query = searchQuery . toLowerCase ( ) ;
226+ if ( normalizedQuery ) {
173227 available = available . filter (
174- ( p ) => p . name . toLowerCase ( ) . includes ( query ) || p . type . toLowerCase ( ) . includes ( query ) ,
228+ ( p ) =>
229+ p . name . toLowerCase ( ) . includes ( normalizedQuery ) ||
230+ p . type . toLowerCase ( ) . includes ( normalizedQuery ) ,
175231 ) ;
176232 }
177233
@@ -191,14 +247,32 @@ function ClientTypeRoutesContentInner({
191247 }
192248
193249 return groups ;
194- } , [ providers , clientRoutes , searchQuery ] ) ;
250+ } , [ providers , normalizedQuery , routeByProviderId ] ) ;
195251
196252 // Check if there are any available providers
197253 const hasAvailableProviders = useMemo ( ( ) => {
198254 return PROVIDER_TYPE_ORDER . some ( ( type ) => groupedAvailableProviders [ type ] . length > 0 ) ;
199255 } , [ groupedAvailableProviders ] ) ;
200256
201- const activeItem = activeId ? items . find ( ( item ) => item . id === activeId ) : null ;
257+ const itemsById = useMemo ( ( ) => {
258+ const map = new Map < string , ProviderConfigItem > ( ) ;
259+ for ( const item of items ) {
260+ map . set ( item . id , item ) ;
261+ }
262+ return map ;
263+ } , [ items ] ) ;
264+
265+ const itemIds = useMemo ( ( ) => items . map ( ( item ) => item . id ) , [ items ] ) ;
266+
267+ const itemIndexById = useMemo ( ( ) => {
268+ const map = new Map < string , number > ( ) ;
269+ items . forEach ( ( item , index ) => {
270+ map . set ( item . id , index ) ;
271+ } ) ;
272+ return map ;
273+ } , [ items ] ) ;
274+
275+ const activeItem = activeId ? itemsById . get ( activeId ) ?? null : null ;
202276
203277 const handleToggle = ( item : ProviderConfigItem ) => {
204278 if ( item . route ) {
@@ -246,10 +320,11 @@ function ClientTypeRoutesContentInner({
246320
247321 if ( ! over || active . id === over . id ) return ;
248322
249- const oldIndex = items . findIndex ( ( item ) => item . id === active . id ) ;
250- const newIndex = items . findIndex ( ( item ) => item . id === over . id ) ;
323+ const oldIndex = itemIndexById . get ( active . id as string ) ;
324+ const newIndex = itemIndexById . get ( over . id as string ) ;
251325
252- if ( oldIndex === - 1 || newIndex === - 1 ) return ;
326+ if ( oldIndex === undefined || newIndex === undefined ) return ;
327+ if ( oldIndex === newIndex ) return ;
253328
254329 const newItems = arrayMove ( items , oldIndex , newIndex ) ;
255330
@@ -307,7 +382,7 @@ function ClientTypeRoutesContentInner({
307382 onDragEnd = { handleDragEnd }
308383 >
309384 < SortableContext
310- items = { items . map ( ( item ) => item . id ) }
385+ items = { itemIds }
311386 strategy = { verticalListSortingStrategy }
312387 >
313388 < div className = "space-y-2" >
@@ -320,7 +395,7 @@ function ClientTypeRoutesContentInner({
320395 streamingCount = {
321396 countsByProviderAndClient . get ( `${ item . provider . id } :${ clientType } ` ) || 0
322397 }
323- stats = { providerStats [ item . provider . id ] }
398+ stats = { stableProviderStats [ item . provider . id ] }
324399 isToggling = { toggleRoute . isPending || createRoute . isPending }
325400 onToggle = { ( ) => handleToggle ( item ) }
326401 onDelete = { item . route ? ( ) => handleDeleteRoute ( item . route ! . id ) : undefined }
@@ -333,12 +408,12 @@ function ClientTypeRoutesContentInner({
333408 { activeItem && (
334409 < ProviderRowContent
335410 item = { activeItem }
336- index = { items . findIndex ( ( i ) => i . id === activeItem . id ) }
411+ index = { itemIndexById . get ( activeItem . id ) ?? 0 }
337412 clientType = { clientType }
338413 streamingCount = {
339414 countsByProviderAndClient . get ( `${ activeItem . provider . id } :${ clientType } ` ) || 0
340415 }
341- stats = { providerStats [ activeItem . provider . id ] }
416+ stats = { stableProviderStats [ activeItem . provider . id ] }
342417 isToggling = { false }
343418 isOverlay
344419 onToggle = { ( ) => { } }
0 commit comments