diff --git a/src/pages/chat/components/messages/MessageItem.tsx b/src/pages/chat/components/messages/MessageItem.tsx
index bae761b..2040e6c 100644
--- a/src/pages/chat/components/messages/MessageItem.tsx
+++ b/src/pages/chat/components/messages/MessageItem.tsx
@@ -1,8 +1,10 @@
import { Button, Dropdown, Space, Tag, Tooltip, Avatar } from 'antd';
+import { useMemo } from 'react';
import { CopyOutlined, SyncOutlined } from '@ant-design/icons';
import type { BranchInfo, ChatMessage } from '../../../../api';
import type { Agent } from '../../../../api/agents';
import Markdown from '../../../../components/Markdown';
+import { formatMessageContent } from '../../utils/format';
import type { CopyMode } from '../../utils/copy';
import { ReasoningView, RetrievedView, ToolCallView } from './MetaViews';
import './MessageItem.css';
@@ -21,6 +23,8 @@ export default function MessageItem(props: {
}) {
const { message, agentList, currentAgentId, highlighted, branch, busy, onRegenerate, onSwitchBranch, onCopy, isMobile } = props;
+ const formattedContent = useMemo(() => formatMessageContent(message.content), [message.content]);
+
const speakerType = (message as any)?.speaker?.type as ('user' | 'agent' | undefined);
const speakerId = (message as any)?.speaker?.id as string | undefined;
const isUser = speakerType ? speakerType === 'user' : message.role === 'user';
@@ -66,7 +70,7 @@ export default function MessageItem(props: {
-
{message.content}
+
{formattedContent}
{hasBranches && (
@@ -86,8 +90,8 @@ export default function MessageItem(props: {
trigger={['click']}
menu={{
items: [
- { key: 'plain', label: '复制纯文本', onClick: () => onCopy?.(message.content, 'plain') },
- { key: 'markdown', label: '复制 Markdown', onClick: () => onCopy?.(message.content, 'markdown') }
+ { key: 'plain', label: '复制纯文本', onClick: () => onCopy?.(formattedContent, 'plain') },
+ { key: 'markdown', label: '复制 Markdown', onClick: () => onCopy?.(formattedContent, 'markdown') }
]
}}
>
@@ -113,11 +117,11 @@ export default function MessageItem(props: {
- {message.content.includes(' ? (
- {message.content}
+ {formattedContent.includes(' ? (
+ {formattedContent}
) : (
@$1')
+ __html: formattedContent.replace(/@([^\s]+)/g, '@$1')
}} />
)}
diff --git a/src/pages/chat/utils/format.ts b/src/pages/chat/utils/format.ts
new file mode 100644
index 0000000..45e6d22
--- /dev/null
+++ b/src/pages/chat/utils/format.ts
@@ -0,0 +1,18 @@
+/**
+ * 清理消息内容中的冗余换行
+ * 1. 将 3 个及以上的换行替换为 2 个换行
+ * 2. 将特定的 Markdown 块元素前后的双换行替换为单换行,以减少间距
+ */
+export function formatMessageContent(content: string): string {
+ if (!content) return '';
+
+ return content
+ // 将 3 个及以上的换行替换为 2 个
+ .replace(/\n{3,}/g, '\n\n')
+ // 针对 Markdown 块元素,将双换行缩小为单换行
+ // 匹配 \n\n 后面跟着 #, -, *, >, --- 等
+ .replace(/\n\n(?=[#\-*>]|---)/g, '\n')
+ // 匹配 #, -, *, >, --- 后面跟着 \n\n
+ // 由于 JavaScript 不支持所有环境下的 lookbehind,我们先处理上面的
+ .replace(/([#\-*>]|---)\n\n/g, '$1\n');
+}
diff --git a/src/styles.css b/src/styles.css
index 2d30d5e..84c9e51 100644
--- a/src/styles.css
+++ b/src/styles.css
@@ -751,7 +751,7 @@ body {
}
.chat-body .messages-container {
- max-width: 780px;
+ max-width: 1080px;
width: 100%;
margin: 0 auto;
padding: 16px 12px 80px;
@@ -785,6 +785,7 @@ body {
border: 0;
border-bottom-left-radius: 5px;
box-shadow: none;
+ white-space: normal;
}
.bubble.assistant p {