feat: agent and ui

This commit is contained in:
archer
2023-07-06 19:35:02 +08:00
parent 46f20c7dc3
commit 23642af6e2
44 changed files with 588 additions and 1148 deletions

View File

@@ -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;

View File

@@ -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'}

View File

@@ -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} />

View File

@@ -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);

View File

@@ -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) => {

View File

@@ -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>
</>
);
};

View File

@@ -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>
);

View File

@@ -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

View File

@@ -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>

View File

@@ -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;

View File

@@ -173,7 +173,7 @@ const CreateModal = ({ onClose, onSuccess }: { onClose: () => void; onSuccess: (
boxShadow={'sm'}
{...(getValues('templateId') === item.id
? {
bg: 'myBlue.300'
bg: 'myWhite.600'
}
: {
_hover: {

View File

@@ -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>