diff --git a/web/src/components/message-item/index.less b/web/src/components/message-item/index.less
new file mode 100644
index 000000000..4e6c3b304
--- /dev/null
+++ b/web/src/components/message-item/index.less
@@ -0,0 +1,40 @@
+.messageItem {
+ padding: 24px 0;
+ .messageItemSection {
+ display: inline-block;
+ }
+ .messageItemSectionLeft {
+ width: 70%;
+ }
+ .messageItemSectionRight {
+ width: 40%;
+ }
+ .messageItemContent {
+ display: inline-flex;
+ gap: 20px;
+ }
+ .messageItemContentReverse {
+ flex-direction: row-reverse;
+ }
+ .messageText {
+ .chunkText();
+ padding: 0 14px;
+ background-color: rgba(249, 250, 251, 1);
+ word-break: break-all;
+ }
+ .messageEmpty {
+ width: 300px;
+ }
+
+ .thumbnailImg {
+ max-width: 20px;
+ }
+}
+
+.messageItemLeft {
+ text-align: left;
+}
+
+.messageItemRight {
+ text-align: right;
+}
diff --git a/web/src/components/message-item/index.tsx b/web/src/components/message-item/index.tsx
new file mode 100644
index 000000000..a5064b391
--- /dev/null
+++ b/web/src/components/message-item/index.tsx
@@ -0,0 +1,128 @@
+import { ReactComponent as AssistantIcon } from '@/assets/svg/assistant.svg';
+import { MessageType } from '@/constants/chat';
+import { useTranslate } from '@/hooks/commonHooks';
+import { useGetDocumentUrl } from '@/hooks/documentHooks';
+import { useSelectFileThumbnails } from '@/hooks/knowledgeHook';
+import { useSelectUserInfo } from '@/hooks/userSettingHook';
+import { IReference, Message } from '@/interfaces/database/chat';
+import { IChunk } from '@/interfaces/database/knowledge';
+import classNames from 'classnames';
+import { useMemo } from 'react';
+
+import MarkdownContent from '@/pages/chat/markdown-content';
+import { getExtension, isPdf } from '@/utils/documentUtils';
+import { Avatar, Flex, List } from 'antd';
+import NewDocumentLink from '../new-document-link';
+import SvgIcon from '../svg-icon';
+import styles from './index.less';
+
+const MessageItem = ({
+ item,
+ reference,
+ loading = false,
+ clickDocumentButton,
+}: {
+ item: Message;
+ reference: IReference;
+ loading?: boolean;
+ clickDocumentButton: (documentId: string, chunk: IChunk) => void;
+}) => {
+ const userInfo = useSelectUserInfo();
+ const fileThumbnails = useSelectFileThumbnails();
+ const getDocumentUrl = useGetDocumentUrl();
+ const { t } = useTranslate('chat');
+
+ const isAssistant = item.role === MessageType.Assistant;
+
+ const referenceDocumentList = useMemo(() => {
+ return reference?.doc_aggs ?? [];
+ }, [reference?.doc_aggs]);
+
+ const content = useMemo(() => {
+ let text = item.content;
+ if (text === '') {
+ text = t('searching');
+ }
+ return loading ? text?.concat('~~2$$') : text;
+ }, [item.content, loading, t]);
+
+ return (
+
+
+
+ {item.role === MessageType.User ? (
+
+ ) : (
+
+ )}
+
+ {isAssistant ? '' : userInfo.nickname}
+
+
+
+ {isAssistant && referenceDocumentList.length > 0 && (
+ {
+ const fileThumbnail = fileThumbnails[item.doc_id];
+ const fileExtension = getExtension(item.doc_name);
+ return (
+
+
+ {fileThumbnail ? (
+
+ ) : (
+
+ )}
+
+
+ {item.doc_name}
+
+
+
+ );
+ }}
+ />
+ )}
+
+
+
+
+ );
+};
+
+export default MessageItem;
diff --git a/web/src/hooks/logicHooks.ts b/web/src/hooks/logicHooks.ts
index 0b888839e..b6c2f0e07 100644
--- a/web/src/hooks/logicHooks.ts
+++ b/web/src/hooks/logicHooks.ts
@@ -9,7 +9,14 @@ import { getAuthorization } from '@/utils/authorizationUtil';
import { PaginationProps } from 'antd';
import axios from 'axios';
import { EventSourceParserStream } from 'eventsource-parser/stream';
-import { useCallback, useEffect, useMemo, useState } from 'react';
+import {
+ ChangeEventHandler,
+ useCallback,
+ useEffect,
+ useMemo,
+ useRef,
+ useState,
+} from 'react';
import { useTranslation } from 'react-i18next';
import { useDispatch } from 'umi';
import { useSetModalState, useTranslate } from './commonHooks';
@@ -196,3 +203,39 @@ export const useSendMessageWithSse = (
return { send, answer, done };
};
+
+//#region chat hooks
+
+export const useScrollToBottom = (id?: string) => {
+ const ref = useRef(null);
+
+ const scrollToBottom = useCallback(() => {
+ if (id) {
+ ref.current?.scrollIntoView({ behavior: 'instant' });
+ }
+ }, [id]);
+
+ useEffect(() => {
+ scrollToBottom();
+ }, [scrollToBottom]);
+
+ return ref;
+};
+
+export const useHandleMessageInputChange = () => {
+ const [value, setValue] = useState('');
+
+ const handleInputChange: ChangeEventHandler = (e) => {
+ const value = e.target.value;
+ const nextValue = value.replaceAll('\\n', '\n').replaceAll('\\t', '\t');
+ setValue(nextValue);
+ };
+
+ return {
+ handleInputChange,
+ value,
+ setValue,
+ };
+};
+
+// #endregion
diff --git a/web/src/interfaces/database/flow.ts b/web/src/interfaces/database/flow.ts
index b9d57b91d..517f67013 100644
--- a/web/src/interfaces/database/flow.ts
+++ b/web/src/interfaces/database/flow.ts
@@ -4,7 +4,7 @@ export type DSLComponents = Record;
export interface DSL {
components: DSLComponents;
- history?: any[];
+ history: any[];
path?: string[];
answer?: any[];
graph?: IGraph;
diff --git a/web/src/pages/flow/canvas/edge/index.tsx b/web/src/pages/flow/canvas/edge/index.tsx
index 4aa123b44..614888b64 100644
--- a/web/src/pages/flow/canvas/edge/index.tsx
+++ b/web/src/pages/flow/canvas/edge/index.tsx
@@ -4,7 +4,7 @@ import {
EdgeProps,
getBezierPath,
} from 'reactflow';
-import useStore from '../../store';
+import useGraphStore from '../../store';
import { useMemo } from 'react';
import styles from './index.less';
@@ -21,7 +21,7 @@ export function ButtonEdge({
markerEnd,
selected,
}: EdgeProps) {
- const deleteEdgeById = useStore((state) => state.deleteEdgeById);
+ const deleteEdgeById = useGraphStore((state) => state.deleteEdgeById);
const [edgePath, labelX, labelY] = getBezierPath({
sourceX,
sourceY,
diff --git a/web/src/pages/flow/chat/box.tsx b/web/src/pages/flow/chat/box.tsx
new file mode 100644
index 000000000..38da9550d
--- /dev/null
+++ b/web/src/pages/flow/chat/box.tsx
@@ -0,0 +1,104 @@
+import MessageItem from '@/components/message-item';
+import DocumentPreviewer from '@/components/pdf-previewer';
+import { MessageType } from '@/constants/chat';
+import { useTranslate } from '@/hooks/commonHooks';
+import {
+ useClickDrawer,
+ useFetchConversationOnMount,
+ useGetFileIcon,
+ useGetSendButtonDisabled,
+ useSelectConversationLoading,
+ useSendMessage,
+} from '@/pages/chat/hooks';
+import { buildMessageItemReference } from '@/pages/chat/utils';
+import { Button, Drawer, Flex, Input, Spin } from 'antd';
+
+import styles from './index.less';
+
+const FlowChatBox = () => {
+ const {
+ ref,
+ currentConversation: conversation,
+ addNewestConversation,
+ removeLatestMessage,
+ addNewestAnswer,
+ } = useFetchConversationOnMount();
+ const {
+ handleInputChange,
+ handlePressEnter,
+ value,
+ loading: sendLoading,
+ } = useSendMessage(
+ conversation,
+ addNewestConversation,
+ removeLatestMessage,
+ addNewestAnswer,
+ );
+ const { visible, hideModal, documentId, selectedChunk, clickDocumentButton } =
+ useClickDrawer();
+ const disabled = useGetSendButtonDisabled();
+ useGetFileIcon();
+ const loading = useSelectConversationLoading();
+ const { t } = useTranslate('chat');
+
+ return (
+ <>
+
+
+
+
+ {conversation?.message?.map((message, i) => {
+ return (
+
+ );
+ })}
+
+
+
+
+
+ {t('send')}
+
+ }
+ onPressEnter={handlePressEnter}
+ onChange={handleInputChange}
+ />
+
+
+
+
+ >
+ );
+};
+
+export default FlowChatBox;
diff --git a/web/src/pages/flow/chat/hooks.ts b/web/src/pages/flow/chat/hooks.ts
new file mode 100644
index 000000000..f02c3372e
--- /dev/null
+++ b/web/src/pages/flow/chat/hooks.ts
@@ -0,0 +1,206 @@
+import { MessageType } from '@/constants/chat';
+import { useFetchFlow } from '@/hooks/flow-hooks';
+import {
+ useHandleMessageInputChange,
+ // useScrollToBottom,
+ useSendMessageWithSse,
+} from '@/hooks/logicHooks';
+import { IAnswer } from '@/interfaces/database/chat';
+import { IMessage } from '@/pages/chat/interface';
+import omit from 'lodash/omit';
+import { useCallback, useEffect, useState } from 'react';
+import { useParams } from 'umi';
+import { v4 as uuid } from 'uuid';
+import { Operator } from '../constant';
+import useGraphStore from '../store';
+
+export const useSelectCurrentConversation = () => {
+ const { id: id } = useParams();
+ const findNodeByName = useGraphStore((state) => state.findNodeByName);
+ const [currentMessages, setCurrentMessages] = useState([]);
+
+ const { data: flowDetail } = useFetchFlow();
+ const messages = flowDetail.dsl.history;
+
+ const prologue = findNodeByName(Operator.Begin)?.data?.form?.prologue;
+
+ const addNewestQuestion = useCallback(
+ (message: string, answer: string = '') => {
+ setCurrentMessages((pre) => {
+ return [
+ ...pre,
+ {
+ role: MessageType.User,
+ content: message,
+ id: uuid(),
+ },
+ {
+ role: MessageType.Assistant,
+ content: answer,
+ id: uuid(),
+ },
+ ];
+ });
+ },
+ [],
+ );
+
+ const addNewestAnswer = useCallback(
+ (answer: IAnswer) => {
+ setCurrentMessages((pre) => {
+ const latestMessage = currentMessages?.at(-1);
+
+ if (latestMessage) {
+ return [
+ ...pre.slice(0, -1),
+ {
+ ...latestMessage,
+ content: answer.answer,
+ reference: answer.reference,
+ },
+ ];
+ }
+ return pre;
+ });
+ },
+ [currentMessages],
+ );
+
+ const removeLatestMessage = useCallback(() => {
+ setCurrentMessages((pre) => {
+ const nextMessages = pre?.slice(0, -2) ?? [];
+ return [...pre, ...nextMessages];
+ });
+ }, []);
+
+ const addPrologue = useCallback(() => {
+ if (id === '') {
+ const nextMessage = {
+ role: MessageType.Assistant,
+ content: prologue,
+ id: uuid(),
+ } as IMessage;
+
+ setCurrentMessages({
+ id: '',
+ reference: [],
+ message: [nextMessage],
+ } as any);
+ }
+ }, [id, prologue]);
+
+ useEffect(() => {
+ addPrologue();
+ }, [addPrologue]);
+
+ useEffect(() => {
+ if (id) {
+ setCurrentMessages(messages);
+ }
+ }, [messages, id]);
+
+ return {
+ currentConversation: currentMessages,
+ addNewestQuestion,
+ removeLatestMessage,
+ addNewestAnswer,
+ };
+};
+
+// export const useFetchConversationOnMount = () => {
+// const { conversationId } = useGetChatSearchParams();
+// const fetchConversation = useFetchConversation();
+// const {
+// currentConversation,
+// addNewestQuestion,
+// removeLatestMessage,
+// addNewestAnswer,
+// } = useSelectCurrentConversation();
+// const ref = useScrollToBottom(currentConversation);
+
+// const fetchConversationOnMount = useCallback(() => {
+// if (isConversationIdExist(conversationId)) {
+// fetchConversation(conversationId);
+// }
+// }, [fetchConversation, conversationId]);
+
+// useEffect(() => {
+// fetchConversationOnMount();
+// }, [fetchConversationOnMount]);
+
+// return {
+// currentConversation,
+// addNewestQuestion,
+// ref,
+// removeLatestMessage,
+// addNewestAnswer,
+// };
+// };
+
+export const useSendMessage = (
+ conversation: any,
+ addNewestQuestion: (message: string, answer?: string) => void,
+ removeLatestMessage: () => void,
+ addNewestAnswer: (answer: IAnswer) => void,
+) => {
+ const { id: conversationId } = useParams();
+ const { handleInputChange, value, setValue } = useHandleMessageInputChange();
+
+ const { send, answer, done } = useSendMessageWithSse();
+
+ const sendMessage = useCallback(
+ async (message: string, id?: string) => {
+ const res: Response | undefined = await send({
+ conversation_id: id ?? conversationId,
+ messages: [
+ ...(conversation?.message ?? []).map((x: IMessage) => omit(x, 'id')),
+ {
+ role: MessageType.User,
+ content: message,
+ },
+ ],
+ });
+
+ if (res?.status !== 200) {
+ // cancel loading
+ setValue(message);
+ removeLatestMessage();
+ }
+ },
+ [
+ conversation?.message,
+ conversationId,
+ removeLatestMessage,
+ setValue,
+ send,
+ ],
+ );
+
+ const handleSendMessage = useCallback(
+ async (message: string) => {
+ sendMessage(message);
+ },
+ [sendMessage],
+ );
+
+ useEffect(() => {
+ if (answer.answer) {
+ addNewestAnswer(answer);
+ }
+ }, [answer, addNewestAnswer]);
+
+ const handlePressEnter = useCallback(() => {
+ if (done) {
+ setValue('');
+ handleSendMessage(value.trim());
+ }
+ addNewestQuestion(value);
+ }, [addNewestQuestion, handleSendMessage, done, setValue, value]);
+
+ return {
+ handlePressEnter,
+ handleInputChange,
+ value,
+ loading: !done,
+ };
+};
diff --git a/web/src/pages/flow/chat/index.less b/web/src/pages/flow/chat/index.less
new file mode 100644
index 000000000..8430b1ef6
--- /dev/null
+++ b/web/src/pages/flow/chat/index.less
@@ -0,0 +1,7 @@
+.chatContainer {
+ padding: 0 0 24px 24px;
+ .messageContainer {
+ overflow-y: auto;
+ padding-right: 24px;
+ }
+}
diff --git a/web/src/pages/flow/hooks.ts b/web/src/pages/flow/hooks.ts
index 6dd6d51ac..81152d0ae 100644
--- a/web/src/pages/flow/hooks.ts
+++ b/web/src/pages/flow/hooks.ts
@@ -18,7 +18,7 @@ import { Node, Position, ReactFlowInstance } from 'reactflow';
import { v4 as uuidv4 } from 'uuid';
// import { shallow } from 'zustand/shallow';
import { useParams } from 'umi';
-import useStore, { RFState } from './store';
+import useGraphStore, { RFState } from './store';
import { buildDslComponentsByGraph } from './utils';
const selector = (state: RFState) => ({
@@ -34,7 +34,7 @@ const selector = (state: RFState) => ({
export const useSelectCanvasData = () => {
// return useStore(useShallow(selector)); // throw error
// return useStore(selector, shallow);
- return useStore(selector);
+ return useGraphStore(selector);
};
export const useHandleDrag = () => {
@@ -50,7 +50,7 @@ export const useHandleDrag = () => {
};
export const useHandleDrop = () => {
- const addNode = useStore((state) => state.addNode);
+ const addNode = useGraphStore((state) => state.addNode);
const [reactFlowInstance, setReactFlowInstance] =
useState>();
@@ -124,7 +124,7 @@ export const useShowDrawer = () => {
};
export const useHandleKeyUp = () => {
- const deleteEdge = useStore((state) => state.deleteEdge);
+ const deleteEdge = useGraphStore((state) => state.deleteEdge);
const handleKeyUp: KeyboardEventHandler = useCallback(
(e) => {
if (e.code === 'Delete') {
@@ -141,7 +141,7 @@ export const useSaveGraph = () => {
const { data } = useFetchFlow();
const { setFlow } = useSetFlow();
const { id } = useParams();
- const { nodes, edges } = useStore((state) => state);
+ const { nodes, edges } = useGraphStore((state) => state);
const saveGraph = useCallback(() => {
const dslComponents = buildDslComponentsByGraph(nodes, edges);
setFlow({
@@ -155,7 +155,7 @@ export const useSaveGraph = () => {
};
export const useHandleFormValuesChange = (id?: string) => {
- const updateNodeForm = useStore((state) => state.updateNodeForm);
+ const updateNodeForm = useGraphStore((state) => state.updateNodeForm);
const handleValuesChange = useCallback(
(changedValues: any, values: any) => {
console.info(changedValues, values);
@@ -170,7 +170,7 @@ export const useHandleFormValuesChange = (id?: string) => {
};
const useSetGraphInfo = () => {
- const { setEdges, setNodes } = useStore((state) => state);
+ const { setEdges, setNodes } = useGraphStore((state) => state);
const setGraphInfo = useCallback(
({ nodes = [], edges = [] }: IGraph) => {
if (nodes.length && edges.length) {
@@ -205,7 +205,7 @@ export const useRunGraph = () => {
const { data } = useFetchFlow();
const { runFlow } = useRunFlow();
const { id } = useParams();
- const { nodes, edges } = useStore((state) => state);
+ const { nodes, edges } = useGraphStore((state) => state);
const runGraph = useCallback(() => {
const dslComponents = buildDslComponentsByGraph(nodes, edges);
runFlow({
diff --git a/web/src/pages/flow/store.ts b/web/src/pages/flow/store.ts
index cb63d638e..8f956e551 100644
--- a/web/src/pages/flow/store.ts
+++ b/web/src/pages/flow/store.ts
@@ -16,6 +16,7 @@ import {
} from 'reactflow';
import { create } from 'zustand';
import { devtools } from 'zustand/middleware';
+import { Operator } from './constant';
import { NodeData } from './interface';
export type RFState = {
@@ -33,10 +34,11 @@ export type RFState = {
addNode: (nodes: Node) => void;
deleteEdge: () => void;
deleteEdgeById: (id: string) => void;
+ findNodeByName: (operatorName: Operator) => Node | undefined;
};
// this is our useStore hook that we can use in our components to get parts of the store and call actions
-const useStore = create()(
+const useGraphStore = create()(
devtools((set, get) => ({
nodes: [] as Node[],
edges: [] as Edge[],
@@ -86,6 +88,9 @@ const useStore = create()(
edges: edges.filter((edge) => edge.id !== id),
});
},
+ findNodeByName: (name: Operator) => {
+ return get().nodes.find((x) => x.data.label === name);
+ },
updateNodeForm: (nodeId: string, values: any) => {
set({
nodes: get().nodes.map((node) => {
@@ -100,4 +105,4 @@ const useStore = create()(
})),
);
-export default useStore;
+export default useGraphStore;