const SAMPLE_SCRIPT = `The year is 1977. NASA launches two spacecraft from Cape Canaveral, each carrying a golden record etched with the sounds of Earth — heartbeats, thunder, birdsong, greetings in fifty-five languages. Voyager 1 and Voyager 2 are not coming back. They are bottle messages, hurled into the dark on the slim chance that, in some distant epoch, another civilization picks them up. Forty-seven years later, they are still transmitting.`; const SAMPLE_SCRIPT_VI = `Năm 1977. NASA phóng hai tàu vũ trụ từ Cape Canaveral, mỗi tàu mang theo một chiếc đĩa vàng khắc ghi những âm thanh của Trái Đất — tiếng nhịp tim, tiếng sấm, tiếng chim hót và những lời chào bằng 55 ngôn ngữ khác nhau. Voyager 1 và Voyager 2 sẽ không quay trở lại. Chúng là những bức thư gửi trong chai, được thả vào bóng tối vũ trụ với hy vọng mong manh rằng, trong một kỷ nguyên xa xôi nào đó, một nền văn minh khác sẽ nhận được. Bốn mươi bảy năm sau, chúng vẫn đang tiếp tục truyền tín hiệu.`; const TXT_INPUT = { vi: { language: "Ngôn ngữ", style: "Phong cách", templates: "Mẫu", videoSettings: "Cài đặt Video", aspect: "Tỷ lệ", resolution: "Độ phân giải", scenes: "Số phân cảnh", voiceover: "Giọng đọc", subtitles: "Phụ đề", pacing: "Tốc độ", burnCaptions: "Chèn phụ đề trực tiếp vào video", llmDecides: "LLM tự quyết định", showLess: "Thu gọn", showAll: "Xem tất cả ({count})", hoverTemplate: "Rê chuột vào mẫu để xem thử chuyển động", project: "Dự án", uploadsTitle: "Tài nguyên của bạn", uploadsSub: "Tải lên ảnh tham chiếu", optional: "không bắt buộc", addImagesText: "+ Thêm hình ảnh", addMoreText: "+ Thêm tiếp", uploadsDescHint: "Mô tả hình ảnh (ví dụ: 'Ảnh chân dung CEO', 'Ảnh sản phẩm')", uploadsExpl: "Agent kịch bản sẽ đọc từng mô tả và tự động lồng ghép hình ảnh vào phân cảnh phù hợp. Bỏ qua nếu bạn không muốn dùng ảnh cá nhân.", pipeline: "Tiến trình", pipelineSteps: "Phân tích · Phác thảo · Biên tập · Xuất video", wallTime: "~{time}s thời gian thực tế", generateVideo: "Tạo Video", confirmTitle: "Bắt đầu tạo video?", confirmDesc: "Quy trình sẽ gọi LLM, tải tài nguyên, render Remotion — quá trình này có thể mất vài phút và tốn tài nguyên.", cancel: "Hủy", start: "Bắt đầu", words: "Số từ", estLength: "Độ dài ước tính", wordsCount: "{count} từ", charsCount: "{count} ký tự", estTime: "~{min}:{sec} ước tính", scriptMode: "Kịch bản", articleMode: "Đường dẫn bài viết", audioMode: "Âm thanh", customPrompt: "Yêu cầu tùy chỉnh", customPromptSub: "Gửi trực tiếp đến Agent 1", customPromptPlh: "Hướng dẫn bổ sung về phong cách, giọng điệu, cấu trúc, hình ảnh, bảng màu, tốc độ...", customStyle: "Mô tả phong cách", customStyleSub: "Áp dụng cho toàn bộ video", customStylePlh: "Mô tả phong cách hình ảnh: màu sắc, kiểu chữ, hiệu ứng chuyển cảnh, bố cục, tông màu... Ví dụ: Nền tối với điểm nhấn neon xanh, chữ sans-serif đậm, hiệu ứng fade mượt mà.", templatesOnly: "Chỉ dùng các mẫu bên dưới", templatesOnlyHint: "Bắt buộc Agent 1 phải chọn template cho mọi scene (không cho custom-from-scratch)", aiWrite: "Viết bằng AI", aiWriting: "Đang viết...", aiWriteFail: "Không tạo được phong cách. Thử lại.", aiIdeaPlh: "Nhập ý tưởng phong cách (vd: cyberpunk neon tối, năng động)...", articleUrl: "Đường dẫn bài viết", urlPlaceholder: "Dán URL để trích xuất nội dung bài viết...", dropAudio: "Kéo thả tệp .mp3, .wav, hoặc .m4a", whisperDesc: "Whisper-large tự động chuyển giọng nói thành kịch bản · tối đa 25 phút", browseFiles: "Duyệt tệp", uploadBtn: "Tải lên", trySample: "Dùng thử mẫu", clear: "Xóa", autoSaved: "Đã tự động lưu", relaxed: "THƯ THẢ", snappy: "NHANH", scriptPlaceholder: "Dán kịch bản, lời thuyết minh hoặc nội dung bài viết. Hoặc kéo thả tệp .txt / .md / .srt.", dropFileText: "Thả tệp để tải", }, en: { language: "Language", style: "Style", templates: "Templates", videoSettings: "Video Settings", aspect: "Aspect", resolution: "Resolution", scenes: "Scenes", voiceover: "Voiceover", subtitles: "Subtitles", pacing: "Pacing", burnCaptions: "Burn captions directly into the video", llmDecides: "LLM decides", showLess: "Show less", showAll: "Show all ({count})", hoverTemplate: "Hover a template to preview its motion", project: "Project", uploadsTitle: "Your assets", uploadsSub: "Upload reference images", optional: "optional", addImagesText: "+ Add images", addMoreText: "+ Add more", uploadsDescHint: "Describe this image (e.g. 'CEO portrait', 'product hero shot')", uploadsExpl: "The script agent will read each description and choose whether to weave the image into a fitting scene. Skip this entirely if you don't want to use your own photos.", pipeline: "Pipeline", pipelineSteps: "Analyze · Storyboard · Edit · Render", wallTime: "~{time}s wall time", generateVideo: "Generate Video", confirmTitle: "Generate Video?", confirmDesc: "The pipeline will call LLM, fetch media, and render via Remotion — this may take a few minutes and consume resources.", cancel: "Cancel", start: "Start", words: "Words", estLength: "Est. length", wordsCount: "{count} words", charsCount: "{count} chars", estTime: "~{min}:{sec} est.", scriptMode: "Script", articleMode: "Article URL", audioMode: "Audio", customPrompt: "Custom Prompt", customPromptSub: "Sent directly to Agent 1", customPromptPlh: "Extra instructions for style, tone, structure, media, color palette, pacing...", customStyle: "Style description", customStyleSub: "Applied to the whole video", customStylePlh: "Describe the visual style: colors, typography, transitions, layout, mood... e.g. Dark background with neon-blue accents, bold sans-serif, smooth fade transitions.", templatesOnly: "Use only the templates below", templatesOnlyHint: "Force Agent 1 to pick a template for every scene (no fully-custom scenes)", aiWrite: "Write with AI", aiWriting: "Writing...", aiWriteFail: "Could not generate a style. Try again.", aiIdeaPlh: "Type a style idea (e.g. dark cyberpunk neon, energetic)...", articleUrl: "Article URL", urlPlaceholder: "https://example.com/article", dropAudio: "Drop .mp3, .wav, or .m4a", whisperDesc: "Whisper-large transcribes to script · max 25 min", browseFiles: "Browse files", uploadBtn: "Upload", trySample: "Try sample", clear: "Clear", autoSaved: "Auto-saved", relaxed: "RELAXED", snappy: "SNAPPY", scriptPlaceholder: "Paste a script, narration, or article body. Or drop a .txt / .md / .srt file.", dropFileText: "Drop to load file", } }; const TEMPLATE_TRANSLATIONS = { vi: { "FullImageScene": { name: "Ảnh toàn màn hình", description: "Một ảnh toàn màn hình với hiệu ứng Ken Burns, tùy chọn tiêu đề." }, "CenterImageScene": { name: "Ảnh căn giữa", description: "Ảnh nằm giữa (tròn hoặc chữ nhật) với hiệu ứng lia máy chậm, tùy chọn nhãn." }, "ImageGridScene": { name: "Lưới ảnh", description: "2-4 ảnh xếp cùng nhau (4 ảnh thành lưới 2x2), nhãn tùy chọn; hiệu ứng vào (lật/trượt/zoom/mờ) do agent chọn." }, "ImageTextScene": { name: "Ảnh + chữ", description: "Một ảnh một bên + cột chữ (tiêu đề và nội dung) bên kia." }, "BigStatScene": { name: "Số liệu lớn", description: "Một con số khổng lồ chạy từ 0 kèm nhãn — không có media." }, "QuoteScene": { name: "Trích dẫn", description: "Trích dẫn lớn căn giữa kèm tên người nói — không có media." }, "BulletListScene": { name: "Danh sách", description: "Danh sách dọc hiện từng dòng một, đồng bộ với lời thoại — không có media." }, "CardGridScene": { name: "Lưới thẻ", description: "Một hàng thẻ kính mờ hoạt họa xuất hiện so le." }, "TextOnlyScene": { name: "Chỉ có văn bản", description: "Tiêu đề hoạt họa kèm phụ đề tùy chọn trên nền giấy, không có hình ảnh." }, "VideoBackgroundScene": { name: "Video nền", description: "Video toàn màn hình không có lớp phủ." }, "RankingScrollScene": { name: "Cuộn xếp hạng", description: "Dải cuộn ngang gồm các cột xếp hạng để so sánh/xếp hạng." } }, en: { "FullImageScene": { name: "Full Image", description: "One fullscreen image with Ken Burns motion, optional title." }, "CenterImageScene": { name: "Center Image", description: "Image centered (circle or rectangle) with a slow pan, optional badge." }, "ImageGridScene": { name: "Image Grid", description: "2-4 images laid out together (4 becomes a 2x2 grid), optional labels; entrance effect chosen by the agent." }, "ImageTextScene": { name: "Image + Text", description: "One image on one side and a heading + body text column on the other." }, "BigStatScene": { name: "Big Stat", description: "One giant number that counts up from 0, plus a label — no media." }, "QuoteScene": { name: "Quote", description: "Large centered pull-quote with author attribution — no media." }, "BulletListScene": { name: "Bullet List", description: "Vertical list whose rows reveal one-by-one, synced to narration — no media." }, "CardGridScene": { name: "Card Grid", description: "A row of animated glass cards with staggered pop-in." }, "TextOnlyScene": { name: "Text Only", description: "Animated headline plus optional subtitle on the paper background, no media." }, "VideoBackgroundScene": { name: "Video Background", description: "Fullscreen video with zero overlays." }, "RankingScrollScene": { name: "Ranking Scroll", description: "Horizontal scrolling strip of ranked columns for ranking/comparison videos." } } }; const translateStylePreset = (s, lang) => { if (lang !== 'vi') return s; const translations = { presentation: { name: "Trình chiếu", sub: "Slide tối giản, texture giấy nhám", chips: ["Slide", "Giấy nhám"] }, analytics: { name: "Phân tích", sub: "Biểu đồ, trực quan hóa dữ liệu chuẩn McKinsey", chips: ["Biểu đồ", "Đơn sắc"] }, kids: { name: "Trẻ em", sub: "Hoạt họa sinh động, nhiều màu sắc", chips: ["Năng động", "Nhãn dán"] }, fintech: { name: "Công nghệ tài chính", sub: "Bảng giao dịch, biểu đồ nến", chips: ["Mã chứng khoán", "Biểu đồ nến"] }, documentary: { name: "Phim tài liệu", sub: "Cảm giác phim tài liệu điện ảnh", chips: ["Màu phim", "Nhạc nền"] }, news: { name: "Tin tức", sub: "Đồ họa truyền hình, phần ba dưới màn hình", chips: ["Lower-3rd", "Tin chạy"] }, social_short: { name: "Video ngắn MXH", sub: "Mở đầu giật gân, định dạng dọc", chips: ["Hook ấn tượng", "B-roll"] }, education: { name: "Giáo dục", sub: "Bài giảng giải thích rõ ràng, trực quan", chips: ["Sơ đồ", "Các bước"] }, cinematic: { name: "Điện ảnh", sub: "Góc quay rộng, lia máy chậm, nhạc kịch tính", chips: ["Khung hình rộng", "Hạt nhiễu"] }, tech_startup: { name: "Khởi nghiệp công nghệ", sub: "Hiện đại, chuyển sắc phong phú, hiệu ứng kính", chips: ["Chuyển sắc", "Kính mờ"] }, luxury: { name: "Sang trọng", sub: "Cao cấp, chữ serif có chân, điểm nhấn vàng", chips: ["Chữ có chân", "Sắc vàng"] }, podcast: { name: "Podcast", sub: "Dạng sóng âm, thẻ trích dẫn", chips: ["Sóng âm", "Trích dẫn"] }, report: { name: "Báo cáo", sub: "Trực quan hóa doanh nghiệp, dashboard KPI", chips: ["Bảng biểu", "KPI"] }, infographic: { name: "Đồ họa thông tin", sub: "Minh họa phẳng, cuộn xếp hạng", chips: ["Xếp hạng", "Nghệ thuật phẳng"] }, custom: { name: "Tùy chỉnh", sub: "Tự mô tả phong cách, chọn mẫu", chips: ["Tự do", "Của bạn"] } }; const t = translations[s.id]; return t ? { ...s, name: t.name, sub: t.sub, chips: t.chips } : s; }; const VOICEOVER_LABELS = { vi: { "narrator-warm": "Người dẫn chuyện — giọng nam trầm ấm", "narrator-clear": "Người dẫn chuyện — giọng nữ cao rõ ràng", "doc-reserved": "Phim tài liệu — trang trọng", "creator-bright": "Nhà sáng tạo — tươi vui, nhanh", "off": "Không thuyết minh", }, en: { "narrator-warm": "Narrator — warm baritone", "narrator-clear": "Narrator — clear alto", "doc-reserved": "Documentary — reserved", "creator-bright": "Creator — bright, fast", "off": "No voiceover", } }; const POSITIONS_TRANS = { vi: { top: "Trên", middle: "Giữa", bottom: "Dưới", }, en: { top: "Top", middle: "Mid", bottom: "Bottom", } }; const STYLE_PRESETS = [ { id: "presentation", name: "Presentation", sub: "Minimal slides, paper texture", chips: ["Slides", "Paper"], swatch: ["#FFFFFF", "#E8E2D5", "#222222"] }, { id: "analytics", name: "Analytics", sub: "Charts, McKinsey-clean data viz", chips: ["Charts", "Mono"], swatch: ["#0D1117", "#F0F6FF", "#58A6FF"] }, { id: "kids", name: "Kids", sub: "Bouncy, colorful, cartoon", chips: ["Bouncy", "Stickers"], swatch: ["#FFD700", "#FF69B4", "#87CEEB"] }, { id: "fintech", name: "Fintech", sub: "Trading terminal, candlesticks", chips: ["Tickers", "Candles"], swatch: ["#060B14", "#00D4AA", "#FF4757"] }, { id: "documentary", name: "Documentary", sub: "Cinematic documentary feel", chips: ["LUT", "Score"], swatch: ["#2b2118", "#7a5a3a", "#d6b87a"] }, { id: "news", name: "News", sub: "Broadcast graphics, lower thirds", chips: ["Lower-3rd", "Ticker"], swatch: ["#1a1a1a", "#d62828", "#ffffff"] }, { id: "social_short", name: "Social Short", sub: "Punchy hook, vertical-ready", chips: ["Hook", "B-roll"], swatch: ["#0a0a0a", "#e63946", "#f4f1ec"] }, { id: "education", name: "Education", sub: "Clear, lesson-style explainer", chips: ["Diagrams", "Steps"], swatch: ["#f5f5f0", "#1a73e8", "#fbbc04"] }, { id: "cinematic", name: "Cinematic", sub: "Wide shots, slow pans, score", chips: ["Anamorphic", "Grain"], swatch: ["#1e1813", "#7a5a3a", "#d6b87a"] }, { id: "tech_startup", name: "Tech Startup", sub: "Modern, gradient-rich, glassy", chips: ["Gradient", "Glass"], swatch: ["#0f0f23", "#8b5cf6", "#06b6d4"] }, { id: "luxury", name: "Luxury", sub: "Premium, serif type, gold accents", chips: ["Serif", "Gold"], swatch: ["#0a0a0a", "#d4af37", "#ffffff"] }, { id: "podcast", name: "Podcast", sub: "Waveform-driven, quote cards", chips: ["Waveform", "Quote"], swatch: ["#1a1a2e", "#e94560", "#fafafa"] }, { id: "report", name: "Report", sub: "Corporate, KPI dashboard", chips: ["Tables", "KPI"], swatch: ["#ffffff", "#0a3d62", "#3498db"] }, { id: "infographic", name: "Infographic", sub: "Flat illustration, ranking scroll", chips: ["Ranking", "FlatArt"], swatch: ["#FFFFFF", "#111111", "#F5A623"] }, { id: "custom", name: "Custom", sub: "Describe your own style, pick templates", chips: ["Free-text", "Yours"], swatch: ["#6366F1", "#EC4899", "#F59E0B"] }, ]; // Pre-filled scaffold for the custom-style textarea. Mirrors the field structure // of the built-in style presets so users know what to describe. Seeded only when // the user picks the "custom" style and the field is still empty. const CUSTOM_STYLE_SCAFFOLD = { vi: "PHONG CÁCH: \nBỐ CỤC: \nMÀU SẮC: \nHIỆU ỨNG: \nCHUYỂN CẢNH: \nCHỮ: \nLIÊN TỤC: ", en: "STYLE: \nLAYOUT: \nCOLORS: \nEFFECTS: \nTRANSITIONS: \nTYPOGRAPHY: \nCONTINUITY: ", }; const LANGUAGES = [ { code: "Vietnamese", label: "Tiếng Việt" }, { code: "English", label: "English" }, { code: "Japanese", label: "日本語" }, { code: "Korean", label: "한국어" }, { code: "Chinese", label: "中文" }, { code: "Spanish", label: "Español" }, ]; const SUBTITLE_PRESETS = [ { id: "studio", name: "Studio", text: "#FFFFFF", outline: "#000000", back: "#000000A0", highlight: "#FFD60A" }, { id: "broadcast",name: "Broadcast",text: "#F4F1EC", outline: "#1B1815", back: "#1B181500", highlight: "#FF8A3D" }, { id: "neon", name: "Neon", text: "#F8FAFF", outline: "#0A0E1A", back: "#0A0E1AB0", highlight: "#19E5BE" }, { id: "minimal", name: "Minimal", text: "#FFFFFF", outline: "#00000000",back: "#00000000", highlight: "#FFFFFF" }, ]; const POSITIONS = [ { id: "top", label: "Top" }, { id: "middle", label: "Mid" }, { id: "bottom", label: "Bottom" }, ]; function hexToRgba(h) { if (!h) return "transparent"; let hex = h.replace("#", ""); if (hex.length === 6) hex += "FF"; const r = parseInt(hex.slice(0, 2), 16); const g = parseInt(hex.slice(2, 4), 16); const b = parseInt(hex.slice(4, 6), 16); const a = parseInt(hex.slice(6, 8), 16) / 255; return `rgba(${r},${g},${b},${a})`; } // ── tiny UI primitives ────────────────────────────────────────────── function Field({ label, hint, children, right }) { return (
{right || (hint && {hint})}
{children}
); } function Select({ value, onChange, options }) { return (
); } function NumberStepper({ value, onChange, min = 0, max = 99, step = 1, suffix, zeroLabel, editable }) { const showZeroLabel = zeroLabel && value === 0; const clamp = (v) => Math.max(min, Math.min(max, v)); return (
{editable ? ( { const raw = e.target.value; if (raw === "") { onChange(min); return; } const v = parseInt(raw, 10); if (!Number.isNaN(v)) onChange(clamp(v)); }} style={{ flex: 1, width: "100%", minWidth: 0, textAlign: "center", background: "transparent", border: 0, outline: 0, fontSize: 13, fontFamily: "var(--font-mono)", color: "var(--fg-0)", MozAppearance: "textfield", }} /> ) : (
{showZeroLabel ? zeroLabel : <>{value}{suffix && {suffix}}}
)}
); } function Switch({ value, onChange }) { return ( ); } function CollapsibleSection({ heading, defaultOpen = false, summary, children }) { const [open, setOpen] = React.useState(defaultOpen); return (
{open &&
{children}
}
); } function Segmented({ value, onChange, options }) { return (
{options.map((o) => ( ))}
); } function ColorChip({ value, onChange, allowAlpha = true }) { const swatches = ["#FFFFFF", "#000000", "#FFD60A", "#FF8A3D", "#19E5BE", "#FF4D6D", "#7B61FF", "#3D8BFF"]; const [open, setOpen] = React.useState(false); const ref = React.useRef(null); React.useEffect(() => { const onDoc = (e) => { if (ref.current && !ref.current.contains(e.target)) setOpen(false); }; document.addEventListener("mousedown", onDoc); return () => document.removeEventListener("mousedown", onDoc); }, []); return (
{open && (
{swatches.map((s) => (
onChange(e.target.value + (allowAlpha ? value.slice(7) || "FF" : ""))} style={{ width: "100%", height: 28, background: "transparent", border: "1px solid var(--line)", borderRadius: 4 }} /> {allowAlpha && (
ALPHA
{ const a = parseInt(e.target.value).toString(16).padStart(2, "0").toUpperCase(); onChange(value.slice(0, 7) + a); }} style={{ width: "100%" }} />
)}
)}
); } // ── source pane ───────────────────────────────────────────────────── function SourcePane({ source, setSource, customPrompt, setCustomPrompt, sourceMode, setSourceMode, uiLang = "vi" }) { const t = TXT_INPUT[uiLang] || TXT_INPUT.vi; const [drag, setDrag] = React.useState(false); const fileRef = React.useRef(null); const wordCount = source.trim() ? source.trim().split(/\s+/).length : 0; const charCount = source.length; const estSec = Math.max(0, Math.round(wordCount / 2.6)); const onDrop = (e) => { e.preventDefault(); setDrag(false); const f = e.dataTransfer.files?.[0]; if (f && f.type.startsWith("text") || (f && /\.(txt|md|srt)$/i.test(f.name))) { const r = new FileReader(); r.onload = () => setSource(String(r.result)); r.readAsText(f); } }; return (
{/* tabbed header */}
{[ { id: "script", label: t.scriptMode }, ].map((tab) => ( ))}
{t.wordsCount.replace("{count}", wordCount)} · {t.charsCount.replace("{count}", charCount)} · {t.estTime.replace("{min}", Math.floor(estSec/60)).replace("{sec}", String(estSec%60).padStart(2,"0"))}
{/* body */} {sourceMode === "script" && (
{ e.preventDefault(); setDrag(true); }} onDragOver={(e) => e.preventDefault()} onDragLeave={() => setDrag(false)} onDrop={onDrop} style={{ position: "relative", flex: 1, minHeight: 360, display: "flex", flexDirection: "column", }} >