4.8.13 feature (#3118)

* chore(ui): login page & workflow page (#3046)

* login page & number input & multirow select & llm select

* workflow

* adjust nodes

* New file upload (#3058)

* feat: toolNode aiNode readFileNode adapt new version

* update docker-compose

* update tip

* feat: adapt new file version

* perf: file input

* fix: ts

* feat: add chat history time label (#3024)

* feat:add chat and logs time

* feat: add chat history time label

* code perf

* code perf

---------

Co-authored-by: 勤劳上班的卑微小张 <jiazhan.zhang@ggimage.com>

* add chatType (#3060)

* pref: slow query of full text search (#3044)

* Adapt findLast api;perf: markdown zh format. (#3066)

* perf: context code

* fix: adapt findLast api

* perf: commercial plugin run error

* perf: markdown zh format

* perf: dockerfile proxy (#3067)

* fix ui (#3065)

* fix ui

* fix

* feat: support array reference multi-select (#3041)

* feat: support array reference multi-select

* fix build

* fix

* fix loop multi-select

* adjust condition

* fix get value

* array and non-array conversion

* fix plugin input

* merge func

* feat: iframe code block;perf: workflow selector type (#3076)

* feat: iframe code block

* perf: workflow selector type

* node pluginoutput check (#3074)

* feat: View will move when workflow check error;fix: ui refresh error when continuous file upload (#3077)

* fix: plugin output check

* fix: ui refresh error when continuous file upload

* feat: View will move when workflow check error

* add dispatch try catch (#3075)

* perf: workflow context split (#3083)

* perf: workflow context split

* perf: context

* 4.8.13 test (#3085)

* perf: workflow node ui

* chat iframe url

* feat: support sub route config (#3071)

* feat: support sub route config

* dockerfile

* fix upload

* delete unused code

* 4.8.13 test (#3087)

* fix: image expired

* fix: datacard navbar ui

* perf: build action

* fix: workflow file upload refresh (#3088)

* fix: http tool response (#3097)

* loop node dynamic height (#3092)

* loop node dynamic height

* fix

* fix

* feat: support push chat log (#3093)

* feat: custom uid/metadata

* to: custom info

* fix: chat push latest

* feat: add chat log envs

* refactor: move timer to pushChatLog

* fix: using precise log

---------

Co-authored-by: Finley Ge <m13203533462@163.com>

* 4.8.13 test (#3098)

* perf: loop node refresh

* rename context

* comment

* fix: ts

* perf: push chat log

* array reference check & node ui (#3100)

* feat: loop start add index (#3101)

* feat: loop start add index

* update doc

* 4.8.13 test (#3102)

* fix: loop index;edge parent check

* perf: reference invalid check

* fix: ts

* fix: plugin select files and ai response check (#3104)

* fix: plugin select files and ai response check

* perf: text editor selector;tool call tip;remove invalid image url;

* perf: select file

* perf: drop files

* feat: source id prefix env (#3103)

* 4.8.13 test (#3106)

* perf: select file

* perf: drop files

* perf: env template

* 4.8.13 test (#3107)

* perf: select file

* perf: drop files

* fix: imple mode adapt files

* perf: push chat log (#3109)

* fix: share page load title error (#3111)

* 4.8.13 perf (#3112)

* fix: share page load title error

* update file input doc

* perf: auto add file urls

* perf: auto ser loop node offset height

* 4.8.13 test (#3117)

* perf: plugin

* updat eaction

* feat: add more share config (#3120)

* feat: add more share config

* add i18n en

* fix: missing subroute (#3121)

* perf: outlink config (#3128)

* update action

* perf: outlink config

* fix: ts (#3129)

* 更新 docSite 文档内容 (#3131)

* fix: null pointer (#3130)

* fix: null pointer

* perf: not input text

* update doc url

* perf: outlink default value (#3134)

* update doc (#3136)

* 4.8.13 test (#3137)

* update doc

* perf: completions chat api

* Restore docSite content based on upstream/4.8.13-dev (#3138)

* Restore docSite content based on upstream/4.8.13-dev

* 4813.md缺少更正

* update doc (#3141)

---------

Co-authored-by: heheer <heheer@sealos.io>
Co-authored-by: papapatrick <109422393+Patrickill@users.noreply.github.com>
Co-authored-by: 勤劳上班的卑微小张 <jiazhan.zhang@ggimage.com>
Co-authored-by: Finley Ge <32237950+FinleyGe@users.noreply.github.com>
Co-authored-by: a.e. <49438478+I-Info@users.noreply.github.com>
Co-authored-by: Finley Ge <m13203533462@163.com>
Co-authored-by: Jiangween <145003935+Jiangween@users.noreply.github.com>
This commit is contained in:
Archer
2024-11-13 11:29:53 +08:00
committed by shilin66
parent 72777c341b
commit f680be52f1
449 changed files with 7626 additions and 4180 deletions

View File

@@ -0,0 +1,24 @@
import React from 'react';
import { Box } from '@chakra-ui/react';
import { useMarkdownWidth } from '../hooks';
const IframeBlock = ({ code }: { code: string }) => {
const { width, Ref } = useMarkdownWidth();
return (
<Box w={width} ref={Ref}>
<iframe
src={code}
sandbox="allow-scripts allow-forms allow-popups allow-downloads allow-presentation allow-storage-access-by-user-activation"
referrerPolicy="no-referrer"
style={{
width: '100%',
height: '100%',
minHeight: '70vh',
border: 'none'
}}
/>
</Box>
);
};
export default IframeBlock;

View File

@@ -0,0 +1,25 @@
import React, { useEffect, useRef, useCallback, useState } from 'react';
import { Box } from '@chakra-ui/react';
import MyIcon from '@fastgpt/web/components/common/Icon';
import { useMarkdownWidth } from '../hooks';
const MermaidBlock = ({ code }: { code: string }) => {
const { width, Ref } = useMarkdownWidth();
return (
<Box w={width} ref={Ref}>
<iframe
src={code}
sandbox="allow-scripts allow-forms allow-popups allow-downloads allow-presentation allow-storage-access-by-user-activation"
referrerPolicy="no-referrer"
style={{
width: '100%',
height: '100%',
minHeight: '70vh',
border: 'none'
}}
/>
</Box>
);
};
export default MermaidBlock;

View File

@@ -0,0 +1,34 @@
import { useScreen } from '@fastgpt/web/hooks/useScreen';
import { useSystem } from '@fastgpt/web/hooks/useSystem';
import { useCallback, useEffect, useRef, useState } from 'react';
export const useMarkdownWidth = () => {
const Ref = useRef<HTMLDivElement>(null);
const [width, setWidth] = useState(400);
const { screenWidth } = useScreen();
const { isPc } = useSystem();
const findMarkdownDom = useCallback(() => {
if (!Ref.current) return;
// 一直找到 parent = markdown 的元素
let parent = Ref.current?.parentElement;
while (parent && !parent.className.includes('chat-box-card')) {
parent = parent.parentElement;
}
const ChatItemDom = parent?.parentElement;
const clientWidth = ChatItemDom?.clientWidth ? ChatItemDom.clientWidth - (isPc ? 90 : 60) : 500;
setWidth(clientWidth);
return parent?.parentElement;
}, [isPc]);
useEffect(() => {
findMarkdownDom();
}, [findMarkdownDom, screenWidth, Ref.current]);
return {
Ref,
width
};
};

View File

@@ -22,6 +22,7 @@ const CodeLight = dynamic(() => import('./CodeLight'), { ssr: false });
const MermaidCodeBlock = dynamic(() => import('./img/MermaidCodeBlock'), { ssr: false });
const MdImage = dynamic(() => import('./img/Image'), { ssr: false });
const EChartsCodeBlock = dynamic(() => import('./img/EChartsCodeBlock'), { ssr: false });
const IframeCodeBlock = dynamic(() => import('./codeBlock/Iframe'), { ssr: false });
const ChatGuide = dynamic(() => import('./chat/Guide'), { ssr: false });
const QuestionGuide = dynamic(() => import('./chat/QuestionGuide'), { ssr: false });
@@ -29,11 +30,13 @@ const QuestionGuide = dynamic(() => import('./chat/QuestionGuide'), { ssr: false
const Markdown = ({
source = '',
showAnimation = false,
isDisabled = false
isDisabled = false,
forbidZhFormat = false
}: {
source?: string;
showAnimation?: boolean;
isDisabled?: boolean;
forbidZhFormat?: boolean;
}) => {
const components = useMemo<any>(
() => ({
@@ -46,15 +49,35 @@ const Markdown = ({
);
const formatSource = useMemo(() => {
const formatSource = source
if (showAnimation || forbidZhFormat) return source;
// 保护 URL 格式https://, http://, /api/xxx
const urlPlaceholders: string[] = [];
const textWithProtectedUrls = source.replace(
/(https?:\/\/[^\s<]+[^<.,:;"')\]\s]|\/api\/[^\s]+)(?=\s|$)/g,
(match) => {
urlPlaceholders.push(match);
return `__URL_${urlPlaceholders.length - 1}__`;
}
);
// 处理中文与英文数字之间的分词
const textWithSpaces = textWithProtectedUrls
.replace(
/([\u4e00-\u9fa5\u3000-\u303f])([a-zA-Z0-9])|([a-zA-Z0-9])([\u4e00-\u9fa5\u3000-\u303f])/g,
'$1$3 $2$4'
) // Chinese and english chars separated by space
)
// 处理引用标记
.replace(/\n*(\[QUOTE SIGN\]\(.*\))/g, '$1');
return formatSource;
}, [source]);
// 还原 URL
const finalText = textWithSpaces.replace(
/__URL_(\d+)__/g,
(_, index) => urlPlaceholders[parseInt(index)]
);
return finalText;
}, [forbidZhFormat, showAnimation, source]);
const urlTransform = useCallback((val: string) => {
return val;
@@ -101,6 +124,9 @@ function Code(e: any) {
if (codeType === CodeClassNameEnum.echarts) {
return <EChartsCodeBlock code={strChildren} />;
}
if (codeType === CodeClassNameEnum.iframe) {
return <IframeCodeBlock code={strChildren} />;
}
return (
<CodeLight className={className} codeBlock={codeBlock} match={match}>

View File

@@ -5,7 +5,8 @@ export enum CodeClassNameEnum {
echarts = 'echarts',
quote = 'quote',
files = 'files',
latex = 'latex'
latex = 'latex',
iframe = 'iframe'
}
function htmlTableToLatex(html: string) {

View File

@@ -1,5 +1,6 @@
import React, { useState } from 'react';
import { Image, Skeleton, ImageProps } from '@chakra-ui/react';
import { Skeleton, ImageProps } from '@chakra-ui/react';
import CustomImage from '@fastgpt/web/components/common/Image/MyImage';
export const MyImage = (props: ImageProps) => {
const [isLoading, setIsLoading] = useState(true);
@@ -13,7 +14,7 @@ export const MyImage = (props: ImageProps) => {
justifyContent={'center'}
my={1}
>
<Image
<CustomImage
display={'inline-block'}
borderRadius={'md'}
alt={''}

View File

@@ -82,7 +82,7 @@ const AIChatSettingsModal = ({
{t('common:core.ai.AI settings')}
{feConfigs?.docUrl && (
<Link
href={getDocPath('/docs/course/ai_settings/')}
href={getDocPath('/docs/guide/course/ai_settings/')}
target={'_blank'}
ml={1}
textDecoration={'underline'}

View File

@@ -68,7 +68,9 @@ const SettingLLMModel = ({
<Button
w={'100%'}
justifyContent={'flex-start'}
variant={'whiteBase'}
variant={'whitePrimaryOutline'}
size={'lg'}
fontSize={'sm'}
bg={bg}
_active={{
transform: 'none'
@@ -81,8 +83,9 @@ const SettingLLMModel = ({
w={'18px'}
/>
}
rightIcon={<MyIcon name={'common/select'} w={'1rem'} />}
pl={4}
rightIcon={<MyIcon name={'common/select'} w={'1.2rem'} color={'myGray.500'} />}
px={3}
pr={2}
onClick={onOpenAIChatSetting}
>
<Box flex={1} textAlign={'left'}>

View File

@@ -59,7 +59,9 @@ const FileSelect = ({
return (
<Flex alignItems={'center'}>
<MyIcon name={'core/app/simpleMode/file'} mr={2} w={'20px'} />
<FormLabel {...labelStyle}>{t('app:file_upload')}</FormLabel>
<FormLabel color={'myGray.600'} {...labelStyle}>
{t('app:file_upload')}
</FormLabel>
<ChatFunctionTip type={'file'} />
<Box flex={1} />
<MyTooltip label={t('app:config_file_upload')}>
@@ -68,6 +70,7 @@ const FileSelect = ({
iconSpacing={1}
size={'sm'}
mr={'-5px'}
color={'myGray.600'}
onClick={onOpen}
>
{formLabel}

View File

@@ -87,7 +87,7 @@ const InputGuideConfig = ({
<Flex alignItems={'center'}>
<MyIcon name={'core/app/inputGuides'} mr={2} w={'20px'} />
<Flex alignItems={'center'}>
<FormLabel>{chatT('input_guide')}</FormLabel>
<FormLabel color={'myGray.600'}>{chatT('input_guide')}</FormLabel>
<ChatFunctionTip type={'inputGuide'} />
</Flex>
<Box flex={1} />
@@ -97,6 +97,7 @@ const InputGuideConfig = ({
iconSpacing={1}
size={'sm'}
mr={'-5px'}
color={'myGray.600'}
onClick={onOpen}
>
{formLabel}
@@ -145,7 +146,7 @@ const InputGuideConfig = ({
<Flex mt={8} alignItems={'center'}>
<FormLabel>{chatT('custom_input_guide_url')}</FormLabel>
<Flex
onClick={() => window.open(getDocPath('/docs/course/chat_input_guide'))}
onClick={() => window.open(getDocPath('/docs/guide/course/chat_input_guide/'))}
color={'primary.700'}
alignItems={'center'}
cursor={'pointer'}

View File

@@ -11,7 +11,7 @@ const QGSwitch = (props: SwitchProps) => {
return (
<Flex alignItems={'center'}>
<MyIcon name={'core/chat/QGFill'} mr={2} w={'20px'} />
<FormLabel>{t('common:core.app.Question Guide')}</FormLabel>
<FormLabel color={'myGray.600'}>{t('common:core.app.Question Guide')}</FormLabel>
<ChatFunctionTip type={'nextQuestion'} />
<Box flex={1} />
<Switch {...props} />

View File

@@ -270,7 +270,7 @@ const ScheduledTriggerConfig = ({
<Flex alignItems={'center'}>
<MyIcon name={'core/app/schedulePlan'} w={'20px'} />
<HStack ml={2} flex={1} spacing={1}>
<FormLabel>{t('common:core.app.Interval timer run')}</FormLabel>
<FormLabel color={'myGray.600'}>{t('common:core.app.Interval timer run')}</FormLabel>
<QuestionTip label={t('common:core.app.Interval timer tip')} />
</HStack>
<MyTooltip label={t('common:core.app.Config schedule plan')}>
@@ -279,6 +279,7 @@ const ScheduledTriggerConfig = ({
iconSpacing={1}
size={'sm'}
mr={'-5px'}
color={'myGray.600'}
onClick={onOpen}
>
{formatLabel}

View File

@@ -13,6 +13,7 @@ import MySelect from '@fastgpt/web/components/common/MySelect';
import { defaultTTSConfig } from '@fastgpt/global/core/app/constants';
import ChatFunctionTip from './Tip';
import FormLabel from '@fastgpt/web/components/common/MyBox/FormLabel';
import MyImage from '@fastgpt/web/components/common/Image/MyImage';
const TTSSelect = ({
value = defaultTTSConfig,
@@ -82,7 +83,7 @@ const TTSSelect = ({
return (
<Flex alignItems={'center'}>
<MyIcon name={'core/app/simpleMode/tts'} mr={2} w={'20px'} />
<FormLabel>{t('common:core.app.TTS')}</FormLabel>
<FormLabel color={'myGray.600'}>{t('common:core.app.TTS')}</FormLabel>
<ChatFunctionTip type={'tts'} />
<Box flex={1} />
<MyTooltip label={t('common:core.app.Select TTS')}>
@@ -92,6 +93,7 @@ const TTSSelect = ({
size={'sm'}
mr={'-5px'}
onClick={onOpen}
color={'myGray.600'}
>
{formLabel}
</Button>
@@ -132,7 +134,7 @@ const TTSSelect = ({
<Flex mt={10} justifyContent={'end'}>
{audioPlaying ? (
<Flex>
<Image src="/icon/speaking.gif" w={'24px'} alt={''} />
<MyImage src="/icon/speaking.gif" w={'24px'} alt={''} />
<Button
ml={2}
variant={'grayBase'}

View File

@@ -1,5 +1,5 @@
import { useI18n } from '@/web/context/I18n';
import { Box, Flex, Image } from '@chakra-ui/react';
import { Box, Flex } from '@chakra-ui/react';
import MyImage from '@fastgpt/web/components/common/Image/MyImage';
import QuestionTip from '@fastgpt/web/components/common/MyTooltip/QuestionTip';
import { useTranslation } from 'next-i18next';
import React, { useRef } from 'react';
@@ -58,8 +58,8 @@ const ChatFunctionTip = ({ type }: { type: `${FnTypeEnum}` }) => {
[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'
desc: t('app:open_vision_function_tip'),
imgUrl: '/imgs/app/visionModel.svg'
},
[FnTypeEnum.instruction]: {
icon: '/imgs/app/help.svg',
@@ -77,7 +77,7 @@ const ChatFunctionTip = ({ type }: { type: `${FnTypeEnum}` }) => {
label={
<Box pt={2}>
<Flex alignItems={'flex-start'}>
<Image src={data.icon} w={'36px'} alt={''} />
<MyImage src={data.icon} w={'36px'} alt={''} />
<Box ml={3}>
<Box fontWeight="bold">{data.title}</Box>
<Box fontSize={'xs'} color={'myGray.500'}>
@@ -85,7 +85,7 @@ const ChatFunctionTip = ({ type }: { type: `${FnTypeEnum}` }) => {
</Box>
</Box>
</Flex>
<Image src={data.imgUrl} w={'100%'} minH={['auto', '250px']} mt={2} alt={''} />
<MyImage src={data.imgUrl} w={'100%'} minH={['auto', '250px']} mt={2} alt={''} />
</Box>
}
/>

View File

@@ -65,10 +65,6 @@ const VariableEdit = ({
const { setValue, reset, watch, getValues } = form;
const value = getValues();
const type = watch('type');
const valueType = watch('valueType');
const max = watch('max');
const min = watch('min');
const defaultValue = watch('defaultValue');
const inputTypeList = useMemo(
() =>
@@ -173,7 +169,9 @@ const VariableEdit = ({
{/* Row box */}
<Flex alignItems={'center'}>
<MyIcon name={'core/app/simpleMode/variable'} w={'20px'} />
<FormLabel ml={2}>{t('common:core.module.Variable')}</FormLabel>
<FormLabel ml={2} color={'myGray.600'}>
{t('common:core.module.Variable')}
</FormLabel>
<ChatFunctionTip type={'variable'} />
<Box flex={1} />
<Button
@@ -181,6 +179,7 @@ const VariableEdit = ({
leftIcon={<SmallAddIcon />}
iconSpacing={1}
size={'sm'}
color={'myGray.600'}
mr={'-5px'}
onClick={() => {
reset(addVariable());
@@ -193,60 +192,83 @@ const VariableEdit = ({
{formatVariables.length > 0 && (
<Box mt={2} borderRadius={'md'} overflow={'hidden'} borderWidth={'1px'} borderBottom="none">
<TableContainer>
<Table>
<Thead>
<Table bg={'white'}>
<Thead h={8}>
<Tr>
<Th
borderRadius={'none !important'}
fontSize={'mini'}
bg={'myGray.50'}
p={0}
px={4}
fontWeight={'medium'}
>
{t('workflow:Variable_name')}
</Th>
<Th fontSize={'mini'} bg={'myGray.50'} p={0} px={4} fontWeight={'medium'}>
{t('common:common.Require Input')}
</Th>
<Th
fontSize={'mini'}
borderRadius={'none !important'}
w={'18px !important'}
bg={'myGray.50'}
p={0}
/>
<Th fontSize={'mini'}>{t('workflow:Variable_name')}</Th>
<Th fontSize={'mini'}>{t('app:global_variables_desc')}</Th>
<Th fontSize={'mini'}>{t('common:common.Require Input')}</Th>
<Th fontSize={'mini'} borderRadius={'none !important'}></Th>
px={4}
fontWeight={'medium'}
>
{t('common:common.Operation')}
</Th>
</Tr>
</Thead>
<Tbody>
{formatVariables.map((item) => (
<Tr key={item.id}>
<Td p={0} pl={3}>
<MyIcon name={item.icon as any} w={'16px'} color={'myGray.500'} />
</Td>
<Td>{item.key}</Td>
<Td
maxW={'200px'}
fontSize={'sm'}
whiteSpace={'pre-wrap'}
wordBreak={'break-all'}
px={0}
p={0}
px={4}
h={8}
color={'myGray.900'}
fontSize={'mini'}
fontWeight={'medium'}
>
{item.description || '-'}
<Flex alignItems={'center'}>
<MyIcon name={item.icon as any} w={'16px'} color={'myGray.400'} mr={2} />
{item.key}
</Flex>
</Td>
<Td>{item.required ? '✔' : '-'}</Td>
<Td>
<MyIcon
mr={3}
name={'common/settingLight'}
w={'16px'}
cursor={'pointer'}
onClick={() => {
const formattedItem = {
...item,
list: item.enums || []
};
reset(formattedItem);
}}
/>
<MyIcon
name={'delete'}
w={'16px'}
cursor={'pointer'}
onClick={() =>
onChange(variables.filter((variable) => variable.id !== item.id))
}
/>
<Td p={0} px={4} h={8} color={'myGray.900'} fontSize={'mini'}>
<Flex alignItems={'center'}>
{item.required ? (
<MyIcon name={'check'} w={'16px'} color={'myGray.900'} mr={2} />
) : (
''
)}
</Flex>
</Td>
<Td p={0} px={4} h={8} color={'myGray.600'} fontSize={'mini'}>
<Flex alignItems={'center'}>
<MyIcon
mr={3}
name={'common/settingLight'}
w={'16px'}
cursor={'pointer'}
onClick={() => {
const formattedItem = {
...item,
list: item.enums || []
};
reset(formattedItem);
}}
/>
<MyIcon
name={'delete'}
w={'16px'}
cursor={'pointer'}
onClick={() =>
onChange(variables.filter((variable) => variable.id !== item.id))
}
/>
</Flex>
</Td>
</Tr>
))}
@@ -337,11 +359,7 @@ const VariableEdit = ({
type={'variable'}
isEdit={!!value.key}
inputType={type}
valueType={valueType}
defaultValue={defaultValue}
defaultValueType={defaultValueType}
max={max}
min={min}
onClose={() => reset({})}
onSubmitSuccess={onSubmitSuccess}
onSubmitError={onSubmitError}

View File

@@ -13,17 +13,20 @@ const WelcomeTextConfig = (props: TextareaProps) => {
<>
<Flex alignItems={'center'}>
<MyIcon name={'core/app/simpleMode/chat'} w={'20px'} />
<FormLabel ml={2}>{t('common:core.app.Welcome Text')}</FormLabel>
<FormLabel ml={2} color={'myGray.600'}>
{t('common:core.app.Welcome Text')}
</FormLabel>
<ChatFunctionTip type={'welcome'} />
</Flex>
<MyTextarea
className="nowheel"
iconSrc={'core/app/simpleMode/chat'}
title={t('common:core.app.Welcome Text')}
mt={2}
mt={1.5}
rows={6}
fontSize={'sm'}
bg={'myGray.50'}
bg={'white'}
minW={'384px'}
placeholder={t('common:core.app.tip.welcomeTextTip')}
autoHeight
minH={100}

View File

@@ -34,7 +34,7 @@ const WhisperConfig = ({
return (
<Flex alignItems={'center'}>
<MyIcon name={'core/app/simpleMode/whisper'} mr={2} w={'20px'} />
<FormLabel>{t('common:core.app.Whisper')}</FormLabel>
<FormLabel color={'myGray.600'}>{t('common:core.app.Whisper')}</FormLabel>
<Box flex={1} />
<MyTooltip label={t('common:core.app.Config whisper')}>
<Button
@@ -42,6 +42,7 @@ const WhisperConfig = ({
iconSpacing={1}
size={'sm'}
mr={'-5px'}
color={'myGray.600'}
onClick={onOpen}
>
{formLabel}

View File

@@ -8,7 +8,7 @@ import MyIcon from '@fastgpt/web/components/common/Icon';
import { useRequest2 } from '@fastgpt/web/hooks/useRequest';
import { ChatBoxInputFormType, ChatBoxInputType, SendPromptFnType } from '../type';
import { textareaMinH } from '../constants';
import { UseFormReturn } from 'react-hook-form';
import { useFieldArray, UseFormReturn } from 'react-hook-form';
import { ChatBoxContext } from '../Provider';
import dynamic from 'next/dynamic';
import { useContextSelector } from 'use-context-selector';
@@ -17,6 +17,7 @@ import { documentFileType } from '@fastgpt/global/common/file/constants';
import FilePreview from '../../components/FilePreview';
import { useFileUpload } from '../hooks/useFileUpload';
import ComplianceTip from '@/components/common/ComplianceTip/index';
import { useToast } from '@fastgpt/web/hooks/useToast';
const InputGuideBox = dynamic(() => import('./InputGuideBox'));
@@ -44,6 +45,7 @@ const ChatInput = ({
}) => {
const { isPc } = useSystem();
const { t } = useTranslation();
const { toast } = useToast();
const { setValue, watch, control } = chatForm;
const inputValue = watch('input');
@@ -58,6 +60,10 @@ const ChatInput = ({
fileSelectConfig
} = useContextSelector(ChatBoxContext, (v) => v);
const fileCtrl = useFieldArray({
control,
name: 'files'
});
const {
File,
onOpenSelectFile,
@@ -69,15 +75,15 @@ const ChatInput = ({
showSelectFile,
showSelectImg,
removeFiles,
replaceFiles
replaceFiles,
hasFileUploading
} = useFileUpload({
outLinkAuthData,
chatId: chatId || '',
fileSelectConfig,
control
fileCtrl
});
const havInput = !!inputValue || fileList.length > 0;
const hasFileUploading = fileList.some((item) => !item.url);
const canSendMessage = havInput && !hasFileUploading;
// Upload files
@@ -202,7 +208,7 @@ const ChatInput = ({
<MyTooltip label={selectFileLabel}>
<MyIcon name={selectFileIcon as any} w={'18px'} color={'myGray.600'} />
</MyTooltip>
<File onSelect={(files) => onSelectFile({ files, fileList })} />
<File onSelect={(files) => onSelectFile({ files })} />
</Flex>
)}
@@ -278,9 +284,10 @@ const ChatInput = ({
.filter((file) => {
return file && fileTypeFilter(file);
}) as File[];
onSelectFile({ files, fileList });
onSelectFile({ files });
if (files.length > 0) {
e.preventDefault();
e.stopPropagation();
}
}
@@ -431,7 +438,36 @@ const ChatInput = ({
);
return (
<Box m={['0 auto', '10px auto']} w={'100%'} maxW={['auto', 'min(800px, 100%)']} px={[0, 5]}>
<Box
m={['0 auto', '10px auto']}
w={'100%'}
maxW={['auto', 'min(800px, 100%)']}
px={[0, 5]}
onDragOver={(e) => e.preventDefault()}
onDrop={(e) => {
e.preventDefault();
if (!(showSelectFile || showSelectImg)) return;
const files = Array.from(e.dataTransfer.files);
const droppedFiles = files.filter((file) => fileTypeFilter(file));
if (droppedFiles.length > 0) {
onSelectFile({ files: droppedFiles });
}
const invalidFileName = files
.filter((file) => !fileTypeFilter(file))
.map((file) => file.name)
.join(', ');
if (invalidFileName) {
toast({
status: 'warning',
title: t('chat:unsupported_file_type'),
description: invalidFileName
});
}
}}
>
<Box
pt={fileList.length > 0 ? '0' : ['14px', '18px']}
pb={['14px', '18px']}
@@ -468,7 +504,7 @@ const ChatInput = ({
{RenderTranslateLoading}
{/* file preview */}
<Box px={[2, 4]}>
<Box px={[1, 3]}>
<FilePreview fileList={fileList} removeFiles={removeFiles} />
</Box>

View File

@@ -34,6 +34,9 @@ export type ChatProviderProps = OutLinkChatAuthProps & {
// not chat test params
chatId?: string;
chatType: 'log' | 'chat' | 'share' | 'team';
showRawSource: boolean;
showNodeStatus: boolean;
};
type useChatStoreType = OutLinkChatAuthProps &
@@ -137,7 +140,9 @@ const Provider = ({
chatHistories,
setChatHistories,
variablesForm,
chatType = 'chat',
showRawSource,
showNodeStatus,
chatConfig = {},
children,
...props
@@ -239,7 +244,10 @@ const Provider = ({
chatInputGuide,
outLinkAuthData,
variablesForm,
getHistoryResponseData
getHistoryResponseData,
chatType,
showRawSource,
showNodeStatus
};
return <ChatBoxContext.Provider value={value}>{children}</ChatBoxContext.Provider>;

View File

@@ -1,5 +1,5 @@
import { useCopyData } from '@/web/common/hooks/useCopyData';
import { Flex, FlexProps, Image, css, useTheme } from '@chakra-ui/react';
import { Flex, FlexProps, css, useTheme } from '@chakra-ui/react';
import { ChatSiteItemType } from '@fastgpt/global/core/chat/type';
import MyTooltip from '@fastgpt/web/components/common/MyTooltip';
import React, { useMemo } from 'react';
@@ -9,6 +9,7 @@ import { formatChatValue2InputType } from '../utils';
import { ChatRoleEnum } from '@fastgpt/global/core/chat/constants';
import { ChatBoxContext } from '../Provider';
import { useContextSelector } from 'use-context-selector';
import MyImage from '@fastgpt/web/components/common/Image/MyImage';
export type ChatControllerProps = {
isLastChild: boolean;
@@ -124,7 +125,7 @@ const ChatController = ({
onClick={cancelAudio}
/>
</MyTooltip>
<Image
<MyImage
src="/icon/speaking.gif"
w={'23px'}
alt={''}

View File

@@ -1,5 +1,5 @@
import { Box, BoxProps, Card, Flex } from '@chakra-ui/react';
import React, { useMemo } from 'react';
import React, { useMemo, useRef } from 'react';
import ChatController, { type ChatControllerProps } from './ChatController';
import ChatAvatar from './ChatAvatar';
import { MessageCardStyle } from '../constants';
@@ -22,6 +22,9 @@ import { useTranslation } from 'next-i18next';
import { AIChatItemValueItemType, ChatItemValueItemType } from '@fastgpt/global/core/chat/type';
import { CodeClassNameEnum } from '@/components/Markdown/utils';
import { isEqual } from 'lodash';
import { useSystem } from '@fastgpt/web/hooks/useSystem';
import { formatTimeToChatItemTime } from '@fastgpt/global/common/string/time';
import dayjs from 'dayjs';
const colorMap = {
[ChatStatusEnum.loading]: {
@@ -110,8 +113,10 @@ const AIContentCard = React.memo(function AIContentCard({
const ChatItem = (props: Props) => {
const { type, avatar, statusBoxData, children, isLastChild, questionGuides = [], chat } = props;
const styleMap: BoxProps =
type === ChatRoleEnum.Human
const { isPc } = useSystem();
const styleMap: BoxProps = {
...(type === ChatRoleEnum.Human
? {
order: 0,
borderRadius: '8px 0 8px 8px',
@@ -125,10 +130,17 @@ const ChatItem = (props: Props) => {
justifyContent: 'flex-start',
textAlign: 'left',
bg: 'myGray.50'
};
}),
fontSize: 'mini',
fontWeight: '400',
color: 'myGray.500'
};
const { t } = useTranslation();
const isChatting = useContextSelector(ChatBoxContext, (v) => v.isChatting);
const chatType = useContextSelector(ChatBoxContext, (v) => v.chatType);
const showNodeStatus = useContextSelector(ChatBoxContext, (v) => v.showNodeStatus);
const isChatLog = chatType === 'log';
const { copyData } = useCopyData();
@@ -196,18 +208,38 @@ const ChatItem = (props: Props) => {
}, [chat.obj, chat.value, isChatting]);
return (
<>
<Box
_hover={{
'& .time-label': {
display: 'block'
}
}}
>
{/* control icon */}
<Flex w={'100%'} alignItems={'center'} gap={2} justifyContent={styleMap.justifyContent}>
<Flex w={'100%'} alignItems={'flex-end'} gap={2} justifyContent={styleMap.justifyContent}>
{isChatting && type === ChatRoleEnum.AI && isLastChild ? null : (
<Box order={styleMap.order} ml={styleMap.ml}>
<Flex order={styleMap.order} ml={styleMap.ml} align={'center'} gap={'0.62rem'}>
{chat.time && (isPc || isChatLog) && (
<Box
order={type === ChatRoleEnum.AI ? 2 : 0}
className={'time-label'}
fontSize={styleMap.fontSize}
color={styleMap.color}
fontWeight={styleMap.fontWeight}
display={isChatLog ? 'block' : 'none'}
>
{t(formatTimeToChatItemTime(chat.time) as any, {
time: dayjs(chat.time).format('HH:mm')
}).replace('#', ':')}
</Box>
)}
<ChatController {...props} isLastChild={isLastChild} />
</Box>
</Flex>
)}
<ChatAvatar src={avatar} type={type} />
{/* Workflow status */}
{!!chatStatusMap && statusBoxData && isLastChild && (
{!!chatStatusMap && statusBoxData && isLastChild && showNodeStatus && (
<Flex
alignItems={'center'}
px={3}
@@ -290,7 +322,7 @@ const ChatItem = (props: Props) => {
</Card>
</Box>
))}
</>
</Box>
);
};

View File

@@ -54,6 +54,7 @@ const ContextModal = ({ onClose, dataId }: { onClose: () => void; dataId: string
border={'base'}
_notLast={{ mb: 2 }}
position={'relative'}
bg={i % 2 === 0 ? 'white' : 'myGray.50'}
>
<Box fontWeight={'bold'}>{item.obj}</Box>
<Box>{item.value}</Box>

View File

@@ -6,16 +6,19 @@ import { useTranslation } from 'next-i18next';
import type { SearchDataResponseItemType } from '@fastgpt/global/core/dataset/type';
import QuoteItem from '@/components/core/dataset/QuoteItem';
import RawSourceBox from '@/components/core/dataset/RawSourceBox';
import { getWebReqUrl } from '@fastgpt/web/common/system/utils';
const QuoteModal = ({
rawSearch = [],
onClose,
showDetail,
canEditDataset,
showRawSource,
metadata
}: {
rawSearch: SearchDataResponseItemType[];
onClose: () => void;
showDetail: boolean;
canEditDataset: boolean;
showRawSource: boolean;
metadata?: {
collectionId: string;
sourceId?: string;
@@ -42,13 +45,13 @@ const QuoteModal = ({
h={['90vh', '80vh']}
isCentered
minW={['90vw', '600px']}
iconSrc={!!metadata ? undefined : '/imgs/modal/quote.svg'}
iconSrc={!!metadata ? undefined : getWebReqUrl('/imgs/modal/quote.svg')}
title={
<Box>
{metadata ? (
<RawSourceBox {...metadata} canView={showDetail} />
<RawSourceBox {...metadata} canView={showRawSource} />
) : (
<>{t('core.chat.Quote Amount', { amount: rawSearch.length })}</>
<>{t('common:core.chat.Quote Amount', { amount: rawSearch.length })}</>
)}
<Box fontSize={'xs'} color={'myGray.500'} fontWeight={'normal'}>
{t('common:core.chat.quote.Quote Tip')}
@@ -57,7 +60,11 @@ const QuoteModal = ({
}
>
<ModalBody>
<QuoteList rawSearch={filterResults} showDetail={showDetail} />
<QuoteList
rawSearch={filterResults}
canEditDataset={canEditDataset}
canViewSource={showRawSource}
/>
</ModalBody>
</MyModal>
</>
@@ -68,10 +75,12 @@ export default QuoteModal;
export const QuoteList = React.memo(function QuoteList({
rawSearch = [],
showDetail
canEditDataset,
canViewSource
}: {
rawSearch: SearchDataResponseItemType[];
showDetail: boolean;
canEditDataset: boolean;
canViewSource: boolean;
}) {
const theme = useTheme();
@@ -88,7 +97,11 @@ export const QuoteList = React.memo(function QuoteList({
_hover={{ '& .hover-data': { display: 'flex' } }}
bg={i % 2 === 0 ? 'white' : 'myWhite.500'}
>
<QuoteItem quoteItem={item} canViewSource={showDetail} linkToDataset={showDetail} />
<QuoteItem
quoteItem={item}
canViewSource={canViewSource}
canEditDataset={canEditDataset}
/>
</Box>
))}
</>

View File

@@ -7,12 +7,13 @@ import MyTag from '@fastgpt/web/components/common/Tag/index';
import MyTooltip from '@fastgpt/web/components/common/MyTooltip';
import { getSourceNameIcon } from '@fastgpt/global/core/dataset/utils';
import ChatBoxDivider from '@/components/core/chat/Divider';
import { strIsLink } from '@fastgpt/global/common/string/tools';
import MyIcon from '@fastgpt/web/components/common/Icon';
import { useSystem } from '@fastgpt/web/hooks/useSystem';
import { ChatSiteItemType } from '@fastgpt/global/core/chat/type';
import { addStatisticalDataToHistoryItem } from '@/global/core/chat/utils';
import { useSize } from 'ahooks';
import { useContextSelector } from 'use-context-selector';
import { ChatBoxContext } from '../Provider';
const QuoteModal = dynamic(() => import('./QuoteModal'));
const ContextModal = dynamic(() => import('./ContextModal'));
@@ -20,11 +21,9 @@ const WholeResponseModal = dynamic(() => import('../../../components/WholeRespon
const ResponseTags = ({
showTags,
showDetail,
historyItem
}: {
showTags: boolean;
showDetail: boolean;
historyItem: ChatSiteItemType;
}) => {
const { isPc } = useSystem();
@@ -37,6 +36,7 @@ const ResponseTags = ({
totalRunningTime: runningTime = 0,
historyPreviewLength = 0
} = useMemo(() => addStatisticalDataToHistoryItem(historyItem), [historyItem]);
const [quoteModalData, setQuoteModalData] = useState<{
rawSearch: SearchDataResponseItemType[];
metadata?: {
@@ -47,6 +47,10 @@ const ResponseTags = ({
}>();
const [quoteFolded, setQuoteFolded] = useState<boolean>(true);
const chatType = useContextSelector(ChatBoxContext, (v) => v.chatType);
const showRawSource = useContextSelector(ChatBoxContext, (v) => v.showRawSource);
const notSharePage = useMemo(() => chatType !== 'share', [chatType]);
const {
isOpen: isOpenWholeModal,
onOpen: onOpenWholeModal,
@@ -77,13 +81,20 @@ const ResponseTags = ({
sourceName: item.sourceName,
sourceId: item.sourceId,
icon: getSourceNameIcon({ sourceId: item.sourceId, sourceName: item.sourceName }),
canReadQuote: showDetail || strIsLink(item.sourceId),
collectionId: item.collectionId
}));
}, [quoteList, showDetail]);
}, [quoteList]);
const notEmptyTags =
quoteList.length > 0 ||
(llmModuleAccount === 1 && notSharePage) ||
(llmModuleAccount > 1 && notSharePage) ||
(isPc && runningTime > 0) ||
notSharePage;
return !showTags ? null : (
<>
{/* quote */}
{sourceList.length > 0 && (
<>
<Flex justifyContent={'space-between'} alignItems={'center'}>
@@ -176,7 +187,8 @@ const ResponseTags = ({
</Flex>
</>
)}
{showDetail && (
{notEmptyTags && (
<Flex alignItems={'center'} mt={3} flexWrap={'wrap'} gap={2}>
{quoteList.length > 0 && (
<MyTooltip label={t('chat:view_citations')}>
@@ -190,7 +202,7 @@ const ResponseTags = ({
</MyTag>
</MyTooltip>
)}
{llmModuleAccount === 1 && (
{llmModuleAccount === 1 && notSharePage && (
<>
{historyPreviewLength > 0 && (
<MyTooltip label={t('chat:click_contextual_preview')}>
@@ -206,12 +218,11 @@ const ResponseTags = ({
)}
</>
)}
{llmModuleAccount > 1 && (
{llmModuleAccount > 1 && notSharePage && (
<MyTag type="borderSolid" colorSchema="blue">
{t('chat:multiple_AI_conversations')}
</MyTag>
)}
{isPc && runningTime > 0 && (
<MyTooltip label={t('chat:module_runtime_and')}>
<MyTag colorSchema="purple" type="borderSolid" cursor={'default'}>
@@ -219,29 +230,32 @@ const ResponseTags = ({
</MyTag>
</MyTooltip>
)}
<MyTooltip label={t('common:core.chat.response.Read complete response tips')}>
<MyTag
colorSchema="gray"
type="borderSolid"
cursor={'pointer'}
onClick={onOpenWholeModal}
>
{t('common:core.chat.response.Read complete response')}
</MyTag>
</MyTooltip>
{notSharePage && (
<MyTooltip label={t('common:core.chat.response.Read complete response tips')}>
<MyTag
colorSchema="gray"
type="borderSolid"
cursor={'pointer'}
onClick={onOpenWholeModal}
>
{t('common:core.chat.response.Read complete response')}
</MyTag>
</MyTooltip>
)}
</Flex>
)}
{!!quoteModalData && (
<QuoteModal
{...quoteModalData}
showDetail={showDetail}
canEditDataset={notSharePage}
showRawSource={showRawSource}
onClose={() => setQuoteModalData(undefined)}
/>
)}
{isOpenContextModal && <ContextModal dataId={dataId} onClose={onCloseContextModal} />}
{isOpenWholeModal && (
<WholeResponseModal dataId={dataId} showDetail={showDetail} onClose={onCloseWholeModal} />
)}
{isOpenWholeModal && <WholeResponseModal dataId={dataId} onClose={onCloseWholeModal} />}
</>
);
};

View File

@@ -24,6 +24,7 @@ import QuestionTip from '@fastgpt/web/components/common/MyTooltip/QuestionTip';
import { useDeepCompareEffect } from 'ahooks';
import { VariableItemType } from '@fastgpt/global/core/app/type';
import MyTextarea from '@/components/common/Textarea/MyTextarea';
import MyNumberInput from '@fastgpt/web/components/common/Input/NumberInput';
export const VariableInputItem = ({
item,
@@ -64,14 +65,14 @@ export const VariableInputItem = ({
minH={40}
maxH={160}
bg={'myGray.50'}
{...register(item.key, {
{...register(`variables.${item.key}`, {
required: item.required
})}
/>
)}
{item.type === VariableInputEnum.textarea && (
<Textarea
{...register(item.key, {
{...register(`variables.${item.key}`, {
required: item.required
})}
rows={5}
@@ -82,9 +83,9 @@ export const VariableInputItem = ({
{item.type === VariableInputEnum.select && (
<Controller
key={item.key}
key={`variables.${item.key}`}
control={control}
name={item.key}
name={`variables.${item.key}`}
rules={{ required: item.required }}
render={({ field: { ref, value } }) => {
return (
@@ -96,7 +97,7 @@ export const VariableInputItem = ({
value: item.value
}))}
value={value}
onchange={(e) => setValue(item.key, e)}
onchange={(e) => setValue(`variables.${item.key}`, e)}
/>
);
}}
@@ -104,27 +105,19 @@ export const VariableInputItem = ({
)}
{item.type === VariableInputEnum.numberInput && (
<Controller
key={item.key}
key={`variables.${item.key}`}
control={control}
name={item.key}
name={`variables.${item.key}`}
rules={{ required: item.required, min: item.min, max: item.max }}
render={({ field: { ref, value, onChange } }) => (
<NumberInput
render={({ field: { value, onChange } }) => (
<MyNumberInput
step={1}
min={item.min}
max={item.max}
bg={'white'}
rounded={'md'}
clampValueOnBlur={false}
value={value}
onChange={(valueString) => onChange(Number(valueString))}
>
<NumberInputField ref={ref} bg={'white'} />
<NumberInputStepper>
<NumberIncrementStepper />
<NumberDecrementStepper />
</NumberInputStepper>
</NumberInput>
onChange={onChange}
/>
)}
/>
)}

View File

@@ -22,7 +22,7 @@ const WelcomeBox = ({ welcomeText }: { welcomeText: string }) => {
bg={'white'}
boxShadow={'0 0 8px rgba(0,0,0,0.15)'}
>
<Markdown source={`~~~guide \n${welcomeText}`} />
<Markdown source={`~~~guide \n${welcomeText}`} forbidZhFormat />
</Card>
</Box>
</Box>

View File

@@ -9,21 +9,22 @@ import { getFileIcon } from '@fastgpt/global/common/file/icon';
import { formatFileSize } from '@fastgpt/global/common/file/tools';
import { clone } from 'lodash';
import { getErrText } from '@fastgpt/global/common/error/utils';
import { Control, useFieldArray } from 'react-hook-form';
import { UseFieldArrayReturn } from 'react-hook-form';
import { ChatBoxInputFormType, UserInputFileItemType } from '../type';
import { AppFileSelectConfigType } from '@fastgpt/global/core/app/type';
import { documentFileType } from '@fastgpt/global/common/file/constants';
import { useSystemStore } from '@/web/common/system/useSystemStore';
import { OutLinkChatAuthProps } from '@fastgpt/global/support/permission/chat';
interface UseFileUploadOptions {
outLinkAuthData: any;
type UseFileUploadOptions = {
outLinkAuthData: OutLinkChatAuthProps;
chatId: string;
fileSelectConfig: AppFileSelectConfigType;
control: Control<ChatBoxInputFormType, any>;
}
fileCtrl: UseFieldArrayReturn<ChatBoxInputFormType, 'files', 'id'>;
};
export const useFileUpload = (props: UseFileUploadOptions) => {
const { outLinkAuthData, chatId, fileSelectConfig, control } = props;
const { outLinkAuthData, chatId, fileSelectConfig, fileCtrl } = props;
const { toast } = useToast();
const { t } = useTranslation();
const { feConfigs } = useSystemStore();
@@ -32,16 +33,16 @@ export const useFileUpload = (props: UseFileUploadOptions) => {
update: updateFiles,
remove: removeFiles,
fields: fileList,
replace: replaceFiles
} = useFieldArray({
control: control,
name: 'files'
});
replace: replaceFiles,
append: appendFiles
} = fileCtrl;
const hasFileUploading = fileList.some((item) => !item.url);
const showSelectFile = fileSelectConfig?.canSelectFile;
const showSelectImg = fileSelectConfig?.canSelectImg;
const maxSelectFiles = fileSelectConfig?.maxFiles ?? 10;
const maxSize = (feConfigs?.uploadFileMaxSize || 1024) * 1024 * 1024; // nkb
const canSelectFileAmount = maxSelectFiles - fileList.length;
const { icon: selectFileIcon, label: selectFileLabel } = useMemo(() => {
if (showSelectFile && showSelectImg) {
@@ -66,11 +67,11 @@ export const useFileUpload = (props: UseFileUploadOptions) => {
const { File, onOpen: onOpenSelectFile } = useSelectFile({
fileType: `${showSelectImg ? 'image/*,' : ''} ${showSelectFile ? documentFileType : ''}`,
multiple: true,
maxCount: maxSelectFiles
maxCount: canSelectFileAmount
});
const onSelectFile = useCallback(
async ({ files, fileList }: { files: File[]; fileList: UserInputFileItemType[] }) => {
async ({ files }: { files: File[] }) => {
if (!files || files.length === 0) {
return [];
}
@@ -129,22 +130,11 @@ export const useFileUpload = (props: UseFileUploadOptions) => {
)
);
// 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);
appendFiles(loadFiles);
return loadFiles;
},
[maxSelectFiles, replaceFiles, toast, t, maxSize]
[maxSelectFiles, appendFiles, toast, t, maxSize]
);
const uploadFiles = async () => {
@@ -198,10 +188,23 @@ export const useFileUpload = (props: UseFileUploadOptions) => {
removeFiles(errorFileIndex);
};
const sortFileList = useMemo(() => {
// Sort: Document, image
const sortResult = clone(fileList).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;
});
return sortResult;
}, [fileList]);
return {
File,
onOpenSelectFile,
fileList,
fileList: sortFileList,
onSelectFile,
uploadFiles,
selectFileIcon,
@@ -209,6 +212,7 @@ export const useFileUpload = (props: UseFileUploadOptions) => {
showSelectFile,
showSelectImg,
removeFiles,
replaceFiles
replaceFiles,
hasFileUploading
};
};

View File

@@ -67,6 +67,9 @@ import { useSystem } from '@fastgpt/web/hooks/useSystem';
import { useCreation, useMemoizedFn, useThrottleFn } from 'ahooks';
import MyIcon from '@fastgpt/web/components/common/Icon';
import { mergeChatResponseData } from '@fastgpt/global/core/chat/utils';
import { formatTimeToChatItemTime } from '@fastgpt/global/common/string/time';
import dayjs from 'dayjs';
import { getWebReqUrl } from '@fastgpt/web/common/system/utils';
const ResponseTags = dynamic(() => import('./components/ResponseTags'));
const FeedbackModal = dynamic(() => import('./components/FeedbackModal'));
@@ -108,6 +111,18 @@ type Props = OutLinkChatAuthProps &
onDelMessage?: (e: { contentId: string }) => void;
};
const ChatTimeBox = ({ time }: { time: Date }) => {
const { t } = useTranslation();
return (
<Box w={'100%'} fontSize={'mini'} textAlign={'center'} color={'myGray.500'} fontWeight={'400'}>
{t(formatTimeToChatItemTime(time) as any, {
time: dayjs(time).format('HH#mm')
}).replace('#', ':')}
</Box>
);
};
const ChatBox = (
{
feedbackType = FeedbackTypeEnum.hidden,
@@ -393,7 +408,7 @@ const ChatBox = (
isInteractivePrompt = false
}) => {
variablesForm.handleSubmit(
async (variables) => {
async ({ variables }) => {
if (!onStartChat) return;
if (isChatting) {
toast({
@@ -436,6 +451,7 @@ const ChatBox = (
{
dataId: getNanoid(24),
obj: ChatRoleEnum.Human,
time: new Date(),
value: [
...files.map((file) => ({
type: ChatItemValueTypeEnum.file,
@@ -510,6 +526,12 @@ const ChatBox = (
generatingMessage: (e) => generatingMessage({ ...e, autoTTSResponse }),
variables: requestVariables
});
if (responseData?.[responseData.length - 1]?.error) {
toast({
title: t(responseData[responseData.length - 1].error?.message),
status: 'error'
});
}
isNewChatReplace.current = isNewChat;
@@ -521,6 +543,7 @@ const ChatBox = (
return {
...item,
status: ChatStatusEnum.finish,
time: new Date(),
responseData: item.responseData
? mergeChatResponseData([...item.responseData, ...responseData])
: responseData
@@ -543,6 +566,7 @@ const ChatBox = (
// tts audio
autoTTSResponse && splitText2Audio(responseText, true);
} catch (err: any) {
console.log(err);
toast({
title: t(getErrText(err, 'core.chat.error.Chat error') as any),
status: 'error',
@@ -562,6 +586,7 @@ const ChatBox = (
if (index !== state.length - 1) return item;
return {
...item,
time: new Date(),
status: ChatStatusEnum.finish
};
})
@@ -877,88 +902,97 @@ const ChatBox = (
{/* chat history */}
<Box id={'history'}>
{chatHistories.map((item, index) => (
<Box key={item.dataId} py={5}>
{item.obj === ChatRoleEnum.Human && (
<ChatItem
type={item.obj}
avatar={userAvatar}
chat={item}
onRetry={retryInput(item.dataId)}
onDelete={delOneMessage(item.dataId)}
isLastChild={index === chatHistories.length - 1}
/>
)}
{item.obj === ChatRoleEnum.AI && (
<>
<>
{/* 并且时间和上一条的time相差超过十分钟 */}
{index !== 0 &&
item.time &&
chatHistories[index - 1].time !== undefined &&
new Date(item.time).getTime() -
new Date(chatHistories[index - 1].time!).getTime() >
10 * 60 * 1000 && <ChatTimeBox time={item.time} />}
<Box key={item.dataId} py={6}>
{item.obj === ChatRoleEnum.Human && (
<ChatItem
type={item.obj}
avatar={appAvatar}
avatar={userAvatar}
chat={item}
onRetry={retryInput(item.dataId)}
onDelete={delOneMessage(item.dataId)}
isLastChild={index === chatHistories.length - 1}
{...{
showVoiceIcon,
shareId,
outLinkUid,
teamId,
teamToken,
statusBoxData,
questionGuides,
onMark: onMark(
item,
formatChatValue2InputType(chatHistories[index - 1]?.value)?.text
),
onAddUserLike: onAddUserLike(item),
onCloseUserLike: onCloseUserLike(item),
onAddUserDislike: onAddUserDislike(item),
onReadUserDislike: onReadUserDislike(item)
}}
>
<ResponseTags
showTags={index !== chatHistories.length - 1 || !isChatting}
showDetail={!shareId && !teamId}
historyItem={item}
/>
/>
)}
{item.obj === ChatRoleEnum.AI && (
<>
<ChatItem
type={item.obj}
avatar={appAvatar}
chat={item}
isLastChild={index === chatHistories.length - 1}
{...{
showVoiceIcon,
shareId,
outLinkUid,
teamId,
teamToken,
statusBoxData,
questionGuides,
onMark: onMark(
item,
formatChatValue2InputType(chatHistories[index - 1]?.value)?.text
),
onAddUserLike: onAddUserLike(item),
onCloseUserLike: onCloseUserLike(item),
onAddUserDislike: onAddUserDislike(item),
onReadUserDislike: onReadUserDislike(item)
}}
>
<ResponseTags
showTags={index !== chatHistories.length - 1 || !isChatting}
historyItem={item}
/>
{/* custom feedback */}
{item.customFeedbacks && item.customFeedbacks.length > 0 && (
<Box>
<ChatBoxDivider
icon={'core/app/customFeedback'}
text={t('common:core.app.feedback.Custom feedback')}
/>
{item.customFeedbacks.map((text, i) => (
<Box key={i}>
<MyTooltip
label={t('common:core.app.feedback.close custom feedback')}
>
<Checkbox
onChange={onCloseCustomFeedback(item, i)}
icon={<MyIcon name={'common/check'} w={'12px'} />}
{/* custom feedback */}
{item.customFeedbacks && item.customFeedbacks.length > 0 && (
<Box>
<ChatBoxDivider
icon={'core/app/customFeedback'}
text={t('common:core.app.feedback.Custom feedback')}
/>
{item.customFeedbacks.map((text, i) => (
<Box key={i}>
<MyTooltip
label={t('common:core.app.feedback.close custom feedback')}
>
{text}
</Checkbox>
</MyTooltip>
</Box>
))}
</Box>
)}
{/* admin mark content */}
{showMarkIcon && item.adminFeedback && (
<Box fontSize={'sm'}>
<ChatBoxDivider
icon="core/app/markLight"
text={t('common:core.chat.Admin Mark Content')}
/>
<Box whiteSpace={'pre-wrap'}>
<Box color={'black'}>{item.adminFeedback.q}</Box>
<Box color={'myGray.600'}>{item.adminFeedback.a}</Box>
<Checkbox
onChange={onCloseCustomFeedback(item, i)}
icon={<MyIcon name={'common/check'} w={'12px'} />}
>
{text}
</Checkbox>
</MyTooltip>
</Box>
))}
</Box>
</Box>
)}
</ChatItem>
</>
)}
</Box>
)}
{/* admin mark content */}
{showMarkIcon && item.adminFeedback && (
<Box fontSize={'sm'}>
<ChatBoxDivider
icon="core/app/markLight"
text={t('common:core.chat.Admin Mark Content')}
/>
<Box whiteSpace={'pre-wrap'}>
<Box color={'black'}>{item.adminFeedback.q}</Box>
<Box color={'myGray.600'}>{item.adminFeedback.a}</Box>
</Box>
</Box>
)}
</ChatItem>
</>
)}
</Box>
</>
))}
</Box>
</Box>
@@ -996,7 +1030,7 @@ const ChatBox = (
return (
<Flex flexDirection={'column'} h={'100%'} position={'relative'}>
<Script src="/js/html2pdf.bundle.min.js" strategy="lazyOnload"></Script>
<Script src={getWebReqUrl('/js/html2pdf.bundle.min.js')} strategy="lazyOnload"></Script>
{/* chat box container */}
{RenderRecords}
{/* message input */}

View File

@@ -20,9 +20,9 @@ export type UserInputFileItemType = {
export type ChatBoxInputFormType = {
input: string;
files: UserInputFileItemType[];
files: UserInputFileItemType[]; // global files
chatStarted: boolean;
[key: string]: any;
variables: Record<string, any>;
};
export type ChatBoxInputType = {

View File

@@ -1,5 +1,5 @@
import React, { useCallback, useEffect, useMemo } from 'react';
import { Controller } from 'react-hook-form';
import React, { useCallback, useEffect, useMemo, useRef, useState } from 'react';
import { Controller, useFieldArray } from 'react-hook-form';
import RenderPluginInput from './renderPluginInput';
import { Box, Button, Flex } from '@chakra-ui/react';
import { useTranslation } from 'next-i18next';
@@ -14,7 +14,8 @@ import { useFileUpload } from '../../ChatBox/hooks/useFileUpload';
import FilePreview from '../../components/FilePreview';
import { UserChatItemValueItemType } from '@fastgpt/global/core/chat/type';
import QuestionTip from '@fastgpt/web/components/common/MyTooltip/QuestionTip';
import { ChatBoxInputFormType, UserInputFileItemType } from '../../ChatBox/type';
import { ChatBoxInputFormType } from '../../ChatBox/type';
import { FlowNodeInputItemType } from '@fastgpt/global/core/workflow/type/io';
const RenderInput = () => {
const { t } = useTranslation();
@@ -29,9 +30,7 @@ const RenderInput = () => {
isChatting,
chatConfig,
chatId,
outLinkAuthData,
restartInputStore,
setRestartInputStore
outLinkAuthData
} = useContextSelector(PluginRunContext, (v) => v);
const {
@@ -42,6 +41,11 @@ const RenderInput = () => {
formState: { errors }
} = variablesForm;
/* ===> Global files(abandon) */
const fileCtrl = useFieldArray({
control: variablesForm.control,
name: 'files'
});
const {
File,
onOpenSelectFile,
@@ -52,46 +56,76 @@ const RenderInput = () => {
showSelectFile,
showSelectImg,
removeFiles,
replaceFiles
hasFileUploading
} = useFileUpload({
outLinkAuthData,
chatId: chatId || '',
fileSelectConfig: chatConfig?.fileSelectConfig,
control
fileCtrl
});
const isDisabledInput = histories.length > 0;
useRequest2(uploadFiles, {
manual: false,
errorToast: t('common:upload_file_error'),
refreshDeps: [fileList, outLinkAuthData, chatId]
});
/* Global files(abandon) <=== */
const [restartData, setRestartData] = useState<ChatBoxInputFormType>();
const onClickNewChat = useCallback(
(e: ChatBoxInputFormType, files: UserInputFileItemType[] = []) => {
setRestartInputStore({
...e,
files
});
(e: ChatBoxInputFormType) => {
setRestartData(e);
onNewChat?.();
},
[onNewChat, setRestartInputStore]
[onNewChat, setRestartData]
);
// Get plugin input components
const formatPluginInputs = useMemo(() => {
if (histories.length === 0) return pluginInputs;
try {
const historyValue = histories[0]?.value as UserChatItemValueItemType[];
const inputValueString = historyValue.find((item) => item.type === 'text')?.text?.content;
if (!inputValueString) return pluginInputs;
return JSON.parse(inputValueString) as FlowNodeInputItemType[];
} catch (error) {
console.error('Failed to parse input value:', error);
return pluginInputs;
}
}, [histories, pluginInputs]);
// Reset input value
useEffect(() => {
// Set last run value
if (!isDisabledInput && restartInputStore) {
reset(restartInputStore);
// Set config default value
if (histories.length === 0) {
if (restartData) {
reset(restartData);
setRestartData(undefined);
return;
}
const defaultFormValues = formatPluginInputs.reduce(
(acc, input) => {
acc[input.key] = input.defaultValue;
return acc;
},
{} as Record<string, any>
);
reset({
files: [],
variables: defaultFormValues
});
return;
}
// Set history to default value
const historyVariables = (() => {
const historyValue = histories[0]?.value as UserChatItemValueItemType[];
if (!historyValue) return undefined;
const defaultFormValues = pluginInputs.reduce(
(acc, input) => {
acc[input.key] = input.defaultValue;
return acc;
},
{} as Record<string, any>
);
const historyFormValues = (() => {
if (!isDisabledInput) return undefined;
const historyValue = histories[0].value;
try {
const inputValueString = historyValue.find((item) => item.type === 'text')?.text?.content;
return (
@@ -115,32 +149,25 @@ const RenderInput = () => {
return undefined;
}
})();
// Parse history file
const historyFileList = (() => {
if (!isDisabledInput) return [];
const historyValue = histories[0].value as UserChatItemValueItemType[];
return historyValue.filter((item) => item.type === 'file').map((item) => item.file);
const historyValue = histories[0]?.value as UserChatItemValueItemType[];
return historyValue?.filter((item) => item.type === 'file').map((item) => item.file);
})();
reset({
...(historyFormValues || defaultFormValues),
variables: historyVariables,
files: historyFileList
});
}, [getValues, histories, isDisabledInput, pluginInputs, replaceFiles, reset, restartInputStore]);
// eslint-disable-next-line react-hooks/exhaustive-deps
}, [histories]);
const hasFileUploading = useMemo(() => {
return fileList.some((item) => !item.url);
}, [fileList]);
const [uploading, setUploading] = useState(false);
useRequest2(uploadFiles, {
manual: false,
errorToast: t('common:upload_file_error'),
refreshDeps: [fileList, outLinkAuthData, chatId]
});
const fileUploading = uploading || hasFileUploading;
return (
<>
<Box>
{/* instruction */}
{chatConfig?.instruction && (
<Box
@@ -155,7 +182,7 @@ const RenderInput = () => {
<Markdown source={chatConfig.instruction} />
</Box>
)}
{/* file select */}
{/* file select(Abandoned) */}
{(showSelectFile || showSelectImg) && (
<Box mb={5}>
<Flex alignItems={'center'}>
@@ -175,7 +202,7 @@ const RenderInput = () => {
{t('chat:select')}
</Button>
)}
<File onSelect={(files) => onSelectFile({ files, fileList })} />
<File onSelect={(files) => onSelectFile({ files })} />
</Flex>
<FilePreview
fileList={fileList}
@@ -184,12 +211,12 @@ const RenderInput = () => {
</Box>
)}
{/* Filed */}
{pluginInputs.map((input) => {
{formatPluginInputs.map((input) => {
return (
<Controller
key={input.key}
key={`variables.${input.key}`}
control={control}
name={input.key}
name={`variables.${input.key}`}
rules={{
validate: (value) => {
if (!input.required) return true;
@@ -207,6 +234,7 @@ const RenderInput = () => {
isDisabled={isDisabledInput}
isInvalid={errors && Object.keys(errors).includes(input.key)}
input={input}
setUploading={setUploading}
/>
);
}}
@@ -217,13 +245,14 @@ const RenderInput = () => {
{onStartChat && onNewChat && (
<Flex justifyContent={'end'} mt={8}>
<Button
isLoading={isChatting || hasFileUploading}
isLoading={isChatting}
isDisabled={fileUploading}
onClick={() => {
handleSubmit((e) => {
if (isDisabledInput) {
onClickNewChat(e, fileList);
onClickNewChat(e);
} else {
onSubmit(e, fileList);
onSubmit(e);
}
})();
}}
@@ -232,7 +261,7 @@ const RenderInput = () => {
</Button>
</Flex>
)}
</>
</Box>
);
};

View File

@@ -13,7 +13,7 @@ const RenderResponseDetail = () => {
<>{t('chat:in_progress')}</>
) : (
<Box flex={'1 0 0'} h={'100%'} overflow={'auto'}>
<ResponseBox useMobile={true} response={responseData} showDetail={true} />
<ResponseBox useMobile={true} response={responseData} />
</Box>
);
};

View File

@@ -1,36 +1,150 @@
import {
Box,
Flex,
NumberDecrementStepper,
NumberIncrementStepper,
NumberInput,
NumberInputField,
NumberInputStepper,
Switch,
Textarea
} from '@chakra-ui/react';
import { Box, Button, Flex, Switch, Textarea } from '@chakra-ui/react';
import { WorkflowIOValueTypeEnum } from '@fastgpt/global/core/workflow/constants';
import { FlowNodeInputTypeEnum } from '@fastgpt/global/core/workflow/node/constant';
import { FlowNodeInputItemType } from '@fastgpt/global/core/workflow/type/io';
import FormLabel from '@fastgpt/web/components/common/MyBox/FormLabel';
import MySelect from '@fastgpt/web/components/common/MySelect';
import QuestionTip from '@fastgpt/web/components/common/MyTooltip/QuestionTip';
import { useTranslation } from 'next-i18next';
import dynamic from 'next/dynamic';
import { useFileUpload } from '../../ChatBox/hooks/useFileUpload';
import { useContextSelector } from 'use-context-selector';
import { PluginRunContext } from '../context';
import MyIcon from '@fastgpt/web/components/common/Icon';
import FilePreview from '../../components/FilePreview';
import { useRequest2 } from '@fastgpt/web/hooks/useRequest';
import { useEffect, useMemo } from 'react';
import EmptyTip from '@fastgpt/web/components/common/EmptyTip';
import { useFieldArray } from 'react-hook-form';
import MyNumberInput from '@fastgpt/web/components/common/Input/NumberInput';
import { isEqual } from 'lodash';
const JsonEditor = dynamic(() => import('@fastgpt/web/components/common/Textarea/JsonEditor'));
const FileSelector = ({
input,
setUploading,
onChange,
value
}: {
input: FlowNodeInputItemType;
setUploading: React.Dispatch<React.SetStateAction<boolean>>;
onChange: (...event: any[]) => void;
value: any;
}) => {
const { t } = useTranslation();
const { variablesForm, histories, chatId, outLinkAuthData } = useContextSelector(
PluginRunContext,
(v) => v
);
const fileCtrl = useFieldArray({
control: variablesForm.control,
name: `variables.${input.key}`
});
const {
File,
fileList,
selectFileIcon,
uploadFiles,
onOpenSelectFile,
onSelectFile,
removeFiles,
replaceFiles,
hasFileUploading
} = useFileUpload({
outLinkAuthData,
chatId: chatId || '',
fileSelectConfig: {
canSelectFile: input.canSelectFile ?? true,
canSelectImg: input.canSelectImg ?? false,
maxFiles: input.maxFiles ?? 5
},
// @ts-ignore
fileCtrl
});
useEffect(() => {
if (!Array.isArray(value)) {
replaceFiles([]);
return;
}
// compare file names and update if different
const valueFileNames = value.map((item) => item.name);
const currentFileNames = fileList.map((item) => item.name);
if (!isEqual(valueFileNames, currentFileNames)) {
replaceFiles(value);
}
// eslint-disable-next-line react-hooks/exhaustive-deps
}, [value]);
const isDisabledInput = histories.length > 0;
useRequest2(uploadFiles, {
manual: false,
errorToast: t('common:upload_file_error'),
refreshDeps: [fileList, outLinkAuthData, chatId]
});
useEffect(() => {
setUploading(hasFileUploading);
onChange(
fileList.map((item) => ({
type: item.type,
name: item.name,
url: item.url,
icon: item.icon
}))
);
}, [fileList, hasFileUploading, onChange, setUploading]);
return (
<>
<Flex alignItems={'center'}>
<Box position={'relative'}>
{input.required && (
<Box position={'absolute'} left={-2} top={'-1px'} color={'red.600'}>
*
</Box>
)}
<FormLabel fontWeight={'500'}>{t(input.label as any)}</FormLabel>
</Box>
{input.description && <QuestionTip ml={2} label={t(input.description as any)} />}
<Box flex={1} />
{/* 有历史记录,说明是已经跑过了,不能再新增了 */}
<Button
isDisabled={histories.length !== 0}
leftIcon={<MyIcon name={selectFileIcon as any} w={'16px'} />}
variant={'whiteBase'}
onClick={() => {
onOpenSelectFile();
}}
>
{t('chat:select')}
</Button>
</Flex>
<FilePreview fileList={fileList} removeFiles={isDisabledInput ? undefined : removeFiles} />
{fileList.length === 0 && <EmptyTip py={0} mt={3} text={t('chat:not_select_file')} />}
<File onSelect={(files) => onSelectFile({ files })} />
</>
);
};
const RenderPluginInput = ({
value,
onChange,
isDisabled,
isInvalid,
input
input,
setUploading
}: {
value: any;
onChange: () => void;
onChange: (...event: any[]) => void;
isDisabled?: boolean;
isInvalid: boolean;
input: FlowNodeInputItemType;
setUploading: React.Dispatch<React.SetStateAction<boolean>>;
}) => {
const { t } = useTranslation();
const inputType = input.renderTypeList[0];
@@ -44,6 +158,12 @@ const RenderPluginInput = ({
<MySelect list={input.list} value={value} onchange={onChange} isDisabled={isDisabled} />
);
}
if (inputType === FlowNodeInputTypeEnum.fileSelect) {
return (
<FileSelector onChange={onChange} input={input} setUploading={setUploading} value={value} />
);
}
if (input.valueType === WorkflowIOValueTypeEnum.string) {
return (
<Textarea
@@ -59,20 +179,17 @@ const RenderPluginInput = ({
}
if (input.valueType === WorkflowIOValueTypeEnum.number) {
return (
<NumberInput
<MyNumberInput
step={1}
min={input.min}
max={input.max}
bg={'myGray.50'}
isDisabled={isDisabled}
isInvalid={isInvalid}
>
<NumberInputField value={value} onChange={onChange} defaultValue={input.defaultValue} />
<NumberInputStepper>
<NumberIncrementStepper />
<NumberDecrementStepper />
</NumberInputStepper>
</NumberInput>
value={value}
onChange={onChange}
defaultValue={input.defaultValue}
/>
);
}
if (input.valueType === WorkflowIOValueTypeEnum.boolean) {
@@ -100,22 +217,26 @@ const RenderPluginInput = ({
);
})();
return !!render ? (
<Box _notLast={{ mb: 4 }} px={1}>
<Flex alignItems={'center'} mb={1}>
<Box position={'relative'}>
{input.required && (
<Box position={'absolute'} left={-2} top={'-1px'} color={'red.600'}>
*
</Box>
)}
{t(input.label as any)}
</Box>
{input.description && <QuestionTip ml={2} label={t(input.description as any)} />}
</Flex>
return (
<Box _notLast={{ mb: 4 }}>
{/* label */}
{inputType !== FlowNodeInputTypeEnum.fileSelect && (
<Flex alignItems={'center'} mb={1}>
<Box position={'relative'}>
{input.required && (
<Box position={'absolute'} left={-2} top={'-1px'} color={'red.600'}>
*
</Box>
)}
<FormLabel fontWeight={'500'}>{t(input.label as any)}</FormLabel>
</Box>
{input.description && <QuestionTip ml={2} label={t(input.description as any)} />}
</Flex>
)}
{render}
</Box>
) : null;
);
};
export default RenderPluginInput;

View File

@@ -1,4 +1,4 @@
import React, { ReactNode, useCallback, useMemo, useRef, useState } from 'react';
import React, { ReactNode, useCallback, useMemo, useRef } from 'react';
import { createContext } from 'use-context-selector';
import { PluginRunBoxProps } from './type';
import {
@@ -8,7 +8,6 @@ import {
} from '@fastgpt/global/core/chat/type';
import { FieldValues, useForm } from 'react-hook-form';
import { PluginRunBoxTabEnum } from './constants';
import { useRequest2 } from '@fastgpt/web/hooks/useRequest';
import { useToast } from '@fastgpt/web/hooks/useToast';
import { getNanoid } from '@fastgpt/global/common/string/tools';
import { ChatItemValueTypeEnum, ChatRoleEnum } from '@fastgpt/global/core/chat/constants';
@@ -16,17 +15,16 @@ import { generatingMessageProps } from '../type';
import { SseResponseEventEnum } from '@fastgpt/global/core/workflow/runtime/constants';
import { useTranslation } from 'next-i18next';
import { OutLinkChatAuthProps } from '@fastgpt/global/support/permission/chat';
import { ChatBoxInputFormType, UserInputFileItemType } from '../ChatBox/type';
import { ChatBoxInputFormType } from '../ChatBox/type';
import { chats2GPTMessages } from '@fastgpt/global/core/chat/adapt';
import { getPluginRunUserQuery } from '@fastgpt/global/core/workflow/utils';
import { cloneDeep } from 'lodash';
type PluginRunContextType = OutLinkChatAuthProps &
PluginRunBoxProps & {
isChatting: boolean;
onSubmit: (e: ChatBoxInputFormType, files?: UserInputFileItemType[]) => Promise<any>;
onSubmit: (e: ChatBoxInputFormType) => Promise<any>;
outLinkAuthData: OutLinkChatAuthProps;
restartInputStore?: ChatBoxInputFormType;
setRestartInputStore: React.Dispatch<React.SetStateAction<ChatBoxInputFormType | undefined>>;
};
export const PluginRunContext = createContext<PluginRunContextType>({
@@ -59,8 +57,6 @@ const PluginRunContextProvider = ({
}: PluginRunBoxProps & { children: ReactNode }) => {
const { pluginInputs, onStartChat, setHistories, histories, setTab } = props;
const [restartInputStore, setRestartInputStore] = useState<ChatBoxInputFormType>();
const { toast } = useToast();
const chatController = useRef(new AbortController());
const { t } = useTranslation();
@@ -80,9 +76,7 @@ const PluginRunContextProvider = ({
);
const variablesForm = useForm<ChatBoxInputFormType>({
defaultValues: {
files: []
}
defaultValues: {}
});
const generatingMessage = useCallback(
@@ -179,8 +173,8 @@ const PluginRunContextProvider = ({
[histories]
);
const { runAsync: onSubmit } = useRequest2(
async (e: ChatBoxInputFormType, files?: UserInputFileItemType[]) => {
const onSubmit = useCallback(
async ({ variables, files }: ChatBoxInputFormType) => {
if (!onStartChat) return;
if (isChatting) {
toast({
@@ -199,7 +193,7 @@ const PluginRunContextProvider = ({
{
...getPluginRunUserQuery({
pluginInputs,
variables: e,
variables,
files: files as RuntimeUserPromptType['files']
}),
status: 'finish'
@@ -233,12 +227,33 @@ const PluginRunContextProvider = ({
});
try {
// Remove files icon
const formatVariables = cloneDeep(variables);
for (const key in formatVariables) {
if (Array.isArray(formatVariables[key])) {
formatVariables[key].forEach((item) => {
if (item.url && item.icon) {
delete item.icon;
}
});
}
}
const { responseData } = await onStartChat({
messages: messages,
messages,
controller: chatController.current,
generatingMessage,
variables: e
variables: {
files,
...formatVariables
}
});
if (responseData?.[responseData.length - 1]?.error) {
toast({
title: responseData[responseData.length - 1].error?.message,
status: 'error'
});
}
setHistories((state) =>
state.map((item, index) => {
@@ -262,7 +277,18 @@ const PluginRunContextProvider = ({
})
);
}
}
},
[
abortRequest,
generatingMessage,
isChatting,
onStartChat,
pluginInputs,
setHistories,
setTab,
t,
toast
]
);
const contextValue: PluginRunContextType = {
@@ -270,9 +296,7 @@ const PluginRunContextProvider = ({
isChatting,
onSubmit,
outLinkAuthData,
variablesForm,
restartInputStore,
setRestartInputStore
variablesForm
};
return <PluginRunContext.Provider value={contextValue}>{children}</PluginRunContext.Provider>;
};

View File

@@ -6,6 +6,7 @@ import MyBox from '@fastgpt/web/components/common/MyBox';
import MyIcon from '@fastgpt/web/components/common/Icon';
import { ChatFileTypeEnum } from '@fastgpt/global/core/chat/constants';
import { useSystem } from '@fastgpt/web/hooks/useSystem';
import MyImage from '@fastgpt/web/components/common/Image/MyImage';
const RenderFilePreview = ({
fileList,
@@ -18,13 +19,12 @@ const RenderFilePreview = ({
return fileList.length > 0 ? (
<Flex
maxH={'250px'}
overflowY={'auto'}
overflow={'visible'}
wrap={'wrap'}
pt={3}
userSelect={'none'}
mb={fileList.length > 0 ? 2 : 0}
pr={0.5}
gap={'6px'}
>
{fileList.map((item, index) => {
const isFile = item.type === ChatFileTypeEnum.file;
@@ -33,11 +33,8 @@ const RenderFilePreview = ({
<MyBox
key={index}
maxW={isFile ? 56 : 14}
w={isFile ? '50%' : '12.5%'}
w={isFile ? 'calc(50% - 3px)' : '12.5%'}
aspectRatio={isFile ? 4 : 1}
pr={1.5}
pb={1.5}
mb={0.5}
>
<Box
border={'sm'}
@@ -74,7 +71,7 @@ const RenderFilePreview = ({
/>
)}
{isImage && (
<Image
<MyImage
alt={'img'}
src={item.icon}
w={'full'}

View File

@@ -28,13 +28,24 @@ export const useChat = (params?: { chatId?: string; appId: string; type?: GetCha
// Reset to empty input
const data = variablesForm.getValues();
for (const key in data) {
data[key] = '';
// Reset the old variables to empty
const resetVariables: Record<string, any> = {};
for (const key in data.variables) {
resetVariables[key] = (() => {
if (Array.isArray(data.variables[key])) {
return [];
}
return '';
})();
}
variablesForm.reset({
...data,
...variables
variables: {
...resetVariables,
...variables
}
});
},
[variablesForm]
@@ -42,8 +53,8 @@ export const useChat = (params?: { chatId?: string; appId: string; type?: GetCha
const clearChatRecords = useCallback(() => {
const data = variablesForm.getValues();
for (const key in data) {
variablesForm.setValue(key, '');
for (const key in data.variables) {
variablesForm.setValue(`variables.${key}`, '');
}
ChatBoxRef.current?.restartChat?.();

View File

@@ -39,6 +39,7 @@ import { useTranslation } from 'react-i18next';
import { Controller, useForm } from 'react-hook-form';
import MySelect from '@fastgpt/web/components/common/MySelect';
import MyTextarea from '@/components/common/Textarea/MyTextarea';
import MyNumberInput from '@fastgpt/web/components/common/Input/NumberInput';
type props = {
value: UserChatItemValueItemType | AIChatItemValueItemType;
@@ -244,25 +245,15 @@ const RenderUserFormInteractive = React.memo(function RenderFormInput({
/>
)}
{input.type === FlowNodeInputTypeEnum.numberInput && (
<NumberInput
step={1}
<MyNumberInput
min={input.min}
max={input.max}
isDisabled={interactive.params.submitted}
bg={'white'}
rounded={'md'}
>
<NumberInputField
bg={'white'}
{...register(input.label, {
required: input.required
})}
/>
<NumberInputStepper>
<NumberIncrementStepper />
<NumberDecrementStepper />
</NumberInputStepper>
</NumberInput>
register={register}
name={input.label}
isRequired={input.required}
/>
)}
{input.type === FlowNodeInputTypeEnum.select && (
<Controller

View File

@@ -31,12 +31,10 @@ type sideTabItemType = {
/* Per response value */
export const WholeResponseContent = ({
activeModule,
hideTabs,
showDetail
hideTabs
}: {
activeModule: ChatHistoryItemResType;
hideTabs?: boolean;
showDetail: boolean;
}) => {
const { t } = useTranslation();
@@ -233,10 +231,14 @@ export const WholeResponseContent = ({
{activeModule.quoteList && activeModule.quoteList.length > 0 && (
<Row
label={t('common:core.chat.response.module quoteList')}
rawDom={<QuoteList showDetail={showDetail} rawSearch={activeModule.quoteList} />}
rawDom={<QuoteList canEditDataset canViewSource rawSearch={activeModule.quoteList} />}
/>
)}
</>
{/* dataset concat */}
<>
<Row label={t('chat:response.dataset_concat_length')} value={activeModule?.concatLength} />
</>
{/* classify question */}
<>
<Row
@@ -527,12 +529,10 @@ const SideTabItem = ({
/* Modal main container */
export const ResponseBox = React.memo(function ResponseBox({
response,
showDetail,
hideTabs = false,
useMobile = false
}: {
response: ChatHistoryItemResType[];
showDetail: boolean;
hideTabs?: boolean;
useMobile?: boolean;
}) {
@@ -655,11 +655,7 @@ export const ResponseBox = React.memo(function ResponseBox({
</Box>
</Box>
<Box flex={'5 0 0'} w={0} height={'100%'}>
<WholeResponseContent
activeModule={activeModule}
hideTabs={hideTabs}
showDetail={showDetail}
/>
<WholeResponseContent activeModule={activeModule} hideTabs={hideTabs} />
</Box>
</Flex>
) : (
@@ -719,11 +715,7 @@ export const ResponseBox = React.memo(function ResponseBox({
</Box>
</Flex>
<Box flex={'1 0 0'}>
<WholeResponseContent
activeModule={activeModule}
hideTabs={hideTabs}
showDetail={showDetail}
/>
<WholeResponseContent activeModule={activeModule} hideTabs={hideTabs} />
</Box>
</Flex>
)}
@@ -733,15 +725,7 @@ export const ResponseBox = React.memo(function ResponseBox({
);
});
const WholeResponseModal = ({
showDetail,
onClose,
dataId
}: {
showDetail: boolean;
onClose: () => void;
dataId: string;
}) => {
const WholeResponseModal = ({ onClose, dataId }: { onClose: () => void; dataId: string }) => {
const { t } = useTranslation();
const { getHistoryResponseData } = useContextSelector(ChatBoxContext, (v) => v);
@@ -770,7 +754,7 @@ const WholeResponseModal = ({
}
>
{!!response?.length ? (
<ResponseBox response={response} showDetail={showDetail} />
<ResponseBox response={response} />
) : (
<EmptyTip text={t('chat:no_workflow_response')} />
)}

View File

@@ -45,11 +45,11 @@ const scoreTheme: Record<
const QuoteItem = ({
quoteItem,
canViewSource,
linkToDataset
canEditDataset
}: {
quoteItem: SearchDataResponseItemType;
canViewSource?: boolean;
linkToDataset?: boolean;
canEditDataset?: boolean;
}) => {
const { t } = useTranslation();
const [editInputData, setEditInputData] = useState<{ dataId: string; collectionId: string }>();
@@ -110,89 +110,64 @@ const QuoteItem = ({
>
<Flex alignItems={'center'} mb={3} flexWrap={'wrap'} gap={3}>
{score?.primaryScore && (
<>
{canViewSource ? (
<MyTooltip label={t(SearchScoreTypeMap[score.primaryScore.type]?.desc as any)}>
<Flex
px={'12px'}
py={'5px'}
borderRadius={'md'}
color={'primary.700'}
bg={'primary.50'}
borderWidth={'1px'}
borderColor={'primary.200'}
alignItems={'center'}
fontSize={'sm'}
>
<Box>#{score.primaryScore.index + 1}</Box>
<Box
borderRightColor={'primary.700'}
borderRightWidth={'1px'}
h={'14px'}
mx={2}
/>
<Box>
{t(SearchScoreTypeMap[score.primaryScore.type]?.label as any)}
{SearchScoreTypeMap[score.primaryScore.type]?.showScore
? ` ${score.primaryScore.value.toFixed(4)}`
: ''}
</Box>
</Flex>
</MyTooltip>
) : (
<Flex
px={'12px'}
py={'1px'}
mr={4}
borderRadius={'md'}
color={'primary.700'}
bg={'primary.50'}
borderWidth={'1px'}
borderColor={'primary.200'}
alignItems={'center'}
fontSize={'sm'}
>
<Box>#{score.primaryScore.index + 1}</Box>
</Flex>
)}
</>
)}
{canViewSource &&
score.secondaryScore.map((item, i) => (
<MyTooltip key={item.type} label={t(SearchScoreTypeMap[item.type]?.desc as any)}>
<Box fontSize={'xs'}>
<Flex alignItems={'flex-start'} lineHeight={1.2} mb={1}>
<Box
px={'5px'}
borderWidth={'1px'}
borderRadius={'sm'}
mr={'2px'}
{...(scoreTheme[i] && scoreTheme[i])}
>
<Box transform={'scale(0.9)'}>#{item.index + 1}</Box>
</Box>
<Box transform={'scale(0.9)'}>
{t(SearchScoreTypeMap[item.type]?.label as any)}: {item.value.toFixed(4)}
</Box>
</Flex>
<Box h={'4px'}>
{SearchScoreTypeMap[item.type]?.showScore && (
<Progress
value={item.value * 100}
h={'4px'}
w={'100%'}
size="sm"
borderRadius={'20px'}
{...(scoreTheme[i] && {
colorScheme: scoreTheme[i].colorScheme
})}
bg="#E8EBF0"
/>
)}
</Box>
<MyTooltip label={t(SearchScoreTypeMap[score.primaryScore.type]?.desc as any)}>
<Flex
px={'12px'}
py={'5px'}
borderRadius={'md'}
color={'primary.700'}
bg={'primary.50'}
borderWidth={'1px'}
borderColor={'primary.200'}
alignItems={'center'}
fontSize={'sm'}
>
<Box>#{score.primaryScore.index + 1}</Box>
<Box borderRightColor={'primary.700'} borderRightWidth={'1px'} h={'14px'} mx={2} />
<Box>
{t(SearchScoreTypeMap[score.primaryScore.type]?.label as any)}
{SearchScoreTypeMap[score.primaryScore.type]?.showScore
? ` ${score.primaryScore.value.toFixed(4)}`
: ''}
</Box>
</MyTooltip>
))}
</Flex>
</MyTooltip>
)}
{score.secondaryScore.map((item, i) => (
<MyTooltip key={item.type} label={t(SearchScoreTypeMap[item.type]?.desc as any)}>
<Box fontSize={'xs'}>
<Flex alignItems={'flex-start'} lineHeight={1.2} mb={1}>
<Box
px={'5px'}
borderWidth={'1px'}
borderRadius={'sm'}
mr={'2px'}
{...(scoreTheme[i] && scoreTheme[i])}
>
<Box transform={'scale(0.9)'}>#{item.index + 1}</Box>
</Box>
<Box transform={'scale(0.9)'}>
{t(SearchScoreTypeMap[item.type]?.label as any)}: {item.value.toFixed(4)}
</Box>
</Flex>
<Box h={'4px'}>
{SearchScoreTypeMap[item.type]?.showScore && (
<Progress
value={item.value * 100}
h={'4px'}
w={'100%'}
size="sm"
borderRadius={'20px'}
{...(scoreTheme[i] && {
colorScheme: scoreTheme[i].colorScheme
})}
bg="#E8EBF0"
/>
)}
</Box>
</Box>
</MyTooltip>
))}
</Flex>
<Box flex={'1 0 0'}>
@@ -200,73 +175,71 @@ const QuoteItem = ({
<Box color={'myGray.600'}>{quoteItem.a}</Box>
</Box>
{canViewSource && (
<Flex
alignItems={'center'}
flexWrap={'wrap'}
mt={3}
gap={4}
color={'myGray.500'}
fontSize={'xs'}
>
<MyTooltip label={t('common:core.dataset.Quote Length')}>
<Flex alignItems={'center'}>
<MyIcon name="common/text/t" w={'14px'} mr={1} color={'myGray.500'} />
{quoteItem.q.length + (quoteItem.a?.length || 0)}
</Flex>
</MyTooltip>
<RawSourceBox
fontWeight={'bold'}
color={'black'}
collectionId={quoteItem.collectionId}
sourceName={quoteItem.sourceName}
sourceId={quoteItem.sourceId}
canView={canViewSource}
/>
<Box flex={1} />
{quoteItem.id && (
<MyTooltip label={t('common:core.dataset.data.Edit')}>
<Box
className="hover-data"
visibility={'hidden'}
display={'flex'}
alignItems={'center'}
justifyContent={'center'}
>
<MyIcon
name={'edit'}
w={['16px', '18px']}
h={['16px', '18px']}
cursor={'pointer'}
color={'myGray.600'}
_hover={{
color: 'primary.600'
}}
onClick={() =>
setEditInputData({
dataId: quoteItem.id,
collectionId: quoteItem.collectionId
})
}
/>
</Box>
</MyTooltip>
)}
{linkToDataset && (
<Link
as={NextLink}
<Flex
alignItems={'center'}
flexWrap={'wrap'}
mt={3}
gap={4}
color={'myGray.500'}
fontSize={'xs'}
>
<MyTooltip label={t('common:core.dataset.Quote Length')}>
<Flex alignItems={'center'}>
<MyIcon name="common/text/t" w={'14px'} mr={1} color={'myGray.500'} />
{quoteItem.q.length + (quoteItem.a?.length || 0)}
</Flex>
</MyTooltip>
<RawSourceBox
fontWeight={'bold'}
color={'black'}
collectionId={quoteItem.collectionId}
sourceName={quoteItem.sourceName}
sourceId={quoteItem.sourceId}
canView={canViewSource}
/>
<Box flex={1} />
{quoteItem.id && canEditDataset && (
<MyTooltip label={t('common:core.dataset.data.Edit')}>
<Box
className="hover-data"
visibility={'hidden'}
display={'flex'}
alignItems={'center'}
color={'primary.500'}
href={`/dataset/detail?datasetId=${quoteItem.datasetId}&currentTab=dataCard&collectionId=${quoteItem.collectionId}`}
justifyContent={'center'}
>
{t('common:core.dataset.Go Dataset')}
<MyIcon name={'common/rightArrowLight'} w={'10px'} />
</Link>
)}
</Flex>
)}
<MyIcon
name={'edit'}
w={['16px', '18px']}
h={['16px', '18px']}
cursor={'pointer'}
color={'myGray.600'}
_hover={{
color: 'primary.600'
}}
onClick={() =>
setEditInputData({
dataId: quoteItem.id,
collectionId: quoteItem.collectionId
})
}
/>
</Box>
</MyTooltip>
)}
{canEditDataset && (
<Link
as={NextLink}
className="hover-data"
visibility={'hidden'}
alignItems={'center'}
color={'primary.500'}
href={`/dataset/detail?datasetId=${quoteItem.datasetId}&currentTab=dataCard&collectionId=${quoteItem.collectionId}`}
>
{t('common:core.dataset.Go Dataset')}
<MyIcon name={'common/rightArrowLight'} w={'10px'} />
</Link>
)}
</Flex>
</MyBox>
{editInputData && (

View File

@@ -6,19 +6,23 @@ import { getCollectionSourceAndOpen } from '@/web/core/dataset/hooks/readCollect
import { getSourceNameIcon } from '@fastgpt/global/core/dataset/utils';
import MyIcon from '@fastgpt/web/components/common/Icon';
import { useI18n } from '@/web/context/I18n';
import { ShareChatAuthProps } from '@fastgpt/global/support/permission/chat';
type Props = BoxProps & {
sourceName?: string;
collectionId: string;
sourceId?: string;
canView?: boolean;
};
type Props = BoxProps &
ShareChatAuthProps & {
sourceName?: string;
collectionId: string;
sourceId?: string;
canView?: boolean;
};
const RawSourceBox = ({
sourceId,
collectionId,
sourceName = '',
canView = true,
shareId,
outLinkUid,
...props
}: Props) => {
const { t } = useTranslation();
@@ -27,7 +31,11 @@ const RawSourceBox = ({
const canPreview = !!sourceId && canView;
const icon = useMemo(() => getSourceNameIcon({ sourceId, sourceName }), [sourceId, sourceName]);
const read = getCollectionSourceAndOpen(collectionId);
const read = getCollectionSourceAndOpen({
collectionId,
shareId,
outLinkUid
});
return (
<MyTooltip

View File

@@ -101,7 +101,7 @@ const LafAccountModal = ({
<Box fontSize={'sm'} color={'myGray.500'}>
<Box>{t('common:support.user.Laf account intro')}</Box>
<Box textDecoration={'underline'}>
<Link href={getDocPath('/docs/workflow/modules/laf/')} isExternal>
<Link href={getDocPath('/docs/guide/workbench/workflow/laf/')} isExternal>
{t('common:support.user.Laf account course')}
</Link>
</Box>

View File

@@ -61,6 +61,7 @@ const DefaultPermissionList = ({
}
}}
fontSize={styles?.fontSize}
fontWeight={styles?.fontWeight}
/>
</Box>
<ConfirmModal />

View File

@@ -1,5 +1,6 @@
import { getCaptchaPic } from '@/web/support/user/api';
import { Button, Input, Image, ModalBody, ModalFooter, Skeleton } from '@chakra-ui/react';
import MyImage from '@fastgpt/web/components/common/Image/MyImage';
import MyModal from '@fastgpt/web/components/common/MyModal';
import { useRequest2 } from '@fastgpt/web/hooks/useRequest';
import { useTranslation } from 'next-i18next';
@@ -42,7 +43,7 @@ const SendCodeAuthModal = ({
justifyContent={'center'}
my={1}
>
<Image
<MyImage
borderRadius={'md'}
w={'100%'}
h={'200px'}