import React, { useState, useEffect } from 'react'; import { Download, Share2, Instagram, Youtube, Video, CheckCircle, AlertCircle, X, Loader2, Copy, Wand2, Type, Calendar, Clock, Languages } from 'lucide-react'; import { getApiUrl } from '../config'; import SubtitleModal from './SubtitleModal'; import HookModal from './HookModal'; import TranslateModal from './TranslateModal'; import { renderInBrowser } from '../lib/renderInBrowser'; export default function ResultCard({ clip, index, jobId, uploadPostKey, uploadUserId, geminiApiKey, elevenLabsKey, onPlay, onPause }) { const [showModal, setShowModal] = useState(false); const [showSubtitleModal, setShowSubtitleModal] = useState(false); const videoRef = React.useRef(null); const originalVideoUrl = getApiUrl(clip.video_url); // Never changes — used for Remotion previews const [currentVideoUrl, setCurrentVideoUrl] = useState(originalVideoUrl); const [platforms, setPlatforms] = useState({ tiktok: true, instagram: true, youtube: true }); const [postTitle, setPostTitle] = useState(""); const [postDescription, setPostDescription] = useState(""); const [isScheduling, setIsScheduling] = useState(false); const [scheduleDate, setScheduleDate] = useState(""); const [posting, setPosting] = useState(false); const [postResult, setPostResult] = useState(null); const [isEditing, setIsEditing] = useState(false); const [isSubtitling, setIsSubtitling] = useState(false); const [isHooking, setIsHooking] = useState(false); const [isTranslating, setIsTranslating] = useState(false); const [showHookModal, setShowHookModal] = useState(false); const [showTranslateModal, setShowTranslateModal] = useState(false); const [editError, setEditError] = useState(null); const [clipDuration, setClipDuration] = useState(clip.end && clip.start ? clip.end - clip.start : 30); // Accumulate Remotion layers across operations const [activeLayers, setActiveLayers] = useState({ subtitles: null, hook: null, effects: null }); // Fetch clip duration from transcript endpoint useEffect(() => { if (!jobId || index === undefined) return; fetch(getApiUrl(`/api/clip/${jobId}/${index}/transcript`)) .then(res => res.ok ? res.json() : null) .then(data => { if (data && data.durationSec) setClipDuration(data.durationSec); }) .catch(() => {}); }, [jobId, index]); // Initialize/Reset form when modal opens useEffect(() => { if (showModal) { setPostTitle(clip.video_title_for_youtube_short || "Viral Short"); setPostDescription(clip.video_description_for_instagram || clip.video_description_for_tiktok || ""); setIsScheduling(false); setScheduleDate(""); setPostResult(null); } }, [showModal, clip]); const handleAutoEdit = async () => { setIsEditing(true); setEditError(null); try { const apiKey = geminiApiKey || localStorage.getItem('gemini_key'); if (!apiKey) { throw new Error("Gemini API Key is missing. Please set it in Settings."); } // Try Remotion effects endpoint first const effectsRes = await fetch(getApiUrl('/api/effects/generate'), { method: 'POST', headers: { 'Content-Type': 'application/json', 'X-Gemini-Key': apiKey }, body: JSON.stringify({ job_id: jobId, clip_index: index, input_filename: currentVideoUrl.split('/').pop() }) }); if (effectsRes.ok) { const data = await effectsRes.json(); if (data.effects && data.effects.segments) { const newLayers = { ...activeLayers, effects: data.effects }; setActiveLayers(newLayers); const blobUrl = await renderInBrowser({ videoUrl: originalVideoUrl, durationInSeconds: clipDuration, subtitles: newLayers.subtitles, hook: newLayers.hook, effects: newLayers.effects, }); setCurrentVideoUrl(blobUrl); if (videoRef.current) videoRef.current.load(); return; } } // Fallback: legacy FFmpeg edit endpoint const res = await fetch(getApiUrl('/api/edit'), { method: 'POST', headers: { 'Content-Type': 'application/json', 'X-Gemini-Key': apiKey }, body: JSON.stringify({ job_id: jobId, clip_index: index, input_filename: currentVideoUrl.split('/').pop() }) }); if (!res.ok) { const errText = await res.text(); try { const jsonErr = JSON.parse(errText); throw new Error(jsonErr.detail || errText); } catch (e) { throw new Error(errText); } } const data = await res.json(); if (data.new_video_url) { setCurrentVideoUrl(getApiUrl(data.new_video_url)); if (videoRef.current) { videoRef.current.load(); } } } catch (e) { setEditError(e.message); setTimeout(() => setEditError(null), 5000); } finally { setIsEditing(false); } }; const handleSubtitle = async (options) => { setIsSubtitling(true); setEditError(null); try { if (options.remotion) { // Accumulate layer and render all layers together const newLayers = { ...activeLayers, subtitles: options.remotion }; setActiveLayers(newLayers); const blobUrl = await renderInBrowser({ videoUrl: originalVideoUrl, durationInSeconds: clipDuration, subtitles: newLayers.subtitles, hook: newLayers.hook, effects: newLayers.effects, }); setCurrentVideoUrl(blobUrl); if (videoRef.current) videoRef.current.load(); setShowSubtitleModal(false); return; } // Fallback: legacy FFmpeg const res = await fetch(getApiUrl('/api/subtitle'), { method: 'POST', headers: { 'Content-Type': 'application/json' }, body: JSON.stringify({ job_id: jobId, clip_index: index, position: options.position, font_size: options.fontSize, font_name: options.fontName, font_color: options.fontColor, border_color: options.borderColor, border_width: options.borderWidth, bg_color: options.bgColor, bg_opacity: options.bgOpacity, input_filename: currentVideoUrl.split('/').pop() }) }); if (!res.ok) throw new Error(await res.text()); const data = await res.json(); if (data.new_video_url) { setCurrentVideoUrl(getApiUrl(data.new_video_url)); if (videoRef.current) videoRef.current.load(); setShowSubtitleModal(false); } } catch (e) { setEditError(e.message); setTimeout(() => setEditError(null), 5000); } finally { setIsSubtitling(false); } }; const handleHook = async (hookData) => { setIsHooking(true); setEditError(null); try { if (hookData.remotion) { // Accumulate layer and render all layers together const newLayers = { ...activeLayers, hook: hookData.remotion }; setActiveLayers(newLayers); const blobUrl = await renderInBrowser({ videoUrl: originalVideoUrl, durationInSeconds: clipDuration, subtitles: newLayers.subtitles, hook: newLayers.hook, effects: newLayers.effects, }); setCurrentVideoUrl(blobUrl); if (videoRef.current) videoRef.current.load(); setShowHookModal(false); return; } // Fallback: legacy FFmpeg const payload = typeof hookData === 'string' ? { text: hookData, position: 'top', size: 'M' } : hookData; const res = await fetch(getApiUrl('/api/hook'), { method: 'POST', headers: { 'Content-Type': 'application/json' }, body: JSON.stringify({ job_id: jobId, clip_index: index, text: payload.text, position: payload.position, size: payload.size, input_filename: currentVideoUrl.split('/').pop() }) }); if (!res.ok) throw new Error(await res.text()); const data = await res.json(); if (data.new_video_url) { setCurrentVideoUrl(getApiUrl(data.new_video_url)); if (videoRef.current) videoRef.current.load(); setShowHookModal(false); } } catch (e) { setEditError(e.message); setTimeout(() => setEditError(null), 5000); } finally { setIsHooking(false); } }; const handleTranslate = async (options) => { console.log('[Translate] Starting translation with options:', options); setIsTranslating(true); setEditError(null); try { const apiKey = elevenLabsKey; console.log('[Translate] API Key available:', !!apiKey); if (!apiKey) { throw new Error("ElevenLabs API Key is missing. Please set it in Settings."); } const requestBody = { job_id: jobId, clip_index: index, target_language: options.targetLanguage, input_filename: currentVideoUrl.split('/').pop() }; console.log('[Translate] Request body:', requestBody); console.log('[Translate] Sending request to /api/translate'); const res = await fetch(getApiUrl('/api/translate'), { method: 'POST', headers: { 'Content-Type': 'application/json', 'X-ElevenLabs-Key': apiKey }, body: JSON.stringify(requestBody) }); console.log('[Translate] Response status:', res.status); if (!res.ok) { const errText = await res.text(); console.error('[Translate] Error response:', errText); try { const jsonErr = JSON.parse(errText); throw new Error(jsonErr.detail || errText); } catch (e) { if (e.message !== errText) throw e; throw new Error(errText); } } const data = await res.json(); console.log('[Translate] Success response:', data); if (data.new_video_url) { setCurrentVideoUrl(getApiUrl(data.new_video_url)); if (videoRef.current) { videoRef.current.load(); } setShowTranslateModal(false); } } catch (e) { console.error('[Translate] Exception:', e); setEditError(e.message); setTimeout(() => setEditError(null), 5000); } finally { setIsTranslating(false); } }; const handlePost = async () => { if (!uploadPostKey || !uploadUserId) { setPostResult({ success: false, msg: "Missing API Key or User ID." }); return; } const selectedPlatforms = Object.keys(platforms).filter(k => platforms[k]); if (selectedPlatforms.length === 0) { setPostResult({ success: false, msg: "Select at least one platform." }); return; } if (isScheduling && !scheduleDate) { setPostResult({ success: false, msg: "Please select a date and time." }); return; } setPosting(true); setPostResult(null); try { const payload = { job_id: jobId, clip_index: index, api_key: uploadPostKey, user_id: uploadUserId, platforms: selectedPlatforms, title: postTitle, description: postDescription }; if (isScheduling && scheduleDate) { // Convert to ISO-8601 payload.scheduled_date = new Date(scheduleDate).toISOString(); // Optional: pass timezone if needed, backend defaults to UTC or we can send user's timezone payload.timezone = Intl.DateTimeFormat().resolvedOptions().timeZone; } const res = await fetch(getApiUrl('/api/social/post'), { method: 'POST', headers: { 'Content-Type': 'application/json' }, body: JSON.stringify(payload) }); if (!res.ok) { const errText = await res.text(); try { const jsonErr = JSON.parse(errText); throw new Error(jsonErr.detail || errText); } catch (e) { throw new Error(errText); } } setPostResult({ success: true, msg: isScheduling ? "Scheduled successfully!" : "Posted successfully!" }); setTimeout(() => { setShowModal(false); setPostResult(null); }, 3000); } catch (e) { setPostResult({ success: false, msg: `Failed: ${e.message}` }); } finally { setPosting(false); } }; return (
{clip.video_title_for_youtube_short || "Viral Short Video"}
{clip.video_description_for_tiktok || clip.video_description_for_instagram}