From 035f765130535a4653cc5905bb2d375e28529317 Mon Sep 17 00:00:00 2001 From: samit Date: Thu, 19 Feb 2026 21:05:27 -0800 Subject: [PATCH] added hf token validation --- .../components/steps/dataset-step.tsx | 22 +++++++ .../components/steps/model-selection-step.tsx | 22 +++++++ .../studio/sections/dataset-section.tsx | 22 +++++++ .../studio/sections/model-section.tsx | 22 +++++++ studio/frontend/src/hooks/index.ts | 1 + .../src/hooks/use-hf-token-validation.ts | 59 +++++++++++++++++++ 6 files changed, 148 insertions(+) create mode 100644 studio/frontend/src/hooks/use-hf-token-validation.ts diff --git a/studio/frontend/src/features/onboarding/components/steps/dataset-step.tsx b/studio/frontend/src/features/onboarding/components/steps/dataset-step.tsx index 0275e5a6da..484b10ddb4 100644 --- a/studio/frontend/src/features/onboarding/components/steps/dataset-step.tsx +++ b/studio/frontend/src/features/onboarding/components/steps/dataset-step.tsx @@ -35,6 +35,7 @@ import { import { useDebouncedValue, useHfDatasetSearch, + useHfTokenValidation, useInfiniteScroll, } from "@/hooks"; import { cn, formatCompact } from "@/lib/utils"; @@ -103,10 +104,14 @@ export function DatasetStep() { isLoading, isLoadingMore, fetchMore, + error: hfSearchError, } = useHfDatasetSearch(debouncedQuery, { accessToken: hfToken || undefined, }); + const { error: tokenValidationError, isChecking: isCheckingToken } = + useHfTokenValidation(hfToken); + const resultIds = useMemo(() => hfResults.map((r) => r.id), [hfResults]); const comboboxAnchorRef = useRef(null); @@ -179,6 +184,23 @@ export function DatasetStep() { onChange={(e) => setHfToken(e.target.value)} /> + {(tokenValidationError ?? hfSearchError) && ( +

+ {tokenValidationError ?? hfSearchError} + {" — "} + + Get or update token + +

+ )} + {isCheckingToken && ( +

Checking token…

+ )} diff --git a/studio/frontend/src/features/onboarding/components/steps/model-selection-step.tsx b/studio/frontend/src/features/onboarding/components/steps/model-selection-step.tsx index 3f63775ff9..ce506b6085 100644 --- a/studio/frontend/src/features/onboarding/components/steps/model-selection-step.tsx +++ b/studio/frontend/src/features/onboarding/components/steps/model-selection-step.tsx @@ -34,6 +34,7 @@ import { MODEL_TYPE_TO_HF_TASK } from "@/config/training"; import { useDebouncedValue, useHfModelSearch, + useHfTokenValidation, useInfiniteScroll, } from "@/hooks"; import { formatCompact } from "@/lib/utils"; @@ -80,11 +81,15 @@ export function ModelSelectionStep() { isLoading, isLoadingMore, fetchMore, + error: hfSearchError, } = useHfModelSearch(debouncedQuery, { task, accessToken: hfToken || undefined, }); + const { error: tokenValidationError, isChecking: isCheckingToken } = + useHfTokenValidation(hfToken); + const resultIds = useMemo(() => hfResults.map((r) => r.id), [hfResults]); const comboboxAnchorRef = useRef(null); @@ -126,6 +131,23 @@ export function ModelSelectionStep() { onChange={(e) => setHfToken(e.target.value)} /> + {(tokenValidationError ?? hfSearchError) && ( +

+ {tokenValidationError ?? hfSearchError} + {" — "} + + Get or update token + +

+ )} + {isCheckingToken && ( +

Checking token…

+ )}
diff --git a/studio/frontend/src/features/studio/sections/dataset-section.tsx b/studio/frontend/src/features/studio/sections/dataset-section.tsx index e3f50efb40..d7b97c7ad0 100644 --- a/studio/frontend/src/features/studio/sections/dataset-section.tsx +++ b/studio/frontend/src/features/studio/sections/dataset-section.tsx @@ -25,6 +25,7 @@ import { import { useDebouncedValue, useHfDatasetSearch, + useHfTokenValidation, useInfiniteScroll, } from "@/hooks"; import { formatCompact } from "@/lib/utils"; @@ -102,10 +103,14 @@ export function DatasetSection() { isLoading, isLoadingMore, fetchMore, + error: hfSearchError, } = useHfDatasetSearch(debouncedQuery, { accessToken: hfToken || undefined, }); + const { error: tokenValidationError, isChecking: isCheckingToken } = + useHfTokenValidation(hfToken); + const resultIds = useMemo(() => { const ids = hfResults.map((r) => r.id); if (dataset && !ids.includes(dataset)) { @@ -239,6 +244,23 @@ export function DatasetSection() { + {(tokenValidationError ?? hfSearchError) && ( +

+ {tokenValidationError ?? hfSearchError} + {" — "} + + Get or update token + +

+ )} + {isCheckingToken && ( +

Checking token…

+ )} { const ids = hfResults.map((r) => r.id); if (selectedModel && !ids.includes(selectedModel)) { @@ -555,6 +560,23 @@ export function ModelSection() { onChange={(e) => setHfToken(e.target.value)} /> + {(tokenValidationError ?? hfSearchError) && ( +

+ {tokenValidationError ?? hfSearchError} + {" — "} + + Get or update token + +

+ )} + {isCheckingToken && ( +

Checking token…

+ )} diff --git a/studio/frontend/src/hooks/index.ts b/studio/frontend/src/hooks/index.ts index 16c2c82b75..b6c4fcad14 100644 --- a/studio/frontend/src/hooks/index.ts +++ b/studio/frontend/src/hooks/index.ts @@ -5,4 +5,5 @@ export { useHardwareInfo } from "./use-hardware-info"; export { useHfModelSearch } from "./use-hf-model-search"; export { useHfDatasetSearch } from "./use-hf-dataset-search"; export { useHfDatasetSplits } from "./use-hf-dataset-splits"; +export { useHfTokenValidation } from "./use-hf-token-validation"; export { useInfiniteScroll } from "./use-infinite-scroll"; diff --git a/studio/frontend/src/hooks/use-hf-token-validation.ts b/studio/frontend/src/hooks/use-hf-token-validation.ts new file mode 100644 index 0000000000..1882745eca --- /dev/null +++ b/studio/frontend/src/hooks/use-hf-token-validation.ts @@ -0,0 +1,59 @@ +import { whoAmI } from "@huggingface/hub"; +import { useCallback, useEffect, useRef, useState } from "react"; +import { useDebouncedValue } from "./use-debounced-value"; + +export interface HfTokenValidationState { + isValid: boolean | null; + error: string | null; + isChecking: boolean; +} + +const INITIAL: HfTokenValidationState = { + isValid: null, + error: null, + isChecking: false, +}; + +/** + * Validates the Hugging Face token by calling the whoami-v2 API. + * Debounces the token to avoid excessive requests while typing. + * Returns validation state: isValid (null = not checked), error message, and isChecking. + */ +export function useHfTokenValidation(token: string): HfTokenValidationState { + const debouncedToken = useDebouncedValue(token.trim(), 500); + const [state, setState] = useState(INITIAL); + const versionRef = useRef(0); + + const runCheck = useCallback(async (t: string) => { + if (!t) { + setState({ isValid: null, error: null, isChecking: false }); + return; + } + + const v = ++versionRef.current; + setState((prev) => ({ ...prev, isChecking: true, error: null })); + + try { + await whoAmI({ accessToken: t }); + if (versionRef.current !== v) return; + setState({ isValid: true, error: null, isChecking: false }); + } catch { + if (versionRef.current !== v) return; + setState({ + isValid: false, + error: "invalid or expired token", + isChecking: false, + }); + } + }, []); + + useEffect(() => { + if (!debouncedToken) { + setState(INITIAL); + return; + } + runCheck(debouncedToken); + }, [debouncedToken, runCheck]); + + return state; +}