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 {
|
.messageInputWrapper {
|
||||||
margin-right: 20px;
|
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;
|
border-radius: 8px;
|
||||||
:global(.ant-input-affix-wrapper) {
|
:global(.ant-input-affix-wrapper) {
|
||||||
border-bottom-right-radius: 0;
|
border-bottom-right-radius: 0;
|
||||||
border-bottom-left-radius: 0;
|
border-bottom-left-radius: 0;
|
||||||
}
|
}
|
||||||
.documentCard {
|
}
|
||||||
:global(.ant-card-body) {
|
|
||||||
padding: 10px;
|
.documentCard {
|
||||||
position: relative;
|
:global(.ant-card-body) {
|
||||||
}
|
padding: 10px;
|
||||||
}
|
position: relative;
|
||||||
.listWrapper {
|
width: 100%;
|
||||||
padding: 0 10px;
|
|
||||||
overflow: auto;
|
|
||||||
max-height: 170px;
|
|
||||||
}
|
|
||||||
.inputWrapper {
|
|
||||||
border-radius: 8px;
|
|
||||||
}
|
|
||||||
.deleteIcon {
|
|
||||||
position: absolute;
|
|
||||||
right: -4px;
|
|
||||||
top: -4px;
|
|
||||||
color: #d92d20;
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
.listWrapper {
|
||||||
|
padding: 0 10px;
|
||||||
|
overflow: auto;
|
||||||
|
max-height: 170px;
|
||||||
|
width: 100%;
|
||||||
|
}
|
||||||
|
.inputWrapper {
|
||||||
|
border-radius: 8px;
|
||||||
|
}
|
||||||
|
.deleteIcon {
|
||||||
|
position: absolute;
|
||||||
|
right: -4px;
|
||||||
|
top: -4px;
|
||||||
|
color: #d92d20;
|
||||||
|
}
|
||||||
|
@ -11,11 +11,14 @@ import {
|
|||||||
CloseCircleOutlined,
|
CloseCircleOutlined,
|
||||||
InfoCircleOutlined,
|
InfoCircleOutlined,
|
||||||
LoadingOutlined,
|
LoadingOutlined,
|
||||||
|
PaperClipOutlined,
|
||||||
|
SendOutlined,
|
||||||
} from '@ant-design/icons';
|
} from '@ant-design/icons';
|
||||||
import type { GetProp, UploadFile } from 'antd';
|
import type { GetProp, UploadFile } from 'antd';
|
||||||
import {
|
import {
|
||||||
Button,
|
Button,
|
||||||
Card,
|
Card,
|
||||||
|
Divider,
|
||||||
Flex,
|
Flex,
|
||||||
Input,
|
Input,
|
||||||
List,
|
List,
|
||||||
@ -25,12 +28,9 @@ import {
|
|||||||
Upload,
|
Upload,
|
||||||
UploadProps,
|
UploadProps,
|
||||||
} from 'antd';
|
} from 'antd';
|
||||||
import classNames from 'classnames';
|
|
||||||
import get from 'lodash/get';
|
import get from 'lodash/get';
|
||||||
import { Paperclip } from 'lucide-react';
|
|
||||||
import {
|
import {
|
||||||
ChangeEventHandler,
|
ChangeEventHandler,
|
||||||
KeyboardEventHandler,
|
|
||||||
memo,
|
memo,
|
||||||
useCallback,
|
useCallback,
|
||||||
useEffect,
|
useEffect,
|
||||||
@ -43,6 +43,8 @@ import styles from './index.less';
|
|||||||
type FileType = Parameters<GetProp<UploadProps, 'beforeUpload'>>[0];
|
type FileType = Parameters<GetProp<UploadProps, 'beforeUpload'>>[0];
|
||||||
const { Text } = Typography;
|
const { Text } = Typography;
|
||||||
|
|
||||||
|
const { TextArea } = Input;
|
||||||
|
|
||||||
const getFileId = (file: UploadFile) => get(file, 'response.data.0');
|
const getFileId = (file: UploadFile) => get(file, 'response.data.0');
|
||||||
|
|
||||||
const getFileIds = (fileList: UploadFile[]) => {
|
const getFileIds = (fileList: UploadFile[]) => {
|
||||||
@ -99,6 +101,7 @@ const MessageInput = ({
|
|||||||
const { data: documentInfos, setDocumentIds } = useFetchDocumentInfosByIds();
|
const { data: documentInfos, setDocumentIds } = useFetchDocumentInfosByIds();
|
||||||
const { uploadAndParseDocument } = useUploadAndParseDocument(uploadMethod);
|
const { uploadAndParseDocument } = useUploadAndParseDocument(uploadMethod);
|
||||||
const conversationIdRef = useRef(conversationId);
|
const conversationIdRef = useRef(conversationId);
|
||||||
|
|
||||||
const [fileList, setFileList] = useState<UploadFile[]>([]);
|
const [fileList, setFileList] = useState<UploadFile[]>([]);
|
||||||
|
|
||||||
const handlePreview = async (file: UploadFile) => {
|
const handlePreview = async (file: UploadFile) => {
|
||||||
@ -128,7 +131,6 @@ const MessageInput = ({
|
|||||||
});
|
});
|
||||||
return [...list];
|
return [...list];
|
||||||
});
|
});
|
||||||
|
|
||||||
const ret = await uploadAndParseDocument({
|
const ret = await uploadAndParseDocument({
|
||||||
conversationId: nextConversationId,
|
conversationId: nextConversationId,
|
||||||
fileList: [file],
|
fileList: [file],
|
||||||
@ -148,6 +150,19 @@ const MessageInput = ({
|
|||||||
|
|
||||||
const isUploadingFile = fileList.some((x) => x.status === 'uploading');
|
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 () => {
|
const handlePressEnter = useCallback(async () => {
|
||||||
if (isUploadingFile) return;
|
if (isUploadingFile) return;
|
||||||
const ids = getFileIds(fileList.filter((x) => isUploadSuccess(x)));
|
const ids = getFileIds(fileList.filter((x) => isUploadSuccess(x)));
|
||||||
@ -161,14 +176,6 @@ const MessageInput = ({
|
|||||||
const handleCompositionStart = () => setIsComposing(true);
|
const handleCompositionStart = () => setIsComposing(true);
|
||||||
const handleCompositionEnd = () => setIsComposing(false);
|
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(
|
const handleRemove = useCallback(
|
||||||
async (file: UploadFile) => {
|
async (file: UploadFile) => {
|
||||||
const ids = get(file, 'response.data', []);
|
const ids = get(file, 'response.data', []);
|
||||||
@ -215,23 +222,114 @@ const MessageInput = ({
|
|||||||
}, [conversationId, setFileList]);
|
}, [conversationId, setFileList]);
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<Flex gap={20} vertical className={styles.messageInputWrapper}>
|
<Flex gap={1} vertical className={styles.messageInputWrapper}>
|
||||||
<Flex align="center" gap={8}>
|
<TextArea
|
||||||
<Input.TextArea
|
size="large"
|
||||||
size="large"
|
placeholder={t('sendPlaceholder')}
|
||||||
placeholder={t('sendPlaceholder')}
|
value={value}
|
||||||
value={value}
|
allowClear
|
||||||
disabled={disabled}
|
disabled={disabled}
|
||||||
className={classNames({
|
style={{
|
||||||
[styles.inputWrapper]: fileList.length === 0,
|
border: 'none',
|
||||||
})}
|
boxShadow: 'none',
|
||||||
onKeyDown={handleInputKeyDown}
|
padding: '0px 10px',
|
||||||
onChange={onInputChange}
|
marginTop: 10,
|
||||||
onCompositionStart={handleCompositionStart}
|
}}
|
||||||
onCompositionEnd={handleCompositionEnd}
|
autoSize={{ minRows: 2, maxRows: 10 }}
|
||||||
autoSize={{ minRows: 1, maxRows: 6 }}
|
onKeyDown={handleKeyDown}
|
||||||
/>
|
onChange={onInputChange}
|
||||||
<Space>
|
onCompositionStart={handleCompositionStart}
|
||||||
|
onCompositionEnd={handleCompositionEnd}
|
||||||
|
/>
|
||||||
|
<Divider style={{ margin: '5px 30px 10px 0px' }} />
|
||||||
|
<Flex justify="space-between" align="center">
|
||||||
|
{fileList.length > 0 && (
|
||||||
|
<List
|
||||||
|
grid={{
|
||||||
|
gutter: 16,
|
||||||
|
xs: 1,
|
||||||
|
sm: 1,
|
||||||
|
md: 1,
|
||||||
|
lg: 1,
|
||||||
|
xl: 2,
|
||||||
|
xxl: 4,
|
||||||
|
}}
|
||||||
|
dataSource={fileList}
|
||||||
|
className={styles.listWrapper}
|
||||||
|
renderItem={(item) => {
|
||||||
|
const id = getFileId(item);
|
||||||
|
const documentInfo = getDocumentInfoById(id);
|
||||||
|
const fileExtension = getExtension(documentInfo?.name ?? '');
|
||||||
|
const fileName = item.originFileObj?.name ?? '';
|
||||||
|
|
||||||
|
return (
|
||||||
|
<List.Item>
|
||||||
|
<Card className={styles.documentCard}>
|
||||||
|
<Flex gap={10} align="center">
|
||||||
|
{item.status === 'uploading' ? (
|
||||||
|
<Spin
|
||||||
|
indicator={
|
||||||
|
<LoadingOutlined style={{ fontSize: 24 }} spin />
|
||||||
|
}
|
||||||
|
/>
|
||||||
|
) : item.status === 'error' ? (
|
||||||
|
<InfoCircleOutlined size={30}></InfoCircleOutlined>
|
||||||
|
) : (
|
||||||
|
<FileIcon id={id} name={fileName}></FileIcon>
|
||||||
|
)}
|
||||||
|
<Flex vertical style={{ width: '90%' }}>
|
||||||
|
<Text
|
||||||
|
ellipsis={{ tooltip: fileName }}
|
||||||
|
className={styles.nameText}
|
||||||
|
>
|
||||||
|
<b> {fileName}</b>
|
||||||
|
</Text>
|
||||||
|
{item.status === 'error' ? (
|
||||||
|
t('uploadFailed')
|
||||||
|
) : (
|
||||||
|
<>
|
||||||
|
{item.percent !== 100 ? (
|
||||||
|
t('uploading')
|
||||||
|
) : !item.response ? (
|
||||||
|
t('parsing')
|
||||||
|
) : (
|
||||||
|
<Space>
|
||||||
|
<span>{fileExtension?.toUpperCase()},</span>
|
||||||
|
<span>
|
||||||
|
{formatBytes(
|
||||||
|
getDocumentInfoById(id)?.size ?? 0,
|
||||||
|
)}
|
||||||
|
</span>
|
||||||
|
</Space>
|
||||||
|
)}
|
||||||
|
</>
|
||||||
|
)}
|
||||||
|
</Flex>
|
||||||
|
</Flex>
|
||||||
|
|
||||||
|
{item.status !== 'uploading' && (
|
||||||
|
<span className={styles.deleteIcon}>
|
||||||
|
<CloseCircleOutlined
|
||||||
|
onClick={() => handleRemove(item)}
|
||||||
|
/>
|
||||||
|
</span>
|
||||||
|
)}
|
||||||
|
</Card>
|
||||||
|
</List.Item>
|
||||||
|
);
|
||||||
|
}}
|
||||||
|
/>
|
||||||
|
)}
|
||||||
|
<Flex
|
||||||
|
gap={5}
|
||||||
|
align="center"
|
||||||
|
justify="flex-end"
|
||||||
|
style={{
|
||||||
|
paddingRight: 10,
|
||||||
|
paddingBottom: 10,
|
||||||
|
width: fileList.length > 0 ? '50%' : '100%',
|
||||||
|
}}
|
||||||
|
>
|
||||||
{showUploadIcon && (
|
{showUploadIcon && (
|
||||||
<Upload
|
<Upload
|
||||||
onPreview={handlePreview}
|
onPreview={handlePreview}
|
||||||
@ -243,99 +341,21 @@ const MessageInput = ({
|
|||||||
return false;
|
return false;
|
||||||
}}
|
}}
|
||||||
>
|
>
|
||||||
<Button
|
<Button type={'primary'} disabled={disabled}>
|
||||||
type={'text'}
|
<PaperClipOutlined />
|
||||||
disabled={disabled}
|
</Button>
|
||||||
icon={<Paperclip />}
|
|
||||||
></Button>
|
|
||||||
</Upload>
|
</Upload>
|
||||||
)}
|
)}
|
||||||
<Button
|
<Button
|
||||||
type="primary"
|
type="primary"
|
||||||
onClick={handlePressEnter}
|
onClick={handlePressEnter}
|
||||||
loading={sendLoading}
|
loading={sendLoading}
|
||||||
disabled={sendDisabled || isUploadingFile}
|
disabled={sendDisabled || isUploadingFile || sendLoading}
|
||||||
>
|
>
|
||||||
{t('send')}
|
<SendOutlined />
|
||||||
</Button>
|
</Button>
|
||||||
</Space>
|
</Flex>
|
||||||
</Flex>
|
</Flex>
|
||||||
|
|
||||||
{fileList.length > 0 && (
|
|
||||||
<List
|
|
||||||
grid={{
|
|
||||||
gutter: 16,
|
|
||||||
xs: 1,
|
|
||||||
sm: 1,
|
|
||||||
md: 1,
|
|
||||||
lg: 1,
|
|
||||||
xl: 2,
|
|
||||||
xxl: 4,
|
|
||||||
}}
|
|
||||||
dataSource={fileList}
|
|
||||||
className={styles.listWrapper}
|
|
||||||
renderItem={(item) => {
|
|
||||||
const id = getFileId(item);
|
|
||||||
const documentInfo = getDocumentInfoById(id);
|
|
||||||
const fileExtension = getExtension(documentInfo?.name ?? '');
|
|
||||||
const fileName = item.originFileObj?.name ?? '';
|
|
||||||
|
|
||||||
return (
|
|
||||||
<List.Item>
|
|
||||||
<Card className={styles.documentCard}>
|
|
||||||
<Flex gap={10} align="center">
|
|
||||||
{item.status === 'uploading' ? (
|
|
||||||
<Spin
|
|
||||||
indicator={
|
|
||||||
<LoadingOutlined style={{ fontSize: 24 }} spin />
|
|
||||||
}
|
|
||||||
/>
|
|
||||||
) : item.status === 'error' ? (
|
|
||||||
<InfoCircleOutlined size={30}></InfoCircleOutlined>
|
|
||||||
) : (
|
|
||||||
<FileIcon id={id} name={fileName}></FileIcon>
|
|
||||||
)}
|
|
||||||
<Flex vertical style={{ width: '90%' }}>
|
|
||||||
<Text
|
|
||||||
ellipsis={{ tooltip: fileName }}
|
|
||||||
className={styles.nameText}
|
|
||||||
>
|
|
||||||
<b> {fileName}</b>
|
|
||||||
</Text>
|
|
||||||
{item.status === 'error' ? (
|
|
||||||
t('uploadFailed')
|
|
||||||
) : (
|
|
||||||
<>
|
|
||||||
{item.percent !== 100 ? (
|
|
||||||
t('uploading')
|
|
||||||
) : !item.response ? (
|
|
||||||
t('parsing')
|
|
||||||
) : (
|
|
||||||
<Space>
|
|
||||||
<span>{fileExtension?.toUpperCase()},</span>
|
|
||||||
<span>
|
|
||||||
{formatBytes(
|
|
||||||
getDocumentInfoById(id)?.size ?? 0,
|
|
||||||
)}
|
|
||||||
</span>
|
|
||||||
</Space>
|
|
||||||
)}
|
|
||||||
</>
|
|
||||||
)}
|
|
||||||
</Flex>
|
|
||||||
</Flex>
|
|
||||||
|
|
||||||
{item.status !== 'uploading' && (
|
|
||||||
<span className={styles.deleteIcon}>
|
|
||||||
<CloseCircleOutlined onClick={() => handleRemove(item)} />
|
|
||||||
</span>
|
|
||||||
)}
|
|
||||||
</Card>
|
|
||||||
</List.Item>
|
|
||||||
);
|
|
||||||
}}
|
|
||||||
/>
|
|
||||||
)}
|
|
||||||
</Flex>
|
</Flex>
|
||||||
);
|
);
|
||||||
};
|
};
|
||||||
|
Loading…
x
Reference in New Issue
Block a user