11'use client' ;
22
3- import { useState , useMemo } from 'react' ;
4- import { useQuery , useMutation , useQueryClient , useQueries } from '@tanstack/react-query' ;
3+ import { useState } from 'react' ;
4+ import { useQuery , useMutation , useQueryClient } from '@tanstack/react-query' ;
55import { Button } from '@/components/ui/button' ;
66import { useToast } from '@/hooks/use-toast' ;
77import { Loader2 , Search } from 'lucide-react' ;
8- import { get , del , put } from '@/utils/api' ;
8+ import { get , del , put , post } from '@/utils/api' ;
99import { useDebounce } from '@/hooks/use-debounce' ;
1010import {
1111 Command ,
@@ -21,30 +21,29 @@ import {
2121 PopoverTrigger ,
2222} from '@/components/ui/popover' ;
2323import { PrivateAIKeysTable } from '@/components/private-ai-keys-table' ;
24+ import { CreateAIKeyDialog } from '@/components/create-ai-key-dialog' ;
2425import { PrivateAIKey } from '@/types/private-ai-key' ;
26+ import { usePrivateAIKeysData } from '@/hooks/use-private-ai-keys-data' ;
2527
2628interface User {
2729 id : number ;
2830 email : string ;
29- }
30-
31- interface SpendInfo {
32- spend : number ;
33- expires : string ;
31+ is_active : boolean ;
32+ role : string ;
33+ team_id : number | null ;
3434 created_at : string ;
35- updated_at : string ;
36- max_budget : number | null ;
37- budget_duration : string | null ;
38- budget_reset_at : string | null ;
3935}
4036
37+
38+
4139export default function PrivateAIKeysPage ( ) {
4240 const { toast } = useToast ( ) ;
4341 const queryClient = useQueryClient ( ) ;
4442 const [ searchTerm , setSearchTerm ] = useState ( '' ) ;
4543 const [ isUserSearchOpen , setIsUserSearchOpen ] = useState ( false ) ;
4644 const [ selectedUser , setSelectedUser ] = useState < User | null > ( null ) ;
4745 const [ loadedSpendKeys , setLoadedSpendKeys ] = useState < Set < number > > ( new Set ( ) ) ;
46+ const [ isCreateDialogOpen , setIsCreateDialogOpen ] = useState ( false ) ;
4847 const debouncedSearchTerm = useDebounce ( searchTerm , 300 ) ;
4948
5049 // Queries
@@ -58,25 +57,12 @@ export default function PrivateAIKeysPage() {
5857 const data = await response . json ( ) ;
5958 return data ;
6059 } ,
60+ refetchInterval : 30000 , // Refetch every 30 seconds to detect new keys
61+ refetchIntervalInBackground : true , // Continue polling even when tab is not active
6162 } ) ;
6263
63- // Get unique team IDs from the keys
64- const teamIds = Array . from ( new Set ( privateAIKeys . filter ( key => key . team_id ) . map ( key => key . team_id ) ) ) ;
65-
66- // Fetch team details for each team ID
67- const { data : teamDetails = { } } = useQuery ( {
68- queryKey : [ 'team-details' , teamIds ] ,
69- queryFn : async ( ) => {
70- const teamPromises = teamIds . map ( async ( teamId ) => {
71- const response = await get ( `teams/${ teamId } ` ) ;
72- const data = await response . json ( ) ;
73- return [ teamId , data ] ;
74- } ) ;
75- const teamResults = await Promise . all ( teamPromises ) ;
76- return Object . fromEntries ( teamResults ) ;
77- } ,
78- enabled : teamIds . length > 0 ,
79- } ) ;
64+ // Use shared hook for data fetching
65+ const { teamDetails, teamMembers, spendMap, regions } = usePrivateAIKeysData ( privateAIKeys , loadedSpendKeys ) ;
8066
8167 // Query to get all users for displaying emails
8268 const { data : usersMap = { } } = useQuery < Record < number , User > > ( {
@@ -129,38 +115,40 @@ export default function PrivateAIKeysPage() {
129115 }
130116 } ;
131117
132- // Query to get spend information for each key
133- const spendQueries = useQueries ( {
134- queries : privateAIKeys
135- . filter ( key => loadedSpendKeys . has ( key . id ) )
136- . map ( ( key ) => ( {
137- queryKey : [ 'private-ai-key-spend' , key . id ] ,
138- queryFn : async ( ) => {
139- const response = await get ( `/private-ai-keys/${ key . id } /spend` ) ;
140- const data = await response . json ( ) ;
141- return data as SpendInfo ;
142- } ,
143- refetchInterval : 60000 , // Refetch every minute
144- } ) ) ,
118+ // Mutations
119+ const createKeyMutation = useMutation ( {
120+ mutationFn : async ( data : { name : string ; region_id : number ; owner_id ?: number ; team_id ?: number ; key_type : 'full' | 'llm' | 'vector' } ) => {
121+ const endpoint = data . key_type === 'full' ? 'private-ai-keys' :
122+ data . key_type === 'llm' ? 'private-ai-keys/token' :
123+ 'private-ai-keys/vector-db' ;
124+ const response = await post ( endpoint , data ) ;
125+ return response . json ( ) ;
126+ } ,
127+ onSuccess : ( ) => {
128+ queryClient . invalidateQueries ( { queryKey : [ 'private-ai-keys' ] } ) ;
129+ queryClient . refetchQueries ( { queryKey : [ 'private-ai-keys' ] , exact : true } ) ;
130+ setIsCreateDialogOpen ( false ) ;
131+ toast ( {
132+ title : 'Success' ,
133+ description : 'Private AI key created successfully' ,
134+ } ) ;
135+ } ,
136+ onError : ( error : Error ) => {
137+ toast ( {
138+ title : 'Error' ,
139+ description : error . message ,
140+ variant : 'destructive' ,
141+ } ) ;
142+ } ,
145143 } ) ;
146144
147- // Create a map of spend information
148- const spendMap = useMemo ( ( ) => {
149- return spendQueries . reduce ( ( acc , query , index ) => {
150- if ( query . data ) {
151- acc [ privateAIKeys . filter ( key => loadedSpendKeys . has ( key . id ) ) [ index ] . id ] = query . data ;
152- }
153- return acc ;
154- } , { } as Record < number , SpendInfo > ) ;
155- } , [ spendQueries , privateAIKeys , loadedSpendKeys ] ) ;
156-
157- // Mutations
158145 const deletePrivateAIKeyMutation = useMutation ( {
159146 mutationFn : async ( keyId : number ) => {
160147 await del ( `/private-ai-keys/${ keyId } ` ) ;
161148 } ,
162149 onSuccess : ( ) => {
163150 queryClient . invalidateQueries ( { queryKey : [ 'private-ai-keys' ] } ) ;
151+ queryClient . refetchQueries ( { queryKey : [ 'private-ai-keys' ] , exact : true } ) ;
164152 toast ( {
165153 title : 'Success' ,
166154 description : 'Private AI key deleted successfully' ,
@@ -200,10 +188,33 @@ export default function PrivateAIKeysPage() {
200188 } ,
201189 } ) ;
202190
191+ const handleCreateKey = ( data : {
192+ name : string
193+ region_id : number
194+ key_type : 'full' | 'llm' | 'vector'
195+ owner_id ?: number
196+ team_id ?: number
197+ } ) => {
198+ createKeyMutation . mutate ( data ) ;
199+ } ;
200+
203201 return (
204202 < div className = "space-y-4" >
205203 < div className = "flex justify-between items-center" >
206204 < h1 className = "text-3xl font-bold" > Private AI Keys</ h1 >
205+ < CreateAIKeyDialog
206+ open = { isCreateDialogOpen }
207+ onOpenChange = { setIsCreateDialogOpen }
208+ onSubmit = { handleCreateKey }
209+ isLoading = { createKeyMutation . isPending }
210+ regions = { regions }
211+ teamMembers = { Object . values ( usersMap ) }
212+ showUserAssignment = { true }
213+ currentUser = { undefined }
214+ triggerText = "Create Key"
215+ title = "Create New Private AI Key"
216+ description = "Create a new private AI key for any user or team."
217+ />
207218 </ div >
208219
209220 < div className = "flex items-center gap-2" >
@@ -280,7 +291,7 @@ export default function PrivateAIKeysPage() {
280291 } }
281292 isUpdatingBudget = { updateBudgetPeriodMutation . isPending }
282293 teamDetails = { teamDetails }
283- teamMembers = { Object . values ( usersMap ) }
294+ teamMembers = { teamMembers }
284295 />
285296 </ div >
286297 ) ;
0 commit comments