AI Social Media Scheduler

import React, { useState, useEffect, useCallback, useMemo } from 'react'; import { initializeApp } from 'firebase/app'; import { getAuth, signInAnonymously, signInWithCustomToken, onAuthStateChanged } from 'firebase/auth'; import { getFirestore, collection, query, orderBy, onSnapshot, addDoc, Timestamp } from 'firebase/firestore'; import { Upload, Sparkles, Calendar, Zap, RefreshCw, X, CheckCircle } from 'lucide-react'; // --- Global Variable Access (Canvas Environment) --- const appId = typeof __app_id !== 'undefined' ? __app_id : 'default-app-id'; const firebaseConfig = typeof __firebase_config !== 'undefined' ? JSON.parse(__firebase_config) : {}; const initialAuthToken = typeof __initial_auth_token !== 'undefined' ? __initial_auth_token : null; const apiKey = ""; // API Key is provided by the canvas environment if needed // Utility function for exponential backoff during API calls const withExponentialBackoff = async (fn, maxRetries = 5, delay = 1000) => { for (let i = 0; i < maxRetries; i++) { try { return await fn(); } catch (error) { if (i === maxRetries - 1) throw error; await new Promise(resolve => setTimeout(resolve, delay * (2 ** i) + Math.random() * 100)); } } }; // Converts a File object to a Base64 string for the Gemini API const fileToBase64 = (file) => { return new Promise((resolve, reject) => { const reader = new FileReader(); reader.onloadend = () => { // Extracts the base64 data part after the comma (e.g., "data:image/png;base64,DATA...") const base64String = reader.result.split(',')[1]; resolve(base64String); }; reader.onerror = error => reject(error); reader.readAsDataURL(file); }); }; // --- Firestore Path Utility --- const getPublicCollectionPath = (collectionName) => { return `artifacts/${appId}/public/data/${collectionName}`; }; // --- Main App Component --- const App = () => { const [db, setDb] = useState(null); const [auth, setAuth] = useState(null); const [userId, setUserId] = useState(null); const [isAuthReady, setIsAuthReady] = useState(false); const [error, setError] = useState(null); // Scheduling State const [file, setFile] = useState(null); const [base64File, setBase64File] = useState(null); const [platform, setPlatform] = useState('Instagram'); const [scheduleTime, setScheduleTime] = useState(''); const [isGenerating, setIsGenerating] = useState(false); const [isScheduling, setIsScheduling] = useState(false); const [schedules, setSchedules] = useState([]); // Generated Content State const [generatedTitle, setGeneratedTitle] = useState(''); const [generatedDescription, setGeneratedDescription] = useState(''); const [generatedHashtags, setGeneratedHashtags] = useState([]); // --- 1. Firebase Initialization and Authentication --- useEffect(() => { if (Object.keys(firebaseConfig).length === 0) { setError('Firebase configuration is missing.'); return; } try { const firebaseApp = initializeApp(firebaseConfig); const firestoreDb = getFirestore(firebaseApp); const firebaseAuth = getAuth(firebaseApp); setDb(firestoreDb); setAuth(firebaseAuth); // Authentication Listener const unsubscribe = onAuthStateChanged(firebaseAuth, async (user) => { if (user) { setUserId(user.uid); } else { // Sign in anonymously if no token is available try { if (initialAuthToken) { await signInWithCustomToken(firebaseAuth, initialAuthToken); } else { await signInAnonymously(firebaseAuth); } } catch (e) { console.error("Authentication failed:", e); setError("Failed to authenticate user."); } } setIsAuthReady(true); }); return () => unsubscribe(); } catch (e) { console.error("Firebase initialization failed:", e); setError("Firebase failed to initialize. Check config."); } }, []); // --- 2. Real-time Data Fetching (Schedules) --- useEffect(() => { if (!isAuthReady || !db) return; const path = getPublicCollectionPath('schedules'); const q = query(collection(db, path), orderBy('scheduleTime', 'asc')); const unsubscribe = onSnapshot(q, (snapshot) => { const fetchedSchedules = snapshot.docs.map(doc => ({ id: doc.id, ...doc.data(), scheduleTime: doc.data().scheduleTime.toDate().toISOString(), // Convert Timestamp to string })); setSchedules(fetchedSchedules); }, (err) => { console.error("Error fetching schedules:", err); setError("Could not load schedules from the database."); }); return () => unsubscribe(); }, [isAuthReady, db]); // --- 3. Handlers for UI and File Processing --- const handleFileChange = async (event) => { const selectedFile = event.target.files[0]; if (selectedFile) { setFile(selectedFile); setGeneratedTitle(''); setGeneratedDescription(''); setGeneratedHashtags([]); try { const base64 = await fileToBase64(selectedFile); setBase64File(base64); } catch (e) { console.error("Error converting file to Base64:", e); setError("Failed to process file. Please try again."); setFile(null); setBase64File(null); } } }; const handleReset = () => { setFile(null); setBase64File(null); setGeneratedTitle(''); setGeneratedDescription(''); setGeneratedHashtags([]); setPlatform('Instagram'); setScheduleTime(''); // Reset file input value document.getElementById('file-upload-input').value = ''; }; // --- 4. Gemini API Call for Content Generation --- const generateContent = useCallback(async () => { if (!file || !base64File) { setError("Please upload a file first."); return; } setIsGenerating(true); setError(null); // Determine MIME type based on file extension or type let mimeType = file.type || (file.name.endsWith('.mp4') ? 'video/mp4' : (file.name.endsWith('.jpg') || file.name.endsWith('.jpeg') ? 'image/jpeg' : 'image/png')); // Use a general image/video prompt for the LLM const userPrompt = `Analyze the uploaded media (image or video) and generate a compelling, SEO-friendly title, a detailed description (max 200 characters), and 8 relevant hashtags for social media scheduling, specifically targeting the ${platform} platform. Return the response as a single JSON object.`; const systemPrompt = "You are a world-class social media content strategist and SEO specialist. Your task is to generate highly engaging, concise, and platform-optimized text content based on the provided media. Ensure the description is engaging and the title is click-worthy. The output must be valid JSON matching the provided schema."; const payload = { contents: [ { role: "user", parts: [ { text: userPrompt }, { inlineData: { mimeType: mimeType, data: base64File } } ] } ], systemInstruction: { parts: [{ text: systemPrompt }] }, generationConfig: { responseMimeType: "application/json", responseSchema: { type: "OBJECT", properties: { "title": { "type": "STRING", "description": "A compelling, SEO-friendly title." }, "description": { "type": "STRING", "description": "A concise, engaging description, max 200 characters." }, "hashtags": { "type": "ARRAY", "description": "An array of 8 highly relevant and trending hashtags.", "items": { "type": "STRING" } } }, "required": ["title", "description", "hashtags"] } } }; try { const apiUrl = `https://generativelanguage.googleapis.com/v1beta/models/gemini-2.5-flash-preview-09-2025:generateContent?key=${apiKey}`; const response = await withExponentialBackoff(() => fetch(apiUrl, { method: 'POST', headers: { 'Content-Type': 'application/json' }, body: JSON.stringify(payload) })); if (!response.ok) { const errText = await response.text(); throw new Error(`API call failed: ${response.status} - ${errText}`); } const result = await response.json(); const jsonText = result.candidates?.[0]?.content?.parts?.[0]?.text; if (jsonText) { const generatedContent = JSON.parse(jsonText); setGeneratedTitle(generatedContent.title || ''); setGeneratedDescription(generatedContent.description || ''); // Ensure hashtags are strings and clean them up (remove # if present) const cleanedHashtags = (generatedContent.hashtags || []) .map(tag => typeof tag === 'string' ? tag.replace(/#/g, '') : '') .filter(tag => tag.length > 0) .slice(0, 8); // Limit to 8 setGeneratedHashtags(cleanedHashtags); } else { throw new Error("Invalid response structure from the content generation model."); } } catch (e) { console.error("Content generation error:", e); setError(`Failed to generate content: ${e.message}`); } finally { setIsGenerating(false); } }, [file, base64File, platform]); // --- 5. Scheduling to Firestore --- const scheduleContent = async () => { if (!db || !userId) { setError("Database or user ID not available. Please wait for authentication."); return; } if (!generatedTitle || !scheduleTime) { setError("Please generate content and set a schedule date/time first."); return; } setIsScheduling(true); setError(null); try { const scheduleTimestamp = Timestamp.fromDate(new Date(scheduleTime)); const scheduleData = { userId: userId, platform: platform, scheduleTime: scheduleTimestamp, title: generatedTitle, description: generatedDescription, hashtags: generatedHashtags, createdAt: Timestamp.now(), }; const path = getPublicCollectionPath('schedules'); await addDoc(collection(db, path), scheduleData); // Clear content after successful scheduling handleReset(); } catch (e) { console.error("Error scheduling content:", e); setError(`Failed to schedule content: ${e.message}`); } finally { setIsScheduling(false); } }; // --- Rendering Logic --- const canGenerate = !!file && !isGenerating; const canSchedule = generatedTitle && scheduleTime && !isScheduling; const isReady = isAuthReady && db; if (!isReady) { return (

Initializing Social Scheduler...

); } const platformOptions = ['Instagram', 'YouTube', 'TikTok', 'Facebook', 'X (Twitter)', 'LinkedIn']; const getMimeTypeIcon = (fileName) => { if (!fileName) return "File"; const lowerName = fileName.toLowerCase(); if (lowerName.endsWith('.mp4') || lowerName.endsWith('.mov') || lowerName.endsWith('.avi')) { return 'Video'; } if (lowerName.endsWith('.jpg') || lowerName.endsWith('.jpeg') || lowerName.endsWith('.png') || lowerName.endsWith('.gif')) { return 'Image'; } return 'Media'; }; const formatDate = (isoString) => { const date = new Date(isoString); return date.toLocaleDateString('en-US', { year: 'numeric', month: 'short', day: 'numeric', hour: '2-digit', minute: '2-digit' }); }; const globalStyles = ` @import url('https://fonts.googleapis.com/css2?family=Inter:wght@400;600;700&display=swap'); body { font-family: 'Inter', sans-serif; } `; return (
{/* Fix: Use dangerouslySetInnerHTML for global styles */}