diff --git a/apps/web/src/hooks/use-pipeline-processor.ts b/apps/web/src/hooks/use-pipeline-processor.ts index de9c79d2..3e2378b0 100644 --- a/apps/web/src/hooks/use-pipeline-processor.ts +++ b/apps/web/src/hooks/use-pipeline-processor.ts @@ -145,7 +145,14 @@ export function usePipelineProcessor() { } else { try { const body = JSON.parse(xhr.responseText); - setError(parseApiError(body, xhr.status)); + const parsed = parseApiError(body, xhr.status); + if (typeof parsed === "object" && parsed.type === "feature_not_installed") { + setError( + `Feature "${parsed.featureName}" is not installed. Enable it in Settings → AI Features.`, + ); + } else { + setError(parsed as string); + } } catch { setError(`Processing failed: ${xhr.status}`); } @@ -270,7 +277,12 @@ export function usePipelineProcessor() { errorMsg += ` (${body.errors.length} files failed)`; } } else { - errorMsg = parseApiError(body, response.status); + const parsed = parseApiError(body, response.status); + if (typeof parsed === "object" && parsed.type === "feature_not_installed") { + errorMsg = `Feature "${parsed.featureName}" is not installed. Enable it in Settings → AI Features.`; + } else { + errorMsg = parsed as string; + } } } catch { errorMsg = `Batch processing failed: ${response.status}`; diff --git a/apps/web/src/hooks/use-tool-processor.ts b/apps/web/src/hooks/use-tool-processor.ts index 7975fbec..65dd7684 100644 --- a/apps/web/src/hooks/use-tool-processor.ts +++ b/apps/web/src/hooks/use-tool-processor.ts @@ -233,7 +233,14 @@ export function useToolProcessor(toolId: string) { } else { try { const body = JSON.parse(xhr.responseText); - setError(parseApiError(body, xhr.status)); + const parsed = parseApiError(body, xhr.status); + if (typeof parsed === "object" && parsed.type === "feature_not_installed") { + setError( + `Feature "${parsed.featureName}" is not installed. Enable it in Settings → AI Features.`, + ); + } else { + setError(parsed as string); + } } catch { setError(`Processing failed: ${xhr.status}`); } @@ -354,7 +361,12 @@ export function useToolProcessor(toolId: string) { let errorMsg: string; try { const body = JSON.parse(text); - errorMsg = parseApiError(body, response.status); + const parsed = parseApiError(body, response.status); + if (typeof parsed === "object" && parsed.type === "feature_not_installed") { + errorMsg = `Feature "${parsed.featureName}" is not installed. Enable it in Settings → AI Features.`; + } else { + errorMsg = parsed as string; + } } catch { errorMsg = `Batch processing failed: ${response.status}`; } diff --git a/apps/web/src/lib/api.ts b/apps/web/src/lib/api.ts index f8b09f30..05da8b43 100644 --- a/apps/web/src/lib/api.ts +++ b/apps/web/src/lib/api.ts @@ -1,6 +1,25 @@ const API_BASE = "/api"; -export function parseApiError(body: Record, fallbackStatus: number): string { +export interface FeatureNotInstalledError { + type: "feature_not_installed"; + feature: string; + featureName: string; + estimatedSize: string; +} + +export function parseApiError( + body: Record, + fallbackStatus: number, +): string | FeatureNotInstalledError { + if (body.code === "FEATURE_NOT_INSTALLED") { + return { + type: "feature_not_installed", + feature: body.feature as string, + featureName: body.featureName as string, + estimatedSize: body.estimatedSize as string, + }; + } + const error = typeof body.error === "string" ? body.error : ""; const details = body.details; if (!details) { diff --git a/apps/web/src/stores/features-store.ts b/apps/web/src/stores/features-store.ts new file mode 100644 index 00000000..2d833a7a --- /dev/null +++ b/apps/web/src/stores/features-store.ts @@ -0,0 +1,48 @@ +import type { FeatureBundleState } from "@ashim/shared"; +import { TOOL_BUNDLE_MAP } from "@ashim/shared"; +import { create } from "zustand"; +import { apiGet } from "@/lib/api"; + +interface FeaturesState { + bundles: FeatureBundleState[]; + loaded: boolean; + fetch: () => Promise; + refresh: () => Promise; + isToolInstalled: (toolId: string) => boolean; + getBundleForTool: (toolId: string) => FeatureBundleState | null; +} + +export const useFeaturesStore = create((set, get) => ({ + bundles: [], + loaded: false, + + fetch: async () => { + if (get().loaded) return; + try { + const data = await apiGet<{ bundles: FeatureBundleState[] }>("/v1/features"); + set({ bundles: data.bundles, loaded: true }); + } catch { + set({ loaded: true }); + } + }, + + refresh: async () => { + try { + const data = await apiGet<{ bundles: FeatureBundleState[] }>("/v1/features"); + set({ bundles: data.bundles, loaded: true }); + } catch {} + }, + + isToolInstalled: (toolId: string) => { + const bundleId = TOOL_BUNDLE_MAP[toolId]; + if (!bundleId) return true; + const bundle = get().bundles.find((b) => b.id === bundleId); + return bundle?.status === "installed"; + }, + + getBundleForTool: (toolId: string) => { + const bundleId = TOOL_BUNDLE_MAP[toolId]; + if (!bundleId) return null; + return get().bundles.find((b) => b.id === bundleId) ?? null; + }, +}));