balibabu commited on
Commit
6a6f6eb
·
1 Parent(s): 61ec69c

feat: rename conversation and delete conversation and preview reference image and fetch file thumbnails (#79)

Browse files

* feat: fetch file thumbnails

* feat: preview reference image

* feat: delete conversation

* feat: rename conversation

web/src/components/rename-modal/index.tsx ADDED
@@ -0,0 +1,78 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ import { Form, Input, Modal } from 'antd';
2
+ import { useEffect } from 'react';
3
+ import { IModalManagerChildrenProps } from '../modal-manager';
4
+
5
+ interface IProps extends Omit<IModalManagerChildrenProps, 'showModal'> {
6
+ loading: boolean;
7
+ initialName: string;
8
+ onOk: (name: string) => void;
9
+ showModal?(): void;
10
+ }
11
+
12
+ const RenameModal = ({
13
+ visible,
14
+ hideModal,
15
+ loading,
16
+ initialName,
17
+ onOk,
18
+ }: IProps) => {
19
+ const [form] = Form.useForm();
20
+
21
+ type FieldType = {
22
+ name?: string;
23
+ };
24
+
25
+ const handleOk = async () => {
26
+ const ret = await form.validateFields();
27
+
28
+ return onOk(ret.name);
29
+ };
30
+
31
+ const handleCancel = () => {
32
+ hideModal();
33
+ };
34
+
35
+ const onFinish = (values: any) => {
36
+ console.log('Success:', values);
37
+ };
38
+
39
+ const onFinishFailed = (errorInfo: any) => {
40
+ console.log('Failed:', errorInfo);
41
+ };
42
+
43
+ useEffect(() => {
44
+ form.setFieldValue('name', initialName);
45
+ }, [initialName, form]);
46
+
47
+ return (
48
+ <Modal
49
+ title="Rename"
50
+ open={visible}
51
+ onOk={handleOk}
52
+ onCancel={handleCancel}
53
+ okButtonProps={{ loading }}
54
+ confirmLoading={loading}
55
+ >
56
+ <Form
57
+ name="basic"
58
+ labelCol={{ span: 4 }}
59
+ wrapperCol={{ span: 20 }}
60
+ style={{ maxWidth: 600 }}
61
+ onFinish={onFinish}
62
+ onFinishFailed={onFinishFailed}
63
+ autoComplete="off"
64
+ form={form}
65
+ >
66
+ <Form.Item<FieldType>
67
+ label="Name"
68
+ name="name"
69
+ rules={[{ required: true, message: 'Please input name!' }]}
70
+ >
71
+ <Input />
72
+ </Form.Item>
73
+ </Form>
74
+ </Modal>
75
+ );
76
+ };
77
+
78
+ export default RenameModal;
web/src/hooks/knowledgeHook.ts CHANGED
@@ -150,3 +150,34 @@ export const useFetchKnowledgeList = (
150
 
151
  return list;
152
  };
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
150
 
151
  return list;
152
  };
153
+
154
+ export const useSelectFileThumbnails = () => {
155
+ const fileThumbnails: Record<string, string> = useSelector(
156
+ (state: any) => state.kFModel.fileThumbnails,
157
+ );
158
+
159
+ return fileThumbnails;
160
+ };
161
+
162
+ export const useFetchFileThumbnails = (docIds?: Array<string>) => {
163
+ const dispatch = useDispatch();
164
+ const fileThumbnails = useSelectFileThumbnails();
165
+
166
+ const fetchFileThumbnails = useCallback(
167
+ (docIds: Array<string>) => {
168
+ dispatch({
169
+ type: 'kFModel/fetch_document_thumbnails',
170
+ payload: { doc_ids: docIds.join(',') },
171
+ });
172
+ },
173
+ [dispatch],
174
+ );
175
+
176
+ useEffect(() => {
177
+ if (docIds) {
178
+ fetchFileThumbnails(docIds);
179
+ }
180
+ }, [docIds, fetchFileThumbnails]);
181
+
182
+ return { fileThumbnails, fetchFileThumbnails };
183
+ };
web/src/pages/add-knowledge/components/knowledge-file/index.less CHANGED
@@ -20,9 +20,11 @@
20
  }
21
 
22
  .img {
23
- height: 16px;
24
- width: 16px;
25
- margin-right: 6px;
 
 
26
  }
27
 
28
  .column {
 
20
  }
21
 
22
  .img {
23
+ height: 24px;
24
+ width: 24px;
25
+ margin-right: 10px;
26
+ display: inline-block;
27
+ vertical-align: middle;
28
  }
29
 
30
  .column {
web/src/pages/add-knowledge/components/knowledge-file/index.tsx CHANGED
@@ -22,7 +22,7 @@ import {
22
  } from 'antd';
23
  import type { ColumnsType } from 'antd/es/table';
24
  import { PaginationProps } from 'antd/lib';
25
- import React, { useEffect, useMemo, useState } from 'react';
26
  import { Link, useDispatch, useNavigate, useSelector } from 'umi';
27
  import CreateEPModal from './createEFileModal';
28
  import styles from './index.less';
@@ -46,7 +46,7 @@ const KnowledgeFile = () => {
46
  const [parser_id, setParserId] = useState('0');
47
  let navigate = useNavigate();
48
 
49
- const getKfList = () => {
50
  const payload = {
51
  kb_id: knowledgeBaseId,
52
  };
@@ -55,7 +55,7 @@ const KnowledgeFile = () => {
55
  type: 'kFModel/getKfList',
56
  payload,
57
  });
58
- };
59
 
60
  const throttledGetDocumentList = () => {
61
  dispatch({
@@ -64,23 +64,29 @@ const KnowledgeFile = () => {
64
  });
65
  };
66
 
67
- const setPagination = (pageNumber = 1, pageSize?: number) => {
68
- const pagination: Pagination = {
69
- current: pageNumber,
70
- } as Pagination;
71
- if (pageSize) {
72
- pagination.pageSize = pageSize;
73
- }
74
- dispatch({
75
- type: 'kFModel/setPagination',
76
- payload: pagination,
77
- });
78
- };
 
 
 
79
 
80
- const onPageChange: PaginationProps['onChange'] = (pageNumber, pageSize) => {
81
- setPagination(pageNumber, pageSize);
82
- getKfList();
83
- };
 
 
 
84
 
85
  const pagination: PaginationProps = useMemo(() => {
86
  return {
@@ -92,7 +98,7 @@ const KnowledgeFile = () => {
92
  pageSizeOptions: [1, 2, 10, 20, 50, 100],
93
  onChange: onPageChange,
94
  };
95
- }, [total, kFModel.pagination]);
96
 
97
  useEffect(() => {
98
  if (knowledgeBaseId) {
@@ -107,7 +113,7 @@ const KnowledgeFile = () => {
107
  type: 'kFModel/pollGetDocumentList-stop',
108
  });
109
  };
110
- }, [knowledgeBaseId]);
111
 
112
  const handleInputChange = (
113
  e: React.ChangeEvent<HTMLInputElement | HTMLTextAreaElement>,
@@ -129,14 +135,14 @@ const KnowledgeFile = () => {
129
  });
130
  };
131
 
132
- const showCEFModal = () => {
133
  dispatch({
134
  type: 'kFModel/updateState',
135
  payload: {
136
  isShowCEFwModal: true,
137
  },
138
  });
139
- };
140
 
141
  const actionItems: MenuProps['items'] = useMemo(() => {
142
  return [
@@ -169,7 +175,7 @@ const KnowledgeFile = () => {
169
  // disabled: true,
170
  },
171
  ];
172
- }, []);
173
 
174
  const toChunk = (id: string) => {
175
  navigate(
@@ -187,13 +193,9 @@ const KnowledgeFile = () => {
187
  title: 'Name',
188
  dataIndex: 'name',
189
  key: 'name',
190
- render: (text: any, { id }) => (
191
  <div className={styles.tochunks} onClick={() => toChunk(id)}>
192
- <img
193
- className={styles.img}
194
- src="https://gw.alipayobjects.com/zos/antfincdn/efFD%24IOql2/weixintupian_20170331104822.jpg"
195
- alt=""
196
- />
197
  {text}
198
  </div>
199
  ),
 
22
  } from 'antd';
23
  import type { ColumnsType } from 'antd/es/table';
24
  import { PaginationProps } from 'antd/lib';
25
+ import React, { useCallback, useEffect, useMemo, useState } from 'react';
26
  import { Link, useDispatch, useNavigate, useSelector } from 'umi';
27
  import CreateEPModal from './createEFileModal';
28
  import styles from './index.less';
 
46
  const [parser_id, setParserId] = useState('0');
47
  let navigate = useNavigate();
48
 
49
+ const getKfList = useCallback(() => {
50
  const payload = {
51
  kb_id: knowledgeBaseId,
52
  };
 
55
  type: 'kFModel/getKfList',
56
  payload,
57
  });
58
+ }, [dispatch, knowledgeBaseId]);
59
 
60
  const throttledGetDocumentList = () => {
61
  dispatch({
 
64
  });
65
  };
66
 
67
+ const setPagination = useCallback(
68
+ (pageNumber = 1, pageSize?: number) => {
69
+ const pagination: Pagination = {
70
+ current: pageNumber,
71
+ } as Pagination;
72
+ if (pageSize) {
73
+ pagination.pageSize = pageSize;
74
+ }
75
+ dispatch({
76
+ type: 'kFModel/setPagination',
77
+ payload: pagination,
78
+ });
79
+ },
80
+ [dispatch],
81
+ );
82
 
83
+ const onPageChange: PaginationProps['onChange'] = useCallback(
84
+ (pageNumber: number, pageSize: number) => {
85
+ setPagination(pageNumber, pageSize);
86
+ getKfList();
87
+ },
88
+ [getKfList, setPagination],
89
+ );
90
 
91
  const pagination: PaginationProps = useMemo(() => {
92
  return {
 
98
  pageSizeOptions: [1, 2, 10, 20, 50, 100],
99
  onChange: onPageChange,
100
  };
101
+ }, [total, kFModel.pagination, onPageChange]);
102
 
103
  useEffect(() => {
104
  if (knowledgeBaseId) {
 
113
  type: 'kFModel/pollGetDocumentList-stop',
114
  });
115
  };
116
+ }, [knowledgeBaseId, dispatch, getKfList]);
117
 
118
  const handleInputChange = (
119
  e: React.ChangeEvent<HTMLInputElement | HTMLTextAreaElement>,
 
135
  });
136
  };
137
 
138
+ const showCEFModal = useCallback(() => {
139
  dispatch({
140
  type: 'kFModel/updateState',
141
  payload: {
142
  isShowCEFwModal: true,
143
  },
144
  });
145
+ }, [dispatch]);
146
 
147
  const actionItems: MenuProps['items'] = useMemo(() => {
148
  return [
 
175
  // disabled: true,
176
  },
177
  ];
178
+ }, [knowledgeBaseId, showCEFModal]);
179
 
180
  const toChunk = (id: string) => {
181
  navigate(
 
193
  title: 'Name',
194
  dataIndex: 'name',
195
  key: 'name',
196
+ render: (text: any, { id, thumbnail }) => (
197
  <div className={styles.tochunks} onClick={() => toChunk(id)}>
198
+ <img className={styles.img} src={thumbnail} alt="" />
 
 
 
 
199
  {text}
200
  </div>
201
  ),
web/src/pages/add-knowledge/components/knowledge-file/model.ts CHANGED
@@ -16,6 +16,7 @@ export interface KFModelState extends BaseState {
16
  data: IKnowledgeFile[];
17
  total: number;
18
  currentRecord: Nullable<IKnowledgeFile>;
 
19
  }
20
 
21
  const model: DvaModel<KFModelState> = {
@@ -34,6 +35,7 @@ const model: DvaModel<KFModelState> = {
34
  current: 1,
35
  pageSize: 10,
36
  },
 
37
  },
38
  reducers: {
39
  updateState(state, { payload }) {
@@ -54,6 +56,9 @@ const model: DvaModel<KFModelState> = {
54
  setPagination(state, { payload }) {
55
  return { ...state, pagination: { ...state.pagination, ...payload } };
56
  },
 
 
 
57
  },
58
  effects: {
59
  *createKf({ payload = {} }, { call }) {
@@ -201,6 +206,12 @@ const model: DvaModel<KFModelState> = {
201
  }
202
  return retcode;
203
  },
 
 
 
 
 
 
204
  },
205
  };
206
  export default model;
 
16
  data: IKnowledgeFile[];
17
  total: number;
18
  currentRecord: Nullable<IKnowledgeFile>;
19
+ fileThumbnails: Record<string, string>;
20
  }
21
 
22
  const model: DvaModel<KFModelState> = {
 
35
  current: 1,
36
  pageSize: 10,
37
  },
38
+ fileThumbnails: {} as Record<string, string>,
39
  },
40
  reducers: {
41
  updateState(state, { payload }) {
 
56
  setPagination(state, { payload }) {
57
  return { ...state, pagination: { ...state.pagination, ...payload } };
58
  },
59
+ setFileThumbnails(state, { payload }) {
60
+ return { ...state, fileThumbnails: payload };
61
+ },
62
  },
63
  effects: {
64
  *createKf({ payload = {} }, { call }) {
 
206
  }
207
  return retcode;
208
  },
209
+ *fetch_document_thumbnails({ payload = {} }, { call, put }) {
210
+ const { data } = yield call(kbService.document_thumbnails, payload);
211
+ if (data.retcode === 0) {
212
+ yield put({ type: 'setFileThumbnails', payload: data.data });
213
+ }
214
+ },
215
  },
216
  };
217
  export default model;
web/src/pages/chat/chat-container/index.less CHANGED
@@ -47,3 +47,7 @@
47
  .referenceChunkImage {
48
  width: 10vw;
49
  }
 
 
 
 
 
47
  .referenceChunkImage {
48
  width: 10vw;
49
  }
50
+
51
+ .referenceImagePreview {
52
+ width: 600px;
53
+ }
web/src/pages/chat/chat-container/index.tsx CHANGED
@@ -3,21 +3,12 @@ import { MessageType } from '@/constants/chat';
3
  import { useOneNamespaceEffectsLoading } from '@/hooks/storeHooks';
4
  import { useSelectUserInfo } from '@/hooks/userSettingHook';
5
  import { IReference, Message } from '@/interfaces/database/chat';
6
- import {
7
- Avatar,
8
- Button,
9
- Flex,
10
- Input,
11
- List,
12
- Popover,
13
- Space,
14
- Typography,
15
- } from 'antd';
16
  import classNames from 'classnames';
17
  import { ChangeEventHandler, useCallback, useMemo, useState } from 'react';
18
  import reactStringReplace from 'react-string-replace';
19
  import {
20
- useFetchConversation,
21
  useGetFileIcon,
22
  useScrollToBottom,
23
  useSendMessage,
@@ -26,6 +17,7 @@ import { IClientConversation } from '../interface';
26
 
27
  import Image from '@/components/image';
28
  import NewDocumentLink from '@/components/new-document-link';
 
29
  import { InfoCircleOutlined } from '@ant-design/icons';
30
  import Markdown from 'react-markdown';
31
  import { visitParents } from 'unist-util-visit-parents';
@@ -56,11 +48,10 @@ const MessageItem = ({
56
  reference: IReference;
57
  }) => {
58
  const userInfo = useSelectUserInfo();
 
59
 
60
  const isAssistant = item.role === MessageType.Assistant;
61
 
62
- const getFileIcon = useGetFileIcon();
63
-
64
  const getPopoverContent = useCallback(
65
  (chunkIndex: number) => {
66
  const chunks = reference?.chunks ?? [];
@@ -75,22 +66,35 @@ const MessageItem = ({
75
  gap={10}
76
  className={styles.referencePopoverWrapper}
77
  >
78
- <Image
79
- id={chunkItem?.img_id}
80
- className={styles.referenceChunkImage}
81
- ></Image>
 
 
 
 
 
 
 
 
 
 
82
  <Space direction={'vertical'}>
83
  <div>{chunkItem?.content_with_weight}</div>
84
  {documentId && (
85
- <NewDocumentLink documentId={documentId}>
86
- {document?.doc_name}
87
- </NewDocumentLink>
 
 
 
88
  )}
89
  </Space>
90
  </Flex>
91
  );
92
  },
93
- [reference],
94
  );
95
 
96
  const renderReference = useCallback(
@@ -163,12 +167,13 @@ const MessageItem = ({
163
  dataSource={referenceDocumentList}
164
  renderItem={(item) => (
165
  <List.Item>
166
- <Typography.Text mark>
167
- {/* <SvgIcon name={getFileIcon(item.doc_name)}></SvgIcon> */}
168
- </Typography.Text>
169
- <NewDocumentLink documentId={item.doc_id}>
170
- {item.doc_name}
171
- </NewDocumentLink>
 
172
  </List.Item>
173
  )}
174
  />
@@ -182,11 +187,10 @@ const MessageItem = ({
182
 
183
  const ChatContainer = () => {
184
  const [value, setValue] = useState('');
185
- const conversation: IClientConversation = useFetchConversation();
186
  const { sendMessage } = useSendMessage();
187
  const loading = useOneNamespaceEffectsLoading('chatModel', [
188
  'completeConversation',
189
- 'getConversation',
190
  ]);
191
  const ref = useScrollToBottom();
192
  useGetFileIcon();
 
3
  import { useOneNamespaceEffectsLoading } from '@/hooks/storeHooks';
4
  import { useSelectUserInfo } from '@/hooks/userSettingHook';
5
  import { IReference, Message } from '@/interfaces/database/chat';
6
+ import { Avatar, Button, Flex, Input, List, Popover, Space } from 'antd';
 
 
 
 
 
 
 
 
 
7
  import classNames from 'classnames';
8
  import { ChangeEventHandler, useCallback, useMemo, useState } from 'react';
9
  import reactStringReplace from 'react-string-replace';
10
  import {
11
+ useFetchConversationOnMount,
12
  useGetFileIcon,
13
  useScrollToBottom,
14
  useSendMessage,
 
17
 
18
  import Image from '@/components/image';
19
  import NewDocumentLink from '@/components/new-document-link';
20
+ import { useSelectFileThumbnails } from '@/hooks/knowledgeHook';
21
  import { InfoCircleOutlined } from '@ant-design/icons';
22
  import Markdown from 'react-markdown';
23
  import { visitParents } from 'unist-util-visit-parents';
 
48
  reference: IReference;
49
  }) => {
50
  const userInfo = useSelectUserInfo();
51
+ const fileThumbnails = useSelectFileThumbnails();
52
 
53
  const isAssistant = item.role === MessageType.Assistant;
54
 
 
 
55
  const getPopoverContent = useCallback(
56
  (chunkIndex: number) => {
57
  const chunks = reference?.chunks ?? [];
 
66
  gap={10}
67
  className={styles.referencePopoverWrapper}
68
  >
69
+ <Popover
70
+ placement="topRight"
71
+ content={
72
+ <Image
73
+ id={chunkItem?.img_id}
74
+ className={styles.referenceImagePreview}
75
+ ></Image>
76
+ }
77
+ >
78
+ <Image
79
+ id={chunkItem?.img_id}
80
+ className={styles.referenceChunkImage}
81
+ ></Image>
82
+ </Popover>
83
  <Space direction={'vertical'}>
84
  <div>{chunkItem?.content_with_weight}</div>
85
  {documentId && (
86
+ <Flex gap={'middle'}>
87
+ <img src={fileThumbnails[documentId]} alt="" />
88
+ <NewDocumentLink documentId={documentId}>
89
+ {document?.doc_name}
90
+ </NewDocumentLink>
91
+ </Flex>
92
  )}
93
  </Space>
94
  </Flex>
95
  );
96
  },
97
+ [reference, fileThumbnails],
98
  );
99
 
100
  const renderReference = useCallback(
 
167
  dataSource={referenceDocumentList}
168
  renderItem={(item) => (
169
  <List.Item>
170
+ {/* <SvgIcon name={getFileIcon(item.doc_name)}></SvgIcon> */}
171
+ <Flex gap={'middle'}>
172
+ <img src={fileThumbnails[item.doc_id]}></img>
173
+ <NewDocumentLink documentId={item.doc_id}>
174
+ {item.doc_name}
175
+ </NewDocumentLink>
176
+ </Flex>
177
  </List.Item>
178
  )}
179
  />
 
187
 
188
  const ChatContainer = () => {
189
  const [value, setValue] = useState('');
190
+ const conversation: IClientConversation = useFetchConversationOnMount();
191
  const { sendMessage } = useSendMessage();
192
  const loading = useOneNamespaceEffectsLoading('chatModel', [
193
  'completeConversation',
 
194
  ]);
195
  const ref = useScrollToBottom();
196
  useGetFileIcon();
web/src/pages/chat/hooks.ts CHANGED
@@ -1,6 +1,8 @@
1
  import showDeleteConfirm from '@/components/deleting-confirm';
2
  import { MessageType } from '@/constants/chat';
3
  import { fileIconMap } from '@/constants/common';
 
 
4
  import { IConversation, IDialog } from '@/interfaces/database/chat';
5
  import { getFileExtension } from '@/utils';
6
  import omit from 'lodash/omit';
@@ -14,7 +16,7 @@ import {
14
  VariableTableDataType,
15
  } from './interface';
16
  import { ChatModelState } from './model';
17
- import { isConversationIdNotExist } from './utils';
18
 
19
  export const useFetchDialogList = () => {
20
  const dispatch = useDispatch();
@@ -204,6 +206,24 @@ export const useSelectFirstDialogOnMount = () => {
204
  return dialogList;
205
  };
206
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
207
  //#region conversation
208
 
209
  export const useCreateTemporaryConversation = () => {
@@ -374,30 +394,50 @@ export const useSetConversation = () => {
374
  return { setConversation };
375
  };
376
 
377
- export const useFetchConversation = () => {
378
- const dispatch = useDispatch();
379
- const { conversationId } = useGetChatSearchParams();
380
- const conversation = useSelector(
381
  (state: any) => state.chatModel.currentConversation,
382
  );
383
- const setCurrentConversation = useSetCurrentConversation();
384
 
385
- const fetchConversation = useCallback(() => {
386
- if (isConversationIdNotExist(conversationId)) {
387
- dispatch<any>({
 
 
 
 
 
 
388
  type: 'chatModel/getConversation',
389
  payload: {
 
390
  conversation_id: conversationId,
391
  },
392
  });
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
393
  } else {
394
  setCurrentConversation({} as IClientConversation);
395
  }
396
- }, [dispatch, conversationId, setCurrentConversation]);
397
 
398
  useEffect(() => {
399
- fetchConversation();
400
- }, [fetchConversation]);
401
 
402
  return conversation;
403
  };
@@ -477,4 +517,83 @@ export const useGetFileIcon = () => {
477
  return getFileIcon;
478
  };
479
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
480
  //#endregion
 
1
  import showDeleteConfirm from '@/components/deleting-confirm';
2
  import { MessageType } from '@/constants/chat';
3
  import { fileIconMap } from '@/constants/common';
4
+ import { useSetModalState } from '@/hooks/commonHooks';
5
+ import { useOneNamespaceEffectsLoading } from '@/hooks/storeHooks';
6
  import { IConversation, IDialog } from '@/interfaces/database/chat';
7
  import { getFileExtension } from '@/utils';
8
  import omit from 'lodash/omit';
 
16
  VariableTableDataType,
17
  } from './interface';
18
  import { ChatModelState } from './model';
19
+ import { isConversationIdExist } from './utils';
20
 
21
  export const useFetchDialogList = () => {
22
  const dispatch = useDispatch();
 
206
  return dialogList;
207
  };
208
 
209
+ export const useHandleItemHover = () => {
210
+ const [activated, setActivated] = useState<string>('');
211
+
212
+ const handleItemEnter = (id: string) => {
213
+ setActivated(id);
214
+ };
215
+
216
+ const handleItemLeave = () => {
217
+ setActivated('');
218
+ };
219
+
220
+ return {
221
+ activated,
222
+ handleItemEnter,
223
+ handleItemLeave,
224
+ };
225
+ };
226
+
227
  //#region conversation
228
 
229
  export const useCreateTemporaryConversation = () => {
 
394
  return { setConversation };
395
  };
396
 
397
+ export const useSelectCurrentConversation = () => {
398
+ const conversation: IClientConversation = useSelector(
 
 
399
  (state: any) => state.chatModel.currentConversation,
400
  );
 
401
 
402
+ return conversation;
403
+ };
404
+
405
+ export const useFetchConversation = () => {
406
+ const dispatch = useDispatch();
407
+
408
+ const fetchConversation = useCallback(
409
+ (conversationId: string, needToBeSaved = true) => {
410
+ return dispatch<any>({
411
  type: 'chatModel/getConversation',
412
  payload: {
413
+ needToBeSaved,
414
  conversation_id: conversationId,
415
  },
416
  });
417
+ },
418
+ [dispatch],
419
+ );
420
+
421
+ return fetchConversation;
422
+ };
423
+
424
+ export const useFetchConversationOnMount = () => {
425
+ const { conversationId } = useGetChatSearchParams();
426
+ const conversation = useSelectCurrentConversation();
427
+ const setCurrentConversation = useSetCurrentConversation();
428
+ const fetchConversation = useFetchConversation();
429
+
430
+ const fetchConversationOnMount = useCallback(() => {
431
+ if (isConversationIdExist(conversationId)) {
432
+ fetchConversation(conversationId);
433
  } else {
434
  setCurrentConversation({} as IClientConversation);
435
  }
436
+ }, [fetchConversation, setCurrentConversation, conversationId]);
437
 
438
  useEffect(() => {
439
+ fetchConversationOnMount();
440
+ }, [fetchConversationOnMount]);
441
 
442
  return conversation;
443
  };
 
517
  return getFileIcon;
518
  };
519
 
520
+ export const useRemoveConversation = () => {
521
+ const dispatch = useDispatch();
522
+ const { dialogId } = useGetChatSearchParams();
523
+ const { handleClickConversation } = useClickConversationCard();
524
+
525
+ const removeConversation = (conversationIds: Array<string>) => async () => {
526
+ const ret = await dispatch<any>({
527
+ type: 'chatModel/removeConversation',
528
+ payload: {
529
+ dialog_id: dialogId,
530
+ conversation_ids: conversationIds,
531
+ },
532
+ });
533
+
534
+ if (ret === 0) {
535
+ handleClickConversation('');
536
+ }
537
+
538
+ return ret;
539
+ };
540
+
541
+ const onRemoveConversation = (conversationIds: Array<string>) => {
542
+ showDeleteConfirm({ onOk: removeConversation(conversationIds) });
543
+ };
544
+
545
+ return { onRemoveConversation };
546
+ };
547
+
548
+ export const useRenameConversation = () => {
549
+ const dispatch = useDispatch();
550
+ const [conversation, setConversation] = useState<IClientConversation>(
551
+ {} as IClientConversation,
552
+ );
553
+ const fetchConversation = useFetchConversation();
554
+ const {
555
+ visible: conversationRenameVisible,
556
+ hideModal: hideConversationRenameModal,
557
+ showModal: showConversationRenameModal,
558
+ } = useSetModalState();
559
+
560
+ const onConversationRenameOk = useCallback(
561
+ async (name: string) => {
562
+ const ret = await dispatch<any>({
563
+ type: 'chatModel/setConversation',
564
+ payload: { ...conversation, conversation_id: conversation.id, name },
565
+ });
566
+
567
+ if (ret.retcode === 0) {
568
+ hideConversationRenameModal();
569
+ }
570
+ },
571
+ [dispatch, conversation, hideConversationRenameModal],
572
+ );
573
+
574
+ const loading = useOneNamespaceEffectsLoading('chatModel', [
575
+ 'setConversation',
576
+ ]);
577
+
578
+ const handleShowConversationRenameModal = useCallback(
579
+ async (conversationId: string) => {
580
+ const ret = await fetchConversation(conversationId, false);
581
+ if (ret.retcode === 0) {
582
+ setConversation(ret.data);
583
+ }
584
+ showConversationRenameModal();
585
+ },
586
+ [showConversationRenameModal, fetchConversation],
587
+ );
588
+
589
+ return {
590
+ conversationRenameLoading: loading,
591
+ initialConversationName: conversation.name,
592
+ onConversationRenameOk,
593
+ conversationRenameVisible,
594
+ hideConversationRenameModal,
595
+ showConversationRenameModal: handleShowConversationRenameModal,
596
+ };
597
+ };
598
+
599
  //#endregion
web/src/pages/chat/index.tsx CHANGED
@@ -11,8 +11,9 @@ import {
11
  Space,
12
  Tag,
13
  } from 'antd';
 
14
  import classNames from 'classnames';
15
- import { useCallback, useState } from 'react';
16
  import ChatConfigurationModal from './chat-configuration-modal';
17
  import ChatContainer from './chat-container';
18
  import {
@@ -21,42 +22,88 @@ import {
21
  useFetchConversationList,
22
  useFetchDialog,
23
  useGetChatSearchParams,
 
 
24
  useRemoveDialog,
 
25
  useSelectConversationList,
26
  useSelectFirstDialogOnMount,
27
  useSetCurrentDialog,
28
  } from './hooks';
29
 
 
30
  import styles from './index.less';
31
 
32
  const Chat = () => {
33
  const dialogList = useSelectFirstDialogOnMount();
34
- const [activated, setActivated] = useState<string>('');
35
  const { visible, hideModal, showModal } = useSetModalState();
36
  const { setCurrentDialog, currentDialog } = useSetCurrentDialog();
37
  const { onRemoveDialog } = useRemoveDialog();
 
38
  const { handleClickDialog } = useClickDialogCard();
39
  const { handleClickConversation } = useClickConversationCard();
40
  const { dialogId, conversationId } = useGetChatSearchParams();
41
  const { list: conversationList, addTemporaryConversation } =
42
  useSelectConversationList();
 
 
 
 
 
 
 
 
 
 
 
 
 
 
43
 
44
  useFetchDialog(dialogId, true);
45
 
46
  const handleAppCardEnter = (id: string) => () => {
47
- setActivated(id);
48
  };
49
 
50
- const handleAppCardLeave = () => {
51
- setActivated('');
52
  };
53
 
54
- const handleShowChatConfigurationModal = (dialogId?: string) => () => {
55
- if (dialogId) {
56
- setCurrentDialog(dialogId);
57
- }
58
- showModal();
59
- };
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
60
 
61
  const handleDialogCardClick = (dialogId: string) => () => {
62
  handleClickDialog(dialogId);
@@ -97,7 +144,35 @@ const Chat = () => {
97
  { type: 'divider' },
98
  {
99
  key: '2',
100
- onClick: () => onRemoveDialog([dialogId]),
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
101
  label: (
102
  <Space>
103
  <DeleteOutlined />
@@ -129,7 +204,7 @@ const Chat = () => {
129
  [styles.chatAppCardSelected]: dialogId === x.id,
130
  })}
131
  onMouseEnter={handleAppCardEnter(x.id)}
132
- onMouseLeave={handleAppCardLeave}
133
  onClick={handleDialogCardClick(x.id)}
134
  >
135
  <Flex justify="space-between" align="center">
@@ -176,11 +251,22 @@ const Chat = () => {
176
  key={x.id}
177
  hoverable
178
  onClick={handleConversationCardClick(x.id)}
 
 
179
  className={classNames(styles.chatTitleCard, {
180
  [styles.chatTitleCardSelected]: x.id === conversationId,
181
  })}
182
  >
183
- <div>{x.name}</div>
 
 
 
 
 
 
 
 
 
184
  </Card>
185
  ))}
186
  </Flex>
@@ -194,6 +280,13 @@ const Chat = () => {
194
  hideModal={hideModal}
195
  id={currentDialog.id}
196
  ></ChatConfigurationModal>
 
 
 
 
 
 
 
197
  </Flex>
198
  );
199
  };
 
11
  Space,
12
  Tag,
13
  } from 'antd';
14
+ import { MenuItemProps } from 'antd/lib/menu/MenuItem';
15
  import classNames from 'classnames';
16
+ import { useCallback } from 'react';
17
  import ChatConfigurationModal from './chat-configuration-modal';
18
  import ChatContainer from './chat-container';
19
  import {
 
22
  useFetchConversationList,
23
  useFetchDialog,
24
  useGetChatSearchParams,
25
+ useHandleItemHover,
26
+ useRemoveConversation,
27
  useRemoveDialog,
28
+ useRenameConversation,
29
  useSelectConversationList,
30
  useSelectFirstDialogOnMount,
31
  useSetCurrentDialog,
32
  } from './hooks';
33
 
34
+ import RenameModal from '@/components/rename-modal';
35
  import styles from './index.less';
36
 
37
  const Chat = () => {
38
  const dialogList = useSelectFirstDialogOnMount();
 
39
  const { visible, hideModal, showModal } = useSetModalState();
40
  const { setCurrentDialog, currentDialog } = useSetCurrentDialog();
41
  const { onRemoveDialog } = useRemoveDialog();
42
+ const { onRemoveConversation } = useRemoveConversation();
43
  const { handleClickDialog } = useClickDialogCard();
44
  const { handleClickConversation } = useClickConversationCard();
45
  const { dialogId, conversationId } = useGetChatSearchParams();
46
  const { list: conversationList, addTemporaryConversation } =
47
  useSelectConversationList();
48
+ const { activated, handleItemEnter, handleItemLeave } = useHandleItemHover();
49
+ const {
50
+ activated: conversationActivated,
51
+ handleItemEnter: handleConversationItemEnter,
52
+ handleItemLeave: handleConversationItemLeave,
53
+ } = useHandleItemHover();
54
+ const {
55
+ conversationRenameLoading,
56
+ initialConversationName,
57
+ onConversationRenameOk,
58
+ conversationRenameVisible,
59
+ hideConversationRenameModal,
60
+ showConversationRenameModal,
61
+ } = useRenameConversation();
62
 
63
  useFetchDialog(dialogId, true);
64
 
65
  const handleAppCardEnter = (id: string) => () => {
66
+ handleItemEnter(id);
67
  };
68
 
69
+ const handleConversationCardEnter = (id: string) => () => {
70
+ handleConversationItemEnter(id);
71
  };
72
 
73
+ const handleShowChatConfigurationModal =
74
+ (dialogId?: string): any =>
75
+ (info: any) => {
76
+ info?.domEvent?.preventDefault();
77
+ info?.domEvent?.stopPropagation();
78
+ if (dialogId) {
79
+ setCurrentDialog(dialogId);
80
+ }
81
+ showModal();
82
+ };
83
+
84
+ const handleRemoveDialog =
85
+ (dialogId: string): MenuItemProps['onClick'] =>
86
+ ({ domEvent }) => {
87
+ domEvent.preventDefault();
88
+ domEvent.stopPropagation();
89
+ onRemoveDialog([dialogId]);
90
+ };
91
+
92
+ const handleRemoveConversation =
93
+ (conversationId: string): MenuItemProps['onClick'] =>
94
+ ({ domEvent }) => {
95
+ domEvent.preventDefault();
96
+ domEvent.stopPropagation();
97
+ onRemoveConversation([conversationId]);
98
+ };
99
+
100
+ const handleShowConversationRenameModal =
101
+ (conversationId: string): MenuItemProps['onClick'] =>
102
+ ({ domEvent }) => {
103
+ domEvent.preventDefault();
104
+ domEvent.stopPropagation();
105
+ showConversationRenameModal(conversationId);
106
+ };
107
 
108
  const handleDialogCardClick = (dialogId: string) => () => {
109
  handleClickDialog(dialogId);
 
144
  { type: 'divider' },
145
  {
146
  key: '2',
147
+ onClick: handleRemoveDialog(dialogId),
148
+ label: (
149
+ <Space>
150
+ <DeleteOutlined />
151
+ Delete chat
152
+ </Space>
153
+ ),
154
+ },
155
+ ];
156
+
157
+ return appItems;
158
+ };
159
+
160
+ const buildConversationItems = (conversationId: string) => {
161
+ const appItems: MenuProps['items'] = [
162
+ {
163
+ key: '1',
164
+ onClick: handleShowConversationRenameModal(conversationId),
165
+ label: (
166
+ <Space>
167
+ <EditOutlined />
168
+ Edit
169
+ </Space>
170
+ ),
171
+ },
172
+ { type: 'divider' },
173
+ {
174
+ key: '2',
175
+ onClick: handleRemoveConversation(conversationId),
176
  label: (
177
  <Space>
178
  <DeleteOutlined />
 
204
  [styles.chatAppCardSelected]: dialogId === x.id,
205
  })}
206
  onMouseEnter={handleAppCardEnter(x.id)}
207
+ onMouseLeave={handleItemLeave}
208
  onClick={handleDialogCardClick(x.id)}
209
  >
210
  <Flex justify="space-between" align="center">
 
251
  key={x.id}
252
  hoverable
253
  onClick={handleConversationCardClick(x.id)}
254
+ onMouseEnter={handleConversationCardEnter(x.id)}
255
+ onMouseLeave={handleConversationItemLeave}
256
  className={classNames(styles.chatTitleCard, {
257
  [styles.chatTitleCardSelected]: x.id === conversationId,
258
  })}
259
  >
260
+ <Flex justify="space-between" align="center">
261
+ <div>{x.name}</div>
262
+ {conversationActivated === x.id && x.id !== '' && (
263
+ <section>
264
+ <Dropdown menu={{ items: buildConversationItems(x.id) }}>
265
+ <ChatAppCube className={styles.cubeIcon}></ChatAppCube>
266
+ </Dropdown>
267
+ </section>
268
+ )}
269
+ </Flex>
270
  </Card>
271
  ))}
272
  </Flex>
 
280
  hideModal={hideModal}
281
  id={currentDialog.id}
282
  ></ChatConfigurationModal>
283
+ <RenameModal
284
+ visible={conversationRenameVisible}
285
+ hideModal={hideConversationRenameModal}
286
+ onOk={onConversationRenameOk}
287
+ initialName={initialConversationName}
288
+ loading={conversationRenameLoading}
289
+ ></RenameModal>
290
  </Flex>
291
  );
292
  };
web/src/pages/chat/model.ts CHANGED
@@ -4,6 +4,7 @@ import { message } from 'antd';
4
  import { DvaModel } from 'umi';
5
  import { v4 as uuid } from 'uuid';
6
  import { IClientConversation, IMessage } from './interface';
 
7
 
8
  export interface ChatModelState {
9
  name: string;
@@ -109,11 +110,19 @@ const model: DvaModel<ChatModelState> = {
109
  return data.retcode;
110
  },
111
  *getConversation({ payload }, { call, put }) {
112
- const { data } = yield call(chatService.getConversation, payload);
113
- if (data.retcode === 0) {
 
 
 
 
 
 
 
 
114
  yield put({ type: 'setCurrentConversation', payload: data.data });
115
  }
116
- return data.retcode;
117
  },
118
  *setConversation({ payload }, { call, put }) {
119
  const { data } = yield call(chatService.setConversation, payload);
@@ -138,6 +147,19 @@ const model: DvaModel<ChatModelState> = {
138
  });
139
  }
140
  },
 
 
 
 
 
 
 
 
 
 
 
 
 
141
  },
142
  };
143
 
 
4
  import { DvaModel } from 'umi';
5
  import { v4 as uuid } from 'uuid';
6
  import { IClientConversation, IMessage } from './interface';
7
+ import { getDocumentIdsFromConversionReference } from './utils';
8
 
9
  export interface ChatModelState {
10
  name: string;
 
110
  return data.retcode;
111
  },
112
  *getConversation({ payload }, { call, put }) {
113
+ const { data } = yield call(chatService.getConversation, {
114
+ conversation_id: payload.conversation_id,
115
+ });
116
+ if (data.retcode === 0 && payload.needToBeSaved) {
117
+ yield put({
118
+ type: 'kFModel/fetch_document_thumbnails',
119
+ payload: {
120
+ doc_ids: getDocumentIdsFromConversionReference(data.data),
121
+ },
122
+ });
123
  yield put({ type: 'setCurrentConversation', payload: data.data });
124
  }
125
+ return data;
126
  },
127
  *setConversation({ payload }, { call, put }) {
128
  const { data } = yield call(chatService.setConversation, payload);
 
147
  });
148
  }
149
  },
150
+ *removeConversation({ payload }, { call, put }) {
151
+ const { data } = yield call(chatService.removeConversation, {
152
+ conversation_ids: payload.conversation_ids,
153
+ });
154
+ if (data.retcode === 0) {
155
+ yield put({
156
+ type: 'listConversation',
157
+ payload: { dialog_id: payload.dialog_id },
158
+ });
159
+ message.success('Deleted successfully !');
160
+ }
161
+ return data.retcode;
162
+ },
163
  },
164
  };
165
 
web/src/pages/chat/utils.ts CHANGED
@@ -1,3 +1,4 @@
 
1
  import { EmptyConversationId, variableEnabledFieldMap } from './constants';
2
 
3
  export const excludeUnEnabledVariables = (values: any) => {
@@ -11,6 +12,23 @@ export const excludeUnEnabledVariables = (values: any) => {
11
  );
12
  };
13
 
14
- export const isConversationIdNotExist = (conversationId: string) => {
15
  return conversationId !== EmptyConversationId && conversationId !== '';
16
  };
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ import { IConversation, IReference } from '@/interfaces/database/chat';
2
  import { EmptyConversationId, variableEnabledFieldMap } from './constants';
3
 
4
  export const excludeUnEnabledVariables = (values: any) => {
 
12
  );
13
  };
14
 
15
+ export const isConversationIdExist = (conversationId: string) => {
16
  return conversationId !== EmptyConversationId && conversationId !== '';
17
  };
18
+
19
+ export const getDocumentIdsFromConversionReference = (data: IConversation) => {
20
+ const documentIds = data.reference.reduce(
21
+ (pre: Array<string>, cur: IReference) => {
22
+ cur.doc_aggs
23
+ .map((x) => x.doc_id)
24
+ .forEach((x) => {
25
+ if (pre.every((y) => y !== x)) {
26
+ pre.push(x);
27
+ }
28
+ });
29
+ return pre;
30
+ },
31
+ [],
32
+ );
33
+ return documentIds.join(',');
34
+ };
web/src/services/chatService.ts CHANGED
@@ -11,6 +11,7 @@ const {
11
  setConversation,
12
  completeConversation,
13
  listConversation,
 
14
  } = api;
15
 
16
  const methods = {
@@ -46,6 +47,10 @@ const methods = {
46
  url: completeConversation,
47
  method: 'post',
48
  },
 
 
 
 
49
  } as const;
50
 
51
  const chatService = registerServer<keyof typeof methods>(methods, request);
 
11
  setConversation,
12
  completeConversation,
13
  listConversation,
14
+ removeConversation,
15
  } = api;
16
 
17
  const methods = {
 
47
  url: completeConversation,
48
  method: 'post',
49
  },
50
+ removeConversation: {
51
+ url: removeConversation,
52
+ method: 'post',
53
+ },
54
  } as const;
55
 
56
  const chatService = registerServer<keyof typeof methods>(methods, request);
web/src/services/kbService.ts CHANGED
@@ -13,6 +13,7 @@ const {
13
  document_rm,
14
  document_create,
15
  document_change_parser,
 
16
  chunk_list,
17
  create_chunk,
18
  set_chunk,
@@ -75,6 +76,10 @@ const methods = {
75
  url: document_change_parser,
76
  method: 'post',
77
  },
 
 
 
 
78
  // chunk管理
79
  chunk_list: {
80
  url: chunk_list,
 
13
  document_rm,
14
  document_create,
15
  document_change_parser,
16
+ document_thumbnails,
17
  chunk_list,
18
  create_chunk,
19
  set_chunk,
 
76
  url: document_change_parser,
77
  method: 'post',
78
  },
79
+ document_thumbnails: {
80
+ url: document_thumbnails,
81
+ method: 'get',
82
+ },
83
  // chunk管理
84
  chunk_list: {
85
  url: chunk_list,
web/src/utils/api.ts CHANGED
@@ -42,6 +42,7 @@ export default {
42
  document_create: `${api_host}/document/create`,
43
  document_run: `${api_host}/document/run`,
44
  document_change_parser: `${api_host}/document/change_parser`,
 
45
 
46
  setDialog: `${api_host}/dialog/set`,
47
  getDialog: `${api_host}/dialog/get`,
 
42
  document_create: `${api_host}/document/create`,
43
  document_run: `${api_host}/document/run`,
44
  document_change_parser: `${api_host}/document/change_parser`,
45
+ document_thumbnails: `${api_host}/document/thumbnails`,
46
 
47
  setDialog: `${api_host}/dialog/set`,
48
  getDialog: `${api_host}/dialog/get`,