From 63a79ed91c2ee1f49b4bf0856cdc018794505c57 Mon Sep 17 00:00:00 2001 From: shrimbly Date: Sat, 7 Feb 2026 00:07:47 +1300 Subject: [PATCH] feat: add localStorage caching for model list and schema parameters Adds 48-hour client-side caching to persist model data across dev server restarts and page refreshes, eliminating repeated API calls during hot reloading in development mode. Co-Authored-By: Claude Opus 4.5 --- src/components/modals/ModelSearchDialog.tsx | 44 ++++++++++++++++++ src/components/nodes/ModelParameters.tsx | 50 ++++++++++++++++++++- 2 files changed, 92 insertions(+), 2 deletions(-) diff --git a/src/components/modals/ModelSearchDialog.tsx b/src/components/modals/ModelSearchDialog.tsx index 262eb273..f8fd79d2 100644 --- a/src/components/modals/ModelSearchDialog.tsx +++ b/src/components/modals/ModelSearchDialog.tsx @@ -8,6 +8,38 @@ import { useReactFlow } from "@xyflow/react"; import { ProviderType, RecentModel } from "@/types"; import { ProviderModel, ModelCapability } from "@/lib/providers/types"; +// localStorage cache for models (persists across dev server restarts) +const MODELS_CACHE_KEY = "node-banana-models-cache"; +const MODELS_CACHE_TTL = 48 * 60 * 60 * 1000; // 48 hours + +interface ModelsCacheEntry { + models: ProviderModel[]; + timestamp: number; +} + +function getCachedModels(cacheKey: string): ModelsCacheEntry | null { + try { + const cache = JSON.parse(localStorage.getItem(MODELS_CACHE_KEY) || "{}"); + const entry = cache[cacheKey]; + if (entry && Date.now() - entry.timestamp < MODELS_CACHE_TTL) { + return entry; + } + } catch { + // Ignore cache errors + } + return null; +} + +function setCachedModels(cacheKey: string, models: ProviderModel[]) { + try { + const cache = JSON.parse(localStorage.getItem(MODELS_CACHE_KEY) || "{}"); + cache[cacheKey] = { models, timestamp: Date.now() }; + localStorage.setItem(MODELS_CACHE_KEY, JSON.stringify(cache)); + } catch { + // Ignore cache errors + } +} + // Provider icons — all normalized to w-3.5 h-3.5 with viewBoxes cropped to fill consistently const ReplicateIcon = () => ( @@ -139,6 +171,16 @@ export function ModelSearchDialog({ // Increment version to track this request const thisVersion = ++requestVersionRef.current; + // Build cache key from filters + const cacheKey = `${providerFilter}:${capabilityFilter}:${debouncedSearch}`; + + // Check localStorage cache first + const cached = getCachedModels(cacheKey); + if (cached) { + setModels(cached.models); + return; + } + setIsLoading(true); setError(null); @@ -187,6 +229,8 @@ export function ModelSearchDialog({ if (data.success && data.models) { setModels(data.models); + // Cache the successful result + setCachedModels(cacheKey, data.models); } else { setError(data.error || "Failed to fetch models"); setModels([]); diff --git a/src/components/nodes/ModelParameters.tsx b/src/components/nodes/ModelParameters.tsx index 4b71292f..c7f08684 100644 --- a/src/components/nodes/ModelParameters.tsx +++ b/src/components/nodes/ModelParameters.tsx @@ -6,6 +6,40 @@ import { ModelParameter } from "@/lib/providers/types"; import { useProviderApiKeys } from "@/store/workflowStore"; import { deduplicatedFetch } from "@/utils/deduplicatedFetch"; +// localStorage cache for model schemas (persists across dev server restarts) +const SCHEMA_CACHE_KEY = "node-banana-schema-cache"; +const SCHEMA_CACHE_TTL = 48 * 60 * 60 * 1000; // 48 hours + +interface SchemaCacheEntry { + parameters: ModelParameter[]; + inputs: ModelInputDef[]; + timestamp: number; +} + +function getCachedSchema(modelId: string, provider: string): SchemaCacheEntry | null { + try { + const cache = JSON.parse(localStorage.getItem(SCHEMA_CACHE_KEY) || "{}"); + const key = `${provider}:${modelId}`; + const entry = cache[key]; + if (entry && Date.now() - entry.timestamp < SCHEMA_CACHE_TTL) { + return entry; + } + } catch { + // Ignore cache errors + } + return null; +} + +function setCachedSchema(modelId: string, provider: string, parameters: ModelParameter[], inputs: ModelInputDef[]) { + try { + const cache = JSON.parse(localStorage.getItem(SCHEMA_CACHE_KEY) || "{}"); + cache[`${provider}:${modelId}`] = { parameters, inputs, timestamp: Date.now() }; + localStorage.setItem(SCHEMA_CACHE_KEY, JSON.stringify(cache)); + } catch { + // Ignore cache errors + } +} + interface ModelParametersProps { modelId: string; provider: ProviderType; @@ -44,6 +78,14 @@ export function ModelParameters({ } const fetchSchema = async () => { + // Check localStorage cache first + const cached = getCachedSchema(modelId, provider); + if (cached) { + setSchema(cached.parameters); + onInputsLoaded?.(cached.inputs); + return; + } + setIsLoading(true); setError(null); @@ -75,11 +117,15 @@ export function ModelParameters({ const data = await response.json(); const params = data.parameters || []; + const inputs = data.inputs || []; setSchema(params); + // Cache the successful result + setCachedSchema(modelId, provider, params, inputs); + // Pass inputs to parent for dynamic handle rendering - if (data.inputs && onInputsLoaded) { - onInputsLoaded(data.inputs); + if (onInputsLoaded) { + onInputsLoaded(inputs); } } catch (err) { console.error("Failed to fetch model schema:", err);