Plugin runtime (#2050)

* feat: plugin run (#1950)

* feat: plugin run

* fix

* ui

* fix

* change user input type

* fix

* fix

* temp

* split out plugin chat

* perf: chatbox

* perf: chatbox

* fix: plugin runtime (#2032)

* fix: plugin runtime

* fix

* fix build

* fix build

* perf: chat send prompt

* perf: chat log ux

* perf: chatbox context and share page plugin runtime

* perf: plugin run time config

* fix: ts

* feat: doc

* perf: isPc check

* perf: variable input render

* feat: app search

* fix: response box height

* fix: phone ui

* perf: lock

* perf: plugin route

* fix: chat (#2049)

---------

Co-authored-by: heheer <71265218+newfish-cmyk@users.noreply.github.com>
This commit is contained in:
Archer
2024-07-15 22:50:48 +08:00
committed by GitHub
parent 090c880860
commit b5c98a4f63
126 changed files with 5012 additions and 4317 deletions

View File

@@ -1,262 +0,0 @@
import {
Box,
BoxProps,
Card,
Flex,
useTheme,
Accordion,
AccordionItem,
AccordionButton,
AccordionPanel,
AccordionIcon,
Image
} from '@chakra-ui/react';
import React, { useMemo } from 'react';
import ChatController, { type ChatControllerProps } from './ChatController';
import ChatAvatar from './ChatAvatar';
import { MessageCardStyle } from '../constants';
import { formatChatValue2InputType } from '../utils';
import Markdown, { CodeClassName } from '@/components/Markdown';
import styles from '../index.module.scss';
import MyIcon from '@fastgpt/web/components/common/Icon';
import {
ChatItemValueTypeEnum,
ChatRoleEnum,
ChatStatusEnum
} from '@fastgpt/global/core/chat/constants';
import FilesBlock from './FilesBox';
import { ChatBoxContext } from '../Provider';
import Avatar from '@/components/Avatar';
import { useContextSelector } from 'use-context-selector';
const colorMap = {
[ChatStatusEnum.loading]: {
bg: 'myGray.100',
color: 'myGray.600'
},
[ChatStatusEnum.running]: {
bg: 'green.50',
color: 'green.700'
},
[ChatStatusEnum.finish]: {
bg: 'green.50',
color: 'green.700'
}
};
const ChatItem = ({
type,
avatar,
statusBoxData,
children,
isLastChild,
questionGuides = [],
...chatControllerProps
}: {
type: ChatRoleEnum.Human | ChatRoleEnum.AI;
avatar?: string;
statusBoxData?: {
status: `${ChatStatusEnum}`;
name: string;
};
questionGuides?: string[];
children?: React.ReactNode;
} & ChatControllerProps) => {
const styleMap: BoxProps =
type === ChatRoleEnum.Human
? {
order: 0,
borderRadius: '8px 0 8px 8px',
justifyContent: 'flex-end',
textAlign: 'right',
bg: 'primary.100'
}
: {
order: 1,
borderRadius: '0 8px 8px 8px',
justifyContent: 'flex-start',
textAlign: 'left',
bg: 'myGray.50'
};
const isChatting = useContextSelector(ChatBoxContext, (v) => v.isChatting);
const { chat } = chatControllerProps;
const ContentCard = useMemo(() => {
if (type === 'Human') {
const { text, files = [] } = formatChatValue2InputType(chat.value);
return (
<>
{files.length > 0 && <FilesBlock files={files} />}
<Markdown source={text} />
</>
);
}
/* AI */
return (
<Flex flexDirection={'column'} key={chat.dataId} gap={2}>
{chat.value.map((value, i) => {
const key = `${chat.dataId}-ai-${i}`;
if (value.text) {
let source = (value.text?.content || '').trim();
if (!source && chat.value.length > 1) return null;
if (
isLastChild &&
!isChatting &&
questionGuides.length > 0 &&
i === chat.value.length - 1
) {
source = `${source}
\`\`\`${CodeClassName.questionGuide}
${JSON.stringify(questionGuides)}`;
}
return (
<Markdown
key={key}
source={source}
showAnimation={isLastChild && isChatting && i === chat.value.length - 1}
/>
);
}
if (value.type === ChatItemValueTypeEnum.tool && value.tools) {
return (
<Box key={key}>
{value.tools.map((tool) => {
const toolParams = (() => {
try {
return JSON.stringify(JSON.parse(tool.params), null, 2);
} catch (error) {
return tool.params;
}
})();
const toolResponse = (() => {
try {
return JSON.stringify(JSON.parse(tool.response), null, 2);
} catch (error) {
return tool.response;
}
})();
return (
<Box key={tool.id}>
<Accordion allowToggle>
<AccordionItem borderTop={'none'} borderBottom={'none'}>
<AccordionButton
w={'auto'}
bg={'white'}
borderRadius={'md'}
borderWidth={'1px'}
borderColor={'myGray.200'}
boxShadow={'1'}
_hover={{
bg: 'auto'
}}
>
<Avatar src={tool.toolAvatar} w={'1rem'} mr={2} />
<Box mr={1} fontSize={'sm'}>
{tool.toolName}
</Box>
{isChatting && !tool.response && (
<MyIcon name={'common/loading'} w={'14px'} />
)}
<AccordionIcon color={'myGray.600'} ml={5} />
</AccordionButton>
<AccordionPanel
py={0}
px={0}
mt={0}
borderRadius={'md'}
overflow={'hidden'}
maxH={'500px'}
overflowY={'auto'}
>
{toolParams && toolParams !== '{}' && (
<Markdown
source={`~~~json#Input
${toolParams}`}
/>
)}
{toolResponse && (
<Markdown
source={`~~~json#Response
${toolResponse}`}
/>
)}
</AccordionPanel>
</AccordionItem>
</Accordion>
</Box>
);
})}
</Box>
);
}
return null;
})}
</Flex>
);
}, [chat.dataId, chat.value, isChatting, isLastChild, questionGuides, type]);
const chatStatusMap = useMemo(() => {
if (!statusBoxData?.status) return;
return colorMap[statusBoxData.status];
}, [statusBoxData?.status]);
return (
<>
{/* control icon */}
<Flex w={'100%'} alignItems={'center'} gap={2} justifyContent={styleMap.justifyContent}>
{isChatting && type === ChatRoleEnum.AI && isLastChild ? null : (
<Box order={styleMap.order} ml={styleMap.ml}>
<ChatController {...chatControllerProps} isLastChild={isLastChild} />
</Box>
)}
<ChatAvatar src={avatar} type={type} />
{!!chatStatusMap && statusBoxData && isLastChild && (
<Flex
alignItems={'center'}
px={3}
py={'1.5px'}
borderRadius="md"
bg={chatStatusMap.bg}
fontSize={'sm'}
>
<Box
className={styles.statusAnimation}
bg={chatStatusMap.color}
w="8px"
h="8px"
borderRadius={'50%'}
mt={'1px'}
/>
<Box ml={2} color={'myGray.600'}>
{statusBoxData.name}
</Box>
</Flex>
)}
</Flex>
{/* content */}
<Box mt={['6px', 2]} textAlign={styleMap.textAlign}>
<Card
className="markdown"
{...MessageCardStyle}
bg={styleMap.bg}
borderRadius={styleMap.borderRadius}
textAlign={'left'}
>
{ContentCard}
{children}
</Card>
</Box>
</>
);
};
export default React.memo(ChatItem);

View File

@@ -1,325 +0,0 @@
import React, { useMemo, useState } from 'react';
import { Box, useTheme, Flex, Image, BoxProps } from '@chakra-ui/react';
import type { ChatHistoryItemResType } from '@fastgpt/global/core/chat/type.d';
import { useTranslation } from 'next-i18next';
import { moduleTemplatesFlat } from '@fastgpt/global/core/workflow/template/constants';
import LightRowTabs from '@fastgpt/web/components/common/Tabs/LightRowTabs';
import MyModal from '@fastgpt/web/components/common/MyModal';
import Markdown from '../../Markdown';
import { QuoteList } from './QuoteModal';
import { DatasetSearchModeMap } from '@fastgpt/global/core/dataset/constants';
import { formatNumber } from '@fastgpt/global/common/math/tools';
import { useI18n } from '@/web/context/I18n';
import QuestionTip from '@fastgpt/web/components/common/MyTooltip/QuestionTip';
function RowRender({
children,
mb,
label,
...props
}: { children: React.ReactNode; label: string } & BoxProps) {
return (
<Box mb={3}>
<Box fontSize={'sm'} mb={mb} flex={'0 0 90px'}>
{label}:
</Box>
<Box borderRadius={'sm'} fontSize={['xs', 'sm']} bg={'myGray.50'} {...props}>
{children}
</Box>
</Box>
);
}
function Row({
label,
value,
rawDom
}: {
label: string;
value?: string | number | boolean | object;
rawDom?: React.ReactNode;
}) {
const theme = useTheme();
const val = value || rawDom;
const isObject = typeof value === 'object';
const formatValue = useMemo(() => {
if (isObject) {
return `~~~json\n${JSON.stringify(value, null, 2)}`;
}
return `${value}`;
}, [isObject, value]);
if (rawDom) {
return (
<RowRender label={label} mb={1}>
{rawDom}
</RowRender>
);
}
if (val === undefined || val === '' || val === 'undefined') return null;
return (
<RowRender
label={label}
mb={isObject ? 0 : 1}
{...(isObject
? { transform: 'translateY(-3px)' }
: value
? { px: 3, py: 2, border: theme.borders.base }
: {})}
>
<Markdown source={formatValue} />
</RowRender>
);
}
const WholeResponseModal = ({
response,
showDetail,
onClose
}: {
response: ChatHistoryItemResType[];
showDetail: boolean;
onClose: () => void;
}) => {
const { t } = useTranslation();
return (
<MyModal
isCentered
isOpen={true}
onClose={onClose}
h={['90vh', '80vh']}
minW={['90vw', '600px']}
iconSrc="/imgs/modal/wholeRecord.svg"
title={
<Flex alignItems={'center'}>
{t('core.chat.response.Complete Response')}
<QuestionTip ml={2} label={'从左往右,为各个模块的响应顺序'}></QuestionTip>
</Flex>
}
>
<Flex h={'100%'} flexDirection={'column'}>
<ResponseBox response={response} showDetail={showDetail} />
</Flex>
</MyModal>
);
};
export default WholeResponseModal;
export const ResponseBox = React.memo(function ResponseBox({
response,
showDetail,
hideTabs = false
}: {
response: ChatHistoryItemResType[];
showDetail: boolean;
hideTabs?: boolean;
}) {
const theme = useTheme();
const { t } = useTranslation();
const { workflowT } = useI18n();
const list = useMemo(
() =>
response.map((item, i) => ({
label: (
<Flex alignItems={'center'} justifyContent={'center'} px={2}>
<Image
mr={2}
src={
item.moduleLogo ||
moduleTemplatesFlat.find((template) => item.moduleType === template.flowNodeType)
?.avatar
}
alt={''}
w={['14px', '16px']}
/>
{t(item.moduleName)}
</Flex>
),
value: `${i}`
})),
[response, t]
);
const [currentTab, setCurrentTab] = useState(`0`);
const activeModule = useMemo(() => response[Number(currentTab)], [currentTab, response]);
return (
<>
{!hideTabs && (
<Box>
<LightRowTabs list={list} value={currentTab} onChange={setCurrentTab} />
</Box>
)}
<Box py={2} px={4} flex={'1 0 0'} overflow={'auto'}>
<>
<Row label={t('core.chat.response.module name')} value={t(activeModule.moduleName)} />
{activeModule?.totalPoints !== undefined && (
<Row
label={t('support.wallet.usage.Total points')}
value={formatNumber(activeModule.totalPoints)}
/>
)}
<Row
label={t('core.chat.response.module time')}
value={`${activeModule?.runningTime || 0}s`}
/>
<Row label={t('core.chat.response.module model')} value={activeModule?.model} />
<Row label={t('core.chat.response.module tokens')} value={`${activeModule?.tokens}`} />
<Row
label={t('core.chat.response.Tool call tokens')}
value={`${activeModule?.toolCallTokens}`}
/>
<Row label={t('core.chat.response.module query')} value={activeModule?.query} />
<Row
label={t('core.chat.response.context total length')}
value={activeModule?.contextTotalLen}
/>
<Row label={workflowT('response.Error')} value={activeModule?.error} />
</>
{/* ai chat */}
<>
<Row
label={t('core.chat.response.module temperature')}
value={activeModule?.temperature}
/>
<Row label={t('core.chat.response.module maxToken')} value={activeModule?.maxToken} />
<Row
label={t('core.chat.response.module historyPreview')}
rawDom={
activeModule.historyPreview ? (
<Box px={3} py={2} border={theme.borders.base} borderRadius={'md'}>
{activeModule.historyPreview?.map((item, i) => (
<Box
key={i}
_notLast={{
borderBottom: '1px solid',
borderBottomColor: 'myWhite.700',
mb: 2
}}
pb={2}
>
<Box fontWeight={'bold'}>{item.obj}</Box>
<Box whiteSpace={'pre-wrap'}>{item.value}</Box>
</Box>
))}
</Box>
) : (
''
)
}
/>
</>
{/* dataset search */}
<>
{activeModule?.searchMode && (
<Row
label={t('core.dataset.search.search mode')}
// @ts-ignore
value={t(DatasetSearchModeMap[activeModule.searchMode]?.title)}
/>
)}
<Row label={t('core.chat.response.module similarity')} value={activeModule?.similarity} />
<Row label={t('core.chat.response.module limit')} value={activeModule?.limit} />
<Row
label={t('core.chat.response.search using reRank')}
value={`${activeModule?.searchUsingReRank}`}
/>
<Row
label={t('core.chat.response.Extension model')}
value={activeModule?.extensionModel}
/>
<Row
label={t('support.wallet.usage.Extension result')}
value={`${activeModule?.extensionResult}`}
/>
{activeModule.quoteList && activeModule.quoteList.length > 0 && (
<Row
label={t('core.chat.response.module quoteList')}
rawDom={<QuoteList showDetail={showDetail} rawSearch={activeModule.quoteList} />}
/>
)}
</>
{/* classify question */}
<>
<Row label={t('core.chat.response.module cq result')} value={activeModule?.cqResult} />
<Row
label={t('core.chat.response.module cq')}
value={(() => {
if (!activeModule?.cqList) return '';
return activeModule.cqList.map((item) => `* ${item.value}`).join('\n');
})()}
/>
</>
{/* if-else */}
<>
<Row
label={t('core.chat.response.module if else Result')}
value={activeModule?.ifElseResult}
/>
</>
{/* extract */}
<>
<Row
label={t('core.chat.response.module extract description')}
value={activeModule?.extractDescription}
/>
<Row
label={t('core.chat.response.module extract result')}
value={activeModule?.extractResult}
/>
</>
{/* http */}
<>
<Row label={'Headers'} value={activeModule?.headers} />
<Row label={'Params'} value={activeModule?.params} />
<Row label={'Body'} value={activeModule?.body} />
<Row
label={t('core.chat.response.module http result')}
value={activeModule?.httpResult}
/>
</>
{/* plugin */}
<>
<Row label={t('core.chat.response.plugin output')} value={activeModule?.pluginOutput} />
{activeModule?.pluginDetail && activeModule?.pluginDetail.length > 0 && (
<Row
label={t('core.chat.response.Plugin response detail')}
rawDom={<ResponseBox response={activeModule.pluginDetail} showDetail={showDetail} />}
/>
)}
</>
{/* text output */}
<Row label={t('core.chat.response.text output')} value={activeModule?.textOutput} />
{/* tool call */}
{activeModule?.toolDetail && activeModule?.toolDetail.length > 0 && (
<Row
label={t('core.chat.response.Tool call response detail')}
rawDom={<ResponseBox response={activeModule.toolDetail} showDetail={showDetail} />}
/>
)}
{/* code */}
<Row label={workflowT('response.Custom outputs')} value={activeModule?.customOutputs} />
<Row label={workflowT('response.Custom inputs')} value={activeModule?.customInputs} />
<Row label={workflowT('response.Code log')} value={activeModule?.codeLog} />
</Box>
</>
);
});

View File

@@ -10,6 +10,7 @@ import { getUnreadCount } from '@/web/support/user/inform/api';
import dynamic from 'next/dynamic';
import Auth from './auth';
import { useSystem } from '@fastgpt/web/hooks/useSystem';
const Navbar = dynamic(() => import('./navbar'));
const NavbarPhone = dynamic(() => import('./navbarPhone'));
const UpdateInviteModal = dynamic(() => import('@/components/support/user/team/UpdateInviteModal'));
@@ -43,7 +44,8 @@ const phoneUnShowLayoutRoute: Record<string, boolean> = {
const Layout = ({ children }: { children: JSX.Element }) => {
const router = useRouter();
const { Loading } = useLoading();
const { loading, setScreenWidth, isPc, feConfigs, isNotSufficientModal } = useSystemStore();
const { loading, feConfigs, isNotSufficientModal } = useSystemStore();
const { isPc } = useSystem();
const { userInfo } = useUserStore();
const isChatPage = useMemo(
@@ -51,21 +53,6 @@ const Layout = ({ children }: { children: JSX.Element }) => {
[router.pathname, router.query]
);
// listen screen width
useEffect(() => {
const resize = throttle(() => {
setScreenWidth(document.documentElement.clientWidth);
}, 300);
window.addEventListener('resize', resize);
resize();
return () => {
window.removeEventListener('resize', resize);
};
}, [setScreenWidth]);
const { data, refetch: refetchUnRead } = useQuery(['getUnreadCount'], getUnreadCount, {
enabled: !!userInfo && !!feConfigs.isPlus,
refetchInterval: 10000

View File

@@ -32,20 +32,22 @@ export const useFolderDrag = ({
e.preventDefault();
setTargetId(undefined);
},
onDrop: async (e: DragEvent<HTMLDivElement>) => {
e.preventDefault();
setTrue();
...(isFolder && {
onDrop: async (e: DragEvent<HTMLDivElement>) => {
e.preventDefault();
setTrue();
try {
if (targetId && dragId && targetId !== dragId) {
await onDrop(dragId, targetId);
}
} catch (error) {}
try {
if (targetId && dragId && targetId !== dragId) {
await onDrop(dragId, targetId);
}
} catch (error) {}
setTargetId(undefined);
setDragId(undefined);
setFalse();
},
setTargetId(undefined);
setDragId(undefined);
setFalse();
}
}),
...(activeStyles &&
targetId === dataId && {
...activeStyles

View File

@@ -18,6 +18,7 @@ import { ChatBoxContext } from '../Provider';
import dynamic from 'next/dynamic';
import { useContextSelector } from 'use-context-selector';
import { getNanoid } from '@fastgpt/global/common/string/tools';
import { useSystem } from '@fastgpt/web/hooks/useSystem';
const InputGuideBox = dynamic(() => import('./InputGuideBox'));
@@ -53,7 +54,9 @@ const ChatInput = ({
const { isChatting, whisperConfig, autoTTSResponse, chatInputGuide, outLinkAuthData } =
useContextSelector(ChatBoxContext, (v) => v);
const { isPc, whisperModel } = useSystemStore();
const { whisperModel } = useSystemStore();
const { isPc } = useSystem();
const canvasRef = useRef<HTMLCanvasElement>(null);
const { t } = useTranslation();

View File

@@ -15,38 +15,53 @@ import {
defaultWhisperConfig
} from '@fastgpt/global/core/app/constants';
import { createContext } from 'use-context-selector';
import { FieldValues, UseFormReturn } from 'react-hook-form';
import { VariableInputEnum } from '@fastgpt/global/core/workflow/constants';
export type ChatProviderProps = OutLinkChatAuthProps & {
appAvatar?: string;
chatConfig?: AppChatConfigType;
type useChatStoreType = OutLinkChatAuthProps & {
welcomeText: string;
variableList: 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>>;
chatHistories: ChatSiteItemType[];
setChatHistories: React.Dispatch<React.SetStateAction<ChatSiteItemType[]>>;
isChatting: boolean;
chatInputGuide: ChatInputGuideConfigType;
outLinkAuthData: OutLinkChatAuthProps;
variablesForm: UseFormReturn<FieldValues, any>;
// not chat test params
chatId?: string;
};
type useChatStoreType = OutLinkChatAuthProps &
ChatProviderProps & {
welcomeText: string;
variableList: 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;
};
export const ChatBoxContext = createContext<useChatStoreType>({
welcomeText: '',
variableList: [],
@@ -100,27 +115,27 @@ export const ChatBoxContext = createContext<useChatStoreType>({
open: false,
customUrl: ''
},
outLinkAuthData: {}
outLinkAuthData: {},
// @ts-ignore
variablesForm: undefined
});
export type ChatProviderProps = OutLinkChatAuthProps & {
chatConfig?: AppChatConfigType;
// not chat test params
chatId?: string;
children: React.ReactNode;
};
const Provider = ({
shareId,
outLinkUid,
teamId,
teamToken,
chatConfig = {},
children
}: ChatProviderProps) => {
const [chatHistories, setChatHistories] = useState<ChatSiteItemType[]>([]);
chatHistories,
setChatHistories,
variablesForm,
chatConfig = {},
children,
...props
}: ChatProviderProps & {
children: React.ReactNode;
}) => {
const {
welcomeText = '',
variables = [],
@@ -167,12 +182,13 @@ const Provider = ({
);
const value: useChatStoreType = {
...props,
shareId,
outLinkUid,
teamId,
teamToken,
welcomeText,
variableList: variables,
variableList: variables.filter((item) => item.type !== VariableInputEnum.custom),
questionGuide,
ttsConfig,
whisperConfig,
@@ -191,7 +207,8 @@ const Provider = ({
setChatHistories,
isChatting,
chatInputGuide,
outLinkAuthData
outLinkAuthData,
variablesForm
};
return <ChatBoxContext.Provider value={value}>{children}</ChatBoxContext.Provider>;

View File

@@ -0,0 +1,158 @@
import { Box, BoxProps, Card, Flex } from '@chakra-ui/react';
import React, { useMemo } from 'react';
import ChatController, { type ChatControllerProps } from './ChatController';
import ChatAvatar from './ChatAvatar';
import { MessageCardStyle } from '../constants';
import { formatChatValue2InputType } from '../utils';
import Markdown from '@/components/Markdown';
import styles from '../index.module.scss';
import { ChatRoleEnum, ChatStatusEnum } from '@fastgpt/global/core/chat/constants';
import FilesBlock from './FilesBox';
import { ChatBoxContext } from '../Provider';
import { useContextSelector } from 'use-context-selector';
import AIResponseBox from '../../../components/AIResponseBox';
const colorMap = {
[ChatStatusEnum.loading]: {
bg: 'myGray.100',
color: 'myGray.600'
},
[ChatStatusEnum.running]: {
bg: 'green.50',
color: 'green.700'
},
[ChatStatusEnum.finish]: {
bg: 'green.50',
color: 'green.700'
}
};
const ChatItem = ({
type,
avatar,
statusBoxData,
children,
isLastChild,
questionGuides = [],
...chatControllerProps
}: {
type: ChatRoleEnum.Human | ChatRoleEnum.AI;
avatar?: string;
statusBoxData?: {
status: `${ChatStatusEnum}`;
name: string;
};
questionGuides?: string[];
children?: React.ReactNode;
} & ChatControllerProps) => {
const styleMap: BoxProps =
type === ChatRoleEnum.Human
? {
order: 0,
borderRadius: '8px 0 8px 8px',
justifyContent: 'flex-end',
textAlign: 'right',
bg: 'primary.100'
}
: {
order: 1,
borderRadius: '0 8px 8px 8px',
justifyContent: 'flex-start',
textAlign: 'left',
bg: 'myGray.50'
};
const isChatting = useContextSelector(ChatBoxContext, (v) => v.isChatting);
const { chat } = chatControllerProps;
const ContentCard = useMemo(() => {
if (type === 'Human') {
const { text, files = [] } = formatChatValue2InputType(chat.value);
return (
<>
{files.length > 0 && <FilesBlock files={files} />}
<Markdown source={text} />
</>
);
}
/* AI */
return (
<Flex flexDirection={'column'} key={chat.dataId} gap={2}>
{chat.value.map((value, i) => {
const key = `${chat.dataId}-ai-${i}`;
return (
<AIResponseBox
key={key}
value={value}
index={i}
chat={chat}
isLastChild={isLastChild}
isChatting={isChatting}
questionGuides={questionGuides}
/>
);
})}
</Flex>
);
}, [chat, isChatting, isLastChild, questionGuides, type]);
const chatStatusMap = useMemo(() => {
if (!statusBoxData?.status) return;
return colorMap[statusBoxData.status];
}, [statusBoxData?.status]);
return (
<>
{/* control icon */}
<Flex w={'100%'} alignItems={'center'} gap={2} justifyContent={styleMap.justifyContent}>
{isChatting && type === ChatRoleEnum.AI && isLastChild ? null : (
<Box order={styleMap.order} ml={styleMap.ml}>
<ChatController {...chatControllerProps} isLastChild={isLastChild} />
</Box>
)}
<ChatAvatar src={avatar} type={type} />
{!!chatStatusMap && statusBoxData && isLastChild && (
<Flex
alignItems={'center'}
px={3}
py={'1.5px'}
borderRadius="md"
bg={chatStatusMap.bg}
fontSize={'sm'}
>
<Box
className={styles.statusAnimation}
bg={chatStatusMap.color}
w="8px"
h="8px"
borderRadius={'50%'}
mt={'1px'}
/>
<Box ml={2} color={'myGray.600'}>
{statusBoxData.name}
</Box>
</Flex>
)}
</Flex>
{/* content */}
<Box mt={['6px', 2]} textAlign={styleMap.textAlign}>
<Card
className="markdown"
{...MessageCardStyle}
bg={styleMap.bg}
borderRadius={styleMap.borderRadius}
textAlign={'left'}
>
{ContentCard}
{children}
</Card>
</Box>
</>
);
};
export default React.memo(ChatItem);

View File

@@ -1,6 +1,6 @@
import { Box, Flex, Grid } from '@chakra-ui/react';
import MdImage from '@/components/Markdown/img/Image';
import { UserInputFileItemType } from '@/components/ChatBox/type';
import { UserInputFileItemType } from '@/components/core/chat/ChatContainer/ChatBox/type';
const FilesBlock = ({ files }: { files: UserInputFileItemType[] }) => {
return (

View File

@@ -4,8 +4,8 @@ import { ModalBody, Box, useTheme } from '@chakra-ui/react';
import MyModal from '@fastgpt/web/components/common/MyModal';
import { useTranslation } from 'next-i18next';
import type { SearchDataResponseItemType } from '@fastgpt/global/core/dataset/type';
import QuoteItem from '../../core/dataset/QuoteItem';
import RawSourceBox from '../../core/dataset/RawSourceBox';
import QuoteItem from '@/components/core/dataset/QuoteItem';
import RawSourceBox from '@/components/core/dataset/RawSourceBox';
const QuoteModal = ({
rawSearch = [],

View File

@@ -3,7 +3,6 @@ import { type ChatHistoryItemResType } from '@fastgpt/global/core/chat/type.d';
import { DispatchNodeResponseType } from '@fastgpt/global/core/workflow/runtime/type.d';
import { Flex, useDisclosure, useTheme, Box } from '@chakra-ui/react';
import { useTranslation } from 'next-i18next';
import { useSystemStore } from '@/web/common/system/useSystemStore';
import type { SearchDataResponseItemType } from '@fastgpt/global/core/dataset/type';
import dynamic from 'next/dynamic';
import MyTag from '@fastgpt/web/components/common/Tag/index';
@@ -13,10 +12,11 @@ import { getSourceNameIcon } from '@fastgpt/global/core/dataset/utils';
import ChatBoxDivider from '@/components/core/chat/Divider';
import { strIsLink } from '@fastgpt/global/common/string/tools';
import MyIcon from '@fastgpt/web/components/common/Icon';
import { useSystem } from '@fastgpt/web/hooks/useSystem';
const QuoteModal = dynamic(() => import('./QuoteModal'));
const ContextModal = dynamic(() => import('./ContextModal'));
const WholeResponseModal = dynamic(() => import('./WholeResponseModal'));
const WholeResponseModal = dynamic(() => import('../../../components/WholeResponseModal'));
const isLLMNode = (item: ChatHistoryItemResType) =>
item.moduleType === FlowNodeTypeEnum.chatNode || item.moduleType === FlowNodeTypeEnum.tools;
@@ -29,7 +29,7 @@ const ResponseTags = ({
showDetail: boolean;
}) => {
const theme = useTheme();
const { isPc } = useSystemStore();
const { isPc } = useSystem();
const { t } = useTranslation();
const [quoteModalData, setQuoteModalData] = useState<{
rawSearch: SearchDataResponseItemType[];

View File

@@ -1,32 +1,27 @@
import { VariableItemType } from '@fastgpt/global/core/app/type.d';
import React from 'react';
import { UseFormReturn } from 'react-hook-form';
import { Controller, UseFormReturn } from 'react-hook-form';
import { useTranslation } from 'next-i18next';
import { Box, Button, Card, Input, Textarea } from '@chakra-ui/react';
import { Box, Button, Card, FormControl, Input, Textarea } from '@chakra-ui/react';
import ChatAvatar from './ChatAvatar';
import { MessageCardStyle } from '../constants';
import { VariableInputEnum } from '@fastgpt/global/core/workflow/constants';
import MySelect from '@fastgpt/web/components/common/MySelect';
import MyIcon from '@fastgpt/web/components/common/Icon';
import { ChatBoxInputFormType } from '../type.d';
import { useRefresh } from '@fastgpt/web/hooks/useRefresh';
import { useContextSelector } from 'use-context-selector';
import { ChatBoxContext } from '../Provider';
const VariableInput = ({
appAvatar,
variableList,
chatForm,
onSubmitVariables
chatStarted
}: {
appAvatar?: string;
variableList: VariableItemType[];
onSubmitVariables: (e: Record<string, any>) => void;
chatStarted: boolean;
chatForm: UseFormReturn<ChatBoxInputFormType>;
}) => {
const { t } = useTranslation();
const { register, setValue, handleSubmit: handleSubmitChat, watch } = chatForm;
const variables = watch('variables');
const chatStarted = watch('chatStarted');
const { refresh } = useRefresh();
const { appAvatar, variableList, variablesForm } = useContextSelector(ChatBoxContext, (v) => v);
const { register, getValues, setValue, handleSubmit: handleSubmitChat, control } = variablesForm;
return (
<Box py={3}>
@@ -61,7 +56,7 @@ const VariableInput = ({
{item.type === VariableInputEnum.input && (
<Input
bg={'myWhite.400'}
{...register(`variables.${item.key}`, {
{...register(item.key, {
required: item.required
})}
/>
@@ -69,7 +64,7 @@ const VariableInput = ({
{item.type === VariableInputEnum.textarea && (
<Textarea
bg={'myWhite.400'}
{...register(`variables.${item.key}`, {
{...register(item.key, {
required: item.required
})}
rows={5}
@@ -77,19 +72,24 @@ const VariableInput = ({
/>
)}
{item.type === VariableInputEnum.select && (
<MySelect
width={'100%'}
list={(item.enums || []).map((item) => ({
label: item.value,
value: item.value
}))}
{...register(`variables.${item.key}`, {
required: item.required
})}
value={variables[item.key]}
onchange={(e) => {
refresh();
setValue(`variables.${item.key}`, e);
<Controller
key={item.key}
control={control}
name={item.key}
rules={{ required: item.required }}
render={({ field: { ref, value } }) => {
return (
<MySelect
ref={ref}
width={'100%'}
list={(item.enums || []).map((item) => ({
label: item.value,
value: item.value
}))}
value={value}
onchange={(e) => setValue(item.key, e)}
/>
);
}}
/>
)}
@@ -100,8 +100,8 @@ const VariableInput = ({
leftIcon={<MyIcon name={'core/chat/chatFill'} w={'16px'} />}
size={'sm'}
maxW={'100px'}
onClick={handleSubmitChat((data) => {
onSubmitVariables(data);
onClick={handleSubmitChat(() => {
chatForm.setValue('chatStarted', true);
})}
>
{t('core.chat.Start Chat')}

View File

@@ -3,8 +3,12 @@ import React from 'react';
import { MessageCardStyle } from '../constants';
import Markdown from '@/components/Markdown';
import ChatAvatar from './ChatAvatar';
import { useContextSelector } from 'use-context-selector';
import { ChatBoxContext } from '../Provider';
const WelcomeBox = ({ welcomeText }: { welcomeText: string }) => {
const appAvatar = useContextSelector(ChatBoxContext, (v) => v.appAvatar);
const WelcomeBox = ({ appAvatar, welcomeText }: { appAvatar?: string; welcomeText: string }) => {
return (
<Box py={3}>
{/* avatar */}

View File

@@ -9,7 +9,6 @@ import React, {
useEffect
} from 'react';
import Script from 'next/script';
import { throttle } from 'lodash';
import type {
AIChatItemValueItemType,
ChatSiteItemType,
@@ -20,7 +19,6 @@ import { getErrText } from '@fastgpt/global/common/error/utils';
import { Box, Flex, Checkbox } from '@chakra-ui/react';
import { EventNameEnum, eventBus } from '@/web/common/utils/eventbus';
import { chats2GPTMessages } from '@fastgpt/global/core/chat/adapt';
import { VariableInputEnum } from '@fastgpt/global/core/workflow/constants';
import { useForm } from 'react-hook-form';
import { useRouter } from 'next/router';
import { useSystemStore } from '@/web/common/system/useSystemStore';
@@ -35,30 +33,25 @@ import type { AdminMarkType } from './components/SelectMarkCollection';
import MyTooltip from '@fastgpt/web/components/common/MyTooltip';
import { postQuestionGuide } from '@/web/core/ai/api';
import type {
generatingMessageProps,
StartChatFnProps,
ComponentRef,
ChatBoxInputType,
ChatBoxInputFormType
} from './type.d';
import type { ComponentRef, ChatBoxInputType, ChatBoxInputFormType } from './type.d';
import type { StartChatFnProps, generatingMessageProps } from '../type';
import ChatInput from './Input/ChatInput';
import ChatBoxDivider from '../core/chat/Divider';
import ChatBoxDivider from '../../Divider';
import { OutLinkChatAuthProps } from '@fastgpt/global/support/permission/chat';
import { getNanoid } from '@fastgpt/global/common/string/tools';
import { ChatItemValueTypeEnum, ChatRoleEnum } from '@fastgpt/global/core/chat/constants';
import { formatChatValue2InputType } from './utils';
import { textareaMinH } from './constants';
import { SseResponseEventEnum } from '@fastgpt/global/core/workflow/runtime/constants';
import ChatProvider, { ChatBoxContext } from './Provider';
import ChatProvider, { ChatBoxContext, ChatProviderProps } from './Provider';
import ChatItem from './components/ChatItem';
import dynamic from 'next/dynamic';
import { useCreation } from 'ahooks';
import { AppChatConfigType } from '@fastgpt/global/core/app/type';
import type { StreamResponseType } from '@/web/common/api/fetch';
import { useContextSelector } from 'use-context-selector';
import { useSystem } from '@fastgpt/web/hooks/useSystem';
import { useThrottleFn } from 'ahooks';
const ResponseTags = dynamic(() => import('./components/ResponseTags'));
const FeedbackModal = dynamic(() => import('./components/FeedbackModal'));
@@ -74,29 +67,26 @@ enum FeedbackTypeEnum {
hidden = 'hidden'
}
type Props = OutLinkChatAuthProps & {
feedbackType?: `${FeedbackTypeEnum}`;
showMarkIcon?: boolean; // admin mark dataset
showVoiceIcon?: boolean;
showEmptyIntro?: boolean;
appAvatar?: string;
userAvatar?: string;
chatConfig?: AppChatConfigType;
showFileSelector?: boolean;
active?: boolean; // can use
appId: string;
type Props = OutLinkChatAuthProps &
ChatProviderProps & {
feedbackType?: `${FeedbackTypeEnum}`;
showMarkIcon?: boolean; // admin mark dataset
showVoiceIcon?: boolean;
showEmptyIntro?: boolean;
userAvatar?: string;
showFileSelector?: boolean;
active?: boolean; // can use
appId: string;
// not chat test params
chatId?: string;
// not chat test params
onUpdateVariable?: (e: Record<string, any>) => void;
onStartChat?: (e: StartChatFnProps) => Promise<
StreamResponseType & {
isNewChat?: boolean;
}
>;
onDelMessage?: (e: { contentId: string }) => void;
};
onStartChat?: (e: StartChatFnProps) => Promise<
StreamResponseType & {
isNewChat?: boolean;
}
>;
onDelMessage?: (e: { contentId: string }) => void;
};
/*
The input is divided into sections
@@ -122,7 +112,6 @@ const ChatBox = (
outLinkUid,
teamId,
teamToken,
onUpdateVariable,
onStartChat,
onDelMessage
}: Props,
@@ -132,10 +121,12 @@ const ChatBox = (
const router = useRouter();
const { t } = useTranslation();
const { toast } = useToast();
const { isPc, setLoading, feConfigs } = useSystemStore();
const { setLoading, 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 [feedbackId, setFeedbackId] = useState<string>();
@@ -156,6 +147,7 @@ const ChatBox = (
splitText2Audio,
chatHistories,
setChatHistories,
variablesForm,
isChatting
} = useContextSelector(ChatBoxContext, (v) => v);
@@ -164,41 +156,44 @@ const ChatBox = (
defaultValues: {
input: '',
files: [],
variables: {},
chatStarted: false
}
});
const { setValue, watch, handleSubmit } = chatForm;
const chatStarted = watch('chatStarted');
/* variable */
const filterVariableNodes = useCreation(
() => variableList.filter((item) => item.type !== VariableInputEnum.custom),
[variableList]
);
const { setValue, watch } = chatForm;
const chatStartedWatch = watch('chatStarted');
const chatStarted = chatStartedWatch || chatHistories.length > 0 || variableList.length === 0;
// 滚动到底部
const scrollToBottom = (behavior: 'smooth' | 'auto' = 'smooth') => {
if (!ChatBoxRef.current) return;
ChatBoxRef.current.scrollTo({
top: ChatBoxRef.current.scrollHeight,
behavior
});
};
const scrollToBottom = useCallback((behavior: 'smooth' | 'auto' = 'smooth', delay = 0) => {
setTimeout(() => {
if (!ChatBoxRef.current) {
setTimeout(() => {
scrollToBottom(behavior);
}, 500);
} else {
ChatBoxRef.current.scrollTo({
top: ChatBoxRef.current.scrollHeight,
behavior
});
}
}, delay);
}, []);
// 聊天信息生成中……获取当前滚动条位置,判断是否需要滚动到底部
const generatingScroll = useCallback(
throttle(() => {
const { run: generatingScroll } = useThrottleFn(
() => {
if (!ChatBoxRef.current) return;
const isBottom =
ChatBoxRef.current.scrollTop + ChatBoxRef.current.clientHeight + 150 >=
ChatBoxRef.current.scrollHeight;
isBottom && scrollToBottom('auto');
}, 100),
[]
},
{
wait: 100
}
);
// eslint-disable-next-line react-hooks/exhaustive-deps
const generatingMessage = useCallback(
({
event,
@@ -291,7 +286,7 @@ const ChatBox = (
})
};
} else if (event === SseResponseEventEnum.updateVariables && variables) {
setValue('variables', variables);
variablesForm.reset(variables);
}
return item;
@@ -299,7 +294,7 @@ const ChatBox = (
);
generatingScroll();
},
[generatingScroll, setChatHistories, setValue, splitText2Audio]
[generatingScroll, setChatHistories, splitText2Audio, variablesForm]
);
// 重置输入内容
@@ -347,13 +342,14 @@ const ChatBox = (
}
} catch (error) {}
},
[questionGuide, shareId, outLinkUid, teamId, teamToken]
[questionGuide, shareId, outLinkUid, teamId, teamToken, scrollToBottom]
);
/* Abort chat completions, questionGuide */
const abortRequest = useCallback(() => {
chatController.current?.abort('stop');
questionGuideController.current?.abort('stop');
pluginController.current?.abort('stop');
}, []);
/**
@@ -369,8 +365,8 @@ const ChatBox = (
autoTTSResponse?: boolean;
history?: ChatSiteItemType[];
}) => {
handleSubmit(
async ({ variables }) => {
variablesForm.handleSubmit(
async (variables) => {
if (!onStartChat) return;
if (isChatting) {
toast({
@@ -455,9 +451,7 @@ const ChatBox = (
// 清空输入内容
resetInputVal({});
setQuestionGuide([]);
setTimeout(() => {
scrollToBottom();
}, 100);
scrollToBottom('smooth', 100);
try {
// create abort obj
const abortSignal = new AbortController();
@@ -470,8 +464,8 @@ const ChatBox = (
responseText,
isNewChat = false
} = await onStartChat({
chatList: newChatList,
messages,
messages: messages.slice(0, -1),
responseChatItemId: responseChatId,
controller: abortSignal,
generatingMessage: (e) => generatingMessage({ ...e, autoTTSResponse }),
variables: requestVariables
@@ -542,7 +536,7 @@ const ChatBox = (
autoTTSResponse && finishSegmentedAudio();
},
(err) => {
console.log(err?.variables);
console.log(err);
}
)();
},
@@ -553,18 +547,19 @@ const ChatBox = (
finishSegmentedAudio,
generatingMessage,
generatingScroll,
handleSubmit,
isChatting,
isPc,
onStartChat,
resetInputVal,
scrollToBottom,
setAudioPlayingChatId,
setChatHistories,
splitText2Audio,
startSegmentedAudio,
t,
toast,
variableList
variableList,
variablesForm
]
);
@@ -720,7 +715,7 @@ const ChatBox = (
},
[appId, chatId, feedbackType, setChatHistories, teamId, teamToken]
);
const onADdUserDislike = useCallback(
const onAddUserDislike = useCallback(
(chat: ChatSiteItemType) => {
if (
feedbackType !== FeedbackTypeEnum.user ||
@@ -797,30 +792,18 @@ const ChatBox = (
[appId, chatId, setChatHistories]
);
const resetVariables = useCallback(
(e: Record<string, any> = {}) => {
const value: Record<string, any> = { ...e };
filterVariableNodes?.forEach((item) => {
value[item.key] = e[item.key] || '';
});
setValue('variables', value);
},
[filterVariableNodes, setValue]
);
const showEmpty = useMemo(
() =>
feConfigs?.show_emptyChat &&
showEmptyIntro &&
chatHistories.length === 0 &&
!filterVariableNodes?.length &&
!variableList?.length &&
!welcomeText,
[
chatHistories.length,
feConfigs?.show_emptyChat,
showEmptyIntro,
filterVariableNodes?.length,
variableList?.length,
welcomeText
]
);
@@ -878,12 +861,10 @@ const ChatBox = (
// output data
useImperativeHandle(ref, () => ({
getChatHistories: () => chatHistories,
resetVariables,
resetHistory(e) {
restartChat() {
abortRequest();
setValue('chatStarted', e.length > 0);
setChatHistories(e);
setValue('chatStarted', false);
scrollToBottom('smooth', 500);
},
scrollToBottom,
sendPrompt: (question: string) => {
@@ -900,41 +881,33 @@ const ChatBox = (
<Box ref={ChatBoxRef} flex={'1 0 0'} h={0} w={'100%'} overflow={'overlay'} px={[4, 0]} pb={3}>
<Box id="chat-container" maxW={['100%', '92%']} h={'100%'} mx={'auto'}>
{showEmpty && <Empty />}
{!!welcomeText && <WelcomeBox appAvatar={appAvatar} welcomeText={welcomeText} />}
{!!welcomeText && <WelcomeBox welcomeText={welcomeText} />}
{/* variable input */}
{!!filterVariableNodes?.length && (
<VariableInput
appAvatar={appAvatar}
variableList={filterVariableNodes}
chatForm={chatForm}
onSubmitVariables={(data) => {
setValue('chatStarted', true);
onUpdateVariable?.(data);
}}
/>
{!!variableList?.length && (
<VariableInput chatStarted={chatStarted} chatForm={chatForm} />
)}
{/* chat history */}
<Box id={'history'}>
{chatHistories.map((item, index) => (
<Box key={item.dataId} py={5}>
{item.obj === 'Human' && (
{item.obj === ChatRoleEnum.Human && (
<ChatItem
type={item.obj}
avatar={item.obj === 'Human' ? userAvatar : appAvatar}
avatar={userAvatar}
chat={item}
onRetry={retryInput(item.dataId)}
onDelete={delOneMessage(item.dataId)}
isLastChild={index === chatHistories.length - 1}
/>
)}
{item.obj === 'AI' && (
{item.obj === ChatRoleEnum.AI && (
<>
<ChatItem
type={item.obj}
avatar={appAvatar}
chat={item}
isLastChild={index === chatHistories.length - 1}
{...(item.obj === 'AI' && {
{...(item.obj === ChatRoleEnum.AI && {
showVoiceIcon,
shareId,
outLinkUid,
@@ -948,7 +921,7 @@ const ChatBox = (
),
onAddUserLike: onAddUserLike(item),
onCloseUserLike: onCloseUserLike(item),
onAddUserDislike: onADdUserDislike(item),
onAddUserDislike: onAddUserDislike(item),
onReadUserDislike: onReadUserDislike(item)
})}
>
@@ -997,7 +970,7 @@ const ChatBox = (
</Box>
</Box>
{/* message input */}
{onStartChat && (chatStarted || filterVariableNodes.length === 0) && active && appId && (
{onStartChat && chatStarted && active && appId && (
<ChatInput
onSendMessage={sendPrompt}
onStop={() => chatController.current?.abort('stop')}

View File

@@ -7,15 +7,6 @@ import {
} from '@fastgpt/global/core/chat/type';
import { SseResponseEventEnum } from '@fastgpt/global/core/workflow/runtime/constants';
export type generatingMessageProps = {
event: SseResponseEventEnum;
text?: string;
name?: string;
status?: 'running' | 'finish';
tool?: ToolModuleResponseItemType;
variables?: Record<string, any>;
};
export type UserInputFileItemType = {
id: string;
rawFile?: File;
@@ -28,7 +19,6 @@ export type UserInputFileItemType = {
export type ChatBoxInputFormType = {
input: string;
files: UserInputFileItemType[];
variables: Record<string, any>;
chatStarted: boolean;
};
@@ -37,18 +27,8 @@ export type ChatBoxInputType = {
files?: UserInputFileItemType[];
};
export type StartChatFnProps = {
chatList: ChatSiteItemType[];
messages: ChatCompletionMessageParam[];
controller: AbortController;
variables: Record<string, any>;
generatingMessage: (e: generatingMessageProps) => void;
};
export type ComponentRef = {
getChatHistories: () => ChatSiteItemType[];
resetVariables: (data?: Record<string, any>) => void;
resetHistory: (history: ChatSiteItemType[]) => void;
restartChat: () => void;
scrollToBottom: (behavior?: 'smooth' | 'auto') => void;
sendPrompt: (question: string) => void;
};

View File

@@ -2,7 +2,11 @@ import { ChatItemValueItemType } from '@fastgpt/global/core/chat/type';
import { ChatBoxInputType, UserInputFileItemType } from './type';
import { getNanoid } from '@fastgpt/global/common/string/tools';
export const formatChatValue2InputType = (value: ChatItemValueItemType[]): ChatBoxInputType => {
export const formatChatValue2InputType = (value?: ChatItemValueItemType[]): ChatBoxInputType => {
if (!value) {
return { text: '', files: [] };
}
if (!Array.isArray(value)) {
console.error('value is error', value);
return { text: '', files: [] };

View File

@@ -0,0 +1,69 @@
import React from 'react';
import { Controller } from 'react-hook-form';
import RenderPluginInput from './renderPluginInput';
import { Button, Flex } from '@chakra-ui/react';
import { useTranslation } from 'react-i18next';
import { useContextSelector } from 'use-context-selector';
import { PluginRunContext } from '../context';
const RenderInput = () => {
const { pluginInputs, variablesForm, histories, onStartChat, onNewChat, onSubmit, isChatting } =
useContextSelector(PluginRunContext, (v) => v);
const { t } = useTranslation();
const {
control,
handleSubmit,
formState: { errors }
} = variablesForm;
const isDisabledInput = histories.length > 0;
return (
<>
{pluginInputs.map((input) => {
return (
<Controller
key={input.key}
control={control}
name={input.key}
rules={{ required: input.required }}
render={({ field: { onChange, value } }) => {
return (
<RenderPluginInput
value={value}
onChange={onChange}
label={input.label}
description={input.description}
isDisabled={isDisabledInput}
valueType={input.valueType}
placeholder={input.placeholder}
required={input.required}
min={input.min}
max={input.max}
isInvalid={errors && Object.keys(errors).includes(input.key)}
/>
);
}}
/>
);
})}
{onStartChat && onNewChat && (
<Flex justifyContent={'end'} mt={8}>
<Button
isLoading={isChatting}
onClick={() => {
if (histories.length > 0) {
return onNewChat();
}
handleSubmit(onSubmit)();
}}
>
{histories.length > 0 ? t('common.Restart') : t('common.Run')}
</Button>
</Flex>
)}
</>
);
};
export default RenderInput;

View File

@@ -0,0 +1,59 @@
import { Box } from '@chakra-ui/react';
import React, { useMemo } from 'react';
import { useContextSelector } from 'use-context-selector';
import { PluginRunContext } from '../context';
import Markdown from '@/components/Markdown';
import { FlowNodeTypeEnum } from '@fastgpt/global/core/workflow/node/constant';
import AIResponseBox from '../../../components/AIResponseBox';
const RenderOutput = () => {
const { histories, isChatting } = useContextSelector(PluginRunContext, (v) => v);
const pluginOutputs = useMemo(() => {
const pluginOutputs = histories?.[1]?.responseData?.find(
(item) => item.moduleType === FlowNodeTypeEnum.pluginOutput
)?.pluginOutput;
return JSON.stringify(pluginOutputs, null, 2);
}, [histories]);
return (
<>
<Box border={'base'} rounded={'md'} bg={'myGray.25'}>
<Box p={4} color={'myGray.900'}>
<Box color={'myGray.900'} fontWeight={'bold'}>
</Box>
{histories.length > 0 && histories[1]?.value.length > 0 ? (
<Box mt={2}>
{histories[1].value.map((value, i) => {
const key = `${histories[1].dataId}-ai-${i}`;
return (
<AIResponseBox
key={key}
value={value}
index={i}
chat={histories[1]}
isLastChild={true}
isChatting={isChatting}
questionGuides={[]}
/>
);
})}
</Box>
) : null}
</Box>
</Box>
<Box border={'base'} mt={4} rounded={'md'} bg={'myGray.25'}>
<Box p={4} color={'myGray.900'} fontWeight={'bold'}>
<Box></Box>
{histories.length > 0 && histories[1].responseData ? (
<Markdown source={`~~~json\n${pluginOutputs}`} />
) : null}
</Box>
</Box>
</>
);
};
export default RenderOutput;

View File

@@ -0,0 +1,21 @@
import { ResponseBox } from '../../../components/WholeResponseModal';
import React from 'react';
import { useContextSelector } from 'use-context-selector';
import { PluginRunContext } from '../context';
import { Box } from '@chakra-ui/react';
const RenderResponseDetail = () => {
const { histories, isChatting } = useContextSelector(PluginRunContext, (v) => v);
const responseData = histories?.[1]?.responseData || [];
return isChatting ? (
<>{'进行中'}</>
) : (
<Box flex={'1 0 0'} h={'100%'} overflow={'auto'}>
<ResponseBox response={responseData} showDetail={true} />
</Box>
);
};
export default RenderResponseDetail;

View File

@@ -0,0 +1,118 @@
import {
Box,
Flex,
NumberDecrementStepper,
NumberIncrementStepper,
NumberInput,
NumberInputField,
NumberInputStepper,
Switch,
Textarea
} from '@chakra-ui/react';
import { WorkflowIOValueTypeEnum } from '@fastgpt/global/core/workflow/constants';
import QuestionTip from '@fastgpt/web/components/common/MyTooltip/QuestionTip';
import { useTranslation } from 'next-i18next';
import dynamic from 'next/dynamic';
const JsonEditor = dynamic(() => import('@fastgpt/web/components/common/Textarea/JsonEditor'));
const RenderPluginInput = ({
value,
onChange,
label,
description,
isDisabled,
valueType,
placeholder,
required,
min,
max,
isInvalid
}: {
value: any;
onChange: () => void;
label: string;
description?: string;
isDisabled?: boolean;
valueType: WorkflowIOValueTypeEnum | undefined;
placeholder?: string;
required?: boolean;
min?: number;
max?: number;
isInvalid: boolean;
}) => {
const { t } = useTranslation();
const render = (() => {
if (valueType === WorkflowIOValueTypeEnum.string) {
return (
<Textarea
value={value}
onChange={onChange}
isDisabled={isDisabled}
placeholder={t(placeholder)}
bg={'myGray.50'}
isInvalid={isInvalid}
/>
);
}
if (valueType === WorkflowIOValueTypeEnum.number) {
return (
<NumberInput
step={1}
min={min}
max={max}
bg={'myGray.50'}
isDisabled={isDisabled}
isInvalid={isInvalid}
>
<NumberInputField value={value} onChange={onChange} />
<NumberInputStepper>
<NumberIncrementStepper />
<NumberDecrementStepper />
</NumberInputStepper>
</NumberInput>
);
}
if (valueType === WorkflowIOValueTypeEnum.boolean) {
return (
<Switch
isChecked={value}
onChange={onChange}
isDisabled={isDisabled}
isInvalid={isInvalid}
/>
);
}
return (
<JsonEditor
bg={'myGray.50'}
placeholder={t(placeholder || '')}
resize
value={value}
onChange={onChange}
isInvalid={isInvalid}
/>
);
})();
return !!render ? (
<Box _notLast={{ mb: 4 }} px={1}>
<Flex alignItems={'center'} mb={1}>
<Box position={'relative'}>
{required && (
<Box position={'absolute'} left={-2} top={'-1px'} color={'red.600'}>
*
</Box>
)}
{label}
</Box>
{description && <QuestionTip ml={2} label={description} />}
</Flex>
{render}
</Box>
) : null;
};
export default RenderPluginInput;

View File

@@ -0,0 +1,5 @@
export enum PluginRunBoxTabEnum {
input = 'input',
output = 'output',
detail = 'detail'
}

View File

@@ -0,0 +1,234 @@
import React, { ReactNode, useCallback, useMemo, useRef, useState } from 'react';
import { createContext } from 'use-context-selector';
import { PluginRunBoxProps } from './type';
import { AIChatItemValueItemType, ChatSiteItemType } from '@fastgpt/global/core/chat/type';
import { FieldValues } from 'react-hook-form';
import { PluginRunBoxTabEnum } from './constants';
import { useRequest2 } from '@fastgpt/web/hooks/useRequest';
import { useToast } from '@fastgpt/web/hooks/useToast';
import { getNanoid } from '@fastgpt/global/common/string/tools';
import { ChatItemValueTypeEnum, ChatRoleEnum } from '@fastgpt/global/core/chat/constants';
import { generatingMessageProps } from '../type';
import { SseResponseEventEnum } from '@fastgpt/global/core/workflow/runtime/constants';
import { getPluginRunContent } from '@fastgpt/global/core/app/plugin/utils';
type PluginRunContextType = PluginRunBoxProps & {
isChatting: boolean;
onSubmit: (e: FieldValues) => Promise<any>;
};
export const PluginRunContext = createContext<PluginRunContextType>({
pluginInputs: [],
//@ts-ignore
variablesForm: undefined,
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.');
}
});
const PluginRunContextProvider = ({
children,
...props
}: PluginRunBoxProps & { children: ReactNode }) => {
const { pluginInputs, onStartChat, setHistories, histories, setTab } = props;
const { toast } = useToast();
const chatController = useRef(new AbortController());
/* Abort chat completions, questionGuide */
const abortRequest = useCallback(() => {
chatController.current?.abort('stop');
}, []);
const generatingMessage = useCallback(
({ event, text = '', status, name, tool, variables }: generatingMessageProps) => {
setHistories((state) =>
state.map((item, index) => {
if (index !== state.length - 1 || item.obj !== ChatRoleEnum.AI) return item;
const lastValue: AIChatItemValueItemType = JSON.parse(
JSON.stringify(item.value[item.value.length - 1])
);
if (event === SseResponseEventEnum.flowNodeStatus && status) {
return {
...item,
status,
moduleName: name
};
} else if (
(event === SseResponseEventEnum.answer || event === SseResponseEventEnum.fastAnswer) &&
text
) {
if (!lastValue || !lastValue.text) {
const newValue: AIChatItemValueItemType = {
type: ChatItemValueTypeEnum.text,
text: {
content: text
}
};
return {
...item,
value: item.value.concat(newValue)
};
} else {
lastValue.text.content += text;
return {
...item,
value: item.value.slice(0, -1).concat(lastValue)
};
}
} else if (event === SseResponseEventEnum.toolCall && tool) {
const val: AIChatItemValueItemType = {
type: ChatItemValueTypeEnum.tool,
tools: [tool]
};
return {
...item,
value: item.value.concat(val)
};
} else if (
event === SseResponseEventEnum.toolParams &&
tool &&
lastValue.type === ChatItemValueTypeEnum.tool &&
lastValue?.tools
) {
lastValue.tools = lastValue.tools.map((item) => {
if (item.id === tool.id) {
item.params += tool.params;
}
return item;
});
return {
...item,
value: item.value.slice(0, -1).concat(lastValue)
};
} else if (event === SseResponseEventEnum.toolResponse && tool) {
// replace tool response
return {
...item,
value: item.value.map((val) => {
if (val.type === ChatItemValueTypeEnum.tool && val.tools) {
const tools = val.tools.map((item) =>
item.id === tool.id ? { ...item, response: tool.response } : item
);
return {
...val,
tools
};
}
return val;
})
};
}
return item;
})
);
},
[setHistories]
);
const isChatting = useMemo(
() => histories[histories.length - 1] && histories[histories.length - 1]?.status !== 'finish',
[histories]
);
const { runAsync: onSubmit } = useRequest2(async (e: FieldValues) => {
if (!onStartChat) return;
if (isChatting) {
toast({
title: '正在聊天中...请等待结束',
status: 'warning'
});
return;
}
setTab(PluginRunBoxTabEnum.output);
// reset controller
abortRequest();
const abortSignal = new AbortController();
chatController.current = abortSignal;
setHistories([
{
dataId: getNanoid(24),
obj: ChatRoleEnum.Human,
status: 'finish',
value: [
{
type: ChatItemValueTypeEnum.text,
text: {
content: getPluginRunContent({
pluginInputs
})
}
}
]
},
{
dataId: getNanoid(24),
obj: ChatRoleEnum.AI,
value: [
{
type: ChatItemValueTypeEnum.text,
text: {
content: ''
}
}
],
status: 'loading'
}
]);
try {
const { responseData } = await onStartChat({
messages: [],
controller: chatController.current,
generatingMessage,
variables: e
});
setHistories((state) =>
state.map((item, index) => {
if (index !== state.length - 1) return item;
return {
...item,
status: 'finish',
responseData
};
})
);
} catch (err: any) {
toast({ title: err.message, status: 'error' });
setHistories((state) =>
state.map((item, index) => {
if (index !== state.length - 1) return item;
return {
...item,
status: 'finish'
};
})
);
}
});
const contextValue: PluginRunContextType = {
...props,
isChatting,
onSubmit
};
return <PluginRunContext.Provider value={contextValue}>{children}</PluginRunContext.Provider>;
};
export default PluginRunContextProvider;

View File

@@ -0,0 +1,30 @@
import React from 'react';
import { PluginRunBoxTabEnum } from './constants';
import { PluginRunBoxProps } from './type';
import RenderInput from './components/RenderInput';
import PluginRunContextProvider, { PluginRunContext } from './context';
import { useContextSelector } from 'use-context-selector';
import RenderOutput from './components/RenderOutput';
import RenderResponseDetail from './components/RenderResponseDetail';
const PluginRunBox = () => {
const { tab } = useContextSelector(PluginRunContext, (v) => v);
return (
<>
{tab === PluginRunBoxTabEnum.input && <RenderInput />}
{tab === PluginRunBoxTabEnum.output && <RenderOutput />}
{tab === PluginRunBoxTabEnum.detail && <RenderResponseDetail />}
</>
);
};
const Render = (props: PluginRunBoxProps) => {
return (
<PluginRunContextProvider {...props}>
<PluginRunBox />
</PluginRunContextProvider>
);
};
export default Render;

View File

@@ -0,0 +1,22 @@
import { ChatSiteItemType } from '@fastgpt/global/core/chat/type';
import { FlowNodeInputItemType } from '@fastgpt/global/core/workflow/type/io';
import { FieldValues, UseFormReturn } from 'react-hook-form';
import { PluginRunBoxTabEnum } from './constants';
import { OutLinkChatAuthProps } from '@fastgpt/global/support/permission/chat';
import React from 'react';
import { onStartChatType } from '../type';
export type PluginRunBoxProps = OutLinkChatAuthProps & {
pluginInputs: FlowNodeInputItemType[];
variablesForm: UseFormReturn<FieldValues, any>;
histories: ChatSiteItemType[]; // chatHistories[1] is the response
setHistories: React.Dispatch<React.SetStateAction<ChatSiteItemType[]>>;
onStartChat?: onStartChatType;
onNewChat?: () => void;
appId: string;
chatId?: string;
tab: PluginRunBoxTabEnum;
setTab: React.Dispatch<React.SetStateAction<PluginRunBoxTabEnum>>;
};

View File

@@ -0,0 +1,26 @@
import { StreamResponseType } from '@/web/common/api/fetch';
import { ChatCompletionMessageParam } from '@fastgpt/global/core/ai/type';
import { ChatSiteItemType, ToolModuleResponseItemType } from '@fastgpt/global/core/chat/type';
export type generatingMessageProps = {
event: SseResponseEventEnum;
text?: string;
name?: string;
status?: 'running' | 'finish';
tool?: ToolModuleResponseItemType;
variables?: Record<string, any>;
};
export type StartChatFnProps = {
messages: ChatCompletionMessageParam[];
responseChatItemId?: string;
controller: AbortController;
variables: Record<string, any>;
generatingMessage: (e: generatingMessageProps) => void;
};
export type onStartChatType = (e: StartChatFnProps) => Promise<
StreamResponseType & {
isNewChat?: boolean;
}
>;

View File

@@ -0,0 +1,63 @@
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 { ComponentRef as ChatComponentRef } from './ChatBox/type';
export const useChat = () => {
const ChatBoxRef = useRef<ChatComponentRef>(null);
const [chatRecords, setChatRecords] = useState<ChatSiteItemType[]>([]);
const variablesForm = useForm();
// plugin
const [pluginRunTab, setPluginRunTab] = useState<PluginRunBoxTabEnum>(PluginRunBoxTabEnum.input);
const resetChatRecords = useCallback(
(props?: { records?: ChatSiteItemType[]; variables?: Record<string, any> }) => {
const { records = [], variables = {} } = props || {};
setChatRecords(records);
// Reset to empty input
const data = variablesForm.getValues();
for (const key in data) {
data[key] = '';
}
variablesForm.reset({
...data,
...variables
});
setTimeout(
() => {
ChatBoxRef.current?.restartChat?.();
},
ChatBoxRef.current?.restartChat ? 0 : 500
);
},
[variablesForm, setChatRecords]
);
const clearChatRecords = useCallback(() => {
setChatRecords([]);
const data = variablesForm.getValues();
for (const key in data) {
variablesForm.setValue(key, '');
}
console.log(ChatBoxRef.current);
ChatBoxRef.current?.restartChat?.();
}, [variablesForm]);
return {
ChatBoxRef,
chatRecords,
setChatRecords,
variablesForm,
pluginRunTab,
setPluginRunTab,
clearChatRecords,
resetChatRecords
};
};

View File

@@ -0,0 +1,127 @@
import Markdown, { CodeClassName } from '@/components/Markdown';
import {
Accordion,
AccordionButton,
AccordionIcon,
AccordionItem,
AccordionPanel,
Box
} from '@chakra-ui/react';
import { ChatItemValueTypeEnum } from '@fastgpt/global/core/chat/constants';
import {
AIChatItemValueItemType,
ChatSiteItemType,
UserChatItemValueItemType
} from '@fastgpt/global/core/chat/type';
import React from 'react';
import MyIcon from '@fastgpt/web/components/common/Icon';
import Avatar from '@/components/Avatar';
type props = {
value: UserChatItemValueItemType | AIChatItemValueItemType;
index: number;
chat: ChatSiteItemType;
isLastChild: boolean;
isChatting: boolean;
questionGuides: string[];
};
const AIResponseBox = ({ value, index, chat, isLastChild, isChatting, questionGuides }: props) => {
if (value.text) {
let source = (value.text?.content || '').trim();
// First empty line
if (!source && chat.value.length > 1) return null;
// computed question guide
if (
isLastChild &&
!isChatting &&
questionGuides.length > 0 &&
index === chat.value.length - 1
) {
source = `${source}
\`\`\`${CodeClassName.questionGuide}
${JSON.stringify(questionGuides)}`;
}
return (
<Markdown
source={source}
showAnimation={isLastChild && isChatting && index === chat.value.length - 1}
/>
);
}
if (value.type === ChatItemValueTypeEnum.tool && value.tools) {
return (
<Box>
{value.tools.map((tool) => {
const toolParams = (() => {
try {
return JSON.stringify(JSON.parse(tool.params), null, 2);
} catch (error) {
return tool.params;
}
})();
const toolResponse = (() => {
try {
return JSON.stringify(JSON.parse(tool.response), null, 2);
} catch (error) {
return tool.response;
}
})();
return (
<Accordion key={tool.id} allowToggle>
<AccordionItem borderTop={'none'} borderBottom={'none'}>
<AccordionButton
w={'auto'}
bg={'white'}
borderRadius={'md'}
borderWidth={'1px'}
borderColor={'myGray.200'}
boxShadow={'1'}
_hover={{
bg: 'auto'
}}
>
<Avatar src={tool.toolAvatar} w={'1rem'} h={'1rem'} mr={2} />
<Box mr={1} fontSize={'sm'}>
{tool.toolName}
</Box>
{isChatting && !tool.response && <MyIcon name={'common/loading'} w={'14px'} />}
<AccordionIcon color={'myGray.600'} ml={5} />
</AccordionButton>
<AccordionPanel
py={0}
px={0}
mt={0}
borderRadius={'md'}
overflow={'hidden'}
maxH={'500px'}
overflowY={'auto'}
>
{toolParams && toolParams !== '{}' && (
<Markdown
source={`~~~json#Input
${toolParams}`}
/>
)}
{toolResponse && (
<Markdown
source={`~~~json#Response
${toolResponse}`}
/>
)}
</AccordionPanel>
</AccordionItem>
</Accordion>
);
})}
</Box>
);
}
return null;
};
export default React.memo(AIResponseBox);

View File

@@ -0,0 +1,347 @@
import React, { useMemo, useState } from 'react';
import { Box, useTheme, Flex, Image, BoxProps } from '@chakra-ui/react';
import type { ChatHistoryItemResType } from '@fastgpt/global/core/chat/type.d';
import { useTranslation } from 'next-i18next';
import { moduleTemplatesFlat } from '@fastgpt/global/core/workflow/template/constants';
import LightRowTabs from '@fastgpt/web/components/common/Tabs/LightRowTabs';
import MyModal from '@fastgpt/web/components/common/MyModal';
import Markdown from '@/components/Markdown';
import { QuoteList } from '../ChatContainer/ChatBox/components/QuoteModal';
import { DatasetSearchModeMap } from '@fastgpt/global/core/dataset/constants';
import { formatNumber } from '@fastgpt/global/common/math/tools';
import { useI18n } from '@/web/context/I18n';
import QuestionTip from '@fastgpt/web/components/common/MyTooltip/QuestionTip';
function RowRender({
children,
mb,
label,
...props
}: { children: React.ReactNode; label: string } & BoxProps) {
return (
<Box mb={3}>
<Box fontSize={'sm'} mb={mb} flex={'0 0 90px'}>
{label}:
</Box>
<Box borderRadius={'sm'} fontSize={['xs', 'sm']} bg={'myGray.50'} {...props}>
{children}
</Box>
</Box>
);
}
function Row({
label,
value,
rawDom
}: {
label: string;
value?: string | number | boolean | object;
rawDom?: React.ReactNode;
}) {
const theme = useTheme();
const val = value || rawDom;
const isObject = typeof value === 'object';
const formatValue = useMemo(() => {
if (isObject) {
return `~~~json\n${JSON.stringify(value, null, 2)}`;
}
return `${value}`;
}, [isObject, value]);
if (rawDom) {
return (
<RowRender label={label} mb={1}>
{rawDom}
</RowRender>
);
}
if (val === undefined || val === '' || val === 'undefined') return null;
return (
<RowRender
label={label}
mb={isObject ? 0 : 1}
{...(isObject
? { transform: 'translateY(-3px)' }
: value
? { px: 3, py: 2, border: theme.borders.base }
: {})}
>
<Markdown source={formatValue} />
</RowRender>
);
}
const WholeResponseModal = ({
response,
showDetail,
onClose
}: {
response: ChatHistoryItemResType[];
showDetail: boolean;
onClose: () => void;
}) => {
const { t } = useTranslation();
return (
<MyModal
isCentered
isOpen={true}
onClose={onClose}
h={['90vh', '80vh']}
minW={['90vw', '600px']}
iconSrc="/imgs/modal/wholeRecord.svg"
title={
<Flex alignItems={'center'}>
{t('core.chat.response.Complete Response')}
<QuestionTip ml={2} label={'从左往右,为各个模块的响应顺序'}></QuestionTip>
</Flex>
}
>
<ResponseBox response={response} showDetail={showDetail} />
</MyModal>
);
};
export default WholeResponseModal;
export const ResponseBox = React.memo(function ResponseBox({
response,
showDetail,
hideTabs = false
}: {
response: ChatHistoryItemResType[];
showDetail: boolean;
hideTabs?: boolean;
}) {
const theme = useTheme();
const { t } = useTranslation();
const { workflowT } = useI18n();
const list = useMemo(
() =>
response.map((item, i) => ({
label: (
<Flex alignItems={'center'} justifyContent={'center'} px={2}>
<Image
mr={2}
src={
item.moduleLogo ||
moduleTemplatesFlat.find((template) => item.moduleType === template.flowNodeType)
?.avatar
}
alt={''}
w={['14px', '16px']}
/>
{t(item.moduleName)}
</Flex>
),
value: `${i}`
})),
[response, t]
);
const [currentTab, setCurrentTab] = useState(`0`);
const activeModule = useMemo(() => response[Number(currentTab)], [currentTab, response]);
return (
<Box
{...(hideTabs ? { overflow: 'auto' } : { display: 'flex', flexDirection: 'column' })}
h={'100%'}
>
{!hideTabs && (
<Box>
<LightRowTabs
list={list}
value={currentTab}
inlineStyles={{ pt: 0 }}
onChange={setCurrentTab}
/>
</Box>
)}
{activeModule && (
<Box
py={2}
px={4}
{...(hideTabs
? {}
: {
flex: '1 0 0',
overflow: 'auto'
})}
>
<>
<Row label={t('core.chat.response.module name')} value={t(activeModule.moduleName)} />
{activeModule?.totalPoints !== undefined && (
<Row
label={t('support.wallet.usage.Total points')}
value={formatNumber(activeModule.totalPoints)}
/>
)}
<Row
label={t('core.chat.response.module time')}
value={`${activeModule?.runningTime || 0}s`}
/>
<Row label={t('core.chat.response.module model')} value={activeModule?.model} />
<Row label={t('core.chat.response.module tokens')} value={`${activeModule?.tokens}`} />
<Row
label={t('core.chat.response.Tool call tokens')}
value={`${activeModule?.toolCallTokens}`}
/>
<Row label={t('core.chat.response.module query')} value={activeModule?.query} />
<Row
label={t('core.chat.response.context total length')}
value={activeModule?.contextTotalLen}
/>
<Row label={workflowT('response.Error')} value={activeModule?.error} />
</>
{/* ai chat */}
<>
<Row
label={t('core.chat.response.module temperature')}
value={activeModule?.temperature}
/>
<Row label={t('core.chat.response.module maxToken')} value={activeModule?.maxToken} />
<Row
label={t('core.chat.response.module historyPreview')}
rawDom={
activeModule.historyPreview ? (
<Box px={3} py={2} border={theme.borders.base} borderRadius={'md'}>
{activeModule.historyPreview?.map((item, i) => (
<Box
key={i}
_notLast={{
borderBottom: '1px solid',
borderBottomColor: 'myWhite.700',
mb: 2
}}
pb={2}
>
<Box fontWeight={'bold'}>{item.obj}</Box>
<Box whiteSpace={'pre-wrap'}>{item.value}</Box>
</Box>
))}
</Box>
) : (
''
)
}
/>
</>
{/* dataset search */}
<>
{activeModule?.searchMode && (
<Row
label={t('core.dataset.search.search mode')}
// @ts-ignore
value={t(DatasetSearchModeMap[activeModule.searchMode]?.title)}
/>
)}
<Row
label={t('core.chat.response.module similarity')}
value={activeModule?.similarity}
/>
<Row label={t('core.chat.response.module limit')} value={activeModule?.limit} />
<Row
label={t('core.chat.response.search using reRank')}
value={`${activeModule?.searchUsingReRank}`}
/>
<Row
label={t('core.chat.response.Extension model')}
value={activeModule?.extensionModel}
/>
<Row
label={t('support.wallet.usage.Extension result')}
value={`${activeModule?.extensionResult}`}
/>
{activeModule.quoteList && activeModule.quoteList.length > 0 && (
<Row
label={t('core.chat.response.module quoteList')}
rawDom={<QuoteList showDetail={showDetail} rawSearch={activeModule.quoteList} />}
/>
)}
</>
{/* classify question */}
<>
<Row label={t('core.chat.response.module cq result')} value={activeModule?.cqResult} />
<Row
label={t('core.chat.response.module cq')}
value={(() => {
if (!activeModule?.cqList) return '';
return activeModule.cqList.map((item) => `* ${item.value}`).join('\n');
})()}
/>
</>
{/* if-else */}
<>
<Row
label={t('core.chat.response.module if else Result')}
value={activeModule?.ifElseResult}
/>
</>
{/* extract */}
<>
<Row
label={t('core.chat.response.module extract description')}
value={activeModule?.extractDescription}
/>
<Row
label={t('core.chat.response.module extract result')}
value={activeModule?.extractResult}
/>
</>
{/* http */}
<>
<Row label={'Headers'} value={activeModule?.headers} />
<Row label={'Params'} value={activeModule?.params} />
<Row label={'Body'} value={activeModule?.body} />
<Row
label={t('core.chat.response.module http result')}
value={activeModule?.httpResult}
/>
</>
{/* plugin */}
<>
<Row label={t('core.chat.response.plugin output')} value={activeModule?.pluginOutput} />
{activeModule?.pluginDetail && activeModule?.pluginDetail.length > 0 && (
<Row
label={t('core.chat.response.Plugin response detail')}
rawDom={
<ResponseBox response={activeModule.pluginDetail} showDetail={showDetail} />
}
/>
)}
</>
{/* text output */}
<Row label={t('core.chat.response.text output')} value={activeModule?.textOutput} />
{/* tool call */}
{activeModule?.toolDetail && activeModule?.toolDetail.length > 0 && (
<Row
label={t('core.chat.response.Tool call response detail')}
rawDom={<ResponseBox response={activeModule.toolDetail} showDetail={showDetail} />}
/>
)}
{/* code */}
<Row label={workflowT('response.Custom outputs')} value={activeModule?.customOutputs} />
<Row label={workflowT('response.Custom inputs')} value={activeModule?.customInputs} />
<Row label={workflowT('response.Code log')} value={activeModule?.codeLog} />
</Box>
)}
</Box>
);
});