连调工作流接口以及优化首页样式等

This commit is contained in:
北枳 2025-07-13 23:20:53 +08:00
parent d72d0e6457
commit 53b6d87179
41 changed files with 3175 additions and 2427 deletions

View File

@ -1 +1 @@
export const BASE_URL = "https://pre.movieflow.api.huiying.video" export const BASE_URL = "https://77.smartvideo.py.qikongjian.com"

View File

@ -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;

View File

@ -61,6 +61,16 @@ export interface ScriptEpisode {
video_url?: string; video_url?: string;
} }
// 新-获取剧集列表
export const getScriptEpisodeListNew = async (data: any): Promise<ApiResponse<any>> => {
return post<ApiResponse<any>>('/movie/list_movie_projects', data);
};
// 新-创建接口
export const createScriptEpisodeNew = async (data: any): Promise<ApiResponse<any>> => {
return post<ApiResponse<any>>('/movie/create_movie_project', data);
};
// 创建剧集 // 创建剧集
export const createScriptEpisode = async (data: CreateScriptEpisodeRequest): Promise<ApiResponse<ScriptEpisode>> => { export const createScriptEpisode = async (data: CreateScriptEpisodeRequest): Promise<ApiResponse<ScriptEpisode>> => {
// return post<ApiResponse<ScriptEpisode>>('/script_episode/create', data); // return post<ApiResponse<ScriptEpisode>>('/script_episode/create', data);

View File

@ -72,31 +72,7 @@ export interface DeleteScriptProjectRequest {
// 创建剧本项目 // 创建剧本项目
export const createScriptProject = async (data: CreateScriptProjectRequest): Promise<ApiResponse<ScriptProject>> => { export const createScriptProject = async (data: CreateScriptProjectRequest): Promise<ApiResponse<ScriptProject>> => {
// return post<ApiResponse<ScriptProject>>('/script_project/create', data); return post<ApiResponse<ScriptProject>>('/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);
});
}; };
// 获取剧本项目列表 // 获取剧本项目列表
@ -113,3 +89,13 @@ export const updateScriptProject = async (data: UpdateScriptProjectRequest): Pro
export const deleteScriptProject = async (data: DeleteScriptProjectRequest): Promise<ApiResponse<ScriptProject>> => { export const deleteScriptProject = async (data: DeleteScriptProjectRequest): Promise<ApiResponse<ScriptProject>> => {
return post<ApiResponse<ScriptProject>>('/script_project/delete', data); return post<ApiResponse<ScriptProject>>('/script_project/delete', data);
}; };
// 获取剧本项目详情请求数据类型
export interface GetScriptProjectDetailRequest {
id: number;
}
// 获取剧本项目详情
export const getScriptProjectDetail = async (data: GetScriptProjectDetailRequest): Promise<ApiResponse<ScriptProject>> => {
return post<ApiResponse<ScriptProject>>('/script_project/detail', data);
};

View File

@ -1,6 +1,69 @@
import { post } from './request'; import { post } from './request';
import { ProjectTypeEnum } from './enums'; import { ProjectTypeEnum } from '@/app/model/enums';
import { ApiResponse } from './common'; import { ApiResponse } from '@/api/common';
// API 响应类型
interface BaseApiResponse<T> {
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 { export interface Scene {
@ -50,7 +113,7 @@ export interface VideoToSceneRequest {
export type ConvertScenePromptRequest = ScriptToSceneRequest | VideoToSceneRequest; export type ConvertScenePromptRequest = ScriptToSceneRequest | VideoToSceneRequest;
// 转换分镜头响应接口 // 转换分镜头响应接口
export type ConvertScenePromptResponse = ApiResponse<ScenePrompts>; export type ConvertScenePromptResponse = BaseApiResponse<ScenePrompts>;
/** /**
* *
@ -116,3 +179,41 @@ export const convertVideoToScene = async (
project_type: ProjectTypeEnum.VIDEO_TO_VIDEO project_type: ProjectTypeEnum.VIDEO_TO_VIDEO
}); });
}; };
// 新-获取剧集详情
export const detailScriptEpisodeNew = async (data: { project_id: string }): Promise<ApiResponse<any>> => {
return post<ApiResponse<any>>('/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<ApiResponse<any>> => {
return post<ApiResponse<any>>('/movie/get_movie_project_name', data);
return {
code: 0,
successful: true,
message: 'success',
data: {
name: '提取出视频标题'
}
}
}
// 获取 数据 全量(需轮询)
export const getRunningStreamData = async (data: { project_id: string }): Promise<ApiResponse<any>> => {
return post<ApiResponse<any>>('/movie/get_status', data);
};

View File

@ -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));
}
}

View File

@ -2,6 +2,12 @@
@tailwind components; @tailwind components;
@tailwind utilities; @tailwind utilities;
*,
*:after,
*:before {
box-sizing: border-box;
}
:root { :root {
--foreground-rgb: 0, 0, 0; --foreground-rgb: 0, 0, 0;
--background-start-rgb: 214, 219, 220; --background-start-rgb: 214, 219, 220;
@ -10,6 +16,10 @@
--text-secondary: #a0aec0; --text-secondary: #a0aec0;
--text-primary: #fff; --text-primary: #fff;
--ui-level1-layerbase: #131416; --ui-level1-layerbase: #131416;
/* 3dwave */
--index: calc(1vh + 1vw);
--transition: cubic-bezier(0.1, 0.7, 0, 1);
} }
@media (prefers-color-scheme: dark) { @media (prefers-color-scheme: dark) {
@ -99,6 +109,9 @@ body {
.hide-scrollbar::-webkit-scrollbar { .hide-scrollbar::-webkit-scrollbar {
display: none !important; display: none !important;
} }
*::-webkit-scrollbar {
display: none !important;
}
@layer base { @layer base {
* { * {

136
app/model/enums.ts Normal file
View File

@ -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;

176
app/model/sse_response.ts Normal file
View File

@ -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
};
}
}

File diff suppressed because it is too large Load Diff

View File

@ -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 (
<ImageWave
images={imageUrls}
containerWidth="90vw"
containerHeight="60vh"
itemWidth="calc(var(--index) * 2)"
itemHeight="calc(var(--index) * 12)"
gap="0.1rem"
autoAnimate={true}
autoAnimateInterval={100}
/>
);
};

View File

@ -14,12 +14,8 @@ export function DashboardLayout({ children }: DashboardLayoutProps) {
return ( return (
<div className="min-h-screen bg-background"> <div className="min-h-screen bg-background">
<Sidebar collapsed={sidebarCollapsed} onToggle={setSidebarCollapsed} /> <Sidebar collapsed={sidebarCollapsed} onToggle={setSidebarCollapsed} />
<div className="w-full">
<TopBar collapsed={sidebarCollapsed} onToggleSidebar={() => setSidebarCollapsed(!sidebarCollapsed)} /> <TopBar collapsed={sidebarCollapsed} onToggleSidebar={() => setSidebarCollapsed(!sidebarCollapsed)} />
<main className="mt-16">
{children} {children}
</main>
</div>
</div> </div>
); );
} }

View File

@ -6,6 +6,7 @@ import { usePathname } from 'next/navigation';
import { cn } from '@/public/lib/utils'; import { cn } from '@/public/lib/utils';
import { Button } from '@/components/ui/button'; import { Button } from '@/components/ui/button';
import { Separator } from '@/components/ui/separator'; import { Separator } from '@/components/ui/separator';
import { GradientText } from '@/components/ui/gradient-text';
import { import {
Home, Home,
FolderOpen, FolderOpen,
@ -46,7 +47,7 @@ export function Sidebar({ collapsed, onToggle }: SidebarProps) {
{/* Backdrop */} {/* Backdrop */}
{!collapsed && ( {!collapsed && (
<div <div
className="fixed inset-0 bg-[#000000bf] z-40" className="fixed inset-0 bg-[#000000bf] z-[998]"
onClick={() => onToggle(true)} onClick={() => onToggle(true)}
/> />
)} )}
@ -54,7 +55,7 @@ export function Sidebar({ collapsed, onToggle }: SidebarProps) {
{/* Sidebar */} {/* Sidebar */}
<div <div
className={cn( className={cn(
'fixed left-0 top-0 z-50 h-full w-64 bg-[#131416] transition-transform duration-300', 'fixed left-0 top-0 z-[999] h-full w-64 bg-[#131416] transition-transform duration-300',
collapsed ? '-translate-x-full' : 'translate-x-0' collapsed ? '-translate-x-full' : 'translate-x-0'
)} )}
> >
@ -62,8 +63,12 @@ export function Sidebar({ collapsed, onToggle }: SidebarProps) {
<div className="flex h-16 items-center justify-between px-4"> <div className="flex h-16 items-center justify-between px-4">
<div className="flex items-center space-x-2"> <div className="flex items-center space-x-2">
<Video className="h-8 w-8 text-primary" /> <Video className="h-8 w-8 text-primary" />
<span className="text-xl font-bold text-primary"> <span className="text-xl font-bold">
Movie Flow <GradientText
text="MovieFlow"
startPercentage={30}
endPercentage={70}
/>
</span> </span>
</div> </div>
<Button <Button

View File

@ -2,6 +2,7 @@
import '../pages/style/top-bar.css'; import '../pages/style/top-bar.css';
import { Button } from '@/components/ui/button'; import { Button } from '@/components/ui/button';
import { GradientText } from '@/components/ui/gradient-text';
import { import {
DropdownMenu, DropdownMenu,
DropdownMenuContent, DropdownMenuContent,
@ -36,7 +37,7 @@ export function TopBar({ collapsed, onToggleSidebar }: { collapsed: boolean; onT
}; };
return ( return (
<div className="fixed right-0 top-0 left-0 h-16 header"> <div className="fixed right-0 top-0 left-0 h-16 header z-50">
<div className="h-full flex items-center justify-between pr-6 pl-2"> <div className="h-full flex items-center justify-between pr-6 pl-2">
<div className="flex items-center space-x-4"> <div className="flex items-center space-x-4">
<Button className='button-NxtqWZ' variant="ghost" size="sm" onClick={onToggleSidebar}> <Button className='button-NxtqWZ' variant="ghost" size="sm" onClick={onToggleSidebar}>
@ -50,10 +51,22 @@ export function TopBar({ collapsed, onToggleSidebar }: { collapsed: boolean; onT
> >
<span className="translate"> <span className="translate">
<span> <span>
<h1 className="logo text-2xl font-bold bg-gradient-to-r from-primary to-white bg-clip-text text-transparent">Movie Flow</h1> <h1 className="logo text-2xl font-bold">
<GradientText
text="MovieFlow"
startPercentage={30}
endPercentage={70}
/>
</h1>
</span> </span>
<span> <span>
<h1 className="logo text-2xl font-bold bg-gradient-to-r from-primary to-white bg-clip-text text-transparent">Movie Flow</h1> <h1 className="logo text-2xl font-bold">
<GradientText
text="MovieFlow"
startPercentage={30}
endPercentage={70}
/>
</h1>
</span> </span>
</span> </span>
</div> </div>

View File

@ -1,62 +1,20 @@
"use client"; "use client";
import { useState, useEffect, useRef } from 'react'; import { useState, useEffect, useRef, useCallback } from 'react';
import { Card } from '@/components/ui/card'; import { Select, SelectContent, SelectItem, SelectTrigger, SelectValue } from '@/components/ui/select';
import { Button } from '@/components/ui/button'; 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 { ArrowLeft, ChevronDown, ChevronUp, Video, ListOrdered, Play, Loader2, Pause, MoreHorizontal, Edit2, Check, X, RefreshCw, Lightbulb, Package, Crown, ArrowUp } from 'lucide-react';
import { useRouter, useSearchParams } from 'next/navigation'; import { useRouter, useSearchParams } from 'next/navigation';
import { Sheet, SheetContent, SheetHeader, SheetTitle, SheetTrigger, SheetClose } from "@/components/ui/sheet"; import { Sheet, SheetContent, SheetHeader, SheetTitle, SheetTrigger, SheetClose } from "@/components/ui/sheet";
import { Input } from "@/components/ui/input"; import { Input } from "@/components/ui/input";
import { Textarea } from "@/components/ui/textarea";
import './style/create-to-video2.css'; import './style/create-to-video2.css';
import { ResizableHandle, ResizablePanel, ResizablePanelGroup } from "@/components/ui/resizable"; import { Dropdown, Menu } from 'antd';
import { Skeleton } from "@/components/ui/skeleton";
import LiquidGlass from '@/plugins/liquid-glass/index'
import { Dropdown } from 'antd';
import type { MenuProps } from 'antd'; import type { MenuProps } from 'antd';
import Image from 'next/image';
import dynamic from 'next/dynamic'; import dynamic from 'next/dynamic';
import { ProjectTypeEnum, ModeEnum, ResolutionEnum } from "@/api/enums"; import { ModeEnum, ResolutionEnum } from "@/app/model/enums";
import { createScriptEpisode, CreateScriptEpisodeRequest, updateScriptEpisode, UpdateScriptEpisodeRequest } from "@/api/script_episode"; import { createScriptEpisodeNew, getScriptEpisodeListNew, CreateScriptEpisodeRequest } from "@/api/script_episode";
import { getUploadToken, uploadToQiniu } from "@/api/common"; import { getUploadToken, uploadToQiniu } from "@/api/common";
import { convertScriptToScene, convertVideoToScene } from "@/api/video_flow"; import { EmptyStateAnimation } from '@/components/common/EmptyStateAnimation2';
import { EmptyStateAnimation } from '@/components/common/EmptyStateAnimation';
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'; 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 [isCreating, setIsCreating] = useState(false);
const [generatedVideoList, setGeneratedVideoList] = useState<any[]>([]); const [generatedVideoList, setGeneratedVideoList] = useState<any[]>([]);
const [projectName, setProjectName] = useState('默认名称'); 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 // 在客户端挂载后读取localStorage
useEffect(() => { useEffect(() => {
if (typeof window !== 'undefined') { if (typeof window !== 'undefined') {
const currentUser = JSON.parse(localStorage.getItem('currentUser') || '{}');
console.log('currentUser', currentUser);
setUserId(currentUser.id);
const savedProjectName = localStorage.getItem('projectName'); const savedProjectName = localStorage.getItem('projectName');
if (savedProjectName) { if (savedProjectName) {
setProjectName(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 () => { const handleUploadVideo = async () => {
console.log('upload video'); console.log('upload video');
// 打开文件选择器 // 打开文件选择器
@ -124,73 +134,28 @@ export function CreateToVideo2() {
} }
const handleCreateVideo = async () => { const handleCreateVideo = async () => {
setIsCreating(true);
// 创建剧集数据 // 创建剧集数据
const episodeData: CreateScriptEpisodeRequest = { let episodeData: any = {
title: "episode default", user_id: String(userId),
script_id: projectId, script: script,
status: 1, mode: selectedMode,
summary: script resolution: selectedResolution
}; };
// 调用创建剧集API // 调用创建剧集API
const episodeResponse = await createScriptEpisode(episodeData); const episodeResponse = await createScriptEpisodeNew(episodeData);
console.log('episodeResponse', episodeResponse);
if (episodeResponse.code !== 0) { if (episodeResponse.code !== 0) {
console.error(`创建剧集失败: ${episodeResponse.message}`); console.error(`创建剧集失败: ${episodeResponse.message}`);
alert(`创建剧集失败: ${episodeResponse.message}`); alert(`创建剧集失败: ${episodeResponse.message}`);
return; return;
} }
let episodeId = episodeResponse.data.id; let episodeId = episodeResponse.data.project_id;
// let episodeId = '9c34fcc4-c8d8-44fc-879e-9bd56f608c76';
if (videoUrl || script) { router.push(`/create/work-flow?episodeId=${episodeId}`);
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); setIsCreating(false);
} }
}
}
// 下拉菜单项配置 // 下拉菜单项配置
const modeItems: MenuProps['items'] = [ const modeItems: MenuProps['items'] = [
@ -206,9 +171,8 @@ export function CreateToVideo2() {
<div className="flex flex-col gap-1 p-1"> <div className="flex flex-col gap-1 p-1">
<div className="flex items-center gap-2"> <div className="flex items-center gap-2">
<span className="text-base font-medium">Auto</span> <span className="text-base font-medium">Auto</span>
<Crown className="w-4 h-4 text-yellow-500" />
</div> </div>
<span className="text-sm text-gray-400">Automatically selects the best model for optimal efficiency</span> <span className="text-sm text-gray-400">Automatically execute the workflow, you can't edit the workflow before it's finished.</span>
</div> </div>
), ),
}, },
@ -220,7 +184,7 @@ export function CreateToVideo2() {
<span className="text-base font-medium">Manual</span> <span className="text-base font-medium">Manual</span>
<Crown className="w-4 h-4 text-yellow-500" /> <Crown className="w-4 h-4 text-yellow-500" />
</div> </div>
<span className="text-sm text-gray-400">Offers reliable, consistent performance every time</span> <span className="text-sm text-gray-400">Manually control the workflow, you can control the workflow everywhere.</span>
</div> </div>
), ),
}, },
@ -277,12 +241,12 @@ export function CreateToVideo2() {
// 处理模式选择 // 处理模式选择
const handleModeSelect: MenuProps['onClick'] = ({ key }) => { const handleModeSelect: MenuProps['onClick'] = ({ key }) => {
setSelectedMode(Number(key) as ModeEnum); setSelectedMode(key as ModeEnum);
}; };
// 处理分辨率选择 // 处理分辨率选择
const handleResolutionSelect: MenuProps['onClick'] = ({ key }) => { const handleResolutionSelect: MenuProps['onClick'] = ({ key }) => {
setSelectedResolution(Number(key) as ResolutionEnum); setSelectedResolution(key as ResolutionEnum);
}; };
const handleStartCreating = () => { const handleStartCreating = () => {
@ -293,71 +257,65 @@ export function CreateToVideo2() {
// 处理编辑器聚焦 // 处理编辑器聚焦
const handleEditorFocus = () => { const handleEditorFocus = () => {
setIsFocus(true); setIsFocus(true);
if (editorRef.current && script) { if (editorRef.current) {
// 创建范围对象
const range = document.createRange(); const range = document.createRange();
const selection = window.getSelection(); const selection = window.getSelection();
// 获取编辑器内的文本节点
const textNode = Array.from(editorRef.current.childNodes).find( const textNode = Array.from(editorRef.current.childNodes).find(
node => node.nodeType === Node.TEXT_NODE node => node.nodeType === Node.TEXT_NODE
) || editorRef.current.appendChild(document.createTextNode(script)); );
// 设置范围到文本末尾 if (!textNode) {
range.setStart(textNode, script.length); const newTextNode = document.createTextNode(script || '');
range.setEnd(textNode, script.length); 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?.removeAllRanges();
selection?.addRange(range); selection?.addRange(range);
} }
}; };
// 处理编辑器内容变化 const handleCompositionStart = () => {
const handleEditorChange = (e: React.FormEvent<HTMLDivElement>) => { setIsComposing(true);
const script = e.currentTarget.textContent || '';
setInputText(script);
}; };
// 引导步骤 const handleCompositionEnd = (e: React.CompositionEvent<HTMLDivElement>) => {
const steps: Step[] = [ setIsComposing(false);
{ handleEditorChange(e as any);
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 handleEditorChange = (e: React.FormEvent<HTMLDivElement>) => {
const handleJoyrideCallback = (data: any) => { // 如果正在输入中文,不要更新内容
const { status } = data; if (isComposing) return;
if (status === 'finished' || status === 'skipped') {
setRunTour(false); const newText = e.currentTarget.textContent || '';
// 可以在这里存储用户已完成引导的状态 setInputText(newText);
if (typeof window !== 'undefined') {
localStorage.setItem('hasCompletedTour', 'true'); 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,56 +332,175 @@ export function CreateToVideo2() {
setIsClient(true); setIsClient(true);
}, []); }, []);
// 渲染剧集卡片
const renderEpisodeCard = (episode: any) => {
return ( return (
<div <div
ref={containerRef} key={episode.project_id}
className="container mx-auto overflow-hidden custom-scrollbar h-[calc(100vh-10rem)]" 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}`)}
> >
{isClient && ( {/* 视频缩略图 */}
<JoyrideNoSSR <div className="relative h-[180px] bg-gradient-to-br from-purple-500/20 to-blue-500/20 overflow-hidden">
steps={steps} {episode.final_video_url ? (
run={runTour} <video
continuous src={episode.final_video_url}
showSkipButton className="w-full h-full object-cover"
showProgress muted
styles={{ loop
options: { onMouseEnter={(e) => (e.target as HTMLVideoElement).play()}
primaryColor: '#4F46E5', onMouseLeave={(e) => (e.target as HTMLVideoElement).pause()}
zIndex: 10000,
backgroundColor: '#1F2937',
overlayColor: 'rgba(0, 0, 0, 0.5)',
textColor: '#F3F4F6',
arrowColor: '#1F2937',
}
}}
disableOverlayClose
spotlightClicks
hideCloseButton
callback={handleJoyrideCallback}
locale={{
back: 'Back',
close: 'Close',
last: 'Finish',
next: 'Next',
skip: 'Skip'
}}
/> />
) : (
<div className="flex items-center justify-center h-full">
<Video className="w-12 h-12 text-white/30" />
</div>
)} )}
<div className='min-h-[100%] flex flex-col justify-center items-center'>
{/* 空状态 */} {/* 播放按钮覆盖 */}
<EmptyStateAnimation className='h-[calc(100vh - 20rem)]' /> <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">
<div className='video-tool-component relative w-[1080px]'> <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' ? '已完成' : '进行中'}
</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 || '未命名剧集'}
</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">...</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"></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)]'> <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 ? ( {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)}> <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' /> <ChevronUp className='w-4 h-4' />
<span className='text-sm'>Click to action</span> <span className='text-sm'>Click to action</span>
</div> </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)}> <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' /> <ChevronDown className='w-4 h-4' />
</div> </div>
)} )}
@ -443,11 +520,16 @@ export function CreateToVideo2() {
<div className='relative flex items-center gap-4 h-[94px]'> <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 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]'> <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' /> <Video className='w-4 h-4' />
)}
</div> </div>
<div className='w-full h-[22px] flex items-center justify-center rounded-[0 0 6px 6px] bg-white/[0.03]'> <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]'>Add Video</span> <span className='text-xs opacity-30 cursor-[inherit]'>
{isUploading ? '上传中...' : 'Add Video'}
</span>
</div> </div>
</div> </div>
{videoUrl && ( {videoUrl && (
@ -470,6 +552,8 @@ export function CreateToVideo2() {
onFocus={handleEditorFocus} onFocus={handleEditorFocus}
onBlur={() => setIsFocus(false)} onBlur={() => setIsFocus(false)}
onInput={handleEditorChange} onInput={handleEditorChange}
onCompositionStart={handleCompositionStart}
onCompositionEnd={handleCompositionEnd}
suppressContentEditableWarning suppressContentEditableWarning
> >
{script} {script}
@ -507,7 +591,7 @@ export function CreateToVideo2() {
<span className='text-nowrap opacity-70'> <span className='text-nowrap opacity-70'>
{selectedMode === ModeEnum.AUTOMATIC ? 'Auto' : 'Manual'} {selectedMode === ModeEnum.AUTOMATIC ? 'Auto' : 'Manual'}
</span> </span>
<Crown className='w-4 h-4 text-yellow-500' /> <Crown className={`w-4 h-4 text-yellow-500 ${selectedMode === ModeEnum.AUTOMATIC ? 'hidden' : ''}`} />
</div> </div>
</Dropdown> </Dropdown>
@ -528,7 +612,7 @@ export function CreateToVideo2() {
selectedResolution === ResolutionEnum.FULL_HD_1080P ? '1080P' : selectedResolution === ResolutionEnum.FULL_HD_1080P ? '1080P' :
selectedResolution === ResolutionEnum.UHD_2K ? '2K' : '4K'} selectedResolution === ResolutionEnum.UHD_2K ? '2K' : '4K'}
</span> </span>
<Crown className='w-4 h-4 text-yellow-500' /> <Crown className={`w-4 h-4 text-yellow-500 ${selectedResolution === ResolutionEnum.HD_720P ? 'hidden' : ''}`} />
</div> </div>
</Dropdown> </Dropdown>
</div> </div>
@ -554,7 +638,12 @@ export function CreateToVideo2() {
</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> </div>
</div> )}
</>
); );
} }

View File

@ -1,12 +1,13 @@
"use client"; "use client";
import { useState, useRef, useEffect } from "react"; 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 "./style/home-page2.css";
import { useRouter } from "next/navigation"; 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 { 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 { import {
createScriptProject, createScriptProject,
CreateScriptProjectRequest CreateScriptProjectRequest
@ -15,7 +16,7 @@ import {
ProjectTypeEnum, ProjectTypeEnum,
ModeEnum, ModeEnum,
ResolutionEnum ResolutionEnum
} from '@/api/enums'; } from '@/app/model/enums';
import { import {
getResourcesList, getResourcesList,
Resource Resource
@ -24,6 +25,7 @@ import {
export function HomePage2() { export function HomePage2() {
const router = useRouter(); const router = useRouter();
const [activeTool, setActiveTool] = useState("stretch"); const [activeTool, setActiveTool] = useState("stretch");
const [dropPosition, setDropPosition] = useState<"left" | "right">("left");
const [isCreating, setIsCreating] = useState(false); const [isCreating, setIsCreating] = useState(false);
const [createdProjectId, setCreatedProjectId] = useState<number | null>(null); const [createdProjectId, setCreatedProjectId] = useState<number | null>(null);
const [resources, setResources] = useState<Resource[]>([]); const [resources, setResources] = useState<Resource[]>([]);
@ -76,6 +78,8 @@ export function HomePage2() {
try { try {
setIsCreating(true); setIsCreating(true);
router.push(`/create`);
return;
// 使用默认值 // 使用默认值
const projectType = ProjectTypeEnum.SCRIPT_TO_VIDEO; 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 ( return (
<div className="min-h-screen bg-[var(--background)]" ref={containerRef}> <div className="min-h-screen bg-[var(--background)]" ref={containerRef}>
<div className="flex relative" style={{height: 'calc(100vh - 4rem)'}}> <div className="flex relative" style={{height: '100vh'}}>
{/* 工具栏-列表形式切换 */} {/* 工具栏-列表形式切换 */}
<div className="absolute top-[1rem] right-6 w-[8rem] flex justify-end"> <div className="absolute top-[8rem] z-[50] right-6 w-[8rem] flex justify-end">
<div role="group" className="flex p-1 bg-white/20 backdrop-blur-[15px] w-full rounded-[3rem]"> <LiquidButton className="w-[8rem] h-[3rem] text-sm"
<button onClick={(e) => {
className={`flex items-center justify-center h-10 transition-all duration-300 w-1/2 rounded-[3rem] e.stopPropagation();
${activeTool === "stretch" ? "bg-white/20 text-white" : "hover:bg-white/10 text-white/30"}`} handleToolChange(activeTool === "stretch" ? "right" : "left");
onClick={() => setActiveTool("stretch")} }}
> >
<AlignHorizontalSpaceAround className="w-5 h-5" /> <div className="relative flex items-center justify-around gap-4 w-[8rem] h-[3rem] p-2">
</button> <div
<button className={`cursor-pointer relative z-10 transition-opacity duration-300 ${activeTool === "stretch" ? "opacity-100" : "opacity-50"}`}>
className={`flex items-center justify-center h-10 transition-all duration-300 w-1/2 rounded-[3rem] <AlignHorizontalSpaceAround className="w-4 h-4 text-white" />
${activeTool === "table" ? "bg-white/20 text-white" : "hover:bg-white/10 text-white/30"}`}
onClick={() => setActiveTool("table")}
>
<Table className="w-5 h-5" />
</button>
</div> </div>
<div
className={`cursor-pointer relative z-10 transition-opacity duration-300 ${activeTool === "table" ? "opacity-100" : "opacity-50"}`}>
<Table className="w-4 h-4 text-white" />
</div>
{/* 水滴动画 */}
<AnimatePresence>
<motion.div
className="absolute w-10 h-8 bg-[rgba(255,255,255,0.35)] rounded-full backdrop-blur-[2px] z-[1]"
initial={{ x: dropPosition === "left" ? -32 : 32 }}
animate={{
x: dropPosition === "left" ? -32 : 32,
scale: [1, 1.2, 1],
}}
transition={{
type: "spring",
stiffness: 300,
damping: 30,
scale: {
duration: 0.2,
}
}}
/>
</AnimatePresence>
</div>
</LiquidButton>
</div> </div>
{/* 屏风式视频布局 */} {/* 屏风式视频布局 */}
<div <div
className={`absolute top-[4rem] w-full transition-all duration-500 className={`absolute w-full h-[calc(100vh - 4rem)] transition-all duration-500
${activeTool === "stretch" ? "opacity-100 translate-x-0" : "opacity-0 translate-x-[-100%] pointer-events-none"} ${activeTool === "stretch" ? "opacity-100 translate-x-0" : "opacity-0 translate-x-[-100%] pointer-events-none"}
`} `}
style={{ style={{
height: 'calc(100% - 8rem)', height: '100%',
display: 'flex', display: 'flex',
alignItems: 'center' alignItems: 'center',
marginTop: '4rem'
}} }}
> >
<VideoScreenLayout videos={videos} /> <VideoCarouselLayout videos={videos} />
</div> </div>
{/* 网格式视频布局 */} {/* 网格式视频布局 */}
<div <div
className={`absolute top-[4rem] w-full transition-all duration-500 max-h-[calc(100vh-8rem)] overflow-y-auto hide-scrollbar className={`absolute top-[8rem] w-full transition-all duration-500 max-h-[calc(100vh-8rem)] overflow-y-auto hide-scrollbar
${activeTool === "table" ? "opacity-100 translate-x-0" : "opacity-0 translate-x-[100%] pointer-events-none"} ${activeTool === "table" ? "opacity-100 translate-x-0" : "opacity-0 translate-x-[100%] pointer-events-none"}
`} `}
> >
@ -155,37 +186,21 @@ export function HomePage2() {
/> />
</div> </div>
{/* Create Project Button */} <div className="w-[8rem] h-[8rem] rounded-[50%] overflow-hidden fixed bottom-[3rem] left-[50%] -translate-x-1/2 z-50">
<div className="fixed bottom-[3rem] left-[50%] -translate-x-1/2 z-50"> <LiquidButton className="w-[8rem] h-[8rem] text-lg">
<motion.div <div className="flex items-center justify-center gap-2"
className="relative group" onClick={(e) => {
whileHover={!isCreating ? { scale: 1.05 } : {}} e.stopPropagation();
whileTap={!isCreating ? { scale: 0.95 } : {}} handleCreateProject();
onClick={handleCreateProject} }}>
>
{/* 玻璃按钮 */}
<motion.div
className={`add-project-btn relative flex items-center gap-3 px-6 py-4 rounded-2xl
bg-white/20 backdrop-blur-md cursor-pointer
shadow-[0_8px_32px_rgba(0,0,0,0.2)] group-hover:shadow-[0_8px_32px_rgba(0,0,0,0.4)]
transition-all duration-300 ${isCreating ? 'opacity-70 cursor-not-allowed' : ''}`}
initial={false}
whileHover={!isCreating ? {
backgroundColor: "rgba(255, 255, 255, 0.15)"
} : {}}
>
{isCreating ? ( {isCreating ? (
<Loader2 className="w-6 h-6 text-white animate-spin" /> <Loader2 className="w-6 h-6 text-white animate-spin" />
) : ( ) : (
<Plus className="w-6 h-6 text-white" /> <Clapperboard className="w-6 h-6 text-white" />
)} )}
<div className="btn-text text-lg font-medium bg-gradient-to-r text-white
bg-clip-text text-transparent">
{isCreating ? "Action..." : "Action"} {isCreating ? "Action..." : "Action"}
</div> </div>
</motion.div> </LiquidButton>
</motion.div>
</div> </div>
</div> </div>

View File

@ -73,7 +73,7 @@ export default function Login() {
<div className="main-container login-page relative"> <div className="main-container login-page relative">
{/* logo Movie Flow */} {/* logo Movie Flow */}
<div className='login-logo'> <div className='login-logo'>
<span className="logo-heart">Movie Flow</span> <span className="logo-heart">MovieFlow</span>
</div> </div>
<div className="left-panel"> <div className="left-panel">

View File

@ -7,7 +7,7 @@ import { Textarea } from '@/components/ui/textarea';
import { Select, SelectContent, SelectItem, SelectTrigger, SelectValue } from "@/components/ui/select"; import { Select, SelectContent, SelectItem, SelectTrigger, SelectValue } from "@/components/ui/select";
import { ArrowLeft, Loader2 } from 'lucide-react'; import { ArrowLeft, Loader2 } from 'lucide-react';
import { useRouter } from 'next/navigation'; import { useRouter } from 'next/navigation';
import { ModeEnum, ResolutionEnum } from "@/api/enums"; import { ModeEnum, ResolutionEnum } from "@/app/model/enums";
import { createScriptEpisode, CreateScriptEpisodeRequest, updateScriptEpisode, UpdateScriptEpisodeRequest } from "@/api/script_episode"; import { createScriptEpisode, CreateScriptEpisodeRequest, updateScriptEpisode, UpdateScriptEpisodeRequest } from "@/api/script_episode";
import { convertScriptToScene } from "@/api/video_flow"; import { convertScriptToScene } from "@/api/video_flow";

View File

@ -1,132 +0,0 @@
.video-tool-component {
position: fixed;
left: 50%;
bottom: 1rem;
--tw-translate-x: calc(-50% + 34.5px);
transform: translate(var(--tw-translate-x), var(--tw-translate-y)) rotate(var(--tw-rotate)) skew(var(--tw-skew-x)) skewY(var(--tw-skew-y)) scaleX(var(--tw-scale-x)) scaleY(var(--tw-scale-y));
}
.video-storyboard-tools {
border: 1px solid rgba(255, 255, 255, .2);
box-shadow: 0 4px 20px #0009;
}
.video-storyboard-tools .tool-submit-button {
display: flex;
height: 36px;
width: 120px;
cursor: pointer;
align-items: center;
justify-content: center;
gap: 2px;
border-radius: 10px;
font-size: .875rem;
line-height: 1.25rem;
font-weight: 600;
--tw-text-opacity: 1;
color: rgb(29 33 41 / var(--tw-text-opacity));
background-color: #fff;
}
.video-storyboard-tools .tool-submit-button.disabled {
background-color: #fff;
opacity: .3;
cursor: not-allowed;
}
.storyboard-tools-tab {
border-radius: 16px 16px 0 0;
background: #ffffff0d;
}
.storyboard-tools-tab .tab-item {
position: relative;
cursor: pointer;
--tw-text-opacity: 1;
color: rgb(255 255 255 / var(--tw-text-opacity));
line-height: 32px;
}
.storyboard-tools-tab .tab-item.active,
.storyboard-tools-tab .tab-item.active span {
opacity: 1;
}
.storyboard-tools-tab .tab-item.active:after {
content: "";
position: absolute;
bottom: -8px;
left: 50%;
width: 30px;
height: 2px;
border-radius: 2px;
background-color: #fff;
transform: translate(-50%);
}
.video-prompt-editor .editor-content {
line-height: 26px;
outline: none;
white-space: pre-wrap;
}
.video-prompt-editor .editor-content[contenteditable] {
display: inline-block;
width: 100%;
}
.video-prompt-editor.focus {
background-color: #ffffff0d;
}
.underline {
text-decoration-line: underline;
}
.tool-scroll-box-content {
scrollbar-width: none;
-ms-overflow-style: none;
}
.tool-operation-button {
display: flex;
height: 36px;
cursor: pointer;
align-items: center;
gap: 6px;
border-radius: 20px;
padding-left: 16px;
padding-right: 16px;
font-size: .875rem;
line-height: 1.25rem;
background-color: #ffffff0d;
}
.tool-operation-button:hover {
background-color: #ffffff1a;
}
/* 下拉菜单样式 */
.mode-dropdown.ant-dropdown .ant-dropdown-menu {
background: rgba(25, 27, 30, 0.95);
backdrop-filter: blur(15px);
border: 1px solid rgba(255, 255, 255, 0.1);
border-radius: 12px;
padding: 8px;
min-width: 280px;
}
.mode-dropdown.ant-dropdown .ant-dropdown-menu-item {
padding: 12px;
border-radius: 8px;
color: rgba(255, 255, 255, 0.9);
transition: all 0.3s ease;
}
.mode-dropdown.ant-dropdown .ant-dropdown-menu-item:hover {
background: rgba(255, 255, 255, 0.1);
}
.mode-dropdown.ant-dropdown .ant-dropdown-menu-item-selected {
background: rgba(255, 255, 255, 0.1);
}

View File

@ -1,9 +1,7 @@
.video-tool-component { .video-tool-component {
position: fixed; position: fixed;
left: 50%;
bottom: 1rem; bottom: 1rem;
--tw-translate-x: calc(-50% + 34.5px); z-index: 9;
transform: translate(var(--tw-translate-x), var(--tw-translate-y)) rotate(var(--tw-rotate)) skew(var(--tw-skew-x)) skewY(var(--tw-skew-y)) scaleX(var(--tw-scale-x)) scaleY(var(--tw-scale-y));
} }
.video-storyboard-tools { .video-storyboard-tools {

View File

@ -1,6 +1,7 @@
.add-project-btn .btn-text { .add-project-btn .btn-text {
font-weight: 600; font-weight: 600;
} }
.add-project-btn { .add-project-btn {
display: flex; display: flex;
align-items: center; align-items: center;

View File

@ -6,7 +6,7 @@ import { Card } from '@/components/ui/card';
import { Select, SelectContent, SelectItem, SelectTrigger, SelectValue } from "@/components/ui/select"; import { Select, SelectContent, SelectItem, SelectTrigger, SelectValue } from "@/components/ui/select";
import { ArrowLeft, Upload, Loader2 } from 'lucide-react'; import { ArrowLeft, Upload, Loader2 } from 'lucide-react';
import { useRouter } from 'next/navigation'; import { useRouter } from 'next/navigation';
import { ModeEnum, ResolutionEnum } from "@/api/enums"; import { ModeEnum, ResolutionEnum } from "@/app/model/enums";
import { createScriptEpisode, CreateScriptEpisodeRequest, updateScriptEpisode, UpdateScriptEpisodeRequest } from "@/api/script_episode"; import { createScriptEpisode, CreateScriptEpisodeRequest, updateScriptEpisode, UpdateScriptEpisodeRequest } from "@/api/script_episode";
import { convertVideoToScene } from "@/api/video_flow"; import { convertVideoToScene } from "@/api/video_flow";
import { getUploadToken, uploadToQiniu } from "@/api/common"; import { getUploadToken, uploadToQiniu } from "@/api/common";

View File

@ -8,7 +8,7 @@ import { ErrorBoundary } from "@/components/ui/error-boundary";
import { TaskInfo } from "./work-flow/task-info"; import { TaskInfo } from "./work-flow/task-info";
import { MediaViewer } from "./work-flow/media-viewer"; import { MediaViewer } from "./work-flow/media-viewer";
import { ThumbnailGrid } from "./work-flow/thumbnail-grid"; import { ThumbnailGrid } from "./work-flow/thumbnail-grid";
import { useWorkflowData } from "./work-flow/use-workflow-data"; import { useWorkflowData } from "./work-flow/use-workflow-data.ts";
import { usePlaybackControls } from "./work-flow/use-playback-controls"; import { usePlaybackControls } from "./work-flow/use-playback-controls";
import { AlertCircle, RefreshCw } from "lucide-react"; import { AlertCircle, RefreshCw } from "lucide-react";
import { motion } from "framer-motion"; import { motion } from "framer-motion";

View File

@ -572,6 +572,7 @@ export function MediaViewer({
</AnimatePresence> </AnimatePresence>
{/* 底部控制区域 */} {/* 底部控制区域 */}
{ taskVideos[currentSketchIndex] && (
<AnimatePresence> <AnimatePresence>
<motion.div <motion.div
className="absolute bottom-4 left-4 z-[11] flex items-center gap-3" className="absolute bottom-4 left-4 z-[11] flex items-center gap-3"
@ -613,6 +614,7 @@ export function MediaViewer({
{renderVolumeControls()} {renderVolumeControls()}
</motion.div> </motion.div>
</AnimatePresence> </AnimatePresence>
)}
</div> </div>
); );
}; };

View File

@ -56,10 +56,7 @@ export function TaskInfo({ isLoading, taskObject, currentLoadingText, dataLoadEr
animate={{ opacity: 1 }} animate={{ opacity: 1 }}
transition={{ duration: 0.5 }} transition={{ duration: 0.5 }}
> >
{taskObject?.projectName ? {taskObject?.title || '正在加载项目信息...'}
`${taskObject.projectName}${taskObject.taskName}` :
'正在加载项目信息...'
}
</motion.div> </motion.div>
{/* 加载状态显示 */} {/* 加载状态显示 */}
@ -157,7 +154,7 @@ export function TaskInfo({ isLoading, taskObject, currentLoadingText, dataLoadEr
return ( return (
<> <>
<div className="title-JtMejk"> <div className="title-JtMejk">
{taskObject?.projectName}{taskObject?.taskName} {taskObject?.title || '正在加载项目信息...'}
</div> </div>
{currentLoadingText === 'Task completed' ? ( {currentLoadingText === 'Task completed' ? (

View File

@ -0,0 +1,239 @@
import { useState, useEffect, useCallback } from 'react';
import { detailScriptEpisodeNew, getScriptTitle, getRunningStreamData, StreamData } from '@/api/video_flow';
import { useSearchParams } from 'next/navigation';
// 步骤映射
const STEP_MAP = {
'sketch': '1',
'character': '2',
'video': '3',
'music': '4',
'final_video': '6'
} as const;
type ApiStep = keyof typeof STEP_MAP;
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 const useApiData = () => {
const searchParams = useSearchParams();
const episodeId = searchParams.get('episodeId');
const [title, setTitle] = useState<string>('');
const [currentStep, setCurrentStep] = useState<string>('1');
const [currentLoadingText, setCurrentLoadingText] = useState<string>('');
const [needStreamData, setNeedStreamData] = useState<boolean>(false);
const [taskData, setTaskData] = useState<TaskData>({});
const [streamInterval, setStreamInterval] = useState<NodeJS.Timeout | null>(null);
// 处理流式数据
const handleStreamData = useCallback((streamData: StreamData) => {
const { category, message, data, status } = streamData;
// 更新加载文本
setCurrentLoadingText(message);
// 根据类别更新任务数据
setTaskData(prevData => {
const newData = { ...prevData };
switch (category) {
case 'sketch':
if (!newData.sketch) newData.sketch = [];
// 更新或追加分镜数据
const existingSketchIndex = newData.sketch.findIndex(s => s.url === data.url);
if (existingSketchIndex === -1) {
newData.sketch.push(data);
} else {
newData.sketch[existingSketchIndex] = data;
}
break;
case 'character':
if (!newData.roles) newData.roles = [];
// 更新或追加角色数据
const existingRoleIndex = newData.roles.findIndex(r => r.name === data.name);
if (existingRoleIndex === -1) {
newData.roles.push(data);
} else {
newData.roles[existingRoleIndex] = data;
}
break;
case 'video':
if (!newData.videos) newData.videos = [];
// 更新或追加视频数据
const existingVideoIndex = newData.videos.findIndex(v => v.url === data.url);
if (existingVideoIndex === -1) {
newData.videos.push(data);
} else {
newData.videos[existingVideoIndex] = data;
}
break;
case 'music':
if (!newData.music) newData.music = [];
newData.music = [data];
break;
case 'final_video':
newData.final = data;
break;
}
return newData;
});
// 如果状态为 completed停止获取流式数据
if (status === 'completed' || streamData.all_completed) {
setNeedStreamData(false);
}
}, []);
// 获取流式数据
const fetchStreamData = useCallback(async () => {
if (!episodeId || !needStreamData) return;
try {
const streamData = await getRunningStreamData({ episodeId });
handleStreamData(streamData);
} catch (error) {
console.error('获取流式数据失败:', error);
}
}, [episodeId, needStreamData, handleStreamData]);
// 启动流式数据轮询
useEffect(() => {
if (needStreamData && !streamInterval) {
const interval = setInterval(fetchStreamData, 10000); // 修改为10秒
setStreamInterval(interval);
} else if (!needStreamData && streamInterval) {
clearInterval(streamInterval);
setStreamInterval(null);
}
return () => {
if (streamInterval) {
clearInterval(streamInterval);
}
};
}, [needStreamData, fetchStreamData, streamInterval]);
// 获取标题的轮询函数
const pollTitle = useCallback(async () => {
if (!episodeId) return;
try {
const response = await getScriptTitle({ project_id: episodeId });
if (response.successful && response.data) {
setTitle(response.data.name);
return true;
}
} catch (error) {
console.error('获取标题失败:', error);
}
return false;
}, [episodeId]);
// 获取剧集详情
const fetchEpisodeDetail = useCallback(async () => {
if (!episodeId) return;
try {
const response = await detailScriptEpisodeNew({ project_id: episodeId });
if (response.successful) {
const { name, status, step, last_message, data } = response.data;
// 设置标题
if (name) {
setTitle(name);
}
// 设置步骤
if (step && STEP_MAP[step as ApiStep]) {
setCurrentStep(STEP_MAP[step as ApiStep]);
}
// 设置加载文本
setCurrentLoadingText(last_message || '');
// 设置是否需要流式数据
setNeedStreamData(status === 'running');
// 设置任务数据
if (data) {
setTaskData(data);
}
return true;
}
} catch (error) {
console.error('获取剧集详情失败:', error);
}
return false;
}, [episodeId]);
// 初始化数据
useEffect(() => {
let titleInterval: NodeJS.Timeout;
const initData = async () => {
const detailSuccess = await fetchEpisodeDetail();
// 如果详情接口没有返回标题,开始轮询标题
if (detailSuccess && !title) {
titleInterval = setInterval(async () => {
const success = await pollTitle();
if (success) {
clearInterval(titleInterval);
}
}, 3000);
}
};
initData();
return () => {
if (titleInterval) {
clearInterval(titleInterval);
}
};
}, [episodeId, fetchEpisodeDetail, pollTitle, title]);
return {
title,
currentStep,
currentLoadingText,
needStreamData,
taskData
};
};

View File

@ -0,0 +1,406 @@
'use client';
import { useState, useEffect } from 'react';
import { useSearchParams } from 'next/navigation';
import { detailScriptEpisodeNew, getScriptTitle, getRunningStreamData } from '@/api/video_flow';
// 步骤映射
const STEP_MAP = {
'initializing': '0',
'sketch': '1',
'character': '2',
'video': '3',
'music': '4',
'final_video': '6'
} as const;
// 执行loading文字映射
const LOADING_TEXT_MAP = {
initializing: 'initializing...',
sketch: (count: number, total: number) => `Generating sketch ${count + 1 > total ? total : count + 1}/${total}...`,
sketchComplete: 'Sketch generation complete',
character: 'Drawing characters...',
newCharacter: (count: number, total: number) => `Drawing character ${count + 1 > total ? total : count + 1}/${total}...`,
video: (count: number, total: number) => `Generating video ${count + 1 > total ? total : count + 1}/${total}...`,
videoComplete: 'Video generation complete',
audio: 'Generating background audio...',
postProduction: (step: string) => `Post-production: ${step}...`,
final: 'Generating final product...',
complete: 'Task completed'
} as const;
type ApiStep = keyof typeof STEP_MAP;
// 添加 TaskObject 接口
interface TaskObject {
taskStatus: string;
title: string;
currentLoadingText: string;
sketchCount?: number;
totalSketchCount?: number;
isGeneratingSketch?: boolean;
isGeneratingVideo?: boolean;
roles?: any[];
music?: any[];
final?: any;
}
export function useWorkflowData() {
const searchParams = useSearchParams();
const episodeId = searchParams.get('episodeId');
// 更新 taskObject 的类型
const [taskObject, setTaskObject] = useState<TaskObject | null>(null);
const [taskSketch, setTaskSketch] = useState<any[]>([]);
const [taskVideos, setTaskVideos] = useState<any[]>([]);
const [sketchCount, setSketchCount] = useState(0);
const [isLoading, setIsLoading] = useState(true);
const [currentStep, setCurrentStep] = useState('0');
const [currentSketchIndex, setCurrentSketchIndex] = useState(0);
const [isGeneratingSketch, setIsGeneratingSketch] = useState(false);
const [isGeneratingVideo, setIsGeneratingVideo] = useState(false);
const [currentLoadingText, setCurrentLoadingText] = useState('正在加载项目数据...');
const [totalSketchCount, setTotalSketchCount] = useState(0);
const [roles, setRoles] = useState<any[]>([]);
const [music, setMusic] = useState<any[]>([]);
const [final, setFinal] = useState<any>(null);
const [dataLoadError, setDataLoadError] = useState<string | null>(null);
const [needStreamData, setNeedStreamData] = useState(false);
// 获取流式数据
const fetchStreamData = async () => {
if (!episodeId || !needStreamData) return;
try {
const response = await getRunningStreamData({ project_id: episodeId });
if (!response.successful) {
throw new Error(response.message);
}
let loadingText: any = LOADING_TEXT_MAP.initializing;
let finalStep = '1', sketchCount = 0;
const all_task_data = response.data;
// all_task_data 下标0 和 下标1 换位置
const temp = all_task_data[0];
all_task_data[0] = all_task_data[1];
all_task_data[1] = temp;
console.log('all_task_data', all_task_data);
for (const task of all_task_data) {
// 如果有已完成的数据,同步到状态
if (task.task_name === 'generate_sketch' && task.task_result) {
if (task.task_result.data.length >= 0 && taskSketch.length !== task.task_result.data.length) {
// 正在生成草图中 替换 sketch 数据
const sketchList = [];
for (const sketch of task.task_result.data) {
sketchList.push({
url: sketch.image_path,
script: sketch.sketch_name
});
}
setTaskSketch(sketchList);
setSketchCount(sketchList.length);
setIsGeneratingSketch(true);
setCurrentSketchIndex(sketchList.length - 1);
loadingText = LOADING_TEXT_MAP.sketch(sketchList.length, task.task_result.total_count);
}
if (task.task_status === 'COMPLETED') {
// 草图生成完成
setIsGeneratingSketch(false);
sketchCount = task.task_result.total_count;
console.log('----------草图生成完成', sketchCount);
loadingText = LOADING_TEXT_MAP.sketchComplete;
finalStep = '2';
}
setTotalSketchCount(task.task_result.total_count);
}
if (task.task_name === 'generate_character' && task.task_result) {
if (task.task_result.data.length >= 0 && roles.length !== task.task_result.data.length) {
// 正在生成角色中 替换角色数据
const characterList = [];
for (const character of task.task_result.data) {
characterList.push({
name: character.character_name,
url: character.image_path,
sound: null,
soundDescription: '',
roleDescription: ''
});
}
setRoles(characterList);
loadingText = LOADING_TEXT_MAP.newCharacter(characterList.length, task.task_result.total_count);
}
if (task.task_status === 'COMPLETED') {
console.log('----------角色生成完成,有几个分镜', sketchCount);
// 角色生成完成
finalStep = '3';
loadingText = LOADING_TEXT_MAP.video(0, sketchCount);
}
}
if (task.task_name === 'generate_videos' && task.task_result) {
if (task.task_result.data.length >= 0 && taskVideos.length !== task.task_result.data.length) {
// 正在生成视频中 替换视频数据
const videoList = [];
for (const video of task.task_result.data) {
// 每一项 video 有多个视频 先默认取第一个
videoList.push({
url: video[0].qiniuVideoUrl,
script: video[0].operation.metadata.video.prompt,
audio: null,
});
}
setTaskVideos(videoList);
setIsGeneratingVideo(true);
setCurrentSketchIndex(videoList.length - 1);
loadingText = LOADING_TEXT_MAP.video(videoList.length, task.task_result.total_count);
}
if (task.task_status === 'COMPLETED') {
console.log('----------视频生成完成');
// 视频生成完成
setIsGeneratingVideo(false);
finalStep = '4';
// 暂时没有音频生成 直接跳过
finalStep = '5';
loadingText = LOADING_TEXT_MAP.postProduction('post-production...');
}
}
if (task.task_name === 'generate_final_video') {
if (task.task_result && task.task_result.video) {
setFinal({
url: task.task_result.video,
})
finalStep = '6';
loadingText = LOADING_TEXT_MAP.complete;
// 停止轮询
setNeedStreamData(false);
}
}
}
console.log('----------finalStep', finalStep);
// 设置步骤
setCurrentStep(finalStep);
setTaskObject(prev => {
if (!prev) return null;
return {
...prev,
taskStatus: finalStep
};
});
setCurrentLoadingText(loadingText);
} catch (error) {
console.error('获取数据失败:', error);
}
};
// 轮询获取流式数据
useEffect(() => {
let interval: NodeJS.Timeout;
if (needStreamData) {
interval = setInterval(fetchStreamData, 10000);
fetchStreamData(); // 立即执行一次
}
return () => {
if (interval) {
clearInterval(interval);
}
};
}, [needStreamData]);
// 初始化数据
const initializeWorkflow = async () => {
if (!episodeId) {
setDataLoadError('缺少必要的参数');
setIsLoading(false);
return;
}
try {
setIsLoading(true);
setCurrentLoadingText('正在加载项目数据...');
// 获取剧集详情
const response = await detailScriptEpisodeNew({ project_id: episodeId });
if (!response.successful) {
throw new Error(response.message);
}
const { name, status, data } = response.data;
setIsLoading(false);
// 设置初始数据
setTaskObject({
taskStatus: '0',
title: name || 'generating...',
currentLoadingText: status === 'COMPLETED' ? LOADING_TEXT_MAP.complete : LOADING_TEXT_MAP.initializing
});
// 设置标题
if (!name) {
// 如果没有标题,轮询获取
const titleResponse = await getScriptTitle({ project_id: episodeId });
console.log('titleResponse', titleResponse);
if (titleResponse.successful) {
setTaskObject((prev: TaskObject | null) => ({
...(prev || {}),
title: titleResponse.data.name
} as TaskObject));
}
}
let loadingText: any = LOADING_TEXT_MAP.initializing;
if (status === 'COMPLETED') {
loadingText = LOADING_TEXT_MAP.complete;
}
// 如果有已完成的数据,同步到状态
let finalStep = '1';
if (data) {
if (data.sketch && data.sketch.data && data.sketch.data.length > 0) {
const sketchList = [];
for (const sketch of data.sketch.data) {
sketchList.push({
url: sketch.image_path,
script: sketch.sketch_name,
});
}
setTaskSketch(sketchList);
setSketchCount(sketchList.length);
setTotalSketchCount(data.sketch.total_count);
// 设置为最后一个草图
if (data.sketch.total_count > data.sketch.data.length) {
setIsGeneratingSketch(true);
setCurrentSketchIndex(data.sketch.data.length - 1);
loadingText = LOADING_TEXT_MAP.sketch(data.sketch.data.length, data.sketch.total_count);
} else {
finalStep = '2';
if (!data.character || !data.character.data || !data.character.data.length) {
loadingText = LOADING_TEXT_MAP.newCharacter(0, data.character.total_count);
}
}
}
if (data.character && data.character.data && data.character.data.length > 0) {
const characterList = [];
for (const character of data.character.data) {
characterList.push({
name: character.character_name,
url: character.image_path,
sound: null,
soundDescription: '',
roleDescription: ''
});
}
setRoles(characterList);
if (data.character.total_count > data.character.data.length) {
loadingText = LOADING_TEXT_MAP.newCharacter(data.character.data.length, data.character.total_count);
} else {
finalStep = '3';
if (!data.video || !data.video.data || !data.video.data.length) {
loadingText = LOADING_TEXT_MAP.video(0, data.video.total_count || data.sketch.total_count);
}
}
}
if (data.video && data.video.data && data.video.data.length > 0) {
const videoList = [];
for (const video of data.video.data) {
// 每一项 video 有多个视频 先默认取第一个
videoList.push({
url: video[0].qiniuVideoUrl,
script: video[0].operation.metadata.video.prompt,
audio: null,
});
}
setTaskVideos(videoList);
// 如果在视频步骤,设置为最后一个视频
if (data.video.total_count > data.video.data.length) {
setIsGeneratingVideo(true);
setCurrentSketchIndex(data.video.data.length - 1);
loadingText = LOADING_TEXT_MAP.video(data.video.data.length, data.video.total_count);
} else {
finalStep = '4';
loadingText = LOADING_TEXT_MAP.audio;
// 暂时没有音频生成 直接跳过
finalStep = '5';
loadingText = LOADING_TEXT_MAP.postProduction('post-production...');
}
}
if (data.final_video && data.final_video.video) {
setFinal({
url: data.final_video.video
});
finalStep = '6';
loadingText = LOADING_TEXT_MAP.complete;
}
}
// 设置步骤
setCurrentStep(finalStep);
setTaskObject(prev => {
if (!prev) return null;
return {
...prev,
taskStatus: finalStep
};
});
console.log('---------loadingText', loadingText);
setCurrentLoadingText(loadingText);
// 设置是否需要获取流式数据
setNeedStreamData(status !== 'COMPLETED' && finalStep !== '6');
} catch (error) {
console.error('初始化失败:', error);
setDataLoadError('加载失败,请重试');
setIsLoading(false);
}
};
// 重试加载数据
const retryLoadData = () => {
setDataLoadError(null);
// 重置所有状态
setTaskSketch([]);
setTaskVideos([]);
setSketchCount(0);
setTotalSketchCount(0);
setRoles([]);
setMusic([]);
setFinal(null);
setCurrentSketchIndex(0);
setCurrentStep('0');
// 重新初始化
initializeWorkflow();
};
// 初始化
useEffect(() => {
initializeWorkflow();
}, [episodeId]);
return {
taskObject,
taskSketch,
taskVideos,
sketchCount,
isLoading,
currentStep,
currentSketchIndex,
isGeneratingSketch,
isGeneratingVideo,
currentLoadingText,
totalSketchCount,
roles,
music,
final,
dataLoadError,
setCurrentSketchIndex,
retryLoadData,
};
}

View File

@ -35,24 +35,7 @@ export function useWorkflowData() {
setCurrentLoadingText('项目数据加载完成'); setCurrentLoadingText('项目数据加载完成');
} catch (error) { } catch (error) {
console.warn('接口获取数据失败使用本地fallback数据:', error); // 报错
try {
// 使用本地fallback数据
setCurrentLoadingText('正在加载本地数据...');
const randomIndex = Math.floor(Math.random() * MOCK_DATA.length);
selectedMockData = MOCK_DATA[randomIndex];
console.log('使用本地fallback数据:', selectedMockData);
setCurrentLoadingText('本地数据加载完成');
} catch (fallbackError) {
console.error('本地数据加载也失败:', fallbackError);
// 最后的fallback - 直接使用第一组数据
selectedMockData = MOCK_DATA[0];
setDataLoadError('服务器连接失败,已切换到演示模式');
setCurrentLoadingText('演示数据加载完成');
}
} finally { } finally {
setIsLoadingData(false); setIsLoadingData(false);
} }

198
components/ui/ImageWave.tsx Normal file
View File

@ -0,0 +1,198 @@
import React, { useState, useEffect, useCallback, useRef } from 'react';
import styled from 'styled-components';
interface ImageWaveProps {
// 图片列表数据
images: string[];
// 容器宽度
containerWidth?: string;
// 容器高度
containerHeight?: string;
// 单个图片宽度
itemWidth?: string;
// 单个图片高度
itemHeight?: string;
// 图片间距
gap?: string;
// 是否开启自动动画
autoAnimate?: boolean;
// 自动动画间隔时间(ms)
autoAnimateInterval?: number;
}
const Wrapper = styled.div<{ width?: string; height?: string }>`
display: flex;
align-items: center;
justify-content: center;
width: ${props => props.width || '100vw'};
height: ${props => props.height || '100vh'};
background-color: transparent;
`;
const Items = styled.div<{ gap?: string }>`
display: flex;
gap: ${props => props.gap || '0.4rem'};
perspective: calc(var(--index) * 35);
&.has-expanded .item:not(.expanded) {
filter: grayscale(1) brightness(0.3);
}
&.has-expanded .item:hover {
filter: grayscale(1) brightness(0.3);
}
&.has-expanded .item.expanded:hover {
filter: inherit;
transform: translateZ(calc(var(--index) * 10));
}
`;
const Item = styled.div<{ width?: string; height?: string }>`
width: ${props => props.width || 'calc(var(--index) * 3)'};
height: ${props => props.height || 'calc(var(--index) * 12)'};
background-color: #222;
background-size: cover;
background-position: center;
cursor: pointer;
filter: grayscale(1) brightness(0.5);
transition: transform 1.25s var(--transition),
filter 3s var(--transition),
width 1.25s var(--transition),
margin 1.25s var(--transition);
will-change: transform, filter, rotateY, width;
&::before {
content: '';
position: absolute;
width: 20px;
height: 100%;
right: calc(var(--index) * -1);
}
&::after {
left: calc(var(--index) * -1);
}
&:hover {
filter: inherit;
transform: translateZ(calc(var(--index) * 7.8));
}
&:hover + * {
filter: inherit;
transform: translateZ(calc(var(--index) * 6.8)) rotateY(35deg);
z-index: -1;
}
&:hover + * + * {
filter: inherit;
transform: translateZ(calc(var(--index) * 5.6)) rotateY(40deg);
z-index: -2;
}
&:hover + * + * + * {
filter: inherit;
transform: translateZ(calc(var(--index) * 3.6)) rotateY(30deg);
z-index: -3;
}
&:hover + * + * + * + * {
filter: inherit;
transform: translateZ(calc(var(--index) * .6)) rotateY(15deg);
z-index: -4;
}
&:has(+ :hover) {
filter: inherit;
transform: translateZ(calc(var(--index) * 6.8)) rotateY(-35deg);
}
&:has(+ * + :hover) {
filter: inherit;
transform: translateZ(calc(var(--index) * 5.6)) rotateY(-40deg);
}
&:has(+ * + * + :hover) {
filter: inherit;
transform: translateZ(calc(var(--index) * 3.6)) rotateY(-30deg);
}
&:has(+ * + * + * + :hover) {
filter: inherit;
transform: translateZ(calc(var(--index) * .6)) rotateY(-15deg);
}
&.expanded {
width: 28vw;
filter: inherit;
z-index: 100;
transform: translateZ(calc(var(--index) * 7.8));
margin: 0.45vw;
}
`;
export const ImageWave: React.FC<ImageWaveProps> = ({
images,
containerWidth,
containerHeight,
itemWidth,
itemHeight,
gap,
autoAnimate = false,
autoAnimateInterval = 2000,
}) => {
const [currentExpandedItem, setCurrentExpandedItem] = useState<number | null>(null);
const itemsRef = useRef<HTMLDivElement>(null);
const autoAnimateRef = useRef<number | null>(null);
const currentIndexRef = useRef<number>(0);
const handleItemClick = (index: number) => {
if (currentExpandedItem === index) {
setCurrentExpandedItem(null);
} else {
setCurrentExpandedItem(index);
}
};
const handleOutsideClick = useCallback((e: MouseEvent) => {
if (itemsRef.current && !itemsRef.current.contains(e.target as Node)) {
setCurrentExpandedItem(null);
}
}, []);
const handleKeyDown = useCallback((e: KeyboardEvent) => {
if (e.key === 'Escape') {
setCurrentExpandedItem(null);
}
}, []);
useEffect(() => {
document.addEventListener('click', handleOutsideClick);
document.addEventListener('keydown', handleKeyDown);
return () => {
document.removeEventListener('click', handleOutsideClick);
document.removeEventListener('keydown', handleKeyDown);
};
}, [handleOutsideClick, handleKeyDown]);
useEffect(() => {
return () => {
if (autoAnimateRef.current) {
clearTimeout(autoAnimateRef.current);
}
};
}, [autoAnimate]);
return (
<Wrapper width={containerWidth} height={containerHeight}>
<Items ref={itemsRef} gap={gap} className={currentExpandedItem !== null ? 'has-expanded' : ''}>
{images.map((image, index) => (
<Item
key={index}
width={itemWidth}
height={itemHeight}
className={`item ${currentExpandedItem === index ? 'expanded' : ''}`}
style={{ backgroundImage: `url(${image})` }}
onClick={() => handleItemClick(index)}
tabIndex={0}
/>
))}
</Items>
</Wrapper>
);
};

View File

@ -0,0 +1,37 @@
import { cn } from "@/public/lib/utils";
interface GradientTextProps {
text: string;
startColor?: string;
endColor?: string;
startPercentage?: number;
endPercentage?: number;
className?: string;
glowIntensity?: {
primary?: number;
secondary?: number;
};
}
export function GradientText({
text,
startColor = '#6AF4F9',
endColor = '#C73BFF',
startPercentage = 0,
endPercentage = 100,
className
}: GradientTextProps) {
return (
<span
className={cn(
"bg-gradient-to-r bg-clip-text text-transparent",
className
)}
style={{
backgroundImage: `linear-gradient(to right, ${startColor} ${startPercentage}%, ${endColor} ${endPercentage}%)`
}}
>
{text}
</span>
);
}

View File

@ -0,0 +1,384 @@
"use client"
import * as React from "react"
import { Slot } from "@radix-ui/react-slot"
import { cva, type VariantProps } from "class-variance-authority"
import { cn } from "@/lib/utils"
const buttonVariants = cva(
"inline-flex items-center cursor-pointer justify-center gap-2 whitespace-nowrap rounded-md text-sm font-medium transition-colors focus-visible:outline-none focus-visible:ring-1 focus-visible:ring-ring disabled:pointer-events-none disabled:opacity-50 [&_svg]:pointer-events-none [&_svg]:size-4 [&_svg]:shrink-0",
{
variants: {
variant: {
default: "bg-primary text-primary-foreground hover:bg-primary/90",
destructive:
"bg-destructive text-primary-foreground hover:bg-destructive/90",
cool: "dark:inset-shadow-2xs dark:inset-shadow-white/10 bg-linear-to-t border border-b-2 border-zinc-950/40 from-primary to-primary/85 shadow-md shadow-primary/20 ring-1 ring-inset ring-white/25 transition-[filter] duration-200 hover:brightness-110 active:brightness-90 dark:border-x-0 text-primary-foreground dark:text-primary-foreground dark:border-t-0 dark:border-primary/50 dark:ring-white/5",
outline:
"border border-input bg-background hover:bg-accent hover:text-accent-foreground",
secondary:
"bg-secondary text-secondary-foreground hover:bg-secondary/80",
ghost: "hover:bg-accent hover:text-accent-foreground",
link: "text-primary underline-offset-4 hover:underline",
},
size: {
default: "h-9 px-4 py-2",
sm: "h-8 rounded-md px-3 text-xs",
lg: "h-10 rounded-md px-8",
icon: "h-9 w-9",
},
},
defaultVariants: {
variant: "default",
size: "default",
},
}
)
export interface ButtonProps
extends React.ButtonHTMLAttributes<HTMLButtonElement>,
VariantProps<typeof buttonVariants> {
asChild?: boolean
}
const Button = React.forwardRef<HTMLButtonElement, ButtonProps>(
({ className, variant, size, asChild = false, ...props }, ref) => {
const Comp = asChild ? Slot : "button"
return (
<Comp
className={cn(buttonVariants({ variant, size, className }))}
ref={ref}
{...props}
/>
)
}
)
Button.displayName = "Button"
export { Button, buttonVariants, liquidbuttonVariants, LiquidButton }
const liquidbuttonVariants = cva(
"inline-flex items-center transition-colors justify-center cursor-pointer gap-2 whitespace-nowrap rounded-md text-sm font-medium transition-[color,box-shadow] disabled:pointer-events-none disabled:opacity-50 [&_svg]:pointer-events-none [&_svg:not([class*='size-'])]:size-4 shrink-0 [&_svg]:shrink-0 outline-none focus-visible:border-ring focus-visible:ring-ring/50 focus-visible:ring-[3px] aria-invalid:ring-destructive/20 dark:aria-invalid:ring-destructive/40 aria-invalid:border-destructive",
{
variants: {
variant: {
default: "bg-transparent hover:scale-105 duration-300 transition text-primary",
destructive:
"bg-destructive text-white hover:bg-destructive/90 focus-visible:ring-destructive/20 dark:focus-visible:ring-destructive/40",
outline:
"border border-input bg-background hover:bg-accent hover:text-accent-foreground",
secondary:
"bg-secondary text-secondary-foreground hover:bg-secondary/80",
ghost: "hover:bg-accent hover:text-accent-foreground",
link: "text-primary underline-offset-4 hover:underline",
},
size: {
default: "h-9 px-4 py-2 has-[>svg]:px-3",
sm: "h-8 text-xs gap-1.5 px-4 has-[>svg]:px-4",
lg: "h-10 rounded-md px-6 has-[>svg]:px-4",
xl: "h-12 rounded-md px-8 has-[>svg]:px-6",
xxl: "h-14 rounded-md px-10 has-[>svg]:px-8",
icon: "size-9",
custom: "h-[60px] w-[200px] text-base",
square: "w-[120px] h-[120px] text-lg", // 添加正方形尺寸
},
},
defaultVariants: {
variant: "default",
size: "xxl",
},
}
)
function LiquidButton({
className,
variant,
size,
asChild = false,
children,
...props
}: React.ComponentProps<"button"> &
VariantProps<typeof liquidbuttonVariants> & {
asChild?: boolean
}) {
const Comp = asChild ? Slot : "button"
return (
<>
<Comp
data-slot="button"
className={cn(
"relative",
liquidbuttonVariants({ variant, size, className })
)}
{...props}
>
<div className="absolute top-0 left-0 z-0 h-full w-full rounded-full
shadow-[0_0_6px_rgba(0,0,0,0.03),0_2px_6px_rgba(0,0,0,0.08),inset_3px_3px_0.5px_-3px_rgba(0,0,0,0.9),inset_-3px_-3px_0.5px_-3px_rgba(0,0,0,0.85),inset_1px_1px_1px_-0.5px_rgba(0,0,0,0.6),inset_-1px_-1px_1px_-0.5px_rgba(0,0,0,0.6),inset_0_0_6px_6px_rgba(0,0,0,0.12),inset_0_0_2px_2px_rgba(0,0,0,0.06),0_0_12px_rgba(255,255,255,0.15)]
transition-all
dark:shadow-[0_0_8px_rgba(0,0,0,0.03),0_2px_6px_rgba(0,0,0,0.08),inset_3px_3px_0.5px_-3.5px_rgba(255,255,255,0.09),inset_-3px_-3px_0.5px_-3.5px_rgba(255,255,255,0.85),inset_1px_1px_1px_-0.5px_rgba(255,255,255,0.6),inset_-1px_-1px_1px_-0.5px_rgba(255,255,255,0.6),inset_0_0_6px_6px_rgba(255,255,255,0.12),inset_0_0_2px_2px_rgba(255,255,255,0.06),0_0_12px_rgba(0,0,0,0.15)]" />
<div
className="absolute top-0 left-0 isolate -z-10 h-full w-full overflow-hidden rounded-md"
style={{ backdropFilter: 'url("#container-glass")' }}
/>
<div className="z-10 pointer-events-auto">
{children}
</div>
<GlassFilter />
</Comp>
</>
)
}
function GlassFilter() {
return (
<svg className="hidden">
<defs>
<filter
id="container-glass"
x="0%"
y="0%"
width="100%"
height="100%"
colorInterpolationFilters="sRGB"
>
{/* Generate turbulent noise for distortion */}
<feTurbulence
type="fractalNoise"
baseFrequency="0.05 0.05"
numOctaves="1"
seed="1"
result="turbulence"
/>
{/* Blur the turbulence pattern slightly */}
<feGaussianBlur in="turbulence" stdDeviation="2" result="blurredNoise" />
{/* Displace the source graphic with the noise */}
<feDisplacementMap
in="SourceGraphic"
in2="blurredNoise"
scale="70"
xChannelSelector="R"
yChannelSelector="B"
result="displaced"
/>
{/* Apply overall blur on the final result */}
<feGaussianBlur in="displaced" stdDeviation="4" result="finalBlur" />
{/* Output the result */}
<feComposite in="finalBlur" in2="finalBlur" operator="over" />
</filter>
</defs>
</svg>
);
}
type ColorVariant =
| "default"
| "primary"
| "success"
| "error"
| "gold"
| "bronze";
interface MetalButtonProps
extends React.ButtonHTMLAttributes<HTMLButtonElement> {
variant?: ColorVariant;
}
const colorVariants: Record<
ColorVariant,
{
outer: string;
inner: string;
button: string;
textColor: string;
textShadow: string;
}
> = {
default: {
outer: "bg-gradient-to-b from-[#000] to-[#A0A0A0]",
inner: "bg-gradient-to-b from-[#FAFAFA] via-[#3E3E3E] to-[#E5E5E5]",
button: "bg-gradient-to-b from-[#B9B9B9] to-[#969696]",
textColor: "text-white",
textShadow: "[text-shadow:_0_-1px_0_rgb(80_80_80_/_100%)]",
},
primary: {
outer: "bg-gradient-to-b from-[#000] to-[#A0A0A0]",
inner: "bg-gradient-to-b from-primary via-secondary to-muted",
button: "bg-gradient-to-b from-primary to-primary/40",
textColor: "text-white",
textShadow: "[text-shadow:_0_-1px_0_rgb(30_58_138_/_100%)]",
},
success: {
outer: "bg-gradient-to-b from-[#005A43] to-[#7CCB9B]",
inner: "bg-gradient-to-b from-[#E5F8F0] via-[#00352F] to-[#D1F0E6]",
button: "bg-gradient-to-b from-[#9ADBC8] to-[#3E8F7C]",
textColor: "text-[#FFF7F0]",
textShadow: "[text-shadow:_0_-1px_0_rgb(6_78_59_/_100%)]",
},
error: {
outer: "bg-gradient-to-b from-[#5A0000] to-[#FFAEB0]",
inner: "bg-gradient-to-b from-[#FFDEDE] via-[#680002] to-[#FFE9E9]",
button: "bg-gradient-to-b from-[#F08D8F] to-[#A45253]",
textColor: "text-[#FFF7F0]",
textShadow: "[text-shadow:_0_-1px_0_rgb(146_64_14_/_100%)]",
},
gold: {
outer: "bg-gradient-to-b from-[#917100] to-[#EAD98F]",
inner: "bg-gradient-to-b from-[#FFFDDD] via-[#856807] to-[#FFF1B3]",
button: "bg-gradient-to-b from-[#FFEBA1] to-[#9B873F]",
textColor: "text-[#FFFDE5]",
textShadow: "[text-shadow:_0_-1px_0_rgb(178_140_2_/_100%)]",
},
bronze: {
outer: "bg-gradient-to-b from-[#864813] to-[#E9B486]",
inner: "bg-gradient-to-b from-[#EDC5A1] via-[#5F2D01] to-[#FFDEC1]",
button: "bg-gradient-to-b from-[#FFE3C9] to-[#A36F3D]",
textColor: "text-[#FFF7F0]",
textShadow: "[text-shadow:_0_-1px_0_rgb(124_45_18_/_100%)]",
},
};
const metalButtonVariants = (
variant: ColorVariant = "default",
isPressed: boolean,
isHovered: boolean,
isTouchDevice: boolean,
) => {
const colors = colorVariants[variant];
const transitionStyle = "all 250ms cubic-bezier(0.1, 0.4, 0.2, 1)";
return {
wrapper: cn(
"relative inline-flex transform-gpu rounded-md p-[1.25px] will-change-transform",
colors.outer,
),
wrapperStyle: {
transform: isPressed
? "translateY(2.5px) scale(0.99)"
: "translateY(0) scale(1)",
boxShadow: isPressed
? "0 1px 2px rgba(0, 0, 0, 0.15)"
: isHovered && !isTouchDevice
? "0 4px 12px rgba(0, 0, 0, 0.12)"
: "0 3px 8px rgba(0, 0, 0, 0.08)",
transition: transitionStyle,
transformOrigin: "center center",
},
inner: cn(
"absolute inset-[1px] transform-gpu rounded-lg will-change-transform",
colors.inner,
),
innerStyle: {
transition: transitionStyle,
transformOrigin: "center center",
filter:
isHovered && !isPressed && !isTouchDevice ? "brightness(1.05)" : "none",
},
button: cn(
"relative z-10 m-[1px] rounded-md inline-flex h-11 transform-gpu cursor-pointer items-center justify-center overflow-hidden rounded-md px-6 py-2 text-sm leading-none font-semibold will-change-transform outline-none",
colors.button,
colors.textColor,
colors.textShadow,
),
buttonStyle: {
transform: isPressed ? "scale(0.97)" : "scale(1)",
transition: transitionStyle,
transformOrigin: "center center",
filter:
isHovered && !isPressed && !isTouchDevice ? "brightness(1.02)" : "none",
},
};
};
const ShineEffect = ({ isPressed }: { isPressed: boolean }) => {
return (
<div
className={cn(
"pointer-events-none absolute inset-0 z-20 overflow-hidden transition-opacity duration-300",
isPressed ? "opacity-20" : "opacity-0",
)}
>
<div className="absolute inset-0 rounded-md bg-gradient-to-r from-transparent via-neutral-100 to-transparent" />
</div>
);
};
export const MetalButton = React.forwardRef<
HTMLButtonElement,
MetalButtonProps
>(({ children, className, variant = "default", ...props }, ref) => {
const [isPressed, setIsPressed] = React.useState(false);
const [isHovered, setIsHovered] = React.useState(false);
const [isTouchDevice, setIsTouchDevice] = React.useState(false);
React.useEffect(() => {
setIsTouchDevice("ontouchstart" in window || navigator.maxTouchPoints > 0);
}, []);
const buttonText = children || "Button";
const variants = metalButtonVariants(
variant,
isPressed,
isHovered,
isTouchDevice,
);
const handleInternalMouseDown = () => {
setIsPressed(true);
};
const handleInternalMouseUp = () => {
setIsPressed(false);
};
const handleInternalMouseLeave = () => {
setIsPressed(false);
setIsHovered(false);
};
const handleInternalMouseEnter = () => {
if (!isTouchDevice) {
setIsHovered(true);
}
};
const handleInternalTouchStart = () => {
setIsPressed(true);
};
const handleInternalTouchEnd = () => {
setIsPressed(false);
};
const handleInternalTouchCancel = () => {
setIsPressed(false);
};
return (
<div className={variants.wrapper} style={variants.wrapperStyle}>
<div className={variants.inner} style={variants.innerStyle}></div>
<button
ref={ref}
className={cn(variants.button, className)}
style={variants.buttonStyle}
{...props}
onMouseDown={handleInternalMouseDown}
onMouseUp={handleInternalMouseUp}
onMouseLeave={handleInternalMouseLeave}
onMouseEnter={handleInternalMouseEnter}
onTouchStart={handleInternalTouchStart}
onTouchEnd={handleInternalTouchEnd}
onTouchCancel={handleInternalTouchCancel}
>
<ShineEffect isPressed={isPressed} />
{buttonText}
{isHovered && !isPressed && !isTouchDevice && (
<div className="pointer-events-none absolute inset-0 bg-gradient-to-t rounded-lg from-transparent to-white/5" />
)}
</button>
</div>
);
});
MetalButton.displayName = "MetalButton";

View File

@ -107,7 +107,7 @@ export function ScriptTabContent({
const isActive = currentSketchIndex === index; const isActive = currentSketchIndex === index;
return ( return (
<motion.div <motion.div
key={script.id} key={index}
className={cn( className={cn(
'flex-shrink-0 cursor-pointer transition-all duration-300', 'flex-shrink-0 cursor-pointer transition-all duration-300',
isActive ? 'text-white' : 'text-white/50 hover:text-white/80' isActive ? 'text-white' : 'text-white/50 hover:text-white/80'

View File

@ -0,0 +1,443 @@
import React, { useRef, useEffect, useState } from 'react';
import styled, { keyframes } from 'styled-components';
interface Video {
id: string;
url: string;
title: string;
}
interface VideoCarouselProps {
videos: Video[];
width?: string;
height?: string;
}
// 添加动画关键帧
const coverFlow = keyframes`
0% {
z-index: 1;
transform: translate3d(-80%, 0, 0) rotateY(-33deg);
}
20% {
scale: 1;
transform: translate3d(0%, 0, 0) rotateY(-33deg);
}
40% {
transform: translate3d(15%, 0, 0) rotateY(-20deg);
}
/* Half way */
50% {
scale: 1.2;
transform: translate3d(0, 0, 0) rotateY(0deg);
}
60% {
transform: translate3d(-15%, 0, 0) rotateY(20deg);
}
80% {
scale: 1;
transform: translate3d(0%, 0, 0) rotateY(33deg);
}
100% {
z-index: 1;
transform: translate3d(80%, 0, 0) rotateY(33deg);
}
`;
const shrink = keyframes`
0% {
scale: 0.75;
opacity: 0;
transform-origin: -150% 50%;
}
5% {
opacity: 1;
}
10% {
opacity: 1;
scale: 1;
transform-origin: 50% 50%;
}
40% {
scale: 1;
opacity: 1;
transform: translate3d(0, 0, 0vmin);
}
50% {
scale: 1.2;
opacity: 1;
transform: translate3d(0, 0, 15vmin);
}
60% {
scale: 1;
opacity: 1;
transform: translate3d(0, 0, 0vmin);
}
90% {
opacity: 1;
transform-origin: 50% 50%;
scale: 1;
}
95% {
opacity: 1;
}
100% {
scale: 0.75;
opacity: 0;
transform-origin: 250% 50%;
}
`;
const z = keyframes`
0%, 100% {
z-index: 1;
}
50% {
z-index: 100;
}
`;
const Container = styled.div<{ $width?: string; $height?: string }>`
display: grid;
place-items: center;
align-items: end;
width: ${props => props.$width || '100vw'};
height: ${props => props.$height || '100vh'};
overflow: hidden;
background: black;
position: relative;
`;
const VideoList = styled.ul`
width: 100%;
height: 100%;
display: flex;
list-style-type: none;
margin: 0;
overflow-x: auto;
overflow-y: hidden;
scroll-snap-type: x mandatory;
contain: inline-size;
align-items: center;
`;
const VideoItem = styled.li`
width: 30%;
aspect-ratio: 16/9;
display: grid;
place-items: center;
flex: 1 0 30cqw;
position: relative;
height: calc(30cqw * 9/16);
width: 30cqw;
view-timeline: --cover;
view-timeline-axis: inline;
transform-style: preserve-3d;
perspective: 100vmin;
background: transparent;
animation: ${z} both linear;
animation-timeline: --cover;
animation-range: cover;
&[aria-hidden=false] {
scroll-snap-align: center;
}
&.center {
z-index: 100;
}
`;
const VideoWrapper = styled.div`
height: 100%;
width: 100%;
aspect-ratio: 16/9;
transform-origin: 100% 50%;
transform-style: preserve-3d;
perspective: 100vmin;
position: absolute;
top: 50%;
left: 50%;
translate: -50% -50%;
will-change: transform;
animation: ${shrink} both linear;
animation-timeline: --cover;
animation-range: cover;
${VideoItem}.center & {
max-height: calc(150% + 2vmin);
}
`;
const Video = styled.video`
width: 100%;
height: 100%;
object-fit: cover;
-webkit-box-reflect: below 2vmin linear-gradient(transparent 0 50%, hsl(0 0% 100% / 0.75) 100%);
transform: translateZ(0);
will-change: transform;
animation: ${coverFlow} both linear;
animation-timeline: --cover;
animation-range: cover;
border-radius: 1vmin;
${VideoItem}.center & {
-webkit-box-reflect: unset;
}
`;
const VideoReflection = styled(Video)`
width: 100%;
height: 100%;
opacity: 0;
transition: opacity 0.3s ease;
mask-image: linear-gradient(to top, rgba(0, 0, 0, 1) 0%, rgba(0, 0, 0, 0) 100%);
scale: 1 !important;
${VideoItem}.center & {
opacity: 0.75;
scale: 1 !important;
object-position: center bottom;
}
`;
const ReflectionWrapper = styled.div`
position: absolute;
left: 0;
margin-top: calc(6vmin * 1.2);
width: 100%;
height: calc((100vh - 4rem) / 8);
pointer-events: none;
overflow-y: hidden;
${VideoItem}.center & {
scale: 1.19998;
transform: scale(1, -1) translateZ(0) !important;
}
`;
const ControlsWrapper = styled.div`
position: absolute;
bottom: -1rem;
left: -1rem;
display: flex;
gap: 0.5rem;
z-index: 1000;
opacity: 0;
transition: opacity 0.3s ease;
${VideoItem}.center:hover & {
opacity: 1;
}
`;
const ControlButton = styled.button`
background: rgba(0, 0, 0, 0.6);
border: none;
border-radius: 50%;
width: 2rem;
height: 2rem;
display: flex;
align-items: center;
justify-content: center;
cursor: pointer;
color: white;
transition: background-color 0.3s ease;
&:hover {
background: rgba(0, 0, 0, 0.8);
}
svg {
width: 0.8rem;
height: 0.8rem;
}
`;
const VideoCarousel: React.FC<VideoCarouselProps> = ({ videos, width, height }) => {
const containerRef = useRef<HTMLUListElement>(null);
const [currentPlayingVideo, setCurrentPlayingVideo] = useState<HTMLVideoElement | null>(null);
const [isPlaying, setIsPlaying] = useState(true);
const [isMuted, setIsMuted] = useState(true);
const PADDING = 4;
const togglePlay = (video: HTMLVideoElement, reflectionVideo: HTMLVideoElement) => {
if (video.paused) {
video.play();
reflectionVideo.play();
setIsPlaying(true);
} else {
video.pause();
reflectionVideo.pause();
setIsPlaying(false);
}
};
const toggleMute = (video: HTMLVideoElement) => {
video.muted = !video.muted;
setIsMuted(video.muted);
};
const handleVideoPlayback = () => {
if (!containerRef.current) return;
requestAnimationFrame(() => {
const containerWidth = containerRef.current!.offsetWidth;
const containerCenter = containerWidth / 2;
const items = containerRef.current!.querySelectorAll('li');
items.forEach(item => {
const video = item.querySelector('video:not([data-reflection])') as HTMLVideoElement;
const reflectionVideo = item.querySelector('video[data-reflection]') as HTMLVideoElement;
if (!video || !reflectionVideo) return;
const rect = item.getBoundingClientRect();
const itemCenter = rect.left + rect.width / 2;
const isCenter = Math.abs(itemCenter - containerCenter) < rect.width / 3;
item.classList.toggle('center', isCenter);
if (isCenter) {
if (currentPlayingVideo !== video) {
if (currentPlayingVideo) {
currentPlayingVideo.pause();
}
video.play().catch(() => {});
reflectionVideo.play().catch(() => {});
setCurrentPlayingVideo(video);
}
} else {
if (video !== currentPlayingVideo) {
video.pause();
reflectionVideo.pause();
video.currentTime = 0;
reflectionVideo.currentTime = 0;
}
}
});
});
};
useEffect(() => {
const container = containerRef.current;
if (!container) return;
let scrollBounds = {
min: 0,
max: 0
};
const setScrollBounds = () => {
const items = container.children;
if (items.length === 0) return;
items[items.length - 1].scrollIntoView();
scrollBounds.max = container.scrollLeft + (items[0] as HTMLElement).offsetWidth;
items[0].scrollIntoView();
scrollBounds.min = container.scrollLeft - (items[0] as HTMLElement).offsetWidth;
};
const handleScroll = () => {
if (container.scrollLeft < scrollBounds.min) {
container.scrollLeft = scrollBounds.max;
} else if (container.scrollLeft > scrollBounds.max) {
container.scrollLeft = scrollBounds.min;
}
handleVideoPlayback();
};
setScrollBounds();
handleVideoPlayback();
container.addEventListener('scroll', handleScroll);
window.addEventListener('resize', setScrollBounds);
return () => {
container.removeEventListener('scroll', handleScroll);
window.removeEventListener('resize', setScrollBounds);
};
}, []);
const paddedVideos = [
...videos.slice(-PADDING),
...videos,
...videos.slice(0, PADDING)
];
return (
<Container $width={width} $height={height}>
<VideoList ref={containerRef}>
{paddedVideos.map((video, index) => (
<VideoItem
key={`${video.id}-${index}`}
aria-hidden={index < PADDING || index >= videos.length + PADDING}
>
<VideoWrapper>
<Video
muted={isMuted}
loop
playsInline
preload="none"
poster={`${video.url}?vframe/jpg/offset/1`}
src={video.url}
title={video.title}
/>
<ControlsWrapper>
<ControlButton
onClick={(e) => {
e.stopPropagation();
const videoEl = e.currentTarget.closest(VideoItem.toString())?.querySelector('video:not([data-reflection])') as HTMLVideoElement;
const reflectionEl = e.currentTarget.closest(VideoItem.toString())?.querySelector('video[data-reflection]') as HTMLVideoElement;
if (videoEl && reflectionEl) togglePlay(videoEl, reflectionEl);
}}
>
{isPlaying ? (
<svg viewBox="0 0 24 24" fill="currentColor">
<path d="M6 19h4V5H6v14zm8-14v14h4V5h-4z"/>
</svg>
) : (
<svg viewBox="0 0 24 24" fill="currentColor">
<path d="M8 5v14l11-7z"/>
</svg>
)}
</ControlButton>
<ControlButton
onClick={(e) => {
e.stopPropagation();
const videoEl = e.currentTarget.closest(VideoItem.toString())?.querySelector('video:not([data-reflection])') as HTMLVideoElement;
if (videoEl) toggleMute(videoEl);
}}
>
{isMuted ? (
<svg viewBox="0 0 24 24" fill="currentColor">
<path d="M16.5 12c0-1.77-1.02-3.29-2.5-4.03v2.21l2.45 2.45c.03-.2.05-.41.05-.63zm2.5 0c0 .94-.2 1.82-.54 2.64l1.51 1.51C20.63 14.91 21 13.5 21 12c0-4.28-2.99-7.86-7-8.77v2.06c2.89.86 5 3.54 5 6.71zM4.27 3L3 4.27 7.73 9H3v6h4l5 5v-6.73l4.25 4.25c-.67.52-1.42.93-2.25 1.18v2.06c1.38-.31 2.63-.95 3.69-1.81L19.73 21 21 19.73l-9-9L4.27 3zM12 4L9.91 6.09 12 8.18V4z"/>
</svg>
) : (
<svg viewBox="0 0 24 24" fill="currentColor">
<path d="M3 9v6h4l5 5V4L7 9H3zm13.5 3c0-1.77-1.02-3.29-2.5-4.03v8.05c1.48-.73 2.5-2.25 2.5-4.02zM14 3.23v2.06c2.89.86 5 3.54 5 6.71s-2.11 5.85-5 6.71v2.06c4.01-.91 7-4.49 7-8.77s-2.99-7.86-7-8.77z"/>
</svg>
)}
</ControlButton>
</ControlsWrapper>
<ReflectionWrapper>
<VideoReflection
muted
loop
playsInline
preload="none"
poster={`${video.url}?vframe/jpg/offset/1`}
src={video.url}
data-reflection="true"
/>
</ReflectionWrapper>
</VideoWrapper>
</VideoItem>
))}
</VideoList>
</Container>
);
};
export default VideoCarousel;

View File

@ -0,0 +1,24 @@
import React from 'react';
import VideoCarousel from './ui/video-carousel';
interface VideoScreenLayoutProps {
videos: {
id: string;
url: string;
title: string;
}[];
}
export const VideoCarouselLayout = ({ videos }: VideoScreenLayoutProps) => {
return (
<>
{videos.length && (
<VideoCarousel
videos={videos}
width="100vw"
height="calc(100vh - 4rem)"
/>
)}
</>
)
}

View File

@ -132,30 +132,6 @@ function VideoGridLayoutComponent({ videos, onEdit, onDelete }: VideoGridLayoutP
<Play className="w-12 h-12 text-white/90" /> <Play className="w-12 h-12 text-white/90" />
</div> </div>
{/* 顶部操作按钮 */}
<div
className={`absolute top-4 right-4 flex gap-2 transition-all duration-300 transform
${hoveredId === video.id ? 'translate-y-0 opacity-100' : 'translate-y-[-10px] opacity-0'}
`}
>
<Button
variant="ghost"
size="icon"
className="w-8 h-8 rounded-full bg-black/40 hover:bg-black/60 backdrop-blur-sm"
onClick={() => onEdit?.(video.id)}
>
<Edit2 className="w-4 h-4 text-white" />
</Button>
<Button
variant="ghost"
size="icon"
className="w-8 h-8 rounded-full bg-black/40 hover:bg-black/60 backdrop-blur-sm"
onClick={() => onDelete?.(video.id)}
>
<Trash2 className="w-4 h-4 text-white" />
</Button>
</div>
{/* 底部控制区域 */} {/* 底部控制区域 */}
<div <div
className={`absolute bottom-4 left-4 right-4 transition-all duration-300 transform className={`absolute bottom-4 left-4 right-4 transition-all duration-300 transform

View File

@ -1,332 +0,0 @@
'use client'; // Add this to ensure it's a client component
import React, { useState, useRef, useEffect, useCallback } from 'react';
import { ChevronLeft, ChevronRight, Volume2, VolumeX, Play, Pause } from 'lucide-react';
import { Button } from '@/components/ui/button';
import dynamic from 'next/dynamic';
interface VideoScreenLayoutProps {
videos: {
id: string;
url: string;
title: string;
}[];
}
function VideoScreenLayoutComponent({ videos }: VideoScreenLayoutProps) {
const [currentIndex, setCurrentIndex] = useState(0);
const [isAnimating, setIsAnimating] = useState(false);
const [isMuted, setIsMuted] = useState(true); // 默认静音
const [volume, setVolume] = useState(0.8); // 添加音量状态
const [isPlaying, setIsPlaying] = useState(true); // 播放状态
const containerRef = useRef<HTMLDivElement>(null);
const videoRefs = useRef<(HTMLVideoElement | null)[]>([]);
useEffect(() => {
setCurrentIndex(videos.length > 2 ? 1 : 0);
}, [videos.length]);
// 确保视频refs数组长度与videos数组一致
useEffect(() => {
videoRefs.current = videoRefs.current.slice(0, videos.length);
}, [videos.length]);
// 计算每个面板的样式
const getPanelStyle = (index: number) => {
const position = index - currentIndex;
const scale = Math.max(0.6, 1 - Math.abs(position) * 0.2);
const zIndex = 10 - Math.abs(position);
const opacity = Math.max(0.4, 1 - Math.abs(position) * 0.3);
let transform = `
perspective(1000px)
scale(${scale})
translateX(${position * 100}%)
`;
// 添加侧面板的 3D 旋转效果
if (position !== 0) {
const rotateY = position > 0 ? -15 : 15;
transform += ` rotateY(${rotateY}deg)`;
}
return {
transform,
zIndex,
opacity,
};
};
// 切换静音状态
const toggleMute = () => {
const currentVideo = videoRefs.current[currentIndex];
if (currentVideo) {
currentVideo.muted = !currentVideo.muted;
setIsMuted(currentVideo.muted);
}
};
// 音量控制函数
const handleVolumeChange = (newVolume: number) => {
setVolume(newVolume);
const currentVideo = videoRefs.current[currentIndex];
if (currentVideo) {
currentVideo.volume = newVolume;
}
};
// 应用音量设置到视频元素
const applyVolumeSettings = useCallback((videoElement: HTMLVideoElement) => {
if (videoElement) {
videoElement.volume = volume;
videoElement.muted = isMuted;
}
}, [volume, isMuted]);
// 处理切换
const handleSlide = useCallback((direction: 'prev' | 'next') => {
if (isAnimating) return;
setIsAnimating(true);
const newIndex = direction === 'next'
? (currentIndex + 1) % videos.length
: (currentIndex - 1 + videos.length) % videos.length;
setCurrentIndex(newIndex);
// 动画结束后重置状态并同步新视频的静音状态
setTimeout(() => {
setIsAnimating(false);
// 同步新视频的静音状态到UI并应用音量设置
const newVideo = videoRefs.current[newIndex];
if (newVideo) {
setIsMuted(newVideo.muted);
applyVolumeSettings(newVideo);
// 根据当前播放状态控制新视频
if (isPlaying) {
newVideo.play().catch(() => {
setIsPlaying(false);
});
} else {
newVideo.pause();
}
}
}, 500);
}, [isAnimating, currentIndex, videos.length, isPlaying, applyVolumeSettings]);
// 音量设置变化时应用到当前视频
useEffect(() => {
const currentVideo = videoRefs.current[currentIndex];
if (currentVideo) {
applyVolumeSettings(currentVideo);
}
}, [volume, isMuted, currentIndex, applyVolumeSettings]);
// 播放状态变化时应用到当前视频
useEffect(() => {
const currentVideo = videoRefs.current[currentIndex];
if (currentVideo) {
if (isPlaying) {
currentVideo.play().catch(() => {
// 处理自动播放策略限制
setIsPlaying(false);
});
} else {
currentVideo.pause();
}
}
}, [isPlaying, currentIndex]);
// 播放/暂停控制
const togglePlay = () => {
setIsPlaying(!isPlaying);
};
// 键盘事件监听器 - 添加左右箭头键控制
useEffect(() => {
const handleKeyDown = (event: KeyboardEvent) => {
// 检查是否正在动画中,如果是则不处理
if (isAnimating) return;
switch (event.key) {
case 'ArrowLeft':
event.preventDefault(); // 阻止默认行为
handleSlide('prev');
break;
case 'ArrowRight':
event.preventDefault(); // 阻止默认行为
handleSlide('next');
break;
}
};
// 添加键盘事件监听器
window.addEventListener('keydown', handleKeyDown);
// 清理函数 - 组件卸载时移除监听器
return () => {
window.removeEventListener('keydown', handleKeyDown);
};
}, [isAnimating, handleSlide]); // 添加handleSlide到依赖项
return (
<div className="relative w-full h-[360px] overflow-hidden bg-[var(--background)]">
{/* 视频面板容器 */}
<div
ref={containerRef}
className="absolute inset-0 flex items-center justify-center"
style={{
perspective: '1000px',
transformStyle: 'preserve-3d',
}}
>
{videos.map((video, index) => (
<div
key={video.id}
className="absolute w-[640px] h-[360px] transition-all duration-500 ease-out"
style={getPanelStyle(index)}
>
<div className="relative w-full h-full overflow-hidden rounded-lg">
{/* 视频 - Add suppressHydrationWarning to prevent className mismatch warnings */}
<video
ref={(el) => (videoRefs.current[index] = el)}
src={video.url}
suppressHydrationWarning
className="w-full h-full object-cover"
autoPlay={index === currentIndex ? isPlaying : false}
loop
muted={index === currentIndex ? isMuted : true} // 只有当前视频受状态控制
playsInline
preload={`${index === currentIndex ? 'auto' : 'none'}`}
poster={`${video.url}?vframe/jpg/offset/1`}
onLoadedData={() => {
if (index === currentIndex && videoRefs.current[index]) {
applyVolumeSettings(videoRefs.current[index]!);
// 根据播放状态决定是否播放
if (isPlaying) {
videoRefs.current[index]!.play().catch(() => {
setIsPlaying(false);
});
}
}
}}
onPlay={() => {
if (index === currentIndex) {
setIsPlaying(true);
}
}}
onPause={() => {
if (index === currentIndex) {
setIsPlaying(false);
}
}}
/>
{/* 视频标题和控制 - 只在中间面板显示 */}
{index === currentIndex && (
<>
{/* 音量控制区域 */}
<div className="absolute top-4 right-4 flex items-center gap-2">
{/* 静音按钮 */}
<Button
variant="ghost"
size="icon"
className="w-10 h-10 rounded-full bg-black/40 hover:bg-black/60 backdrop-blur-sm"
onClick={toggleMute}
title={isMuted ? "取消静音" : "静音"}
>
{isMuted ? (
<VolumeX className="w-5 h-5 text-white" />
) : (
<Volume2 className="w-5 h-5 text-white" />
)}
</Button>
{/* 音量滑块 */}
<div className="flex items-center gap-1 bg-black/40 rounded-full px-2 py-2 backdrop-blur-sm">
<input
type="range"
min="0"
max="1"
step="0.1"
value={volume}
onChange={(e) => handleVolumeChange(parseFloat(e.target.value))}
className="w-12 h-1 bg-white/20 rounded-lg appearance-none cursor-pointer
[&::-webkit-slider-thumb]:appearance-none [&::-webkit-slider-thumb]:w-3 [&::-webkit-slider-thumb]:h-3
[&::-webkit-slider-thumb]:rounded-full [&::-webkit-slider-thumb]:bg-white
[&::-webkit-slider-thumb]:cursor-pointer [&::-webkit-slider-thumb]:shadow-lg
[&::-moz-range-thumb]:w-3 [&::-moz-range-thumb]:h-3 [&::-moz-range-thumb]:rounded-full
[&::-moz-range-thumb]:bg-white [&::-moz-range-thumb]:cursor-pointer
[&::-moz-range-thumb]:border-none [&::-moz-range-thumb]:shadow-lg"
style={{
background: `linear-gradient(to right, white 0%, white ${volume * 100}%, rgba(255,255,255,0.2) ${volume * 100}%, rgba(255,255,255,0.2) 100%)`
}}
/>
<span className="text-xs text-white/70 w-6 text-center">
{Math.round(volume * 100)}%
</span>
</div>
</div>
{/* 底部控制区域 */}
<div className="absolute bottom-16 left-4">
{/* 播放/暂停按钮 */}
<Button
variant="ghost"
size="icon"
className="w-10 h-10 rounded-full bg-black/40 hover:bg-black/60 backdrop-blur-sm"
onClick={togglePlay}
title={isPlaying ? "暂停" : "播放"}
>
{isPlaying ? (
<Pause className="w-5 h-5 text-white" />
) : (
<Play className="w-5 h-5 text-white" />
)}
</Button>
</div>
{/* 视频标题 */}
<div className="absolute bottom-0 left-0 right-0 p-4 bg-gradient-to-t from-black/80 to-transparent">
<h3 className="text-white text-lg font-medium">{video.title}</h3>
</div>
</>
)}
{/* 玻璃态遮罩 - 侧面板半透明效果 */}
{index !== currentIndex && (
<div className="absolute inset-0 bg-black/20 backdrop-blur-sm" />
)}
</div>
</div>
))}
</div>
{/* 切换按钮 */}
<Button
variant="ghost"
size="icon"
className="absolute left-4 top-1/2 -translate-y-1/2 w-12 h-12 rounded-full bg-white/10 hover:bg-white/20 backdrop-blur-sm transition-all"
onClick={() => handleSlide('prev')}
disabled={isAnimating}
>
<ChevronLeft className="w-6 h-6 text-white" />
</Button>
<Button
variant="ghost"
size="icon"
className="absolute right-4 top-1/2 -translate-y-1/2 w-12 h-12 rounded-full bg-white/10 hover:bg-white/20 backdrop-blur-sm transition-all"
onClick={() => handleSlide('next')}
disabled={isAnimating}
>
<ChevronRight className="w-6 h-6 text-white" />
</Button>
</div>
);
}
// Export as a client-only component to prevent hydration issues
export const VideoScreenLayout = dynamic(() => Promise.resolve(VideoScreenLayoutComponent), {
ssr: false
});

6
lib/utils.ts Normal file
View File

@ -0,0 +1,6 @@
import { type ClassValue, clsx } from "clsx"
import { twMerge } from "tailwind-merge"
export function cn(...inputs: ClassValue[]) {
return twMerge(clsx(inputs))
}

578
package-lock.json generated
View File

@ -34,7 +34,7 @@
"@radix-ui/react-select": "^2.2.5", "@radix-ui/react-select": "^2.2.5",
"@radix-ui/react-separator": "^1.1.0", "@radix-ui/react-separator": "^1.1.0",
"@radix-ui/react-slider": "^1.3.5", "@radix-ui/react-slider": "^1.3.5",
"@radix-ui/react-slot": "^1.1.0", "@radix-ui/react-slot": "^1.2.3",
"@radix-ui/react-switch": "^1.1.0", "@radix-ui/react-switch": "^1.1.0",
"@radix-ui/react-tabs": "^1.1.0", "@radix-ui/react-tabs": "^1.1.0",
"@radix-ui/react-toast": "^1.2.1", "@radix-ui/react-toast": "^1.2.1",
@ -48,13 +48,14 @@
"@types/react": "18.2.22", "@types/react": "18.2.22",
"@types/react-beautiful-dnd": "^13.1.8", "@types/react-beautiful-dnd": "^13.1.8",
"@types/react-dom": "18.2.7", "@types/react-dom": "18.2.7",
"@types/styled-components": "^5.1.34",
"@types/three": "^0.177.0", "@types/three": "^0.177.0",
"@types/wavesurfer.js": "^6.0.12", "@types/wavesurfer.js": "^6.0.12",
"antd": "^5.26.2", "antd": "^5.26.2",
"autoprefixer": "10.4.15", "autoprefixer": "10.4.15",
"axios": "^1.10.0", "axios": "^1.10.0",
"babel-loader": "^10.0.0", "babel-loader": "^10.0.0",
"class-variance-authority": "^0.7.0", "class-variance-authority": "^0.7.1",
"clsx": "^2.1.1", "clsx": "^2.1.1",
"cmdk": "^1.0.0", "cmdk": "^1.0.0",
"date-fns": "^3.6.0", "date-fns": "^3.6.0",
@ -86,7 +87,7 @@
"sonner": "^1.5.0", "sonner": "^1.5.0",
"styled-components": "^6.1.19", "styled-components": "^6.1.19",
"swiper": "^11.2.8", "swiper": "^11.2.8",
"tailwind-merge": "^2.5.2", "tailwind-merge": "^2.6.0",
"tailwindcss": "3.3.3", "tailwindcss": "3.3.3",
"tailwindcss-animate": "^1.0.7", "tailwindcss-animate": "^1.0.7",
"three": "^0.177.0", "three": "^0.177.0",
@ -1200,24 +1201,6 @@
} }
} }
}, },
"node_modules/@radix-ui/react-accordion/node_modules/@radix-ui/react-slot": {
"version": "1.2.3",
"resolved": "https://registry.npmmirror.com/@radix-ui/react-slot/-/react-slot-1.2.3.tgz",
"integrity": "sha512-aeNmHnBxbi2St0au6VBVC7JXFlhLlOnvIIlePNniyUNAClzmtAUEY8/pBiK3iHjufOlwA+c20/8jngo7xcrg8A==",
"license": "MIT",
"dependencies": {
"@radix-ui/react-compose-refs": "1.1.2"
},
"peerDependencies": {
"@types/react": "*",
"react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc"
},
"peerDependenciesMeta": {
"@types/react": {
"optional": true
}
}
},
"node_modules/@radix-ui/react-accordion/node_modules/@radix-ui/react-use-controllable-state": { "node_modules/@radix-ui/react-accordion/node_modules/@radix-ui/react-use-controllable-state": {
"version": "1.2.2", "version": "1.2.2",
"resolved": "https://registry.npmmirror.com/@radix-ui/react-use-controllable-state/-/react-use-controllable-state-1.2.2.tgz", "resolved": "https://registry.npmmirror.com/@radix-ui/react-use-controllable-state/-/react-use-controllable-state-1.2.2.tgz",
@ -1339,24 +1322,6 @@
} }
} }
}, },
"node_modules/@radix-ui/react-alert-dialog/node_modules/@radix-ui/react-slot": {
"version": "1.2.3",
"resolved": "https://registry.npmmirror.com/@radix-ui/react-slot/-/react-slot-1.2.3.tgz",
"integrity": "sha512-aeNmHnBxbi2St0au6VBVC7JXFlhLlOnvIIlePNniyUNAClzmtAUEY8/pBiK3iHjufOlwA+c20/8jngo7xcrg8A==",
"license": "MIT",
"dependencies": {
"@radix-ui/react-compose-refs": "1.1.2"
},
"peerDependencies": {
"@types/react": "*",
"react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc"
},
"peerDependenciesMeta": {
"@types/react": {
"optional": true
}
}
},
"node_modules/@radix-ui/react-arrow": { "node_modules/@radix-ui/react-arrow": {
"version": "1.1.0", "version": "1.1.0",
"resolved": "https://registry.npmjs.org/@radix-ui/react-arrow/-/react-arrow-1.1.0.tgz", "resolved": "https://registry.npmjs.org/@radix-ui/react-arrow/-/react-arrow-1.1.0.tgz",
@ -1402,21 +1367,6 @@
} }
} }
}, },
"node_modules/@radix-ui/react-aspect-ratio/node_modules/@radix-ui/react-compose-refs": {
"version": "1.1.2",
"resolved": "https://registry.npmmirror.com/@radix-ui/react-compose-refs/-/react-compose-refs-1.1.2.tgz",
"integrity": "sha512-z4eqJvfiNnFMHIIvXP3CY57y2WJs5g2v3X0zm9mEJkrkNv4rDxu+sg9Jh8EkXyeqBkB7SOcboo9dMVqhyrACIg==",
"license": "MIT",
"peerDependencies": {
"@types/react": "*",
"react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc"
},
"peerDependenciesMeta": {
"@types/react": {
"optional": true
}
}
},
"node_modules/@radix-ui/react-aspect-ratio/node_modules/@radix-ui/react-primitive": { "node_modules/@radix-ui/react-aspect-ratio/node_modules/@radix-ui/react-primitive": {
"version": "2.1.3", "version": "2.1.3",
"resolved": "https://registry.npmmirror.com/@radix-ui/react-primitive/-/react-primitive-2.1.3.tgz", "resolved": "https://registry.npmmirror.com/@radix-ui/react-primitive/-/react-primitive-2.1.3.tgz",
@ -1440,24 +1390,6 @@
} }
} }
}, },
"node_modules/@radix-ui/react-aspect-ratio/node_modules/@radix-ui/react-slot": {
"version": "1.2.3",
"resolved": "https://registry.npmmirror.com/@radix-ui/react-slot/-/react-slot-1.2.3.tgz",
"integrity": "sha512-aeNmHnBxbi2St0au6VBVC7JXFlhLlOnvIIlePNniyUNAClzmtAUEY8/pBiK3iHjufOlwA+c20/8jngo7xcrg8A==",
"license": "MIT",
"dependencies": {
"@radix-ui/react-compose-refs": "1.1.2"
},
"peerDependencies": {
"@types/react": "*",
"react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc"
},
"peerDependenciesMeta": {
"@types/react": {
"optional": true
}
}
},
"node_modules/@radix-ui/react-avatar": { "node_modules/@radix-ui/react-avatar": {
"version": "1.1.10", "version": "1.1.10",
"resolved": "https://registry.npmmirror.com/@radix-ui/react-avatar/-/react-avatar-1.1.10.tgz", "resolved": "https://registry.npmmirror.com/@radix-ui/react-avatar/-/react-avatar-1.1.10.tgz",
@ -1485,21 +1417,6 @@
} }
} }
}, },
"node_modules/@radix-ui/react-avatar/node_modules/@radix-ui/react-compose-refs": {
"version": "1.1.2",
"resolved": "https://registry.npmmirror.com/@radix-ui/react-compose-refs/-/react-compose-refs-1.1.2.tgz",
"integrity": "sha512-z4eqJvfiNnFMHIIvXP3CY57y2WJs5g2v3X0zm9mEJkrkNv4rDxu+sg9Jh8EkXyeqBkB7SOcboo9dMVqhyrACIg==",
"license": "MIT",
"peerDependencies": {
"@types/react": "*",
"react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc"
},
"peerDependenciesMeta": {
"@types/react": {
"optional": true
}
}
},
"node_modules/@radix-ui/react-avatar/node_modules/@radix-ui/react-context": { "node_modules/@radix-ui/react-avatar/node_modules/@radix-ui/react-context": {
"version": "1.1.2", "version": "1.1.2",
"resolved": "https://registry.npmmirror.com/@radix-ui/react-context/-/react-context-1.1.2.tgz", "resolved": "https://registry.npmmirror.com/@radix-ui/react-context/-/react-context-1.1.2.tgz",
@ -1538,24 +1455,6 @@
} }
} }
}, },
"node_modules/@radix-ui/react-avatar/node_modules/@radix-ui/react-slot": {
"version": "1.2.3",
"resolved": "https://registry.npmmirror.com/@radix-ui/react-slot/-/react-slot-1.2.3.tgz",
"integrity": "sha512-aeNmHnBxbi2St0au6VBVC7JXFlhLlOnvIIlePNniyUNAClzmtAUEY8/pBiK3iHjufOlwA+c20/8jngo7xcrg8A==",
"license": "MIT",
"dependencies": {
"@radix-ui/react-compose-refs": "1.1.2"
},
"peerDependencies": {
"@types/react": "*",
"react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc"
},
"peerDependenciesMeta": {
"@types/react": {
"optional": true
}
}
},
"node_modules/@radix-ui/react-avatar/node_modules/@radix-ui/react-use-callback-ref": { "node_modules/@radix-ui/react-avatar/node_modules/@radix-ui/react-use-callback-ref": {
"version": "1.1.1", "version": "1.1.1",
"resolved": "https://registry.npmmirror.com/@radix-ui/react-use-callback-ref/-/react-use-callback-ref-1.1.1.tgz", "resolved": "https://registry.npmmirror.com/@radix-ui/react-use-callback-ref/-/react-use-callback-ref-1.1.1.tgz",
@ -1699,24 +1598,6 @@
} }
} }
}, },
"node_modules/@radix-ui/react-checkbox/node_modules/@radix-ui/react-slot": {
"version": "1.2.3",
"resolved": "https://registry.npmmirror.com/@radix-ui/react-slot/-/react-slot-1.2.3.tgz",
"integrity": "sha512-aeNmHnBxbi2St0au6VBVC7JXFlhLlOnvIIlePNniyUNAClzmtAUEY8/pBiK3iHjufOlwA+c20/8jngo7xcrg8A==",
"license": "MIT",
"dependencies": {
"@radix-ui/react-compose-refs": "1.1.2"
},
"peerDependencies": {
"@types/react": "*",
"react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc"
},
"peerDependenciesMeta": {
"@types/react": {
"optional": true
}
}
},
"node_modules/@radix-ui/react-checkbox/node_modules/@radix-ui/react-use-controllable-state": { "node_modules/@radix-ui/react-checkbox/node_modules/@radix-ui/react-use-controllable-state": {
"version": "1.2.2", "version": "1.2.2",
"resolved": "https://registry.npmmirror.com/@radix-ui/react-use-controllable-state/-/react-use-controllable-state-1.2.2.tgz", "resolved": "https://registry.npmmirror.com/@radix-ui/react-use-controllable-state/-/react-use-controllable-state-1.2.2.tgz",
@ -1915,24 +1796,6 @@
} }
} }
}, },
"node_modules/@radix-ui/react-collapsible/node_modules/@radix-ui/react-slot": {
"version": "1.2.3",
"resolved": "https://registry.npmmirror.com/@radix-ui/react-slot/-/react-slot-1.2.3.tgz",
"integrity": "sha512-aeNmHnBxbi2St0au6VBVC7JXFlhLlOnvIIlePNniyUNAClzmtAUEY8/pBiK3iHjufOlwA+c20/8jngo7xcrg8A==",
"license": "MIT",
"dependencies": {
"@radix-ui/react-compose-refs": "1.1.2"
},
"peerDependencies": {
"@types/react": "*",
"react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc"
},
"peerDependenciesMeta": {
"@types/react": {
"optional": true
}
}
},
"node_modules/@radix-ui/react-collapsible/node_modules/@radix-ui/react-use-controllable-state": { "node_modules/@radix-ui/react-collapsible/node_modules/@radix-ui/react-use-controllable-state": {
"version": "1.2.2", "version": "1.2.2",
"resolved": "https://registry.npmmirror.com/@radix-ui/react-use-controllable-state/-/react-use-controllable-state-1.2.2.tgz", "resolved": "https://registry.npmmirror.com/@radix-ui/react-use-controllable-state/-/react-use-controllable-state-1.2.2.tgz",
@ -2006,6 +1869,24 @@
} }
} }
}, },
"node_modules/@radix-ui/react-collection/node_modules/@radix-ui/react-slot": {
"version": "1.1.0",
"resolved": "https://registry.npmmirror.com/@radix-ui/react-slot/-/react-slot-1.1.0.tgz",
"integrity": "sha512-FUCf5XMfmW4dtYl69pdS4DbxKy8nj4M7SafBgPllysxmdachynNflAdp/gCsnYWNDnge6tI9onzMp5ARYc1KNw==",
"license": "MIT",
"dependencies": {
"@radix-ui/react-compose-refs": "1.1.0"
},
"peerDependencies": {
"@types/react": "*",
"react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc"
},
"peerDependenciesMeta": {
"@types/react": {
"optional": true
}
}
},
"node_modules/@radix-ui/react-compose-refs": { "node_modules/@radix-ui/react-compose-refs": {
"version": "1.1.0", "version": "1.1.0",
"resolved": "https://registry.npmjs.org/@radix-ui/react-compose-refs/-/react-compose-refs-1.1.0.tgz", "resolved": "https://registry.npmjs.org/@radix-ui/react-compose-refs/-/react-compose-refs-1.1.0.tgz",
@ -2421,24 +2302,6 @@
} }
} }
}, },
"node_modules/@radix-ui/react-context-menu/node_modules/@radix-ui/react-slot": {
"version": "1.2.3",
"resolved": "https://registry.npmmirror.com/@radix-ui/react-slot/-/react-slot-1.2.3.tgz",
"integrity": "sha512-aeNmHnBxbi2St0au6VBVC7JXFlhLlOnvIIlePNniyUNAClzmtAUEY8/pBiK3iHjufOlwA+c20/8jngo7xcrg8A==",
"license": "MIT",
"dependencies": {
"@radix-ui/react-compose-refs": "1.1.2"
},
"peerDependencies": {
"@types/react": "*",
"react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc"
},
"peerDependenciesMeta": {
"@types/react": {
"optional": true
}
}
},
"node_modules/@radix-ui/react-context-menu/node_modules/@radix-ui/react-use-callback-ref": { "node_modules/@radix-ui/react-context-menu/node_modules/@radix-ui/react-use-callback-ref": {
"version": "1.1.1", "version": "1.1.1",
"resolved": "https://registry.npmmirror.com/@radix-ui/react-use-callback-ref/-/react-use-callback-ref-1.1.1.tgz", "resolved": "https://registry.npmmirror.com/@radix-ui/react-use-callback-ref/-/react-use-callback-ref-1.1.1.tgz",
@ -2801,24 +2664,6 @@
} }
} }
}, },
"node_modules/@radix-ui/react-dialog/node_modules/@radix-ui/react-slot": {
"version": "1.2.3",
"resolved": "https://registry.npmmirror.com/@radix-ui/react-slot/-/react-slot-1.2.3.tgz",
"integrity": "sha512-aeNmHnBxbi2St0au6VBVC7JXFlhLlOnvIIlePNniyUNAClzmtAUEY8/pBiK3iHjufOlwA+c20/8jngo7xcrg8A==",
"license": "MIT",
"dependencies": {
"@radix-ui/react-compose-refs": "1.1.2"
},
"peerDependencies": {
"@types/react": "*",
"react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc"
},
"peerDependenciesMeta": {
"@types/react": {
"optional": true
}
}
},
"node_modules/@radix-ui/react-dialog/node_modules/@radix-ui/react-use-callback-ref": { "node_modules/@radix-ui/react-dialog/node_modules/@radix-ui/react-use-callback-ref": {
"version": "1.1.1", "version": "1.1.1",
"resolved": "https://registry.npmmirror.com/@radix-ui/react-use-callback-ref/-/react-use-callback-ref-1.1.1.tgz", "resolved": "https://registry.npmmirror.com/@radix-ui/react-use-callback-ref/-/react-use-callback-ref-1.1.1.tgz",
@ -3087,21 +2932,6 @@
} }
} }
}, },
"node_modules/@radix-ui/react-label/node_modules/@radix-ui/react-compose-refs": {
"version": "1.1.2",
"resolved": "https://registry.npmmirror.com/@radix-ui/react-compose-refs/-/react-compose-refs-1.1.2.tgz",
"integrity": "sha512-z4eqJvfiNnFMHIIvXP3CY57y2WJs5g2v3X0zm9mEJkrkNv4rDxu+sg9Jh8EkXyeqBkB7SOcboo9dMVqhyrACIg==",
"license": "MIT",
"peerDependencies": {
"@types/react": "*",
"react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc"
},
"peerDependenciesMeta": {
"@types/react": {
"optional": true
}
}
},
"node_modules/@radix-ui/react-label/node_modules/@radix-ui/react-primitive": { "node_modules/@radix-ui/react-label/node_modules/@radix-ui/react-primitive": {
"version": "2.1.3", "version": "2.1.3",
"resolved": "https://registry.npmmirror.com/@radix-ui/react-primitive/-/react-primitive-2.1.3.tgz", "resolved": "https://registry.npmmirror.com/@radix-ui/react-primitive/-/react-primitive-2.1.3.tgz",
@ -3125,24 +2955,6 @@
} }
} }
}, },
"node_modules/@radix-ui/react-label/node_modules/@radix-ui/react-slot": {
"version": "1.2.3",
"resolved": "https://registry.npmmirror.com/@radix-ui/react-slot/-/react-slot-1.2.3.tgz",
"integrity": "sha512-aeNmHnBxbi2St0au6VBVC7JXFlhLlOnvIIlePNniyUNAClzmtAUEY8/pBiK3iHjufOlwA+c20/8jngo7xcrg8A==",
"license": "MIT",
"dependencies": {
"@radix-ui/react-compose-refs": "1.1.2"
},
"peerDependencies": {
"@types/react": "*",
"react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc"
},
"peerDependenciesMeta": {
"@types/react": {
"optional": true
}
}
},
"node_modules/@radix-ui/react-menu": { "node_modules/@radix-ui/react-menu": {
"version": "2.1.2", "version": "2.1.2",
"resolved": "https://registry.npmjs.org/@radix-ui/react-menu/-/react-menu-2.1.2.tgz", "resolved": "https://registry.npmjs.org/@radix-ui/react-menu/-/react-menu-2.1.2.tgz",
@ -3182,6 +2994,24 @@
} }
} }
}, },
"node_modules/@radix-ui/react-menu/node_modules/@radix-ui/react-slot": {
"version": "1.1.0",
"resolved": "https://registry.npmmirror.com/@radix-ui/react-slot/-/react-slot-1.1.0.tgz",
"integrity": "sha512-FUCf5XMfmW4dtYl69pdS4DbxKy8nj4M7SafBgPllysxmdachynNflAdp/gCsnYWNDnge6tI9onzMp5ARYc1KNw==",
"license": "MIT",
"dependencies": {
"@radix-ui/react-compose-refs": "1.1.0"
},
"peerDependencies": {
"@types/react": "*",
"react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc"
},
"peerDependenciesMeta": {
"@types/react": {
"optional": true
}
}
},
"node_modules/@radix-ui/react-menubar": { "node_modules/@radix-ui/react-menubar": {
"version": "1.1.2", "version": "1.1.2",
"resolved": "https://registry.npmjs.org/@radix-ui/react-menubar/-/react-menubar-1.1.2.tgz", "resolved": "https://registry.npmjs.org/@radix-ui/react-menubar/-/react-menubar-1.1.2.tgz",
@ -3532,24 +3362,6 @@
} }
} }
}, },
"node_modules/@radix-ui/react-popover/node_modules/@radix-ui/react-slot": {
"version": "1.2.3",
"resolved": "https://registry.npmmirror.com/@radix-ui/react-slot/-/react-slot-1.2.3.tgz",
"integrity": "sha512-aeNmHnBxbi2St0au6VBVC7JXFlhLlOnvIIlePNniyUNAClzmtAUEY8/pBiK3iHjufOlwA+c20/8jngo7xcrg8A==",
"license": "MIT",
"dependencies": {
"@radix-ui/react-compose-refs": "1.1.2"
},
"peerDependencies": {
"@types/react": "*",
"react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc"
},
"peerDependenciesMeta": {
"@types/react": {
"optional": true
}
}
},
"node_modules/@radix-ui/react-popover/node_modules/@radix-ui/react-use-callback-ref": { "node_modules/@radix-ui/react-popover/node_modules/@radix-ui/react-use-callback-ref": {
"version": "1.1.1", "version": "1.1.1",
"resolved": "https://registry.npmmirror.com/@radix-ui/react-use-callback-ref/-/react-use-callback-ref-1.1.1.tgz", "resolved": "https://registry.npmmirror.com/@radix-ui/react-use-callback-ref/-/react-use-callback-ref-1.1.1.tgz",
@ -3797,6 +3609,24 @@
} }
} }
}, },
"node_modules/@radix-ui/react-primitive/node_modules/@radix-ui/react-slot": {
"version": "1.1.0",
"resolved": "https://registry.npmmirror.com/@radix-ui/react-slot/-/react-slot-1.1.0.tgz",
"integrity": "sha512-FUCf5XMfmW4dtYl69pdS4DbxKy8nj4M7SafBgPllysxmdachynNflAdp/gCsnYWNDnge6tI9onzMp5ARYc1KNw==",
"license": "MIT",
"dependencies": {
"@radix-ui/react-compose-refs": "1.1.0"
},
"peerDependencies": {
"@types/react": "*",
"react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc"
},
"peerDependenciesMeta": {
"@types/react": {
"optional": true
}
}
},
"node_modules/@radix-ui/react-progress": { "node_modules/@radix-ui/react-progress": {
"version": "1.1.7", "version": "1.1.7",
"resolved": "https://registry.npmmirror.com/@radix-ui/react-progress/-/react-progress-1.1.7.tgz", "resolved": "https://registry.npmmirror.com/@radix-ui/react-progress/-/react-progress-1.1.7.tgz",
@ -3821,21 +3651,6 @@
} }
} }
}, },
"node_modules/@radix-ui/react-progress/node_modules/@radix-ui/react-compose-refs": {
"version": "1.1.2",
"resolved": "https://registry.npmmirror.com/@radix-ui/react-compose-refs/-/react-compose-refs-1.1.2.tgz",
"integrity": "sha512-z4eqJvfiNnFMHIIvXP3CY57y2WJs5g2v3X0zm9mEJkrkNv4rDxu+sg9Jh8EkXyeqBkB7SOcboo9dMVqhyrACIg==",
"license": "MIT",
"peerDependencies": {
"@types/react": "*",
"react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc"
},
"peerDependenciesMeta": {
"@types/react": {
"optional": true
}
}
},
"node_modules/@radix-ui/react-progress/node_modules/@radix-ui/react-context": { "node_modules/@radix-ui/react-progress/node_modules/@radix-ui/react-context": {
"version": "1.1.2", "version": "1.1.2",
"resolved": "https://registry.npmmirror.com/@radix-ui/react-context/-/react-context-1.1.2.tgz", "resolved": "https://registry.npmmirror.com/@radix-ui/react-context/-/react-context-1.1.2.tgz",
@ -3874,24 +3689,6 @@
} }
} }
}, },
"node_modules/@radix-ui/react-progress/node_modules/@radix-ui/react-slot": {
"version": "1.2.3",
"resolved": "https://registry.npmmirror.com/@radix-ui/react-slot/-/react-slot-1.2.3.tgz",
"integrity": "sha512-aeNmHnBxbi2St0au6VBVC7JXFlhLlOnvIIlePNniyUNAClzmtAUEY8/pBiK3iHjufOlwA+c20/8jngo7xcrg8A==",
"license": "MIT",
"dependencies": {
"@radix-ui/react-compose-refs": "1.1.2"
},
"peerDependencies": {
"@types/react": "*",
"react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc"
},
"peerDependenciesMeta": {
"@types/react": {
"optional": true
}
}
},
"node_modules/@radix-ui/react-radio-group": { "node_modules/@radix-ui/react-radio-group": {
"version": "1.3.7", "version": "1.3.7",
"resolved": "https://registry.npmmirror.com/@radix-ui/react-radio-group/-/react-radio-group-1.3.7.tgz", "resolved": "https://registry.npmmirror.com/@radix-ui/react-radio-group/-/react-radio-group-1.3.7.tgz",
@ -4097,24 +3894,6 @@
} }
} }
}, },
"node_modules/@radix-ui/react-radio-group/node_modules/@radix-ui/react-slot": {
"version": "1.2.3",
"resolved": "https://registry.npmmirror.com/@radix-ui/react-slot/-/react-slot-1.2.3.tgz",
"integrity": "sha512-aeNmHnBxbi2St0au6VBVC7JXFlhLlOnvIIlePNniyUNAClzmtAUEY8/pBiK3iHjufOlwA+c20/8jngo7xcrg8A==",
"license": "MIT",
"dependencies": {
"@radix-ui/react-compose-refs": "1.1.2"
},
"peerDependencies": {
"@types/react": "*",
"react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc"
},
"peerDependenciesMeta": {
"@types/react": {
"optional": true
}
}
},
"node_modules/@radix-ui/react-radio-group/node_modules/@radix-ui/react-use-callback-ref": { "node_modules/@radix-ui/react-radio-group/node_modules/@radix-ui/react-use-callback-ref": {
"version": "1.1.1", "version": "1.1.1",
"resolved": "https://registry.npmmirror.com/@radix-ui/react-use-callback-ref/-/react-use-callback-ref-1.1.1.tgz", "resolved": "https://registry.npmmirror.com/@radix-ui/react-use-callback-ref/-/react-use-callback-ref-1.1.1.tgz",
@ -4370,24 +4149,6 @@
} }
} }
}, },
"node_modules/@radix-ui/react-scroll-area/node_modules/@radix-ui/react-slot": {
"version": "1.2.3",
"resolved": "https://registry.npmmirror.com/@radix-ui/react-slot/-/react-slot-1.2.3.tgz",
"integrity": "sha512-aeNmHnBxbi2St0au6VBVC7JXFlhLlOnvIIlePNniyUNAClzmtAUEY8/pBiK3iHjufOlwA+c20/8jngo7xcrg8A==",
"license": "MIT",
"dependencies": {
"@radix-ui/react-compose-refs": "1.1.2"
},
"peerDependencies": {
"@types/react": "*",
"react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc"
},
"peerDependenciesMeta": {
"@types/react": {
"optional": true
}
}
},
"node_modules/@radix-ui/react-scroll-area/node_modules/@radix-ui/react-use-callback-ref": { "node_modules/@radix-ui/react-scroll-area/node_modules/@radix-ui/react-use-callback-ref": {
"version": "1.1.1", "version": "1.1.1",
"resolved": "https://registry.npmmirror.com/@radix-ui/react-use-callback-ref/-/react-use-callback-ref-1.1.1.tgz", "resolved": "https://registry.npmmirror.com/@radix-ui/react-use-callback-ref/-/react-use-callback-ref-1.1.1.tgz",
@ -4725,24 +4486,6 @@
} }
} }
}, },
"node_modules/@radix-ui/react-select/node_modules/@radix-ui/react-slot": {
"version": "1.2.3",
"resolved": "https://registry.npmmirror.com/@radix-ui/react-slot/-/react-slot-1.2.3.tgz",
"integrity": "sha512-aeNmHnBxbi2St0au6VBVC7JXFlhLlOnvIIlePNniyUNAClzmtAUEY8/pBiK3iHjufOlwA+c20/8jngo7xcrg8A==",
"license": "MIT",
"dependencies": {
"@radix-ui/react-compose-refs": "1.1.2"
},
"peerDependencies": {
"@types/react": "*",
"react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc"
},
"peerDependenciesMeta": {
"@types/react": {
"optional": true
}
}
},
"node_modules/@radix-ui/react-select/node_modules/@radix-ui/react-use-callback-ref": { "node_modules/@radix-ui/react-select/node_modules/@radix-ui/react-use-callback-ref": {
"version": "1.1.1", "version": "1.1.1",
"resolved": "https://registry.npmmirror.com/@radix-ui/react-use-callback-ref/-/react-use-callback-ref-1.1.1.tgz", "resolved": "https://registry.npmmirror.com/@radix-ui/react-use-callback-ref/-/react-use-callback-ref-1.1.1.tgz",
@ -5070,24 +4813,6 @@
} }
} }
}, },
"node_modules/@radix-ui/react-slider/node_modules/@radix-ui/react-slot": {
"version": "1.2.3",
"resolved": "https://registry.npmmirror.com/@radix-ui/react-slot/-/react-slot-1.2.3.tgz",
"integrity": "sha512-aeNmHnBxbi2St0au6VBVC7JXFlhLlOnvIIlePNniyUNAClzmtAUEY8/pBiK3iHjufOlwA+c20/8jngo7xcrg8A==",
"license": "MIT",
"dependencies": {
"@radix-ui/react-compose-refs": "1.1.2"
},
"peerDependencies": {
"@types/react": "*",
"react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc"
},
"peerDependenciesMeta": {
"@types/react": {
"optional": true
}
}
},
"node_modules/@radix-ui/react-slider/node_modules/@radix-ui/react-use-controllable-state": { "node_modules/@radix-ui/react-slider/node_modules/@radix-ui/react-use-controllable-state": {
"version": "1.2.2", "version": "1.2.2",
"resolved": "https://registry.npmmirror.com/@radix-ui/react-use-controllable-state/-/react-use-controllable-state-1.2.2.tgz", "resolved": "https://registry.npmmirror.com/@radix-ui/react-use-controllable-state/-/react-use-controllable-state-1.2.2.tgz",
@ -5156,11 +4881,12 @@
} }
}, },
"node_modules/@radix-ui/react-slot": { "node_modules/@radix-ui/react-slot": {
"version": "1.1.0", "version": "1.2.3",
"resolved": "https://registry.npmjs.org/@radix-ui/react-slot/-/react-slot-1.1.0.tgz", "resolved": "https://registry.npmmirror.com/@radix-ui/react-slot/-/react-slot-1.2.3.tgz",
"integrity": "sha512-FUCf5XMfmW4dtYl69pdS4DbxKy8nj4M7SafBgPllysxmdachynNflAdp/gCsnYWNDnge6tI9onzMp5ARYc1KNw==", "integrity": "sha512-aeNmHnBxbi2St0au6VBVC7JXFlhLlOnvIIlePNniyUNAClzmtAUEY8/pBiK3iHjufOlwA+c20/8jngo7xcrg8A==",
"license": "MIT",
"dependencies": { "dependencies": {
"@radix-ui/react-compose-refs": "1.1.0" "@radix-ui/react-compose-refs": "1.1.2"
}, },
"peerDependencies": { "peerDependencies": {
"@types/react": "*", "@types/react": "*",
@ -5172,6 +4898,21 @@
} }
} }
}, },
"node_modules/@radix-ui/react-slot/node_modules/@radix-ui/react-compose-refs": {
"version": "1.1.2",
"resolved": "https://registry.npmmirror.com/@radix-ui/react-compose-refs/-/react-compose-refs-1.1.2.tgz",
"integrity": "sha512-z4eqJvfiNnFMHIIvXP3CY57y2WJs5g2v3X0zm9mEJkrkNv4rDxu+sg9Jh8EkXyeqBkB7SOcboo9dMVqhyrACIg==",
"license": "MIT",
"peerDependencies": {
"@types/react": "*",
"react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc"
},
"peerDependenciesMeta": {
"@types/react": {
"optional": true
}
}
},
"node_modules/@radix-ui/react-switch": { "node_modules/@radix-ui/react-switch": {
"version": "1.2.5", "version": "1.2.5",
"resolved": "https://registry.npmmirror.com/@radix-ui/react-switch/-/react-switch-1.2.5.tgz", "resolved": "https://registry.npmmirror.com/@radix-ui/react-switch/-/react-switch-1.2.5.tgz",
@ -5260,24 +5001,6 @@
} }
} }
}, },
"node_modules/@radix-ui/react-switch/node_modules/@radix-ui/react-slot": {
"version": "1.2.3",
"resolved": "https://registry.npmmirror.com/@radix-ui/react-slot/-/react-slot-1.2.3.tgz",
"integrity": "sha512-aeNmHnBxbi2St0au6VBVC7JXFlhLlOnvIIlePNniyUNAClzmtAUEY8/pBiK3iHjufOlwA+c20/8jngo7xcrg8A==",
"license": "MIT",
"dependencies": {
"@radix-ui/react-compose-refs": "1.1.2"
},
"peerDependencies": {
"@types/react": "*",
"react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc"
},
"peerDependenciesMeta": {
"@types/react": {
"optional": true
}
}
},
"node_modules/@radix-ui/react-switch/node_modules/@radix-ui/react-use-controllable-state": { "node_modules/@radix-ui/react-switch/node_modules/@radix-ui/react-use-controllable-state": {
"version": "1.2.2", "version": "1.2.2",
"resolved": "https://registry.npmmirror.com/@radix-ui/react-use-controllable-state/-/react-use-controllable-state-1.2.2.tgz", "resolved": "https://registry.npmmirror.com/@radix-ui/react-use-controllable-state/-/react-use-controllable-state-1.2.2.tgz",
@ -5548,24 +5271,6 @@
} }
} }
}, },
"node_modules/@radix-ui/react-tabs/node_modules/@radix-ui/react-slot": {
"version": "1.2.3",
"resolved": "https://registry.npmmirror.com/@radix-ui/react-slot/-/react-slot-1.2.3.tgz",
"integrity": "sha512-aeNmHnBxbi2St0au6VBVC7JXFlhLlOnvIIlePNniyUNAClzmtAUEY8/pBiK3iHjufOlwA+c20/8jngo7xcrg8A==",
"license": "MIT",
"dependencies": {
"@radix-ui/react-compose-refs": "1.1.2"
},
"peerDependencies": {
"@types/react": "*",
"react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc"
},
"peerDependenciesMeta": {
"@types/react": {
"optional": true
}
}
},
"node_modules/@radix-ui/react-tabs/node_modules/@radix-ui/react-use-callback-ref": { "node_modules/@radix-ui/react-tabs/node_modules/@radix-ui/react-use-callback-ref": {
"version": "1.1.1", "version": "1.1.1",
"resolved": "https://registry.npmmirror.com/@radix-ui/react-use-callback-ref/-/react-use-callback-ref-1.1.1.tgz", "resolved": "https://registry.npmmirror.com/@radix-ui/react-use-callback-ref/-/react-use-callback-ref-1.1.1.tgz",
@ -5851,24 +5556,6 @@
} }
} }
}, },
"node_modules/@radix-ui/react-toggle-group/node_modules/@radix-ui/react-slot": {
"version": "1.2.3",
"resolved": "https://registry.npmmirror.com/@radix-ui/react-slot/-/react-slot-1.2.3.tgz",
"integrity": "sha512-aeNmHnBxbi2St0au6VBVC7JXFlhLlOnvIIlePNniyUNAClzmtAUEY8/pBiK3iHjufOlwA+c20/8jngo7xcrg8A==",
"license": "MIT",
"dependencies": {
"@radix-ui/react-compose-refs": "1.1.2"
},
"peerDependencies": {
"@types/react": "*",
"react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc"
},
"peerDependenciesMeta": {
"@types/react": {
"optional": true
}
}
},
"node_modules/@radix-ui/react-toggle-group/node_modules/@radix-ui/react-use-callback-ref": { "node_modules/@radix-ui/react-toggle-group/node_modules/@radix-ui/react-use-callback-ref": {
"version": "1.1.1", "version": "1.1.1",
"resolved": "https://registry.npmmirror.com/@radix-ui/react-use-callback-ref/-/react-use-callback-ref-1.1.1.tgz", "resolved": "https://registry.npmmirror.com/@radix-ui/react-use-callback-ref/-/react-use-callback-ref-1.1.1.tgz",
@ -5924,21 +5611,6 @@
"integrity": "sha512-XnbHrrprsNqZKQhStrSwgRUQzoCI1glLzdw79xiZPoofhGICeZRSQ3dIxAKH1gb3OHfNf4d6f+vAv3kil2eggA==", "integrity": "sha512-XnbHrrprsNqZKQhStrSwgRUQzoCI1glLzdw79xiZPoofhGICeZRSQ3dIxAKH1gb3OHfNf4d6f+vAv3kil2eggA==",
"license": "MIT" "license": "MIT"
}, },
"node_modules/@radix-ui/react-toggle/node_modules/@radix-ui/react-compose-refs": {
"version": "1.1.2",
"resolved": "https://registry.npmmirror.com/@radix-ui/react-compose-refs/-/react-compose-refs-1.1.2.tgz",
"integrity": "sha512-z4eqJvfiNnFMHIIvXP3CY57y2WJs5g2v3X0zm9mEJkrkNv4rDxu+sg9Jh8EkXyeqBkB7SOcboo9dMVqhyrACIg==",
"license": "MIT",
"peerDependencies": {
"@types/react": "*",
"react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc"
},
"peerDependenciesMeta": {
"@types/react": {
"optional": true
}
}
},
"node_modules/@radix-ui/react-toggle/node_modules/@radix-ui/react-primitive": { "node_modules/@radix-ui/react-toggle/node_modules/@radix-ui/react-primitive": {
"version": "2.1.3", "version": "2.1.3",
"resolved": "https://registry.npmmirror.com/@radix-ui/react-primitive/-/react-primitive-2.1.3.tgz", "resolved": "https://registry.npmmirror.com/@radix-ui/react-primitive/-/react-primitive-2.1.3.tgz",
@ -5962,24 +5634,6 @@
} }
} }
}, },
"node_modules/@radix-ui/react-toggle/node_modules/@radix-ui/react-slot": {
"version": "1.2.3",
"resolved": "https://registry.npmmirror.com/@radix-ui/react-slot/-/react-slot-1.2.3.tgz",
"integrity": "sha512-aeNmHnBxbi2St0au6VBVC7JXFlhLlOnvIIlePNniyUNAClzmtAUEY8/pBiK3iHjufOlwA+c20/8jngo7xcrg8A==",
"license": "MIT",
"dependencies": {
"@radix-ui/react-compose-refs": "1.1.2"
},
"peerDependencies": {
"@types/react": "*",
"react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc"
},
"peerDependenciesMeta": {
"@types/react": {
"optional": true
}
}
},
"node_modules/@radix-ui/react-toggle/node_modules/@radix-ui/react-use-controllable-state": { "node_modules/@radix-ui/react-toggle/node_modules/@radix-ui/react-use-controllable-state": {
"version": "1.2.2", "version": "1.2.2",
"resolved": "https://registry.npmmirror.com/@radix-ui/react-use-controllable-state/-/react-use-controllable-state-1.2.2.tgz", "resolved": "https://registry.npmmirror.com/@radix-ui/react-use-controllable-state/-/react-use-controllable-state-1.2.2.tgz",
@ -6255,24 +5909,6 @@
} }
} }
}, },
"node_modules/@radix-ui/react-tooltip/node_modules/@radix-ui/react-slot": {
"version": "1.2.3",
"resolved": "https://registry.npmmirror.com/@radix-ui/react-slot/-/react-slot-1.2.3.tgz",
"integrity": "sha512-aeNmHnBxbi2St0au6VBVC7JXFlhLlOnvIIlePNniyUNAClzmtAUEY8/pBiK3iHjufOlwA+c20/8jngo7xcrg8A==",
"license": "MIT",
"dependencies": {
"@radix-ui/react-compose-refs": "1.1.2"
},
"peerDependencies": {
"@types/react": "*",
"react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc"
},
"peerDependenciesMeta": {
"@types/react": {
"optional": true
}
}
},
"node_modules/@radix-ui/react-tooltip/node_modules/@radix-ui/react-use-callback-ref": { "node_modules/@radix-ui/react-tooltip/node_modules/@radix-ui/react-use-callback-ref": {
"version": "1.1.1", "version": "1.1.1",
"resolved": "https://registry.npmmirror.com/@radix-ui/react-use-callback-ref/-/react-use-callback-ref-1.1.1.tgz", "resolved": "https://registry.npmmirror.com/@radix-ui/react-use-callback-ref/-/react-use-callback-ref-1.1.1.tgz",
@ -7135,6 +6771,17 @@
"integrity": "sha512-jIBvWWShCvlBqBNIZt0KAshWpvSjhkwkEu4ZUcASoAvhmrgAUI2t1dXrjSL4xXVLB4FznPrIsX3nKXFl/Dt4vA==", "integrity": "sha512-jIBvWWShCvlBqBNIZt0KAshWpvSjhkwkEu4ZUcASoAvhmrgAUI2t1dXrjSL4xXVLB4FznPrIsX3nKXFl/Dt4vA==",
"license": "MIT" "license": "MIT"
}, },
"node_modules/@types/styled-components": {
"version": "5.1.34",
"resolved": "https://registry.npmmirror.com/@types/styled-components/-/styled-components-5.1.34.tgz",
"integrity": "sha512-mmiVvwpYklFIv9E8qfxuPyIt/OuyIrn6gMOAMOFUO3WJfSrSE+sGUoa4PiZj77Ut7bKZpaa6o1fBKS/4TOEvnA==",
"license": "MIT",
"dependencies": {
"@types/hoist-non-react-statics": "*",
"@types/react": "*",
"csstype": "^3.0.2"
}
},
"node_modules/@types/stylis": { "node_modules/@types/stylis": {
"version": "4.2.5", "version": "4.2.5",
"resolved": "https://registry.npmmirror.com/@types/stylis/-/stylis-4.2.5.tgz", "resolved": "https://registry.npmmirror.com/@types/stylis/-/stylis-4.2.5.tgz",
@ -8157,22 +7804,15 @@
} }
}, },
"node_modules/class-variance-authority": { "node_modules/class-variance-authority": {
"version": "0.7.0", "version": "0.7.1",
"resolved": "https://registry.npmjs.org/class-variance-authority/-/class-variance-authority-0.7.0.tgz", "resolved": "https://registry.npmmirror.com/class-variance-authority/-/class-variance-authority-0.7.1.tgz",
"integrity": "sha512-jFI8IQw4hczaL4ALINxqLEXQbWcNjoSkloa4IaufXCJr6QawJyw7tuRysRsrE8w2p/4gGaxKIt/hX3qz/IbD1A==", "integrity": "sha512-Ka+9Trutv7G8M6WT6SeiRWz792K5qEqIGEGzXKhAE6xOWAY6pPH8U+9IY3oCMv6kqTmLsv7Xh/2w2RigkePMsg==",
"license": "Apache-2.0",
"dependencies": { "dependencies": {
"clsx": "2.0.0" "clsx": "^2.1.1"
}, },
"funding": { "funding": {
"url": "https://joebell.co.uk" "url": "https://polar.sh/cva"
}
},
"node_modules/class-variance-authority/node_modules/clsx": {
"version": "2.0.0",
"resolved": "https://registry.npmjs.org/clsx/-/clsx-2.0.0.tgz",
"integrity": "sha512-rQ1+kcj+ttHG0MKVGBUXwayCCF1oh39BF5COIpRzuCEv8Mwjv0XucrI2ExNTOn9IlLifGClWQcU9BrZORvtw6Q==",
"engines": {
"node": ">=6"
} }
}, },
"node_modules/classnames": { "node_modules/classnames": {
@ -8236,8 +7876,9 @@
}, },
"node_modules/clsx": { "node_modules/clsx": {
"version": "2.1.1", "version": "2.1.1",
"resolved": "https://registry.npmjs.org/clsx/-/clsx-2.1.1.tgz", "resolved": "https://registry.npmmirror.com/clsx/-/clsx-2.1.1.tgz",
"integrity": "sha512-eYm0QWBtUrBWZWG0d386OGAw16Z995PiOVo2B7bjWSbHedGl5e0ZWaq65kOGgUSNesEIDkB9ISbTg/JK9dhCZA==", "integrity": "sha512-eYm0QWBtUrBWZWG0d386OGAw16Z995PiOVo2B7bjWSbHedGl5e0ZWaq65kOGgUSNesEIDkB9ISbTg/JK9dhCZA==",
"license": "MIT",
"engines": { "engines": {
"node": ">=6" "node": ">=6"
} }
@ -8296,24 +7937,6 @@
} }
} }
}, },
"node_modules/cmdk/node_modules/@radix-ui/react-slot": {
"version": "1.2.3",
"resolved": "https://registry.npmmirror.com/@radix-ui/react-slot/-/react-slot-1.2.3.tgz",
"integrity": "sha512-aeNmHnBxbi2St0au6VBVC7JXFlhLlOnvIIlePNniyUNAClzmtAUEY8/pBiK3iHjufOlwA+c20/8jngo7xcrg8A==",
"license": "MIT",
"dependencies": {
"@radix-ui/react-compose-refs": "1.1.2"
},
"peerDependencies": {
"@types/react": "*",
"react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc"
},
"peerDependenciesMeta": {
"@types/react": {
"optional": true
}
}
},
"node_modules/color-convert": { "node_modules/color-convert": {
"version": "2.0.1", "version": "2.0.1",
"resolved": "https://registry.npmjs.org/color-convert/-/color-convert-2.0.1.tgz", "resolved": "https://registry.npmjs.org/color-convert/-/color-convert-2.0.1.tgz",
@ -13555,9 +13178,10 @@
} }
}, },
"node_modules/tailwind-merge": { "node_modules/tailwind-merge": {
"version": "2.5.3", "version": "2.6.0",
"resolved": "https://registry.npmjs.org/tailwind-merge/-/tailwind-merge-2.5.3.tgz", "resolved": "https://registry.npmmirror.com/tailwind-merge/-/tailwind-merge-2.6.0.tgz",
"integrity": "sha512-d9ZolCAIzom1nf/5p4LdD5zvjmgSxY0BGgdSvmXIoMYAiPdAW/dSpP7joCDYFY7r/HkEa2qmPtkgsu0xjQeQtw==", "integrity": "sha512-P+Vu1qXfzediirmHOC3xKGAYeZtPcV9g76X+xg2FD4tYgR71ewMA35Y3sCz3zhiN/dwefRpJX0yBcgwi1fXNQA==",
"license": "MIT",
"funding": { "funding": {
"type": "github", "type": "github",
"url": "https://github.com/sponsors/dcastil" "url": "https://github.com/sponsors/dcastil"

View File

@ -35,7 +35,7 @@
"@radix-ui/react-select": "^2.2.5", "@radix-ui/react-select": "^2.2.5",
"@radix-ui/react-separator": "^1.1.0", "@radix-ui/react-separator": "^1.1.0",
"@radix-ui/react-slider": "^1.3.5", "@radix-ui/react-slider": "^1.3.5",
"@radix-ui/react-slot": "^1.1.0", "@radix-ui/react-slot": "^1.2.3",
"@radix-ui/react-switch": "^1.1.0", "@radix-ui/react-switch": "^1.1.0",
"@radix-ui/react-tabs": "^1.1.0", "@radix-ui/react-tabs": "^1.1.0",
"@radix-ui/react-toast": "^1.2.1", "@radix-ui/react-toast": "^1.2.1",
@ -49,13 +49,14 @@
"@types/react": "18.2.22", "@types/react": "18.2.22",
"@types/react-beautiful-dnd": "^13.1.8", "@types/react-beautiful-dnd": "^13.1.8",
"@types/react-dom": "18.2.7", "@types/react-dom": "18.2.7",
"@types/styled-components": "^5.1.34",
"@types/three": "^0.177.0", "@types/three": "^0.177.0",
"@types/wavesurfer.js": "^6.0.12", "@types/wavesurfer.js": "^6.0.12",
"antd": "^5.26.2", "antd": "^5.26.2",
"autoprefixer": "10.4.15", "autoprefixer": "10.4.15",
"axios": "^1.10.0", "axios": "^1.10.0",
"babel-loader": "^10.0.0", "babel-loader": "^10.0.0",
"class-variance-authority": "^0.7.0", "class-variance-authority": "^0.7.1",
"clsx": "^2.1.1", "clsx": "^2.1.1",
"cmdk": "^1.0.0", "cmdk": "^1.0.0",
"date-fns": "^3.6.0", "date-fns": "^3.6.0",
@ -87,7 +88,7 @@
"sonner": "^1.5.0", "sonner": "^1.5.0",
"styled-components": "^6.1.19", "styled-components": "^6.1.19",
"swiper": "^11.2.8", "swiper": "^11.2.8",
"tailwind-merge": "^2.5.2", "tailwind-merge": "^2.6.0",
"tailwindcss": "3.3.3", "tailwindcss": "3.3.3",
"tailwindcss-animate": "^1.0.7", "tailwindcss-animate": "^1.0.7",
"three": "^0.177.0", "three": "^0.177.0",

View File

@ -58,10 +58,25 @@ module.exports = {
from: { height: "var(--radix-accordion-content-height)" }, from: { height: "var(--radix-accordion-content-height)" },
to: { height: 0 }, to: { height: 0 },
}, },
"liquid-toggle": {
"0%": {
filter: "url(#toggle-glass) blur(2px)",
transform: "scale(1)",
},
"50%": {
filter: "url(#toggle-glass) blur(1px)",
transform: "scale(1.05)",
},
"100%": {
filter: "url(#toggle-glass) blur(2px)",
transform: "scale(1)",
}
}
}, },
animation: { animation: {
"accordion-down": "accordion-down 0.2s ease-out", "accordion-down": "accordion-down 0.2s ease-out",
"accordion-up": "accordion-up 0.2s ease-out", "accordion-up": "accordion-up 0.2s ease-out",
"liquid-toggle": "liquid-toggle 2s ease-in-out infinite",
}, },
transitionDelay: { transitionDelay: {
'100': '100ms', '100': '100ms',

51
test/movie.http Normal file
View File

@ -0,0 +1,51 @@
@host = https://77.smartvideo.py.qikongjian.com
### Create a movie project
POST http://localhost:8000/movie/create_movie_project
Content-Type: application/json
{
"script": "spider man saves the world from a virus.",
"user_id": "user123",
"mode": "auto",
"resolution": "720p"
}
### Get movie project name
POST http://localhost:8000/movie/get_movie_project_name
Content-Type: application/json
{
"project_id": "17cc4231-4bbe-45bc-97cf-0fcba05e3c09"
}
### Get movie project detail
POST https://77.smartvideo.py.qikongjian.com/movie/get_movie_project_detail
Content-Type: application/json
{
"project_id": "dbbc1f06-2459-4d3e-bbff-7b11ecf0f293"
}
### List movie projects
POST http://localhost:8000/movie/list_movie_projects
Content-Type: application/json
{
"user_id": "user123"
}
### Generate movie
POST http://localhost:8000/movie/generate_movie
Content-Type: application/json
{
"project_id": "21763816-f688-472c-87a4-eac7bfbfaac9"
}
### Get status
POST https://77.smartvideo.py.qikongjian.com/movie/get_status
Content-Type: application/json
{
"project_id": "dbbc1f06-2459-4d3e-bbff-7b11ecf0f293"
}