"use client"; import { useState, useRef, useEffect, useMemo } 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, Popover } 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 { Eye, Check, ArrowRight, X } from 'lucide-react'; import TemplatePreviewModal from '@/components/common/TemplatePreviewModal'; import { PcTemplateModal } from "@/components/ChatInputBox/PcTemplateModal" 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'; import { useTemplateStoryServiceHook } from '@/app/service/Interaction/templateStoryService'; import { StoryTemplateEntity } from '@/app/service/domain/Entities'; import { useUploadFile } from '@/app/service/domain/service'; 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); // Template modal states (align with FamousTemplate usage) const [isModalOpen, setIsModalOpen] = useState(false); const [initialTemplateId, setInitialTemplateId] = useState(undefined); const [isTemplateCreating, setIsTemplateCreating] = useState(false); const [isRoleGenerating, setIsRoleGenerating] = useState<{ [key: string]: boolean }>({}); const [isItemGenerating, setIsItemGenerating] = useState<{ [key: string]: boolean }>({}); const [currentTemplate, setCurrentTemplate] = useState(null); const [inputPlaceholder, setInputPlaceholder] = useState(''); const [templateTitle, setTemplateTitle] = useState(''); const [isMentionOpen, setIsMentionOpen] = useState(false); const { isMobile, isDesktop } = useDeviceType(); const router = useRouter(); /** Template list for mention popover */ const { templateStoryList, getTemplateStoryList } = useTemplateStoryServiceHook(); const isTemplateSelected = useMemo(() => { return templateStoryList.find(i => i.pcode === configOptions.pcode); }, [configOptions.pcode, templateStoryList]); useEffect(() => { void getTemplateStoryList(); }, [getTemplateStoryList]); /** Clear inputText when currentTemplate changes */ useEffect(() => { if (currentTemplate) { setInputText(''); } }, [currentTemplate]); /** Determine if input textarea should be shown based on selected template's freeInput */ const shouldShowInput = useMemo(() => { if (!isTemplateSelected) { return true; } return isTemplateSelected.freeInput && isTemplateSelected.freeInput.length > 0; }, [isTemplateSelected]); const characterInputRef = useRef(null); const sceneInputRef = useRef(null); const propInputRef = useRef(null); const { uploadFile } = useUploadFile(); /** Clear current template related states */ const clearTemplateSelection = () => { handleConfigChange('pcode', ''); setInputPlaceholder(''); setTemplateTitle(''); setPhotos([]); }; /** Apply selected template to current form state */ const applyTemplateSelection = (template: StoryTemplateEntity) => { const characterPhotos = (template.storyRole || []).map((role, index) => ({ url: role.photo_url, type: 'character' as const, id: `character-${Date.now()}-${index}`, name: role.role_name, description: role.role_description, })).filter(p => p.url); setPhotos(prev => [...prev, ...characterPhotos]); setInputPlaceholder(template.generateText || template.name); setTemplateTitle(template.name); handleConfigChange('pcode', template.pcode || ''); }; /** 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]; void (async () => { try { const uploadedUrl = await uploadFile(file); setPhotos(prevPhotos => { const updatedPhotos = [...prevPhotos]; updatedPhotos[replacingIndex] = { ...updatedPhotos[replacingIndex], url: uploadedUrl, }; return updatedPhotos; }); } finally { setReplacingIndex(null); } })(); } else { // Add new photos void (async () => { const fileArray = Array.from(files); const uploadedUrls = await Promise.all(fileArray.map((file) => uploadFile(file))); const newPhotos: PhotoItem[] = uploadedUrls.map((url, index) => ({ url, type, id: `${type}-${Date.now()}-${index}`, })); setPhotos(prevPhotos => [...prevPhotos, ...newPhotos]); })(); } event.target.value = ''; }; /** Handle configuration change */ const handleConfigChange = ( key: K, value: ConfigOptions[K], exclude?: boolean ) => { if (exclude && key === 'pcode') { clearTemplateSelection(); } if (key === 'videoDuration') { if (value === '8s') { setConfigOptions(prev => ({ ...prev, expansion_mode: false })); } else { setConfigOptions(prev => ({ ...prev, expansion_mode: true })); } } 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 void (async () => { let uploadedUrl: string | undefined; if (data.file.size > 0) { uploadedUrl = await uploadFile(data.file); } setPhotos(prevPhotos => { const updatedPhotos = [...prevPhotos]; const existingPhoto = updatedPhotos[editingIndex]; updatedPhotos[editingIndex] = { ...existingPhoto, ...(uploadedUrl ? { url: uploadedUrl } : {}), ...(data.name && { name: data.name }), ...(data.description && { description: data.description }), }; return updatedPhotos; }); })(); } else { // Add mode - create new photo void (async () => { const uploadedUrl = await uploadFile(data.file); const newPhoto: PhotoItem = { url: uploadedUrl, type: currentItemType, id: `${currentItemType}-${Date.now()}`, ...(data.name && { name: data.name }), ...(data.description && { description: data.description }), }; setPhotos(prevPhotos => [...prevPhotos, newPhoto]); })(); } }; /** Handle video creation */ const handleCreate = async () => { if (isCreating) return; if (shouldShowInput && !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, use_img2video: 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 */}
{templateTitle && (
{templateTitle}
)} {/* Photo Preview Section - Top */} {photos.length > 0 && (
)} {/* Text Input Area - Middle */} {shouldShowInput && (