diff --git a/src/components/UploadPicker.js b/src/components/UploadPicker.js index 2d9879d..ce0e85f 100644 --- a/src/components/UploadPicker.js +++ b/src/components/UploadPicker.js @@ -395,5 +395,12 @@ export function createUploadPicker({ anchorContainer, onSelect, onClear, maxImag const getSelectedUrls = () => selectedEntries.map(e => e.url); - return { trigger, panel, reset, setMaxImages, getSelectedUrls }; + // Programmatically select an image (e.g. for demo mode) without uploading + const setImage = (url, thumbnail) => { + selectedEntries = [{ url, thumbnail: thumbnail || url }]; + updateTrigger(); + fireOnSelect(); + }; + + return { trigger, panel, reset, setMaxImages, getSelectedUrls, setImage }; } diff --git a/src/components/VideoStudio.js b/src/components/VideoStudio.js index cac9574..a921f2e 100644 --- a/src/components/VideoStudio.js +++ b/src/components/VideoStudio.js @@ -1,5 +1,5 @@ import { muapi } from '../lib/muapi.js'; -import { t2vModels, getAspectRatiosForVideoModel, getDurationsForModel, getResolutionsForVideoModel, i2vModels, getAspectRatiosForI2VModel, getDurationsForI2VModel, getResolutionsForI2VModel } from '../lib/models.js'; +import { t2vModels, getAspectRatiosForVideoModel, getDurationsForModel, getResolutionsForVideoModel, i2vModels, getAspectRatiosForI2VModel, getDurationsForI2VModel, getResolutionsForI2VModel, v2vModels } from '../lib/models.js'; import { AuthModal } from './AuthModal.js'; import { createUploadPicker } from './UploadPicker.js'; @@ -20,8 +20,10 @@ export function VideoStudio() { let dropdownOpen = null; let uploadedImageUrl = null; let imageMode = false; // false = t2v models, true = i2v models + let v2vMode = false; // true = video-to-video tools mode + let uploadedVideoUrl = null; - const getCurrentModels = () => imageMode ? i2vModels : t2vModels; + const getCurrentModels = () => v2vMode ? v2vModels : (imageMode ? i2vModels : t2vModels); const getCurrentAspectRatios = (id) => imageMode ? getAspectRatiosForI2VModel(id) : getAspectRatiosForVideoModel(id); const getCurrentDurations = (id) => imageMode ? getDurationsForI2VModel(id) : getDurationsForModel(id); const getCurrentResolutions = (id) => imageMode ? getResolutionsForI2VModel(id) : getResolutionsForVideoModel(id); @@ -76,6 +78,12 @@ export function VideoStudio() { anchorContainer: container, onSelect: ({ url }) => { uploadedImageUrl = url; + // Clear video mode if active + if (v2vMode) { + uploadedVideoUrl = null; + v2vMode = false; + showVideoIcon(); + } if (!imageMode) { imageMode = true; selectedModel = i2vModels[0].id; @@ -84,6 +92,7 @@ export function VideoStudio() { updateControlsForModel(selectedModel); } textarea.placeholder = 'Describe the motion or effect (optional)'; + textarea.disabled = false; }, onClear: () => { uploadedImageUrl = null; @@ -93,11 +102,124 @@ export function VideoStudio() { document.getElementById('v-model-btn-label').textContent = selectedModelName; updateControlsForModel(selectedModel); textarea.placeholder = 'Describe the video you want to create'; + textarea.disabled = false; } }); topRow.appendChild(picker.trigger); container.appendChild(picker.panel); + // --- Video Upload Picker (Video-to-Video) --- + const videoFileInput = document.createElement('input'); + videoFileInput.type = 'file'; + videoFileInput.accept = 'video/*'; + videoFileInput.className = 'hidden'; + + const videoPickerBtn = document.createElement('button'); + videoPickerBtn.type = 'button'; + videoPickerBtn.title = 'Upload video to remove watermark'; + videoPickerBtn.className = 'w-10 h-10 shrink-0 rounded-xl border transition-all flex items-center justify-center relative overflow-hidden mt-1.5 bg-white/5 border-white/10 hover:bg-white/10 hover:border-primary/40 group'; + + const videoIconEl = document.createElement('div'); + videoIconEl.className = 'flex items-center justify-center w-full h-full'; + videoIconEl.innerHTML = ``; + + const videoSpinnerEl = document.createElement('div'); + videoSpinnerEl.className = 'hidden items-center justify-center w-full h-full'; + videoSpinnerEl.innerHTML = `◌`; + + const videoReadyEl = document.createElement('div'); + videoReadyEl.className = 'hidden items-center justify-center w-full h-full'; + videoReadyEl.innerHTML = ``; + + videoPickerBtn.appendChild(videoFileInput); + videoPickerBtn.appendChild(videoIconEl); + videoPickerBtn.appendChild(videoSpinnerEl); + videoPickerBtn.appendChild(videoReadyEl); + + const showVideoIcon = () => { + videoIconEl.classList.replace('hidden', 'flex'); + videoSpinnerEl.classList.add('hidden'); videoSpinnerEl.classList.remove('flex'); + videoReadyEl.classList.add('hidden'); videoReadyEl.classList.remove('flex'); + videoPickerBtn.classList.remove('border-primary/60'); + videoPickerBtn.classList.add('border-white/10'); + videoPickerBtn.title = 'Upload video to remove watermark'; + }; + + const showVideoSpinner = () => { + videoIconEl.classList.add('hidden'); videoIconEl.classList.remove('flex'); + videoSpinnerEl.classList.replace('hidden', 'flex'); + videoReadyEl.classList.add('hidden'); videoReadyEl.classList.remove('flex'); + }; + + const showVideoReady = (filename) => { + videoIconEl.classList.add('hidden'); videoIconEl.classList.remove('flex'); + videoSpinnerEl.classList.add('hidden'); videoSpinnerEl.classList.remove('flex'); + videoReadyEl.classList.replace('hidden', 'flex'); + videoPickerBtn.classList.remove('border-white/10'); + videoPickerBtn.classList.add('border-primary/60'); + videoPickerBtn.title = `${filename} — click to clear`; + }; + + const clearVideoUpload = () => { + uploadedVideoUrl = null; + v2vMode = false; + showVideoIcon(); + selectedModel = t2vModels[0].id; + selectedModelName = t2vModels[0].name; + document.getElementById('v-model-btn-label').textContent = selectedModelName; + updateControlsForModel(selectedModel); + textarea.placeholder = 'Describe the video you want to create'; + textarea.disabled = false; + }; + + videoPickerBtn.onclick = (e) => { + e.stopPropagation(); + if (uploadedVideoUrl) { + clearVideoUpload(); + } else { + videoFileInput.click(); + } + }; + + videoFileInput.onchange = async (e) => { + const file = e.target.files[0]; + if (!file) return; + + const apiKey = localStorage.getItem('muapi_key'); + if (!apiKey) { + AuthModal(() => videoFileInput.click()); + return; + } + + showVideoSpinner(); + try { + const url = await muapi.uploadFile(file); + uploadedVideoUrl = url; + showVideoReady(file.name); + + // Switch to v2v mode + if (imageMode) { + picker.reset(); + uploadedImageUrl = null; + imageMode = false; + } + v2vMode = true; + selectedModel = v2vModels[0].id; + selectedModelName = v2vModels[0].name; + document.getElementById('v-model-btn-label').textContent = selectedModelName; + updateControlsForModel(selectedModel); + textarea.placeholder = 'Video ready — click Generate to remove watermark'; + textarea.disabled = true; + } catch (err) { + console.error('[VideoStudio] Video upload failed:', err); + showVideoIcon(); + alert(`Video upload failed: ${err.message}`); + } + videoFileInput.value = ''; + }; + + topRow.appendChild(videoPickerBtn); + const textarea = document.createElement('textarea'); textarea.placeholder = 'Describe the video you want to create'; textarea.className = 'flex-1 bg-transparent border-none text-white text-base md:text-xl placeholder:text-muted focus:outline-none resize-none pt-2.5 leading-relaxed min-h-[40px] max-h-[150px] md:max-h-[250px] overflow-y-auto custom-scrollbar'; @@ -193,6 +315,17 @@ export function VideoStudio() { const updateControlsForModel = (modelId) => { const model = getCurrentModels().find(m => m.id === modelId); + // In v2v mode, hide all parameter controls — no prompt/AR/duration/etc needed + if (v2vMode) { + arBtn.style.display = 'none'; + durationBtn.style.display = 'none'; + resolutionBtn.style.display = 'none'; + qualityBtn.style.display = 'none'; + extendBanner.classList.add('hidden'); + extendBanner.classList.remove('flex'); + return; + } + // Aspect ratio const availableArs = getCurrentAspectRatios(modelId); if (availableArs.length > 0) { @@ -266,31 +399,72 @@ export function VideoStudio() { `; const list = dropdown.querySelector('#v-model-list-container'); - const renderModels = (filter = '') => { - list.innerHTML = ''; - const filtered = getCurrentModels().filter(m => m.name.toLowerCase().includes(filter.toLowerCase()) || m.id.toLowerCase().includes(filter.toLowerCase())); - filtered.forEach(m => { - const item = document.createElement('div'); - item.className = `flex items-center justify-between p-3.5 hover:bg-white/5 rounded-2xl cursor-pointer transition-all border border-transparent hover:border-white/5 ${selectedModel === m.id ? 'bg-white/5 border-white/5' : ''}`; - item.innerHTML = ` -