Browse Source

Feat: Share agent dialog box externally #3221 (#9005)

### What problem does this PR solve?

Feat: Share agent dialog box externally #3221

### Type of change


- [x] New Feature (non-breaking change which adds functionality)
tags/v0.20.0
balibabu 3 months ago
parent
commit
5f0ec005ba
No account linked to committer's email address

+ 1
- 1
web/public/iconfont.js
File diff suppressed because it is too large
View File


+ 7
- 3
web/src/components/next-message-item/group-button.tsx View File

showLikeButton: boolean; showLikeButton: boolean;
audioBinary?: string; audioBinary?: string;
showLoudspeaker?: boolean; showLoudspeaker?: boolean;
showLog?: boolean;
} }


export const AssistantGroupButton = ({ export const AssistantGroupButton = ({
audioBinary, audioBinary,
showLikeButton, showLikeButton,
showLoudspeaker = true, showLoudspeaker = true,
showLog = true,
}: IProps) => { }: IProps) => {
const { visible, hideModal, showModal, onFeedbackOk, loading } = const { visible, hideModal, showModal, onFeedbackOk, loading } =
useSendFeedback(messageId); useSendFeedback(messageId);
<PromptIcon style={{ fontSize: '16px' }} /> <PromptIcon style={{ fontSize: '16px' }} />
</Radio.Button> </Radio.Button>
)} )}
<ToggleGroupItem value="f" onClick={handleShowLogSheet}>
<NotebookText className="size-4" />
</ToggleGroupItem>
{showLog && (
<ToggleGroupItem value="f" onClick={handleShowLogSheet}>
<NotebookText className="size-4" />
</ToggleGroupItem>
)}
</ToggleGroup> </ToggleGroup>
{visible && ( {visible && (
<FeedbackModal <FeedbackModal

+ 3
- 0
web/src/components/next-message-item/index.tsx View File

index: number; index: number;
showLikeButton?: boolean; showLikeButton?: boolean;
showLoudspeaker?: boolean; showLoudspeaker?: boolean;
showLog?: boolean;
} }


function MessageItem({ function MessageItem({
showLoudspeaker = true, showLoudspeaker = true,
visibleAvatar = true, visibleAvatar = true,
children, children,
showLog,
}: IProps) { }: IProps) {
const { theme } = useTheme(); const { theme } = useTheme();
const isAssistant = item.role === MessageType.Assistant; const isAssistant = item.role === MessageType.Assistant;
showLikeButton={showLikeButton} showLikeButton={showLikeButton}
audioBinary={item.audio_binary} audioBinary={item.audio_binary}
showLoudspeaker={showLoudspeaker} showLoudspeaker={showLoudspeaker}
showLog={showLog}
></AssistantGroupButton> ></AssistantGroupButton>
) : ( ) : (
<UserGroupButton <UserGroupButton

+ 1
- 0
web/src/constants/setting.ts View File

System = 'system', System = 'system',
Api = 'api', Api = 'api',
Team = 'team', Team = 'team',
MCP = 'mcp',
Logout = 'logout', Logout = 'logout',
} }



+ 7
- 2
web/src/hooks/logic-hooks/navigate-hooks.ts View File

navigate(Routes.Chat); navigate(Routes.Chat);
}, [navigate]); }, [navigate]);


const navigateToAgentList = useCallback(() => {
const navigateToAgents = useCallback(() => {
navigate(Routes.Agents); navigate(Routes.Agents);
}, [navigate]); }, [navigate]);


const navigateToAgentList = useCallback(() => {
navigate(Routes.AgentList);
}, [navigate]);

const navigateToAgent = useCallback( const navigateToAgent = useCallback(
(id: string) => () => { (id: string) => () => {
navigate(`${Routes.Agent}/${id}`); navigate(`${Routes.Agent}/${id}`);
navigateToChunkParsedResult, navigateToChunkParsedResult,
getQueryString, getQueryString,
navigateToChunk, navigateToChunk,
navigateToAgentList,
navigateToAgents,
navigateToAgent, navigateToAgent,
navigateToAgentTemplates, navigateToAgentTemplates,
navigateToSearchList, navigateToSearchList,
navigateToSearch, navigateToSearch,
navigateToFiles, navigateToFiles,
navigateToAgentList,
}; };
}; };

+ 30
- 0
web/src/hooks/use-agent-request.ts View File

FetchInputForm = 'fetchInputForm', FetchInputForm = 'fetchInputForm',
FetchVersionList = 'fetchVersionList', FetchVersionList = 'fetchVersionList',
FetchVersion = 'fetchVersion', FetchVersion = 'fetchVersion',
FetchAgentAvatar = 'fetchAgentAvatar',
} }


export const EmptyDsl = { export const EmptyDsl = {


return { data, loading }; return { data, loading };
}; };

export const useFetchAgentAvatar = (): {
data: IFlow;
loading: boolean;
refetch: () => void;
} => {
const { sharedId } = useGetSharedChatSearchParams();

const {
data,
isFetching: loading,
refetch,
} = useQuery({
queryKey: [AgentApiAction.FetchAgentAvatar],
initialData: {} as IFlow,
refetchOnReconnect: false,
refetchOnMount: false,
refetchOnWindowFocus: false,
gcTime: 0,
queryFn: async () => {
if (!sharedId) return {};
const { data } = await agentService.fetchAgentAvatar(sharedId);

return data?.data ?? {};
},
});

return { data, loading, refetch };
};

+ 1
- 1
web/src/layouts/components/header/index.tsx View File

{ path: '/knowledge', name: t('knowledgeBase'), icon: KnowledgeBaseIcon }, { path: '/knowledge', name: t('knowledgeBase'), icon: KnowledgeBaseIcon },
{ path: '/chat', name: t('chat'), icon: MessageOutlined }, { path: '/chat', name: t('chat'), icon: MessageOutlined },
{ path: '/search', name: t('search'), icon: SearchOutlined }, { path: '/search', name: t('search'), icon: SearchOutlined },
{ path: '/flow', name: t('flow'), icon: GraphIcon },
{ path: '/agent-list', name: t('flow'), icon: GraphIcon },
{ path: '/file', name: t('fileManager'), icon: FileIcon }, { path: '/file', name: t('fileManager'), icon: FileIcon },
], ],
[t], [t],

+ 1
- 0
web/src/locales/en.ts View File

view: 'View', view: 'View',
modelsToBeAddedTooltip: modelsToBeAddedTooltip:
'If your model provider is not listed but claims to be "OpenAI-compatible", select the OpenAI-API-compatible card to add the relevant model(s). ', 'If your model provider is not listed but claims to be "OpenAI-compatible", select the OpenAI-API-compatible card to add the relevant model(s). ',
mcp: 'MCP',
}, },
message: { message: {
registered: 'Registered!', registered: 'Registered!',

+ 32
- 34
web/src/pages/agent/chat/box.tsx View File

import { MessageType } from '@/constants/chat'; import { MessageType } from '@/constants/chat';
import { useGetFileIcon } from '@/pages/chat/hooks'; import { useGetFileIcon } from '@/pages/chat/hooks';
import { Spin } from 'antd';


import { useSendNextMessage } from './hooks';
import { useSendAgentMessage } from './use-send-agent-message';


import MessageInput from '@/components/message-input'; import MessageInput from '@/components/message-input';
import MessageItem from '@/components/next-message-item'; import MessageItem from '@/components/next-message-item';
handleInputChange, handleInputChange,
handlePressEnter, handlePressEnter,
value, value,
loading,
ref, ref,
derivedMessages, derivedMessages,
stopOutputMessage, stopOutputMessage,
sendFormMessage, sendFormMessage,
findReferenceByMessageId, findReferenceByMessageId,
} = useSendNextMessage();
} = useSendAgentMessage();


const { visible, hideModal, documentId, selectedChunk, clickDocumentButton } = const { visible, hideModal, documentId, selectedChunk, clickDocumentButton } =
useClickDrawer(); useClickDrawer();
<section className="flex flex-1 flex-col px-5 h-[90vh]"> <section className="flex flex-1 flex-col px-5 h-[90vh]">
<div className="flex-1 overflow-auto"> <div className="flex-1 overflow-auto">
<div> <div>
<Spin spinning={loading}>
{derivedMessages?.map((message, i) => {
return (
<MessageItem
loading={
message.role === MessageType.Assistant &&
sendLoading &&
derivedMessages.length - 1 === i
}
key={buildMessageUuidWithRole(message)}
nickname={userInfo.nickname}
avatar={userInfo.avatar}
avatarDialog={canvasInfo.avatar}
item={message}
reference={findReferenceByMessageId(message.id)}
clickDocumentButton={clickDocumentButton}
index={i}
showLikeButton={false}
sendLoading={sendLoading}
>
<DebugContent
parameters={buildInputList(message)}
ok={handleOk(message)}
isNext={false}
btnText={'Submit'}
></DebugContent>
</MessageItem>
);
})}
</Spin>
{/* <Spin spinning={sendLoading}> */}
{derivedMessages?.map((message, i) => {
return (
<MessageItem
loading={
message.role === MessageType.Assistant &&
sendLoading &&
derivedMessages.length - 1 === i
}
key={buildMessageUuidWithRole(message)}
nickname={userInfo.nickname}
avatar={userInfo.avatar}
avatarDialog={canvasInfo.avatar}
item={message}
reference={findReferenceByMessageId(message.id)}
clickDocumentButton={clickDocumentButton}
index={i}
showLikeButton={false}
sendLoading={sendLoading}
>
<DebugContent
parameters={buildInputList(message)}
ok={handleOk(message)}
isNext={false}
btnText={'Submit'}
></DebugContent>
</MessageItem>
);
})}
{/* </Spin> */}
</div> </div>
<div ref={ref} /> <div ref={ref} />
</div> </div>

web/src/pages/agent/chat/hooks.ts → web/src/pages/agent/chat/use-send-agent-message.ts View File

useHandleMessageInputChange, useHandleMessageInputChange,
useSelectDerivedMessages, useSelectDerivedMessages,
} from '@/hooks/logic-hooks'; } from '@/hooks/logic-hooks';
import { useFetchAgent } from '@/hooks/use-agent-request';
import { import {
IEventList, IEventList,
IInputEvent, IInputEvent,
import useGraphStore from '../store'; import useGraphStore from '../store';
import { receiveMessageError } from '../utils'; import { receiveMessageError } from '../utils';


export const useSelectNextMessages = () => {
const { data: flowDetail, loading } = useFetchAgent();
const reference = flowDetail.dsl.retrieval;
const {
derivedMessages,
ref,
addNewestQuestion,
addNewestAnswer,
removeLatestMessage,
removeMessageById,
removeMessagesAfterCurrentMessage,
addNewestOneQuestion,
addNewestOneAnswer,
} = useSelectDerivedMessages();

return {
reference,
loading,
derivedMessages,
ref,
addNewestQuestion,
addNewestAnswer,
removeLatestMessage,
removeMessageById,
addNewestOneQuestion,
addNewestOneAnswer,
removeMessagesAfterCurrentMessage,
};
};

function findMessageFromList(eventList: IEventList) {
export function findMessageFromList(eventList: IEventList) {
const messageEventList = eventList.filter( const messageEventList = eventList.filter(
(x) => x.event === MessageEventType.Message, (x) => x.event === MessageEventType.Message,
) as IMessageEvent[]; ) as IMessageEvent[];
}; };
} }


function findInputFromList(eventList: IEventList) {
export function findInputFromList(eventList: IEventList) {
const inputEvent = eventList.find( const inputEvent = eventList.find(
(x) => x.event === MessageEventType.UserInputs, (x) => x.event === MessageEventType.UserInputs,
) as IInputEvent; ) as IInputEvent;
return get(eventList.at(-1), 'data.outputs._ERROR'); return get(eventList.at(-1), 'data.outputs._ERROR');
} }


const useGetBeginNodePrologue = () => {
export const useGetBeginNodePrologue = () => {
const getNode = useGraphStore((state) => state.getNode); const getNode = useGraphStore((state) => state.getNode);


return useMemo(() => { return useMemo(() => {
}, [getNode]); }, [getNode]);
}; };


export const useSendNextMessage = () => {
export function useFindMessageReference(answerList: IEventList) {
const [messageEndEventList, setMessageEndEventList] = useState<
IMessageEndEvent[]
>([]);

const findReferenceByMessageId = useCallback(
(messageId: string) => {
const event = messageEndEventList.find(
(item) => item.message_id === messageId,
);
if (event) {
return (event?.data as IMessageEndData)?.reference;
}
},
[messageEndEventList],
);

useEffect(() => {
const messageEndEvent = answerList.find(
(x) => x.event === MessageEventType.MessageEnd,
);
if (messageEndEvent) {
setMessageEndEventList((list) => {
const nextList = [...list];
if (
nextList.every((x) => x.message_id !== messageEndEvent.message_id)
) {
nextList.push(messageEndEvent as IMessageEndEvent);
}
return nextList;
});
}
}, [answerList]);

return { findReferenceByMessageId };
}

export const useSendAgentMessage = (url?: string) => {
const { id: agentId } = useParams();
const { handleInputChange, value, setValue } = useHandleMessageInputChange();
const inputs = useSelectBeginNodeDataInputs();
const { send, answerList, done, stopOutputMessage } = useSendMessageBySSE(
url || api.runCanvas,
);
const { findReferenceByMessageId } = useFindMessageReference(answerList);
const prologue = useGetBeginNodePrologue();
const { const {
reference,
loading,
derivedMessages, derivedMessages,
ref, ref,
removeLatestMessage, removeLatestMessage,
removeMessageById, removeMessageById,
addNewestOneQuestion, addNewestOneQuestion,
addNewestOneAnswer, addNewestOneAnswer,
} = useSelectNextMessages();
const { id: agentId } = useParams();
const { handleInputChange, value, setValue } = useHandleMessageInputChange();
const { refetch } = useFetchAgent();
} = useSelectDerivedMessages();
const { addEventList } = useContext(AgentChatLogContext); const { addEventList } = useContext(AgentChatLogContext);
const inputs = useSelectBeginNodeDataInputs();
const [messageEndEventList, setMessageEndEventList] = useState<
IMessageEndEvent[]
>([]);

const { send, answerList, done, stopOutputMessage } = useSendMessageBySSE(
api.runCanvas,
);

const prologue = useGetBeginNodePrologue();


const sendMessage = useCallback( const sendMessage = useCallback(
async ({ message }: { message: Message; messages?: Message[] }) => { async ({ message }: { message: Message; messages?: Message[] }) => {
setValue(message.content); setValue(message.content);
removeLatestMessage(); removeLatestMessage();
} else { } else {
refetch(); // pull the message list after sending the message successfully
// refetch(); // pull the message list after sending the message successfully
} }
}, },
[agentId, send, inputs, setValue, removeLatestMessage, refetch],
[agentId, send, inputs, setValue, removeLatestMessage],
); );


const handleSendMessage = useCallback(
async (message: Message) => {
sendMessage({ message });
const sendFormMessage = useCallback(
(body: { id?: string; inputs: Record<string, BeginQuery> }) => {
send(body);
addNewestOneQuestion({
content: Object.entries(body.inputs)
.map(([key, val]) => `${key}: ${val.value}`)
.join('<br/>'),
role: MessageType.User,
});
}, },
[sendMessage],
[addNewestOneQuestion, send],
); );


useEffect(() => {
const messageEndEvent = answerList.find(
(x) => x.event === MessageEventType.MessageEnd,
);
if (messageEndEvent) {
setMessageEndEventList((list) => {
const nextList = [...list];
if (
nextList.every((x) => x.message_id !== messageEndEvent.message_id)
) {
nextList.push(messageEndEvent as IMessageEndEvent);
}
return nextList;
const handlePressEnter = useCallback(() => {
if (trim(value) === '') return;
const id = uuid();
if (done) {
setValue('');
sendMessage({
message: { id, content: value.trim(), role: MessageType.User },
}); });
} }
}, [addEventList.length, answerList]);
addNewestOneQuestion({
content: value,
id,
role: MessageType.User,
});
}, [value, done, addNewestOneQuestion, setValue, sendMessage]);


useEffect(() => { useEffect(() => {
const { content, id } = findMessageFromList(answerList); const { content, id } = findMessageFromList(answerList);
} }
}, [answerList, addNewestOneAnswer]); }, [answerList, addNewestOneAnswer]);


const handlePressEnter = useCallback(() => {
if (trim(value) === '') return;
const id = uuid();
if (done) {
setValue('');
handleSendMessage({ id, content: value.trim(), role: MessageType.User });
}
addNewestOneQuestion({
content: value,
id,
role: MessageType.User,
});
}, [value, done, addNewestOneQuestion, setValue, handleSendMessage]);

const sendFormMessage = useCallback(
(body: { id?: string; inputs: Record<string, BeginQuery> }) => {
send(body);
addNewestOneQuestion({
content: Object.entries(body.inputs)
.map(([key, val]) => `${key}: ${val.value}`)
.join('<br/>'),
role: MessageType.User,
});
},
[addNewestOneQuestion, send],
);

const findReferenceByMessageId = useCallback(
(messageId: string) => {
const event = messageEndEventList.find(
(item) => item.message_id === messageId,
);
if (event) {
return (event?.data as IMessageEndData)?.reference;
}
},
[messageEndEventList],
);

useEffect(() => { useEffect(() => {
if (prologue) { if (prologue) {
addNewestOneAnswer({ addNewestOneAnswer({
}, [addNewestOneAnswer, agentId, prologue, send, sendFormMessage]); }, [addNewestOneAnswer, agentId, prologue, send, sendFormMessage]);


useEffect(() => { useEffect(() => {
addEventList(answerList);
if (typeof addEventList === 'function') {
addEventList(answerList);
}
}, [addEventList, answerList]); }, [addEventList, answerList]);


return { return {
handleInputChange, handleInputChange,
value, value,
sendLoading: !done, sendLoading: !done,
reference,
loading,
derivedMessages, derivedMessages,
ref, ref,
removeMessageById, removeMessageById,

+ 1
- 1
web/src/pages/agent/embed-dialog/index.tsx View File



const generateIframeSrc = useCallback(() => { const generateIframeSrc = useCallback(() => {
const { visibleAvatar, locale } = values; const { visibleAvatar, locale } = values;
let src = `${location.origin}/chat/share?shared_id=${token}&from=${from}&auth=${beta}`;
let src = `${location.origin}/next-chat/share?shared_id=${token}&from=${from}&auth=${beta}`;
if (visibleAvatar) { if (visibleAvatar) {
src += '&visible_avatar=1'; src += '&visible_avatar=1';
} }

+ 18
- 4
web/src/pages/agent/index.tsx View File

import { SharedFrom } from '@/constants/chat'; import { SharedFrom } from '@/constants/chat';
import { useSetModalState } from '@/hooks/common-hooks'; import { useSetModalState } from '@/hooks/common-hooks';
import { useNavigatePage } from '@/hooks/logic-hooks/navigate-hooks'; import { useNavigatePage } from '@/hooks/logic-hooks/navigate-hooks';
import { useFetchUserInfo } from '@/hooks/user-setting-hooks';
import { ReactFlowProvider } from '@xyflow/react'; import { ReactFlowProvider } from '@xyflow/react';
import { import {
ChevronDown, ChevronDown,
import EmbedDialog from './embed-dialog'; import EmbedDialog from './embed-dialog';
import { useHandleExportOrImportJsonFile } from './hooks/use-export-json'; import { useHandleExportOrImportJsonFile } from './hooks/use-export-json';
import { useFetchDataOnMount } from './hooks/use-fetch-data'; import { useFetchDataOnMount } from './hooks/use-fetch-data';
import { useGetBeginNodeDataInputs } from './hooks/use-get-begin-query';
import {
useGetBeginNodeDataInputs,
useGetBeginNodeDataQueryIsSafe,
} from './hooks/use-get-begin-query';
import { useOpenDocument } from './hooks/use-open-document'; import { useOpenDocument } from './hooks/use-open-document';
import { import {
useSaveGraph, useSaveGraph,
showModal: showChatDrawer, showModal: showChatDrawer,
} = useSetModalState(); } = useSetModalState();
const { t } = useTranslation(); const { t } = useTranslation();
const { data: userInfo } = useFetchUserInfo();

const openDocument = useOpenDocument(); const openDocument = useOpenDocument();
const { const {
handleExportJson, handleExportJson,
hideFileUploadModal, hideFileUploadModal,
} = useHandleExportOrImportJsonFile(); } = useHandleExportOrImportJsonFile();
const { saveGraph, loading } = useSaveGraph(); const { saveGraph, loading } = useSaveGraph();
const { flowDetail } = useFetchDataOnMount();
const { flowDetail: agentDetail } = useFetchDataOnMount();
const inputs = useGetBeginNodeDataInputs(); const inputs = useGetBeginNodeDataInputs();
const { handleRun } = useSaveGraphBeforeOpeningDebugDrawer(showChatDrawer); const { handleRun } = useSaveGraphBeforeOpeningDebugDrawer(showChatDrawer);
const handleRunAgent = useCallback(() => { const handleRunAgent = useCallback(() => {
const { showEmbedModal, hideEmbedModal, embedVisible, beta } = const { showEmbedModal, hideEmbedModal, embedVisible, beta } =
useShowEmbedModal(); useShowEmbedModal();


const isBeginNodeDataQuerySafe = useGetBeginNodeDataQueryIsSafe();

return ( return (
<section className="h-full"> <section className="h-full">
<PageHeader> <PageHeader>
</BreadcrumbItem> </BreadcrumbItem>
<BreadcrumbSeparator /> <BreadcrumbSeparator />
<BreadcrumbItem> <BreadcrumbItem>
<BreadcrumbPage>{flowDetail.title}</BreadcrumbPage>
<BreadcrumbPage>{agentDetail.title}</BreadcrumbPage>
</BreadcrumbItem> </BreadcrumbItem>
</BreadcrumbList> </BreadcrumbList>
</Breadcrumb> </Breadcrumb>
{t('flow.export')} {t('flow.export')}
</AgentDropdownMenuItem> </AgentDropdownMenuItem>
<DropdownMenuSeparator /> <DropdownMenuSeparator />
<AgentDropdownMenuItem onClick={showEmbedModal}>
<AgentDropdownMenuItem
onClick={showEmbedModal}
disabled={
!isBeginNodeDataQuerySafe ||
userInfo.nickname !== agentDetail.nickname
}
>
<ScreenShare /> <ScreenShare />
{t('common.embedIntoSite')} {t('common.embedIntoSite')}
</AgentDropdownMenuItem> </AgentDropdownMenuItem>

+ 23
- 4
web/src/pages/agents/agent-templates.tsx View File

import { PageHeader } from '@/components/page-header'; import { PageHeader } from '@/components/page-header';
import {
Breadcrumb,
BreadcrumbItem,
BreadcrumbLink,
BreadcrumbList,
BreadcrumbPage,
BreadcrumbSeparator,
} from '@/components/ui/breadcrumb';
import { useSetModalState } from '@/hooks/common-hooks'; import { useSetModalState } from '@/hooks/common-hooks';
import { useNavigatePage } from '@/hooks/logic-hooks/navigate-hooks'; import { useNavigatePage } from '@/hooks/logic-hooks/navigate-hooks';
import { useFetchAgentTemplates, useSetAgent } from '@/hooks/use-agent-request'; import { useFetchAgentTemplates, useSetAgent } from '@/hooks/use-agent-request';


return ( return (
<section> <section>
<PageHeader
back={navigateToAgentList}
title={t('flow.createGraph')}
></PageHeader>
<PageHeader>
<Breadcrumb>
<BreadcrumbList>
<BreadcrumbItem>
<BreadcrumbLink onClick={navigateToAgentList}>
Agent
</BreadcrumbLink>
</BreadcrumbItem>
<BreadcrumbSeparator />
<BreadcrumbItem>
<BreadcrumbPage>{t('flow.createGraph')}</BreadcrumbPage>
</BreadcrumbItem>
</BreadcrumbList>
</Breadcrumb>
</PageHeader>
<div className="grid gap-6 sm:grid-cols-1 md:grid-cols-2 lg:grid-cols-4 xl:grid-cols-6 2xl:grid-cols-8 max-h-[94vh] overflow-auto px-8"> <div className="grid gap-6 sm:grid-cols-1 md:grid-cols-2 lg:grid-cols-4 xl:grid-cols-6 2xl:grid-cols-8 max-h-[94vh] overflow-auto px-8">
{list?.map((x) => { {list?.map((x) => {
return ( return (

+ 9
- 118
web/src/pages/next-chats/hooks/use-send-shared-message.ts View File

import { MessageType, SharedFrom } from '@/constants/chat';
import { useCreateNextSharedConversation } from '@/hooks/chat-hooks';
import {
useHandleMessageInputChange,
useSelectDerivedMessages,
useSendMessageWithSse,
} from '@/hooks/logic-hooks';
import { Message } from '@/interfaces/database/chat';
import { message } from 'antd';
import { get } from 'lodash';
import { SharedFrom } from '@/constants/chat';
import { useSendAgentMessage } from '@/pages/agent/chat/use-send-agent-message';
import trim from 'lodash/trim'; import trim from 'lodash/trim';
import { useCallback, useEffect, useState } from 'react';
import { useSearchParams } from 'umi'; import { useSearchParams } from 'umi';
import { v4 as uuid } from 'uuid';

const isCompletionError = (res: any) =>
res && (res?.response.status !== 200 || res?.data?.code !== 0);


export const useSendButtonDisabled = (value: string) => { export const useSendButtonDisabled = (value: string) => {
return trim(value) === ''; return trim(value) === '';
}; };
}; };


export const useSendSharedMessage = () => {
const {
from,
sharedId: conversationId,
data: data,
} = useGetSharedChatSearchParams();
const { createSharedConversation: setConversation } =
useCreateNextSharedConversation();
const { handleInputChange, value, setValue } = useHandleMessageInputChange();
const { send, answer, done, stopOutputMessage } = useSendMessageWithSse(
`/api/v1/${from === SharedFrom.Agent ? 'agentbots' : 'chatbots'}/${conversationId}/completions`,
);
const {
derivedMessages,
ref,
removeLatestMessage,
addNewestAnswer,
addNewestQuestion,
} = useSelectDerivedMessages();
const [hasError, setHasError] = useState(false);

const sendMessage = useCallback(
async (message: Message, id?: string) => {
const res = await send({
conversation_id: id ?? conversationId,
quote: true,
question: message.content,
session_id: get(derivedMessages, '0.session_id'),
});
export function useSendNextSharedMessage() {
const { from, sharedId: conversationId } = useGetSharedChatSearchParams();
const url = `/api/v1/${from === SharedFrom.Agent ? 'agentbots' : 'chatbots'}/${conversationId}/completions`;


if (isCompletionError(res)) {
// cancel loading
setValue(message.content);
removeLatestMessage();
}
},
[send, conversationId, derivedMessages, setValue, removeLatestMessage],
);

const handleSendMessage = useCallback(
async (message: Message) => {
if (conversationId !== '') {
sendMessage(message);
} else {
const data = await setConversation('user id');
if (data.code === 0) {
const id = data.data.id;
sendMessage(message, id);
}
}
},
[conversationId, setConversation, sendMessage],
);

const fetchSessionId = useCallback(async () => {
const payload = { question: '' };
const ret = await send({ ...payload, ...data });
if (isCompletionError(ret)) {
message.error(ret?.data.message);
setHasError(true);
}
}, [data, send]);

useEffect(() => {
fetchSessionId();
}, [fetchSessionId, send]);

useEffect(() => {
if (answer.answer) {
addNewestAnswer(answer);
}
}, [answer, addNewestAnswer]);

const handlePressEnter = useCallback(
(documentIds: string[]) => {
if (trim(value) === '') return;
const id = uuid();
if (done) {
setValue('');
addNewestQuestion({
content: value,
doc_ids: documentIds,
id,
role: MessageType.User,
});
handleSendMessage({
content: value.trim(),
id,
role: MessageType.User,
});
}
},
[addNewestQuestion, done, handleSendMessage, setValue, value],
);
const ret = useSendAgentMessage(url);


return { return {
handlePressEnter,
handleInputChange,
value,
sendLoading: !done,
ref,
loading: false,
derivedMessages,
hasError,
stopOutputMessage,
...ret,
hasError: false,
}; };
};
}

+ 0
- 13
web/src/pages/next-chats/share/index.less View File

.chatWrapper {
height: 100vh;
}

.chatContainer {
padding: 10px;
box-sizing: border-box;
height: 100%;
.messageContainer {
overflow-y: auto;
padding-right: 6px;
}
}

+ 109
- 7
web/src/pages/next-chats/share/index.tsx View File

import ChatContainer from './large';
import MessageInput from '@/components/message-input';
import MessageItem from '@/components/next-message-item';
import PdfDrawer from '@/components/pdf-drawer';
import { useClickDrawer } from '@/components/pdf-drawer/hooks';
import { MessageType, SharedFrom } from '@/constants/chat';
import { useFetchNextConversationSSE } from '@/hooks/chat-hooks';
import { useFetchAgentAvatar } from '@/hooks/use-agent-request';
import { cn } from '@/lib/utils';
import i18n from '@/locales/config';
import { useSendButtonDisabled } from '@/pages/chat/hooks';
import { buildMessageUuidWithRole } from '@/utils/chat';
import React, { forwardRef, useMemo } from 'react';
import {
useGetSharedChatSearchParams,
useSendNextSharedMessage,
} from '../hooks/use-send-shared-message';


import styles from './index.less';
const ChatContainer = () => {
const {
sharedId: conversationId,
from,
locale,
visibleAvatar,
} = useGetSharedChatSearchParams();
const { visible, hideModal, documentId, selectedChunk, clickDocumentButton } =
useClickDrawer();

const {
handlePressEnter,
handleInputChange,
value,
sendLoading,
ref,
derivedMessages,
hasError,
stopOutputMessage,
findReferenceByMessageId,
} = useSendNextSharedMessage();
const sendDisabled = useSendButtonDisabled(value);

const useFetchAvatar = useMemo(() => {
return from === SharedFrom.Agent
? useFetchAgentAvatar
: useFetchNextConversationSSE;
}, [from]);

React.useEffect(() => {
if (locale && i18n.language !== locale) {
i18n.changeLanguage(locale);
}
}, [locale, visibleAvatar]);
const { data: avatarData } = useFetchAvatar();

if (!conversationId) {
return <div>empty</div>;
}


const SharedChat = () => {
return ( return (
<div className={styles.chatWrapper}>
<ChatContainer></ChatContainer>
</div>
<section className="h-[100vh]">
<section className={cn('flex flex-1 flex-col p-2.5 h-full')}>
<div className={cn('flex flex-1 flex-col overflow-auto pr-2')}>
<div>
{derivedMessages?.map((message, i) => {
return (
<MessageItem
visibleAvatar={visibleAvatar}
key={buildMessageUuidWithRole(message)}
avatarDialog={avatarData.avatar}
item={message}
nickname="You"
reference={findReferenceByMessageId(message.id)}
loading={
message.role === MessageType.Assistant &&
sendLoading &&
derivedMessages?.length - 1 === i
}
index={i}
clickDocumentButton={clickDocumentButton}
showLikeButton={false}
showLoudspeaker={false}
showLog={false}
></MessageItem>
);
})}
</div>
<div ref={ref} />
</div>

<MessageInput
isShared
value={value}
disabled={hasError}
sendDisabled={sendDisabled}
conversationId={conversationId}
onInputChange={handleInputChange}
onPressEnter={handlePressEnter}
sendLoading={sendLoading}
uploadMethod="external_upload_and_parse"
showUploadIcon={false}
stopOutputMessage={stopOutputMessage}
></MessageInput>
</section>
{visible && (
<PdfDrawer
visible={visible}
hideModal={hideModal}
documentId={documentId}
chunk={selectedChunk}
></PdfDrawer>
)}
</section>
); );
}; };


export default SharedChat;
export default forwardRef(ChatContainer);

+ 0
- 123
web/src/pages/next-chats/share/large.tsx View File

import MessageInput from '@/components/message-input';
import MessageItem from '@/components/message-item';
import PdfDrawer from '@/components/pdf-drawer';
import { useClickDrawer } from '@/components/pdf-drawer/hooks';
import { MessageType, SharedFrom } from '@/constants/chat';
import { useFetchNextConversationSSE } from '@/hooks/chat-hooks';
import { useFetchFlowSSE } from '@/hooks/flow-hooks';
import i18n from '@/locales/config';
import { useSendButtonDisabled } from '@/pages/chat/hooks';
import { buildMessageUuidWithRole } from '@/utils/chat';
import { Flex, Spin } from 'antd';
import React, { forwardRef, useMemo } from 'react';
import {
useGetSharedChatSearchParams,
useSendSharedMessage,
} from '../hooks/use-send-shared-message';
import { buildMessageItemReference } from '../utils';
import styles from './index.less';

const ChatContainer = () => {
const {
sharedId: conversationId,
from,
locale,
visibleAvatar,
} = useGetSharedChatSearchParams();
const { visible, hideModal, documentId, selectedChunk, clickDocumentButton } =
useClickDrawer();

const {
handlePressEnter,
handleInputChange,
value,
sendLoading,
loading,
ref,
derivedMessages,
hasError,
stopOutputMessage,
} = useSendSharedMessage();
const sendDisabled = useSendButtonDisabled(value);

const useFetchAvatar = useMemo(() => {
return from === SharedFrom.Agent
? useFetchFlowSSE
: useFetchNextConversationSSE;
}, [from]);
React.useEffect(() => {
if (locale && i18n.language !== locale) {
i18n.changeLanguage(locale);
}
}, [locale, visibleAvatar]);
const { data: avatarData } = useFetchAvatar();

if (!conversationId) {
return <div>empty</div>;
}

return (
<>
<Flex flex={1} className={styles.chatContainer} vertical>
<Flex flex={1} vertical className={styles.messageContainer}>
<div>
<Spin spinning={loading}>
{derivedMessages?.map((message, i) => {
return (
<MessageItem
visibleAvatar={visibleAvatar}
key={buildMessageUuidWithRole(message)}
avatarDialog={avatarData?.avatar}
item={message}
nickname="You"
reference={buildMessageItemReference(
{
message: derivedMessages,
reference: [],
},
message,
)}
loading={
message.role === MessageType.Assistant &&
sendLoading &&
derivedMessages?.length - 1 === i
}
index={i}
clickDocumentButton={clickDocumentButton}
showLikeButton={false}
showLoudspeaker={false}
></MessageItem>
);
})}
</Spin>
</div>
<div ref={ref} />
</Flex>

<MessageInput
isShared
value={value}
disabled={hasError}
sendDisabled={sendDisabled}
conversationId={conversationId}
onInputChange={handleInputChange}
onPressEnter={handlePressEnter}
sendLoading={sendLoading}
uploadMethod="external_upload_and_parse"
showUploadIcon={false}
stopOutputMessage={stopOutputMessage}
></MessageInput>
</Flex>
{visible && (
<PdfDrawer
visible={visible}
hideModal={hideModal}
documentId={documentId}
chunk={selectedChunk}
></PdfDrawer>
)}
</>
);
};

export default forwardRef(ChatContainer);

+ 2
- 3
web/src/pages/next-chats/utils.ts View File

import { MessageType } from '@/constants/chat';
import { EmptyConversationId, MessageType } from '@/constants/chat';
import { IConversation, IReference } from '@/interfaces/database/chat'; import { IConversation, IReference } from '@/interfaces/database/chat';
import { isEmpty } from 'lodash'; import { isEmpty } from 'lodash';
import { EmptyConversationId } from './constants';
import { IMessage } from './interface';
import { IMessage } from '../chat/interface';


export const isConversationIdExist = (conversationId: string) => { export const isConversationIdExist = (conversationId: string) => {
return conversationId !== EmptyConversationId && conversationId !== ''; return conversationId !== EmptyConversationId && conversationId !== '';

+ 4
- 0
web/src/pages/user-setting/constants.tsx View File

ProfileIcon, ProfileIcon,
TeamIcon, TeamIcon,
} from '@/assets/icon/Icon'; } from '@/assets/icon/Icon';
import { IconFont } from '@/components/icon-font';
import { LLMFactory } from '@/constants/llm'; import { LLMFactory } from '@/constants/llm';
import { UserSettingRouteKey } from '@/constants/setting'; import { UserSettingRouteKey } from '@/constants/setting';
import { MonitorOutlined } from '@ant-design/icons'; import { MonitorOutlined } from '@ant-design/icons';
[UserSettingRouteKey.Team]: <TeamIcon />, [UserSettingRouteKey.Team]: <TeamIcon />,
[UserSettingRouteKey.Logout]: <LogOutIcon />, [UserSettingRouteKey.Logout]: <LogOutIcon />,
[UserSettingRouteKey.Api]: <ApiIcon />, [UserSettingRouteKey.Api]: <ApiIcon />,
[UserSettingRouteKey.MCP]: (
<IconFont name="mcp" className="size-6"></IconFont>
),
}; };


export * from '@/constants/setting'; export * from '@/constants/setting';

+ 14
- 0
web/src/routes.ts View File

Agent = '/agent', Agent = '/agent',
AgentTemplates = '/agent-templates', AgentTemplates = '/agent-templates',
Agents = '/agents', Agents = '/agents',
AgentList = '/agent-list',
Searches = '/next-searches', Searches = '/next-searches',
Search = '/next-search', Search = '/next-search',
Chats = '/next-chats', Chats = '/next-chats',
component: '@/pages/chat/share', component: '@/pages/chat/share',
layout: false, layout: false,
}, },
{
path: '/next-chat/share',
component: '@/pages/next-chats/share',
layout: false,
},
{ {
path: '/', path: '/',
component: '@/layouts', component: '@/layouts',
path: '/user-setting/api', path: '/user-setting/api',
component: '@/pages/user-setting/setting-api', component: '@/pages/user-setting/setting-api',
}, },
{
path: `/user-setting${Routes.Mcp}`,
component: `@/pages${Routes.ProfileMcp}`,
},
], ],
}, },
{ {
path: '/flow', path: '/flow',
component: '@/pages/flow/list', component: '@/pages/flow/list',
}, },
{
path: Routes.AgentList,
component: `@/pages/${Routes.Agents}`,
},
{ {
path: '/flow/:id', path: '/flow/:id',
component: '@/pages/flow', component: '@/pages/flow',

+ 5
- 0
web/src/services/agent-service.ts View File

fetchVersionList, fetchVersionList,
fetchVersion, fetchVersion,
fetchCanvas, fetchCanvas,
fetchAgentAvatar,
} = api; } = api;


const methods = { const methods = {
url: inputForm, url: inputForm,
method: 'get', method: 'get',
}, },
fetchAgentAvatar: {
url: fetchAgentAvatar,
method: 'get',
},
} as const; } as const;


const agentService = registerNextServer<keyof typeof methods>(methods); const agentService = registerNextServer<keyof typeof methods>(methods);

+ 1
- 0
web/src/utils/api.ts View File

fetchVersionList: (id: string) => `${api_host}/canvas/getlistversion/${id}`, fetchVersionList: (id: string) => `${api_host}/canvas/getlistversion/${id}`,
fetchVersion: (id: string) => `${api_host}/canvas/getversion/${id}`, fetchVersion: (id: string) => `${api_host}/canvas/getversion/${id}`,
fetchCanvas: (id: string) => `${api_host}/canvas/get/${id}`, fetchCanvas: (id: string) => `${api_host}/canvas/get/${id}`,
fetchAgentAvatar: (id: string) => `${api_host}/canvas/getsse/${id}`,


// mcp server // mcp server
listMcpServer: `${api_host}/mcp_server/list`, listMcpServer: `${api_host}/mcp_server/list`,

Loading…
Cancel
Save