video-flow-b/components/pages/create-to-video2.tsx
2025-07-18 07:05:47 +08:00

700 lines
29 KiB
TypeScript
Raw Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

"use client";
import { useState, useEffect, useRef, useCallback } from 'react';
import { Select, SelectContent, SelectItem, SelectTrigger, SelectValue } from '@/components/ui/select';
import { ArrowLeft, ChevronDown, ChevronUp, Video, ListOrdered, Play, Loader2, Pause, MoreHorizontal, Edit2, Check, X, RefreshCw, Lightbulb, Package, Crown, ArrowUp, Search, Filter, Grid, Grid3X3, Calendar, Clock, Eye, Heart, Share2 } from 'lucide-react';
import { useRouter, useSearchParams } from 'next/navigation';
import { Sheet, SheetContent, SheetHeader, SheetTitle, SheetTrigger, SheetClose } from "@/components/ui/sheet";
import { Input } from "@/components/ui/input";
import './style/create-to-video2.css';
import { Dropdown, Menu } from 'antd';
import type { MenuProps } from 'antd';
import dynamic from 'next/dynamic';
import { ModeEnum, ResolutionEnum } from "@/app/model/enums";
import { createScriptEpisodeNew, getScriptEpisodeListNew, CreateScriptEpisodeRequest } from "@/api/script_episode";
import { getUploadToken, uploadToQiniu } from "@/api/common";
import { EmptyStateAnimation } from '@/components/common/EmptyStateAnimation2';
const ideaText = 'a cute capybara with an orange on its head, staring into the distance and walking forward';
export function CreateToVideo2() {
const router = useRouter();
const searchParams = useSearchParams();
const projectId = searchParams.get('projectId') ? parseInt(searchParams.get('projectId')!) : 0;
const [isClient, setIsClient] = useState(false);
const [isExpanded, setIsExpanded] = useState(false);
const [videoUrl, setVideoUrl] = useState('');
const [isUploading, setIsUploading] = useState(false);
const containerRef = useRef<HTMLDivElement>(null);
const [activeTab, setActiveTab] = useState('script');
const [isFocus, setIsFocus] = useState(false);
const [selectedMode, setSelectedMode] = useState<ModeEnum>(ModeEnum.AUTOMATIC);
const [selectedResolution, setSelectedResolution] = useState<ResolutionEnum>(ResolutionEnum.HD_720P);
const [script, setInputText] = useState('');
const editorRef = useRef<HTMLDivElement>(null);
const [runTour, setRunTour] = useState(true);
const [episodeId, setEpisodeId] = useState<number>(0);
const [isCreating, setIsCreating] = useState(false);
const [generatedVideoList, setGeneratedVideoList] = useState<any[]>([]);
const [projectName, setProjectName] = useState('默认名称');
const [episodeList, setEpisodeList] = useState<any[]>([]);
const [currentPage, setCurrentPage] = useState(1);
const [totalPage, setTotalPage] = useState(1);
const [limit, setLimit] = useState(12);
const [isLoading, setIsLoading] = useState(false);
const [hasMore, setHasMore] = useState(true);
const [searchQuery, setSearchQuery] = useState('');
const [filterStatus, setFilterStatus] = useState<string>('all');
const [sortBy, setSortBy] = useState<string>('created_at');
const [isLoadingMore, setIsLoadingMore] = useState(false);
const scrollContainerRef = useRef<HTMLDivElement>(null);
const [isSmartAssistantExpanded, setIsSmartAssistantExpanded] = useState(false);
const [userId, setUserId] = useState<number>(0);
const [isComposing, setIsComposing] = useState(false);
// 在客户端挂载后读取localStorage
useEffect(() => {
if (typeof window !== 'undefined') {
const currentUser = JSON.parse(localStorage.getItem('currentUser') || '{}');
console.log('currentUser', currentUser);
setUserId(currentUser.id);
const savedProjectName = localStorage.getItem('projectName');
if (savedProjectName) {
setProjectName(savedProjectName);
}
getEpisodeList(currentUser.id);
}
}, []);
// 获取剧集列表
const getEpisodeList = async (userId: number) => {
if (isLoading || isLoadingMore) return;
console.log('getEpisodeList', userId);
setIsLoading(true);
try {
const params = {
user_id: String(userId),
};
const episodeListResponse = await getScriptEpisodeListNew(params);
console.log('episodeListResponse', episodeListResponse);
if (episodeListResponse.code === 0) {
setEpisodeList(episodeListResponse.data.movie_projects);
// 每一项 有
// final_video_url: "", // 生成的视频地址
// last_message: "",
// name: "After the Flood", // 剧集名称
// project_id: "9c34fcc4-c8d8-44fc-879e-9bd56f608c76", // 剧集ID
// status: "INIT", // 剧集状态 INIT 初始化
// step: "INIT" // 剧集步骤 INIT 初始化
}
} catch (error) {
console.error('Failed to fetch episode list:', error);
} finally {
setIsLoading(false);
setIsLoadingMore(false);
}
};
const handleUploadVideo = async () => {
console.log('upload video');
// 打开文件选择器
const input = document.createElement('input');
input.type = 'file';
input.accept = 'video/*';
input.onchange = async (e) => {
const file = (e.target as HTMLInputElement).files?.[0];
if (file) {
try {
setIsUploading(true);
// 获取上传token
const { token } = await getUploadToken();
// 上传到七牛云
const videoUrl = await uploadToQiniu(file, token);
// 上传成功设置视频URL
setVideoUrl(videoUrl);
console.log('视频上传成功:', videoUrl);
} catch (error) {
console.error('上传错误:', error);
alert('上传失败,请稍后重试');
} finally {
setIsUploading(false);
}
}
}
input.click();
}
const handleCreateVideo = async () => {
setIsCreating(true);
// 创建剧集数据
let episodeData: any = {
user_id: String(userId),
script: script,
mode: selectedMode,
resolution: selectedResolution
};
// 调用创建剧集API
const episodeResponse = await createScriptEpisodeNew(episodeData);
console.log('episodeResponse', episodeResponse);
if (episodeResponse.code !== 0) {
console.error(`创建剧集失败: ${episodeResponse.message}`);
alert(`创建剧集失败: ${episodeResponse.message}`);
return;
}
let episodeId = episodeResponse.data.project_id;
// let episodeId = '9c34fcc4-c8d8-44fc-879e-9bd56f608c76';
router.push(`/create/work-flow?episodeId=${episodeId}`);
setIsCreating(false);
}
// 下拉菜单项配置
const modeItems: MenuProps['items'] = [
{
type: 'group',
label: (
<div className="text-white/50 text-xs px-2 pb-2">Mode</div>
),
children: [
{
key: ModeEnum.AUTOMATIC,
label: (
<div className="flex flex-col gap-1 p-1">
<div className="flex items-center gap-2">
<span className="text-base font-medium">Auto</span>
</div>
<span className="text-sm text-gray-400">Automatically execute the workflow, you can't edit the workflow before it's finished.</span>
</div>
),
},
{
key: ModeEnum.MANUAL,
label: (
<div className="flex flex-col gap-1 p-1">
<div className="flex items-center gap-2">
<span className="text-base font-medium">Manual</span>
<Crown className="w-4 h-4 text-yellow-500" />
</div>
<span className="text-sm text-gray-400">Manually control the workflow, you can control the workflow everywhere.</span>
</div>
),
},
],
},
];
// 分辨率选项配置
const resolutionItems: MenuProps['items'] = [
{
type: 'group',
label: (
<div className="text-white/50 text-xs px-2 pb-2">Resolution</div>
),
children: [
{
key: ResolutionEnum.HD_720P,
label: (
<div className="flex items-center justify-between p-1">
<span className="text-base">720P</span>
</div>
),
},
{
key: ResolutionEnum.FULL_HD_1080P,
label: (
<div className="flex items-center justify-between p-1">
<span className="text-base">1080P</span>
<Crown className="w-4 h-4 text-yellow-500" />
</div>
),
},
{
key: ResolutionEnum.UHD_2K,
label: (
<div className="flex items-center justify-between p-1">
<span className="text-base">2K</span>
<Crown className="w-4 h-4 text-yellow-500" />
</div>
),
},
{
key: ResolutionEnum.UHD_4K,
label: (
<div className="flex items-center justify-between p-1">
<span className="text-base">4K</span>
<Crown className="w-4 h-4 text-yellow-500" />
</div>
),
},
],
},
];
// 处理模式选择
const handleModeSelect: MenuProps['onClick'] = ({ key }) => {
setSelectedMode(key as ModeEnum);
};
// 处理分辨率选择
const handleResolutionSelect: MenuProps['onClick'] = ({ key }) => {
setSelectedResolution(key as ResolutionEnum);
};
const handleStartCreating = () => {
setActiveTab('script');
setInputText(ideaText);
}
// 处理编辑器聚焦
const handleEditorFocus = () => {
setIsFocus(true);
if (editorRef.current) {
const range = document.createRange();
const selection = window.getSelection();
const textNode = Array.from(editorRef.current.childNodes).find(
node => node.nodeType === Node.TEXT_NODE
);
if (!textNode) {
const newTextNode = document.createTextNode(script || '');
editorRef.current.appendChild(newTextNode);
range.setStart(newTextNode, (script || '').length);
range.setEnd(newTextNode, (script || '').length);
} else {
range.setStart(textNode, textNode.textContent?.length || 0);
range.setEnd(textNode, textNode.textContent?.length || 0);
}
selection?.removeAllRanges();
selection?.addRange(range);
}
};
const handleEditorChange = (e: React.FormEvent<HTMLDivElement>) => {
const newText = e.currentTarget.textContent || '';
// 如果正在输入中文,只更新内部文本,不更新状态
if (isComposing) {
return;
}
// 更新状态
setInputText(newText);
// 保存当前选区位置
const selection = window.getSelection();
if (selection && selection.rangeCount > 0) {
const range = selection.getRangeAt(0);
const currentPosition = range.startOffset;
// 使用 requestAnimationFrame 确保在下一帧恢复光标位置
requestAnimationFrame(() => {
if (editorRef.current) {
// 找到或创建文本节点
let textNode = Array.from(editorRef.current.childNodes).find(
node => node.nodeType === Node.TEXT_NODE
) as Text;
if (!textNode) {
textNode = document.createTextNode(newText);
editorRef.current.appendChild(textNode);
}
// 计算正确的光标位置
const finalPosition = Math.min(currentPosition, textNode.length);
// 设置新的选区
const newRange = document.createRange();
newRange.setStart(textNode, finalPosition);
newRange.setEnd(textNode, finalPosition);
selection.removeAllRanges();
selection.addRange(newRange);
}
});
}
};
// 处理中文输入开始
const handleCompositionStart = () => {
setIsComposing(true);
};
// 处理中文输入结束
const handleCompositionEnd = (e: React.CompositionEvent<HTMLDivElement>) => {
setIsComposing(false);
// 在输入完成后更新内容
const newText = e.currentTarget.textContent || '';
setInputText(newText);
// 保存并恢复光标位置
const selection = window.getSelection();
if (selection && selection.rangeCount > 0) {
const range = selection.getRangeAt(0);
const currentPosition = range.startOffset;
requestAnimationFrame(() => {
if (editorRef.current) {
let textNode = Array.from(editorRef.current.childNodes).find(
node => node.nodeType === Node.TEXT_NODE
) as Text;
if (!textNode) {
textNode = document.createTextNode(newText);
editorRef.current.appendChild(textNode);
}
// 计算正确的光标位置
const finalPosition = Math.min(currentPosition, textNode.length);
// 设置新的选区
const newRange = document.createRange();
newRange.setStart(textNode, finalPosition);
newRange.setEnd(textNode, finalPosition);
selection.removeAllRanges();
selection.addRange(newRange);
}
});
}
};
// 检查是否需要显示引导
useEffect(() => {
if (typeof window !== 'undefined') {
const hasCompletedTour = localStorage.getItem('hasCompletedTour');
if (hasCompletedTour) {
setRunTour(false);
}
}
}, []);
useEffect(() => {
setIsClient(true);
}, []);
// 渲染剧集卡片
const renderEpisodeCard = (episode: any) => {
return (
<div
key={episode.project_id}
className="group relative bg-white/[0.08] backdrop-blur-[20px] border border-white/[0.12] rounded-[12px] overflow-hidden hover:bg-white/[0.12] transition-all duration-300 hover:shadow-[0_8px_32px_rgba(0,0,0,0.4)] cursor-pointer"
onClick={() => router.push(`/create/work-flow?episodeId=${episode.project_id}`)}
>
{/* 视频缩略图 */}
<div className="relative h-[180px] bg-gradient-to-br from-purple-500/20 to-blue-500/20 overflow-hidden">
{episode.final_video_url ? (
<video
src={episode.final_video_url}
className="w-full h-full object-cover"
muted
loop
onMouseEnter={(e) => (e.target as HTMLVideoElement).play()}
onMouseLeave={(e) => (e.target as HTMLVideoElement).pause()}
/>
) : (
<div className="flex items-center justify-center h-full">
<Video className="w-12 h-12 text-white/30" />
</div>
)}
{/* 播放按钮覆盖 */}
<div className="absolute inset-0 bg-black/20 flex items-center justify-center opacity-0 group-hover:opacity-100 transition-opacity duration-300">
<div className="w-12 h-12 bg-white/20 backdrop-blur-sm rounded-full flex items-center justify-center">
<Play className="w-6 h-6 text-white ml-1" />
</div>
</div>
{/* 状态标签 */}
<div className="absolute top-3 left-3">
<span className={`px-2 py-1 rounded-full text-xs font-medium ${
episode.status === 'COMPLETED' ? 'bg-green-500/80 text-white' :
episode.status !== 'COMPLETED' ? 'bg-yellow-500/80 text-white' :
'bg-gray-500/80 text-white'
}`}>
{episode.status === 'COMPLETED' ? 'finished' : 'processing'}
</span>
</div>
{/* 时长标签 */}
{episode.duration && (
<div className="absolute bottom-3 right-3">
<span className="px-2 py-1 bg-black/60 backdrop-blur-sm rounded text-xs text-white">
{episode.duration}
</span>
</div>
)}
</div>
{/* 内容区域 */}
<div className="p-4">
<h3 className="text-white font-medium text-sm mb-2 line-clamp-2 group-hover:text-blue-300 transition-colors">
{episode.name || episode.title || 'Unnamed episode'}
</h3>
{/* 元数据 */}
<div className="flex items-center justify-between text-xs text-white/40">
<div className="flex items-center gap-2">
<Calendar className="w-3 h-3" />
<span>{new Date(episode.created_at).toLocaleDateString()}</span>
</div>
<div className="flex items-center gap-3">
<div className="flex items-center gap-1">
<Clock className="w-3 h-3" />
<span>{new Date(episode.updated_at).toLocaleTimeString([], {hour: '2-digit', minute:'2-digit'})}</span>
</div>
</div>
</div>
</div>
{/* 操作按钮 */}
<div className="absolute top-3 right-3 opacity-0 group-hover:opacity-100 transition-opacity duration-300">
<div className="flex gap-2">
<button className="w-8 h-8 bg-white/20 backdrop-blur-sm rounded-full flex items-center justify-center hover:bg-white/30 transition-colors">
<Share2 className="w-4 h-4 text-white" />
</button>
</div>
</div>
</div>
);
};
return (
<>
<div className="container mx-auto h-[calc(100vh-6rem)] flex flex-col absolute top-[4rem] left-0 right-0 px-[1rem]">
{/* 优化后的主要内容区域 */}
<div className="flex-1 min-h-0">
<div
ref={scrollContainerRef}
className="h-full overflow-y-auto custom-scrollbar"
style={{
scrollbarWidth: 'thin',
scrollbarColor: 'rgba(255,255,255,0.1) transparent'
}}
>
{isLoading && episodeList.length === 0 ? (
/* 优化的加载状态 */
<div className="grid grid-cols-1 sm:grid-cols-2 lg:grid-cols-3 xl:grid-cols-4 2xl:grid-cols-5 gap-6 pb-6">
{[...Array(10)].map((_, index) => (
<div
key={index}
className="bg-white/[0.05] backdrop-blur-[20px] border border-white/[0.08] rounded-2xl overflow-hidden animate-pulse"
>
<div className="h-[200px] bg-gradient-to-br from-white/[0.08] to-white/[0.04]">
<div className="h-full bg-white/[0.06] animate-pulse"></div>
</div>
<div className="p-5">
<div className="h-4 bg-white/[0.08] rounded-lg mb-3 animate-pulse"></div>
<div className="h-3 bg-white/[0.06] rounded-lg mb-4 w-3/4 animate-pulse"></div>
<div className="flex justify-between">
<div className="h-3 bg-white/[0.06] rounded-lg w-20 animate-pulse"></div>
<div className="h-3 bg-white/[0.06] rounded-lg w-16 animate-pulse"></div>
</div>
</div>
</div>
))}
</div>
) : episodeList.length > 0 ? (
/* 优化的剧集网格 */
<div className="pb-8">
<div className="grid grid-cols-1 sm:grid-cols-2 lg:grid-cols-3 xl:grid-cols-4 2xl:grid-cols-5 gap-6">
{episodeList.map(renderEpisodeCard)}
</div>
{/* 加载更多指示器 */}
{isLoadingMore && (
<div className="flex justify-center py-12">
<div className="flex items-center gap-3 px-6 py-3 bg-white/[0.05] backdrop-blur-[20px] border border-white/[0.08] rounded-xl">
<Loader2 className="w-5 h-5 animate-spin text-blue-400" />
<span className="text-white/70 font-medium">Loading more episodes...</span>
</div>
</div>
)}
{/* 到底提示 */}
{!hasMore && episodeList.length > 0 && (
<div className="flex justify-center py-12">
<div className="text-center">
<div className="w-12 h-12 bg-white/[0.05] backdrop-blur-[20px] border border-white/[0.08] rounded-xl flex items-center justify-center mx-auto mb-3">
<Check className="w-6 h-6 text-green-400" />
</div>
<p className="text-white/50 text-sm">All episodes loaded</p>
</div>
</div>
)}
</div>
) : (
<></>
)}
</div>
</div>
</div>
{/* 创建工具栏 */}
<div className='video-tool-component relative max-w-[1080px] w-full left-[50%] translate-x-[-50%]'>
<div className='video-storyboard-tools grid gap-4 rounded-[20px] bg-white/[0.08] backdrop-blur-[20px] border border-white/[0.12] shadow-[0_8px_32px_rgba(0,0,0,0.3)]'>
{isExpanded ? (
<div className='absolute top-0 bottom-0 left-0 right-0 z-[1] grid justify-items-center place-content-center rounded-[20px] bg-[#191B1E] bg-opacity-[0.3] backdrop-blur-[1.5px] cursor-pointer' onClick={() => setIsExpanded(false)}>
<ChevronUp className='w-4 h-4' />
<span className='text-sm'>Click to action</span>
</div>
) : (
<div className='absolute top-[-8px] left-[50%] z-[2] flex items-center justify-center w-[46px] h-4 rounded-[15px] bg-[#fff3] translate-x-[-50%] cursor-pointer hover:bg-[#ffffff1a]' onClick={() => setIsExpanded(true)}>
<ChevronDown className='w-4 h-4' />
</div>
)}
<div className='storyboard-tools-tab relative flex gap-8 px-4 py-[10px]'>
<div className={`tab-item ${activeTab === 'script' ? 'active' : ''} cursor-pointer`} onClick={() => setActiveTab('script')}>
<span className='text-lg opacity-60'>script</span>
</div>
<div className={`tab-item ${activeTab === 'clone' ? 'active' : ''} cursor-pointer`} onClick={() => setActiveTab('clone')}>
<span className='text-lg opacity-60'>clone</span>
</div>
</div>
<div className={`flex-shrink-0 p-4 overflow-hidden transition-all duration-300 pt-0 gap-4 ${isExpanded ? 'h-[16px]' : 'h-[162px]'}`}>
<div className='video-creation-tool-container flex flex-col gap-4'>
{activeTab === 'clone' && (
<div className='relative flex items-center gap-4 h-[94px]'>
<div className='relative w-[72px] rounded-[6px] overflow-hidden cursor-pointer ant-dropdown-trigger' onClick={handleUploadVideo}>
<div className='relative flex items-center justify-center w-[72px] h-[72px] rounded-[6px 6px 0 0] bg-white/[0.05] transition-all overflow-hidden hover:bg-white/[0.10]'>
{isUploading ? (
<Loader2 className='w-4 h-4 animate-spin' />
) : (
<Video className='w-4 h-4' />
)}
</div>
<div className='w-full h-[22px] flex items-center justify-center rounded-[0 0 6px 6px] bg-white/[0.03]'>
<span className='text-xs opacity-30 cursor-[inherit]'>
{isUploading ? '上传中...' : 'Add Video'}
</span>
</div>
</div>
{videoUrl && (
<div className='relative w-[72px] rounded-[6px] overflow-hidden cursor-pointer ant-dropdown-trigger'>
<div className='relative flex items-center justify-center w-[72px] h-[72px] rounded-[6px 6px 0 0] bg-white/[0.05] transition-all overflow-hidden hover:bg-white/[0.10]'>
<video src={videoUrl} className='w-full h-full object-cover' />
</div>
</div>
)}
</div>
)}
{activeTab === 'script' && (
<div className='relative flex items-center gap-4 h-[94px]'>
<div className={`video-prompt-editor relative flex flex-1 self-stretch items-center w-0 rounded-[6px] ${isFocus ? 'focus' : ''}`}>
<div
ref={editorRef}
className='editor-content flex-1 w-0 max-h-[78px] min-h-[26px] h-auto gap-4 pl-[10px] rounded-[10px] leading-[26px] text-sm border-none overflow-y-auto cursor-text'
contentEditable
style={{ paddingRight: '10px' }}
onFocus={handleEditorFocus}
onBlur={() => setIsFocus(false)}
onInput={handleEditorChange}
onCompositionStart={handleCompositionStart}
onCompositionEnd={handleCompositionEnd}
suppressContentEditableWarning
>
{script}
</div>
<div
className={`custom-placeholder absolute top-[50%] left-[10px] z-10 translate-y-[-50%] flex items-center gap-1 pointer-events-none text-[14px] leading-[26px] text-white/[0.40] ${script ? 'opacity-0' : 'opacity-100'}`}
>
<span>Describe the content you want to action. Get an </span>
<b
className='idea-link inline-flex items-center gap-0.5 text-white/[0.50] font-normal cursor-pointer pointer-events-auto underline'
onClick={() => setInputText(ideaText)}
>
<Lightbulb className='w-4 h-4' />idea
</b>
</div>
</div>
</div>
)}
<div className='flex gap-3'>
<div className='tool-scroll-box relative flex-1 w-0'>
<div className='tool-scroll-box-content overflow-x-auto scrollbar-hide'>
<div className='flex items-center flex-1 gap-3'>
<Dropdown
menu={{
items: modeItems,
onClick: handleModeSelect,
selectedKeys: [selectedMode.toString()],
}}
trigger={['click']}
overlayClassName="mode-dropdown"
placement="bottomLeft"
>
<div className='tool-operation-button ant-dropdown-trigger'>
<Package className='w-4 h-4' />
<span className='text-nowrap opacity-70'>
{selectedMode === ModeEnum.AUTOMATIC ? 'Auto' : 'Manual'}
</span>
<Crown className={`w-4 h-4 text-yellow-500 ${selectedMode === ModeEnum.AUTOMATIC ? 'hidden' : ''}`} />
</div>
</Dropdown>
<Dropdown
menu={{
items: resolutionItems,
onClick: handleResolutionSelect,
selectedKeys: [selectedResolution.toString()],
}}
trigger={['click']}
overlayClassName="mode-dropdown"
placement="bottomLeft"
>
<div className='tool-operation-button ant-dropdown-trigger'>
<Video className='w-4 h-4' />
<span className='text-nowrap opacity-70'>
{selectedResolution === ResolutionEnum.HD_720P ? '720P' :
selectedResolution === ResolutionEnum.FULL_HD_1080P ? '1080P' :
selectedResolution === ResolutionEnum.UHD_2K ? '2K' : '4K'}
</span>
<Crown className={`w-4 h-4 text-yellow-500 ${selectedResolution === ResolutionEnum.HD_720P ? 'hidden' : ''}`} />
</div>
</Dropdown>
</div>
</div>
</div>
<div className='flex items-center gap-3'>
<div className={`tool-submit-button ${videoUrl || script ? '' : 'disabled'} ${isCreating ? 'loading' : ''}`} onClick={isCreating ? undefined : handleCreateVideo}>
{isCreating ? (
<>
<Loader2 className='w-4 h-4 animate-spin' />
Actioning...
</>
) : (
<>
<ArrowUp className='w-4 h-4' />
Action
</>
)}
</div>
</div>
</div>
</div>
</div>
</div>
</div>
{episodeList.length === 0 && !isLoading && (
<div className='h-full flex flex-col items-center fixed top-[4rem] left-0 right-0 bottom-0'>
<EmptyStateAnimation className='' />
</div>
)}
</>
);
}