Commit 82fa431a by Archer Committed by GitHub

perf: chatui (#7070)

* perf: chatui

* fix: reuse workflow reference input check
parent 778fb386
......@@ -60,8 +60,18 @@ export const getHandleId = (
return `${nodeId}-${type}-${key}`;
};
/**
* 判断输入值是否应按工作流引用解析。
* settingDatasetQuotePrompt 内部渲染 Reference 选择器,虽然 renderType 不是 reference,
* 但它的值仍是 [nodeId, outputId],运行时必须解析成知识库检索结果。
*/
export const nodeInputIsReference = (input: FlowNodeInputItemType) => {
if (input.renderTypeList?.[input?.selectedTypeIndex || 0] === FlowNodeInputTypeEnum.reference) {
const renderType = input.renderTypeList?.[input?.selectedTypeIndex || 0];
if (
renderType === FlowNodeInputTypeEnum.reference ||
renderType === FlowNodeInputTypeEnum.settingDatasetQuotePrompt
) {
return true;
}
......
......@@ -120,6 +120,16 @@ describe('nodeInputIsReference', () => {
};
expect(nodeInputIsReference(input)).toBe(true);
});
it('should treat settingDatasetQuotePrompt as reference', () => {
const input: FlowNodeInputItemType = {
key: NodeInputKeyEnum.aiChatDatasetQuote,
label: 'Dataset quote',
renderTypeList: [FlowNodeInputTypeEnum.settingDatasetQuotePrompt],
valueType: WorkflowIOValueTypeEnum.datasetQuote
};
expect(nodeInputIsReference(input)).toBe(true);
});
});
describe('getGuideModule', () => {
......
......@@ -398,7 +398,6 @@ const ChatInput = ({
</Flex>
);
}, [
isPc,
InputLeftComponent,
canUploadFile,
selectFileLabel,
......@@ -410,13 +409,10 @@ const ChatInput = ({
isStopping,
isChatting,
canSendMessage,
disableSend,
onOpenSelectFile,
onSelectFile,
handleSend,
handleStop,
onStopChat,
onStopSettled
handleStop
]);
const activeStyles: FlexProps = {
......@@ -430,7 +426,7 @@ const ChatInput = ({
w={'100%'}
maxW={['100%', '780px']}
mx={'auto'}
pb={['calc(12px + env(safe-area-inset-bottom))', 0]}
pb={0}
onDragOver={(e) => e.preventDefault()}
onDrop={(e) => {
e.preventDefault();
......@@ -462,7 +458,7 @@ const ChatInput = ({
h={isDefaultInputHeight ? ChatInputDefaultHeight : undefined}
minH={mobilePreSpeak ? '48px' : ChatInputDefaultHeight}
p={mobilePreSpeak ? [0, 4] : 4}
mb={4}
mb={0}
position={'relative'}
borderRadius={['xl', 'xxl']}
bg={'white'}
......@@ -527,7 +523,7 @@ const ChatInput = ({
{!mobilePreSpeak && <Box>{RenderButtonGroup}</Box>}
</Flex>
<ComplianceTip type={'chat'} pt={0} pb={0} />
<ComplianceTip type={'chat'} pt={4} pb={0} />
</Box>
);
};
......
......@@ -268,11 +268,15 @@ const EditForm = ({
<QuestionTip ml={1} label={t('app:use_computer_desc')} />
</Flex>
{showSandbox && enableSandbox && (
<Box mr={2}>
<Box mr={2}>
{showSandbox && enableSandbox ? (
<SandboxTipTag />
</Box>
)}
) : (
<MyTag>
{t(showSandbox ? 'app:sandbox_free_not_support' : 'app:sandbox_not_support_tip')}
</MyTag>
)}
</Box>
<Switch
isChecked={appForm.aiSettings.useAgentSandbox ?? false}
onChange={(e) => onChangeAgentSandbox(e.target.checked)}
......
import { useCallback, useEffect, useRef, type Dispatch, type SetStateAction } from 'react';
import { useCallback, useEffect, type Dispatch, type SetStateAction } from 'react';
import { useDisclosure } from '@chakra-ui/react';
import { useTranslation } from 'next-i18next';
import type {
......@@ -13,8 +13,8 @@ import { useConfirm } from '@fastgpt/web/hooks/useConfirm';
/**
* 管理 ChatAgent 表单中的 Skill 选择与 sandbox 开关联动。
*
* Skill 运行依赖 Agent sandbox:选择 Skill 时会自动打开 sandbox;已有 Skill 时不允许手动关闭
* sandbox;系统未配置或套餐不可用时统一给出提示,避免表单保存出无法运行的组合状态。
* Skill 运行依赖 Agent sandbox:选择 Skill 时会自动打开 sandbox;系统未配置或套餐不可用时
* 不允许开启 sandbox,但保留关闭入口,避免历史配置无法自助修复。
*/
export const useAgentSkillSelect = ({
appForm,
......@@ -36,7 +36,6 @@ export const useAgentSkillSelect = ({
onOpen: onOpenRecharge,
onClose: onCloseRecharge
} = useDisclosure();
const hasShownSandboxUnavailableWarningRef = useRef(false);
const {
isOpen: isOpenSkillSelect,
onOpen: onOpenSkillSelect,
......@@ -124,18 +123,14 @@ export const useAgentSkillSelect = ({
return;
}
if (!enableSandbox) {
openConfirm({
title: t('skill:sandbox_plan_not_supported_title'),
customContent: t('skill:sandbox_plan_not_supported_content'),
onConfirm: isTeamAdmin ? onOpenRecharge : undefined,
confirmText: isTeamAdmin ? t('skill:sandbox_upgrade_action') : t('common:Close'),
cancelText: t('common:Close'),
showCancel: isTeamAdmin
})();
toast({
status: 'warning',
title: t('app:sandbox_free_not_support')
});
return;
}
}
if (!checked && hasSelectedAgentSkills) {
if (!checked && enableSandbox && hasSelectedAgentSkills) {
toast({
status: 'warning',
title: t('skill:sandbox_disable_blocked_toast')
......@@ -150,39 +145,13 @@ export const useAgentSkillSelect = ({
}
}));
},
[
enableSandbox,
hasSelectedAgentSkills,
setAppForm,
showSandbox,
t,
toast,
openConfirm,
isTeamAdmin,
onOpenRecharge
]
[enableSandbox, hasSelectedAgentSkills, setAppForm, showSandbox, t, toast]
);
// 按系统/套餐能力同步修复历史 Skill 与虚拟机开关状态。
// 套餐/系统支持时,Skill 仍自动保持 sandbox 开启;不支持时保留历史值,交给用户手动关闭。
useEffect(() => {
const sandboxAvailable = showSandbox && enableSandbox;
if (!sandboxAvailable && appForm.aiSettings.useAgentSandbox) {
setAppForm((state) => ({
...state,
aiSettings: {
...state.aiSettings,
useAgentSandbox: false
}
}));
if (hasSelectedAgentSkills && !hasShownSandboxUnavailableWarningRef.current) {
hasShownSandboxUnavailableWarningRef.current = true;
openSkillSelect();
}
return;
}
if (sandboxAvailable && hasSelectedAgentSkills && !appForm.aiSettings.useAgentSandbox) {
setAppForm((state) => ({
...state,
......@@ -196,7 +165,6 @@ export const useAgentSkillSelect = ({
appForm.aiSettings.useAgentSandbox,
enableSandbox,
hasSelectedAgentSkills,
openSkillSelect,
setAppForm,
showSandbox
]);
......
......@@ -278,7 +278,7 @@ const Header = ({
}
if (!enableSandbox) {
toast({
title: t('skill:sandbox_plan_not_supported_title'),
title: t('app:sandbox_free_not_support'),
status: 'warning'
});
return false;
......
......@@ -75,6 +75,7 @@ const EditForm = ({
const { appDetail } = useContextSelector(AppContext, (v) => v);
const selectDatasets = useMemo(() => appForm?.dataset?.datasets, [appForm]);
const [, startTst] = useTransition();
const isAgentSandboxEnabled = !!appForm.aiSettings.useAgentSandbox;
const {
isOpen: isOpenDatasetSelect,
......@@ -259,31 +260,28 @@ const EditForm = ({
<FormLabel ml={2}>{t('app:use_agent_sandbox')}</FormLabel>
<QuestionTip ml={1} label={t('app:use_computer_desc')} />
</Flex>
{showSandbox ? (
enableSandbox ? (
<>
<Box mr={2}>
<SandboxTipTag />
</Box>
<Switch
isChecked={appForm.aiSettings.useAgentSandbox ?? false}
onChange={(e) => {
setAppForm((state) => ({
...state,
aiSettings: {
...state.aiSettings,
useAgentSandbox: e.target.checked
}
}));
}}
/>
</>
<Box mr={2}>
{showSandbox && enableSandbox ? (
<SandboxTipTag />
) : (
<SandboxNotSupportTip type="freeDisable" />
)
) : (
<SandboxNotSupportTip type="systemDisable" />
)}
<SandboxNotSupportTip type={showSandbox ? 'freeDisable' : 'systemDisable'} />
)}
</Box>
<Switch
isChecked={isAgentSandboxEnabled}
onChange={(e) => {
const checked = e.target.checked;
if (checked && (!showSandbox || !enableSandbox)) return;
setAppForm((state) => ({
...state,
aiSettings: {
...state.aiSettings,
useAgentSandbox: checked
}
}));
}}
/>
</Flex>
</Box>
......
......@@ -432,18 +432,14 @@ const NodeAgent = ({ data, selected }: NodeProps<FlowNodeItemType>) => {
return;
}
if (!enableSandbox) {
openConfirm({
title: t('skill:sandbox_plan_not_supported_title'),
customContent: t('skill:sandbox_plan_not_supported_content'),
onConfirm: isTeamAdmin ? onOpenRecharge : undefined,
confirmText: isTeamAdmin ? t('skill:sandbox_upgrade_action') : t('common:Close'),
cancelText: t('common:Close'),
showCancel: isTeamAdmin
})();
toast({
status: 'warning',
title: t('app:sandbox_free_not_support')
});
return;
}
}
if (!checked && selectedAgentSkills.length > 0) {
if (!checked && enableSandbox && selectedAgentSkills.length > 0) {
toast({
status: 'warning',
title: t('skill:sandbox_disable_blocked_toast')
......@@ -469,10 +465,7 @@ const NodeAgent = ({ data, selected }: NodeProps<FlowNodeItemType>) => {
selectedAgentSkills.length,
showSandbox,
t,
toast,
openConfirm,
isTeamAdmin,
onOpenRecharge
toast
]
);
const skillsRenderType = useMemo(
......@@ -554,7 +547,13 @@ const NodeAgent = ({ data, selected }: NodeProps<FlowNodeItemType>) => {
>
<InputLabel nodeId={nodeId} input={sandboxInput} />
<Flex alignItems={'center'} gap={1} className={'nodrag'}>
{showSandbox && enableSandbox && <SandboxTipTag />}
{showSandbox && enableSandbox ? (
<SandboxTipTag />
) : (
<MyTag>
{t(showSandbox ? 'app:sandbox_free_not_support' : 'app:sandbox_not_support_tip')}
</MyTag>
)}
<Switch
isChecked={!!sandboxInput.value}
onChange={(e) => onChangeAgentSandbox(e.target.checked)}
......
import React from 'react';
import type { FlowNodeInputItemType } from '@fastgpt/global/core/workflow/type/io';
import { Box, Flex } from '@chakra-ui/react';
import { Box, Flex, Switch } from '@chakra-ui/react';
import { FlowNodeInputTypeEnum } from '@fastgpt/global/core/workflow/node/constant';
import dynamic from 'next/dynamic';
import InputLabel from './Label';
......@@ -13,6 +13,8 @@ import { NodeInputKeyEnum } from '@fastgpt/global/core/workflow/constants';
import SandboxTipTag from '@/pageComponents/app/detail/components/SandboxTipTag';
import SandboxNotSupportTip from '@/pageComponents/app/detail/components/SandboxNotSupportTip';
import { useUserStore } from '@/web/support/user/useUserStore';
import MyTag from '@fastgpt/web/components/common/Tag/index';
import { useTranslation } from 'next-i18next';
const RenderList: Record<
FlowNodeInputTypeEnum,
......@@ -105,6 +107,7 @@ type Props = {
mb?: number;
};
const RenderInput = ({ flowInputList, nodeId, CustomComponent, mb = 5 }: Props) => {
const { t } = useTranslation();
const { feConfigs } = useSystemStore();
const { teamPlanStatus } = useUserStore();
const enableSandbox = !teamPlanStatus?.standard || !!teamPlanStatus?.standard?.enableSandbox;
......@@ -144,14 +147,37 @@ const RenderInput = ({ flowInputList, nodeId, CustomComponent, mb = 5 }: Props)
if (!RenderItem) return null;
const renderInput =
input.key === NodeInputKeyEnum.useAgentSandbox
? {
...input,
customRender: ({
value,
onChange
}: {
value: boolean;
onChange?: (value: boolean) => void;
}) => (
<Switch
isChecked={!!value}
onChange={(e) => {
const checked = e.target.checked;
if (checked && (!showSandbox || !enableSandbox)) return;
onChange?.(checked);
}}
/>
)
}
: input;
return {
Component: (
<RenderItem.Component inputs={filterProInputs} item={input} nodeId={nodeId} />
<RenderItem.Component inputs={filterProInputs} item={renderInput} nodeId={nodeId} />
),
LableRightComponent: RenderItem.LableRightComponent ? (
<RenderItem.LableRightComponent
inputs={filterProInputs}
item={input}
item={renderInput}
nodeId={nodeId}
/>
) : undefined
......@@ -179,17 +205,21 @@ const RenderInput = ({ flowInputList, nodeId, CustomComponent, mb = 5 }: Props)
{/* tmp */}
{input.key === NodeInputKeyEnum.useAgentSandbox ? (
showSandbox ? (
enableSandbox ? (
<Flex alignItems={'center'} gap={1}>
RenderComponent ? (
<Flex alignItems={'center'} gap={1}>
{showSandbox && enableSandbox ? (
<SandboxTipTag />
{RenderComponent!.Component}
</Flex>
) : (
<SandboxNotSupportTip type="freeDisable" />
)
) : (
<MyTag>
{t(
showSandbox ? 'app:sandbox_free_not_support' : 'app:sandbox_not_support_tip'
)}
</MyTag>
)}
{RenderComponent.Component}
</Flex>
) : (
<SandboxNotSupportTip type="systemDisable" />
<SandboxNotSupportTip type={showSandbox ? 'freeDisable' : 'systemDisable'} />
)
) : (
<>
......
......@@ -216,7 +216,7 @@ export const WorkflowUtilsProvider = ({ children }: { children: ReactNode }) =>
status: 'warning',
title: !showSandbox
? t('skill:sandbox_system_not_configured_toast')
: t('skill:sandbox_plan_not_supported_title')
: t('app:sandbox_free_not_support')
});
}
return;
......
......@@ -198,9 +198,7 @@ const AppChatWindow = () => {
>
<IconButton
aria-label="Open history"
icon={
<MyIcon name="core/chat/sidebar/menu" w="20px" h="20px" color="currentColor" />
}
icon={<MyIcon name="core/chat/sidebar/menu" w="20px" h="20px" color="currentColor" />}
variant="unstyled"
{...mobileChatHeaderIconButtonStyle}
onClick={onOpenSlider}
......@@ -208,7 +206,13 @@ const AppChatWindow = () => {
<Flex alignItems="center" minW={0} flex="1" justifyContent="center" px={3}>
<Avatar src={chatBoxData.app.avatar} w="20px" borderRadius="6px" />
<Box ml={2} fontSize="16px" fontWeight={500} color="myGray.900" className="textEllipsis">
<Box
ml={2}
fontSize="16px"
fontWeight={500}
color="myGray.900"
className="textEllipsis"
>
{chatBoxData.app.name}
</Box>
</Flex>
......@@ -219,7 +223,7 @@ const AppChatWindow = () => {
</Flex>
)}
<Box flex={'1 0 0'} bg={'white'}>
<Box flex={'1 0 0'} bg={'white'} pb={4}>
{isPlugin ? (
<CustomPluginRunBox
appId={appId}
......
......@@ -40,6 +40,7 @@ const ChatSliderList = () => {
updateTime: Date;
chatGenerateStatus?: ChatGenerateStatusEnum;
hasBeenRead?: boolean;
isTemporary?: boolean;
}[] = scopedHistories.map((item) => {
const isActiveChat = item.chatId === activeChatId && chatBoxData.chatId === item.chatId;
......@@ -64,6 +65,7 @@ const ChatSliderList = () => {
updateTime: Date;
chatGenerateStatus?: ChatGenerateStatusEnum;
hasBeenRead?: boolean;
isTemporary?: boolean;
} = {
id: activeChatId,
title: getDisplayHistoryTitle({
......@@ -71,6 +73,7 @@ const ChatSliderList = () => {
fallbackTitle: t('common:core.chat.New Chat')
}),
updateTime: new Date(),
isTemporary: true,
chatGenerateStatus:
chatBoxData.chatId === activeChatId ? chatBoxData.chatGenerateStatus : undefined,
hasBeenRead: chatBoxData.chatId === activeChatId ? chatBoxData.hasBeenRead : undefined
......@@ -112,7 +115,7 @@ const ChatSliderList = () => {
overflowY={'auto'}
overflowX={'hidden'}
sx={{
'& > div:last-of-type': {
'& > div > div:last-of-type:not(.chatHistoryItem)': {
color: 'var(--chakra-colors-myGray-400)'
}
}}
......@@ -120,6 +123,7 @@ const ChatSliderList = () => {
{concatHistory.map((item, i) => (
<Flex
position={'relative'}
className="chatHistoryItem"
key={item.id}
alignItems={'center'}
p="8px"
......@@ -172,7 +176,7 @@ const ChatSliderList = () => {
bg={'primary.500'}
flexShrink={0}
/>
) : (
) : item.isTemporary ? null : (
<Box
className="time"
display={'block'}
......
......@@ -72,7 +72,7 @@ const Chat = () => {
flex="1 0 0"
w={0}
position="relative"
pr={datasetCiteData ? 0 : undefined}
pr={datasetCiteData ? 0 : [0, '16px']}
insertProps={{
...rightWindowStyle,
...(datasetCiteData
......
......@@ -21,7 +21,8 @@ import {
getAppChatConfig,
getHandleId,
isValidReferenceValue,
isValidReferenceValueFormat
isValidReferenceValueFormat,
nodeInputIsReference
} from '@fastgpt/global/core/workflow/utils';
import { type TFunction } from 'next-i18next';
import {
......@@ -525,7 +526,7 @@ export const getNodeAllSource = ({
const parentNode = getNodeById(parentId);
if (parentNode) {
parentNode.inputs.forEach((input) => {
if (!input.renderTypeList?.includes(FlowNodeInputTypeEnum.reference)) return;
if (!nodeInputIsReference(input)) return;
const val = input.value as ReferenceItemValueType | undefined;
if (!Array.isArray(val) || val.length < 2) return;
const [refNodeId] = val;
......@@ -748,8 +749,7 @@ export const checkWorkflowNodeAndConnection = ({
if (Array.isArray(input.value) && input.value.length === 0) return true;
}
// check reference invalid
const renderType = input.renderTypeList[input.selectedTypeIndex || 0];
if (renderType === FlowNodeInputTypeEnum.reference) {
if (nodeInputIsReference(input)) {
// 无效引用时,返回 true
const checkValueValid = (value: ReferenceItemValueType) => {
const nodeId = value?.[0];
......
......@@ -6,7 +6,7 @@ import {
appWorkflow2AgentForm
} from '@/pageComponents/app/detail/Edit/ChatAgent/utils';
import { FlowNodeTypeEnum } from '@fastgpt/global/core/workflow/node/constant';
import { NodeInputKeyEnum } from '@fastgpt/global/core/workflow/constants';
import { NodeInputKeyEnum, NodeOutputKeyEnum } from '@fastgpt/global/core/workflow/constants';
import { getDefaultAppForm } from '@fastgpt/global/core/app/utils';
import type { AppFormEditFormType } from '@fastgpt/global/core/app/formEdit/type';
......@@ -94,6 +94,20 @@ describe('form2AppWorkflow', () => {
expect(result.nodes).toHaveLength(4);
expect(result.edges).toHaveLength(2);
const datasetNode = result.nodes.find(
(node) => node.flowNodeType === FlowNodeTypeEnum.datasetSearchNode
);
const aiNode = result.nodes.find((node) => node.flowNodeType === FlowNodeTypeEnum.chatNode);
expect(
aiNode?.inputs.find((input) => input.key === NodeInputKeyEnum.aiChatDatasetQuote)?.value
).toEqual([datasetNode?.nodeId, NodeOutputKeyEnum.datasetQuoteQA]);
expect(
result.edges.some(
(edge) => edge.source === datasetNode?.nodeId && edge.target === aiNode?.nodeId
)
).toBe(true);
});
});
......
Markdown is supported
0% or
You are about to add 0 people to the discussion. Proceed with caution.
Finish editing this message first!
Please register or sign in to comment