V4.7-alpha (#985)

Co-authored-by: heheer <71265218+newfish-cmyk@users.noreply.github.com>
This commit is contained in:
Archer
2024-03-13 10:50:02 +08:00
committed by GitHub
parent 5bca15f12f
commit 9501c3f3a1
170 changed files with 5786 additions and 2342 deletions

View File

@@ -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')}>