diff --git a/src/components/Header.js b/src/components/Header.js index f13abd5..b90d20f 100644 --- a/src/components/Header.js +++ b/src/components/Header.js @@ -1,3 +1,5 @@ +import { SettingsModal } from './SettingsModal.js'; + export function Header(navigate) { const header = document.createElement('header'); header.className = 'w-full flex flex-col z-50 sticky top-0'; @@ -73,8 +75,7 @@ export function Header(navigate) { `; keyBtn.onclick = () => { - localStorage.removeItem('muapi_key'); - window.location.reload(); + document.body.appendChild(SettingsModal()); }; rightPart.appendChild(keyBtn); diff --git a/src/components/VideoStudio.js b/src/components/VideoStudio.js index 33089f9..afc298e 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, v2vModels } from '../lib/models.js'; +import { t2vModels, getAspectRatiosForVideoModel, getDurationsForModel, getResolutionsForVideoModel, i2vModels, getAspectRatiosForI2VModel, getDurationsForI2VModel, getResolutionsForI2VModel, v2vModels, getModesForModel } from '../lib/models.js'; import { AuthModal } from './AuthModal.js'; import { createUploadPicker } from './UploadPicker.js'; import { savePendingJob, removePendingJob, getPendingJobs } from '../lib/pendingJobs.js'; @@ -16,6 +16,7 @@ export function VideoStudio() { let selectedDuration = defaultModel.inputs?.duration?.default || 5; let selectedResolution = defaultModel.inputs?.resolution?.default || ''; let selectedQuality = defaultModel.inputs?.quality?.default || ''; + let selectedMode = ''; let lastGenerationId = null; let lastGenerationModel = null; let dropdownOpen = null; @@ -28,6 +29,7 @@ export function VideoStudio() { const getCurrentAspectRatios = (id) => imageMode ? getAspectRatiosForI2VModel(id) : getAspectRatiosForVideoModel(id); const getCurrentDurations = (id) => imageMode ? getDurationsForI2VModel(id) : getDurationsForModel(id); const getCurrentResolutions = (id) => imageMode ? getResolutionsForI2VModel(id) : getResolutionsForVideoModel(id); + const getCurrentModes = (id) => getModesForModel(id); const getCurrentModel = () => getCurrentModels().find(m => m.id === selectedModel); const getQualitiesForModel = (id) => { const model = getCurrentModels().find(m => m.id === id); @@ -284,11 +286,16 @@ export function VideoStudio() { `, selectedQuality || 'basic', 'v-quality-btn'); + const modeBtn = createControlBtn(` + + `, selectedMode || 'normal', 'v-mode-btn'); + controlsLeft.appendChild(modelBtn); controlsLeft.appendChild(arBtn); controlsLeft.appendChild(durationBtn); controlsLeft.appendChild(resolutionBtn); controlsLeft.appendChild(qualityBtn); + controlsLeft.appendChild(modeBtn); // Initial visibility (t2v mode) const initDurations = getDurationsForModel(defaultModel.id); @@ -296,6 +303,7 @@ export function VideoStudio() { const initResolutions = getResolutionsForVideoModel(defaultModel.id); resolutionBtn.style.display = initResolutions.length > 0 ? 'flex' : 'none'; qualityBtn.style.display = 'none'; + modeBtn.style.display = getModesForModel(defaultModel.id).length > 0 ? 'flex' : 'none'; const generateBtn = document.createElement('button'); generateBtn.className = 'bg-primary text-black px-6 md:px-8 py-3 md:py-3.5 rounded-xl md:rounded-[1.5rem] font-black text-sm md:text-base hover:shadow-glow hover:scale-105 active:scale-95 transition-all flex items-center justify-center gap-2.5 w-full sm:w-auto shadow-lg'; @@ -322,6 +330,7 @@ export function VideoStudio() { durationBtn.style.display = 'none'; resolutionBtn.style.display = 'none'; qualityBtn.style.display = 'none'; + modeBtn.style.display = 'none'; extendBanner.classList.add('hidden'); extendBanner.classList.remove('flex'); return; @@ -368,6 +377,17 @@ export function VideoStudio() { qualityBtn.style.display = 'none'; } + // Mode + const modes = getCurrentModes(modelId); + if (modes.length > 0) { + selectedMode = model?.inputs?.mode?.default || modes[0]; + document.getElementById('v-mode-btn-label').textContent = selectedMode; + modeBtn.style.display = 'flex'; + } else { + selectedMode = ''; + modeBtn.style.display = 'none'; + } + // Extend banner (extend model only) if (model?.requiresRequestId) { extendBanner.classList.remove('hidden'); @@ -568,6 +588,28 @@ export function VideoStudio() { list.appendChild(item); }); dropdown.appendChild(list); + + } else if (type === 'mode') { + dropdown.classList.add('max-w-[200px]'); + dropdown.innerHTML = `
Mode
`; + const list = document.createElement('div'); + list.className = 'flex flex-col gap-1'; + getCurrentModes(selectedModel).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 group'; + item.innerHTML = ` + ${m} + ${selectedMode === m ? '' : ''} + `; + item.onclick = (e) => { + e.stopPropagation(); + selectedMode = m; + document.getElementById('v-mode-btn-label').textContent = m; + closeDropdown(); + }; + list.appendChild(item); + }); + dropdown.appendChild(list); } // Position dropdown @@ -600,6 +642,7 @@ export function VideoStudio() { durationBtn.onclick = toggleDropdown('duration', durationBtn); resolutionBtn.onclick = toggleDropdown('resolution', resolutionBtn); qualityBtn.onclick = toggleDropdown('quality', qualityBtn); + modeBtn.onclick = toggleDropdown('mode', modeBtn); window.addEventListener('click', closeDropdown); container.appendChild(dropdown); @@ -934,6 +977,7 @@ export function VideoStudio() { const resolutions = getCurrentResolutions(selectedModel); if (resolutions.length > 0) i2vParams.resolution = selectedResolution; if (selectedQuality) i2vParams.quality = selectedQuality; + if (selectedMode) i2vParams.mode = selectedMode; const res = await muapi.generateI2V(i2vParams); console.log('[VideoStudio] I2V response:', res); @@ -976,6 +1020,7 @@ export function VideoStudio() { if (resolutions.length > 0) params.resolution = selectedResolution; if (selectedQuality) params.quality = selectedQuality; + if (selectedMode) params.mode = selectedMode; const res = await muapi.generateVideo(params); diff --git a/src/lib/models.js b/src/lib/models.js index 54f643f..f7774f5 100644 --- a/src/lib/models.js +++ b/src/lib/models.js @@ -7961,6 +7961,14 @@ export const getResolutionsForI2VModel = (modelId) => { return []; }; +export const getModesForModel = (modelId) => { + const model = [...t2vModels, ...i2vModels].find(m => m.id === modelId); + if (!model) return []; + const modeInput = model.inputs?.mode; + if (modeInput?.enum) return modeInput.enum; + return []; +}; + export const getResolutionsForI2IModel = (modelId) => { const model = getI2IModelById(modelId); if (!model) return []; diff --git a/src/lib/muapi.js b/src/lib/muapi.js index 4d88729..3bd5cc2 100644 --- a/src/lib/muapi.js +++ b/src/lib/muapi.js @@ -183,6 +183,7 @@ export class MuapiClient { if (params.duration) finalPayload.duration = params.duration; if (params.resolution) finalPayload.resolution = params.resolution; if (params.quality) finalPayload.quality = params.quality; + if (params.mode) finalPayload.mode = params.mode; if (params.image_url) finalPayload.image_url = params.image_url; console.log('[Muapi] Video Request:', url); @@ -329,6 +330,7 @@ export class MuapiClient { if (params.duration) finalPayload.duration = params.duration; if (params.resolution) finalPayload.resolution = params.resolution; if (params.quality) finalPayload.quality = params.quality; + if (params.mode) finalPayload.mode = params.mode; console.log('[Muapi] I2V Request:', url); console.log('[Muapi] I2V Payload:', finalPayload);