Feat: Displays the embedded page of the chat module #3221 (#9532)

### What problem does this PR solve?

Feat: Displays the embedded page of the chat module #3221
Feat: Let the agen operator support the selection of tts model #3221

### Type of change


- [x] New Feature (non-breaking change which adds functionality)
This commit is contained in:
balibabu
2025-08-18 18:02:13 +08:00
committed by GitHub
parent fe32952825
commit 9d0fed601d
21 changed files with 710 additions and 308 deletions

View File

@ -0,0 +1,48 @@
import { useFetchAppConf } from '@/hooks/logic-hooks';
import { RefreshCcw } from 'lucide-react';
import { PropsWithChildren } from 'react';
import { RAGFlowAvatar } from './ragflow-avatar';
import { Button } from './ui/button';
type EmbedContainerProps = {
title: string;
avatar?: string;
handleReset?(): void;
} & PropsWithChildren;
export function EmbedContainer({
title,
avatar,
children,
handleReset,
}: EmbedContainerProps) {
const appConf = useFetchAppConf();
return (
<section className="h-[100vh] flex justify-center items-center">
<div className="w-40 flex gap-2 absolute left-3 top-12 items-center">
<img src="/logo.svg" alt="" />
<span className="text-2xl font-bold">{appConf.appName}</span>
</div>
<div className=" w-[80vw] border rounded-lg">
<div className="flex justify-between items-center border-b p-3">
<div className="flex gap-2 items-center">
<RAGFlowAvatar avatar={avatar} name={title} isPerson />
<div className="text-xl text-foreground">{title}</div>
</div>
<Button
variant={'secondary'}
className="text-sm text-foreground cursor-pointer"
onClick={handleReset}
>
<div className="flex gap-1 items-center">
<RefreshCcw size={14} />
<span className="text-lg ">Reset</span>
</div>
</Button>
</div>
{children}
</div>
</section>
);
}

View File

@ -23,6 +23,7 @@ import {
} from '@/constants/common';
import { useTranslate } from '@/hooks/common-hooks';
import { IModalProps } from '@/interfaces/common';
import { Routes } from '@/routes';
import { zodResolver } from '@hookform/resolvers/zod';
import { memo, useCallback, useMemo } from 'react';
import { useForm, useWatch } from 'react-hook-form';
@ -68,7 +69,7 @@ function EmbedDialog({
const generateIframeSrc = useCallback(() => {
const { visibleAvatar, locale } = values;
let src = `${location.origin}/next-chat/share?shared_id=${token}&from=${from}&auth=${beta}`;
let src = `${location.origin}${from === SharedFrom.Agent ? Routes.AgentShare : Routes.ChatShare}?shared_id=${token}&from=${from}&auth=${beta}`;
if (visibleAvatar) {
src += '&visible_avatar=1';
}

View File

@ -0,0 +1,87 @@
import { useSetModalState, useTranslate } from '@/hooks/common-hooks';
import { useFetchManualSystemTokenList } from '@/hooks/user-setting-hooks';
import { useCallback } from 'react';
import message from '../ui/message';
export const useShowTokenEmptyError = () => {
const { t } = useTranslate('chat');
const showTokenEmptyError = useCallback(() => {
message.error(t('tokenError'));
}, [t]);
return { showTokenEmptyError };
};
export const useShowBetaEmptyError = () => {
const { t } = useTranslate('chat');
const showBetaEmptyError = useCallback(() => {
message.error(t('betaError'));
}, [t]);
return { showBetaEmptyError };
};
export const useFetchTokenListBeforeOtherStep = () => {
const { showTokenEmptyError } = useShowTokenEmptyError();
const { showBetaEmptyError } = useShowBetaEmptyError();
const { data: tokenList, fetchSystemTokenList } =
useFetchManualSystemTokenList();
let token = '',
beta = '';
if (Array.isArray(tokenList) && tokenList.length > 0) {
token = tokenList[0].token;
beta = tokenList[0].beta;
}
token =
Array.isArray(tokenList) && tokenList.length > 0 ? tokenList[0].token : '';
const handleOperate = useCallback(async () => {
const ret = await fetchSystemTokenList();
const list = ret;
if (Array.isArray(list) && list.length > 0) {
if (!list[0].beta) {
showBetaEmptyError();
return false;
}
return list[0]?.token;
} else {
showTokenEmptyError();
return false;
}
}, [fetchSystemTokenList, showBetaEmptyError, showTokenEmptyError]);
return {
token,
beta,
handleOperate,
};
};
export const useShowEmbedModal = () => {
const {
visible: embedVisible,
hideModal: hideEmbedModal,
showModal: showEmbedModal,
} = useSetModalState();
const { handleOperate, token, beta } = useFetchTokenListBeforeOtherStep();
const handleShowEmbedModal = useCallback(async () => {
const succeed = await handleOperate();
if (succeed) {
showEmbedModal();
}
}, [handleOperate, showEmbedModal]);
return {
showEmbedModal: handleShowEmbedModal,
hideEmbedModal,
embedVisible,
embedToken: token,
beta,
};
};

View File

@ -16,7 +16,7 @@ import { Funnel } from 'lucide-react';
import { useFormContext, useWatch } from 'react-hook-form';
import { useTranslation } from 'react-i18next';
import { z } from 'zod';
import { NextLLMSelect } from './llm-select/next';
import { NextInnerLLMSelectProps, NextLLMSelect } from './llm-select/next';
import { Button } from './ui/button';
const ModelTypes = [
@ -38,7 +38,10 @@ export const LargeModelFilterFormSchema = {
llm_filter: z.string().optional(),
};
export function LargeModelFormField() {
type LargeModelFormFieldProps = Pick<NextInnerLLMSelectProps, 'showTTSModel'>;
export function LargeModelFormField({
showTTSModel,
}: LargeModelFormFieldProps) {
const form = useFormContext();
const { t } = useTranslation();
const filter = useWatch({ control: form.control, name: 'llm_filter' });
@ -85,7 +88,11 @@ export function LargeModelFormField() {
/>
<FormControl>
<NextLLMSelect {...field} filter={filter} />
<NextLLMSelect
{...field}
filter={filter}
showTTSModel={showTTSModel}
/>
</FormControl>
</section>

View File

@ -1,29 +1,41 @@
import { LlmModelType } from '@/constants/knowledge';
import { useComposeLlmOptionsByModelTypes } from '@/hooks/llm-hooks';
import * as SelectPrimitive from '@radix-ui/react-select';
import { forwardRef, memo, useState } from 'react';
import { forwardRef, memo, useMemo, useState } from 'react';
import { LlmSettingFieldItems } from '../llm-setting-items/next';
import { Popover, PopoverContent, PopoverTrigger } from '../ui/popover';
import { Select, SelectTrigger, SelectValue } from '../ui/select';
interface IProps {
export interface NextInnerLLMSelectProps {
id?: string;
value?: string;
onInitialValue?: (value: string, option: any) => void;
onChange?: (value: string) => void;
disabled?: boolean;
filter?: string;
showTTSModel?: boolean;
}
const NextInnerLLMSelect = forwardRef<
React.ElementRef<typeof SelectPrimitive.Trigger>,
IProps
>(({ value, disabled, filter }, ref) => {
NextInnerLLMSelectProps
>(({ value, disabled, filter, showTTSModel = false }, ref) => {
const [isPopoverOpen, setIsPopoverOpen] = useState(false);
const modelTypes =
filter === 'all' || filter === undefined
? [LlmModelType.Chat, LlmModelType.Image2text]
: [filter as LlmModelType];
const ttsModel = useMemo(() => {
return showTTSModel ? [LlmModelType.TTS] : [];
}, [showTTSModel]);
const modelTypes = useMemo(() => {
if (filter === LlmModelType.Chat) {
return [LlmModelType.Chat];
} else if (filter === LlmModelType.Image2text) {
return [LlmModelType.Image2text, ...ttsModel];
} else {
return [LlmModelType.Chat, LlmModelType.Image2text, ...ttsModel];
}
}, [filter, ttsModel]);
const modelOptions = useComposeLlmOptionsByModelTypes(modelTypes);
return (

View File

@ -14,6 +14,7 @@ import {
} from '@/components/file-upload';
import { Button } from '@/components/ui/button';
import { Textarea } from '@/components/ui/textarea';
import { cn } from '@/lib/utils';
import { CircleStop, Paperclip, Send, Upload, X } from 'lucide-react';
import * as React from 'react';
import { toast } from 'sonner';
@ -135,7 +136,11 @@ export function NextMessageInput({
disabled={isUploading || disabled || sendLoading}
onKeyDown={handleKeyDown}
/>
<div className="flex items-center justify-between gap-1.5">
<div
className={cn('flex items-center justify-between gap-1.5', {
'justify-end': !showUploadIcon,
})}
>
{showUploadIcon && (
<FileUploadTrigger asChild>
<Button

View File

@ -1,6 +1,10 @@
import message from '@/components/ui/message';
import { ChatSearchParams } from '@/constants/chat';
import { IConversation, IDialog } from '@/interfaces/database/chat';
import {
IConversation,
IDialog,
IExternalChatInfo,
} from '@/interfaces/database/chat';
import { IAskRequestBody } from '@/interfaces/request/chat';
import { IClientConversation } from '@/pages/next-chats/chat/interface';
import { useGetSharedChatSearchParams } from '@/pages/next-chats/hooks/use-send-shared-message';
@ -32,6 +36,7 @@ export const enum ChatApiAction {
FetchMindMap = 'fetchMindMap',
FetchRelatedQuestions = 'fetchRelatedQuestions',
UploadAndParse = 'upload_and_parse',
FetchExternalChatInfo = 'fetchExternalChatInfo',
}
export const useGetChatSearchParams = () => {
@ -418,6 +423,29 @@ export function useUploadAndParseFile() {
return { data, loading, uploadAndParseFile: mutateAsync };
}
export const useFetchExternalChatInfo = () => {
const { sharedId: id } = useGetSharedChatSearchParams();
const {
data,
isFetching: loading,
refetch,
} = useQuery<IExternalChatInfo>({
queryKey: [ChatApiAction.FetchExternalChatInfo, id],
gcTime: 0,
initialData: {} as IExternalChatInfo,
enabled: !!id,
refetchOnWindowFocus: false,
queryFn: async () => {
const { data } = await chatService.fetchExternalChatInfo(id!);
return data?.data;
},
});
return { data, loading, refetch };
};
//#endregion
//#region search page

View File

@ -172,3 +172,9 @@ export interface IStats {
round: [string, number][];
thumb_up: [string, number][];
}
export interface IExternalChatInfo {
avatar?: string;
title: string;
prologue?: string;
}

View File

@ -128,7 +128,7 @@ function AgentForm({ node }: INextOperatorForm) {
<FormWrapper>
<FormContainer>
{isSubAgent && <DescriptionField></DescriptionField>}
<LargeModelFormField></LargeModelFormField>
<LargeModelFormField showTTSModel></LargeModelFormField>
{findLlmByUuid(llmId)?.model_type === LlmModelType.Image2text && (
<QueryVariable
name="visual_files_var"

View File

@ -0,0 +1,65 @@
import { SharedFrom } from '@/constants/chat';
import { useSetModalState } from '@/hooks/common-hooks';
import { IEventList } from '@/hooks/use-send-message';
import { useSendAgentMessage } from '@/pages/agent/chat/use-send-agent-message';
import trim from 'lodash/trim';
import { useCallback, useState } from 'react';
import { useSearchParams } from 'umi';
export const useSendButtonDisabled = (value: string) => {
return trim(value) === '';
};
export const useGetSharedChatSearchParams = () => {
const [searchParams] = useSearchParams();
const data_prefix = 'data_';
const data = Object.fromEntries(
searchParams
.entries()
.filter(([key]) => key.startsWith(data_prefix))
.map(([key, value]) => [key.replace(data_prefix, ''), value]),
);
return {
from: searchParams.get('from') as SharedFrom,
sharedId: searchParams.get('shared_id'),
locale: searchParams.get('locale'),
data: data,
visibleAvatar: searchParams.get('visible_avatar')
? searchParams.get('visible_avatar') !== '1'
: true,
};
};
export const useSendNextSharedMessage = (
addEventList: (data: IEventList, messageId: string) => void,
) => {
const { from, sharedId: conversationId } = useGetSharedChatSearchParams();
const url = `/api/v1/${from === SharedFrom.Agent ? 'agentbots' : 'chatbots'}/${conversationId}/completions`;
const [params, setParams] = useState<any[]>([]);
const {
visible: parameterDialogVisible,
hideModal: hideParameterDialog,
showModal: showParameterDialog,
} = useSetModalState();
const ret = useSendAgentMessage(url, addEventList, params);
const ok = useCallback(
(params: any[]) => {
setParams(params);
hideParameterDialog();
},
[hideParameterDialog],
);
return {
...ret,
hasError: false,
parameterDialogVisible,
hideParameterDialog,
showParameterDialog,
ok,
};
};

View File

@ -1,18 +1,13 @@
import { useFetchTokenListBeforeOtherStep } from '@/components/embed-dialog/use-show-embed-dialog';
import { SharedFrom } from '@/constants/chat';
import {
useSetModalState,
useShowDeleteConfirm,
useTranslate,
} from '@/hooks/common-hooks';
import { useShowDeleteConfirm } from '@/hooks/common-hooks';
import {
useCreateSystemToken,
useFetchManualSystemTokenList,
useFetchSystemTokenList,
useRemoveSystemToken,
} from '@/hooks/user-setting-hooks';
import { IStats } from '@/interfaces/database/chat';
import { useQueryClient } from '@tanstack/react-query';
import { message } from 'antd';
import { useCallback } from 'react';
export const useOperateApiKey = (idKey: string, dialogId?: string) => {
@ -62,94 +57,11 @@ export const useSelectChartStatsList = (): ChartStatsType => {
}, {} as ChartStatsType);
};
export const useShowTokenEmptyError = () => {
const { t } = useTranslate('chat');
const showTokenEmptyError = useCallback(() => {
message.error(t('tokenError'));
}, [t]);
return { showTokenEmptyError };
};
export const useShowBetaEmptyError = () => {
const { t } = useTranslate('chat');
const showBetaEmptyError = useCallback(() => {
message.error(t('betaError'));
}, [t]);
return { showBetaEmptyError };
};
const getUrlWithToken = (token: string, from: string = 'chat') => {
const { protocol, host } = window.location;
return `${protocol}//${host}/chat/share?shared_id=${token}&from=${from}`;
};
export const useFetchTokenListBeforeOtherStep = () => {
const { showTokenEmptyError } = useShowTokenEmptyError();
const { showBetaEmptyError } = useShowBetaEmptyError();
const { data: tokenList, fetchSystemTokenList } =
useFetchManualSystemTokenList();
let token = '',
beta = '';
if (Array.isArray(tokenList) && tokenList.length > 0) {
token = tokenList[0].token;
beta = tokenList[0].beta;
}
token =
Array.isArray(tokenList) && tokenList.length > 0 ? tokenList[0].token : '';
const handleOperate = useCallback(async () => {
const ret = await fetchSystemTokenList();
const list = ret;
if (Array.isArray(list) && list.length > 0) {
if (!list[0].beta) {
showBetaEmptyError();
return false;
}
return list[0]?.token;
} else {
showTokenEmptyError();
return false;
}
}, [fetchSystemTokenList, showBetaEmptyError, showTokenEmptyError]);
return {
token,
beta,
handleOperate,
};
};
export const useShowEmbedModal = () => {
const {
visible: embedVisible,
hideModal: hideEmbedModal,
showModal: showEmbedModal,
} = useSetModalState();
const { handleOperate, token, beta } = useFetchTokenListBeforeOtherStep();
const handleShowEmbedModal = useCallback(async () => {
const succeed = await handleOperate();
if (succeed) {
showEmbedModal();
}
}, [handleOperate, showEmbedModal]);
return {
showEmbedModal: handleShowEmbedModal,
hideEmbedModal,
embedVisible,
embedToken: token,
beta,
};
};
export const usePreviewChat = (idKey: string) => {
const { handleOperate } = useFetchTokenListBeforeOtherStep();

View File

@ -1,3 +1,5 @@
import EmbedDialog from '@/components/embed-dialog';
import { useShowEmbedModal } from '@/components/embed-dialog/use-show-embed-dialog';
import { PageHeader } from '@/components/page-header';
import {
Breadcrumb,
@ -35,7 +37,6 @@ import { useTranslation } from 'react-i18next';
import { useParams } from 'umi';
import AgentCanvas from './canvas';
import { DropdownProvider } from './canvas/context';
import EmbedDialog from './embed-dialog';
import { useHandleExportOrImportJsonFile } from './hooks/use-export-json';
import { useFetchDataOnMount } from './hooks/use-fetch-data';
import { useGetBeginNodeDataInputs } from './hooks/use-get-begin-query';
@ -44,7 +45,6 @@ import {
useSaveGraphBeforeOpeningDebugDrawer,
useWatchAgentChange,
} from './hooks/use-save-graph';
import { useShowEmbedModal } from './hooks/use-show-dialog';
import { SettingDialog } from './setting-dialog';
import { UploadAgentDialog } from './upload-agent-dialog';
import { useAgentHistoryManager } from './use-agent-history-manager';

View File

@ -0,0 +1,233 @@
import { EmbedContainer } from '@/components/embed-container';
import { FileUploadProps } from '@/components/file-upload';
import { NextMessageInput } from '@/components/message-input/next';
import MessageItem from '@/components/next-message-item';
import PdfDrawer from '@/components/pdf-drawer';
import { useClickDrawer } from '@/components/pdf-drawer/hooks';
import { MessageType } from '@/constants/chat';
import {
useFetchExternalAgentInputs,
useUploadCanvasFileWithProgress,
} from '@/hooks/use-agent-request';
import { cn } from '@/lib/utils';
import i18n from '@/locales/config';
import DebugContent from '@/pages/agent/debug-content';
import { useCacheChatLog } from '@/pages/agent/hooks/use-cache-chat-log';
import { useAwaitCompentData } from '@/pages/agent/hooks/use-chat-logic';
import { IInputs } from '@/pages/agent/interface';
import { useSendButtonDisabled } from '@/pages/chat/hooks';
import { buildMessageUuidWithRole } from '@/utils/chat';
import { isEmpty } from 'lodash';
import React, { forwardRef, useCallback, useState } from 'react';
import {
useGetSharedChatSearchParams,
useSendNextSharedMessage,
} from '../hooks/use-send-shared-message';
import { ParameterDialog } from './parameter-dialog';
const ChatContainer = () => {
const {
sharedId: conversationId,
locale,
visibleAvatar,
} = useGetSharedChatSearchParams();
const { visible, hideModal, documentId, selectedChunk, clickDocumentButton } =
useClickDrawer();
const { uploadCanvasFile, loading } =
useUploadCanvasFileWithProgress(conversationId);
const {
addEventList,
setCurrentMessageId,
currentEventListWithoutMessageById,
clearEventList,
} = useCacheChatLog();
const {
handlePressEnter,
handleInputChange,
value,
sendLoading,
scrollRef,
messageContainerRef,
derivedMessages,
hasError,
stopOutputMessage,
findReferenceByMessageId,
appendUploadResponseList,
parameterDialogVisible,
showParameterDialog,
sendFormMessage,
addNewestOneAnswer,
ok,
resetSession,
} = useSendNextSharedMessage(addEventList);
const { buildInputList, handleOk, isWaitting } = useAwaitCompentData({
derivedMessages,
sendFormMessage,
canvasId: conversationId as string,
});
const sendDisabled = useSendButtonDisabled(value);
const { data: inputsData } = useFetchExternalAgentInputs();
const [agentInfo, setAgentInfo] = useState<IInputs>({
avatar: '',
title: '',
inputs: {},
prologue: '',
});
const handleUploadFile: NonNullable<FileUploadProps['onUpload']> =
useCallback(
async (files, options) => {
const ret = await uploadCanvasFile({ files, options });
appendUploadResponseList(ret.data, files);
},
[appendUploadResponseList, uploadCanvasFile],
);
React.useEffect(() => {
if (locale && i18n.language !== locale) {
i18n.changeLanguage(locale);
}
}, [locale, visibleAvatar]);
React.useEffect(() => {
const { avatar, title, inputs } = inputsData;
setAgentInfo({
avatar,
title,
inputs: inputs,
prologue: '',
});
}, [inputsData, setAgentInfo]);
React.useEffect(() => {
if (inputsData.prologue) {
addNewestOneAnswer({
answer: inputsData.prologue,
});
}
}, [inputsData.prologue, addNewestOneAnswer]);
React.useEffect(() => {
if (inputsData && inputsData.inputs && !isEmpty(inputsData.inputs)) {
showParameterDialog();
}
}, [inputsData, showParameterDialog]);
const handleInputsModalOk = (params: any[]) => {
ok(params);
};
const handleReset = () => {
resetSession();
clearEventList();
};
if (!conversationId) {
return <div>empty</div>;
}
return (
<>
<EmbedContainer
title={agentInfo.title}
avatar={agentInfo.avatar}
handleReset={handleReset}
>
<div className="flex flex-1 flex-col p-2.5 h-[90vh] m-3">
<div
className={cn(
'flex flex-1 flex-col overflow-auto scrollbar-auto m-auto w-5/6',
)}
ref={messageContainerRef}
>
<div>
{derivedMessages?.map((message, i) => {
return (
<MessageItem
visibleAvatar={visibleAvatar}
conversationId={conversationId}
currentEventListWithoutMessageById={
currentEventListWithoutMessageById
}
setCurrentMessageId={setCurrentMessageId}
key={buildMessageUuidWithRole(message)}
item={message}
nickname="You"
reference={findReferenceByMessageId(message.id)}
loading={
message.role === MessageType.Assistant &&
sendLoading &&
derivedMessages?.length - 1 === i
}
isShare={true}
avatarDialog={agentInfo.avatar}
agentName={agentInfo.title}
index={i}
clickDocumentButton={clickDocumentButton}
showLikeButton={false}
showLoudspeaker={false}
showLog={false}
sendLoading={sendLoading}
>
{message.role === MessageType.Assistant &&
derivedMessages.length - 1 === i && (
<DebugContent
parameters={buildInputList(message)}
message={message}
ok={handleOk(message)}
isNext={false}
btnText={'Submit'}
></DebugContent>
)}
{message.role === MessageType.Assistant &&
derivedMessages.length - 1 !== i && (
<div>
<div>{message?.data?.tips}</div>
<div>
{buildInputList(message)?.map((item) => item.value)}
</div>
</div>
)}
</MessageItem>
);
})}
</div>
<div ref={scrollRef} />
</div>
<div className="flex w-full justify-center mb-8">
<div className="w-5/6">
<NextMessageInput
isShared
value={value}
disabled={hasError || isWaitting}
sendDisabled={sendDisabled || isWaitting}
conversationId={conversationId}
onInputChange={handleInputChange}
onPressEnter={handlePressEnter}
sendLoading={sendLoading}
stopOutputMessage={stopOutputMessage}
onUpload={handleUploadFile}
isUploading={loading || isWaitting}
></NextMessageInput>
</div>
</div>
</div>
</EmbedContainer>
{visible && (
<PdfDrawer
visible={visible}
hideModal={hideModal}
documentId={documentId}
chunk={selectedChunk}
></PdfDrawer>
)}
{parameterDialogVisible && (
<ParameterDialog
// hideModal={hideParameterDialog}
ok={handleInputsModalOk}
data={agentInfo.inputs}
></ParameterDialog>
)}
</>
);
};
export default forwardRef(ChatContainer);

View File

@ -1,3 +1,5 @@
import EmbedDialog from '@/components/embed-dialog';
import { useShowEmbedModal } from '@/components/embed-dialog/use-show-embed-dialog';
import { PageHeader } from '@/components/page-header';
import {
Breadcrumb,
@ -9,6 +11,7 @@ import {
} from '@/components/ui/breadcrumb';
import { Button } from '@/components/ui/button';
import { Card, CardContent, CardHeader, CardTitle } from '@/components/ui/card';
import { SharedFrom } from '@/constants/chat';
import { useSetModalState } from '@/hooks/common-hooks';
import { useNavigatePage } from '@/hooks/logic-hooks/navigate-hooks';
import {
@ -20,6 +23,7 @@ import { cn } from '@/lib/utils';
import { isEmpty } from 'lodash';
import { ArrowUpRight, LogOut, Send } from 'lucide-react';
import { useTranslation } from 'react-i18next';
import { useParams } from 'umi';
import { useHandleClickConversationCard } from '../hooks/use-click-card';
import { ChatSettings } from './app-settings/chat-settings';
import { MultipleChatBox } from './chat-box/multiple-chat-box';
@ -29,6 +33,7 @@ import { useAddChatBox } from './use-add-box';
import { useSwitchDebugMode } from './use-switch-debug-mode';
export default function Chat() {
const { id } = useParams();
const { navigateToChatList } = useNavigatePage();
const { data } = useFetchDialog();
const { t } = useTranslation();
@ -46,6 +51,9 @@ export default function Chat() {
hasThreeChatBox,
} = useAddChatBox();
const { showEmbedModal, hideEmbedModal, embedVisible, beta } =
useShowEmbedModal();
const { conversationId, isNew } = useGetChatSearchParams();
const { isDebugMode, switchDebugMode } = useSwitchDebugMode();
@ -87,7 +95,7 @@ export default function Chat() {
</BreadcrumbItem>
</BreadcrumbList>
</Breadcrumb>
<Button>
<Button onClick={showEmbedModal}>
<Send />
{t('common.embedIntoSite')}
</Button>
@ -133,6 +141,16 @@ export default function Chat() {
</CardContent>
</Card>
</div>
{embedVisible && (
<EmbedDialog
visible={embedVisible}
hideModal={hideEmbedModal}
token={id!}
from={SharedFrom.Chat}
beta={beta}
isAgent={false}
></EmbedDialog>
)}
</section>
);
}

View File

@ -1,10 +1,20 @@
import { SharedFrom } from '@/constants/chat';
import { useSetModalState } from '@/hooks/common-hooks';
import { IEventList } from '@/hooks/use-send-message';
import { useSendAgentMessage } from '@/pages/agent/chat/use-send-agent-message';
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 trim from 'lodash/trim';
import { useCallback, useState } from 'react';
import { useCallback, useEffect, useState } from 'react';
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) => {
return trim(value) === '';
@ -30,36 +40,114 @@ export const useGetSharedChatSearchParams = () => {
};
};
export const useSendNextSharedMessage = (
addEventList: (data: IEventList, messageId: string) => void,
) => {
const { from, sharedId: conversationId } = useGetSharedChatSearchParams();
const url = `/api/v1/${from === SharedFrom.Agent ? 'agentbots' : 'chatbots'}/${conversationId}/completions`;
const [params, setParams] = useState<any[]>([]);
export const useSendSharedMessage = () => {
const {
visible: parameterDialogVisible,
hideModal: hideParameterDialog,
showModal: showParameterDialog,
} = useSetModalState();
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,
removeLatestMessage,
addNewestAnswer,
addNewestQuestion,
scrollRef,
messageContainerRef,
removeAllMessages,
} = useSelectDerivedMessages();
const [hasError, setHasError] = useState(false);
const ret = useSendAgentMessage(url, addEventList, params);
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'),
});
const ok = useCallback(
(params: any[]) => {
setParams(params);
hideParameterDialog();
if (isCompletionError(res)) {
// cancel loading
setValue(message.content);
removeLatestMessage();
}
},
[hideParameterDialog],
[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);
}
}, [send]);
useEffect(() => {
fetchSessionId();
}, [fetchSessionId]);
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],
);
return {
...ret,
hasError: false,
parameterDialogVisible,
hideParameterDialog,
showParameterDialog,
ok,
handlePressEnter,
handleInputChange,
value,
sendLoading: !done,
loading: false,
derivedMessages,
hasError,
stopOutputMessage,
scrollRef,
messageContainerRef,
removeAllMessages,
};
};

View File

@ -1,166 +1,75 @@
import { FileUploadProps } from '@/components/file-upload';
import { EmbedContainer } from '@/components/embed-container';
import { NextMessageInput } from '@/components/message-input/next';
import MessageItem from '@/components/next-message-item';
import MessageItem from '@/components/message-item';
import PdfDrawer from '@/components/pdf-drawer';
import { useClickDrawer } from '@/components/pdf-drawer/hooks';
import { RAGFlowAvatar } from '@/components/ragflow-avatar';
import { Button } from '@/components/ui/button';
import { MessageType } from '@/constants/chat';
import { useFetchAppConf } from '@/hooks/logic-hooks';
import {
useFetchExternalAgentInputs,
useUploadCanvasFileWithProgress,
} from '@/hooks/use-agent-request';
import { cn } from '@/lib/utils';
import { MessageType, SharedFrom } from '@/constants/chat';
import { useFetchNextConversationSSE } from '@/hooks/chat-hooks';
import { useFetchFlowSSE } from '@/hooks/flow-hooks';
import { useFetchExternalChatInfo } from '@/hooks/use-chat-request';
import i18n from '@/locales/config';
import DebugContent from '@/pages/agent/debug-content';
import { useCacheChatLog } from '@/pages/agent/hooks/use-cache-chat-log';
import { useAwaitCompentData } from '@/pages/agent/hooks/use-chat-logic';
import { IInputs } from '@/pages/agent/interface';
import { useSendButtonDisabled } from '@/pages/chat/hooks';
import { buildMessageUuidWithRole } from '@/utils/chat';
import { isEmpty } from 'lodash';
import { RefreshCcw } from 'lucide-react';
import React, { forwardRef, useCallback, useState } from 'react';
import React, { forwardRef, useMemo } from 'react';
import {
useGetSharedChatSearchParams,
useSendNextSharedMessage,
useSendSharedMessage,
} from '../hooks/use-send-shared-message';
import { ParameterDialog } from './parameter-dialog';
import { buildMessageItemReference } from '../utils';
const ChatContainer = () => {
const {
sharedId: conversationId,
from,
locale,
visibleAvatar,
} = useGetSharedChatSearchParams();
const { visible, hideModal, documentId, selectedChunk, clickDocumentButton } =
useClickDrawer();
const { uploadCanvasFile, loading } =
useUploadCanvasFileWithProgress(conversationId);
const {
addEventList,
setCurrentMessageId,
currentEventListWithoutMessageById,
clearEventList,
} = useCacheChatLog();
const {
handlePressEnter,
handleInputChange,
value,
sendLoading,
scrollRef,
messageContainerRef,
derivedMessages,
hasError,
stopOutputMessage,
findReferenceByMessageId,
appendUploadResponseList,
parameterDialogVisible,
showParameterDialog,
sendFormMessage,
addNewestOneAnswer,
ok,
resetSession,
} = useSendNextSharedMessage(addEventList);
const { buildInputList, handleOk, isWaitting } = useAwaitCompentData({
derivedMessages,
sendFormMessage,
canvasId: conversationId as string,
});
scrollRef,
messageContainerRef,
removeAllMessages,
} = useSendSharedMessage();
const sendDisabled = useSendButtonDisabled(value);
const appConf = useFetchAppConf();
const { data: inputsData } = useFetchExternalAgentInputs();
const [agentInfo, setAgentInfo] = useState<IInputs>({
avatar: '',
title: '',
inputs: {},
prologue: '',
});
const handleUploadFile: NonNullable<FileUploadProps['onUpload']> =
useCallback(
async (files, options) => {
const ret = await uploadCanvasFile({ files, options });
appendUploadResponseList(ret.data, files);
},
[appendUploadResponseList, uploadCanvasFile],
);
const { data: chatInfo } = useFetchExternalChatInfo();
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();
React.useEffect(() => {
const { avatar, title, inputs } = inputsData;
setAgentInfo({
avatar,
title,
inputs: inputs,
prologue: '',
});
}, [inputsData, setAgentInfo]);
React.useEffect(() => {
if (inputsData.prologue) {
addNewestOneAnswer({
answer: inputsData.prologue,
});
}
}, [inputsData.prologue, addNewestOneAnswer]);
React.useEffect(() => {
if (inputsData && inputsData.inputs && !isEmpty(inputsData.inputs)) {
showParameterDialog();
}
}, [inputsData, showParameterDialog]);
const handleInputsModalOk = (params: any[]) => {
ok(params);
};
const handleReset = () => {
resetSession();
clearEventList();
};
if (!conversationId) {
return <div>empty</div>;
}
return (
<section className="h-[100vh] flex justify-center items-center">
<div className="w-40 flex gap-2 absolute left-3 top-12 items-center">
<img src="/logo.svg" alt="" />
<span className="text-2xl font-bold">{appConf.appName}</span>
</div>
<div className=" w-[80vw] border rounded-lg">
<div className="flex justify-between items-center border-b p-3">
<div className="flex gap-2 items-center">
<RAGFlowAvatar
avatar={agentInfo.avatar}
name={agentInfo.title}
isPerson
/>
<div className="text-xl text-foreground">{agentInfo.title}</div>
</div>
<Button
variant={'secondary'}
className="text-sm text-foreground cursor-pointer"
onClick={() => {
handleReset();
}}
>
<div className="flex gap-1 items-center">
<RefreshCcw size={14} />
<span className="text-lg ">Reset</span>
</div>
</Button>
</div>
<>
<EmbedContainer
title={chatInfo.title}
avatar={chatInfo.avatar}
handleReset={removeAllMessages}
>
<div className="flex flex-1 flex-col p-2.5 h-[90vh] m-3">
<div
className={cn(
'flex flex-1 flex-col overflow-auto scrollbar-auto m-auto w-5/6',
)}
className={
'flex flex-1 flex-col overflow-auto scrollbar-auto m-auto w-5/6'
}
ref={messageContainerRef}
>
<div>
@ -168,51 +77,27 @@ const ChatContainer = () => {
return (
<MessageItem
visibleAvatar={visibleAvatar}
conversationId={conversationId}
currentEventListWithoutMessageById={
currentEventListWithoutMessageById
}
setCurrentMessageId={setCurrentMessageId}
key={buildMessageUuidWithRole(message)}
avatarDialog={avatarData?.avatar}
item={message}
nickname="You"
reference={findReferenceByMessageId(message.id)}
reference={buildMessageItemReference(
{
message: derivedMessages,
reference: [],
},
message,
)}
loading={
message.role === MessageType.Assistant &&
sendLoading &&
derivedMessages?.length - 1 === i
}
isShare={true}
avatarDialog={agentInfo.avatar}
agentName={agentInfo.title}
index={i}
clickDocumentButton={clickDocumentButton}
showLikeButton={false}
showLoudspeaker={false}
showLog={false}
sendLoading={sendLoading}
>
{message.role === MessageType.Assistant &&
derivedMessages.length - 1 === i && (
<DebugContent
parameters={buildInputList(message)}
message={message}
ok={handleOk(message)}
isNext={false}
btnText={'Submit'}
></DebugContent>
)}
{message.role === MessageType.Assistant &&
derivedMessages.length - 1 !== i && (
<div>
<div>{message?.data?.tips}</div>
<div>
{buildInputList(message)?.map((item) => item.value)}
</div>
</div>
)}
</MessageItem>
></MessageItem>
);
})}
</div>
@ -223,20 +108,20 @@ const ChatContainer = () => {
<NextMessageInput
isShared
value={value}
disabled={hasError || isWaitting}
sendDisabled={sendDisabled || isWaitting}
disabled={hasError}
sendDisabled={sendDisabled}
conversationId={conversationId}
onInputChange={handleInputChange}
onPressEnter={handlePressEnter}
sendLoading={sendLoading}
uploadMethod="external_upload_and_parse"
showUploadIcon={false}
stopOutputMessage={stopOutputMessage}
onUpload={handleUploadFile}
isUploading={loading || isWaitting}
></NextMessageInput>
</div>
</div>
</div>
</div>
</EmbedContainer>
{visible && (
<PdfDrawer
visible={visible}
@ -245,14 +130,7 @@ const ChatContainer = () => {
chunk={selectedChunk}
></PdfDrawer>
)}
{parameterDialogVisible && (
<ParameterDialog
// hideModal={hideParameterDialog}
ok={handleInputsModalOk}
data={agentInfo.inputs}
></ParameterDialog>
)}
</section>
</>
);
};

View File

@ -1,3 +1,4 @@
import { useFetchTokenListBeforeOtherStep } from '@/components/embed-dialog/use-show-embed-dialog';
import HightLightMarkdown from '@/components/highlight-markdown';
import { Modal } from '@/components/ui/modal/modal';
import { RAGFlowSelect } from '@/components/ui/select';
@ -9,7 +10,6 @@ import {
import { useTranslate } from '@/hooks/common-hooks';
import { message } from 'antd';
import { useCallback, useEffect, useMemo, useState } from 'react';
import { useFetchTokenListBeforeOtherStep } from '../agent/hooks/use-show-dialog';
type IEmbedAppModalProps = {
open: any;

View File

@ -38,6 +38,8 @@ export enum Routes {
ResultView = `${Chunk}${Result}`,
KnowledgeGraph = '/knowledge-graph',
AgentLogPage = '/agent-log-page',
AgentShare = '/agent/share',
ChatShare = `${Chats}/share`,
}
const routes = [
@ -57,8 +59,13 @@ const routes = [
layout: false,
},
{
path: '/next-chat/share',
component: '@/pages/next-chats/share',
path: Routes.ChatShare,
component: `@/pages${Routes.ChatShare}`,
layout: false,
},
{
path: Routes.AgentShare,
component: `@/pages${Routes.AgentShare}`,
layout: false,
},
{

View File

@ -28,6 +28,7 @@ const {
getRelatedQuestions,
listNextDialog,
upload_and_parse,
fetchExternalChatInfo,
} = api;
const methods = {
@ -131,6 +132,10 @@ const methods = {
method: 'post',
url: upload_and_parse,
},
fetchExternalChatInfo: {
url: fetchExternalChatInfo,
method: 'get',
},
} as const;
const chatService = registerNextServer<keyof typeof methods>(methods);

View File

@ -111,6 +111,8 @@ export default {
// next chat
listNextDialog: `${api_host}/dialog/next`,
fetchExternalChatInfo: (id: string) =>
`${ExternalApi}${api_host}/chatbots/${id}/info`,
// file manager
listFile: `${api_host}/file/list`,