一、Chat页面输入框的修改
1. macOS配置
我使用MacBook Pro,chip 是 Apple M3 Pro,Memory是18GB,macOS是 Sonoma 14.6.1。
2. 修改chat输入框代码
目前RAGFlow前端的chat功能,输入的内容是单行的,不能主动使用Shift+Enter实现分行。根据 src/pages/chat/index.tsx
文件,可以看出该文件是聊天页面的主入口,整体结构是将聊天内容通过 <ChatContainer />
组件呈现。因此,如果要实现多行文本框功能,主要修改点会在 ChatContainer
组件的实现中。
在 chat/chat-container/index.tsx
中,可以看到消息输入功能是通过 <MessageInput />
组件实现的。如果需要将单行输入框改为支持多行输入的 TextArea
,需要修改 MessageInput
组件的实现。
修改src/components/message-input/index.tsx的代码如下:
return (<FlexclassName={styles.messageInputWrapper}style={{backgroundColor: '#f7f8fa', // 淡灰色背景border: '1px solid #e0e0e0', // 外部边框颜色borderRadius: '12px', // 圆角增加为原来的 1.5 倍padding: '10px 12px', // 内边距boxShadow: '0 2px 8px rgba(0, 0, 0, 0.1)', // 添加阴影}}vertical>{/* 输入框 */}<Input.TextAreasize="large"placeholder={t('sendPlaceholder')}value={value}disabled={disabled}autoSize={{ minRows: 1, maxRows: 6 }} // 默认一行,自动调整至 6 行style={{flex: 1,border: 'none', // 禁用自带边框outline: 'none', // 去掉选中高亮boxShadow: 'none', // 禁用焦点样式resize: 'none', // 禁用用户手动调整大小fontSize: '14px',lineHeight: '20px', // 行高,保证单行内容视觉效果padding: '0', // 去掉多余的填充// overflow: 'hidden', // 禁止滚动条显示backgroundColor: '#f7f8fa', // 与外层背景色一致}}onPressEnter={(e) => {if (!e.shiftKey) {e.preventDefault();handlePressEnter();}}}onChange={onInputChange as ChangeEventHandler<HTMLTextAreaElement>}/>{/* 按钮区域 */}<Flexjustify="space-between"align="center"style={{marginTop: '8px',}}>{showUploadIcon && (<UploadonPreview={handlePreview}onChange={handleChange}multiple={false}onRemove={handleRemove}showUploadList={false}beforeUpload={() => {return false;}}><Buttontype={'text'}disabled={disabled}icon={<SvgIconname="paper-clip"width={18}height={22}disabled={disabled}></SvgIcon>}></Button></Upload>)}<Buttontype="primary"onClick={handlePressEnter}loading={sendLoading}disabled={sendDisabled || isUploadingFile}style={{height: '40px',borderRadius: '12px', // 按钮圆角同步调整padding: '0 16px',}}>{t('send')}</Button></Flex>
实际页面输入效果如下:
2.1 替换Input为 Input.TextArea
将 Input
替换为 Input.TextArea
,并添加 autoSize
属性,以实现多行输入框的自动伸缩功能。
2.2 修改发送逻辑
在原有逻辑中,按 Enter
会直接触发消息发送。对于多行输入框,需要支持:
-
按
Shift + Enter
换行。 -
按
Enter
发送消息。
上面代码中,onPressEnter
事件已经处理了此逻辑。
二、Agent Flow页面中输入框的修改
项目的Agent页面上还有chat,改了component下的message-input,对这个chat不起作用。修改src/pages/flow/box.tsx,关键点说明:
-
Input.TextArea
的使用- 替换了原来的
Input
,支持多行输入。 autoSize
参数允许输入框高度根据内容自动扩展。
- 替换了原来的
-
Shift + Enter
处理- 检测
e.shiftKey
是否被按下。 - 当
Shift
被按下时,不触发消息发送,只换行。 - 当未按下
Shift
时,发送消息并阻止默认行为。
- 检测
-
suffix
按钮- 保留了发送按钮的逻辑,用户也可以点击按钮发送消息。
return (<><Flex flex={1} className={styles.chatContainer} vertical><Flex flex={1} vertical className={styles.messageContainer}><div><Spin spinning={loading}>{derivedMessages?.map((message, i) => {return (<MessageItemloading={message.role === MessageType.Assistant &&sendLoading &&derivedMessages.length - 1 === i}key={message.id}nickname={userInfo.nickname}avatar={userInfo.avatar}item={message}reference={buildMessageItemReference({ message: derivedMessages, reference },message,)}clickDocumentButton={clickDocumentButton}index={i}showLikeButton={false}sendLoading={sendLoading}></MessageItem>);})}</Spin></div><div ref={ref} /></Flex><Flexalign="flex-start" // 改为 flex-start,使内容顶部对齐style={{padding: '12px 20px',backgroundColor: '#ffffff', // 白色背景borderTop: '1px solid #e8e8e8', // 分割线颜色position: 'sticky', // 固定在底部bottom: 0,zIndex: 100, // 确保浮于内容上方}}><Input.TextAreaplaceholder={t('sendPlaceholder')}value={value}autoSize={{ minRows: 1, maxRows: 6 }} // 自动调整高度onChange={handleInputChange as React.ChangeEventHandler<HTMLTextAreaElement>}onPressEnter={(e) => {if (!e.shiftKey) { // Shift+Enter 换行e.preventDefault();handlePressEnter();}}}style={{flex: 1,border: '1px solid #e0e0e0', // 边框颜色borderRadius: '8px', // 圆角边框padding: '10px 12px',fontSize: '14px',lineHeight: '20px',boxShadow: 'none', // 去除阴影resize: 'none', // 禁止拖动调整大小}}/><Buttontype="primary"onClick={handlePressEnter}loading={sendLoading}style={{marginLeft: '10px',borderRadius: '8px',padding: '0 16px',height: '40px',fontSize: '14px',display: 'flex',alignItems: 'center', // 保持内容居中justifyContent: 'center',marginTop: 'auto', // 自动保持按钮与输入框底部对齐}}>{t('send')}</Button></Flex></Flex><PdfDrawervisible={visible}hideModal={hideModal}documentId={documentId}chunk={selectedChunk}></PdfDrawer></>);
实际页面输入效果如下:
三、消息框中的显示内容的修改
虽然对话的多行输入没有问题了,对话chat上的消息显示没有跟随输入分行,只是将分行的地方加了一个空格,显得很怪异,现在将chat的消息显示也适配一下多行。
1. 修改src/components/message-item/index.tsx:
要实现消息内容中的换行处理,确保用户输入的内容能够正确地显示多行,我们需要确保在 MessageItem
组件中渲染消息文本时能够正确处理换行符。
修改目标:
- 支持多行显示:当用户发送多行消息时,确保文本能够按行显示,而不仅仅是将换行符替换为空格。
- CSS 样式处理:通过合适的 CSS 属性(如
white-space: pre-line
)来保留换行符。
主要改动:
- 在
MessageItem
组件中确保显示消息的部分使用正确的white-space
样式。 - 如果
item.content
包含换行符,它们将被正确处理并显示为多行。
import { ReactComponent as AssistantIcon } from '@/assets/svg/assistant.svg';
import { MessageType } from '@/constants/chat';
import { useSetModalState } from '@/hooks/common-hooks';
import { IReference } from '@/interfaces/database/chat';
import { IChunk } from '@/interfaces/database/knowledge';
import classNames from 'classnames';
import { memo, useCallback, useEffect, useMemo, useState } from 'react';import {useFetchDocumentInfosByIds,useFetchDocumentThumbnailsByIds,
} from '@/hooks/document-hooks';
import { IRegenerateMessage, IRemoveMessageById } from '@/hooks/logic-hooks';
import { IMessage } from '@/pages/chat/interface';
import MarkdownContent from '@/pages/chat/markdown-content';
import { getExtension, isImage } from '@/utils/document-util';
import { Avatar, Button, Flex, List, Space, Typography } from 'antd';
import FileIcon from '../file-icon';
import IndentedTreeModal from '../indented-tree/modal';
import NewDocumentLink from '../new-document-link';
import { AssistantGroupButton, UserGroupButton } from './group-button';
import styles from './index.less';const { Text } = Typography;interface IProps extends Partial<IRemoveMessageById>, IRegenerateMessage {item: IMessage;reference: IReference;loading?: boolean;sendLoading?: boolean;nickname?: string;avatar?: string;clickDocumentButton?: (documentId: string, chunk: IChunk) => void;index: number;showLikeButton?: boolean;
}const MessageItem = ({item,reference,loading = false,avatar = '',sendLoading = false,clickDocumentButton,index,removeMessageById,regenerateMessage,showLikeButton = true,
}: IProps) => {const isAssistant = item.role === MessageType.Assistant;const isUser = item.role === MessageType.User;const { data: documentList, setDocumentIds } = useFetchDocumentInfosByIds();const { data: documentThumbnails, setDocumentIds: setIds } =useFetchDocumentThumbnailsByIds();const { visible, hideModal, showModal } = useSetModalState();const [clickedDocumentId, setClickedDocumentId] = useState('');const referenceDocumentList = useMemo(() => {return reference?.doc_aggs ?? [];}, [reference?.doc_aggs]);const handleUserDocumentClick = useCallback((id: string) => () => {setClickedDocumentId(id);showModal();},[showModal],);const handleRegenerateMessage = useCallback(() => {regenerateMessage?.(item);}, [regenerateMessage, item]);useEffect(() => {const ids = item?.doc_ids ?? [];if (ids.length) {setDocumentIds(ids);const documentIds = ids.filter((x) => !(x in documentThumbnails));if (documentIds.length) {setIds(documentIds);}}}, [item.doc_ids, setDocumentIds, setIds, documentThumbnails]);return (<divclassName={classNames(styles.messageItem, {[styles.messageItemLeft]: item.role === MessageType.Assistant,[styles.messageItemRight]: item.role === MessageType.User,})}><sectionclassName={classNames(styles.messageItemSection, {[styles.messageItemSectionLeft]: item.role === MessageType.Assistant,[styles.messageItemSectionRight]: item.role === MessageType.User,})}><divclassName={classNames(styles.messageItemContent, {[styles.messageItemContentReverse]: item.role === MessageType.User,})}>{item.role === MessageType.User ? (<Avatarsize={40}src={avatar ??'https://zos.alipayobjects.com/rmsportal/jkjgkEfvpUPVyRjUImniVslZfWPnJuuZ.png'}/>) : (<AssistantIcon></AssistantIcon>)}<Flex vertical gap={8} flex={1}><Space>{isAssistant ? (index !== 0 && (<AssistantGroupButtonmessageId={item.id}content={item.content}prompt={item.prompt}showLikeButton={showLikeButton}audioBinary={item.audio_binary}></AssistantGroupButton>)) : (<UserGroupButtoncontent={item.content}messageId={item.id}removeMessageById={removeMessageById}regenerateMessage={regenerateMessage && handleRegenerateMessage}sendLoading={sendLoading}></UserGroupButton>)}{/* <b>{isAssistant ? '' : nickname}</b> */}</Space><divclassName={isAssistant ? styles.messageText : styles.messageUserText}style={{ whiteSpace: 'pre-line' }} // 保留换行符并自动换行><MarkdownContentloading={loading}content={item.content}reference={reference}clickDocumentButton={clickDocumentButton}></MarkdownContent></div>{isAssistant && referenceDocumentList.length > 0 && (<ListbordereddataSource={referenceDocumentList}renderItem={(item) => {return (<List.Item><Flex gap={'small'} align="center"><FileIconid={item.doc_id}name={item.doc_name}></FileIcon><NewDocumentLinkdocumentId={item.doc_id}documentName={item.doc_name}prefix="document">{item.doc_name}</NewDocumentLink></Flex></List.Item>);}}/>)}{isUser && documentList.length > 0 && (<ListbordereddataSource={documentList}renderItem={(item) => {// TODO:const fileThumbnail =documentThumbnails[item.id] || documentThumbnails[item.id];const fileExtension = getExtension(item.name);return (<List.Item><Flex gap={'small'} align="center"><FileIcon id={item.id} name={item.name}></FileIcon>{isImage(fileExtension) ? (<NewDocumentLinkdocumentId={item.id}documentName={item.name}prefix="document">{item.name}</NewDocumentLink>) : (<Buttontype={'text'}onClick={handleUserDocumentClick(item.id)}><Textstyle={{ maxWidth: '40vw' }}ellipsis={{ tooltip: item.name }}>{item.name}</Text></Button>)}</Flex></List.Item>);}}/>)}</Flex></div></section>{visible && (<IndentedTreeModalvisible={visible}hideModal={hideModal}documentId={clickedDocumentId}></IndentedTreeModal>)}</div>);
};export default memo(MessageItem);
2. 修改src/components/message-item/index.less:
要确保文本内容(特别是多行消息)能够正确显示换行符并且样式合理,我们可以对现有的 .messageText
和 .messageUserText
样式做一些调整。以下是针对 index.less
样式的改进:
关键改动:
- 保留换行符: 使用
white-space: pre-line
来保留文本中的换行符(\n
),并且自动换行。 - 避免内容溢出: 适当设置
word-break
和overflow-wrap
属性,以确保长单词或无空格的长文本能够正确换行,避免溢出。 - 简化重复的
.messageText
和.messageUserText
样式: 让这两者有一个统一的基础样式,便于管理。
.messageItem {padding: 24px 0;.messageItemSection {display: inline-block;}.messageItemSectionLeft {width: 80%;}.messageItemSectionRight {// width: 80%;// max-width: 50vw;}.messageItemContent {display: inline-flex;gap: 20px;flex-wrap: wrap; // 允许内容换行}.messageItemContentReverse {flex-direction: row-reverse;}.messageText {.chunkText();padding: 0 14px;background-color: rgba(249, 250, 251, 1);word-break: break-all;}/* 共同的文本样式基础 */.messageTextBase {padding: 6px 10px;border-radius: 8px;word-wrap: break-word; // 强制长单词换行overflow-wrap: break-word; // 强制长单词换行white-space: pre-line; // 保留换行符并换行}/* Assistant 消息文本样式 */.messageText {.chunkText();.messageTextBase();background-color: #e6f4ff;word-break: break-word; // 自动换行}/* User 消息文本样式 */.messageUserText {.chunkText();.messageTextBase();background-color: rgb(248, 247, 247);word-break: break-word; // 自动换行text-align: justify; // 用户消息文本两端对齐}.messageEmpty {width: 300px;}.thumbnailImg {max-width: 20px;}
}.messageItemLeft {text-align: left;
}.messageItemRight {text-align: right;
}
实际对话消息,显示如下: