diff --git a/api/constants.ts b/api/constants.ts index 7089bbb..02fd0d9 100644 --- a/api/constants.ts +++ b/api/constants.ts @@ -1 +1 @@ -export const BASE_URL = "https://pre.movieflow.api.huiying.video" +export const BASE_URL = "https://77.smartvideo.py.qikongjian.com" diff --git a/api/enums.ts b/api/enums.ts deleted file mode 100644 index 8a8b1f2..0000000 --- a/api/enums.ts +++ /dev/null @@ -1,65 +0,0 @@ -// 主项目(产品)类型枚举 -export enum ProjectTypeEnum { - SCRIPT_TO_VIDEO = 1, // "剧本转视频" - VIDEO_TO_VIDEO = 2, // "视频复刻视频" -} - -// 模式枚举 -export enum ModeEnum { - MANUAL = 1, // "手动" - AUTOMATIC = 2, // "自动" -} - -// 分辨率枚举 -export enum ResolutionEnum { - HD_720P = 1, // "720p" - FULL_HD_1080P = 2, // "1080p" - UHD_2K = 3, // "2k" - UHD_4K = 4, // "4k" -} - -// 项目类型映射 -export const ProjectTypeMap = { - [ProjectTypeEnum.SCRIPT_TO_VIDEO]: { - value: "script_to_video", - label: "script", - tab: "script" - }, - [ProjectTypeEnum.VIDEO_TO_VIDEO]: { - value: "video_to_video", - label: "clone", - tab: "clone" - } -} as const; - -// 模式映射 -export const ModeMap = { - [ModeEnum.MANUAL]: { - value: "manual", - label: "手动" - }, - [ModeEnum.AUTOMATIC]: { - value: "automatic", - label: "自动" - } -} as const; - -// 分辨率映射 -export const ResolutionMap = { - [ResolutionEnum.HD_720P]: { - value: "720p", - label: "720P" - }, - [ResolutionEnum.FULL_HD_1080P]: { - value: "1080p", - label: "1080P" - }, - [ResolutionEnum.UHD_2K]: { - value: "2k", - label: "2K" - }, - [ResolutionEnum.UHD_4K]: { - value: "4k", - label: "4K" - } -} as const; \ No newline at end of file diff --git a/api/script_episode.ts b/api/script_episode.ts index 72dd527..3bd0767 100644 --- a/api/script_episode.ts +++ b/api/script_episode.ts @@ -61,6 +61,16 @@ export interface ScriptEpisode { video_url?: string; } +// 新-获取剧集列表 +export const getScriptEpisodeListNew = async (data: any): Promise> => { + return post>('/movie/list_movie_projects', data); +}; + +// 新-创建接口 +export const createScriptEpisodeNew = async (data: any): Promise> => { + return post>('/movie/create_movie_project', data); +}; + // 创建剧集 export const createScriptEpisode = async (data: CreateScriptEpisodeRequest): Promise> => { // return post>('/script_episode/create', data); diff --git a/api/script_project.ts b/api/script_project.ts index 41263b8..832a189 100644 --- a/api/script_project.ts +++ b/api/script_project.ts @@ -72,31 +72,7 @@ export interface DeleteScriptProjectRequest { // 创建剧本项目 export const createScriptProject = async (data: CreateScriptProjectRequest): Promise> => { - // return post>('/script_project/create', data); - return new Promise((resolve) => { - setTimeout(() => { - resolve({ - code: 0, - message: 'success', - data: { - id: 1, - title: '2025·07·03·001', - script_author: 'king', - characters: [], - summary: '', - project_type: 1, - status: 1, - cate_tags: [], - creator_name: 'king', - mode: 1, - resolution: 1, - created_at: '2025-07-03 10:00:00', - updated_at: '2025-07-03 10:00:00' - }, - successful: true - }); - }, 0); - }); + return post>('/script_project/create', data); }; // 获取剧本项目列表 @@ -112,4 +88,14 @@ export const updateScriptProject = async (data: UpdateScriptProjectRequest): Pro // 删除剧本项目 export const deleteScriptProject = async (data: DeleteScriptProjectRequest): Promise> => { return post>('/script_project/delete', data); +}; + +// 获取剧本项目详情请求数据类型 +export interface GetScriptProjectDetailRequest { + id: number; +} + +// 获取剧本项目详情 +export const getScriptProjectDetail = async (data: GetScriptProjectDetailRequest): Promise> => { + return post>('/script_project/detail', data); }; \ No newline at end of file diff --git a/api/video_flow.ts b/api/video_flow.ts index 91b381c..4944dc8 100644 --- a/api/video_flow.ts +++ b/api/video_flow.ts @@ -1,6 +1,69 @@ import { post } from './request'; -import { ProjectTypeEnum } from './enums'; -import { ApiResponse } from './common'; +import { ProjectTypeEnum } from '@/app/model/enums'; +import { ApiResponse } from '@/api/common'; + +// API 响应类型 +interface BaseApiResponse { + code: number; + successful: boolean; + message: string; + data: T; +} + +// 剧集详情数据类型 +interface EpisodeDetail { + project_id: string; + name: string; + status: 'running' | 'completed'; + step: 'sketch' | 'character' | 'video' | 'music' | 'final_video'; + last_message: string; + data: TaskData | null; + mode: 'auto' | 'manual'; + resolution: '1080p' | '4k'; +} + +// 任务数据类型 +interface TaskData { + sketch?: Array<{ + url: string; + script: string; + bg_rgb: string[]; + }>; + roles?: Array<{ + name: string; + url: string; + sound: string; + soundDescription: string; + roleDescription: string; + }>; + videos?: Array<{ + url: string; + script: string; + audio: string; + }>; + music?: Array<{ + url: string; + script: string; + name: string; + duration: string; + totalDuration: string; + isLooped: boolean; + }>; + final?: { + url: string; + }; +} + +// 流式数据类型 +export interface StreamData { + category: 'sketch' | 'character' | 'video' | 'music' | 'final_video'; + message: string; + data: any; + status: 'running' | 'completed'; + total?: number; + completed?: number; + all_completed?: boolean; +} // 场景/分镜头数据结构 export interface Scene { @@ -50,7 +113,7 @@ export interface VideoToSceneRequest { export type ConvertScenePromptRequest = ScriptToSceneRequest | VideoToSceneRequest; // 转换分镜头响应接口 -export type ConvertScenePromptResponse = ApiResponse; +export type ConvertScenePromptResponse = BaseApiResponse; /** * 将剧本或视频转换为分镜头提示词 @@ -116,3 +179,41 @@ export const convertVideoToScene = async ( project_type: ProjectTypeEnum.VIDEO_TO_VIDEO }); }; + +// 新-获取剧集详情 +export const detailScriptEpisodeNew = async (data: { project_id: string }): Promise> => { + return post>('/movie/get_movie_project_detail', data); + return { + code: 0, + successful: true, + message: 'success', + data: { + project_id: 'uuid', + name: '没有返回就调 name 接口', + status: 'running', + step: 'sketch', + last_message: 'loading detail info...', + data: null, + mode: 'auto', + resolution: '1080p', + } + } +}; + +// 获取 title 接口 +export const getScriptTitle = async (data: { project_id: string }): Promise> => { + return post>('/movie/get_movie_project_name', data); + return { + code: 0, + successful: true, + message: 'success', + data: { + name: '提取出视频标题' + } + } +} + +// 获取 数据 全量(需轮询) +export const getRunningStreamData = async (data: { project_id: string }): Promise> => { + return post>('/movie/get_status', data); +}; \ No newline at end of file diff --git a/app/api/auth/google/callback/route.ts b/app/api/auth/google/callback/route.ts deleted file mode 100644 index f07125e..0000000 --- a/app/api/auth/google/callback/route.ts +++ /dev/null @@ -1,80 +0,0 @@ -import { NextRequest, NextResponse } from 'next/server'; - -// Prevent static rendering of this route -export const dynamic = 'force-dynamic'; - -/** - * Handle Google OAuth callback - * In a real app, this would: - * 1. Exchange the authorization code for tokens - * 2. Verify the token and get user info from Google - * 3. Create or update the user in your database - * 4. Set session/cookies - * 5. Redirect to the app - */ -export async function GET(request: NextRequest) { - const searchParams = request.nextUrl.searchParams; - const code = searchParams.get('code'); - const error = searchParams.get('error'); - const state = searchParams.get('state'); - - console.log('Google OAuth callback received', { - hasCode: !!code, - error: error || 'none', - hasState: !!state, - url: request.url - }); - - // Handle errors from Google - if (error) { - console.error('Google OAuth error:', error); - return NextResponse.redirect(new URL(`/login?error=${encodeURIComponent(error)}`, request.url)); - } - - if (!code) { - console.error('No authorization code received from Google'); - return NextResponse.redirect(new URL('/login?error=no_code', request.url)); - } - - // The state parameter validation will happen client-side - // since we're storing the original state in sessionStorage - // We'll add the state to the redirect URL so the client can validate it - - try { - console.log('Processing OAuth callback with code', code.substring(0, 5) + '...'); - - // In a real app, you would exchange the code for tokens - // and validate the tokens here - - // For this demo, we'll just simulate a successful login - // by redirecting with a mock session token - const redirectUrl = new URL('/', request.url); - - // Mock user data that would normally come from Google - const mockUser = { - id: 'google-123456', - name: 'Google User', - email: 'user@gmail.com', - picture: 'https://i.pravatar.cc/150', - }; - - // In a real app, you would set cookies or session data here - - // Simulate setting a session by adding a URL parameter - // In a real app, don't pass sensitive data in URL parameters - redirectUrl.searchParams.set('session', 'demo-session-token'); - redirectUrl.searchParams.set('user', encodeURIComponent(JSON.stringify(mockUser))); - - // Pass the state back to the client for validation - if (state) { - redirectUrl.searchParams.set('state', state); - } - - console.log('Redirecting to:', redirectUrl.toString()); - return NextResponse.redirect(redirectUrl); - } catch (error) { - console.error('Failed to process Google authentication:', error); - const errorMessage = error instanceof Error ? error.message : 'Unknown error'; - return NextResponse.redirect(new URL(`/login?error=auth_failed&details=${encodeURIComponent(errorMessage)}`, request.url)); - } -} \ No newline at end of file diff --git a/app/globals.css b/app/globals.css index 8be86cf..b23fdb9 100644 --- a/app/globals.css +++ b/app/globals.css @@ -2,6 +2,12 @@ @tailwind components; @tailwind utilities; +*, +*:after, +*:before { + box-sizing: border-box; +} + :root { --foreground-rgb: 0, 0, 0; --background-start-rgb: 214, 219, 220; @@ -10,6 +16,10 @@ --text-secondary: #a0aec0; --text-primary: #fff; --ui-level1-layerbase: #131416; + + /* 3dwave */ + --index: calc(1vh + 1vw); + --transition: cubic-bezier(0.1, 0.7, 0, 1); } @media (prefers-color-scheme: dark) { @@ -99,6 +109,9 @@ body { .hide-scrollbar::-webkit-scrollbar { display: none !important; } +*::-webkit-scrollbar { + display: none !important; +} @layer base { * { diff --git a/app/model/enums.ts b/app/model/enums.ts new file mode 100644 index 0000000..6e15184 --- /dev/null +++ b/app/model/enums.ts @@ -0,0 +1,136 @@ +// 主项目(产品)类型枚举 +export enum ProjectTypeEnum { + SCRIPT_TO_VIDEO = 1, // "剧本转视频" + VIDEO_TO_VIDEO = 2, // "视频复刻视频" +} + +// 模式枚举 +export enum ModeEnum { + MANUAL = 'manual', // "手动" + AUTOMATIC = 'automatic', // "自动" +} + +// 分辨率枚举 +export enum ResolutionEnum { + HD_720P = '720p', // "720p" + FULL_HD_1080P = '1080p', // "1080p" + UHD_2K = '2k', // "2k" + UHD_4K = '4k', // "4k" +} + +// 工作流阶段枚举 +export enum FlowStageEnum { + PREVIEW = 1, // "预览图列表分镜草图" + CHARACTERS = 2, // "角色列表" + SHOTS = 3, // "分镜视频列表" + BGM = 4, // "背景音乐列表" + POST_PRODUCTION = 5, // "后期操作中" + MERGE_VIDEO = 6, // "合并视频完成" + ERROR = 99, // "出错" +} + +// 任务状态枚举 +export enum TaskStatusEnum { + PENDING = "pending", // "待处理" + PROCESSING = "processing", // "处理中" + COMPLETED = "completed", // "已完成" + FAILED = "failed", // "失败" +} + +// 项目类型映射 +export const ProjectTypeMap = { + [ProjectTypeEnum.SCRIPT_TO_VIDEO]: { + value: "script_to_video", + label: "script", + tab: "script" + }, + [ProjectTypeEnum.VIDEO_TO_VIDEO]: { + value: "video_to_video", + label: "clone", + tab: "clone" + } +} as const; + +// 模式映射 +export const ModeMap = { + [ModeEnum.MANUAL]: { + value: "manual", + label: "手动" + }, + [ModeEnum.AUTOMATIC]: { + value: "automatic", + label: "自动" + } +} as const; + +// 分辨率映射 +export const ResolutionMap = { + [ResolutionEnum.HD_720P]: { + value: "720p", + label: "720P" + }, + [ResolutionEnum.FULL_HD_1080P]: { + value: "1080p", + label: "1080P" + }, + [ResolutionEnum.UHD_2K]: { + value: "2k", + label: "2K" + }, + [ResolutionEnum.UHD_4K]: { + value: "4k", + label: "4K" + } +} as const; + +// 工作流阶段映射 +export const FlowStageMap = { + [FlowStageEnum.PREVIEW]: { + value: "preview", + label: "预览图列表分镜草图" + }, + [FlowStageEnum.CHARACTERS]: { + value: "characters", + label: "角色列表" + }, + [FlowStageEnum.SHOTS]: { + value: "shots", + label: "分镜视频列表" + }, + [FlowStageEnum.BGM]: { + value: "bgm", + label: "背景音乐列表" + }, + [FlowStageEnum.POST_PRODUCTION]: { + value: "post_production", + label: "后期操作中" + }, + [FlowStageEnum.MERGE_VIDEO]: { + value: "merge_video", + label: "合并视频完成" + }, + [FlowStageEnum.ERROR]: { + value: "error", + label: "出错" + } +} as const; + +// 任务状态映射 +export const TaskStatusMap = { + [TaskStatusEnum.PENDING]: { + value: "pending", + label: "待处理" + }, + [TaskStatusEnum.PROCESSING]: { + value: "processing", + label: "处理中" + }, + [TaskStatusEnum.COMPLETED]: { + value: "completed", + label: "已完成" + }, + [TaskStatusEnum.FAILED]: { + value: "failed", + label: "失败" + } +} as const; \ No newline at end of file diff --git a/app/model/sse_response.ts b/app/model/sse_response.ts new file mode 100644 index 0000000..e381a2e --- /dev/null +++ b/app/model/sse_response.ts @@ -0,0 +1,176 @@ +/** + * SSE 响应数据结构 + * 对应 Python 后端的 sse_response.py + */ + +import { TaskStatusEnum, FlowStageEnum } from './enums'; + +// 重新导出枚举以供其他模块使用 +export { TaskStatusEnum, FlowStageEnum }; + +// 完成状态对象 +export interface CompletionStatus { + total: number; + completed: number; + all_completed: boolean; +} + +// 预览图对象 +export interface PreviewObj { + scene_id: number; + prompt: string; + order: number; + preview_pic_url?: string; + status: string; + updated_at?: string; +} + +// 角色对象 +export interface CharacterObj { + name: string; + desc: string; + image_url?: string; + voice_desc: string; + voice_url?: string; +} + +// 分镜视频对象 +export interface ShotObj { + scene_id: number; + prompt: string; + order: number; + transition: string; + volume: number; + video_url?: string; + status: string; + updated_at?: string; +} + +// 背景音乐对象 +export interface BgmObj { + bgm_id?: number; + name?: string; + url?: string; + status: string; +} + +// 后期制作对象 +export interface PostProductionObj { + episode_status: number; + is_processing: boolean; + all_completed: boolean; +} + +// 最终视频对象 +export interface FinalVideoObj { + video_url?: string; + is_completed: boolean; +} + +// SSE 响应数据联合类型 +export type SseResponseData = + | PreviewObj + | CharacterObj + | ShotObj + | BgmObj + | PostProductionObj + | FinalVideoObj + | null; + +// SSE 响应对象 +export interface SseResponse { + code: number; + message: string; + data?: SseResponseData; + stage: number; + stage_index: number; + status: string; + completion_status?: CompletionStatus; +} + +// 使用统一的枚举 +export const WorkflowStageEnum = FlowStageEnum; +export const WorkflowStatusEnum = TaskStatusEnum; + +// 类型守卫函数 +export function isPreviewObj(data: SseResponseData): data is PreviewObj { + return data !== null && typeof data === 'object' && 'scene_id' in data && 'preview_pic_url' in data; +} + +export function isCharacterObj(data: SseResponseData): data is CharacterObj { + return data !== null && typeof data === 'object' && 'name' in data && 'voice_desc' in data; +} + +export function isShotObj(data: SseResponseData): data is ShotObj { + return data !== null && typeof data === 'object' && 'scene_id' in data && 'video_url' in data; +} + +export function isBgmObj(data: SseResponseData): data is BgmObj { + return data !== null && typeof data === 'object' && 'bgm_id' in data; +} + +export function isPostProductionObj(data: SseResponseData): data is PostProductionObj { + return data !== null && typeof data === 'object' && 'episode_status' in data && 'is_processing' in data; +} + +export function isFinalVideoObj(data: SseResponseData): data is FinalVideoObj { + return data !== null && typeof data === 'object' && 'video_url' in data && 'is_completed' in data; +} + +// 工厂函数 +export class SseResponseFactory { + static success( + stage: number = 0, + stage_index: number = 0, + status: string = '', + message: string = '操作成功', + data?: SseResponseData, + completion_status?: CompletionStatus + ): SseResponse { + return { + code: 0, + stage: stage, + stage_index: stage_index, + status: status, + message: message, + data: data, + completion_status: completion_status + }; + } + + static fail( + stage: number = 0, + stage_index: number = 0, + status: string = '', + code: number = 1, + message: string = '操作失败', + data?: SseResponseData + ): SseResponse { + return { + code: code, + stage: stage, + stage_index: stage_index, + status: status, + message: message, + data: data + }; + } + + static withCompletionStatus( + stage: number, + stage_index: number, + status: string, + message: string, + completion_status: CompletionStatus + ): SseResponse { + return { + code: 0, + stage: stage, + stage_index: stage_index, + status: status, + message: message, + data: null, + completion_status: completion_status + }; + } +} diff --git a/components/common/EmptyStateAnimation.tsx b/components/common/EmptyStateAnimation.tsx index e5f131f..86254fd 100644 --- a/components/common/EmptyStateAnimation.tsx +++ b/components/common/EmptyStateAnimation.tsx @@ -1,264 +1,119 @@ "use client"; -import { useState, useEffect, useRef } from 'react'; +import React, { useState, useEffect, useRef, useCallback } from 'react'; import { ArrowUp } from 'lucide-react'; -import Image from 'next/image'; import gsap from 'gsap'; -import { SplitText } from 'gsap/SplitText'; +import { ImageWave } from '@/components/ui/ImageWave'; -// 注册 SplitText 插件 -if (typeof window !== 'undefined') { - gsap.registerPlugin(SplitText); +interface AnimationStageProps { + shouldStart: boolean; + onComplete: () => void; } -const ideaText = 'a cute capybara with an orange on its head, staring into the distance and walking forward'; - -const AnimatedText = ({ text, onComplete, shouldStart }: { text: string; onComplete: () => void; shouldStart: boolean }) => { +// 动画1:模拟输入和点击 +const InputAnimation: React.FC = ({ shouldStart, onComplete }) => { const containerRef = useRef(null); - const titleRef = useRef(null); - const inputContainerRef = useRef(null); const inputRef = useRef(null); const cursorRef = useRef(null); + const buttonRef = useRef(null); const mouseRef = useRef(null); - const buttonRef = useRef(null); - const animationRef = useRef(null); const [displayText, setDisplayText] = useState(''); const demoText = "a cute capybara with an orange on its head"; useEffect(() => { - if (containerRef.current && typeof window !== 'undefined' && shouldStart) { - // 清理之前的动画 - if (animationRef.current) { - animationRef.current.kill(); - } + if (!shouldStart || !containerRef.current) return; // 重置状态 setDisplayText(''); - if (containerRef.current) { - containerRef.current.style.opacity = '1'; + + const tl = gsap.timeline({ + onComplete: () => { + setTimeout(onComplete, 500); } + }); - // 设置初始状态 - gsap.set([titleRef.current, inputContainerRef.current], { - opacity: 1 - }); - - // 创建主时间轴 - const mainTl = gsap.timeline(); - - // 1. 显示标题和输入框 - mainTl.fromTo(titleRef.current, { - y: -30, - opacity: 0 - }, { - y: 0, - opacity: 1, - duration: 0.3, - ease: "power2.out" - }) - .fromTo(inputContainerRef.current, { + // 1. 显示输入框和鼠标 + tl.fromTo([inputRef.current, mouseRef.current], { scale: 0.9, opacity: 0 }, { scale: 1, opacity: 1, - duration: 0.2, - ease: "back.out(1.7)" - }, "-=0.3"); - - // 2. 显示鼠标指针并移动到输入框 - mainTl.fromTo(mouseRef.current, - { opacity: 0, x: -100, y: -50 }, - { - opacity: 1, - x: -150, - y: 0, duration: 0.3, - ease: "power2.out" - }, - "+=0.5" - ); + ease: "back.out(1.7)", + stagger: 0.1 + }); - // 3. 鼠标移动到输入框中心 - mainTl.to(mouseRef.current, { - x: 0, - y: 0, - duration: 0.2, - ease: "power2.inOut" - }); + // 2. 鼠标移动到输入框中心 + tl.to(mouseRef.current, { + x: 20, + y: 0, + duration: 0.3 + }); - // 4. 输入框聚焦效果 - mainTl.to(inputRef.current, { - scale: 1.05, - rotationY: 1, - rotationX: 15, - transformOrigin: "center center", - boxShadow: ` - inset 0 3px 0 rgba(255,255,255,0.35), - inset 0 -3px 0 rgba(0,0,0,0.2), - inset 3px 0 0 rgba(255,255,255,0.15), - inset -3px 0 0 rgba(0,0,0,0.08), - 0 0 0 3px rgba(59, 130, 246, 0.4), - 0 4px 8px rgba(0,0,0,0.15), - 0 12px 24px rgba(0,0,0,0.2), - 0 20px 40px rgba(0,0,0,0.15), - 0 0 0 1px rgba(59, 130, 246, 0.6) - `, - borderColor: 'rgba(79, 70, 229, 0.8)', - duration: 0.1, - ease: "back.out(1.7)" - }, "+=0.2"); + // 3. 输入框聚焦效果 + tl.to(inputRef.current, { + scale: 1.02, + boxShadow: '0 0 0 2px rgba(59, 130, 246, 0.5)', + duration: 0.2 + }); - // 5. 显示按钮 - mainTl.fromTo(buttonRef.current, - { scale: 0, opacity: 0 }, - { scale: 1, opacity: 1, duration: 0.1, ease: "back.out(1.7)" }, - "+=0.3" - ); - - // 隐藏鼠标指针 - mainTl.to(mouseRef.current, { - opacity: 0, - duration: 0.1 - }, "+=0.2"); - - // 6. 打字动画 - const typingDuration = demoText.length * 0.01; - let currentChar = 0; - - mainTl.to({}, { + // 4. 打字动画 + const typingDuration = demoText.length * 0.05; + tl.to({}, { duration: typingDuration, - ease: "none", onUpdate: function() { const progress = this.progress(); const targetChar = Math.floor(progress * demoText.length); - if (targetChar !== currentChar && targetChar <= demoText.length) { - currentChar = targetChar; - setDisplayText(demoText.slice(0, currentChar)); - } - } - }, "+=0.3"); + setDisplayText(demoText.slice(0, targetChar)); + } + }); - // 7. 隐藏光标 - mainTl.to(cursorRef.current, { - opacity: 0, - duration: 0.1 - }, "+=0.2"); - - // 重新显示鼠标指针并移动到按钮 - mainTl.fromTo(mouseRef.current, - { opacity: 0, x: 0, y: 0 }, - { - opacity: 1, - duration: 0.1, - ease: "power2.out" - }, - "-=0.3" - ); - - // 鼠标移动到按钮 - mainTl.to(mouseRef.current, { - x: 20, - y: 10, - duration: 0.1, + // 6. 鼠标移动到按钮位置(调整移动时间和缓动函数) + tl.to(mouseRef.current, { + x: 650, + y: 0, + duration: 0.8, ease: "power2.inOut" - }, "+=0.5"); + }); - // 8. 输入框失焦效果 - mainTl.to(inputRef.current, { - scale: 1, - rotationY: 0, - rotationX: 0, - transformOrigin: "center center", - boxShadow: ` - inset 0 2px 0 rgba(255,255,255,0.25), - inset 0 -2px 0 rgba(0,0,0,0.15), - inset 2px 0 0 rgba(255,255,255,0.1), - inset -2px 0 0 rgba(0,0,0,0.05), - 0 2px 4px rgba(0,0,0,0.1), - 0 8px 16px rgba(0,0,0,0.15), - 0 16px 32px rgba(0,0,0,0.1), - 0 0 0 1px rgba(255,255,255,0.1) - `, - duration: 0.1, - ease: "power2.out" - }, "+=0.2"); + // 7. 等待一小段时间 + tl.to({}, { duration: 0.2 }); - // 点击效果 - mainTl.to(mouseRef.current, { + // 8. 点击效果 + tl.to(mouseRef.current, { scale: 0.8, duration: 0.1, - ease: "power2.out", yoyo: true, repeat: 1 - }) - .to(buttonRef.current, { + }).to(buttonRef.current, { scale: 0.95, duration: 0.1, - ease: "power2.out", yoyo: true, repeat: 1 - }, "-=0.2") - .to(buttonRef.current, { - boxShadow: "0 0 20px rgba(79, 70, 229, 0.6)", - duration: 0.1, - ease: "power2.out" - }, "-=0.1"); + }, "<"); - // 停留展示时间 - mainTl.to({}, { duration: 0.1 }); + // 9. 等待一小段时间 + tl.to({}, { duration: 0.3 }); - // 退场动画 - mainTl.to(containerRef.current, { + // 10. 整体淡出 + tl.to(containerRef.current, { opacity: 0, - y: -50, - duration: 0.3, - ease: "power2.in", - onComplete: () => { - onComplete(); - } - }); + y: -20, + duration: 0.3 + }); - animationRef.current = mainTl; - } - - return () => { - if (animationRef.current) { - animationRef.current.kill(); - } - }; }, [shouldStart, onComplete]); return ( -
- {/* 标题 */} -
-

- You can input script to generate video -

-
- - {/* 输入框模拟 */} -
+
+
- + {displayText}
- {/* Action按钮 */} -
Action -
+ - {/* 鼠标指针 */}
- - + +
@@ -301,639 +143,186 @@ const AnimatedText = ({ text, onComplete, shouldStart }: { text: string; onCompl ); }; -// 阶段文字解释组件 -const StageExplanation = ({ - text, - stage, - shouldStart, - onComplete -}: { - text: string; - stage: 'images' | 'replacing' | 'merging'; - shouldStart: boolean; - onComplete: () => void; -}) => { - const textRef = useRef(null); - const splitRef = useRef(null); - const animationRef = useRef(null); +// 动画2:ImageWave 动画展示 +const WaveAnimation: React.FC = ({ shouldStart, onComplete }) => { + const containerRef = useRef(null); + const [showWave, setShowWave] = useState(false); + const [autoAnimate, setAutoAnimate] = useState(false); - useEffect(() => { - if (textRef.current && typeof window !== 'undefined' && shouldStart) { - // 重置显示 - textRef.current.style.opacity = '1'; - - // 清理之前的动画 - if (splitRef.current) { - splitRef.current.revert(); - } - if (animationRef.current) { - animationRef.current.kill(); - } - - // 创建分割文本 - splitRef.current = new SplitText(textRef.current, { type: "words" }); - - let tl = gsap.timeline({ - onComplete: () => { - // 延迟后开始退场动画 - setTimeout(() => { - const exitTl = gsap.timeline({ - onComplete: () => { - onComplete(); - } - }); - - // 不同阶段的退场动画 - switch (stage) { - case 'images': - exitTl.to(splitRef.current.words, { - y: -50, - opacity: 0, - duration: 0.2, - ease: "power2.in", - stagger: 0.05 - }); - break; - case 'replacing': - exitTl.to(splitRef.current.words, { - scale: 0, - rotation: 360, - opacity: 0, - duration: 0.4, - ease: "back.in(2)", - stagger: 0.08 - }); - break; - case 'merging': - exitTl.to(splitRef.current.words, { - x: "random(-200, 200)", - y: "random(-100, -200)", - opacity: 0, - duration: 0.7, - ease: "power3.in", - stagger: 0.06 - }); - break; - } - }, 1000); // 显示1秒 - } - }); - - // 不同阶段的入场动画 - switch (stage) { - case 'images': - // 从下方弹跳进入 - tl.from(splitRef.current.words, { - y: 100, - opacity: 0, - duration: 0.5, - ease: "bounce.out", - stagger: 0.1 - }); - break; - case 'replacing': - // 旋转缩放进入 - tl.from(splitRef.current.words, { - scale: 0, - rotation: -180, - opacity: 0, - duration: 0.4, - ease: "back.out(2)", - stagger: 0.08 - }); - break; - case 'merging': - // 从四周飞入 - tl.from(splitRef.current.words, { - x: "random(-300, 300)", - y: "random(-200, 200)", - opacity: 0, - duration: 0.7, - ease: "power3.out", - stagger: 0.06 - }); - break; - } - - animationRef.current = tl; - } - - return () => { - if (animationRef.current) { - animationRef.current.kill(); - } - if (splitRef.current) { - splitRef.current.revert(); - } - }; - }, [shouldStart, stage, onComplete]); - - return ( -
-

{text}

-
- ); -}; - -const ImageQueue = ({ shouldStart, onComplete }: { shouldStart: boolean; onComplete: () => void }) => { - const imagesRef = useRef(null); - const [currentStage, setCurrentStage] = useState<'images' | 'replacing' | 'merging'>('images'); - const [replacementIndex, setReplacementIndex] = useState(0); - const [showStageText, setShowStageText] = useState(false); - const finalVideoContainerRef = useRef(null); - const imageUrls = [ 'https://d3phaj0sisr2ct.cloudfront.net/app/gen4/object-reference/welcome-ref-1.jpg', 'https://d3phaj0sisr2ct.cloudfront.net/app/gen4/object-reference/welcome-ref-2.jpg', 'https://d3phaj0sisr2ct.cloudfront.net/app/gen4/object-reference/welcome-ref-3.jpg', - 'https://d3phaj0sisr2ct.cloudfront.net/app/gen4/object-reference/welcome-ref-4.jpg' + 'https://d3phaj0sisr2ct.cloudfront.net/app/gen4/object-reference/welcome-ref-4.jpg', + 'https://d3phaj0sisr2ct.cloudfront.net/app/gen4/object-reference/welcome-ref-1.jpg', + 'https://d3phaj0sisr2ct.cloudfront.net/app/gen4/object-reference/welcome-ref-2.jpg', + 'https://d3phaj0sisr2ct.cloudfront.net/app/gen4/object-reference/welcome-ref-3.jpg', + 'https://d3phaj0sisr2ct.cloudfront.net/app/gen4/object-reference/welcome-ref-4.jpg', + 'https://d3phaj0sisr2ct.cloudfront.net/app/gen4/object-reference/welcome-ref-3.jpg', + 'https://d3phaj0sisr2ct.cloudfront.net/app/gen4/object-reference/welcome-ref-2.jpg', + 'https://d3phaj0sisr2ct.cloudfront.net/app/gen4/object-reference/welcome-ref-1.jpg', ]; - const videoUrls = [ - 'https://cdn.qikongjian.com/videos/1750385931_99a8fb42-af89-4ae9-841a-a49869f026bd_text_to_video_0.mp4', - 'https://cdn.qikongjian.com/videos/1750389908_37d4fffa-8516-43a3-a423-fc0274f40e8a_text_to_video_0.mp4', - 'https://cdn.qikongjian.com/videos/1750384661_d8e30b79-828e-48cd-9025-ab62a996717c_text_to_video_0.mp4', - 'https://cdn.qikongjian.com/videos/1750320040_4b47996e-7c70-490e-8433-80c7df990fdd_text_to_video_0.mp4' - ]; - - const finalVideoUrl = 'https://cdn.qikongjian.com/videos/1750385931_99a8fb42-af89-4ae9-841a-a49869f026bd_text_to_video_0.mp4'; - - // 阶段文字映射 - const stageTexts = { - images: 'Then, these are the key frames split from the storyboard', - replacing: 'Then, these are the videos corresponding to the split storyboard', - merging: 'Finally, efficiently edit the perfect video' - }; - - // 重置函数 - const resetComponent = () => { - // 清理最终视频容器 - if (finalVideoContainerRef.current) { - finalVideoContainerRef.current.remove(); - finalVideoContainerRef.current = null; - } - - // 额外清理:查找并移除所有可能的最终视频容器 - const allFinalVideos = document.querySelectorAll('div[class*="fixed"][class*="top-1/2"][class*="w-[400px]"]'); - allFinalVideos.forEach(container => { - if (container.parentNode === document.body) { - container.remove(); - } - }); - - // 重置所有状态 - setCurrentStage('images'); - setReplacementIndex(0); - setShowStageText(false); - - // 重置DOM结构 - if (imagesRef.current) { - imagesRef.current.style.display = 'flex'; - Array.from(imagesRef.current.children).forEach((container, index) => { - const htmlContainer = container as HTMLElement; - - // 移除视频元素 - const videos = htmlContainer.querySelectorAll('video'); - videos.forEach(video => video.remove()); - - // 使用GSAP重置容器样式 - gsap.set(htmlContainer, { - position: 'static', - x: 0, - y: index % 2 ? 20 : -20, - opacity: 1, - scale: 1, - rotation: 0 - }); - - // 确保图片显示 - const img = htmlContainer.querySelector('img'); - if (img) { - img.style.opacity = '1'; - } - }); - } - }; - - // 当shouldStart改变时重置并开始新的动画 useEffect(() => { - if (shouldStart) { - resetComponent(); - // 延迟显示阶段文字 - setTimeout(() => { - setShowStageText(true); - }, 500); + if (!shouldStart) { + setShowWave(false); + setAutoAnimate(false); + return; } + + // 显示 ImageWave + setShowWave(true); + + // 延迟开始自动动画 + const startTimeout = setTimeout(() => { + setAutoAnimate(true); + }, 300); + + return () => { + clearTimeout(startTimeout); + }; }, [shouldStart]); - // 阶段文字完成回调 - const handleStageTextComplete = () => { - setShowStageText(false); - // 根据当前阶段决定下一步 - if (currentStage === 'images') { - // 开始图片入场动画 - startImagesAnimation(); - } else if (currentStage === 'replacing') { - // 继续图片替换视频 - // replacementIndex会自动处理 - } else if (currentStage === 'merging') { - // 开始合并动画 - startMergingAnimation(); - } - }; - - // 图片入场动画 - const startImagesAnimation = () => { - if (imagesRef.current) { - const images = imagesRef.current.children; - - gsap.set(images, { - x: window.innerWidth, - opacity: 0, - rotation: 45 - }); - - const tl = gsap.timeline({ - onComplete: () => { - // 图片动画完成后,切换到替换阶段 - setTimeout(() => { - setCurrentStage('replacing'); - setShowStageText(true); - }, 1000); - } - }); - - tl.to(images, { - x: 0, - opacity: 1, - rotation: 0, - duration: 1, - ease: "elastic.out(1, 0.5)", - stagger: { - amount: 1, - from: "random" - } - }); - } - }; - - // 开始合并动画 - const startMergingAnimation = () => { - if (imagesRef.current) { - const containers = Array.from(imagesRef.current.children) as HTMLElement[]; - const videos = containers.map(container => container.querySelector('video')).filter(Boolean) as HTMLVideoElement[]; - - const finalVideoContainer = document.getElementById('final-video-container') as HTMLDivElement; - if (videos.length > 0 && finalVideoContainer) { - // 创建最终的大视频容器 - - const finalVideo = document.createElement('video'); - finalVideo.src = finalVideoUrl; - finalVideo.autoplay = true; - finalVideo.loop = true; - finalVideo.muted = true; - finalVideo.playsInline = true; - finalVideo.className = 'w-full h-full object-cover'; - - finalVideoContainer.appendChild(finalVideo); - finalVideoContainerRef.current = finalVideoContainer; - - // 等待最终视频准备就绪 - const onFinalVideoReady = () => { - finalVideo.removeEventListener('canplay', onFinalVideoReady); - - finalVideo.play().then(() => { - const tl = gsap.timeline({ - onComplete: () => { - // 移除原有的小视频容器 - if (imagesRef.current) { - imagesRef.current.style.display = 'none'; - } - - // 延迟3秒后调用完成回调,开始下一轮循环 - setTimeout(() => { - onComplete(); - }, 3000); - } - }); - - // 执行合并动画 - tl.to(containers, { - rotation: "random(-720, 720)", - x: "random(-200, 200)", - y: "random(-200, 200)", - scale: 0.8, - duration: 0.7, - ease: "power2.out", - stagger: 0.1 - }) - .to(containers, { - x: 0, - y: 0, - rotation: 0, - scale: 1, - duration: 0.5, - ease: "power2.inOut", - stagger: 0.05 - }) - .to(containers, { - scale: 1.2, - duration: 0.1, - ease: "power2.inOut" - }) - .to(finalVideoContainer, { - opacity: 1, - scale: 1, - duration: 0.2, - ease: "power2.out" - }, "-=0.2") - .to(containers, { - opacity: 0, - scale: 0.8, - duration: 0.2, - ease: "power2.in" - }, "-=0.5") - .to(finalVideoContainer, { - scale: 1, - duration: 0.1, - ease: "back.out(1.7)" - }); - }).catch(() => { - console.error('最终视频播放失败'); - finalVideoContainer.remove(); - onComplete(); - }); - }; - - finalVideo.addEventListener('canplay', onFinalVideoReady); - finalVideo.addEventListener('error', () => { - console.error('最终视频加载失败'); - finalVideoContainer.remove(); - onComplete(); - }); - - finalVideo.load(); + const handleAnimationComplete = () => { + // 动画完成后淡出并触发完成回调 + gsap.to(containerRef.current, { + opacity: 0, + scale: 0.9, + duration: 0.3, + onComplete: () => { + setAutoAnimate(false); + setShowWave(false); + onComplete(); } - } + }); }; - // 图片到视频的替换动画 - useEffect(() => { - if (currentStage === 'replacing' && imagesRef.current && replacementIndex < imageUrls.length && shouldStart && !showStageText) { - const targetContainer = imagesRef.current.children[replacementIndex] as HTMLElement; - - if (targetContainer) { - // 创建视频元素 - const video = document.createElement('video'); - video.src = videoUrls[replacementIndex]; - video.autoplay = true; - video.loop = true; - video.muted = true; - video.playsInline = true; - video.className = 'absolute inset-0 w-full h-full object-cover opacity-0'; - - // 添加视频到容器中 - targetContainer.style.position = 'relative'; - targetContainer.appendChild(video); - - // 等待视频准备好播放 - const onCanPlay = () => { - video.removeEventListener('canplay', onCanPlay); - video.removeEventListener('error', onError); - - // 开始播放 - video.play().then(() => { - // 创建无缝替换动画 - const tl = gsap.timeline({ - onComplete: () => { - // 移除图片元素 - const img = targetContainer.querySelector('img'); - if (img) { - img.remove(); - } - - // 移除绝对定位,让视频正常占位 - video.className = 'w-full h-full object-cover'; - - // 检查是否是最后一个视频 - if (replacementIndex === imageUrls.length - 1) { - // 最后一个视频替换完成,切换到合并阶段 - setTimeout(() => { - setCurrentStage('merging'); - setShowStageText(true); - }, 1000); - } else { - // 延迟后替换下一个 - setTimeout(() => { - setReplacementIndex(prev => prev + 1); - }, 600); - } - } - }); - - // 同时淡入视频和淡出图片 - tl.to(video, { - opacity: 1, - duration: 0.1, - ease: "power2.inOut" - }) - .to(targetContainer.querySelector('img'), { - opacity: 0, - duration: 0.1, - ease: "power2.inOut" - }, 0) - .to(targetContainer, { - scale: 1.02, - duration: 0.1, - ease: "power2.out", - yoyo: true, - repeat: 1 - }, 0.1); - }).catch(onError); - }; - - const onError = () => { - video.removeEventListener('canplay', onCanPlay); - video.removeEventListener('error', onError); - console.error('视频加载或播放失败:', videoUrls[replacementIndex]); - - // 移除失败的视频元素 - video.remove(); - - // 跳过到下一个 - if (replacementIndex === imageUrls.length - 1) { - setTimeout(() => { - setCurrentStage('merging'); - setShowStageText(true); - }, 500); - } else { - setTimeout(() => { - setReplacementIndex(prev => prev + 1); - }, 500); - } - }; - - video.addEventListener('canplay', onCanPlay); - video.addEventListener('error', onError); - - // 加载视频 - video.load(); - } - } - }, [currentStage, replacementIndex, shouldStart, showStageText]); - return ( - <> - {showStageText && ( - - )} -
- {imageUrls.map((url, index) => ( -
- {`reference-${index -
- ))} -
- +
+ +
); }; -// 主要的空状态动画组件 -export const EmptyStateAnimation = ({className}: {className: string}) => { - const [showText, setShowText] = useState(false); - const [showImages, setShowImages] = useState(false); - const [animationCycle, setAnimationCycle] = useState(0); - const [isClient, setIsClient] = useState(false); +// 动画3:图片墙打破,显示视频 +const FinalAnimation: React.FC = ({ shouldStart, onComplete }) => { + const containerRef = useRef(null); + const videoRef = useRef(null); + const [showVideo, setShowVideo] = useState(false); - // 全局清理函数 - const globalCleanup = () => { - // 清理最终视频容器中的视频元素 - const finalVideoContainer = document.getElementById('final-video-container') as HTMLDivElement; - if (finalVideoContainer) { - const finalVideo = finalVideoContainer.querySelector('video'); - if (finalVideo) { - finalVideo.remove(); - } - // 重置容器透明度 - finalVideoContainer.style.opacity = '0'; - } + const videoUrl = 'https://cdn.qikongjian.com/videos/1750385931_99a8fb42-af89-4ae9-841a-a49869f026bd_text_to_video_0.mp4'; - // 清理所有图片容器中的视频元素 - const allVideoElements = document.querySelectorAll('video'); - allVideoElements.forEach(video => { - // 确保视频不是在final-video-container中(已经处理过了) - if (!finalVideoContainer?.contains(video)) { - // 停止播放并移除 - video.pause(); - video.removeAttribute('src'); - video.load(); // 释放资源 - video.remove(); + useEffect(() => { + if (!shouldStart || !containerRef.current) return; + + const tl = gsap.timeline({ + onComplete: () => { + setTimeout(() => { + // 淡出视频 + gsap.to(containerRef.current, { + opacity: 0, + scale: 0.9, + duration: 0.3, + onComplete + }); + }, 3000); } }); - // 清理任何可能遗留的视频容器 - const imageContainers = document.querySelectorAll('[class*="w-[200px]"][class*="h-[150px]"]'); - imageContainers.forEach(container => { - const videos = container.querySelectorAll('video'); - videos.forEach(video => { - video.pause(); - video.removeAttribute('src'); - video.load(); - video.remove(); - }); - }); - }; + // 显示容器 + tl.fromTo(containerRef.current, + { opacity: 0, scale: 0.9 }, + { opacity: 1, scale: 1, duration: 0.3 } + ); - // 循环控制 - const startNextCycle = () => { - // 先进行全局清理 - globalCleanup(); - - setAnimationCycle(prev => prev + 1); - setShowImages(false); - - // 延迟一下再显示文字,确保清理完成 - setTimeout(() => { - setShowText(true); - }, 100); - }; + // 显示视频 + setShowVideo(true); - const handleTextComplete = () => { - setShowText(false); - setTimeout(() => { - setShowImages(true); - }, 100); - }; - - const handleImagesComplete = () => { - setShowImages(false); - // 延迟后开始新的循环 - setTimeout(() => { - startNextCycle(); - }, 1000); - }; - - // 开始第一轮动画 - useEffect(() => { - if (isClient && animationCycle === 0) { - startNextCycle(); - } - }, [isClient]); - - // 组件卸载时清理 - useEffect(() => { - return () => { - globalCleanup(); - }; - }, []); - - useEffect(() => { - setIsClient(true); - }, []); - - if (!isClient) { - return null; - } + }, [shouldStart, onComplete]); return ( -
- {showText && ( - - )} - {showImages && ( - + {showVideo && ( +
+ ); +}; -
+// 主组件 +export const EmptyStateAnimation = ({ className }: { className: string }) => { + const [currentStage, setCurrentStage] = useState<'input' | 'wave' | 'final'>('input'); + const [animationCycle, setAnimationCycle] = useState(0); + const [isReady, setIsReady] = useState(true); + + const handleStageComplete = useCallback(() => { + // 先将当前阶段标记为不可执行 + setIsReady(false); + + // 延迟切换到下一个阶段 + setTimeout(() => { + switch (currentStage) { + case 'input': + setCurrentStage('wave'); + break; + case 'wave': + setCurrentStage('final'); + break; + case 'final': + setAnimationCycle(prev => prev + 1); + setCurrentStage('input'); + break; + } + + // 给下一个阶段一些准备时间 + setTimeout(() => { + setIsReady(true); + }, 100); + }, 300); + }, [currentStage]); + + return ( +
+ + +
); }; \ No newline at end of file diff --git a/components/common/EmptyStateAnimation2.tsx b/components/common/EmptyStateAnimation2.tsx new file mode 100644 index 0000000..87c9ac0 --- /dev/null +++ b/components/common/EmptyStateAnimation2.tsx @@ -0,0 +1,43 @@ +"use client"; + +import React, { useState, useEffect, useRef, useCallback } from 'react'; +import { ArrowUp } from 'lucide-react'; +import gsap from 'gsap'; +import { ImageWave } from '@/components/ui/ImageWave'; + +// 主组件 +export const EmptyStateAnimation = ({ className }: { className: string }) => { + const imageUrls = [ + 'https://d3phaj0sisr2ct.cloudfront.net/app/gen4/object-reference/welcome-ref-1.jpg', + 'https://d3phaj0sisr2ct.cloudfront.net/app/gen4/object-reference/welcome-ref-2.jpg', + 'https://d3phaj0sisr2ct.cloudfront.net/app/gen4/object-reference/welcome-ref-3.jpg', + 'https://d3phaj0sisr2ct.cloudfront.net/app/gen4/object-reference/welcome-ref-4.jpg', + 'https://d3phaj0sisr2ct.cloudfront.net/app/gen4/object-reference/welcome-ref-1.jpg', + 'https://d3phaj0sisr2ct.cloudfront.net/app/gen4/object-reference/welcome-ref-2.jpg', + 'https://d3phaj0sisr2ct.cloudfront.net/app/gen4/object-reference/welcome-ref-3.jpg', + 'https://d3phaj0sisr2ct.cloudfront.net/app/gen4/object-reference/welcome-ref-4.jpg', + 'https://d3phaj0sisr2ct.cloudfront.net/app/gen4/object-reference/welcome-ref-1.jpg', + 'https://d3phaj0sisr2ct.cloudfront.net/app/gen4/object-reference/welcome-ref-2.jpg', + 'https://d3phaj0sisr2ct.cloudfront.net/app/gen4/object-reference/welcome-ref-3.jpg', + 'https://d3phaj0sisr2ct.cloudfront.net/app/gen4/object-reference/welcome-ref-4.jpg', + 'https://d3phaj0sisr2ct.cloudfront.net/app/gen4/object-reference/welcome-ref-1.jpg', + 'https://d3phaj0sisr2ct.cloudfront.net/app/gen4/object-reference/welcome-ref-2.jpg', + 'https://d3phaj0sisr2ct.cloudfront.net/app/gen4/object-reference/welcome-ref-3.jpg', + 'https://d3phaj0sisr2ct.cloudfront.net/app/gen4/object-reference/welcome-ref-4.jpg', + 'https://d3phaj0sisr2ct.cloudfront.net/app/gen4/object-reference/welcome-ref-1.jpg', + 'https://d3phaj0sisr2ct.cloudfront.net/app/gen4/object-reference/welcome-ref-2.jpg', + ]; + + return ( + + ); +}; \ No newline at end of file diff --git a/components/layout/dashboard-layout.tsx b/components/layout/dashboard-layout.tsx index bc4583c..dfae4e5 100644 --- a/components/layout/dashboard-layout.tsx +++ b/components/layout/dashboard-layout.tsx @@ -14,12 +14,8 @@ export function DashboardLayout({ children }: DashboardLayoutProps) { return (
-
- setSidebarCollapsed(!sidebarCollapsed)} /> -
- {children} -
-
+ setSidebarCollapsed(!sidebarCollapsed)} /> + {children}
); } \ No newline at end of file diff --git a/components/layout/sidebar.tsx b/components/layout/sidebar.tsx index 636ef75..4553f18 100644 --- a/components/layout/sidebar.tsx +++ b/components/layout/sidebar.tsx @@ -6,6 +6,7 @@ import { usePathname } from 'next/navigation'; import { cn } from '@/public/lib/utils'; import { Button } from '@/components/ui/button'; import { Separator } from '@/components/ui/separator'; +import { GradientText } from '@/components/ui/gradient-text'; import { Home, FolderOpen, @@ -46,7 +47,7 @@ export function Sidebar({ collapsed, onToggle }: SidebarProps) { {/* Backdrop */} {!collapsed && (
onToggle(true)} /> )} @@ -54,7 +55,7 @@ export function Sidebar({ collapsed, onToggle }: SidebarProps) { {/* Sidebar */}
@@ -62,8 +63,12 @@ export function Sidebar({ collapsed, onToggle }: SidebarProps) {
diff --git a/components/pages/create-to-video2.tsx b/components/pages/create-to-video2.tsx index 2f75527..0ddc75a 100644 --- a/components/pages/create-to-video2.tsx +++ b/components/pages/create-to-video2.tsx @@ -1,62 +1,20 @@ "use client"; -import { useState, useEffect, useRef } from 'react'; -import { Card } from '@/components/ui/card'; -import { Button } from '@/components/ui/button'; -import { ArrowLeft, ChevronDown, ChevronUp, Video, ListOrdered, Play, Loader2, Pause, MoreHorizontal, Edit2, Check, X, RefreshCw, Lightbulb, Package, Crown, ArrowUp } from 'lucide-react'; +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 { Textarea } from "@/components/ui/textarea"; import './style/create-to-video2.css'; -import { ResizableHandle, ResizablePanel, ResizablePanelGroup } from "@/components/ui/resizable"; -import { Skeleton } from "@/components/ui/skeleton"; -import LiquidGlass from '@/plugins/liquid-glass/index' -import { Dropdown } from 'antd'; +import { Dropdown, Menu } from 'antd'; import type { MenuProps } from 'antd'; -import Image from 'next/image'; import dynamic from 'next/dynamic'; -import { ProjectTypeEnum, ModeEnum, ResolutionEnum } from "@/api/enums"; -import { createScriptEpisode, CreateScriptEpisodeRequest, updateScriptEpisode, UpdateScriptEpisodeRequest } from "@/api/script_episode"; +import { ModeEnum, ResolutionEnum } from "@/app/model/enums"; +import { createScriptEpisodeNew, getScriptEpisodeListNew, CreateScriptEpisodeRequest } from "@/api/script_episode"; import { getUploadToken, uploadToQiniu } from "@/api/common"; -import { convertScriptToScene, convertVideoToScene } from "@/api/video_flow"; -import { EmptyStateAnimation } from '@/components/common/EmptyStateAnimation'; +import { EmptyStateAnimation } from '@/components/common/EmptyStateAnimation2'; -const JoyrideNoSSR = dynamic(() => import('react-joyride'), { - ssr: false, -}); - -// 导入Step类型 -import type { Step } from 'react-joyride'; -// interface Step { -// target: string; -// content: string; -// placement?: 'top' | 'bottom' | 'left' | 'right'; -// } - -// 添加自定义滚动条样式 -const scrollbarStyles = ` - .custom-scrollbar::-webkit-scrollbar { - width: 4px; - } - .custom-scrollbar::-webkit-scrollbar-track { - background: rgba(255, 255, 255, 0.05); - border-radius: 2px; - } - .custom-scrollbar::-webkit-scrollbar-thumb { - background: rgba(255, 255, 255, 0.1); - border-radius: 2px; - } - .custom-scrollbar::-webkit-scrollbar-thumb:hover { - background: rgba(255, 255, 255, 0.2); - } -`; - -interface SceneVideo { - id: number; - video_url: string; - script: any; -} const ideaText = 'a cute capybara with an orange on its head, staring into the distance and walking forward'; @@ -80,17 +38,69 @@ export function CreateToVideo2() { const [isCreating, setIsCreating] = useState(false); const [generatedVideoList, setGeneratedVideoList] = useState([]); const [projectName, setProjectName] = useState('默认名称'); + const [episodeList, setEpisodeList] = useState([]); + 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('all'); + const [sortBy, setSortBy] = useState('created_at'); + const [isLoadingMore, setIsLoadingMore] = useState(false); + const scrollContainerRef = useRef(null); + const [isSmartAssistantExpanded, setIsSmartAssistantExpanded] = useState(false); + const [userId, setUserId] = useState(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'); // 打开文件选择器 @@ -124,72 +134,27 @@ export function CreateToVideo2() { } const handleCreateVideo = async () => { + setIsCreating(true); // 创建剧集数据 - const episodeData: CreateScriptEpisodeRequest = { - title: "episode default", - script_id: projectId, - status: 1, - summary: script + let episodeData: any = { + user_id: String(userId), + script: script, + mode: selectedMode, + resolution: selectedResolution }; // 调用创建剧集API - const episodeResponse = await createScriptEpisode(episodeData); + 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.id; - - if (videoUrl || script) { - try { - setIsCreating(true); - let convertResponse; - - // 根据选中的选项卡调用相应的API - if (activeTab === 'script') { - // 剧本模式:调用convertScriptToScene (第43-56行) - if (!script.trim()) { - alert('请输入剧本内容'); - return; - } - convertResponse = await convertScriptToScene(script, episodeId, projectId); - } else { - // 视频模式:调用convertVideoToScene (第56-69行) - if (!videoUrl) { - alert('请先上传视频'); - return; - } - if (!episodeId) { - alert('Episode ID not available'); - return; - } - convertResponse = await convertVideoToScene(videoUrl, episodeId, projectId); - } - // 更新剧集 - const updateEpisodeData: UpdateScriptEpisodeRequest = { - id: episodeId, - atmosphere: convertResponse.data.atmosphere, - summary: convertResponse.data.summary, - scene: convertResponse.data.scene, - characters: convertResponse.data.characters, - }; - const updateEpisodeResponse = await updateScriptEpisode(updateEpisodeData); - - // 检查转换结果 - if (convertResponse.code === 0) { - // 成功创建后跳转到work-flow页面, 并设置episodeId 和 projectType - router.push(`/create/work-flow?episodeId=${episodeResponse.data.id}`); - } else { - alert(`转换失败: ${convertResponse.message}`); - } - } catch (error) { - console.error('创建过程出错:', error); - alert("创建项目时发生错误,请稍后重试"); - } finally { - setIsCreating(false); - } - } + let episodeId = episodeResponse.data.project_id; + // let episodeId = '9c34fcc4-c8d8-44fc-879e-9bd56f608c76'; + router.push(`/create/work-flow?episodeId=${episodeId}`); + setIsCreating(false); } // 下拉菜单项配置 @@ -206,9 +171,8 @@ export function CreateToVideo2() {
Auto -
- Automatically selects the best model for optimal efficiency + Automatically execute the workflow, you can't edit the workflow before it's finished.
), }, @@ -220,7 +184,7 @@ export function CreateToVideo2() { Manual
- Offers reliable, consistent performance every time + Manually control the workflow, you can control the workflow everywhere.
), }, @@ -277,12 +241,12 @@ export function CreateToVideo2() { // 处理模式选择 const handleModeSelect: MenuProps['onClick'] = ({ key }) => { - setSelectedMode(Number(key) as ModeEnum); + setSelectedMode(key as ModeEnum); }; // 处理分辨率选择 const handleResolutionSelect: MenuProps['onClick'] = ({ key }) => { - setSelectedResolution(Number(key) as ResolutionEnum); + setSelectedResolution(key as ResolutionEnum); }; const handleStartCreating = () => { @@ -293,70 +257,64 @@ export function CreateToVideo2() { // 处理编辑器聚焦 const handleEditorFocus = () => { setIsFocus(true); - if (editorRef.current && script) { - // 创建范围对象 + 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 - ) || editorRef.current.appendChild(document.createTextNode(script)); + ); + + 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); + } - // 设置范围到文本末尾 - range.setStart(textNode, script.length); - range.setEnd(textNode, script.length); - - // 应用选择 selection?.removeAllRanges(); selection?.addRange(range); } }; - // 处理编辑器内容变化 - const handleEditorChange = (e: React.FormEvent) => { - const script = e.currentTarget.textContent || ''; - setInputText(script); + const handleCompositionStart = () => { + setIsComposing(true); }; - // 引导步骤 - const steps: Step[] = [ - { - target: '.video-storyboard-tools', - content: 'Welcome to AI Video Creation Tool! This is the main creation area.', - placement: 'top', - }, - { - target: '.storyboard-tools-tab', - content: 'Choose between Script mode to create videos from text, or Clone mode to recreate existing videos.', - placement: 'bottom', - }, - { - target: '.video-prompt-editor', - content: 'Describe your video content here. Our AI will generate videos based on your description.', - placement: 'top', - }, - { - target: '.tool-operation-button', - content: 'Select different creation modes and video resolutions to customize your output.', - placement: 'top', - }, - { - target: '.tool-submit-button', - content: 'Click here to start creating your video once you are ready!', - placement: 'left', - }, - ]; + const handleCompositionEnd = (e: React.CompositionEvent) => { + setIsComposing(false); + handleEditorChange(e as any); + }; - // 处理引导结束 - const handleJoyrideCallback = (data: any) => { - const { status } = data; - if (status === 'finished' || status === 'skipped') { - setRunTour(false); - // 可以在这里存储用户已完成引导的状态 - if (typeof window !== 'undefined') { - localStorage.setItem('hasCompletedTour', 'true'); - } + const handleEditorChange = (e: React.FormEvent) => { + // 如果正在输入中文,不要更新内容 + if (isComposing) return; + + 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; + + setTimeout(() => { + if (editorRef.current) { + const textNode = Array.from(editorRef.current.childNodes).find( + node => node.nodeType === Node.TEXT_NODE + ); + + if (textNode) { + const newRange = document.createRange(); + newRange.setStart(textNode, currentPosition); + newRange.setEnd(textNode, currentPosition); + selection.removeAllRanges(); + selection.addRange(newRange); + } + } + }, 0); } }; @@ -374,180 +332,305 @@ export function CreateToVideo2() { setIsClient(true); }, []); - return ( -
- {isClient && ( - - )} -
- {/* 空状态 */} - - {/* 工具栏 */} -
-
- {isExpanded ? ( -
setIsExpanded(false)}> - {/* 图标 展开按钮 */} - - Click to action -
- ) : ( -
setIsExpanded(true)}> - {/* 图标 折叠按钮 */} - -
- )} - -
-
setActiveTab('script')}> - script -
-
setActiveTab('clone')}> - clone + // 渲染剧集卡片 + const renderEpisodeCard = (episode: any) => { + return ( +
router.push(`/create/work-flow?episodeId=${episode.project_id}`)} + > + {/* 视频缩略图 */} +
+ {episode.final_video_url ? ( +
+ + {/* 内容区域 */} +
+

+ {episode.name || '未命名剧集'} +

+ + {/* 元数据 */} +
+
+ + {new Date(episode.created_at).toLocaleDateString()} +
+ +
+ +
+ + {new Date(episode.updated_at).toLocaleTimeString([], {hour: '2-digit', minute:'2-digit'})}
+
+
+ + {/* 操作按钮 */} +
+
+ +
+
+
+ ); + }; -
-
- {activeTab === 'clone' && ( -
-
-
- {/* 图标 添加视频 */} -
-
- Add Video + return ( + <> +
+ {/* 优化后的主要内容区域 */} +
+
+ {isLoading && episodeList.length === 0 ? ( + /* 优化的加载状态 */ +
+ {[...Array(10)].map((_, index) => ( +
+
+
+
+
+
+
+
+
+
- {videoUrl && ( -
-
-
-
- )}
- )} - {activeTab === 'script' && ( -
-
-
setIsFocus(false)} - onInput={handleEditorChange} - suppressContentEditableWarning - > - {script} -
-
- Describe the content you want to action. Get an - setInputText(ideaText)} - > - idea - -
+ ))} +
+ ) : episodeList.length > 0 ? ( + /* 优化的剧集网格 */ +
+
+ {episodeList.map(renderEpisodeCard)} +
+ + {/* 加载更多指示器 */} + {isLoadingMore && ( +
+
+ + 加载更多剧集中...
)} -
-
-
-
- -
- - - {selectedMode === ModeEnum.AUTOMATIC ? 'Auto' : 'Manual'} - - -
-
- - -
-
-
+ + {/* 到底提示 */} + {!hasMore && episodeList.length > 0 && ( +
+
+
+
+

已加载全部剧集

-
-
- {isCreating ? ( - <> - - Actioning... - + )} +
+ ) : ( + <> + )} +
+
+
+ + {/* 创建工具栏 */} +
+
+ {isExpanded ? ( +
setIsExpanded(false)}> + + Click to action +
+ ) : ( +
setIsExpanded(true)}> + +
+ )} + +
+
setActiveTab('script')}> + script +
+
setActiveTab('clone')}> + clone +
+
+ +
+
+ {activeTab === 'clone' && ( +
+
+
+ {isUploading ? ( + ) : ( - <> - - Action - +
+
+ + {isUploading ? '上传中...' : 'Add Video'} + +
+
+ {videoUrl && ( +
+
+
+
+ )} +
+ )} + {activeTab === 'script' && ( +
+
+
setIsFocus(false)} + onInput={handleEditorChange} + onCompositionStart={handleCompositionStart} + onCompositionEnd={handleCompositionEnd} + suppressContentEditableWarning + > + {script} +
+
+ Describe the content you want to action. Get an + setInputText(ideaText)} + > + idea + +
+
+
+ )} +
+
+
+
+ +
+ + + {selectedMode === ModeEnum.AUTOMATIC ? 'Auto' : 'Manual'} + + +
+
+ + +
+
+
+
+
+
+
+
+ {isCreating ? ( + <> + + Actioning... + + ) : ( + <> + + Action + + )}
@@ -555,6 +638,12 @@ export function CreateToVideo2() {
-
+ + {episodeList.length === 0 && !isLoading && ( +
+ +
+ )} + ); -} \ No newline at end of file +} \ No newline at end of file diff --git a/components/pages/home-page2.tsx b/components/pages/home-page2.tsx index 2110bc5..77b8644 100644 --- a/components/pages/home-page2.tsx +++ b/components/pages/home-page2.tsx @@ -1,12 +1,13 @@ "use client"; import { useState, useRef, useEffect } from "react"; -import { Plus, Table, AlignHorizontalSpaceAround, Loader2 } from "lucide-react"; +import { Table, AlignHorizontalSpaceAround, Loader2, Clapperboard } from "lucide-react"; import "./style/home-page2.css"; import { useRouter } from "next/navigation"; -import { VideoScreenLayout } from '@/components/video-screen-layout'; +import { VideoCarouselLayout } from '@/components/video-carousel-layout'; import { VideoGridLayout } from '@/components/video-grid-layout'; -import { motion } from "framer-motion"; +import { motion, AnimatePresence } from "framer-motion"; +import { LiquidButton } from "@/components/ui/liquid-glass-button"; import { createScriptProject, CreateScriptProjectRequest @@ -15,7 +16,7 @@ import { ProjectTypeEnum, ModeEnum, ResolutionEnum -} from '@/api/enums'; +} from '@/app/model/enums'; import { getResourcesList, Resource @@ -24,6 +25,7 @@ import { export function HomePage2() { const router = useRouter(); const [activeTool, setActiveTool] = useState("stretch"); + const [dropPosition, setDropPosition] = useState<"left" | "right">("left"); const [isCreating, setIsCreating] = useState(false); const [createdProjectId, setCreatedProjectId] = useState(null); const [resources, setResources] = useState([]); @@ -76,6 +78,8 @@ export function HomePage2() { try { setIsCreating(true); + router.push(`/create`); + return; // 使用默认值 const projectType = ProjectTypeEnum.SCRIPT_TO_VIDEO; @@ -105,46 +109,73 @@ export function HomePage2() { } }; + // 处理工具切换 + const handleToolChange = (position: "left" | "right") => { + setDropPosition(position); + setActiveTool(position === "left" ? "stretch" : "table"); + }; + return (
-
+
{/* 工具栏-列表形式切换 */} -
-
- -