From dd0b78ce9d66065f886098cbceb1cc402e016138 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=E5=8C=97=E6=9E=B3?= <7854742+wang_rumeng@user.noreply.gitee.com> Date: Mon, 15 Sep 2025 10:44:39 +0800 Subject: [PATCH] =?UTF-8?q?=E5=89=A7=E6=9C=AC=E5=87=BA=E6=9D=A5=E6=95=B0?= =?UTF-8?q?=E6=8D=AE=E5=89=8D=E7=9A=84loading?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .env.development | 8 +- api/DTO/movieEdit.ts | 2 +- components/pages/work-flow.tsx | 52 ++++---- components/pages/work-flow/media-viewer.tsx | 38 ++---- components/pages/work-flow/script-loading.tsx | 126 ++++++++++++++++++ .../pages/work-flow/use-workflow-data.tsx | 3 +- 6 files changed, 171 insertions(+), 58 deletions(-) create mode 100644 components/pages/work-flow/script-loading.tsx diff --git a/.env.development b/.env.development index e9bd8c3..74f1a42 100644 --- a/.env.development +++ b/.env.development @@ -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 \ No newline at end of file diff --git a/api/DTO/movieEdit.ts b/api/DTO/movieEdit.ts index c7681f2..9a457f4 100644 --- a/api/DTO/movieEdit.ts +++ b/api/DTO/movieEdit.ts @@ -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; // 标题 diff --git a/components/pages/work-flow.tsx b/components/pages/work-flow.tsx index 793f632..29191b6 100644 --- a/components/pages/work-flow.tsx +++ b/components/pages/work-flow.tsx @@ -279,34 +279,30 @@ const WorkFlow = React.memo(function WorkFlow() { className="videoContainer-qteKNi" ref={containerRef} > - {isLoading ? ( - - ) : ( -
- setIsSmartChatBoxOpen(true)} - setVideoPreview={(url, id) => { - setPreviewVideoUrl(url); - setPreviewVideoId(id); - }} - showGotoCutButton={showGotoCutButton || editingStatus !== 'idle'} - onGotoCut={generateEditPlan} - isSmartChatBoxOpen={isSmartChatBoxOpen} - onRetryVideo={(video_id) => handleRetryVideo(video_id)} - /> -
- )} +
+ setIsSmartChatBoxOpen(true)} + setVideoPreview={(url, id) => { + setPreviewVideoUrl(url); + setPreviewVideoId(id); + }} + showGotoCutButton={showGotoCutButton || editingStatus !== 'idle'} + onGotoCut={generateEditPlan} + isSmartChatBoxOpen={isSmartChatBoxOpen} + onRetryVideo={(video_id) => handleRetryVideo(video_id)} + /> +
{taskObject.currentStage !== 'script' && (
diff --git a/components/pages/work-flow/media-viewer.tsx b/components/pages/work-flow/media-viewer.tsx index 6aa656f..ddda0f6 100644 --- a/components/pages/work-flow/media-viewer.tsx +++ b/components/pages/work-flow/media-viewer.tsx @@ -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 ( -
- { - scriptData ? ( - - ) : ( -
-
- - - -
-
- -
-
- ) - } +
+ {scriptData ? ( + + ) : ( + + )}
); }; diff --git a/components/pages/work-flow/script-loading.tsx b/components/pages/work-flow/script-loading.tsx new file mode 100644 index 0000000..0997c31 --- /dev/null +++ b/components/pages/work-flow/script-loading.tsx @@ -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 = ({ isCompleted = false, estimatedMs = 80000 }) => { + const [progress, setProgress] = useState(0); + const intervalRef = useRef(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 ( +
+
+ {/* subtle animated halo */} + {/*
+ +
+ +
+
*/} + + + + {stageMessage} + + + +
+
+ +
+
+ Generating Script... + {Math.round(progress)}% +
+
+
+
+ ); +}; + +export default ScriptLoading; + + diff --git a/components/pages/work-flow/use-workflow-data.tsx b/components/pages/work-flow/use-workflow-data.tsx index d743dd4..95c282c 100644 --- a/components/pages/work-flow/use-workflow-data.tsx +++ b/components/pages/work-flow/use-workflow-data.tsx @@ -44,7 +44,7 @@ export function useWorkflowData({ onEditPlanGenerated, editingStatus, onExportFa let tempTaskObject = useRef({ 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) {