feat: app ui
This commit is contained in:
@@ -24,7 +24,7 @@ const API = ({ modelId }: { modelId: string }) => {
|
||||
}, []);
|
||||
|
||||
return (
|
||||
<Flex flexDirection={'column'} h={'100%'}>
|
||||
<Flex flexDirection={'column'} pt={[0, 5]} h={'100%'}>
|
||||
<Box display={['none', 'flex']} px={5} alignItems={'center'}>
|
||||
<Box flex={1}>
|
||||
AppId:
|
||||
|
||||
@@ -161,7 +161,9 @@ const Settings = ({ modelId }: { modelId: string }) => {
|
||||
position={'relative'}
|
||||
maxW={['auto', '800px']}
|
||||
>
|
||||
<Box fontSize={['md', 'xl']}>基本信息</Box>
|
||||
<Box fontSize={['md', 'xl']} fontWeight={'bold'}>
|
||||
基本信息
|
||||
</Box>
|
||||
<Flex mt={5} alignItems={'center'}>
|
||||
<Box w={['60px', '100px', '140px']} flexShrink={0}>
|
||||
头像
|
||||
|
||||
@@ -108,7 +108,7 @@ ${e.password ? `密码为: ${e.password}` : ''}`;
|
||||
};
|
||||
|
||||
return (
|
||||
<Box position={'relative'} px={5} minH={'50vh'}>
|
||||
<Box position={'relative'} pt={[0, 5]} px={5} minH={'50vh'}>
|
||||
<Flex justifyContent={'space-between'}>
|
||||
<Box fontWeight={'bold'}>
|
||||
免登录聊天窗口
|
||||
|
||||
@@ -0,0 +1,66 @@
|
||||
import { AppModuleItemType } from '@/types/app';
|
||||
import { AppSchema } from '@/types/mongoSchema';
|
||||
import React, { useState, useEffect, useRef, useMemo } from 'react';
|
||||
import { Box, useOutsideClick, Flex, IconButton } from '@chakra-ui/react';
|
||||
import MyIcon from '@/components/Icon';
|
||||
import { useChat } from '@/hooks/useChat';
|
||||
|
||||
const ChatTest = ({
|
||||
app,
|
||||
modules,
|
||||
onClose
|
||||
}: {
|
||||
app: AppSchema;
|
||||
modules?: AppModuleItemType[];
|
||||
onClose: () => void;
|
||||
}) => {
|
||||
const isOpen = useMemo(() => !!modules, [modules]);
|
||||
|
||||
const { ChatBox, ChatInput, ChatBoxParentRef, setChatHistory } = useChat({
|
||||
appId: app._id
|
||||
});
|
||||
|
||||
return (
|
||||
<Flex
|
||||
zIndex={3}
|
||||
flexDirection={'column'}
|
||||
position={'absolute'}
|
||||
top={5}
|
||||
right={0}
|
||||
h={isOpen ? '95%' : '0'}
|
||||
w={isOpen ? '460px' : '0'}
|
||||
bg={'white'}
|
||||
boxShadow={'3px 0 20px rgba(0,0,0,0.2)'}
|
||||
borderRadius={'md'}
|
||||
overflow={'hidden'}
|
||||
transition={'.2s ease'}
|
||||
>
|
||||
<Flex py={4} px={5} whiteSpace={'nowrap'}>
|
||||
<Box fontSize={'xl'} fontWeight={'bold'} flex={1}>
|
||||
调试预览
|
||||
</Box>
|
||||
<IconButton
|
||||
className="chat"
|
||||
size={'sm'}
|
||||
icon={<MyIcon name={'clearLight'} w={'14px'} />}
|
||||
variant={'base'}
|
||||
borderRadius={'md'}
|
||||
aria-label={'delete'}
|
||||
onClick={(e) => {
|
||||
e.stopPropagation();
|
||||
setChatHistory([]);
|
||||
}}
|
||||
/>
|
||||
</Flex>
|
||||
<Box ref={ChatBoxParentRef} flex={1} px={5} overflow={'overlay'}>
|
||||
<ChatBox appAvatar={app.avatar} />
|
||||
</Box>
|
||||
|
||||
<Box px={5}>
|
||||
<ChatInput />
|
||||
</Box>
|
||||
</Flex>
|
||||
);
|
||||
};
|
||||
|
||||
export default ChatTest;
|
||||
@@ -1,4 +1,4 @@
|
||||
import React, { useCallback, useEffect, useRef } from 'react';
|
||||
import React, { useCallback, useEffect, useRef, useState } from 'react';
|
||||
import ReactFlow, {
|
||||
Background,
|
||||
Controls,
|
||||
@@ -27,6 +27,7 @@ import dynamic from 'next/dynamic';
|
||||
|
||||
import MyIcon from '@/components/Icon';
|
||||
import ButtonEdge from './components/modules/ButtonEdge';
|
||||
import MyTooltip from '@/components/MyTooltip';
|
||||
const NodeChat = dynamic(() => import('./components/NodeChat'), {
|
||||
ssr: false
|
||||
});
|
||||
@@ -48,13 +49,16 @@ const NodeQuestionInput = dynamic(() => import('./components/NodeQuestionInput')
|
||||
const TemplateList = dynamic(() => import('./components/TemplateList'), {
|
||||
ssr: false
|
||||
});
|
||||
const ChatTest = dynamic(() => import('./components/ChatTest'), {
|
||||
ssr: false
|
||||
});
|
||||
const NodeCQNode = dynamic(() => import('./components/NodeCQNode'), {
|
||||
ssr: false
|
||||
});
|
||||
|
||||
import 'reactflow/dist/style.css';
|
||||
import styles from './index.module.scss';
|
||||
import { AppModuleTemplateItemType } from '@/types/app';
|
||||
import { AppModuleItemType, AppModuleTemplateItemType } from '@/types/app';
|
||||
|
||||
const nanoid = customAlphabet('abcdefghijklmnopqrstuvwxyz1234567890', 6);
|
||||
|
||||
@@ -83,6 +87,7 @@ const AppEdit = ({ app, onBack }: Props) => {
|
||||
onOpen: onOpenTemplate,
|
||||
onClose: onCloseTemplate
|
||||
} = useDisclosure();
|
||||
const [testModules, setTestModules] = useState<AppModuleItemType[]>();
|
||||
|
||||
const onChangeNode = useCallback(
|
||||
({ moduleId, key, type = 'inputs', value, valueKey = 'value' }: FlowModuleItemChangeProps) => {
|
||||
@@ -149,6 +154,41 @@ const AppEdit = ({ app, onBack }: Props) => {
|
||||
},
|
||||
[onChangeNode, onDelNode, setNodes, x, y, zoom]
|
||||
);
|
||||
const flow2Modules = useCallback(() => {
|
||||
const modules: AppModuleItemType[] = nodes.map((item) => ({
|
||||
...item.data,
|
||||
position: item.position,
|
||||
onChangeNode: undefined,
|
||||
onDelNode: undefined,
|
||||
outputs: item.data.outputs.map((output) => ({
|
||||
...output,
|
||||
targets: [] as FlowOutputTargetItemType[]
|
||||
}))
|
||||
}));
|
||||
|
||||
// update inputs and outputs
|
||||
modules.forEach((module) => {
|
||||
module.inputs.forEach((input) => {
|
||||
input.connected = !!edges.find(
|
||||
(edge) => edge.target === module.moduleId && edge.targetHandle === input.key
|
||||
);
|
||||
});
|
||||
module.outputs.forEach((output) => {
|
||||
output.targets = edges
|
||||
.filter(
|
||||
(edge) =>
|
||||
edge.source === module.moduleId &&
|
||||
edge.sourceHandle === output.key &&
|
||||
edge.targetHandle
|
||||
)
|
||||
.map((edge) => ({
|
||||
moduleId: edge.target,
|
||||
key: edge.targetHandle || ''
|
||||
}));
|
||||
});
|
||||
});
|
||||
return modules;
|
||||
}, [edges, nodes]);
|
||||
|
||||
const onDelConnect = useCallback(
|
||||
(id: string) => {
|
||||
@@ -177,42 +217,8 @@ const AppEdit = ({ app, onBack }: Props) => {
|
||||
|
||||
const { mutate: onclickSave, isLoading } = useRequest({
|
||||
mutationFn: () => {
|
||||
const modules = nodes.map((item) => ({
|
||||
...item.data,
|
||||
position: item.position,
|
||||
onChangeNode: undefined,
|
||||
onDelNode: undefined,
|
||||
outputs: item.data.outputs.map((output) => ({
|
||||
...output,
|
||||
targets: [] as FlowOutputTargetItemType[]
|
||||
}))
|
||||
}));
|
||||
console.log(modules);
|
||||
|
||||
// update inputs and outputs
|
||||
modules.forEach((module) => {
|
||||
module.inputs.forEach((input) => {
|
||||
input.connected = !!edges.find(
|
||||
(edge) => edge.target === module.moduleId && edge.targetHandle === input.key
|
||||
);
|
||||
});
|
||||
module.outputs.forEach((output) => {
|
||||
output.targets = edges
|
||||
.filter(
|
||||
(edge) =>
|
||||
edge.source === module.moduleId &&
|
||||
edge.sourceHandle === output.key &&
|
||||
edge.targetHandle
|
||||
)
|
||||
.map((edge) => ({
|
||||
moduleId: edge.target,
|
||||
key: edge.targetHandle || ''
|
||||
}));
|
||||
});
|
||||
});
|
||||
|
||||
return putAppById(app._id, {
|
||||
modules
|
||||
modules: flow2Modules()
|
||||
});
|
||||
},
|
||||
successToast: '保存配置成功',
|
||||
@@ -244,10 +250,11 @@ const AppEdit = ({ app, onBack }: Props) => {
|
||||
|
||||
useEffect(() => {
|
||||
initData(JSON.parse(JSON.stringify(app)));
|
||||
}, [app]);
|
||||
}, [app, initData]);
|
||||
|
||||
return (
|
||||
<Flex h={'100%'} flexDirection={'column'} bg={'#fff'}>
|
||||
{/* header */}
|
||||
<Flex py={3} px={5} borderBottom={theme.borders.base} alignItems={'center'}>
|
||||
<IconButton
|
||||
size={'sm'}
|
||||
@@ -263,20 +270,42 @@ const AppEdit = ({ app, onBack }: Props) => {
|
||||
<Box ml={5} fontSize={'xl'} flex={1}>
|
||||
{app.name}
|
||||
</Box>
|
||||
<IconButton
|
||||
icon={<MyIcon name={'save'} w={'16px'} />}
|
||||
borderRadius={'lg'}
|
||||
isLoading={isLoading}
|
||||
aria-label={'save'}
|
||||
bg={'myBlue.200'}
|
||||
variant={'base'}
|
||||
border={'none'}
|
||||
color={'myGray.900'}
|
||||
_hover={{
|
||||
bg: 'myBlue.300'
|
||||
}}
|
||||
onClick={onclickSave}
|
||||
/>
|
||||
{testModules ? (
|
||||
<IconButton
|
||||
mr={6}
|
||||
icon={<SmallCloseIcon fontSize={'25px'} />}
|
||||
variant={'base'}
|
||||
color={'myGray.600'}
|
||||
borderRadius={'lg'}
|
||||
aria-label={''}
|
||||
onClick={() => setTestModules(undefined)}
|
||||
/>
|
||||
) : (
|
||||
<MyTooltip label={'测试对话'}>
|
||||
<IconButton
|
||||
mr={6}
|
||||
icon={<MyIcon name={'chatLight'} w={'16px'} />}
|
||||
borderRadius={'lg'}
|
||||
aria-label={'save'}
|
||||
variant={'base'}
|
||||
onClick={() => {
|
||||
// @ts-ignore
|
||||
onclickSave();
|
||||
setTestModules(flow2Modules());
|
||||
}}
|
||||
/>
|
||||
</MyTooltip>
|
||||
)}
|
||||
|
||||
<MyTooltip label={'保存配置'}>
|
||||
<IconButton
|
||||
icon={<MyIcon name={'save'} w={'16px'} />}
|
||||
borderRadius={'lg'}
|
||||
isLoading={isLoading}
|
||||
aria-label={'save'}
|
||||
onClick={onclickSave}
|
||||
/>
|
||||
</MyTooltip>
|
||||
</Flex>
|
||||
<Box
|
||||
flex={'1 0 0'}
|
||||
@@ -288,6 +317,7 @@ const AppEdit = ({ app, onBack }: Props) => {
|
||||
return false;
|
||||
}}
|
||||
>
|
||||
{/* open module template */}
|
||||
<IconButton
|
||||
position={'absolute'}
|
||||
top={5}
|
||||
@@ -334,7 +364,9 @@ const AppEdit = ({ app, onBack }: Props) => {
|
||||
showInteractive={false}
|
||||
/>
|
||||
</ReactFlow>
|
||||
|
||||
<TemplateList isOpen={isOpenTemplate} onAddNode={onAddNode} onClose={onCloseTemplate} />
|
||||
<ChatTest modules={testModules} app={app} onClose={() => setTestModules(undefined)} />
|
||||
</Box>
|
||||
</Flex>
|
||||
);
|
||||
|
||||
@@ -1,14 +1,15 @@
|
||||
import React, { useState, useEffect, useMemo, useCallback, useRef } from 'react';
|
||||
import { useRouter } from 'next/router';
|
||||
import { Box, Flex, useTheme } from '@chakra-ui/react';
|
||||
import { Box, Flex, IconButton, useTheme } from '@chakra-ui/react';
|
||||
import { useUserStore } from '@/store/user';
|
||||
import dynamic from 'next/dynamic';
|
||||
import { defaultApp } from '@/constants/model';
|
||||
|
||||
import Tabs from '@/components/Tabs';
|
||||
import SlideTabs from '@/components/SlideTabs';
|
||||
import Settings from './components/Settings';
|
||||
import { defaultApp } from '@/constants/model';
|
||||
import Avatar from '@/components/Avatar';
|
||||
import MyIcon from '@/components/Icon';
|
||||
import PageContainer from '@/components/PageContainer';
|
||||
|
||||
const EditApp = dynamic(() => import('./components/edit'), {
|
||||
@@ -54,11 +55,11 @@ const AppDetail = ({ currentTab }: { currentTab: `${TabEnum}` }) => {
|
||||
|
||||
const tabList = useMemo(
|
||||
() => [
|
||||
{ label: '基础信息', id: TabEnum.settings, icon: 'text' },
|
||||
...(isOwner ? [{ label: '编排', id: TabEnum.edit, icon: 'edit' }] : []),
|
||||
{ label: '分享', id: TabEnum.share, icon: 'edit' },
|
||||
{ label: 'API', id: TabEnum.API, icon: 'edit' },
|
||||
{ label: '立即对话', id: 'startChat', icon: 'chat' }
|
||||
{ label: '概览', id: TabEnum.settings, icon: 'overviewLight' },
|
||||
...(isOwner ? [{ label: '高级设置', id: TabEnum.edit, icon: 'settingLight' }] : []),
|
||||
{ label: '链接分享', id: TabEnum.share, icon: 'shareLight' },
|
||||
{ label: 'API访问', id: TabEnum.API, icon: 'apiLight' },
|
||||
{ label: '立即对话', id: 'startChat', icon: 'chatLight' }
|
||||
],
|
||||
[isOwner]
|
||||
);
|
||||
@@ -82,7 +83,13 @@ const AppDetail = ({ currentTab }: { currentTab: `${TabEnum}` }) => {
|
||||
<PageContainer>
|
||||
<Box display={['block', 'flex']} h={'100%'}>
|
||||
{/* pc tab */}
|
||||
<Box display={['none', 'block']} p={4} w={'200px'} borderRight={theme.borders.base}>
|
||||
<Box
|
||||
display={['none', 'flex']}
|
||||
flexDirection={'column'}
|
||||
p={4}
|
||||
w={'200px'}
|
||||
borderRight={theme.borders.base}
|
||||
>
|
||||
<Flex mb={4} alignItems={'center'}>
|
||||
<Avatar src={appDetail.avatar} w={'34px'} borderRadius={'lg'} />
|
||||
<Box ml={2} fontWeight={'bold'}>
|
||||
@@ -90,6 +97,7 @@ const AppDetail = ({ currentTab }: { currentTab: `${TabEnum}` }) => {
|
||||
</Box>
|
||||
</Flex>
|
||||
<SlideTabs
|
||||
flex={1}
|
||||
mx={'auto'}
|
||||
mt={2}
|
||||
w={'100%'}
|
||||
@@ -103,6 +111,27 @@ const AppDetail = ({ currentTab }: { currentTab: `${TabEnum}` }) => {
|
||||
}
|
||||
}}
|
||||
/>
|
||||
<Flex
|
||||
alignItems={'center'}
|
||||
cursor={'pointer'}
|
||||
py={2}
|
||||
px={3}
|
||||
borderRadius={'md'}
|
||||
_hover={{ bg: 'myGray.100' }}
|
||||
onClick={() => router.replace('/app/list')}
|
||||
>
|
||||
<IconButton
|
||||
mr={3}
|
||||
icon={<MyIcon name={'backFill'} w={'18px'} color={'myBlue.600'} />}
|
||||
bg={'white'}
|
||||
boxShadow={'1px 1px 9px rgba(0,0,0,0.15)'}
|
||||
h={'28px'}
|
||||
size={'sm'}
|
||||
borderRadius={'50%'}
|
||||
aria-label={''}
|
||||
/>
|
||||
我的应用
|
||||
</Flex>
|
||||
</Box>
|
||||
{/* phone tab */}
|
||||
<Box display={['block', 'none']} textAlign={'center'} px={5} py={3}>
|
||||
|
||||
Reference in New Issue
Block a user