import { useState, useEffect } from "react"; import { Settings as SettingsIcon, Bell, Brain, Cloud, Image, Upload, X, Plug, Key } from "lucide-react"; import { useQuery, useMutation } from "@tanstack/react-query"; import Sidebar from "@/components/sidebar"; import TopBar from "@/components/topbar"; import { Card, CardContent, CardDescription, CardHeader, CardTitle } from "@/components/ui/card"; import { Label } from "@/components/ui/label"; import { Switch } from "@/components/ui/switch"; import { Input } from "@/components/ui/input"; import { Textarea } from "@/components/ui/textarea"; import { Button } from "@/components/ui/button"; import { useToast } from "@/hooks/use-toast"; import { apiRequest, queryClient } from "@/lib/queryClient"; import { ModelCombobox } from "@/components/model-combobox"; export default function SettingsMobile() { const [sidebarOpen, setSidebarOpen] = useState(false); const [notifications, setNotifications] = useState(true); const [autoPublish, setAutoPublish] = useState(true); const [cloudName, setCloudName] = useState(""); const [apiKey, setApiKey] = useState(""); const [apiSecret, setApiSecret] = useState(""); const [endpointUrl, setEndpointUrl] = useState(""); const [publicUrl, setPublicUrl] = useState(""); const [openrouterApiKey, setOpenrouterApiKey] = useState(""); const [openrouterModel, setOpenrouterModel] = useState("anthropic/claude-sonnet-4.5"); const [openrouterSystemPrompt, setOpenrouterSystemPrompt] = useState("Tu es un expert en marketing des réseaux sociaux. Génère 3 variations de textes engageants pour des publications Facebook et Instagram à partir des informations produit fournies. Chaque variation doit être unique, captivante et optimisée pour l'engagement."); const [logoFile, setLogoFile] = useState(null); const [logoPreview, setLogoPreview] = useState(null); const [externalApiKey, setExternalApiKey] = useState(""); const { toast } = useToast(); const { data: session } = useQuery<{ id: string; username: string; role: string }>({ queryKey: ["/api/auth/session"], retry: false, }); const isAdmin = session?.role === "admin"; const { data: cloudinaryConfig } = useQuery({ queryKey: ['/api/cloudinary/config'], }); const { data: openrouterConfig } = useQuery({ queryKey: ['/api/openrouter/config'], }); const { data: openrouterModels, isLoading: modelsLoading } = useQuery({ queryKey: ['/api/openrouter/models'], // Liste servie par un appel sortant vers OpenRouter : elle ne bouge pas d'une // heure à l'autre, inutile de la redemander à chaque retour sur l'onglet. staleTime: 60 * 60 * 1000, }); useEffect(() => { if (cloudinaryConfig) { setCloudName((cloudinaryConfig as any).cloudName || ""); setApiKey((cloudinaryConfig as any).apiKey || ""); setEndpointUrl((cloudinaryConfig as any).endpointUrl || ""); setPublicUrl((cloudinaryConfig as any).publicUrl || ""); if ((cloudinaryConfig as any).logoUrl) { setLogoPreview((cloudinaryConfig as any).logoUrl); } } }, [cloudinaryConfig]); useEffect(() => { if (openrouterConfig) { setOpenrouterModel((openrouterConfig as any).model || "anthropic/claude-sonnet-4.5"); setOpenrouterSystemPrompt((openrouterConfig as any).systemPrompt || "Tu es un expert en marketing des réseaux sociaux. Génère 3 variations de textes engageants pour des publications Facebook et Instagram à partir des informations produit fournies. Chaque variation doit être unique, captivante et optimisée pour l'engagement."); } }, [openrouterConfig]); const hasExistingConfig = !!openrouterConfig; const { data: externalApiConfig } = useQuery({ queryKey: ['/api/settings/external-api'], enabled: isAdmin, }); const hasExistingExternalApiConfig = !!(externalApiConfig as any)?.configured; const saveExternalApiMutation = useMutation({ mutationFn: () => apiRequest('POST', '/api/settings/external-api', { apiKey: externalApiKey }), onSuccess: () => { queryClient.invalidateQueries({ queryKey: ['/api/settings/external-api'] }); setExternalApiKey(""); toast({ title: "Clé API sauvegardée", description: "La clé API externe a été enregistrée" }); }, onError: () => { toast({ title: "Erreur", description: "Impossible de sauvegarder la clé API", variant: "destructive" }); }, }); const deleteExternalApiMutation = useMutation({ mutationFn: () => apiRequest('DELETE', '/api/settings/external-api', {}), onSuccess: () => { queryClient.invalidateQueries({ queryKey: ['/api/settings/external-api'] }); toast({ title: "Clé API supprimée", description: "L'accès à l'API externe a été révoqué" }); }, onError: () => { toast({ title: "Erreur", description: "Impossible de supprimer la clé API", variant: "destructive" }); }, }); const saveCloudinaryMutation = useMutation({ mutationFn: () => { const payload: any = { cloudName, endpointUrl: endpointUrl || undefined, publicUrl: publicUrl || undefined, }; if (apiKey && apiKey.trim() !== "") { payload.apiKey = apiKey; } if (apiSecret && apiSecret.trim() !== "") { payload.apiSecret = apiSecret; } return apiRequest('POST', '/api/cloudinary/config', payload); }, onSuccess: () => { queryClient.invalidateQueries({ queryKey: ['/api/cloudinary/config'] }); toast({ title: "Configuration sauvegardée", description: "Vos paramètres Cloudinary ont été enregistrés", }); setApiKey(""); setApiSecret(""); }, onError: () => { toast({ title: "Erreur", description: "Impossible de sauvegarder la configuration", variant: "destructive", }); }, }); const saveOpenrouterMutation = useMutation({ mutationFn: () => { const payload: any = { model: openrouterModel, systemPrompt: openrouterSystemPrompt, }; if (openrouterApiKey && openrouterApiKey.trim() !== "") { payload.apiKey = openrouterApiKey; } return apiRequest('POST', '/api/openrouter/config', payload); }, onSuccess: () => { queryClient.invalidateQueries({ queryKey: ['/api/openrouter/config'] }); toast({ title: "Configuration sauvegardée", description: "Vos paramètres OpenRouter ont été enregistrés", }); setOpenrouterApiKey(""); }, onError: () => { toast({ title: "Erreur", description: "Impossible de sauvegarder la configuration OpenRouter", variant: "destructive", }); }, }); const uploadLogoMutation = useMutation({ mutationFn: async (file: File) => { const formData = new FormData(); formData.append('logo', file); const response = await fetch('/api/cloudinary/logo', { method: 'POST', credentials: 'include', body: formData, }); if (!response.ok) { const error = await response.json(); throw new Error(error.error || 'Failed to upload logo'); } return response.json(); }, onSuccess: () => { queryClient.invalidateQueries({ queryKey: ['/api/cloudinary/config'] }); toast({ title: "Logo uploadé", description: "Votre logo a été enregistré avec succès", }); setLogoFile(null); }, onError: (error: Error) => { toast({ title: "Erreur", description: error.message || "Impossible d'uploader le logo", variant: "destructive", }); }, }); const deleteLogoMutation = useMutation({ mutationFn: () => apiRequest('DELETE', '/api/cloudinary/logo', {}), onSuccess: () => { queryClient.invalidateQueries({ queryKey: ['/api/cloudinary/config'] }); setLogoPreview(null); setLogoFile(null); toast({ title: "Logo supprimé", description: "Votre logo a été supprimé avec succès", }); }, onError: () => { toast({ title: "Erreur", description: "Impossible de supprimer le logo", variant: "destructive", }); }, }); const handleLogoChange = (e: React.ChangeEvent) => { const file = e.target.files?.[0]; if (file) { if (!file.type.startsWith('image/')) { toast({ title: "Erreur", description: "Veuillez sélectionner une image", variant: "destructive", }); return; } setLogoFile(file); const reader = new FileReader(); reader.onloadend = () => { setLogoPreview(reader.result as string); }; reader.readAsDataURL(file); } }; return (
{sidebarOpen && (
setSidebarOpen(false)} /> )}
setSidebarOpen(false)} />
setSidebarOpen(!sidebarOpen)} /> {/* MOBILE: Single column layout */}

Paramètres

Configurez votre application

{/* Notifications */} Notifications Gérez vos préférences de notification

Recevez des notifications pour les publications

Publier automatiquement à l'heure programmée

{/* OpenRouter */} Configuration OpenRouter (IA) Configurez OpenRouter pour la génération de texte avec l'IA
setOpenrouterApiKey(e.target.value)} placeholder={hasExistingConfig ? "••••••••••••••••" : "sk-or-v1-..."} className="min-h-[48px]" data-testid="input-openrouter-api-key" />

{hasExistingConfig ? ( <> ✓ Clé API configurée - Laissez vide pour garder la clé actuelle ) : ( <> Obtenez votre clé API sur{" "} openrouter.ai )}

Choisissez le modèle d'IA pour générer vos publications