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
+14 -2
View File
@@ -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}`;
+14 -2
View File
@@ -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}`;
}
+20 -1
View File
@@ -1,6 +1,25 @@
const API_BASE = "/api";
export function parseApiError(body: Record<string, unknown>, fallbackStatus: number): string {
export interface FeatureNotInstalledError {
type: "feature_not_installed";
feature: string;
featureName: string;
estimatedSize: string;
}
export function parseApiError(
body: Record<string, unknown>,
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) {
+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;
},
}));