mirror of
https://github.com/snapotter-hq/SnapOtter.git
synced 2026-08-03 07:46:42 +02:00
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:
@@ -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}`;
|
||||
|
||||
@@ -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
@@ -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) {
|
||||
|
||||
@@ -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;
|
||||
},
|
||||
}));
|
||||
Reference in New Issue
Block a user