import { useState, useEffect } from "react"; import { Settings as SettingsIcon, Bell, Key, Shield, Cloud, Brain, Image, Upload, X, Video, Plug, Mic } 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"; import { SiTiktok } from "react-icons/si"; export default function Settings() { 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 [ffmpegApiUrl, setFfmpegApiUrl] = useState(""); const [ffmpegApiKey, setFfmpegApiKey] = useState(""); const [externalApiKey, setExternalApiKey] = useState(""); const [geminiApiKey, setGeminiApiKey] = useState(""); const [tiktokClientKey, setTiktokClientKey] = useState(""); const [tiktokClientSecret, setTiktokClientSecret] = 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'], }); const { data: ffmpegConfig } = useQuery({ queryKey: ['/api/ffmpeg/config'], }); const { data: tiktokConfig } = useQuery<{ configured: boolean; clientKey: string; hasClientSecret: boolean; redirectUri: string; }>({ queryKey: ['/api/tiktok/config'], }); const { data: externalApiConfig } = useQuery({ queryKey: ['/api/settings/external-api'], enabled: isAdmin, }); const { data: geminiConfig } = useQuery({ queryKey: ['/api/settings/gemini'], enabled: isAdmin, }); 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]); useEffect(() => { if (ffmpegConfig) { setFfmpegApiUrl((ffmpegConfig as any).apiUrl || ""); } }, [ffmpegConfig]); useEffect(() => { if (tiktokConfig) { // Le client secret n'est jamais renvoyé par l'API : il reste vide setTiktokClientKey(tiktokConfig.clientKey || ""); } }, [tiktokConfig]); useEffect(() => { if (geminiConfig) { // Don't pre-fill - user must enter key manually } }, [geminiConfig]); // Vérifier si une config OpenRouter existe déjà const hasExistingConfig = !!openrouterConfig; const hasExistingFfmpegConfig = !!ffmpegConfig; const hasExistingGeminiConfig = !!(geminiConfig as any)?.configured; const saveCloudinaryMutation = useMutation({ mutationFn: () => { const payload: any = { cloudName, endpointUrl: endpointUrl || undefined, publicUrl: publicUrl || undefined, }; // N'inclure apiKey et apiSecret que s'ils ne sont pas vides 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(""); // Clear the API key after saving setApiSecret(""); // Clear the secret after saving }, onError: () => { toast({ title: "Erreur", description: "Impossible de sauvegarder la configuration", variant: "destructive", }); }, }); const saveOpenrouterMutation = useMutation({ mutationFn: () => { const payload: any = { model: openrouterModel, systemPrompt: openrouterSystemPrompt, }; // N'inclure apiKey que s'il n'est pas vide 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(""); // Clear the API key after saving }, onError: () => { toast({ title: "Erreur", description: "Impossible de sauvegarder la configuration OpenRouter", variant: "destructive", }); }, }); const saveGeminiMutation = useMutation({ mutationFn: () => { const payload: any = {}; if (geminiApiKey && geminiApiKey.trim() !== "") { payload.apiKey = geminiApiKey; } return apiRequest('POST', '/api/settings/gemini', payload); }, onSuccess: () => { queryClient.invalidateQueries({ queryKey: ['/api/settings/gemini'] }); toast({ title: "Configuration sauvegardée", description: "Votre clé API Gemini a été enregistrée", }); setGeminiApiKey(""); }, onError: () => { toast({ title: "Erreur", description: "Impossible de sauvegarder la configuration Gemini", 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 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 saveTiktokMutation = useMutation({ mutationFn: () => { const payload: any = { clientKey: tiktokClientKey.trim() }; // Le secret n'est envoyé que s'il est saisi : sinon on conserve l'existant if (tiktokClientSecret.trim() !== "") { payload.clientSecret = tiktokClientSecret.trim(); } return apiRequest('PUT', '/api/tiktok/config', payload); }, onSuccess: () => { queryClient.invalidateQueries({ queryKey: ['/api/tiktok/config'] }); setTiktokClientSecret(""); toast({ title: "Configuration TikTok enregistrée", description: "Vous pouvez maintenant connecter les comptes TikTok de vos magasins" }); }, onError: async (error: any) => { toast({ title: "Erreur", description: error?.message || "Impossible d'enregistrer la configuration TikTok", 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 saveFfmpegMutation = useMutation({ mutationFn: () => { const payload: any = { apiUrl: ffmpegApiUrl, }; // N'inclure apiKey que s'il n'est pas vide if (ffmpegApiKey && ffmpegApiKey.trim() !== "") { payload.apiKey = ffmpegApiKey; } return apiRequest('POST', '/api/ffmpeg/config', payload); }, onSuccess: () => { queryClient.invalidateQueries({ queryKey: ['/api/ffmpeg/config'] }); toast({ title: "Configuration sauvegardée", description: "Vos paramètres FFmpeg ont été enregistrés", }); setFfmpegApiKey(""); // Clear the API key after saving }, onError: () => { toast({ title: "Erreur", description: "Impossible de sauvegarder la configuration FFmpeg", 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)} />

Paramètres

Configurez votre application

Notifications Gérez vos préférences de notification

Recevez des notifications pour les publications

Publier automatiquement à l'heure programmée

Configuration OpenRouter (IA) Configurez OpenRouter pour la génération de texte avec l'IA
setOpenrouterApiKey(e.target.value)} placeholder={hasExistingConfig ? "••••••••••••••••" : "sk-or-v1-..."} 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