|
1 | 1 | /** |
2 | 2 | * SessionDetailPanel — 会话详情面板(原料 / 笔记预览两视图;v0.11.5 产物视图下线)。 |
3 | 3 | * |
4 | | - * @ai-context: v0.7.1 自 SessionsPage 拆出(豁免清单登记拆分计划)——质量报告、 |
5 | | - * 大纲、视图模式为面板内部状态(仅依赖 sessionId),与列表页解耦, |
6 | | - * 列表页聚焦管理操作(筛选/批量/转化)。 |
7 | | - * @ai-context: REQ-031(融合停止异步化):fusing 时显示"融合中"标记, |
8 | | - * session:fused 到达后父层自动刷新 detail 重挂本面板。 |
9 | | - * @ai-context: REQ-080 降级分级:live:asr-degraded 一次性横幅(父层透传)。 |
10 | | - * @ai-context: v0.11.5(spec 5️⃣):产物视图下线(ArtifactView 删除)——课后精修 |
11 | | - * 入口迁移到面板层;进入原料视图懒触发 auto_refine_session(幂等: |
12 | | - * 已精修屏跳过),session:refined 事件驱动重新拉详情(屏卡 rendered 回填), |
13 | | - * refine-skipped 徽标提示(模型未下载降级链)。 |
| 4 | + * @ai-context: v0.7.1 自 SessionsPage 拆出(豁免清单登记拆分计划)——质量报告、大纲、 |
| 5 | + * 视图模式为面板内部状态(仅依赖 sessionId),与列表页解耦。 |
| 6 | + * @ai-context: REQ-031 融合停止异步化:fusing 时显示"融合中"标记,session:fused 到达后父层自动 |
| 7 | + * 刷新 detail 重挂本面板。REQ-080 降级分级:live:asr-degraded 一次性横幅(父层透传)。 |
| 8 | + * @ai-context: v0.11.5(spec 5️⃣):产物视图下线(ArtifactView 删除)——课后精修入口迁移到面板层; |
| 9 | + * 进入原料视图懒触发 auto_refine_session(幂等:已精修屏跳过),session:refined 事件 |
| 10 | + * 驱动重新拉详情(屏卡 rendered 回填),refine-skipped 徽标提示(模型未下载降级链)。 |
| 11 | + * @ai-context: 批 5 T2(C9 拆件 A):297 → ≤150(C9 的 150 严于通用 300 ⇒ 取严者)。 |
| 12 | + * 四块搬出——原文视图 → `session-detail/SessionRawView.tsx`;切换器行 + `preview` |
| 13 | + * 分支 → `session-detail/SessionViewHost.tsx`(过渡形态,T10 改注册表驱动); |
| 14 | + * 可信度总览卡 + 两裁决面板 + `refineMsg` 行 → `session-detail/SessionAuxBlocks.tsx`。 |
| 15 | + * 行为等价由 `SessionDetailPanel.test.tsx` 的 P1–P4 在拆前拆后都绿证明。 |
| 16 | + * @ai-context: 本件仍是**唯一数据面**(`useSessionDetailData`)与**唯一视图状态持有者** |
| 17 | + * (`viewMode` / 深链快照写入——裁决 D1;C5 会在 T10 改判 `:64` 的复位 effect)。 |
| 18 | + * @ai-context: DOM 顺序契约(逐字不变):`SessionDetailHeader` → `SessionQualityCard` → |
| 19 | + * `SpeakerSwitchCard` → 切换器行(含 `SessionRefineSection`)→ `refineMsg` 行 |
| 20 | + * → 视图区 → 两个裁决面板;全部为**同级兄弟**,无新增 wrapper。 |
14 | 21 | */ |
15 | 22 | import { useEffect, useState } from "react"; |
16 | 23 | import { useSessionDetailData } from "../hooks/useSessionDetailData"; |
17 | 24 | import SessionDetailHeader from "./session-detail/SessionDetailHeader"; |
18 | | -import SessionRefineSection from "./session-detail/SessionRefineSection"; |
19 | | -import SessionScreenCards from "./session-detail/SessionScreenCards"; |
20 | | -import ImageGallery from "../components/ImageGallery"; |
21 | | -import NotePreviewView from "../components/NotePreviewView"; |
22 | | -import ProofreadPanel from "../components/ProofreadPanel"; |
23 | | -import SecondPassPanel from "../components/SecondPassPanel"; |
| 25 | +import SessionRawView from "./session-detail/SessionRawView"; |
| 26 | +import SessionViewHost from "./session-detail/SessionViewHost"; |
| 27 | +import { SessionAuxPanels, SessionQualityCard } from "./session-detail/SessionAuxBlocks"; |
24 | 28 | import WebArticleView from "../components/WebArticleView"; |
25 | 29 | import SpeakerSwitchCard from "../components/SpeakerSwitchCard"; |
26 | | -import type { SessionDetail } from "../types"; |
27 | | -import { fmtMs } from "../utils/fmt"; |
28 | | -import { Text } from "../ui/primitives"; |
| 30 | +import type { GlossaryTerm, SessionDetail } from "../types"; |
29 | 31 |
|
30 | | -/** 通用小按钮基础样式(视图切换组与精修工具条复用) */ |
31 | | -const btn: React.CSSProperties = { padding: "5px 10px", cursor: "pointer", fontSize: 12 }; |
32 | | - |
33 | | -const SOURCE_LABEL: Record<string, string> = { |
34 | | - subtitle: "字幕", |
35 | | - asr: "语音", |
36 | | - fused: "融合", |
37 | | -}; |
| 32 | +/** 术语表 summary 的三种文案(null=加载中 / 0 条 / N 条)——原文案逐字,条件整串注入视图 */ |
| 33 | +const glossarySummaryOf = (glossary: GlossaryTerm[] | null): string => |
| 34 | + glossary === null |
| 35 | + ? "加载中…" |
| 36 | + : glossary.length === 0 |
| 37 | + ? "无术语(画面高频 × 语音低频未命中)" |
| 38 | + : `${glossary.length} 条(画面高频 × 语音低频)`; |
38 | 39 |
|
39 | 40 | interface Props { |
40 | 41 | detail: SessionDetail; |
41 | | - /** 本会话是否融合中(父层 fusingId === detail.session.id) */ |
42 | | - fusing: boolean; |
43 | | - /** 关键降级一次性横幅(null=无) */ |
44 | | - degradedBanner: string | null; |
45 | | - /** 转为笔记(父层负责 toast 反馈与列表刷新) */ |
| 42 | + /** 本会话是否融合中(父层 fusingId === detail.session.id) */ fusing: boolean; |
| 43 | + /** 关键降级一次性横幅(null=无) */ degradedBanner: string | null; |
| 44 | + /** 转为笔记 / 删除会话 / 重新拉详情(三者都由父层负责反馈与刷新) */ |
46 | 45 | onToNote: (id: number) => void; |
47 | | - /** 删除会话(父层负责确认/反馈/刷新) */ |
48 | 46 | onRemove: (id: number) => void; |
49 | | - /** 重新拉详情(v0.11.5:session:refined 事件驱动屏卡结构回填) */ |
50 | 47 | onRefreshDetail: (id: number) => void; |
51 | | - /** v0.16.1:工作台深链任务 id(对话页任务视图跳转——自动切预览视图并展开工作台) */ |
| 48 | + /** v0.16.1 工作台深链:任务 id / 消费完成回调(App 清空 focus)/ 精修任务启动回调(→ 对话页) */ |
52 | 49 | autoRefineTaskId?: number | null; |
53 | | - /** v0.16.1:autoTaskId 消费完成回调(App 清空 focus——防陈旧值跨导航复触发) */ |
54 | 50 | onAutoTaskConsumed?: () => void; |
55 | | - /** v0.16.1:精修任务启动回调(→ AI 对话页) */ |
56 | 51 | onRefineTaskStarted?: (sessionId: number, taskId: number) => void; |
57 | 52 | } |
58 | 53 |
|
59 | 54 | export default function SessionDetailPanel({ detail, fusing, degradedBanner, onToNote, onRemove, onRefreshDetail, autoRefineTaskId, onAutoTaskConsumed, onRefineTaskStarted }: Props) { |
60 | | - // v0.5.0 M7(REQ-052)+ v0.6.0 M6(REQ-081):两视图(v0.11.5 产物视图下线) |
61 | | - const [viewMode, setViewMode] = useState<"raw" | "preview">("raw"); |
| 55 | + const [viewMode, setViewMode] = useState<"raw" | "preview">("raw"); // v0.5.0 M7 + v0.6.0 M6:两视图 |
62 | 56 | const sessionId = detail.session.id; |
63 | | - // v0.5.0 M7:会话切换回到原料视图(裁决 D1:viewMode 状态留面板;数据面重置见 useSessionDetailData) |
| 57 | + // v0.5.0 M7:会话切换回到原料视图(裁决 D1:viewMode 状态留面板;数据面重置见 hook) |
64 | 58 | useEffect(() => { setViewMode("raw"); }, [sessionId]); |
65 | | - // 数据面(质量/术语/baseUrl/屏→OCR 分组)+ 精修链路(懒触发/事件监听/手动入口/深链快照) |
66 | | - // + 屏卡瞬时态(框选/单屏 toast——由 hook 持有以保证跨原料/预览视图切换不丢,见 hook 文件头) |
| 59 | + // 数据面(质量/术语/baseUrl/屏→OCR 分组)+ 精修链路 + 屏卡瞬时态(hook 持有 ⇒ 跨视图切换不丢) |
67 | 60 | const { |
68 | 61 | quality, glossary, baseUrl, ocrBlocksByScreen, refining, refineMsg, deepTaskId, setDeepTaskId, startRefine, |
69 | 62 | selectingScreen, setSelectingScreen, panelToast, showPanelToast, clearPanelToast, |
70 | 63 | } = useSessionDetailData({ detail, viewMode, onRefreshDetail }); |
71 | 64 | // REQ-282(v0.19.6):标题行内改名 —— 连同改名状态与提交逻辑拆至 session-detail/SessionDetailHeader.tsx |
72 | | - // v0.20.2(REQ-268):离线精修(第二遍)裁决面板显隐(会话切换即关) |
| 65 | + // v0.20.2(REQ-268/270):两个裁决面板的显隐 —— 「会话切换即关」的 effect 随面板搬至 |
| 66 | + // `SessionAuxPanels`(state 留本件:工具条三按钮在切换器行里,就近持有回调更少暴露面)。 |
73 | 67 | const [showPass2, setShowPass2] = useState(false); |
74 | | - useEffect(() => setShowPass2(false), [sessionId]); |
75 | | - // v0.20.2(REQ-270):LLM 文本校对面板显隐(会话切换即关) |
76 | 68 | const [showProofread, setShowProofread] = useState(false); |
77 | | - useEffect(() => setShowProofread(false), [sessionId]); |
78 | | - |
79 | | - // v0.16.1:工作台深链——autoTaskId 到达即切预览视图(精修卡所在视图——原默认 raw)。 |
80 | | - // 深链快照(deepTaskId)由 useSessionDetailData 持有:App 侧 focus 清空发生在本面板 |
81 | | - // effect 之后,若直接透传 prop 会在卡片挂载前被置空(竞态——工作台永不展开); |
82 | | - // 快照 + 会话切换清除保证"只消费一次、不跨会话遗留"。切换 effect 按裁决 D1 留面板。 |
| 69 | + // v0.16.1 工作台深链:autoTaskId 到达即切预览视图。深链快照由 hook 持有——App 侧 focus 清空 |
| 70 | + // 早于本层 effect,直接透传 prop 会在卡片挂载前被置空(竞态);快照 + 会话切换清除保证 |
| 71 | + // 「只消费一次、不跨会话遗留」。切换 effect 按裁决 D1 留面板。 |
83 | 72 | useEffect(() => { |
84 | 73 | if (autoRefineTaskId != null) { |
85 | 74 | setDeepTaskId(autoRefineTaskId); |
@@ -107,191 +96,53 @@ export default function SessionDetailPanel({ detail, fusing, degradedBanner, onT |
107 | 96 | return ( |
108 | 97 | <> |
109 | 98 | {/* 降级横幅 + 详情头(改名/状态行/融合中徽标/操作)—— 拆至 session-detail/SessionDetailHeader.tsx */} |
110 | | - <SessionDetailHeader |
111 | | - detail={detail} |
112 | | - fusing={fusing} |
113 | | - degradedBanner={degradedBanner} |
114 | | - onToNote={onToNote} |
115 | | - onRemove={onRemove} |
116 | | - onRefreshDetail={onRefreshDetail} |
117 | | - /> |
| 99 | + <SessionDetailHeader detail={detail} fusing={fusing} degradedBanner={degradedBanner} onToNote={onToNote} onRemove={onRemove} onRefreshDetail={onRefreshDetail} /> |
118 | 100 |
|
119 | | - {/* M6(REQ-076):可信度总览卡片 */} |
120 | | - {quality && ( |
121 | | - <div style={{ display: "flex", gap: 8, flexWrap: "wrap", marginBottom: 8 }}> |
122 | | - {( |
123 | | - [ |
124 | | - ["低置信段", quality.low_confidence_count, quality.low_confidence_count > 0 ? "#dc2626" : "#6b7280"], |
125 | | - ["OCR 低分", quality.low_score_ocr_count, "#b45309"], |
126 | | - ["unknown 区", quality.unknown_region_count, "#7c3aed"], |
127 | | - ["AI 复核候选", quality.ai_candidate_count, "#2563eb"], |
128 | | - ] as const |
129 | | - ).map(([label, count, color]) => ( |
130 | | - <span |
131 | | - key={label} |
132 | | - style={{ |
133 | | - fontSize: 11, |
134 | | - color, |
135 | | - background: "#f9fafb", |
136 | | - border: "1px solid #e5e7eb", |
137 | | - borderRadius: 10, |
138 | | - padding: "2px 8px", |
139 | | - }} |
140 | | - > |
141 | | - {label} {count} |
142 | | - </span> |
143 | | - ))} |
144 | | - {quality.low_confidence_segments.length > 0 && ( |
145 | | - <details style={{ fontSize: 11, color: "#6b7280" }}> |
146 | | - <summary style={{ cursor: "pointer" }}>低置信列表({quality.low_confidence_segments.length})</summary> |
147 | | - {quality.low_confidence_segments.map((s) => ( |
148 | | - <div key={s.segment_id} style={{ marginTop: 3 }}> |
149 | | - [{fmtMs(s.start_ms)}] {s.text}({s.confidence.toFixed(2)}) |
150 | | - </div> |
151 | | - ))} |
152 | | - </details> |
153 | | - )} |
154 | | - </div> |
155 | | - )} |
| 101 | + {/* M6(REQ-076):可信度总览卡片 —— 拆至 SessionAuxBlocks.tsx 的 SessionQualityCard */} |
| 102 | + {quality && <SessionQualityCard quality={quality} />} |
156 | 103 |
|
157 | 104 | {/* v0.7.2(REQ-153):讲者切换(弱化版说话人分离——懒加载幂等; |
158 | 105 | v0.12.1:图文会话跳过(无音频,不再误报红色错误)) */} |
159 | 106 | <SpeakerSwitchCard sessionId={sessionId} kind={detail.session.kind} /> |
160 | 107 |
|
161 | | - {/* 两视图切换(原料 / 笔记预览——REQ-081;v0.11.5 产物视图下线) */} |
162 | | - <div style={{ display: "flex", alignItems: "center", gap: 6, marginBottom: 10 }}> |
163 | | - {( |
164 | | - [ |
165 | | - ["raw", "原料视图"], |
166 | | - ["preview", "笔记预览"], |
167 | | - ] as const |
168 | | - ).map(([mode, label]) => ( |
169 | | - <button |
170 | | - key={mode} |
171 | | - onClick={() => setViewMode(mode)} |
172 | | - style={{ |
173 | | - ...btn, |
174 | | - borderRadius: 6, |
175 | | - border: viewMode === mode ? "1px solid #0d9488" : "1px solid #e5e7eb", |
176 | | - background: viewMode === mode ? "#ccfbf1" : "#fff", |
177 | | - color: viewMode === mode ? "#0f766e" : "#374151", |
178 | | - }} |
179 | | - > |
180 | | - {label} |
181 | | - </button> |
182 | | - ))} |
183 | | - {/* v0.11.5(spec 5️⃣)+ v0.20.2(REQ-268/270):精修工具条三按钮 |
184 | | - —— 拆至 session-detail/SessionRefineSection.tsx(refineMsg 提示行与两裁决面板 |
185 | | - 挂载保留原位,DOM 顺序逐字不变) */} |
186 | | - <SessionRefineSection |
187 | | - refining={refining} |
188 | | - onStartRefine={startRefine} |
189 | | - canSecondPass={detail.session.status === "finished" && detail.session.kind !== "photo"} |
190 | | - onOpenPass2={() => setShowPass2(true)} |
191 | | - onOpenProofread={() => setShowProofread(true)} |
192 | | - /> |
193 | | - </div> |
194 | | - {refineMsg && ( |
195 | | - <div style={{ fontSize: 11, color: refining ? "#b45309" : "#0d9488", marginBottom: 6 }}> |
196 | | - {refining ? "⏳ " : ""}{refineMsg} |
197 | | - </div> |
198 | | - )} |
199 | | - |
200 | | - {viewMode === "preview" ? ( |
201 | | - <NotePreviewView |
202 | | - sessionId={sessionId} |
203 | | - autoTaskId={deepTaskId} |
204 | | - onTaskStarted={onRefineTaskStarted} |
| 108 | + {/* 切换器行 + 视图区 —— 拆至 SessionViewHost.tsx(过渡形态:手写 2 按钮切换组逐字 |
| 109 | + 搬移,T10 再改注册表驱动的多视图宿主);children = 原文视图节点 */} |
| 110 | + <SessionViewHost |
| 111 | + sessionId={sessionId} |
| 112 | + viewMode={viewMode} |
| 113 | + onViewModeChange={setViewMode} |
| 114 | + autoRefineTaskId={deepTaskId} |
| 115 | + onRefineTaskStarted={onRefineTaskStarted} |
| 116 | + refining={refining} |
| 117 | + onStartRefine={startRefine} |
| 118 | + canSecondPass={detail.session.status === "finished" && detail.session.kind !== "photo"} |
| 119 | + onOpenPass2={() => setShowPass2(true)} |
| 120 | + onOpenProofread={() => setShowProofread(true)} |
| 121 | + > |
| 122 | + <SessionRawView |
| 123 | + detail={detail} |
| 124 | + baseUrl={baseUrl} |
| 125 | + ocrBlocksByScreen={ocrBlocksByScreen} |
| 126 | + selectingScreen={selectingScreen} |
| 127 | + onSelectScreen={setSelectingScreen} |
| 128 | + panelToast={panelToast} |
| 129 | + onShowToast={showPanelToast} |
| 130 | + onClearToast={clearPanelToast} |
| 131 | + glossarySummary={glossarySummaryOf(glossary)} |
| 132 | + glossary={glossary} |
205 | 133 | /> |
206 | | - ) : ( |
207 | | - <> |
208 | | - {/* 转写时间轴(字幕为主,语音/融合弱化;段 id 锚点供大纲/搜索跳转) */} |
209 | | - <h3 style={{ fontSize: 13, margin: "12px 0 6px" }}>转写时间轴</h3> |
210 | | - <div style={{ display: "flex", flexDirection: "column", gap: 4 }}> |
211 | | - {/* v0.11.7:图文会话空态语义区分(无讲述内容);其余会话维持原文案 */} |
212 | | - {detail.segments.length === 0 && ( |
213 | | - <Text as="p" size={5} tone="ink-3"> |
214 | | - {detail.session.kind === "photo" ? "本会话无讲述内容(图文采集)" : "本会话无转写段"} |
215 | | - </Text> |
216 | | - )} |
217 | | - {detail.segments.map((seg) => ( |
218 | | - <div key={seg.id} id={`seg-${sessionId}-${seg.id}`} style={{ display: "flex", gap: 8, alignItems: "baseline" }}> |
219 | | - <Text tone="ink-3" style={{ fontSize: 11, width: 70, flexShrink: 0, fontVariantNumeric: "tabular-nums" }}> |
220 | | - {fmtMs(seg.start_ms)} – {fmtMs(seg.end_ms)} |
221 | | - </Text> |
222 | | - <span style={{ fontSize: 11, flexShrink: 0, color: seg.source === "subtitle" ? "#0d9488" : "#9ca3af", width: 36 }}> |
223 | | - {SOURCE_LABEL[seg.source] ?? seg.source} |
224 | | - </span> |
225 | | - <span style={{ fontSize: 13, color: seg.source === "fused" ? "#b45309" : "#374151" }}>{seg.text}</span> |
226 | | - </div> |
227 | | - ))} |
228 | | - </div> |
229 | | - |
230 | | - {/* 画面要点屏卡流(v0.7.3 区间/标题/正文/标签/配图/结构徽标 + v0.7.7 框选截取) |
231 | | - —— 拆至 session-detail/SessionScreenCards.tsx;toast/框选态**留在 useSessionDetailData**(屏卡子树随 viewMode 卸载,状态不能跟着走) */} |
232 | | - <SessionScreenCards |
233 | | - sessionId={sessionId} |
234 | | - kind={detail.session.kind} |
235 | | - screens={detail.screens} |
236 | | - ocrBlockCount={detail.ocr_blocks.length} |
237 | | - baseUrl={baseUrl} |
238 | | - ocrBlocksByScreen={ocrBlocksByScreen} |
239 | | - selectingScreen={selectingScreen} |
240 | | - onSelectScreen={setSelectingScreen} |
241 | | - panelToast={panelToast} |
242 | | - onShowToast={showPanelToast} |
243 | | - onClearToast={clearPanelToast} |
244 | | - /> |
| 134 | + </SessionViewHost> |
245 | 135 |
|
246 | | - {/* 术语表(v0.11.5 spec 8️⃣:词汇表移出笔记 → 会话详情展示; |
247 | | - 分析层 glossary 产出——画面高频 × 语音低频交叉,score 降序) */} |
248 | | - <h3 style={{ fontSize: 13, margin: "16px 0 6px" }}>📖 术语表</h3> |
249 | | - <details |
250 | | - style={{ |
251 | | - fontSize: 12, |
252 | | - border: "1px solid #e5e7eb", |
253 | | - borderRadius: 8, |
254 | | - padding: "8px 10px", |
255 | | - background: "#fafafa", |
256 | | - marginBottom: 8, |
257 | | - }} |
258 | | - > |
259 | | - <summary style={{ cursor: "pointer", color: "#0f766e", fontWeight: 600 }}> |
260 | | - {glossary === null |
261 | | - ? "加载中…" |
262 | | - : glossary.length === 0 |
263 | | - ? "无术语(画面高频 × 语音低频未命中)" |
264 | | - : `${glossary.length} 条(画面高频 × 语音低频)`} |
265 | | - </summary> |
266 | | - {glossary !== null && glossary.length > 0 && ( |
267 | | - <div style={{ marginTop: 6, display: "flex", flexDirection: "column", gap: 3 }}> |
268 | | - {glossary.map((g) => ( |
269 | | - <div key={g.term} style={{ display: "flex", gap: 8, alignItems: "baseline" }}> |
270 | | - <span style={{ fontWeight: 600, color: "#111827" }}>{g.term}</span> |
271 | | - <span style={{ fontSize: 11, color: "#6b7280" }}> |
272 | | - 画面 ×{g.ocrCount} / 语音 ×{g.asrCount} |
273 | | - </span> |
274 | | - <Text tone="ink-3" style={{ fontSize: 10, marginLeft: "auto" }}> |
275 | | - 分 {g.score.toFixed(1)} |
276 | | - </Text> |
277 | | - </div> |
278 | | - ))} |
279 | | - </div> |
280 | | - )} |
281 | | - </details> |
282 | | - |
283 | | - {/* 参考图集(v0.5.0 M6:REQ-051 三层图结构) */} |
284 | | - <h3 style={{ fontSize: 13, margin: "16px 0 6px" }}>参考图集</h3> |
285 | | - <ImageGallery sessionId={sessionId} /> |
286 | | - </> |
287 | | - )} |
288 | | - |
289 | | - {/* v0.20.2(REQ-268):离线精修(第二遍)裁决面板 */} |
290 | | - {showPass2 && <SecondPassPanel sessionId={sessionId} onClose={() => setShowPass2(false)} />} |
291 | | - {/* v0.20.2(REQ-270):LLM 文本校对面板 */} |
292 | | - {showProofread && ( |
293 | | - <ProofreadPanel sessionId={sessionId} onClose={() => setShowProofread(false)} /> |
294 | | - )} |
| 136 | + {/* refineMsg 行 + 两个裁决面板 —— 拆至 SessionAuxBlocks.tsx 的 SessionAuxPanels */} |
| 137 | + <SessionAuxPanels |
| 138 | + sessionId={sessionId} |
| 139 | + refineMsg={refineMsg} |
| 140 | + refining={refining} |
| 141 | + showPass2={showPass2} |
| 142 | + showProofread={showProofread} |
| 143 | + onClosePass2={() => setShowPass2(false)} |
| 144 | + onCloseProofread={() => setShowProofread(false)} |
| 145 | + /> |
295 | 146 | </> |
296 | 147 | ); |
297 | 148 | } |
0 commit comments