mirror of
https://git.mirrors.martin98.com/https://github.com/infiniflow/ragflow.git
synced 2025-06-30 07:15:11 +08:00
Feat: multiline text input for chat (#5317)
### What problem does this PR solve? Improves the chat interface by adding a multiline chat area that grows when multiple lines exists. Some images: * Empty: <img width="1334" alt="image" src="https://github.com/user-attachments/assets/e8a68b46-def9-45af-b5b1-db0f0b67e6d8" /> * With multiple lines and documents: <img width="1070" alt="image" src="https://github.com/user-attachments/assets/ff976c5c-08fa-492f-9fc0-17512c95f9f2" /> ### Type of change - [X] New Feature (non-breaking change which adds functionality)
This commit is contained in:
parent
7600ebd263
commit
06e0c7d1a9
@ -1,29 +1,38 @@
|
||||
.messageInputWrapper {
|
||||
margin-right: 20px;
|
||||
background-color: rgba(255, 255, 255, 0.1);
|
||||
padding: '0px 0px 10px 0px';
|
||||
background-color: #ffffff;
|
||||
border: 1px solid #d9d9d9;
|
||||
&:hover {
|
||||
border-color: #40a9ff;
|
||||
box-shadow: #40a9ff;
|
||||
}
|
||||
border-radius: 8px;
|
||||
:global(.ant-input-affix-wrapper) {
|
||||
border-bottom-right-radius: 0;
|
||||
border-bottom-left-radius: 0;
|
||||
}
|
||||
.documentCard {
|
||||
}
|
||||
|
||||
.documentCard {
|
||||
:global(.ant-card-body) {
|
||||
padding: 10px;
|
||||
position: relative;
|
||||
width: 100%;
|
||||
}
|
||||
}
|
||||
.listWrapper {
|
||||
}
|
||||
.listWrapper {
|
||||
padding: 0 10px;
|
||||
overflow: auto;
|
||||
max-height: 170px;
|
||||
}
|
||||
.inputWrapper {
|
||||
width: 100%;
|
||||
}
|
||||
.inputWrapper {
|
||||
border-radius: 8px;
|
||||
}
|
||||
.deleteIcon {
|
||||
}
|
||||
.deleteIcon {
|
||||
position: absolute;
|
||||
right: -4px;
|
||||
top: -4px;
|
||||
color: #d92d20;
|
||||
}
|
||||
}
|
||||
|
@ -11,11 +11,14 @@ import {
|
||||
CloseCircleOutlined,
|
||||
InfoCircleOutlined,
|
||||
LoadingOutlined,
|
||||
PaperClipOutlined,
|
||||
SendOutlined,
|
||||
} from '@ant-design/icons';
|
||||
import type { GetProp, UploadFile } from 'antd';
|
||||
import {
|
||||
Button,
|
||||
Card,
|
||||
Divider,
|
||||
Flex,
|
||||
Input,
|
||||
List,
|
||||
@ -25,12 +28,9 @@ import {
|
||||
Upload,
|
||||
UploadProps,
|
||||
} from 'antd';
|
||||
import classNames from 'classnames';
|
||||
import get from 'lodash/get';
|
||||
import { Paperclip } from 'lucide-react';
|
||||
import {
|
||||
ChangeEventHandler,
|
||||
KeyboardEventHandler,
|
||||
memo,
|
||||
useCallback,
|
||||
useEffect,
|
||||
@ -43,6 +43,8 @@ import styles from './index.less';
|
||||
type FileType = Parameters<GetProp<UploadProps, 'beforeUpload'>>[0];
|
||||
const { Text } = Typography;
|
||||
|
||||
const { TextArea } = Input;
|
||||
|
||||
const getFileId = (file: UploadFile) => get(file, 'response.data.0');
|
||||
|
||||
const getFileIds = (fileList: UploadFile[]) => {
|
||||
@ -99,6 +101,7 @@ const MessageInput = ({
|
||||
const { data: documentInfos, setDocumentIds } = useFetchDocumentInfosByIds();
|
||||
const { uploadAndParseDocument } = useUploadAndParseDocument(uploadMethod);
|
||||
const conversationIdRef = useRef(conversationId);
|
||||
|
||||
const [fileList, setFileList] = useState<UploadFile[]>([]);
|
||||
|
||||
const handlePreview = async (file: UploadFile) => {
|
||||
@ -128,7 +131,6 @@ const MessageInput = ({
|
||||
});
|
||||
return [...list];
|
||||
});
|
||||
|
||||
const ret = await uploadAndParseDocument({
|
||||
conversationId: nextConversationId,
|
||||
fileList: [file],
|
||||
@ -148,6 +150,19 @@ const MessageInput = ({
|
||||
|
||||
const isUploadingFile = fileList.some((x) => x.status === 'uploading');
|
||||
|
||||
const handleKeyDown = useCallback(
|
||||
async (event: React.KeyboardEvent<HTMLTextAreaElement>) => {
|
||||
// check if it was shift + enter
|
||||
if (event.key === 'Enter' && event.shiftKey) return;
|
||||
if (event.key !== 'Enter') return;
|
||||
if (sendDisabled || isUploadingFile || sendLoading) return;
|
||||
|
||||
event.preventDefault();
|
||||
handlePressEnter();
|
||||
},
|
||||
[fileList, onPressEnter, isUploadingFile],
|
||||
);
|
||||
|
||||
const handlePressEnter = useCallback(async () => {
|
||||
if (isUploadingFile) return;
|
||||
const ids = getFileIds(fileList.filter((x) => isUploadSuccess(x)));
|
||||
@ -161,14 +176,6 @@ const MessageInput = ({
|
||||
const handleCompositionStart = () => setIsComposing(true);
|
||||
const handleCompositionEnd = () => setIsComposing(false);
|
||||
|
||||
const handleInputKeyDown: KeyboardEventHandler<HTMLTextAreaElement> = (e) => {
|
||||
if (e.key === 'Enter' && !e.nativeEvent.shiftKey) {
|
||||
if (isComposing || sendLoading) return;
|
||||
e.preventDefault();
|
||||
handlePressEnter();
|
||||
}
|
||||
};
|
||||
|
||||
const handleRemove = useCallback(
|
||||
async (file: UploadFile) => {
|
||||
const ids = get(file, 'response.data', []);
|
||||
@ -215,52 +222,27 @@ const MessageInput = ({
|
||||
}, [conversationId, setFileList]);
|
||||
|
||||
return (
|
||||
<Flex gap={20} vertical className={styles.messageInputWrapper}>
|
||||
<Flex align="center" gap={8}>
|
||||
<Input.TextArea
|
||||
<Flex gap={1} vertical className={styles.messageInputWrapper}>
|
||||
<TextArea
|
||||
size="large"
|
||||
placeholder={t('sendPlaceholder')}
|
||||
value={value}
|
||||
allowClear
|
||||
disabled={disabled}
|
||||
className={classNames({
|
||||
[styles.inputWrapper]: fileList.length === 0,
|
||||
})}
|
||||
onKeyDown={handleInputKeyDown}
|
||||
style={{
|
||||
border: 'none',
|
||||
boxShadow: 'none',
|
||||
padding: '0px 10px',
|
||||
marginTop: 10,
|
||||
}}
|
||||
autoSize={{ minRows: 2, maxRows: 10 }}
|
||||
onKeyDown={handleKeyDown}
|
||||
onChange={onInputChange}
|
||||
onCompositionStart={handleCompositionStart}
|
||||
onCompositionEnd={handleCompositionEnd}
|
||||
autoSize={{ minRows: 1, maxRows: 6 }}
|
||||
/>
|
||||
<Space>
|
||||
{showUploadIcon && (
|
||||
<Upload
|
||||
onPreview={handlePreview}
|
||||
onChange={handleChange}
|
||||
multiple={false}
|
||||
onRemove={handleRemove}
|
||||
showUploadList={false}
|
||||
beforeUpload={() => {
|
||||
return false;
|
||||
}}
|
||||
>
|
||||
<Button
|
||||
type={'text'}
|
||||
disabled={disabled}
|
||||
icon={<Paperclip />}
|
||||
></Button>
|
||||
</Upload>
|
||||
)}
|
||||
<Button
|
||||
type="primary"
|
||||
onClick={handlePressEnter}
|
||||
loading={sendLoading}
|
||||
disabled={sendDisabled || isUploadingFile}
|
||||
>
|
||||
{t('send')}
|
||||
</Button>
|
||||
</Space>
|
||||
</Flex>
|
||||
|
||||
<Divider style={{ margin: '5px 30px 10px 0px' }} />
|
||||
<Flex justify="space-between" align="center">
|
||||
{fileList.length > 0 && (
|
||||
<List
|
||||
grid={{
|
||||
@ -327,7 +309,9 @@ const MessageInput = ({
|
||||
|
||||
{item.status !== 'uploading' && (
|
||||
<span className={styles.deleteIcon}>
|
||||
<CloseCircleOutlined onClick={() => handleRemove(item)} />
|
||||
<CloseCircleOutlined
|
||||
onClick={() => handleRemove(item)}
|
||||
/>
|
||||
</span>
|
||||
)}
|
||||
</Card>
|
||||
@ -336,6 +320,42 @@ const MessageInput = ({
|
||||
}}
|
||||
/>
|
||||
)}
|
||||
<Flex
|
||||
gap={5}
|
||||
align="center"
|
||||
justify="flex-end"
|
||||
style={{
|
||||
paddingRight: 10,
|
||||
paddingBottom: 10,
|
||||
width: fileList.length > 0 ? '50%' : '100%',
|
||||
}}
|
||||
>
|
||||
{showUploadIcon && (
|
||||
<Upload
|
||||
onPreview={handlePreview}
|
||||
onChange={handleChange}
|
||||
multiple={false}
|
||||
onRemove={handleRemove}
|
||||
showUploadList={false}
|
||||
beforeUpload={() => {
|
||||
return false;
|
||||
}}
|
||||
>
|
||||
<Button type={'primary'} disabled={disabled}>
|
||||
<PaperClipOutlined />
|
||||
</Button>
|
||||
</Upload>
|
||||
)}
|
||||
<Button
|
||||
type="primary"
|
||||
onClick={handlePressEnter}
|
||||
loading={sendLoading}
|
||||
disabled={sendDisabled || isUploadingFile || sendLoading}
|
||||
>
|
||||
<SendOutlined />
|
||||
</Button>
|
||||
</Flex>
|
||||
</Flex>
|
||||
</Flex>
|
||||
);
|
||||
};
|
||||
|
Loading…
x
Reference in New Issue
Block a user