mirror of
https://github.com/labring/FastGPT.git
synced 2025-07-30 10:28:42 +00:00
chat box
This commit is contained in:
30
client/src/components/ChatBox/index.module.scss
Normal file
30
client/src/components/ChatBox/index.module.scss
Normal file
@@ -0,0 +1,30 @@
|
||||
.stopIcon {
|
||||
animation: zoomStopIcon 0.4s infinite alternate;
|
||||
}
|
||||
@keyframes zoomStopIcon {
|
||||
0% {
|
||||
transform: scale(0.8);
|
||||
}
|
||||
100% {
|
||||
transform: scale(1.2);
|
||||
}
|
||||
}
|
||||
|
||||
.newChat {
|
||||
.modelListContainer {
|
||||
height: 0;
|
||||
overflow: hidden;
|
||||
}
|
||||
.modelList {
|
||||
border-radius: 6px;
|
||||
}
|
||||
&:hover {
|
||||
.modelListContainer {
|
||||
height: 60vh;
|
||||
}
|
||||
.modelList {
|
||||
box-shadow: 0 0 5px rgba($color: #000000, $alpha: 0.05);
|
||||
border: 1px solid #dee0e2;
|
||||
}
|
||||
}
|
||||
}
|
601
client/src/components/ChatBox/index.tsx
Normal file
601
client/src/components/ChatBox/index.tsx
Normal file
@@ -0,0 +1,601 @@
|
||||
import React, {
|
||||
useCallback,
|
||||
useRef,
|
||||
useState,
|
||||
useMemo,
|
||||
forwardRef,
|
||||
useImperativeHandle,
|
||||
ForwardedRef
|
||||
} from 'react';
|
||||
import { throttle } from 'lodash';
|
||||
import { ChatSiteItemType } from '@/types/chat';
|
||||
import { useToast } from '@/hooks/useToast';
|
||||
import { useCopyData, voiceBroadcast, hasVoiceApi } from '@/utils/tools';
|
||||
import { Box, Card, Flex, Input, Textarea, Button, useTheme } from '@chakra-ui/react';
|
||||
import { useUserStore } from '@/store/user';
|
||||
|
||||
import { Types } from 'mongoose';
|
||||
import { HUMAN_ICON } from '@/constants/chat';
|
||||
import Markdown from '@/components/Markdown';
|
||||
import MyIcon from '@/components/Icon';
|
||||
import Avatar from '@/components/Avatar';
|
||||
|
||||
import { adaptChatItem_openAI } from '@/utils/plugin/openai';
|
||||
import { VariableItemType } from '@/types/app';
|
||||
import { VariableInputEnum } from '@/constants/app';
|
||||
import { useForm } from 'react-hook-form';
|
||||
import MySelect from '@/components/Select';
|
||||
import { MessageItemType } from '@/pages/api/openapi/v1/chat/completions';
|
||||
import styles from './index.module.scss';
|
||||
import MyTooltip from '../MyTooltip';
|
||||
|
||||
const textareaMinH = '22px';
|
||||
export type StartChatFnProps = {
|
||||
messages: MessageItemType[];
|
||||
controller: AbortController;
|
||||
variables: Record<string, any>;
|
||||
generatingMessage: (text: string) => void;
|
||||
};
|
||||
|
||||
export type ComponentRef = {
|
||||
resetVariables: (data?: Record<string, any>) => void;
|
||||
resetHistory: (history: ChatSiteItemType[]) => void;
|
||||
};
|
||||
|
||||
const VariableLabel = ({
|
||||
required = false,
|
||||
children
|
||||
}: {
|
||||
required?: boolean;
|
||||
children: React.ReactNode | string;
|
||||
}) => (
|
||||
<Box as={'label'} display={'inline-block'} position={'relative'} mb={1}>
|
||||
{children}
|
||||
{required && (
|
||||
<Box position={'absolute'} top={'-2px'} right={'-10px'} color={'red.500'} fontWeight={'bold'}>
|
||||
*
|
||||
</Box>
|
||||
)}
|
||||
</Box>
|
||||
);
|
||||
|
||||
const ChatBox = (
|
||||
{
|
||||
appAvatar,
|
||||
variableModules,
|
||||
welcomeText,
|
||||
onUpdateVariable,
|
||||
onStartChat,
|
||||
onDelMessage
|
||||
}: {
|
||||
appAvatar: string;
|
||||
variableModules?: VariableItemType[];
|
||||
welcomeText?: string;
|
||||
onUpdateVariable?: (e: Record<string, any>) => void;
|
||||
onStartChat: (e: StartChatFnProps) => Promise<{ responseText: string }>;
|
||||
onDelMessage?: (e: { id?: string; index: number }) => void;
|
||||
},
|
||||
ref: ForwardedRef<ComponentRef>
|
||||
) => {
|
||||
const ChatBoxRef = useRef<HTMLDivElement>(null);
|
||||
const theme = useTheme();
|
||||
const { copyData } = useCopyData();
|
||||
const { toast } = useToast();
|
||||
const { userInfo } = useUserStore();
|
||||
const TextareaDom = useRef<HTMLTextAreaElement>(null);
|
||||
const controller = useRef(new AbortController());
|
||||
|
||||
const [variables, setVariables] = useState<Record<string, any>>({});
|
||||
const [chatHistory, setChatHistory] = useState<ChatSiteItemType[]>([]);
|
||||
|
||||
const isChatting = useMemo(
|
||||
() => chatHistory[chatHistory.length - 1]?.status === 'loading',
|
||||
[chatHistory]
|
||||
);
|
||||
const variableIsFinish = useMemo(() => {
|
||||
if (!variableModules || chatHistory.length > 0) return true;
|
||||
|
||||
for (let i = 0; i < variableModules.length; i++) {
|
||||
const item = variableModules[i];
|
||||
if (item.required && !variables[item.key]) {
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
return true;
|
||||
}, [chatHistory.length, variableModules, variables]);
|
||||
|
||||
const isLargeWidth = ChatBoxRef?.current?.clientWidth && ChatBoxRef?.current?.clientWidth >= 900;
|
||||
|
||||
const { register, reset, setValue, handleSubmit } = useForm<Record<string, any>>({
|
||||
defaultValues: variables
|
||||
});
|
||||
|
||||
// 滚动到底部
|
||||
const scrollToBottom = useCallback(
|
||||
(behavior: 'smooth' | 'auto' = 'smooth') => {
|
||||
if (!ChatBoxRef.current) return;
|
||||
ChatBoxRef.current.scrollTo({
|
||||
top: ChatBoxRef.current.scrollHeight,
|
||||
behavior
|
||||
});
|
||||
},
|
||||
[ChatBoxRef]
|
||||
);
|
||||
// 聊天信息生成中……获取当前滚动条位置,判断是否需要滚动到底部
|
||||
const generatingScroll = useCallback(
|
||||
throttle(() => {
|
||||
if (!ChatBoxRef.current) return;
|
||||
const isBottom =
|
||||
ChatBoxRef.current.scrollTop + ChatBoxRef.current.clientHeight + 150 >=
|
||||
ChatBoxRef.current.scrollHeight;
|
||||
|
||||
isBottom && scrollToBottom('auto');
|
||||
}, 100),
|
||||
[]
|
||||
);
|
||||
// eslint-disable-next-line react-hooks/exhaustive-deps
|
||||
const generatingMessage = useCallback(
|
||||
(text: string) => {
|
||||
setChatHistory((state) =>
|
||||
state.map((item, index) => {
|
||||
if (index !== state.length - 1) return item;
|
||||
return {
|
||||
...item,
|
||||
value: item.value + text
|
||||
};
|
||||
})
|
||||
);
|
||||
generatingScroll();
|
||||
},
|
||||
[generatingScroll, setChatHistory]
|
||||
);
|
||||
|
||||
// 复制内容
|
||||
const onclickCopy = useCallback(
|
||||
(value: string) => {
|
||||
const val = value.replace(/\n+/g, '\n');
|
||||
copyData(val);
|
||||
},
|
||||
[copyData]
|
||||
);
|
||||
|
||||
// 重置输入内容
|
||||
const resetInputVal = useCallback((val: string) => {
|
||||
if (!TextareaDom.current) return;
|
||||
|
||||
setTimeout(() => {
|
||||
/* 回到最小高度 */
|
||||
if (TextareaDom.current) {
|
||||
TextareaDom.current.value = val;
|
||||
TextareaDom.current.style.height =
|
||||
val === '' ? textareaMinH : `${TextareaDom.current.scrollHeight}px`;
|
||||
}
|
||||
}, 100);
|
||||
}, []);
|
||||
|
||||
/**
|
||||
* user confirm send prompt
|
||||
*/
|
||||
const sendPrompt = useCallback(
|
||||
async (data: Record<string, any> = {}) => {
|
||||
if (isChatting) {
|
||||
toast({
|
||||
title: '正在聊天中...请等待结束',
|
||||
status: 'warning'
|
||||
});
|
||||
return;
|
||||
}
|
||||
// get input value
|
||||
const value = TextareaDom.current?.value || '';
|
||||
const val = value.trim().replace(/\n\s*/g, '\n');
|
||||
|
||||
if (!val) {
|
||||
toast({
|
||||
title: '内容为空',
|
||||
status: 'warning'
|
||||
});
|
||||
return;
|
||||
}
|
||||
|
||||
const newChatList: ChatSiteItemType[] = [
|
||||
...chatHistory,
|
||||
{
|
||||
_id: String(new Types.ObjectId()),
|
||||
obj: 'Human',
|
||||
value: val,
|
||||
status: 'finish'
|
||||
},
|
||||
{
|
||||
_id: String(new Types.ObjectId()),
|
||||
obj: 'AI',
|
||||
value: '',
|
||||
status: 'loading'
|
||||
}
|
||||
];
|
||||
|
||||
// 插入内容
|
||||
setChatHistory(newChatList);
|
||||
|
||||
// 清空输入内容
|
||||
resetInputVal('');
|
||||
setTimeout(() => {
|
||||
scrollToBottom();
|
||||
}, 100);
|
||||
|
||||
try {
|
||||
// create abort obj
|
||||
const abortSignal = new AbortController();
|
||||
controller.current = abortSignal;
|
||||
|
||||
const messages = adaptChatItem_openAI({ messages: newChatList, reserveId: true });
|
||||
|
||||
await onStartChat({
|
||||
messages,
|
||||
controller: abortSignal,
|
||||
generatingMessage,
|
||||
variables: data
|
||||
});
|
||||
|
||||
// 设置聊天内容为完成状态
|
||||
setChatHistory((state) =>
|
||||
state.map((item, index) => {
|
||||
if (index !== state.length - 1) return item;
|
||||
return {
|
||||
...item,
|
||||
status: 'finish'
|
||||
};
|
||||
})
|
||||
);
|
||||
|
||||
setTimeout(() => {
|
||||
generatingScroll();
|
||||
TextareaDom.current?.focus();
|
||||
}, 100);
|
||||
} catch (err: any) {
|
||||
toast({
|
||||
title: typeof err === 'string' ? err : err?.message || '聊天出错了~',
|
||||
status: 'warning',
|
||||
duration: 5000,
|
||||
isClosable: true
|
||||
});
|
||||
|
||||
resetInputVal(value);
|
||||
|
||||
setChatHistory(newChatList.slice(0, newChatList.length - 2));
|
||||
}
|
||||
},
|
||||
[
|
||||
isChatting,
|
||||
chatHistory,
|
||||
setChatHistory,
|
||||
resetInputVal,
|
||||
toast,
|
||||
scrollToBottom,
|
||||
onStartChat,
|
||||
generatingMessage,
|
||||
generatingScroll
|
||||
]
|
||||
);
|
||||
|
||||
useImperativeHandle(ref, () => ({
|
||||
resetVariables(e) {
|
||||
const defaultVal: Record<string, any> = {};
|
||||
variableModules?.forEach((item) => {
|
||||
defaultVal[item.key] = '';
|
||||
});
|
||||
|
||||
reset(e || defaultVal);
|
||||
setVariables(e || defaultVal);
|
||||
},
|
||||
resetHistory(e) {
|
||||
setChatHistory(e);
|
||||
}
|
||||
}));
|
||||
|
||||
const controlIconStyle = {
|
||||
w: '14px',
|
||||
cursor: 'pointer',
|
||||
p: 1,
|
||||
bg: 'white',
|
||||
borderRadius: 'lg',
|
||||
boxShadow: '0 0 5px rgba(0,0,0,0.1)',
|
||||
border: theme.borders.base,
|
||||
mr: 3
|
||||
};
|
||||
const controlContainerStyle = {
|
||||
className: 'control',
|
||||
display: ['flex', 'none'],
|
||||
color: 'myGray.400',
|
||||
pl: 1,
|
||||
mt: 2,
|
||||
position: 'absolute' as any,
|
||||
zIndex: 1
|
||||
};
|
||||
|
||||
return (
|
||||
<Flex flexDirection={'column'} h={'100%'}>
|
||||
<Box ref={ChatBoxRef} flex={'1 0 0'} overflow={'overlay'} px={[2, 5]}>
|
||||
{/* variable input */}
|
||||
{(variableModules || welcomeText) && (
|
||||
<Flex alignItems={'flex-start'} py={2}>
|
||||
{/* avatar */}
|
||||
<Avatar
|
||||
src={appAvatar}
|
||||
w={isLargeWidth ? '34px' : '24px'}
|
||||
h={isLargeWidth ? '34px' : '24px'}
|
||||
order={1}
|
||||
mr={['6px', 2]}
|
||||
/>
|
||||
{/* message */}
|
||||
<Flex order={2} pt={2} maxW={`calc(100% - ${isLargeWidth ? '75px' : '58px'})`}>
|
||||
<Card bg={'white'} px={4} py={3} borderRadius={'0 8px 8px 8px'}>
|
||||
{welcomeText && (
|
||||
<Box mb={2} pb={2} borderBottom={theme.borders.base}>
|
||||
{welcomeText}
|
||||
</Box>
|
||||
)}
|
||||
{variableModules && (
|
||||
<Box>
|
||||
{variableModules.map((item) => (
|
||||
<Box w={'min(100%,300px)'} key={item.id} mb={4}>
|
||||
<VariableLabel required={item.required}>{item.label}</VariableLabel>
|
||||
{item.type === VariableInputEnum.input && (
|
||||
<Input
|
||||
{...register(item.key, {
|
||||
required: item.required
|
||||
})}
|
||||
/>
|
||||
)}
|
||||
{item.type === VariableInputEnum.select && (
|
||||
<MySelect
|
||||
width={'100%'}
|
||||
list={(item.enums || []).map((item) => ({
|
||||
label: item.value,
|
||||
value: item.value
|
||||
}))}
|
||||
{...register(item.key, {
|
||||
required: item.required
|
||||
})}
|
||||
onchange={(e) => {
|
||||
setValue(item.key, e);
|
||||
// setRefresh((state) => !state);
|
||||
}}
|
||||
/>
|
||||
)}
|
||||
</Box>
|
||||
))}
|
||||
{!variableIsFinish && (
|
||||
<Button
|
||||
leftIcon={<MyIcon name={'chatFill'} w={'16px'} />}
|
||||
size={'sm'}
|
||||
maxW={'100px'}
|
||||
borderRadius={'lg'}
|
||||
onClick={handleSubmit((data) => {
|
||||
onUpdateVariable?.(data);
|
||||
setVariables(data);
|
||||
})}
|
||||
>
|
||||
{'开始对话'}
|
||||
</Button>
|
||||
)}
|
||||
</Box>
|
||||
)}
|
||||
</Card>
|
||||
</Flex>
|
||||
</Flex>
|
||||
)}
|
||||
{/* chat history */}
|
||||
<Box id={'history'}>
|
||||
{chatHistory.map((item, index) => (
|
||||
<Flex
|
||||
key={item._id}
|
||||
alignItems={'flex-start'}
|
||||
py={2}
|
||||
_hover={{
|
||||
'& .control': {
|
||||
display: 'flex'
|
||||
}
|
||||
}}
|
||||
>
|
||||
{item.obj === 'Human' && <Box flex={1} />}
|
||||
{/* avatar */}
|
||||
<Avatar
|
||||
src={item.obj === 'Human' ? userInfo?.avatar || HUMAN_ICON : appAvatar}
|
||||
w={isLargeWidth ? '34px' : '24px'}
|
||||
h={isLargeWidth ? '34px' : '24px'}
|
||||
{...(item.obj === 'AI'
|
||||
? {
|
||||
order: 1,
|
||||
mr: ['6px', 2]
|
||||
}
|
||||
: {
|
||||
order: 3,
|
||||
ml: ['6px', 2]
|
||||
})}
|
||||
/>
|
||||
{/* message */}
|
||||
<Box order={2} pt={2} maxW={`calc(100% - ${isLargeWidth ? '75px' : '58px'})`}>
|
||||
{item.obj === 'AI' ? (
|
||||
<Box w={'100%'}>
|
||||
<Card bg={'white'} px={4} py={3} borderRadius={'0 8px 8px 8px'}>
|
||||
<Markdown
|
||||
source={item.value}
|
||||
isChatting={index === chatHistory.length - 1 && isChatting}
|
||||
/>
|
||||
</Card>
|
||||
<Flex {...controlContainerStyle}>
|
||||
<MyTooltip label={'复制'}>
|
||||
<MyIcon
|
||||
{...controlIconStyle}
|
||||
name={'copy'}
|
||||
_hover={{ color: 'myBlue.700' }}
|
||||
onClick={() => onclickCopy(item.value)}
|
||||
/>
|
||||
</MyTooltip>
|
||||
{onDelMessage && (
|
||||
<MyTooltip label={'删除'}>
|
||||
<MyIcon
|
||||
{...controlIconStyle}
|
||||
name={'delete'}
|
||||
_hover={{ color: 'red.600' }}
|
||||
onClick={() => {
|
||||
setChatHistory((state) =>
|
||||
state.filter((chat) => chat._id !== item._id)
|
||||
);
|
||||
onDelMessage({
|
||||
id: item._id,
|
||||
index
|
||||
});
|
||||
}}
|
||||
/>
|
||||
</MyTooltip>
|
||||
)}
|
||||
{hasVoiceApi && (
|
||||
<MyTooltip label={'语音播报'}>
|
||||
<MyIcon
|
||||
{...controlIconStyle}
|
||||
name={'voice'}
|
||||
_hover={{ color: '#E74694' }}
|
||||
onClick={() => voiceBroadcast({ text: item.value })}
|
||||
/>
|
||||
</MyTooltip>
|
||||
)}
|
||||
</Flex>
|
||||
</Box>
|
||||
) : (
|
||||
<Box position={'relative'}>
|
||||
<Card
|
||||
className="markdown"
|
||||
whiteSpace={'pre-wrap'}
|
||||
px={4}
|
||||
py={3}
|
||||
borderRadius={'8px 0 8px 8px'}
|
||||
bg={'myBlue.300'}
|
||||
>
|
||||
<Box as={'p'}>{item.value}</Box>
|
||||
</Card>
|
||||
<Flex {...controlContainerStyle} right={0}>
|
||||
<MyTooltip label={'复制'}>
|
||||
<MyIcon
|
||||
{...controlIconStyle}
|
||||
name={'copy'}
|
||||
_hover={{ color: 'myBlue.700' }}
|
||||
onClick={() => onclickCopy(item.value)}
|
||||
/>
|
||||
</MyTooltip>
|
||||
{onDelMessage && (
|
||||
<MyTooltip label={'删除'}>
|
||||
<MyIcon
|
||||
{...controlIconStyle}
|
||||
mr={0}
|
||||
name={'delete'}
|
||||
_hover={{ color: 'red.600' }}
|
||||
onClick={() => {
|
||||
setChatHistory((state) =>
|
||||
state.filter((chat) => chat._id !== item._id)
|
||||
);
|
||||
onDelMessage({
|
||||
id: item._id,
|
||||
index
|
||||
});
|
||||
}}
|
||||
/>
|
||||
</MyTooltip>
|
||||
)}
|
||||
</Flex>
|
||||
</Box>
|
||||
)}
|
||||
</Box>
|
||||
</Flex>
|
||||
))}
|
||||
</Box>
|
||||
</Box>
|
||||
{variableIsFinish ? (
|
||||
<Box m={['0 auto', '20px auto']} w={'100%'} maxW={['auto', 'min(750px, 100%)']} px={[0, 5]}>
|
||||
<Box
|
||||
py={'18px'}
|
||||
position={'relative'}
|
||||
boxShadow={`0 0 10px rgba(0,0,0,0.1)`}
|
||||
borderTop={['1px solid', 0]}
|
||||
borderTopColor={'gray.200'}
|
||||
borderRadius={['none', 'md']}
|
||||
backgroundColor={'white'}
|
||||
>
|
||||
{/* 输入框 */}
|
||||
<Textarea
|
||||
ref={TextareaDom}
|
||||
py={0}
|
||||
pr={['45px', '55px']}
|
||||
border={'none'}
|
||||
_focusVisible={{
|
||||
border: 'none'
|
||||
}}
|
||||
isDisabled={isChatting}
|
||||
placeholder="提问"
|
||||
resize={'none'}
|
||||
rows={1}
|
||||
height={'22px'}
|
||||
lineHeight={'22px'}
|
||||
maxHeight={'150px'}
|
||||
maxLength={-1}
|
||||
overflowY={'auto'}
|
||||
whiteSpace={'pre-wrap'}
|
||||
wordBreak={'break-all'}
|
||||
boxShadow={'none !important'}
|
||||
color={'myGray.900'}
|
||||
onChange={(e) => {
|
||||
const textarea = e.target;
|
||||
textarea.style.height = textareaMinH;
|
||||
textarea.style.height = `${textarea.scrollHeight}px`;
|
||||
}}
|
||||
onKeyDown={(e) => {
|
||||
// 触发快捷发送
|
||||
if (e.keyCode === 13 && !e.shiftKey) {
|
||||
handleSubmit(sendPrompt)();
|
||||
e.preventDefault();
|
||||
}
|
||||
// 全选内容
|
||||
// @ts-ignore
|
||||
e.key === 'a' && e.ctrlKey && e.target?.select();
|
||||
}}
|
||||
/>
|
||||
{/* 发送和等待按键 */}
|
||||
<Flex
|
||||
alignItems={'center'}
|
||||
justifyContent={'center'}
|
||||
h={'25px'}
|
||||
w={'25px'}
|
||||
position={'absolute'}
|
||||
right={['12px', '20px']}
|
||||
bottom={'15px'}
|
||||
>
|
||||
{isChatting ? (
|
||||
<MyIcon
|
||||
className={styles.stopIcon}
|
||||
width={['22px', '25px']}
|
||||
height={['22px', '25px']}
|
||||
cursor={'pointer'}
|
||||
name={'stop'}
|
||||
color={'gray.500'}
|
||||
onClick={() => controller.current?.abort()}
|
||||
/>
|
||||
) : (
|
||||
<MyIcon
|
||||
name={'chatSend'}
|
||||
width={['18px', '20px']}
|
||||
height={['18px', '20px']}
|
||||
cursor={'pointer'}
|
||||
color={'gray.500'}
|
||||
onClick={handleSubmit(sendPrompt)}
|
||||
/>
|
||||
)}
|
||||
</Flex>
|
||||
</Box>
|
||||
</Box>
|
||||
) : null}
|
||||
</Flex>
|
||||
);
|
||||
};
|
||||
|
||||
export default React.memo(forwardRef(ChatBox));
|
@@ -1 +1 @@
|
||||
<?xml version="1.0" standalone="no"?><!DOCTYPE svg PUBLIC "-//W3C//DTD SVG 1.1//EN" "http://www.w3.org/Graphics/SVG/1.1/DTD/svg11.dtd"><svg t="1679805221456" class="icon" viewBox="0 0 1024 1024" version="1.1" xmlns="http://www.w3.org/2000/svg" p-id="1173" xmlns:xlink="http://www.w3.org/1999/xlink" width="48" height="48"><path d="M267.3 834.6h-96.5c-27.4 0-49.7-22.3-49.7-49.7V115.2c0-27.4 22.3-49.7 49.7-49.7H727c27.4 0 49.7 22.3 49.7 49.7v96.5h-42.6v-96.5c0-3.9-3.2-7.1-7.1-7.1H170.8c-3.9 0-7.1 3.2-7.1 7.1v669.7c0 3.9 3.2 7.1 7.1 7.1h96.5v42.6z" p-id="1174"></path><path d="M851.9 959.5H295.7c-27.4 0-49.7-22.3-49.7-49.7V240.1c0-27.4 22.3-49.7 49.7-49.7h556.2c27.4 0 49.7 22.3 49.7 49.7v669.7c-0.1 27.4-22.3 49.7-49.7 49.7zM295.7 233c-3.9 0-7.1 3.2-7.1 7.1v669.7c0 3.9 3.2 7.1 7.1 7.1h556.2c3.9 0 7.1-3.2 7.1-7.1V240.1c0-3.9-3.2-7.1-7.1-7.1H295.7z" p-id="1175"></path></svg>
|
||||
<?xml version="1.0" standalone="no"?><!DOCTYPE svg PUBLIC "-//W3C//DTD SVG 1.1//EN" "http://www.w3.org/Graphics/SVG/1.1/DTD/svg11.dtd"><svg t="1689057990782" class="icon" viewBox="0 0 1024 1024" version="1.1" xmlns="http://www.w3.org/2000/svg" p-id="1770" xmlns:xlink="http://www.w3.org/1999/xlink"><path d="M878.5 255.1H770V146.7c0-43.6-35.5-79.1-79.1-79.1H145.2c-43.6 0-79.1 35.5-79.1 79.1v545.8c0 43.6 35.5 79.1 79.1 79.1h108.4V880c0 43.6 35.5 79.1 79.1 79.1h545.8c43.6 0 79.1-35.5 79.1-79.1V334.2c-0.1-43.6-35.6-79.1-79.1-79.1zM145.2 707.5c-8.3 0-15.1-6.8-15.1-15.1V146.7c0-8.3 6.8-15.1 15.1-15.1H691c8.3 0 15.1 6.8 15.1 15.1v545.8c0 8.3-6.8 15.1-15.1 15.1H145.2zM893.5 880c0 8.3-6.8 15.1-15.1 15.1H332.7c-8.3 0-15.1-6.8-15.1-15.1V771.5H691c43.6 0 79.1-35.5 79.1-79.1V319.1h108.4c8.3 0 15.1 6.8 15.1 15.1V880z" p-id="1771"></path></svg>
|
Before Width: | Height: | Size: 878 B After Width: | Height: | Size: 840 B |
1
client/src/components/Icon/icons/light/setTop.svg
Normal file
1
client/src/components/Icon/icons/light/setTop.svg
Normal file
@@ -0,0 +1 @@
|
||||
<?xml version="1.0" standalone="no"?><!DOCTYPE svg PUBLIC "-//W3C//DTD SVG 1.1//EN" "http://www.w3.org/Graphics/SVG/1.1/DTD/svg11.dtd"><svg t="1688993655221" class="icon" viewBox="0 0 1024 1024" version="1.1" xmlns="http://www.w3.org/2000/svg" p-id="1709" xmlns:xlink="http://www.w3.org/1999/xlink" ><path d="M354.7912723 841.95191467l94.44951229 94.4495123c13.67032415 13.67032415 26.71926992 19.88410785 47.22475616 19.88410785 13.67032415 0 33.554432-6.83516208 47.22475614-19.88410785l19.88410786-20.50548622c20.50548622-19.88410785 33.554432-47.22475615 33.554432-73.94402608v-60.27370192c0-6.83516208 6.83516208-26.71926992 13.67032414-33.554432l175.22870044-175.22870045c6.83516208-6.83516208 19.88410785-13.67032415 33.554432-13.67032415h60.27370192c26.71926992 0 60.27370192-13.67032415 73.94402608-33.554432l20.50548622-19.88410785c6.83516208-6.83516208 13.67032415-19.88410785 13.67032415-40.38959408 0-13.67032415-6.83516208-33.554432-19.88410785-47.22475614l-329.95191467-329.95191467c-13.67032415-13.67032415-26.71926992-19.88410785-47.22475615-19.88410786-13.67032415 0-33.554432 6.83516208-47.22475614 19.88410786l-13.67032414 20.50548622c-20.50548622 19.88410785-33.554432 54.05991822-33.554432 73.94402607v60.27370193c0 6.83516208-6.83516208 26.71926992-13.67032416 33.554432L307.56651615 451.1049197c-6.83516208 6.83516208-19.88410785 13.67032415-26.71926993 13.67032415H213.11700385c-26.71926992 0-60.27370192 13.67032415-73.94402607 33.554432l-19.88410786 19.88410785c-19.88410785 20.50548622-19.88410785 60.27370192 0 87.61435022L213.73838222 700.27764622l141.05289008 141.67426845z m-87.61435022-94.4495123l47.22475614 47.22475615-47.22475614-47.22475615z m639.3983431-262.22167229c-6.83516208 6.83516208-20.50548622 13.67032415-33.554432 13.67032414h-60.27370193c-26.71926992 0-60.27370192 13.67032415-73.94402607 33.554432l-175.22870043 175.22870045c-19.88410785 19.88410785-33.554432 47.22475615-33.554432 73.94402608v60.27370192c0 6.83516208-6.83516208 26.71926992-13.67032416 33.554432l-19.88410784 19.88410785-329.95191467-336.78707674 19.88410784-20.50548623c6.83516208 0 19.88410785-6.83516208 26.71926993-6.83516207h60.27370193c26.71926992 0 60.27370192-13.67032415 73.94402607-33.554432l175.22870045-175.22870045c26.71926992-20.50548622 40.38959408-54.05991822 40.38959407-73.94402606V182.04808533c0-6.83516208 6.83516208-26.71926992 13.67032415-33.554432l13.67032415-13.67032415 329.95191466 329.95191467-13.67032415 20.50548623z" p-id="1710"></path><path d="M269.04105718 753.0948077l33.554432 33.554432-164.66526815 164.66526815-33.554432-33.554432c0.62137837 0 164.66526815-164.66526815 164.66526815-164.66526815z" p-id="1711"></path></svg>
|
After Width: | Height: | Size: 2.6 KiB |
1
client/src/components/Icon/icons/modules/variable.svg
Normal file
1
client/src/components/Icon/icons/modules/variable.svg
Normal file
@@ -0,0 +1 @@
|
||||
<?xml version="1.0" standalone="no"?><!DOCTYPE svg PUBLIC "-//W3C//DTD SVG 1.1//EN" "http://www.w3.org/Graphics/SVG/1.1/DTD/svg11.dtd"><svg t="1688977884412" class="icon" viewBox="0 0 1682 1024" version="1.1" xmlns="http://www.w3.org/2000/svg" p-id="12489" xmlns:xlink="http://www.w3.org/1999/xlink" ><path d="M906.971429 548.571429l234.057142 234.057142c29.257143 29.257143 29.257143 73.142857 0 102.4-29.257143 29.257143-73.142857 29.257143-102.4 0L804.571429 650.971429l-234.057143 234.057142c-29.257143 29.257143-73.142857 29.257143-102.4 0s-29.257143-73.142857 0-102.4l234.057143-234.057142-234.057143-234.057143c-29.257143-29.257143-29.257143-73.142857 0-102.4s73.142857-29.257143 102.4 0L804.571429 446.171429l234.057142-234.057143c29.257143-29.257143 73.142857-29.257143 102.4 0s29.257143 73.142857 0 102.4l-234.057142 234.057143zM256 980.114286c-14.628571 0-36.571429-7.314286-51.2-21.942857C80.457143 841.142857 0 665.6 0 490.057143 0 307.2 73.142857 146.285714 204.8 21.942857c29.257143-29.257143 73.142857-29.257143 102.4 0 29.257143 29.257143 29.257143 73.142857 0 102.4C204.8 219.428571 146.285714 351.085714 146.285714 490.057143c0 131.657143 58.514286 270.628571 160.914286 365.714286 29.257143 29.257143 29.257143 73.142857 0 102.4-14.628571 14.628571-29.257143 21.942857-51.2 21.942857z m1104.457143-7.314286c-21.942857 0-36.571429-7.314286-51.2-21.942857-29.257143-29.257143-29.257143-73.142857 0-102.4 102.4-95.085714 160.914286-226.742857 160.914286-365.714286s-58.514286-263.314286-160.914286-351.085714c-29.257143-29.257143-36.571429-73.142857-7.314286-102.4 29.257143-29.257143 73.142857-36.571429 102.4-7.314286C1536 138.971429 1609.142857 307.2 1609.142857 490.057143c0 182.857143-73.142857 343.771429-204.8 468.114286-7.314286 7.314286-29.257143 14.628571-43.885714 14.628571z" p-id="12490"></path></svg>
|
After Width: | Height: | Size: 1.8 KiB |
1
client/src/components/Icon/icons/modules/welcomeText.svg
Normal file
1
client/src/components/Icon/icons/modules/welcomeText.svg
Normal file
@@ -0,0 +1 @@
|
||||
<?xml version="1.0" standalone="no"?><!DOCTYPE svg PUBLIC "-//W3C//DTD SVG 1.1//EN" "http://www.w3.org/Graphics/SVG/1.1/DTD/svg11.dtd"><svg t="1688977803658" class="icon" viewBox="0 0 1024 1024" version="1.1" xmlns="http://www.w3.org/2000/svg" p-id="8763" xmlns:xlink="http://www.w3.org/1999/xlink"><path d="M512.000557 0c282.779765 0 511.999889 229.220124 511.999888 511.999889 0 271.404463-211.166563 493.478849-478.163374 510.886845l1.068521 1.113043H60.03822a13.356519 13.356519 0 0 1-10.885563-21.12556c37.509557-52.491119 62.241378-105.004499 74.239984-157.517879A509.951889 509.951889 0 0 1 0.000668 511.999889C0.000668 229.220124 229.220792 0 512.000557 0z m0 80.139113C273.497652 80.139113 80.139781 273.496984 80.139781 511.999889c0 101.286935 34.905036 197.008653 97.680674 273.563766l6.366608 7.568694 26.312342 30.675472-8.971129 39.40173a458.2399 458.2399 0 0 1-25.377386 77.623636l-1.335651 3.027477h351.476793l14.335997-0.934956c223.610386-14.580866 399.248609-198.856305 403.166521-423.268082L943.861332 511.999889c0-238.502905-193.357871-431.860776-431.860775-431.860776z m109.078237 205.601347c90.735285 0 163.72866 75.553375 163.72866 168.069528 0 59.102596-30.007646 113.085193-78.358244 143.449013l4.140521-2.715826-122.323452 124.727625a106.896672 106.896672 0 0 1-143.40449 8.281042l-4.051478-3.428173-3.917912-3.673043-123.725886-126.063277-5.943651-4.185043c-40.626078-30.052167-66.003464-77.779461-67.895638-130.070232l-0.133565-6.322086c0-92.516154 72.993375-168.069529 163.7064-168.069528 40.959991 0 79.137374 15.560344 108.365889 42.073034l0.734609 0.667826 5.721042-5.008695a160.834748 160.834748 0 0 1 97.257718-37.620861z m0 80.139113c-30.853559 0-58.879987 17.786431-73.438593 46.035468-14.914779 28.939124-56.275466 28.983646-71.234767 0.044522l-2.270608-4.162782c-15.137388-25.867125-41.917208-41.917208-71.234767-41.917208-45.83512 0-83.567286 39.067818-83.567287 87.930415 0 31.454602 15.760692 59.814944 40.804165 75.553375l2.582261 1.780869a40.069557 40.069557 0 0 1 4.674781 4.095999l125.88519 128.267103a26.713038 26.713038 0 0 0 37.776688-0.356174l125.462233-127.888668a40.069557 40.069557 0 0 1 7.301564-5.854607l4.095999-2.738086c22.66156-16.183649 36.730427-43.141556 36.730427-72.859811 0-48.862598-37.709905-87.930416-83.545025-87.930415z" p-id="8764"></path></svg>
|
After Width: | Height: | Size: 2.3 KiB |
1
client/src/components/Icon/icons/voice.svg
Normal file
1
client/src/components/Icon/icons/voice.svg
Normal file
@@ -0,0 +1 @@
|
||||
<?xml version="1.0" standalone="no"?><!DOCTYPE svg PUBLIC "-//W3C//DTD SVG 1.1//EN" "http://www.w3.org/Graphics/SVG/1.1/DTD/svg11.dtd"><svg t="1689059818626" class="icon" viewBox="0 0 1024 1024" version="1.1" xmlns="http://www.w3.org/2000/svg" p-id="2851" xmlns:xlink="http://www.w3.org/1999/xlink" width="64" height="64"><path d="M370.675057 293.943709c-20.172608-25.93621-17.290806-66.281425 8.645403-86.454032 25.93621-23.054409 66.281425-17.290806 86.454032 8.645403 69.163226 83.572231 103.744839 190.198871 103.744839 299.707312 0 106.62664-34.581613 213.253279-103.744839 296.82551-20.172608 25.93621-60.517823 31.699812-86.454032 8.645403-25.93621-20.172608-28.818011-60.517823-8.645403-86.454032 48.990618-60.517823 74.926828-138.326451 74.926827-219.016881s-25.93621-161.38086-74.926827-221.898683z m288.180107-184.435268c-23.054409-25.93621-20.172608-66.281425 8.645403-89.335833s69.163226-20.172608 92.217634 8.645403c109.508441 135.44465 164.262661 311.234516 164.262661 487.024381 0 172.908064-54.75422 348.69793-164.262661 484.14258-23.054409 28.818011-63.399624 31.699812-92.217634 8.645403-28.818011-20.172608-31.699812-63.399624-8.645403-89.335833 89.335833-112.390242 135.44465-259.362096 135.44465-403.45215 0-146.971855-46.108817-293.943709-135.44465-406.333951zM108.431159 449.560967c-17.290806-25.93621-14.409005-60.517823 8.645403-80.69043 23.054409-17.290806 60.517823-14.409005 77.808629 8.645403 31.699812 37.463414 48.990618 89.335833 48.990618 138.326452s-17.290806 97.981236-48.990618 135.44465c-17.290806 23.054409-54.75422 28.818011-77.808629 8.645403s-25.93621-54.75422-8.645403-77.808629c14.409005-20.172608 23.054409-43.227016 23.054409-66.281424 0-25.93621-8.645403-48.990618-23.054409-66.281425z" p-id="2852"></path></svg>
|
After Width: | Height: | Size: 1.7 KiB |
@@ -46,12 +46,19 @@ const map = {
|
||||
appLight: require('./icons/light/app.svg').default,
|
||||
appFill: require('./icons/fill/app.svg').default,
|
||||
meLight: require('./icons/light/me.svg').default,
|
||||
meFill: require('./icons/fill/me.svg').default
|
||||
meFill: require('./icons/fill/me.svg').default,
|
||||
welcomeText: require('./icons/modules/welcomeText.svg').default,
|
||||
variable: require('./icons/modules/variable.svg').default,
|
||||
setTop: require('./icons/light/setTop.svg').default,
|
||||
voice: require('./icons/voice.svg').default
|
||||
};
|
||||
|
||||
export type IconName = keyof typeof map;
|
||||
|
||||
const MyIcon = ({ name, w = 'auto', h = 'auto', ...props }: { name: IconName } & IconProps) => {
|
||||
const MyIcon = (
|
||||
{ name, w = 'auto', h = 'auto', ...props }: { name: IconName } & IconProps,
|
||||
ref: any
|
||||
) => {
|
||||
return map[name] ? (
|
||||
<Icon
|
||||
as={map[name]}
|
||||
@@ -65,4 +72,4 @@ const MyIcon = ({ name, w = 'auto', h = 'auto', ...props }: { name: IconName } &
|
||||
) : null;
|
||||
};
|
||||
|
||||
export default MyIcon;
|
||||
export default React.forwardRef(MyIcon);
|
||||
|
@@ -14,6 +14,7 @@ const MyTooltip = ({ children, ...props }: TooltipProps) => {
|
||||
py={2}
|
||||
borderRadius={'8px'}
|
||||
whiteSpace={'pre-wrap'}
|
||||
shouldWrapChildren
|
||||
{...props}
|
||||
>
|
||||
{children}
|
||||
|
@@ -4,7 +4,6 @@ import {
|
||||
Box,
|
||||
MenuList,
|
||||
MenuItem,
|
||||
MenuButton,
|
||||
Button,
|
||||
useDisclosure,
|
||||
useOutsideClick
|
||||
@@ -44,7 +43,13 @@ const MySelect = ({ placeholder, value, width = 'auto', list, onchange, ...props
|
||||
|
||||
return (
|
||||
<Menu autoSelect={false} isOpen={isOpen} onOpen={onOpen} onClose={onClose}>
|
||||
<Box ref={SelectRef} position={'relative'} onClick={() => (isOpen ? onClose() : onOpen())}>
|
||||
<Box
|
||||
ref={SelectRef}
|
||||
position={'relative'}
|
||||
onClick={() => {
|
||||
isOpen ? onClose() : onOpen();
|
||||
}}
|
||||
>
|
||||
<Button
|
||||
ref={ref}
|
||||
width={width}
|
||||
@@ -65,6 +70,7 @@ const MySelect = ({ placeholder, value, width = 'auto', list, onchange, ...props
|
||||
{...props}
|
||||
>
|
||||
{list.find((item) => item.value === value)?.label || placeholder}
|
||||
<Box flex={1} />
|
||||
<ChevronDownIcon />
|
||||
</Button>
|
||||
|
||||
|
@@ -2,7 +2,7 @@ import React, { useMemo } from 'react';
|
||||
import { Box, type BoxProps } from '@chakra-ui/react';
|
||||
|
||||
interface Props extends BoxProps {
|
||||
children: string;
|
||||
children: React.ReactNode | React.ReactNode[];
|
||||
colorSchema?: 'blue' | 'green' | 'gray';
|
||||
}
|
||||
|
||||
@@ -33,7 +33,7 @@ const Tag = ({ children, colorSchema = 'blue', ...props }: Props) => {
|
||||
border={'1px solid'}
|
||||
px={2}
|
||||
lineHeight={1}
|
||||
py={'2px'}
|
||||
py={1}
|
||||
borderRadius={'md'}
|
||||
fontSize={'xs'}
|
||||
{...theme}
|
||||
|
Reference in New Issue
Block a user