H5 workflwo 页面暂存

This commit is contained in:
北枳 2025-09-18 19:29:05 +08:00
parent 5b7dc2fe47
commit 5efe68b63b
4 changed files with 555 additions and 43 deletions

View File

@ -13,7 +13,7 @@
}
/* 大屏幕适配 */
@media (width >= 1024px) {
@media (width >=1024px) {
.container-H2sRZG {
width: 85%;
}
@ -28,19 +28,19 @@
height: calc(100vh - 80px);
min-height: 500px;
}
.splashContainer-otuV_A {
gap: 8px;
}
.media-Ocdu1O {
gap: 8px;
}
.videoContainer-qteKNi {
min-height: 200px;
}
.imageGrid-ymZV9z {
flex-shrink: 0;
height: 110px;
@ -48,17 +48,17 @@
display: flex;
overflow-x: auto;
}
.title-JtMejk {
font-size: 1.1rem;
line-height: 28px;
}
.subtitle-had8uE {
font-size: 13px;
line-height: 18px;
}
.info-UUGkPJ {
gap: 4px;
}
@ -71,34 +71,35 @@
height: calc(100vh - 60px);
min-height: 450px;
}
.splashContainer-otuV_A {
gap: 5px;
}
.videoContainer-qteKNi {
min-height: 160px;
}
.title-JtMejk {
font-size: 1rem;
line-height: 24px;
}
.subtitle-had8uE {
font-size: 12px;
line-height: 16px;
}
.imageGrid-ymZV9z {
height: 90px;
gap: 6px;
}
.info-UUGkPJ {
gap: 3px;
}
}
/* 默认小屏幕布局 */
.splashContainer-otuV_A {
box-sizing: border-box;
@ -110,13 +111,13 @@
}
/* 大屏幕布局 */
@media (width >= 1024px) {
@media (width >=1024px) {
.splashContainer-otuV_A {
box-sizing: border-box;
grid-template-rows: auto 1fr;
gap: 10px;
height: 100%;
display: grid;
box-sizing: border-box;
grid-template-rows: auto 1fr;
gap: 10px;
height: 100%;
display: grid;
}
}
.content-vPGYx8 {
@ -128,8 +129,7 @@
box-sizing: border-box;
flex-direction: column;
gap: 5px;
display: flex;
;
display: flex;;
}
.title-JtMejk {
box-sizing: border-box;
@ -152,8 +152,10 @@
}
.media-Ocdu1O {
position: relative;
flex-direction: column;
gap: 8px;
width: 100%;
height: 100%;
flex: 1;
display: flex;
@ -163,6 +165,7 @@
display: grid;
grid-auto-rows: auto;
justify-content: center;
align-items: center;
}
.videoContainer-qteKNi {
flex: 1;
@ -192,14 +195,15 @@
gap: 8px;
min-width: 0;
padding: 0;
display: flex
;
display: flex;
overflow: hidden;
}
.secondary-_HxO1W {
color: #fff;
background: #1d1e23;
}
.large-_aHMgD {
letter-spacing: .01em;
border-radius: 8px;
@ -209,6 +213,7 @@
font-weight: 600;
line-height: 24px;
}
.videoPlaybackButton-uFNO1b {
-webkit-backdrop-filter: blur(10px);
backdrop-filter: blur(10px);
@ -220,6 +225,7 @@
bottom: 12px;
right: 12px;
}
.imageGrid-ymZV9z {
flex-shrink: 0;
height: 140px;
@ -228,17 +234,18 @@
overflow-x: auto;
}
@media (height >= 880px) {
@media (height >=880px) {
.imageGrid-ymZV9z {
/* flex: 1; */
height: auto;
min-height: 0;
display: grid;
grid-auto-flow: column;
grid-auto-columns: minmax(25%, 1fr);
overflow-x: unset;
/* flex: 1; */
height: auto;
min-height: 0;
display: grid;
grid-auto-flow: column;
grid-auto-columns: minmax(25%, 1fr);
overflow-x: unset;
}
}
.image-x5Y2Sg {
object-fit: cover;
object-position: center;

View File

@ -4,6 +4,8 @@ import "./style/work-flow.css";
import { EditModal } from "@/components/ui/edit-modal";
import { TaskInfo } from "./work-flow/task-info";
import H5TaskInfo from "./work-flow/H5TaskInfo";
import H5MediaViewer from "./work-flow/H5MediaViewer";
import { MediaViewer } from "./work-flow/media-viewer";
import { ThumbnailGrid } from "./work-flow/thumbnail-grid";
import { useWorkflowData } from "./work-flow/use-workflow-data";
@ -19,8 +21,11 @@ import { showEditingNotification } from "@/components/pages/work-flow/editing-no
import { exportVideoWithRetry } from '@/utils/export-service';
// 临时禁用视频编辑功能
// import { EditPoint as EditPointType } from './work-flow/video-edit/types';
import { AIEditingIframeButton } from './work-flow/ai-editing-iframe';
import { useDeviceType } from '@/hooks/useDeviceType';
const WorkFlow = React.memo(function WorkFlow() {
const { isMobile, isTablet, isDesktop } = useDeviceType();
useEffect(() => {
console.log("init-WorkFlow");
return () => {
@ -377,12 +382,19 @@ Please process this video editing request.`;
*/
return (
<div className="w-full overflow-hidden h-full px-[1rem] pb-[1rem]">
<div className={`w-full overflow-hidden h-full ${isDesktop ? 'px-[1rem] pb-[1rem]' : ''}`}>
<div className="w-full h-full">
<div className="splashContainer-otuV_A">
<div className="content-vPGYx8">
<div className="info-UUGkPJ">
<TaskInfo
{isMobile || isTablet ? (
<H5TaskInfo
title={taskObject.title}
current={currentSketchIndex + 1}
taskObject={taskObject}
/>
) : (
<TaskInfo
taskObject={taskObject}
currentLoadingText={currentLoadingText}
roles={taskObject.roles.data}
@ -390,27 +402,50 @@ Please process this video editing request.`;
showGotoCutButton={showGotoCutButton || editingStatus !== 'idle'}
onGotoCut={generateEditPlan}
setIsPauseWorkFlow={setIsPauseWorkFlow}
/>
/>
)}
</div>
</div>
<div className="media-Ocdu1O rounded-lg">
<div
className="videoContainer-qteKNi"
className={`videoContainer-qteKNi ${!isDesktop ? '!w-full' : ''}`}
ref={containerRef}
>
<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
{isDesktop ? (
<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>
) : (
<H5MediaViewer
taskObject={taskObject}
scriptData={scriptData}
currentSketchIndex={currentSketchIndex}
isVideoPlaying={isVideoPlaying}
onEditModalOpen={handleEditModalOpen}
onToggleVideoPlay={toggleVideoPlay}
mode={mode}
setIsPauseWorkFlow={setIsPauseWorkFlow}
setAnyAttribute={setAnyAttribute}
isPauseWorkFlow={isPauseWorkFlow}
applyScript={applyScript}
mode={mode}
onOpenChat={() => setIsSmartChatBoxOpen(true)}
setVideoPreview={(url, id) => {
setPreviewVideoUrl(url);
@ -422,7 +457,7 @@ Please process this video editing request.`;
onRetryVideo={(video_id) => handleRetryVideo(video_id)}
// 临时禁用视频编辑功能: enableVideoEdit={true} onVideoEditDescriptionSubmit={handleVideoEditDescriptionSubmit}
/>
</div>
)}
</div>
{taskObject.currentStage !== 'script' && (
<div className="h-[123px] w-[calc((100vh-6rem-200px)/9*16)]">

View File

@ -0,0 +1,312 @@
'use client';
import React, { useEffect, useMemo, useRef, useState } from 'react';
import { Carousel } from 'antd';
import type { CarouselRef } from 'antd/es/carousel';
import { Play, Scissors, MessageCircleMore, RotateCcw } from 'lucide-react';
import { TaskObject } from '@/api/DTO/movieEdit';
import { ScriptRenderer } from '@/components/script-renderer/ScriptRenderer';
import ScriptLoading from './script-loading';
import { getFirstFrame } from '@/utils/tools';
interface H5MediaViewerProps {
/** 任务对象,包含各阶段数据 */
taskObject: TaskObject;
/** 剧本数据(仅剧本阶段使用) */
scriptData: any;
/** 当前索引(视频/分镜阶段用于定位) */
currentSketchIndex: number;
/** 渲染模式(仅剧本阶段透传) */
mode: string;
/** 以下为剧本阶段透传必需项(与桌面版保持一致) */
setIsPauseWorkFlow: (isPause: boolean) => void;
setAnyAttribute: any;
isPauseWorkFlow: boolean;
applyScript: any;
/** 打开智能对话 */
onOpenChat?: () => void;
/** 设置聊天预览视频 */
setVideoPreview?: (url: string, id: string) => void;
/** 显示跳转至剪辑平台按钮 */
showGotoCutButton?: boolean;
/** 跳转至剪辑平台 */
onGotoCut?: () => void;
/** 智能对话是否打开H5可忽略布局调整仅占位 */
isSmartChatBoxOpen?: boolean;
/** 失败重试生成视频 */
onRetryVideo?: (video_id: string) => void;
}
/**
* H5
* - 使 antd Carousel /
* - /
*/
export function H5MediaViewer({
taskObject,
scriptData,
currentSketchIndex,
mode,
setIsPauseWorkFlow,
setAnyAttribute,
isPauseWorkFlow,
applyScript,
onOpenChat,
setVideoPreview,
showGotoCutButton,
onGotoCut,
isSmartChatBoxOpen,
onRetryVideo
}: H5MediaViewerProps) {
const carouselRef = useRef<CarouselRef>(null);
const videoRefs = useRef<Array<HTMLVideoElement | null>>([]);
const rootRef = useRef<HTMLDivElement | null>(null);
const [activeIndex, setActiveIndex] = useState<number>(0);
const [isPlaying, setIsPlaying] = useState<boolean>(false);
// 计算当前阶段类型
const stage = taskObject.currentStage;
// 生成各阶段对应的 slides 数据
const videoUrls = useMemo(() => {
if (stage === 'final_video') {
return taskObject.final?.url ? [taskObject.final.url] : [];
}
if (stage === 'video') {
// 注意:不再过滤,保持与原始数组长度一致,避免索引错位
const list = (taskObject.videos?.data ?? []) as Array<any>;
return list.map(v => (Array.isArray(v?.urls) && v.urls.length > 0 ? v.urls[0] : '')) as string[];
}
return [];
}, [stage, taskObject.final?.url, taskObject.videos?.data]);
const imageUrls = useMemo(() => {
if (stage === 'scene' || stage === 'character') {
const roles = (taskObject.roles?.data ?? []) as Array<any>;
const scenes = (taskObject.scenes?.data ?? []) as Array<any>;
return [...roles, ...scenes].map(item => item?.url).filter(Boolean) as string[];
}
return [];
}, [stage, taskObject.roles?.data, taskObject.scenes?.data]);
// 占位,避免未使用警告
useEffect(() => {
void isSmartChatBoxOpen;
}, [isSmartChatBoxOpen]);
// 同步外部索引到 Carousel
useEffect(() => {
if (stage === 'video' || stage === 'scene' || stage === 'character') {
const target = Math.max(0, currentSketchIndex);
carouselRef.current?.goTo(target, false);
setActiveIndex(target);
setIsPlaying(false);
// 切换时暂停全部视频
videoRefs.current.forEach(v => v?.pause());
}
}, [currentSketchIndex, stage]);
// 阶段变更时重置状态
useEffect(() => {
setActiveIndex(0);
setIsPlaying(false);
videoRefs.current.forEach(v => v?.pause());
}, [stage]);
const handleAfterChange = (index: number) => {
setActiveIndex(index);
setIsPlaying(false);
videoRefs.current.forEach(v => v?.pause());
};
const togglePlay = () => {
if (stage !== 'final_video' && stage !== 'video') return;
const currentVideo = videoRefs.current[activeIndex] ?? null;
if (!currentVideo) return;
if (currentVideo.paused) {
currentVideo.play().then(() => setIsPlaying(true)).catch(() => setIsPlaying(false));
} else {
currentVideo.pause();
setIsPlaying(false);
}
};
// 渲染视频 slide
const renderVideoSlides = () => (
<div data-alt="carousel-wrapper" className="relative w-full aspect-video min-h-[200px] overflow-hidden rounded-lg">
<Carousel
ref={carouselRef}
key={`h5-carousel-video-${stage}-${videoUrls.length}`}
arrows
dots={false}
infinite={false}
afterChange={handleAfterChange}
className="absolute inset-0"
slidesToShow={1}
adaptiveHeight
>
{videoUrls.map((url, idx) => {
const hasUrl = typeof url === 'string' && url.length > 0;
const raw = (taskObject.videos?.data ?? [])[idx] as any;
const status = raw?.video_status;
const videoId = raw?.video_id as string | undefined;
return (
<div key={`h5-video-${idx}`} data-alt="video-slide" className="relative w-full h-full">
{hasUrl ? (
<>
<video
ref={(el) => (videoRefs.current[idx] = el)}
className="w-full h-full object-cover [transform:translateZ(0)] [backface-visibility:hidden] [will-change:transform] bg-black"
src={url}
preload="metadata"
playsInline
controls={isPlaying && activeIndex === idx}
loop
poster={getFirstFrame(url)}
crossOrigin="anonymous"
onLoadedMetadata={() => {}}
onPlay={() => {
if (activeIndex === idx) setIsPlaying(true);
}}
onPause={() => {
if (activeIndex === idx) setIsPlaying(false);
}}
onCanPlay={() => {}}
onError={() => {}}
/>
{activeIndex === idx && !isPlaying && (
<button
type="button"
onClick={togglePlay}
data-alt="play-toggle"
className="absolute left-1/2 top-1/2 -translate-x-1/2 -translate-y-1/2 w-16 h-16 rounded-full bg-black/60 text-white flex items-center justify-center active:scale-95"
aria-label={'Play'}
>
<Play className="w-8 h-8" />
</button>
)}
{/* 顶部操作按钮 */}
<div data-alt="video-actions" className="absolute top-2 right-2 z-10 flex items-center gap-2">
{showGotoCutButton && (
<button
type="button"
onClick={onGotoCut}
data-alt="goto-cut-button"
className="w-9 h-9 rounded-full bg-black/50 text-white flex items-center justify-center active:scale-95"
aria-label={'Go to editing platform'}
>
<Scissors className="w-5 h-5" />
</button>
)}
{!!setVideoPreview && !!onOpenChat && hasUrl && (
<button
type="button"
onClick={() => {
setVideoPreview(url, videoId || '');
onOpenChat();
}}
data-alt="open-chat-button"
className="w-9 h-9 rounded-full bg-black/50 text-white flex items-center justify-center active:scale-95"
aria-label={'Open chat for editing'}
>
<MessageCircleMore className="w-5 h-5" />
</button>
)}
</div>
</>
) : (
<div className="w-full aspect-video min-h-[200px] flex items-center justify-center bg-black/10" data-alt="video-status">
{status === 0 && (
<span className="text-blue-500 text-base">Generating...</span>
)}
{status === 2 && (
<div className="flex flex-col items-center justify-center gap-3">
<div className="text-4xl"></div>
<span className="text-red-500 text-base">Generate failed</span>
{onRetryVideo && videoId && (
<button
type="button"
onClick={() => onRetryVideo(videoId)}
data-alt="retry-button"
className="px-3 py-1.5 rounded-full bg-black/60 text-white text-sm inline-flex items-center gap-1 active:scale-95"
aria-label={'Retry'}
>
<RotateCcw className="w-4 h-4" />
Retry
</button>
)}
</div>
)}
{status !== 0 && status !== 2 && (
<span className="text-white/70 text-base">Pending</span>
)}
</div>
)}
</div>
);
})}
</Carousel>
</div>
);
// 渲染图片 slide
const renderImageSlides = () => (
<div data-alt="carousel-wrapper" className="relative w-full aspect-video min-h-[200px] overflow-hidden rounded-lg">
<Carousel
ref={carouselRef}
key={`h5-carousel-image-${stage}-${imageUrls.length}`}
arrows
dots={false}
infinite={false}
afterChange={handleAfterChange}
className="absolute inset-0"
slidesToShow={1}
adaptiveHeight
>
{imageUrls.map((url, idx) => (
<div key={`h5-image-${idx}`} data-alt="image-slide" className="relative w-full h-full">
<img src={url} alt="scene" className="w-full h-full object-cover" />
</div>
))}
</Carousel>
</div>
);
// 剧本阶段:不使用 Carousel沿用 ScriptRenderer
if (stage === 'script') {
return (
<div data-alt="script-content" className="w-full h-full">
{scriptData ? (
<ScriptRenderer
data={scriptData}
setIsPauseWorkFlow={setIsPauseWorkFlow}
setAnyAttribute={setAnyAttribute}
isPauseWorkFlow={isPauseWorkFlow}
applyScript={applyScript}
mode={mode}
/>
) : (
<ScriptLoading isCompleted={!!scriptData} />
)}
</div>
);
}
// 其他阶段:使用 Carousel
return (
<div ref={rootRef} data-alt="h5-media-viewer" className="w-[100vw] relative px-4">
{stage === 'final_video' && videoUrls.length > 0 && renderVideoSlides()}
{stage === 'video' && videoUrls.length > 0 && renderVideoSlides()}
{(stage === 'scene' || stage === 'character') && imageUrls.length > 0 && renderImageSlides()}
<style jsx global>{`
[data-alt='carousel-wrapper'] .slick-slide { display: block; }
.slick-list { width: 100%;height: 100%; }
`}</style>
</div>
);
}
export default H5MediaViewer;

View File

@ -0,0 +1,158 @@
'use client'
import React, { useMemo } from 'react'
import { TaskObject } from '@/api/DTO/movieEdit'
import { GlassIconButton } from '@/components/ui/glass-icon-button'
import { Pencil, RotateCcw, Download, ArrowDownWideNarrow, Scissors, Maximize, Minimize, MessageCircleMore } from 'lucide-react'
interface H5TaskInfoProps {
/** 标题文案 */
title: string
/** 当前分镜序号从1开始 */
current: number
/** 任务对象(用于读取总数等信息) */
taskObject: TaskObject
/** video聊天编辑 */
onEditWithChat?: () => void
/** 下载:全部分镜 */
onDownloadAll?: () => void
/** 下载:最终成片 */
onDownloadFinal?: () => void
/** 下载:当前分镜视频 */
onDownloadCurrent?: () => void
/** video失败时显示重试 */
showRetry?: boolean
onRetry?: () => void
className?: string
}
const H5TaskInfo: React.FC<H5TaskInfoProps> = ({
title,
current,
taskObject,
onEditWithChat,
onDownloadAll,
onDownloadFinal,
onDownloadCurrent,
showRetry,
onRetry,
className
}) => {
const total = useMemo(() => {
if (taskObject.currentStage === 'video' || taskObject.currentStage === 'final_video') {
return taskObject.videos?.total_count || taskObject.videos?.data?.length || 0
}
if (taskObject.currentStage === 'scene' || taskObject.currentStage === 'character') {
const rolesTotal = taskObject.roles?.total_count || taskObject.roles?.data?.length || 0
const scenesTotal = taskObject.scenes?.total_count || taskObject.scenes?.data?.length || 0
return rolesTotal + scenesTotal
}
return 0
}, [taskObject])
const shouldShowCount = taskObject.currentStage !== 'script'
const displayCurrent = useMemo(() => {
if (taskObject.currentStage === 'video' || taskObject.currentStage === 'final_video') {
return Math.max(current, 1)
}
if (taskObject.currentStage === 'scene' || taskObject.currentStage === 'character') {
const bounded = Math.min(Math.max(current, 1), Math.max(total, 1))
return bounded
}
return 0
}, [taskObject, current, total])
return (
<div
data-alt="h5-header"
className={`absolute top-0 left-0 right-0 z-[50] pr-2 ${className || ''}`}
>
<div
data-alt="h5-header-bar"
className="flex items-start justify-between"
>
<div data-alt="title-area" className="flex flex-col min-w-0 bg-gradient-to-b from-slate-900/80 via-slate-900/40 to-transparent backdrop-blur-sm rounded-lg p-4">
<h1
data-alt="title"
className="text-white text-lg font-bold"
title={title}
>
{title || '...'}
</h1>
{shouldShowCount && (
<span data-alt="shot-count" className="flex items-center gap-4 text-sm text-slate-300">
{displayCurrent}/{Math.max(total, 1)}
</span>
)}
</div>
<div data-alt="actions" className="flex flex-col items-center gap-2">
{taskObject.currentStage === 'final_video' && (
<div data-alt="final-video-actions" className="flex flex-col items-center gap-2">
<GlassIconButton
data-alt="download-all-button"
className="w-10 h-10 bg-gradient-to-br from-purple-500/80 to-purple-600/80 backdrop-blur-xl border border-purple-400/30 rounded-full flex items-center justify-center hover:from-purple-400/80 hover:to-purple-500/80 transition-all"
icon={ArrowDownWideNarrow}
size="sm"
aria-label="download-all"
onClick={onDownloadAll}
/>
<GlassIconButton
data-alt="download-final-button"
className="w-10 h-10 bg-gradient-to-br from-amber-500/80 to-yellow-600/80 backdrop-blur-xl border border-amber-400/30 rounded-full flex items-center justify-center hover:from-amber-400/80 hover:to-yellow-500/80 transition-all"
icon={Download}
size="sm"
aria-label="download-final"
onClick={onDownloadFinal}
/>
</div>
)}
{taskObject.currentStage === 'video' && (
<div data-alt="video-actions" className="flex flex-col items-center gap-2">
<GlassIconButton
data-alt="edit-with-chat-button"
className="w-10 h-10 bg-gradient-to-br from-blue-500/80 to-blue-600/80 backdrop-blur-xl border border-blue-400/30 rounded-full flex items-center justify-center hover:from-blue-400/80 hover:to-blue-500/80 transition-all"
icon={MessageCircleMore}
size="sm"
aria-label="edit-with-chat"
onClick={onEditWithChat}
/>
<GlassIconButton
data-alt="download-all-button"
className="w-10 h-10 bg-gradient-to-br from-purple-500/80 to-purple-600/80 backdrop-blur-xl border border-purple-400/30 rounded-full flex items-center justify-center hover:from-purple-400/80 hover:to-purple-500/80 transition-all"
icon={ArrowDownWideNarrow}
size="sm"
aria-label="download-all"
onClick={onDownloadAll}
/>
<GlassIconButton
data-alt="download-current-button"
className="w-10 h-10 bg-gradient-to-br from-amber-500/80 to-yellow-600/80 backdrop-blur-xl border border-amber-400/30 rounded-full flex items-center justify-center hover:from-amber-400/80 hover:to-yellow-500/80 transition-all"
icon={Download}
size="sm"
aria-label="download-current"
onClick={onDownloadCurrent}
/>
{showRetry && (
<GlassIconButton
data-alt="retry-button"
className="w-10 h-10 bg-gradient-to-br from-purple-500/80 to-purple-600/80 backdrop-blur-xl border border-purple-400/30 rounded-full flex items-center justify-center hover:from-purple-400/80 hover:to-purple-500/80 transition-all"
icon={RotateCcw}
size="sm"
aria-label="retry"
onClick={onRetry}
/>
)}
</div>
)}
</div>
</div>
</div>
)
}
export default H5TaskInfo