"use client"; import { useState, useRef } from 'react'; import { PhotoPreviewSection } from '../PhotoPreview'; import type { PhotoItem, PhotoType } from '../PhotoPreview/types'; import { PlusOutlined, UserOutlined, CameraOutlined, BulbOutlined, ArrowRightOutlined, SettingOutlined, } from '@ant-design/icons'; import { Dropdown } from 'antd'; import { ConfigPanel } from './ConfigPanel'; import { MobileConfigModal } from './MobileConfigModal'; import { AddItemModal } from './AddItemModal'; import { defaultConfig } from './config-options'; import type { ConfigOptions } from './config-options'; import { useDeviceType } from '@/hooks/useDeviceType'; import { MovieProjectService, MovieProjectMode } from '@/app/service/Interaction/MovieProjectService'; import type { CreateMovieProjectV4Request } from '@/api/DTO/movie_start_dto'; import { getCurrentUser } from '@/lib/auth'; import { useRouter } from 'next/navigation'; export default function VideoCreationForm() { const [photos, setPhotos] = useState([]); const [inputText, setInputText] = useState(''); const [configOptions, setConfigOptions] = useState(defaultConfig); const [configModalVisible, setConfigModalVisible] = useState(false); const [addItemModalVisible, setAddItemModalVisible] = useState(false); const [currentItemType, setCurrentItemType] = useState(null); const [editingIndex, setEditingIndex] = useState(null); const [replacingIndex, setReplacingIndex] = useState(null); const [isCreating, setIsCreating] = useState(false); const { isMobile, isDesktop } = useDeviceType(); const router = useRouter(); const characterInputRef = useRef(null); const sceneInputRef = useRef(null); const propInputRef = useRef(null); /** Handle file upload */ const handleFileUpload = (event: React.ChangeEvent, type: PhotoType) => { const files = event.target.files; if (!files || files.length === 0) return; // Check if we're replacing an existing photo if (replacingIndex !== null) { const file = files[0]; setPhotos(prevPhotos => { const updatedPhotos = [...prevPhotos]; updatedPhotos[replacingIndex] = { ...updatedPhotos[replacingIndex], url: URL.createObjectURL(file), }; return updatedPhotos; }); setReplacingIndex(null); } else { // Add new photos const newPhotos: PhotoItem[] = Array.from(files).map((file, index) => ({ url: URL.createObjectURL(file), type, id: `${type}-${Date.now()}-${index}`, })); setPhotos(prevPhotos => [...prevPhotos, ...newPhotos]); } event.target.value = ''; }; /** Handle configuration change */ const handleConfigChange = ( key: K, value: ConfigOptions[K] ) => { setConfigOptions(prev => ({ ...prev, [key]: value })); }; /** Handle editing a photo */ const handleEditPhoto = (index: number) => { const photo = photos[index]; setCurrentItemType(photo.type); setEditingIndex(index); setAddItemModalVisible(true); }; /** Handle replacing a photo */ const handleReplacePhoto = (index: number) => { const photo = photos[index]; setReplacingIndex(index); // Trigger the appropriate file input based on photo type switch (photo.type) { case 'character': characterInputRef.current?.click(); break; case 'scene': sceneInputRef.current?.click(); break; case 'prop': propInputRef.current?.click(); break; } }; /** Handle adding photos by type */ const handleAddPhotoByType = (type: PhotoType) => { // Reset replacing index to ensure we're adding, not replacing setReplacingIndex(null); // Trigger the appropriate file input switch (type) { case 'character': characterInputRef.current?.click(); break; case 'scene': sceneInputRef.current?.click(); break; case 'prop': propInputRef.current?.click(); break; } }; /** Handle item submission from modal */ const handleItemSubmit = (data: { name: string; description: string; file: File; index?: number }) => { if (!currentItemType) return; // Check if we're editing or adding if (typeof data.index === 'number' && editingIndex !== null) { // Edit mode - update existing photo setPhotos(prevPhotos => { const updatedPhotos = [...prevPhotos]; const existingPhoto = updatedPhotos[editingIndex]; updatedPhotos[editingIndex] = { ...existingPhoto, // Only update URL if a new file was selected (not the dummy file) ...(data.file.size > 0 && { url: URL.createObjectURL(data.file) }), ...(data.name && { name: data.name }), ...(data.description && { description: data.description }), }; return updatedPhotos; }); console.log('Updated item:', { ...data, type: currentItemType, index: editingIndex }); } else { // Add mode - create new photo const newPhoto: PhotoItem = { url: URL.createObjectURL(data.file), type: currentItemType, id: `${currentItemType}-${Date.now()}`, ...(data.name && { name: data.name }), ...(data.description && { description: data.description }), }; setPhotos(prevPhotos => [...prevPhotos, newPhoto]); console.log('Added item:', { ...data, type: currentItemType }); } }; /** Handle video creation */ const handleCreate = async () => { if (isCreating) return; if (!inputText.trim()) { window.msg?.warning('Please enter your story description'); return; } try { setIsCreating(true); const user = getCurrentUser(); if (!user?.id) { window.msg?.error('Please login first'); return; } /** Separate photos by type */ const characterPhotos = photos.filter(p => p.type === 'character'); const scenePhotos = photos.filter(p => p.type === 'scene'); const propPhotos = photos.filter(p => p.type === 'prop'); /** Build request parameters */ const requestParams: CreateMovieProjectV4Request = { script: inputText, mode: configOptions.mode, resolution: '720p', language: configOptions.language, aspect_ratio: configOptions.aspect_ratio, expansion_mode: configOptions.expansion_mode, video_duration: configOptions.videoDuration, is_image_to_video: photos.length > 0, pcode: configOptions.pcode === 'portrait' ? '' : configOptions.pcode, }; /** Add character briefs if exists */ if (characterPhotos.length > 0) { requestParams.character_briefs = characterPhotos.map(photo => ({ character_name: photo.name || 'Character', character_description: photo.description || '', image_url: photo.url, })); } /** Add scene briefs if exists */ if (scenePhotos.length > 0) { requestParams.scene_briefs = scenePhotos.map(photo => ({ scene_name: photo.name || 'Scene', scene_description: photo.description || '', image_url: photo.url, scene_type: 'custom', })); } /** Add prop briefs if exists */ if (propPhotos.length > 0) { requestParams.prop_briefs = propPhotos.map(photo => ({ prop_name: photo.name || 'Prop', prop_description: photo.description || '', image_url: photo.url, prop_type: 'custom', })); } console.log('Creating video with params:', requestParams); /** Call MovieProjectService V4 API */ const result = await MovieProjectService.createProject( MovieProjectMode.V4, requestParams ); console.log('Video creation successful, project_id:', result.project_id); window.msg?.success(`Video created successfully! Project ID: ${result.project_id}`); router.push(`/movies/work-flow?episodeId=${result.project_id}`); /** TODO: Navigate to project detail page or next step */ // window.location.href = `/movies/${result.project_id}`; } catch (error) { console.error('Failed to create video:', error); if (error instanceof Error && error.message !== '操作已取消') { window.msg?.error(error.message || 'Failed to create video'); } } finally { setIsCreating(false); } }; return (
{/* Main Content Area with Border */}
{/* Photo Preview Section - Top */} {photos.length > 0 && (
)} {/* Text Input Area - Middle */}