This commit is contained in:
archer 2025-05-30 12:54:25 +08:00
parent 3b0f0a8108
commit e74ab643fe
No known key found for this signature in database
GPG Key ID: 4446499B846D4A9E
16 changed files with 310 additions and 395 deletions

View File

@ -50,6 +50,7 @@ export type SelectProps<T = any> = Omit<ButtonProps, 'onChange'> & {
showBorder?: boolean;
}[];
isLoading?: boolean;
showAvatar?: boolean;
onChange?: (val: T) => any | Promise<any>;
ScrollData?: ReturnType<typeof useScrollPagination>['ScrollData'];
customOnOpen?: () => void;
@ -79,6 +80,7 @@ const MySelect = <T = any,>(
list = [],
onChange,
isLoading = false,
showAvatar = true,
ScrollData,
customOnOpen,
customOnClose,
@ -255,7 +257,7 @@ const MySelect = <T = any,>(
/>
) : (
<>
{selectItem?.icon && (
{selectItem?.icon && showAvatar && (
<Avatar
mr={2}
src={selectItem.icon as any}

View File

@ -19,6 +19,7 @@
"image_upload": "Image upload",
"no_gate_available": "No portal available",
"no_gate_to_delete": "There is no gate to delete",
"quick_app": "Quick Application",
"slogan": "slogan",
"status": "state",
"suggestion_ratio_1_1": "Suggested ratio 1:1",

View File

@ -20,6 +20,7 @@
"image_upload": "图片上传",
"no_gate_available": "没有可用门户",
"no_gate_to_delete": "没有可以删除的门户了",
"quick_app": "快捷应用",
"slogan": "标语",
"status": "状态",
"suggestion_ratio_1_1": "建议比例 1:1",

View File

@ -19,6 +19,7 @@
"image_upload": "圖片上傳",
"no_gate_available": "沒有可用門戶",
"no_gate_to_delete": "沒有可以刪除的門戶了",
"quick_app": "快捷應用",
"slogan": "標語",
"status": "狀態",
"suggestion_ratio_1_1": "建議比例 1:1",

View File

@ -110,7 +110,14 @@ const OneRowSelector = ({ list, onChange, disableTip, ...props }: Props) => {
</Box>
);
};
const MultipleRowSelector = ({ list, onChange, disableTip, placeholder, ...props }: Props) => {
const MultipleRowSelector = ({
list,
onChange,
disableTip,
placeholder,
showAvatar = true,
...props
}: Props) => {
const { t } = useTranslation();
const { llmModelList, embeddingModelList, ttsModelList, sttModelList, reRankModelList } =
useSystemStore();
@ -193,17 +200,20 @@ const MultipleRowSelector = ({ list, onChange, disableTip, placeholder, ...props
return (
<HStack spacing={1}>
<Avatar
borderRadius={'0'}
mr={2}
src={modelData?.avatar}
fallbackSrc={HUGGING_FACE_ICON}
w={avatarSize}
/>
{showAvatar && (
<Avatar
borderRadius={'0'}
mr={2}
src={modelData?.avatar}
fallbackSrc={HUGGING_FACE_ICON}
w={avatarSize}
/>
)}
<Box>{modelData?.name}</Box>
</HStack>
);
}, [modelList, props.value, t, avatarSize]);
}, [modelList, props.value, t, showAvatar, avatarSize]);
return (
<Box

View File

@ -1,5 +1,5 @@
import React, { useRef, useCallback, useMemo, useState, useEffect, useContext } from 'react';
import { Box, Flex, Textarea, IconButton, useBreakpointValue, Button } from '@chakra-ui/react';
import { Box, Flex, Textarea, IconButton, useBreakpointValue } from '@chakra-ui/react';
import MyIcon from '@fastgpt/web/components/common/Icon';
import { useSystemStore } from '@/web/common/system/useSystemStore';
import { getWebDefaultLLMModel } from '@/web/common/system/utils';
@ -23,7 +23,8 @@ import dynamic from 'next/dynamic';
import { AppContext } from '@/pageComponents/app/detail/context';
import { AppFormContext } from '@/pages/chat/gate/index';
import Icon from '@fastgpt/web/components/common/Icon';
import GateSelect from '@fastgpt/web/components/common/MySelect/GateSelect';
import AIModelSelector from '@/components/Select/AIModelSelector';
import type { FlowNodeTemplateType } from '@fastgpt/global/core/workflow/type/node';
const GateToolSelect = dynamic(
() => import('@/pageComponents/app/detail/Gate/components/GateToolSelect'),
@ -46,8 +47,8 @@ type Props = {
resetInputVal: (val: ChatBoxInputType) => void;
chatForm: UseFormReturn<ChatBoxInputFormType>;
placeholder?: string;
selectedToolIds?: string[];
onSelectedToolIdsChange?: (toolIds: string[]) => void;
selectedTools?: FlowNodeTemplateType[];
onSelectTools?: (toolIds: FlowNodeTemplateType[]) => void;
};
const GateChatInput = ({
@ -57,8 +58,8 @@ const GateChatInput = ({
resetInputVal,
chatForm,
placeholder,
selectedToolIds: externalSelectedToolIds,
onSelectedToolIdsChange
selectedTools: externalSelectedToolIds,
onSelectTools
}: Props) => {
const { t } = useTranslation();
const { isPc } = useSystem();
@ -78,10 +79,9 @@ const GateChatInput = ({
const isChatting = useContextSelector(ChatBoxContext, (v) => v.isChatting);
const fileSelectConfig = useContextSelector(ChatBoxContext, (v) => v.fileSelectConfig);
// 如果有外部传入的工具选择,使用外部的;否则使用内部状态
const [internalSelectedToolIds, setInternalSelectedToolIds] = useState<string[]>([]);
const selectedToolIds = externalSelectedToolIds ?? internalSelectedToolIds;
const setSelectedToolIds = onSelectedToolIdsChange ?? setInternalSelectedToolIds;
// eslint-disable-next-line react-hooks/exhaustive-deps
const selectedTools = externalSelectedToolIds ?? [];
const setSelectedToolIds = onSelectTools!;
const { appDetail } = useContextSelector(AppContext, (v) => v);
const { llmModelList } = useSystemStore();
@ -93,10 +93,7 @@ const GateChatInput = ({
const [selectedModel, setSelectedModel] = useState(defaultModel);
const showModelSelector = useMemo(() => {
return (
router.pathname.startsWith('/chat/gate') &&
!router.pathname.includes('/chat/gate/application')
);
return router.pathname === '/chat/gate';
}, [router.pathname]);
// 是否显示工具选择器
@ -188,7 +185,7 @@ const GateChatInput = ({
text: textareaValue.trim(),
files: fileList,
gateModel: showModelSelector ? selectedModel : undefined,
selectedTool: selectedToolIds.length > 0 ? selectedToolIds.join(',') : null // 将工具ID数组转换为逗号分隔的字符串
selectedTool: selectedTools.length > 0 ? selectedTools.join(',') : null // 将工具ID数组转换为逗号分隔的字符串
});
replaceFiles([]);
},
@ -200,7 +197,7 @@ const GateChatInput = ({
replaceFiles,
showModelSelector,
selectedModel,
selectedToolIds
selectedTools
]
);
@ -214,8 +211,9 @@ const GateChatInput = ({
boxShadow="0px 5px 16px -4px rgba(19, 51, 107, 0.08)"
borderRadius="20px"
position="relative"
p={4}
pb="56px"
px={4}
pb={'62px'}
pt={fileList.length > 0 ? 0 : 4}
overflow="hidden"
transition="all 0.2s ease"
_hover={{
@ -335,24 +333,18 @@ const GateChatInput = ({
>
<Flex align="center" gap={2} overflow="hidden" maxW="65%" flexShrink={1} flexWrap="nowrap">
{showModelSelector && (
<GateSelect
value={selectedModel}
<AIModelSelector
list={modelList}
value={selectedModel}
showAvatar={false}
onChange={setSelectedModel}
minW="128px"
maxW="180px"
w="auto"
bg="#F9F9F9"
border="0.5px solid #E0E0E0"
borderRadius="10px"
color="#485264"
h="36px"
fontSize="14px"
bg={'myGray.50'}
borderRadius={'lg'}
/>
)}
{showTools && (
<GateToolSelect
selectedToolIds={selectedToolIds}
selectedTools={selectedTools}
onToolsChange={setSelectedToolIds}
buttonSize={buttonSize}
/>

View File

@ -14,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, Checkbox, Flex, Text } from '@chakra-ui/react';
import { Box, Checkbox, Flex, HStack, Text } 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';
@ -74,6 +74,9 @@ import { useRouter } from 'next/router';
import { getTeamGateConfig, getTeamGateConfigCopyRight } from '@/web/support/user/team/gate/api';
import type { getGateConfigCopyRightResponse } from '@fastgpt/global/support/user/team/gate/api';
import type { GateSchemaType } from '@fastgpt/global/support/user/team/gate/type';
import type { FlowNodeTemplateType } from '@fastgpt/global/core/workflow/type/node';
import type { AppListItemType } from '@fastgpt/global/core/app/type';
import Avatar from '@fastgpt/web/components/common/Avatar';
const FeedbackModal = dynamic(() => import('./components/FeedbackModal'));
const ReadFeedbackModal = dynamic(() => import('./components/ReadFeedbackModal'));
@ -96,8 +99,9 @@ type Props = OutLinkChatAuthProps &
showVoiceIcon?: boolean;
showEmptyIntro?: boolean;
active?: boolean; // can use
selectedToolIds?: string[];
onSelectedToolIdsChange?: (toolIds: string[]) => void;
selectedTools?: FlowNodeTemplateType[];
onSelectTools?: (toolIds: FlowNodeTemplateType[]) => void;
recommendApps?: AppListItemType[];
onStartChat?: (e: StartChatFnProps) => Promise<
StreamResponseType & {
@ -115,8 +119,9 @@ const ChatBox = ({
active = true,
onStartChat,
chatType,
selectedToolIds,
onSelectedToolIdsChange
selectedTools,
onSelectTools,
recommendApps = []
}: Props) => {
const ScrollContainerRef = useRef<HTMLDivElement>(null);
const { t } = useTranslation();
@ -421,7 +426,7 @@ const ChatBox = ({
const router = useRouter();
const inGateRoute = useMemo(() => {
return router.pathname.startsWith('/chat/gate');
return router.pathname === '/chat/gate';
}, [router.pathname]);
/**
* user confirm send prompt
@ -1124,11 +1129,7 @@ const ChatBox = ({
const { userInfo } = useUserStore();
const showWelcome = useMemo(() => {
return (
router.pathname.startsWith('/chat/gate') &&
!router.pathname.includes('/chat/gate/application') &&
chatRecords.length === 0
);
return router.pathname === '/chat/gate' && chatRecords.length === 0;
}, [router.pathname, chatRecords.length]);
return (
@ -1169,6 +1170,29 @@ const ChatBox = ({
{/* message input */}
{onStartChat && chatStarted && active && !isInteractive && (
<Box w={{ base: 'calc(100% - 48px)', md: '700px' }} maxH="132px" h="100%" px={0}>
<HStack mb={3}>
{recommendApps.map((item) => (
<HStack
gap={1}
key={item._id}
border={'base'}
px={2}
py={1}
borderRadius={'sm'}
cursor={'pointer'}
_hover={{
bg: 'primary.50',
borderColor: 'primary.400'
}}
onClick={() => {
router.push(`/chat/gate/application?appId=${item._id}`);
}}
>
<Avatar src={item.avatar} w="1rem" h="1rem" borderRadius={'sm'} />
<Box fontSize={'sm'}>{item.name}</Box>
</HStack>
))}
</HStack>
<GateChatInput
onSendMessage={sendPrompt}
onStop={() => chatController.current?.abort('stop')}
@ -1176,8 +1200,8 @@ const ChatBox = ({
resetInputVal={resetInputVal}
chatForm={chatForm}
placeholder={gateConfig?.placeholderText || '你可以问我任何问题'}
selectedToolIds={selectedToolIds}
onSelectedToolIdsChange={onSelectedToolIdsChange}
selectedTools={selectedTools}
onSelectTools={onSelectTools}
/>
</Box>
)}
@ -1205,7 +1229,7 @@ const ChatBox = ({
{RenderRecords}
{/* 移动端下的输入框和版权信息容器 */}
<Flex direction="column" w="100%" mb={{ base: '12px', sm: 0 }} gap="12px">
<Flex direction="column" w="100%" mb={{ base: '12px', sm: 0 }}>
{/* message input */}
{onStartChat && chatStarted && active && !isInteractive && (
<Box
@ -1225,8 +1249,8 @@ const ChatBox = ({
resetInputVal={resetInputVal}
chatForm={chatForm}
placeholder={gateConfig?.placeholderText || t('common:gate.placeholder')}
selectedToolIds={selectedToolIds}
onSelectedToolIdsChange={onSelectedToolIdsChange}
selectedTools={selectedTools}
onSelectTools={onSelectTools}
/>
)}
{!inGateRoute && (

View File

@ -7,7 +7,6 @@ import { useToast } from '@fastgpt/web/hooks/useToast';
import ShareGateModal from './ShareModol';
import { AppTypeEnum } from '@fastgpt/global/core/app/constants';
import { getMyAppsGate, postCreateApp, putAppById } from '@/web/core/app/api';
import { useUserStore } from '@/web/support/user/useUserStore';
import { emptyTemplates } from '@/web/core/app/templates';
import { saveGateConfig } from './HomeTable';
import type { GateSchemaType } from '@fastgpt/global/support/user/team/gate/type';
@ -32,7 +31,10 @@ const ConfigButtons = ({ tab, appForm, gateConfig, copyRightConfig }: Props) =>
const { runAsync: saveHomeConfig, loading: savingHome } = useRequest2(
async () => {
if (!!gateConfig) {
await saveGateConfig(gateConfig);
await saveGateConfig({
...gateConfig,
status: true
});
toast({
title: t('common:save_success'),
status: 'success'
@ -89,7 +91,6 @@ const ConfigButtons = ({ tab, appForm, gateConfig, copyRightConfig }: Props) =>
const gateApp = apps.find((app) => app.type === AppTypeEnum.gate);
const currentTeamAvatar = copyRightConfig?.logo;
const currentSlogan = gateConfig?.slogan;
console.log('gateApp', gateApp, currentTeamAvatar, currentSlogan, nodes, edges);
if (gateApp) {
if (
gateApp.avatar !== currentTeamAvatar ||
@ -104,25 +105,17 @@ const ConfigButtons = ({ tab, appForm, gateConfig, copyRightConfig }: Props) =>
nodes,
edges
});
toast({
title: t('common:update_success'),
status: 'success'
});
}
} else {
await postCreateApp({
avatar: gateConfig?.logo,
name: gateConfig?.name,
name: 'App',
intro: gateConfig?.slogan,
type: AppTypeEnum.gate,
modules: emptyTemplates[AppTypeEnum.gate].nodes,
edges: emptyTemplates[AppTypeEnum.gate].edges,
chatConfig: emptyTemplates[AppTypeEnum.gate].chatConfig
});
toast({
title: t('common:create_success'),
status: 'success'
});
}
} catch (error) {
toast({

View File

@ -22,7 +22,6 @@ import { v1Workflow2V2 } from '@/web/core/workflow/adapt';
import { useMount } from 'ahooks';
import type { AppDetailType, AppSimpleEditFormType } from '@fastgpt/global/core/app/type';
import { useSimpleAppSnapshots } from '@/pageComponents/app/detail/Gate/useSnapshots';
import { Dropdown } from 'react-day-picker';
import MyIcon from '@fastgpt/web/components/common/Icon';
import AddQuickAppModal from './AddQuickAppModal';
import { listQuickApps } from '@/web/support/user/team/gate/quickApp';
@ -43,7 +42,6 @@ type Props = {
tools: string[];
slogan: string;
placeholderText: string;
onStatusChange?: (status: boolean) => void;
onSloganChange?: (slogan: string) => void;
onPlaceholderChange?: (text: string) => void;
onToolsChange?: (tools: string[]) => void;
@ -54,7 +52,6 @@ const HomeTable = ({
appDetail,
slogan,
placeholderText,
onStatusChange,
onSloganChange,
onPlaceholderChange,
onAppFormChange
@ -165,12 +162,6 @@ const HomeTable = ({
letterSpacing: '0.25px'
};
// 响应式工具布局
const handleStatusChange = (val: string) => {
onStatusChange?.(val === 'enabled');
};
const handleSloganChange = (val: string) => {
onSloganChange?.(val);
};
@ -287,75 +278,6 @@ const HomeTable = ({
pb={6}
pt={{ base: 4, md: 6 }}
>
{/* 状态选择 */}
<FormControl display="flex" flexDirection="column" gap={spacing.sm} w="full">
<FormLabel
fontWeight={formStyles.fontWeight}
fontSize={formStyles.fontSize}
lineHeight={formStyles.lineHeight}
letterSpacing={formStyles.letterSpacing}
color="myGray.700"
mb="0"
>
{t('account_gate:status')}
</FormLabel>
<RadioGroup value={status ? 'enabled' : 'disabled'} onChange={handleStatusChange}>
<Stack direction={{ base: 'column', sm: 'row' }} spacing={spacing.md}>
<Flex
alignItems="center"
p={`${spacing.sm} ${spacing.lg} ${spacing.sm} ${spacing.md}`}
borderWidth="1px"
borderColor={status ? 'primary.500' : 'myGray.200'}
borderRadius="7px"
bg={status ? 'blue.50' : 'white'}
transition="all 0.2s ease-in-out"
_hover={{
bg: status ? 'blue.100' : 'myGray.50',
borderColor: status ? 'primary.600' : 'myGray.300',
boxShadow: '0px 2px 4px rgba(0, 0, 0, 0.05)',
transform: 'translateY(-1px)'
}}
>
<Radio value="enabled" colorScheme="blue">
<Text
fontSize={formStyles.fontSize}
lineHeight={formStyles.lineHeight}
fontWeight={formStyles.fontWeight}
letterSpacing={formStyles.letterSpacing}
>
{t('account_gate:enabled')}
</Text>
</Radio>
</Flex>
<Flex
alignItems="center"
p={`${spacing.sm} ${spacing.lg} ${spacing.sm} ${spacing.md}`}
borderWidth="1px"
borderColor={!status ? 'primary.500' : 'myGray.200'}
borderRadius="7px"
bg={!status ? 'blue.50' : 'white'}
transition="all 0.2s ease-in-out"
_hover={{
bg: !status ? 'blue.100' : 'myGray.50',
borderColor: !status ? 'primary.600' : 'myGray.300',
boxShadow: '0px 2px 4px rgba(0, 0, 0, 0.05)',
transform: 'translateY(-1px)'
}}
>
<Radio value="disabled" colorScheme="blue">
<Text
fontSize={formStyles.fontSize}
lineHeight={formStyles.lineHeight}
fontWeight={formStyles.fontWeight}
letterSpacing={formStyles.letterSpacing}
>
{t('account_gate:disabled')}
</Text>
</Radio>
</Flex>
</Stack>
</RadioGroup>
</FormControl>
{/* 快捷应用 */}
<FormControl
display={'flex'}
@ -365,7 +287,7 @@ const HomeTable = ({
gap={'8px'}
>
{/* 标题行 */}
<Flex alignItems={'center'} gap={'4px'}>
<Flex alignItems={'center'} gap={'1'}>
<Text
color={'var(--Gray-Modern-600, #485264)'}
fontFamily={'PingFang SC'}

View File

@ -1,11 +1,10 @@
import { Box, Button, Flex, Grid, useDisclosure, Text } from '@chakra-ui/react';
import React, { useMemo, useState, useCallback, useEffect, useRef } from 'react';
import React, { useState, useCallback } from 'react';
import MyIcon from '@fastgpt/web/components/common/Icon';
import { useTranslation } from 'next-i18next';
import { SmallAddIcon } from '@chakra-ui/icons';
import type { AppSimpleEditFormType } from '@fastgpt/global/core/app/type';
import MyTooltip from '@fastgpt/web/components/common/MyTooltip';
import { theme } from '@fastgpt/web/styles/theme';
import {
FlowNodeInputTypeEnum,
FlowNodeTypeEnum
@ -113,9 +112,8 @@ const ToolSelect = ({
<>
{/* 标题区域 */}
<Flex alignItems="center" justifyContent="space-between" width="100%">
<Flex alignItems="center" gap={spacing.xs}>
<Flex alignItems="center" gap={1}>
<Text
ml={2}
fontWeight={formStyles.fontWeight}
fontSize={formStyles.fontSize}
lineHeight={formStyles.lineHeight}
@ -124,7 +122,7 @@ const ToolSelect = ({
>
{t('common:core.app.Tool call')}
</Text>
<QuestionTip ml={1} label={t('app:plugin_dispatch_tip')} />
<QuestionTip label={t('app:plugin_dispatch_tip')} />
</Flex>
{/* 已有工具时显示新增按钮 */}

View File

@ -1,7 +1,6 @@
import { Box, Flex } from '@chakra-ui/react';
import { Box, Flex, HStack } from '@chakra-ui/react';
import React, { useEffect, useMemo, useState } from 'react';
import { useSafeState } from 'ahooks';
import type { AppDetailType, AppSimpleEditFormType } from '@fastgpt/global/core/app/type';
import { useContextSelector } from 'use-context-selector';
import { useChatGate } from '../useChatGate';
@ -11,6 +10,9 @@ import { useChatStore } from '@/web/core/chat/context/useChatStore';
import MyBox from '@fastgpt/web/components/common/MyBox';
import { cardStyles } from '../constants';
import ChatQuoteList from '@/pageComponents/chat/ChatQuoteList';
import { useRequest2 } from '@fastgpt/web/hooks/useRequest';
import { getQuickApps, listQuickApps } from '@/web/support/user/team/gate/quickApp';
import Avatar from '@fastgpt/web/components/common/Avatar';
type Props = {
appForm: AppSimpleEditFormType;
@ -18,31 +20,17 @@ type Props = {
appDetail: AppDetailType; // 添加 appDetail prop
};
const ChatGate = ({ appForm, setRenderEdit, appDetail }: Props) => {
console.log('appDetai', appDetail);
console.log('appform', appForm);
const datasetCiteData = useContextSelector(ChatItemContext, (v) => v.datasetCiteData);
const setCiteModalData = useContextSelector(ChatItemContext, (v) => v.setCiteModalData);
// 添加 selectedToolIds 状态管理
const [selectedToolIds, setSelectedToolIds] = useState<string[]>([]);
const [workflowData] = useSafeState({
nodes: appDetail.modules || [],
edges: appDetail.edges || []
});
useEffect(() => {
setRenderEdit(!datasetCiteData);
}, [datasetCiteData, setRenderEdit]);
const { ChatContainer, restartChat, loading } = useChatGate({
...workflowData,
chatConfig: appForm.chatConfig,
const { ChatContainer } = useChatGate({
appForm,
isReady: true,
appDetail,
selectedToolIds, // 传递 selectedToolIds
onSelectedToolIdsChange: setSelectedToolIds // 传递更新函数
appDetail
});
return (

View File

@ -12,25 +12,31 @@ import {
ModalHeader,
ModalBody,
ModalCloseButton,
useDisclosure
useDisclosure,
HStack
} from '@chakra-ui/react';
import { useTranslation } from 'next-i18next';
import MyIcon from '@fastgpt/web/components/common/Icon';
import Avatar from '@fastgpt/web/components/common/Avatar';
import { useRequest2 } from '@fastgpt/web/hooks/useRequest';
import { getTeamGateConfig } from '@/web/support/user/team/gate/api';
import { getSystemPlugTemplates, getTeamPlugTemplates } from '@/web/core/app/api/plugin';
import type { NodeTemplateListItemType } from '@fastgpt/global/core/workflow/type/node.d';
import {
getPreviewPluginNode,
getSystemPlugTemplates,
getTeamPlugTemplates
} from '@/web/core/app/api/plugin';
import EmptyTip from '@fastgpt/web/components/common/EmptyTip';
import type { FlowNodeTemplateType } from '@fastgpt/global/core/workflow/type/node';
import MyBox from '@fastgpt/web/components/common/MyBox';
type GateToolSelectProps = {
selectedToolIds: string[];
onToolsChange: (toolIds: string[]) => void;
selectedTools: FlowNodeTemplateType[];
onToolsChange: (tools: FlowNodeTemplateType[]) => void;
buttonSize?: string;
};
const GateToolSelect = ({
selectedToolIds,
selectedTools,
onToolsChange,
buttonSize = 'md'
}: GateToolSelectProps) => {
@ -77,60 +83,83 @@ const GateToolSelect = ({
}, [gateConfig?.tools, allAvailableTools]);
// 处理单个工具的选择/取消选择
const handleToolSelect = useCallback(
(toolId: string, checked: boolean) => {
const newSelectedIds = checked
? [...selectedToolIds, toolId]
: selectedToolIds.filter((id) => id !== toolId);
onToolsChange(newSelectedIds);
const { runAsync: handleToolSelect, loading: loadingToolSelect } = useRequest2(
async (toolId: string, checked: boolean) => {
const tool = allAvailableTools.find((item) => item.id === toolId);
if (!tool) return;
if (checked) {
const res = await getPreviewPluginNode({ appId: tool.id });
onToolsChange([...selectedTools, res]);
} else {
onToolsChange(selectedTools.filter((item) => item.pluginId !== toolId));
}
},
[selectedToolIds, onToolsChange]
{
refreshDeps: [allAvailableTools, selectedTools, onToolsChange]
}
);
const selectedCount = selectedToolIds.length;
const selectedCount = selectedTools.length;
const loading = loadingGateConfig || loadingSystemPlugins || loadingTeamPlugins;
// 调试信息
console.log('GateToolSelect Debug:', {
isOpen,
loading,
availableTools: availableTools.length,
gateConfigTools: gateConfig?.tools?.length || 0,
systemPlugins: systemPlugins.length,
teamPlugins: teamPlugins.length,
allAvailableTools: allAvailableTools.length
});
return (
return availableTools.length > 0 ? (
<>
<Button
leftIcon={
<MyIcon name={'support/gate/chat/toolkitLine'} w={'18px'} h={'18px'} color="blue.500" />
}
size={buttonSize}
display="flex"
padding="8px 12px"
justifyContent="center"
alignItems="center"
gap="4px"
iconSpacing="4px"
borderRadius="9999px"
border="0.5px solid var(--Royal-Blue-200, #C5D7FF)"
background="var(--light-fastgpt-primary-container-low, #F0F4FF)"
color="blue.500"
fontWeight="500"
onClick={() => {
console.log('Button clicked, opening modal');
onOpen();
}}
flexShrink={0}
_hover={{
background: 'var(--light-fastgpt-primary-container-low, #E6EDFF)'
}}
>
<Box display={{ base: 'none', md: 'block' }}>{t('common:tool_select')}:&nbsp;</Box>
{selectedCount}
</Button>
{availableTools.length > 1 ? (
<Button
leftIcon={
<MyIcon name={'support/gate/chat/toolkitLine'} w={'18px'} h={'18px'} color="blue.500" />
}
size={buttonSize}
display="flex"
padding="8px 12px"
justifyContent="center"
alignItems="center"
gap="4px"
iconSpacing="4px"
borderRadius="9999px"
border="0.5px solid var(--Royal-Blue-200, #C5D7FF)"
background="var(--light-fastgpt-primary-container-low, #F0F4FF)"
color="blue.500"
fontWeight="500"
onClick={onOpen}
flexShrink={0}
_hover={{
background: 'var(--light-fastgpt-primary-container-low, #E6EDFF)'
}}
>
<Box display={{ base: 'none', md: 'block' }}>{t('common:tool_select')}:&nbsp;</Box>
{selectedCount}
</Button>
) : (
<MyBox
isLoading={loadingToolSelect}
display={'flex'}
alignItems={'center'}
gap={2}
h={'40px'}
borderRadius={'lg'}
border={'base'}
px={3}
cursor={'pointer'}
userSelect={'none'}
_hover={{
borderColor: 'primary.300'
}}
{...(selectedTools.length > 0 && {
borderColor: 'primary.400 !important',
bg: 'primary.50'
})}
onClick={() => {
handleToolSelect(availableTools[0].id, selectedTools.length === 0);
}}
>
<Avatar src={availableTools[0].avatar} w="1.2rem" h="1.2rem" borderRadius={'sm'} />
<Text display={['none', 'none', 'block']} fontSize="md" fontWeight="medium">
{availableTools[0].name}
</Text>
</MyBox>
)}
<Modal isOpen={isOpen} onClose={onClose} size="md">
<ModalOverlay />
@ -152,76 +181,83 @@ const GateToolSelect = ({
<ModalCloseButton />
<ModalBody pb={6}>
{loading ? (
<Flex justify="center" py={8}>
<Text fontSize="sm" color="myGray.500">
{t('common:Loading')}
</Text>
</Flex>
) : availableTools.length === 0 ? (
<Box py={8} textAlign="center">
<EmptyTip text="暂无可用工具" />
<Text fontSize="sm" color="myGray.500" mt={3}>
</Text>
</Box>
) : (
<VStack align="stretch" spacing={2}>
{availableTools.map((tool) => (
<Box
key={tool.id}
p={4}
borderRadius="md"
cursor="pointer"
border="1px solid"
borderColor="gray.200"
transition="all 0.2s"
_hover={{
bg: 'blue.50',
borderColor: 'blue.300'
}}
onClick={() => handleToolSelect(tool.id, !selectedToolIds.includes(tool.id))}
>
<Flex align="center">
<Checkbox
size="md"
isChecked={selectedToolIds.includes(tool.id)}
onChange={(e) => {
e.stopPropagation();
handleToolSelect(tool.id, e.target.checked);
}}
mr={4}
colorScheme="blue"
/>
<Avatar src={tool.avatar} w="32px" h="32px" mr={3} />
<Box flex={1}>
<Text fontSize="md" fontWeight="medium" color="myGray.900">
{tool.name}
</Text>
{tool.intro && (
<Text fontSize="sm" color="myGray.600" mt={1} noOfLines={2}>
{tool.intro}
<MyBox isLoading={loadingToolSelect}>
{loading ? (
<Flex justify="center" py={8}>
<Text fontSize="sm" color="myGray.500">
{t('common:Loading')}
</Text>
</Flex>
) : availableTools.length === 0 ? (
<Box py={8} textAlign="center">
<EmptyTip text="暂无可用工具" />
<Text fontSize="sm" color="myGray.500" mt={3}>
</Text>
</Box>
) : (
<VStack align="stretch" spacing={2}>
{availableTools.map((tool) => (
<Box
key={tool.id}
p={4}
borderRadius="md"
cursor="pointer"
border="1px solid"
borderColor="gray.200"
transition="all 0.2s"
_hover={{
bg: 'blue.50',
borderColor: 'blue.300'
}}
onClick={() =>
handleToolSelect(
tool.id,
!selectedTools.some((item) => item.pluginId === tool.id)
)
}
>
<Flex align="center">
<Checkbox
size="md"
isChecked={selectedTools.some((item) => item.pluginId === tool.id)}
onChange={(e) => {
e.stopPropagation();
handleToolSelect(tool.id, e.target.checked);
}}
mr={4}
colorScheme="blue"
/>
<Avatar src={tool.avatar} w="32px" h="32px" mr={3} />
<Box flex={1}>
<Text fontSize="md" fontWeight="medium" color="myGray.900">
{tool.name}
</Text>
)}
</Box>
</Flex>
</Box>
))}
</VStack>
)}
{tool.intro && (
<Text fontSize="sm" color="myGray.600" mt={1} noOfLines={2}>
{tool.intro}
</Text>
)}
</Box>
</Flex>
</Box>
))}
</VStack>
)}
{selectedToolIds.length > 0 && (
<Box mt={4} p={3} bg="blue.50" borderRadius="md">
<Text fontSize="sm" color="blue.700">
{selectedToolIds.length}
</Text>
</Box>
)}
{selectedTools.length > 0 && (
<Box mt={4} p={3} bg="blue.50" borderRadius="md">
<Text fontSize="sm" color="blue.700">
{selectedTools.length}
</Text>
</Box>
)}
</MyBox>
</ModalBody>
</ModalContent>
</Modal>
</>
);
) : null;
};
export default React.memo(GateToolSelect);

View File

@ -1,16 +1,10 @@
import { useUserStore } from '@/web/support/user/useUserStore';
import React, { useCallback, useEffect, useMemo } from 'react';
import React, { useCallback, useEffect, useState } from 'react';
import type { StartChatFnProps } from '@/components/core/chat/ChatContainer/type';
import { streamFetch } from '@/web/common/api/fetch';
import { useMemoizedFn } from 'ahooks';
import { useMemoizedFn, useSafeState } from 'ahooks';
import { useContextSelector } from 'use-context-selector';
import type { StoreNodeItemType } from '@fastgpt/global/core/workflow/type/node';
import type { StoreEdgeItemType } from '@fastgpt/global/core/workflow/type/edge';
import { FlowNodeTypeEnum } from '@fastgpt/global/core/workflow/node/constant';
import { AppTypeEnum } from '@fastgpt/global/core/app/constants';
import dynamic from 'next/dynamic';
import { Box } from '@chakra-ui/react';
import type { AppChatConfigType, AppDetailType } from '@fastgpt/global/core/app/type';
import type { AppDetailType, AppSimpleEditFormType } from '@fastgpt/global/core/app/type';
import ChatBox from '@/components/core/chat/ChatContainer/ChatBox';
import { useChatStore } from '@/web/core/chat/context/useChatStore';
import { ChatItemContext } from '@/web/core/chat/context/chatItemContext';
@ -20,29 +14,39 @@ import { useTranslation } from 'next-i18next';
import { ChatContext } from '@/web/core/chat/context/chatContext';
import { getChatTitleFromChatMessage } from '@fastgpt/global/core/chat/utils';
import { GPTMessages2Chats } from '@fastgpt/global/core/chat/adapt';
const PluginRunBox = dynamic(() => import('@/components/core/chat/ChatContainer/PluginRunBox'));
import { form2AppWorkflow } from '@/web/core/app/utils';
import type { FlowNodeTemplateType } from '@fastgpt/global/core/workflow/type/node';
import { listQuickApps } from '@/web/support/user/team/gate/quickApp';
export const useChatGate = ({
selectedToolIds,
onSelectedToolIdsChange,
nodes,
edges,
chatConfig,
appForm,
isReady,
appDetail
}: {
selectedToolIds?: string[];
onSelectedToolIdsChange?: (toolIds: string[]) => void;
nodes: StoreNodeItemType[];
edges: StoreEdgeItemType[];
chatConfig: AppChatConfigType;
appForm: AppSimpleEditFormType;
isReady: boolean;
appDetail: AppDetailType;
}) => {
const { t } = useTranslation();
const { userInfo } = useUserStore();
const { setChatId, chatId, appId } = useChatStore();
const [selectedTools, setSelectedTools] = useState<FlowNodeTemplateType[]>([]);
const [workflowData, setWorkflowData] = useSafeState({
nodes: appDetail.modules || [],
edges: appDetail.edges || []
});
useEffect(() => {
const { nodes, edges } = form2AppWorkflow(
{
...appForm,
selectedTools
},
t
);
setWorkflowData({ nodes, edges });
}, [appForm, selectedTools, setWorkflowData, t]);
const onUpdateHistoryTitle = useContextSelector(ChatContext, (v) => v.onUpdateHistoryTitle);
const startChat = useMemoizedFn(
@ -61,19 +65,18 @@ export const useChatGate = ({
data: {
// Send histories and user messages
messages: histories,
nodes,
edges,
nodes: workflowData.nodes,
edges: workflowData.edges,
variables,
responseChatItemId,
appId,
appName: t('chat:chat_gate_app', { name: appDetail.name }),
chatId,
chatConfig,
chatConfig: appForm.chatConfig,
metadata: {
source: 'web',
userAgent: navigator.userAgent
},
selectedToolIds: selectedToolIds || []
}
},
onMessage: generatingMessage,
abortCtrl: controller
@ -99,22 +102,18 @@ export const useChatGate = ({
const resetVariables = useContextSelector(ChatItemContext, (v) => v.resetVariables);
const clearChatRecords = useContextSelector(ChatItemContext, (v) => v.clearChatRecords);
const pluginInputs = useMemo(() => {
return nodes.find((node) => node.flowNodeType === FlowNodeTypeEnum.pluginInput)?.inputs || [];
}, [nodes]);
// Set chat box data
useEffect(() => {
setChatBoxData({
userAvatar: userInfo?.avatar,
appId: appId,
app: {
chatConfig,
chatConfig: appForm.chatConfig,
name: appDetail.name,
avatar: appDetail.avatar,
intro: appDetail.intro,
type: appDetail.type,
pluginInputs
pluginInputs: []
}
});
}, [
@ -122,9 +121,8 @@ export const useChatGate = ({
appDetail.intro,
appDetail.name,
appDetail.type,
appForm.chatConfig,
appId,
chatConfig,
pluginInputs,
setChatBoxData,
userInfo?.avatar
]);
@ -151,29 +149,24 @@ export const useChatGate = ({
setChatId();
}, [clearChatRecords, setChatId]);
const CustomChatContainer = useMemoizedFn(() =>
appDetail.type === AppTypeEnum.plugin ? (
<Box p={5} pb={16}>
<PluginRunBox
appId={appId}
chatId={chatId}
onNewChat={restartChat}
onStartChat={startChat}
/>
</Box>
) : (
<ChatBox
isReady={isReady}
appId={appId}
chatId={chatId}
showMarkIcon
chatType={'chat'}
onStartChat={startChat}
selectedToolIds={selectedToolIds}
onSelectedToolIdsChange={onSelectedToolIdsChange}
/>
)
);
// 精选应用
const { data: recommendApps = [] } = useRequest2(listQuickApps, {
manual: false
});
const CustomChatContainer = useMemoizedFn(() => (
<ChatBox
isReady={isReady}
appId={appId}
chatId={chatId}
showMarkIcon
chatType={'chat'}
onStartChat={startChat}
selectedTools={selectedTools}
onSelectTools={setSelectedTools}
recommendApps={recommendApps}
/>
));
return {
ChatContainer: CustomChatContainer,

View File

@ -29,9 +29,9 @@ type TabType = 'home' | 'copyright' | 'app' | 'logs';
const GatewayConfig = () => {
const { t } = useTranslation();
const [gateConfig, setGateConfig] = useState<GateSchemaType | undefined>(undefined);
const [gateConfig, setGateConfig] = useState<GateSchemaType>();
// 添加 appForm 状态
const [appForm, setAppForm] = useState<AppSimpleEditFormType | undefined>(undefined);
const [appForm, setAppForm] = useState<AppSimpleEditFormType>();
//从 appForm 中获取 selectedTools的 id 组成 string 数组
//gateConfig?.tools 改成

View File

@ -64,7 +64,6 @@ export type Props = {
chatId: string;
chatConfig: AppChatConfigType;
metadata?: Record<string, any>;
selectedToolIds?: string[];
};
async function handler(req: NextApiRequest, res: NextApiResponse) {
@ -81,8 +80,7 @@ async function handler(req: NextApiRequest, res: NextApiResponse) {
appId,
chatConfig,
chatId,
metadata = {},
selectedToolIds = []
metadata = {}
} = req.body as Props;
try {
if (!Array.isArray(nodes)) {
@ -91,47 +89,6 @@ async function handler(req: NextApiRequest, res: NextApiResponse) {
if (!Array.isArray(edges)) {
throw new Error('Edges is not array');
}
//对边进行过滤只保留selectedToolIds中的边
console.log('selectedToolIds', selectedToolIds);
// 创建从 pluginId 到 nodeId 的映射
const pluginIdToNodeIdMap = new Map<string, string>();
nodes.forEach((node) => {
if (node.pluginId) {
pluginIdToNodeIdMap.set(node.pluginId, node.nodeId);
}
});
console.log('pluginIdToNodeIdMap', Object.fromEntries(pluginIdToNodeIdMap));
// 获取选中工具对应的 nodeId 集合
const selectedNodeIds = new Set<string>();
selectedToolIds.forEach((pluginId) => {
const nodeId = pluginIdToNodeIdMap.get(pluginId);
if (nodeId) {
selectedNodeIds.add(nodeId);
}
});
console.log('selectedNodeIds', Array.from(selectedNodeIds));
// 过滤边:保留第一个边和目标节点在选中工具中的边
const filteredEdges = edges.filter((edge, index) => {
// 保留第一个边
if (index === 0) {
return true;
}
// 保留目标节点在选中工具中的边
return selectedNodeIds.has(edge.target);
});
console.log('Original edges count:', edges.length);
console.log('Filtered edges count:', filteredEdges.length);
console.log('Filtered edges:', filteredEdges);
// 使用过滤后的边
edges = filteredEdges;
const chatMessages = GPTMessages2Chats(messages);
// console.log(JSON.stringify(chatMessages, null, 2), '====', chatMessages.length);

View File

@ -10,9 +10,6 @@ import { useTranslation } from 'next-i18next';
import FoldButton from '@/pageComponents/chat/gatechat/FoldButton';
import type { StartChatFnProps } from '@/components/core/chat/ChatContainer/type';
import PageContainer from '@/components/PageContainer';
import SideBar from '@/components/SideBar';
import ChatHistorySlider from '@/pageComponents/chat/ChatHistorySlider';
import SliderApps from '@/pageComponents/chat/SliderApps';
import ChatHeader from '@/pageComponents/chat/ChatHeader';
import { useUserStore } from '@/web/support/user/useUserStore';
import { serviceSideProps } from '@/web/common/i18n/utils';