From fc7e0316eedd82507683f9e1c1dcfc36f25b8785 Mon Sep 17 00:00:00 2001 From: Your Name Date: Sat, 7 Feb 2026 21:19:26 +0800 Subject: [PATCH] =?UTF-8?q?=E5=A2=9E=E5=BC=BA=E5=9B=BE=E7=89=87markdown?= =?UTF-8?q?=E6=B8=B2=E6=9F=93=E5=85=9C=E5=BA=95=E9=81=BF=E5=85=8D=E7=BB=93?= =?UTF-8?q?=E6=9E=9C=E4=B8=8D=E6=98=BE=E7=A4=BA?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- components/elements/response.tsx | 153 +++++++++++++++++++++++++------ 1 file changed, 124 insertions(+), 29 deletions(-) diff --git a/components/elements/response.tsx b/components/elements/response.tsx index 6e6d314..a8fedd8 100644 --- a/components/elements/response.tsx +++ b/components/elements/response.tsx @@ -21,6 +21,43 @@ type ResponseProps = ComponentProps & { isProcessingWatermarkRemove?: boolean; }; +function splitMarkdownImages(content: string) { + const segments: Array< + | { type: "text"; value: string } + | { type: "image"; alt?: string; src: string } + > = []; + + const imageRegex = /!\[([^\]]*)\]\((https?:\/\/[^\s)]+)\)/g; + let lastIndex = 0; + let match: RegExpExecArray | null; + + while ((match = imageRegex.exec(content)) !== null) { + if (match.index > lastIndex) { + segments.push({ + type: "text", + value: content.slice(lastIndex, match.index), + }); + } + + segments.push({ + type: "image", + alt: match[1], + src: match[2], + }); + + lastIndex = imageRegex.lastIndex; + } + + if (lastIndex < content.length) { + segments.push({ + type: "text", + value: content.slice(lastIndex), + }); + } + + return segments; +} + // 注意:移除了 memo 的自定义比较函数,确保 isProcessing* 状态变化时组件能正确重新渲染 // 之前的 memo 比较函数可能导致 Streamdown 内部的 ImagePreview 组件没有正确更新 export const Response = ({ @@ -38,24 +75,25 @@ export const Response = ({ isProcessingAngleEdit, isProcessingWatermarkRemove, ...props -}: ResponseProps) => ( - *:first-child]:mt-0 [&>*:last-child]:mb-0 [&_code]:whitespace-pre-wrap [&_code]:break-words [&_pre]:max-w-full [&_pre]:overflow-x-auto", - className - )} - components={{ - img: ({ src, alt }: { src?: string; alt?: string }) => { - if (!src) { - return null; - } - // 使用 key 强制在处理状态变化时重新渲染组件 - const processingKey = `${isProcessingEnhance}-${isProcessingRmbg}-${isProcessingExtend}-${isProcessingAngleEdit}-${isProcessingWatermarkRemove}`; - return { + const streamdownClassName = cn( + "size-full [&>*:first-child]:mt-0 [&>*:last-child]:mb-0 [&_code]:whitespace-pre-wrap [&_code]:break-words [&_pre]:max-w-full [&_pre]:overflow-x-auto", + className + ); + const processingKey = `${isProcessingEnhance}-${isProcessingRmbg}-${isProcessingExtend}-${isProcessingAngleEdit}-${isProcessingWatermarkRemove}`; + + const streamdownComponents = { + img: ({ src, alt }: { src?: string; alt?: string }) => { + if (!src) { + return null; + } + + return ( + ; - }, - video: ({ src, alt }: { src?: string; alt?: string }) => { - if (!src) { - return null; - } - return ; - }, - }} - {...props} - /> -); + /> + ); + }, + video: ({ src, alt }: { src?: string; alt?: string }) => { + if (!src) { + return null; + } + return ; + }, + }; + + const rawText = typeof props.children === "string" ? props.children : null; + if (rawText && rawText.includes("![") && rawText.includes("](")) { + const segments = splitMarkdownImages(rawText); + const hasImageSegments = segments.some((segment) => segment.type === "image"); + + if (hasImageSegments) { + return ( +
+ {segments.map((segment, index) => { + if (segment.type === "image") { + return ( + + ); + } + + if (!segment.value.trim()) { + return null; + } + + return ( + + {segment.value} + + ); + })} +
+ ); + } + } + + return ( + + ); +}; Response.displayName = "Response";