优化消息内容的换行显示,解决间距过大的问题
parent
5f947b4d64
commit
9c208cd8e1
|
|
@ -1,8 +1,10 @@
|
||||||
import { Button, Dropdown, Space, Tag, Tooltip, Avatar } from 'antd';
|
import { Button, Dropdown, Space, Tag, Tooltip, Avatar } from 'antd';
|
||||||
|
import { useMemo } from 'react';
|
||||||
import { CopyOutlined, SyncOutlined } from '@ant-design/icons';
|
import { CopyOutlined, SyncOutlined } from '@ant-design/icons';
|
||||||
import type { BranchInfo, ChatMessage } from '../../../../api';
|
import type { BranchInfo, ChatMessage } from '../../../../api';
|
||||||
import type { Agent } from '../../../../api/agents';
|
import type { Agent } from '../../../../api/agents';
|
||||||
import Markdown from '../../../../components/Markdown';
|
import Markdown from '../../../../components/Markdown';
|
||||||
|
import { formatMessageContent } from '../../utils/format';
|
||||||
import type { CopyMode } from '../../utils/copy';
|
import type { CopyMode } from '../../utils/copy';
|
||||||
import { ReasoningView, RetrievedView, ToolCallView } from './MetaViews';
|
import { ReasoningView, RetrievedView, ToolCallView } from './MetaViews';
|
||||||
import './MessageItem.css';
|
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 { 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 speakerType = (message as any)?.speaker?.type as ('user' | 'agent' | undefined);
|
||||||
const speakerId = (message as any)?.speaker?.id as string | undefined;
|
const speakerId = (message as any)?.speaker?.id as string | undefined;
|
||||||
const isUser = speakerType ? speakerType === 'user' : message.role === 'user';
|
const isUser = speakerType ? speakerType === 'user' : message.role === 'user';
|
||||||
|
|
@ -66,7 +70,7 @@ export default function MessageItem(props: {
|
||||||
</span>
|
</span>
|
||||||
</div>
|
</div>
|
||||||
<div className={`bubble ${bubbleRole}`}>
|
<div className={`bubble ${bubbleRole}`}>
|
||||||
<Markdown>{message.content}</Markdown>
|
<Markdown>{formattedContent}</Markdown>
|
||||||
<div className="message-item-actions">
|
<div className="message-item-actions">
|
||||||
{hasBranches && (
|
{hasBranches && (
|
||||||
<Space size={2}>
|
<Space size={2}>
|
||||||
|
|
@ -86,8 +90,8 @@ export default function MessageItem(props: {
|
||||||
trigger={['click']}
|
trigger={['click']}
|
||||||
menu={{
|
menu={{
|
||||||
items: [
|
items: [
|
||||||
{ key: 'plain', label: '复制纯文本', onClick: () => onCopy?.(message.content, 'plain') },
|
{ key: 'plain', label: '复制纯文本', onClick: () => onCopy?.(formattedContent, 'plain') },
|
||||||
{ key: 'markdown', label: '复制 Markdown', onClick: () => onCopy?.(message.content, 'markdown') }
|
{ key: 'markdown', label: '复制 Markdown', onClick: () => onCopy?.(formattedContent, 'markdown') }
|
||||||
]
|
]
|
||||||
}}
|
}}
|
||||||
>
|
>
|
||||||
|
|
@ -113,11 +117,11 @@ export default function MessageItem(props: {
|
||||||
<div className="message-item-user">
|
<div className="message-item-user">
|
||||||
<div className="message-item-user-content-wrapper">
|
<div className="message-item-user-content-wrapper">
|
||||||
<div className={`bubble ${bubbleRole}`}>
|
<div className={`bubble ${bubbleRole}`}>
|
||||||
{message.content.includes(' ? (
|
{formattedContent.includes(' ? (
|
||||||
<Markdown>{message.content}</Markdown>
|
<Markdown>{formattedContent}</Markdown>
|
||||||
) : (
|
) : (
|
||||||
<span dangerouslySetInnerHTML={{
|
<span dangerouslySetInnerHTML={{
|
||||||
__html: message.content.replace(/@([^\s]+)/g, '<span class="mention">@$1</span>')
|
__html: formattedContent.replace(/@([^\s]+)/g, '<span class="mention">@$1</span>')
|
||||||
}} />
|
}} />
|
||||||
)}
|
)}
|
||||||
</div>
|
</div>
|
||||||
|
|
|
||||||
|
|
@ -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');
|
||||||
|
}
|
||||||
|
|
@ -751,7 +751,7 @@ body {
|
||||||
}
|
}
|
||||||
|
|
||||||
.chat-body .messages-container {
|
.chat-body .messages-container {
|
||||||
max-width: 780px;
|
max-width: 1080px;
|
||||||
width: 100%;
|
width: 100%;
|
||||||
margin: 0 auto;
|
margin: 0 auto;
|
||||||
padding: 16px 12px 80px;
|
padding: 16px 12px 80px;
|
||||||
|
|
@ -785,6 +785,7 @@ body {
|
||||||
border: 0;
|
border: 0;
|
||||||
border-bottom-left-radius: 5px;
|
border-bottom-left-radius: 5px;
|
||||||
box-shadow: none;
|
box-shadow: none;
|
||||||
|
white-space: normal;
|
||||||
}
|
}
|
||||||
|
|
||||||
.bubble.assistant p {
|
.bubble.assistant p {
|
||||||
|
|
|
||||||
Loading…
Reference in New Issue