一、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 (
<Flex
className={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.TextArea
size="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>}
/>
{/* 按钮区域 */}
<Flex
justify="space-between"
align="center"
style={{
marginTop: '8px',
}}
>
{showUploadIcon && (
<Upload
onPreview={handlePreview}
onChange={handleChange}
multiple={false}
onRemove={handleRemove}
showUploadList={false}
beforeUpload={() => {
return false;
}}
>
<Button
type={'text'}
disabled={disabled}
icon={
<SvgIcon
name="paper-clip"
width={18}
height={22}
disabled={disabled}
></SvgIcon>
}
></Button>
</Upload>
)}
<Button
type="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 (
<MessageItem
loading={
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>
<Flex
align="flex-start" // 改为 flex-start,使内容顶部对齐
style={{
padding: '12px 20px',
backgroundColor: '#ffffff', // 白色背景
borderTop: '1px solid #e8e8e8', // 分割线颜色
position: 'sticky', // 固定在底部
bottom: 0,
zIndex: 100, // 确保浮于内容上方
}}
>
<Input.TextArea
placeholder={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', // 禁止拖动调整大小
}}
/>
<Button
type="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>
<PdfDrawer
visible={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 (
<div
className={classNames(styles.messageItem, {
[styles.messageItemLeft]: item.role === MessageType.Assistant,
[styles.messageItemRight]: item.role === MessageType.User,
})}
>
<section
className={classNames(styles.messageItemSection, {
[styles.messageItemSectionLeft]: item.role === MessageType.Assistant,
[styles.messageItemSectionRight]: item.role === MessageType.User,
})}
>
<div
className={classNames(styles.messageItemContent, {
[styles.messageItemContentReverse]: item.role === MessageType.User,
})}
>
{item.role === MessageType.User ? (
<Avatar
size={40}
src={
avatar ??
'https://zos.alipayobjects.com/rmsportal/jkjgkEfvpUPVyRjUImniVslZfWPnJuuZ.png'
}
/>
) : (
<AssistantIcon></AssistantIcon>
)}
<Flex vertical gap={8} flex={1}>
<Space>
{isAssistant ? (
index !== 0 && (
<AssistantGroupButton
messageId={item.id}
content={item.content}
prompt={item.prompt}
showLikeButton={showLikeButton}
audioBinary={item.audio_binary}
></AssistantGroupButton>
)
) : (
<UserGroupButton
content={item.content}
messageId={item.id}
removeMessageById={removeMessageById}
regenerateMessage={
regenerateMessage && handleRegenerateMessage
}
sendLoading={sendLoading}
></UserGroupButton>
)}
{/* <b>{isAssistant ? '' : nickname}</b> */}
</Space>
<div
className={
isAssistant ? styles.messageText : styles.messageUserText
}
style={{ whiteSpace: 'pre-line' }} // 保留换行符并自动换行
>
<MarkdownContent
loading={loading}
content={item.content}
reference={reference}
clickDocumentButton={clickDocumentButton}
></MarkdownContent>
</div>
{isAssistant && referenceDocumentList.length > 0 && (
<List
bordered
dataSource={referenceDocumentList}
renderItem={(item) => {
return (
<List.Item>
<Flex gap={'small'} align="center">
<FileIcon
id={item.doc_id}
name={item.doc_name}
></FileIcon>
<NewDocumentLink
documentId={item.doc_id}
documentName={item.doc_name}
prefix="document"
>
{item.doc_name}
</NewDocumentLink>
</Flex>
</List.Item>
);
}}
/>
)}
{isUser && documentList.length > 0 && (
<List
bordered
dataSource={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) ? (
<NewDocumentLink
documentId={item.id}
documentName={item.name}
prefix="document"
>
{item.name}
</NewDocumentLink>
) : (
<Button
type={'text'}
onClick={handleUserDocumentClick(item.id)}
>
<Text
style={{ maxWidth: '40vw' }}
ellipsis={{ tooltip: item.name }}
>
{item.name}
</Text>
</Button>
)}
</Flex>
</List.Item>
);
}}
/>
)}
</Flex>
</div>
</section>
{visible && (
<IndentedTreeModal
visible={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;
}
实际对话消息,显示如下: