forked from 77media/video-flow
691 lines
34 KiB
TypeScript
691 lines
34 KiB
TypeScript
"use client";
|
|
|
|
import { useState, useRef, useEffect, useMemo } from 'react';
|
|
import { PhotoPreviewSection } from '../PhotoPreview';
|
|
import type { PhotoItem, PhotoType } from '../PhotoPreview/types';
|
|
import { ActionButton } from '@/components/common/ActionButton';
|
|
import {
|
|
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 { CircleArrowRight, X, Clapperboard } 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';
|
|
import { useAppDispatch, useAppSelector } from '@/lib/store/hooks';
|
|
import { clearSelection } from '@/lib/store/creationTemplateSlice';
|
|
import { Tooltip } from "antd";
|
|
import { useTypewriterText } from '@/hooks/useTypewriterText';
|
|
|
|
export default function VideoCreationForm() {
|
|
const [photos, setPhotos] = useState<PhotoItem[]>([]);
|
|
const [inputText, setInputText] = useState('');
|
|
const [configOptions, setConfigOptions] = useState<ConfigOptions>(defaultConfig);
|
|
const [configModalVisible, setConfigModalVisible] = useState(false);
|
|
const [addItemModalVisible, setAddItemModalVisible] = useState(false);
|
|
const [currentItemType, setCurrentItemType] = useState<PhotoType | null>(null);
|
|
const [editingIndex, setEditingIndex] = useState<number | null>(null);
|
|
const [replacingIndex, setReplacingIndex] = useState<number | null>(null);
|
|
const [isCreating, setIsCreating] = useState(false);
|
|
// Template modal states (align with FamousTemplate usage)
|
|
const [isModalOpen, setIsModalOpen] = useState(false);
|
|
const [initialTemplateId, setInitialTemplateId] = useState<string | undefined>(undefined);
|
|
const [isTemplateCreating, setIsTemplateCreating] = useState(false);
|
|
const [isRoleGenerating, setIsRoleGenerating] = useState<{ [key: string]: boolean }>({});
|
|
const [isItemGenerating, setIsItemGenerating] = useState<{ [key: string]: boolean }>({});
|
|
const [currentTemplate, setCurrentTemplate] = useState<StoryTemplateEntity | null>(null);
|
|
const [generateText, setGenerateText] = useState<string>('');
|
|
const [templateTitle, setTemplateTitle] = useState<string>('');
|
|
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]);
|
|
|
|
/** Dynamic placeholder typing from template freeInput tips */
|
|
const placeholderList = useMemo(() => {
|
|
if (isTemplateSelected?.freeInput?.length) {
|
|
return (isTemplateSelected.freeInput
|
|
.map((i) => i.user_tips)
|
|
.filter(Boolean) as string[]);
|
|
}
|
|
return ['Describe the story you want to make...'];
|
|
}, [isTemplateSelected]);
|
|
|
|
const dynamicPlaceholder = useTypewriterText(placeholderList, {
|
|
typingMs: 36,
|
|
pauseMs: 2200,
|
|
resetMs: 300,
|
|
});
|
|
|
|
const characterInputRef = useRef<HTMLInputElement>(null);
|
|
const sceneInputRef = useRef<HTMLInputElement>(null);
|
|
const propInputRef = useRef<HTMLInputElement>(null);
|
|
const mainTextInputRef = useRef<HTMLTextAreaElement>(null);
|
|
const { uploadFile } = useUploadFile();
|
|
const dispatch = useAppDispatch();
|
|
const selectedTemplateId = useAppSelector(state => state.creationTemplate.selectedTemplateId);
|
|
|
|
useEffect(() => {
|
|
setTimeout(() => {
|
|
mainTextInputRef.current?.focus();
|
|
}, 0);
|
|
}, [mainTextInputRef]);
|
|
/** Clear current template related states */
|
|
const clearTemplateSelection = () => {
|
|
handleConfigChange('pcode', '');
|
|
setGenerateText('');
|
|
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]);
|
|
setGenerateText(template.generateText || template.name);
|
|
setTemplateTitle(template.name);
|
|
handleConfigChange('pcode', template.pcode || '');
|
|
setTimeout(() => {
|
|
mainTextInputRef.current?.focus();
|
|
}, 0);
|
|
};
|
|
|
|
/** Apply template selected from global store (e.g., FamousTemplate) */
|
|
useEffect(() => {
|
|
if (!selectedTemplateId) return;
|
|
const selected = templateStoryList.find(t => (t.id || t.template_id) === selectedTemplateId);
|
|
if (!selected) return;
|
|
clearTemplateSelection();
|
|
applyTemplateSelection(selected as StoryTemplateEntity);
|
|
dispatch(clearSelection());
|
|
}, [selectedTemplateId, templateStoryList]);
|
|
|
|
/** Handle file upload */
|
|
const handleFileUpload = (event: React.ChangeEvent<HTMLInputElement>, 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 = <K extends keyof ConfigOptions>(
|
|
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: isTemplateSelected ? 'unlimited' : 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<CreateMovieProjectV4Request>(
|
|
MovieProjectMode.V4,
|
|
requestParams
|
|
);
|
|
|
|
console.log('Video creation successful, 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 (
|
|
<div data-alt="video-creation-form" className="w-full h-full flex flex-col">
|
|
{/* Main Content Area with Border */}
|
|
<div
|
|
data-alt="content-container"
|
|
className="flex-1 pt-2 border border-white/10 rounded-3xl bg-gradient-to-br from-[#1a1a1a]/50 to-[#0a0a0a]/50 backdrop-blur-sm overflow-hidden flex flex-col"
|
|
>
|
|
{templateTitle && (
|
|
<div data-alt="template-title" className="px-4 flex items-center">
|
|
<div data-alt="template-title-text" className="text-white text-xs font-bold truncate">
|
|
{templateTitle}
|
|
</div>
|
|
<button
|
|
data-alt="clear-template"
|
|
className="ml-2 p-1 rounded hover:bg-white/10 text-gray-400 hover:text-white"
|
|
onClick={() => {
|
|
clearTemplateSelection();
|
|
}}
|
|
>
|
|
<X className="w-3 h-3" />
|
|
</button>
|
|
</div>
|
|
)}
|
|
{/* Photo Preview Section - Top */}
|
|
{photos.length > 0 && (
|
|
<div data-alt="photo-preview-wrapper" className="pt-2 pl-4 pb-0">
|
|
<PhotoPreviewSection
|
|
photos={photos}
|
|
onEdit={handleEditPhoto}
|
|
onReplace={handleReplacePhoto}
|
|
/>
|
|
</div>
|
|
)}
|
|
{/* Template Description */}
|
|
{generateText && (
|
|
<div data-alt="template-description-wrapper" className="px-4 pt-1">
|
|
<div data-alt="template-description-text" className="text-white/25 text-sm">{generateText}</div>
|
|
</div>
|
|
)}
|
|
{/* Text Input Area - Middle */}
|
|
{shouldShowInput && (
|
|
<div data-alt="text-input-wrapper" className="flex-1 flex px-4 py-2">
|
|
<textarea
|
|
data-alt="main-text-input"
|
|
ref={mainTextInputRef}
|
|
className="w-full h-full bg-transparent text-gray-300 text-base placeholder:italic placeholder-gray-400 resize-none outline-none border-none"
|
|
style={{
|
|
minHeight: '6.25rem'
|
|
}}
|
|
placeholder={dynamicPlaceholder}
|
|
value={inputText}
|
|
onChange={(e) => setInputText(e.target.value)}
|
|
/>
|
|
</div>
|
|
)}
|
|
|
|
{/* Control Panel - Bottom */}
|
|
<div data-alt="control-panel" className="pl-4 pr-2 py-2 flex items-center justify-between gap-2">
|
|
{/* Left Side - Upload and Options */}
|
|
<div data-alt="left-controls" className="flex items-center gap-2">
|
|
{/* Upload Button with Dropdown */}
|
|
{/* <Dropdown
|
|
menu={{
|
|
items: [
|
|
{
|
|
key: 'character',
|
|
icon: <UserOutlined className="text-cyan-400" />,
|
|
label: <span className="text-gray-300">Character</span>,
|
|
onClick: () => handleAddPhotoByType('character'),
|
|
},
|
|
{
|
|
key: 'scene',
|
|
icon: <CameraOutlined className="text-cyan-400" />,
|
|
label: <span className="text-gray-300">Scene</span>,
|
|
onClick: () => handleAddPhotoByType('scene'),
|
|
},
|
|
{
|
|
key: 'prop',
|
|
icon: <BulbOutlined className="text-cyan-400" />,
|
|
label: <span className="text-gray-300">Prop</span>,
|
|
onClick: () => handleAddPhotoByType('prop'),
|
|
},
|
|
],
|
|
className: 'bg-[#1a1a1a] border border-white/10'
|
|
}}
|
|
trigger={['click']}
|
|
>
|
|
<button
|
|
data-alt="upload-button"
|
|
className="w-8 h-8 rounded-full border border-white/20 bg-transparent hover:bg-white/5 hover:border-cyan-400/60 transition-all duration-200 flex items-center justify-center text-gray-300 hover:text-cyan-400"
|
|
>
|
|
<PlusOutlined className="text-base font-bold" />
|
|
</button>
|
|
</Dropdown> */}
|
|
|
|
{/* Hidden file inputs */}
|
|
<input
|
|
ref={characterInputRef}
|
|
type="file"
|
|
accept="image/*"
|
|
multiple
|
|
className="hidden"
|
|
onChange={(e) => handleFileUpload(e, 'character')}
|
|
/>
|
|
<input
|
|
ref={sceneInputRef}
|
|
type="file"
|
|
accept="image/*"
|
|
multiple
|
|
className="hidden"
|
|
onChange={(e) => handleFileUpload(e, 'scene')}
|
|
/>
|
|
<input
|
|
ref={propInputRef}
|
|
type="file"
|
|
accept="image/*"
|
|
multiple
|
|
className="hidden"
|
|
onChange={(e) => handleFileUpload(e, 'prop')}
|
|
/>
|
|
|
|
{/* Mention Button with Popover (template list) */}
|
|
<Popover
|
|
placement="topLeft"
|
|
trigger={["click"]}
|
|
arrow={false}
|
|
open={isMentionOpen}
|
|
onOpenChange={(open) => setIsMentionOpen(open)}
|
|
content={
|
|
<div
|
|
data-alt="mention-popover-content"
|
|
className="w-64 max-h-64 overflow-y-auto"
|
|
>
|
|
{templateStoryList && templateStoryList.length > 0 ? (
|
|
<div className="grid grid-cols-1">
|
|
{templateStoryList.map((tpl) => {
|
|
const tplKey = (tpl as any).id || (tpl as any).template_id;
|
|
return (
|
|
<div
|
|
key={tplKey}
|
|
data-alt={`mention-template-item-${tplKey}`}
|
|
className="group relative px-3 py-2 rounded-lg text-gray-200 hover:bg-white/10 transition-all duration-300 text-sm overflow-hidden hover:h-[106px]"
|
|
>
|
|
{/* Background image on hover */}
|
|
{Array.isArray((tpl as any).image_url) && (tpl as any).image_url.length > 0 && (
|
|
<img
|
|
data-alt="mention-template-bg"
|
|
src={(tpl as any).image_url[0]}
|
|
alt=""
|
|
loading="lazy"
|
|
aria-hidden="true"
|
|
className="absolute inset-0 w-full h-full object-cover opacity-0 group-hover:opacity-100 transition-opacity duration-300"
|
|
/>
|
|
)}
|
|
{/* Dark overlay for readability on hover */}
|
|
<div
|
|
data-alt="mention-template-overlay"
|
|
className="pointer-events-none absolute inset-0 bg-black/70 opacity-0 group-hover:opacity-100 transition-opacity duration-300"
|
|
/>
|
|
<div className={`relative z-20 flex items-center gap-3 cursor-pointer h-full ${tpl.pcode === configOptions.pcode ? 'text-custom-blue' : ''}`}
|
|
onClick={() => {
|
|
setIsMentionOpen(false);
|
|
const url = (tpl as any).show_url as string | undefined;
|
|
if (url) {
|
|
setCurrentTemplate(tpl as StoryTemplateEntity);
|
|
} else {
|
|
setInitialTemplateId(tplKey);
|
|
setIsModalOpen(true);
|
|
}
|
|
}}
|
|
>
|
|
<span className="truncate group-hover:hidden">{tpl.name}</span>
|
|
{/* Floating actions on hover, right-aligned and overlaying the name */}
|
|
<div className="absolute right-0 top-1/2 -translate-y-1/2 flex items-center gap-1 opacity-0 group-hover:opacity-100 pointer-events-none group-hover:pointer-events-auto transition-opacity z-30">
|
|
<button
|
|
data-alt={`mention-template-use-${tplKey}`}
|
|
className="px-0 py-1 text-xs rounded-full hover:text-custom-blue/80"
|
|
onClick={(e) => {
|
|
e.stopPropagation();
|
|
setIsMentionOpen(false);
|
|
clearTemplateSelection();
|
|
const selected = tpl as StoryTemplateEntity;
|
|
applyTemplateSelection(selected);
|
|
}}
|
|
>
|
|
<CircleArrowRight className="w-5 h-5" />
|
|
</button>
|
|
</div>
|
|
{/* Description at bottom on hover */}
|
|
<div
|
|
data-alt="mention-template-desc"
|
|
className="absolute top-0 left-0 right-0 bottom-0 text-xs text-white/90 opacity-0 group-hover:opacity-100 translate-y-2 group-hover:translate-y-0 transition-all"
|
|
>
|
|
<div
|
|
data-alt="mention-template-desc-scroll"
|
|
className="overflow-hidden"
|
|
>
|
|
<div className="pr-5 line-clamp-5">{(tpl as any).generateText || tpl.name}</div>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
);
|
|
})}
|
|
</div>
|
|
) : (
|
|
<div data-alt="mention-no-templates" className="text-gray-400 text-sm">No Avaliable Templates</div>
|
|
)}
|
|
</div>
|
|
}
|
|
>
|
|
<Tooltip placement="top" title="Inspiration Lab">
|
|
<button
|
|
data-alt="mention-button"
|
|
className={`w-8 h-8 rounded-full border border-white/20 bg-transparent hover:bg-white/5 hover:border-cyan-400/60 transition-all duration-200 flex items-center justify-center text-gray-300 hover:text-cyan-400 ${isTemplateSelected ? 'text-yellow-500' : ''}`}
|
|
>
|
|
<span className="text-base font-bold">@</span>
|
|
</button>
|
|
</Tooltip>
|
|
</Popover>
|
|
|
|
{/* Configuration - Desktop: Full Panel, Mobile: Setting Icon */}
|
|
{isDesktop ? (
|
|
<ConfigPanel
|
|
configOptions={configOptions}
|
|
onConfigChange={handleConfigChange}
|
|
isMobile={isMobile}
|
|
isDesktop={isDesktop}
|
|
disableDuration={isTemplateSelected ? true : false}
|
|
/>
|
|
) : (
|
|
<button
|
|
data-alt="settings-button"
|
|
className="w-8 h-8 rounded-full border border-white/20 bg-transparent hover:bg-white/5 hover:border-cyan-400/60 transition-all duration-200 flex items-center justify-center text-gray-300 hover:text-cyan-400"
|
|
onClick={() => setConfigModalVisible(true)}
|
|
>
|
|
<SettingOutlined className="text-base" />
|
|
</button>
|
|
)}
|
|
</div>
|
|
|
|
{/* Right Side - Create Button */}
|
|
<ActionButton
|
|
isCreating={isCreating}
|
|
handleCreateVideo={handleCreate}
|
|
icon={<Clapperboard className="text-base font-bold w-5 h-5" />}
|
|
width="w-10"
|
|
height="h-10"
|
|
/>
|
|
</div>
|
|
</div>
|
|
|
|
{/* Mobile Configuration Modal */}
|
|
<MobileConfigModal
|
|
visible={configModalVisible}
|
|
onClose={() => setConfigModalVisible(false)}
|
|
configOptions={configOptions}
|
|
onConfigChange={handleConfigChange}
|
|
disableDuration={isTemplateSelected ? true : false}
|
|
/>
|
|
|
|
{/* Add Item Modal */}
|
|
<AddItemModal
|
|
visible={addItemModalVisible}
|
|
onClose={() => {
|
|
setAddItemModalVisible(false);
|
|
setCurrentItemType(null);
|
|
setEditingIndex(null);
|
|
}}
|
|
itemType={currentItemType}
|
|
onSubmit={handleItemSubmit}
|
|
editData={
|
|
editingIndex !== null && photos[editingIndex]
|
|
? {
|
|
index: editingIndex,
|
|
name: photos[editingIndex].name,
|
|
description: photos[editingIndex].description,
|
|
imageUrl: photos[editingIndex].url,
|
|
}
|
|
: undefined
|
|
}
|
|
/>
|
|
<PcTemplateModal
|
|
isTemplateCreating={isTemplateCreating}
|
|
setIsTemplateCreating={setIsTemplateCreating}
|
|
isRoleGenerating={isRoleGenerating}
|
|
setIsRoleGenerating={setIsRoleGenerating}
|
|
isItemGenerating={isItemGenerating}
|
|
setIsItemGenerating={setIsItemGenerating}
|
|
isOpen={isModalOpen}
|
|
onClose={() => setIsModalOpen(false)}
|
|
initialTemplateId={initialTemplateId}
|
|
configOptions={{ mode: "auto", resolution: "720p", language: "english", videoDuration: "auto" }}
|
|
/>
|
|
<TemplatePreviewModal
|
|
open={!!currentTemplate && !!currentTemplate.show_url}
|
|
videoUrl={currentTemplate?.show_url || ''}
|
|
onClose={() => setCurrentTemplate(null)}
|
|
title={currentTemplate?.name || ''}
|
|
description={currentTemplate?.generateText || currentTemplate?.name}
|
|
onPrimaryAction={() => {
|
|
if (!currentTemplate) return;
|
|
clearTemplateSelection();
|
|
applyTemplateSelection(currentTemplate);
|
|
setCurrentTemplate(null)
|
|
}}
|
|
primaryLabel="Try this"
|
|
/>
|
|
</div>
|
|
);
|
|
}
|
|
|