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 (
{tool.name}
- {needsDownload && (
+ {status === "not_installed" && (
)}
+ {status === "queued" && (
+
+ )}
+ {status === "installing" && (
+
+ )}
);
})}
@@ -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 (
);
})}