mirror of
https://github.com/snapotter-hq/SnapOtter.git
synced 2026-08-03 07:46:42 +02:00
fix: show queued/installing status for AI tools on main page
Main page tool cards and home page tool lists only subscribed to server-side bundle state, which only reflects the actively downloading feature. Queued features appeared as plain download icons instead of showing their queued/installing status. Now subscribes to client-side installing and queued state from the features store, matching the settings page behavior.
This commit is contained in:
@@ -1,6 +1,6 @@
|
|||||||
import type { Tool } from "@snapotter/shared";
|
import type { Tool } from "@snapotter/shared";
|
||||||
import { PYTHON_SIDECAR_TOOLS, TOOL_BUNDLE_MAP } 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 { useMemo } from "react";
|
||||||
import { Link } from "react-router-dom";
|
import { Link } from "react-router-dom";
|
||||||
import { ICON_MAP } from "@/lib/icon-map";
|
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 isAiTool = (PYTHON_SIDECAR_TOOLS as readonly string[]).includes(tool.id);
|
||||||
const bundles = useFeaturesStore((s) => s.bundles);
|
const bundles = useFeaturesStore((s) => s.bundles);
|
||||||
const isInstalled = useMemo(() => {
|
const installing = useFeaturesStore((s) => s.installing);
|
||||||
if (!isAiTool) return true;
|
const queued = useFeaturesStore((s) => s.queued);
|
||||||
|
const aiStatus = useMemo(() => {
|
||||||
|
if (!isAiTool) return "installed";
|
||||||
const bundleId = TOOL_BUNDLE_MAP[tool.id];
|
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);
|
const bundle = bundles.find((b) => b.id === bundleId);
|
||||||
return bundle?.status === "installed";
|
return bundle?.status === "installed" ? "installed" : "not_installed";
|
||||||
}, [isAiTool, tool.id, bundles]);
|
}, [isAiTool, tool.id, bundles, installing, queued]);
|
||||||
const showDownloadBadge = isAiTool && !isInstalled;
|
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div className="group flex items-center gap-3 relative">
|
<div className="group flex items-center gap-3 relative">
|
||||||
@@ -50,7 +53,11 @@ export function ToolCard({ tool }: ToolCardProps) {
|
|||||||
Experimental
|
Experimental
|
||||||
</span>
|
</span>
|
||||||
)}
|
)}
|
||||||
{showDownloadBadge && <Download className="h-3.5 w-3.5 text-muted-foreground" />}
|
{aiStatus === "not_installed" && <Download className="h-3.5 w-3.5 text-muted-foreground" />}
|
||||||
|
{aiStatus === "queued" && <Clock className="h-3.5 w-3.5 text-muted-foreground" />}
|
||||||
|
{aiStatus === "installing" && (
|
||||||
|
<Loader2 className="h-3.5 w-3.5 text-muted-foreground animate-spin" />
|
||||||
|
)}
|
||||||
</Link>
|
</Link>
|
||||||
</div>
|
</div>
|
||||||
);
|
);
|
||||||
|
|||||||
@@ -1,6 +1,6 @@
|
|||||||
import { CATEGORIES, PYTHON_SIDECAR_TOOLS, TOOLS } from "@snapotter/shared";
|
import { CATEGORIES, PYTHON_SIDECAR_TOOLS, TOOL_BUNDLE_MAP, TOOLS } from "@snapotter/shared";
|
||||||
import { Download, Loader2 } from "lucide-react";
|
import { Clock, Download, Loader2 } from "lucide-react";
|
||||||
import { useCallback, useEffect } from "react";
|
import { useCallback, useEffect, useMemo } from "react";
|
||||||
import { useNavigate } from "react-router-dom";
|
import { useNavigate } from "react-router-dom";
|
||||||
import { ImageViewer } from "@/components/common/image-viewer";
|
import { ImageViewer } from "@/components/common/image-viewer";
|
||||||
import { MultiImageViewer } from "@/components/common/multi-image-viewer";
|
import { MultiImageViewer } from "@/components/common/multi-image-viewer";
|
||||||
@@ -25,7 +25,7 @@ export function HomePage() {
|
|||||||
} = useFileStore();
|
} = useFileStore();
|
||||||
const navigate = useNavigate();
|
const navigate = useNavigate();
|
||||||
const { fetch: fetchSettings, defaultToolView, loaded: settingsLoaded } = useSettingsStore();
|
const { fetch: fetchSettings, defaultToolView, loaded: settingsLoaded } = useSettingsStore();
|
||||||
const { fetch: fetchFeatures, isToolInstalled } = useFeaturesStore();
|
const { fetch: fetchFeatures, bundles, installing, queued } = useFeaturesStore();
|
||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
reset();
|
reset();
|
||||||
@@ -42,6 +42,19 @@ export function HomePage() {
|
|||||||
}
|
}
|
||||||
}, [settingsLoaded, defaultToolView, files.length, navigate]);
|
}, [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(
|
const handleFiles = useCallback(
|
||||||
(newFiles: File[]) => {
|
(newFiles: File[]) => {
|
||||||
reset();
|
reset();
|
||||||
@@ -96,8 +109,7 @@ export function HomePage() {
|
|||||||
const Icon =
|
const Icon =
|
||||||
(ICON_MAP[tool.icon] as React.ComponentType<{ className?: string }>) ??
|
(ICON_MAP[tool.icon] as React.ComponentType<{ className?: string }>) ??
|
||||||
ICON_MAP.FileImage;
|
ICON_MAP.FileImage;
|
||||||
const isAi = (PYTHON_SIDECAR_TOOLS as readonly string[]).includes(id);
|
const status = getToolStatus(id);
|
||||||
const needsDownload = isAi && !isToolInstalled(id);
|
|
||||||
return (
|
return (
|
||||||
<button
|
<button
|
||||||
key={id}
|
key={id}
|
||||||
@@ -109,9 +121,15 @@ export function HomePage() {
|
|||||||
<Icon className="h-4 w-4" />
|
<Icon className="h-4 w-4" />
|
||||||
</div>
|
</div>
|
||||||
<span className="text-xs font-medium text-foreground">{tool.name}</span>
|
<span className="text-xs font-medium text-foreground">{tool.name}</span>
|
||||||
{needsDownload && (
|
{status === "not_installed" && (
|
||||||
<Download className="h-3.5 w-3.5 text-muted-foreground ml-auto" />
|
<Download className="h-3.5 w-3.5 text-muted-foreground ml-auto" />
|
||||||
)}
|
)}
|
||||||
|
{status === "queued" && (
|
||||||
|
<Clock className="h-3.5 w-3.5 text-muted-foreground ml-auto" />
|
||||||
|
)}
|
||||||
|
{status === "installing" && (
|
||||||
|
<Loader2 className="h-3.5 w-3.5 text-muted-foreground ml-auto animate-spin" />
|
||||||
|
)}
|
||||||
</button>
|
</button>
|
||||||
);
|
);
|
||||||
})}
|
})}
|
||||||
@@ -139,8 +157,7 @@ export function HomePage() {
|
|||||||
const Icon =
|
const Icon =
|
||||||
(ICON_MAP[tool.icon] as React.ComponentType<{ className?: string }>) ??
|
(ICON_MAP[tool.icon] as React.ComponentType<{ className?: string }>) ??
|
||||||
ICON_MAP.FileImage;
|
ICON_MAP.FileImage;
|
||||||
const isAi = (PYTHON_SIDECAR_TOOLS as readonly string[]).includes(tool.id);
|
const status = getToolStatus(tool.id);
|
||||||
const needsDownload = isAi && !isToolInstalled(tool.id);
|
|
||||||
return (
|
return (
|
||||||
<button
|
<button
|
||||||
key={tool.id}
|
key={tool.id}
|
||||||
@@ -150,9 +167,15 @@ export function HomePage() {
|
|||||||
>
|
>
|
||||||
<Icon className="h-4 w-4 text-muted-foreground shrink-0" />
|
<Icon className="h-4 w-4 text-muted-foreground shrink-0" />
|
||||||
<span className="text-sm">{tool.name}</span>
|
<span className="text-sm">{tool.name}</span>
|
||||||
{needsDownload && (
|
{status === "not_installed" && (
|
||||||
<Download className="h-3.5 w-3.5 text-muted-foreground ml-auto" />
|
<Download className="h-3.5 w-3.5 text-muted-foreground ml-auto" />
|
||||||
)}
|
)}
|
||||||
|
{status === "queued" && (
|
||||||
|
<Clock className="h-3.5 w-3.5 text-muted-foreground ml-auto" />
|
||||||
|
)}
|
||||||
|
{status === "installing" && (
|
||||||
|
<Loader2 className="h-3.5 w-3.5 text-muted-foreground ml-auto animate-spin" />
|
||||||
|
)}
|
||||||
</button>
|
</button>
|
||||||
);
|
);
|
||||||
})}
|
})}
|
||||||
|
|||||||
Reference in New Issue
Block a user