feat: agent and ui
This commit is contained in:
@@ -1,394 +0,0 @@
|
||||
import React, { useState, useCallback } from 'react';
|
||||
import { useRouter } from 'next/router';
|
||||
import {
|
||||
Card,
|
||||
Flex,
|
||||
Box,
|
||||
Button,
|
||||
useDisclosure,
|
||||
Modal,
|
||||
ModalOverlay,
|
||||
ModalContent,
|
||||
ModalBody,
|
||||
ModalHeader,
|
||||
ModalFooter,
|
||||
ModalCloseButton,
|
||||
Grid,
|
||||
useTheme,
|
||||
IconButton,
|
||||
Tooltip,
|
||||
Textarea
|
||||
} from '@chakra-ui/react';
|
||||
import { useUserStore } from '@/store/user';
|
||||
import { useQuery } from '@tanstack/react-query';
|
||||
import Avatar from '@/components/Avatar';
|
||||
import { AddIcon, DeleteIcon, QuestionOutlineIcon } from '@chakra-ui/icons';
|
||||
import { putAppById } from '@/api/app';
|
||||
import { useToast } from '@/hooks/useToast';
|
||||
import { useLoading } from '@/hooks/useLoading';
|
||||
import { useForm } from 'react-hook-form';
|
||||
import MyIcon from '@/components/Icon';
|
||||
import MySlider from '@/components/Slider';
|
||||
|
||||
const Kb = ({ modelId }: { modelId: string }) => {
|
||||
const theme = useTheme();
|
||||
const router = useRouter();
|
||||
const { toast } = useToast();
|
||||
const { appDetail, loadKbList, loadAppDetail } = useUserStore();
|
||||
const { Loading, setIsLoading } = useLoading();
|
||||
const [selectedIdList, setSelectedIdList] = useState<string[]>([]);
|
||||
const [refresh, setRefresh] = useState(false);
|
||||
const { register, reset, getValues, setValue } = useForm({
|
||||
defaultValues: {
|
||||
searchSimilarity: appDetail.chat.searchSimilarity,
|
||||
searchLimit: appDetail.chat.searchLimit,
|
||||
searchEmptyText: appDetail.chat.searchEmptyText
|
||||
}
|
||||
});
|
||||
|
||||
const {
|
||||
isOpen: isOpenKbSelect,
|
||||
onOpen: onOpenKbSelect,
|
||||
onClose: onCloseKbSelect
|
||||
} = useDisclosure();
|
||||
const {
|
||||
isOpen: isOpenEditParams,
|
||||
onOpen: onOpenEditParams,
|
||||
onClose: onCloseEditParams
|
||||
} = useDisclosure();
|
||||
|
||||
const onchangeKb = useCallback(
|
||||
async (
|
||||
data: {
|
||||
relatedKbs?: string[];
|
||||
searchSimilarity?: number;
|
||||
searchLimit?: number;
|
||||
searchEmptyText?: string;
|
||||
} = {}
|
||||
) => {
|
||||
setIsLoading(true);
|
||||
try {
|
||||
await putAppById(modelId, {
|
||||
chat: {
|
||||
...appDetail.chat,
|
||||
...data
|
||||
}
|
||||
});
|
||||
loadAppDetail(modelId, true);
|
||||
} catch (err: any) {
|
||||
toast({
|
||||
title: err?.message || '更新失败',
|
||||
status: 'error'
|
||||
});
|
||||
}
|
||||
setIsLoading(false);
|
||||
},
|
||||
[setIsLoading, modelId, appDetail.chat, loadAppDetail, toast]
|
||||
);
|
||||
|
||||
// init kb select list
|
||||
const { isLoading, data: kbList = [] } = useQuery(['loadKbList'], () => loadKbList());
|
||||
|
||||
return (
|
||||
<Box position={'relative'} px={5} minH={'50vh'}>
|
||||
<Box fontWeight={'bold'}>关联的知识库({appDetail.chat?.relatedKbs.length})</Box>
|
||||
{(() => {
|
||||
const kbs =
|
||||
appDetail.chat?.relatedKbs
|
||||
?.map((id) => kbList.find((kb) => kb._id === id))
|
||||
.filter((item) => item) || [];
|
||||
return (
|
||||
<Grid
|
||||
mt={2}
|
||||
gridTemplateColumns={[
|
||||
'repeat(1,1fr)',
|
||||
'repeat(2,1fr)',
|
||||
'repeat(3,1fr)',
|
||||
'repeat(4,1fr)'
|
||||
]}
|
||||
gridGap={[3, 4]}
|
||||
>
|
||||
<Card
|
||||
p={3}
|
||||
border={theme.borders.base}
|
||||
boxShadow={'sm'}
|
||||
cursor={'pointer'}
|
||||
bg={'myGray.100'}
|
||||
_hover={{
|
||||
bg: 'white',
|
||||
color: 'myBlue.800'
|
||||
}}
|
||||
onClick={() => {
|
||||
reset({
|
||||
searchSimilarity: appDetail.chat.searchSimilarity,
|
||||
searchLimit: appDetail.chat.searchLimit,
|
||||
searchEmptyText: appDetail.chat.searchEmptyText
|
||||
});
|
||||
onOpenEditParams();
|
||||
}}
|
||||
>
|
||||
<Flex alignItems={'center'} h={'38px'} fontWeight={'bold'}>
|
||||
<IconButton
|
||||
mr={2}
|
||||
size={'sm'}
|
||||
borderRadius={'lg'}
|
||||
icon={<MyIcon name={'edit'} w={'14px'} color={'myGray.600'} />}
|
||||
aria-label={''}
|
||||
variant={'base'}
|
||||
/>
|
||||
调整搜索参数
|
||||
</Flex>
|
||||
<Flex mt={3} h={'30px'} color={'myGray.600'} fontSize={'sm'}>
|
||||
相似度: {appDetail.chat.searchSimilarity}, 单次搜索数量:{' '}
|
||||
{appDetail.chat.searchLimit}, 空搜索时拒绝回复:{' '}
|
||||
{appDetail.chat.searchEmptyText !== '' ? 'true' : 'false'}
|
||||
</Flex>
|
||||
</Card>
|
||||
<Card
|
||||
p={3}
|
||||
border={theme.borders.base}
|
||||
boxShadow={'sm'}
|
||||
cursor={'pointer'}
|
||||
bg={'myGray.100'}
|
||||
_hover={{
|
||||
bg: 'white',
|
||||
color: 'myBlue.800'
|
||||
}}
|
||||
onClick={() => {
|
||||
setSelectedIdList(
|
||||
appDetail.chat?.relatedKbs ? [...appDetail.chat?.relatedKbs] : []
|
||||
);
|
||||
onOpenKbSelect();
|
||||
}}
|
||||
>
|
||||
<Flex alignItems={'center'} h={'38px'} fontWeight={'bold'}>
|
||||
<IconButton
|
||||
mr={2}
|
||||
size={'sm'}
|
||||
borderRadius={'lg'}
|
||||
icon={<AddIcon />}
|
||||
aria-label={''}
|
||||
variant={'base'}
|
||||
/>
|
||||
选择关联知识库
|
||||
</Flex>
|
||||
<Flex mt={3} h={'30px'} color={'myGray.600'} fontSize={'sm'}>
|
||||
关联知识库,让 AI 应用回答你的特有内容。
|
||||
</Flex>
|
||||
</Card>
|
||||
{kbs.map((item) =>
|
||||
item ? (
|
||||
<Card
|
||||
key={item._id}
|
||||
p={3}
|
||||
border={theme.borders.base}
|
||||
boxShadow={'sm'}
|
||||
_hover={{
|
||||
boxShadow: 'lg',
|
||||
'& .detailBtn': {
|
||||
display: 'block'
|
||||
},
|
||||
'& .delete': {
|
||||
display: 'block'
|
||||
}
|
||||
}}
|
||||
>
|
||||
<Flex alignItems={'center'} h={'38px'}>
|
||||
<Avatar src={item.avatar} w={['26px', '32px', '38px']}></Avatar>
|
||||
<Box ml={3} fontWeight={'bold'} fontSize={['md', 'lg', 'xl']}>
|
||||
{item.name}
|
||||
</Box>
|
||||
</Flex>
|
||||
<Flex mt={3} alignItems={'flex-end'} justifyContent={'flex-end'} h={'30px'}>
|
||||
<Button
|
||||
mr={3}
|
||||
className="detailBtn"
|
||||
display={['flex', 'none']}
|
||||
variant={'base'}
|
||||
size={'sm'}
|
||||
onClick={() => router.push(`/kb?kbId=${item._id}`)}
|
||||
>
|
||||
查看详情
|
||||
</Button>
|
||||
<IconButton
|
||||
className="delete"
|
||||
display={['flex', 'none']}
|
||||
icon={<DeleteIcon />}
|
||||
variant={'outline'}
|
||||
aria-label={'delete'}
|
||||
size={'sm'}
|
||||
_hover={{ color: 'red.600' }}
|
||||
onClick={() => {
|
||||
const ids = appDetail.chat?.relatedKbs
|
||||
? [...appDetail.chat.relatedKbs]
|
||||
: [];
|
||||
const i = ids.findIndex((id) => id === item._id);
|
||||
ids.splice(i, 1);
|
||||
onchangeKb({ relatedKbs: ids });
|
||||
}}
|
||||
/>
|
||||
</Flex>
|
||||
</Card>
|
||||
) : null
|
||||
)}
|
||||
</Grid>
|
||||
);
|
||||
})()}
|
||||
{/* select kb modal */}
|
||||
<Modal isOpen={isOpenKbSelect} onClose={onCloseKbSelect}>
|
||||
<ModalOverlay />
|
||||
<ModalContent
|
||||
display={'flex'}
|
||||
flexDirection={'column'}
|
||||
w={'800px'}
|
||||
maxW={'90vw'}
|
||||
h={['90vh', 'auto']}
|
||||
>
|
||||
<ModalHeader>关联的知识库({selectedIdList.length})</ModalHeader>
|
||||
<ModalCloseButton />
|
||||
<ModalBody
|
||||
flex={['1 0 0', '0 0 auto']}
|
||||
maxH={'80vh'}
|
||||
overflowY={'auto'}
|
||||
display={'grid'}
|
||||
gridTemplateColumns={['repeat(1,1fr)', 'repeat(2,1fr)', 'repeat(3,1fr)']}
|
||||
gridGap={3}
|
||||
>
|
||||
{kbList.map((item) => (
|
||||
<Card
|
||||
key={item._id}
|
||||
p={3}
|
||||
border={theme.borders.base}
|
||||
boxShadow={'sm'}
|
||||
h={'80px'}
|
||||
cursor={'pointer'}
|
||||
order={appDetail.chat?.relatedKbs?.includes(item._id) ? 0 : 1}
|
||||
_hover={{
|
||||
boxShadow: 'md'
|
||||
}}
|
||||
{...(selectedIdList.includes(item._id)
|
||||
? {
|
||||
bg: 'myBlue.300'
|
||||
}
|
||||
: {})}
|
||||
onClick={() => {
|
||||
let ids = [...selectedIdList];
|
||||
if (!selectedIdList.includes(item._id)) {
|
||||
ids = ids.concat(item._id);
|
||||
} else {
|
||||
const i = ids.findIndex((id) => id === item._id);
|
||||
ids.splice(i, 1);
|
||||
}
|
||||
|
||||
ids = ids.filter((id) => kbList.find((item) => item._id === id));
|
||||
setSelectedIdList(ids);
|
||||
}}
|
||||
>
|
||||
<Flex alignItems={'center'} h={'38px'}>
|
||||
<Avatar src={item.avatar} w={['24px', '28px', '32px']}></Avatar>
|
||||
<Box ml={3} fontWeight={'bold'} fontSize={['md', 'lg', 'xl']}>
|
||||
{item.name}
|
||||
</Box>
|
||||
</Flex>
|
||||
</Card>
|
||||
))}
|
||||
</ModalBody>
|
||||
|
||||
<ModalFooter>
|
||||
<Button
|
||||
onClick={() => {
|
||||
onCloseKbSelect();
|
||||
onchangeKb({ relatedKbs: selectedIdList });
|
||||
}}
|
||||
>
|
||||
完成
|
||||
</Button>
|
||||
</ModalFooter>
|
||||
</ModalContent>
|
||||
</Modal>
|
||||
{/* edit mode */}
|
||||
<Modal isOpen={isOpenEditParams} onClose={onCloseEditParams}>
|
||||
<ModalOverlay />
|
||||
<ModalContent display={'flex'} flexDirection={'column'} w={'600px'} maxW={'90vw'}>
|
||||
<ModalHeader>搜索参数调整</ModalHeader>
|
||||
<ModalCloseButton />
|
||||
<ModalBody>
|
||||
<Flex pt={3} pb={5}>
|
||||
<Box flex={'0 0 100px'}>
|
||||
相似度
|
||||
<Tooltip label={'高相似度推荐0.8及以上。'}>
|
||||
<QuestionOutlineIcon ml={1} />
|
||||
</Tooltip>
|
||||
</Box>
|
||||
<MySlider
|
||||
markList={[
|
||||
{ label: '0', value: 0 },
|
||||
{ label: '1', value: 1 }
|
||||
]}
|
||||
min={0}
|
||||
max={1}
|
||||
step={0.01}
|
||||
value={getValues('searchSimilarity')}
|
||||
onChange={(val) => {
|
||||
setValue('searchSimilarity', val);
|
||||
setRefresh(!refresh);
|
||||
}}
|
||||
/>
|
||||
</Flex>
|
||||
<Flex py={8}>
|
||||
<Box flex={'0 0 100px'}>单次搜索数量</Box>
|
||||
<Box flex={1}>
|
||||
<MySlider
|
||||
markList={[
|
||||
{ label: '1', value: 1 },
|
||||
{ label: '20', value: 20 }
|
||||
]}
|
||||
min={1}
|
||||
max={20}
|
||||
value={getValues('searchLimit')}
|
||||
onChange={(val) => {
|
||||
setValue('searchLimit', val);
|
||||
setRefresh(!refresh);
|
||||
}}
|
||||
/>
|
||||
</Box>
|
||||
</Flex>
|
||||
<Flex pt={3}>
|
||||
<Box flex={'0 0 100px'}>空搜索回复</Box>
|
||||
<Box flex={1}>
|
||||
<Textarea
|
||||
rows={5}
|
||||
maxLength={500}
|
||||
placeholder={
|
||||
'若填写该内容,没有搜索到对应内容时,将直接回复填写的内容。\n为了连贯上下文,FastGpt 会取部分上一个聊天的搜索记录作为补充,因此在连续对话时,该功能可能会失效。'
|
||||
}
|
||||
{...register('searchEmptyText')}
|
||||
></Textarea>
|
||||
</Box>
|
||||
</Flex>
|
||||
</ModalBody>
|
||||
<ModalFooter>
|
||||
<Button variant={'base'} mr={3} onClick={onCloseEditParams}>
|
||||
取消
|
||||
</Button>
|
||||
<Button
|
||||
onClick={() => {
|
||||
onCloseEditParams();
|
||||
onchangeKb({
|
||||
searchSimilarity: getValues('searchSimilarity'),
|
||||
searchLimit: getValues('searchLimit'),
|
||||
searchEmptyText: getValues('searchEmptyText')
|
||||
});
|
||||
}}
|
||||
>
|
||||
完成
|
||||
</Button>
|
||||
</ModalFooter>
|
||||
</ModalContent>
|
||||
</Modal>
|
||||
<Loading loading={isLoading} fixed={false} />
|
||||
</Box>
|
||||
);
|
||||
};
|
||||
|
||||
export default Kb;
|
||||
@@ -2,7 +2,6 @@ import React, { useCallback, useState } from 'react';
|
||||
import {
|
||||
Flex,
|
||||
Box,
|
||||
Tooltip,
|
||||
Button,
|
||||
TableContainer,
|
||||
Table,
|
||||
@@ -37,6 +36,7 @@ import { formatTimeToChatTime, useCopyData, getErrText } from '@/utils/tools';
|
||||
import { useForm } from 'react-hook-form';
|
||||
import { defaultShareChat } from '@/constants/model';
|
||||
import type { ShareChatEditType } from '@/types/app';
|
||||
import MyTooltip from '@/components/MyTooltip';
|
||||
|
||||
const Share = ({ modelId }: { modelId: string }) => {
|
||||
const { toast } = useToast();
|
||||
@@ -112,9 +112,9 @@ ${e.password ? `密码为: ${e.password}` : ''}`;
|
||||
<Flex justifyContent={'space-between'}>
|
||||
<Box fontWeight={'bold'}>
|
||||
免登录聊天窗口
|
||||
<Tooltip label="可以直接分享该模型给其他用户去进行对话,对方无需登录即可直接进行对话。注意,这个功能会消耗你账号的tokens。请保管好链接和密码。">
|
||||
<MyTooltip label="可以直接分享该模型给其他用户去进行对话,对方无需登录即可直接进行对话。注意,这个功能会消耗你账号的tokens。请保管好链接和密码。">
|
||||
<QuestionOutlineIcon ml={1} />
|
||||
</Tooltip>
|
||||
</MyTooltip>
|
||||
</Box>
|
||||
<Button
|
||||
variant={'base'}
|
||||
|
||||
@@ -10,13 +10,7 @@ const NodeAnswer = ({
|
||||
data: { moduleId, inputs, outputs, onChangeNode, ...props }
|
||||
}: NodeProps<FlowModuleItemType>) => {
|
||||
return (
|
||||
<NodeCard
|
||||
minW={'400px'}
|
||||
logo={'/icon/logo.png'}
|
||||
name={'SSE 响应'}
|
||||
moduleId={moduleId}
|
||||
{...props}
|
||||
>
|
||||
<NodeCard minW={'400px'} moduleId={moduleId} {...props}>
|
||||
<Divider text="Input" />
|
||||
<Container>
|
||||
<RenderInput moduleId={moduleId} onChangeNode={onChangeNode} flowInputList={inputs} />
|
||||
|
||||
@@ -0,0 +1,136 @@
|
||||
import React from 'react';
|
||||
import { NodeProps } from 'reactflow';
|
||||
import { Box, Input, Button, Flex } from '@chakra-ui/react';
|
||||
import NodeCard from './modules/NodeCard';
|
||||
import { FlowModuleItemType } from '@/types/flow';
|
||||
import Divider from './modules/Divider';
|
||||
import Container from './modules/Container';
|
||||
import RenderInput from './render/RenderInput';
|
||||
import type { ClassifyQuestionAgentItemType } from '@/types/app';
|
||||
import { Handle, Position } from 'reactflow';
|
||||
import { customAlphabet } from 'nanoid';
|
||||
const nanoid = customAlphabet('abcdefghijklmnopqrstuvwxyz1234567890', 4);
|
||||
import MyIcon from '@/components/Icon';
|
||||
import { FlowOutputItemTypeEnum } from '@/constants/flow';
|
||||
|
||||
const NodeCQNode = ({
|
||||
data: { moduleId, inputs, outputs, onChangeNode, ...props }
|
||||
}: NodeProps<FlowModuleItemType>) => {
|
||||
return (
|
||||
<NodeCard minW={'400px'} moduleId={moduleId} {...props}>
|
||||
<Divider text="Input" />
|
||||
<Container>
|
||||
<RenderInput
|
||||
moduleId={moduleId}
|
||||
onChangeNode={onChangeNode}
|
||||
flowInputList={inputs}
|
||||
CustomComponent={{
|
||||
agents: ({
|
||||
key: agentKey,
|
||||
value: agents = []
|
||||
}: {
|
||||
key: string;
|
||||
value?: ClassifyQuestionAgentItemType[];
|
||||
}) => (
|
||||
<Box>
|
||||
{agents.map((item, i) => (
|
||||
<Flex key={item.key} mb={4} alignItems={'center'}>
|
||||
<MyIcon
|
||||
mr={2}
|
||||
name={'minus'}
|
||||
w={'14px'}
|
||||
cursor={'pointer'}
|
||||
color={'myGray.600'}
|
||||
_hover={{ color: 'myGray.900' }}
|
||||
onClick={() => {
|
||||
const newInputValue = agents.filter((input) => input.key !== item.key);
|
||||
const newOutputVal = outputs.filter((output) => output.key !== item.key);
|
||||
|
||||
onChangeNode({
|
||||
moduleId,
|
||||
type: 'inputs',
|
||||
key: agentKey,
|
||||
value: newInputValue
|
||||
});
|
||||
onChangeNode({
|
||||
moduleId,
|
||||
type: 'outputs',
|
||||
key: agentKey,
|
||||
value: newOutputVal
|
||||
});
|
||||
}}
|
||||
/>
|
||||
<Box flex={1}>
|
||||
<Box flex={1}>意图{i + 1}</Box>
|
||||
<Box position={'relative'}>
|
||||
<Input
|
||||
mt={1}
|
||||
defaultValue={item.value}
|
||||
onChange={(e) => {
|
||||
const newVal = agents.map((val) =>
|
||||
val.key === item.key
|
||||
? {
|
||||
...val,
|
||||
value: e.target.value
|
||||
}
|
||||
: val
|
||||
);
|
||||
onChangeNode({
|
||||
moduleId,
|
||||
key: agentKey,
|
||||
value: newVal
|
||||
});
|
||||
}}
|
||||
/>
|
||||
<Handle
|
||||
style={{
|
||||
top: '50%',
|
||||
right: '-14px',
|
||||
transform: 'translate(50%,-50%)',
|
||||
width: '12px',
|
||||
height: '12px',
|
||||
background: '#9CA2A8'
|
||||
}}
|
||||
type="source"
|
||||
id={item.key}
|
||||
position={Position.Right}
|
||||
/>
|
||||
</Box>
|
||||
</Box>
|
||||
</Flex>
|
||||
))}
|
||||
<Button
|
||||
onClick={() => {
|
||||
const key = nanoid();
|
||||
const newInputValue = agents.concat({ value: '', key });
|
||||
const newOutputValue = outputs.concat({
|
||||
key,
|
||||
label: '',
|
||||
type: FlowOutputItemTypeEnum.hidden,
|
||||
targets: []
|
||||
});
|
||||
onChangeNode({
|
||||
moduleId,
|
||||
type: 'inputs',
|
||||
key: agentKey,
|
||||
value: newInputValue
|
||||
});
|
||||
onChangeNode({
|
||||
moduleId,
|
||||
type: 'outputs',
|
||||
key: agentKey,
|
||||
value: newOutputValue
|
||||
});
|
||||
}}
|
||||
>
|
||||
添加意图
|
||||
</Button>
|
||||
</Box>
|
||||
)
|
||||
}}
|
||||
/>
|
||||
</Container>
|
||||
</NodeCard>
|
||||
);
|
||||
};
|
||||
export default React.memo(NodeCQNode);
|
||||
@@ -26,7 +26,7 @@ const NodeKbSearch = ({
|
||||
onChangeNode={onChangeNode}
|
||||
flowInputList={inputs}
|
||||
CustomComponent={{
|
||||
kb_ids: ({ key, value, onChangeNode }) => (
|
||||
kb_ids: ({ key, value }) => (
|
||||
<KBSelect
|
||||
relatedKbs={value}
|
||||
onChange={(e) => {
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
import React from 'react';
|
||||
import { Box, Flex } from '@chakra-ui/react';
|
||||
import React, { useRef } from 'react';
|
||||
import { Box, Flex, useOutsideClick } from '@chakra-ui/react';
|
||||
import { ModuleTemplates } from '@/constants/flow/ModuleTemplate';
|
||||
import type { AppModuleTemplateItemType } from '@/types/app';
|
||||
import type { XYPosition } from 'reactflow';
|
||||
@@ -7,62 +7,85 @@ import Avatar from '@/components/Avatar';
|
||||
|
||||
const ModuleStoreList = ({
|
||||
isOpen,
|
||||
onAddNode
|
||||
onAddNode,
|
||||
onClose
|
||||
}: {
|
||||
isOpen: boolean;
|
||||
onAddNode: (e: { template: AppModuleTemplateItemType; position: XYPosition }) => void;
|
||||
onClose: () => void;
|
||||
}) => {
|
||||
const ContextMenuRef = useRef(null);
|
||||
|
||||
useOutsideClick({
|
||||
ref: ContextMenuRef,
|
||||
handler: () => {
|
||||
onClose();
|
||||
}
|
||||
});
|
||||
|
||||
return (
|
||||
<Flex
|
||||
flexDirection={'column'}
|
||||
position={'absolute'}
|
||||
top={'65px'}
|
||||
left={0}
|
||||
h={isOpen ? '90%' : '0'}
|
||||
w={isOpen ? '360px' : '0'}
|
||||
bg={'white'}
|
||||
zIndex={1}
|
||||
boxShadow={'3px 0 20px rgba(0,0,0,0.2)'}
|
||||
borderRadius={'20px'}
|
||||
overflow={'hidden'}
|
||||
transition={'.2s ease'}
|
||||
px={'15px'}
|
||||
userSelect={'none'}
|
||||
>
|
||||
<Box w={'330px'} py={4} fontSize={'xl'} fontWeight={'bold'}>
|
||||
添加模块
|
||||
</Box>
|
||||
<Box w={'330px'} flex={'1 0 0'} overflow={'overlay'}>
|
||||
{ModuleTemplates.map((item) =>
|
||||
item.list.map((item) => (
|
||||
<Flex
|
||||
key={item.name}
|
||||
alignItems={'center'}
|
||||
p={5}
|
||||
cursor={'pointer'}
|
||||
_hover={{ bg: 'myWhite.600' }}
|
||||
borderRadius={'md'}
|
||||
draggable
|
||||
onDragEnd={(e) => {
|
||||
if (e.clientX < 400) return;
|
||||
onAddNode({
|
||||
template: item,
|
||||
position: { x: e.clientX, y: e.clientY }
|
||||
});
|
||||
}}
|
||||
>
|
||||
<Avatar src={item.logo} w={'34px'} />
|
||||
<Box ml={5} flex={'1 0 0'}>
|
||||
<Box color={'black'}>{item.name}</Box>
|
||||
<Box color={'myGray.500'} fontSize={'sm'}>
|
||||
{item.intro}
|
||||
<>
|
||||
<Box
|
||||
zIndex={2}
|
||||
display={isOpen ? 'block' : 'none'}
|
||||
position={'fixed'}
|
||||
top={0}
|
||||
left={0}
|
||||
right={0}
|
||||
bottom={0}
|
||||
></Box>
|
||||
<Flex
|
||||
zIndex={3}
|
||||
ref={ContextMenuRef}
|
||||
flexDirection={'column'}
|
||||
position={'absolute'}
|
||||
top={'65px'}
|
||||
left={0}
|
||||
h={isOpen ? '90%' : '0'}
|
||||
w={isOpen ? '360px' : '0'}
|
||||
bg={'white'}
|
||||
boxShadow={'3px 0 20px rgba(0,0,0,0.2)'}
|
||||
borderRadius={'20px'}
|
||||
overflow={'hidden'}
|
||||
transition={'.2s ease'}
|
||||
px={'15px'}
|
||||
userSelect={'none'}
|
||||
>
|
||||
<Box w={'330px'} py={4} fontSize={'xl'} fontWeight={'bold'}>
|
||||
添加模块
|
||||
</Box>
|
||||
<Box w={'330px'} flex={'1 0 0'} overflow={'overlay'}>
|
||||
{ModuleTemplates.map((item) =>
|
||||
item.list.map((item) => (
|
||||
<Flex
|
||||
key={item.name}
|
||||
alignItems={'center'}
|
||||
p={5}
|
||||
cursor={'pointer'}
|
||||
_hover={{ bg: 'myWhite.600' }}
|
||||
borderRadius={'md'}
|
||||
draggable
|
||||
onDragEnd={(e) => {
|
||||
// if (e.clientX < 400) return;
|
||||
onAddNode({
|
||||
template: item,
|
||||
position: { x: e.clientX, y: e.clientY }
|
||||
});
|
||||
}}
|
||||
>
|
||||
<Avatar src={item.logo} w={'34px'} borderRadius={'0'} />
|
||||
<Box ml={5} flex={'1 0 0'}>
|
||||
<Box color={'black'}>{item.name}</Box>
|
||||
<Box color={'myGray.500'} fontSize={'sm'}>
|
||||
{item.intro}
|
||||
</Box>
|
||||
</Box>
|
||||
</Box>
|
||||
</Flex>
|
||||
))
|
||||
)}
|
||||
</Box>
|
||||
</Flex>
|
||||
</Flex>
|
||||
))
|
||||
)}
|
||||
</Box>
|
||||
</Flex>
|
||||
</>
|
||||
);
|
||||
};
|
||||
|
||||
|
||||
@@ -1,6 +1,7 @@
|
||||
import React from 'react';
|
||||
import { Box, Tooltip } from '@chakra-ui/react';
|
||||
import { Box } from '@chakra-ui/react';
|
||||
import { QuestionOutlineIcon } from '@chakra-ui/icons';
|
||||
import MyTooltip from '@/components/MyTooltip';
|
||||
|
||||
const Label = ({
|
||||
required = false,
|
||||
@@ -19,9 +20,9 @@ const Label = ({
|
||||
</Box>
|
||||
)}
|
||||
{description && (
|
||||
<Tooltip label={description}>
|
||||
<MyTooltip label={description}>
|
||||
<QuestionOutlineIcon display={['none', 'inline']} fontSize={'12px'} mb={1} ml={1} />
|
||||
</Tooltip>
|
||||
</MyTooltip>
|
||||
)}
|
||||
</Box>
|
||||
);
|
||||
|
||||
@@ -4,7 +4,6 @@ import {
|
||||
Box,
|
||||
Textarea,
|
||||
Input,
|
||||
Tooltip,
|
||||
NumberInput,
|
||||
NumberInputField,
|
||||
NumberInputStepper,
|
||||
@@ -16,6 +15,7 @@ import { QuestionOutlineIcon } from '@chakra-ui/icons';
|
||||
import { Handle, Position } from 'reactflow';
|
||||
import MySelect from '@/components/Select';
|
||||
import MySlider from '@/components/Slider';
|
||||
import MyTooltip from '@/components/MyTooltip';
|
||||
|
||||
const Label = ({
|
||||
required = false,
|
||||
@@ -34,9 +34,9 @@ const Label = ({
|
||||
</Box>
|
||||
)}
|
||||
{description && (
|
||||
<Tooltip label={description}>
|
||||
<MyTooltip label={description}>
|
||||
<QuestionOutlineIcon display={['none', 'inline']} ml={1} />
|
||||
</Tooltip>
|
||||
</MyTooltip>
|
||||
)}
|
||||
</Box>
|
||||
);
|
||||
@@ -49,14 +49,7 @@ const RenderBody = ({
|
||||
}: {
|
||||
flowInputList: FlowInputItemType[];
|
||||
moduleId: string;
|
||||
CustomComponent?: Record<
|
||||
string,
|
||||
(e: {
|
||||
key: string;
|
||||
value: any;
|
||||
onChangeNode: FlowModuleItemType['onChangeNode'];
|
||||
}) => React.ReactNode
|
||||
>;
|
||||
CustomComponent?: Record<string, (e: FlowInputItemType) => React.ReactNode>;
|
||||
onChangeNode: FlowModuleItemType['onChangeNode'];
|
||||
}) => {
|
||||
return (
|
||||
@@ -65,9 +58,11 @@ const RenderBody = ({
|
||||
(item) =>
|
||||
item.type !== FlowInputItemTypeEnum.hidden && (
|
||||
<Box key={item.key} _notLast={{ mb: 7 }} position={'relative'}>
|
||||
<Label required={item.required} description={item.description}>
|
||||
{item.label}
|
||||
</Label>
|
||||
{!!item.label && (
|
||||
<Label required={item.required} description={item.description}>
|
||||
{item.label}
|
||||
</Label>
|
||||
)}
|
||||
<Box mt={2} className={'nodrag'}>
|
||||
{item.type === FlowInputItemTypeEnum.numberInput && (
|
||||
<NumberInput
|
||||
@@ -151,9 +146,7 @@ const RenderBody = ({
|
||||
</Box>
|
||||
)}
|
||||
{item.type === FlowInputItemTypeEnum.custom && CustomComponent[item.key] && (
|
||||
<>
|
||||
{CustomComponent[item.key]({ key: item.key, value: item.value, onChangeNode })}
|
||||
</>
|
||||
<>{CustomComponent[item.key]({ ...item })}</>
|
||||
)}
|
||||
{item.type === FlowInputItemTypeEnum.target && (
|
||||
<Handle
|
||||
|
||||
@@ -1,9 +1,10 @@
|
||||
import React from 'react';
|
||||
import type { FlowOutputItemType } from '@/types/flow';
|
||||
import { Box, Tooltip, Flex } from '@chakra-ui/react';
|
||||
import { Box, Flex } from '@chakra-ui/react';
|
||||
import { FlowOutputItemTypeEnum } from '@/constants/flow';
|
||||
import { QuestionOutlineIcon } from '@chakra-ui/icons';
|
||||
import { Handle, Position } from 'reactflow';
|
||||
import MyTooltip from '@/components/MyTooltip';
|
||||
|
||||
const Label = ({
|
||||
children,
|
||||
@@ -14,9 +15,9 @@ const Label = ({
|
||||
}) => (
|
||||
<Flex as={'label'} justifyContent={'right'} alignItems={'center'} position={'relative'}>
|
||||
{description && (
|
||||
<Tooltip label={description}>
|
||||
<MyTooltip label={description}>
|
||||
<QuestionOutlineIcon display={['none', 'inline']} mr={1} />
|
||||
</Tooltip>
|
||||
</MyTooltip>
|
||||
)}
|
||||
{children}
|
||||
</Flex>
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
import React, { useCallback, useEffect } from 'react';
|
||||
import React, { useCallback, useEffect, useRef } from 'react';
|
||||
import ReactFlow, {
|
||||
Background,
|
||||
Controls,
|
||||
@@ -7,7 +7,8 @@ import ReactFlow, {
|
||||
useNodesState,
|
||||
useEdgesState,
|
||||
XYPosition,
|
||||
Connection
|
||||
Connection,
|
||||
useViewport
|
||||
} from 'reactflow';
|
||||
import { Box, Flex, IconButton, useTheme, useDisclosure } from '@chakra-ui/react';
|
||||
import { SmallCloseIcon } from '@chakra-ui/icons';
|
||||
@@ -47,6 +48,9 @@ const NodeQuestionInput = dynamic(() => import('./components/NodeQuestionInput')
|
||||
const TemplateList = dynamic(() => import('./components/TemplateList'), {
|
||||
ssr: false
|
||||
});
|
||||
const NodeCQNode = dynamic(() => import('./components/NodeCQNode'), {
|
||||
ssr: false
|
||||
});
|
||||
|
||||
import 'reactflow/dist/style.css';
|
||||
import styles from './index.module.scss';
|
||||
@@ -60,14 +64,18 @@ const nodeTypes = {
|
||||
[FlowModuleTypeEnum.chatNode]: NodeChat,
|
||||
[FlowModuleTypeEnum.kbSearchNode]: NodeKbSearch,
|
||||
[FlowModuleTypeEnum.tfSwitchNode]: NodeTFSwitch,
|
||||
[FlowModuleTypeEnum.answerNode]: NodeAnswer
|
||||
[FlowModuleTypeEnum.answerNode]: NodeAnswer,
|
||||
[FlowModuleTypeEnum.classifyQuestionNode]: NodeCQNode
|
||||
};
|
||||
const edgeTypes = {
|
||||
buttonedge: ButtonEdge
|
||||
};
|
||||
type Props = { app: AppSchema; onBack: () => void };
|
||||
|
||||
const AppEdit = ({ app, onBack }: { app: AppSchema; onBack: () => void }) => {
|
||||
const AppEdit = ({ app, onBack }: Props) => {
|
||||
const reactFlowWrapper = useRef<HTMLDivElement>(null);
|
||||
const theme = useTheme();
|
||||
const { x, y, zoom } = useViewport();
|
||||
const [nodes, setNodes, onNodesChange] = useNodesState<FlowModuleItemType>([]);
|
||||
const [edges, setEdges, onEdgesChange] = useEdgesState([]);
|
||||
const {
|
||||
@@ -77,24 +85,33 @@ const AppEdit = ({ app, onBack }: { app: AppSchema; onBack: () => void }) => {
|
||||
} = useDisclosure();
|
||||
|
||||
const onChangeNode = useCallback(
|
||||
({ moduleId, key, value, valueKey = 'value' }: FlowModuleItemChangeProps) => {
|
||||
({ moduleId, key, type = 'inputs', value, valueKey = 'value' }: FlowModuleItemChangeProps) => {
|
||||
setNodes((nodes) =>
|
||||
nodes.map((node) => {
|
||||
if (node.id !== moduleId) return node;
|
||||
if (type === 'inputs') {
|
||||
return {
|
||||
...node,
|
||||
data: {
|
||||
...node.data,
|
||||
inputs: node.data.inputs.map((item) => {
|
||||
if (item.key === key) {
|
||||
return {
|
||||
...item,
|
||||
[valueKey]: value
|
||||
};
|
||||
}
|
||||
return item;
|
||||
})
|
||||
}
|
||||
};
|
||||
}
|
||||
|
||||
return {
|
||||
...node,
|
||||
data: {
|
||||
...node.data,
|
||||
inputs: node.data.inputs.map((item) => {
|
||||
if (item.key === key) {
|
||||
return {
|
||||
...item,
|
||||
[valueKey]: value
|
||||
};
|
||||
}
|
||||
return item;
|
||||
})
|
||||
outputs: value
|
||||
}
|
||||
};
|
||||
})
|
||||
@@ -111,12 +128,17 @@ const AppEdit = ({ app, onBack }: { app: AppSchema; onBack: () => void }) => {
|
||||
);
|
||||
const onAddNode = useCallback(
|
||||
({ template, position }: { template: AppModuleTemplateItemType; position: XYPosition }) => {
|
||||
if (!reactFlowWrapper.current) return;
|
||||
const reactFlowBounds = reactFlowWrapper.current.getBoundingClientRect();
|
||||
const mouseX = (position.x - reactFlowBounds.left - x) / zoom - 100;
|
||||
const mouseY = (position.y - reactFlowBounds.top - y) / zoom;
|
||||
|
||||
setNodes((state) =>
|
||||
state.concat(
|
||||
appModule2FlowNode({
|
||||
item: {
|
||||
...template,
|
||||
position,
|
||||
position: { x: mouseX, y: mouseY },
|
||||
moduleId: nanoid()
|
||||
},
|
||||
onChangeNode,
|
||||
@@ -125,7 +147,7 @@ const AppEdit = ({ app, onBack }: { app: AppSchema; onBack: () => void }) => {
|
||||
)
|
||||
);
|
||||
},
|
||||
[onChangeNode, onDelNode, setNodes]
|
||||
[onChangeNode, onDelNode, setNodes, x, y, zoom]
|
||||
);
|
||||
|
||||
const onDelConnect = useCallback(
|
||||
@@ -245,6 +267,13 @@ const AppEdit = ({ app, onBack }: { app: AppSchema; onBack: () => void }) => {
|
||||
borderRadius={'lg'}
|
||||
isLoading={isLoading}
|
||||
aria-label={'save'}
|
||||
bg={'myBlue.200'}
|
||||
variant={'base'}
|
||||
border={'none'}
|
||||
color={'myGray.900'}
|
||||
_hover={{
|
||||
bg: 'myBlue.300'
|
||||
}}
|
||||
onClick={onclickSave}
|
||||
/>
|
||||
</Flex>
|
||||
@@ -270,43 +299,50 @@ const AppEdit = ({ app, onBack }: { app: AppSchema; onBack: () => void }) => {
|
||||
transition={'0.2s ease'}
|
||||
aria-label={''}
|
||||
zIndex={1}
|
||||
boxShadow={'1px 1px 6px #4e83fd'}
|
||||
onClick={() => (isOpenTemplate ? onCloseTemplate() : onOpenTemplate())}
|
||||
boxShadow={'2px 2px 6px #85b1ff'}
|
||||
onClick={() => {
|
||||
isOpenTemplate ? onCloseTemplate() : onOpenTemplate();
|
||||
}}
|
||||
/>
|
||||
<ReactFlowProvider>
|
||||
<ReactFlow
|
||||
className={styles.panel}
|
||||
nodes={nodes}
|
||||
edges={edges}
|
||||
minZoom={0.4}
|
||||
maxZoom={1.5}
|
||||
fitView
|
||||
defaultEdgeOptions={edgeOptions}
|
||||
connectionLineStyle={connectionLineStyle}
|
||||
nodeTypes={nodeTypes}
|
||||
edgeTypes={edgeTypes}
|
||||
onNodesChange={onNodesChange}
|
||||
onEdgesChange={onEdgesChange}
|
||||
onConnect={(connect) => {
|
||||
connect.sourceHandle &&
|
||||
connect.targetHandle &&
|
||||
onConnect({
|
||||
connect
|
||||
});
|
||||
}}
|
||||
>
|
||||
<Background />
|
||||
<Controls
|
||||
position={'bottom-center'}
|
||||
style={{ display: 'flex' }}
|
||||
showInteractive={false}
|
||||
/>
|
||||
</ReactFlow>
|
||||
</ReactFlowProvider>
|
||||
<TemplateList isOpen={isOpenTemplate} onAddNode={onAddNode} />
|
||||
<ReactFlow
|
||||
ref={reactFlowWrapper}
|
||||
className={styles.panel}
|
||||
nodes={nodes}
|
||||
edges={edges}
|
||||
minZoom={0.4}
|
||||
maxZoom={1.5}
|
||||
fitView
|
||||
defaultEdgeOptions={edgeOptions}
|
||||
connectionLineStyle={connectionLineStyle}
|
||||
nodeTypes={nodeTypes}
|
||||
edgeTypes={edgeTypes}
|
||||
onNodesChange={onNodesChange}
|
||||
onEdgesChange={onEdgesChange}
|
||||
onConnect={(connect) => {
|
||||
connect.sourceHandle &&
|
||||
connect.targetHandle &&
|
||||
onConnect({
|
||||
connect
|
||||
});
|
||||
}}
|
||||
>
|
||||
<Background />
|
||||
<Controls
|
||||
position={'bottom-center'}
|
||||
style={{ display: 'flex' }}
|
||||
showInteractive={false}
|
||||
/>
|
||||
</ReactFlow>
|
||||
<TemplateList isOpen={isOpenTemplate} onAddNode={onAddNode} onClose={onCloseTemplate} />
|
||||
</Box>
|
||||
</Flex>
|
||||
);
|
||||
};
|
||||
|
||||
export default AppEdit;
|
||||
const Flow = (data: Props) => (
|
||||
<ReactFlowProvider>
|
||||
<AppEdit {...data} />
|
||||
</ReactFlowProvider>
|
||||
);
|
||||
|
||||
export default Flow;
|
||||
|
||||
@@ -173,7 +173,7 @@ const CreateModal = ({ onClose, onSuccess }: { onClose: () => void; onSuccess: (
|
||||
boxShadow={'sm'}
|
||||
{...(getValues('templateId') === item.id
|
||||
? {
|
||||
bg: 'myBlue.300'
|
||||
bg: 'myWhite.600'
|
||||
}
|
||||
: {
|
||||
_hover: {
|
||||
|
||||
@@ -120,7 +120,13 @@ const MyApps = () => {
|
||||
}}
|
||||
/>
|
||||
</Flex>
|
||||
<Box className={styles.intro} py={2} fontSize={'sm'} color={'myGray.600'}>
|
||||
<Box
|
||||
className={styles.intro}
|
||||
py={2}
|
||||
wordBreak={'break-all'}
|
||||
fontSize={'sm'}
|
||||
color={'myGray.600'}
|
||||
>
|
||||
{app.intro || '这个应用还没写介绍~'}
|
||||
</Box>
|
||||
</Card>
|
||||
|
||||
Reference in New Issue
Block a user