Skip to content

Commit 9dcd49b

Browse files
committed
refactor(components): 聊天 markdown 并入 NoteMarkdown 并删旧件
1 parent d138760 commit 9dcd49b

6 files changed

Lines changed: 121 additions & 67 deletions

File tree

‎app/src/components/ChatMessageList.tsx‎

Lines changed: 5 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -11,7 +11,9 @@
1111
*/
1212
import { useEffect, useRef } from "react";
1313
import type { ChatMessage, KbHit } from "../types";
14-
import ChatMessageMarkdown from "./ChatMessageMarkdown";
14+
// 批 7 T14:站点 #2 `ChatMessageMarkdown` 已并入 `NoteMarkdown`(`content` 槽 = 聊天模式,
15+
// 渲染树逐字节不变 ⇒ 本文件只换 import 源与标签名,props 一字未改)。
16+
import NoteMarkdown from "./NoteMarkdown";
1517
import CitationChips from "./CitationChips";
1618
import { parseKbMeta } from "../utils/kbHits";
1719
import { Surface, Text } from "../ui/primitives";
@@ -114,7 +116,7 @@ export default function ChatMessageList({ messages, streaming, onRegenerate, onE
114116
)}
115117
</Text>
116118
<Surface level="canvas" style={{ padding: "8px 12px" }}>
117-
<ChatMessageMarkdown content={m.content} />
119+
<NoteMarkdown content={m.content} />
118120
{/* v0.19.1:引用 chips(answer 与 hits-only 引导同款展示) */}
119121
{meta && meta.hits.length > 0 && (
120122
<CitationChips hits={meta.hits} onOpenNote={onOpenCitedNote} />
@@ -143,7 +145,7 @@ export default function ChatMessageList({ messages, streaming, onRegenerate, onE
143145
{streaming?.text !== null && streaming?.text !== undefined && (
144146
<div style={{ display: "flex", justifyContent: "flex-start", marginBottom: 12 }}>
145147
<Surface level="canvas" style={{ padding: "8px 12px", maxWidth: "86%" }}>
146-
<ChatMessageMarkdown content={streaming.text} />
148+
<NoteMarkdown content={streaming.text} />
147149
{(streaming.hits?.length ?? 0) > 0 && <CitationChips hits={streaming.hits ?? []} onOpenNote={onOpenCitedNote} />}
148150
<span style={{ display: "inline-block", width: 6, height: 14, background: "#0d9488", verticalAlign: "text-bottom", marginLeft: 2 }} />
149151
</Surface>

‎app/src/components/ChatMessageMarkdown.tsx‎

Lines changed: 0 additions & 52 deletions
This file was deleted.

‎app/src/components/NoteMarkdown.tsx‎

Lines changed: 106 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -1,11 +1,29 @@
11
/**
22
* NoteMarkdown — 笔记阅读视图 Markdown 渲染(H5 自 NotesPage 拆分)。
33
*
4-
* @ai-context: **react-markdown 站点 #1**(全站运行时站点恰 2,另一个是 `ChatMessageMarkdown`;
5-
* 判据 `views/note/noteViews.test.tsx:281-286`)。批 7 T13 拆件后本件只留
6-
* 「四条 remark 插件 + rehype-katex + URL 消毒 + 组件映射装配」这四件事,
7-
* 自定义渲染件与它们的闭包状态**整段**搬进 `noteMarkdownComponents.tsx`(工厂形态)——
8-
* 拆件的唯一目的是给 T14 的「#2 并入 #1」腾出净增头寸(本件拆前只剩 5 行余量)。
4+
* @ai-context: **react-markdown 站点 #1;批 7 T14 把站点 #2 `ChatMessageMarkdown`(52 行)并入本件
5+
* 后删除该文件 ⇒ 全站运行时站点 2 → 1、`remark-*`/`rehype-*` 站点 8 → 4**;判据 =
6+
* `views/note/noteViews.test.tsx:281-286`(三条站点断言,见该文件的 N5)。原站点 #2 的
7+
* 三个消费点(`ChatMessageList.tsx:117` / `:146` · `TaskConversationView.tsx:189`)
8+
* 改为调用本件(`content` 槽,见下)。
9+
* @ai-context: **两模式、两棵渲染树,各自逐字保留**:① 笔记模式(`note` 槽 = 今天的形态**一字未改**);
10+
* ② 聊天模式(`content` 槽 = 并入前 `ChatMessageMarkdown` 的语义**逐字**)。T14 是
11+
* **纯搬迁**(§C26.4 · §C9.19 第 1 条「只搬不改」)⇒ 两侧的 DOM 输出都必须**逐字节不变**:
12+
* 聊天侧保留它自己的 wrapper `<div>`、**三条** remark 插件(无荧光笔)、**默认** URL 消毒
13+
* 与自定 `code` 渲染件;笔记侧保留四条插件 + `noteUrlTransform` + 组件映射工厂。
14+
* @ai-context: **为什么不做「单路径 + `previewMaxChars?` / `codeRenderer?` 参数化」**(计划 Interfaces
15+
* 的原拟形态):① 聊天侧入参是**裸字符串**、没有 `Note`(那一侧根本给不出 `note`/`searchQuery`
16+
* /`onTaskToggle`/`onImageOpen` 四槽);② 两侧的 `components` 映射、外层 wrapper、
17+
* `urlTransform` 全都不同 ⇒ 参数化**必然**改掉其中一侧的 DOM 形态(C10.1 逐字禁止改渲染形态)
18+
* ⇒ 并入的可行形态只能是「同一模块内两条渲染路径,各自逐字保留」。截断语义仍由调用方
19+
* (`TaskConversationView`)用 `truncatePreview` 施加 —— 与并入前**同一处、同一默认值**,
20+
* 不在渲染器内部新增第二条截断路径(那会是行为改动,违反「只搬不改」)。
21+
* @ai-context: 站点判据是**按文件**的(`noteViews.test.tsx` 的 `edgesOf`)⇒ 本件里出现第二条
22+
* `ReactMarkdown` 调用不改变该读数;但本件**仍只许有那 4 行** `remark-*`/`rehype-*` 说明符
23+
* 的 import(多一行会把插件站点读数抬高、污染 C8 的锚)。
24+
* @ai-context: 批 7 T13 拆件后,本件的 `components` 槽由 `noteMarkdownComponents.tsx` 的工厂产出
25+
* (自定义渲染件与它们的闭包状态整段搬去那边);T13 拆件的目的是给 T14 的并入腾头寸
26+
* (拆前只剩 5 行余量)。
927
* @ai-context: 集中全部自定义渲染:任务清单勾选回写(H1)、标题锚点(M5)、
1028
* 搜索高亮(M6)、时间戳回链(L6 radix)、图片/代码/表格样式。
1129
* —— 逐条实现与修复背景见 `noteMarkdownComponents.tsx` 的同名 `@ai-context`。
@@ -18,6 +36,9 @@ import remarkBreaks from "remark-breaks";
1836
import remarkGfm from "remark-gfm";
1937
import remarkMath from "remark-math";
2038
import rehypeKatex from "rehype-katex";
39+
// 批 7 T14:顶层 `katex` 的裸 CSS 说明符随聊天路径一并搬入本件(**不丢** —— 丢掉它 = 全站 `.katex`
40+
// 排版掉样式;两侧都渲染 `.katex` 结点,而全仓只有这一条 CSS 边)。C20.1 的四形态普查按此读数。
41+
import "katex/dist/katex.min.css";
2142
import type { Note } from "../types";
2243
import { remarkMarkHighlight } from "../utils/remarkMarkHighlight";
2344
import { noteMarkdownComponents } from "./noteMarkdownComponents";
@@ -44,7 +65,10 @@ export type RemarkPlugin = NonNullable<Parameters<typeof ReactMarkdown>[0]["rema
4465
*/
4566
export type MarkdownComponents = NonNullable<Parameters<typeof ReactMarkdown>[0]["components"]>;
4667

47-
interface Props {
68+
/**
69+
* **笔记模式**的入参与今天**逐字同义**(T14 未增删任何一项)。
70+
*/
71+
interface NoteModeProps {
4872
note: Note;
4973
/** 搜索关键词(空串=不高亮)——由 NoteReadingView 按 searchActive 门控传入 */
5074
searchQuery: string;
@@ -68,6 +92,18 @@ interface Props {
6892
remarkPluginsExtra?: readonly RemarkPlugin[];
6993
}
7094

95+
/**
96+
* **聊天模式**的入参(批 7 T14 并入的站点 #2 形态)—— 只有一条:裸 markdown 文本。
97+
*
98+
* @ai-context 判据:这条槽的存在使得「3 个消费点改调 `NoteMarkdown`」不必伪造 `Note`。缺省不出现
99+
* ⇒ 笔记模式的类型面**一字未变**(`note` 仍是必填)。
100+
*/
101+
interface ChatModeProps {
102+
content: string;
103+
}
104+
105+
export type NoteMarkdownProps = NoteModeProps | ChatModeProps;
106+
71107
/**
72108
* `[[ts:ms]]` 回链的**内部锚点**语法 + URL 消毒的唯一出口(批 6 T26 实测)。
73109
*
@@ -76,14 +112,26 @@ interface Props {
76112
* **根本没渲染**(落成 `<a href="">⏱ 00:52</a>`,下面的 `tsMatch` 永不命中;比 R5.5 顺带④ 记的还坏)。
77113
* 安全面零放松:只放行这一种形态(解码回规范形),其余全交回默认消毒器;该 href 仅用于识别,最终渲染成 `<span>`。
78114
* ⇄ 规范形的**匹配**在 `noteMarkdownComponents.tsx` 的 `TS_HREF_RE`(`a` 渲染件用)。
115+
* ⚠️ 聊天模式**不走**它(并入前 `ChatMessageMarkdown` 也没有 `urlTransform`):那一侧保留默认消毒器,
116+
* 否则 `[[ts:ms]]` 会在聊天里变成回链芯片 —— 那是**行为变化**,「只搬不改」不允许。
79117
*/
80118
const TS_HREF_ENCODED_RE = /^%5B%5Bts:(\d+)%5D%5D$/i;
81119
const noteUrlTransform = (url: string): string => {
82120
const m = TS_HREF_ENCODED_RE.exec(url);
83121
return m ? `[[ts:${m[1]}]]` : defaultUrlTransform(url);
84122
};
85123

86-
export default function NoteMarkdown({ note, searchQuery, onTaskToggle, onOpenSession, onOpenSessionAt, onImageOpen, remarkPluginsExtra }: Props) {
124+
/**
125+
* 全站唯一的 markdown 渲染入口(批 7 T14 归一后的形态)。
126+
*
127+
* @ai-context 分派:`content` 槽 ⇒ 聊天模式(`ChatMarkdown`,并入前的站点 #2 逐字);
128+
* 否则 ⇒ 笔记模式(今天的路径逐字)。**两模式不会互相切换**(同一调用点的槽固定)⇒
129+
* `ChatMarkdown` 与笔记模式的 hooks(`noteMarkdownComponents` 工厂内的两个 `useMemo`/两个 `useRef`)
130+
* 各自在各自的分支里无条件执行,hooks 顺序与并入前**同序**。
131+
*/
132+
export default function NoteMarkdown(props: NoteMarkdownProps) {
133+
if ("content" in props) return <ChatMarkdown content={props.content} />;
134+
const { note, searchQuery, onTaskToggle, onOpenSession, onOpenSessionAt, onImageOpen, remarkPluginsExtra } = props;
87135
return (
88136
<ReactMarkdown
89137
// v0.15:remark-breaks——单换行(软换行)渲染为 <br>,与编辑态所见一致
@@ -102,3 +150,54 @@ export default function NoteMarkdown({ note, searchQuery, onTaskToggle, onOpenSe
102150
</ReactMarkdown>
103151
);
104152
}
153+
154+
/* ------------------------------------------------------------------------------------------------
155+
* 以下为批 7 T14 并入的**站点 #2**(原 `components/ChatMessageMarkdown.tsx`,52 行):
156+
* 组件体、`PREVIEW_MAX_CHARS` 与 `truncatePreview` **逐字**搬来(只去缩进/改组件名与可见性),
157+
* 语义一字未改 —— 证据 = 并入前/后两棵树的渲染输出逐字节对拍(§C26.4,探针在 `tmp/t14/`)。
158+
* ---------------------------------------------------------------------------------------------- */
159+
160+
/** 深度预览截断(轨迹/结果展开用——长文不撑爆 DOM) */
161+
export const PREVIEW_MAX_CHARS = 2000;
162+
163+
export function truncatePreview(text: string, max: number = PREVIEW_MAX_CHARS): string {
164+
if (text.length <= max) return text;
165+
return `${text.slice(0, max)}\n\n…(内容过长已截断,仅展示前 ${max} 字符)`;
166+
}
167+
168+
/**
169+
* 聊天消息的渲染体(并入前 = `ChatMessageMarkdown` 的默认导出)。
170+
*
171+
* @ai-context: 聊天渲染专用轻量栈(GFM + 数学 + 换行),**不复用**笔记域的组件映射
172+
* (那是笔记域——任务勾选回写/时间戳回链/图片组件耦合笔记上下文);
173+
* 聊天消息无这些语义,保持渲染器单一职责。
174+
* @ai-context 为什么保留独立 wrapper `<div>`:并入前它就带 `fontSize: 13.5 / lineHeight: 1.7 /
175+
* wordBreak: break-word`,而笔记模式**不套 wrapper**(`NoteMarkdown.test.tsx:89` 甚至
176+
* 断言根下无 `div`)⇒ 两者不能共用一层。
177+
*/
178+
function ChatMarkdown({ content }: ChatModeProps) {
179+
return (
180+
<div style={{ fontSize: 13.5, lineHeight: 1.7, wordBreak: "break-word" }}>
181+
<ReactMarkdown
182+
remarkPlugins={[remarkGfm, remarkMath, remarkBreaks]}
183+
rehypePlugins={[rehypeKatex]}
184+
components={{
185+
code: ({ className, children }) => {
186+
const isBlock = className?.includes("language-");
187+
const text = String(children).replace(/\n$/, "");
188+
if (isBlock) {
189+
return (
190+
<pre style={{ background: "#f6f8fa", padding: 10, borderRadius: 6, overflowX: "auto" }}>
191+
<code className={className}>{text}</code>
192+
</pre>
193+
);
194+
}
195+
return <code style={{ background: "#f1f5f9", padding: "1px 4px", borderRadius: 4 }}>{text}</code>;
196+
},
197+
}}
198+
>
199+
{content}
200+
</ReactMarkdown>
201+
</div>
202+
);
203+
}

‎app/src/components/TaskConversationView.tsx‎

Lines changed: 5 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -11,7 +11,10 @@
1111
import { useState } from "react";
1212
import { invoke } from "@tauri-apps/api/core";
1313
import type { AiEnrichResult, AiTaskRecord, AiTurn } from "../types";
14-
import ChatMessageMarkdown, { truncatePreview } from "./ChatMessageMarkdown";
14+
// 批 7 T14:站点 #2 `ChatMessageMarkdown` 已并入 `NoteMarkdown`(`content` 槽 = 聊天模式);
15+
// `truncatePreview` / `PREVIEW_MAX_CHARS` 随代码搬到同一件 ⇒ 截断仍在**本文件**(调用方)施加,
16+
// 默认值 2000 与并入前逐字相同(`chatHelpers.test.ts` 的三条判据钉住)。
17+
import NoteMarkdown, { truncatePreview } from "./NoteMarkdown";
1518
import { Surface, Text } from "../ui/primitives";
1619

1720
interface Props {
@@ -186,7 +189,7 @@ export default function TaskConversationView({ task, turns, refTitle, onOpenSess
186189
</button>
187190
{openResult && (
188191
<Surface level="canvas" style={{ padding: "10px 14px", marginTop: 6, maxHeight: 420, overflowY: "auto" }}>
189-
<ChatMessageMarkdown content={truncatePreview(extractResultMarkdown(task.resultJson))} />
192+
<NoteMarkdown content={truncatePreview(extractResultMarkdown(task.resultJson))} />
190193
</Surface>
191194
)}
192195
</div>

‎app/src/components/chatHelpers.test.ts‎

Lines changed: 3 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -7,7 +7,9 @@
77
*/
88
import { describe, expect, it } from "vitest";
99
import { parseUsage } from "./ChatMessageList";
10-
import { truncatePreview } from "./ChatMessageMarkdown";
10+
// 批 7 T14:`truncatePreview` 随 `ChatMessageMarkdown` 并入 `NoteMarkdown`(搬迁的必需后果:
11+
// 只换 import 源,三条断言与实参一字未改)。
12+
import { truncatePreview } from "./NoteMarkdown";
1113

1214
describe("parseUsage", () => {
1315
it("解析 OpenAI 口径 total_tokens", () => {

‎app/src/ui/primitives/surfaceBaseline.ts‎

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -114,7 +114,7 @@ export const FROZEN_BORDER_BY_FILE: Readonly<Record<string, number>> = {
114114
"components/NoteEditView.tsx": 4, "components/NoteHeaderActions.tsx": 1,
115115
"components/NoteImage.tsx": 1, "components/NoteLinkToSystem.tsx": 2,
116116
"components/NoteListBatchMenu.tsx": 2, "components/NoteListToolbar.tsx": 3,
117-
"components/NoteListView.tsx": 1, "components/NoteMarkdown.tsx": 1, "components/NoteMoveToGroupMenu.tsx": 1,
117+
"components/NoteListView.tsx": 1, "components/NoteMoveToGroupMenu.tsx": 1,
118118
"components/NotePreviewView.tsx": 3, "components/NoteReadingView.tsx": 2,
119119
"components/NoteRowContextMenu.tsx": 1, "components/OcrDeviceSetting.tsx": 2,
120120
"components/PhotoCapturePanel.tsx": 3, "components/PracticeQuestionsOverlays.tsx": 4,
@@ -174,7 +174,7 @@ export const FROZEN_RADIUS_OUTLIER_BY_FILE: Readonly<Record<string, number>> = {
174174
"components/ModelDiskPanel.tsx": 1, "components/NoteAiDialog.tsx": 4,
175175
"components/NoteEditView.tsx": 1, "components/NoteHeaderActions.tsx": 2,
176176
"components/NoteImage.tsx": 2, "components/NoteLinkToSystem.tsx": 2,
177-
"components/NoteListToolbar.tsx": 2, "components/NoteMoveToGroupMenu.tsx": 1,
177+
"components/NoteListToolbar.tsx": 2, "components/NoteMarkdown.tsx": 1, "components/NoteMoveToGroupMenu.tsx": 1,
178178
"components/NotePreviewView.tsx": 4, "components/NoteRowContextMenu.tsx": 1,
179179
"components/OcrDeviceSetting.tsx": 3, "components/PhotoCapturePanel.tsx": 7,
180180
"components/PracticeQuestionsOverlays.tsx": 1, "components/PromoteCardButton.tsx": 3,

0 commit comments

Comments
 (0)