mirror of
https://github.com/labring/FastGPT.git
synced 2025-08-02 20:58:12 +00:00
V4.7-alpha (#985)
Co-authored-by: heheer <71265218+newfish-cmyk@users.noreply.github.com>
This commit is contained in:
@@ -1,13 +1,13 @@
|
||||
import React from 'react';
|
||||
import { ModalBody, Box, useTheme, Flex, Image } from '@chakra-ui/react';
|
||||
import { ChatItemType } from '@fastgpt/global/core/chat/type';
|
||||
import { ModalBody, Box, useTheme } from '@chakra-ui/react';
|
||||
import MyModal from '../MyModal';
|
||||
import { DispatchNodeResponseType } from '@fastgpt/global/core/module/runtime/type.d';
|
||||
|
||||
const ContextModal = ({
|
||||
context = [],
|
||||
onClose
|
||||
}: {
|
||||
context: ChatItemType[];
|
||||
context: DispatchNodeResponseType['historyPreview'];
|
||||
onClose: () => void;
|
||||
}) => {
|
||||
const theme = useTheme();
|
||||
@@ -17,7 +17,7 @@ const ContextModal = ({
|
||||
isOpen={true}
|
||||
onClose={onClose}
|
||||
iconSrc="/imgs/modal/chatHistory.svg"
|
||||
title={`完整对话记录(${context.length}条)`}
|
||||
title={`上下文预览(${context.length}条)`}
|
||||
h={['90vh', '80vh']}
|
||||
minW={['90vw', '600px']}
|
||||
isCentered
|
||||
|
@@ -1,36 +1,24 @@
|
||||
import { useSpeech } from '@/web/common/hooks/useSpeech';
|
||||
import { useSystemStore } from '@/web/common/system/useSystemStore';
|
||||
import { Box, Flex, Image, Spinner, Textarea } from '@chakra-ui/react';
|
||||
import React, { useRef, useEffect, useCallback, useState, useTransition } from 'react';
|
||||
import React, { useRef, useEffect, useCallback, useMemo } from 'react';
|
||||
import { useTranslation } from 'next-i18next';
|
||||
import MyTooltip from '../MyTooltip';
|
||||
import MyIcon from '@fastgpt/web/components/common/Icon';
|
||||
import { useRouter } from 'next/router';
|
||||
import { useSelectFile } from '@/web/common/file/hooks/useSelectFile';
|
||||
import { compressImgFileAndUpload } from '@/web/common/file/controller';
|
||||
import { customAlphabet } from 'nanoid';
|
||||
import { IMG_BLOCK_KEY } from '@fastgpt/global/core/chat/constants';
|
||||
import { ChatFileTypeEnum } from '@fastgpt/global/core/chat/constants';
|
||||
import { addDays } from 'date-fns';
|
||||
import { useRequest } from '@/web/common/hooks/useRequest';
|
||||
import { MongoImageTypeEnum } from '@fastgpt/global/common/file/image/constants';
|
||||
import { OutLinkChatAuthProps } from '@fastgpt/global/support/permission/chat';
|
||||
import { ChatBoxInputFormType, ChatBoxInputType, UserInputFileItemType } from './type';
|
||||
import { textareaMinH } from './constants';
|
||||
import { UseFormReturn, useFieldArray } from 'react-hook-form';
|
||||
const nanoid = customAlphabet('abcdefghijklmnopqrstuvwxyz1234567890', 6);
|
||||
|
||||
enum FileTypeEnum {
|
||||
image = 'image',
|
||||
file = 'file'
|
||||
}
|
||||
type FileItemType = {
|
||||
id: string;
|
||||
rawFile: File;
|
||||
type: `${FileTypeEnum}`;
|
||||
name: string;
|
||||
icon: string; // img is base64
|
||||
src?: string;
|
||||
};
|
||||
|
||||
const MessageInput = ({
|
||||
onChange,
|
||||
onSendMessage,
|
||||
onStop,
|
||||
isChatting,
|
||||
@@ -40,17 +28,29 @@ const MessageInput = ({
|
||||
shareId,
|
||||
outLinkUid,
|
||||
teamId,
|
||||
teamToken
|
||||
teamToken,
|
||||
chatForm
|
||||
}: OutLinkChatAuthProps & {
|
||||
onChange?: (e: string) => void;
|
||||
onSendMessage: (e: string) => void;
|
||||
onSendMessage: (val: ChatBoxInputType) => void;
|
||||
onStop: () => void;
|
||||
isChatting: boolean;
|
||||
showFileSelector?: boolean;
|
||||
TextareaDom: React.MutableRefObject<HTMLTextAreaElement | null>;
|
||||
resetInputVal: (val: string) => void;
|
||||
resetInputVal: (val: ChatBoxInputType) => void;
|
||||
chatForm: UseFormReturn<ChatBoxInputFormType>;
|
||||
}) => {
|
||||
const [, startSts] = useTransition();
|
||||
const { setValue, watch, control } = chatForm;
|
||||
const inputValue = watch('input');
|
||||
const {
|
||||
update: updateFile,
|
||||
remove: removeFile,
|
||||
fields: fileList,
|
||||
append: appendFile,
|
||||
replace: replaceFile
|
||||
} = useFieldArray({
|
||||
control,
|
||||
name: 'files'
|
||||
});
|
||||
|
||||
const {
|
||||
isSpeaking,
|
||||
@@ -64,45 +64,38 @@ const MessageInput = ({
|
||||
const { isPc } = useSystemStore();
|
||||
const canvasRef = useRef<HTMLCanvasElement>(null);
|
||||
const { t } = useTranslation();
|
||||
const textareaMinH = '22px';
|
||||
const [fileList, setFileList] = useState<FileItemType[]>([]);
|
||||
const havInput = !!TextareaDom.current?.value || fileList.length > 0;
|
||||
|
||||
const havInput = !!inputValue || fileList.length > 0;
|
||||
|
||||
/* file selector and upload */
|
||||
const { File, onOpen: onOpenSelectFile } = useSelectFile({
|
||||
fileType: 'image/*',
|
||||
multiple: true,
|
||||
maxCount: 10
|
||||
});
|
||||
|
||||
const { mutate: uploadFile } = useRequest({
|
||||
mutationFn: async (file: FileItemType) => {
|
||||
if (file.type === FileTypeEnum.image) {
|
||||
mutationFn: async ({ file, fileIndex }: { file: UserInputFileItemType; fileIndex: number }) => {
|
||||
if (file.type === ChatFileTypeEnum.image && file.rawFile) {
|
||||
try {
|
||||
const src = await compressImgFileAndUpload({
|
||||
const url = await compressImgFileAndUpload({
|
||||
type: MongoImageTypeEnum.chatImage,
|
||||
file: file.rawFile,
|
||||
maxW: 4329,
|
||||
maxH: 4329,
|
||||
maxSize: 1024 * 1024 * 5,
|
||||
// 30 day expired.
|
||||
// 7 day expired.
|
||||
expiredTime: addDays(new Date(), 7),
|
||||
shareId,
|
||||
outLinkUid,
|
||||
teamId,
|
||||
teamToken
|
||||
});
|
||||
setFileList((state) =>
|
||||
state.map((item) =>
|
||||
item.id === file.id
|
||||
? {
|
||||
...item,
|
||||
src: `${location.origin}${src}`
|
||||
}
|
||||
: item
|
||||
)
|
||||
);
|
||||
updateFile(fileIndex, {
|
||||
...file,
|
||||
url: `${location.origin}${url}`
|
||||
});
|
||||
} catch (error) {
|
||||
setFileList((state) => state.filter((item) => item.id !== file.id));
|
||||
removeFile(fileIndex);
|
||||
console.log(error);
|
||||
return Promise.reject(error);
|
||||
}
|
||||
@@ -110,7 +103,6 @@ const MessageInput = ({
|
||||
},
|
||||
errorToast: t('common.Upload File Failed')
|
||||
});
|
||||
|
||||
const onSelectFile = useCallback(
|
||||
async (files: File[]) => {
|
||||
if (!files || files.length === 0) {
|
||||
@@ -119,7 +111,7 @@ const MessageInput = ({
|
||||
const loadFiles = await Promise.all(
|
||||
files.map(
|
||||
(file) =>
|
||||
new Promise<FileItemType>((resolve, reject) => {
|
||||
new Promise<UserInputFileItemType>((resolve, reject) => {
|
||||
if (file.type.includes('image')) {
|
||||
const reader = new FileReader();
|
||||
reader.readAsDataURL(file);
|
||||
@@ -127,11 +119,10 @@ const MessageInput = ({
|
||||
const item = {
|
||||
id: nanoid(),
|
||||
rawFile: file,
|
||||
type: FileTypeEnum.image,
|
||||
type: ChatFileTypeEnum.image,
|
||||
name: file.name,
|
||||
icon: reader.result as string
|
||||
};
|
||||
uploadFile(item);
|
||||
resolve(item);
|
||||
};
|
||||
reader.onerror = () => {
|
||||
@@ -141,7 +132,7 @@ const MessageInput = ({
|
||||
resolve({
|
||||
id: nanoid(),
|
||||
rawFile: file,
|
||||
type: FileTypeEnum.file,
|
||||
type: ChatFileTypeEnum.file,
|
||||
name: file.name,
|
||||
icon: 'file/pdf'
|
||||
});
|
||||
@@ -149,29 +140,28 @@ const MessageInput = ({
|
||||
})
|
||||
)
|
||||
);
|
||||
appendFile(loadFiles);
|
||||
|
||||
setFileList((state) => [...state, ...loadFiles]);
|
||||
loadFiles.forEach((file, i) =>
|
||||
uploadFile({
|
||||
file,
|
||||
fileIndex: i + fileList.length
|
||||
})
|
||||
);
|
||||
},
|
||||
[uploadFile]
|
||||
[appendFile, fileList.length, uploadFile]
|
||||
);
|
||||
|
||||
/* on send */
|
||||
const handleSend = useCallback(async () => {
|
||||
const textareaValue = TextareaDom.current?.value || '';
|
||||
|
||||
const images = fileList.filter((item) => item.type === FileTypeEnum.image);
|
||||
const imagesText =
|
||||
images.length === 0
|
||||
? ''
|
||||
: `\`\`\`${IMG_BLOCK_KEY}
|
||||
${images.map((img) => JSON.stringify({ src: img.src })).join('\n')}
|
||||
\`\`\`
|
||||
`;
|
||||
|
||||
const inputMessage = `${imagesText}${textareaValue}`;
|
||||
|
||||
onSendMessage(inputMessage);
|
||||
setFileList([]);
|
||||
}, [TextareaDom, fileList, onSendMessage]);
|
||||
onSendMessage({
|
||||
text: textareaValue.trim(),
|
||||
files: fileList
|
||||
});
|
||||
replaceFile([]);
|
||||
}, [TextareaDom, fileList, onSendMessage, replaceFile]);
|
||||
|
||||
useEffect(() => {
|
||||
if (!stream) {
|
||||
@@ -231,7 +221,7 @@ ${images.map((img) => JSON.stringify({ src: img.src })).join('\n')}
|
||||
|
||||
{/* file preview */}
|
||||
<Flex wrap={'wrap'} px={[2, 4]} userSelect={'none'}>
|
||||
{fileList.map((item) => (
|
||||
{fileList.map((item, index) => (
|
||||
<Box
|
||||
key={item.id}
|
||||
border={'1px solid rgba(0,0,0,0.12)'}
|
||||
@@ -240,11 +230,11 @@ ${images.map((img) => JSON.stringify({ src: img.src })).join('\n')}
|
||||
rounded={'md'}
|
||||
position={'relative'}
|
||||
_hover={{
|
||||
'.close-icon': { display: item.src ? 'block' : 'none' }
|
||||
'.close-icon': { display: item.url ? 'block' : 'none' }
|
||||
}}
|
||||
>
|
||||
{/* uploading */}
|
||||
{!item.src && (
|
||||
{!item.url && (
|
||||
<Flex
|
||||
position={'absolute'}
|
||||
alignItems={'center'}
|
||||
@@ -272,12 +262,12 @@ ${images.map((img) => JSON.stringify({ src: img.src })).join('\n')}
|
||||
right={'-8px'}
|
||||
top={'-8px'}
|
||||
onClick={() => {
|
||||
setFileList((state) => state.filter((file) => file.id !== item.id));
|
||||
removeFile(index);
|
||||
}}
|
||||
className="close-icon"
|
||||
display={['', 'none']}
|
||||
/>
|
||||
{item.type === FileTypeEnum.image && (
|
||||
{item.type === ChatFileTypeEnum.image && (
|
||||
<Image
|
||||
alt={'img'}
|
||||
src={item.icon}
|
||||
@@ -335,14 +325,12 @@ ${images.map((img) => JSON.stringify({ src: img.src })).join('\n')}
|
||||
boxShadow={'none !important'}
|
||||
color={'myGray.900'}
|
||||
isDisabled={isSpeaking}
|
||||
value={inputValue}
|
||||
onChange={(e) => {
|
||||
const textarea = e.target;
|
||||
textarea.style.height = textareaMinH;
|
||||
textarea.style.height = `${textarea.scrollHeight}px`;
|
||||
|
||||
startSts(() => {
|
||||
onChange?.(textarea.value);
|
||||
});
|
||||
setValue('input', textarea.value);
|
||||
}}
|
||||
onKeyDown={(e) => {
|
||||
// enter send.(pc or iframe && enter and unPress shift)
|
||||
@@ -406,7 +394,7 @@ ${images.map((img) => JSON.stringify({ src: img.src })).join('\n')}
|
||||
if (isSpeaking) {
|
||||
return stopSpeak();
|
||||
}
|
||||
startSpeak(resetInputVal);
|
||||
startSpeak((text) => resetInputVal({ text }));
|
||||
}}
|
||||
>
|
||||
<MyTooltip label={isSpeaking ? t('core.chat.Stop Speak') : t('core.chat.Record')}>
|
||||
|
@@ -1,5 +1,6 @@
|
||||
import React, { useMemo, useState } from 'react';
|
||||
import type { ChatHistoryItemResType } from '@fastgpt/global/core/chat/type.d';
|
||||
import { type ChatHistoryItemResType } from '@fastgpt/global/core/chat/type.d';
|
||||
import { DispatchNodeResponseType } from '@fastgpt/global/core/module/runtime/type.d';
|
||||
import type { ChatItemType } from '@fastgpt/global/core/chat/type';
|
||||
import { Flex, BoxProps, useDisclosure, useTheme, Box } from '@chakra-ui/react';
|
||||
import { useTranslation } from 'next-i18next';
|
||||
@@ -14,15 +15,18 @@ import ChatBoxDivider from '@/components/core/chat/Divider';
|
||||
import { strIsLink } from '@fastgpt/global/common/string/tools';
|
||||
import MyIcon from '@fastgpt/web/components/common/Icon';
|
||||
|
||||
const QuoteModal = dynamic(() => import('./QuoteModal'), { ssr: false });
|
||||
const ContextModal = dynamic(() => import('./ContextModal'), { ssr: false });
|
||||
const WholeResponseModal = dynamic(() => import('./WholeResponseModal'), { ssr: false });
|
||||
const QuoteModal = dynamic(() => import('./QuoteModal'));
|
||||
const ContextModal = dynamic(() => import('./ContextModal'));
|
||||
const WholeResponseModal = dynamic(() => import('./WholeResponseModal'));
|
||||
|
||||
const isLLMNode = (item: ChatHistoryItemResType) =>
|
||||
item.moduleType === FlowNodeTypeEnum.chatNode || item.moduleType === FlowNodeTypeEnum.tools;
|
||||
|
||||
const ResponseTags = ({
|
||||
responseData = [],
|
||||
flowResponses = [],
|
||||
showDetail
|
||||
}: {
|
||||
responseData?: ChatHistoryItemResType[];
|
||||
flowResponses?: ChatHistoryItemResType[];
|
||||
showDetail: boolean;
|
||||
}) => {
|
||||
const theme = useTheme();
|
||||
@@ -36,7 +40,8 @@ const ResponseTags = ({
|
||||
sourceName: string;
|
||||
};
|
||||
}>();
|
||||
const [contextModalData, setContextModalData] = useState<ChatItemType[]>();
|
||||
const [contextModalData, setContextModalData] =
|
||||
useState<DispatchNodeResponseType['historyPreview']>();
|
||||
const {
|
||||
isOpen: isOpenWholeModal,
|
||||
onOpen: onOpenWholeModal,
|
||||
@@ -44,18 +49,29 @@ const ResponseTags = ({
|
||||
} = useDisclosure();
|
||||
|
||||
const {
|
||||
chatAccount,
|
||||
llmModuleAccount,
|
||||
quoteList = [],
|
||||
sourceList = [],
|
||||
historyPreview = [],
|
||||
runningTime = 0
|
||||
} = useMemo(() => {
|
||||
const chatData = responseData.find((item) => item.moduleType === FlowNodeTypeEnum.chatNode);
|
||||
const quoteList = responseData
|
||||
.filter((item) => item.moduleType === FlowNodeTypeEnum.chatNode)
|
||||
const flatResponse = flowResponses
|
||||
.map((item) => {
|
||||
if (item.pluginDetail || item.toolDetail) {
|
||||
return [item, ...(item.pluginDetail || []), ...(item.toolDetail || [])];
|
||||
}
|
||||
return item;
|
||||
})
|
||||
.flat();
|
||||
|
||||
const chatData = flatResponse.find(isLLMNode);
|
||||
|
||||
const quoteList = flatResponse
|
||||
.filter((item) => item.moduleType === FlowNodeTypeEnum.datasetSearchNode)
|
||||
.map((item) => item.quoteList)
|
||||
.flat()
|
||||
.filter(Boolean) as SearchDataResponseItemType[];
|
||||
|
||||
const sourceList = quoteList.reduce(
|
||||
(acc: Record<string, SearchDataResponseItemType[]>, cur) => {
|
||||
if (!acc[cur.collectionId]) {
|
||||
@@ -67,8 +83,7 @@ const ResponseTags = ({
|
||||
);
|
||||
|
||||
return {
|
||||
chatAccount: responseData.filter((item) => item.moduleType === FlowNodeTypeEnum.chatNode)
|
||||
.length,
|
||||
llmModuleAccount: flatResponse.filter(isLLMNode).length,
|
||||
quoteList,
|
||||
sourceList: Object.values(sourceList)
|
||||
.flat()
|
||||
@@ -80,16 +95,16 @@ const ResponseTags = ({
|
||||
collectionId: item.collectionId
|
||||
})),
|
||||
historyPreview: chatData?.historyPreview,
|
||||
runningTime: +responseData.reduce((sum, item) => sum + (item.runningTime || 0), 0).toFixed(2)
|
||||
runningTime: +flowResponses.reduce((sum, item) => sum + (item.runningTime || 0), 0).toFixed(2)
|
||||
};
|
||||
}, [showDetail, responseData]);
|
||||
}, [showDetail, flowResponses]);
|
||||
|
||||
const TagStyles: BoxProps = {
|
||||
mr: 2,
|
||||
bg: 'transparent'
|
||||
};
|
||||
|
||||
return responseData.length === 0 ? null : (
|
||||
return flowResponses.length === 0 ? null : (
|
||||
<>
|
||||
{sourceList.length > 0 && (
|
||||
<>
|
||||
@@ -148,10 +163,10 @@ const ResponseTags = ({
|
||||
</Tag>
|
||||
</MyTooltip>
|
||||
)}
|
||||
{chatAccount === 1 && (
|
||||
{llmModuleAccount === 1 && (
|
||||
<>
|
||||
{historyPreview.length > 0 && (
|
||||
<MyTooltip label={'点击查看完整对话记录'}>
|
||||
<MyTooltip label={'点击查看上下文预览'}>
|
||||
<Tag
|
||||
colorSchema="green"
|
||||
cursor={'pointer'}
|
||||
@@ -164,7 +179,7 @@ const ResponseTags = ({
|
||||
)}
|
||||
</>
|
||||
)}
|
||||
{chatAccount > 1 && (
|
||||
{llmModuleAccount > 1 && (
|
||||
<Tag colorSchema="blue" {...TagStyles}>
|
||||
多组 AI 对话
|
||||
</Tag>
|
||||
@@ -196,7 +211,7 @@ const ResponseTags = ({
|
||||
)}
|
||||
{isOpenWholeModal && (
|
||||
<WholeResponseModal
|
||||
response={responseData}
|
||||
response={flowResponses}
|
||||
showDetail={showDetail}
|
||||
onClose={onCloseWholeModal}
|
||||
/>
|
||||
|
@@ -2,7 +2,7 @@ import React, { useMemo, useState } from 'react';
|
||||
import { Box, useTheme, Flex, Image } from '@chakra-ui/react';
|
||||
import type { ChatHistoryItemResType } from '@fastgpt/global/core/chat/type.d';
|
||||
import { useTranslation } from 'next-i18next';
|
||||
import { moduleTemplatesFlat } from '@/web/core/modules/template/system';
|
||||
import { moduleTemplatesFlat } from '@fastgpt/global/core/module/template/constants';
|
||||
|
||||
import Tabs from '../Tabs';
|
||||
import MyModal from '../MyModal';
|
||||
@@ -143,6 +143,11 @@ const ResponseBox = React.memo(function ResponseBox({
|
||||
/>
|
||||
<Row label={t('core.chat.response.module model')} value={activeModule?.model} />
|
||||
<Row label={t('core.chat.response.module tokens')} value={`${activeModule?.tokens}`} />
|
||||
<Row
|
||||
label={t('core.chat.response.Tool call tokens')}
|
||||
value={`${activeModule?.toolCallTokens}`}
|
||||
/>
|
||||
|
||||
<Row label={t('core.chat.response.module query')} value={activeModule?.query} />
|
||||
<Row
|
||||
label={t('core.chat.response.context total length')}
|
||||
@@ -182,12 +187,6 @@ const ResponseBox = React.memo(function ResponseBox({
|
||||
)
|
||||
}
|
||||
/>
|
||||
{activeModule.quoteList && activeModule.quoteList.length > 0 && (
|
||||
<Row
|
||||
label={t('core.chat.response.module quoteList')}
|
||||
rawDom={<QuoteList showDetail={showDetail} rawSearch={activeModule.quoteList} />}
|
||||
/>
|
||||
)}
|
||||
</>
|
||||
|
||||
{/* dataset search */}
|
||||
@@ -213,6 +212,12 @@ const ResponseBox = React.memo(function ResponseBox({
|
||||
label={t('support.wallet.usage.Extension result')}
|
||||
value={`${activeModule?.extensionResult}`}
|
||||
/>
|
||||
{activeModule.quoteList && activeModule.quoteList.length > 0 && (
|
||||
<Row
|
||||
label={t('core.chat.response.module quoteList')}
|
||||
rawDom={<QuoteList showDetail={showDetail} rawSearch={activeModule.quoteList} />}
|
||||
/>
|
||||
)}
|
||||
</>
|
||||
|
||||
{/* classify question */}
|
||||
@@ -276,7 +281,7 @@ const ResponseBox = React.memo(function ResponseBox({
|
||||
)}
|
||||
{activeModule?.pluginDetail && activeModule?.pluginDetail.length > 0 && (
|
||||
<Row
|
||||
label={t('core.chat.response.Plugin Resonse Detail')}
|
||||
label={t('core.chat.response.Plugin response detail')}
|
||||
rawDom={<ResponseBox response={activeModule.pluginDetail} showDetail={showDetail} />}
|
||||
/>
|
||||
)}
|
||||
@@ -284,6 +289,14 @@ const ResponseBox = React.memo(function ResponseBox({
|
||||
|
||||
{/* text output */}
|
||||
<Row label={t('core.chat.response.text output')} value={activeModule?.textOutput} />
|
||||
|
||||
{/* tool call */}
|
||||
{activeModule?.toolDetail && activeModule?.toolDetail.length > 0 && (
|
||||
<Row
|
||||
label={t('core.chat.response.Tool call response detail')}
|
||||
rawDom={<ResponseBox response={activeModule.toolDetail} showDetail={showDetail} />}
|
||||
/>
|
||||
)}
|
||||
</Box>
|
||||
</>
|
||||
);
|
||||
|
@@ -0,0 +1,23 @@
|
||||
import Avatar from '@/components/Avatar';
|
||||
import { Box } from '@chakra-ui/react';
|
||||
import { useTheme } from '@chakra-ui/system';
|
||||
import React from 'react';
|
||||
|
||||
const ChatAvatar = ({ src, type }: { src?: string; type: 'Human' | 'AI' }) => {
|
||||
const theme = useTheme();
|
||||
return (
|
||||
<Box
|
||||
w={['28px', '34px']}
|
||||
h={['28px', '34px']}
|
||||
p={'2px'}
|
||||
borderRadius={'sm'}
|
||||
border={theme.borders.base}
|
||||
boxShadow={'0 0 5px rgba(0,0,0,0.1)'}
|
||||
bg={type === 'Human' ? 'white' : 'primary.50'}
|
||||
>
|
||||
<Avatar src={src} w={'100%'} h={'100%'} />
|
||||
</Box>
|
||||
);
|
||||
};
|
||||
|
||||
export default React.memo(ChatAvatar);
|
@@ -0,0 +1,236 @@
|
||||
import { useCopyData } from '@/web/common/hooks/useCopyData';
|
||||
import { useAudioPlay } from '@/web/common/utils/voice';
|
||||
import { Flex, FlexProps, Image, css, useTheme } from '@chakra-ui/react';
|
||||
import { ChatSiteItemType } from '@fastgpt/global/core/chat/type';
|
||||
import { AppTTSConfigType } from '@fastgpt/global/core/module/type';
|
||||
import { OutLinkChatAuthProps } from '@fastgpt/global/support/permission/chat';
|
||||
import MyTooltip from '@fastgpt/web/components/common/MyTooltip';
|
||||
import React from 'react';
|
||||
import { useTranslation } from 'next-i18next';
|
||||
import MyIcon from '@fastgpt/web/components/common/Icon';
|
||||
import { formatChatValue2InputType } from '../utils';
|
||||
import { ChatRoleEnum } from '@fastgpt/global/core/chat/constants';
|
||||
|
||||
export type ChatControllerProps = {
|
||||
isChatting: boolean;
|
||||
chat: ChatSiteItemType;
|
||||
setChatHistories?: React.Dispatch<React.SetStateAction<ChatSiteItemType[]>>;
|
||||
showVoiceIcon?: boolean;
|
||||
ttsConfig?: AppTTSConfigType;
|
||||
onRetry?: () => void;
|
||||
onDelete?: () => void;
|
||||
onMark?: () => void;
|
||||
onReadUserDislike?: () => void;
|
||||
onCloseUserLike?: () => void;
|
||||
onAddUserLike?: () => void;
|
||||
onAddUserDislike?: () => void;
|
||||
};
|
||||
|
||||
const ChatController = ({
|
||||
isChatting,
|
||||
chat,
|
||||
setChatHistories,
|
||||
showVoiceIcon,
|
||||
ttsConfig,
|
||||
onReadUserDislike,
|
||||
onCloseUserLike,
|
||||
onMark,
|
||||
onRetry,
|
||||
onDelete,
|
||||
onAddUserDislike,
|
||||
onAddUserLike,
|
||||
shareId,
|
||||
outLinkUid,
|
||||
teamId,
|
||||
teamToken
|
||||
}: OutLinkChatAuthProps & ChatControllerProps & FlexProps) => {
|
||||
const theme = useTheme();
|
||||
const { t } = useTranslation();
|
||||
const { copyData } = useCopyData();
|
||||
const { audioLoading, audioPlaying, hasAudio, playAudio, cancelAudio } = useAudioPlay({
|
||||
ttsConfig,
|
||||
shareId,
|
||||
outLinkUid,
|
||||
teamId,
|
||||
teamToken
|
||||
});
|
||||
const controlIconStyle = {
|
||||
w: '14px',
|
||||
cursor: 'pointer',
|
||||
p: '5px',
|
||||
bg: 'white',
|
||||
borderRight: theme.borders.base
|
||||
};
|
||||
const controlContainerStyle = {
|
||||
className: 'control',
|
||||
color: 'myGray.400',
|
||||
display: 'flex'
|
||||
};
|
||||
|
||||
return (
|
||||
<Flex
|
||||
{...controlContainerStyle}
|
||||
borderRadius={'sm'}
|
||||
overflow={'hidden'}
|
||||
border={theme.borders.base}
|
||||
// 最后一个子元素,没有border
|
||||
css={css({
|
||||
'& > *:last-child, & > *:last-child svg': {
|
||||
borderRight: 'none',
|
||||
borderRadius: 'md'
|
||||
}
|
||||
})}
|
||||
>
|
||||
<MyTooltip label={t('common.Copy')}>
|
||||
<MyIcon
|
||||
{...controlIconStyle}
|
||||
name={'copy'}
|
||||
_hover={{ color: 'primary.600' }}
|
||||
onClick={() => copyData(formatChatValue2InputType(chat.value).text || '')}
|
||||
/>
|
||||
</MyTooltip>
|
||||
{!!onDelete && !isChatting && (
|
||||
<>
|
||||
{onRetry && (
|
||||
<MyTooltip label={t('core.chat.retry')}>
|
||||
<MyIcon
|
||||
{...controlIconStyle}
|
||||
name={'common/retryLight'}
|
||||
_hover={{ color: 'green.500' }}
|
||||
onClick={onRetry}
|
||||
/>
|
||||
</MyTooltip>
|
||||
)}
|
||||
<MyTooltip label={t('common.Delete')}>
|
||||
<MyIcon
|
||||
{...controlIconStyle}
|
||||
name={'delete'}
|
||||
_hover={{ color: 'red.600' }}
|
||||
onClick={onDelete}
|
||||
/>
|
||||
</MyTooltip>
|
||||
</>
|
||||
)}
|
||||
{showVoiceIcon &&
|
||||
hasAudio &&
|
||||
(audioLoading ? (
|
||||
<MyTooltip label={t('common.Loading')}>
|
||||
<MyIcon {...controlIconStyle} name={'common/loading'} />
|
||||
</MyTooltip>
|
||||
) : audioPlaying ? (
|
||||
<Flex alignItems={'center'}>
|
||||
<MyTooltip label={t('core.chat.tts.Stop Speech')}>
|
||||
<MyIcon
|
||||
{...controlIconStyle}
|
||||
borderRight={'none'}
|
||||
name={'core/chat/stopSpeech'}
|
||||
color={'#E74694'}
|
||||
onClick={() => cancelAudio()}
|
||||
/>
|
||||
</MyTooltip>
|
||||
<Image src="/icon/speaking.gif" w={'23px'} alt={''} borderRight={theme.borders.base} />
|
||||
</Flex>
|
||||
) : (
|
||||
<MyTooltip label={t('core.app.TTS')}>
|
||||
<MyIcon
|
||||
{...controlIconStyle}
|
||||
name={'common/voiceLight'}
|
||||
_hover={{ color: '#E74694' }}
|
||||
onClick={async () => {
|
||||
const response = await playAudio({
|
||||
buffer: chat.ttsBuffer,
|
||||
chatItemId: chat.dataId,
|
||||
text: formatChatValue2InputType(chat.value).text || ''
|
||||
});
|
||||
|
||||
if (!setChatHistories || !response.buffer) return;
|
||||
setChatHistories((state) =>
|
||||
state.map((item) =>
|
||||
item.dataId === chat.dataId
|
||||
? {
|
||||
...item,
|
||||
ttsBuffer: response.buffer
|
||||
}
|
||||
: item
|
||||
)
|
||||
);
|
||||
}}
|
||||
/>
|
||||
</MyTooltip>
|
||||
))}
|
||||
{!!onMark && (
|
||||
<MyTooltip label={t('core.chat.Mark')}>
|
||||
<MyIcon
|
||||
{...controlIconStyle}
|
||||
name={'core/app/markLight'}
|
||||
_hover={{ color: '#67c13b' }}
|
||||
onClick={onMark}
|
||||
/>
|
||||
</MyTooltip>
|
||||
)}
|
||||
{chat.obj === ChatRoleEnum.AI && (
|
||||
<>
|
||||
{!!onCloseUserLike && chat.userGoodFeedback && (
|
||||
<MyTooltip label={t('core.chat.feedback.Close User Like')}>
|
||||
<MyIcon
|
||||
{...controlIconStyle}
|
||||
color={'white'}
|
||||
bg={'green.500'}
|
||||
fontWeight={'bold'}
|
||||
name={'core/chat/feedback/goodLight'}
|
||||
onClick={onCloseUserLike}
|
||||
/>
|
||||
</MyTooltip>
|
||||
)}
|
||||
{!!onReadUserDislike && chat.userBadFeedback && (
|
||||
<MyTooltip label={t('core.chat.feedback.Read User dislike')}>
|
||||
<MyIcon
|
||||
{...controlIconStyle}
|
||||
color={'white'}
|
||||
bg={'#FC9663'}
|
||||
fontWeight={'bold'}
|
||||
name={'core/chat/feedback/badLight'}
|
||||
onClick={onReadUserDislike}
|
||||
/>
|
||||
</MyTooltip>
|
||||
)}
|
||||
{!!onAddUserLike && (
|
||||
<MyIcon
|
||||
{...controlIconStyle}
|
||||
{...(!!chat.userGoodFeedback
|
||||
? {
|
||||
color: 'white',
|
||||
bg: 'green.500',
|
||||
fontWeight: 'bold'
|
||||
}
|
||||
: {
|
||||
_hover: { color: 'green.600' }
|
||||
})}
|
||||
name={'core/chat/feedback/goodLight'}
|
||||
onClick={onAddUserLike}
|
||||
/>
|
||||
)}
|
||||
{!!onAddUserDislike && (
|
||||
<MyIcon
|
||||
{...controlIconStyle}
|
||||
{...(!!chat.userBadFeedback
|
||||
? {
|
||||
color: 'white',
|
||||
bg: '#FC9663',
|
||||
fontWeight: 'bold',
|
||||
onClick: onAddUserDislike
|
||||
}
|
||||
: {
|
||||
_hover: { color: '#FB7C3C' },
|
||||
onClick: onAddUserDislike
|
||||
})}
|
||||
name={'core/chat/feedback/badLight'}
|
||||
/>
|
||||
)}
|
||||
</>
|
||||
)}
|
||||
</Flex>
|
||||
);
|
||||
};
|
||||
|
||||
export default React.memo(ChatController);
|
236
projects/app/src/components/ChatBox/components/ChatItem.tsx
Normal file
236
projects/app/src/components/ChatBox/components/ChatItem.tsx
Normal file
@@ -0,0 +1,236 @@
|
||||
import {
|
||||
Box,
|
||||
BoxProps,
|
||||
Card,
|
||||
Flex,
|
||||
useTheme,
|
||||
Accordion,
|
||||
AccordionItem,
|
||||
AccordionButton,
|
||||
AccordionPanel,
|
||||
AccordionIcon,
|
||||
Button,
|
||||
Image,
|
||||
Grid
|
||||
} from '@chakra-ui/react';
|
||||
import React, { useMemo } from 'react';
|
||||
import ChatController, { type ChatControllerProps } from './ChatController';
|
||||
import ChatAvatar from './ChatAvatar';
|
||||
import { MessageCardStyle } from '../constants';
|
||||
import { formatChatValue2InputType } from '../utils';
|
||||
import Markdown, { CodeClassName } from '@/components/Markdown';
|
||||
import styles from '../index.module.scss';
|
||||
import MyIcon from '@fastgpt/web/components/common/Icon';
|
||||
import {
|
||||
ChatItemValueTypeEnum,
|
||||
ChatRoleEnum,
|
||||
ChatStatusEnum
|
||||
} from '@fastgpt/global/core/chat/constants';
|
||||
import FilesBlock from './FilesBox';
|
||||
|
||||
const colorMap = {
|
||||
[ChatStatusEnum.loading]: {
|
||||
bg: 'myGray.100',
|
||||
color: 'myGray.600'
|
||||
},
|
||||
[ChatStatusEnum.running]: {
|
||||
bg: 'green.50',
|
||||
color: 'green.700'
|
||||
},
|
||||
[ChatStatusEnum.finish]: {
|
||||
bg: 'green.50',
|
||||
color: 'green.700'
|
||||
}
|
||||
};
|
||||
|
||||
const ChatItem = ({
|
||||
type,
|
||||
avatar,
|
||||
statusBoxData,
|
||||
children,
|
||||
isLastChild,
|
||||
questionGuides = [],
|
||||
...chatControllerProps
|
||||
}: {
|
||||
type: ChatRoleEnum.Human | ChatRoleEnum.AI;
|
||||
avatar?: string;
|
||||
statusBoxData?: {
|
||||
status: `${ChatStatusEnum}`;
|
||||
name: string;
|
||||
};
|
||||
isLastChild?: boolean;
|
||||
questionGuides?: string[];
|
||||
children?: React.ReactNode;
|
||||
} & ChatControllerProps) => {
|
||||
const theme = useTheme();
|
||||
const styleMap: BoxProps =
|
||||
type === ChatRoleEnum.Human
|
||||
? {
|
||||
order: 0,
|
||||
borderRadius: '8px 0 8px 8px',
|
||||
justifyContent: 'flex-end',
|
||||
textAlign: 'right',
|
||||
bg: 'primary.100'
|
||||
}
|
||||
: {
|
||||
order: 1,
|
||||
borderRadius: '0 8px 8px 8px',
|
||||
justifyContent: 'flex-start',
|
||||
textAlign: 'left',
|
||||
bg: 'myGray.50'
|
||||
};
|
||||
const { chat, isChatting } = chatControllerProps;
|
||||
|
||||
const ContentCard = useMemo(() => {
|
||||
if (type === 'Human') {
|
||||
const { text, files = [] } = formatChatValue2InputType(chat.value);
|
||||
|
||||
return (
|
||||
<>
|
||||
{files.length > 0 && <FilesBlock files={files} />}
|
||||
<Markdown source={text} isChatting={false} />
|
||||
</>
|
||||
);
|
||||
}
|
||||
/* AI */
|
||||
return (
|
||||
<Flex flexDirection={'column'} gap={2}>
|
||||
{chat.value.map((value, i) => {
|
||||
const key = `${chat.dataId}-ai-${i}`;
|
||||
if (value.text) {
|
||||
let source = value.text?.content || '';
|
||||
|
||||
if (isLastChild && !isChatting && questionGuides.length > 0) {
|
||||
source = `${source}
|
||||
\`\`\`${CodeClassName.questionGuide}
|
||||
${JSON.stringify(questionGuides)}`;
|
||||
}
|
||||
|
||||
return <Markdown key={key} source={source} isChatting={isLastChild && isChatting} />;
|
||||
}
|
||||
if (value.type === ChatItemValueTypeEnum.tool && value.tools) {
|
||||
return (
|
||||
<Box key={key}>
|
||||
{value.tools.map((tool) => {
|
||||
const toolParams = (() => {
|
||||
try {
|
||||
return JSON.stringify(JSON.parse(tool.params), null, 2);
|
||||
} catch (error) {
|
||||
return tool.params;
|
||||
}
|
||||
})();
|
||||
const toolResponse = (() => {
|
||||
try {
|
||||
return JSON.stringify(JSON.parse(tool.response), null, 2);
|
||||
} catch (error) {
|
||||
return tool.response;
|
||||
}
|
||||
})();
|
||||
return (
|
||||
<Box key={tool.id}>
|
||||
<Accordion allowToggle>
|
||||
<AccordionItem borderTop={'none'} borderBottom={'none'}>
|
||||
<AccordionButton
|
||||
w={'auto'}
|
||||
bg={'white'}
|
||||
borderRadius={'md'}
|
||||
borderWidth={'1px'}
|
||||
borderColor={'myGray.200'}
|
||||
boxShadow={'1'}
|
||||
_hover={{
|
||||
bg: 'auto',
|
||||
color: 'primary.600'
|
||||
}}
|
||||
>
|
||||
<Image src={tool.toolAvatar} alt={''} w={'14px'} mr={2} />
|
||||
<Box mr={1}>{tool.toolName}</Box>
|
||||
{isChatting && !tool.response && (
|
||||
<MyIcon name={'common/loading'} w={'14px'} />
|
||||
)}
|
||||
<AccordionIcon color={'myGray.600'} ml={5} />
|
||||
</AccordionButton>
|
||||
<AccordionPanel
|
||||
py={0}
|
||||
px={0}
|
||||
mt={0}
|
||||
borderRadius={'md'}
|
||||
overflow={'hidden'}
|
||||
maxH={'500px'}
|
||||
overflowY={'auto'}
|
||||
>
|
||||
{toolParams && (
|
||||
<Markdown
|
||||
source={`~~~json#Input
|
||||
${toolParams}`}
|
||||
/>
|
||||
)}
|
||||
{toolResponse && (
|
||||
<Markdown
|
||||
source={`~~~json#Response
|
||||
${toolResponse}`}
|
||||
/>
|
||||
)}
|
||||
</AccordionPanel>
|
||||
</AccordionItem>
|
||||
</Accordion>
|
||||
</Box>
|
||||
);
|
||||
})}
|
||||
</Box>
|
||||
);
|
||||
}
|
||||
})}
|
||||
</Flex>
|
||||
);
|
||||
}, [chat.dataId, chat.value, isChatting, isLastChild, questionGuides, type]);
|
||||
|
||||
const chatStatusMap = useMemo(() => {
|
||||
if (!statusBoxData?.status) return;
|
||||
return colorMap[statusBoxData.status];
|
||||
}, [statusBoxData?.status]);
|
||||
|
||||
return (
|
||||
<>
|
||||
{/* control icon */}
|
||||
<Flex w={'100%'} alignItems={'center'} gap={2} justifyContent={styleMap.justifyContent}>
|
||||
{isChatting && type === ChatRoleEnum.AI && isLastChild ? null : (
|
||||
<Box order={styleMap.order} ml={styleMap.ml}>
|
||||
<ChatController {...chatControllerProps} />
|
||||
</Box>
|
||||
)}
|
||||
<ChatAvatar src={avatar} type={type} />
|
||||
|
||||
{!!chatStatusMap && statusBoxData && isLastChild && (
|
||||
<Flex alignItems={'center'} px={3} py={'1.5px'} borderRadius="md" bg={chatStatusMap.bg}>
|
||||
<Box
|
||||
className={styles.statusAnimation}
|
||||
bg={chatStatusMap.color}
|
||||
w="8px"
|
||||
h="8px"
|
||||
borderRadius={'50%'}
|
||||
mt={'1px'}
|
||||
/>
|
||||
<Box ml={2} color={'myGray.600'}>
|
||||
{statusBoxData.name}
|
||||
</Box>
|
||||
</Flex>
|
||||
)}
|
||||
</Flex>
|
||||
{/* content */}
|
||||
<Box mt={['6px', 2]} textAlign={styleMap.textAlign}>
|
||||
<Card
|
||||
className="markdown"
|
||||
{...MessageCardStyle}
|
||||
bg={styleMap.bg}
|
||||
borderRadius={styleMap.borderRadius}
|
||||
textAlign={'left'}
|
||||
>
|
||||
{ContentCard}
|
||||
{children}
|
||||
</Card>
|
||||
</Box>
|
||||
</>
|
||||
);
|
||||
};
|
||||
|
||||
export default ChatItem;
|
23
projects/app/src/components/ChatBox/components/Empty.tsx
Normal file
23
projects/app/src/components/ChatBox/components/Empty.tsx
Normal file
@@ -0,0 +1,23 @@
|
||||
import Markdown from '@/components/Markdown';
|
||||
import { useMarkdown } from '@/web/common/hooks/useMarkdown';
|
||||
import { Box, Card } from '@chakra-ui/react';
|
||||
import React from 'react';
|
||||
|
||||
const Empty = () => {
|
||||
const { data: chatProblem } = useMarkdown({ url: '/chatProblem.md' });
|
||||
const { data: versionIntro } = useMarkdown({ url: '/versionIntro.md' });
|
||||
|
||||
return (
|
||||
<Box pt={6} w={'85%'} maxW={'600px'} m={'auto'} alignItems={'center'} justifyContent={'center'}>
|
||||
{/* version intro */}
|
||||
<Card p={4} mb={10} minH={'200px'}>
|
||||
<Markdown source={versionIntro} />
|
||||
</Card>
|
||||
<Card p={4} minH={'600px'}>
|
||||
<Markdown source={chatProblem} />
|
||||
</Card>
|
||||
</Box>
|
||||
);
|
||||
};
|
||||
|
||||
export default React.memo(Empty);
|
22
projects/app/src/components/ChatBox/components/FilesBox.tsx
Normal file
22
projects/app/src/components/ChatBox/components/FilesBox.tsx
Normal file
@@ -0,0 +1,22 @@
|
||||
import { Box, Flex, Grid } from '@chakra-ui/react';
|
||||
import MdImage from '@/components/Markdown/img/Image';
|
||||
import { UserInputFileItemType } from '@/components/ChatBox/type';
|
||||
|
||||
const FilesBlock = ({ files }: { files: UserInputFileItemType[] }) => {
|
||||
return (
|
||||
<Grid gridTemplateColumns={['1fr', '1fr 1fr']} gap={4}>
|
||||
{files.map(({ id, type, name, url }, i) => {
|
||||
if (type === 'image') {
|
||||
return (
|
||||
<Box key={i} rounded={'md'} flex={'1 0 0'} minW={'120px'}>
|
||||
<MdImage src={url} />
|
||||
</Box>
|
||||
);
|
||||
}
|
||||
return null;
|
||||
})}
|
||||
</Grid>
|
||||
);
|
||||
};
|
||||
|
||||
export default FilesBlock;
|
119
projects/app/src/components/ChatBox/components/VariableInput.tsx
Normal file
119
projects/app/src/components/ChatBox/components/VariableInput.tsx
Normal file
@@ -0,0 +1,119 @@
|
||||
import { VariableItemType } from '@fastgpt/global/core/module/type';
|
||||
import React, { useState } from 'react';
|
||||
import { UseFormReturn } from 'react-hook-form';
|
||||
import { useTranslation } from 'next-i18next';
|
||||
import { Box, Button, Card, Input, Textarea } from '@chakra-ui/react';
|
||||
import ChatAvatar from './ChatAvatar';
|
||||
import { MessageCardStyle } from '../constants';
|
||||
import { VariableInputEnum } from '@fastgpt/global/core/module/constants';
|
||||
import MySelect from '@fastgpt/web/components/common/MySelect';
|
||||
import MyIcon from '@fastgpt/web/components/common/Icon';
|
||||
import { ChatBoxInputFormType } from '../type.d';
|
||||
|
||||
const VariableInput = ({
|
||||
appAvatar,
|
||||
variableModules,
|
||||
variableIsFinish,
|
||||
chatForm,
|
||||
onSubmitVariables
|
||||
}: {
|
||||
appAvatar?: string;
|
||||
variableModules: VariableItemType[];
|
||||
variableIsFinish: boolean;
|
||||
onSubmitVariables: (e: Record<string, any>) => void;
|
||||
chatForm: UseFormReturn<ChatBoxInputFormType>;
|
||||
}) => {
|
||||
const { t } = useTranslation();
|
||||
const [refresh, setRefresh] = useState(false);
|
||||
const { register, setValue, handleSubmit: handleSubmitChat, watch } = chatForm;
|
||||
const variables = watch('variables');
|
||||
|
||||
return (
|
||||
<Box py={3}>
|
||||
{/* avatar */}
|
||||
<ChatAvatar src={appAvatar} type={'AI'} />
|
||||
{/* message */}
|
||||
<Box textAlign={'left'}>
|
||||
<Card
|
||||
order={2}
|
||||
mt={2}
|
||||
w={'400px'}
|
||||
{...MessageCardStyle}
|
||||
bg={'white'}
|
||||
boxShadow={'0 0 8px rgba(0,0,0,0.15)'}
|
||||
>
|
||||
{variableModules.map((item) => (
|
||||
<Box key={item.id} mb={4}>
|
||||
<Box as={'label'} display={'inline-block'} position={'relative'} mb={1}>
|
||||
{item.label}
|
||||
{item.required && (
|
||||
<Box
|
||||
position={'absolute'}
|
||||
top={'-2px'}
|
||||
right={'-10px'}
|
||||
color={'red.500'}
|
||||
fontWeight={'bold'}
|
||||
>
|
||||
*
|
||||
</Box>
|
||||
)}
|
||||
</Box>
|
||||
{item.type === VariableInputEnum.input && (
|
||||
<Input
|
||||
isDisabled={variableIsFinish}
|
||||
bg={'myWhite.400'}
|
||||
{...register(`variables.${item.key}`, {
|
||||
required: item.required
|
||||
})}
|
||||
/>
|
||||
)}
|
||||
{item.type === VariableInputEnum.textarea && (
|
||||
<Textarea
|
||||
isDisabled={variableIsFinish}
|
||||
bg={'myWhite.400'}
|
||||
{...register(`variables.${item.key}`, {
|
||||
required: item.required
|
||||
})}
|
||||
rows={5}
|
||||
maxLength={4000}
|
||||
/>
|
||||
)}
|
||||
{item.type === VariableInputEnum.select && (
|
||||
<MySelect
|
||||
width={'100%'}
|
||||
isDisabled={variableIsFinish}
|
||||
list={(item.enums || []).map((item) => ({
|
||||
label: item.value,
|
||||
value: item.value
|
||||
}))}
|
||||
{...register(`variables.${item.key}`, {
|
||||
required: item.required
|
||||
})}
|
||||
value={variables[item.key]}
|
||||
onchange={(e) => {
|
||||
setValue(`variables.${item.key}`, e);
|
||||
setRefresh((state) => !state);
|
||||
}}
|
||||
/>
|
||||
)}
|
||||
</Box>
|
||||
))}
|
||||
{!variableIsFinish && (
|
||||
<Button
|
||||
leftIcon={<MyIcon name={'core/chat/chatFill'} w={'16px'} />}
|
||||
size={'sm'}
|
||||
maxW={'100px'}
|
||||
onClick={handleSubmitChat((data) => {
|
||||
onSubmitVariables(data);
|
||||
})}
|
||||
>
|
||||
{t('core.chat.Start Chat')}
|
||||
</Button>
|
||||
)}
|
||||
</Card>
|
||||
</Box>
|
||||
</Box>
|
||||
);
|
||||
};
|
||||
|
||||
export default React.memo(VariableInput);
|
@@ -0,0 +1,28 @@
|
||||
import { Box, Card } from '@chakra-ui/react';
|
||||
import React from 'react';
|
||||
import { MessageCardStyle } from '../constants';
|
||||
import Markdown from '@/components/Markdown';
|
||||
import ChatAvatar from './ChatAvatar';
|
||||
|
||||
const WelcomeBox = ({ appAvatar, welcomeText }: { appAvatar?: string; welcomeText: string }) => {
|
||||
return (
|
||||
<Box py={3}>
|
||||
{/* avatar */}
|
||||
<ChatAvatar src={appAvatar} type={'AI'} />
|
||||
{/* message */}
|
||||
<Box textAlign={'left'}>
|
||||
<Card
|
||||
order={2}
|
||||
mt={2}
|
||||
{...MessageCardStyle}
|
||||
bg={'white'}
|
||||
boxShadow={'0 0 8px rgba(0,0,0,0.15)'}
|
||||
>
|
||||
<Markdown source={`~~~guide \n${welcomeText}`} isChatting={false} />
|
||||
</Card>
|
||||
</Box>
|
||||
</Box>
|
||||
);
|
||||
};
|
||||
|
||||
export default WelcomeBox;
|
13
projects/app/src/components/ChatBox/constants.ts
Normal file
13
projects/app/src/components/ChatBox/constants.ts
Normal file
@@ -0,0 +1,13 @@
|
||||
import { BoxProps } from '@chakra-ui/react';
|
||||
|
||||
export const textareaMinH = '22px';
|
||||
|
||||
export const MessageCardStyle: BoxProps = {
|
||||
px: 4,
|
||||
py: 3,
|
||||
borderRadius: '0 8px 8px 8px',
|
||||
boxShadow: 'none',
|
||||
display: 'inline-block',
|
||||
maxW: ['calc(100% - 25px)', 'calc(100% - 40px)'],
|
||||
color: 'myGray.900'
|
||||
};
|
78
projects/app/src/components/ChatBox/hooks/useChatBox.tsx
Normal file
78
projects/app/src/components/ChatBox/hooks/useChatBox.tsx
Normal file
@@ -0,0 +1,78 @@
|
||||
import { ExportChatType } from '@/types/chat';
|
||||
import { ChatItemType } from '@fastgpt/global/core/chat/type';
|
||||
import { useCallback } from 'react';
|
||||
import { htmlTemplate } from '@/constants/common';
|
||||
import { fileDownload } from '@/web/common/file/utils';
|
||||
|
||||
export const useChatBox = () => {
|
||||
const onExportChat = useCallback(
|
||||
({ type, history }: { type: ExportChatType; history: ChatItemType[] }) => {
|
||||
const getHistoryHtml = () => {
|
||||
const historyDom = document.getElementById('history');
|
||||
if (!historyDom) return;
|
||||
const dom = Array.from(historyDom.children).map((child, i) => {
|
||||
const avatar = `<img src="${
|
||||
child.querySelector<HTMLImageElement>('.avatar')?.src
|
||||
}" alt="" />`;
|
||||
|
||||
const chatContent = child.querySelector<HTMLDivElement>('.markdown');
|
||||
|
||||
if (!chatContent) {
|
||||
return '';
|
||||
}
|
||||
|
||||
const chatContentClone = chatContent.cloneNode(true) as HTMLDivElement;
|
||||
|
||||
const codeHeader = chatContentClone.querySelectorAll('.code-header');
|
||||
codeHeader.forEach((childElement: any) => {
|
||||
childElement.remove();
|
||||
});
|
||||
|
||||
return `<div class="chat-item">
|
||||
${avatar}
|
||||
${chatContentClone.outerHTML}
|
||||
</div>`;
|
||||
});
|
||||
|
||||
const html = htmlTemplate.replace('{{CHAT_CONTENT}}', dom.join('\n'));
|
||||
return html;
|
||||
};
|
||||
|
||||
const map: Record<ExportChatType, () => void> = {
|
||||
md: () => {
|
||||
fileDownload({
|
||||
text: history.map((item) => item.value).join('\n\n'),
|
||||
type: 'text/markdown',
|
||||
filename: 'chat.md'
|
||||
});
|
||||
},
|
||||
html: () => {
|
||||
const html = getHistoryHtml();
|
||||
html &&
|
||||
fileDownload({
|
||||
text: html,
|
||||
type: 'text/html',
|
||||
filename: '聊天记录.html'
|
||||
});
|
||||
},
|
||||
pdf: () => {
|
||||
const html = getHistoryHtml();
|
||||
|
||||
html &&
|
||||
// @ts-ignore
|
||||
html2pdf(html, {
|
||||
margin: 0,
|
||||
filename: `聊天记录.pdf`
|
||||
});
|
||||
}
|
||||
};
|
||||
|
||||
map[type]();
|
||||
},
|
||||
[]
|
||||
);
|
||||
|
||||
return {
|
||||
onExportChat
|
||||
};
|
||||
};
|
File diff suppressed because it is too large
Load Diff
53
projects/app/src/components/ChatBox/type.d.ts
vendored
Normal file
53
projects/app/src/components/ChatBox/type.d.ts
vendored
Normal file
@@ -0,0 +1,53 @@
|
||||
import { ChatCompletionMessageParam } from '@fastgpt/global/core/ai/type';
|
||||
import { ChatFileTypeEnum } from '@fastgpt/global/core/chat/constants';
|
||||
import {
|
||||
ChatItemValueItemType,
|
||||
ChatSiteItemType,
|
||||
ToolModuleResponseItemType
|
||||
} from '@fastgpt/global/core/chat/type';
|
||||
import { SseResponseEventEnum } from '@fastgpt/global/core/module/runtime/constants';
|
||||
|
||||
export type generatingMessageProps = {
|
||||
event: `${SseResponseEventEnum}`;
|
||||
text?: string;
|
||||
name?: string;
|
||||
status?: 'running' | 'finish';
|
||||
tool?: ToolModuleResponseItemType;
|
||||
};
|
||||
|
||||
export type UserInputFileItemType = {
|
||||
id: string;
|
||||
rawFile?: File;
|
||||
type: `${ChatFileTypeEnum}`;
|
||||
name: string;
|
||||
icon: string; // img is base64
|
||||
url?: string;
|
||||
};
|
||||
|
||||
export type ChatBoxInputFormType = {
|
||||
input: string;
|
||||
files: UserInputFileItemType[];
|
||||
variables: Record<string, any>;
|
||||
chatStarted: boolean;
|
||||
};
|
||||
|
||||
export type ChatBoxInputType = {
|
||||
text?: string;
|
||||
files?: UserInputFileItemType[];
|
||||
};
|
||||
|
||||
export type StartChatFnProps = {
|
||||
chatList: ChatSiteItemType[];
|
||||
messages: ChatCompletionMessageParam[];
|
||||
controller: AbortController;
|
||||
variables: Record<string, any>;
|
||||
generatingMessage: (e: generatingMessageProps) => void;
|
||||
};
|
||||
|
||||
export type ComponentRef = {
|
||||
getChatHistories: () => ChatSiteItemType[];
|
||||
resetVariables: (data?: Record<string, any>) => void;
|
||||
resetHistory: (history: ChatSiteItemType[]) => void;
|
||||
scrollToBottom: (behavior?: 'smooth' | 'auto') => void;
|
||||
sendPrompt: (question: string) => void;
|
||||
};
|
33
projects/app/src/components/ChatBox/utils.ts
Normal file
33
projects/app/src/components/ChatBox/utils.ts
Normal file
@@ -0,0 +1,33 @@
|
||||
import { ChatItemValueItemType } from '@fastgpt/global/core/chat/type';
|
||||
import { ChatBoxInputType, UserInputFileItemType } from './type';
|
||||
import { getNanoid } from '@fastgpt/global/common/string/tools';
|
||||
|
||||
export const formatChatValue2InputType = (value: ChatItemValueItemType[]): ChatBoxInputType => {
|
||||
if (!Array.isArray(value)) {
|
||||
console.error('value is error', value);
|
||||
return { text: '', files: [] };
|
||||
}
|
||||
const text = value
|
||||
.filter((item) => item.text?.content)
|
||||
.map((item) => item.text?.content || '')
|
||||
.join('');
|
||||
const files =
|
||||
(value
|
||||
.map((item) =>
|
||||
item.type === 'file' && item.file
|
||||
? {
|
||||
id: getNanoid(),
|
||||
type: item.file.type,
|
||||
name: item.file.name,
|
||||
icon: '',
|
||||
url: item.file.url
|
||||
}
|
||||
: undefined
|
||||
)
|
||||
.filter(Boolean) as UserInputFileItemType[]) || [];
|
||||
|
||||
return {
|
||||
text,
|
||||
files
|
||||
};
|
||||
};
|
Reference in New Issue
Block a user