diff --git a/package.json b/package.json index e3457f33..eb072dd0 100644 --- a/package.json +++ b/package.json @@ -27,6 +27,7 @@ "clsx": "^2.1.1", "cmdk": "^1.1.1", "lucide-react": "^1.45.0", + "marked": "^18.1.1", "radix-ui": "^1.6.7", "react": "^19.2.0", "react-day-picker": "^10.0.1", diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml index 678d4d7d..6fab7dc4 100644 --- a/pnpm-lock.yaml +++ b/pnpm-lock.yaml @@ -47,6 +47,9 @@ importers: lucide-react: specifier: ^1.45.0 version: 1.45.0(react@19.2.8) + marked: + specifier: ^18.1.1 + version: 18.1.1 radix-ui: specifier: ^1.6.7 version: 1.6.7(@types/react-dom@19.2.7(@types/react@19.2.18))(@types/react@19.2.18)(react-dom@19.2.8(react@19.2.8))(react@19.2.8) @@ -2538,6 +2541,11 @@ packages: magic-string@1.4.3: resolution: {integrity: sha512-z12OxmaPGE0F4xlpGdjuAUckipLpQlTAuAmyGhNoD7ODpYUzvDfvtxUbjM/jwznyP178oju/KDdyi220wNJ0RQ==} + marked@18.1.1: + resolution: {integrity: sha512-zEGgrtUG8crk+TrMacJSVABU4DYDDkS8uqgU3ohrn/2fW9C2AEfUrO3m6IhpIpQpl7YcVpiXXC9cSMWDANT3dg==} + engines: {node: '>= 20'} + hasBin: true + math-intrinsics@1.1.0: resolution: {integrity: sha512-/IXtbwEk5HTPyEwyKX6hGkYXxM9nbj64B+ilVJnC/R6B0pH5G4V3b0pVbL7DBj4tkhBAppbQUlf6F6Xl9LHu1g==} engines: {node: '>= 0.4'} @@ -5529,6 +5537,8 @@ snapshots: dependencies: '@jridgewell/sourcemap-codec': 1.6.0 + marked@18.1.1: {} + math-intrinsics@1.1.0: {} media-typer@1.1.1: {} diff --git a/src/traffic/Conversation.tsx b/src/traffic/Conversation.tsx index e7424f4c..2c7250fd 100644 --- a/src/traffic/Conversation.tsx +++ b/src/traffic/Conversation.tsx @@ -19,6 +19,7 @@ import { cn } from "@/lib/utils"; import { useHitTone, useMarker, type Mark } from "@/security/Highlight"; import { prettyJson } from "@/prettyJson"; import { BodyText } from "./drawer/Payload"; +import { Markdown } from "./Markdown"; import type { RequestRow, Transcript, @@ -687,7 +688,8 @@ export function BlockLine({ b }: { b: Block }) { const t = useText(conversationText); return ( - + {/* 用户自己打的字原样显示,模型写的按 Markdown 排 */} + ); } @@ -731,7 +733,7 @@ function EarlierBlocks({ messages }: { messages: readonly TranscriptMessage[] }) } /** 一块里的几段,上下排。相邻的图片和附件排成一行 */ -export function Parts({ parts }: { parts: readonly TranscriptPart[] }) { +export function Parts({ parts, plain = false }: { parts: readonly TranscriptPart[]; plain?: boolean }) { const runs: (TranscriptPart | TranscriptPart[])[] = []; for (const p of parts) { const chip = p.kind === "image" || p.kind === "other"; @@ -749,17 +751,17 @@ export function Parts({ parts }: { parts: readonly TranscriptPart[] }) { ))} ) : ( - + ), )} ); } -function Part({ p }: { p: TranscriptPart }) { +function Part({ p, plain }: { p: TranscriptPart; plain: boolean }) { switch (p.kind) { case "text": - return ; + return ; case "thinking": return ; case "tool_call": @@ -773,27 +775,34 @@ function Part({ p }: { p: TranscriptPart }) { } /** - * 说的话:原样的空白和换行,不当 Markdown 解析(模型写的 `**` 和 `#` 就是那几个字符)。 - * 长的先显示开头,「展开全部」看整段。 + * 说的话:模型写的(回答、思考、系统提示)按 Markdown 排(`Markdown`);`plain` 的(用户打的字) + * 原样的空白和换行。长的先显示开头,「展开全部」看整段。 */ -export function Prose({ text, muted = false }: { text: string; muted?: boolean }) { +export function Prose({ text, muted = false, plain = false }: { text: string; muted?: boolean; plain?: boolean }) { const t = useText(conversationText); const [all, setAll] = useState(false); const head = useMemo(() => clip(text, PROSE), [text]); const cut = head !== null && !all; const mark = useMarker(); + const ellipsis = …; return (
{/* 上边 2px:一行字的中线和左边标签的中线对齐(标签那一格 24px 高,字的行高 19.5px) */} -

- {mark(cut ? head : text)} - {cut && …} -

+ {plain ? ( +

+ {mark(cut ? head : text)} + {cut && ellipsis} +

+ ) : ( +
+ +
+ )} {head !== null && (