feat: clarify ad recreation workflow steps
This commit is contained in:
@@ -112,6 +112,16 @@ type SubjectPlanningRef = ImageRef & { view: string; roleHint: string }
|
||||
type SubjectStyleMode = "transparent_human" | "source_actor"
|
||||
type StoryboardVisualMode = NonNullable<StoryboardScene["visual_mode"]>
|
||||
type RowPlanPatch = Partial<Pick<AudioStoryboardRow, "visualMode" | "needsProduct" | "needsSubject" | "subjectDescription" | "visualPlan" | "firstFramePlan" | "lastFramePlan" | "productIntegration" | "productPlacement">>
|
||||
type WorkflowStepId = "input" | "source" | "audio" | "visual" | "subject" | "product" | "script" | "scene" | "video"
|
||||
type WorkflowStepStatus = "blocked" | "pending" | "running" | "ready" | "paused"
|
||||
type WorkflowStep = {
|
||||
id: WorkflowStepId
|
||||
no: string
|
||||
title: string
|
||||
detail: string
|
||||
judge: string
|
||||
status: WorkflowStepStatus
|
||||
}
|
||||
|
||||
const VISUAL_MODE_OPTIONS: Array<{ value: StoryboardVisualMode; label: string; description: string }> = [
|
||||
{ value: "person_only", label: "人物/情绪", description: "只拍人物、状态、痛点或口播,不强制露产品。" },
|
||||
@@ -297,6 +307,131 @@ function countSubjectAssetViews(job: Job | null) {
|
||||
0)
|
||||
}
|
||||
|
||||
function countEndpointFramePairs(job: Job | null) {
|
||||
if (!job) return 0
|
||||
return job.frames.filter((frame) => endpointAssetRef(frame, "first_frame") && endpointAssetRef(frame, "last_frame")).length
|
||||
}
|
||||
|
||||
function stepStatus({ ready, running, blocked, paused }: { ready?: boolean; running?: boolean; blocked?: boolean; paused?: boolean }): WorkflowStepStatus {
|
||||
if (running) return "running"
|
||||
if (ready) return "ready"
|
||||
if (paused) return "paused"
|
||||
if (blocked) return "blocked"
|
||||
return "pending"
|
||||
}
|
||||
|
||||
function buildWorkflowSteps({
|
||||
job,
|
||||
submitting,
|
||||
audioReady,
|
||||
audioRunning,
|
||||
transcriptCount,
|
||||
visualReady,
|
||||
visualRunning,
|
||||
subjectAssetCount,
|
||||
productAssetCount,
|
||||
endpointFramePairCount,
|
||||
generatedVideoCount,
|
||||
}: {
|
||||
job: Job | null
|
||||
submitting: boolean
|
||||
audioReady: boolean
|
||||
audioRunning: boolean
|
||||
transcriptCount: number
|
||||
visualReady: boolean
|
||||
visualRunning: boolean
|
||||
subjectAssetCount: number
|
||||
productAssetCount: number
|
||||
endpointFramePairCount: number
|
||||
generatedVideoCount: number
|
||||
}): WorkflowStep[] {
|
||||
const hasSourceVideo = !!job?.video_url
|
||||
const downloading = !!job && ["created", "downloading"].includes(job.status)
|
||||
const storyboardReady = transcriptCount > 0
|
||||
const endpointTargetCount = Math.max(transcriptCount, 0)
|
||||
return [
|
||||
{
|
||||
id: "input",
|
||||
no: "01",
|
||||
title: "素材输入",
|
||||
detail: job ? `当前 ${shortId(job.id)}` : "待链接/上传",
|
||||
judge: "有当前素材任务即通过;输入框只负责创建或切换任务。",
|
||||
status: stepStatus({ ready: !!job, running: submitting }),
|
||||
},
|
||||
{
|
||||
id: "source",
|
||||
no: "02",
|
||||
title: "源视频下载",
|
||||
detail: hasSourceVideo ? "源视频已就绪" : downloading ? "下载中" : "待下载",
|
||||
judge: "job.video_url 存在即通过;created/downloading 视为运行中。",
|
||||
status: stepStatus({ ready: hasSourceVideo, running: downloading, blocked: !job }),
|
||||
},
|
||||
{
|
||||
id: "audio",
|
||||
no: "03",
|
||||
title: "音频文案",
|
||||
detail: audioReady ? `${transcriptCount} 段字幕` : "待转写/分析",
|
||||
judge: "audio_script.source_text 有内容,或 transcript 逐句时间轴有内容即通过。",
|
||||
status: stepStatus({ ready: audioReady, running: audioRunning, blocked: !hasSourceVideo }),
|
||||
},
|
||||
{
|
||||
id: "visual",
|
||||
no: "04",
|
||||
title: "抽帧参考",
|
||||
detail: visualReady ? `${job?.frames.length ?? 0} 张参考帧` : "待抽帧",
|
||||
judge: "job.frames.length 大于 0 即通过;这些帧只做主体重构证据。",
|
||||
status: stepStatus({ ready: visualReady, running: visualRunning, blocked: !hasSourceVideo }),
|
||||
},
|
||||
{
|
||||
id: "subject",
|
||||
no: "05",
|
||||
title: "相似主体",
|
||||
detail: subjectAssetCount ? `${subjectAssetCount} 张白底视图` : "待生成主体",
|
||||
judge: "关键帧里存在 subject_assets 即通过;生成的是类似创新主体,不复刻原人。",
|
||||
status: stepStatus({ ready: subjectAssetCount > 0, blocked: !visualReady }),
|
||||
},
|
||||
{
|
||||
id: "product",
|
||||
no: "06",
|
||||
title: "产品素材池",
|
||||
detail: productAssetCount ? `${productAssetCount} 张产品图` : "待上传/识别",
|
||||
judge: "product_refs 有记录即通过;不限量,但每条视频后续最多挑 6 张相关图。",
|
||||
status: stepStatus({ ready: productAssetCount > 0, blocked: !job }),
|
||||
},
|
||||
{
|
||||
id: "script",
|
||||
no: "07",
|
||||
title: "分镜文案",
|
||||
detail: storyboardReady ? `${transcriptCount} 条分镜` : "待音频时间轴",
|
||||
judge: "逐句时间轴生成后进入分镜;新口播可按单段或整片改写。",
|
||||
status: stepStatus({ ready: storyboardReady, running: audioRunning, blocked: !audioReady }),
|
||||
},
|
||||
{
|
||||
id: "scene",
|
||||
no: "08",
|
||||
title: "画面首尾帧",
|
||||
detail: endpointTargetCount ? `${endpointFramePairCount}/${endpointTargetCount} 组首尾帧` : "待分镜",
|
||||
judge: "每条分镜先确定场景+人+产品+动作,再生成 asset 类型首帧/尾帧;keyframe 不算通过。",
|
||||
status: stepStatus({ ready: endpointTargetCount > 0 && endpointFramePairCount >= endpointTargetCount, blocked: !storyboardReady }),
|
||||
},
|
||||
{
|
||||
id: "video",
|
||||
no: "09",
|
||||
title: "视频候选",
|
||||
detail: generatedVideoCount ? `${generatedVideoCount} 条历史` : "生成入口暂停",
|
||||
judge: "当前不直接调视频模型;首尾帧审核后才开放单条或批量提交。",
|
||||
status: generatedVideoCount > 0 ? "ready" : "paused",
|
||||
},
|
||||
]
|
||||
}
|
||||
|
||||
function workflowStepMap(steps: WorkflowStep[]) {
|
||||
return steps.reduce((acc, step) => {
|
||||
acc[step.id] = step
|
||||
return acc
|
||||
}, {} as Record<WorkflowStepId, WorkflowStep>)
|
||||
}
|
||||
|
||||
function guessSubjectKind(name: string): SubjectKind {
|
||||
return /人|人物|模特|骨架|身体|脸|手|person|people|human|body|face|hand|character/i.test(name)
|
||||
? "living"
|
||||
@@ -1142,6 +1277,21 @@ export function AdRecreationBoard({
|
||||
const visualReady = (job?.frames.length ?? 0) > 0
|
||||
const subjectAssetCount = countSubjectAssetViews(job)
|
||||
const productAssetCount = job?.product_refs?.length ?? 0
|
||||
const endpointFramePairCount = countEndpointFramePairs(job)
|
||||
const workflowSteps = buildWorkflowSteps({
|
||||
job,
|
||||
submitting: data.submitting,
|
||||
audioReady,
|
||||
audioRunning,
|
||||
transcriptCount,
|
||||
visualReady,
|
||||
visualRunning,
|
||||
subjectAssetCount,
|
||||
productAssetCount,
|
||||
endpointFramePairCount,
|
||||
generatedVideoCount: generatedVideos.length,
|
||||
})
|
||||
const workflow = workflowStepMap(workflowSteps)
|
||||
const statusMessage = job?.message?.startsWith("视频生成已提交")
|
||||
? "历史候选视频已保留;当前已暂停直接提交视频,先逐条生成并审核首尾帧。"
|
||||
: job?.message
|
||||
@@ -1338,9 +1488,12 @@ export function AdRecreationBoard({
|
||||
</div>
|
||||
</header>
|
||||
|
||||
<WorkflowOrderBar steps={workflowSteps} />
|
||||
|
||||
<div className="grid min-h-0 flex-1 grid-cols-[320px_minmax(0,1fr)] gap-3">
|
||||
<MaterialColumn
|
||||
data={data}
|
||||
step={workflow.input}
|
||||
jobs={jobs}
|
||||
job={job}
|
||||
activeJobId={activeJobId}
|
||||
@@ -1357,7 +1510,7 @@ export function AdRecreationBoard({
|
||||
<div className="min-w-0">
|
||||
<div className="flex items-center gap-2">
|
||||
<span className="inline-flex h-7 w-7 items-center justify-center rounded-md bg-rose-500/12 text-rose-100"><Mic className="h-3.5 w-3.5" /></span>
|
||||
<span className="font-mono text-[12px] text-white/36">02</span>
|
||||
<WorkflowStepBadge step={workflow.source} compact />
|
||||
<h2 className="text-[15px] font-semibold leading-tight text-white">源视频解析与参考帧</h2>
|
||||
</div>
|
||||
<div className="mt-1 truncate text-[11px] text-white/38" title={statusMessage}>
|
||||
@@ -1396,10 +1549,10 @@ export function AdRecreationBoard({
|
||||
</details>
|
||||
</div>
|
||||
<div className="mt-2 grid grid-cols-1 gap-1.5 xl:grid-cols-4">
|
||||
<PipelineLane label="音频文案路" detail={audioReady ? `${transcriptCount} 段字幕可规划` : "字幕 / 讲话人 / 节奏"} ready={audioReady} running={audioRunning} />
|
||||
<PipelineLane label="视频视觉路" detail={visualReady ? `${job?.frames.length ?? 0} 张参考帧` : "关键帧 / 主体 / 场景"} ready={visualReady} running={visualRunning} />
|
||||
<PipelineLane label="主体资产" detail={subjectAssetCount ? `${subjectAssetCount} 张主体视图` : "人工选帧后生成"} ready={subjectAssetCount > 0} />
|
||||
<PipelineLane label="产品资产" detail={productAssetCount ? `${productAssetCount} 张产品图` : "上传后自动识别"} ready={productAssetCount > 0} />
|
||||
<PipelineLane step={workflow.audio} />
|
||||
<PipelineLane step={workflow.visual} />
|
||||
<PipelineLane step={workflow.subject} />
|
||||
<PipelineLane step={workflow.product} />
|
||||
</div>
|
||||
</header>
|
||||
|
||||
@@ -1419,6 +1572,10 @@ export function AdRecreationBoard({
|
||||
onJobUpdate={data.onJobUpdate}
|
||||
onDeleteVideo={data.onDeleteVideo}
|
||||
runtimeModels={runtimeModels}
|
||||
productStep={workflow.product}
|
||||
scriptStep={workflow.script}
|
||||
sceneStep={workflow.scene}
|
||||
videoStep={workflow.video}
|
||||
/>
|
||||
</div>
|
||||
</section>
|
||||
@@ -1430,6 +1587,7 @@ export function AdRecreationBoard({
|
||||
|
||||
function MaterialColumn({
|
||||
data,
|
||||
step,
|
||||
jobs,
|
||||
job,
|
||||
activeJobId,
|
||||
@@ -1440,6 +1598,7 @@ function MaterialColumn({
|
||||
onStartProduction,
|
||||
}: {
|
||||
data: NodeData
|
||||
step: WorkflowStep
|
||||
jobs: Job[]
|
||||
job: Job | null
|
||||
activeJobId: string | null
|
||||
@@ -1454,7 +1613,7 @@ function MaterialColumn({
|
||||
<header className="shrink-0 border-b border-white/10 pb-3">
|
||||
<div className="mb-2 flex items-center gap-2">
|
||||
<span className="inline-flex h-8 w-8 items-center justify-center rounded-md bg-rose-500/12 text-rose-100"><Plus className="h-4 w-4" /></span>
|
||||
<span className="font-mono text-[12px] text-white/36">01</span>
|
||||
<WorkflowStepBadge step={step} compact />
|
||||
</div>
|
||||
<h2 className="text-[15px] font-semibold leading-tight text-white">素材输入</h2>
|
||||
<p className="mt-1 text-[12px] leading-snug text-white/42">一个素材就是一次文件任务</p>
|
||||
@@ -2192,12 +2351,20 @@ function AudioStoryboardPlanPanel({
|
||||
onJobUpdate,
|
||||
onDeleteVideo,
|
||||
runtimeModels,
|
||||
productStep,
|
||||
scriptStep,
|
||||
sceneStep,
|
||||
videoStep,
|
||||
}: {
|
||||
job: Job | null
|
||||
selectedFrames: Set<number>
|
||||
onJobUpdate?: (job: Job) => void
|
||||
onDeleteVideo?: (videoId: string) => void
|
||||
runtimeModels?: RuntimeModels
|
||||
productStep: WorkflowStep
|
||||
scriptStep: WorkflowStep
|
||||
sceneStep: WorkflowStep
|
||||
videoStep: WorkflowStep
|
||||
}) {
|
||||
const [storyboardSaveBusyRow, setStoryboardSaveBusyRow] = useState<number | null>(null)
|
||||
const [batchStoryboardSaveBusy, setBatchStoryboardSaveBusy] = useState(false)
|
||||
@@ -2630,7 +2797,10 @@ function AudioStoryboardPlanPanel({
|
||||
<section className="mt-3 rounded-lg border border-white/10 bg-black/28 p-2.5">
|
||||
<div className="mb-2 flex items-start justify-between gap-3">
|
||||
<div>
|
||||
<SectionTitle icon={<Sparkles className="h-4 w-4" />} title="信息流复刻分镜工作台" />
|
||||
<div className="flex flex-wrap items-center gap-2">
|
||||
<SectionTitle icon={<Sparkles className="h-4 w-4" />} title="信息流复刻分镜工作台" />
|
||||
<WorkflowStepBadge step={scriptStep} compact />
|
||||
</div>
|
||||
<p className="mt-1 text-[11px] leading-snug text-white/42">每条分镜纵向排列;行内完成原内容、新文案、画面/产品和视频候选。关键帧选择与相似主体重构在源视频工作区下方统一处理。</p>
|
||||
</div>
|
||||
<div className="grid shrink-0 grid-cols-3 gap-2 text-[11px] text-white/45">
|
||||
@@ -2645,6 +2815,7 @@ function AudioStoryboardPlanPanel({
|
||||
<div className="min-w-0">
|
||||
<div className="flex items-center gap-2">
|
||||
<SectionTitle icon={<Package className="h-4 w-4" />} title="同一产品素材池 / 视角标注" />
|
||||
<WorkflowStepBadge step={productStep} compact />
|
||||
<ModelTrace trace={productModelTrace(runtimeModels)} compact />
|
||||
<span className="rounded-md border border-white/10 bg-white/[0.04] px-2 py-0.5 text-[10px] text-white/42">{productItems.length ? `${productItems.length} 张素材` : "素材池不限量"}</span>
|
||||
{(productAnalyzing || productAngleBusy) && (
|
||||
@@ -2778,7 +2949,7 @@ function AudioStoryboardPlanPanel({
|
||||
<p className="line-clamp-2 text-[10.5px] leading-snug" title={row.source}>{row.source}</p>
|
||||
</StoryboardPlanCell>
|
||||
|
||||
<StoryboardPlanCell label="新口播文案">
|
||||
<StoryboardPlanCell label={`${scriptStep.no} 新口播文案`}>
|
||||
<textarea
|
||||
value={copyText}
|
||||
onChange={(event) => patchRowCopy(row.index, event.target.value)}
|
||||
@@ -2795,7 +2966,7 @@ function AudioStoryboardPlanPanel({
|
||||
</button>
|
||||
</StoryboardPlanCell>
|
||||
|
||||
<StoryboardPlanCell label="画面规划 / 产品融入">
|
||||
<StoryboardPlanCell label={`${sceneStep.no} 画面规划 / 产品融入`}>
|
||||
<div className="grid gap-1.5">
|
||||
<div className="grid grid-cols-[minmax(0,1fr)_auto_auto] items-center gap-1.5">
|
||||
<select
|
||||
@@ -2864,7 +3035,7 @@ function AudioStoryboardPlanPanel({
|
||||
<div className="grid gap-1.5 md:grid-cols-[minmax(0,1fr)_88px_88px]">
|
||||
<div className="rounded border border-white/10 bg-black/24 px-2 py-1.5 text-[10px] leading-snug text-white/42">
|
||||
<div className="mb-1 flex items-center justify-between gap-2">
|
||||
<span className="text-white/54">首尾帧闸门</span>
|
||||
<span className="text-white/54">{sceneStep.no} 首尾帧闸门</span>
|
||||
<span className={endpointAssetRef(referenceFrame, "first_frame") && endpointAssetRef(referenceFrame, "last_frame") ? "text-emerald-100/75" : "text-amber-100/72"}>
|
||||
{endpointAssetRef(referenceFrame, "first_frame") && endpointAssetRef(referenceFrame, "last_frame") ? "可进入视频候选" : "先看图再生视频"}
|
||||
</span>
|
||||
@@ -2912,7 +3083,7 @@ function AudioStoryboardPlanPanel({
|
||||
</div>
|
||||
</StoryboardPlanCell>
|
||||
|
||||
<StoryboardPlanCell label="视频候选 / 待生成" className="xl:border-r-0">
|
||||
<StoryboardPlanCell label={`${videoStep.no} 视频候选 / 待生成`} className="xl:border-r-0">
|
||||
<StoryboardVideoSlots
|
||||
job={job}
|
||||
videos={rowVideos}
|
||||
@@ -3910,6 +4081,74 @@ function Metric({ label, value, compact }: { label: string; value: string; compa
|
||||
)
|
||||
}
|
||||
|
||||
const workflowStatusLabels: Record<WorkflowStepStatus, string> = {
|
||||
blocked: "未解锁",
|
||||
pending: "待处理",
|
||||
running: "运行中",
|
||||
ready: "已通过",
|
||||
paused: "已暂停",
|
||||
}
|
||||
|
||||
function workflowStatusClass(status: WorkflowStepStatus) {
|
||||
if (status === "ready") return "border-emerald-300/25 bg-emerald-400/10 text-emerald-100"
|
||||
if (status === "running") return "border-cyan-300/25 bg-cyan-400/10 text-cyan-100"
|
||||
if (status === "paused") return "border-amber-300/22 bg-amber-300/[0.08] text-amber-100/76"
|
||||
if (status === "blocked") return "border-white/8 bg-white/[0.025] text-white/32"
|
||||
return "border-white/10 bg-white/[0.035] text-white/45"
|
||||
}
|
||||
|
||||
function workflowStatusIcon(status: WorkflowStepStatus) {
|
||||
if (status === "running") return <Loader2 className="h-3 w-3 animate-spin" />
|
||||
if (status === "ready") return <Check className="h-3 w-3" />
|
||||
if (status === "paused") return <AlertTriangle className="h-3 w-3" />
|
||||
return <Circle className="h-2.5 w-2.5" />
|
||||
}
|
||||
|
||||
function WorkflowOrderBar({ steps }: { steps: WorkflowStep[] }) {
|
||||
return (
|
||||
<div className="mb-3 shrink-0 rounded-lg border border-white/10 bg-white/[0.035] p-2">
|
||||
<div className="mb-1.5 flex items-center justify-between gap-3">
|
||||
<div className="flex items-center gap-2 text-[11px] font-semibold text-white/66">
|
||||
<PanelRight className="h-3.5 w-3.5 text-rose-200" />
|
||||
流程顺序
|
||||
</div>
|
||||
<div className="truncate text-[10.5px] text-white/36">编号按解锁条件判定,悬停每步可查看完整判断逻辑。</div>
|
||||
</div>
|
||||
<div className="grid grid-cols-[repeat(9,minmax(116px,1fr))] gap-1.5 overflow-x-auto">
|
||||
{steps.map((step) => (
|
||||
<div
|
||||
key={step.id}
|
||||
title={`判定:${step.judge}`}
|
||||
className={`min-h-[58px] rounded-md border px-2 py-1.5 ${workflowStatusClass(step.status)}`}
|
||||
>
|
||||
<div className="flex items-center justify-between gap-1">
|
||||
<div className="min-w-0 truncate">
|
||||
<span className="mr-1 font-mono text-[11px]">{step.no}</span>
|
||||
<span className="text-[11px] font-semibold">{step.title}</span>
|
||||
</div>
|
||||
{workflowStatusIcon(step.status)}
|
||||
</div>
|
||||
<div className="mt-1 truncate text-[10px] opacity-80">{step.detail}</div>
|
||||
<div className="mt-0.5 truncate text-[9.5px] opacity-60">判定:{step.judge}</div>
|
||||
</div>
|
||||
))}
|
||||
</div>
|
||||
</div>
|
||||
)
|
||||
}
|
||||
|
||||
function WorkflowStepBadge({ step, compact }: { step: WorkflowStep; compact?: boolean }) {
|
||||
return (
|
||||
<span
|
||||
title={`判定:${step.judge}`}
|
||||
className={`inline-flex shrink-0 items-center gap-1 rounded-md border font-mono ${workflowStatusClass(step.status)} ${compact ? "px-1.5 py-0.5 text-[10.5px]" : "px-2 py-1 text-[11px]"}`}
|
||||
>
|
||||
<span>{step.no}</span>
|
||||
{!compact && <span className="font-sans">{step.title}</span>}
|
||||
</span>
|
||||
)
|
||||
}
|
||||
|
||||
function SectionTitle({ icon, title }: { icon: ReactNode; title: string }) {
|
||||
return (
|
||||
<h3 className="flex items-center gap-2 text-[13px] font-semibold text-white">
|
||||
@@ -3919,15 +4158,19 @@ function SectionTitle({ icon, title }: { icon: ReactNode; title: string }) {
|
||||
)
|
||||
}
|
||||
|
||||
function StatusPill({ ready, running }: { ready: boolean; running?: boolean }) {
|
||||
function WorkflowStatusPill({ status }: { status: WorkflowStepStatus }) {
|
||||
return (
|
||||
<span className={`inline-flex items-center gap-1 rounded-md border px-2 py-0.5 text-[11px] ${running ? "border-cyan-300/25 text-cyan-100 bg-cyan-400/10" : ready ? "border-emerald-300/25 text-emerald-100 bg-emerald-400/10" : "border-white/10 text-white/42 bg-white/[0.03]"}`}>
|
||||
{running ? <Loader2 className="h-3 w-3 animate-spin" /> : ready ? <Check className="h-3 w-3" /> : <Circle className="h-2.5 w-2.5" />}
|
||||
{running ? "运行中" : ready ? "已就绪" : "待处理"}
|
||||
<span className={`inline-flex items-center gap-1 rounded-md border px-2 py-0.5 text-[11px] ${workflowStatusClass(status)}`}>
|
||||
{workflowStatusIcon(status)}
|
||||
{workflowStatusLabels[status]}
|
||||
</span>
|
||||
)
|
||||
}
|
||||
|
||||
function StatusPill({ ready, running }: { ready: boolean; running?: boolean }) {
|
||||
return <WorkflowStatusPill status={stepStatus({ ready, running })} />
|
||||
}
|
||||
|
||||
function ActionButton({
|
||||
children,
|
||||
disabled,
|
||||
@@ -3969,14 +4212,17 @@ function Requirement({ label, ready, detail }: { label: string; ready: boolean;
|
||||
)
|
||||
}
|
||||
|
||||
function PipelineLane({ label, detail, ready, running }: { label: string; detail: string; ready: boolean; running?: boolean }) {
|
||||
function PipelineLane({ step }: { step: WorkflowStep }) {
|
||||
return (
|
||||
<div className="flex min-h-9 items-center justify-between gap-2 rounded-md border border-white/10 bg-black/24 px-2.5 py-1.5">
|
||||
<div className="min-w-0">
|
||||
<div className="truncate text-[11px] font-semibold text-white/64">{label}</div>
|
||||
<div className="mt-0.5 truncate text-[10px] text-white/34" title={detail}>{detail}</div>
|
||||
<div className="truncate text-[11px] font-semibold text-white/64">
|
||||
<span className="mr-1 font-mono text-white/38">{step.no}</span>
|
||||
{step.title}
|
||||
</div>
|
||||
<div className="mt-0.5 truncate text-[10px] text-white/34" title={`判定:${step.judge}`}>{step.detail}</div>
|
||||
</div>
|
||||
<StatusPill ready={ready} running={running} />
|
||||
<WorkflowStatusPill status={step.status} />
|
||||
</div>
|
||||
)
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user