Skip to content

Commit 0bf849d

Browse files
committed
refactor(sessions): split raw view out of detail panel
1 parent 4bee9d7 commit 0bf849d

4 files changed

Lines changed: 467 additions & 228 deletions

File tree

Lines changed: 79 additions & 228 deletions
Original file line numberDiff line numberDiff line change
@@ -1,85 +1,74 @@
11
/**
22
* SessionDetailPanel — 会话详情面板(原料 / 笔记预览两视图;v0.11.5 产物视图下线)。
33
*
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。
1421
*/
1522
import { useEffect, useState } from "react";
1623
import { useSessionDetailData } from "../hooks/useSessionDetailData";
1724
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";
2428
import WebArticleView from "../components/WebArticleView";
2529
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";
2931

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} 条(画面高频 × 语音低频)`;
3839

3940
interface Props {
4041
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+
/** 转为笔记 / 删除会话 / 重新拉详情(三者都由父层负责反馈与刷新) */
4645
onToNote: (id: number) => void;
47-
/** 删除会话(父层负责确认/反馈/刷新) */
4846
onRemove: (id: number) => void;
49-
/** 重新拉详情(v0.11.5:session:refined 事件驱动屏卡结构回填) */
5047
onRefreshDetail: (id: number) => void;
51-
/** v0.16.1:工作台深链任务 id(对话页任务视图跳转——自动切预览视图并展开工作台) */
48+
/** v0.16.1 工作台深链:任务 id / 消费完成回调(App 清空 focus)/ 精修任务启动回调(→ 对话页) */
5249
autoRefineTaskId?: number | null;
53-
/** v0.16.1:autoTaskId 消费完成回调(App 清空 focus——防陈旧值跨导航复触发) */
5450
onAutoTaskConsumed?: () => void;
55-
/** v0.16.1:精修任务启动回调(→ AI 对话页) */
5651
onRefineTaskStarted?: (sessionId: number, taskId: number) => void;
5752
}
5853

5954
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:两视图
6256
const sessionId = detail.session.id;
63-
// v0.5.0 M7:会话切换回到原料视图(裁决 D1:viewMode 状态留面板;数据面重置见 useSessionDetailData)
57+
// v0.5.0 M7:会话切换回到原料视图(裁决 D1:viewMode 状态留面板;数据面重置见 hook)
6458
useEffect(() => { setViewMode("raw"); }, [sessionId]);
65-
// 数据面(质量/术语/baseUrl/屏→OCR 分组)+ 精修链路(懒触发/事件监听/手动入口/深链快照)
66-
// + 屏卡瞬时态(框选/单屏 toast——由 hook 持有以保证跨原料/预览视图切换不丢,见 hook 文件头)
59+
// 数据面(质量/术语/baseUrl/屏→OCR 分组)+ 精修链路 + 屏卡瞬时态(hook 持有 ⇒ 跨视图切换不丢)
6760
const {
6861
quality, glossary, baseUrl, ocrBlocksByScreen, refining, refineMsg, deepTaskId, setDeepTaskId, startRefine,
6962
selectingScreen, setSelectingScreen, panelToast, showPanelToast, clearPanelToast,
7063
} = useSessionDetailData({ detail, viewMode, onRefreshDetail });
7164
// 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 留本件:工具条三按钮在切换器行里,就近持有回调更少暴露面)。
7367
const [showPass2, setShowPass2] = useState(false);
74-
useEffect(() => setShowPass2(false), [sessionId]);
75-
// v0.20.2(REQ-270):LLM 文本校对面板显隐(会话切换即关)
7668
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 留面板。
8372
useEffect(() => {
8473
if (autoRefineTaskId != null) {
8574
setDeepTaskId(autoRefineTaskId);
@@ -107,191 +96,53 @@ export default function SessionDetailPanel({ detail, fusing, degradedBanner, onT
10796
return (
10897
<>
10998
{/* 降级横幅 + 详情头(改名/状态行/融合中徽标/操作)—— 拆至 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} />
118100

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} />}
156103

157104
{/* v0.7.2(REQ-153):讲者切换(弱化版说话人分离——懒加载幂等;
158105
v0.12.1:图文会话跳过(无音频,不再误报红色错误)) */}
159106
<SpeakerSwitchCard sessionId={sessionId} kind={detail.session.kind} />
160107

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}
205133
/>
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>
245135

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+
/>
295146
</>
296147
);
297148
}

0 commit comments

Comments
 (0)