import { useState, useCallback, useRef, useEffect } from "react"; import { useQuery, useMutation } from "@tanstack/react-query"; import { useLocation } from "wouter"; import { Send, Sparkles, Video, Music, Type, Calendar, Upload, Play, Pause, Volume2, ChevronRight, Loader2, Check, RefreshCw, ChevronLeft, Menu, Mic } from "lucide-react"; import { useDropzone } from "react-dropzone"; import Sidebar from "@/components/sidebar"; import { Button } from "@/components/ui/button"; import { Card, CardContent, CardHeader, CardTitle } from "@/components/ui/card"; import { Textarea } from "@/components/ui/textarea"; import { Switch } from "@/components/ui/switch"; import { Label } from "@/components/ui/label"; import { Slider } from "@/components/ui/slider"; import { useToast } from "@/hooks/use-toast"; import { VoicePicker, type VoiceSettings } from "@/components/reels/voice-picker"; import { DEFAULT_TTS_STYLE, DEFAULT_VOICE } from "@shared/voices"; import { apiRequest, queryClient, handleUnauthorized } from "@/lib/queryClient"; import type { SocialPage, Media } from "@shared/schema"; import { SiFacebook, SiTiktok } from "react-icons/si"; import { DateTimePicker } from "@/components/datetime-picker"; import { Sheet, SheetContent, SheetTrigger } from "@/components/ui/sheet"; import { Checkbox } from "@/components/ui/checkbox"; import { Select, SelectContent, SelectItem, SelectTrigger, SelectValue } from "@/components/ui/select"; interface MusicTrack { id: string; title: string; artist: string; albumName: string; duration: number; previewUrl: string; downloadUrl: string; imageUrl: string; license: string; } type Step = 'video' | 'music' | 'text' | 'publish'; export default function MobileNewReel() { const [, navigate] = useLocation(); const { toast } = useToast(); const audioRef = useRef(null); // État du workflow const [currentStep, setCurrentStep] = useState('video'); // État des données const [selectedVideoId, setSelectedVideoId] = useState(null); const [selectedVideo, setSelectedVideo] = useState(null); const [selectedTrack, setSelectedTrack] = useState(null); const [overlayText, setOverlayText] = useState(''); const [productInfo, setProductInfo] = useState(''); const [generatedVariants, setGeneratedVariants] = useState([]); const [selectedPages, setSelectedPages] = useState([]); const [scheduledDate, setScheduledDate] = useState(undefined); const [musicVolume, setMusicVolume] = useState([25]); // État TTS const [ttsEnabled, setTtsEnabled] = useState(false); const [voiceSettings, setVoiceSettings] = useState({ engine: 'gemini', voice: DEFAULT_VOICE.gemini, style: DEFAULT_TTS_STYLE, }); const { engine: ttsEngine, voice: ttsVoice, style: ttsStyle } = voiceSettings; // État audio preview const [isPlaying, setIsPlaying] = useState(null); // Désactivée par défaut : double le temps de rendu, utile seulement pour une vidéo tremblée const [stabilize, setStabilize] = useState(false); const [enableEndingEffect, setEnableEndingEffect] = useState(true); // TTS Sync state const [syncInfo, setSyncInfo] = useState<{ wordDuration: number; audioDuration: number; wordCount: number; isHealthy: boolean; warnings: string[]; } | null>(null); // Auto-calculate TTS sync useEffect(() => { if (!ttsEnabled || !overlayText.trim()) { setSyncInfo(null); return; } const timer = setTimeout(() => { apiRequest('POST', '/api/reels/sync-info', { text: overlayText, ttsEngine, ttsVoice, }) .then(r => r.json()) .then(data => setSyncInfo(data)) .catch(() => setSyncInfo(null)); }, 800); return () => clearTimeout(timer); }, [overlayText, ttsEnabled, ttsEngine, ttsVoice]); const { data: pages = [] } = useQuery({ queryKey: ['/api/pages'], }); // Destinations acceptant une vidéo verticale const facebookPages = pages.filter(p => p.platform === 'facebook'); const tiktokAccounts = pages.filter(p => p.platform === 'tiktok'); const renderTargetCheckbox = (page: SocialPage) => (
{ if (checked) setSelectedPages([...selectedPages, page.id]); else setSelectedPages(selectedPages.filter(id => id !== page.id)); }} />
); const { data: allMedia = [] } = useQuery({ queryKey: ['/api/media'], }); const videoList = allMedia.filter(m => m.type === 'video').slice(0, 12); // --- Added for Internal Audio Tracks --- const { data: internalTracksResponse = [], isLoading: internalTracksLoading } = useQuery({ queryKey: ['/api/audio-tracks'], }); const internalTracks: MusicTrack[] = (internalTracksResponse || []).map(t => ({ id: `internal_${t.id}`, title: t.title, artist: t.fileName || t.title, albumName: "Bibliothèque Interne", duration: t.duration || 0, previewUrl: t.url, downloadUrl: t.url, imageUrl: "", license: "Internal" })); // --- End Internal Tracks --- const uploadMutation = useMutation({ mutationFn: async (file: File) => { const formData = new FormData(); formData.append("file", file); const response = await fetch("/api/media/upload", { method: "POST", body: formData, credentials: "include", }); if (!response.ok) { if (response.status === 401) handleUnauthorized("/api/media/upload"); const errorText = await response.text(); throw new Error(`Upload failed: ${response.status} ${response.statusText} - ${errorText.substring(0, 100)}`); } return response.json(); }, onSuccess: (data) => { queryClient.invalidateQueries({ queryKey: ["/api/media"] }); setSelectedVideoId(data.id); setSelectedVideo(data); toast({ title: "Succès", description: "Vidéo téléchargée" }); }, onError: (error) => { console.error("Upload error details:", error); toast({ title: "Erreur Upload", description: error.message || "Échec inattendu", variant: "destructive", duration: 5000 }); }, }); const generateTextMutation = useMutation({ mutationFn: async (productInfoText: string) => { const response = await apiRequest('POST', '/api/reels/generate-text', { productInfo: productInfoText, }); return response.json(); }, onSuccess: (data: any) => { setGeneratedVariants(data.variants || []); toast({ title: "Généré", description: "Variations créées" }); }, }); const createReelMutation = useMutation({ mutationFn: async (data: any) => { const response = await apiRequest('POST', '/api/reels', data); return response.json(); }, onSuccess: async (data) => { await queryClient.invalidateQueries({ queryKey: ['/api/scheduled-posts'], refetchType: 'all' }); toast({ title: data.success ? "Reel créé !" : "Attention", description: data.success ? "Publication réussie" : "Certaines erreurs", }); navigate('/'); }, onError: (error: any) => { toast({ title: "Erreur", description: error.message, variant: "destructive" }); }, }); const onDrop = useCallback((acceptedFiles: File[]) => { if (acceptedFiles.length > 0 && acceptedFiles[0].type.startsWith('video/')) { uploadMutation.mutate(acceptedFiles[0]); } }, []); const { getRootProps, getInputProps, isDragActive, open } = useDropzone({ onDrop, accept: { "video/*": [".mp4", ".mov", ".webm"] }, noClick: true, noKeyboard: true, }); const togglePlayPreview = async (track: MusicTrack) => { if (isPlaying === track.id) { audioRef.current?.pause(); setIsPlaying(null); } else { if (audioRef.current) { audioRef.current.src = track.previewUrl; try { await audioRef.current.play(); setIsPlaying(track.id); } catch { setIsPlaying(null); } } } }; const handleCreateReel = () => { if (!selectedVideoId || selectedPages.length === 0) { toast({ title: "Erreur", description: "Vidéo et page requises", variant: "destructive" }); return; } createReelMutation.mutate({ videoMediaId: selectedVideoId, musicTrackId: selectedTrack?.id, overlayText: overlayText, description: overlayText, pageIds: selectedPages, scheduledFor: scheduledDate?.toISOString(), musicVolume: musicVolume[0] / 100, stabilize: stabilize, drawText: true, ttsEnabled, ttsEngine, ttsVoice, ttsStyle, enableEndingEffect, }); }; const steps: Step[] = ['video', 'music', 'text', 'publish']; const currentStepIndex = steps.indexOf(currentStep); const goNext = () => { if (currentStepIndex < steps.length - 1) setCurrentStep(steps[currentStepIndex + 1]); }; const goBack = () => { if (currentStepIndex > 0) setCurrentStep(steps[currentStepIndex - 1]); }; return (
{/* Header Mobile */}

Nouveau Reel

{currentStepIndex + 1}/{steps.length}
{/* VIDEO STEP */} {currentStep === 'video' && (
{selectedVideo ? (
) : (
{videoList.map((media) => (
{ setSelectedVideoId(media.id); setSelectedVideo(media); }} className={`relative aspect-[9/16] bg-black rounded-lg overflow-hidden border-2 transition-all ${selectedVideoId === media.id ? 'border-primary' : 'border-transparent'}`}>
))}
)}
Stabilisation Vidéo (Anti-tremblements)
Corrige les secousses (Recommandé main levée). Désactiver sur trépied.
setStabilize(!!checked)} />
{selectedVideo && ( )}
)} {/* MUSIC STEP */} {currentStep === 'music' && (
)} {/* TEXT STEP */} {currentStep === 'text' && (
Assistant IA