From bbb04a4d2ae44f112f6d76aa209e781a577c23fd Mon Sep 17 00:00:00 2001 From: Michael SCHAL Date: Fri, 3 Apr 2026 11:34:14 +0200 Subject: [PATCH] fix: immediate AI loading feedback + auto-fallback for deprecated model - BulkAiAnalyzer: set isAnalyzing=true immediately on click (before context fetch), so the spinner appears instantly instead of after 1-2 seconds of silence - analyze/route.ts: if configured model returns 400/404 (e.g., deprecated google/gemini-flash-1.5), automatically retry with google/gemini-2.0-flash-001 - batch-analyze/route.ts: same fallback logic Co-Authored-By: Claude Sonnet 4.6 --- src/app/api/ai/analyze/route.ts | 18 ++++++++++++++++-- src/app/api/ai/batch-analyze/route.ts | 18 ++++++++++++------ .../grid/components/bulk-ai-analyzer.tsx | 5 ++++- 3 files changed, 32 insertions(+), 9 deletions(-) diff --git a/src/app/api/ai/analyze/route.ts b/src/app/api/ai/analyze/route.ts index c2dd588..8c9e842 100644 --- a/src/app/api/ai/analyze/route.ts +++ b/src/app/api/ai/analyze/route.ts @@ -54,8 +54,22 @@ export async function POST(req: NextRequest) { } } - const client = new OpenRouterClient({ apiKey, model }); - const result = await client.analyzeProduct(enrichedBody); + const FALLBACK_MODEL = "google/gemini-2.0-flash-001"; + let client = new OpenRouterClient({ apiKey, model }); + let result; + try { + result = await client.analyzeProduct(enrichedBody); + } catch (modelErr) { + // If configured model fails (e.g., deprecated), retry with fallback + const msg = modelErr instanceof Error ? modelErr.message : ""; + if (model !== FALLBACK_MODEL && (msg.includes("400") || msg.includes("404") || msg.includes("OpenRouter error"))) { + console.warn(`[AI] Model "${model}" failed (${msg}), retrying with fallback ${FALLBACK_MODEL}`); + client = new OpenRouterClient({ apiKey, model: FALLBACK_MODEL }); + result = await client.analyzeProduct(enrichedBody); + } else { + throw modelErr; + } + } return NextResponse.json(result); } catch (err) { diff --git a/src/app/api/ai/batch-analyze/route.ts b/src/app/api/ai/batch-analyze/route.ts index 88f4fba..15bcf45 100644 --- a/src/app/api/ai/batch-analyze/route.ts +++ b/src/app/api/ai/batch-analyze/route.ts @@ -107,14 +107,12 @@ REPONDS EN JSON VALIDE uniquement : const userPrompt = `${products.length} produits du rayon "${rayon}" (sur ${supplierStats?.totalProducts ?? products.length} au total). ${JSON.stringify(products, null, 2)}`; - const response = await fetch("https://openrouter.ai/api/v1/chat/completions", { + const FALLBACK_MODEL = "google/gemini-2.0-flash-001"; + const tryModel = async (modelToTry: string) => fetch("https://openrouter.ai/api/v1/chat/completions", { method: "POST", - headers: { - "Authorization": `Bearer ${apiKey}`, - "Content-Type": "application/json", - }, + headers: { "Authorization": `Bearer ${apiKey}`, "Content-Type": "application/json" }, body: JSON.stringify({ - model, + model: modelToTry, messages: [ { role: "system", content: systemPrompt }, { role: "user", content: userPrompt } @@ -124,6 +122,14 @@ ${JSON.stringify(products, null, 2)}`; }), }); + let response = await tryModel(model); + + // Fallback if configured model is deprecated/not found + if (!response.ok && response.status === 400 && model !== FALLBACK_MODEL) { + console.warn(`[batch-analyze] Model "${model}" returned 400, retrying with fallback ${FALLBACK_MODEL}`); + response = await tryModel(FALLBACK_MODEL); + } + if (response.status === 429) { const retryAfter = response.headers.get("Retry-After") || response.headers.get("x-ratelimit-reset-requests"); const waitSeconds = retryAfter ? parseInt(retryAfter, 10) : 60; diff --git a/src/features/grid/components/bulk-ai-analyzer.tsx b/src/features/grid/components/bulk-ai-analyzer.tsx index b315fdc..f1b8908 100644 --- a/src/features/grid/components/bulk-ai-analyzer.tsx +++ b/src/features/grid/components/bulk-ai-analyzer.tsx @@ -41,6 +41,10 @@ export function BulkAiAnalyzer() { const { rows } = useGridStore.getState(); isCancelledRef.current = false; + // Show immediate visual feedback before any async work + setIsAnalyzing(true); + setProgress({ current: 0, total: 0, message: "Chargement du contexte...", errors: 0 }); + // 1. Fetch AI Context for the Supplier let supplierContext = ""; if (supplierCode) { @@ -166,7 +170,6 @@ export function BulkAiAnalyzer() { // 2c. Context profiling en micro-batches asynchrones // Le score est déjà calculé par score-engine.ts (champ row.score sur chaque ProductRow). - setIsAnalyzing(true); setProgress({ current: 0, total: payloadsWithSales.length, message: "Calcul du contexte...", errors: 0 }); const SCORING_BATCH_SIZE = 25;