diff --git a/apps/web/src/components/common/tool-card.tsx b/apps/web/src/components/common/tool-card.tsx index f90194bc..6cfd2341 100644 --- a/apps/web/src/components/common/tool-card.tsx +++ b/apps/web/src/components/common/tool-card.tsx @@ -1,6 +1,6 @@ import type { Tool } from "@snapotter/shared"; import { PYTHON_SIDECAR_TOOLS, TOOL_BUNDLE_MAP } from "@snapotter/shared"; -import { Download, FileImage, Star } from "lucide-react"; +import { Clock, Download, FileImage, Loader2, Star } from "lucide-react"; import { useMemo } from "react"; import { Link } from "react-router-dom"; import { ICON_MAP } from "@/lib/icon-map"; @@ -17,14 +17,17 @@ export function ToolCard({ tool }: ToolCardProps) { const isAiTool = (PYTHON_SIDECAR_TOOLS as readonly string[]).includes(tool.id); const bundles = useFeaturesStore((s) => s.bundles); - const isInstalled = useMemo(() => { - if (!isAiTool) return true; + const installing = useFeaturesStore((s) => s.installing); + const queued = useFeaturesStore((s) => s.queued); + const aiStatus = useMemo(() => { + if (!isAiTool) return "installed"; const bundleId = TOOL_BUNDLE_MAP[tool.id]; - if (!bundleId) return true; + if (!bundleId) return "installed"; + if (queued.includes(bundleId)) return "queued"; + if (installing[bundleId]) return "installing"; const bundle = bundles.find((b) => b.id === bundleId); - return bundle?.status === "installed"; - }, [isAiTool, tool.id, bundles]); - const showDownloadBadge = isAiTool && !isInstalled; + return bundle?.status === "installed" ? "installed" : "not_installed"; + }, [isAiTool, tool.id, bundles, installing, queued]); return (
@@ -50,7 +53,11 @@ export function ToolCard({ tool }: ToolCardProps) { Experimental )} - {showDownloadBadge && } + {aiStatus === "not_installed" && } + {aiStatus === "queued" && } + {aiStatus === "installing" && ( + + )}
); diff --git a/apps/web/src/pages/home-page.tsx b/apps/web/src/pages/home-page.tsx index 3be7a057..dd1121d0 100644 --- a/apps/web/src/pages/home-page.tsx +++ b/apps/web/src/pages/home-page.tsx @@ -1,6 +1,6 @@ -import { CATEGORIES, PYTHON_SIDECAR_TOOLS, TOOLS } from "@snapotter/shared"; -import { Download, Loader2 } from "lucide-react"; -import { useCallback, useEffect } from "react"; +import { CATEGORIES, PYTHON_SIDECAR_TOOLS, TOOL_BUNDLE_MAP, TOOLS } from "@snapotter/shared"; +import { Clock, Download, Loader2 } from "lucide-react"; +import { useCallback, useEffect, useMemo } from "react"; import { useNavigate } from "react-router-dom"; import { ImageViewer } from "@/components/common/image-viewer"; import { MultiImageViewer } from "@/components/common/multi-image-viewer"; @@ -25,7 +25,7 @@ export function HomePage() { } = useFileStore(); const navigate = useNavigate(); const { fetch: fetchSettings, defaultToolView, loaded: settingsLoaded } = useSettingsStore(); - const { fetch: fetchFeatures, isToolInstalled } = useFeaturesStore(); + const { fetch: fetchFeatures, bundles, installing, queued } = useFeaturesStore(); useEffect(() => { reset(); @@ -42,6 +42,19 @@ export function HomePage() { } }, [settingsLoaded, defaultToolView, files.length, navigate]); + const getToolStatus = useMemo(() => { + return (toolId: string) => { + const isAi = (PYTHON_SIDECAR_TOOLS as readonly string[]).includes(toolId); + if (!isAi) return "installed"; + const bundleId = TOOL_BUNDLE_MAP[toolId]; + if (!bundleId) return "installed"; + if (queued.includes(bundleId)) return "queued"; + if (installing[bundleId]) return "installing"; + const bundle = bundles.find((b) => b.id === bundleId); + return bundle?.status === "installed" ? "installed" : "not_installed"; + }; + }, [bundles, installing, queued]); + const handleFiles = useCallback( (newFiles: File[]) => { reset(); @@ -96,8 +109,7 @@ export function HomePage() { const Icon = (ICON_MAP[tool.icon] as React.ComponentType<{ className?: string }>) ?? ICON_MAP.FileImage; - const isAi = (PYTHON_SIDECAR_TOOLS as readonly string[]).includes(id); - const needsDownload = isAi && !isToolInstalled(id); + const status = getToolStatus(id); return ( ); })} @@ -139,8 +157,7 @@ export function HomePage() { const Icon = (ICON_MAP[tool.icon] as React.ComponentType<{ className?: string }>) ?? ICON_MAP.FileImage; - const isAi = (PYTHON_SIDECAR_TOOLS as readonly string[]).includes(tool.id); - const needsDownload = isAi && !isToolInstalled(tool.id); + const status = getToolStatus(tool.id); return ( ); })}