balibabu commited on
Commit
d3b461d
·
1 Parent(s): c260733

feat: Add MessageInput to the external chat page #1880 (#1963)

Browse files

### What problem does this PR solve?
feat: Add MessageInput to the external chat page #1880

### Type of change

- [x] New Feature (non-breaking change which adds functionality)

web/src/assets/svg/paper-clip.svg ADDED
web/src/components/api-service/chat-overview-modal/index.tsx CHANGED
@@ -50,13 +50,8 @@ const ChatOverviewModal = ({
50
  hideModal: hideApiKeyModal,
51
  showModal: showApiKeyModal,
52
  } = useSetModalState();
53
- const {
54
- embedVisible,
55
- hideEmbedModal,
56
- showEmbedModal,
57
- embedToken,
58
- errorContextHolder,
59
- } = useShowEmbedModal(id, idKey);
60
 
61
  const { pickerValue, setPickerValue } = useFetchNextStats();
62
 
@@ -64,7 +59,7 @@ const ChatOverviewModal = ({
64
  return current && current > dayjs().endOf('day');
65
  };
66
 
67
- const { handlePreview, contextHolder } = usePreviewChat(id, idKey);
68
 
69
  return (
70
  <>
@@ -138,8 +133,6 @@ const ChatOverviewModal = ({
138
  visible={embedVisible}
139
  hideModal={hideEmbedModal}
140
  ></EmbedModal>
141
- {contextHolder}
142
- {errorContextHolder}
143
  </Modal>
144
  </>
145
  );
 
50
  hideModal: hideApiKeyModal,
51
  showModal: showApiKeyModal,
52
  } = useSetModalState();
53
+ const { embedVisible, hideEmbedModal, showEmbedModal, embedToken } =
54
+ useShowEmbedModal(id, idKey);
 
 
 
 
 
55
 
56
  const { pickerValue, setPickerValue } = useFetchNextStats();
57
 
 
59
  return current && current > dayjs().endOf('day');
60
  };
61
 
62
+ const { handlePreview } = usePreviewChat(id, idKey);
63
 
64
  return (
65
  <>
 
133
  visible={embedVisible}
134
  hideModal={hideEmbedModal}
135
  ></EmbedModal>
 
 
136
  </Modal>
137
  </>
138
  );
web/src/components/api-service/hooks.ts CHANGED
@@ -63,13 +63,12 @@ export const useSelectChartStatsList = (): ChartStatsType => {
63
  };
64
 
65
  export const useShowTokenEmptyError = () => {
66
- const [messageApi, contextHolder] = message.useMessage();
67
  const { t } = useTranslate('chat');
68
 
69
  const showTokenEmptyError = useCallback(() => {
70
- messageApi.error(t('tokenError'));
71
- }, [messageApi, t]);
72
- return { showTokenEmptyError, contextHolder };
73
  };
74
 
75
  const getUrlWithToken = (token: string) => {
@@ -78,7 +77,7 @@ const getUrlWithToken = (token: string) => {
78
  };
79
 
80
  const useFetchTokenListBeforeOtherStep = (dialogId: string, idKey: string) => {
81
- const { showTokenEmptyError, contextHolder } = useShowTokenEmptyError();
82
 
83
  const { data: tokenList, refetch } = useFetchTokenList({ [idKey]: dialogId });
84
 
@@ -98,7 +97,6 @@ const useFetchTokenListBeforeOtherStep = (dialogId: string, idKey: string) => {
98
 
99
  return {
100
  token,
101
- contextHolder,
102
  handleOperate,
103
  };
104
  };
@@ -110,8 +108,10 @@ export const useShowEmbedModal = (dialogId: string, idKey: string) => {
110
  showModal: showEmbedModal,
111
  } = useSetModalState();
112
 
113
- const { handleOperate, token, contextHolder } =
114
- useFetchTokenListBeforeOtherStep(dialogId, idKey);
 
 
115
 
116
  const handleShowEmbedModal = useCallback(async () => {
117
  const succeed = await handleOperate();
@@ -125,15 +125,11 @@ export const useShowEmbedModal = (dialogId: string, idKey: string) => {
125
  hideEmbedModal,
126
  embedVisible,
127
  embedToken: token,
128
- errorContextHolder: contextHolder,
129
  };
130
  };
131
 
132
  export const usePreviewChat = (dialogId: string, idKey: string) => {
133
- const { handleOperate, contextHolder } = useFetchTokenListBeforeOtherStep(
134
- dialogId,
135
- idKey,
136
- );
137
 
138
  const open = useCallback((t: string) => {
139
  window.open(getUrlWithToken(t), '_blank');
@@ -148,6 +144,5 @@ export const usePreviewChat = (dialogId: string, idKey: string) => {
148
 
149
  return {
150
  handlePreview,
151
- contextHolder,
152
  };
153
  };
 
63
  };
64
 
65
  export const useShowTokenEmptyError = () => {
 
66
  const { t } = useTranslate('chat');
67
 
68
  const showTokenEmptyError = useCallback(() => {
69
+ message.error(t('tokenError'));
70
+ }, [t]);
71
+ return { showTokenEmptyError };
72
  };
73
 
74
  const getUrlWithToken = (token: string) => {
 
77
  };
78
 
79
  const useFetchTokenListBeforeOtherStep = (dialogId: string, idKey: string) => {
80
+ const { showTokenEmptyError } = useShowTokenEmptyError();
81
 
82
  const { data: tokenList, refetch } = useFetchTokenList({ [idKey]: dialogId });
83
 
 
97
 
98
  return {
99
  token,
 
100
  handleOperate,
101
  };
102
  };
 
108
  showModal: showEmbedModal,
109
  } = useSetModalState();
110
 
111
+ const { handleOperate, token } = useFetchTokenListBeforeOtherStep(
112
+ dialogId,
113
+ idKey,
114
+ );
115
 
116
  const handleShowEmbedModal = useCallback(async () => {
117
  const succeed = await handleOperate();
 
125
  hideEmbedModal,
126
  embedVisible,
127
  embedToken: token,
 
128
  };
129
  };
130
 
131
  export const usePreviewChat = (dialogId: string, idKey: string) => {
132
+ const { handleOperate } = useFetchTokenListBeforeOtherStep(dialogId, idKey);
 
 
 
133
 
134
  const open = useCallback((t: string) => {
135
  window.open(getUrlWithToken(t), '_blank');
 
144
 
145
  return {
146
  handlePreview,
 
147
  };
148
  };
web/src/components/message-input/index.less CHANGED
@@ -1,11 +1,23 @@
1
  .messageInputWrapper {
2
  margin-right: 20px;
 
 
 
 
 
 
3
  .documentCard {
4
  :global(.ant-card-body) {
5
  padding: 10px;
6
  position: relative;
7
  }
8
  }
 
 
 
 
 
 
9
  .deleteIcon {
10
  position: absolute;
11
  right: -4px;
 
1
  .messageInputWrapper {
2
  margin-right: 20px;
3
+ background-color: #f5f5f8;
4
+ border-radius: 8px;
5
+ :global(.ant-input-affix-wrapper) {
6
+ border-bottom-right-radius: 0;
7
+ border-bottom-left-radius: 0;
8
+ }
9
  .documentCard {
10
  :global(.ant-card-body) {
11
  padding: 10px;
12
  position: relative;
13
  }
14
  }
15
+ .listWrapper {
16
+ padding: 0 10px;
17
+ }
18
+ .inputWrapper {
19
+ border-radius: 8px;
20
+ }
21
  .deleteIcon {
22
  position: absolute;
23
  right: -4px;
web/src/components/message-input/index.tsx CHANGED
@@ -1,14 +1,13 @@
1
  import { Authorization } from '@/constants/authorization';
2
  import { useTranslate } from '@/hooks/common-hooks';
3
- import { useRemoveNextDocument } from '@/hooks/document-hooks';
 
 
 
4
  import { getAuthorization } from '@/utils/authorization-util';
5
  import { getExtension } from '@/utils/document-util';
6
- import {
7
- CloseCircleOutlined,
8
- LoadingOutlined,
9
- PlusOutlined,
10
- UploadOutlined,
11
- } from '@ant-design/icons';
12
  import type { GetProp, UploadFile } from 'antd';
13
  import {
14
  Button,
@@ -22,10 +21,11 @@ import {
22
  Upload,
23
  UploadProps,
24
  } from 'antd';
 
25
  import get from 'lodash/get';
26
- import { ChangeEventHandler, useCallback, useState } from 'react';
27
  import FileIcon from '../file-icon';
28
-
29
  import styles from './index.less';
30
 
31
  type FileType = Parameters<GetProp<UploadProps, 'beforeUpload'>>[0];
@@ -33,6 +33,14 @@ const { Text } = Typography;
33
 
34
  const getFileId = (file: UploadFile) => get(file, 'response.data.0');
35
 
 
 
 
 
 
 
 
 
36
  interface IProps {
37
  disabled: boolean;
38
  value: string;
@@ -41,6 +49,7 @@ interface IProps {
41
  onPressEnter(documentIds: string[]): void;
42
  onInputChange: ChangeEventHandler<HTMLInputElement>;
43
  conversationId: string;
 
44
  }
45
 
46
  const getBase64 = (file: FileType): Promise<string> =>
@@ -59,9 +68,11 @@ const MessageInput = ({
59
  sendLoading,
60
  onInputChange,
61
  conversationId,
 
62
  }: IProps) => {
63
  const { t } = useTranslate('chat');
64
  const { removeDocument } = useRemoveNextDocument();
 
65
 
66
  const [fileList, setFileList] = useState<UploadFile[]>([]);
67
 
@@ -78,9 +89,7 @@ const MessageInput = ({
78
 
79
  const handlePressEnter = useCallback(async () => {
80
  if (isUploadingFile) return;
81
- const ids = fileList.reduce((pre, cur) => {
82
- return pre.concat(get(cur, 'response.data', []));
83
- }, []);
84
 
85
  onPressEnter(ids);
86
  setFileList([]);
@@ -99,13 +108,18 @@ const MessageInput = ({
99
  [removeDocument],
100
  );
101
 
102
- const uploadButton = (
103
- <button style={{ border: 0, background: 'none' }} type="button">
104
- <PlusOutlined />
105
- <div style={{ marginTop: 8 }}>Upload</div>
106
- </button>
107
  );
108
 
 
 
 
 
 
109
  return (
110
  <Flex gap={20} vertical className={styles.messageInputWrapper}>
111
  <Input
@@ -113,23 +127,30 @@ const MessageInput = ({
113
  placeholder={t('sendPlaceholder')}
114
  value={value}
115
  disabled={disabled}
 
116
  suffix={
117
  <Space>
118
- <Upload
119
- action="/v1/document/upload_and_parse"
120
- // listType="picture-card"
121
- fileList={fileList}
122
- onPreview={handlePreview}
123
- onChange={handleChange}
124
- multiple
125
- headers={{ [Authorization]: getAuthorization() }}
126
- data={{ conversation_id: conversationId }}
127
- method="post"
128
- onRemove={handleRemove}
129
- showUploadList={false}
130
- >
131
- <Button icon={<UploadOutlined />}></Button>
132
- </Upload>
 
 
 
 
 
 
133
  <Button
134
  type="primary"
135
  onClick={handlePressEnter}
@@ -143,71 +164,58 @@ const MessageInput = ({
143
  onPressEnter={handlePressEnter}
144
  onChange={onInputChange}
145
  />
146
- {/* <Upload
147
- action="/v1/document/upload_and_parse"
148
- listType="picture-card"
149
- fileList={fileList}
150
- onPreview={handlePreview}
151
- onChange={handleChange}
152
- multiple
153
- headers={{ [Authorization]: getAuthorization() }}
154
- data={{ conversation_id: conversationId }}
155
- method="post"
156
- onRemove={handleRemove}
157
- >
158
- {fileList.length >= 8 ? null : uploadButton}
159
- </Upload> */}
160
  {fileList.length > 0 && (
161
  <List
162
  grid={{
163
  gutter: 16,
164
  xs: 1,
165
- sm: 2,
166
- md: 2,
167
  lg: 1,
168
  xl: 2,
169
  xxl: 4,
170
  }}
171
  dataSource={fileList}
 
172
  renderItem={(item) => {
173
  const fileExtension = getExtension(item.name);
 
174
 
175
  return (
176
  <List.Item>
177
  <Card className={styles.documentCard}>
178
- <>
179
- <Flex gap={10} align="center">
180
- {item.status === 'uploading' || !item.response ? (
181
- <Spin
182
- indicator={
183
- <LoadingOutlined style={{ fontSize: 24 }} spin />
184
- }
185
- />
 
 
 
 
 
 
 
 
 
 
 
 
 
186
  ) : (
187
- <FileIcon
188
- id={getFileId(item)}
189
- name={item.name}
190
- ></FileIcon>
 
 
191
  )}
192
- <Flex vertical style={{ width: '90%' }}>
193
- <Text
194
- ellipsis={{ tooltip: item.name }}
195
- className={styles.nameText}
196
- >
197
- <b> {item.name}</b>
198
- </Text>
199
- {item.percent !== 100 ? (
200
- '上传中'
201
- ) : !item.response ? (
202
- '解析中'
203
- ) : (
204
- <Space>
205
- <span>{fileExtension?.toUpperCase()},</span>
206
- </Space>
207
- )}
208
- </Flex>
209
  </Flex>
210
- </>
211
 
212
  {item.status !== 'uploading' && (
213
  <CloseCircleOutlined
 
1
  import { Authorization } from '@/constants/authorization';
2
  import { useTranslate } from '@/hooks/common-hooks';
3
+ import {
4
+ useFetchDocumentInfosByIds,
5
+ useRemoveNextDocument,
6
+ } from '@/hooks/document-hooks';
7
  import { getAuthorization } from '@/utils/authorization-util';
8
  import { getExtension } from '@/utils/document-util';
9
+ import { formatBytes } from '@/utils/file-util';
10
+ import { CloseCircleOutlined, LoadingOutlined } from '@ant-design/icons';
 
 
 
 
11
  import type { GetProp, UploadFile } from 'antd';
12
  import {
13
  Button,
 
21
  Upload,
22
  UploadProps,
23
  } from 'antd';
24
+ import classNames from 'classnames';
25
  import get from 'lodash/get';
26
+ import { ChangeEventHandler, useCallback, useEffect, useState } from 'react';
27
  import FileIcon from '../file-icon';
28
+ import SvgIcon from '../svg-icon';
29
  import styles from './index.less';
30
 
31
  type FileType = Parameters<GetProp<UploadProps, 'beforeUpload'>>[0];
 
33
 
34
  const getFileId = (file: UploadFile) => get(file, 'response.data.0');
35
 
36
+ const getFileIds = (fileList: UploadFile[]) => {
37
+ const ids = fileList.reduce((pre, cur) => {
38
+ return pre.concat(get(cur, 'response.data', []));
39
+ }, []);
40
+
41
+ return ids;
42
+ };
43
+
44
  interface IProps {
45
  disabled: boolean;
46
  value: string;
 
49
  onPressEnter(documentIds: string[]): void;
50
  onInputChange: ChangeEventHandler<HTMLInputElement>;
51
  conversationId: string;
52
+ uploadUrl?: string;
53
  }
54
 
55
  const getBase64 = (file: FileType): Promise<string> =>
 
68
  sendLoading,
69
  onInputChange,
70
  conversationId,
71
+ uploadUrl = '/v1/document/upload_and_parse',
72
  }: IProps) => {
73
  const { t } = useTranslate('chat');
74
  const { removeDocument } = useRemoveNextDocument();
75
+ const { data: documentInfos, setDocumentIds } = useFetchDocumentInfosByIds();
76
 
77
  const [fileList, setFileList] = useState<UploadFile[]>([]);
78
 
 
89
 
90
  const handlePressEnter = useCallback(async () => {
91
  if (isUploadingFile) return;
92
+ const ids = getFileIds(fileList);
 
 
93
 
94
  onPressEnter(ids);
95
  setFileList([]);
 
108
  [removeDocument],
109
  );
110
 
111
+ const getDocumentInfoById = useCallback(
112
+ (id: string) => {
113
+ return documentInfos.find((x) => x.id === id);
114
+ },
115
+ [documentInfos],
116
  );
117
 
118
+ useEffect(() => {
119
+ const ids = getFileIds(fileList);
120
+ setDocumentIds(ids);
121
+ }, [fileList, setDocumentIds]);
122
+
123
  return (
124
  <Flex gap={20} vertical className={styles.messageInputWrapper}>
125
  <Input
 
127
  placeholder={t('sendPlaceholder')}
128
  value={value}
129
  disabled={disabled}
130
+ className={classNames({ [styles.inputWrapper]: fileList.length === 0 })}
131
  suffix={
132
  <Space>
133
+ {conversationId && (
134
+ <Upload
135
+ action={uploadUrl}
136
+ fileList={fileList}
137
+ onPreview={handlePreview}
138
+ onChange={handleChange}
139
+ multiple
140
+ headers={{ [Authorization]: getAuthorization() }}
141
+ data={{ conversation_id: conversationId }}
142
+ method="post"
143
+ onRemove={handleRemove}
144
+ showUploadList={false}
145
+ >
146
+ <Button
147
+ type={'text'}
148
+ icon={
149
+ <SvgIcon name="paper-clip" width={18} height={22}></SvgIcon>
150
+ }
151
+ ></Button>
152
+ </Upload>
153
+ )}
154
  <Button
155
  type="primary"
156
  onClick={handlePressEnter}
 
164
  onPressEnter={handlePressEnter}
165
  onChange={onInputChange}
166
  />
167
+
 
 
 
 
 
 
 
 
 
 
 
 
 
168
  {fileList.length > 0 && (
169
  <List
170
  grid={{
171
  gutter: 16,
172
  xs: 1,
173
+ sm: 1,
174
+ md: 1,
175
  lg: 1,
176
  xl: 2,
177
  xxl: 4,
178
  }}
179
  dataSource={fileList}
180
+ className={styles.listWrapper}
181
  renderItem={(item) => {
182
  const fileExtension = getExtension(item.name);
183
+ const id = getFileId(item);
184
 
185
  return (
186
  <List.Item>
187
  <Card className={styles.documentCard}>
188
+ <Flex gap={10} align="center">
189
+ {item.status === 'uploading' || !item.response ? (
190
+ <Spin
191
+ indicator={
192
+ <LoadingOutlined style={{ fontSize: 24 }} spin />
193
+ }
194
+ />
195
+ ) : (
196
+ <FileIcon id={id} name={item.name}></FileIcon>
197
+ )}
198
+ <Flex vertical style={{ width: '90%' }}>
199
+ <Text
200
+ ellipsis={{ tooltip: item.name }}
201
+ className={styles.nameText}
202
+ >
203
+ <b> {item.name}</b>
204
+ </Text>
205
+ {item.percent !== 100 ? (
206
+ t('uploading')
207
+ ) : !item.response ? (
208
+ t('parsing')
209
  ) : (
210
+ <Space>
211
+ <span>{fileExtension?.toUpperCase()},</span>
212
+ <span>
213
+ {formatBytes(getDocumentInfoById(id)?.size ?? 0)}
214
+ </span>
215
+ </Space>
216
  )}
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
217
  </Flex>
218
+ </Flex>
219
 
220
  {item.status !== 'uploading' && (
221
  <CloseCircleOutlined
web/src/locales/en.ts CHANGED
@@ -422,6 +422,8 @@ The above is the content you need to summarize.`,
422
  extensionTitle: 'Chrome Extension',
423
  tokenError: 'Please create API Token first!',
424
  searching: 'searching...',
 
 
425
  },
426
  setting: {
427
  profile: 'Profile',
 
422
  extensionTitle: 'Chrome Extension',
423
  tokenError: 'Please create API Token first!',
424
  searching: 'searching...',
425
+ parsing: 'Parsing',
426
+ uploading: 'Uploading',
427
  },
428
  setting: {
429
  profile: 'Profile',
web/src/locales/zh-traditional.ts CHANGED
@@ -392,6 +392,8 @@ export default {
392
  extensionTitle: 'Chrome 插件',
393
  tokenError: '請先創建 Api Token!',
394
  searching: '搜索中',
 
 
395
  },
396
  setting: {
397
  profile: '概述',
 
392
  extensionTitle: 'Chrome 插件',
393
  tokenError: '請先創建 Api Token!',
394
  searching: '搜索中',
395
+ parsing: '解析中',
396
+ uploading: '上傳中',
397
  },
398
  setting: {
399
  profile: '概述',
web/src/locales/zh.ts CHANGED
@@ -409,6 +409,8 @@ export default {
409
  extensionTitle: 'Chrome 插件',
410
  tokenError: '请先创建 Api Token!',
411
  searching: '搜索中',
 
 
412
  },
413
  setting: {
414
  profile: '概要',
 
409
  extensionTitle: 'Chrome 插件',
410
  tokenError: '请先创建 Api Token!',
411
  searching: '搜索中',
412
+ parsing: '解析中',
413
+ uploading: '上传中',
414
  },
415
  setting: {
416
  profile: '概要',
web/src/pages/chat/share/large.tsx CHANGED
@@ -1,8 +1,8 @@
 
1
  import MessageItem from '@/components/message-item';
2
  import { MessageType } from '@/constants/chat';
3
- import { useTranslate } from '@/hooks/common-hooks';
4
  import { useSendButtonDisabled } from '@/pages/chat/hooks';
5
- import { Button, Flex, Input, Spin } from 'antd';
6
  import { forwardRef } from 'react';
7
  import {
8
  useCreateSharedConversationOnMount,
@@ -10,11 +10,9 @@ import {
10
  useSendSharedMessage,
11
  } from '../shared-hooks';
12
  import { buildMessageItemReference } from '../utils';
13
-
14
  import styles from './index.less';
15
 
16
  const ChatContainer = () => {
17
- const { t } = useTranslate('chat');
18
  const { conversationId } = useCreateSharedConversationOnMount();
19
  const {
20
  currentConversation: conversation,
@@ -65,24 +63,17 @@ const ChatContainer = () => {
65
  </div>
66
  <div ref={ref} />
67
  </Flex>
68
- <Input
69
- size="large"
70
- placeholder={t('sendPlaceholder')}
71
  value={value}
72
- // disabled={disabled}
73
- suffix={
74
- <Button
75
- type="primary"
76
- onClick={handlePressEnter}
77
- loading={sendLoading}
78
- disabled={sendDisabled}
79
- >
80
- {t('send')}
81
- </Button>
82
- }
83
  onPressEnter={handlePressEnter}
84
- onChange={handleInputChange}
85
- />
 
86
  </Flex>
87
  </>
88
  );
 
1
+ import MessageInput from '@/components/message-input';
2
  import MessageItem from '@/components/message-item';
3
  import { MessageType } from '@/constants/chat';
 
4
  import { useSendButtonDisabled } from '@/pages/chat/hooks';
5
+ import { Flex, Spin } from 'antd';
6
  import { forwardRef } from 'react';
7
  import {
8
  useCreateSharedConversationOnMount,
 
10
  useSendSharedMessage,
11
  } from '../shared-hooks';
12
  import { buildMessageItemReference } from '../utils';
 
13
  import styles from './index.less';
14
 
15
  const ChatContainer = () => {
 
16
  const { conversationId } = useCreateSharedConversationOnMount();
17
  const {
18
  currentConversation: conversation,
 
63
  </div>
64
  <div ref={ref} />
65
  </Flex>
66
+
67
+ <MessageInput
 
68
  value={value}
69
+ disabled={false}
70
+ sendDisabled={sendDisabled}
71
+ conversationId={conversationId}
72
+ onInputChange={handleInputChange}
 
 
 
 
 
 
 
73
  onPressEnter={handlePressEnter}
74
+ sendLoading={sendLoading}
75
+ uploadUrl="/v1/api/document/upload_and_parse"
76
+ ></MessageInput>
77
  </Flex>
78
  </>
79
  );
web/src/utils/file-util.ts CHANGED
@@ -85,3 +85,16 @@ export const downloadFile = ({
85
  downloadElement.click();
86
  document.body.removeChild(downloadElement);
87
  };
 
 
 
 
 
 
 
 
 
 
 
 
 
 
85
  downloadElement.click();
86
  document.body.removeChild(downloadElement);
87
  };
88
+
89
+ const Units = ['bytes', 'KB', 'MB', 'GB', 'TB', 'PB', 'EB', 'ZB', 'YB'];
90
+
91
+ export const formatBytes = (x: string | number) => {
92
+ let l = 0,
93
+ n = (typeof x === 'string' ? parseInt(x, 10) : x) || 0;
94
+
95
+ while (n >= 1024 && ++l) {
96
+ n = n / 1024;
97
+ }
98
+
99
+ return n.toFixed(n < 10 && l > 0 ? 1 : 0) + ' ' + Units[l];
100
+ };