feat: add frontend features store and FEATURE_NOT_INSTALLED error handling

Add Zustand features store for tracking AI feature bundle state with
fetch, refresh, isToolInstalled, and getBundleForTool methods. Extend
parseApiError to return structured FeatureNotInstalledError objects
when the backend returns FEATURE_NOT_INSTALLED, and handle them in
both tool and pipeline processor hooks with user-friendly messages.
This commit is contained in:
ashim-hq
2026-04-18 02:45:43 +08:00
parent 494bb3d78b
commit 0b9c760e5e
4 changed files with 96 additions and 5 deletions
+48
View File
@@ -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<void>;
refresh: () => Promise<void>;
isToolInstalled: (toolId: string) => boolean;
getBundleForTool: (toolId: string) => FeatureBundleState | null;
}
export const useFeaturesStore = create<FeaturesState>((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;
},
}));