4.8.9 test fix (#2291)

* perf: read file icon

* perf:icon

* fix: i18n

* perf: hide pro api

* perf: upload expired time

* perf: upload file frequency limit

* perf: upload file ux

* perf: input file tip

* perf: qa custom chunk size

* feat: dataset openapi

* fix: auth dataset list

* fix: openapi doc

* perf: zero temperature change to 0.01

* perf: read file prompt

* perf: read file prompt

* perf: free plan tip

* feat: cron job usage
This commit is contained in:
Archer
2024-08-08 10:07:24 +08:00
committed by GitHub
parent 7b388b287a
commit 3ba9c21828
42 changed files with 822 additions and 813 deletions

Binary file not shown.

After

Width:  |  Height:  |  Size: 31 KiB

File diff suppressed because one or more lines are too long

Before

Width:  |  Height:  |  Size: 1.7 MiB

View File

@@ -0,0 +1,4 @@
<svg viewBox="0 0 36 36" fill="none" xmlns="http://www.w3.org/2000/svg">
<rect width="36" height="36" rx="6" fill="#F0F4FF"/>
<path fill-rule="evenodd" clip-rule="evenodd" d="M17.9998 9.83496C13.4905 9.83496 9.83496 13.4905 9.83496 17.9998C9.83496 22.5091 13.4905 26.1646 17.9998 26.1646C22.5091 26.1646 26.1646 22.5091 26.1646 17.9998C26.1646 13.4905 22.5091 9.83496 17.9998 9.83496ZM7.83496 17.9998C7.83496 12.3859 12.3859 7.83496 17.9998 7.83496C23.6136 7.83496 28.1646 12.3859 28.1646 17.9998C28.1646 23.6136 23.6136 28.1646 17.9998 28.1646C12.3859 28.1646 7.83496 23.6136 7.83496 17.9998ZM18.2222 14.4384C17.815 14.3686 17.3962 14.4451 17.04 14.6545C16.6838 14.8638 16.4132 15.1924 16.2761 15.5822C16.0929 16.1032 15.522 16.3769 15.001 16.1937C14.48 16.0104 14.2062 15.4395 14.3895 14.9185C14.6833 14.0832 15.2633 13.3788 16.0267 12.9302C16.7901 12.4815 17.6876 12.3175 18.5603 12.4672C19.433 12.6169 20.2246 13.0707 20.7949 13.7481C21.365 14.4253 21.6771 15.2825 21.6759 16.1677C21.6754 17.6145 20.6035 18.5625 19.8559 19.0609C19.4516 19.3305 19.0544 19.5284 18.7622 19.6582C18.6147 19.7238 18.4904 19.7736 18.4006 19.8078C18.3556 19.8249 18.3191 19.8382 18.2923 19.8477L18.2597 19.8591L18.2492 19.8627L18.2454 19.864L18.2439 19.8645C18.2436 19.8646 18.2427 19.8649 17.9264 18.9162L18.2427 19.8649C17.7187 20.0396 17.1524 19.7564 16.9778 19.2325C16.8032 18.7087 17.0861 18.1426 17.6097 17.9677L17.6223 17.9633C17.6359 17.9585 17.6584 17.9503 17.6886 17.9388C17.7492 17.9157 17.8397 17.8796 17.9499 17.8306C18.1733 17.7313 18.4634 17.5855 18.7465 17.3968C19.3734 16.9789 19.6759 16.5524 19.6759 16.1668L19.6759 16.1653C19.6765 15.7522 19.5309 15.3521 19.2648 15.0361C18.9987 14.72 18.6294 14.5083 18.2222 14.4384ZM16.9998 22.5822C16.9998 22.0299 17.4475 21.5822 17.9998 21.5822H18.0089C18.5612 21.5822 19.0089 22.0299 19.0089 22.5822C19.0089 23.1344 18.5612 23.5822 18.0089 23.5822H17.9998C17.4475 23.5822 16.9998 23.1344 16.9998 22.5822Z" fill="#3370FF"/>
</svg>

After

Width:  |  Height:  |  Size: 1.9 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 37 KiB

View File

@@ -20,6 +20,8 @@ import { defaultAppSelectFileConfig } from '@fastgpt/global/core/app/constants';
import ChatFunctionTip from './Tip';
import FormLabel from '@fastgpt/web/components/common/MyBox/FormLabel';
import { useMount } from 'ahooks';
import { useSystemStore } from '@/web/common/system/useSystemStore';
import QuestionTip from '@fastgpt/web/components/common/MyTooltip/QuestionTip';
const FileSelect = ({
forbidVision = false,
@@ -31,7 +33,9 @@ const FileSelect = ({
onChange: (e: AppFileSelectConfigType) => void;
}) => {
const { t } = useTranslation();
const { feConfigs } = useSystemStore();
const { isOpen, onOpen, onClose } = useDisclosure();
const maxSelectFiles = Math.min(feConfigs?.uploadFileMaxAmount ?? 20, 30);
const formLabel = useMemo(
() =>
@@ -106,22 +110,27 @@ const FileSelect = ({
)}
</HStack>
{!forbidVision && (
<Box mt={2} color={'myGray.500'} fontSize={'xs'}>
{t('app:image_upload_tip')}
</Box>
<Flex mt={2} color={'myGray.500'}>
<Box fontSize={'xs'}>{t('app:image_upload_tip')}</Box>
<ChatFunctionTip type="visionModel" />
</Flex>
)}
<Box mt={6}>
<FormLabel>{t('app:upload_file_max_amount')}</FormLabel>
<HStack spacing={1}>
<FormLabel>{t('app:upload_file_max_amount')}</FormLabel>
<QuestionTip label={t('app:upload_file_max_amount_tip')} />
</HStack>
<Box mt={5}>
<MySlider
markList={[
{ label: '1', value: 1 },
{ label: '20', value: 20 }
{ label: `${maxSelectFiles}`, value: maxSelectFiles }
]}
width={'100%'}
min={1}
max={20}
max={maxSelectFiles}
step={1}
value={value.maxFiles ?? 5}
onChange={(e) => {

View File

@@ -97,12 +97,8 @@ const TTSSelect = ({
</Button>
</MyTooltip>
<MyModal
title={
<>
<MyIcon name={'core/app/simpleMode/tts'} mr={2} w={'20px'} />
{t('common:core.app.TTS')}
</>
}
iconSrc="core/app/simpleMode/tts"
title={t('common:core.app.TTS')}
isOpen={isOpen}
onClose={onCloseTTSModal}
w={'500px'}

View File

@@ -10,7 +10,8 @@ enum FnTypeEnum {
tts = 'tts',
variable = 'variable',
welcome = 'welcome',
file = 'file'
file = 'file',
visionModel = 'visionModel'
}
const ChatFunctionTip = ({ type }: { type: `${FnTypeEnum}` }) => {
@@ -52,7 +53,13 @@ const ChatFunctionTip = ({ type }: { type: `${FnTypeEnum}` }) => {
icon: '/imgs/app/welcome-icon.svg',
title: t('app:file_upload'),
desc: t('app:file_upload_tip'),
imgUrl: '/imgs/app/fileUploadPlaceholder.svg'
imgUrl: '/imgs/app/fileUploadPlaceholder.png'
},
[FnTypeEnum.visionModel]: {
icon: '/imgs/app/question.svg',
title: t('app:vision_model_title'),
desc: t('app:llm_use_vision_tip'),
imgUrl: '/imgs/app/visionModel.png'
}
});
const data = map.current[type];
@@ -62,8 +69,8 @@ const ChatFunctionTip = ({ type }: { type: `${FnTypeEnum}` }) => {
maxW={'420px'}
ml={1}
label={
<Box>
<Flex>
<Box pt={2}>
<Flex alignItems={'flex-start'}>
<Image src={data.icon} w={'36px'} alt={''} />
<Box ml={3}>
<Box fontWeight="bold">{data.title}</Box>

View File

@@ -1,6 +1,15 @@
import { useSpeech } from '@/web/common/hooks/useSpeech';
import { useSystemStore } from '@/web/common/system/useSystemStore';
import { Box, Flex, HStack, Image, Spinner, Textarea } from '@chakra-ui/react';
import {
Box,
CircularProgress,
CircularProgressLabel,
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';
@@ -22,6 +31,8 @@ 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';
import MyBox from '@fastgpt/web/components/common/MyBox';
import { getErrText } from '@fastgpt/global/common/error/utils';
const InputGuideBox = dynamic(() => import('./InputGuideBox'));
@@ -103,6 +114,7 @@ const ChatInput = ({
multiple: true,
maxCount: maxSelectFiles
});
// Upload files
useRequest2(
async () => {
const filterFiles = fileList.filter((item) => item.status === 0);
@@ -110,30 +122,48 @@ const ChatInput = ({
if (filterFiles.length === 0) return;
replaceFiles(fileList.map((item) => ({ ...item, status: 1 })));
let errorFileIndex: number[] = [];
for (const file of filterFiles) {
if (!file.rawFile) continue;
await Promise.allSettled(
filterFiles.map(async (file) => {
const copyFile = clone(file);
copyFile.status = 1;
if (!copyFile.rawFile) return;
try {
const { fileId, previewUrl } = await uploadFile2DB({
file: file.rawFile,
bucketName: 'chat',
metadata: {
chatId
}
});
try {
const fileIndex = fileList.findIndex((item) => item.id === file.id)!;
updateFiles(fileList.findIndex((item) => item.id === file.id)!, {
...file,
status: 1,
url: `${location.origin}${previewUrl}`
});
} catch (error) {
removeFiles(fileList.findIndex((item) => item.id === file.id)!);
console.log(error);
return Promise.reject(error);
}
}
// Start upload and update process
const { previewUrl } = await uploadFile2DB({
file: copyFile.rawFile,
bucketName: 'chat',
metadata: {
chatId
},
percentListen(e) {
copyFile.process = e;
if (!copyFile.url) {
updateFiles(fileIndex, copyFile);
}
}
});
// Update file url
copyFile.url = `${location.origin}${previewUrl}`;
updateFiles(fileIndex, copyFile);
} catch (error) {
errorFileIndex.push(fileList.findIndex((item) => item.id === file.id)!);
toast({
status: 'warning',
title: t(
getErrText(error, t('common:error.upload_file_error_filename', { name: file.name }))
)
});
}
})
);
removeFiles(errorFileIndex);
},
{
manual: false,
@@ -211,20 +241,23 @@ const ChatInput = ({
);
replaceFiles(concatFileList);
},
[fileList, maxSelectFiles, replaceFiles, toast, t]
[fileList, maxSelectFiles, replaceFiles, toast, t, maxSize]
);
/* on send */
const handleSend = async (val?: string) => {
if (!canSendMessage) return;
const textareaValue = val || TextareaDom.current?.value || '';
const handleSend = useCallback(
async (val?: string) => {
if (!canSendMessage) return;
const textareaValue = val || TextareaDom.current?.value || '';
onSendMessage({
text: textareaValue.trim(),
files: fileList
});
replaceFiles([]);
};
onSendMessage({
text: textareaValue.trim(),
files: fileList
});
replaceFiles([]);
},
[TextareaDom, canSendMessage, fileList, onSendMessage, replaceFiles]
);
/* whisper init */
const { whisperModel } = useSystemStore();
@@ -278,10 +311,360 @@ const ChatInput = ({
startSpeak(finishWhisperTranscription);
}, [finishWhisperTranscription, isSpeaking, startSpeak, stopSpeak]);
const RenderTranslateLoading = useMemo(
() => (
<Flex
position={'absolute'}
top={0}
bottom={0}
left={0}
right={0}
zIndex={10}
pl={5}
alignItems={'center'}
bg={'white'}
color={'primary.500'}
visibility={isSpeaking && isTransCription ? 'visible' : 'hidden'}
>
<Spinner size={'sm'} mr={4} />
{t('common:core.chat.Converting to text')}
</Flex>
),
[isSpeaking, isTransCription, t]
);
const RenderFilePreview = useMemo(
() =>
fileList.length > 0 ? (
<Flex
maxH={'250px'}
overflowY={'auto'}
wrap={'wrap'}
px={[2, 4]}
pt={3}
userSelect={'none'}
gap={2}
mb={fileList.length > 0 ? 2 : 0}
>
{fileList.map((item, index) => (
<MyBox
key={index}
border={'sm'}
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={{
'.close-icon': { display: 'block' }
}}
>
<MyIcon
name={'closeSolid'}
w={'16px'}
h={'16px'}
color={'myGray.700'}
cursor={'pointer'}
_hover={{ color: 'red.500' }}
position={'absolute'}
bg={'white'}
right={'-8px'}
top={'-8px'}
onClick={() => removeFiles(index)}
className="close-icon"
display={['', 'none']}
zIndex={10}
/>
{item.type === ChatFileTypeEnum.image && (
<Image
alt={'img'}
src={item.icon}
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>
)}
{/* Process */}
{!item.url && (
<Flex
position={'absolute'}
inset="0"
bg="rgba(255,255,255,0.4)"
alignItems="center"
justifyContent="center"
>
<CircularProgress
value={item.process}
color="primary.600"
bg={'white'}
size={isPc ? '30px' : '35px'}
>
{/* <CircularProgressLabel>{item.process ?? 0}%</CircularProgressLabel> */}
</CircularProgress>
</Flex>
)}
</MyBox>
))}
</Flex>
) : null,
[fileList, isPc, removeFiles]
);
const RenderTextarea = useMemo(
() => (
<Flex alignItems={'flex-end'} mt={fileList.length > 0 ? 1 : 0} pl={[2, 4]}>
{/* file selector */}
{(showSelectFile || showSelectImg) && (
<Flex
h={'22px'}
alignItems={'center'}
justifyContent={'center'}
cursor={'pointer'}
transform={'translateY(1px)'}
onClick={() => {
if (isSpeaking) return;
onOpenSelectFile();
}}
>
<MyTooltip label={selectFileTip}>
<MyIcon name={selectFileIcon as any} w={'18px'} color={'myGray.600'} />
</MyTooltip>
<File onSelect={onSelectFile} />
</Flex>
)}
{/* input area */}
<Textarea
ref={TextareaDom}
py={0}
pl={2}
pr={['30px', '48px']}
border={'none'}
_focusVisible={{
border: 'none'
}}
placeholder={
isSpeaking ? t('common:core.chat.Speaking') : t('common:core.chat.Type a message')
}
resize={'none'}
rows={1}
height={'22px'}
lineHeight={'22px'}
maxHeight={'50vh'}
maxLength={-1}
overflowY={'auto'}
whiteSpace={'pre-wrap'}
wordBreak={'break-all'}
boxShadow={'none !important'}
color={'myGray.900'}
isDisabled={isSpeaking}
value={inputValue}
fontSize={['md', 'sm']}
onChange={(e) => {
const textarea = e.target;
textarea.style.height = textareaMinH;
textarea.style.height = `${textarea.scrollHeight}px`;
setValue('input', textarea.value);
}}
onKeyDown={(e) => {
// enter send.(pc or iframe && enter and unPress shift)
const isEnter = e.keyCode === 13;
if (isEnter && TextareaDom.current && (e.ctrlKey || e.altKey)) {
// Add a new line
const index = TextareaDom.current.selectionStart;
const val = TextareaDom.current.value;
TextareaDom.current.value = `${val.slice(0, index)}\n${val.slice(index)}`;
TextareaDom.current.selectionStart = index + 1;
TextareaDom.current.selectionEnd = index + 1;
TextareaDom.current.style.height = textareaMinH;
TextareaDom.current.style.height = `${TextareaDom.current.scrollHeight}px`;
return;
}
// 全选内容
// @ts-ignore
e.key === 'a' && e.ctrlKey && e.target?.select();
if ((isPc || window !== parent) && e.keyCode === 13 && !e.shiftKey) {
handleSend();
e.preventDefault();
}
}}
onPaste={(e) => {
const clipboardData = e.clipboardData;
if (clipboardData && (showSelectFile || showSelectImg)) {
const items = clipboardData.items;
const files = Array.from(items)
.map((item) => (item.kind === 'file' ? item.getAsFile() : undefined))
.filter((file) => {
console.log(file);
return file && fileTypeFilter(file);
}) as File[];
onSelectFile(files);
if (files.length > 0) {
e.stopPropagation();
}
}
}}
/>
<Flex alignItems={'center'} position={'absolute'} right={[2, 4]} bottom={['10px', '12px']}>
{/* voice-input */}
{whisperConfig.open && !havInput && !isChatting && !!whisperModel && (
<>
<canvas
ref={canvasRef}
style={{
height: '30px',
width: isSpeaking && !isTransCription ? '100px' : 0,
background: 'white',
zIndex: 0
}}
/>
{isSpeaking && (
<MyTooltip label={t('common:core.chat.Cancel Speak')}>
<Flex
mr={2}
alignItems={'center'}
justifyContent={'center'}
flexShrink={0}
h={['26px', '32px']}
w={['26px', '32px']}
borderRadius={'md'}
cursor={'pointer'}
_hover={{ bg: '#F5F5F8' }}
onClick={() => stopSpeak(true)}
>
<MyIcon
name={'core/chat/cancelSpeak'}
width={['20px', '22px']}
height={['20px', '22px']}
/>
</Flex>
</MyTooltip>
)}
<MyTooltip
label={
isSpeaking ? t('common:core.chat.Finish Speak') : t('common:core.chat.Record')
}
>
<Flex
mr={2}
alignItems={'center'}
justifyContent={'center'}
flexShrink={0}
h={['26px', '32px']}
w={['26px', '32px']}
borderRadius={'md'}
cursor={'pointer'}
_hover={{ bg: '#F5F5F8' }}
onClick={onWhisperRecord}
>
<MyIcon
name={isSpeaking ? 'core/chat/finishSpeak' : 'core/chat/recordFill'}
width={['20px', '22px']}
height={['20px', '22px']}
color={isSpeaking ? 'primary.500' : 'myGray.600'}
/>
</Flex>
</MyTooltip>
</>
)}
{/* send and stop icon */}
{isSpeaking ? (
<Box color={'#5A646E'} w={'36px'} textAlign={'right'} whiteSpace={'nowrap'}>
{speakingTimeString}
</Box>
) : (
<Flex
alignItems={'center'}
justifyContent={'center'}
flexShrink={0}
h={['28px', '32px']}
w={['28px', '32px']}
borderRadius={'md'}
bg={
isSpeaking || isChatting
? ''
: !havInput || hasFileUploading
? '#E5E5E5'
: 'primary.500'
}
cursor={havInput ? 'pointer' : 'not-allowed'}
lineHeight={1}
onClick={() => {
if (isChatting) {
return onStop();
}
return handleSend();
}}
>
{isChatting ? (
<MyIcon
animation={'zoomStopIcon 0.4s infinite alternate'}
width={['22px', '25px']}
height={['22px', '25px']}
cursor={'pointer'}
name={'stop'}
color={'gray.500'}
/>
) : (
<MyTooltip label={t('common:core.chat.Send Message')}>
<MyIcon
name={'core/chat/sendFill'}
width={['18px', '20px']}
height={['18px', '20px']}
color={'white'}
/>
</MyTooltip>
)}
</Flex>
)}
</Flex>
</Flex>
),
[
File,
TextareaDom,
fileList.length,
handleSend,
hasFileUploading,
havInput,
inputValue,
isChatting,
isPc,
isSpeaking,
isTransCription,
onOpenSelectFile,
onSelectFile,
onStop,
onWhisperRecord,
selectFileIcon,
selectFileTip,
setValue,
showSelectFile,
showSelectImg,
speakingTimeString,
stopSpeak,
t,
whisperConfig.open,
whisperModel
]
);
return (
<Box m={['0 auto', '10px auto']} w={'100%'} maxW={['auto', 'min(800px, 100%)']} px={[0, 5]}>
<Box
pt={fileList.length > 0 ? '10px' : ['14px', '18px']}
pt={fileList.length > 0 ? '0' : ['14px', '18px']}
pb={['14px', '18px']}
position={'relative'}
boxShadow={isSpeaking ? `0 0 10px rgba(54,111,255,0.4)` : `0 0 10px rgba(0,0,0,0.2)`}
@@ -313,317 +696,12 @@ const ChatInput = ({
)}
{/* translate loading */}
<Flex
position={'absolute'}
top={0}
bottom={0}
left={0}
right={0}
zIndex={10}
pl={5}
alignItems={'center'}
bg={'white'}
color={'primary.500'}
visibility={isSpeaking && isTransCription ? 'visible' : 'hidden'}
>
<Spinner size={'sm'} mr={4} />
{t('common:core.chat.Converting to text')}
</Flex>
{RenderTranslateLoading}
{/* file preview */}
<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 #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={{
'.close-icon': { display: item.url ? 'block' : 'none' }
}}
>
{/* uploading */}
{!item.url && (
<Flex
position={'absolute'}
alignItems={'center'}
justifyContent={'center'}
rounded={'md'}
color={'primary.500'}
top={0}
left={0}
bottom={0}
right={0}
bg={'rgba(255,255,255,0.8)'}
>
<Spinner />
</Flex>
)}
<MyIcon
name={'closeSolid'}
w={'16px'}
h={'16px'}
color={'myGray.700'}
cursor={'pointer'}
_hover={{ color: 'red.500' }}
position={'absolute'}
bg={'white'}
right={'-8px'}
top={'-8px'}
onClick={() => {
removeFiles(index);
}}
className="close-icon"
display={['', 'none']}
/>
{item.type === ChatFileTypeEnum.image && (
<Image
alt={'img'}
src={item.icon}
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>
{RenderFilePreview}
<Flex alignItems={'flex-end'} mt={fileList.length > 0 ? 1 : 0} pl={[2, 4]}>
{/* file selector */}
{(showSelectFile || showSelectImg) && (
<Flex
h={'22px'}
alignItems={'center'}
justifyContent={'center'}
cursor={'pointer'}
transform={'translateY(1px)'}
onClick={() => {
if (isSpeaking) return;
onOpenSelectFile();
}}
>
<MyTooltip label={selectFileTip}>
<MyIcon name={selectFileIcon as any} w={'18px'} color={'myGray.600'} />
</MyTooltip>
<File onSelect={onSelectFile} />
</Flex>
)}
{/* input area */}
<Textarea
ref={TextareaDom}
py={0}
pl={2}
pr={['30px', '48px']}
border={'none'}
_focusVisible={{
border: 'none'
}}
placeholder={
isSpeaking ? t('common:core.chat.Speaking') : t('common:core.chat.Type a message')
}
resize={'none'}
rows={1}
height={'22px'}
lineHeight={'22px'}
maxHeight={'50vh'}
maxLength={-1}
overflowY={'auto'}
whiteSpace={'pre-wrap'}
wordBreak={'break-all'}
boxShadow={'none !important'}
color={'myGray.900'}
isDisabled={isSpeaking}
value={inputValue}
fontSize={['md', 'sm']}
onChange={(e) => {
const textarea = e.target;
textarea.style.height = textareaMinH;
textarea.style.height = `${textarea.scrollHeight}px`;
setValue('input', textarea.value);
}}
onKeyDown={(e) => {
// enter send.(pc or iframe && enter and unPress shift)
const isEnter = e.keyCode === 13;
if (isEnter && TextareaDom.current && (e.ctrlKey || e.altKey)) {
// Add a new line
const index = TextareaDom.current.selectionStart;
const val = TextareaDom.current.value;
TextareaDom.current.value = `${val.slice(0, index)}\n${val.slice(index)}`;
TextareaDom.current.selectionStart = index + 1;
TextareaDom.current.selectionEnd = index + 1;
TextareaDom.current.style.height = textareaMinH;
TextareaDom.current.style.height = `${TextareaDom.current.scrollHeight}px`;
return;
}
// 全选内容
// @ts-ignore
e.key === 'a' && e.ctrlKey && e.target?.select();
if ((isPc || window !== parent) && e.keyCode === 13 && !e.shiftKey) {
handleSend();
e.preventDefault();
}
}}
onPaste={(e) => {
const clipboardData = e.clipboardData;
if (clipboardData && (showSelectFile || showSelectImg)) {
const items = clipboardData.items;
const files = Array.from(items)
.map((item) => (item.kind === 'file' ? item.getAsFile() : undefined))
.filter((file) => {
console.log(file);
return file && fileTypeFilter(file);
}) as File[];
onSelectFile(files);
if (files.length > 0) {
e.stopPropagation();
}
}
}}
/>
<Flex
alignItems={'center'}
position={'absolute'}
right={[2, 4]}
bottom={['10px', '12px']}
>
{/* voice-input */}
{whisperConfig.open && !havInput && !isChatting && !!whisperModel && (
<>
<canvas
ref={canvasRef}
style={{
height: '30px',
width: isSpeaking && !isTransCription ? '100px' : 0,
background: 'white',
zIndex: 0
}}
/>
{isSpeaking && (
<MyTooltip label={t('common:core.chat.Cancel Speak')}>
<Flex
mr={2}
alignItems={'center'}
justifyContent={'center'}
flexShrink={0}
h={['26px', '32px']}
w={['26px', '32px']}
borderRadius={'md'}
cursor={'pointer'}
_hover={{ bg: '#F5F5F8' }}
onClick={() => stopSpeak(true)}
>
<MyIcon
name={'core/chat/cancelSpeak'}
width={['20px', '22px']}
height={['20px', '22px']}
/>
</Flex>
</MyTooltip>
)}
<MyTooltip
label={
isSpeaking ? t('common:core.chat.Finish Speak') : t('common:core.chat.Record')
}
>
<Flex
mr={2}
alignItems={'center'}
justifyContent={'center'}
flexShrink={0}
h={['26px', '32px']}
w={['26px', '32px']}
borderRadius={'md'}
cursor={'pointer'}
_hover={{ bg: '#F5F5F8' }}
onClick={onWhisperRecord}
>
<MyIcon
name={isSpeaking ? 'core/chat/finishSpeak' : 'core/chat/recordFill'}
width={['20px', '22px']}
height={['20px', '22px']}
color={isSpeaking ? 'primary.500' : 'myGray.600'}
/>
</Flex>
</MyTooltip>
</>
)}
{/* send and stop icon */}
{isSpeaking ? (
<Box color={'#5A646E'} w={'36px'} textAlign={'right'} whiteSpace={'nowrap'}>
{speakingTimeString}
</Box>
) : (
<Flex
alignItems={'center'}
justifyContent={'center'}
flexShrink={0}
h={['28px', '32px']}
w={['28px', '32px']}
borderRadius={'md'}
bg={
isSpeaking || isChatting
? ''
: !havInput || hasFileUploading
? '#E5E5E5'
: 'primary.500'
}
cursor={havInput ? 'pointer' : 'not-allowed'}
lineHeight={1}
onClick={() => {
if (isChatting) {
return onStop();
}
return handleSend();
}}
>
{isChatting ? (
<MyIcon
animation={'zoomStopIcon 0.4s infinite alternate'}
width={['22px', '25px']}
height={['22px', '25px']}
cursor={'pointer'}
name={'stop'}
color={'gray.500'}
/>
) : (
<MyTooltip label={t('common:core.chat.Send Message')}>
<MyIcon
name={'core/chat/sendFill'}
width={['18px', '20px']}
height={['18px', '20px']}
color={'white'}
/>
</MyTooltip>
)}
</Flex>
)}
</Flex>
</Flex>
{RenderTextarea}
</Box>
</Box>
);

View File

@@ -15,6 +15,7 @@ export type UserInputFileItemType = {
icon: string; // img is base64
status: 0 | 1; // 0: uploading, 1: success
url?: string;
process?: number;
};
export type ChatBoxInputFormType = {

View File

@@ -430,10 +430,6 @@ const PlanUsage = () => {
{isFreeTeam ? (
<>
<Flex mt="2" color={'#485264'} fontSize="sm">
<Box>{t('common:support.wallet.Plan reset time')}:</Box>
<Box ml={2}>{formatTime2YMD(standardPlan?.expiredTime)}</Box>
</Flex>
<Box mt="2" color={'#485264'} fontSize="sm">
{t('common:info.free_plan')}
</Box>

View File

@@ -7,8 +7,21 @@ import { removeFilesByPaths } from '@fastgpt/service/common/file/utils';
import { NextAPI } from '@/service/middleware/entry';
import { createFileToken } from '@fastgpt/service/support/permission/controller';
import { ReadFileBaseUrl } from '@fastgpt/global/common/file/constants';
import { addLog } from '@fastgpt/service/common/system/log';
import { authFrequencyLimit } from '@/service/common/frequencyLimit/api';
import { addSeconds } from 'date-fns';
const authUploadLimit = (tmbId: string) => {
if (!global.feConfigs.uploadFileMaxAmount) return;
return authFrequencyLimit({
eventId: `${tmbId}-uploadfile`,
maxAmount: global.feConfigs.uploadFileMaxAmount * 2,
expiredTime: addSeconds(new Date(), 30) // 30s
});
};
async function handler(req: NextApiRequest, res: NextApiResponse<any>) {
const start = Date.now();
/* Creates the multer uploader */
const upload = getUploadModel({
maxSize: (global.feConfigs?.uploadFileMaxSize || 500) * 1024 * 1024
@@ -16,9 +29,13 @@ async function handler(req: NextApiRequest, res: NextApiResponse<any>) {
const filePaths: string[] = [];
try {
const { teamId, tmbId } = await authCert({ req, authToken: true });
await authUploadLimit(tmbId);
const { file, bucketName, metadata } = await upload.doUpload(req, res);
const { teamId, tmbId } = await authCert({ req, authToken: true });
addLog.info(`Upload file success ${file.originalname}, cost ${Date.now() - start}ms`);
if (!bucketName) {
throw new Error('bucketName is empty');
@@ -34,22 +51,15 @@ async function handler(req: NextApiRequest, res: NextApiResponse<any>) {
metadata: metadata
});
jsonRes<{
fileId: string;
previewUrl: string;
}>(res, {
data: {
fileId,
previewUrl: `${ReadFileBaseUrl}?filename=${file.originalname}&token=${await createFileToken(
{
bucketName,
teamId,
tmbId,
fileId
}
)}`
}
});
return {
fileId,
previewUrl: `${ReadFileBaseUrl}?filename=${file.originalname}&token=${await createFileToken({
bucketName,
teamId,
tmbId,
fileId
})}`
};
} catch (error) {
jsonRes(res, {
code: 500,

View File

@@ -36,6 +36,7 @@ async function handler(req: ApiRequestProps<GetDatasetListBody>) {
return await authDataset({
req,
authToken: true,
authApiKey: true,
per: ReadPermissionVal,
datasetId: parentId
});

View File

@@ -37,11 +37,12 @@ type DatasetImportContextType = {
setSources: React.Dispatch<React.SetStateAction<ImportSourceItemType[]>>;
} & TrainingFiledType;
type ChunkSizeFieldType = 'embeddingChunkSize';
type ChunkSizeFieldType = 'embeddingChunkSize' | 'qaChunkSize';
export type ImportFormType = {
mode: TrainingModeEnum;
way: ImportProcessWayEnum;
embeddingChunkSize: number;
qaChunkSize: number;
customSplitChar: string;
qaPrompt: string;
webSelector: string;
@@ -147,7 +148,6 @@ const DatasetImportContextProvider = ({ children }: { children: React.ReactNode
steps
});
// -----
const vectorModel = datasetDetail.vectorModel;
const agentModel = datasetDetail.agentModel;
@@ -156,6 +156,7 @@ const DatasetImportContextProvider = ({ children }: { children: React.ReactNode
mode: TrainingModeEnum.chunk,
way: ImportProcessWayEnum.auto,
embeddingChunkSize: vectorModel?.defaultToken || 512,
qaChunkSize: Math.min(agentModel.maxResponse * 1, agentModel.maxContext * 0.7),
customSplitChar: '',
qaPrompt: Prompt_AgentQA.description,
webSelector: ''
@@ -168,6 +169,7 @@ const DatasetImportContextProvider = ({ children }: { children: React.ReactNode
const mode = processParamsForm.watch('mode');
const way = processParamsForm.watch('way');
const embeddingChunkSize = processParamsForm.watch('embeddingChunkSize');
const qaChunkSize = processParamsForm.watch('qaChunkSize');
const customSplitChar = processParamsForm.watch('customSplitChar');
const modeStaticParams: Record<TrainingModeEnum, TrainingFiledType> = {
@@ -180,7 +182,7 @@ const DatasetImportContextProvider = ({ children }: { children: React.ReactNode
showChunkInput: false,
showPromptInput: false,
charsPointsPrice: agentModel.charsPointsPrice,
priceTip: t('core.dataset.import.Auto mode Estimated Price Tips', {
priceTip: t('common:core.dataset.import.Auto mode Estimated Price Tips', {
price: agentModel.charsPointsPrice
}),
uploadRate: 100
@@ -195,22 +197,22 @@ const DatasetImportContextProvider = ({ children }: { children: React.ReactNode
showChunkInput: true,
showPromptInput: false,
charsPointsPrice: vectorModel.charsPointsPrice,
priceTip: t('core.dataset.import.Embedding Estimated Price Tips', {
priceTip: t('common:core.dataset.import.Embedding Estimated Price Tips', {
price: vectorModel.charsPointsPrice
}),
uploadRate: 150
},
[TrainingModeEnum.qa]: {
chunkSizeField: 'embeddingChunkSize' as ChunkSizeFieldType,
chunkSizeField: 'qaChunkSize' as ChunkSizeFieldType,
chunkOverlapRatio: 0,
maxChunkSize: 4096,
minChunkSize: 512,
autoChunkSize: agentModel.maxContext * 0.55 || 6000,
chunkSize: embeddingChunkSize || agentModel.maxContext * 0.55 || 6000,
maxChunkSize: Math.min(agentModel.maxResponse * 4, agentModel.maxContext * 0.7),
minChunkSize: 4000,
autoChunkSize: Math.min(agentModel.maxResponse * 1, agentModel.maxContext * 0.7),
chunkSize: qaChunkSize,
showChunkInput: true,
showPromptInput: true,
charsPointsPrice: agentModel.charsPointsPrice,
priceTip: t('core.dataset.import.QA Estimated Price Tips', {
priceTip: t('common:core.dataset.import.QA Estimated Price Tips', {
price: agentModel?.charsPointsPrice
}),
uploadRate: 30
@@ -228,7 +230,6 @@ const DatasetImportContextProvider = ({ children }: { children: React.ReactNode
customSplitChar
}
};
const chunkSize = wayStaticPrams[way].chunkSize;
const contextValue = {

View File

@@ -42,7 +42,8 @@ function DataProcess({ showPreviewChunks = true }: { showPreviewChunks: boolean
showChunkInput,
showPromptInput,
maxChunkSize,
priceTip
priceTip,
chunkSize
} = useContextSelector(DatasetImportContext, (v) => v);
const { getValues, setValue, register, watch } = processParamsForm;
const { toast } = useToast();
@@ -74,8 +75,15 @@ function DataProcess({ showPreviewChunks = true }: { showPreviewChunks: boolean
);
return (
<Box h={'100%'} display={['block', 'flex']} gap={5} fontSize={'sm'}>
<Box flex={'1 0 0'} minW={['auto', '540px']} maxW={'600px'}>
<Box h={'100%'} display={['block', 'flex']} fontSize={'sm'}>
<Box
flex={'1 0 0'}
minW={['auto', '540px']}
maxW={'600px'}
h={['auto', '100%']}
overflow={'auto'}
pr={[0, 3]}
>
<Flex alignItems={'center'}>
<MyIcon name={'common/settingLight'} w={'20px'} />
<Box fontSize={'md'}>{t('common:core.dataset.import.Data process params')}</Box>
@@ -138,7 +146,7 @@ function DataProcess({ showPreviewChunks = true }: { showPreviewChunks: boolean
}}
>
<MyTooltip
label={t('core.dataset.import.Chunk Range', {
label={t('common:core.dataset.import.Chunk Range', {
min: minChunkSize,
max: maxChunkSize
})}
@@ -148,6 +156,7 @@ function DataProcess({ showPreviewChunks = true }: { showPreviewChunks: boolean
step={100}
min={minChunkSize}
max={maxChunkSize}
value={chunkSize}
onChange={(e) => {
setValue(chunkSizeField, +e);
}}
@@ -279,7 +288,7 @@ function DataProcess({ showPreviewChunks = true }: { showPreviewChunks: boolean
</Button>
</Flex>
</Box>
<Box flex={'1 0 0'} w={['auto', '0']}>
<Box flex={'1 0 0'} w={['auto', '0']} h={['auto', '100%']} overflow={'auto'} pl={[0, 3]}>
<Preview showPreviewChunks={showPreviewChunks} />
</Box>

View File

@@ -0,0 +1,8 @@
import { AuthFrequencyLimitProps } from '@fastgpt/global/common/frequenctLimit/type';
import { POST } from '@fastgpt/service/common/api/plusRequest';
export const authFrequencyLimit = (data: AuthFrequencyLimitProps) => {
if (!global.feConfigs.isPlus) return;
return POST('/common/freequencyLimit/auth', data);
};

View File

@@ -1,4 +1,5 @@
import { getUserChatInfoAndAuthTeamPoints } from '@/service/support/permission/auth/team';
import { pushChatUsage } from '@/service/support/wallet/usage/push';
import { defaultApp } from '@/web/core/app/constants';
import { getNextTimeByCronStringAndTimezone } from '@fastgpt/global/common/string/time';
import { getNanoid } from '@fastgpt/global/common/string/tools';
@@ -9,6 +10,7 @@ import {
initWorkflowEdgeStatus,
storeNodes2RuntimeNodes
} from '@fastgpt/global/core/workflow/runtime/utils';
import { UsageSourceEnum } from '@fastgpt/global/support/wallet/usage/constants';
import { addLog } from '@fastgpt/service/common/system/log';
import { MongoApp } from '@fastgpt/service/core/app/schema';
import { dispatchWorkFlow } from '@fastgpt/service/core/workflow/dispatch';
@@ -29,7 +31,7 @@ export const getScheduleTriggerApp = async () => {
const { user } = await getUserChatInfoAndAuthTeamPoints(app.tmbId);
try {
await dispatchWorkFlow({
const { flowUsages } = await dispatchWorkFlow({
chatId: getNanoid(),
user,
mode: 'chat',
@@ -53,6 +55,14 @@ export const getScheduleTriggerApp = async () => {
detail: false,
maxRunTimes: 200
});
pushChatUsage({
appName: app.name,
appId: app._id,
teamId: String(app.teamId),
tmbId: String(app.tmbId),
source: UsageSourceEnum.cronJob,
flowUsages
});
} catch (error) {
addLog.error('Schedule trigger error', error);
}

View File

@@ -13,6 +13,7 @@ import {
import { defaultDatasetDetail } from '../constants';
import { DatasetUpdateBody } from '@fastgpt/global/core/dataset/api';
import { DatasetItemType, DatasetTagType } from '@fastgpt/global/core/dataset/type';
import { useSystemStore } from '@/web/common/system/useSystemStore';
type DatasetPageContextType = {
datasetId: string;
@@ -83,6 +84,7 @@ export const DatasetPageContextProvider = ({
datasetId: string;
}) => {
const { t } = useTranslation();
const { feConfigs } = useSystemStore();
// dataset detail
const [datasetDetail, setDatasetDetail] = useState(defaultDatasetDetail);
@@ -123,6 +125,8 @@ export const DatasetPageContextProvider = ({
const [allDatasetTags, setAllDatasetTags] = useState<DatasetTagType[]>([]);
const loadAllDatasetTags = async ({ id }: { id: string }) => {
if (!feConfigs?.isPlus) return;
const { list } = await getAllTags(id);
setAllDatasetTags(list);
};

View File

@@ -4,7 +4,6 @@ import { UserAuthTypeEnum } from '@fastgpt/global/support/user/auth/constants';
import { useTranslation } from 'next-i18next';
import { useSystemStore } from '@/web/common/system/useSystemStore';
import { useRequest2 } from '@fastgpt/web/hooks/useRequest';
import { i18nT } from '@fastgpt/web/i18n/utils';
let timer: NodeJS.Timeout;
export const useSendCode = () => {
@@ -29,8 +28,8 @@ export const useSendCode = () => {
}, 1000);
},
{
successToast: i18nT('user:password.code_sended'),
errorToast: i18nT('user:password.code_send_error'),
successToast: t('user:password.code_sended'),
errorToast: t('user:password.code_send_error'),
refreshDeps: [codeCountDown, feConfigs?.googleClientVerKey]
}
);