forked from 77media/video-flow
剧本出来数据前的loading
This commit is contained in:
parent
89448ef0be
commit
dd0b78ce9d
@ -1,10 +1,10 @@
|
||||
|
||||
|
||||
# NEXT_PUBLIC_JAVA_URL = https://77.app.java.auth.qikongjian.com
|
||||
# NEXT_PUBLIC_BASE_URL = https://77.smartvideo.py.qikongjian.com
|
||||
NEXT_PUBLIC_JAVA_URL = https://77.app.java.auth.qikongjian.com
|
||||
NEXT_PUBLIC_BASE_URL = https://77.smartvideo.py.qikongjian.com
|
||||
# NEXT_PUBLIC_CUT_URL = https://smartcut.movieflow.ai
|
||||
NEXT_PUBLIC_JAVA_URL = https://auth.movieflow.ai
|
||||
NEXT_PUBLIC_BASE_URL = https://api.video.movieflow.ai
|
||||
# NEXT_PUBLIC_JAVA_URL = https://auth.movieflow.ai
|
||||
# NEXT_PUBLIC_BASE_URL = https://api.video.movieflow.ai
|
||||
NEXT_PUBLIC_CUT_URL = https://smartcut.movieflow.ai
|
||||
# 失败率
|
||||
NEXT_PUBLIC_ERROR_CONFIG = 0.1
|
||||
@ -675,7 +675,7 @@ export const LOADING_TEXT_MAP = {
|
||||
} as const;
|
||||
|
||||
export type Status = 'IN_PROGRESS' | 'COMPLETED' | 'FAILED';
|
||||
export type Stage = 'script' | 'character' | 'scene' | 'shot_sketch' | 'video' | 'final_video';
|
||||
export type Stage = 'init' | 'script' | 'character' | 'scene' | 'shot_sketch' | 'video' | 'final_video';
|
||||
// 添加 TaskObject 接口
|
||||
export interface TaskObject {
|
||||
title: string; // 标题
|
||||
|
||||
@ -279,34 +279,30 @@ const WorkFlow = React.memo(function WorkFlow() {
|
||||
className="videoContainer-qteKNi"
|
||||
ref={containerRef}
|
||||
>
|
||||
{isLoading ? (
|
||||
<Skeleton className="w-full aspect-video rounded-lg" />
|
||||
) : (
|
||||
<div className={`relative heroVideo-FIzuK1 ${['script'].includes(taskObject.currentStage) ? 'h-[calc(100vh-6rem)] w-[calc((100vh-6rem)/9*16)]' : 'h-[calc(100vh-6rem-200px)] w-[calc((100vh-6rem-200px)/9*16)]'}`} style={{ aspectRatio: "16 / 9" }} key={taskObject.currentStage+'_'+currentSketchIndex}>
|
||||
<MediaViewer
|
||||
taskObject={taskObject}
|
||||
scriptData={scriptData}
|
||||
currentSketchIndex={currentSketchIndex}
|
||||
isVideoPlaying={isVideoPlaying}
|
||||
onEditModalOpen={handleEditModalOpen}
|
||||
onToggleVideoPlay={toggleVideoPlay}
|
||||
setIsPauseWorkFlow={setIsPauseWorkFlow}
|
||||
setAnyAttribute={setAnyAttribute}
|
||||
isPauseWorkFlow={isPauseWorkFlow}
|
||||
applyScript={applyScript}
|
||||
mode={mode}
|
||||
onOpenChat={() => setIsSmartChatBoxOpen(true)}
|
||||
setVideoPreview={(url, id) => {
|
||||
setPreviewVideoUrl(url);
|
||||
setPreviewVideoId(id);
|
||||
}}
|
||||
showGotoCutButton={showGotoCutButton || editingStatus !== 'idle'}
|
||||
onGotoCut={generateEditPlan}
|
||||
isSmartChatBoxOpen={isSmartChatBoxOpen}
|
||||
onRetryVideo={(video_id) => handleRetryVideo(video_id)}
|
||||
/>
|
||||
</div>
|
||||
)}
|
||||
<div className={`relative heroVideo-FIzuK1 ${['script'].includes(taskObject.currentStage) ? 'h-[calc(100vh-6rem)] w-[calc((100vh-6rem)/9*16)]' : 'h-[calc(100vh-6rem-200px)] w-[calc((100vh-6rem-200px)/9*16)]'}`} style={{ aspectRatio: "16 / 9" }} key={taskObject.currentStage+'_'+currentSketchIndex}>
|
||||
<MediaViewer
|
||||
taskObject={taskObject}
|
||||
scriptData={scriptData}
|
||||
currentSketchIndex={currentSketchIndex}
|
||||
isVideoPlaying={isVideoPlaying}
|
||||
onEditModalOpen={handleEditModalOpen}
|
||||
onToggleVideoPlay={toggleVideoPlay}
|
||||
setIsPauseWorkFlow={setIsPauseWorkFlow}
|
||||
setAnyAttribute={setAnyAttribute}
|
||||
isPauseWorkFlow={isPauseWorkFlow}
|
||||
applyScript={applyScript}
|
||||
mode={mode}
|
||||
onOpenChat={() => setIsSmartChatBoxOpen(true)}
|
||||
setVideoPreview={(url, id) => {
|
||||
setPreviewVideoUrl(url);
|
||||
setPreviewVideoId(id);
|
||||
}}
|
||||
showGotoCutButton={showGotoCutButton || editingStatus !== 'idle'}
|
||||
onGotoCut={generateEditPlan}
|
||||
isSmartChatBoxOpen={isSmartChatBoxOpen}
|
||||
onRetryVideo={(video_id) => handleRetryVideo(video_id)}
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
{taskObject.currentStage !== 'script' && (
|
||||
<div className="h-[123px] w-[calc((100vh-6rem-200px)/9*16)]">
|
||||
|
||||
@ -8,6 +8,7 @@ import { GlassIconButton } from '@/components/ui/glass-icon-button';
|
||||
import { ScriptRenderer } from '@/components/script-renderer/ScriptRenderer';
|
||||
import { mockScriptData } from '@/components/script-renderer/mock';
|
||||
import { Skeleton } from '@/components/ui/skeleton';
|
||||
import ScriptLoading from './script-loading';
|
||||
import { TaskObject } from '@/api/DTO/movieEdit';
|
||||
import { Button, Tooltip } from 'antd';
|
||||
import { downloadVideo, downloadAllVideos, getFirstFrame } from '@/utils/tools';
|
||||
@ -675,30 +676,19 @@ export const MediaViewer = React.memo(function MediaViewer({
|
||||
// 渲染剧本
|
||||
const renderScriptContent = () => {
|
||||
return (
|
||||
<div className="relative w-full h-full bg-white/10 rounded-lg overflow-hidden p-2">
|
||||
{
|
||||
scriptData ? (
|
||||
<ScriptRenderer
|
||||
data={scriptData}
|
||||
setIsPauseWorkFlow={setIsPauseWorkFlow}
|
||||
setAnyAttribute={setAnyAttribute}
|
||||
isPauseWorkFlow={isPauseWorkFlow}
|
||||
applyScript={applyScript}
|
||||
mode={mode}
|
||||
/>
|
||||
) : (
|
||||
<div className="flex gap-2 w-full h-full">
|
||||
<div className="w-[70%] h-full rounded-lg gap-2 flex flex-col">
|
||||
<Skeleton className="w-full h-[33%] rounded-lg" />
|
||||
<Skeleton className="w-full h-[33%] rounded-lg" />
|
||||
<Skeleton className="w-full h-[33%] rounded-lg" />
|
||||
</div>
|
||||
<div className="w-[30%] h-full rounded-lg">
|
||||
<Skeleton className="w-full h-full rounded-lg" />
|
||||
</div>
|
||||
</div>
|
||||
)
|
||||
}
|
||||
<div className="relative w-full h-full rounded-lg overflow-hidden p-2">
|
||||
{scriptData ? (
|
||||
<ScriptRenderer
|
||||
data={scriptData}
|
||||
setIsPauseWorkFlow={setIsPauseWorkFlow}
|
||||
setAnyAttribute={setAnyAttribute}
|
||||
isPauseWorkFlow={isPauseWorkFlow}
|
||||
applyScript={applyScript}
|
||||
mode={mode}
|
||||
/>
|
||||
) : (
|
||||
<ScriptLoading isCompleted={!!scriptData} />
|
||||
)}
|
||||
</div>
|
||||
);
|
||||
};
|
||||
|
||||
126
components/pages/work-flow/script-loading.tsx
Normal file
126
components/pages/work-flow/script-loading.tsx
Normal file
@ -0,0 +1,126 @@
|
||||
'use client';
|
||||
|
||||
import React, { useEffect, useMemo, useRef, useState } from 'react';
|
||||
import { motion, AnimatePresence } from 'framer-motion';
|
||||
import { Loader2 } from 'lucide-react';
|
||||
|
||||
interface ScriptLoadingProps {
|
||||
/** When true, progress snaps to 100% */
|
||||
isCompleted?: boolean;
|
||||
/** Estimated total duration in ms to reach ~95% (default 80s) */
|
||||
estimatedMs?: number;
|
||||
}
|
||||
|
||||
/**
|
||||
* Dark-themed loading with spinner, staged copy and progress bar.
|
||||
* Progress linearly approaches 95% over estimatedMs, then snaps to 100% if isCompleted=true.
|
||||
*/
|
||||
export const ScriptLoading: React.FC<ScriptLoadingProps> = ({ isCompleted = false, estimatedMs = 80000 }) => {
|
||||
const [progress, setProgress] = useState<number>(0);
|
||||
const intervalRef = useRef<NodeJS.Timeout | null>(null);
|
||||
|
||||
const targetWhenPending = 95; // cap before data arrives
|
||||
const tickMs = 200; // update cadence
|
||||
|
||||
// staged messages by progress
|
||||
const stageMessage = useMemo(() => {
|
||||
if (progress >= 100) return 'Done';
|
||||
if (progress >= 95) return 'Almost done, please wait...';
|
||||
if (progress >= 82) return 'Generating script and prompt...';
|
||||
if (progress >= 63) return 'Polishing dialogue and transitions...';
|
||||
if (progress >= 45) return 'Arranging shots and rhythm...';
|
||||
if (progress >= 28) return 'Shaping characters and scene details...';
|
||||
if (progress >= 12) return 'Outlining the story...';
|
||||
return 'Waking up the creative engine...';
|
||||
}, [progress]);
|
||||
|
||||
// progress auto-increment
|
||||
useEffect(() => {
|
||||
const desiredTarget = isCompleted ? 100 : targetWhenPending;
|
||||
|
||||
// when completed, quickly animate to 100
|
||||
if (isCompleted) {
|
||||
setProgress((prev) => (prev < 100 ? Math.max(prev, 96) : 100));
|
||||
}
|
||||
|
||||
if (intervalRef.current) clearInterval(intervalRef.current);
|
||||
|
||||
// compute linear increment to reach target in remaining estimated time
|
||||
const totalTicks = Math.ceil(estimatedMs / tickMs);
|
||||
const baseIncrement = (targetWhenPending - 0) / Math.max(totalTicks, 1);
|
||||
|
||||
intervalRef.current = setInterval(() => {
|
||||
setProgress((prev) => {
|
||||
const target = isCompleted ? 100 : desiredTarget;
|
||||
if (prev >= target) return prev;
|
||||
const remaining = target - prev;
|
||||
const step = Math.max(Math.min(baseIncrement, remaining * 0.25), 0.2);
|
||||
const next = Math.min(prev + step, target);
|
||||
return Number(next.toFixed(2));
|
||||
});
|
||||
}, tickMs);
|
||||
|
||||
return () => {
|
||||
if (intervalRef.current) clearInterval(intervalRef.current);
|
||||
};
|
||||
}, [isCompleted, estimatedMs]);
|
||||
|
||||
const widthStyle = { width: `${Math.min(progress, 100)}%` };
|
||||
|
||||
return (
|
||||
<div data-alt="script-loading-container" className="flex w-full h-full items-center justify-center">
|
||||
<div data-alt="loading-stack" className="flex w-full h-full flex-col items-center justify-center gap-6 px-4">
|
||||
{/* subtle animated halo */}
|
||||
{/* <div data-alt="spinner-wrapper" className="relative">
|
||||
<motion.div
|
||||
className="absolute inset-0 rounded-full blur-2xl"
|
||||
initial={{ opacity: 0 }}
|
||||
animate={{ opacity: 1 }}
|
||||
transition={{ duration: 0.8 }}
|
||||
style={{
|
||||
background: 'conic-gradient(from 0deg, rgba(139,92,246,0.25), rgba(34,211,238,0.25), rgba(139,92,246,0.25))',
|
||||
width: 120,
|
||||
height: 120
|
||||
}}
|
||||
/>
|
||||
<div className="relative flex items-center justify-center">
|
||||
<Loader2 className="h-10 w-10 text-cyan-300 animate-spin" />
|
||||
</div>
|
||||
</div> */}
|
||||
|
||||
<AnimatePresence mode="wait">
|
||||
<motion.div
|
||||
key={stageMessage}
|
||||
data-alt="loading-message"
|
||||
initial={{ opacity: 0, y: 6 }}
|
||||
animate={{ opacity: 1, y: 0 }}
|
||||
exit={{ opacity: 0, y: -6 }}
|
||||
transition={{ duration: 0.25 }}
|
||||
className="text-center text-base md:text-lg text-white/90"
|
||||
>
|
||||
{stageMessage}
|
||||
</motion.div>
|
||||
</AnimatePresence>
|
||||
|
||||
<div data-alt="progress-wrapper" className="w-full max-w-xl">
|
||||
<div className="h-2 w-full rounded-full bg-white/10 overflow-hidden">
|
||||
<motion.div
|
||||
data-alt="progress-bar"
|
||||
className="h-full rounded-full bg-gradient-to-r from-indigo-400 via-cyan-300 to-indigo-400"
|
||||
animate={widthStyle}
|
||||
transition={{ type: 'tween', ease: 'easeOut', duration: 0.25 }}
|
||||
/>
|
||||
</div>
|
||||
<div className="mt-2 flex items-center justify-between text-xs text-white/60">
|
||||
<span data-alt="progress-label">Generating Script...</span>
|
||||
<span data-alt="progress-percent">{Math.round(progress)}%</span>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
};
|
||||
|
||||
export default ScriptLoading;
|
||||
|
||||
|
||||
@ -44,7 +44,7 @@ export function useWorkflowData({ onEditPlanGenerated, editingStatus, onExportFa
|
||||
let tempTaskObject = useRef<TaskObject>({
|
||||
title: '',
|
||||
tags: [],
|
||||
currentStage: 'script' as Stage,
|
||||
currentStage: 'init' as Stage,
|
||||
status: 'IN_PROGRESS' as Status,
|
||||
roles: {
|
||||
data: [],
|
||||
@ -550,6 +550,7 @@ export function useWorkflowData({ onEditPlanGenerated, editingStatus, onExportFa
|
||||
taskCurrent.title = name || 'generating...';
|
||||
taskCurrent.tags = tags || [];
|
||||
taskCurrent.status = status as Status;
|
||||
taskCurrent.currentStage = 'script';
|
||||
|
||||
// 设置标题
|
||||
if (!name) {
|
||||
|
||||
Loading…
x
Reference in New Issue
Block a user