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

@@ -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={

View File

@@ -53,6 +53,8 @@ export const ToolTargetHandle = ({ show, nodeId }: ToolHandleProps) => {
w={handleSize}
h={handleSize}
border={'4px solid #8774EE'}
rounded={'xs'}
bg={'white'}
transform={'translate(0,0) rotate(45deg)'}
pointerEvents={'none'}
/>
@@ -105,6 +107,8 @@ export const ToolSourceHandle = () => {
w={handleSize}
h={handleSize}
border={'4px solid #8774EE'}
rounded={'xs'}
bg={'white'}
transform={'translate(0,0) rotate(45deg)'}
pointerEvents={'none'}
/>

View File

@@ -20,7 +20,6 @@ import { getNanoid } from '@fastgpt/global/common/string/tools';
import { useContextSelector } from 'use-context-selector';
import { WorkflowContext } from '../../../context';
import { moduleTemplatesFlat } from '@fastgpt/global/core/workflow/template/constants';
import { QuestionOutlineIcon } from '@chakra-ui/icons';
import MyTooltip from '@fastgpt/web/components/common/MyTooltip';
import { useRequest2 } from '@fastgpt/web/hooks/useRequest';
import { useWorkflowUtils } from '../../hooks/useUtils';
@@ -29,6 +28,7 @@ import { getDocPath } from '@/web/common/system/doc';
import { WorkflowNodeEdgeContext } from '../../../context/workflowInitContext';
import { WorkflowEventContext } from '../../../context/workflowEventContext';
import MyImage from '@fastgpt/web/components/common/Image/MyImage';
import MyIconButton from '@fastgpt/web/components/common/Icon/button';
type Props = FlowNodeItemType & {
children?: React.ReactNode | React.ReactNode[] | string;
@@ -166,7 +166,7 @@ const NodeCard = (props: Props) => {
<ToolTargetHandle show={showToolHandle} nodeId={nodeId} />
{/* avatar and name */}
<Flex alignItems={'center'}>
<Flex alignItems={'center'} mb={intro ? 1 : 0}>
{node?.flowNodeType !== FlowNodeTypeEnum.stopTool && (
<Flex
alignItems={'center'}
@@ -202,15 +202,13 @@ const NodeCard = (props: Props) => {
<Box ml={2} fontSize={'18px'} fontWeight={'medium'} color={'myGray.900'}>
{t(name as any)}
</Box>
<MyIcon
className="controller-rename"
<Button
display={'none'}
name={'edit'}
w={'14px'}
variant={'grayGhost'}
size={'xs'}
ml={0.5}
className="controller-rename"
cursor={'pointer'}
ml={1}
color={'myGray.500'}
_hover={{ color: 'primary.600' }}
onClick={() => {
onOpenCustomTitleModal({
defaultVal: name,
@@ -230,7 +228,9 @@ const NodeCard = (props: Props) => {
}
});
}}
/>
>
<MyIcon name={'edit'} w={'14px'} />
</Button>
<Box flex={1} />
{hasNewVersion && (
<MyTooltip label={t('app:app.modules.click to update')}>
@@ -248,7 +248,7 @@ const NodeCard = (props: Props) => {
onClick={onOpenConfirmSync(onClickSyncVersion)}
>
<Box>{t('app:app.modules.has new version')}</Box>
<QuestionOutlineIcon ml={1} />
<MyIcon name={'help'} w={'14px'} ml={1} />
</Button>
</MyTooltip>
)}
@@ -263,32 +263,20 @@ const NodeCard = (props: Props) => {
/>
}
>
<Box
fontSize={'sm'}
color={'primary.700'}
p={1}
rounded={'sm'}
cursor={'default'}
_hover={{ bg: 'rgba(17, 24, 36, 0.05)' }}
>
<Button variant={'grayGhost'} size={'xs'} color={'primary.600'} px={1}>
{t('common:core.module.Diagram')}
</Box>
</Button>
</MyTooltip>
)}
{!!nodeTemplate?.diagram && node?.courseUrl && (
<Box bg={'myGray.300'} w={'1px'} h={'12px'} mx={1} />
<Box bg={'myGray.300'} w={'1px'} h={'12px'} ml={1} mr={0.5} />
)}
{node?.courseUrl && !hasNewVersion && (
<MyTooltip label={t('workflow:Node.Open_Node_Course')}>
<MyIcon
cursor={'pointer'}
name="book"
color={'primary.600'}
w={'18px'}
<MyIconButton
ml={1}
_hover={{
color: 'primary.800'
}}
icon="book"
color={'primary.600'}
onClick={() => window.open(getDocPath(node.courseUrl || ''), '_blank')}
/>
</MyTooltip>
@@ -378,7 +366,7 @@ const NodeCard = (props: Props) => {
>
<NodeDebugResponse nodeId={nodeId} debugResult={debugResult} />
{Header}
<Flex flexDirection={'column'} flex={1} my={!isFolded ? 4 : 0} gap={2}>
<Flex flexDirection={'column'} flex={1} my={!isFolded ? 3 : 0} gap={2}>
{!isFolded ? children : <Box h={4} />}
</Flex>
{RenderHandle}
@@ -521,7 +509,7 @@ const MenuRender = React.memo(function MenuRender({
className="nodrag controller-menu"
display={'none'}
flexDirection={'column'}
gap={3}
gap={2}
position={'absolute'}
top={'-20px'}
right={0}
@@ -532,16 +520,18 @@ const MenuRender = React.memo(function MenuRender({
pt={'20px'}
>
{menuList.map((item) => (
<Box key={item.icon}>
<Button
size={'xs'}
variant={item.variant}
leftIcon={<MyIcon name={item.icon as any} w={'13px'} />}
onClick={item.onClick}
>
{t(item.label as any)}
</Button>
</Box>
<Button
key={item.icon}
h={8}
fontSize={'sm'}
pl={2}
pr={6}
variant={item.variant}
leftIcon={<MyIcon name={item.icon as any} w={'16px'} mr={-1} />}
onClick={item.onClick}
>
{t(item.label as any)}
</Button>
))}
</Box>
<DebugInputModal />
@@ -592,31 +582,32 @@ const NodeIntro = React.memo(function NodeIntro({
<Box fontSize={'sm'} color={'myGray.500'} flex={'1 0 0'}>
{t(intro as any)}
</Box>
<Flex
p={'7px'}
rounded={'sm'}
alignItems={'center'}
_hover={{
bg: NodeIsTool ? 'myGray.100' : 'transparent'
}}
cursor={NodeIsTool ? 'pointer' : 'default'}
onClick={() => {
if (!NodeIsTool) return;
onOpenIntroModal({
defaultVal: intro,
onSuccess(e) {
onChangeNode({
nodeId,
type: 'attr',
key: 'intro',
value: e
});
}
});
}}
>
<MyIcon name={'edit'} w={'18px'} opacity={NodeIsTool ? 1 : 0} />
</Flex>
{NodeIsTool && (
<Flex
p={'7px'}
rounded={'sm'}
alignItems={'center'}
_hover={{
bg: 'myGray.100'
}}
cursor={'pointer'}
onClick={() => {
onOpenIntroModal({
defaultVal: intro,
onSuccess(e) {
onChangeNode({
nodeId,
type: 'attr',
key: 'intro',
value: e
});
}
});
}}
>
<MyIcon name={'edit'} w={'18px'} />
</Flex>
)}
</Flex>
<EditIntroModal maxLength={500} />
</>

View File

@@ -115,7 +115,6 @@ function Reference({
content: workflowT('confirm_delete_field_tip')
});
const onChangeNode = useContextSelector(WorkflowContext, (v) => v.onChangeNode);
const nodeList = useContextSelector(WorkflowContext, (v) => v.nodeList);
const keys = useMemo(() => {
return inputs.map((input) => input.key);
@@ -199,7 +198,7 @@ function Reference({
/>
<MyIcon
className="delete"
className={'delete'}
name={'delete'}
w={'14px'}
color={'myGray.500'}

View File

@@ -69,7 +69,7 @@ const SelectDatasetRender = ({ inputs = [], item, nodeId }: RenderInputProps) =>
w={'100%'}
>
<Button
h={'36px'}
h={10}
leftIcon={<MyIcon name={'common/selectLight'} w={'14px'} />}
onClick={onOpenDatasetSelect}
>
@@ -79,19 +79,20 @@ const SelectDatasetRender = ({ inputs = [], item, nodeId }: RenderInputProps) =>
<Flex
key={item._id}
alignItems={'center'}
h={'36px'}
h={10}
border={theme.borders.base}
borderColor={'myGray.200'}
px={2}
borderRadius={'md'}
>
<Avatar src={item.avatar} w={'24px'}></Avatar>
<Avatar src={item.avatar} w={'18px'} borderRadius={'xs'} />
<Box
ml={3}
ml={1.5}
flex={'1 0 0'}
w={0}
className="textEllipsis"
fontWeight={'bold'}
fontSize={['md', 'lg']}
fontSize={['sm', 'sm']}
>
{item.name}
</Box>

View File

@@ -4,7 +4,6 @@ import { Box, Button, Flex } from '@chakra-ui/react';
import { FlowNodeOutputTypeEnum } from '@fastgpt/global/core/workflow/node/constant';
import { NodeOutputKeyEnum } from '@fastgpt/global/core/workflow/constants';
import OutputLabel from './Label';
import { RenderOutputProps } from './type';
import { useTranslation } from 'next-i18next';
import { SmallAddIcon } from '@chakra-ui/icons';
import VariableTable from '../../NodePluginIO/VariableTable';
@@ -18,11 +17,6 @@ import { defaultOutput } from './FieldEditModal';
const FieldEditModal = dynamic(() => import('./FieldEditModal'));
const RenderList: {
types: FlowNodeOutputTypeEnum[];
Component: React.ComponentType<RenderOutputProps>;
}[] = [];
const RenderOutput = ({
nodeId,
flowOutputList
@@ -77,7 +71,7 @@ const RenderOutput = ({
alignItems={'center'}
position={'relative'}
>
<Box position={'relative'} fontWeight={'medium'}>
<Box position={'relative'} fontWeight={'medium'} fontSize={'sm'}>
{t((addOutput.label || 'common:core.workflow.Custom outputs') as any)}
</Box>
{addOutput.description && <QuestionTip ml={1} label={addOutput.description} />}

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