feat: add FeatureInstallPrompt component for uninstalled AI tool pages

Show an install prompt instead of the normal tool UI when an AI feature
bundle is not installed. Admins see a one-click install button with SSE
progress tracking and polling fallback; non-admins see a message to
contact their administrator.
This commit is contained in:
ashim-hq
2026-04-18 02:50:56 +08:00
parent fe1ae8436d
commit 6e9a79191a
2 changed files with 211 additions and 1 deletions
+19 -1
View File
@@ -1,4 +1,4 @@
import { TOOLS } from "@ashim/shared";
import { PYTHON_SIDECAR_TOOLS, TOOLS } from "@ashim/shared";
import {
CheckCircle2,
ChevronLeft,
@@ -16,15 +16,18 @@ import { type BgPreviewState, ImageViewer } from "@/components/common/image-view
import { ReviewPanel } from "@/components/common/review-panel";
import { SideBySideComparison } from "@/components/common/side-by-side-comparison";
import { ThumbnailStrip } from "@/components/common/thumbnail-strip";
import { FeatureInstallPrompt } from "@/components/features/feature-install-prompt";
import { AppLayout } from "@/components/layout/app-layout";
import { CropCanvas } from "@/components/tools/crop-canvas";
import type { EraserCanvasRef } from "@/components/tools/eraser-canvas";
import { EraserCanvas } from "@/components/tools/eraser-canvas";
import type { PreviewTransform } from "@/components/tools/rotate-settings";
import { useAuth } from "@/hooks/use-auth";
import { useMobile } from "@/hooks/use-mobile";
import { formatFileSize } from "@/lib/download";
import { ICON_MAP } from "@/lib/icon-map";
import { getToolRegistryEntry } from "@/lib/tool-registry";
import { useFeaturesStore } from "@/stores/features-store";
import { useFileStore } from "@/stores/file-store";
/** Formats that browsers can render in <img> tags. */
@@ -106,6 +109,13 @@ export function ToolPage() {
() => (toolId ? getToolRegistryEntry(toolId) : undefined),
[toolId],
);
const isAiTool = toolId ? (PYTHON_SIDECAR_TOOLS as readonly string[]).includes(toolId) : false;
const getBundleForTool = useFeaturesStore((s) => s.getBundleForTool);
const isToolInstalled = useFeaturesStore((s) => s.isToolInstalled);
const featureBundle = toolId ? getBundleForTool(toolId) : null;
const toolInstalled = toolId ? isToolInstalled(toolId) : true;
const { hasPermission } = useAuth();
const isAdmin = hasPermission("settings:write");
const {
files,
entries,
@@ -234,6 +244,14 @@ export function ToolPage() {
);
}
if (isAiTool && !toolInstalled && featureBundle) {
return (
<AppLayout>
<FeatureInstallPrompt bundle={featureBundle} isAdmin={isAdmin} />
</AppLayout>
);
}
const IconComponent =
(ICON_MAP[tool.icon] as React.ComponentType<{ className?: string }>) ?? FileImage;