fix(ia): afficher la cause réelle d'un échec de génération de texte

La route /api/ai/generate connaissait la cause exacte de l'échec
(configuration absente, clé invalide, crédits épuisés, modèle inconnu)
mais la remplaçait par un « Failed to generate text » en 500, et
l'interface affichait de son côté un message figé. Sans accès aux logs
du serveur, l'utilisateur n'avait donc aucun moyen de savoir quoi
corriger.

Le motif renvoyé par OpenRouter est désormais extrait, tronqué et
propagé jusqu'au bandeau d'erreur, avec un code HTTP qui distingue un
problème de configuration (400) d'un refus du service tiers (502).
La clé API n'apparaît à aucun moment dans ces messages.

Même traitement pour /api/reels/generate-text, qui souffrait du même
masquage.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_018uUPZ1GNpEkYB1dpXHYPek
This commit is contained in:
Claude committed 2026-08-12 07:49:23 +00:00
1 parent 0570088722
commit c69fe4e462
9 files changed
+92 -18

No files matched your search

+23
View File
@@ -20,6 +20,29 @@ export function handleUnauthorized(url: string) {
window.location.href = "/login";
}
/**
* Extrait un message lisible d'une erreur d'API.
* apiRequest lève des erreurs de la forme `500: {"error":"…"}` : sans ce
* décodage, l'interface ne peut afficher qu'un message générique alors que le
* serveur a renvoyé la cause exacte.
*/
export function getErrorMessage(error: unknown, fallback: string): string {
if (!(error instanceof Error) || !error.message) return fallback;
const withoutStatus = error.message.replace(/^\d{3}:\s*/, '').trim();
if (!withoutStatus) return fallback;
try {
const parsed = JSON.parse(withoutStatus);
const message = parsed?.error || parsed?.message;
if (typeof message === 'string' && message.trim()) return message.trim();
} catch {
// Corps non-JSON : on affiche le texte tel quel
}
return withoutStatus.startsWith('<') ? fallback : withoutStatus;
}
async function throwIfResNotOk(res: Response, url: string) {
if (!res.ok) {
if (res.status === 401) {
+3 -3
View File
@@ -32,7 +32,7 @@ import { Checkbox } from "@/components/ui/checkbox";
import { Select, SelectContent, SelectItem, SelectTrigger, SelectValue } from "@/components/ui/select";
import { Dialog, DialogContent } from "@/components/ui/dialog";
import { useToast } from "@/hooks/use-toast";
import { apiRequest, queryClient, handleUnauthorized } from "@/lib/queryClient";
import { apiRequest, queryClient, handleUnauthorized, getErrorMessage } from "@/lib/queryClient";
import type { SocialPage, Media, ScheduledPost } from "@shared/schema";
import { PreviewModal } from "@/components/preview-modal";
import { DateTimePicker } from "@/components/datetime-picker";
@@ -230,10 +230,10 @@ export default function NewPostMobile() {
});
queryClient.invalidateQueries({ queryKey: ['/api/ai/generations'] });
},
onError: () => {
onError: (error: unknown) => {
toast({
title: "Erreur",
description: "Impossible de générer le texte",
description: getErrorMessage(error, "Impossible de générer le texte"),
variant: "destructive",
});
},
+2 -2
View File
@@ -13,7 +13,7 @@ import Sidebar from "@/components/sidebar";
import { Checkbox } from "@/components/ui/checkbox";
import { DateTimePicker } from "@/components/datetime-picker";
import { useQuery, useMutation } from "@tanstack/react-query";
import { apiRequest, queryClient, handleUnauthorized } from "@/lib/queryClient";
import { apiRequest, queryClient, handleUnauthorized, getErrorMessage } from "@/lib/queryClient";
import type { Media, SocialPage } from "@shared/schema";
import { SiFacebook, SiTiktok } from "react-icons/si";
@@ -91,7 +91,7 @@ export default function MobileRemotionVideoPage() {
const generateTextMutation = useMutation({
mutationFn: async (text: string) => (await apiRequest('POST', '/api/reels/generate-text', { productInfo: text })).json(),
onSuccess: (data: any) => { setGeneratedVariants(data.variants || []); toast({ title: "Textes générés" }); },
onError: () => toast({ title: "Erreur IA", variant: "destructive" }),
onError: (error: unknown) => toast({ title: "Erreur IA", description: getErrorMessage(error, "Impossible de générer le texte"), variant: "destructive" }),
});
const handleFileChange = (e: React.ChangeEvent<HTMLInputElement>) => {
+3 -3
View File
@@ -31,7 +31,7 @@ import { Checkbox } from "@/components/ui/checkbox";
import { Select, SelectContent, SelectItem, SelectTrigger, SelectValue } from "@/components/ui/select";
import { Dialog, DialogContent } from "@/components/ui/dialog";
import { useToast } from "@/hooks/use-toast";
import { apiRequest, queryClient, handleUnauthorized } from "@/lib/queryClient";
import { apiRequest, queryClient, handleUnauthorized, getErrorMessage } from "@/lib/queryClient";
import { MediaThumbnail } from "@/components/media-thumbnail";
import type { SocialPage, Media, ScheduledPost } from "@shared/schema";
import { PreviewModal } from "@/components/preview-modal";
@@ -226,10 +226,10 @@ export default function NewPost() {
});
queryClient.invalidateQueries({ queryKey: ['/api/ai/generations'] });
},
onError: () => {
onError: (error: unknown) => {
toast({
title: "Erreur",
description: "Impossible de générer le texte",
description: getErrorMessage(error, "Impossible de générer le texte"),
variant: "destructive",
});
},
+3 -3
View File
@@ -25,7 +25,7 @@ import {
SelectValue,
} from "@/components/ui/select";
import { useToast } from "@/hooks/use-toast";
import { apiRequest, queryClient, handleUnauthorized } from "@/lib/queryClient";
import { apiRequest, queryClient, handleUnauthorized, getErrorMessage } from "@/lib/queryClient";
import type { SocialPage, Media } from "@shared/schema";
import { SiFacebook, SiTiktok } from "react-icons/si";
import { MediaThumbnail } from "@/components/media-thumbnail";
@@ -235,10 +235,10 @@ export default function NewReel() {
description: `${variants.length} variations créées`,
});
},
onError: () => {
onError: (error: unknown) => {
toast({
title: "Erreur",
description: "Impossible de générer le texte",
description: getErrorMessage(error, "Impossible de générer le texte"),
variant: "destructive",
});
},
+2 -2
View File
@@ -8,7 +8,7 @@ import { Textarea } from "@/components/ui/textarea";
import { UploadCloud, Video, Loader2, Check, Sparkles, Mic, Volume2, Music, Play, Pause, Send } from "lucide-react";
import { Slider } from "@/components/ui/slider";
import { useQuery, useMutation } from "@tanstack/react-query";
import { apiRequest, queryClient, handleUnauthorized } from "@/lib/queryClient";
import { apiRequest, queryClient, handleUnauthorized, getErrorMessage } from "@/lib/queryClient";
import { Label } from "@/components/ui/label";
import { Switch } from "@/components/ui/switch";
import { Checkbox } from "@/components/ui/checkbox";
@@ -93,7 +93,7 @@ export default function RemotionVideoPage() {
setGeneratedVariants(data.variants || []);
toast({ title: "Textes générés", description: "Cliquez pour appliquer." });
},
onError: () => toast({ title: "Erreur IA", variant: "destructive" }),
onError: (error: unknown) => toast({ title: "Erreur IA", description: getErrorMessage(error, "Impossible de générer le texte"), variant: "destructive" }),
});
const handleFileChange = (e: React.ChangeEvent<HTMLInputElement>) => {