V4.8.14 dev (#3234)

* feat: rewrite chat context (#3176)

* feat: add app auto execute (#3115)

* feat: add app auto execute

* auto exec configtion

* chatting animation

* change icon

* fix

* fix

* fix link

* feat: add chat context to all chatbox

* perf: loading ui

---------

Co-authored-by: heheer <heheer@sealos.io>

* app auto exec (#3179)

* add chat records loaded state (#3184)

* perf: chat store reset storage (#3186)

* perf: chat store reset storage

* perf: auto exec code

* chore: workflow ui (#3175)

* chore: workflow ui

* fix

* change icon color config

* change popover to mymenu

* 4.8.14 test (#3189)

* update doc

* fix: token check

* perf: icon button

* update doc

* feat: share page support configuration Whether to allow the original view (#3194)

* update doc

* perf: fix index (#3206)

* perf: i18n

* perf: Add service entry (#3226)

* 4.8.14 test (#3228)

* fix: ai log

* fix: text splitter

* fix: reference unselect & user form description & simple to advance (#3229)

* fix: reference unselect & user form description & simple to advance

* change abort position

* perf

* perf: code (#3232)

* perf: code

* update doc

* fix: create btn permission (#3233)

* update doc

* fix: refresh chatbox listener

* perf: check invalid reference

* perf: check invalid reference

* update doc

* fix: ui props

---------

Co-authored-by: heheer <heheer@sealos.io>
This commit is contained in:
Archer
2024-11-26 12:02:58 +08:00
committed by GitHub
parent 7e1d31b5a9
commit 8aa6b53760
221 changed files with 3831 additions and 2737 deletions

View File

@@ -5,8 +5,8 @@
module.exports = {
i18n: {
defaultLocale: 'zh',
locales: ['en', 'zh', 'zh_TW'],
defaultLocale: 'zh-CN',
locales: ['en', 'zh-CN', 'zh-TW'],
localeDetection: false
},
localePath:

View File

@@ -85,7 +85,8 @@ const nextConfig = {
experimental: {
// 优化 Server Components 的构建和运行,避免不必要的客户端打包。
serverComponentsExternalPackages: ['mongoose', 'pg', '@node-rs/jieba', 'duck-duck-scrape'],
outputFileTracingRoot: path.join(__dirname, '../../')
outputFileTracingRoot: path.join(__dirname, '../../'),
instrumentationHook: true
}
};

View File

@@ -1,6 +1,6 @@
{
"name": "app",
"version": "4.8.13",
"version": "4.8.14",
"private": false,
"scripts": {
"dev": "next dev",

View File

@@ -0,0 +1,8 @@
<svg width="36" height="37" viewBox="0 0 36 37" fill="none" xmlns="http://www.w3.org/2000/svg">
<rect y="0.617554" width="36" height="36" rx="6" fill="#F0EEFF"/>
<path d="M14.8629 19.2382L16.7322 14.679C16.7937 14.5289 16.94 14.4308 17.1023 14.4308H19.7262C20.0163 14.4308 20.2099 14.7299 20.0912 14.9946L18.9433 17.552C18.884 17.6843 18.9808 17.8339 19.1258 17.8339H21.1135C21.4463 17.8339 21.6336 18.2166 21.4294 18.4793L17.7127 23.2613C17.4643 23.5809 16.9528 23.3717 16.9996 22.9696L17.3439 20.0131C17.3577 19.8943 17.2648 19.79 17.1452 19.79H15.233C14.9485 19.79 14.755 19.5014 14.8629 19.2382Z" fill="#8774EE"/>
<path d="M10.2067 23.1292C9.73113 23.4045 9.11745 23.2438 8.89074 22.7433C7.9809 20.7346 7.7568 18.4713 8.27046 16.3077C8.86108 13.8199 10.3822 11.6529 12.5212 10.2521C14.6601 8.85118 17.2545 8.32284 19.7709 8.77564C21.9595 9.16945 23.9446 10.2794 25.4224 11.9162C25.7906 12.324 25.6927 12.9508 25.2503 13.2767C24.8079 13.6026 24.1892 13.503 23.811 13.1043C22.6426 11.8729 21.1061 11.0377 19.4185 10.734C17.4028 10.3713 15.3247 10.7945 13.6114 11.9167C11.898 13.0388 10.6796 14.7746 10.2065 16.7673C9.81043 18.4356 9.96196 20.1778 10.6239 21.741C10.8382 22.247 10.6822 22.854 10.2067 23.1292Z" fill="#8774EE"/>
<path fill-rule="evenodd" clip-rule="evenodd" d="M10.6398 21.7804C10.6586 21.8296 10.6739 21.8797 10.6859 21.9302C10.7953 22.3903 10.6241 22.8876 10.2067 23.1292C9.73113 23.4045 9.11745 23.2438 8.89074 22.7433C8.85672 22.6682 8.82366 22.5927 8.79156 22.5169C8.77571 22.4795 8.7601 22.442 8.74472 22.4044C7.95817 20.4821 7.78549 18.3505 8.27046 16.3077C8.86108 13.8199 10.3822 11.6529 12.5212 10.2521C14.6601 8.85118 17.2545 8.32284 19.7709 8.77564C21.8373 9.14745 23.7223 10.1576 25.1701 11.6469C25.1984 11.676 25.2265 11.7053 25.2545 11.7348C25.3112 11.7945 25.3671 11.855 25.4224 11.9162C25.7906 12.324 25.6927 12.9508 25.2503 13.2767C24.8619 13.5628 24.3377 13.521 23.9596 13.2368C23.9181 13.2056 23.8784 13.1715 23.8408 13.1346C23.8307 13.1247 23.8207 13.1146 23.811 13.1043C22.6426 11.8729 21.1061 11.0377 19.4185 10.734C17.4028 10.3713 15.3247 10.7945 13.6114 11.9167C11.898 13.0388 10.6796 14.7746 10.2065 16.7673C9.81043 18.4356 9.96196 20.1778 10.6239 21.741C10.6294 21.7541 10.6347 21.7672 10.6398 21.7804Z" fill="#8774EE"/>
<path d="M26.3355 15.2101C26.8441 15.0022 27.4302 15.2451 27.5866 15.7719C28.1567 17.6924 28.1388 19.7484 27.5225 21.6707C26.8045 23.9101 25.3203 25.8248 23.3306 27.0784C21.3408 28.3319 18.973 28.8442 16.6431 28.5251C14.6431 28.2511 12.7808 27.3797 11.2946 26.0363C10.8869 25.6678 10.9209 25.0344 11.328 24.6653C11.7351 24.2963 12.3607 24.3327 12.7774 24.691C13.9423 25.6928 15.377 26.3432 16.9131 26.5536C18.7794 26.8092 20.6761 26.3989 22.2699 25.3948C23.8636 24.3907 25.0526 22.8569 25.6277 21.0632C26.1011 19.5867 26.1337 18.0119 25.733 16.5286C25.5897 15.9981 25.8269 15.4181 26.3355 15.2101Z" fill="#8774EE"/>
<path fill-rule="evenodd" clip-rule="evenodd" d="M25.7226 16.4874C25.7258 16.5011 25.7293 16.5149 25.733 16.5286C26.1337 18.0119 26.1011 19.5867 25.6277 21.0632C25.0526 22.8569 23.8636 24.3907 22.2699 25.3948C20.6761 26.3989 18.7794 26.8092 16.9131 26.5536C15.377 26.3432 13.9423 25.6928 12.7774 24.691C12.7666 24.6817 12.7557 24.6727 12.7446 24.6639C12.7035 24.631 12.6605 24.6011 12.6161 24.5743C12.2111 24.3299 11.6854 24.3414 11.328 24.6653C10.9209 25.0344 10.8869 25.6678 11.2946 26.0363C11.3557 26.0916 11.4175 26.1461 11.48 26.1997C11.5108 26.2262 11.5417 26.2525 11.5729 26.2786C13.0103 27.4846 14.766 28.268 16.6431 28.5251C18.973 28.8442 21.3408 28.3319 23.3306 27.0784C25.3203 25.8248 26.8045 23.9101 27.5225 21.6707C28.101 19.8666 28.1523 17.9447 27.685 16.1275C27.6749 16.0882 27.6646 16.0489 27.654 16.0096C27.6325 15.9302 27.61 15.8509 27.5866 15.7719C27.4302 15.2451 26.8441 15.0022 26.3355 15.2101C25.889 15.3927 25.6517 15.8619 25.6973 16.3327C25.7023 16.3844 25.7107 16.436 25.7226 16.4874Z" fill="#8774EE"/>
</svg>

After

Width:  |  Height:  |  Size: 3.8 KiB

File diff suppressed because one or more lines are too long

After

Width:  |  Height:  |  Size: 15 KiB

View File

@@ -2,7 +2,7 @@ import React, { useMemo } from 'react';
import { Box, BoxProps, Flex, Link, LinkProps } from '@chakra-ui/react';
import { useRouter } from 'next/router';
import { useUserStore } from '@/web/support/user/useUserStore';
import { useChatStore } from '@/web/core/chat/context/storeChat';
import { useChatStore } from '@/web/core/chat/context/useChatStore';
import { HUMAN_ICON } from '@fastgpt/global/common/system/constants';
import NextLink from 'next/link';
import Badge from '../Badge';
@@ -23,14 +23,14 @@ const Navbar = ({ unread }: { unread: number }) => {
const router = useRouter();
const { userInfo } = useUserStore();
const { gitStar, feConfigs } = useSystemStore();
const { lastChatAppId, lastChatId } = useChatStore();
const { lastChatAppId } = useChatStore();
const navbarList = useMemo(
() => [
{
label: t('common:navbar.Chat'),
icon: 'core/chat/chatLight',
activeIcon: 'core/chat/chatFill',
link: `/chat?appId=${lastChatAppId}&chatId=${lastChatId}`,
link: `/chat?appId=${lastChatAppId}`,
activeLink: ['/chat']
},
{
@@ -55,7 +55,7 @@ const Navbar = ({ unread }: { unread: number }) => {
activeLink: ['/account']
}
],
[lastChatAppId, lastChatId, t]
[lastChatAppId, t]
);
const itemStyles: BoxProps & LinkProps = {
@@ -84,6 +84,7 @@ const Navbar = ({ unread }: { unread: number }) => {
h={'100%'}
w={'100%'}
userSelect={'none'}
pb={2}
>
{/* logo */}
<Box
@@ -155,6 +156,7 @@ const Navbar = ({ unread }: { unread: number }) => {
href={`/account?currentTab=inform`}
mb={0}
color={'myGray.500'}
height={'48px'}
>
<Badge count={unread}>
<MyIcon name={'support/user/informLight'} width={'22px'} height={'22px'} />
@@ -171,6 +173,7 @@ const Navbar = ({ unread }: { unread: number }) => {
target="_blank"
mb={0}
color={'myGray.500'}
height={'48px'}
>
<MyIcon name={'common/courseLight'} width={'24px'} height={'24px'} />
</Link>
@@ -186,6 +189,7 @@ const Navbar = ({ unread }: { unread: number }) => {
{...hoverStyle}
mt={0}
color={'myGray.500'}
height={'48px'}
>
<MyIcon name={'common/gitInlight'} width={'26px'} height={'26px'} />
</Link>

View File

@@ -1,7 +1,7 @@
import React, { useMemo } from 'react';
import { useRouter } from 'next/router';
import { Flex, Box } from '@chakra-ui/react';
import { useChatStore } from '@/web/core/chat/context/storeChat';
import { useChatStore } from '@/web/core/chat/context/useChatStore';
import { useTranslation } from 'next-i18next';
import Badge from '../Badge';
import MyIcon from '@fastgpt/web/components/common/Icon';
@@ -9,14 +9,14 @@ import MyIcon from '@fastgpt/web/components/common/Icon';
const NavbarPhone = ({ unread }: { unread: number }) => {
const router = useRouter();
const { t } = useTranslation();
const { lastChatAppId, lastChatId } = useChatStore();
const { lastChatAppId } = useChatStore();
const navbarList = useMemo(
() => [
{
label: t('common:navbar.Chat'),
icon: 'core/chat/chatLight',
activeIcon: 'core/chat/chatFill',
link: `/chat?appId=${lastChatAppId}&chatId=${lastChatId}`,
link: `/chat?appId=${lastChatAppId}`,
activeLink: ['/chat'],
unread: 0
},
@@ -45,7 +45,7 @@ const NavbarPhone = ({ unread }: { unread: number }) => {
unread
}
],
[t, lastChatAppId, lastChatId, unread]
[t, lastChatAppId, unread]
);
return (

View File

@@ -1,5 +1,5 @@
import { langMap } from '@/web/common/utils/i18n';
import { Avatar, Box, Flex } from '@chakra-ui/react';
import { Box, Flex } from '@chakra-ui/react';
import MySelect from '@fastgpt/web/components/common/MySelect';
import { useI18nLng } from '@fastgpt/web/hooks/useI18n';
import { useTranslation } from 'next-i18next';
@@ -14,7 +14,7 @@ const I18nLngSelector = () => {
return Object.entries(langMap).map(([key, lang]) => ({
label: (
<Flex alignItems={'center'}>
<MyIcon borderRadius={'0'} mr={2} name={lang.avatar as any} w={'14px'} h={'9px'} />
<MyIcon borderRadius={'0'} mr={2} name={lang.avatar as any} w={'1rem'} />
<Box>{lang.label}</Box>
</Flex>
),

View File

@@ -0,0 +1,87 @@
import { Box, Button, Flex, ModalBody, Switch, Textarea, useDisclosure } from '@chakra-ui/react';
import { defaultAutoExecuteConfig } from '@fastgpt/global/core/app/constants';
import { AppAutoExecuteConfigType } from '@fastgpt/global/core/app/type';
import MyIcon from '@fastgpt/web/components/common/Icon';
import FormLabel from '@fastgpt/web/components/common/MyBox/FormLabel';
import { useTranslation } from 'react-i18next';
import ChatFunctionTip from './Tip';
import MyTooltip from '@fastgpt/web/components/common/MyTooltip';
import MyModal from '@fastgpt/web/components/common/MyModal';
const AutoExecConfig = ({
value = defaultAutoExecuteConfig,
onChange
}: {
value?: AppAutoExecuteConfigType;
onChange: (e: AppAutoExecuteConfigType) => void;
}) => {
const { t } = useTranslation();
const { isOpen, onOpen, onClose } = useDisclosure();
const isOpenAutoExec = value.open;
const defaultPrompt = value.defaultPrompt;
const formLabel = isOpenAutoExec
? t('common:core.app.whisper.Open')
: t('common:core.app.whisper.Close');
return (
<Flex alignItems={'center'}>
<MyIcon name={'core/app/simpleMode/autoExec'} mr={2} w={'20px'} />
<FormLabel color={'myGray.600'}>{t('app:auto_execute')}</FormLabel>
<ChatFunctionTip type={'autoExec'} />
<Box flex={1} />
<MyTooltip label={t('common:core.app.Config_auto_execute')}>
<Button
variant={'transparentBase'}
iconSpacing={1}
size={'sm'}
mr={'-5px'}
onClick={onOpen}
color={'myGray.600'}
>
{formLabel}
</Button>
</MyTooltip>
<MyModal
title={t('common:core.app.Auto execute')}
iconSrc="core/app/simpleMode/autoExec"
isOpen={isOpen}
onClose={onClose}
>
<ModalBody>
<Flex justifyContent={'space-between'} alignItems={'center'}>
<FormLabel flex={'0 0 100px'}>{t('app:open_auto_execute')}</FormLabel>
<Switch
isChecked={isOpenAutoExec}
onChange={(e) => {
onChange({
...value,
open: e.target.checked
});
}}
/>
</Flex>
{isOpenAutoExec && (
<Box mt={4}>
<FormLabel mb={1}>{t('common:core.app.schedule.Default prompt')}</FormLabel>
<Textarea
value={defaultPrompt}
rows={8}
bg={'myGray.50'}
placeholder={t('app:auto_execute_default_prompt_placeholder')}
onChange={(e) => {
onChange({
...value,
defaultPrompt: e.target.value
});
}}
/>
</Box>
)}
</ModalBody>
</MyModal>
</Flex>
);
};
export default AutoExecConfig;

View File

@@ -139,7 +139,7 @@ const InputGuideConfig = ({
onOpenLexiconConfig();
}}
>
{chatT('config_input_guide_lexicon')}
{t('chat:config_input_guide_lexicon')}
</Button>
</Flex>
<>
@@ -152,7 +152,7 @@ const InputGuideConfig = ({
cursor={'pointer'}
>
<MyIcon name={'book'} w={'17px'} ml={4} mr={1} color={'myGray.600'} />
{commonT('common.Documents')}
{t('common:common.Documents')}
</Flex>
<Box flex={'1 0 0'} />
</Flex>

View File

@@ -23,7 +23,7 @@ import FormLabel from '@fastgpt/web/components/common/MyBox/FormLabel';
const MultipleRowSelect = dynamic(
() => import('@fastgpt/web/components/common/MySelect/MultipleRowSelect')
);
import { i18nT } from '@fastgpt/web/i18n/utils';
// options type:
enum CronJobTypeEnum {
month = 'month',
@@ -233,24 +233,24 @@ const ScheduledTriggerConfig = ({
}
if (cronField[0] === 'month') {
return t('core.app.schedule.Every month', {
return t('common:core.app.schedule.Every month', {
day: cronField[1],
hour: cronField[2]
});
}
if (cronField[0] === 'week') {
return t('core.app.schedule.Every week', {
return t('common:core.app.schedule.Every week', {
day: cronField[1] === 0 ? t('app:day') : cronField[1],
hour: cronField[2]
});
}
if (cronField[0] === 'day') {
return t('core.app.schedule.Every day', {
return t('common:core.app.schedule.Every day', {
hour: cronField[1]
});
}
if (cronField[0] === 'interval') {
return t('core.app.schedule.Interval', {
return t('common:core.app.schedule.Interval', {
interval: cronField[1]
});
}

View File

@@ -12,7 +12,8 @@ enum FnTypeEnum {
welcome = 'welcome',
file = 'file',
visionModel = 'visionModel',
instruction = 'instruction'
instruction = 'instruction',
autoExec = 'autoExec'
}
const ChatFunctionTip = ({ type }: { type: `${FnTypeEnum}` }) => {
@@ -66,6 +67,12 @@ const ChatFunctionTip = ({ type }: { type: `${FnTypeEnum}` }) => {
title: t('workflow:plugin.Instructions'),
desc: t('workflow:plugin.Instruction_Tip'),
imgUrl: '/imgs/app/instruction.svg'
},
[FnTypeEnum.autoExec]: {
icon: '/imgs/app/autoExec-icon.svg',
title: t('common:core.app.Auto execute'),
desc: t('app:auto_execute_tip'),
imgUrl: '/imgs/app/autoExec.svg'
}
});
const data = map.current[type];

View File

@@ -31,6 +31,7 @@ import ChatFunctionTip from './Tip';
import FormLabel from '@fastgpt/web/components/common/MyBox/FormLabel';
import QuestionTip from '@fastgpt/web/components/common/MyTooltip/QuestionTip';
import InputTypeConfig from '@/pages/app/detail/components/WorkflowComponents/Flow/nodes/NodePluginIO/InputTypeConfig';
import MyIconButton from '@fastgpt/web/components/common/Icon/button';
export const defaultVariable: VariableItemType = {
id: nanoid(),
@@ -190,92 +191,59 @@ const VariableEdit = ({
</Flex>
{/* Form render */}
{formatVariables.length > 0 && (
<Box mt={2} borderRadius={'md'} overflow={'hidden'} borderWidth={'1px'} borderBottom="none">
<TableContainer>
<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'}
bg={'myGray.50'}
p={0}
px={4}
fontWeight={'medium'}
>
{t('common:common.Operation')}
</Th>
<TableContainer mt={2} borderRadius={'md'} overflow={'hidden'} borderWidth={'1px'}>
<Table variant={'workflow'}>
<Thead>
<Tr>
<Th>{t('workflow:Variable_name')}</Th>
<Th>{t('common:common.Require Input')}</Th>
<Th>{t('common:common.Operation')}</Th>
</Tr>
</Thead>
<Tbody>
{formatVariables.map((item, index) => (
<Tr key={item.id}>
<Td fontWeight={'medium'}>
<Flex alignItems={'center'}>
<MyIcon name={item.icon as any} w={'16px'} color={'myGray.400'} mr={2} />
{item.key}
</Flex>
</Td>
<Td>
<Flex alignItems={'center'}>
{item.required ? (
<MyIcon name={'check'} w={'16px'} color={'myGray.900'} mr={2} />
) : (
''
)}
</Flex>
</Td>
<Td>
<Flex>
<MyIconButton
icon={'common/settingLight'}
onClick={() => {
const formattedItem = {
...item,
list: item.enums || []
};
reset(formattedItem);
}}
/>
<MyIconButton
icon={'delete'}
hoverColor={'red.500'}
onClick={() =>
onChange(variables.filter((variable) => variable.id !== item.id))
}
/>
</Flex>
</Td>
</Tr>
</Thead>
<Tbody>
{formatVariables.map((item) => (
<Tr key={item.id}>
<Td
p={0}
px={4}
h={8}
color={'myGray.900'}
fontSize={'mini'}
fontWeight={'medium'}
>
<Flex alignItems={'center'}>
<MyIcon name={item.icon as any} w={'16px'} color={'myGray.400'} mr={2} />
{item.key}
</Flex>
</Td>
<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>
))}
</Tbody>
</Table>
</TableContainer>
</Box>
))}
</Tbody>
</Table>
</TableContainer>
)}
{/* Edit modal */}

View File

@@ -1,7 +1,7 @@
import MyIcon from '@fastgpt/web/components/common/Icon';
import MyTooltip from '@fastgpt/web/components/common/MyTooltip';
import { Box, Button, Flex, ModalBody, useDisclosure, Switch } from '@chakra-ui/react';
import React, { useMemo } from 'react';
import React from 'react';
import { useTranslation } from 'next-i18next';
import type { AppWhisperConfigType } from '@fastgpt/global/core/app/type.d';
import MyModal from '@fastgpt/web/components/common/MyModal';
@@ -24,12 +24,9 @@ const WhisperConfig = ({
const isOpenWhisper = value.open;
const isAutoSend = value.autoSend;
const formLabel = useMemo(() => {
if (!isOpenWhisper) {
return t('common:core.app.whisper.Close');
}
return t('common:core.app.whisper.Open');
}, [t, isOpenWhisper]);
const formLabel = isOpenWhisper
? t('common:core.app.whisper.Open')
: t('common:core.app.whisper.Close');
return (
<Flex alignItems={'center'}>

View File

@@ -33,32 +33,29 @@ const ChatInput = ({
onStop,
TextareaDom,
resetInputVal,
chatForm,
appId
chatForm
}: {
onSendMessage: SendPromptFnType;
onStop: () => void;
TextareaDom: React.MutableRefObject<HTMLTextAreaElement | null>;
resetInputVal: (val: ChatBoxInputType) => void;
chatForm: UseFormReturn<ChatBoxInputFormType>;
appId: string;
}) => {
const { isPc } = useSystem();
const { t } = useTranslation();
const { toast } = useToast();
const { isPc } = useSystem();
const { setValue, watch, control } = chatForm;
const inputValue = watch('input');
const {
chatId,
isChatting,
whisperConfig,
autoTTSResponse,
chatInputGuide,
outLinkAuthData,
fileSelectConfig
} = useContextSelector(ChatBoxContext, (v) => v);
const outLinkAuthData = useContextSelector(ChatBoxContext, (v) => v.outLinkAuthData);
const appId = useContextSelector(ChatBoxContext, (v) => v.appId);
const chatId = useContextSelector(ChatBoxContext, (v) => v.chatId);
const isChatting = useContextSelector(ChatBoxContext, (v) => v.isChatting);
const whisperConfig = useContextSelector(ChatBoxContext, (v) => v.whisperConfig);
const autoTTSResponse = useContextSelector(ChatBoxContext, (v) => v.autoTTSResponse);
const chatInputGuide = useContextSelector(ChatBoxContext, (v) => v.chatInputGuide);
const fileSelectConfig = useContextSelector(ChatBoxContext, (v) => v.fileSelectConfig);
const fileCtrl = useFieldArray({
control,
@@ -78,8 +75,6 @@ const ChatInput = ({
replaceFiles,
hasFileUploading
} = useFileUpload({
outLinkAuthData,
chatId: chatId || '',
fileSelectConfig,
fileCtrl
});

View File

@@ -2,76 +2,73 @@ import React, { useState, useMemo, useCallback } from 'react';
import { useAudioPlay } from '@/web/common/utils/voice';
import { OutLinkChatAuthProps } from '@fastgpt/global/support/permission/chat';
import {
AppChatConfigType,
AppAutoExecuteConfigType,
AppFileSelectConfigType,
AppTTSConfigType,
AppWhisperConfigType,
ChatInputGuideConfigType,
VariableItemType
} from '@fastgpt/global/core/app/type';
import { ChatHistoryItemResType, ChatSiteItemType } from '@fastgpt/global/core/chat/type';
import { ChatHistoryItemResType } from '@fastgpt/global/core/chat/type';
import {
defaultAppSelectFileConfig,
defaultAutoExecuteConfig,
defaultChatInputGuideConfig,
defaultTTSConfig,
defaultWhisperConfig
} from '@fastgpt/global/core/app/constants';
import { createContext } from 'use-context-selector';
import { UseFormReturn } from 'react-hook-form';
import { createContext, useContextSelector } from 'use-context-selector';
import { VariableInputEnum } from '@fastgpt/global/core/workflow/constants';
import { getChatResData } from '@/web/core/chat/api';
import { ChatBoxInputFormType } from './type';
import { ChatItemContext } from '@/web/core/chat/context/chatItemContext';
import { ChatRecordContext } from '@/web/core/chat/context/chatRecordContext';
export type ChatProviderProps = OutLinkChatAuthProps & {
appAvatar?: string;
export type ChatProviderProps = {
appId: string;
chatConfig?: AppChatConfigType;
chatId: string;
outLinkAuthData?: OutLinkChatAuthProps;
chatHistories: ChatSiteItemType[];
setChatHistories: React.Dispatch<React.SetStateAction<ChatSiteItemType[]>>;
variablesForm: UseFormReturn<ChatBoxInputFormType, any>;
// not chat test params
chatId?: string;
chatType: 'log' | 'chat' | 'share' | 'team';
showRawSource: boolean;
showNodeStatus: boolean;
};
type useChatStoreType = OutLinkChatAuthProps &
ChatProviderProps & {
welcomeText: string;
variableList: VariableItemType[];
allVariableList: VariableItemType[];
questionGuide: boolean;
ttsConfig: AppTTSConfigType;
whisperConfig: AppWhisperConfigType;
autoTTSResponse: boolean;
startSegmentedAudio: () => Promise<any>;
splitText2Audio: (text: string, done?: boolean | undefined) => void;
finishSegmentedAudio: () => void;
audioLoading: boolean;
audioPlaying: boolean;
hasAudio: boolean;
playAudioByText: ({
text,
buffer
}: {
text: string;
buffer?: Uint8Array | undefined;
}) => Promise<{
buffer?: Uint8Array | undefined;
}>;
cancelAudio: () => void;
audioPlayingChatId: string | undefined;
setAudioPlayingChatId: React.Dispatch<React.SetStateAction<string | undefined>>;
isChatting: boolean;
chatInputGuide: ChatInputGuideConfigType;
outLinkAuthData: OutLinkChatAuthProps;
getHistoryResponseData: ({ dataId }: { dataId: string }) => Promise<ChatHistoryItemResType[]>;
fileSelectConfig: AppFileSelectConfigType;
};
type useChatStoreType = ChatProviderProps & {
welcomeText: string;
variableList: VariableItemType[];
allVariableList: VariableItemType[];
questionGuide: boolean;
ttsConfig: AppTTSConfigType;
whisperConfig: AppWhisperConfigType;
autoTTSResponse: boolean;
autoExecute: AppAutoExecuteConfigType;
startSegmentedAudio: () => Promise<any>;
splitText2Audio: (text: string, done?: boolean | undefined) => void;
finishSegmentedAudio: () => void;
audioLoading: boolean;
audioPlaying: boolean;
hasAudio: boolean;
playAudioByText: ({
text,
buffer
}: {
text: string;
buffer?: Uint8Array | undefined;
}) => Promise<{
buffer?: Uint8Array | undefined;
}>;
cancelAudio: () => void;
audioPlayingChatId: string | undefined;
setAudioPlayingChatId: React.Dispatch<React.SetStateAction<string | undefined>>;
isChatting: boolean;
chatInputGuide: ChatInputGuideConfigType;
getHistoryResponseData: ({ dataId }: { dataId: string }) => Promise<ChatHistoryItemResType[]>;
fileSelectConfig: AppFileSelectConfigType;
appId: string;
chatId: string;
outLinkAuthData: OutLinkChatAuthProps;
};
export const ChatBoxContext = createContext<useChatStoreType>({
welcomeText: '',
@@ -95,10 +92,6 @@ export const ChatBoxContext = createContext<useChatStoreType>({
splitText2Audio: function (text: string, done?: boolean | undefined): void {
throw new Error('Function not implemented.');
},
chatHistories: [],
setChatHistories: function (value: React.SetStateAction<ChatSiteItemType[]>): void {
throw new Error('Function not implemented.');
},
isChatting: false,
audioLoading: false,
audioPlaying: false,
@@ -132,23 +125,24 @@ export const ChatBoxContext = createContext<useChatStoreType>({
});
const Provider = ({
shareId,
outLinkUid,
teamId,
teamToken,
chatHistories,
setChatHistories,
variablesForm,
appId,
chatId,
outLinkAuthData = {},
chatType = 'chat',
showRawSource,
showNodeStatus,
chatConfig = {},
children,
...props
}: ChatProviderProps & {
children: React.ReactNode;
}) => {
const chatConfig = useContextSelector(
ChatItemContext,
(v) => v.chatBoxData?.app?.chatConfig || {}
);
const chatRecords = useContextSelector(ChatRecordContext, (v) => v.chatRecords);
const setChatRecords = useContextSelector(ChatRecordContext, (v) => v.setChatRecords);
const {
welcomeText = '',
variables = [],
@@ -156,19 +150,10 @@ const Provider = ({
ttsConfig = defaultTTSConfig,
whisperConfig = defaultWhisperConfig,
chatInputGuide = defaultChatInputGuideConfig,
fileSelectConfig = defaultAppSelectFileConfig
fileSelectConfig = defaultAppSelectFileConfig,
autoExecute = defaultAutoExecuteConfig
} = useMemo(() => chatConfig, [chatConfig]);
const outLinkAuthData = useMemo(
() => ({
shareId,
outLinkUid,
teamId,
teamToken
}),
[shareId, outLinkUid, teamId, teamToken]
);
// segment audio
const [audioPlayingChatId, setAudioPlayingChatId] = useState<string>();
const {
@@ -190,37 +175,34 @@ const Provider = ({
const isChatting = useMemo(
() =>
chatHistories[chatHistories.length - 1] &&
chatHistories[chatHistories.length - 1]?.status !== 'finish',
[chatHistories]
chatRecords[chatRecords.length - 1] &&
chatRecords[chatRecords.length - 1]?.status !== 'finish',
[chatRecords]
);
const getHistoryResponseData = useCallback(
async ({ dataId }: { dataId: string }) => {
const aimItem = chatHistories.find((item) => item.dataId === dataId)!;
if (!!aimItem?.responseData || !props.chatId) {
const aimItem = chatRecords.find((item) => item.dataId === dataId)!;
if (!!aimItem?.responseData || !chatId) {
return aimItem.responseData || [];
} else {
let resData = await getChatResData({
appId: props.appId,
chatId: props.chatId,
appId: appId,
chatId: chatId,
dataId,
...outLinkAuthData
});
setChatHistories((state) =>
setChatRecords((state) =>
state.map((item) => (item.dataId === dataId ? { ...item, responseData: resData } : item))
);
return resData;
}
},
[chatHistories, outLinkAuthData, props.appId, props.chatId, setChatHistories]
[chatRecords, chatId, appId, outLinkAuthData, setChatRecords]
);
const value: useChatStoreType = {
...props,
shareId,
outLinkUid,
teamId,
teamToken,
welcomeText,
autoExecute,
variableList: variables.filter((item) => item.type !== VariableInputEnum.custom),
allVariableList: variables,
questionGuide,
@@ -238,12 +220,11 @@ const Provider = ({
cancelAudio,
audioPlayingChatId,
setAudioPlayingChatId,
chatHistories,
setChatHistories,
isChatting,
chatInputGuide,
appId,
chatId,
outLinkAuthData,
variablesForm,
getHistoryResponseData,
chatType,
showRawSource,

View File

@@ -10,6 +10,7 @@ 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';
import { ChatRecordContext } from '@/web/core/chat/context/chatRecordContext';
export type ChatControllerProps = {
isLastChild: boolean;
@@ -24,6 +25,19 @@ export type ChatControllerProps = {
onAddUserDislike?: () => void;
};
const controlIconStyle = {
w: '14px',
cursor: 'pointer',
p: '5px',
bg: 'white',
borderRight: 'base'
};
const controlContainerStyle = {
className: 'control',
color: 'myGray.400',
display: 'flex'
};
const ChatController = ({
chat,
showVoiceIcon,
@@ -35,34 +49,21 @@ const ChatController = ({
onAddUserDislike,
onAddUserLike
}: ChatControllerProps & FlexProps) => {
const theme = useTheme();
const {
isChatting,
setChatHistories,
audioLoading,
audioPlaying,
hasAudio,
playAudioByText,
cancelAudio,
audioPlayingChatId,
setAudioPlayingChatId
} = useContextSelector(ChatBoxContext, (v) => v);
const controlIconStyle = {
w: '14px',
cursor: 'pointer',
p: '5px',
bg: 'white',
borderRight: theme.borders.base
};
const controlContainerStyle = {
className: 'control',
color: 'myGray.400',
display: 'flex'
};
const { t } = useTranslation();
const { copyData } = useCopyData();
const setChatRecords = useContextSelector(ChatRecordContext, (v) => v.setChatRecords);
const isChatting = useContextSelector(ChatBoxContext, (v) => v.isChatting);
const audioLoading = useContextSelector(ChatBoxContext, (v) => v.audioLoading);
const audioPlaying = useContextSelector(ChatBoxContext, (v) => v.audioPlaying);
const hasAudio = useContextSelector(ChatBoxContext, (v) => v.hasAudio);
const playAudioByText = useContextSelector(ChatBoxContext, (v) => v.playAudioByText);
const cancelAudio = useContextSelector(ChatBoxContext, (v) => v.cancelAudio);
const audioPlayingChatId = useContextSelector(ChatBoxContext, (v) => v.audioPlayingChatId);
const setAudioPlayingChatId = useContextSelector(ChatBoxContext, (v) => v.setAudioPlayingChatId);
const chatType = useContextSelector(ChatBoxContext, (v) => v.chatType);
const chatText = useMemo(() => formatChatValue2InputType(chat.value).text || '', [chat.value]);
return (
@@ -70,7 +71,7 @@ const ChatController = ({
{...controlContainerStyle}
borderRadius={'sm'}
overflow={'hidden'}
border={theme.borders.base}
border={'base'}
// 最后一个子元素没有border
css={css({
'& > *:last-child, & > *:last-child svg': {
@@ -87,7 +88,7 @@ const ChatController = ({
onClick={() => copyData(chatText)}
/>
</MyTooltip>
{!!onDelete && !isChatting && (
{!!onDelete && !isChatting && chatType !== 'log' && (
<>
{onRetry && (
<MyTooltip label={t('common:core.chat.retry')}>
@@ -125,12 +126,7 @@ const ChatController = ({
onClick={cancelAudio}
/>
</MyTooltip>
<MyImage
src="/icon/speaking.gif"
w={'23px'}
alt={''}
borderRight={theme.borders.base}
/>
<MyImage src="/icon/speaking.gif" w={'23px'} alt={''} borderRight={'base'} />
</Flex>
);
}
@@ -154,8 +150,8 @@ const ChatController = ({
text: chatText
});
if (!setChatHistories || !response.buffer) return;
setChatHistories((state) =>
if (!setChatRecords || !response.buffer) return;
setChatRecords((state) =>
state.map((item) =>
item.dataId === chat.dataId
? {

View File

@@ -216,7 +216,7 @@ const ChatItem = (props: Props) => {
}}
>
{/* control icon */}
<Flex w={'100%'} alignItems={'flex-end'} gap={2} justifyContent={styleMap.justifyContent}>
<Flex w={'100%'} alignItems={'center'} gap={2} justifyContent={styleMap.justifyContent}>
{isChatting && type === ChatRoleEnum.AI && isLastChild ? null : (
<Flex order={styleMap.order} ml={styleMap.ml} align={'center'} gap={'0.62rem'}>
{chat.time && (isPc || isChatLog) && (

View File

@@ -19,10 +19,12 @@ const ContextModal = ({ onClose, dataId }: { onClose: () => void; dataId: string
const flatResData: ChatHistoryItemResType[] =
res
?.map((item) => {
if (item.pluginDetail || item.toolDetail) {
return [item, ...(item.pluginDetail || []), ...(item.toolDetail || [])];
}
return item;
return [
item,
...(item.pluginDetail || []),
...(item.toolDetail || []),
...(item.loopDetail || [])
];
})
.flat() || [];
return flatResData.find(isLLMNode)?.historyPreview || [];

View File

@@ -7,18 +7,22 @@ import type { SearchDataResponseItemType } from '@fastgpt/global/core/dataset/ty
import QuoteItem from '@/components/core/dataset/QuoteItem';
import RawSourceBox from '@/components/core/dataset/RawSourceBox';
import { getWebReqUrl } from '@fastgpt/web/common/system/utils';
import { useContextSelector } from 'use-context-selector';
import { ChatBoxContext } from '../Provider';
const QuoteModal = ({
rawSearch = [],
onClose,
canEditDataset,
showRawSource,
chatItemId,
metadata
}: {
rawSearch: SearchDataResponseItemType[];
onClose: () => void;
canEditDataset: boolean;
showRawSource: boolean;
chatItemId: string;
metadata?: {
collectionId: string;
sourceId?: string;
@@ -37,6 +41,13 @@ const QuoteModal = ({
[metadata, rawSearch]
);
const RawSourceBoxProps = useContextSelector(ChatBoxContext, (v) => ({
appId: v.appId,
chatId: v.chatId,
chatItemId,
...(v.outLinkAuthData || {})
}));
return (
<>
<MyModal
@@ -49,7 +60,7 @@ const QuoteModal = ({
title={
<Box>
{metadata ? (
<RawSourceBox {...metadata} canView={showRawSource} />
<RawSourceBox {...metadata} {...RawSourceBoxProps} canView={showRawSource} />
) : (
<>{t('common:core.chat.Quote Amount', { amount: rawSearch.length })}</>
)}
@@ -64,6 +75,7 @@ const QuoteModal = ({
rawSearch={filterResults}
canEditDataset={canEditDataset}
canViewSource={showRawSource}
chatItemId={chatItemId}
/>
</ModalBody>
</MyModal>
@@ -74,16 +86,25 @@ const QuoteModal = ({
export default QuoteModal;
export const QuoteList = React.memo(function QuoteList({
chatItemId,
rawSearch = [],
canEditDataset,
canViewSource
}: {
chatItemId?: string;
rawSearch: SearchDataResponseItemType[];
canEditDataset: boolean;
canViewSource: boolean;
}) {
const theme = useTheme();
const RawSourceBoxProps = useContextSelector(ChatBoxContext, (v) => ({
chatItemId,
appId: v.appId,
chatId: v.chatId,
...(v.outLinkAuthData || {})
}));
return (
<>
{rawSearch.map((item, i) => (
@@ -101,6 +122,7 @@ export const QuoteList = React.memo(function QuoteList({
quoteItem={item}
canViewSource={canViewSource}
canEditDataset={canEditDataset}
{...RawSourceBoxProps}
/>
</Box>
))}

View File

@@ -250,6 +250,7 @@ const ResponseTags = ({
{!!quoteModalData && (
<QuoteModal
{...quoteModalData}
chatItemId={historyItem.dataId}
canEditDataset={notSharePage}
showRawSource={showRawSource}
onClose={() => setQuoteModalData(undefined)}

View File

@@ -0,0 +1,19 @@
import { Box } from '@chakra-ui/react';
import React from 'react';
import { useTranslation } from 'next-i18next';
import { formatTimeToChatItemTime } from '@fastgpt/global/common/string/time';
import dayjs from 'dayjs';
const TimeBox = ({ 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>
);
};
export default TimeBox;

View File

@@ -1,17 +1,7 @@
import React, { useMemo } from 'react';
import React, { useEffect, useMemo } from 'react';
import { Controller, UseFormReturn } from 'react-hook-form';
import { useTranslation } from 'next-i18next';
import {
Box,
Button,
Card,
NumberDecrementStepper,
NumberIncrementStepper,
NumberInput,
NumberInputField,
NumberInputStepper,
Textarea
} from '@chakra-ui/react';
import { Box, Button, Card, Textarea } from '@chakra-ui/react';
import ChatAvatar from './ChatAvatar';
import { MessageCardStyle } from '../constants';
import { VariableInputEnum } from '@fastgpt/global/core/workflow/constants';
@@ -21,10 +11,10 @@ import { ChatBoxInputFormType } from '../type.d';
import { useContextSelector } from 'use-context-selector';
import { ChatBoxContext } from '../Provider';
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';
import { ChatItemContext } from '@/web/core/chat/context/chatItemContext';
export const VariableInputItem = ({
item,
@@ -47,13 +37,7 @@ export const VariableInputItem = ({
>
{item.label}
{item.required && (
<Box
position={'absolute'}
top={'-2px'}
left={'-8px'}
color={'red.500'}
fontWeight={'bold'}
>
<Box position={'absolute'} top={'-2px'} left={'-8px'} color={'red.500'}>
*
</Box>
)}
@@ -134,8 +118,11 @@ const VariableInput = ({
}) => {
const { t } = useTranslation();
const { appAvatar, variableList, variablesForm } = useContextSelector(ChatBoxContext, (v) => v);
const { reset, handleSubmit: handleSubmitChat } = variablesForm;
const appAvatar = useContextSelector(ChatItemContext, (v) => v.chatBoxData?.app?.avatar);
const variablesForm = useContextSelector(ChatItemContext, (v) => v.variablesForm);
const variableList = useContextSelector(ChatBoxContext, (v) => v.variableList);
const { setValue, handleSubmit: handleSubmitChat } = variablesForm;
const defaultValues = useMemo(() => {
return variableList.reduce((acc: Record<string, any>, item) => {
@@ -144,9 +131,12 @@ const VariableInput = ({
}, {});
}, [variableList]);
useDeepCompareEffect(() => {
reset(defaultValues);
}, [defaultValues]);
useEffect(() => {
const values = variablesForm.getValues('variables');
// If form is not empty, do not reset the variables
if (Object.values(values).filter(Boolean).length > 0) return;
setValue('variables', defaultValues);
}, [defaultValues, setValue, variablesForm]);
return (
<Box py={3}>

View File

@@ -4,10 +4,10 @@ import { MessageCardStyle } from '../constants';
import Markdown from '@/components/Markdown';
import ChatAvatar from './ChatAvatar';
import { useContextSelector } from 'use-context-selector';
import { ChatBoxContext } from '../Provider';
import { ChatItemContext } from '@/web/core/chat/context/chatItemContext';
const WelcomeBox = ({ welcomeText }: { welcomeText: string }) => {
const appAvatar = useContextSelector(ChatBoxContext, (v) => v.appAvatar);
const appAvatar = useContextSelector(ChatItemContext, (v) => v.chatBoxData?.app?.avatar);
return (
<Box py={3}>

View File

@@ -14,21 +14,24 @@ 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';
import { useContextSelector } from 'use-context-selector';
import { ChatBoxContext } from '../Provider';
type UseFileUploadOptions = {
outLinkAuthData: OutLinkChatAuthProps;
chatId: string;
fileSelectConfig: AppFileSelectConfigType;
fileCtrl: UseFieldArrayReturn<ChatBoxInputFormType, 'files', 'id'>;
};
export const useFileUpload = (props: UseFileUploadOptions) => {
const { outLinkAuthData, chatId, fileSelectConfig, fileCtrl } = props;
const { fileSelectConfig, fileCtrl } = props;
const { toast } = useToast();
const { t } = useTranslation();
const { feConfigs } = useSystemStore();
const outLinkAuthData = useContextSelector(ChatBoxContext, (v) => v.outLinkAuthData);
const appId = useContextSelector(ChatBoxContext, (v) => v.appId);
const chatId = useContextSelector(ChatBoxContext, (v) => v.chatId);
const {
update: updateFiles,
remove: removeFiles,
@@ -137,7 +140,7 @@ export const useFileUpload = (props: UseFileUploadOptions) => {
[maxSelectFiles, appendFiles, toast, t, maxSize]
);
const uploadFiles = async () => {
const uploadFiles = useCallback(async () => {
const filterFiles = fileList.filter((item) => item.status === 0);
if (filterFiles.length === 0) return;
@@ -158,7 +161,10 @@ export const useFileUpload = (props: UseFileUploadOptions) => {
const { previewUrl } = await uploadFile2DB({
file: copyFile.rawFile,
bucketName: 'chat',
outLinkAuthData,
data: {
appId,
...outLinkAuthData
},
metadata: {
chatId
},
@@ -186,7 +192,7 @@ export const useFileUpload = (props: UseFileUploadOptions) => {
);
removeFiles(errorFileIndex);
};
}, [appId, chatId, fileList, outLinkAuthData, removeFiles, replaceFiles, t, toast, updateFiles]);
const sortFileList = useMemo(() => {
// Sort: Document, image

View File

@@ -3,9 +3,7 @@ import React, {
useRef,
useState,
useMemo,
forwardRef,
useImperativeHandle,
ForwardedRef,
useEffect
} from 'react';
import Script from 'next/script';
@@ -16,7 +14,7 @@ import type {
} from '@fastgpt/global/core/chat/type.d';
import { useToast } from '@fastgpt/web/hooks/useToast';
import { getErrText } from '@fastgpt/global/common/error/utils';
import { Box, Flex, Checkbox, BoxProps } from '@chakra-ui/react';
import { Box, Checkbox } from '@chakra-ui/react';
import { EventNameEnum, eventBus } from '@/web/common/utils/eventbus';
import { chats2GPTMessages } from '@fastgpt/global/core/chat/adapt';
import { useForm } from 'react-hook-form';
@@ -25,6 +23,7 @@ import { useSystemStore } from '@/web/common/system/useSystemStore';
import { useTranslation } from 'next-i18next';
import {
closeCustomFeedback,
delChatRecordById,
updateChatAdminFeedback,
updateChatUserFeedback
} from '@/web/core/chat/api';
@@ -33,12 +32,7 @@ import type { AdminMarkType } from './components/SelectMarkCollection';
import MyTooltip from '@fastgpt/web/components/common/MyTooltip';
import { postQuestionGuide } from '@/web/core/ai/api';
import type {
ComponentRef,
ChatBoxInputType,
ChatBoxInputFormType,
SendPromptFnType
} from './type.d';
import type { ChatBoxInputType, ChatBoxInputFormType, SendPromptFnType } from './type.d';
import type { StartChatFnProps, generatingMessageProps } from '../type';
import ChatInput from './Input/ChatInput';
import ChatBoxDivider from '../../Divider';
@@ -67,9 +61,11 @@ 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';
import { ChatRecordContext } from '@/web/core/chat/context/chatRecordContext';
import { ChatItemContext } from '@/web/core/chat/context/chatItemContext';
import TimeBox from './components/TimeBox';
import MyBox from '@fastgpt/web/components/common/MyBox';
const ResponseTags = dynamic(() => import('./components/ResponseTags'));
const FeedbackModal = dynamic(() => import('./components/FeedbackModal'));
@@ -87,75 +83,45 @@ enum FeedbackTypeEnum {
type Props = OutLinkChatAuthProps &
ChatProviderProps & {
isReady?: boolean;
feedbackType?: `${FeedbackTypeEnum}`;
showMarkIcon?: boolean; // admin mark dataset
showVoiceIcon?: boolean;
showEmptyIntro?: boolean;
userAvatar?: string;
active?: boolean; // can use
appId: string;
ScrollData: ({
children,
...props
}: {
children: React.ReactNode;
ScrollContainerRef?: React.RefObject<HTMLDivElement>;
} & BoxProps) => React.JSX.Element;
// not chat test params
onStartChat?: (e: StartChatFnProps) => Promise<
StreamResponseType & {
isNewChat?: boolean;
}
>;
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,
showMarkIcon = false,
showVoiceIcon = true,
showEmptyIntro = false,
appAvatar,
userAvatar,
active = true,
appId,
chatId,
shareId,
outLinkUid,
teamId,
teamToken,
onStartChat,
onDelMessage,
ScrollData
}: Props,
ref: ForwardedRef<ComponentRef>
) => {
const ChatBoxRef = useRef<HTMLDivElement>(null);
const ChatBox = ({
isReady = true,
feedbackType = FeedbackTypeEnum.hidden,
showMarkIcon = false,
showVoiceIcon = true,
showEmptyIntro = false,
active = true,
shareId,
outLinkUid,
teamId,
teamToken,
onStartChat
}: Props) => {
const ScrollContainerRef = useRef<HTMLDivElement>(null);
const router = useRouter();
const { t } = useTranslation();
const { toast } = useToast();
const { setLoading, feConfigs } = useSystemStore();
const { feConfigs } = useSystemStore();
const { isPc } = useSystem();
const TextareaDom = useRef<HTMLTextAreaElement>(null);
const chatController = useRef(new AbortController());
const questionGuideController = useRef(new AbortController());
const pluginController = useRef(new AbortController());
const isNewChatReplace = useRef(false);
const [isLoading, setIsLoading] = useState(false);
const [feedbackId, setFeedbackId] = useState<string>();
const [readFeedbackData, setReadFeedbackData] = useState<{
dataId: string;
@@ -164,26 +130,35 @@ const ChatBox = (
const [adminMarkData, setAdminMarkData] = useState<AdminMarkType & { dataId: string }>();
const [questionGuides, setQuestionGuide] = useState<string[]>([]);
const {
welcomeText,
variableList,
allVariableList,
questionGuide,
startSegmentedAudio,
finishSegmentedAudio,
setAudioPlayingChatId,
splitText2Audio,
chatHistories,
setChatHistories,
variablesForm,
isChatting
} = useContextSelector(ChatBoxContext, (v) => v);
const appAvatar = useContextSelector(ChatItemContext, (v) => v.chatBoxData?.app?.avatar);
const userAvatar = useContextSelector(ChatItemContext, (v) => v.chatBoxData?.userAvatar);
const ChatBoxRef = useContextSelector(ChatItemContext, (v) => v.ChatBoxRef);
const variablesForm = useContextSelector(ChatItemContext, (v) => v.variablesForm);
const chatRecords = useContextSelector(ChatRecordContext, (v) => v.chatRecords);
const setChatRecords = useContextSelector(ChatRecordContext, (v) => v.setChatRecords);
const isChatRecordsLoaded = useContextSelector(ChatRecordContext, (v) => v.isChatRecordsLoaded);
const setIsChatRecordsLoaded = useContextSelector(
ChatRecordContext,
(v) => v.setIsChatRecordsLoaded
);
const ScrollData = useContextSelector(ChatRecordContext, (v) => v.ScrollData);
const appId = useContextSelector(ChatBoxContext, (v) => v.appId);
const chatId = useContextSelector(ChatBoxContext, (v) => v.chatId);
const outLinkAuthData = useContextSelector(ChatBoxContext, (v) => v.outLinkAuthData);
const welcomeText = useContextSelector(ChatBoxContext, (v) => v.welcomeText);
const variableList = useContextSelector(ChatBoxContext, (v) => v.variableList);
const allVariableList = useContextSelector(ChatBoxContext, (v) => v.allVariableList);
const questionGuide = useContextSelector(ChatBoxContext, (v) => v.questionGuide);
const autoExecute = useContextSelector(ChatBoxContext, (v) => v.autoExecute);
const startSegmentedAudio = useContextSelector(ChatBoxContext, (v) => v.startSegmentedAudio);
const finishSegmentedAudio = useContextSelector(ChatBoxContext, (v) => v.finishSegmentedAudio);
const setAudioPlayingChatId = useContextSelector(ChatBoxContext, (v) => v.setAudioPlayingChatId);
const splitText2Audio = useContextSelector(ChatBoxContext, (v) => v.splitText2Audio);
const isChatting = useContextSelector(ChatBoxContext, (v) => v.isChatting);
// Workflow running, there are user input or selection
const isInteractive = useMemo(
() => checkIsInteractiveByHistories(chatHistories),
[chatHistories]
);
const isInteractive = useMemo(() => checkIsInteractiveByHistories(chatRecords), [chatRecords]);
// compute variable input is finish.
const chatForm = useForm<ChatBoxInputFormType>({
@@ -195,18 +170,18 @@ const ChatBox = (
});
const { setValue, watch } = chatForm;
const chatStartedWatch = watch('chatStarted');
const chatStarted = chatStartedWatch || chatHistories.length > 0 || variableList.length === 0;
const chatStarted = chatStartedWatch || chatRecords.length > 0 || variableList.length === 0;
// 滚动到底部
const scrollToBottom = useMemoizedFn((behavior: 'smooth' | 'auto' = 'smooth', delay = 0) => {
setTimeout(() => {
if (!ChatBoxRef.current) {
if (!ScrollContainerRef.current) {
setTimeout(() => {
scrollToBottom(behavior);
}, 500);
} else {
ChatBoxRef.current.scrollTo({
top: ChatBoxRef.current.scrollHeight,
ScrollContainerRef.current.scrollTo({
top: ScrollContainerRef.current.scrollHeight,
behavior
});
}
@@ -216,10 +191,10 @@ const ChatBox = (
// 聊天信息生成中……获取当前滚动条位置,判断是否需要滚动到底部
const { run: generatingScroll } = useThrottleFn(
(force?: boolean) => {
if (!ChatBoxRef.current) return;
if (!ScrollContainerRef.current) return;
const isBottom =
ChatBoxRef.current.scrollTop + ChatBoxRef.current.clientHeight + 150 >=
ChatBoxRef.current.scrollHeight;
ScrollContainerRef.current.scrollTop + ScrollContainerRef.current.clientHeight + 150 >=
ScrollContainerRef.current.scrollHeight;
if (isBottom || force) {
scrollToBottom('auto');
@@ -241,7 +216,7 @@ const ChatBox = (
autoTTSResponse,
variables
}: generatingMessageProps & { autoTTSResponse?: boolean }) => {
setChatHistories((state) =>
setChatRecords((state) =>
state.map((item, index) => {
if (index !== state.length - 1) return item;
if (item.obj !== ChatRoleEnum.AI) return item;
@@ -370,11 +345,9 @@ const ChatBox = (
const result = await postQuestionGuide(
{
appId,
messages: chats2GPTMessages({ messages: histories, reserveId: false }).slice(-6),
shareId,
outLinkUid,
teamId,
teamToken
...outLinkAuthData
},
abortSignal
);
@@ -386,14 +359,14 @@ const ChatBox = (
}
} catch (error) {}
},
[questionGuide, shareId, outLinkUid, teamId, teamToken, scrollToBottom]
[questionGuide, appId, outLinkAuthData, scrollToBottom]
);
/* Abort chat completions, questionGuide */
const abortRequest = useMemoizedFn(() => {
chatController.current?.abort('stop');
questionGuideController.current?.abort('stop');
pluginController.current?.abort('stop');
const abortRequest = useMemoizedFn((signal: string = 'stop') => {
chatController.current?.abort(signal);
questionGuideController.current?.abort(signal);
pluginController.current?.abort(signal);
});
/**
@@ -403,9 +376,10 @@ const ChatBox = (
({
text = '',
files = [],
history = chatHistories,
history = chatRecords,
autoTTSResponse = false,
isInteractivePrompt = false
isInteractivePrompt = false,
hideInUI = false
}) => {
variablesForm.handleSubmit(
async ({ variables = {} }) => {
@@ -452,6 +426,7 @@ const ChatBox = (
dataId: getNanoid(24),
obj: ChatRoleEnum.Human,
time: new Date(),
hideInUI,
value: [
...files.map((file) => ({
type: ChatItemValueTypeEnum.file,
@@ -491,7 +466,7 @@ const ChatBox = (
];
// Update histories(Interactive input does not require new session rounds)
setChatHistories(
setChatRecords(
isInteractivePrompt
? // 把交互的结果存储到对话记录中,交互模式下,不需要新的会话轮次
setUserSelectResultToHistories(newChatList.slice(0, -2), text)
@@ -533,11 +508,9 @@ const ChatBox = (
});
}
isNewChatReplace.current = isNewChat;
// Set last chat finish status
let newChatHistories: ChatSiteItemType[] = [];
setChatHistories((state) => {
setChatRecords((state) => {
newChatHistories = state.map((item, index) => {
if (index !== state.length - 1) return item;
return {
@@ -577,11 +550,11 @@ const ChatBox = (
if (!err?.responseText) {
resetInputVal({ text, files });
// 这里的 newChatList 没包含用户交互输入的内容,所以重置后刚好是正确的。
setChatHistories(newChatList.slice(0, newChatList.length - 2));
setChatRecords(newChatList.slice(0, newChatList.length - 2));
}
// set finish status
setChatHistories((state) =>
setChatRecords((state) =>
state.map((item, index) => {
if (index !== state.length - 1) return item;
return {
@@ -603,26 +576,37 @@ const ChatBox = (
);
// retry input
const onDelMessage = useCallback(
(contentId: string) => {
return delChatRecordById({
appId,
chatId,
contentId,
...outLinkAuthData
});
},
[appId, chatId, outLinkAuthData]
);
const retryInput = useMemoizedFn((dataId?: string) => {
if (!dataId || !onDelMessage) return;
return async () => {
setLoading(true);
const index = chatHistories.findIndex((item) => item.dataId === dataId);
const delHistory = chatHistories.slice(index);
setIsLoading(true);
const index = chatRecords.findIndex((item) => item.dataId === dataId);
const delHistory = chatRecords.slice(index);
try {
await Promise.all(
delHistory.map((item) => {
if (item.dataId) {
return onDelMessage({ contentId: item.dataId });
return onDelMessage(item.dataId);
}
})
);
setChatHistories((state) => (index === 0 ? [] : state.slice(0, index)));
setChatRecords((state) => (index === 0 ? [] : state.slice(0, index)));
sendPrompt({
...formatChatValue2InputType(delHistory[0].value),
history: chatHistories.slice(0, index)
history: chatRecords.slice(0, index)
});
} catch (error) {
toast({
@@ -630,27 +614,22 @@ const ChatBox = (
title: getErrText(error, 'Retry failed')
});
}
setLoading(false);
setIsLoading(false);
};
});
// delete one message(One human and the ai response)
const delOneMessage = useMemoizedFn((dataId?: string) => {
if (!dataId || !onDelMessage) return;
const delOneMessage = useMemoizedFn((dataId: string) => {
return () => {
setChatHistories((state) => {
setChatRecords((state) => {
let aiIndex = -1;
return state.filter((chat, i) => {
if (chat.dataId === dataId) {
aiIndex = i + 1;
onDelMessage({
contentId: dataId
});
onDelMessage(dataId);
return false;
} else if (aiIndex === i && chat.obj === ChatRoleEnum.AI && chat.dataId) {
onDelMessage({
contentId: chat.dataId
});
onDelMessage(chat.dataId);
return false;
}
return true;
@@ -694,7 +673,7 @@ const ChatBox = (
if (!chat.dataId || !chatId || !appId) return;
const isGoodFeedback = !!chat.userGoodFeedback;
setChatHistories((state) =>
setChatRecords((state) =>
state.map((chatItem) =>
chatItem.dataId === chat.dataId
? {
@@ -722,7 +701,7 @@ const ChatBox = (
if (feedbackType !== FeedbackTypeEnum.admin) return;
return () => {
if (!chat.dataId || !chatId || !appId) return;
setChatHistories((state) =>
setChatRecords((state) =>
state.map((chatItem) =>
chatItem.dataId === chat.dataId ? { ...chatItem, userGoodFeedback: undefined } : chatItem
)
@@ -748,7 +727,7 @@ const ChatBox = (
if (chat.userBadFeedback) {
return () => {
if (!chat.dataId || !chatId || !appId) return;
setChatHistories((state) =>
setChatRecords((state) =>
state.map((chatItem) =>
chatItem.dataId === chat.dataId ? { ...chatItem, userBadFeedback: undefined } : chatItem
)
@@ -789,7 +768,7 @@ const ChatBox = (
index: i
});
// update dom
setChatHistories((state) =>
setChatRecords((state) =>
state.map((chatItem) =>
chatItem.obj === ChatRoleEnum.AI && chatItem.dataId === chat.dataId
? {
@@ -807,11 +786,11 @@ const ChatBox = (
() =>
feConfigs?.show_emptyChat &&
showEmptyIntro &&
chatHistories.length === 0 &&
chatRecords.length === 0 &&
!variableList?.length &&
!welcomeText,
[
chatHistories.length,
chatRecords.length,
feConfigs?.show_emptyChat,
showEmptyIntro,
variableList?.length,
@@ -820,26 +799,21 @@ const ChatBox = (
);
const statusBoxData = useCreation(() => {
if (!isChatting) return;
const chatContent = chatHistories[chatHistories.length - 1];
const chatContent = chatRecords[chatRecords.length - 1];
if (!chatContent) return;
return {
status: chatContent.status || ChatStatusEnum.loading,
name: t(chatContent.moduleName || ('' as any)) || t('common:common.Loading')
};
}, [chatHistories, isChatting, t]);
}, [chatRecords, isChatting, t]);
// page change and abort request
useEffect(() => {
isNewChatReplace.current = false;
setQuestionGuide([]);
return () => {
chatController.current?.abort('leave');
if (!isNewChatReplace.current) {
questionGuideController.current?.abort('leave');
}
};
}, [router.query]);
setValue('chatStarted', false);
abortRequest('leave');
}, [router.query, setValue, chatId]);
// add listener
useEffect(() => {
@@ -866,14 +840,31 @@ const ChatBox = (
eventBus.off(EventNameEnum.sendQuestion);
eventBus.off(EventNameEnum.editQuestion);
};
}, [resetInputVal, sendPrompt]);
}, [isReady, resetInputVal, sendPrompt]);
// Auto send prompt
useEffect(() => {
if (
isReady &&
autoExecute.open &&
chatStarted &&
chatRecords.length === 0 &&
isChatRecordsLoaded
) {
sendPrompt({
text: autoExecute.defaultPrompt || 'AUTO_EXECUTE',
hideInUI: true
});
}
}, [isReady, chatStarted, autoExecute?.open, chatRecords, isChatRecordsLoaded]);
// output data
useImperativeHandle(ref, () => ({
useImperativeHandle(ChatBoxRef, () => ({
restartChat() {
abortRequest();
setChatHistories([]);
setChatRecords([]);
setIsChatRecordsLoaded(false);
setValue('chatStarted', false);
},
scrollToBottom(behavior = 'auto') {
@@ -884,7 +875,7 @@ const ChatBox = (
const RenderRecords = useMemo(() => {
return (
<ScrollData
ScrollContainerRef={ChatBoxRef}
ScrollContainerRef={ScrollContainerRef}
flex={'1 0 0'}
h={0}
w={'100%'}
@@ -901,98 +892,94 @@ const ChatBox = (
)}
{/* chat history */}
<Box id={'history'}>
{chatHistories.map((item, index) => (
<>
{chatRecords.map((item, index) => (
<Box key={item.dataId}>
{/* 并且时间和上一条的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} />}
chatRecords[index - 1].time !== undefined &&
new Date(item.time).getTime() - new Date(chatRecords[index - 1].time!).getTime() >
10 * 60 * 1000 && <TimeBox time={item.time} />}
<Box key={item.dataId} py={6}>
{item.obj === ChatRoleEnum.Human && (
<Box py={item.hideInUI ? 0 : 6}>
{item.obj === ChatRoleEnum.Human && !item.hideInUI && (
<ChatItem
type={item.obj}
avatar={userAvatar}
chat={item}
onRetry={retryInput(item.dataId)}
onDelete={delOneMessage(item.dataId)}
isLastChild={index === chatHistories.length - 1}
isLastChild={index === chatRecords.length - 1}
/>
)}
{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')}
<ChatItem
type={item.obj}
avatar={appAvatar}
chat={item}
isLastChild={index === chatRecords.length - 1}
{...{
showVoiceIcon,
shareId,
outLinkUid,
teamId,
teamToken,
statusBoxData,
questionGuides,
onMark: onMark(
item,
formatChatValue2InputType(chatRecords[index - 1]?.value)?.text
),
onAddUserLike: onAddUserLike(item),
onCloseUserLike: onCloseUserLike(item),
onAddUserDislike: onAddUserDislike(item),
onReadUserDislike: onReadUserDislike(item)
}}
>
<ResponseTags
showTags={index !== chatRecords.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'} />}
>
<Checkbox
onChange={onCloseCustomFeedback(item, i)}
icon={<MyIcon name={'common/check'} w={'12px'} />}
>
{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>
{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>
</Box>
)}
</ChatItem>
</>
</Box>
)}
</ChatItem>
)}
</Box>
</>
</Box>
))}
</Box>
</Box>
@@ -1002,7 +989,7 @@ const ChatBox = (
ScrollData,
appAvatar,
chatForm,
chatHistories,
chatRecords,
chatStarted,
delOneMessage,
isChatting,
@@ -1029,23 +1016,28 @@ const ChatBox = (
]);
return (
<Flex flexDirection={'column'} h={'100%'} position={'relative'}>
<MyBox
isLoading={isLoading}
display={'flex'}
flexDirection={'column'}
h={'100%'}
position={'relative'}
>
<Script src={getWebReqUrl('/js/html2pdf.bundle.min.js')} strategy="lazyOnload"></Script>
{/* chat box container */}
{RenderRecords}
{/* message input */}
{onStartChat && chatStarted && active && appId && !isInteractive && (
{onStartChat && chatStarted && active && !isInteractive && (
<ChatInput
onSendMessage={sendPrompt}
onStop={() => chatController.current?.abort('stop')}
TextareaDom={TextareaDom}
resetInputVal={resetInputVal}
chatForm={chatForm}
appId={appId}
/>
)}
{/* user feedback modal */}
{!!feedbackId && chatId && appId && (
{!!feedbackId && chatId && (
<FeedbackModal
appId={appId}
teamId={teamId}
@@ -1056,7 +1048,7 @@ const ChatBox = (
outLinkUid={outLinkUid}
onClose={() => setFeedbackId(undefined)}
onSuccess={(content: string) => {
setChatHistories((state) =>
setChatRecords((state) =>
state.map((item) =>
item.dataId === feedbackId ? { ...item, userBadFeedback: content } : item
)
@@ -1071,7 +1063,7 @@ const ChatBox = (
content={readFeedbackData.content}
onClose={() => setReadFeedbackData(undefined)}
onCloseFeedback={() => {
setChatHistories((state) =>
setChatRecords((state) =>
state.map((chatItem) =>
chatItem.dataId === readFeedbackData.dataId
? { ...chatItem, userBadFeedback: undefined }
@@ -1106,7 +1098,7 @@ const ChatBox = (
});
// update dom
setChatHistories((state) =>
setChatRecords((state) =>
state.map((chatItem) =>
chatItem.dataId === adminMarkData.dataId
? {
@@ -1124,7 +1116,7 @@ const ChatBox = (
dataId: readFeedbackData.dataId,
userBadFeedback: undefined
});
setChatHistories((state) =>
setChatRecords((state) =>
state.map((chatItem) =>
chatItem.dataId === readFeedbackData.dataId
? { ...chatItem, userBadFeedback: undefined }
@@ -1136,17 +1128,16 @@ const ChatBox = (
}}
/>
)}
</Flex>
</MyBox>
);
};
const ForwardChatBox = forwardRef(ChatBox);
const ChatBoxContainer = (props: Props, ref: ForwardedRef<ComponentRef>) => {
const ChatBoxContainer = (props: Props) => {
return (
<ChatProvider {...props}>
<ForwardChatBox {...props} ref={ref} />
<ChatBox {...props} />
</ChatProvider>
);
};
export default React.memo(forwardRef(ChatBoxContainer));
export default React.memo(ChatBoxContainer);

View File

@@ -29,6 +29,7 @@ export type ChatBoxInputType = {
text?: string;
files?: UserInputFileItemType[];
isInteractivePrompt?: boolean;
hideInUI?: boolean;
};
export type SendPromptFnType = (

View File

@@ -1,4 +1,4 @@
import React, { useCallback, useEffect, useMemo, useRef, useState } from 'react';
import React, { useCallback, useEffect, useMemo, useState } from 'react';
import { Controller, useFieldArray } from 'react-hook-form';
import RenderPluginInput from './renderPluginInput';
import { Box, Button, Flex } from '@chakra-ui/react';
@@ -16,34 +16,36 @@ import { UserChatItemValueItemType } from '@fastgpt/global/core/chat/type';
import QuestionTip from '@fastgpt/web/components/common/MyTooltip/QuestionTip';
import { ChatBoxInputFormType } from '../../ChatBox/type';
import { FlowNodeInputItemType } from '@fastgpt/global/core/workflow/type/io';
import { ChatItemContext } from '@/web/core/chat/context/chatItemContext';
import { ChatRecordContext } from '@/web/core/chat/context/chatRecordContext';
const RenderInput = () => {
const { t } = useTranslation();
const {
pluginInputs,
variablesForm,
histories,
onStartChat,
onNewChat,
onSubmit,
isChatting,
chatConfig,
chatId,
outLinkAuthData
} = useContextSelector(PluginRunContext, (v) => v);
const pluginInputs = useContextSelector(ChatItemContext, (v) => v.chatBoxData?.app?.pluginInputs);
const variablesForm = useContextSelector(ChatItemContext, (v) => v.variablesForm);
const histories = useContextSelector(ChatRecordContext, (v) => v.chatRecords);
const onStartChat = useContextSelector(PluginRunContext, (v) => v.onStartChat);
const onNewChat = useContextSelector(PluginRunContext, (v) => v.onNewChat);
const onSubmit = useContextSelector(PluginRunContext, (v) => v.onSubmit);
const isChatting = useContextSelector(PluginRunContext, (v) => v.isChatting);
const fileSelectConfig = useContextSelector(PluginRunContext, (v) => v.fileSelectConfig);
const instruction = useContextSelector(PluginRunContext, (v) => v.instruction);
const chatId = useContextSelector(PluginRunContext, (v) => v.chatId);
const outLinkAuthData = useContextSelector(PluginRunContext, (v) => v.outLinkAuthData);
const {
control,
handleSubmit,
reset,
getValues,
formState: { errors }
} = variablesForm;
/* ===> Global files(abandon) */
const fileCtrl = useFieldArray({
control: variablesForm.control,
control,
name: 'files'
});
const {
@@ -58,9 +60,7 @@ const RenderInput = () => {
removeFiles,
hasFileUploading
} = useFileUpload({
outLinkAuthData,
chatId: chatId || '',
fileSelectConfig: chatConfig?.fileSelectConfig,
fileSelectConfig,
fileCtrl
});
const isDisabledInput = histories.length > 0;
@@ -169,7 +169,7 @@ const RenderInput = () => {
return (
<Box>
{/* instruction */}
{chatConfig?.instruction && (
{instruction && (
<Box
border={'1px solid'}
borderColor={'myGray.250'}
@@ -179,7 +179,7 @@ const RenderInput = () => {
color={'myGray.600'}
mb={4}
>
<Markdown source={chatConfig.instruction} />
<Markdown source={instruction} />
</Box>
)}
{/* file select(Abandoned) */}

View File

@@ -7,9 +7,13 @@ import { FlowNodeTypeEnum } from '@fastgpt/global/core/workflow/node/constant';
import AIResponseBox from '../../../components/AIResponseBox';
import { useTranslation } from 'next-i18next';
import ComplianceTip from '@/components/common/ComplianceTip/index';
import { ChatRecordContext } from '@/web/core/chat/context/chatRecordContext';
const RenderOutput = () => {
const { histories, isChatting } = useContextSelector(PluginRunContext, (v) => v);
const { t } = useTranslation();
const histories = useContextSelector(ChatRecordContext, (v) => v.chatRecords);
const isChatting = useContextSelector(PluginRunContext, (v) => v.isChatting);
const pluginOutputs = useMemo(() => {
const pluginOutputs = histories?.[1]?.responseData?.find(
(item) => item.moduleType === FlowNodeTypeEnum.pluginOutput

View File

@@ -4,16 +4,20 @@ import { useContextSelector } from 'use-context-selector';
import { PluginRunContext } from '../context';
import { Box } from '@chakra-ui/react';
import { useTranslation } from 'next-i18next';
import { ChatRecordContext } from '@/web/core/chat/context/chatRecordContext';
const RenderResponseDetail = () => {
const { histories, isChatting } = useContextSelector(PluginRunContext, (v) => v);
const { t } = useTranslation();
const responseData = histories?.[1]?.responseData || [];
const chatRecords = useContextSelector(ChatRecordContext, (v) => v.chatRecords);
const isChatting = useContextSelector(PluginRunContext, (v) => v.isChatting);
const responseData = chatRecords?.[1]?.responseData || [];
return isChatting ? (
<>{t('chat:in_progress')}</>
) : (
<Box flex={'1 0 0'} h={'100%'} overflow={'auto'}>
<ResponseBox useMobile={true} response={responseData} />
<ResponseBox useMobile={true} response={responseData} dataId={chatRecords?.[1]?.dataId} />
</Box>
);
};

View File

@@ -9,7 +9,6 @@ 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';
@@ -18,6 +17,8 @@ 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';
import { ChatItemContext } from '@/web/core/chat/context/chatItemContext';
import { ChatRecordContext } from '@/web/core/chat/context/chatRecordContext';
const JsonEditor = dynamic(() => import('@fastgpt/web/components/common/Textarea/JsonEditor'));
@@ -33,10 +34,9 @@ const FileSelector = ({
value: any;
}) => {
const { t } = useTranslation();
const { variablesForm, histories, chatId, outLinkAuthData } = useContextSelector(
PluginRunContext,
(v) => v
);
const variablesForm = useContextSelector(ChatItemContext, (v) => v.variablesForm);
const histories = useContextSelector(ChatRecordContext, (v) => v.chatRecords);
const fileCtrl = useFieldArray({
control: variablesForm.control,
@@ -53,8 +53,6 @@ const FileSelector = ({
replaceFiles,
hasFileUploading
} = useFileUpload({
outLinkAuthData,
chatId: chatId || '',
fileSelectConfig: {
canSelectFile: input.canSelectFile ?? true,
canSelectImg: input.canSelectImg ?? false,
@@ -83,7 +81,7 @@ const FileSelector = ({
useRequest2(uploadFiles, {
manual: false,
errorToast: t('common:upload_file_error'),
refreshDeps: [fileList, outLinkAuthData, chatId]
refreshDeps: [fileList]
});
useEffect(() => {

View File

@@ -1,12 +1,8 @@
import React, { ReactNode, useCallback, useMemo, useRef } from 'react';
import { createContext } from 'use-context-selector';
import { createContext, useContextSelector } from 'use-context-selector';
import { PluginRunBoxProps } from './type';
import {
AIChatItemValueItemType,
ChatSiteItemType,
RuntimeUserPromptType
} from '@fastgpt/global/core/chat/type';
import { FieldValues, useForm } from 'react-hook-form';
import { AIChatItemValueItemType, RuntimeUserPromptType } from '@fastgpt/global/core/chat/type';
import { FieldValues } from 'react-hook-form';
import { PluginRunBoxTabEnum } from './constants';
import { useToast } from '@fastgpt/web/hooks/useToast';
import { getNanoid } from '@fastgpt/global/common/string/tools';
@@ -14,48 +10,51 @@ import { ChatItemValueTypeEnum, ChatRoleEnum } from '@fastgpt/global/core/chat/c
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 } from '../ChatBox/type';
import { chats2GPTMessages } from '@fastgpt/global/core/chat/adapt';
import { getPluginRunUserQuery } from '@fastgpt/global/core/workflow/utils';
import { cloneDeep } from 'lodash';
import { ChatItemContext } from '@/web/core/chat/context/chatItemContext';
import { ChatRecordContext } from '@/web/core/chat/context/chatRecordContext';
import { AppFileSelectConfigType } from '@fastgpt/global/core/app/type';
import { defaultAppSelectFileConfig } from '@fastgpt/global/core/app/constants';
type PluginRunContextType = OutLinkChatAuthProps &
PluginRunBoxProps & {
isChatting: boolean;
onSubmit: (e: ChatBoxInputFormType) => Promise<any>;
outLinkAuthData: OutLinkChatAuthProps;
};
type PluginRunContextType = PluginRunBoxProps & {
isChatting: boolean;
onSubmit: (e: ChatBoxInputFormType) => Promise<any>;
instruction: string;
fileSelectConfig: AppFileSelectConfigType;
};
export const PluginRunContext = createContext<PluginRunContextType>({
pluginInputs: [],
histories: [],
setHistories: function (value: React.SetStateAction<ChatSiteItemType[]>): void {
throw new Error('Function not implemented.');
},
appId: '',
tab: PluginRunBoxTabEnum.input,
setTab: function (value: React.SetStateAction<PluginRunBoxTabEnum>): void {
throw new Error('Function not implemented.');
},
isChatting: false,
onSubmit: function (e: FieldValues): Promise<any> {
throw new Error('Function not implemented.');
},
outLinkAuthData: {},
//@ts-ignore
variablesForm: undefined
instruction: '',
fileSelectConfig: defaultAppSelectFileConfig,
appId: '',
chatId: '',
outLinkAuthData: {}
});
const PluginRunContextProvider = ({
shareId,
outLinkUid,
teamId,
teamToken,
children,
...props
}: PluginRunBoxProps & { children: ReactNode }) => {
const { pluginInputs, onStartChat, setHistories, histories, setTab } = props;
const { onStartChat } = props;
const pluginInputs = useContextSelector(ChatItemContext, (v) => v.chatBoxData?.app?.pluginInputs);
const setTab = useContextSelector(ChatItemContext, (v) => v.setPluginRunTab);
const setChatRecords = useContextSelector(ChatRecordContext, (v) => v.setChatRecords);
const chatRecords = useContextSelector(ChatRecordContext, (v) => v.chatRecords);
const chatConfig = useContextSelector(ChatItemContext, (v) => v.chatBoxData?.app?.chatConfig);
const { instruction = '', fileSelectConfig = defaultAppSelectFileConfig } = useMemo(
() => chatConfig || {},
[chatConfig]
);
const { toast } = useToast();
const chatController = useRef(new AbortController());
@@ -65,23 +64,9 @@ const PluginRunContextProvider = ({
chatController.current?.abort('stop');
}, []);
const outLinkAuthData = useMemo(
() => ({
shareId,
outLinkUid,
teamId,
teamToken
}),
[shareId, outLinkUid, teamId, teamToken]
);
const variablesForm = useForm<ChatBoxInputFormType>({
defaultValues: {}
});
const generatingMessage = useCallback(
({ event, text = '', status, name, tool }: generatingMessageProps) => {
setHistories((state) =>
setChatRecords((state) =>
state.map((item, index) => {
if (index !== state.length - 1 || item.obj !== ChatRoleEnum.AI) return item;
@@ -165,12 +150,14 @@ const PluginRunContextProvider = ({
})
);
},
[setHistories]
[setChatRecords]
);
const isChatting = useMemo(
() => histories[histories.length - 1] && histories[histories.length - 1]?.status !== 'finish',
[histories]
() =>
chatRecords[chatRecords.length - 1] &&
chatRecords[chatRecords.length - 1]?.status !== 'finish',
[chatRecords]
);
const onSubmit = useCallback(
@@ -189,7 +176,7 @@ const PluginRunContextProvider = ({
const abortSignal = new AbortController();
chatController.current = abortSignal;
setHistories([
setChatRecords([
{
...getPluginRunUserQuery({
pluginInputs,
@@ -255,7 +242,7 @@ const PluginRunContextProvider = ({
});
}
setHistories((state) =>
setChatRecords((state) =>
state.map((item, index) => {
if (index !== state.length - 1) return item;
return {
@@ -267,7 +254,7 @@ const PluginRunContextProvider = ({
);
} catch (err: any) {
toast({ title: err.message, status: 'error' });
setHistories((state) =>
setChatRecords((state) =>
state.map((item, index) => {
if (index !== state.length - 1) return item;
return {
@@ -284,7 +271,7 @@ const PluginRunContextProvider = ({
isChatting,
onStartChat,
pluginInputs,
setHistories,
setChatRecords,
setTab,
t,
toast
@@ -295,8 +282,8 @@ const PluginRunContextProvider = ({
...props,
isChatting,
onSubmit,
outLinkAuthData,
variablesForm
instruction,
fileSelectConfig
};
return <PluginRunContext.Provider value={contextValue}>{children}</PluginRunContext.Provider>;
};

View File

@@ -2,29 +2,23 @@ import React from 'react';
import { PluginRunBoxTabEnum } from './constants';
import { PluginRunBoxProps } from './type';
import RenderInput from './components/RenderInput';
import PluginRunContextProvider, { PluginRunContext } from './context';
import PluginRunContextProvider from './context';
import { useContextSelector } from 'use-context-selector';
import RenderOutput from './components/RenderOutput';
import RenderResponseDetail from './components/RenderResponseDetail';
import { ChatItemContext } from '@/web/core/chat/context/chatItemContext';
const PluginRunBox = () => {
const { tab } = useContextSelector(PluginRunContext, (v) => v);
const PluginRunBox = (props: PluginRunBoxProps) => {
const tab = useContextSelector(ChatItemContext, (v) => v.pluginRunTab);
const formatTab = props.showTab || tab;
return (
<>
{tab === PluginRunBoxTabEnum.input && <RenderInput />}
{tab === PluginRunBoxTabEnum.output && <RenderOutput />}
{tab === PluginRunBoxTabEnum.detail && <RenderResponseDetail />}
</>
);
};
const Render = (props: PluginRunBoxProps) => {
return (
<PluginRunContextProvider {...props}>
<PluginRunBox />
{formatTab === PluginRunBoxTabEnum.input && <RenderInput />}
{formatTab === PluginRunBoxTabEnum.output && <RenderOutput />}
{formatTab === PluginRunBoxTabEnum.detail && <RenderResponseDetail />}
</PluginRunContextProvider>
);
};
export default Render;
export default PluginRunBox;

View File

@@ -7,18 +7,12 @@ import React from 'react';
import { onStartChatType } from '../type';
import { ChatBoxInputFormType } from '../ChatBox/type';
export type PluginRunBoxProps = OutLinkChatAuthProps & {
pluginInputs: FlowNodeInputItemType[];
variablesForm: UseFormReturn<ChatBoxInputFormType, any>;
histories: ChatSiteItemType[]; // chatHistories[1] is the response
setHistories: React.Dispatch<React.SetStateAction<ChatSiteItemType[]>>;
export type PluginRunBoxProps = {
appId: string;
chatId: string;
outLinkAuthData?: OutLinkChatAuthProps;
onStartChat?: onStartChatType;
onNewChat?: () => void;
appId: string;
chatId?: string;
tab: PluginRunBoxTabEnum;
setTab: React.Dispatch<React.SetStateAction<PluginRunBoxTabEnum>>;
chatConfig?: AppChatConfigType;
showTab?: PluginRunBoxTabEnum; // 如何设置了该字段,全局都 tab 不生效
};

View File

@@ -1,12 +1,13 @@
import React, { useMemo } from 'react';
import React from 'react';
import { FieldArrayWithId } from 'react-hook-form';
import { ChatBoxInputFormType } from '../ChatBox/type';
import { Box, CircularProgress, Flex, HStack, Image } from '@chakra-ui/react';
import { Box, CircularProgress, Flex, HStack } from '@chakra-ui/react';
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';
import { getFileIcon } from '@fastgpt/global/common/file/icon';
const RenderFilePreview = ({
fileList,
@@ -29,6 +30,8 @@ const RenderFilePreview = ({
{fileList.map((item, index) => {
const isFile = item.type === ChatFileTypeEnum.file;
const isImage = item.type === ChatFileTypeEnum.image;
const icon = getFileIcon(item.name);
return (
<MyBox
key={index}
@@ -73,7 +76,7 @@ const RenderFilePreview = ({
{isImage && (
<MyImage
alt={'img'}
src={item.icon}
src={item.icon || item.url}
w={'full'}
h={'full'}
borderRadius={'md'}
@@ -82,7 +85,7 @@ const RenderFilePreview = ({
)}
{isFile && (
<HStack alignItems={'center'} h={'full'}>
<MyIcon name={item.icon as any} w={['1.5rem', '2rem']} h={['1.5rem', '2rem']} />
<MyIcon name={icon as any} w={['1.5rem', '2rem']} h={['1.5rem', '2rem']} />
<Box flex={'1 0 0'} pr={2} className="textEllipsis" fontSize={'xs'}>
{item.name}
</Box>

View File

@@ -1,113 +0,0 @@
import { ChatSiteItemType } from '@fastgpt/global/core/chat/type';
import { useCallback, useRef, useState } from 'react';
import { useForm } from 'react-hook-form';
import { PluginRunBoxTabEnum } from './PluginRunBox/constants';
import {
ChatBoxInputFormType,
ComponentRef as ChatComponentRef,
SendPromptFnType
} from './ChatBox/type';
import { eventBus, EventNameEnum } from '@/web/common/utils/eventbus';
import { getChatRecords } from '@/web/core/chat/api';
import { ChatStatusEnum } from '@fastgpt/global/core/chat/constants';
import { getNanoid } from '@fastgpt/global/common/string/tools';
import { useScrollPagination } from '@fastgpt/web/hooks/useScrollPagination';
import { PaginationResponse } from '@fastgpt/web/common/fetch/type';
import type { getPaginationRecordsBody } from '@/pages/api/core/chat/getPaginationRecords';
import { GetChatTypeEnum } from '@/global/core/chat/constants';
export const useChat = (params?: { chatId?: string; appId: string; type?: GetChatTypeEnum }) => {
const ChatBoxRef = useRef<ChatComponentRef>(null);
const variablesForm = useForm<ChatBoxInputFormType>();
// plugin
const [pluginRunTab, setPluginRunTab] = useState<PluginRunBoxTabEnum>(PluginRunBoxTabEnum.input);
const resetVariables = useCallback(
(props?: { variables?: Record<string, any> }) => {
const { variables = {} } = props || {};
// Reset to empty input
const data = variablesForm.getValues();
// 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: {
...resetVariables,
...variables
}
});
},
[variablesForm]
);
const clearChatRecords = useCallback(() => {
const data = variablesForm.getValues();
for (const key in data.variables) {
variablesForm.setValue(`variables.${key}`, '');
}
ChatBoxRef.current?.restartChat?.();
}, [variablesForm]);
const {
data: chatRecords,
ScrollData,
setData: setChatRecords,
total: totalRecordsCount
} = useScrollPagination(
async (data: getPaginationRecordsBody): Promise<PaginationResponse<ChatSiteItemType>> => {
const res = await getChatRecords(data);
// First load scroll to bottom
if (data.offset === 0) {
function scrollToBottom() {
requestAnimationFrame(
ChatBoxRef?.current ? () => ChatBoxRef?.current?.scrollToBottom?.() : scrollToBottom
);
}
scrollToBottom();
}
return {
...res,
list: res.list.map((item) => ({
...item,
dataId: item.dataId || getNanoid(),
status: ChatStatusEnum.finish
}))
};
},
{
pageSize: 10,
refreshDeps: [params],
params,
scrollLoadType: 'top'
}
);
return {
ChatBoxRef,
variablesForm,
pluginRunTab,
setPluginRunTab,
clearChatRecords,
resetVariables,
chatRecords,
ScrollData,
setChatRecords,
totalRecordsCount
};
};
export const onSendPrompt: SendPromptFnType = (e) => eventBus.emit(EventNameEnum.sendQuestion, e);

View File

@@ -8,12 +8,6 @@ import {
Box,
Button,
Flex,
Input,
NumberDecrementStepper,
NumberIncrementStepper,
NumberInput,
NumberInputField,
NumberInputStepper,
Textarea
} from '@chakra-ui/react';
import { ChatItemValueTypeEnum } from '@fastgpt/global/core/chat/constants';
@@ -31,15 +25,16 @@ import {
UserSelectInteractive
} from '@fastgpt/global/core/workflow/template/system/interactive/type';
import { isEqual } from 'lodash';
import { onSendPrompt } from '../ChatContainer/useChat';
import FormLabel from '@fastgpt/web/components/common/MyBox/FormLabel';
import QuestionTip from '@fastgpt/web/components/common/MyTooltip/QuestionTip';
import { FlowNodeInputTypeEnum } from '@fastgpt/global/core/workflow/node/constant';
import { useTranslation } from 'react-i18next';
import { useTranslation } from 'next-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';
import { SendPromptFnType } from '../ChatContainer/ChatBox/type';
import { eventBus, EventNameEnum } from '@/web/common/utils/eventbus';
type props = {
value: UserChatItemValueItemType | AIChatItemValueItemType;
@@ -47,6 +42,8 @@ type props = {
isChatting: boolean;
};
const onSendPrompt: SendPromptFnType = (e) => eventBus.emit(EventNameEnum.sendQuestion, e);
const RenderText = React.memo(function RenderText({
showAnimation,
text
@@ -215,6 +212,7 @@ const RenderUserFormInteractive = React.memo(function RenderFormInput({
return (
<Flex flexDirection={'column'} gap={2} w={'250px'}>
{interactive.params.description && <Markdown source={interactive.params.description} />}
{interactive.params.inputForm?.map((input) => (
<Box key={input.label}>
<Flex mb={1} alignItems={'center'}>

View File

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

View File

@@ -9,6 +9,7 @@ import MyTooltip from '@fastgpt/web/components/common/MyTooltip';
import dynamic from 'next/dynamic';
import MyBox from '@fastgpt/web/components/common/MyBox';
import { SearchScoreTypeEnum, SearchScoreTypeMap } from '@fastgpt/global/core/dataset/constants';
import type { readCollectionSourceBody } from '@/pages/api/core/dataset/collection/read';
const InputDataModal = dynamic(() => import('@/pages/dataset/detail/components/InputDataModal'));
@@ -45,12 +46,13 @@ const scoreTheme: Record<
const QuoteItem = ({
quoteItem,
canViewSource,
canEditDataset
canEditDataset,
...RawSourceBoxProps
}: {
quoteItem: SearchDataResponseItemType;
canViewSource?: boolean;
canEditDataset?: boolean;
}) => {
} & Omit<readCollectionSourceBody, 'collectionId'>) => {
const { t } = useTranslation();
const [editInputData, setEditInputData] = useState<{ dataId: string; collectionId: string }>();
@@ -196,6 +198,7 @@ const QuoteItem = ({
sourceName={quoteItem.sourceName}
sourceId={quoteItem.sourceId}
canView={canViewSource}
{...RawSourceBoxProps}
/>
<Box flex={1} />
{quoteItem.id && canEditDataset && (

View File

@@ -6,10 +6,10 @@ 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';
import type { readCollectionSourceBody } from '@/pages/api/core/dataset/collection/read';
type Props = BoxProps &
ShareChatAuthProps & {
readCollectionSourceBody & {
sourceName?: string;
collectionId: string;
sourceId?: string;
@@ -18,11 +18,18 @@ type Props = BoxProps &
const RawSourceBox = ({
sourceId,
collectionId,
sourceName = '',
canView = true,
collectionId,
appId,
chatId,
chatItemId,
shareId,
outLinkUid,
teamId,
teamToken,
...props
}: Props) => {
const { t } = useTranslation();
@@ -33,8 +40,13 @@ const RawSourceBox = ({
const icon = useMemo(() => getSourceNameIcon({ sourceId, sourceName }), [sourceId, sourceName]);
const read = getCollectionSourceAndOpen({
collectionId,
appId,
chatId,
chatItemId,
shareId,
outLinkUid
outLinkUid,
teamId,
teamToken
});
return (
@@ -56,7 +68,7 @@ const RawSourceBox = ({
: {})}
{...props}
>
<MyIcon name={icon as any} w={['16px', '20px']} mr={2} />
<MyIcon name={icon as any} w={['1rem', '1.25rem']} mr={2} />
<Box
maxW={['200px', '300px']}
className={props.className ?? 'textEllipsis'}

View File

@@ -45,6 +45,18 @@ const SearchParamsTip = ({
borderRadius={'lg'}
borderWidth={'1px'}
borderColor={'primary.1'}
sx={{
'&::-webkit-scrollbar': {
height: '6px',
borderRadius: '4px'
},
'&::-webkit-scrollbar-thumb': {
backgroundColor: 'myGray.250 !important',
'&:hover': {
backgroundColor: 'myGray.300 !important'
}
}
}}
>
<Table fontSize={'xs'} overflow={'overlay'}>
<Thead>

View File

@@ -2,5 +2,6 @@ import { ChatCompletionMessageParam } from '@fastgpt/global/core/ai/type.d';
import type { OutLinkChatAuthProps } from '@fastgpt/global/support/permission/chat.d';
export type CreateQuestionGuideParams = OutLinkChatAuthProps & {
appId: string;
messages: ChatCompletionMessageParam[];
};

View File

@@ -15,7 +15,6 @@ export type AppUpdateParams = {
};
export type PostPublishAppProps = {
type: AppTypeEnum;
nodes: AppSchema['modules'];
edges: AppSchema['edges'];
chatConfig: AppSchema['chatConfig'];

View File

@@ -5,7 +5,8 @@ import { AppTypeEnum } from '@fastgpt/global/core/app/constants';
import { RequestPaging } from '@/types';
import { GetChatTypeEnum } from '@/global/core/chat/constants';
import { ChatSourceEnum } from '@fastgpt/global/core/chat/constants';
export type GetChatSpeechProps = {
export type GetChatSpeechProps = OutLinkChatAuthProps & {
appId: string;
ttsConfig: AppTTSConfigType;
input: string;
shareId?: string;
@@ -73,7 +74,7 @@ export type DelHistoryProps = OutLinkChatAuthProps & {
chatId: string;
};
export type ClearHistoriesProps = OutLinkChatAuthProps & {
appId?: string;
appId: string;
};
/* -------- chat item ---------- */

View File

@@ -28,10 +28,12 @@ export function addStatisticalDataToHistoryItem(historyItem: ChatItemType) {
const flatResData: ChatHistoryItemResType[] =
historyItem.responseData
?.map((item) => {
if (item.pluginDetail || item.toolDetail) {
return [item, ...(item.pluginDetail || []), ...(item.toolDetail || [])];
}
return item;
return [
item,
...(item.pluginDetail || []),
...(item.toolDetail || []),
...(item.loopDetail || [])
];
})
.flat() || [];

View File

@@ -0,0 +1,55 @@
import { exit } from 'process';
/*
Init system
*/
export async function register() {
try {
if (process.env.NEXT_RUNTIME === 'nodejs') {
// 基础系统初始化
const [
{ connectMongo },
{ systemStartCb },
{ initGlobalVariables },
{ getInitConfig },
{ initVectorStore },
{ initRootUser },
{ getSystemPluginCb },
{ startMongoWatch },
{ startCron },
{ startTrainingQueue }
] = await Promise.all([
import('@fastgpt/service/common/mongo/init'),
import('@fastgpt/service/common/system/tools'),
import('@/service/common/system'),
import('@/service/common/system'),
import('@fastgpt/service/common/vectorStore/controller'),
import('@/service/mongo'),
import('@/service/core/app/plugin'),
import('@/service/common/system/volumnMongoWatch'),
import('@/service/common/system/cron'),
import('@/service/core/dataset/training/utils')
]);
// 执行初始化流程
systemStartCb();
initGlobalVariables();
// Connect to MongoDB
await connectMongo();
//init system configinit vector databaseinit root user
await Promise.all([getInitConfig(), initVectorStore(), initRootUser()]);
getSystemPluginCb();
startMongoWatch();
startCron();
startTrainingQueue(true);
console.log('Init system success');
}
} catch (error) {
console.log('Init system error', error);
exit(1);
}
}

View File

@@ -2,7 +2,7 @@
Read db file content and response 3000 words
*/
import type { NextApiResponse } from 'next';
import { authFile } from '@fastgpt/service/support/permission/auth/file';
import { authCollectionFile } from '@fastgpt/service/support/permission/auth/file';
import { NextAPI } from '@/service/middleware/entry';
import { DatasetSourceReadTypeEnum } from '@fastgpt/global/core/dataset/constants';
import { readDatasetSourceRawText } from '@fastgpt/service/core/dataset/read';
@@ -26,7 +26,7 @@ async function handler(req: ApiRequestProps<PreviewContextProps>, res: NextApiRe
const { teamId } = await (async () => {
if (type === DatasetSourceReadTypeEnum.fileLocal) {
return authFile({
return authCollectionFile({
req,
authToken: true,
authApiKey: true,

View File

@@ -9,7 +9,17 @@ 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';
import { authChatCert } from '@/service/support/permission/auth/chat';
import { authChatCrud } from '@/service/support/permission/auth/chat';
import { authDataset } from '@fastgpt/service/support/permission/dataset/auth';
import { OutLinkChatAuthProps } from '@fastgpt/global/support/permission/chat';
import { WritePermissionVal } from '@fastgpt/global/support/permission/constant';
export type UploadChatFileProps = {
appId: string;
} & OutLinkChatAuthProps;
export type UploadDatasetFileProps = {
datasetId: string;
};
const authUploadLimit = (tmbId: string) => {
if (!global.feConfigs.uploadFileMaxAmount) return;
@@ -28,15 +38,43 @@ async function handler(req: NextApiRequest, res: NextApiResponse<any>) {
const upload = getUploadModel({
maxSize: global.feConfigs?.uploadFileMaxSize
});
const { file, bucketName, metadata } = await upload.doUpload(req, res);
const { file, bucketName, metadata, data } = await upload.doUpload<
UploadChatFileProps | UploadDatasetFileProps
>(req, res);
filePaths.push(file.path);
const { teamId, tmbId, outLinkUid } = await authChatCert({
req,
authToken: true,
authApiKey: true
});
await authUploadLimit(outLinkUid || tmbId);
const { teamId, uid } = await (async () => {
if (bucketName === 'chat') {
const chatData = data as UploadChatFileProps;
const authData = await authChatCrud({
req,
authToken: true,
authApiKey: true,
...chatData
});
return {
teamId: authData.teamId,
uid: authData.uid
};
}
if (bucketName === 'dataset') {
const chatData = data as UploadDatasetFileProps;
const authData = await authDataset({
datasetId: chatData.datasetId,
per: WritePermissionVal,
req,
authToken: true,
authApiKey: true
});
return {
teamId: authData.teamId,
uid: authData.tmbId
};
}
return Promise.reject('bucketName is empty');
})();
await authUploadLimit(uid);
addLog.info(`Upload file success ${file.originalname}, cost ${Date.now() - start}ms`);
@@ -46,7 +84,7 @@ async function handler(req: NextApiRequest, res: NextApiResponse<any>) {
const fileId = await uploadFile({
teamId,
tmbId,
uid,
bucketName,
path: file.path,
filename: file.originalname,
@@ -61,7 +99,7 @@ async function handler(req: NextApiRequest, res: NextApiResponse<any>) {
previewUrl: `${ReadFileBaseUrl}/${file.originalname}?token=${await createFileToken({
bucketName,
teamId,
tmbId,
uid,
fileId
})}`
}

View File

@@ -1,16 +1,19 @@
import type { NextApiRequest, NextApiResponse } from 'next';
import { jsonRes } from '@fastgpt/service/common/response';
import { connectToDatabase } from '@/service/mongo';
import { authChatCert } from '@/service/support/permission/auth/chat';
import { uploadMongoImg } from '@fastgpt/service/common/file/image/controller';
import { UploadImgProps } from '@fastgpt/global/common/file/api';
import { authCert } from '@fastgpt/service/support/permission/auth/common';
/*
Upload avatar image
*/
export default async function handler(req: NextApiRequest, res: NextApiResponse) {
try {
await connectToDatabase();
const body = req.body as UploadImgProps;
const { teamId } = await authChatCert({ req, authToken: true });
const { teamId } = await authCert({ req, authToken: true });
const imgId = await uploadMongoImg({
teamId,

View File

@@ -1,15 +1,25 @@
import type { NextApiRequest, NextApiResponse } from 'next';
import type { NextApiResponse } from 'next';
import { jsonRes } from '@fastgpt/service/common/response';
import { connectToDatabase } from '@/service/mongo';
import type { CreateQuestionGuideParams } from '@/global/core/ai/api.d';
import { pushQuestionGuideUsage } from '@/service/support/wallet/usage/push';
import { createQuestionGuide } from '@fastgpt/service/core/ai/functions/createQuestionGuide';
import { authChatCert } from '@/service/support/permission/auth/chat';
import { ApiRequestProps } from '@fastgpt/service/type/next';
import { NextAPI } from '@/service/middleware/entry';
import { OutLinkChatAuthProps } from '@fastgpt/global/support/permission/chat';
import { ChatCompletionMessageParam } from '@fastgpt/global/core/ai/type';
export default async function handler(req: NextApiRequest, res: NextApiResponse<any>) {
async function handler(
req: ApiRequestProps<
OutLinkChatAuthProps & {
messages: ChatCompletionMessageParam[];
}
>,
res: NextApiResponse<any>
) {
try {
await connectToDatabase();
const { messages } = req.body as CreateQuestionGuideParams;
const { messages } = req.body;
const { tmbId, teamId } = await authChatCert({
req,
@@ -40,3 +50,5 @@ export default async function handler(req: NextApiRequest, res: NextApiResponse<
});
}
}
export default NextAPI(handler);

View File

@@ -0,0 +1,47 @@
import type { NextApiResponse } from 'next';
import { jsonRes } from '@fastgpt/service/common/response';
import { connectToDatabase } from '@/service/mongo';
import type { CreateQuestionGuideParams } from '@/global/core/ai/api.d';
import { pushQuestionGuideUsage } from '@/service/support/wallet/usage/push';
import { createQuestionGuide } from '@fastgpt/service/core/ai/functions/createQuestionGuide';
import { authChatCrud } from '@/service/support/permission/auth/chat';
import { ApiRequestProps } from '@fastgpt/service/type/next';
import { NextAPI } from '@/service/middleware/entry';
async function handler(req: ApiRequestProps<CreateQuestionGuideParams>, res: NextApiResponse<any>) {
try {
await connectToDatabase();
const { messages } = req.body;
const { tmbId, teamId } = await authChatCrud({
req,
authToken: true,
authApiKey: true,
...req.body
});
const qgModel = global.llmModels[0];
const { result, tokens } = await createQuestionGuide({
messages,
model: qgModel.model
});
jsonRes(res, {
data: result
});
pushQuestionGuideUsage({
tokens,
teamId,
tmbId
});
} catch (err) {
jsonRes(res, {
code: 500,
error: err
});
}
}
export default NextAPI(handler);

View File

@@ -43,10 +43,10 @@ async function handler(
});
return { id: appId };
} else {
await MongoApp.findByIdAndUpdate(appId, { type: AppTypeEnum.workflow });
}
await MongoApp.findByIdAndUpdate(appId, { type: AppTypeEnum.workflow });
return {};
}

View File

@@ -8,17 +8,14 @@ import { beforeUpdateAppFormat } from '@fastgpt/service/core/app/controller';
import { getNextTimeByCronStringAndTimezone } from '@fastgpt/global/common/string/time';
import { PostPublishAppProps } from '@/global/core/app/api';
import { WritePermissionVal } from '@fastgpt/global/support/permission/constant';
import { ApiRequestProps } from '@fastgpt/service/type/next';
async function handler(req: NextApiRequest, res: NextApiResponse<any>): Promise<{}> {
async function handler(
req: ApiRequestProps<PostPublishAppProps>,
res: NextApiResponse<any>
): Promise<{}> {
const { appId } = req.query as { appId: string };
const {
nodes = [],
edges = [],
chatConfig,
type,
isPublish,
versionName
} = req.body as PostPublishAppProps;
const { nodes = [], edges = [], chatConfig, isPublish, versionName } = req.body;
const { tmbId } = await authApp({ appId, req, per: WritePermissionVal, authToken: true });
@@ -50,11 +47,13 @@ async function handler(req: NextApiRequest, res: NextApiResponse<any>): Promise<
chatConfig,
updateTime: new Date(),
version: 'v2',
type,
scheduledTriggerConfig: chatConfig?.scheduledTriggerConfig,
scheduledTriggerNextTime: chatConfig?.scheduledTriggerConfig?.cronString
? getNextTimeByCronStringAndTimezone(chatConfig.scheduledTriggerConfig)
: null,
// 只有发布才会更新定时器
...(isPublish && {
scheduledTriggerConfig: chatConfig?.scheduledTriggerConfig,
scheduledTriggerNextTime: chatConfig?.scheduledTriggerConfig?.cronString
? getNextTimeByCronStringAndTimezone(chatConfig.scheduledTriggerConfig)
: null
}),
'pluginData.nodeVersion': _id
},
{

View File

@@ -1,16 +1,22 @@
import type { NextApiRequest, NextApiResponse } from 'next';
import { sseErrRes } from '@fastgpt/service/common/response';
import { SseResponseEventEnum } from '@fastgpt/global/core/workflow/runtime/constants';
import {
DispatchNodeResponseKeyEnum,
SseResponseEventEnum
} from '@fastgpt/global/core/workflow/runtime/constants';
import { responseWrite } from '@fastgpt/service/common/response';
import { pushChatUsage } from '@/service/support/wallet/usage/push';
import { UsageSourceEnum } from '@fastgpt/global/support/wallet/usage/constants';
import type { UserChatItemType } from '@fastgpt/global/core/chat/type';
import type { AIChatItemType, UserChatItemType } from '@fastgpt/global/core/chat/type';
import { authApp } from '@fastgpt/service/support/permission/app/auth';
import { dispatchWorkFlow } from '@fastgpt/service/core/workflow/dispatch';
import { authCert } from '@fastgpt/service/support/permission/auth/common';
import { getUserChatInfoAndAuthTeamPoints } from '@/service/support/permission/auth/team';
import { StoreEdgeItemType } from '@fastgpt/global/core/workflow/type/edge';
import { removeEmptyUserInput } from '@fastgpt/global/core/chat/utils';
import {
concatHistories,
getChatTitleFromChatMessage,
removeEmptyUserInput
} from '@fastgpt/global/core/chat/utils';
import { ReadPermissionVal } from '@fastgpt/global/support/permission/constant';
import { AppTypeEnum } from '@fastgpt/global/core/app/constants';
import {
@@ -18,27 +24,37 @@ import {
updatePluginInputByVariables
} from '@fastgpt/global/core/workflow/utils';
import { NextAPI } from '@/service/middleware/entry';
import { GPTMessages2Chats } from '@fastgpt/global/core/chat/adapt';
import { chatValue2RuntimePrompt, GPTMessages2Chats } from '@fastgpt/global/core/chat/adapt';
import { ChatCompletionMessageParam } from '@fastgpt/global/core/ai/type';
import { AppChatConfigType } from '@fastgpt/global/core/app/type';
import {
getLastInteractiveValue,
getMaxHistoryLimitFromNodes,
getWorkflowEntryNodeIds,
initWorkflowEdgeStatus,
rewriteNodeOutputByHistories,
storeNodes2RuntimeNodes
storeNodes2RuntimeNodes,
textAdaptGptResponse
} from '@fastgpt/global/core/workflow/runtime/utils';
import { StoreNodeItemType } from '@fastgpt/global/core/workflow/type/node';
import { getWorkflowResponseWrite } from '@fastgpt/service/core/workflow/dispatch/utils';
import { WORKFLOW_MAX_RUN_TIMES } from '@fastgpt/service/core/workflow/constants';
import { getPluginInputsFromStoreNodes } from '@fastgpt/global/core/app/plugin/utils';
import { getChatItems } from '@fastgpt/service/core/chat/controller';
import { MongoChat } from '@fastgpt/service/core/chat/chatSchema';
import { getSystemTime } from '@fastgpt/global/common/time/timezone';
import { ChatRoleEnum, ChatSourceEnum } from '@fastgpt/global/core/chat/constants';
import { saveChat, updateInteractiveChat } from '@fastgpt/service/core/chat/saveChat';
export type Props = {
messages: ChatCompletionMessageParam[];
responseChatItemId: string;
nodes: StoreNodeItemType[];
edges: StoreEdgeItemType[];
variables: Record<string, any>;
appId: string;
appName: string;
chatId: string;
chatConfig: AppChatConfigType;
};
@@ -55,10 +71,12 @@ async function handler(req: NextApiRequest, res: NextApiResponse) {
nodes = [],
edges = [],
messages = [],
responseChatItemId,
variables = {},
appName,
appId,
chatConfig
chatConfig,
chatId
} = req.body as Props;
try {
if (!Array.isArray(nodes)) {
@@ -71,15 +89,12 @@ async function handler(req: NextApiRequest, res: NextApiResponse) {
// console.log(JSON.stringify(chatMessages, null, 2), '====', chatMessages.length);
/* user auth */
const [{ app }, { teamId, tmbId }] = await Promise.all([
authApp({ req, authToken: true, appId, per: ReadPermissionVal }),
authCert({
req,
authToken: true
})
]);
// auth balance
const { user } = await getUserChatInfoAndAuthTeamPoints(tmbId);
const { app, teamId, tmbId } = await authApp({
req,
authToken: true,
appId,
per: ReadPermissionVal
});
const isPlugin = app.type === AppTypeEnum.plugin;
@@ -99,48 +114,79 @@ async function handler(req: NextApiRequest, res: NextApiResponse) {
return latestHumanChat;
})();
let runtimeNodes = storeNodes2RuntimeNodes(nodes, getWorkflowEntryNodeIds(nodes, chatMessages));
const limit = getMaxHistoryLimitFromNodes(nodes);
const [{ histories }, chatDetail, { user }] = await Promise.all([
getChatItems({
appId,
chatId,
offset: 0,
limit,
field: `dataId obj value nodeOutputs`
}),
MongoChat.findOne({ appId: app._id, chatId }, 'source variableList variables'),
// auth balance
getUserChatInfoAndAuthTeamPoints(tmbId)
]);
// Plugin need to replace inputs
if (chatDetail?.variables) {
variables = {
...chatDetail.variables,
...variables
};
}
const newHistories = concatHistories(histories, chatMessages);
// Get runtimeNodes
let runtimeNodes = storeNodes2RuntimeNodes(nodes, getWorkflowEntryNodeIds(nodes, newHistories));
if (isPlugin) {
runtimeNodes = updatePluginInputByVariables(runtimeNodes, variables);
variables = {};
} else {
if (!userQuestion.value) {
throw new Error('Params Error');
}
}
runtimeNodes = rewriteNodeOutputByHistories(newHistories, runtimeNodes);
runtimeNodes = rewriteNodeOutputByHistories(chatMessages, runtimeNodes);
const workflowResponseWrite = getWorkflowResponseWrite({
res,
detail: true,
streamResponse: true
streamResponse: true,
id: chatId,
showNodeStatus: true
});
/* start process */
const { flowResponses, flowUsages } = await dispatchWorkFlow({
const { flowResponses, assistantResponses, newVariables, flowUsages } = await dispatchWorkFlow({
res,
requestOrigin: req.headers.origin,
mode: 'test',
user,
uid: tmbId,
runningAppInfo: {
id: appId,
teamId,
tmbId
},
uid: tmbId,
user,
chatId,
responseChatItemId,
runtimeNodes,
runtimeEdges: initWorkflowEdgeStatus(edges, chatMessages),
runtimeEdges: initWorkflowEdgeStatus(edges, newHistories),
variables,
query: removeEmptyUserInput(userQuestion.value),
chatConfig,
histories: chatMessages,
histories: newHistories,
stream: true,
maxRunTimes: WORKFLOW_MAX_RUN_TIMES,
workflowStreamResponse: workflowResponseWrite
});
workflowResponseWrite({
event: SseResponseEventEnum.answer,
data: textAdaptGptResponse({
text: null,
finish_reason: 'stop'
})
});
responseWrite({
res,
event: SseResponseEventEnum.answer,
@@ -152,7 +198,44 @@ async function handler(req: NextApiRequest, res: NextApiResponse) {
data: JSON.stringify(flowResponses)
});
res.end();
// save chat
const isInteractiveRequest = !!getLastInteractiveValue(histories);
const { text: userInteractiveVal } = chatValue2RuntimePrompt(userQuestion.value);
const newTitle = isPlugin
? variables.cTime ?? getSystemTime(user.timezone)
: getChatTitleFromChatMessage(userQuestion);
const aiResponse: AIChatItemType & { dataId?: string } = {
dataId: responseChatItemId,
obj: ChatRoleEnum.AI,
value: assistantResponses,
[DispatchNodeResponseKeyEnum.nodeResponse]: flowResponses
};
if (isInteractiveRequest) {
await updateInteractiveChat({
chatId,
appId: app._id,
userInteractiveVal,
aiResponse,
newVariables
});
} else {
await saveChat({
chatId,
appId: app._id,
teamId,
tmbId: tmbId,
nodes,
appChatConfig: chatConfig,
variables: newVariables,
isUpdateUseTime: false, // owner update use time
newTitle,
source: ChatSourceEnum.test,
content: [userQuestion, aiResponse]
});
}
pushChatUsage({
appName,
@@ -165,8 +248,8 @@ async function handler(req: NextApiRequest, res: NextApiResponse) {
} catch (err: any) {
res.status(500);
sseErrRes(res, err);
res.end();
}
res.end();
}
export default NextAPI(handler);

View File

@@ -1,44 +1,48 @@
import type { NextApiRequest, NextApiResponse } from 'next';
import { jsonRes } from '@fastgpt/service/common/response';
import { authCert } from '@fastgpt/service/support/permission/auth/common';
import type { NextApiResponse } from 'next';
import { MongoChat } from '@fastgpt/service/core/chat/chatSchema';
import { MongoChatItem } from '@fastgpt/service/core/chat/chatItemSchema';
import { ClearHistoriesProps } from '@/global/core/chat/api';
import { authOutLink } from '@/service/support/permission/auth/outLink';
import { ChatSourceEnum } from '@fastgpt/global/core/chat/constants';
import { authTeamSpaceToken } from '@/service/support/permission/auth/team';
import { NextAPI } from '@/service/middleware/entry';
import { deleteChatFiles } from '@fastgpt/service/core/chat/controller';
import { mongoSessionRun } from '@fastgpt/service/common/mongo/sessionRun';
import { ApiRequestProps } from '@fastgpt/service/type/next';
import { authChatCrud } from '@/service/support/permission/auth/chat';
/* clear chat history */
async function handler(req: NextApiRequest, res: NextApiResponse) {
const { appId, shareId, outLinkUid, teamId, teamToken } = req.query as ClearHistoriesProps;
async function handler(req: ApiRequestProps<{}, ClearHistoriesProps>, res: NextApiResponse) {
const { appId, shareId, outLinkUid, teamId, teamToken } = req.query;
let chatAppId = appId!;
const {
teamId: chatTeamId,
tmbId,
uid,
authType
} = await authChatCrud({
req,
authToken: true,
authApiKey: true,
...req.query
});
const match = await (async () => {
if (shareId && outLinkUid) {
const { appId, uid } = await authOutLink({ shareId, outLinkUid });
chatAppId = appId;
if (shareId && outLinkUid && authType === 'outLink') {
return {
shareId,
outLinkUid: uid
};
}
if (teamId && teamToken) {
const { uid } = await authTeamSpaceToken({ teamId, teamToken });
return {
teamId,
teamId: chatTeamId,
appId,
outLinkUid: uid
};
}
if (appId) {
const { tmbId } = await authCert({ req, authToken: true, authApiKey: true });
if (teamId && teamToken && authType === 'teamDomain') {
return {
teamId: chatTeamId,
appId,
outLinkUid: uid
};
}
if (authType === 'token') {
return {
teamId: chatTeamId,
tmbId,
appId,
source: ChatSourceEnum.online
@@ -54,24 +58,22 @@ async function handler(req: NextApiRequest, res: NextApiResponse) {
await deleteChatFiles({ chatIdList: idList });
await mongoSessionRun(async (session) => {
return mongoSessionRun(async (session) => {
await MongoChatItem.deleteMany(
{
appId: chatAppId,
appId,
chatId: { $in: idList }
},
{ session }
);
await MongoChat.deleteMany(
{
appId: chatAppId,
appId,
chatId: { $in: idList }
},
{ session }
);
});
jsonRes(res);
}
export default NextAPI(handler);

View File

@@ -7,7 +7,6 @@ import { authChatCrud } from '@/service/support/permission/auth/chat';
import { mongoSessionRun } from '@fastgpt/service/common/mongo/sessionRun';
import { NextAPI } from '@/service/middleware/entry';
import { ApiRequestProps } from '@fastgpt/service/type/next';
import { WritePermissionVal } from '@fastgpt/global/support/permission/constant';
import { deleteChatFiles } from '@fastgpt/service/core/chat/controller';
/* clear chat history */
@@ -18,8 +17,7 @@ async function handler(req: ApiRequestProps<{}, DelHistoryProps>, res: NextApiRe
req,
authToken: true,
authApiKey: true,
...req.query,
per: WritePermissionVal
...req.query
});
await deleteChatFiles({ chatIdList: [chatId] });

View File

@@ -4,7 +4,6 @@ import { connectToDatabase } from '@/service/mongo';
import type { AdminUpdateFeedbackParams } from '@/global/core/chat/api.d';
import { MongoChatItem } from '@fastgpt/service/core/chat/chatItemSchema';
import { authChatCrud } from '@/service/support/permission/auth/chat';
import { ReadPermissionVal } from '@fastgpt/global/support/permission/constant';
/* 初始化我的聊天框,需要身份验证 */
export default async function handler(req: NextApiRequest, res: NextApiResponse) {
@@ -20,9 +19,9 @@ export default async function handler(req: NextApiRequest, res: NextApiResponse)
await authChatCrud({
req,
authToken: true,
authApiKey: true,
appId,
chatId,
per: ReadPermissionVal
chatId
});
await MongoChatItem.findOneAndUpdate(

View File

@@ -5,7 +5,6 @@ import { authCert } from '@fastgpt/service/support/permission/auth/common';
import type { CloseCustomFeedbackParams } from '@/global/core/chat/api.d';
import { MongoChatItem } from '@fastgpt/service/core/chat/chatItemSchema';
import { authChatCrud } from '@/service/support/permission/auth/chat';
import { ReadPermissionVal } from '@fastgpt/global/support/permission/constant';
import { mongoSessionRun } from '@fastgpt/service/common/mongo/sessionRun';
/* remove custom feedback */
@@ -21,9 +20,9 @@ export default async function handler(req: NextApiRequest, res: NextApiResponse)
await authChatCrud({
req,
authToken: true,
authApiKey: true,
appId,
chatId,
per: ReadPermissionVal
chatId
});
await authCert({ req, authToken: true });

View File

@@ -1,71 +1,42 @@
import type { NextApiRequest, NextApiResponse } from 'next';
import { jsonRes } from '@fastgpt/service/common/response';
import { connectToDatabase } from '@/service/mongo';
import type { NextApiResponse } from 'next';
import { MongoChatItem } from '@fastgpt/service/core/chat/chatItemSchema';
import { UpdateChatFeedbackProps } from '@fastgpt/global/core/chat/api';
import { authChatCrud } from '@/service/support/permission/auth/chat';
import { ReadPermissionVal } from '@fastgpt/global/support/permission/constant';
import { NextAPI } from '@/service/middleware/entry';
import { ApiRequestProps } from '@fastgpt/service/type/next';
/* 初始化我的聊天框,需要身份验证 */
async function handler(req: NextApiRequest, res: NextApiResponse) {
const {
appId,
chatId,
dataId,
shareId,
teamId,
teamToken,
outLinkUid,
userBadFeedback,
userGoodFeedback
} = req.body as UpdateChatFeedbackProps;
async function handler(req: ApiRequestProps<UpdateChatFeedbackProps>, res: NextApiResponse) {
const { appId, chatId, dataId, userBadFeedback, userGoodFeedback } = req.body;
try {
await connectToDatabase();
await authChatCrud({
req,
authToken: true,
authApiKey: true,
appId,
teamId,
teamToken,
chatId,
shareId,
outLinkUid,
per: ReadPermissionVal
});
if (!dataId) {
throw new Error('dataId is required');
}
await MongoChatItem.findOneAndUpdate(
{
appId,
chatId,
dataId
},
{
$unset: {
...(userBadFeedback === undefined && { userBadFeedback: '' }),
...(userGoodFeedback === undefined && { userGoodFeedback: '' })
},
$set: {
...(userBadFeedback !== undefined && { userBadFeedback }),
...(userGoodFeedback !== undefined && { userGoodFeedback })
}
}
);
jsonRes(res);
} catch (err) {
jsonRes(res, {
code: 500,
error: err
});
if (!chatId || !dataId) {
return Promise.reject('chatId or dataId is empty');
}
await authChatCrud({
req,
authToken: true,
authApiKey: true,
...req.body
});
await MongoChatItem.findOneAndUpdate(
{
appId,
chatId,
dataId
},
{
$unset: {
...(userBadFeedback === undefined && { userBadFeedback: '' }),
...(userGoodFeedback === undefined && { userGoodFeedback: '' })
},
$set: {
...(userBadFeedback !== undefined && { userBadFeedback }),
...(userGoodFeedback !== undefined && { userGoodFeedback })
}
}
);
}
export default NextAPI(handler);

View File

@@ -7,6 +7,8 @@ import { NextAPI } from '@/service/middleware/entry';
import { ApiRequestProps, ApiResponseType } from '@fastgpt/service/type/next';
import { PaginationProps, PaginationResponse } from '@fastgpt/web/common/fetch/type';
import { GetHistoriesProps } from '@/global/core/chat/api';
import { addMonths } from 'date-fns';
export type getHistoriesQuery = {};
export type getHistoriesBody = PaginationProps<GetHistoriesProps>;
@@ -17,8 +19,7 @@ async function handler(
req: ApiRequestProps<getHistoriesBody, getHistoriesQuery>,
res: ApiResponseType<any>
): Promise<PaginationResponse<getHistoriesResponse>> {
const { appId, shareId, outLinkUid, teamId, teamToken, offset, pageSize, source } =
req.body as getHistoriesBody;
const { appId, shareId, outLinkUid, teamId, teamToken, offset, pageSize, source } = req.body;
const match = await (async () => {
if (shareId && outLinkUid) {
@@ -28,7 +29,7 @@ async function handler(
shareId,
outLinkUid: uid,
updateTime: {
$gte: new Date(new Date().setDate(new Date().getDate() - 30))
$gte: addMonths(new Date(), -1)
}
};
}
@@ -62,7 +63,8 @@ async function handler(
await MongoChat.find(match, 'chatId title top customTitle appId updateTime')
.sort({ top: -1, updateTime: -1 })
.skip(offset)
.limit(pageSize),
.limit(pageSize)
.lean(),
MongoChat.countDocuments(match)
]);

View File

@@ -1,7 +1,6 @@
import type { ApiRequestProps, ApiResponseType } from '@fastgpt/service/type/next';
import { NextAPI } from '@/service/middleware/entry';
import { GetChatRecordsProps } from '@/global/core/chat/api';
import { ReadPermissionVal } from '@fastgpt/global/support/permission/constant';
import { DispatchNodeResponseKeyEnum } from '@fastgpt/global/core/workflow/runtime/constants';
import { transformPreviewHistories } from '@/global/core/chat/utils';
import { AppTypeEnum } from '@fastgpt/global/core/app/constants';
@@ -11,7 +10,6 @@ import { MongoApp } from '@fastgpt/service/core/app/schema';
import { AppErrEnum } from '@fastgpt/global/common/error/code/app';
import { ChatItemValueTypeEnum, ChatRoleEnum } from '@fastgpt/global/core/chat/constants';
import { filterPublicNodeResponseData } from '@fastgpt/global/core/chat/utils';
import { authOutLink } from '@/service/support/permission/auth/outLink';
import { GetChatTypeEnum } from '@/global/core/chat/constants';
import { PaginationProps, PaginationResponse } from '@fastgpt/web/common/fetch/type';
import { ChatItemType } from '@fastgpt/global/core/chat/type';
@@ -42,14 +40,13 @@ async function handler(
};
}
const [app] = await Promise.all([
const [app, { responseDetail, showNodeStatus, authType }] = await Promise.all([
MongoApp.findById(appId, 'type').lean(),
authChatCrud({
req,
authToken: true,
authApiKey: true,
...req.body,
per: ReadPermissionVal
...req.body
})
]);
@@ -57,21 +54,14 @@ async function handler(
return Promise.reject(AppErrEnum.unExist);
}
const isPlugin = app.type === AppTypeEnum.plugin;
const shareChat = await (async () => {
if (type === GetChatTypeEnum.outLink)
return await authOutLink({
shareId: req.body.shareId,
outLinkUid: req.body.outLinkUid
}).then((result) => result.shareChat);
})();
const isOutLink = authType === GetChatTypeEnum.outLink;
const fieldMap = {
[GetChatTypeEnum.normal]: `dataId obj value adminFeedback userBadFeedback userGoodFeedback time ${
[GetChatTypeEnum.normal]: `dataId obj value adminFeedback userBadFeedback userGoodFeedback time hideInUI ${
DispatchNodeResponseKeyEnum.nodeResponse
} ${loadCustomFeedbacks ? 'customFeedbacks' : ''}`,
[GetChatTypeEnum.outLink]: `dataId obj value userGoodFeedback userBadFeedback adminFeedback time ${DispatchNodeResponseKeyEnum.nodeResponse}`,
[GetChatTypeEnum.team]: `dataId obj value userGoodFeedback userBadFeedback adminFeedback time ${DispatchNodeResponseKeyEnum.nodeResponse}`
[GetChatTypeEnum.outLink]: `dataId obj value userGoodFeedback userBadFeedback adminFeedback time hideInUI ${DispatchNodeResponseKeyEnum.nodeResponse}`,
[GetChatTypeEnum.team]: `dataId obj value userGoodFeedback userBadFeedback adminFeedback time hideInUI ${DispatchNodeResponseKeyEnum.nodeResponse}`
};
const { total, histories } = await getChatItems({
@@ -82,10 +72,8 @@ async function handler(
limit: pageSize
});
const responseDetail = !shareChat || shareChat.responseDetail;
// Remove important information
if (shareChat && app.type !== AppTypeEnum.plugin) {
if (isOutLink && app.type !== AppTypeEnum.plugin) {
histories.forEach((item) => {
if (item.obj === ChatRoleEnum.AI) {
item.responseData = filterPublicNodeResponseData({
@@ -93,7 +81,7 @@ async function handler(
responseDetail
});
if (shareChat.showNodeStatus === false) {
if (showNodeStatus === false) {
item.value = item.value.filter((v) => v.type !== ChatItemValueTypeEnum.tool);
}
}

View File

@@ -1,17 +1,11 @@
import { authChatCrud } from '@/service/support/permission/auth/chat';
import {
ManagePermissionVal,
ReadPermissionVal
} from '@fastgpt/global/support/permission/constant';
import { MongoChatItem } from '@fastgpt/service/core/chat/chatItemSchema';
import { ChatRoleEnum } from '@fastgpt/global/core/chat/constants';
import type { ApiRequestProps, ApiResponseType } from '@fastgpt/service/type/next';
import { NextAPI } from '@/service/middleware/entry';
import { ChatHistoryItemResType } from '@fastgpt/global/core/chat/type';
import { OutLinkChatAuthProps } from '@fastgpt/global/support/permission/chat';
import { authApp } from '@fastgpt/service/support/permission/app/auth';
import { filterPublicNodeResponseData } from '@fastgpt/global/core/chat/utils';
import { MongoOutLink } from '@fastgpt/service/support/outLink/schema';
export type getResDataQuery = OutLinkChatAuthProps & {
chatId?: string;
@@ -32,29 +26,13 @@ async function handler(
return {};
}
// 1. Un login api: share chat, team chat
// 2. Login api: account chat, chat log
const authData = await (() => {
try {
return authChatCrud({
req,
authToken: true,
authApiKey: true,
...req.query,
per: ReadPermissionVal
});
} catch (error) {
return authApp({
req,
authToken: true,
authApiKey: true,
appId,
per: ManagePermissionVal
});
}
})();
const [chatData] = await Promise.all([
const [{ responseDetail }, chatData] = await Promise.all([
authChatCrud({
req,
authToken: true,
authApiKey: true,
...req.query
}),
MongoChatItem.findOne(
{
appId,
@@ -62,8 +40,7 @@ async function handler(
dataId
},
'obj responseData'
).lean(),
shareId ? MongoOutLink.findOne({ shareId }).lean() : Promise.resolve(null)
).lean()
]);
if (chatData?.obj !== ChatRoleEnum.AI) {
@@ -73,8 +50,7 @@ async function handler(
const flowResponses = chatData.responseData ?? {};
return req.query.shareId
? filterPublicNodeResponseData({
// @ts-ignore
responseDetail: authData.responseDetail,
responseDetail,
flowResponses: chatData.responseData
})
: flowResponses;

View File

@@ -3,7 +3,7 @@ import { MongoChatInputGuide } from '@fastgpt/service/core/chat/inputGuide/schem
import { NextAPI } from '@/service/middleware/entry';
import { ApiRequestProps } from '@fastgpt/service/type/next';
import { OutLinkChatAuthProps } from '@fastgpt/global/support/permission/chat';
import { authChatCert } from '@/service/support/permission/auth/chat';
import { authChatCrud } from '@/service/support/permission/auth/chat';
import { MongoApp } from '@fastgpt/service/core/app/schema';
import { AppErrEnum } from '@fastgpt/global/common/error/code/app';
import { replaceRegChars } from '@fastgpt/global/common/string/tools';
@@ -21,7 +21,7 @@ async function handler(
const { appId, searchKey } = req.body;
// tmp auth
const { teamId } = await authChatCert({ req, authToken: true });
const { teamId } = await authChatCrud({ req, authToken: true, ...req.body });
const app = await MongoApp.findOne({ _id: appId, teamId });
if (!app) {
return Promise.reject(AppErrEnum.unAuthApp);

View File

@@ -5,21 +5,19 @@ import { authChatCrud } from '@/service/support/permission/auth/chat';
import type { DeleteChatItemProps } from '@/global/core/chat/api.d';
import { NextAPI } from '@/service/middleware/entry';
import { ApiRequestProps } from '@fastgpt/service/type/next';
import { WritePermissionVal } from '@fastgpt/global/support/permission/constant';
async function handler(req: ApiRequestProps<{}, DeleteChatItemProps>, res: NextApiResponse) {
const { appId, chatId, contentId } = req.query;
if (!contentId || !chatId) {
return jsonRes(res);
return Promise.reject('contentId or chatId is empty');
}
await authChatCrud({
req,
authToken: true,
authApiKey: true,
...req.query,
per: WritePermissionVal
...req.query
});
await MongoChatItem.deleteOne({
@@ -28,7 +26,7 @@ async function handler(req: ApiRequestProps<{}, DeleteChatItemProps>, res: NextA
dataId: contentId
});
jsonRes(res);
return;
}
export default NextAPI(handler);

View File

@@ -1,33 +1,35 @@
import type { NextApiRequest, NextApiResponse } from 'next';
import type { NextApiResponse } from 'next';
import { jsonRes } from '@fastgpt/service/common/response';
import { connectToDatabase } from '@/service/mongo';
import { GetChatSpeechProps } from '@/global/core/chat/api.d';
import { text2Speech } from '@fastgpt/service/core/ai/audio/speech';
import { pushAudioSpeechUsage } from '@/service/support/wallet/usage/push';
import { authChatCert } from '@/service/support/permission/auth/chat';
import { authChatCrud } from '@/service/support/permission/auth/chat';
import { authType2UsageSource } from '@/service/support/wallet/usage/utils';
import { getAudioSpeechModel } from '@fastgpt/service/core/ai/model';
import { MongoTTSBuffer } from '@fastgpt/service/common/buffer/tts/schema';
import { NextAPI } from '@/service/middleware/entry';
import { ApiRequestProps } from '@fastgpt/service/type/next';
/*
1. get tts from chatItem store
2. get tts from ai
4. push bill
*/
export default async function handler(req: NextApiRequest, res: NextApiResponse) {
async function handler(req: ApiRequestProps<GetChatSpeechProps>, res: NextApiResponse) {
try {
await connectToDatabase();
const { ttsConfig, input } = req.body as GetChatSpeechProps;
const { ttsConfig, input } = req.body;
if (!ttsConfig.model || !ttsConfig.voice) {
throw new Error('model or voice not found');
}
const { teamId, tmbId, authType } = await authChatCert({
const { teamId, tmbId, authType } = await authChatCrud({
req,
authToken: true,
authApiKey: true
authApiKey: true,
...req.body
});
const ttsModel = getAudioSpeechModel(ttsConfig.model);
@@ -90,3 +92,5 @@ export default async function handler(req: NextApiRequest, res: NextApiResponse)
});
}
}
export default NextAPI(handler);

View File

@@ -16,11 +16,11 @@ async function handler(req: NextApiRequest, res: NextApiResponse) {
let { chatId, shareId, outLinkUid } = req.query as InitOutLinkChatProps;
// auth link permission
const { shareChat, uid, appId } = await authOutLink({ shareId, outLinkUid });
const { outLinkConfig, uid, appId } = await authOutLink({ shareId, outLinkUid });
// auth app permission
const [tmb, chat, app] = await Promise.all([
MongoTeamMember.findById(shareChat.tmbId, '_id userId').populate('userId', 'avatar').lean(),
MongoTeamMember.findById(outLinkConfig.tmbId, '_id userId').populate('userId', 'avatar').lean(),
MongoChat.findOne({ appId, chatId, shareId }).lean(),
MongoApp.findById(appId).lean()
]);
@@ -35,7 +35,6 @@ async function handler(req: NextApiRequest, res: NextApiResponse) {
}
const { nodes, chatConfig } = await getAppLatestVersion(app._id, app);
// pick share response field
jsonRes<InitChatResponse>(res, {
data: {
@@ -66,9 +65,3 @@ async function handler(req: NextApiRequest, res: NextApiResponse) {
}
export default NextAPI(handler);
export const config = {
api: {
responseLimit: '10mb'
}
};

View File

@@ -1,4 +1,4 @@
import type { NextApiRequest, NextApiResponse } from 'next';
import type { NextApiResponse } from 'next';
import { jsonRes } from '@fastgpt/service/common/response';
import { getGuideModule, getAppChatConfig } from '@fastgpt/global/core/workflow/utils';
import { getChatModelNameListByModules } from '@/service/core/app/workflow';
@@ -12,12 +12,13 @@ import { ChatErrEnum } from '@fastgpt/global/common/error/code/chat';
import { getAppLatestVersion } from '@fastgpt/service/core/app/version/controller';
import { FlowNodeTypeEnum } from '@fastgpt/global/core/workflow/node/constant';
import { NextAPI } from '@/service/middleware/entry';
import { ApiRequestProps } from '@fastgpt/service/type/next';
async function handler(req: NextApiRequest, res: NextApiResponse) {
let { teamId, appId, chatId, teamToken } = req.query as InitTeamChatProps;
async function handler(req: ApiRequestProps<InitTeamChatProps>, res: NextApiResponse) {
let { teamId, appId, chatId, teamToken } = req.query;
if (!teamId || !appId || !teamToken) {
throw new Error('teamId, appId, teamToken are required');
return Promise.reject('teamId, appId, teamToken are required');
}
const { uid } = await authTeamSpaceToken({
@@ -32,7 +33,7 @@ async function handler(req: NextApiRequest, res: NextApiResponse) {
]);
if (!app) {
throw new Error(AppErrEnum.unExist);
return Promise.reject(AppErrEnum.unExist);
}
// auth chat permission
@@ -43,8 +44,6 @@ async function handler(req: NextApiRequest, res: NextApiResponse) {
// get app and history
const { nodes, chatConfig } = await getAppLatestVersion(app._id, app);
// pick share response field
jsonRes<InitChatResponse>(res, {
data: {
chatId,
@@ -74,9 +73,3 @@ async function handler(req: NextApiRequest, res: NextApiResponse) {
}
export default NextAPI(handler);
export const config = {
api: {
responseLimit: '10mb'
}
};

View File

@@ -77,7 +77,7 @@ async function handler(req: NextApiRequest, res: NextApiResponse<any>): CreateCo
// 2. upload file
const fileId = await uploadFile({
teamId,
tmbId,
uid: tmbId,
bucketName,
path: file.path,
filename: file.originalname,

View File

@@ -5,40 +5,142 @@ import { DatasetCollectionTypeEnum } from '@fastgpt/global/core/dataset/constant
import { createFileToken } from '@fastgpt/service/support/permission/controller';
import { BucketNameEnum, ReadFileBaseUrl } from '@fastgpt/global/common/file/constants';
import { ReadPermissionVal } from '@fastgpt/global/support/permission/constant';
import { ShareChatAuthProps } from '@fastgpt/global/support/permission/chat';
import { OutLinkChatAuthProps } from '@fastgpt/global/support/permission/chat';
import { DatasetErrEnum } from '@fastgpt/global/common/error/code/dataset';
import { MongoChatItem } from '@fastgpt/service/core/chat/chatItemSchema';
import { AIChatItemType, ChatHistoryItemResType } from '@fastgpt/global/core/chat/type';
import { authChatCrud } from '@/service/support/permission/auth/chat';
import { getCollectionWithDataset } from '@fastgpt/service/core/dataset/controller';
export type readCollectionSourceQuery = {};
export type readCollectionSourceBody = {
collectionId: string;
} & ShareChatAuthProps;
appId?: string;
chatId?: string;
chatItemId?: string;
} & OutLinkChatAuthProps;
export type readCollectionSourceResponse = {
type: 'url';
value: string;
};
const authCollectionInChat = async ({
collectionId,
appId,
chatId,
chatItemId
}: {
collectionId: string;
appId: string;
chatId: string;
chatItemId: string;
}) => {
try {
const chatItem = (await MongoChatItem.findOne(
{
appId,
chatId,
dataId: chatItemId
},
'responseData'
).lean()) as AIChatItemType;
if (!chatItem) return Promise.reject(DatasetErrEnum.unAuthDatasetFile);
// 找 responseData 里,是否有该文档 id
const responseData = chatItem.responseData || [];
const flatResData: ChatHistoryItemResType[] =
responseData
?.map((item) => {
return [
item,
...(item.pluginDetail || []),
...(item.toolDetail || []),
...(item.loopDetail || [])
];
})
.flat() || [];
if (
flatResData.some((item) => {
if (item.quoteList) {
return item.quoteList.some((quote) => quote.collectionId === collectionId);
}
return false;
})
) {
return true;
}
} catch (error) {}
return Promise.reject(DatasetErrEnum.unAuthDatasetFile);
};
async function handler(
req: ApiRequestProps<readCollectionSourceBody, readCollectionSourceQuery>
): Promise<readCollectionSourceResponse> {
const { collection, teamId, tmbId } = await authDatasetCollection({
req,
authToken: true,
authApiKey: true,
collectionId: req.body.collectionId,
per: ReadPermissionVal
});
const { collectionId, appId, chatId, chatItemId, shareId, outLinkUid, teamId, teamToken } =
req.body;
const {
collection,
teamId: userTeamId,
tmbId: uid,
authType
} = await (async () => {
if (!appId || !chatId || !chatItemId) {
return authDatasetCollection({
req,
authToken: true,
authApiKey: true,
collectionId: req.body.collectionId,
per: ReadPermissionVal
});
}
/*
1. auth chat read permission
2. auth collection quote in chat
3. auth outlink open show quote
*/
const [authRes, collection] = await Promise.all([
authChatCrud({
req,
authToken: true,
appId,
chatId,
shareId,
outLinkUid,
teamId,
teamToken
}),
getCollectionWithDataset(collectionId),
authCollectionInChat({ appId, chatId, chatItemId, collectionId })
]);
if (!authRes.showRawSource) {
return Promise.reject(DatasetErrEnum.unAuthDatasetFile);
}
return {
...authRes,
collection
};
})();
const sourceUrl = await (async () => {
if (collection.type === DatasetCollectionTypeEnum.file && collection.fileId) {
const token = await createFileToken({
bucketName: BucketNameEnum.dataset,
teamId,
tmbId,
fileId: collection.fileId
teamId: userTeamId,
uid,
fileId: collection.fileId,
customExpireMinutes: authType === 'outLink' ? 5 : undefined
});
return `${ReadFileBaseUrl}?token=${token}`;
return `${ReadFileBaseUrl}/${collection.name}?token=${token}`;
}
if (collection.type === DatasetCollectionTypeEnum.link && collection.rawLink) {
return collection.rawLink;

View File

@@ -4,7 +4,7 @@ import { authCert } from '@fastgpt/service/support/permission/auth/common';
import { NextAPI } from '@/service/middleware/entry';
import { ApiRequestProps } from '@fastgpt/service/type/next';
import { OwnerPermissionVal } from '@fastgpt/global/support/permission/constant';
import { authFile } from '@fastgpt/service/support/permission/auth/file';
import { authCollectionFile } from '@fastgpt/service/support/permission/auth/file';
export type PostPreviewFilesChunksProps = {
type: DatasetSourceReadTypeEnum;
@@ -35,7 +35,7 @@ async function handler(
const { teamId } = await (async () => {
if (type === DatasetSourceReadTypeEnum.fileLocal) {
return authFile({
return authCollectionFile({
req,
authToken: true,
authApiKey: true,

View File

@@ -1,7 +1,7 @@
import { MongoOutLink } from '@fastgpt/service/support/outLink/schema';
import type { OutLinkEditType } from '@fastgpt/global/support/outLink/type.d';
import { authOutLinkCrud } from '@fastgpt/service/support/permission/publish/authLink';
import { OwnerPermissionVal } from '@fastgpt/global/support/permission/constant';
import { ManagePermissionVal } from '@fastgpt/global/support/permission/constant';
import type { ApiRequestProps } from '@fastgpt/service/type/next';
import { NextAPI } from '@/service/middleware/entry';
import { CommonErrEnum } from '@fastgpt/global/common/error/code/common';
@@ -30,7 +30,7 @@ async function handler(
return Promise.reject(CommonErrEnum.missingParams);
}
await authOutLinkCrud({ req, outLinkId: _id, authToken: true, per: OwnerPermissionVal });
await authOutLinkCrud({ req, outLinkId: _id, authToken: true, per: ManagePermissionVal });
await MongoOutLink.findByIdAndUpdate(_id, {
name,

View File

@@ -4,7 +4,7 @@ import { getUploadModel } from '@fastgpt/service/common/file/multer';
import { removeFilesByPaths } from '@fastgpt/service/common/file/utils';
import fs from 'fs';
import { pushWhisperUsage } from '@/service/support/wallet/usage/push';
import { authChatCert } from '@/service/support/permission/auth/chat';
import { authChatCrud } from '@/service/support/permission/auth/chat';
import { OutLinkChatAuthProps } from '@fastgpt/global/support/permission/chat';
import { NextAPI } from '@/service/middleware/entry';
import { aiTranscriptions } from '@fastgpt/service/core/ai/audio/transcriptions';
@@ -27,6 +27,7 @@ async function handler(req: NextApiRequest, res: NextApiResponse<any>) {
}
>(req, res);
req.body.appId = appId;
req.body.shareId = shareId;
req.body.outLinkUid = outLinkUid;
req.body.teamId = spaceTeamId;
@@ -43,7 +44,7 @@ async function handler(req: NextApiRequest, res: NextApiResponse<any>) {
}
// auth role
const { teamId, tmbId } = await authChatCert({ req, authToken: true });
const { teamId, tmbId } = await authChatCrud({ req, authToken: true, ...req.body });
// auth app
// const app = await MongoApp.findById(appId, 'modules').lean();

View File

@@ -86,7 +86,7 @@ type AuthResponseType = {
showNodeStatus?: boolean;
authType: `${AuthUserTypeEnum}`;
apikey?: string;
canWrite: boolean;
responseAllData: boolean;
outLinkUserId?: string;
sourceName?: string;
};
@@ -160,7 +160,7 @@ async function handler(req: NextApiRequest, res: NextApiResponse) {
authType,
sourceName,
apikey,
canWrite,
responseAllData,
outLinkUserId = customUid,
showNodeStatus
} = await (async () => {
@@ -328,12 +328,9 @@ async function handler(req: NextApiRequest, res: NextApiResponse) {
await updateInteractiveChat({
chatId,
appId: app._id,
teamId,
tmbId: tmbId,
userInteractiveVal,
aiResponse,
newVariables,
newTitle
newVariables
});
} else {
await saveChat({
@@ -361,7 +358,7 @@ async function handler(req: NextApiRequest, res: NextApiResponse) {
addLog.info(`completions running time: ${(Date.now() - startTime) / 1000}s`);
/* select fe response field */
const feResponseData = canWrite
const feResponseData = responseAllData
? flowResponses
: filterPublicNodeResponseData({ flowResponses, responseDetail });
@@ -482,10 +479,10 @@ const authShareChat = async ({
tmbId,
user,
app,
responseDetail,
apikey: '',
authType,
canWrite: false,
responseAllData: false,
responseDetail,
outLinkUserId: uid,
showNodeStatus
};
@@ -525,10 +522,10 @@ const authTeamSpaceChat = async ({
tmbId: app.tmbId,
user,
app,
responseDetail: true,
authType: AuthUserTypeEnum.outLink,
apikey: '',
canWrite: false,
responseAllData: false,
responseDetail: true,
outLinkUserId: uid
};
};
@@ -610,11 +607,11 @@ const authHeaderRequest = async ({
tmbId,
user,
app,
responseDetail: true,
apikey,
authType,
sourceName,
canWrite: true
responseAllData: true,
responseDetail: true
};
};

View File

@@ -1,12 +1,10 @@
import React, { useMemo } from 'react';
import { Flex, Box, useTheme } from '@chakra-ui/react';
import { Flex, Box } from '@chakra-ui/react';
import { useTranslation } from 'next-i18next';
import { HUMAN_ICON } from '@fastgpt/global/common/system/constants';
import { getInitChatInfo } from '@/web/core/chat/api';
import MyBox from '@fastgpt/web/components/common/MyBox';
import { getNanoid } from '@fastgpt/global/common/string/tools';
import { AppTypeEnum } from '@fastgpt/global/core/app/constants';
import { useChat } from '@/components/core/chat/ChatContainer/useChat';
import dynamic from 'next/dynamic';
import LightRowTabs from '@fastgpt/web/components/common/Tabs/LightRowTabs';
@@ -14,53 +12,52 @@ import { PluginRunBoxTabEnum } from '@/components/core/chat/ChatContainer/Plugin
import CloseIcon from '@fastgpt/web/components/common/Icon/close';
import ChatBox from '@/components/core/chat/ChatContainer/ChatBox';
import { useSystem } from '@fastgpt/web/hooks/useSystem';
import { useQuery } from '@tanstack/react-query';
import { PcHeader } from '@/pages/chat/components/ChatHeader';
import { GetChatTypeEnum } from '@/global/core/chat/constants';
import ChatItemContextProvider, { ChatItemContext } from '@/web/core/chat/context/chatItemContext';
import ChatRecordContextProvider, {
ChatRecordContext
} from '@/web/core/chat/context/chatRecordContext';
import { useRequest2 } from '@fastgpt/web/hooks/useRequest';
import { useContextSelector } from 'use-context-selector';
const PluginRunBox = dynamic(() => import('@/components/core/chat/ChatContainer/PluginRunBox'));
const DetailLogsModal = ({
appId,
chatId,
onClose
}: {
type Props = {
appId: string;
chatId: string;
onClose: () => void;
}) => {
};
const DetailLogsModal = ({ appId, chatId, onClose }: Props) => {
const { t } = useTranslation();
const { isPc } = useSystem();
const theme = useTheme();
const params = useMemo(() => {
return {
chatId,
appId,
loadCustomFeedbacks: true,
type: GetChatTypeEnum.normal
};
}, [appId, chatId]);
const {
ChatBoxRef,
variablesForm,
pluginRunTab,
setPluginRunTab,
resetVariables,
chatRecords,
ScrollData,
setChatRecords,
totalRecordsCount
} = useChat(params);
const resetVariables = useContextSelector(ChatItemContext, (v) => v.resetVariables);
const setChatBoxData = useContextSelector(ChatItemContext, (v) => v.setChatBoxData);
const pluginRunTab = useContextSelector(ChatItemContext, (v) => v.pluginRunTab);
const setPluginRunTab = useContextSelector(ChatItemContext, (v) => v.setPluginRunTab);
const { data: chat, isFetching } = useQuery(
['getChatDetail', chatId],
() => getInitChatInfo({ appId, chatId, loadCustomFeedbacks: true }),
const chatRecords = useContextSelector(ChatRecordContext, (v) => v.chatRecords);
const totalRecordsCount = useContextSelector(ChatRecordContext, (v) => v.totalRecordsCount);
const { data: chat, loading: isFetching } = useRequest2(
async () => {
const res = await getInitChatInfo({ appId, chatId, loadCustomFeedbacks: true });
res.userAvatar = HUMAN_ICON;
setChatBoxData(res);
resetVariables({
variables: res.variables
});
return res;
},
{
onSuccess(res) {
resetVariables({
variables: res.variables
});
manual: false,
refreshDeps: [chatId],
onError(e) {
onClose();
}
}
);
@@ -68,12 +65,11 @@ const DetailLogsModal = ({
const title = chat?.title;
const chatModels = chat?.app?.chatModels;
const isPlugin = chat?.app.type === AppTypeEnum.plugin;
const loading = isFetching;
return (
<>
<MyBox
isLoading={loading}
isLoading={isFetching}
display={'flex'}
flexDirection={'column'}
zIndex={3}
@@ -124,7 +120,7 @@ const DetailLogsModal = ({
alignItems={'center'}
px={[3, 5]}
h={['46px', '60px']}
borderBottom={theme.borders.base}
borderBottom={'base'}
borderBottomColor={'gray.200'}
color={'myGray.900'}
>
@@ -154,32 +150,15 @@ const DetailLogsModal = ({
<Box pt={2} flex={'1 0 0'}>
{isPlugin ? (
<Box px={5} pt={2} h={'100%'}>
<PluginRunBox
chatConfig={chat?.app?.chatConfig}
pluginInputs={chat?.app.pluginInputs}
variablesForm={variablesForm}
histories={chatRecords}
setHistories={setChatRecords}
appId={chat.appId}
tab={pluginRunTab}
setTab={setPluginRunTab}
/>
<PluginRunBox appId={appId} chatId={chatId} />
</Box>
) : (
<ChatBox
ScrollData={ScrollData}
ref={ChatBoxRef}
chatHistories={chatRecords}
setChatHistories={setChatRecords}
variablesForm={variablesForm}
appAvatar={chat?.app.avatar}
userAvatar={HUMAN_ICON}
appId={appId}
chatId={chatId}
feedbackType={'admin'}
showMarkIcon
showVoiceIcon={false}
chatConfig={chat?.app?.chatConfig}
appId={appId}
chatId={chatId}
chatType="log"
showRawSource
showNodeStatus
@@ -191,4 +170,24 @@ const DetailLogsModal = ({
</>
);
};
export default DetailLogsModal;
const Render = (props: Props) => {
const { appId, chatId } = props;
const params = useMemo(() => {
return {
chatId,
appId,
loadCustomFeedbacks: true,
type: GetChatTypeEnum.normal
};
}, [appId, chatId]);
return (
<ChatItemContextProvider>
<ChatRecordContextProvider params={params}>
<DetailLogsModal {...props} />
</ChatRecordContextProvider>
</ChatItemContextProvider>
);
};
export default Render;

View File

@@ -11,6 +11,7 @@ import { useSelectFile } from '@/web/common/file/hooks/useSelectFile';
import { fileToBase64 } from '@/web/common/file/utils';
import { useSystemStore } from '@/web/common/system/useSystemStore';
import MyImage from '@fastgpt/web/components/common/Image/MyImage';
import { subRoute } from '@fastgpt/web/common/system/utils';
enum UsingWayEnum {
link = 'link',
@@ -74,7 +75,7 @@ const SelectUsingWayModal = ({ share, onClose }: { share: OutLinkSchema; onClose
});
const baseUrl = feConfigs?.customSharePageDomain || location?.origin;
const linkUrl = `${baseUrl}/chat/share?shareId=${share?.shareId}${
const linkUrl = `${baseUrl}${subRoute ? `${subRoute}/` : '/'}chat/share?shareId=${share?.shareId}${
getValues('showHistory') ? '' : '&showHistory=0'
}`;

View File

@@ -423,7 +423,7 @@ function EditLinkModal({
</Flex>
<Switch {...register('responseDetail')} isChecked={responseDetail} />
</Flex>
{/* <Flex alignItems={'center'} mt={4} justify={'space-between'} height={'36px'}>
<Flex alignItems={'center'} mt={4} justify={'space-between'} height={'36px'}>
<Flex alignItems={'center'}>
<FormLabel>{t('common:support.outlink.share.show_complete_quote')}</FormLabel>
<QuestionTip
@@ -431,8 +431,17 @@ function EditLinkModal({
label={t('common:support.outlink.share.show_complete_quote_tips' || '')}
></QuestionTip>
</Flex>
<Switch {...register('showRawSource')} isChecked={showRawSource} />
</Flex> */}
<Switch
{...register('showRawSource', {
onChange(e) {
if (e.target.checked) {
setValue('responseDetail', true);
}
}
})}
isChecked={showRawSource}
/>
</Flex>
</Box>
</ModalBody>

View File

@@ -10,7 +10,7 @@ import {
ModalFooter
} from '@chakra-ui/react';
import { useRouter } from 'next/router';
import { AppSchema } from '@fastgpt/global/core/app/type.d';
import { AppSchema, AppSimpleEditFormType } from '@fastgpt/global/core/app/type.d';
import { useTranslation } from 'next-i18next';
import Avatar from '@fastgpt/web/components/common/Avatar';
import MyIcon from '@fastgpt/web/components/common/Icon';
@@ -19,21 +19,25 @@ import { useSystemStore } from '@/web/common/system/useSystemStore';
import { AppContext } from '@/pages/app/detail/components/context';
import { useContextSelector } from 'use-context-selector';
import MyMenu from '@fastgpt/web/components/common/MyMenu';
import { useI18n } from '@/web/context/I18n';
import MyModal from '@fastgpt/web/components/common/MyModal';
import { useRequest2 } from '@fastgpt/web/hooks/useRequest';
import { postTransition2Workflow } from '@/web/core/app/api/app';
import { AppTypeEnum } from '@fastgpt/global/core/app/constants';
import { form2AppWorkflow } from '@/web/core/app/utils';
import { SimpleAppSnapshotType } from './useSnapshots';
const AppCard = () => {
const AppCard = ({
appForm,
setPast
}: {
appForm: AppSimpleEditFormType;
setPast: (value: React.SetStateAction<SimpleAppSnapshotType[]>) => void;
}) => {
const router = useRouter();
const { t } = useTranslation();
const { appT } = useI18n();
const { appDetail, setAppDetail, onOpenInfoEdit, onDelApp } = useContextSelector(
AppContext,
(v) => v
);
const onSaveApp = useContextSelector(AppContext, (v) => v.onSaveApp);
const appDetail = useContextSelector(AppContext, (v) => v.appDetail);
const onOpenInfoEdit = useContextSelector(AppContext, (v) => v.onOpenInfoEdit);
const onDelApp = useContextSelector(AppContext, (v) => v.onDelApp);
const appId = appDetail._id;
const { feConfigs } = useSystemStore();
@@ -42,7 +46,18 @@ const AppCard = () => {
// transition to workflow
const [transitionCreateNew, setTransitionCreateNew] = useState<boolean>();
const { runAsync: onTransition, loading: transiting } = useRequest2(
() => postTransition2Workflow({ appId, createNew: transitionCreateNew }),
async () => {
const { nodes, edges } = form2AppWorkflow(appForm, t);
await onSaveApp({
nodes,
edges,
chatConfig: appForm.chatConfig,
isPublish: false,
versionName: t('app:transition_to_workflow')
});
return postTransition2Workflow({ appId, createNew: transitionCreateNew });
},
{
onSuccess: ({ id }) => {
if (id) {
@@ -52,10 +67,8 @@ const AppCard = () => {
}
});
} else {
setAppDetail((state) => ({
...state,
type: AppTypeEnum.workflow
}));
setPast([]);
router.reload();
}
},
successToast: t('common:common.Success')
@@ -118,7 +131,7 @@ const AppCard = () => {
children: [
{
icon: 'core/app/type/workflow',
label: appT('transition_to_workflow'),
label: t('app:transition_to_workflow'),
onClick: () => setTransitionCreateNew(true)
},
...(appDetail.permission.hasWritePer && feConfigs?.show_team_chat
@@ -159,15 +172,15 @@ const AppCard = () => {
</Box>
{TeamTagsSet && <TagsEditModal onClose={() => setTeamTagsSet(undefined)} />}
{transitionCreateNew !== undefined && (
<MyModal isOpen title={appT('transition_to_workflow')} iconSrc="core/app/type/workflow">
<MyModal isOpen title={t('app:transition_to_workflow')} iconSrc="core/app/type/workflow">
<ModalBody>
<Box mb={3}>{appT('transition_to_workflow_create_new_tip')}</Box>
<Box mb={3}>{t('app:transition_to_workflow_create_new_tip')}</Box>
<HStack cursor={'pointer'} onClick={() => setTransitionCreateNew((state) => !state)}>
<Checkbox
isChecked={transitionCreateNew}
icon={<MyIcon name={'common/check'} w={'12px'} />}
/>
<Box>{appT('transition_to_workflow_create_new_placeholder')}</Box>
<Box>{t('app:transition_to_workflow_create_new_placeholder')}</Box>
</HStack>
</ModalBody>
<ModalFooter>

View File

@@ -1,6 +1,6 @@
import { Box, Flex, IconButton } from '@chakra-ui/react';
import { useTranslation } from 'next-i18next';
import React, { useEffect } from 'react';
import React, { useEffect, useMemo } from 'react';
import MyTooltip from '@fastgpt/web/components/common/MyTooltip';
import MyIcon from '@fastgpt/web/components/common/Icon';
@@ -12,8 +12,13 @@ import { useContextSelector } from 'use-context-selector';
import { AppContext } from '../context';
import { useChatTest } from '../useChatTest';
import { useDatasetStore } from '@/web/core/dataset/store/dataset';
import ChatItemContextProvider from '@/web/core/chat/context/chatItemContext';
import ChatRecordContextProvider from '@/web/core/chat/context/chatRecordContext';
import { useChatStore } from '@/web/core/chat/context/useChatStore';
import MyBox from '@fastgpt/web/components/common/MyBox';
const ChatTest = ({ appForm }: { appForm: AppSimpleEditFormType }) => {
type Props = { appForm: AppSimpleEditFormType };
const ChatTest = ({ appForm }: Props) => {
const { t } = useTranslation();
const { appT } = useI18n();
@@ -32,13 +37,21 @@ const ChatTest = ({ appForm }: { appForm: AppSimpleEditFormType }) => {
setWorkflowData({ nodes, edges });
}, [appForm, setWorkflowData, allDatasets, t]);
const { restartChat, ChatContainer } = useChatTest({
const { ChatContainer, restartChat, loading } = useChatTest({
...workflowData,
chatConfig: appForm.chatConfig
chatConfig: appForm.chatConfig,
isReady: true
});
return (
<Flex position={'relative'} flexDirection={'column'} h={'100%'} py={4}>
<MyBox
isLoading={loading}
display={'flex'}
position={'relative'}
flexDirection={'column'}
h={'100%'}
py={4}
>
<Flex px={[2, 5]}>
<Box fontSize={['md', 'lg']} fontWeight={'bold'} flex={1} color={'myGray.900'}>
{appT('chat_debug')}
@@ -61,8 +74,29 @@ const ChatTest = ({ appForm }: { appForm: AppSimpleEditFormType }) => {
<Box flex={1}>
<ChatContainer />
</Box>
</Flex>
</MyBox>
);
};
export default React.memo(ChatTest);
const Render = ({ appForm }: Props) => {
const { chatId } = useChatStore();
const { appDetail } = useContextSelector(AppContext, (v) => v);
const chatRecordProviderParams = useMemo(
() => ({
chatId: chatId,
appId: appDetail._id
}),
[appDetail._id, chatId]
);
return (
<ChatItemContextProvider>
<ChatRecordContextProvider params={chatRecordProviderParams}>
<ChatTest appForm={appForm} />
</ChatRecordContextProvider>
</ChatItemContextProvider>
);
};
export default React.memo(Render);

View File

@@ -132,7 +132,7 @@ const Edit = ({
flex={'1'}
>
<Box {...cardStyles} boxShadow={'2'}>
<AppCard />
<AppCard appForm={appForm} setPast={setPast} />
</Box>
<Box mt={4} {...cardStyles} boxShadow={'3.5'}>

View File

@@ -87,7 +87,6 @@ const Header = ({
nodes,
edges,
chatConfig: appForm.chatConfig,
type: AppTypeEnum.simple,
isPublish,
versionName
});

View File

@@ -37,7 +37,8 @@ export const compareSimpleAppSnapshot = (
scheduledTriggerConfig: appForm1.chatConfig?.scheduledTriggerConfig || undefined,
chatInputGuide: appForm1.chatConfig?.chatInputGuide || undefined,
fileSelectConfig: appForm1.chatConfig?.fileSelectConfig || undefined,
instruction: appForm1.chatConfig?.instruction || ''
instruction: appForm1.chatConfig?.instruction || '',
autoExecute: appForm1.chatConfig?.autoExecute || undefined
},
{
welcomeText: appForm2.chatConfig?.welcomeText || '',
@@ -48,7 +49,8 @@ export const compareSimpleAppSnapshot = (
scheduledTriggerConfig: appForm2.chatConfig?.scheduledTriggerConfig || undefined,
chatInputGuide: appForm2.chatConfig?.chatInputGuide || undefined,
fileSelectConfig: appForm2.chatConfig?.fileSelectConfig || undefined,
instruction: appForm2.chatConfig?.instruction || ''
instruction: appForm2.chatConfig?.instruction || '',
autoExecute: appForm2.chatConfig?.autoExecute || undefined
}
)
) {

View File

@@ -147,7 +147,6 @@ const AppCard = ({
appDetail.name,
appDetail.permission.hasWritePer,
appDetail.permission.isOwner,
currentTab,
feConfigs?.show_team_chat,
onDelApp,
onOpenImport,
@@ -252,10 +251,6 @@ function ExportPopover({
trigger={'hover'}
w={'8.6rem'}
Trigger={
// <Flex align={'center'} w={'100%'} py={2} px={3}>
// <Avatar src={'export'} borderRadius={'sm'} w={'1rem'} mr={3} />
// {t('app:export_configs')}
// </Flex>
<MyBox display={'flex'} size={'md'} rounded={'4px'} cursor={'pointer'}>
<MyIcon name={'export'} w={'16px'} mr={2} />
<Box fontSize={'sm'}>{t('app:export_configs')}</Box>

View File

@@ -1,5 +1,5 @@
import type { StoreNodeItemType } from '@fastgpt/global/core/workflow/type/node.d';
import React from 'react';
import React, { useMemo } from 'react';
import { SmallCloseIcon } from '@chakra-ui/icons';
import { Box, Flex, IconButton } from '@chakra-ui/react';
import MyIcon from '@fastgpt/web/components/common/Icon';
@@ -14,27 +14,34 @@ import { AppTypeEnum } from '@fastgpt/global/core/app/constants';
import LightRowTabs from '@fastgpt/web/components/common/Tabs/LightRowTabs';
import { PluginRunBoxTabEnum } from '@/components/core/chat/ChatContainer/PluginRunBox/constants';
import CloseIcon from '@fastgpt/web/components/common/Icon/close';
import ChatItemContextProvider, { ChatItemContext } from '@/web/core/chat/context/chatItemContext';
import ChatRecordContextProvider, {
ChatRecordContext
} from '@/web/core/chat/context/chatRecordContext';
import { useChatStore } from '@/web/core/chat/context/useChatStore';
import MyBox from '@fastgpt/web/components/common/MyBox';
const ChatTest = ({
isOpen,
nodes = [],
edges = [],
onClose
}: {
type Props = {
isOpen: boolean;
nodes?: StoreNodeItemType[];
edges?: StoreEdgeItemType[];
onClose: () => void;
}) => {
};
const ChatTest = ({ isOpen, nodes = [], edges = [], onClose }: Props) => {
const { t } = useTranslation();
const { appDetail } = useContextSelector(AppContext, (v) => v);
const isPlugin = appDetail.type === AppTypeEnum.plugin;
const { restartChat, ChatContainer, pluginRunTab, setPluginRunTab, chatRecords } = useChatTest({
const { restartChat, ChatContainer, loading } = useChatTest({
nodes,
edges,
chatConfig: appDetail.chatConfig
chatConfig: appDetail.chatConfig,
isReady: isOpen
});
const pluginRunTab = useContextSelector(ChatItemContext, (v) => v.pluginRunTab);
const setPluginRunTab = useContextSelector(ChatItemContext, (v) => v.setPluginRunTab);
const chatRecords = useContextSelector(ChatRecordContext, (v) => v.chatRecords);
return (
<>
@@ -48,8 +55,10 @@ const ChatTest = ({
right={0}
onClick={onClose}
/>
<Flex
<MyBox
isLoading={loading}
zIndex={300}
display={'flex'}
flexDirection={'column'}
position={'absolute'}
top={5}
@@ -131,9 +140,30 @@ const ChatTest = ({
<Box flex={'1 0 0'} overflow={'auto'}>
<ChatContainer />
</Box>
</Flex>
</MyBox>
</>
);
};
export default React.memo(ChatTest);
const Render = (Props: Props) => {
const { chatId } = useChatStore();
const { appDetail } = useContextSelector(AppContext, (v) => v);
const chatRecordProviderParams = useMemo(
() => ({
chatId: chatId,
appId: appDetail._id
}),
[appDetail._id, chatId]
);
return (
<ChatItemContextProvider>
<ChatRecordContextProvider params={chatRecordProviderParams}>
<ChatTest {...Props} />
</ChatRecordContextProvider>
</ChatItemContextProvider>
);
};
export default React.memo(Render);

View File

@@ -240,7 +240,7 @@ const NodeTemplatesModal = ({ isOpen, onClose }: ModuleTemplateListProps) => {
position={'absolute'}
top={'10px'}
left={0}
pt={'20px'}
pt={5}
pb={4}
h={isOpen ? 'calc(100% - 20px)' : '0'}
w={isOpen ? ['100%', `${sliderWidth}px`] : '0'}
@@ -254,7 +254,7 @@ const NodeTemplatesModal = ({ isOpen, onClose }: ModuleTemplateListProps) => {
{/* Header */}
<Box px={'5'} mb={3} whiteSpace={'nowrap'} overflow={'hidden'}>
{/* Tabs */}
<Flex flex={'1 0 0'} alignItems={'center'} gap={3}>
<Flex flex={'1 0 0'} alignItems={'center'} gap={2}>
<Box flex={'1 0 0'}>
<FillRowTabs
list={[
@@ -288,8 +288,14 @@ const NodeTemplatesModal = ({ isOpen, onClose }: ModuleTemplateListProps) => {
{/* close icon */}
<IconButton
size={'sm'}
icon={<MyIcon name={'common/backFill'} w={'14px'} color={'myGray.700'} />}
borderColor={'myGray.300'}
icon={<MyIcon name={'common/backFill'} w={'14px'} color={'myGray.600'} />}
bg={'myGray.100'}
_hover={{
bg: 'myGray.200',
'& svg': {
color: 'primary.600'
}
}}
variant={'grayBase'}
aria-label={''}
onClick={onClose}

View File

@@ -177,20 +177,15 @@ const ButtonEdge = (props: EdgeProps) => {
position={'absolute'}
transform={arrowTransform}
pointerEvents={'all'}
w={highlightEdge ? '14px' : '10px'}
h={highlightEdge ? '14px' : '10px'}
w={highlightEdge ? '12px' : '10px'}
h={highlightEdge ? '12px' : '10px'}
zIndex={highlightEdge ? defaultZIndex + 1000 : defaultZIndex}
>
<MyIcon
name={'core/workflow/edgeArrow'}
name={highlightEdge ? 'core/workflow/edgeArrowBold' : 'core/workflow/edgeArrow'}
w={'100%'}
color={edgeColor}
{...(highlightEdge
? {
fontWeight: 'bold'
}
: {})}
></MyIcon>
/>
</Flex>
)}
</Box>
@@ -223,7 +218,7 @@ const ButtonEdge = (props: EdgeProps) => {
...style,
...(highlightEdge
? {
strokeWidth: 5
strokeWidth: 4
}
: { strokeWidth: 3, zIndex: 2 })
};

View File

@@ -1,7 +1,7 @@
import { Box, Flex } from '@chakra-ui/react';
import React from 'react';
import MyIcon from '@fastgpt/web/components/common/Icon';
import { useTranslation } from 'react-i18next';
import { useTranslation } from 'next-i18next';
import { nodeTemplate2FlowNode } from '@/web/core/workflow/utils';
import { CommentNode } from '@fastgpt/global/core/workflow/template/system/comment';
import { useContextSelector } from 'use-context-selector';

View File

@@ -333,7 +333,7 @@ export const useDebug = () => {
<Flex alignItems={'center'} mb={1}>
<Box position={'relative'}>
{required && (
<Box position={'absolute'} right={-2} top={'-1px'} color={'red.600'}>
<Box position={'absolute'} left={'-8px'} top={'-2px'} color={'red.600'}>
*
</Box>
)}

View File

@@ -36,8 +36,8 @@ const NodeCode = ({ data, selected }: NodeProps<FlowNodeItemType>) => {
return {
[NodeInputKeyEnum.code]: (item: FlowNodeInputItemType) => {
return (
<Box>
<Flex mb={1} alignItems={'flex-end'}>
<Box mt={-3}>
<Flex mb={2} alignItems={'flex-end'}>
<Box flex={'1'}>{'Javascript ' + workflowT('Code')}</Box>
<Box
cursor={'pointer'}
@@ -88,7 +88,7 @@ const NodeCode = ({ data, selected }: NodeProps<FlowNodeItemType>) => {
</>
)}
<Container>
<IOTitle text={t('common:common.Input')} />
<IOTitle text={t('common:common.Input')} mb={-1} />
<RenderInput
nodeId={nodeId}
flowInputList={commonInputs}

View File

@@ -7,7 +7,7 @@ import { useContextSelector } from 'use-context-selector';
import { WorkflowContext } from '../../context';
import { NodeInputKeyEnum } from '@fastgpt/global/core/workflow/constants';
import MyIcon from '@fastgpt/web/components/common/Icon';
import { useTranslation } from 'react-i18next';
import { useTranslation } from 'next-i18next';
const NodeComment = ({ data }: NodeProps<FlowNodeItemType>) => {
const { nodeId, inputs } = data;

View File

@@ -34,6 +34,7 @@ import { getNanoid } from '@fastgpt/global/common/string/tools';
import IOTitle from '../../components/IOTitle';
import { useContextSelector } from 'use-context-selector';
import { WorkflowContext } from '../../../context';
import MyIconButton from '@fastgpt/web/components/common/Icon/button';
const NodeExtract = ({ data }: NodeProps<FlowNodeItemType>) => {
const { inputs, outputs, nodeId } = data;
@@ -53,14 +54,15 @@ const NodeExtract = ({ data }: NodeProps<FlowNodeItemType>) => {
}: Omit<FlowNodeInputItemType, 'value'> & {
value?: ContextExtractAgentItemType[];
}) => (
<Box>
<Box mt={-2}>
<Flex alignItems={'center'}>
<Box flex={'1 0 0'} fontWeight={'medium'} color={'myGray.600'}>
<Box flex={'1 0 0'} fontSize={'sm'} fontWeight={'medium'} color={'myGray.600'}>
{t('common:core.module.extract.Target field')}
</Box>
<Button
size={'sm'}
variant={'ghost'}
variant={'grayGhost'}
px={2}
color={'myGray.600'}
leftIcon={<AddIcon fontSize={'10px'} />}
onClick={() => setEditExtractField(defaultField)}
@@ -68,48 +70,47 @@ const NodeExtract = ({ data }: NodeProps<FlowNodeItemType>) => {
{t('common:core.module.extract.Add field')}
</Button>
</Flex>
<Box
mt={2}
borderRadius={'md'}
overflow={'hidden'}
borderWidth={'1px'}
borderBottom="none"
>
<TableContainer>
<Table bg={'white'}>
<Thead>
<Tr>
<Th borderRadius={'none !important'}>{t('common:item_name')}</Th>
<Th>{t('common:item_description')}</Th>
<Th>{t('common:required')}</Th>
<Th borderRadius={'none !important'}></Th>
</Tr>
</Thead>
<Tbody>
{extractKeys.map((item, index) => (
<Tr
key={index}
position={'relative'}
whiteSpace={'pre-wrap'}
wordBreak={'break-all'}
>
<Td>{item.key}</Td>
<Td>{item.desc}</Td>
<Td>{item.required ? '✔' : ''}</Td>
<Td whiteSpace={'nowrap'}>
<MyIcon
mr={3}
name={'common/settingLight'}
w={'16px'}
cursor={'pointer'}
<TableContainer borderRadius={'md'} overflow={'hidden'} borderWidth={'1px'} mt={2}>
<Table variant={'workflow'}>
<Thead>
<Tr>
<Th>{t('common:item_name')}</Th>
<Th>{t('common:item_description')}</Th>
<Th>{t('common:required')}</Th>
<Th></Th>
</Tr>
</Thead>
<Tbody>
{extractKeys.map((item, index) => (
<Tr key={index}>
<Td>
<Flex alignItems={'center'}>
<MyIcon name={'checkCircle'} w={'14px'} mr={1} color={'myGray.600'} />
{item.key}
</Flex>
</Td>
<Td>{item.desc}</Td>
<Td>
{item.required ? (
<Flex alignItems={'center'}>
<MyIcon name={'check'} w={'16px'} color={'myGray.900'} mr={2} />
</Flex>
) : (
''
)}
</Td>
<Td>
<Flex>
<MyIconButton
icon={'common/settingLight'}
onClick={() => {
setEditExtractField(item);
}}
/>
<MyIcon
name={'delete'}
w={'16px'}
cursor={'pointer'}
<MyIconButton
icon={'delete'}
hoverColor={'red.500'}
onClick={() => {
onChangeNode({
nodeId,
@@ -128,13 +129,13 @@ const NodeExtract = ({ data }: NodeProps<FlowNodeItemType>) => {
});
}}
/>
</Td>
</Tr>
))}
</Tbody>
</Table>
</TableContainer>
</Box>
</Flex>
</Td>
</Tr>
))}
</Tbody>
</Table>
</TableContainer>
</Box>
)
}),

View File

@@ -3,7 +3,7 @@ import { WorkflowIOValueTypeEnum } from '@fastgpt/global/core/workflow/constants
import { FlowNodeInputTypeEnum } from '@fastgpt/global/core/workflow/node/constant';
import MyModal from '@fastgpt/web/components/common/MyModal';
import React, { useCallback } from 'react';
import { useTranslation } from 'react-i18next';
import { useTranslation } from 'next-i18next';
import MyIcon from '@fastgpt/web/components/common/Icon';
import { UserInputFormItemType } from '@fastgpt/global/core/workflow/template/system/interactive/type';
import { useForm } from 'react-hook-form';

View File

@@ -13,7 +13,6 @@ import {
Box,
Button,
Flex,
FormLabel,
HStack,
Table,
TableContainer,
@@ -24,7 +23,7 @@ import {
Tr
} from '@chakra-ui/react';
import { UserInputFormItemType } from '@fastgpt/global/core/workflow/template/system/interactive/type';
import { useTranslation } from 'react-i18next';
import { useTranslation } from 'next-i18next';
import {
FlowNodeInputMap,
FlowNodeInputTypeEnum,
@@ -37,6 +36,8 @@ import { useContextSelector } from 'use-context-selector';
import { WorkflowContext } from '../../../context';
import InputFormEditModal, { defaultFormInput } from './InputFormEditModal';
import RenderOutput from '../render/RenderOutput';
import FormLabel from '@fastgpt/web/components/common/MyBox/FormLabel';
import MyIconButton from '@fastgpt/web/components/common/Icon/button';
const NodeFormInput = ({ data, selected }: NodeProps<FlowNodeItemType>) => {
const { nodeId, inputs, outputs } = data;
@@ -120,10 +121,14 @@ const NodeFormInput = ({ data, selected }: NodeProps<FlowNodeItemType>) => {
return (
<Box>
<HStack className="nodrag" cursor={'default'} mb={3}>
<FormLabel>{t('workflow:user_form_input_config')}</FormLabel>
<FormLabel fontSize={'sm'} color={'myGray.600'}>
{t('workflow:user_form_input_config')}
</FormLabel>
<Box flex={'1 0 0'} />
<Button
variant={'ghost'}
variant={'grayGhost'}
px={2}
color={'myGray.600'}
leftIcon={<SmallAddIcon />}
iconSpacing={1}
size={'sm'}
@@ -144,17 +149,14 @@ const NodeFormInput = ({ data, selected }: NodeProps<FlowNodeItemType>) => {
/>
)}
</HStack>
<TableContainer borderWidth={'1px'} borderRadius={'md'} borderBottom="none">
<Table bg={'white'}>
<TableContainer borderWidth={'1px'} borderRadius={'md'}>
<Table variant={'workflow'}>
<Thead>
<Tr>
<Th borderBottomLeftRadius={'none !important'}>
{t('workflow:user_form_input_name')}
</Th>
<Th>{t('workflow:user_form_input_name')}</Th>
<Th>{t('workflow:user_form_input_description')}</Th>
<Th>{t('common:common.Require Input')}</Th>
<Th borderBottomRightRadius={'none !important'}>{t('user:operations')}</Th>
<Th>{t('user:operations')}</Th>
</Tr>
</Thead>
<Tbody>
@@ -163,35 +165,35 @@ const NodeFormInput = ({ data, selected }: NodeProps<FlowNodeItemType>) => {
return (
<Tr key={index}>
<Td>
<Flex alignItems={'center'}>
<Flex alignItems={'center'} fontSize={'mini'} fontWeight={'medium'}>
{!!icon && (
<MyIcon name={icon as any} w={'14px'} mr={1} color={'primary.600'} />
<MyIcon name={icon as any} w={'14px'} mr={1} color={'myGray.400'} />
)}
{item.label}
</Flex>
</Td>
<Td>{item.description}</Td>
<Td>{item.required ? '✅' : ''}</Td>
<Td>{item.description || '-'}</Td>
<Td>
<MyIcon
mr={3}
name={'common/settingLight'}
w={'16px'}
cursor={'pointer'}
_hover={{ color: 'primary.600' }}
onClick={() => setEditField(item)}
/>
<MyIcon
className="delete"
name={'delete'}
w={'16px'}
color={'myGray.600'}
cursor={'pointer'}
_hover={{ color: 'red.500' }}
onClick={() => {
onDelete(item.key);
}}
/>
{item.required ? (
<Flex alignItems={'center'}>
<MyIcon name={'check'} w={'16px'} color={'myGray.900'} mr={2} />
</Flex>
) : (
'-'
)}
</Td>
<Td>
<Flex>
<MyIconButton
icon={'common/settingLight'}
onClick={() => setEditField(item)}
/>
<MyIconButton
icon={'delete'}
hoverColor={'red.500'}
onClick={() => onDelete(item.key)}
/>
</Flex>
</Td>
</Tr>
);

View File

@@ -5,11 +5,6 @@ import {
FormControl,
HStack,
Input,
NumberDecrementStepper,
NumberIncrementStepper,
NumberInput,
NumberInputField,
NumberInputStepper,
Stack,
Switch,
Textarea
@@ -28,7 +23,7 @@ import QuestionTip from '@fastgpt/web/components/common/MyTooltip/QuestionTip';
import JsonEditor from '@fastgpt/web/components/common/Textarea/JsonEditor';
import React, { useMemo } from 'react';
import { useFieldArray, UseFormReturn } from 'react-hook-form';
import { useTranslation } from 'react-i18next';
import { useTranslation } from 'next-i18next';
import MyIcon from '@fastgpt/web/components/common/Icon';
import DndDrag, { Draggable } from '@fastgpt/web/components/common/DndDrag';
import MyTextarea from '@/components/common/Textarea/MyTextarea';

View File

@@ -100,6 +100,7 @@ const PluginOutputEditModal = ({
data.key = data?.key?.trim();
data.label = data.key;
data.required = true;
onSubmit({
data,

View File

@@ -1,7 +1,8 @@
import React from 'react';
import MyIcon from '@fastgpt/web/components/common/Icon';
import { Box, Table, Thead, Tbody, Tr, Th, Td, TableContainer, Flex } from '@chakra-ui/react';
import { Table, Thead, Tbody, Tr, Th, Td, TableContainer, Flex } from '@chakra-ui/react';
import { useTranslation } from 'next-i18next';
import MyIconButton from '@fastgpt/web/components/common/Icon/button';
const VariableTable = ({
variables = [],
@@ -16,58 +17,61 @@ const VariableTable = ({
const showToolColumn = variables.some((item) => item.isTool);
return (
<Box bg={'white'} borderRadius={'md'} overflow={'hidden'} border={'base'}>
<TableContainer>
<Table bg={'white'}>
<Thead>
<Tr>
<Th borderBottomLeftRadius={'none !important'}>{t('workflow:Variable_name')}</Th>
<Th>{t('common:core.workflow.Value type')}</Th>
{showToolColumn && <Th>{t('workflow:tool_input')}</Th>}
<Th borderBottomRightRadius={'none !important'}></Th>
<TableContainer
borderRadius={'md'}
overflow={'hidden'}
border={'1px solid'}
borderColor={'myGray.200'}
>
<Table variant={'workflow'}>
<Thead>
<Tr>
<Th>{t('workflow:Variable_name')}</Th>
<Th>{t('common:core.workflow.Value type')}</Th>
{showToolColumn && <Th>{t('workflow:tool_input')}</Th>}
<Th>{t('user:operations')}</Th>
</Tr>
</Thead>
<Tbody>
{variables.map((item, index) => (
<Tr key={item.key}>
<Td>
<Flex alignItems={'center'} fontSize={'xs'}>
{!!item.icon ? (
<MyIcon name={item.icon as any} w={'14px'} mr={1} color={'myGray.600'} />
) : (
<MyIcon name={'checkCircle'} w={'14px'} mr={1} color={'myGray.600'} />
)}
{item.label || item.key}
</Flex>
</Td>
<Td>{item.type}</Td>
{showToolColumn && (
<Td>
{item.isTool ? (
<Flex alignItems={'center'}>
<MyIcon name={'check'} w={'16px'} color={'myGray.900'} mr={2} />
</Flex>
) : (
''
)}
</Td>
)}
<Td>
<Flex>
<MyIconButton icon={'common/settingLight'} onClick={() => onEdit(item.key)} />
<MyIconButton
icon={'delete'}
hoverColor={'red.500'}
onClick={() => onDelete(item.key)}
/>
</Flex>
</Td>
</Tr>
</Thead>
<Tbody>
{variables.map((item) => (
<Tr key={item.key}>
<Td>
<Flex alignItems={'center'}>
{!!item.icon && (
<MyIcon name={item.icon as any} w={'14px'} mr={1} color={'primary.600'} />
)}
{item.label || item.key}
</Flex>
</Td>
<Td>{item.type}</Td>
{showToolColumn && <Th>{item.isTool ? '✅' : '-'}</Th>}
<Td>
<MyIcon
mr={3}
name={'common/settingLight'}
w={'16px'}
cursor={'pointer'}
_hover={{ color: 'primary.600' }}
onClick={() => onEdit(item.key)}
/>
<MyIcon
className="delete"
name={'delete'}
w={'16px'}
color={'myGray.600'}
cursor={'pointer'}
ml={2}
_hover={{ color: 'red.500' }}
onClick={() => {
onDelete(item.key);
}}
/>
</Td>
</Tr>
))}
</Tbody>
</Table>
</TableContainer>
</Box>
))}
</Tbody>
</Table>
</TableContainer>
);
};

View File

@@ -22,6 +22,7 @@ import FileSelect from '@/components/core/app/FileSelect';
import { FlowNodeTypeEnum } from '@fastgpt/global/core/workflow/node/constant';
import { userFilesInput } from '@fastgpt/global/core/workflow/template/system/workflowStart';
import Container from '../components/Container';
import AutoExecConfig from '@/components/core/app/AutoExecConfig';
type ComponentProps = {
chatConfig: AppChatConfigType;
@@ -80,6 +81,9 @@ const NodeUserGuide = ({ data, selected }: NodeProps<FlowNodeItemType>) => {
<Box mt={4} pt={3} borderTop={'base'} borderColor={'myGray.200'}>
<ScheduledTrigger {...componentsProps} />
</Box>
<Box mt={3} pt={3} borderTop={'base'} borderColor={'myGray.200'}>
<AutoExecute {...componentsProps} />
</Box>
<Box mt={3} pt={3} borderTop={'base'} borderColor={'myGray.200'}>
<QuestionInputGuide {...componentsProps} />
</Box>
@@ -128,6 +132,23 @@ function ChatStartVariable({ chatConfig: { variables = [] }, setAppDetail }: Com
return <VariableEdit variables={variables} onChange={(e) => updateVariables(e)} />;
}
function AutoExecute({ chatConfig: { autoExecute }, setAppDetail }: ComponentProps) {
return (
<AutoExecConfig
value={autoExecute}
onChange={(e) =>
setAppDetail((state) => ({
...state,
chatConfig: {
...state.chatConfig,
autoExecute: e
}
}))
}
/>
);
}
function QuestionGuide({ chatConfig: { questionGuide = false }, setAppDetail }: ComponentProps) {
return (
<QGSwitch

View File

@@ -5,7 +5,7 @@ import MyModal from '@fastgpt/web/components/common/MyModal';
import MySelect from '@fastgpt/web/components/common/MySelect';
import React, { useCallback, useMemo } from 'react';
import { useForm } from 'react-hook-form';
import { useTranslation } from 'react-i18next';
import { useTranslation } from 'next-i18next';
import { defaultEditFormData } from '../render/RenderToolInput/EditFieldModal';
import { useRequest2 } from '@fastgpt/web/hooks/useRequest';
import { useContextSelector } from 'use-context-selector';

View File

@@ -16,7 +16,7 @@ import {
Thead,
Tr
} from '@chakra-ui/react';
import { useTranslation } from 'react-i18next';
import { useTranslation } from 'next-i18next';
import { SmallAddIcon } from '@chakra-ui/icons';
import { FlowNodeInputItemType } from '@fastgpt/global/core/workflow/type/io';
import { defaultEditFormData } from '../render/RenderToolInput/EditFieldModal';
@@ -36,7 +36,7 @@ const NodeToolParams = ({ data, selected }: NodeProps<FlowNodeItemType>) => {
<NodeCard selected={selected} {...data}>
<Container>
<Flex alignItems={'center'} justifyContent={'space-between'} mb={1.5}>
<FormLabel>{t('workflow:tool_custom_field')}</FormLabel>
<FormLabel fontSize={'sm'}>{t('workflow:tool_custom_field')}</FormLabel>
<Button
variant={'whiteBase'}
leftIcon={<SmallAddIcon />}
@@ -54,38 +54,89 @@ const NodeToolParams = ({ data, selected }: NodeProps<FlowNodeItemType>) => {
/>
)}
</Flex>
<Box borderRadius={'md'} overflow={'hidden'} border={'base'}>
<TableContainer>
<Table bg={'white'}>
<Thead>
<Tr>
<Th>{t('workflow:tool_params.params_name')}</Th>
<Th>{t('workflow:tool_params.params_description')}</Th>
<Th>{t('common:common.Operation')}</Th>
</Tr>
</Thead>
<Tbody>
{inputs.map((item, index) => (
<Tr
key={index}
position={'relative'}
whiteSpace={'pre-wrap'}
wordBreak={'break-all'}
<TableContainer
borderRadius={'md'}
overflow={'hidden'}
border={'1px solid'}
borderColor={'myGray.200'}
>
<Table bg={'white'}>
<Thead>
<Tr h={8}>
<Th p={0} px={4} bg={'myGray.50'} borderBottomLeftRadius={'none !important'}>
{t('workflow:tool_params.params_name')}
</Th>
<Th p={0} px={4} bg={'myGray.50'}>
{t('workflow:tool_params.params_description')}
</Th>
<Th p={0} px={4} bg={'myGray.50'} borderBottomRightRadius={'none !important'}>
{t('common:common.Operation')}
</Th>
</Tr>
</Thead>
<Tbody>
{inputs.map((item, index) => (
<Tr
key={index}
position={'relative'}
whiteSpace={'pre-wrap'}
wordBreak={'break-all'}
h={10}
>
<Td
p={0}
px={4}
borderBottom={index === inputs.length - 1 ? 'none' : undefined}
>
<Td>{item.key}</Td>
<Td>{item.toolDescription}</Td>
<Td whiteSpace={'nowrap'}>
<MyIcon
<Flex alignItems={'center'} fontSize={'xs'}>
<MyIcon name={'checkCircle'} w={'14px'} mr={1} color={'myGray.600'} />
{item.key}
</Flex>
</Td>
<Td
p={0}
px={4}
borderBottom={index === inputs.length - 1 ? 'none' : undefined}
fontSize={'xs'}
>
{item.toolDescription}
</Td>
<Td
p={0}
px={4}
borderBottom={index === inputs.length - 1 ? 'none' : undefined}
whiteSpace={'nowrap'}
>
<Flex alignItems={'center'}>
<Flex
mr={3}
name={'common/settingLight'}
w={'16px'}
p={1}
color={'myGray.500'}
rounded={'sm'}
alignItems={'center'}
bg={'transparent'}
transition={'background 0.1s'}
cursor={'pointer'}
_hover={{
bg: 'myGray.05',
color: 'primary.600'
}}
onClick={() => setEditField(item)}
/>
<MyIcon
name={'delete'}
w={'16px'}
>
<MyIcon name={'common/settingLight'} w={'16px'} />
</Flex>
<Flex
p={1}
color={'myGray.500'}
rounded={'sm'}
alignItems={'center'}
bg={'transparent'}
transition={'background 0.1s'}
cursor={'pointer'}
_hover={{
bg: 'myGray.05',
color: 'red.500'
}}
onClick={() => {
onChangeNode({
nodeId,
@@ -98,14 +149,16 @@ const NodeToolParams = ({ data, selected }: NodeProps<FlowNodeItemType>) => {
key: item.key
});
}}
/>
</Td>
</Tr>
))}
</Tbody>
</Table>
</TableContainer>
</Box>
>
<MyIcon name={'delete'} w={'16px'} />
</Flex>
</Flex>
</Td>
</Tr>
))}
</Tbody>
</Table>
</TableContainer>
</Container>
</NodeCard>
);

View File

@@ -26,7 +26,7 @@ const NodeTools = ({ data, selected }: NodeProps<FlowNodeItemType>) => {
<RenderOutput nodeId={nodeId} flowOutputList={outputs} />
</Container>
<Box position={'relative'}>
<Box mb={-4} borderBottomLeftRadius={'md'} borderBottomRadius={'md'} overflow={'hidden'}>
<Box mb={-3} borderBottomRadius={'lg'} overflow={'hidden'}>
<Divider
showBorderBottom={false}
icon={<MyIcon name="phoneTabbar/tool" w={'16px'} h={'16px'} />}

View File

@@ -123,7 +123,7 @@ const NodeVariableUpdate = ({ data, selected }: NodeProps<FlowNodeItemType>) =>
return (
<Container key={index} w={'full'} mx={0}>
<Flex alignItems={'center'}>
<Flex w={'60px'}>{t('common:core.workflow.variable')}</Flex>
<Flex w={'80px'}>{t('common:core.workflow.variable')}</Flex>
<VariableSelector
nodeId={nodeId}
variable={updateItem.variable}
@@ -162,7 +162,7 @@ const NodeVariableUpdate = ({ data, selected }: NodeProps<FlowNodeItemType>) =>
)}
</Flex>
<Flex mt={2} w={'full'} alignItems={'center'} className="nodrag">
<Flex w={'60px'}>
<Flex w={'80px'}>
<Box>{t('common:core.workflow.value')}</Box>
<MyTooltip
label={

Some files were not shown because too many files have changed in this diff Show More