'use client'; // 屏4 挑图:0/N异步电影 + 按A/B/C分组选图 + 点图放大 + 单批重试 + 能离开 import { useState } from 'react'; import { useRouter, useParams } from 'next/navigation'; import { ProgressBar } from '@/components/layout/ProgressBar'; import { FlywheelBannerFromSse } from '@/components/FlywheelBanner'; import { ImageProgressCardSkeleton } from '@/components/tasks/ImageProgressCard'; import { ImageProgressHeader } from '@/components/tasks/ImageProgressHeader'; import { ImageActionBar } from '@/components/tasks/ImageActionBar'; import { ImageLightbox } from '@/components/tasks/ImageLightbox'; import { ImageStrategyGroup, ROLE_LABEL } from '@/components/tasks/ImageStrategyGroup'; import { RegenControls, RegenTarget } from '@/components/tasks/RegenControls'; import { SseStatusBar } from '@/components/tasks/SseStatusBar'; import { FatalErrorBanner } from '@/components/tasks/FatalErrorBanner'; import { RejectReasonBanner } from '@/components/tasks/RejectReasonBanner'; import { useSse } from '@/hooks/useSse'; import { useGenerationStore } from '@/stores/generationStore'; import { useTaskStore } from '@/stores/taskStore'; import { ImageCandidate } from '@/types/index'; import { api } from '@/lib/api'; export default function ImageSelectionPage() { const params = useParams<{ id: string }>(); const taskId = params?.id ? Number(params.id) : null; const router = useRouter(); const { imageCandidates, imageDone, imageTotal, failedBatches, flywheelInjected } = useGenerationStore(); const { selectedImageIds, toggleImageSelection } = useTaskStore(); const { connectionStatus, reconnectAttempt } = useSse(taskId); const [zoom, setZoom] = useState(null); const [regenTarget, setRegenTarget] = useState(null); const pendingCount = imageTotal - imageDone; const allDone = imageTotal > 0 && pendingCount === 0; // 按套(A/B/C)分组展示;R2 同套同角色去重,只留最新一张(candidate_id 最大=最新重生) function latestByRole(list: ImageCandidate[]): ImageCandidate[] { const byRole = new Map(); for (const c of list) { const prev = byRole.get(c.role); if (!prev || c.candidate_id > prev.candidate_id) byRole.set(c.role, c); } return Array.from(byRole.values()); } const groups: Array<['A' | 'B' | 'C', ImageCandidate[]]> = (['A', 'B', 'C'] as const).map( (s) => [s, latestByRole(imageCandidates.filter((c) => c.strategy === s))] ); async function handleRetry(_batchRole: string) { // 一期 regenerate = 整体重生(R2 已扩展按角色单批重生,见 handleRegen) if (!taskId) return; await api.post(`/api/v1/tasks/${taskId}/regenerate`); } // R2 单张/单套重生 + 可选人工提示词 async function handleRegen(target: RegenTarget, customPrompt: string) { if (!taskId) return; await api.post(`/api/v1/tasks/${taskId}/regenerate`, { strategy: target.strategy, role: target.role, custom_prompt: customPrompt || undefined, }); } function openRegen(strategy: string, role: string | undefined, label: string) { setRegenTarget({ strategy: strategy as 'A' | 'B' | 'C', role, label }); } async function handleProceedToConfirm() { if (!taskId || selectedImageIds.length === 0) return; for (const candidateId of selectedImageIds) { await api.post(`/api/v1/tasks/${taskId}/image-candidates/select`, { candidate_id: candidateId }); } router.push(`/tasks/${taskId}/confirm`); } return (
{flywheelInjected && ( )}
{imageCandidates.length === 0 ? (
{Array.from({ length: 6 }).map((_, i) => )}
) : ( groups.map(([s, list]) => list.length > 0 ? ( ) : null ) )} {/* 失败批次提示 */} {failedBatches.length > 0 && allDone && (
有 {failedBatches.length} 批没出来。 {failedBatches.filter((b) => b.retryable).map((b) => ( ))}
)} {imageCandidates.length > 0 && ( )}
setZoom(null)} /> setRegenTarget(null)} onSubmit={handleRegen} />
); }