| 1 | import { Loader2, RotateCcw, X } from 'lucide-react' |
| 2 | import { ipc } from '@renderer/lib/ipc' |
| 3 | import { useModelAction } from '@renderer/hooks/useModelAction' |
| 4 | import { hydrateStyleSwitchJob, useGenerateStore, useToastStore } from '@renderer/store' |
| 5 | import { useCancelStyleSwitch } from '../hooks/useCancelStyleSwitch' |
| 6 | import { Tooltip, TooltipContent, TooltipTrigger } from '../../ui/Tooltip' |
| 7 | |
| 8 | export function StyleSwitchJobBar({ sessionId }: { sessionId: string }): React.JSX.Element | null { |
| 9 | const job = useGenerateStore((state) => state.styleSwitchJobs[sessionId] || null) |
| 10 | const { selectedModelConfigId, ensureModelActive } = useModelAction() |
| 11 | const toastError = useToastStore((state) => state.error) |
| 12 | const cancelStyleSwitch = useCancelStyleSwitch(sessionId) |
| 13 | if (!job || job.status === 'completed') return null |
| 14 | |
| 15 | const active = |
| 16 | job.status === 'starting' || job.status === 'running' || job.status === 'cancelling' |
| 17 | const failedPages = job.pages.filter((page) => page.status === 'failed') |
| 18 | const handleRetry = async (): Promise<void> => { |
| 19 | if (active || failedPages.length === 0) return |
| 20 | const modelConfigId = await ensureModelActive(selectedModelConfigId) |
| 21 | if (!modelConfigId) return |
| 22 | useGenerateStore.getState().startStyleSwitch(sessionId, { |
| 23 | styleId: job.styleId, |
| 24 | styleName: job.styleName, |
| 25 | totalPages: failedPages.length, |
| 26 | pages: failedPages.map((page) => ({ ...page, status: 'pending', error: null })) |
| 27 | }) |
| 28 | try { |
| 29 | const result = await ipc.retryFailedStyleSwitchPages({ |
| 30 | sessionId, |
| 31 | styleId: job.styleId, |
| 32 | modelConfigId, |
| 33 | failedRunId: job.runId |
| 34 | }) |
| 35 | if (result.alreadyRunning) { |
| 36 | hydrateStyleSwitchJob(sessionId, await ipc.getStyleSwitchState(sessionId)) |
| 37 | return |
| 38 | } |
| 39 | if (result.runId) { |
| 40 | const currentJob = useGenerateStore.getState().styleSwitchJobs[sessionId] |
| 41 | if (currentJob) { |
| 42 | useGenerateStore.getState().updateStyleSwitchJob(sessionId, { |
| 43 | runId: result.runId, |
| 44 | status: currentJob.status === 'cancelling' ? 'cancelling' : 'running' |
| 45 | }) |
| 46 | } |
| 47 | } |
| 48 | } catch (error) { |
| 49 | const message = error instanceof Error ? error.message : '重试风格切换失败' |
| 50 | useGenerateStore.getState().finishStyleSwitch(sessionId, { status: 'failed', error: message }) |
| 51 | toastError(message) |
| 52 | } |
| 53 | } |
| 54 | |
| 55 | const statusText = |
| 56 | job.status === 'cancelling' |
| 57 | ? '正在取消' |
| 58 | : active |
| 59 | ? `正在切换风格${job.styleName ? `:${job.styleName}` : ''}` |
| 60 | : failedPages.length > 0 |
| 61 | ? `${failedPages.length} 页切换失败` |
| 62 | : '风格切换已停止' |
| 63 | |
| 64 | return ( |
| 65 | <div className="mx-3 mt-1 flex h-9 shrink-0 items-center gap-2 border border-[#cbd8bd] bg-[#f4f8ee] px-3 text-xs text-[#43523a]"> |
| 66 | {active ? <Loader2 className="h-3.5 w-3.5 animate-spin" /> : null} |
| 67 | <span className="min-w-0 flex-1 truncate">{statusText}</span> |
| 68 | <span className="tabular-nums text-[#68775f]"> |
| 69 | {job.pages.filter((page) => page.status === 'completed').length}/{job.totalPages} |
| 70 | </span> |
| 71 | {active ? ( |
| 72 | <Tooltip> |
| 73 | <TooltipTrigger asChild> |
| 74 | <button |
| 75 | type="button" |
| 76 | onClick={() => void cancelStyleSwitch()} |
| 77 | disabled={job.status === 'cancelling'} |
| 78 | className="inline-flex h-6 w-6 items-center justify-center text-[#73514b] hover:bg-[#f3dfd8] disabled:opacity-45" |
| 79 | aria-label="取消风格切换" |
| 80 | > |
| 81 | <X className="h-3.5 w-3.5" /> |
| 82 | </button> |
| 83 | </TooltipTrigger> |
| 84 | <TooltipContent>取消</TooltipContent> |
| 85 | </Tooltip> |
| 86 | ) : failedPages.length > 0 ? ( |
| 87 | <Tooltip> |
| 88 | <TooltipTrigger asChild> |
| 89 | <button |
| 90 | type="button" |
| 91 | onClick={() => void handleRetry()} |
| 92 | className="inline-flex h-6 w-6 items-center justify-center text-[#536943] hover:bg-[#dce9d0]" |
| 93 | aria-label="重试失败页面" |
| 94 | > |
| 95 | <RotateCcw className="h-3.5 w-3.5" /> |
| 96 | </button> |
| 97 | </TooltipTrigger> |
| 98 | <TooltipContent>重试失败页面</TooltipContent> |
| 99 | </Tooltip> |
| 100 | ) : null} |
| 101 | </div> |
| 102 | ) |
| 103 | } |
| 104 |