实现RAGFlow-0.14.1的输入框多行输入和消息框的多行显示

 一、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 组件中渲染消息文本时能够正确处理换行符。

修改目标:

  1. 支持多行显示:当用户发送多行消息时,确保文本能够按行显示,而不仅仅是将换行符替换为空格。
  2. 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 样式的改进:

关键改动:

  1. 保留换行符: 使用 white-space: pre-line 来保留文本中的换行符(\n),并且自动换行。
  2. 避免内容溢出: 适当设置 word-breakoverflow-wrap 属性,以确保长单词或无空格的长文本能够正确换行,避免溢出。
  3. 简化重复的 .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;
}

实际对话消息,显示如下:

本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若转载,请注明出处:/a/930500.html

如若内容造成侵权/违法违规/事实不符,请联系我们进行投诉反馈qq邮箱809451989@qq.com,一经查实,立即删除!

相关文章

【漫话机器学习系列】Adaboost算法

Adaboost&#xff08;Adaptive Boosting&#xff09;是一种经典的集成学习方法&#xff0c;主要思想是通过将多个弱学习器&#xff08;通常是简单模型&#xff0c;如决策树桩&#xff09;加权组合&#xff0c;来提升整体模型的预测能力。Adaboost 是一种自适应的学习方法&#…

第四学期-智能数据分析-期末复习题

智能数据分析期末复习&#xff08;2024春&#xff09; 【考试形式】&#xff1a;闭卷&#xff0c;90分钟&#xff0c;笔试 【题型分布】&#xff1a; 单选题10题&#xff0c;每题3分&#xff0c;共计30分 判断题10题&#xff0c;每题2分&#xff0c;共计20分 填空题5题&…

数据结构初阶---栈和队列

一、栈Stack 1.栈的概念 栈是一种特殊的线性表&#xff0c;只允许在固定的一端进行插入与删除操作。进行插入与删除操作的一端被称之为栈顶&#xff0c;另一端称之为栈底。栈中的数据元素遵循先进后出(FILO)或者说后进先出(LIFO)原则 栈的插入操作称之为压栈/入栈/进栈&…

ASP.NET Web UI 框架 Razor Pages/MVC/Web API/Blazor

前言 &#x1f4a2; &#x1f3af; 记得以前用 Asp.net 做 网页开发 是的时候那时候还是 Web Forms &#xff0c;后来 MVC 出来后也是火的不得了。那个时候还没有所谓的前后端分离一说&#xff0c;像 Vue.js、React 、Angular 的这些前端框架还没出生&#xff0c;那时候 Jquery…

YOLOV11 快速使用教程

概述 这里主要记录使用NVIDIA GPU pytorch 检测系列模型的快速使用方式&#xff0c;可以快速解决一些工业应用的问题&#xff0c;比如&#xff1a;无网、数据大需要改路径、需要记录不同实验结果等问题。 安装 参考官网&#xff0c;自己安装好Python > 3.8和pytorch >…

双绞线直连两台电脑的方法及遇到的问题

文章目录 前言一、步骤二、问题总结&#xff1a;问题1:遇到ping不通的问题。问题2:访问其他电脑上的共享文件时提示输入网络凭证问题3:局域网共享文件时提示“没有权限访问&#xff0c;请与网络管理员联系请求访问权限” 前言 办公室里有两台电脑&#xff0c;一台装了显卡用于…

电子信息工程自动化 基于单片机的出租车计价器设计

摘 要 出租车作为一种城市中非常重要的公共交通工具&#xff0c;他与人们的生活息息相关。所以我也设计了一款出租车计价器&#xff0c;它采用模块化设计&#xff0c;包含里程测量模块、数据存储模块、按键模块、时钟模块、显示模块、语音播报模块六大主要模块。本设计的出租车…

day1:ansible

ansible-doc <module_name>&#xff08;如果没有网&#xff0c;那这个超级有用&#xff09; 这个很有用&#xff0c;用来查单个模块的文档。 ansible-doc -l 列出所有模块 ansible-doc -s <module_name> 查看更详细的模块文档。 ansible-doc --help 使用 --help …

yolov,coco标记的无增强版的水稻病害数据集 共 1448 张图片

之前用过增强图片之后标记的效果不是特别理想&#xff0c;因此这里给大家分享一下使用无增强的版本&#xff1a; yolov,coco标记的无增强版的水稻病害数据集 共 1448 张图片 稻瘟病 细菌性枯萎病 褐斑病 数据集下载&#xff1a; yolov8&#xff1a;https://download.csdn.net…

uniapp微信小程序开发地图多边形渲染,圆形渲染,省市区区域渲染解决方案-(已实测通过)

一、多边形渲染(只需给map组件绑定对应的polygons即可) <mapid="map"class="map":latitude="latitude":longitude="longitude":markers="covers":polyline="polyline":scale="18"@markertap="…

IntelliJ+SpringBoot项目实战(28)--整合Beetl模板框架

在前面的文章里介绍过freemarker&#xff0c;thymeleaf模板引擎&#xff0c;本文介绍另一个性能超高的模板引擎---Beetl&#xff0c;据说此模板引擎的性能远超Freemarker。官网的说法是&#xff0c;Beetl 远超过主流java模板引擎性能(引擎性能5-6倍于FreeMarker&#xff0c;2倍…

jeecg-uniapp 跨域问题解决方法记录

今天折腾这个很恶心的问题,工作需要经验才行,根本没有什么技术难点,都是经验而已 问题在此 发现没有替换掉前缀 :8085/#/pages/login/login:1 Access to XMLHttpRequest at http://192.168.152.32:8194/h5api/api/user/login from origin http://localhost:8085 has been bloc…

AD20 原理图库更新到原理图

一 点击工具&#xff0c;从库更新。快捷键TL 二 点击完成 三 执行变更&#xff0c;最后点击关闭

linux基于systemd自启守护进程 systemctl自定义服务傻瓜式教程

系统服务 书接上文: linux自启任务详解 演示系统:ubuntu 20.04 开发部署项目的时候常常有这样的场景: 业务功能以后台服务的形式提供,部署完成后可以随着系统的重启而自动启动;服务异常挂掉后可以再次拉起 这个功能在ubuntu系统中通常由systemd提供 如果仅仅需要达成上述的场…

Unity 设计模式-策略模式(Strategy Pattern)详解

策略模式&#xff08;Strategy Pattern&#xff09;是一种行为型设计模式&#xff0c;定义了一系列算法&#xff0c;并将每种算法封装到独立的类中&#xff0c;使得它们可以互相替换。策略模式让算法可以在不影响客户端的情况下独立变化&#xff0c;客户端通过与这些策略对象进…

Android环境搭建

Android环境搭建 第一步&#xff1a;安装 Homebrew 执行以下命令来安装 Homebrew&#xff1a; /bin/zsh -c "$(curl -fsSL https://gitee.com/cunkai/HomebrewCN/raw/master/Homebrew.sh)"检测是否安装成功&#xff1a; brew --version第二步&#xff1a;安装 No…

摄影后期学什么_好学吗?

当你按下相机快门&#xff0c;捕捉到那珍贵的瞬间&#xff0c;摄影可还没画上句号哦&#xff01;摄影后期就像是一场神奇的魔法秀&#xff0c;能让你的照片从平凡瞬间变身惊艳大片。那在这场魔法之旅中&#xff0c;咱们得学习哪些厉害的法术呢&#xff1f; 先来说说光影调整这…

短视频矩阵系统saas源码 ---技术源头搭建部署

短视频矩阵系统源码 短视频矩阵系统源码主要有三种框架&#xff1a;Spring、Struts和Hibernate。Spring框架是一个全栈式的Java应用程序开发框架&#xff0c;提供了IOC容器、AOP、事务管理等功能。Struts框架是一个MVC架构的Web应用程序框架&#xff0c;用于将数据模型、Web应用…

mock.js介绍

mock.js http://mockjs.com/ 1、mock的介绍 *** 生成随机数据&#xff0c;拦截 Ajax 请求。** 通过随机数据&#xff0c;模拟各种场景&#xff1b;不需要修改既有代码&#xff0c;就可以拦截 Ajax 请求&#xff0c;返回模拟的响应数据&#xff1b;支持生成随机的文本、数字…

Spring Authorization Server入门 (十二) 实现授权码模式使用前后端分离的登录页面

基于Spring Session的前后端分离文章已发布&#xff1a;《Spring Authorization Server基于Spring Session的前后端分离实现》 2023-12-01修改&#xff1a;在session-data-redis(Github)分支中添加了基于spring-session-data-redis的实现&#xff0c;无需借助nonceId来保持认证…