11/**
2- * SessionDetailPanel — 会话详情面板(原料 / 笔记预览两视图 ;v0.11.5 产物视图下线)。
2+ * SessionDetailPanel — 会话详情面板(注册表驱动的多视图 ;v0.11.5 产物视图下线)。
33 *
44 * @ai -context: v0.7.1 自 SessionsPage 拆出(豁免清单登记拆分计划)——质量报告、大纲、
55 * 视图模式为面板内部状态(仅依赖 sessionId),与列表页解耦。
1313 * 分支 → `session-detail/SessionViewHost.tsx`(过渡形态,T10 改注册表驱动);
1414 * 可信度总览卡 + 两裁决面板 + `refineMsg` 行 → `session-detail/SessionAuxBlocks.tsx`。
1515 * 行为等价由 `SessionDetailPanel.test.tsx` 的 P1–P4 在拆前拆后都绿证明。
16- * @ai -context: 本件仍是**唯一数据面**(`useSessionDetailData`)与**唯一视图状态持有者**
17- * (`viewMode` / 深链快照写入——裁决 D1;C5 会在 T10 改判 `:64` 的复位 effect)。
16+ * @ai -context: 批 5 T10(C5 · 本件是**视图记忆的唯一真身与唯一写入者**):视图态从 `useState`
17+ * 迁到 `useViewMemory("session", …)`,键 = `view:default:session`(**不含 `kind`**;
18+ * 代价:web/photo/video 共享一份记忆 —— 已登记)。`objectType` 常量 `"session"` 与
19+ * 该 hook 同住本件(不变量:hook 与它的键口径不能分居两处)。真正的视图宿主是
20+ * `SessionViewHost`,它收 `viewKey` / `onViewKeyChange` 两个受控 props(它自己的
21+ * 头注写了「为什么 hook 上移到面板而不是下沉到宿主」的完整理由)。
22+ * @ai -context: 本件仍是**唯一数据面**(`useSessionDetailData`);`viewMode` 入参按 plan Step 2
23+ * 的逐字口径映射:`viewKey === views[0].key ? "raw" : "preview"`。**等价性**:该 hook
24+ * 只拿 `viewMode` 做「进入原料视图懒触发 `auto_refine_session`」一个判定
25+ * (`useSessionDetailData.ts:169` `if (viewMode !== "raw") return;`),即**只要「当前
26+ * 是不是默认(原文)视图」这一个布尔**;默认视图的键由注册表 `views[0].key` 唯一决定,
27+ * 故 `raw`/`preview` 两档字面量在这里**退化为「默认 / 非默认」**,与 T2 的等价语义一致。
1828 * @ai -context: DOM 顺序契约(逐字不变):`SessionDetailHeader` → `SessionQualityCard` →
1929 * `SpeakerSwitchCard` → 切换器行(含 `SessionRefineSection`)→ `refineMsg` 行
20- * → 视图区 → 两个裁决面板;全部为**同级兄弟**,无新增 wrapper 。
30+ * → 视图区 → 两个裁决面板;全部为**同级兄弟**(视图区内的两个新包裹层见宿主头注) 。
2131 */
2232import { useEffect , useState } from "react" ;
33+ import { convertFileSrc } from "@tauri-apps/api/core" ;
2334import { useSessionDetailData } from "../hooks/useSessionDetailData" ;
2435import SessionDetailHeader from "./session-detail/SessionDetailHeader" ;
2536import SessionRawView from "./session-detail/SessionRawView" ;
2637import SessionViewHost from "./session-detail/SessionViewHost" ;
2738import { SessionAuxPanels , SessionQualityCard } from "./session-detail/SessionAuxBlocks" ;
2839import WebArticleView from "../components/WebArticleView" ;
2940import SpeakerSwitchCard from "../components/SpeakerSwitchCard" ;
41+ import { useViewMemory } from "../views/useViewMemory" ;
42+ import type { SessionViewSlot , ViewSpec } from "../views/registry" ;
3043import type { GlossaryTerm , SessionDetail } from "../types" ;
3144
3245/** 术语表 summary 的三种文案(null=加载中 / 0 条 / N 条)——原文案逐字,条件整串注入视图 */
@@ -39,6 +52,8 @@ const glossarySummaryOf = (glossary: GlossaryTerm[] | null): string =>
3952
4053interface Props {
4154 detail : SessionDetail ;
55+ /** 视图清单(C1①:由 `SessionsPage` 从 `views/registry` 取后注入;本件不 import `viewsFor`) */
56+ views : readonly ViewSpec < SessionViewSlot > [ ] ;
4257 /** 本会话是否融合中(父层 fusingId === detail.session.id) */ fusing : boolean ;
4358 /** 关键降级一次性横幅(null=无) */ degradedBanner : string | null ;
4459 /** 转为笔记 / 删除会话 / 重新拉详情(三者都由父层负责反馈与刷新) */
@@ -51,31 +66,35 @@ interface Props {
5166 onRefineTaskStarted ?: ( sessionId : number , taskId : number ) => void ;
5267}
5368
54- export default function SessionDetailPanel ( { detail, fusing, degradedBanner, onToNote, onRemove, onRefreshDetail, autoRefineTaskId, onAutoTaskConsumed, onRefineTaskStarted } : Props ) {
55- const [ viewMode , setViewMode ] = useState < "raw" | "preview" > ( "raw" ) ; // v0.5.0 M7 + v0.6.0 M6:两视图
69+ export default function SessionDetailPanel ( { detail, views, fusing, degradedBanner, onToNote, onRemove, onRefreshDetail, autoRefineTaskId, onAutoTaskConsumed, onRefineTaskStarted } : Props ) {
5670 const sessionId = detail . session . id ;
57- // v0.5.0 M7:会话切换回到原料视图(裁决 D1:viewMode 状态留面板;数据面重置见 hook)
58- useEffect ( ( ) => { setViewMode ( "raw" ) ; } , [ sessionId ] ) ;
71+ /** 默认视图键 = 注册表 `views[0].key`(A5①:`registry.ts` 的 `[0]` 就是「原文」) */
72+ const defaultKey = views . length > 0 ? views [ 0 ] . key : "" ;
73+ // 批 5 C5:**显式改判**旧「裁决 D1」——视图态改由 `view:default:session` 记忆持有;**有记忆 ⇒ 用
74+ // 记忆值**(切会话不再静默丢弃用户选择)。**旧的「裁决 D1」在视图记忆范围内作废**(其余部分不受
75+ // 影响),见 v0.22「过程中纠正的计划错误」段。T2 转来的 `:64` 复位 effect **整条删除**(不是加
76+ // 条件):记忆 hook 的惰性初始化 + `validKeys` 校验已覆盖「无记忆 ⇒ 默认 `views[0].key`(原文)」。
77+ const [ viewKey , setViewKey ] = useViewMemory ( "session" , defaultKey , views . map ( ( spec ) => spec . key ) ) ;
5978 // 数据面(质量/术语/baseUrl/屏→OCR 分组)+ 精修链路 + 屏卡瞬时态(hook 持有 ⇒ 跨视图切换不丢)
6079 const {
6180 quality, glossary, baseUrl, ocrBlocksByScreen, refining, refineMsg, deepTaskId, setDeepTaskId, startRefine,
6281 selectingScreen, setSelectingScreen, panelToast, showPanelToast, clearPanelToast,
63- } = useSessionDetailData ( { detail, viewMode, onRefreshDetail } ) ;
82+ } = useSessionDetailData ( { detail, viewMode : viewKey === defaultKey ? "raw" : "preview" , onRefreshDetail } ) ;
6483 // REQ-282(v0.19.6):标题行内改名 —— 连同改名状态与提交逻辑拆至 session-detail/SessionDetailHeader.tsx
6584 // v0.20.2(REQ-268/270):两个裁决面板的显隐 —— 「会话切换即关」的 effect 随面板搬至
6685 // `SessionAuxPanels`(state 留本件:工具条三按钮在切换器行里,就近持有回调更少暴露面)。
6786 const [ showPass2 , setShowPass2 ] = useState ( false ) ;
6887 const [ showProofread , setShowProofread ] = useState ( false ) ;
6988 // v0.16.1 工作台深链:autoTaskId 到达即切预览视图。深链快照由 hook 持有——App 侧 focus 清空
7089 // 早于本层 effect,直接透传 prop 会在卡片挂载前被置空(竞态);快照 + 会话切换清除保证
71- // 「只消费一次、不跨会话遗留」。切换 effect 按裁决 D1 留面板。
90+ // 「只消费一次、不跨会话遗留」。切换 effect 按裁决 D1 留面板(T10 起写入视图记忆) 。
7291 useEffect ( ( ) => {
7392 if ( autoRefineTaskId != null ) {
7493 setDeepTaskId ( autoRefineTaskId ) ;
75- setViewMode ( "preview" ) ;
94+ setViewKey ( "preview" ) ;
7695 onAutoTaskConsumed ?.( ) ;
7796 }
78- } , [ autoRefineTaskId , onAutoTaskConsumed , setDeepTaskId ] ) ;
97+ } , [ autoRefineTaskId , onAutoTaskConsumed , setDeepTaskId , setViewKey ] ) ;
7998
8099 // v0.20.4(REQ-303):web 会话专用详情(文章阅读 + 元数据回链 + 转笔记;
81100 // 无时间轴/屏卡/精修面——h2 标题即页标题,改名在会话列表进行)
@@ -93,6 +112,21 @@ export default function SessionDetailPanel({ detail, fusing, degradedBanner, onT
93112 ) ;
94113 }
95114
115+ // 视图槽(C14②):**数据面全在这里** ⇒ 非默认视图自身零取数、零 `invoke`;`imageUrl` 把
116+ // `convertFileSrc` 收口在容器侧(T9 的 K6:视图自己 import Tauri 会破 `views/**` 的边界)。
117+ const slot : SessionViewSlot = {
118+ detail,
119+ imageUrl : ( imageRef ) => ( imageRef && baseUrl ? convertFileSrc ( `${ baseUrl } /${ imageRef } ` ) : null ) ,
120+ ocrBlocksByScreen,
121+ selectingScreen,
122+ onSelectScreen : setSelectingScreen ,
123+ panelToast,
124+ onShowToast : showPanelToast ,
125+ onClearToast : clearPanelToast ,
126+ autoRefineTaskId : deepTaskId ,
127+ onRefineTaskStarted,
128+ } ;
129+
96130 return (
97131 < >
98132 { /* 降级横幅 + 详情头(改名/状态行/融合中徽标/操作)—— 拆至 session-detail/SessionDetailHeader.tsx */ }
@@ -105,33 +139,33 @@ export default function SessionDetailPanel({ detail, fusing, degradedBanner, onT
105139 v0.12.1:图文会话跳过(无音频,不再误报红色错误)) */ }
106140 < SpeakerSwitchCard sessionId = { sessionId } kind = { detail . session . kind } />
107141
108- { /* 切换器行 + 视图区 —— 拆至 SessionViewHost.tsx(过渡形态:手写 2 按钮切换组逐字
109- 搬移,T10 再改注册表驱动的多视图宿主);children = 原文视图节点 */ }
142+ { /* 注册表驱动的视图宿主(T10):默认视图常驻 + 非默认惰性 + C11 错误槽位;
143+ `resident` = 原文视图节点(默认视图**不经** `React.lazy`,同步渲染后常驻) */ }
110144 < SessionViewHost
111- sessionId = { sessionId }
112- viewMode = { viewMode }
113- onViewModeChange = { setViewMode }
114- autoRefineTaskId = { deepTaskId }
115- onRefineTaskStarted = { onRefineTaskStarted }
145+ views = { views }
146+ viewKey = { viewKey }
147+ onViewKeyChange = { setViewKey }
148+ slot = { slot }
149+ resident = {
150+ < SessionRawView
151+ detail = { detail }
152+ baseUrl = { baseUrl }
153+ ocrBlocksByScreen = { ocrBlocksByScreen }
154+ selectingScreen = { selectingScreen }
155+ onSelectScreen = { setSelectingScreen }
156+ panelToast = { panelToast }
157+ onShowToast = { showPanelToast }
158+ onClearToast = { clearPanelToast }
159+ glossarySummary = { glossarySummaryOf ( glossary ) }
160+ glossary = { glossary }
161+ />
162+ }
116163 refining = { refining }
117164 onStartRefine = { startRefine }
118165 canSecondPass = { detail . session . status === "finished" && detail . session . kind !== "photo" }
119166 onOpenPass2 = { ( ) => setShowPass2 ( true ) }
120167 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 }
133- />
134- </ SessionViewHost >
168+ />
135169
136170 { /* refineMsg 行 + 两个裁决面板 —— 拆至 SessionAuxBlocks.tsx 的 SessionAuxPanels */ }
137171 < SessionAuxPanels
0 commit comments