返回 oh-my-ppt
GenerationStatusPanel.tsx
根目录 / src / renderer / src / components / session-generating / GenerationStatusPanel.tsx
1 import type React from 'react'
2 import { CheckCircle2, CircleAlert, Loader2 } from 'lucide-react'
3 import { Button } from '@renderer/components/ui/Button'
4 import { ModelSplitButton } from '@renderer/components/model/ModelActionButton'
5 import { cn } from '@renderer/lib/utils'
6 import type { ModelActionState } from '@renderer/hooks/useModelAction'
7 import type { GenerationRunStatus, GenerationStageKey } from './types'
8
9 export function GenerationStatusPanel({
10 status,
11 progress,
12 stages,
13 stageLabels,
14 currentStage,
15 completedPageCount,
16 totalPages,
17 error,
18 interruptedLabel,
19 enterEditorLabel,
20 continueRemainingLabel,
21 regenerateLabel,
22 cancelLabel,
23 isCancelling,
24 hasGeneratedPages,
25 canEnterEditor,
26 showEditorShortcut,
27 modelAction,
28 onEnterEditor,
29 onContinueRemaining,
30 onRegenerate,
31 onCancel
32 }: {
33 status: GenerationRunStatus
34 progress: number
35 stages: readonly GenerationStageKey[]
36 stageLabels: Record<GenerationStageKey, string>
37 currentStage: string
38 completedPageCount: number
39 totalPages: number
40 error: string | null
41 interruptedLabel: string
42 enterEditorLabel: string
43 continueRemainingLabel: string
44 regenerateLabel: string
45 cancelLabel: string
46 isCancelling?: boolean
47 hasGeneratedPages: boolean
48 canEnterEditor: boolean
49 showEditorShortcut: boolean
50 modelAction: ModelActionState
51 onEnterEditor: () => void
52 onContinueRemaining: (modelConfigId: string) => void
53 onRegenerate: (modelConfigId: string) => void
54 onCancel: () => void
55 }): React.JSX.Element {
56 if (status === 'failed' || status === 'cancelled') {
57 return (
58 <div className="mb-4 shrink-0 rounded-lg border border-[#d7b5ae]/80 bg-[#fbf1ee]/86 px-4 py-2.5 text-[#93564f] shadow-[0_8px_20px_rgba(120,73,65,0.08)]">
59 <div className="flex flex-col gap-2 lg:flex-row lg:items-center lg:justify-between">
60 <div className="flex min-w-0 items-center gap-2.5">
61 <CircleAlert className="h-4 w-4 shrink-0" />
62 <span className="shrink-0 rounded-md border border-[#d7b5ae]/70 bg-[#fff8f4]/75 px-2 py-1 text-xs font-semibold text-[#8e5a53]">
63 {interruptedLabel}
64 </span>
65 <span className="min-w-0 truncate text-xs text-[#9b6b63]">{error}</span>
66 {canEnterEditor && (
67 <button
68 type="button"
69 onClick={onEnterEditor}
70 className="shrink-0 text-xs font-medium text-[#6f8159] underline-offset-2 hover:underline"
71 >
72 {enterEditorLabel}
73 </button>
74 )}
75 </div>
76
77 <div className="flex shrink-0 flex-wrap gap-1.5">
78 <ModelSplitButton
79 modelAction={modelAction}
80 label={hasGeneratedPages ? continueRemainingLabel : regenerateLabel}
81 tone="subtle"
82 size="sm"
83 onRun={(modelConfigId) => {
84 if (hasGeneratedPages) {
85 onContinueRemaining(modelConfigId)
86 } else {
87 onRegenerate(modelConfigId)
88 }
89 }}
90 />
91 </div>
92 </div>
93 </div>
94 )
95 }
96
97 const activeStageIndex = stages.indexOf(currentStage as GenerationStageKey)
98
99 return (
100 <div className="mb-4 shrink-0 rounded-lg border border-[#d8ccb5] bg-[#fff9ef] px-4 py-2 text-[#435138] shadow-[0_12px_28px_rgba(78,91,63,0.13)]">
101 <div className="flex flex-col gap-2 lg:flex-row lg:items-end lg:gap-4">
102 <div className="min-w-0 flex-1">
103 <div className="mb-1 flex flex-wrap items-center gap-x-4 gap-y-1 text-[11px] text-[#617350]">
104 <span className="flex min-w-0 flex-wrap items-center gap-2 text-[10px] text-[#7d8b63]">
105 {stages.map((stage, index) => {
106 const isActive = index === activeStageIndex
107 const isDone = index < activeStageIndex || status === 'completed'
108 return (
109 <span
110 key={stage}
111 className={cn(
112 'inline-flex items-center gap-1 leading-4',
113 isDone && 'text-[#5f8a43]',
114 isActive && 'font-semibold text-[#365528]',
115 !isDone && !isActive && 'text-[#a09882]'
116 )}
117 >
118 {isDone && <CheckCircle2 className="h-3 w-3" />}
119 {isActive && (status === 'queued' || status === 'running') && (
120 <span className="h-1.5 w-1.5 rounded-full bg-[#4f7b3f]" />
121 )}
122 {stage === 'rendering' && completedPageCount > 0
123 ? `${stageLabels[stage]} ${completedPageCount}/${totalPages}`
124 : stageLabels[stage]}
125 </span>
126 )
127 })}
128 </span>
129 <span className="ml-auto inline-flex shrink-0 items-center gap-2 font-medium">
130 <span className="font-semibold">{progress}%</span>
131 {showEditorShortcut && (
132 <Button
133 size="sm"
134 className="h-6 rounded-md px-2 text-[10px] shadow-none"
135 onClick={onEnterEditor}
136 >
137 {enterEditorLabel}
138 </Button>
139 )}
140 {(status === 'queued' || status === 'running') && (
141 <Button
142 size="sm"
143 variant="outline"
144 className="h-6 rounded-md px-2 text-[10px] shadow-none"
145 disabled={isCancelling}
146 aria-busy={isCancelling || undefined}
147 onClick={onCancel}
148 >
149 {isCancelling && <Loader2 className="mr-1 h-3 w-3 animate-spin" />}
150 {cancelLabel}
151 </Button>
152 )}
153 </span>
154 </div>
155 <div className="h-1.5 overflow-hidden rounded-full border border-[#d8ccb5]/80 bg-[#fffaf1] shadow-[inset_0_1px_2px_rgba(74,58,40,0.12)]">
156 <div
157 className="h-full rounded-full bg-[linear-gradient(90deg,#9ecf8a_0%,#6f9f59_52%,#4f7b3f_100%)] bg-[length:200%_100%] transition-[width] duration-500"
158 style={{
159 width: `${Math.max(2, progress)}%`,
160 animation: 'gen-shimmer-move 2.8s linear infinite'
161 }}
162 />
163 </div>
164 </div>
165 </div>
166 </div>
167 )
168 }
169
169 lines Plain Text