File input (#2270)
* doc * feat: file upload config * perf: chat box file params * feat: markdown show file * feat: chat file store and clear * perf: read file contentType * feat: llm vision config * feat: file url output * perf: plugin error text * perf: image load * feat: ai chat document * perf: file block ui * feat: read file node * feat: file read response field * feat: simple mode support read files * feat: tool call * feat: read file histories * perf: select file * perf: select file config * i18n * i18n * fix: ts; feat: tool response preview result
This commit is contained in:
@@ -1,16 +1,14 @@
|
||||
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 } from 'react';
|
||||
import { Box, Flex, HStack, Image, Spinner, Textarea } from '@chakra-ui/react';
|
||||
import React, { useRef, useEffect, useCallback, useMemo } from 'react';
|
||||
import { useTranslation } from 'next-i18next';
|
||||
import MyTooltip from '@fastgpt/web/components/common/MyTooltip';
|
||||
import MyIcon from '@fastgpt/web/components/common/Icon';
|
||||
import { useSelectFile } from '@/web/common/file/hooks/useSelectFile';
|
||||
import { compressImgFileAndUpload } from '@/web/common/file/controller';
|
||||
import { uploadFile2DB } from '@/web/common/file/controller';
|
||||
import { ChatFileTypeEnum } from '@fastgpt/global/core/chat/constants';
|
||||
import { addDays } from 'date-fns';
|
||||
import { useRequest } from '@fastgpt/web/hooks/useRequest';
|
||||
import { MongoImageTypeEnum } from '@fastgpt/global/common/file/image/constants';
|
||||
import { useRequest2 } from '@fastgpt/web/hooks/useRequest';
|
||||
import { ChatBoxInputFormType, ChatBoxInputType, UserInputFileItemType } from '../type';
|
||||
import { textareaMinH } from '../constants';
|
||||
import { UseFormReturn, useFieldArray } from 'react-hook-form';
|
||||
@@ -19,103 +17,167 @@ import dynamic from 'next/dynamic';
|
||||
import { useContextSelector } from 'use-context-selector';
|
||||
import { getNanoid } from '@fastgpt/global/common/string/tools';
|
||||
import { useSystem } from '@fastgpt/web/hooks/useSystem';
|
||||
import { documentFileType } from '@fastgpt/global/common/file/constants';
|
||||
import { getFileIcon } from '@fastgpt/global/common/file/icon';
|
||||
import { useToast } from '@fastgpt/web/hooks/useToast';
|
||||
import { clone } from 'lodash';
|
||||
import { formatFileSize } from '@fastgpt/global/common/file/tools';
|
||||
|
||||
const InputGuideBox = dynamic(() => import('./InputGuideBox'));
|
||||
|
||||
const fileTypeFilter = (file: File) => {
|
||||
return (
|
||||
file.type.includes('image') ||
|
||||
documentFileType.split(',').some((type) => file.name.endsWith(type.trim()))
|
||||
);
|
||||
};
|
||||
|
||||
const ChatInput = ({
|
||||
onSendMessage,
|
||||
onStop,
|
||||
TextareaDom,
|
||||
showFileSelector = false,
|
||||
resetInputVal,
|
||||
chatForm,
|
||||
appId
|
||||
}: {
|
||||
onSendMessage: (val: ChatBoxInputType & { autoTTSResponse?: boolean }) => void;
|
||||
onStop: () => void;
|
||||
showFileSelector?: boolean;
|
||||
TextareaDom: React.MutableRefObject<HTMLTextAreaElement | null>;
|
||||
resetInputVal: (val: ChatBoxInputType) => void;
|
||||
chatForm: UseFormReturn<ChatBoxInputFormType>;
|
||||
appId: string;
|
||||
}) => {
|
||||
const { isPc } = useSystem();
|
||||
const { toast } = useToast();
|
||||
const { t } = useTranslation();
|
||||
const { feConfigs } = useSystemStore();
|
||||
|
||||
const { setValue, watch, control } = chatForm;
|
||||
const inputValue = watch('input');
|
||||
const {
|
||||
update: updateFile,
|
||||
remove: removeFile,
|
||||
update: updateFiles,
|
||||
remove: removeFiles,
|
||||
fields: fileList,
|
||||
append: appendFile,
|
||||
replace: replaceFile
|
||||
replace: replaceFiles
|
||||
} = useFieldArray({
|
||||
control,
|
||||
name: 'files'
|
||||
});
|
||||
|
||||
const { isChatting, whisperConfig, autoTTSResponse, chatInputGuide, outLinkAuthData } =
|
||||
useContextSelector(ChatBoxContext, (v) => v);
|
||||
const { whisperModel } = useSystemStore();
|
||||
const { isPc } = useSystem();
|
||||
|
||||
const canvasRef = useRef<HTMLCanvasElement>(null);
|
||||
const { t } = useTranslation();
|
||||
const {
|
||||
chatId,
|
||||
isChatting,
|
||||
whisperConfig,
|
||||
autoTTSResponse,
|
||||
chatInputGuide,
|
||||
outLinkAuthData,
|
||||
fileSelectConfig
|
||||
} = useContextSelector(ChatBoxContext, (v) => v);
|
||||
|
||||
const havInput = !!inputValue || fileList.length > 0;
|
||||
const hasFileUploading = fileList.some((item) => !item.url);
|
||||
const canSendMessage = havInput && !hasFileUploading;
|
||||
|
||||
const showSelectFile = fileSelectConfig.canSelectFile;
|
||||
const showSelectImg = fileSelectConfig.canSelectImg;
|
||||
const maxSelectFiles = fileSelectConfig.maxFiles ?? 10;
|
||||
const maxSize = (feConfigs?.uploadFileMaxSize || 1024) * 1024 * 1024; // nkb
|
||||
const { icon: selectFileIcon, tooltip: selectFileTip } = useMemo(() => {
|
||||
if (showSelectFile) {
|
||||
return {
|
||||
icon: 'core/chat/fileSelect',
|
||||
tooltip: t('chat:select_file')
|
||||
};
|
||||
} else if (showSelectImg) {
|
||||
return {
|
||||
icon: 'core/chat/fileSelect',
|
||||
tooltip: t('chat:select_img')
|
||||
};
|
||||
}
|
||||
return {};
|
||||
}, [showSelectFile, showSelectImg, t]);
|
||||
|
||||
/* file selector and upload */
|
||||
const { File, onOpen: onOpenSelectFile } = useSelectFile({
|
||||
fileType: 'image/*',
|
||||
fileType: `${showSelectImg ? 'image/*,' : ''} ${showSelectFile ? documentFileType : ''}`,
|
||||
multiple: true,
|
||||
maxCount: 10
|
||||
maxCount: maxSelectFiles
|
||||
});
|
||||
const { mutate: uploadFile } = useRequest({
|
||||
mutationFn: async ({ file, fileIndex }: { file: UserInputFileItemType; fileIndex: number }) => {
|
||||
if (file.type === ChatFileTypeEnum.image && file.rawFile) {
|
||||
useRequest2(
|
||||
async () => {
|
||||
const filterFiles = fileList.filter((item) => item.status === 0);
|
||||
|
||||
if (filterFiles.length === 0) return;
|
||||
|
||||
replaceFiles(fileList.map((item) => ({ ...item, status: 1 })));
|
||||
|
||||
for (const file of filterFiles) {
|
||||
if (!file.rawFile) continue;
|
||||
|
||||
try {
|
||||
const url = await compressImgFileAndUpload({
|
||||
type: MongoImageTypeEnum.chatImage,
|
||||
const { fileId, previewUrl } = await uploadFile2DB({
|
||||
file: file.rawFile,
|
||||
maxW: 4320,
|
||||
maxH: 4320,
|
||||
maxSize: 1024 * 1024 * 16,
|
||||
// 7 day expired.
|
||||
expiredTime: addDays(new Date(), 7),
|
||||
...outLinkAuthData
|
||||
bucketName: 'chat',
|
||||
metadata: {
|
||||
chatId
|
||||
}
|
||||
});
|
||||
updateFile(fileIndex, {
|
||||
|
||||
updateFiles(fileList.findIndex((item) => item.id === file.id)!, {
|
||||
...file,
|
||||
url
|
||||
status: 1,
|
||||
url: `${location.origin}${previewUrl}`
|
||||
});
|
||||
} catch (error) {
|
||||
removeFile(fileIndex);
|
||||
removeFiles(fileList.findIndex((item) => item.id === file.id)!);
|
||||
console.log(error);
|
||||
return Promise.reject(error);
|
||||
}
|
||||
}
|
||||
},
|
||||
errorToast: t('common:common.Upload File Failed')
|
||||
});
|
||||
{
|
||||
manual: false,
|
||||
errorToast: t('common:upload_file_error'),
|
||||
refreshDeps: [fileList]
|
||||
}
|
||||
);
|
||||
const onSelectFile = useCallback(
|
||||
async (files: File[]) => {
|
||||
if (!files || files.length === 0) {
|
||||
return;
|
||||
}
|
||||
// filter max files
|
||||
if (fileList.length + files.length > maxSelectFiles) {
|
||||
files = files.slice(0, maxSelectFiles - fileList.length);
|
||||
toast({
|
||||
status: 'warning',
|
||||
title: t('chat:file_amount_over', { max: maxSelectFiles })
|
||||
});
|
||||
}
|
||||
|
||||
const filterFilesByMaxSize = files.filter((file) => file.size <= maxSize);
|
||||
if (filterFilesByMaxSize.length < files.length) {
|
||||
toast({
|
||||
status: 'warning',
|
||||
title: t('file:some_file_size_exceeds_limit', { maxSize: formatFileSize(maxSize) })
|
||||
});
|
||||
}
|
||||
|
||||
const loadFiles = await Promise.all(
|
||||
files.map(
|
||||
filterFilesByMaxSize.map(
|
||||
(file) =>
|
||||
new Promise<UserInputFileItemType>((resolve, reject) => {
|
||||
if (file.type.includes('image')) {
|
||||
const reader = new FileReader();
|
||||
reader.readAsDataURL(file);
|
||||
reader.onload = () => {
|
||||
const item = {
|
||||
const item: UserInputFileItemType = {
|
||||
id: getNanoid(6),
|
||||
rawFile: file,
|
||||
type: ChatFileTypeEnum.image,
|
||||
name: file.name,
|
||||
icon: reader.result as string
|
||||
icon: reader.result as string,
|
||||
status: 0
|
||||
};
|
||||
resolve(item);
|
||||
};
|
||||
@@ -128,22 +190,28 @@ const ChatInput = ({
|
||||
rawFile: file,
|
||||
type: ChatFileTypeEnum.file,
|
||||
name: file.name,
|
||||
icon: 'file/pdf'
|
||||
icon: getFileIcon(file.name),
|
||||
status: 0
|
||||
});
|
||||
}
|
||||
})
|
||||
)
|
||||
);
|
||||
appendFile(loadFiles);
|
||||
|
||||
loadFiles.forEach((file, i) =>
|
||||
uploadFile({
|
||||
file,
|
||||
fileIndex: i + fileList.length
|
||||
// Document, image
|
||||
const concatFileList = clone(
|
||||
fileList.concat(loadFiles).sort((a, b) => {
|
||||
if (a.type === ChatFileTypeEnum.image && b.type === ChatFileTypeEnum.file) {
|
||||
return 1;
|
||||
} else if (a.type === ChatFileTypeEnum.file && b.type === ChatFileTypeEnum.image) {
|
||||
return -1;
|
||||
}
|
||||
return 0;
|
||||
})
|
||||
);
|
||||
replaceFiles(concatFileList);
|
||||
},
|
||||
[appendFile, fileList.length, uploadFile]
|
||||
[fileList, maxSelectFiles, replaceFiles, toast, t]
|
||||
);
|
||||
|
||||
/* on send */
|
||||
@@ -155,10 +223,12 @@ const ChatInput = ({
|
||||
text: textareaValue.trim(),
|
||||
files: fileList
|
||||
});
|
||||
replaceFile([]);
|
||||
replaceFiles([]);
|
||||
};
|
||||
|
||||
/* whisper init */
|
||||
const { whisperModel } = useSystemStore();
|
||||
const canvasRef = useRef<HTMLCanvasElement>(null);
|
||||
const {
|
||||
isSpeaking,
|
||||
isTransCription,
|
||||
@@ -194,12 +264,12 @@ const ChatInput = ({
|
||||
files: fileList,
|
||||
autoTTSResponse
|
||||
});
|
||||
replaceFile([]);
|
||||
replaceFiles([]);
|
||||
} else {
|
||||
resetInputVal({ text });
|
||||
}
|
||||
},
|
||||
[autoTTSResponse, fileList, onSendMessage, replaceFile, resetInputVal, whisperConfig?.autoSend]
|
||||
[autoTTSResponse, fileList, onSendMessage, replaceFiles, resetInputVal, whisperConfig?.autoSend]
|
||||
);
|
||||
const onWhisperRecord = useCallback(() => {
|
||||
if (isSpeaking) {
|
||||
@@ -261,13 +331,20 @@ const ChatInput = ({
|
||||
</Flex>
|
||||
|
||||
{/* file preview */}
|
||||
<Flex wrap={'wrap'} px={[2, 4]} userSelect={'none'}>
|
||||
<Flex
|
||||
wrap={'wrap'}
|
||||
px={[2, 4]}
|
||||
userSelect={'none'}
|
||||
gap={2}
|
||||
mb={fileList.length > 0 ? 2 : 0}
|
||||
>
|
||||
{fileList.map((item, index) => (
|
||||
<Box
|
||||
key={item.id}
|
||||
border={'1px solid rgba(0,0,0,0.12)'}
|
||||
mr={2}
|
||||
mb={2}
|
||||
border={'1px solid #E8EBF0'}
|
||||
boxShadow={
|
||||
'0px 2.571px 6.429px 0px rgba(19, 51, 107, 0.08), 0px 0px 0.643px 0px rgba(19, 51, 107, 0.08)'
|
||||
}
|
||||
rounded={'md'}
|
||||
position={'relative'}
|
||||
_hover={{
|
||||
@@ -297,13 +374,13 @@ const ChatInput = ({
|
||||
h={'16px'}
|
||||
color={'myGray.700'}
|
||||
cursor={'pointer'}
|
||||
_hover={{ color: 'primary.500' }}
|
||||
_hover={{ color: 'red.500' }}
|
||||
position={'absolute'}
|
||||
bg={'white'}
|
||||
right={'-8px'}
|
||||
top={'-8px'}
|
||||
onClick={() => {
|
||||
removeFile(index);
|
||||
removeFiles(index);
|
||||
}}
|
||||
className="close-icon"
|
||||
display={['', 'none']}
|
||||
@@ -312,19 +389,27 @@ const ChatInput = ({
|
||||
<Image
|
||||
alt={'img'}
|
||||
src={item.icon}
|
||||
w={['50px', '70px']}
|
||||
h={['50px', '70px']}
|
||||
w={['2rem', '3rem']}
|
||||
h={['2rem', '3rem']}
|
||||
borderRadius={'md'}
|
||||
objectFit={'contain'}
|
||||
/>
|
||||
)}
|
||||
{item.type === ChatFileTypeEnum.file && (
|
||||
<HStack minW={['100px', '150px']} maxW={'250px'} p={2}>
|
||||
<MyIcon name={item.icon as any} w={['1.5rem', '2rem']} h={['1.5rem', '2rem']} />
|
||||
<Box flex={'1 0 0'} className="textEllipsis" fontSize={'xs'}>
|
||||
{item.name}
|
||||
</Box>
|
||||
</HStack>
|
||||
)}
|
||||
</Box>
|
||||
))}
|
||||
</Flex>
|
||||
|
||||
<Flex alignItems={'flex-end'} mt={fileList.length > 0 ? 1 : 0} pl={[2, 4]}>
|
||||
{/* file selector */}
|
||||
{showFileSelector && (
|
||||
{(showSelectFile || showSelectImg) && (
|
||||
<Flex
|
||||
h={'22px'}
|
||||
alignItems={'center'}
|
||||
@@ -336,8 +421,8 @@ const ChatInput = ({
|
||||
onOpenSelectFile();
|
||||
}}
|
||||
>
|
||||
<MyTooltip label={t('common:core.chat.Select Image')}>
|
||||
<MyIcon name={'core/chat/fileSelect'} w={'18px'} color={'myGray.600'} />
|
||||
<MyTooltip label={selectFileTip}>
|
||||
<MyIcon name={selectFileIcon as any} w={'18px'} color={'myGray.600'} />
|
||||
</MyTooltip>
|
||||
<File onSelect={onSelectFile} />
|
||||
</Flex>
|
||||
@@ -404,12 +489,19 @@ const ChatInput = ({
|
||||
}}
|
||||
onPaste={(e) => {
|
||||
const clipboardData = e.clipboardData;
|
||||
if (clipboardData && showFileSelector) {
|
||||
if (clipboardData && (showSelectFile || showSelectImg)) {
|
||||
const items = clipboardData.items;
|
||||
const files = Array.from(items)
|
||||
.map((item) => (item.kind === 'file' ? item.getAsFile() : undefined))
|
||||
.filter(Boolean) as File[];
|
||||
.filter((file) => {
|
||||
console.log(file);
|
||||
return file && fileTypeFilter(file);
|
||||
}) as File[];
|
||||
onSelectFile(files);
|
||||
|
||||
if (files.length > 0) {
|
||||
e.stopPropagation();
|
||||
}
|
||||
}
|
||||
}}
|
||||
/>
|
||||
|
||||
@@ -3,6 +3,7 @@ import { useAudioPlay } from '@/web/common/utils/voice';
|
||||
import { OutLinkChatAuthProps } from '@fastgpt/global/support/permission/chat';
|
||||
import {
|
||||
AppChatConfigType,
|
||||
AppFileSelectConfigType,
|
||||
AppTTSConfigType,
|
||||
AppWhisperConfigType,
|
||||
ChatInputGuideConfigType,
|
||||
@@ -10,6 +11,7 @@ import {
|
||||
} from '@fastgpt/global/core/app/type';
|
||||
import { ChatHistoryItemResType, ChatSiteItemType } from '@fastgpt/global/core/chat/type';
|
||||
import {
|
||||
defaultAppSelectFileConfig,
|
||||
defaultChatInputGuideConfig,
|
||||
defaultTTSConfig,
|
||||
defaultWhisperConfig
|
||||
@@ -64,6 +66,7 @@ type useChatStoreType = OutLinkChatAuthProps &
|
||||
chatInputGuide: ChatInputGuideConfigType;
|
||||
outLinkAuthData: OutLinkChatAuthProps;
|
||||
getHistoryResponseData: ({ dataId }: { dataId: string }) => Promise<ChatHistoryItemResType[]>;
|
||||
fileSelectConfig: AppFileSelectConfigType;
|
||||
};
|
||||
|
||||
export const ChatBoxContext = createContext<useChatStoreType>({
|
||||
@@ -146,7 +149,8 @@ const Provider = ({
|
||||
questionGuide = false,
|
||||
ttsConfig = defaultTTSConfig,
|
||||
whisperConfig = defaultWhisperConfig,
|
||||
chatInputGuide = defaultChatInputGuideConfig
|
||||
chatInputGuide = defaultChatInputGuideConfig,
|
||||
fileSelectConfig = defaultAppSelectFileConfig
|
||||
} = useMemo(() => chatConfig, [chatConfig]);
|
||||
|
||||
const outLinkAuthData = useMemo(
|
||||
@@ -215,6 +219,7 @@ const Provider = ({
|
||||
allVariableList: variables,
|
||||
questionGuide,
|
||||
ttsConfig,
|
||||
fileSelectConfig,
|
||||
whisperConfig,
|
||||
autoTTSResponse,
|
||||
startSegmentedAudio,
|
||||
|
||||
@@ -73,12 +73,11 @@ const ChatItem = ({
|
||||
const ContentCard = useMemo(() => {
|
||||
if (type === 'Human') {
|
||||
const { text, files = [] } = formatChatValue2InputType(chat.value);
|
||||
|
||||
return (
|
||||
<>
|
||||
<Flex flexDirection={'column'} gap={4}>
|
||||
{files.length > 0 && <FilesBlock files={files} />}
|
||||
<Markdown source={text} />
|
||||
</>
|
||||
{text && <Markdown source={text} />}
|
||||
</Flex>
|
||||
);
|
||||
}
|
||||
|
||||
|
||||
@@ -1,22 +1,89 @@
|
||||
import { Box, Flex, Grid } from '@chakra-ui/react';
|
||||
import { Box, Flex, Grid, Text } from '@chakra-ui/react';
|
||||
import MdImage from '@/components/Markdown/img/Image';
|
||||
import { UserInputFileItemType } from '@/components/core/chat/ChatContainer/ChatBox/type';
|
||||
import MyIcon from '@fastgpt/web/components/common/Icon';
|
||||
import React, { useCallback, useLayoutEffect, useMemo, useRef, useState } from 'react';
|
||||
import { clone } from 'lodash';
|
||||
import { ChatFileTypeEnum } from '@fastgpt/global/core/chat/constants';
|
||||
import { useSystem } from '@fastgpt/web/hooks/useSystem';
|
||||
import { useWidthVariable } from '@fastgpt/web/hooks/useWidthVariable';
|
||||
|
||||
const FilesBlock = ({ files }: { files: UserInputFileItemType[] }) => {
|
||||
const chartRef = useRef<HTMLDivElement>(null);
|
||||
const [width, setWidth] = useState(400);
|
||||
const { isPc } = useSystem();
|
||||
const gridColumns = useWidthVariable({
|
||||
width,
|
||||
widthList: [300, 500, 700],
|
||||
list: ['1fr', 'repeat(2, 1fr)', 'repeat(3, 1fr)']
|
||||
});
|
||||
|
||||
// sort files, file->image
|
||||
const sortFiles = useMemo(() => {
|
||||
return clone(files).sort((a, b) => {
|
||||
if (a.type === ChatFileTypeEnum.image && b.type === ChatFileTypeEnum.file) {
|
||||
return 1;
|
||||
} else if (a.type === ChatFileTypeEnum.file && b.type === ChatFileTypeEnum.image) {
|
||||
return -1;
|
||||
}
|
||||
return 0;
|
||||
});
|
||||
}, [files]);
|
||||
|
||||
const computedChatItemWidth = useCallback(() => {
|
||||
if (!chartRef.current) return;
|
||||
|
||||
// 一直找到 parent = markdown 的元素
|
||||
let parent = chartRef.current?.parentElement;
|
||||
while (parent && !parent.className.includes('chat-box-card')) {
|
||||
parent = parent.parentElement;
|
||||
}
|
||||
|
||||
const clientWidth = parent?.clientWidth ?? 400;
|
||||
setWidth(clientWidth);
|
||||
return parent;
|
||||
}, [isPc]);
|
||||
|
||||
useLayoutEffect(() => {
|
||||
computedChatItemWidth();
|
||||
}, [computedChatItemWidth]);
|
||||
|
||||
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 ref={chartRef} gridTemplateColumns={gridColumns} gap={4} alignItems={'flex-start'}>
|
||||
{sortFiles.map(({ id, type, name, url, icon }, i) => (
|
||||
<Box key={i} bg={'white'} borderRadius={'md'} overflow="hidden">
|
||||
{type === 'image' && <MdImage src={url} minH={'100px'} my={0} />}
|
||||
{type === 'file' && (
|
||||
<Flex
|
||||
p={2}
|
||||
w={'100%'}
|
||||
alignItems="center"
|
||||
cursor={'pointer'}
|
||||
onClick={() => {
|
||||
window.open(url);
|
||||
}}
|
||||
>
|
||||
<MyIcon
|
||||
name={icon as any}
|
||||
flexShrink={0}
|
||||
w={['1.5rem', '2rem']}
|
||||
h={['1.5rem', '2rem']}
|
||||
/>
|
||||
<Text
|
||||
ml={2}
|
||||
fontSize={'xs'}
|
||||
overflow="hidden"
|
||||
textOverflow="ellipsis"
|
||||
whiteSpace="nowrap"
|
||||
>
|
||||
{name || url}
|
||||
</Text>
|
||||
</Flex>
|
||||
)}
|
||||
</Box>
|
||||
))}
|
||||
</Grid>
|
||||
);
|
||||
};
|
||||
|
||||
export default FilesBlock;
|
||||
export default React.memo(FilesBlock);
|
||||
|
||||
@@ -75,7 +75,6 @@ type Props = OutLinkChatAuthProps &
|
||||
showVoiceIcon?: boolean;
|
||||
showEmptyIntro?: boolean;
|
||||
userAvatar?: string;
|
||||
showFileSelector?: boolean;
|
||||
active?: boolean; // can use
|
||||
appId: string;
|
||||
|
||||
@@ -105,7 +104,6 @@ const ChatBox = (
|
||||
showEmptyIntro = false,
|
||||
appAvatar,
|
||||
userAvatar,
|
||||
showFileSelector,
|
||||
active = true,
|
||||
appId,
|
||||
chatId,
|
||||
@@ -378,7 +376,9 @@ const ChatBox = (
|
||||
return;
|
||||
}
|
||||
|
||||
// Abort the previous request
|
||||
abortRequest();
|
||||
questionGuideController.current?.abort('stop');
|
||||
|
||||
text = text.trim();
|
||||
|
||||
@@ -390,14 +390,13 @@ const ChatBox = (
|
||||
return;
|
||||
}
|
||||
|
||||
// delete invalid variables, 只保留在 variableList 中的变量
|
||||
// Only declared variables are kept
|
||||
const requestVariables: Record<string, any> = {};
|
||||
allVariableList?.forEach((item) => {
|
||||
requestVariables[item.key] = variables[item.key] || '';
|
||||
});
|
||||
|
||||
const responseChatId = getNanoid(24);
|
||||
questionGuideController.current?.abort('stop');
|
||||
|
||||
// set auto audio playing
|
||||
if (autoTTSResponse) {
|
||||
@@ -980,7 +979,6 @@ const ChatBox = (
|
||||
onStop={() => chatController.current?.abort('stop')}
|
||||
TextareaDom={TextareaDom}
|
||||
resetInputVal={resetInputVal}
|
||||
showFileSelector={showFileSelector}
|
||||
chatForm={chatForm}
|
||||
appId={appId}
|
||||
/>
|
||||
|
||||
@@ -13,6 +13,7 @@ export type UserInputFileItemType = {
|
||||
type: `${ChatFileTypeEnum}`;
|
||||
name: string;
|
||||
icon: string; // img is base64
|
||||
status: 0 | 1; // 0: uploading, 1: success
|
||||
url?: string;
|
||||
};
|
||||
|
||||
|
||||
@@ -1,6 +1,7 @@
|
||||
import { ChatItemValueItemType } from '@fastgpt/global/core/chat/type';
|
||||
import { ChatBoxInputType, UserInputFileItemType } from './type';
|
||||
import { getNanoid } from '@fastgpt/global/common/string/tools';
|
||||
import { getFileIcon } from '@fastgpt/global/common/file/icon';
|
||||
|
||||
export const formatChatValue2InputType = (value?: ChatItemValueItemType[]): ChatBoxInputType => {
|
||||
if (!value) {
|
||||
@@ -15,15 +16,16 @@ export const formatChatValue2InputType = (value?: ChatItemValueItemType[]): Chat
|
||||
.filter((item) => item.text?.content)
|
||||
.map((item) => item.text?.content || '')
|
||||
.join('');
|
||||
|
||||
const files =
|
||||
(value
|
||||
.map((item) =>
|
||||
?.map((item) =>
|
||||
item.type === 'file' && item.file
|
||||
? {
|
||||
id: getNanoid(),
|
||||
id: item.file.url,
|
||||
type: item.file.type,
|
||||
name: item.file.name,
|
||||
icon: '',
|
||||
icon: getFileIcon(item.file.name),
|
||||
url: item.file.url
|
||||
}
|
||||
: undefined
|
||||
|
||||
@@ -105,19 +105,19 @@ ${JSON.stringify(questionGuides)}`;
|
||||
overflowY={'auto'}
|
||||
>
|
||||
{toolParams && toolParams !== '{}' && (
|
||||
<Markdown
|
||||
source={`~~~json#Input
|
||||
${toolParams}`}
|
||||
/>
|
||||
)}
|
||||
{toolResponse && (
|
||||
<Box mt={3}>
|
||||
<Box mb={3}>
|
||||
<Markdown
|
||||
source={`~~~json#Response
|
||||
${toolResponse}`}
|
||||
source={`~~~json#Input
|
||||
${toolParams}`}
|
||||
/>
|
||||
</Box>
|
||||
)}
|
||||
{toolResponse && (
|
||||
<Markdown
|
||||
source={`~~~json#Response
|
||||
${toolResponse}`}
|
||||
/>
|
||||
)}
|
||||
</AccordionPanel>
|
||||
</AccordionItem>
|
||||
</Accordion>
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
import React, { useMemo, useState } from 'react';
|
||||
import { Box, Flex, BoxProps, useDisclosure } from '@chakra-ui/react';
|
||||
import { Box, Flex, BoxProps, useDisclosure, HStack } from '@chakra-ui/react';
|
||||
import type { ChatHistoryItemResType } from '@fastgpt/global/core/chat/type.d';
|
||||
import { useTranslation } from 'next-i18next';
|
||||
import { moduleTemplatesFlat } from '@fastgpt/global/core/workflow/template/constants';
|
||||
@@ -16,6 +16,7 @@ import MyIcon from '@fastgpt/web/components/common/Icon';
|
||||
import { useContextSelector } from 'use-context-selector';
|
||||
import { ChatBoxContext } from '../ChatContainer/ChatBox/Provider';
|
||||
import { useRequest2 } from '@fastgpt/web/hooks/useRequest';
|
||||
import { getFileIcon } from '@fastgpt/global/common/file/icon';
|
||||
|
||||
type sideTabItemType = {
|
||||
moduleLogo?: string;
|
||||
@@ -34,7 +35,7 @@ function RowRender({
|
||||
}: { children: React.ReactNode; label: string } & BoxProps) {
|
||||
return (
|
||||
<Box mb={3}>
|
||||
<Box fontSize={'sm'} mb={mb} flex={'0 0 90px'}>
|
||||
<Box fontSize={'sm'} mb={mb} color={'myGray.800'} flex={'0 0 90px'}>
|
||||
{label}:
|
||||
</Box>
|
||||
<Box borderRadius={'sm'} fontSize={['xs', 'sm']} bg={'myGray.50'} {...props}>
|
||||
@@ -435,9 +436,50 @@ export const WholeResponseContent = ({
|
||||
value={activeModule?.textOutput}
|
||||
/>
|
||||
{/* code */}
|
||||
<Row label={workflowT('response.Custom outputs')} value={activeModule?.customOutputs} />
|
||||
<Row label={workflowT('response.Custom inputs')} value={activeModule?.customInputs} />
|
||||
<Row label={workflowT('response.Code log')} value={activeModule?.codeLog} />
|
||||
<>
|
||||
<Row
|
||||
label={t('workflow:response.Custom outputs')}
|
||||
value={activeModule?.customOutputs}
|
||||
/>
|
||||
<Row label={t('workflow:response.Custom inputs')} value={activeModule?.customInputs} />
|
||||
<Row label={t('workflow:response.Code log')} value={activeModule?.codeLog} />
|
||||
</>
|
||||
|
||||
{/* read files */}
|
||||
<>
|
||||
{activeModule?.readFiles && activeModule?.readFiles.length > 0 && (
|
||||
<Row
|
||||
label={t('workflow:response.read files')}
|
||||
rawDom={
|
||||
<Flex flexWrap={'wrap'} gap={3} px={4} py={2}>
|
||||
{activeModule?.readFiles.map((file, i) => (
|
||||
<HStack
|
||||
key={i}
|
||||
bg={'white'}
|
||||
boxShadow={'base'}
|
||||
borderRadius={'sm'}
|
||||
py={1}
|
||||
px={2}
|
||||
{...(file.url
|
||||
? {
|
||||
cursor: 'pointer',
|
||||
onClick: () => window.open(file.url)
|
||||
}
|
||||
: {})}
|
||||
>
|
||||
<MyIcon name={getFileIcon(file.name) as any} w={'1rem'} />
|
||||
<Box>{file.name}</Box>
|
||||
</HStack>
|
||||
))}
|
||||
</Flex>
|
||||
}
|
||||
/>
|
||||
)}
|
||||
<Row
|
||||
label={t('workflow:response.Read file result')}
|
||||
value={activeModule?.readFilesResult}
|
||||
/>
|
||||
</>
|
||||
</Box>
|
||||
)}
|
||||
</>
|
||||
|
||||
Reference in New Issue
Block a user