import React, { useState, useEffect } from 'react'; import { Play, Sparkles, Copy, AlertCircle, Loader2, CheckCircle2, Download, RotateCcw, ImagePlus, X, History, Trash2, Image as ImageIcon, UploadCloud, LogOut, Settings, Key } from 'lucide-react'; import { initializeApp } from 'firebase/app'; import { getAuth, signInAnonymously, signInWithCustomToken, onAuthStateChanged, GoogleAuthProvider, signInWithPopup, signOut } from 'firebase/auth'; import { getFirestore, collection, addDoc, onSnapshot, deleteDoc, doc, setDoc, getDoc } from 'firebase/firestore'; import { getStorage, ref, uploadBytes, getDownloadURL, deleteObject } from 'firebase/storage'; // --- Firebase Configuration --- const firebaseConfig = { apiKey: "AIzaSyCpUj6JELHpihhrIZIAZMg_SAJ3IZEv5ag", authDomain: "geminfinity-6c318.firebaseapp.com", projectId: "geminfinity-6c318", storageBucket: "geminfinity-6c318.firebasestorage.app", messagingSenderId: "888799027888", appId: "1:888799027888:web:e0e2f1bcd0ad24e9201175", measurementId: "G-DSS9FWB2JX" }; // Initialize Firebase const app = initializeApp(firebaseConfig); const auth = getAuth(app); const googleProvider = new GoogleAuthProvider(); const db = getFirestore(app); const storage = getStorage(app); const artifactAppId = typeof __app_id !== 'undefined' ? __app_id : 'gem-infinity-core'; // --- Data Constants --- const narrativeStylesList = [ { num: 0, name: "ไม่เลือก", icon: "⚪", desc: "ไม่ระบุสไตล์การเล่าเรื่อง", personality: '' }, { num: 1, name: "ขายตรง ดุดัน", icon: "👊", desc: "ดุดัน เร่งด่วน ฟาดด้วยโปรโมชั่น", personality: '[Hard Sell] 👊: ดุดัน เร่งด่วน ฟาดด้วยโปรโมชั่น "ซื้อเดี๋ยวนี้ พรุ่งนี้ราคาขึ้น! อย่ามัวแต่คิด!"' }, { num: 2, name: "ขายเนียน ป้ายยา", icon: "✨", desc: "เพื่อนสาวเม้าท์มอย เนียนๆ ป้ายยา", personality: '[Soft Sell] ✨: เพื่อนสาวเม้าท์มอย เนียนๆ ป้ายยาแนวกันเอง "แก... ฉันไปเจออันนี้มา คือมันดีต่อใจมาก"' }, { num: 3, name: "สายแกะกล่อง", icon: "📦", desc: "ตื่นเต้นเหมือนได้ของใหม่ บรรยายละเอียดยิบ", personality: '[The Unboxer] 📦: ตื่นเต้นเหมือนได้ของใหม่ บรรยาย Texture และสัมผัสละเอียดยิบ' }, { num: 6, name: "ฮีโร่ปะทะวายร้าย", icon: "⚔️", desc: "ปีศาจ (ปัญหา) ปะทะ ฮีโร่ (สินค้า)", personality: '[Villain vs Hero] ⚔️: ปีศาจ (ปัญหา) ปะทะ ฮีโร่ (สินค้า) สู้กันแบบดุเดือด' }, { num: 7, name: "ด่าด้วยความหวังดี", icon: "🤬", desc: "ปากแจ๋วแต่หวังดี ด่ากราดเพื่อเตือนสติ", personality: '[Tough Love] 🤬: ปากแจ๋วแต่หวังดี ด่ากราดเพื่อเตือนสติ "หน้าพังขนาดนี้ยังไม่ดูแลอีกเหรอ! ตื่น!"' }, { num: 12, name: "สงครามอวัยวะ", icon: "🧠", desc: "อวัยวะตีกันเอง เช่น ตับประท้วงสมอง", personality: '[Organ War] 🧠: อวัยวะตีกันเอง (เช่น ตับประท้วงสมองที่สั่งให้ดื่มแอลกอฮอล์)' }, { num: 21, name: "สายช็อตฟีล", icon: "🛑", desc: "สายช็อตฟีล นักดับฝัน ทวนกระแส", personality: '[The De-influencer] 🛑: ทวนกระแส ไม่ป้ายยาแต่ "ห้ามซื้อ!" เปิดด้วยการด่าหรือบอกข้อเสียของฮิตๆ แล้วหักมุมมาขายของตัวเองที่ดีกว่า' }, { num: 25, name: "ตัวมัม ตัวมารดา", icon: "💅", desc: "ตัวมัม ศูนย์กลางจักรวาล POV สุดเริ่ด", personality: '[The Main Character] 💅: เล่าผ่าน POV ของคนที่มั่นใจสุดๆ ชีวิตดีเริ่ด เชิดๆ หยิ่งๆ สินค้าคือพร็อพที่ขาดไม่ได้ในชีวิตสุดเพอร์เฟกต์' }, { num: 39, name: "สายแซะ เสียดสี", icon: "🎭", desc: "สายแซะสังคม หยิบกระแสข่าวมาเสียดสี", personality: '[The Satirist] 🎭: เกาะกระแส Real-time เอามาล้อเลียนขำๆ แล้ววกเข้าสินค้าแบบหน้าดื้อๆ' } ]; const moodKeywordsList = [ { id: "dark_gritty", name: "มืดหม่น ดิบเถื่อน", icon: "🌑", desc: "มืดหม่น ดิบเถื่อน" }, { id: "cinematic_standard", name: "มาตรฐานภาพยนตร์", icon: "🎬", desc: "มาตรฐานภาพยนตร์ แสงเงาสมบูรณ์" }, { id: "bright_airy", name: "สว่าง สดใส", icon: "☀️", desc: "สว่าง สดใส อากาศถ่ายเท" }, { id: "action_explosive", name: "แอ็คชั่น ระเบิดภูเขา", icon: "💥", desc: "ตื่นเต้น เร้าใจ แอ็คชั่น" }, { id: "cyberpunk_neon", name: "ไซเบอร์พังก์ นีออน", icon: "🌆", desc: "นีออนหลากสี โลกอนาคต" }, { id: "mutelu_mystical", name: "สายมู ลึกลับ", icon: "🕉️", desc: "มนตร์ขลัง แสงเทียนสลัว ควันธูป สายมูเตลู" } ]; const visualStylesList = [ { id: "disney_pixar_3d", name: "อนิเมชั่น 3D แบบพิกซาร์", icon: "🏰", desc: "3D ตาโตน่ารักแบบการ์ตูนฝรั่ง", category: "กระแสหลัก", prompt: "3D animated CGI feature film character, pixar animation style, expressive eyes, Unreal Engine 5 render" }, { id: "real_cinematic", name: "คนจริง ภาพยนตร์ฮอลลีวูด", icon: "🎬", desc: "คนจริง แสงสีอลังการเหมือนหนังฮอลลีวูด", category: "กระแสหลัก", prompt: "photorealistic cinematic film still, Hollywood movie quality, dramatic lighting" }, { id: "ghibli", name: "อนิเมะญี่ปุ่น สตูดิโอจิบลิ", icon: "🌿", desc: "ลายเส้นสีน้ำ อะนิเมะญี่ปุ่นยุคเก่า อบอุ่น", category: "กระแสหลัก", prompt: "Studio Ghibli anime style, watercolor painting, hand-drawn animation" }, { id: "claymation", name: "สตอปโมชัน ดินน้ำมัน", icon: "🎨", desc: "ดินน้ำมัน แบบ Pingu", category: "งานคราฟต์", prompt: "claymation stop-motion style, clay sculpted characters, plasticine material" }, { id: "cyberpunk_neon_vis", name: "โลกอนาคต ไซเบอร์พังก์", icon: "🌃", desc: "แสงนีออนจัดจ้าน ล้ำยุค", category: "ดิจิทัลอาร์ต", prompt: "cyberpunk neon style, vibrant neon lights, dark futuristic cityscape" }, { id: "hyper_realistic", name: "ภาพเสมือนจริงขั้นสุด", icon: "👤", desc: "ภาพคนสมจริงตาแตก ชัดทุกรูขุมขน", category: "3D ขั้นสูง", prompt: "hyper-realistic portrait photography, extreme detail, 8K resolution" } ]; const visualCategories = [...new Set(visualStylesList.map(v => v.category))]; const categoryList = [ { id: "คน", name: "คน / บุคคล / อาชีพ" }, { id: "อวัยวะ", name: "อวัยวะภายใน / ร่างกาย" }, { id: "สัตว์", name: "สัตว์เลี้ยง / สัตว์ประหลาด" }, { id: "สกินแคร์", name: "เครื่องสำอาง / สกินแคร์" }, { id: "สิ่งของ", name: "สิ่งของเครื่องใช้ทั่วไป" }, { id: "อาหาร", name: "อาหาร / เครื่องดื่ม" }, { id: "ผลไม้", name: "ผลไม้ / ผัก" }, { id: "ยานพาหนะ", name: "ยานพาหนะ / อะไหล่" }, { id: "ไอที", name: "เครื่องใช้ไฟฟ้า / ไอที" }, { id: "สถานที่", name: "สถานที่ / อาคาร" }, { id: "ต้นไม้", name: "พืช / ต้นไม้ / ดอกไม้" }, { id: "นามธรรม", name: "นามธรรม (เช่น ความรัก, เวลา)" } ]; // --- Utility Functions --- const generateThumbnail = (file) => { return new Promise((resolve) => { const reader = new FileReader(); reader.onload = (e) => { const img = new Image(); img.onload = () => { const canvas = document.createElement('canvas'); const MAX_DIM = 250; let width = img.width; let height = img.height; if (width > height) { if (width > MAX_DIM) { height *= MAX_DIM / width; width = MAX_DIM; } } else { if (height > MAX_DIM) { width *= MAX_DIM / height; height = MAX_DIM; } } canvas.width = width; canvas.height = height; const ctx = canvas.getContext('2d'); ctx.drawImage(img, 0, 0, width, height); resolve(canvas.toDataURL('image/jpeg', 0.8)); }; img.src = e.target.result; }; reader.readAsDataURL(file); }); }; export default function App() { const defaultFormState = { category: 'อวัยวะ', name: 'ตับ', enemy: 'แอลกอฮอล์และไขมัน', product: 'อาหารเสริม Detox Pro', emotion: 'ปากร้าย ดุดัน แต่รักเจ้าของ', narrativeStyles: [7, 12], mood: 'dark_gritty', visualStyle: 'disney_pixar_3d', extraDetails: '', sceneCount: 5, dialogueType: 'normal' // เพิ่มค่าเริ่มต้นของรูปแบบเสียง }; // State const [user, setUser] = useState(null); const [formData, setFormData] = useState(defaultFormState); const [isLoading, setIsLoading] = useState(false); const [result, setResult] = useState(''); const [error, setError] = useState(''); const [copied, setCopied] = useState(false); // Custom API Key Settings const [customApiKey, setCustomApiKey] = useState(''); const [tempApiKey, setTempApiKey] = useState(''); const [showSettings, setShowSettings] = useState(false); const systemApiKey = ""; // Default environment key if any // Image Selection State (Using Gallery Docs) const [charImage, setCharImage] = useState(null); const [prodImage, setProdImage] = useState(null); // Gallery & History State const [historyDocs, setHistoryDocs] = useState([]); const [showHistory, setShowHistory] = useState(false); const [galleryImages, setGalleryImages] = useState([]); const [showGalleryModal, setShowGalleryModal] = useState(false); // false, 'char', 'prod' const [isUploadingGallery, setIsUploadingGallery] = useState(false); // --- Firebase Auth & Firestore Listeners --- useEffect(() => { const initAuth = async () => { try { if (typeof __initial_auth_token !== 'undefined' && __initial_auth_token) { try { await signInWithCustomToken(auth, __initial_auth_token); } catch (tokenErr) { await signInAnonymously(auth); } } else { await signInAnonymously(auth); } } catch (err) { console.error("Auth error:", err); } }; initAuth(); const unsubscribe = onAuthStateChanged(auth, setUser); return () => unsubscribe(); }, []); useEffect(() => { if (!user) return; // Listen to user's saved scripts history const colHistoryRef = collection(db, 'artifacts', artifactAppId, 'users', user.uid, 'scripts'); const unsubscribeHistory = onSnapshot(colHistoryRef, (snapshot) => { const docs = snapshot.docs.map(doc => ({ id: doc.id, ...doc.data() })); docs.sort((a, b) => (b.createdAt || 0) - (a.createdAt || 0)); setHistoryDocs(docs); }, (err) => { console.error("Firestore history error:", err); }); // Listen to user's image gallery const colGalleryRef = collection(db, 'artifacts', artifactAppId, 'users', user.uid, 'gallery'); const unsubscribeGallery = onSnapshot(colGalleryRef, (snapshot) => { const docs = snapshot.docs.map(doc => ({ id: doc.id, ...doc.data() })); docs.sort((a, b) => (b.createdAt || 0) - (a.createdAt || 0)); setGalleryImages(docs); }, (err) => { console.error("Firestore gallery error:", err); }); // Fetch Custom API Key if user is authenticated (not anonymous) if (!user.isAnonymous) { const fetchApiKey = async () => { try { const docRef = doc(db, 'artifacts', artifactAppId, 'users', user.uid, 'settings', 'profile'); const docSnap = await getDoc(docRef); if (docSnap.exists() && docSnap.data().apiKey) { setCustomApiKey(docSnap.data().apiKey); setTempApiKey(docSnap.data().apiKey); } } catch (e) { console.error("Error fetching API Key:", e); } }; fetchApiKey(); } else { setCustomApiKey(''); setTempApiKey(''); } return () => { unsubscribeHistory(); unsubscribeGallery(); }; }, [user]); // --- Google Auth Handlers --- const loginWithGoogle = async () => { try { await signInWithPopup(auth, googleProvider); } catch (error) { console.error("Google login error:", error); alert("เข้าสู่ระบบด้วย Google ไม่สำเร็จ: " + error.message); } }; const handleLogout = async () => { if (window.confirm('คุณต้องการออกจากระบบใช่หรือไม่?')) { try { await signOut(auth); setCustomApiKey(''); // Clear custom API key from memory await signInAnonymously(auth); } catch (error) { console.error("Logout error:", error); } } }; // --- Settings Handlers --- const saveSettings = async () => { if (!user || user.isAnonymous) { alert("โปรดล็อกอินด้วยบัญชี Google ก่อนบันทึก API Key ของคุณ"); return; } try { const docRef = doc(db, 'artifacts', artifactAppId, 'users', user.uid, 'settings', 'profile'); await setDoc(docRef, { apiKey: tempApiKey }, { merge: true }); setCustomApiKey(tempApiKey); setShowSettings(false); alert("บันทึก API Key สำเร็จ!"); } catch (e) { console.error("Error saving API Key", e); alert("เกิดข้อผิดพลาดในการบันทึก"); } }; // --- Input Handlers --- const handleInputChange = (e) => { const { name, value } = e.target; setFormData(prev => ({ ...prev, [name]: value })); }; const toggleNarrativeStyle = (num) => { setFormData(prev => { if (num === 0) return { ...prev, narrativeStyles: [0] }; let newStyles = prev.narrativeStyles.filter(n => n !== 0); if (newStyles.includes(num)) newStyles = newStyles.filter(n => n !== num); else newStyles.push(num); if (newStyles.length === 0) newStyles = [0]; return { ...prev, narrativeStyles: newStyles }; }); }; const resetForm = () => { if(window.confirm('คุณต้องการรีเซ็ตข้อมูลทั้งหมดกลับเป็นค่าเริ่มต้นหรือไม่?')) { setFormData(defaultFormState); setResult(''); setCharImage(null); setProdImage(null); } }; // --- Gallery Upload & Delete --- const handleGalleryUpload = async (e) => { const file = e.target.files[0]; if (!file || !user) return; if (file.size > 5 * 1024 * 1024) { alert("กรุณาอัปโหลดภาพขนาดไม่เกิน 5MB"); return; } setIsUploadingGallery(true); try { const thumb = await generateThumbnail(file); const storagePath = `artifacts/${artifactAppId}/users/${user.uid}/gallery/${Date.now()}_${file.name}`; const storageRef = ref(storage, storagePath); await uploadBytes(storageRef, file); const url = await getDownloadURL(storageRef); const newDoc = { name: file.name, url, storagePath, thumbnailBase64: thumb, mimeType: file.type || "image/jpeg", createdAt: Date.now() }; const docRef = await addDoc(collection(db, 'artifacts', artifactAppId, 'users', user.uid, 'gallery'), newDoc); if (showGalleryModal === 'char') { setCharImage({ id: docRef.id, ...newDoc }); setShowGalleryModal(false); } else if (showGalleryModal === 'prod') { setProdImage({ id: docRef.id, ...newDoc }); setShowGalleryModal(false); } } catch (err) { console.error("Gallery upload error:", err); alert("เกิดข้อผิดพลาดในการอัปโหลดรูปภาพ กรุณาตรวจสอบสิทธิ์ Firebase Storage"); } finally { setIsUploadingGallery(false); e.target.value = null; // reset input } }; const handleGalleryDelete = async (e, docItem) => { e.stopPropagation(); if (!user || !window.confirm("ต้องการลบภาพนี้ออกจากคลังภาพใช่หรือไม่?")) return; try { await deleteDoc(doc(db, 'artifacts', artifactAppId, 'users', user.uid, 'gallery', docItem.id)); if (docItem.storagePath) { await deleteObject(ref(storage, docItem.storagePath)); } if (charImage?.id === docItem.id) setCharImage(null); if (prodImage?.id === docItem.id) setProdImage(null); } catch (err) { console.error("Gallery delete error:", err); } }; const selectGalleryImage = (docItem) => { if (showGalleryModal === 'char') setCharImage(docItem); if (showGalleryModal === 'prod') setProdImage(docItem); setShowGalleryModal(false); }; // --- Generation & DB Save --- const generateContent = async () => { const activeApiKey = customApiKey || systemApiKey; if (!activeApiKey) { setError('กรุณาตั้งค่า Gemini API Key ของคุณในเมนู "ตั้งค่า" (ไอคอนฟันเฟืองด้านบน) ก่อนใช้งาน เพื่อใช้โควต้าบัญชีของคุณเอง'); setShowSettings(true); return; } setIsLoading(true); setError(''); setResult(''); setCopied(false); const selectedNarratives = narrativeStylesList.filter(n => formData.narrativeStyles.includes(n.num)); const narrativePersonalityString = selectedNarratives.map(n => n.personality).filter(p => p !== '').join('\n- '); const selectedMood = moodKeywordsList.find(m => m.id === formData.mood) || moodKeywordsList[0]; const selectedVisual = visualStylesList.find(v => v.id === formData.visualStyle) || visualStylesList[0]; const systemPrompt = ` คุณคือ 'ผู้กำกับอนิเมะไซไฟสงครามร่างกาย' ระดับ Masterpiece และผู้เชี่ยวชาญอัลกอริทึม TikTok/Reels จุดประสงค์: สร้าง Script, Storyboard และ Video Prompts จำนวน ${formData.sceneCount} ฉาก ที่ทรงพลัง เพื่อขายสินค้าแบบ "ยุทโธปกรณ์สนับสนุน" กฎเกณฑ์ (Rules) ขั้นเด็ดขาด: 1. บุคลิกภาพในการเล่าเรื่อง (Narrative Personality): ต้องผสมผสานบุคลิกเหล่านี้เข้าด้วยกันอย่างลงตัวในบทพูดหรือการนำเสนอ: ${narrativePersonalityString || '- ผู้กำกับไซไฟสุดดาร์ก ดุดัน'} 2. ห้ามถือสินค้าเด็ดขาด: สินค้าต้องถูกนำเสนอแบบอลังการในฐานะ "พลังงานเสริม/กล่องยังชีพ" ที่ถูกส่งลงมาจากเบื้องบน 3. การเขียน Video Prompt: ต้องเขียนแบบ "Text-to-Video Ready" อธิบายรูปลักษณ์ตัวละครอย่างละเอียดยิบ ห้ามใช้แค่ชื่อเรียกอวัยวะ ${charImage ? '4. **อ้างอิงตัวละคร:** ผู้ใช้ได้แนบภาพ "ตัวละครหลัก" ให้นำลักษณะเด่นไปเขียนอธิบายใน Video Prompts\n' : ''}${prodImage ? '5. **อ้างอิงสินค้า:** ผู้ใช้ได้แนบภาพ "สินค้ากู้ชีพ" ให้นำลักษณะเด่นไปบรรยายใน Video Prompts\n' : ''} ${formData.dialogueType === 'mute' ? '6. **ห้ามมีบทพูดเด็ดขาด (No Dialogue):** เน้นอธิบายแอคชั่น, ท่าทางต่อสู้, กระบวนการทำงาน, และเสียงประกอบ (SFX/ASMR) แทน ตัวละครไม่ต้องขยับปากพูด' : formData.dialogueType === 'voiceover' ? '6. **รูปแบบเสียงบรรยาย (Voiceover):** ตัวละครในจอไม่ต้องขยับปากพูด ให้ใช้การพากย์เสียงบรรยายทับภาพ (VO)' : '6. **บทสนทนาปกติ:** ตัวละครต้องขยับปากพูดสอดคล้องกับบทสนทนา'} `; const userPrompt = ` สร้างคอนเทนต์วิดีโอสั้นแนว Body Adventure ตามข้อมูลนี้: - หมวดหมู่: ${formData.category} - ชื่อตัวละครหลัก: ${formData.name} - ศัตรู/ปัญหา: ${formData.enemy} - สินค้าที่จะขาย: ${formData.product} - อารมณ์พื้นฐานตัวละคร: ${formData.emotion} - มู้ดและโทน (Mood): ${selectedMood.name} - สไตล์ภาพวิดีโอ (Art Style): ${selectedVisual.name} - รูปแบบเสียง: ${formData.dialogueType === 'mute' ? 'คลิปใบ้/ไม่มีคนพูด เน้นภาพและเสียง SFX' : formData.dialogueType === 'voiceover' ? 'พากย์เสียงบรรยายทับ (VO)' : 'ตัวละครในจอพูดปกติ'} - จำนวนฉากที่ต้องการ: ${formData.sceneCount} ฉาก - ความต้องการพิเศษ: ${formData.extraDetails || 'ไม่มี'} ให้ Output เป็น Markdown จัดหัวข้อให้สวยงามตามลำดับดังนี้: # 🎬 ชื่อวิดีโอ ## 🧬 การออกแบบตัวละคร (Character Design) ## 📖 Story Arc ${formData.dialogueType === 'mute' ? '## 🎵 ลำดับภาพและเสียงประกอบ (Visual & Audio Timeline) (ระบุ [SFX: ] และ [VFX: ])' : '## 🗣️ บทพูดและมุมกล้อง (Directing Script) (ระบุ [SFX: ] และ [VFX: ])'} ## 🎞️ สตอรี่บอร์ด (Storyboard จำนวน ${formData.sceneCount} ฉาก) ## 🎥 Video Prompts (Text-to-Video Ready) ${formData.dialogueType === 'mute' ? `รูปแบบ: ฉากที่ X [Prompt: (อธิบายรูปลักษณ์ตัวละครอ้างอิงจากรูปถ้ามี), (แอคชั่น/การเคลื่อนไหว), (มุมกล้อง), (แสง/บรรยากาศ), ${selectedVisual.prompt ? `"${selectedVisual.prompt}"` : ''}, --ar 9:16]` : formData.dialogueType === 'voiceover' ? `รูปแบบ: ฉากที่ X [Prompt: (อธิบายรูปลักษณ์ตัวละครอ้างอิงจากรูปถ้ามี), (การเคลื่อนไหว), (มุมกล้อง), (แสง/บรรยากาศ), ${selectedVisual.prompt ? `"${selectedVisual.prompt}"` : ''}, --ar 9:16, เสียงบรรยาย (VO): "(บทพากย์)"]` : `รูปแบบ: ฉากที่ X [Prompt: (อธิบายรูปลักษณ์ตัวละครอ้างอิงจากรูปถ้ามี), (การเคลื่อนไหว), (มุมกล้อง), (แสง/บรรยากาศ), ขยับปากพูด "(บทพูด)", ${selectedVisual.prompt ? `"${selectedVisual.prompt}"` : ''}, --ar 9:16]`} (เขียนให้ครบ ${formData.sceneCount} ฉาก ห้ามขาดห้ามเกิน) ## #️⃣ แฮชแท็กดันฟีด (5-7 อัน) `; const apiPayload = { contents: [{ role: "user", parts: [{ text: userPrompt }] }], systemInstruction: { parts: [{ text: systemPrompt }] } }; try { if (charImage?.thumbnailBase64) { const b64 = charImage.thumbnailBase64.split(',')[1]; if (b64) { apiPayload.contents[0].parts.push({ text: "\n[นี่คือภาพอ้างอิงสำหรับ: ตัวละครหลัก]\n" }); apiPayload.contents[0].parts.push({ inlineData: { mimeType: charImage.mimeType || "image/jpeg", data: b64 } }); } } if (prodImage?.thumbnailBase64) { const b64 = prodImage.thumbnailBase64.split(',')[1]; if (b64) { apiPayload.contents[0].parts.push({ text: "\n[นี่คือภาพอ้างอิงสำหรับ: สินค้ากู้ชีพ]\n" }); apiPayload.contents[0].parts.push({ inlineData: { mimeType: prodImage.mimeType || "image/jpeg", data: b64 } }); } } } catch (fetchErr) { console.error("Failed to process image bytes for AI:", fetchErr); } const fetchWithRetry = async (retries = 5, delay = 1000) => { try { const response = await fetch( `https://generativelanguage.googleapis.com/v1beta/models/gemini-2.5-flash-preview-09-2025:generateContent?key=${activeApiKey}`, { method: 'POST', headers: { 'Content-Type': 'application/json' }, body: JSON.stringify(apiPayload) } ); if (!response.ok) { const errData = await response.json(); throw new Error(`API Error: ${response.status} - ${errData.error?.message || 'Unknown error'}`); } const data = await response.json(); return data.candidates[0].content.parts[0].text; } catch (err) { if (retries > 0) { await new Promise(res => setTimeout(res, delay)); return fetchWithRetry(retries - 1, delay * 2); } throw err; } }; try { const generatedText = await fetchWithRetry(); setResult(generatedText); // Save to Firebase DB if (user) { await addDoc(collection(db, 'artifacts', artifactAppId, 'users', user.uid, 'scripts'), { formData, result: generatedText, charImageUrl: charImage ? charImage.url : '', charThumbnailBase64: charImage ? charImage.thumbnailBase64 : '', prodImageUrl: prodImage ? prodImage.url : '', prodThumbnailBase64: prodImage ? prodImage.thumbnailBase64 : '', createdAt: Date.now() }); } } catch (err) { console.error(err); if (err.message.includes('API_KEY_INVALID') || err.message.includes('400')) { setError('API Key ไม่ถูกต้อง กรุณาตรวจสอบ API Key ในเมนูตั้งค่าอีกครั้ง'); setShowSettings(true); } else { setError('เกิดข้อผิดพลาดในการสร้างเนื้อหา: ' + err.message); } } finally { setIsLoading(false); } }; // --- Utils --- const copyToClipboard = (textToCopyParam = null) => { let textToCopy = textToCopyParam; if (!textToCopy) { const promptSection = result.split(/## 🎥 Video Prompts/i)[1]?.split('## #️⃣')[0]; textToCopy = promptSection ? promptSection.trim() : result; } navigator.clipboard.writeText(textToCopy).then(() => { setCopied(true); setTimeout(() => setCopied(false), 2000); }).catch(err => { const textArea = document.createElement("textarea"); textArea.value = textToCopy; document.body.appendChild(textArea); textArea.select(); try { document.execCommand('copy'); setCopied(true); setTimeout(() => setCopied(false), 2000); } catch (err) { console.error('Fallback copy failed', err); } document.body.removeChild(textArea); }); }; const downloadScript = (textContent = result) => { if (!textContent) return; const element = document.createElement("a"); const file = new Blob([textContent], {type: 'text/plain'}); element.href = URL.createObjectURL(file); element.download = `StorySnap_Script_${Date.now()}.txt`; document.body.appendChild(element); element.click(); document.body.removeChild(element); }; const deleteHistory = async (id) => { if (!user || !window.confirm("คุณต้องการลบประวัตินี้ใช่หรือไม่?")) return; try { await deleteDoc(doc(db, 'artifacts', artifactAppId, 'users', user.uid, 'scripts', id)); } catch (err) { console.error("Delete failed:", err); } }; const loadHistoryItem = (item) => { setFormData(item.formData); setResult(item.result); // Try to restore image references from history if they still exist if (item.charImageUrl) { const foundChar = galleryImages.find(g => g.url === item.charImageUrl); setCharImage(foundChar || { url: item.charImageUrl, thumbnailBase64: item.charThumbnailBase64 }); } else { setCharImage(null); } if (item.prodImageUrl) { const foundProd = galleryImages.find(g => g.url === item.prodImageUrl); setProdImage(foundProd || { url: item.prodImageUrl, thumbnailBase64: item.prodThumbnailBase64 }); } else { setProdImage(null); } setShowHistory(false); }; return (
เวอร์ชันเชื่อมต่อคลาวด์ Firebase
กำลังประมวลผลเรนเดอร์ภาพ...
AI กำลังวิเคราะห์ภาพและเขียนบท
(อาจใช้เวลาสักครู่)
{error}
รอคำสั่งสร้างภาพจากผู้กำกับ...
{parts[1]}{parts[2]}
} if (line.startsWith('- ')) return{parts.map((part, i) => i % 2 === 1 ? {part} : part)}
} if(formattedLine.includes('[SFX:') || formattedLine.includes('[VFX:')) { const coloredText = formattedLine.replace(/\[SFX:([^\]]+)\]/g, '[SFX:$1]') .replace(/\[VFX:([^\]]+)\]/g, '[VFX:$1]'); return ; } return{line}
; })}โปรดเข้าสู่ระบบด้วยบัญชี Google ของคุณก่อน เพื่อบันทึก API Key อย่างปลอดภัย
เพื่อใช้งานแบบไม่จำกัดโควต้า โปรดใส่ Gemini API Key ของคุณเอง สามารถรับคีย์ฟรีได้ที่ Google AI Studio
{new Date(docItem.createdAt).toLocaleString('th-TH')}