balibabu
commited on
Commit
·
8207a08
1
Parent(s):
b2c85d3
feat: add FlowChatBox #918 (#1086)
Browse files### What problem does this PR solve?
feat: add FlowChatBox #918
### Type of change
- [x] New Feature (non-breaking change which adds functionality)
- web/src/components/message-item/index.less +40 -0
- web/src/components/message-item/index.tsx +128 -0
- web/src/hooks/logicHooks.ts +44 -1
- web/src/interfaces/database/flow.ts +1 -1
- web/src/pages/flow/canvas/edge/index.tsx +2 -2
- web/src/pages/flow/chat/box.tsx +104 -0
- web/src/pages/flow/chat/hooks.ts +206 -0
- web/src/pages/flow/chat/index.less +7 -0
- web/src/pages/flow/hooks.ts +8 -8
- web/src/pages/flow/store.ts +7 -2
web/src/components/message-item/index.less
ADDED
@@ -0,0 +1,40 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
1 |
+
.messageItem {
|
2 |
+
padding: 24px 0;
|
3 |
+
.messageItemSection {
|
4 |
+
display: inline-block;
|
5 |
+
}
|
6 |
+
.messageItemSectionLeft {
|
7 |
+
width: 70%;
|
8 |
+
}
|
9 |
+
.messageItemSectionRight {
|
10 |
+
width: 40%;
|
11 |
+
}
|
12 |
+
.messageItemContent {
|
13 |
+
display: inline-flex;
|
14 |
+
gap: 20px;
|
15 |
+
}
|
16 |
+
.messageItemContentReverse {
|
17 |
+
flex-direction: row-reverse;
|
18 |
+
}
|
19 |
+
.messageText {
|
20 |
+
.chunkText();
|
21 |
+
padding: 0 14px;
|
22 |
+
background-color: rgba(249, 250, 251, 1);
|
23 |
+
word-break: break-all;
|
24 |
+
}
|
25 |
+
.messageEmpty {
|
26 |
+
width: 300px;
|
27 |
+
}
|
28 |
+
|
29 |
+
.thumbnailImg {
|
30 |
+
max-width: 20px;
|
31 |
+
}
|
32 |
+
}
|
33 |
+
|
34 |
+
.messageItemLeft {
|
35 |
+
text-align: left;
|
36 |
+
}
|
37 |
+
|
38 |
+
.messageItemRight {
|
39 |
+
text-align: right;
|
40 |
+
}
|
web/src/components/message-item/index.tsx
ADDED
@@ -0,0 +1,128 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
1 |
+
import { ReactComponent as AssistantIcon } from '@/assets/svg/assistant.svg';
|
2 |
+
import { MessageType } from '@/constants/chat';
|
3 |
+
import { useTranslate } from '@/hooks/commonHooks';
|
4 |
+
import { useGetDocumentUrl } from '@/hooks/documentHooks';
|
5 |
+
import { useSelectFileThumbnails } from '@/hooks/knowledgeHook';
|
6 |
+
import { useSelectUserInfo } from '@/hooks/userSettingHook';
|
7 |
+
import { IReference, Message } from '@/interfaces/database/chat';
|
8 |
+
import { IChunk } from '@/interfaces/database/knowledge';
|
9 |
+
import classNames from 'classnames';
|
10 |
+
import { useMemo } from 'react';
|
11 |
+
|
12 |
+
import MarkdownContent from '@/pages/chat/markdown-content';
|
13 |
+
import { getExtension, isPdf } from '@/utils/documentUtils';
|
14 |
+
import { Avatar, Flex, List } from 'antd';
|
15 |
+
import NewDocumentLink from '../new-document-link';
|
16 |
+
import SvgIcon from '../svg-icon';
|
17 |
+
import styles from './index.less';
|
18 |
+
|
19 |
+
const MessageItem = ({
|
20 |
+
item,
|
21 |
+
reference,
|
22 |
+
loading = false,
|
23 |
+
clickDocumentButton,
|
24 |
+
}: {
|
25 |
+
item: Message;
|
26 |
+
reference: IReference;
|
27 |
+
loading?: boolean;
|
28 |
+
clickDocumentButton: (documentId: string, chunk: IChunk) => void;
|
29 |
+
}) => {
|
30 |
+
const userInfo = useSelectUserInfo();
|
31 |
+
const fileThumbnails = useSelectFileThumbnails();
|
32 |
+
const getDocumentUrl = useGetDocumentUrl();
|
33 |
+
const { t } = useTranslate('chat');
|
34 |
+
|
35 |
+
const isAssistant = item.role === MessageType.Assistant;
|
36 |
+
|
37 |
+
const referenceDocumentList = useMemo(() => {
|
38 |
+
return reference?.doc_aggs ?? [];
|
39 |
+
}, [reference?.doc_aggs]);
|
40 |
+
|
41 |
+
const content = useMemo(() => {
|
42 |
+
let text = item.content;
|
43 |
+
if (text === '') {
|
44 |
+
text = t('searching');
|
45 |
+
}
|
46 |
+
return loading ? text?.concat('~~2$$') : text;
|
47 |
+
}, [item.content, loading, t]);
|
48 |
+
|
49 |
+
return (
|
50 |
+
<div
|
51 |
+
className={classNames(styles.messageItem, {
|
52 |
+
[styles.messageItemLeft]: item.role === MessageType.Assistant,
|
53 |
+
[styles.messageItemRight]: item.role === MessageType.User,
|
54 |
+
})}
|
55 |
+
>
|
56 |
+
<section
|
57 |
+
className={classNames(styles.messageItemSection, {
|
58 |
+
[styles.messageItemSectionLeft]: item.role === MessageType.Assistant,
|
59 |
+
[styles.messageItemSectionRight]: item.role === MessageType.User,
|
60 |
+
})}
|
61 |
+
>
|
62 |
+
<div
|
63 |
+
className={classNames(styles.messageItemContent, {
|
64 |
+
[styles.messageItemContentReverse]: item.role === MessageType.User,
|
65 |
+
})}
|
66 |
+
>
|
67 |
+
{item.role === MessageType.User ? (
|
68 |
+
<Avatar
|
69 |
+
size={40}
|
70 |
+
src={
|
71 |
+
userInfo.avatar ??
|
72 |
+
'https://zos.alipayobjects.com/rmsportal/jkjgkEfvpUPVyRjUImniVslZfWPnJuuZ.png'
|
73 |
+
}
|
74 |
+
/>
|
75 |
+
) : (
|
76 |
+
<AssistantIcon></AssistantIcon>
|
77 |
+
)}
|
78 |
+
<Flex vertical gap={8} flex={1}>
|
79 |
+
<b>{isAssistant ? '' : userInfo.nickname}</b>
|
80 |
+
<div className={styles.messageText}>
|
81 |
+
<MarkdownContent
|
82 |
+
content={content}
|
83 |
+
reference={reference}
|
84 |
+
clickDocumentButton={clickDocumentButton}
|
85 |
+
></MarkdownContent>
|
86 |
+
</div>
|
87 |
+
{isAssistant && referenceDocumentList.length > 0 && (
|
88 |
+
<List
|
89 |
+
bordered
|
90 |
+
dataSource={referenceDocumentList}
|
91 |
+
renderItem={(item) => {
|
92 |
+
const fileThumbnail = fileThumbnails[item.doc_id];
|
93 |
+
const fileExtension = getExtension(item.doc_name);
|
94 |
+
return (
|
95 |
+
<List.Item>
|
96 |
+
<Flex gap={'small'} align="center">
|
97 |
+
{fileThumbnail ? (
|
98 |
+
<img
|
99 |
+
src={fileThumbnail}
|
100 |
+
className={styles.thumbnailImg}
|
101 |
+
></img>
|
102 |
+
) : (
|
103 |
+
<SvgIcon
|
104 |
+
name={`file-icon/${fileExtension}`}
|
105 |
+
width={24}
|
106 |
+
></SvgIcon>
|
107 |
+
)}
|
108 |
+
|
109 |
+
<NewDocumentLink
|
110 |
+
link={getDocumentUrl(item.doc_id)}
|
111 |
+
preventDefault={!isPdf(item.doc_name)}
|
112 |
+
>
|
113 |
+
{item.doc_name}
|
114 |
+
</NewDocumentLink>
|
115 |
+
</Flex>
|
116 |
+
</List.Item>
|
117 |
+
);
|
118 |
+
}}
|
119 |
+
/>
|
120 |
+
)}
|
121 |
+
</Flex>
|
122 |
+
</div>
|
123 |
+
</section>
|
124 |
+
</div>
|
125 |
+
);
|
126 |
+
};
|
127 |
+
|
128 |
+
export default MessageItem;
|
web/src/hooks/logicHooks.ts
CHANGED
@@ -9,7 +9,14 @@ import { getAuthorization } from '@/utils/authorizationUtil';
|
|
9 |
import { PaginationProps } from 'antd';
|
10 |
import axios from 'axios';
|
11 |
import { EventSourceParserStream } from 'eventsource-parser/stream';
|
12 |
-
import {
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
13 |
import { useTranslation } from 'react-i18next';
|
14 |
import { useDispatch } from 'umi';
|
15 |
import { useSetModalState, useTranslate } from './commonHooks';
|
@@ -196,3 +203,39 @@ export const useSendMessageWithSse = (
|
|
196 |
|
197 |
return { send, answer, done };
|
198 |
};
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
9 |
import { PaginationProps } from 'antd';
|
10 |
import axios from 'axios';
|
11 |
import { EventSourceParserStream } from 'eventsource-parser/stream';
|
12 |
+
import {
|
13 |
+
ChangeEventHandler,
|
14 |
+
useCallback,
|
15 |
+
useEffect,
|
16 |
+
useMemo,
|
17 |
+
useRef,
|
18 |
+
useState,
|
19 |
+
} from 'react';
|
20 |
import { useTranslation } from 'react-i18next';
|
21 |
import { useDispatch } from 'umi';
|
22 |
import { useSetModalState, useTranslate } from './commonHooks';
|
|
|
203 |
|
204 |
return { send, answer, done };
|
205 |
};
|
206 |
+
|
207 |
+
//#region chat hooks
|
208 |
+
|
209 |
+
export const useScrollToBottom = (id?: string) => {
|
210 |
+
const ref = useRef<HTMLDivElement>(null);
|
211 |
+
|
212 |
+
const scrollToBottom = useCallback(() => {
|
213 |
+
if (id) {
|
214 |
+
ref.current?.scrollIntoView({ behavior: 'instant' });
|
215 |
+
}
|
216 |
+
}, [id]);
|
217 |
+
|
218 |
+
useEffect(() => {
|
219 |
+
scrollToBottom();
|
220 |
+
}, [scrollToBottom]);
|
221 |
+
|
222 |
+
return ref;
|
223 |
+
};
|
224 |
+
|
225 |
+
export const useHandleMessageInputChange = () => {
|
226 |
+
const [value, setValue] = useState('');
|
227 |
+
|
228 |
+
const handleInputChange: ChangeEventHandler<HTMLInputElement> = (e) => {
|
229 |
+
const value = e.target.value;
|
230 |
+
const nextValue = value.replaceAll('\\n', '\n').replaceAll('\\t', '\t');
|
231 |
+
setValue(nextValue);
|
232 |
+
};
|
233 |
+
|
234 |
+
return {
|
235 |
+
handleInputChange,
|
236 |
+
value,
|
237 |
+
setValue,
|
238 |
+
};
|
239 |
+
};
|
240 |
+
|
241 |
+
// #endregion
|
web/src/interfaces/database/flow.ts
CHANGED
@@ -4,7 +4,7 @@ export type DSLComponents = Record<string, IOperator>;
|
|
4 |
|
5 |
export interface DSL {
|
6 |
components: DSLComponents;
|
7 |
-
history
|
8 |
path?: string[];
|
9 |
answer?: any[];
|
10 |
graph?: IGraph;
|
|
|
4 |
|
5 |
export interface DSL {
|
6 |
components: DSLComponents;
|
7 |
+
history: any[];
|
8 |
path?: string[];
|
9 |
answer?: any[];
|
10 |
graph?: IGraph;
|
web/src/pages/flow/canvas/edge/index.tsx
CHANGED
@@ -4,7 +4,7 @@ import {
|
|
4 |
EdgeProps,
|
5 |
getBezierPath,
|
6 |
} from 'reactflow';
|
7 |
-
import
|
8 |
|
9 |
import { useMemo } from 'react';
|
10 |
import styles from './index.less';
|
@@ -21,7 +21,7 @@ export function ButtonEdge({
|
|
21 |
markerEnd,
|
22 |
selected,
|
23 |
}: EdgeProps) {
|
24 |
-
const deleteEdgeById =
|
25 |
const [edgePath, labelX, labelY] = getBezierPath({
|
26 |
sourceX,
|
27 |
sourceY,
|
|
|
4 |
EdgeProps,
|
5 |
getBezierPath,
|
6 |
} from 'reactflow';
|
7 |
+
import useGraphStore from '../../store';
|
8 |
|
9 |
import { useMemo } from 'react';
|
10 |
import styles from './index.less';
|
|
|
21 |
markerEnd,
|
22 |
selected,
|
23 |
}: EdgeProps) {
|
24 |
+
const deleteEdgeById = useGraphStore((state) => state.deleteEdgeById);
|
25 |
const [edgePath, labelX, labelY] = getBezierPath({
|
26 |
sourceX,
|
27 |
sourceY,
|
web/src/pages/flow/chat/box.tsx
ADDED
@@ -0,0 +1,104 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
1 |
+
import MessageItem from '@/components/message-item';
|
2 |
+
import DocumentPreviewer from '@/components/pdf-previewer';
|
3 |
+
import { MessageType } from '@/constants/chat';
|
4 |
+
import { useTranslate } from '@/hooks/commonHooks';
|
5 |
+
import {
|
6 |
+
useClickDrawer,
|
7 |
+
useFetchConversationOnMount,
|
8 |
+
useGetFileIcon,
|
9 |
+
useGetSendButtonDisabled,
|
10 |
+
useSelectConversationLoading,
|
11 |
+
useSendMessage,
|
12 |
+
} from '@/pages/chat/hooks';
|
13 |
+
import { buildMessageItemReference } from '@/pages/chat/utils';
|
14 |
+
import { Button, Drawer, Flex, Input, Spin } from 'antd';
|
15 |
+
|
16 |
+
import styles from './index.less';
|
17 |
+
|
18 |
+
const FlowChatBox = () => {
|
19 |
+
const {
|
20 |
+
ref,
|
21 |
+
currentConversation: conversation,
|
22 |
+
addNewestConversation,
|
23 |
+
removeLatestMessage,
|
24 |
+
addNewestAnswer,
|
25 |
+
} = useFetchConversationOnMount();
|
26 |
+
const {
|
27 |
+
handleInputChange,
|
28 |
+
handlePressEnter,
|
29 |
+
value,
|
30 |
+
loading: sendLoading,
|
31 |
+
} = useSendMessage(
|
32 |
+
conversation,
|
33 |
+
addNewestConversation,
|
34 |
+
removeLatestMessage,
|
35 |
+
addNewestAnswer,
|
36 |
+
);
|
37 |
+
const { visible, hideModal, documentId, selectedChunk, clickDocumentButton } =
|
38 |
+
useClickDrawer();
|
39 |
+
const disabled = useGetSendButtonDisabled();
|
40 |
+
useGetFileIcon();
|
41 |
+
const loading = useSelectConversationLoading();
|
42 |
+
const { t } = useTranslate('chat');
|
43 |
+
|
44 |
+
return (
|
45 |
+
<>
|
46 |
+
<Flex flex={1} className={styles.chatContainer} vertical>
|
47 |
+
<Flex flex={1} vertical className={styles.messageContainer}>
|
48 |
+
<div>
|
49 |
+
<Spin spinning={loading}>
|
50 |
+
{conversation?.message?.map((message, i) => {
|
51 |
+
return (
|
52 |
+
<MessageItem
|
53 |
+
loading={
|
54 |
+
message.role === MessageType.Assistant &&
|
55 |
+
sendLoading &&
|
56 |
+
conversation?.message.length - 1 === i
|
57 |
+
}
|
58 |
+
key={message.id}
|
59 |
+
item={message}
|
60 |
+
reference={buildMessageItemReference(conversation, message)}
|
61 |
+
clickDocumentButton={clickDocumentButton}
|
62 |
+
></MessageItem>
|
63 |
+
);
|
64 |
+
})}
|
65 |
+
</Spin>
|
66 |
+
</div>
|
67 |
+
<div ref={ref} />
|
68 |
+
</Flex>
|
69 |
+
<Input
|
70 |
+
size="large"
|
71 |
+
placeholder={t('sendPlaceholder')}
|
72 |
+
value={value}
|
73 |
+
disabled={disabled}
|
74 |
+
suffix={
|
75 |
+
<Button
|
76 |
+
type="primary"
|
77 |
+
onClick={handlePressEnter}
|
78 |
+
loading={sendLoading}
|
79 |
+
disabled={disabled}
|
80 |
+
>
|
81 |
+
{t('send')}
|
82 |
+
</Button>
|
83 |
+
}
|
84 |
+
onPressEnter={handlePressEnter}
|
85 |
+
onChange={handleInputChange}
|
86 |
+
/>
|
87 |
+
</Flex>
|
88 |
+
<Drawer
|
89 |
+
title="Document Previewer"
|
90 |
+
onClose={hideModal}
|
91 |
+
open={visible}
|
92 |
+
width={'50vw'}
|
93 |
+
>
|
94 |
+
<DocumentPreviewer
|
95 |
+
documentId={documentId}
|
96 |
+
chunk={selectedChunk}
|
97 |
+
visible={visible}
|
98 |
+
></DocumentPreviewer>
|
99 |
+
</Drawer>
|
100 |
+
</>
|
101 |
+
);
|
102 |
+
};
|
103 |
+
|
104 |
+
export default FlowChatBox;
|
web/src/pages/flow/chat/hooks.ts
ADDED
@@ -0,0 +1,206 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
1 |
+
import { MessageType } from '@/constants/chat';
|
2 |
+
import { useFetchFlow } from '@/hooks/flow-hooks';
|
3 |
+
import {
|
4 |
+
useHandleMessageInputChange,
|
5 |
+
// useScrollToBottom,
|
6 |
+
useSendMessageWithSse,
|
7 |
+
} from '@/hooks/logicHooks';
|
8 |
+
import { IAnswer } from '@/interfaces/database/chat';
|
9 |
+
import { IMessage } from '@/pages/chat/interface';
|
10 |
+
import omit from 'lodash/omit';
|
11 |
+
import { useCallback, useEffect, useState } from 'react';
|
12 |
+
import { useParams } from 'umi';
|
13 |
+
import { v4 as uuid } from 'uuid';
|
14 |
+
import { Operator } from '../constant';
|
15 |
+
import useGraphStore from '../store';
|
16 |
+
|
17 |
+
export const useSelectCurrentConversation = () => {
|
18 |
+
const { id: id } = useParams();
|
19 |
+
const findNodeByName = useGraphStore((state) => state.findNodeByName);
|
20 |
+
const [currentMessages, setCurrentMessages] = useState<IMessage[]>([]);
|
21 |
+
|
22 |
+
const { data: flowDetail } = useFetchFlow();
|
23 |
+
const messages = flowDetail.dsl.history;
|
24 |
+
|
25 |
+
const prologue = findNodeByName(Operator.Begin)?.data?.form?.prologue;
|
26 |
+
|
27 |
+
const addNewestQuestion = useCallback(
|
28 |
+
(message: string, answer: string = '') => {
|
29 |
+
setCurrentMessages((pre) => {
|
30 |
+
return [
|
31 |
+
...pre,
|
32 |
+
{
|
33 |
+
role: MessageType.User,
|
34 |
+
content: message,
|
35 |
+
id: uuid(),
|
36 |
+
},
|
37 |
+
{
|
38 |
+
role: MessageType.Assistant,
|
39 |
+
content: answer,
|
40 |
+
id: uuid(),
|
41 |
+
},
|
42 |
+
];
|
43 |
+
});
|
44 |
+
},
|
45 |
+
[],
|
46 |
+
);
|
47 |
+
|
48 |
+
const addNewestAnswer = useCallback(
|
49 |
+
(answer: IAnswer) => {
|
50 |
+
setCurrentMessages((pre) => {
|
51 |
+
const latestMessage = currentMessages?.at(-1);
|
52 |
+
|
53 |
+
if (latestMessage) {
|
54 |
+
return [
|
55 |
+
...pre.slice(0, -1),
|
56 |
+
{
|
57 |
+
...latestMessage,
|
58 |
+
content: answer.answer,
|
59 |
+
reference: answer.reference,
|
60 |
+
},
|
61 |
+
];
|
62 |
+
}
|
63 |
+
return pre;
|
64 |
+
});
|
65 |
+
},
|
66 |
+
[currentMessages],
|
67 |
+
);
|
68 |
+
|
69 |
+
const removeLatestMessage = useCallback(() => {
|
70 |
+
setCurrentMessages((pre) => {
|
71 |
+
const nextMessages = pre?.slice(0, -2) ?? [];
|
72 |
+
return [...pre, ...nextMessages];
|
73 |
+
});
|
74 |
+
}, []);
|
75 |
+
|
76 |
+
const addPrologue = useCallback(() => {
|
77 |
+
if (id === '') {
|
78 |
+
const nextMessage = {
|
79 |
+
role: MessageType.Assistant,
|
80 |
+
content: prologue,
|
81 |
+
id: uuid(),
|
82 |
+
} as IMessage;
|
83 |
+
|
84 |
+
setCurrentMessages({
|
85 |
+
id: '',
|
86 |
+
reference: [],
|
87 |
+
message: [nextMessage],
|
88 |
+
} as any);
|
89 |
+
}
|
90 |
+
}, [id, prologue]);
|
91 |
+
|
92 |
+
useEffect(() => {
|
93 |
+
addPrologue();
|
94 |
+
}, [addPrologue]);
|
95 |
+
|
96 |
+
useEffect(() => {
|
97 |
+
if (id) {
|
98 |
+
setCurrentMessages(messages);
|
99 |
+
}
|
100 |
+
}, [messages, id]);
|
101 |
+
|
102 |
+
return {
|
103 |
+
currentConversation: currentMessages,
|
104 |
+
addNewestQuestion,
|
105 |
+
removeLatestMessage,
|
106 |
+
addNewestAnswer,
|
107 |
+
};
|
108 |
+
};
|
109 |
+
|
110 |
+
// export const useFetchConversationOnMount = () => {
|
111 |
+
// const { conversationId } = useGetChatSearchParams();
|
112 |
+
// const fetchConversation = useFetchConversation();
|
113 |
+
// const {
|
114 |
+
// currentConversation,
|
115 |
+
// addNewestQuestion,
|
116 |
+
// removeLatestMessage,
|
117 |
+
// addNewestAnswer,
|
118 |
+
// } = useSelectCurrentConversation();
|
119 |
+
// const ref = useScrollToBottom(currentConversation);
|
120 |
+
|
121 |
+
// const fetchConversationOnMount = useCallback(() => {
|
122 |
+
// if (isConversationIdExist(conversationId)) {
|
123 |
+
// fetchConversation(conversationId);
|
124 |
+
// }
|
125 |
+
// }, [fetchConversation, conversationId]);
|
126 |
+
|
127 |
+
// useEffect(() => {
|
128 |
+
// fetchConversationOnMount();
|
129 |
+
// }, [fetchConversationOnMount]);
|
130 |
+
|
131 |
+
// return {
|
132 |
+
// currentConversation,
|
133 |
+
// addNewestQuestion,
|
134 |
+
// ref,
|
135 |
+
// removeLatestMessage,
|
136 |
+
// addNewestAnswer,
|
137 |
+
// };
|
138 |
+
// };
|
139 |
+
|
140 |
+
export const useSendMessage = (
|
141 |
+
conversation: any,
|
142 |
+
addNewestQuestion: (message: string, answer?: string) => void,
|
143 |
+
removeLatestMessage: () => void,
|
144 |
+
addNewestAnswer: (answer: IAnswer) => void,
|
145 |
+
) => {
|
146 |
+
const { id: conversationId } = useParams();
|
147 |
+
const { handleInputChange, value, setValue } = useHandleMessageInputChange();
|
148 |
+
|
149 |
+
const { send, answer, done } = useSendMessageWithSse();
|
150 |
+
|
151 |
+
const sendMessage = useCallback(
|
152 |
+
async (message: string, id?: string) => {
|
153 |
+
const res: Response | undefined = await send({
|
154 |
+
conversation_id: id ?? conversationId,
|
155 |
+
messages: [
|
156 |
+
...(conversation?.message ?? []).map((x: IMessage) => omit(x, 'id')),
|
157 |
+
{
|
158 |
+
role: MessageType.User,
|
159 |
+
content: message,
|
160 |
+
},
|
161 |
+
],
|
162 |
+
});
|
163 |
+
|
164 |
+
if (res?.status !== 200) {
|
165 |
+
// cancel loading
|
166 |
+
setValue(message);
|
167 |
+
removeLatestMessage();
|
168 |
+
}
|
169 |
+
},
|
170 |
+
[
|
171 |
+
conversation?.message,
|
172 |
+
conversationId,
|
173 |
+
removeLatestMessage,
|
174 |
+
setValue,
|
175 |
+
send,
|
176 |
+
],
|
177 |
+
);
|
178 |
+
|
179 |
+
const handleSendMessage = useCallback(
|
180 |
+
async (message: string) => {
|
181 |
+
sendMessage(message);
|
182 |
+
},
|
183 |
+
[sendMessage],
|
184 |
+
);
|
185 |
+
|
186 |
+
useEffect(() => {
|
187 |
+
if (answer.answer) {
|
188 |
+
addNewestAnswer(answer);
|
189 |
+
}
|
190 |
+
}, [answer, addNewestAnswer]);
|
191 |
+
|
192 |
+
const handlePressEnter = useCallback(() => {
|
193 |
+
if (done) {
|
194 |
+
setValue('');
|
195 |
+
handleSendMessage(value.trim());
|
196 |
+
}
|
197 |
+
addNewestQuestion(value);
|
198 |
+
}, [addNewestQuestion, handleSendMessage, done, setValue, value]);
|
199 |
+
|
200 |
+
return {
|
201 |
+
handlePressEnter,
|
202 |
+
handleInputChange,
|
203 |
+
value,
|
204 |
+
loading: !done,
|
205 |
+
};
|
206 |
+
};
|
web/src/pages/flow/chat/index.less
ADDED
@@ -0,0 +1,7 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
1 |
+
.chatContainer {
|
2 |
+
padding: 0 0 24px 24px;
|
3 |
+
.messageContainer {
|
4 |
+
overflow-y: auto;
|
5 |
+
padding-right: 24px;
|
6 |
+
}
|
7 |
+
}
|
web/src/pages/flow/hooks.ts
CHANGED
@@ -18,7 +18,7 @@ import { Node, Position, ReactFlowInstance } from 'reactflow';
|
|
18 |
import { v4 as uuidv4 } from 'uuid';
|
19 |
// import { shallow } from 'zustand/shallow';
|
20 |
import { useParams } from 'umi';
|
21 |
-
import
|
22 |
import { buildDslComponentsByGraph } from './utils';
|
23 |
|
24 |
const selector = (state: RFState) => ({
|
@@ -34,7 +34,7 @@ const selector = (state: RFState) => ({
|
|
34 |
export const useSelectCanvasData = () => {
|
35 |
// return useStore(useShallow(selector)); // throw error
|
36 |
// return useStore(selector, shallow);
|
37 |
-
return
|
38 |
};
|
39 |
|
40 |
export const useHandleDrag = () => {
|
@@ -50,7 +50,7 @@ export const useHandleDrag = () => {
|
|
50 |
};
|
51 |
|
52 |
export const useHandleDrop = () => {
|
53 |
-
const addNode =
|
54 |
const [reactFlowInstance, setReactFlowInstance] =
|
55 |
useState<ReactFlowInstance<any, any>>();
|
56 |
|
@@ -124,7 +124,7 @@ export const useShowDrawer = () => {
|
|
124 |
};
|
125 |
|
126 |
export const useHandleKeyUp = () => {
|
127 |
-
const deleteEdge =
|
128 |
const handleKeyUp: KeyboardEventHandler = useCallback(
|
129 |
(e) => {
|
130 |
if (e.code === 'Delete') {
|
@@ -141,7 +141,7 @@ export const useSaveGraph = () => {
|
|
141 |
const { data } = useFetchFlow();
|
142 |
const { setFlow } = useSetFlow();
|
143 |
const { id } = useParams();
|
144 |
-
const { nodes, edges } =
|
145 |
const saveGraph = useCallback(() => {
|
146 |
const dslComponents = buildDslComponentsByGraph(nodes, edges);
|
147 |
setFlow({
|
@@ -155,7 +155,7 @@ export const useSaveGraph = () => {
|
|
155 |
};
|
156 |
|
157 |
export const useHandleFormValuesChange = (id?: string) => {
|
158 |
-
const updateNodeForm =
|
159 |
const handleValuesChange = useCallback(
|
160 |
(changedValues: any, values: any) => {
|
161 |
console.info(changedValues, values);
|
@@ -170,7 +170,7 @@ export const useHandleFormValuesChange = (id?: string) => {
|
|
170 |
};
|
171 |
|
172 |
const useSetGraphInfo = () => {
|
173 |
-
const { setEdges, setNodes } =
|
174 |
const setGraphInfo = useCallback(
|
175 |
({ nodes = [], edges = [] }: IGraph) => {
|
176 |
if (nodes.length && edges.length) {
|
@@ -205,7 +205,7 @@ export const useRunGraph = () => {
|
|
205 |
const { data } = useFetchFlow();
|
206 |
const { runFlow } = useRunFlow();
|
207 |
const { id } = useParams();
|
208 |
-
const { nodes, edges } =
|
209 |
const runGraph = useCallback(() => {
|
210 |
const dslComponents = buildDslComponentsByGraph(nodes, edges);
|
211 |
runFlow({
|
|
|
18 |
import { v4 as uuidv4 } from 'uuid';
|
19 |
// import { shallow } from 'zustand/shallow';
|
20 |
import { useParams } from 'umi';
|
21 |
+
import useGraphStore, { RFState } from './store';
|
22 |
import { buildDslComponentsByGraph } from './utils';
|
23 |
|
24 |
const selector = (state: RFState) => ({
|
|
|
34 |
export const useSelectCanvasData = () => {
|
35 |
// return useStore(useShallow(selector)); // throw error
|
36 |
// return useStore(selector, shallow);
|
37 |
+
return useGraphStore(selector);
|
38 |
};
|
39 |
|
40 |
export const useHandleDrag = () => {
|
|
|
50 |
};
|
51 |
|
52 |
export const useHandleDrop = () => {
|
53 |
+
const addNode = useGraphStore((state) => state.addNode);
|
54 |
const [reactFlowInstance, setReactFlowInstance] =
|
55 |
useState<ReactFlowInstance<any, any>>();
|
56 |
|
|
|
124 |
};
|
125 |
|
126 |
export const useHandleKeyUp = () => {
|
127 |
+
const deleteEdge = useGraphStore((state) => state.deleteEdge);
|
128 |
const handleKeyUp: KeyboardEventHandler = useCallback(
|
129 |
(e) => {
|
130 |
if (e.code === 'Delete') {
|
|
|
141 |
const { data } = useFetchFlow();
|
142 |
const { setFlow } = useSetFlow();
|
143 |
const { id } = useParams();
|
144 |
+
const { nodes, edges } = useGraphStore((state) => state);
|
145 |
const saveGraph = useCallback(() => {
|
146 |
const dslComponents = buildDslComponentsByGraph(nodes, edges);
|
147 |
setFlow({
|
|
|
155 |
};
|
156 |
|
157 |
export const useHandleFormValuesChange = (id?: string) => {
|
158 |
+
const updateNodeForm = useGraphStore((state) => state.updateNodeForm);
|
159 |
const handleValuesChange = useCallback(
|
160 |
(changedValues: any, values: any) => {
|
161 |
console.info(changedValues, values);
|
|
|
170 |
};
|
171 |
|
172 |
const useSetGraphInfo = () => {
|
173 |
+
const { setEdges, setNodes } = useGraphStore((state) => state);
|
174 |
const setGraphInfo = useCallback(
|
175 |
({ nodes = [], edges = [] }: IGraph) => {
|
176 |
if (nodes.length && edges.length) {
|
|
|
205 |
const { data } = useFetchFlow();
|
206 |
const { runFlow } = useRunFlow();
|
207 |
const { id } = useParams();
|
208 |
+
const { nodes, edges } = useGraphStore((state) => state);
|
209 |
const runGraph = useCallback(() => {
|
210 |
const dslComponents = buildDslComponentsByGraph(nodes, edges);
|
211 |
runFlow({
|
web/src/pages/flow/store.ts
CHANGED
@@ -16,6 +16,7 @@ import {
|
|
16 |
} from 'reactflow';
|
17 |
import { create } from 'zustand';
|
18 |
import { devtools } from 'zustand/middleware';
|
|
|
19 |
import { NodeData } from './interface';
|
20 |
|
21 |
export type RFState = {
|
@@ -33,10 +34,11 @@ export type RFState = {
|
|
33 |
addNode: (nodes: Node) => void;
|
34 |
deleteEdge: () => void;
|
35 |
deleteEdgeById: (id: string) => void;
|
|
|
36 |
};
|
37 |
|
38 |
// this is our useStore hook that we can use in our components to get parts of the store and call actions
|
39 |
-
const
|
40 |
devtools((set, get) => ({
|
41 |
nodes: [] as Node[],
|
42 |
edges: [] as Edge[],
|
@@ -86,6 +88,9 @@ const useStore = create<RFState>()(
|
|
86 |
edges: edges.filter((edge) => edge.id !== id),
|
87 |
});
|
88 |
},
|
|
|
|
|
|
|
89 |
updateNodeForm: (nodeId: string, values: any) => {
|
90 |
set({
|
91 |
nodes: get().nodes.map((node) => {
|
@@ -100,4 +105,4 @@ const useStore = create<RFState>()(
|
|
100 |
})),
|
101 |
);
|
102 |
|
103 |
-
export default
|
|
|
16 |
} from 'reactflow';
|
17 |
import { create } from 'zustand';
|
18 |
import { devtools } from 'zustand/middleware';
|
19 |
+
import { Operator } from './constant';
|
20 |
import { NodeData } from './interface';
|
21 |
|
22 |
export type RFState = {
|
|
|
34 |
addNode: (nodes: Node) => void;
|
35 |
deleteEdge: () => void;
|
36 |
deleteEdgeById: (id: string) => void;
|
37 |
+
findNodeByName: (operatorName: Operator) => Node | undefined;
|
38 |
};
|
39 |
|
40 |
// this is our useStore hook that we can use in our components to get parts of the store and call actions
|
41 |
+
const useGraphStore = create<RFState>()(
|
42 |
devtools((set, get) => ({
|
43 |
nodes: [] as Node[],
|
44 |
edges: [] as Edge[],
|
|
|
88 |
edges: edges.filter((edge) => edge.id !== id),
|
89 |
});
|
90 |
},
|
91 |
+
findNodeByName: (name: Operator) => {
|
92 |
+
return get().nodes.find((x) => x.data.label === name);
|
93 |
+
},
|
94 |
updateNodeForm: (nodeId: string, values: any) => {
|
95 |
set({
|
96 |
nodes: get().nodes.map((node) => {
|
|
|
105 |
})),
|
106 |
);
|
107 |
|
108 |
+
export default useGraphStore;
|