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>