diff --git a/apps/desktop/src/components/main/sidebar/banner/component.tsx b/apps/desktop/src/components/main/sidebar/banner/component.tsx index 565b5e2c94..6ea22c3e6d 100644 --- a/apps/desktop/src/components/main/sidebar/banner/component.tsx +++ b/apps/desktop/src/components/main/sidebar/banner/component.tsx @@ -11,6 +11,8 @@ export function Banner({ banner: BannerType; onDismiss?: () => void; }) { + const hasProgress = banner.progress !== undefined && banner.progress >= 0; + return (
{banner.primaryAction && ( - +
+ + {hasProgress && ( +
+ )} +
)} {banner.secondaryAction && (
diff --git a/apps/desktop/src/components/main/sidebar/banner/index.tsx b/apps/desktop/src/components/main/sidebar/banner/index.tsx index bcaf52fcf3..4e1e71112a 100644 --- a/apps/desktop/src/components/main/sidebar/banner/index.tsx +++ b/apps/desktop/src/components/main/sidebar/banner/index.tsx @@ -1,6 +1,10 @@ import { AnimatePresence, motion } from "motion/react"; import { useCallback, useEffect, useMemo, useState } from "react"; +import { + events as localSttEvents, + type SupportedSttModel, +} from "@hypr/plugin-local-stt"; import { cn } from "@hypr/utils"; import { useAuth } from "../../../../auth"; @@ -8,6 +12,7 @@ import { useConfigValues } from "../../../../config/use-config"; import { useTabs } from "../../../../store/zustand/tabs"; import { Banner } from "./component"; import { createBannerRegistry, getBannerToShow } from "./registry"; +import type { BannerType } from "./types"; import { useDismissedBanners } from "./useDismissedBanners"; export function BannerArea({ @@ -90,11 +95,26 @@ export function BannerArea({ ], ); - const currentBanner = useMemo( + const registryBanner = useMemo( () => getBannerToShow(registry, isDismissed), [registry, isDismissed], ); + const downloadProgress = useDownloadProgress(); + + const currentBanner: BannerType | null = useMemo(() => { + if (downloadProgress.isDownloading && downloadProgress.model) { + return { + id: "download-progress", + title: "Downloading model", + description: `${downloadProgress.modelDisplayName} is being downloaded...`, + dismissible: false, + progress: downloadProgress.progress, + }; + } + return registryBanner; + }, [downloadProgress, registryBanner]); + const handleDismiss = useCallback(() => { if (currentBanner) { dismissBanner(currentBanner.id); @@ -127,18 +147,77 @@ export function BannerArea({ ); } +let globalShowBanner = false; +let globalBannerTimer: NodeJS.Timeout | null = null; + function useShouldShowBanner(isProfileExpanded: boolean) { const BANNER_CHECK_DELAY_MS = 3000; - const [showBanner, setShowBanner] = useState(false); + const [showBanner, setShowBanner] = useState(globalShowBanner); useEffect(() => { - const timer = setTimeout(() => { + if (!globalShowBanner && !globalBannerTimer) { + globalBannerTimer = setTimeout(() => { + globalShowBanner = true; + setShowBanner(true); + globalBannerTimer = null; + }, BANNER_CHECK_DELAY_MS); + } else if (globalShowBanner) { setShowBanner(true); - }, BANNER_CHECK_DELAY_MS); + } - return () => clearTimeout(timer); + return () => {}; }, []); return !isProfileExpanded && showBanner; } + +const MODEL_DISPLAY_NAMES: Record = { + "am-parakeet-v2": "Parakeet v2", + "am-parakeet-v3": "Parakeet v3", + "am-whisper-large-v3": "Whisper Large v3", + QuantizedTiny: "Whisper Tiny", + QuantizedTinyEn: "Whisper Tiny (English)", + QuantizedBase: "Whisper Base", + QuantizedBaseEn: "Whisper Base (English)", + QuantizedSmall: "Whisper Small", + QuantizedSmallEn: "Whisper Small (English)", + QuantizedLargeTurbo: "Whisper Large Turbo", +}; + +function useDownloadProgress() { + const [model, setModel] = useState(null); + const [progress, setProgress] = useState(0); + const [isDownloading, setIsDownloading] = useState(false); + + useEffect(() => { + const unlisten = localSttEvents.downloadProgressPayload.listen((event) => { + const { model: eventModel, progress: eventProgress } = event.payload; + + if (eventProgress < 0) { + setIsDownloading(false); + setModel(null); + setProgress(0); + } else if (eventProgress >= 100) { + setIsDownloading(false); + setModel(null); + setProgress(0); + } else { + setIsDownloading(true); + setModel(eventModel); + setProgress(Math.max(0, Math.min(100, eventProgress))); + } + }); + + return () => { + void unlisten.then((fn) => fn()); + }; + }, []); + + return { + model, + modelDisplayName: model ? MODEL_DISPLAY_NAMES[model] : "", + progress, + isDownloading, + }; +} diff --git a/apps/desktop/src/components/main/sidebar/banner/types.ts b/apps/desktop/src/components/main/sidebar/banner/types.ts index f32cdab22d..9ece0183df 100644 --- a/apps/desktop/src/components/main/sidebar/banner/types.ts +++ b/apps/desktop/src/components/main/sidebar/banner/types.ts @@ -13,6 +13,7 @@ export type BannerType = { primaryAction?: BannerAction; secondaryAction?: BannerAction; dismissible: boolean; + progress?: number; }; export type BannerCondition = () => boolean;