import React, { useState, useCallback, useRef, useEffect } from 'react' import { useTranslation } from 'react-i18next' import { toast } from 'sonner' import useDialogState from '@/hooks/use-dialog' import { fetchTokenKey, fetchTokenKeysBatch } from '../api' import { ERROR_MESSAGES } from '../constants' import { type ApiKey, type ApiKeysDialogType } from '../types' type ApiKeysContextType = { open: ApiKeysDialogType | null setOpen: (str: ApiKeysDialogType | null) => void currentRow: ApiKey | null setCurrentRow: React.Dispatch> refreshTrigger: number triggerRefresh: () => void resolvedKey: string setResolvedKey: React.Dispatch> resolveRealKey: (id: number) => Promise resolveRealKeysBatch: (ids: number[]) => Promise> resolvedKeys: Record loadingKeys: Record copiedKeyId: number | null markKeyCopied: (id: number) => void } const ApiKeysContext = React.createContext(null) export function ApiKeysProvider({ children }: { children: React.ReactNode }) { const { t } = useTranslation() const [open, setOpen] = useDialogState(null) const [currentRow, setCurrentRow] = useState(null) const [refreshTrigger, setRefreshTrigger] = useState(0) const [resolvedKey, setResolvedKey] = useState('') const [resolvedKeys, setResolvedKeys] = useState>({}) const [loadingKeys, setLoadingKeys] = useState>({}) const pendingRequests = useRef>>({}) const [copiedKeyId, setCopiedKeyId] = useState(null) const copiedTimerRef = useRef>(undefined) useEffect(() => { return () => clearTimeout(copiedTimerRef.current) }, []) const markKeyCopied = useCallback((id: number) => { setCopiedKeyId(id) clearTimeout(copiedTimerRef.current) copiedTimerRef.current = setTimeout(() => setCopiedKeyId(null), 2000) }, []) const triggerRefresh = useCallback(() => { setRefreshTrigger((prev) => prev + 1) }, []) const resolveRealKey = useCallback( async (id: number): Promise => { if (resolvedKeys[id]) return resolvedKeys[id] if (id in pendingRequests.current) return pendingRequests.current[id] const request = (async () => { setLoadingKeys((prev) => ({ ...prev, [id]: true })) try { const res = await fetchTokenKey(id) if (res.success && res.data?.key) { const fullKey = `sk-${res.data.key}` setResolvedKeys((prev) => ({ ...prev, [id]: fullKey })) return fullKey } toast.error(res.message || t(ERROR_MESSAGES.UNEXPECTED)) return null } catch { toast.error(t(ERROR_MESSAGES.UNEXPECTED)) return null } finally { delete pendingRequests.current[id] setLoadingKeys((prev) => { const next = { ...prev } delete next[id] return next }) } })() pendingRequests.current[id] = request return request }, [resolvedKeys, t] ) const resolveRealKeysBatch = useCallback( async (ids: number[]): Promise> => { const uncachedIds = ids.filter((id) => !resolvedKeys[id]) if (uncachedIds.length === 0) { const result: Record = {} for (const id of ids) result[id] = resolvedKeys[id] return result } for (const id of uncachedIds) { setLoadingKeys((prev) => ({ ...prev, [id]: true })) } try { const res = await fetchTokenKeysBatch(uncachedIds) if (res.success && res.data?.keys) { const newKeys: Record = {} for (const [idStr, key] of Object.entries(res.data.keys)) { newKeys[Number(idStr)] = `sk-${key}` } setResolvedKeys((prev) => ({ ...prev, ...newKeys })) const result: Record = { ...newKeys } for (const id of ids) { if (resolvedKeys[id]) result[id] = resolvedKeys[id] } return result } toast.error(res.message || t(ERROR_MESSAGES.UNEXPECTED)) return {} } catch { toast.error(t(ERROR_MESSAGES.UNEXPECTED)) return {} } finally { for (const id of uncachedIds) { setLoadingKeys((prev) => { const next = { ...prev } delete next[id] return next }) } } }, [resolvedKeys, t] ) return ( {children} ) } // eslint-disable-next-line react-refresh/only-export-components export const useApiKeys = () => { const apiKeysContext = React.useContext(ApiKeysContext) if (!apiKeysContext) { throw new Error('useApiKeys has to be used within ') } return apiKeysContext }