Skip to content

Commit

Permalink
Merge pull request #111 from SW13-Monstera/feat/430
Browse files Browse the repository at this point in the history
챗봇 기능 추가
  • Loading branch information
Kim-Hyunjo authored Dec 17, 2023
2 parents f03efaa + 137caf7 commit ef3932b
Show file tree
Hide file tree
Showing 14 changed files with 448 additions and 9 deletions.
17 changes: 17 additions & 0 deletions packages/service/src/Component/Button/FloatingButton/index.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,17 @@
import { IButton } from '../../../types/button';
import { floatingButtonStyle } from './style.css';

const FloatingButton = (props: IButton) => {
return (
<button
type='button'
className={floatingButtonStyle}
onClick={props.onClick}
onMouseOver={props.onMouseOver}
onMouseOut={props.onMouseOut}
>
{props.children}
</button>
);
};
export default FloatingButton;
27 changes: 27 additions & 0 deletions packages/service/src/Component/Button/FloatingButton/style.css.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,27 @@
import { style } from '@vanilla-extract/css';
import { COLOR } from '../../../constants/color';
import { spreadBoxShadow } from '../../../styles/keyframe.css';
import { themeColors } from '../../../styles/theme.css';

export const floatingButtonStyle = style({
position: 'fixed',
bottom: '20px',
right: '20px',
display: 'flex',
alignItems: 'center',
justifyContent: 'center',
width: '56px',
height: '56px',
cursor: 'pointer',
borderRadius: '24px',
background: `linear-gradient(${COLOR.PRIMARY} 0%, rgba(42,90,243) 100%)`,
color: COLOR.BACKGROUND.ALICE_BLUE,
fontSize: '2rem',
boxShadow: `0px 0px 4px ${themeColors.shadow[1]}`,
animation: spreadBoxShadow,
zIndex: '10',

':hover': {
boxShadow: `4px 8px 24px ${themeColors.shadow[1]}`,
},
});
4 changes: 2 additions & 2 deletions packages/service/src/Component/Utils/DefaultSelect/index.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -56,7 +56,7 @@ export const DefaultSelect = ({
},
})}
defaultValue={defaultValue}
onChange={(e: MultiValue<IOption> | SingleValue<IOption>) => {
onChange={(e) => {
onChange(e);
if (isMulti) setSelectedOptions(e);
}}
Expand Down Expand Up @@ -85,7 +85,7 @@ export const DefaultSelect = ({
neutral90: themeColors.text[5],
},
})}
onChange={(e: MultiValue<IOption> | SingleValue<IOption>) => {
onChange={(e) => {
onChange(e);
if (isMulti) setSelectedOptions(e);
}}
Expand Down
192 changes: 192 additions & 0 deletions packages/service/src/Organism/ChatApp/index.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,192 @@
import { getUserInfo } from 'auth/utils/userInfo';
import { AxiosError } from 'axios';
import React, { FormEvent, useEffect, useRef, useState } from 'react';
import { useMutation } from 'react-query';
import { commonApiWrapper } from '../../api/wrapper/common/commonApiWrapper';
import FloatingButton from '../../Component/Button/FloatingButton';
import { displayNoneStyle } from '../../styles/util.css';
import {
chatAppStyle,
chatAppTitleImgStyle,
chatAppTitleStyle,
chatBotTooltipStyle,
ellipsis1Style,
ellipsis2Style,
ellipsis3Style,
ellipsis4Style,
loadingMessageStyle,
messageBotStyle,
messageBotWrapStyle,
messageFormStyle,
messageInputStyle,
messageListStyle,
messageSubmitButtonStyle,
messageUserStyle,
messageUserWrapStyle,
} from './style.css';
import chatgptImg from '../../assets/images/chat-gpt.png';

interface IMessage {
text: string;
isUser: boolean;
scrollRef: React.MutableRefObject<HTMLDivElement | null>;
}

const Message = ({ text, isUser, scrollRef }: IMessage) => {
const [botText, setBotText] = useState('');

useEffect(() => {
scrollRef.current?.scrollIntoView({ behavior: 'smooth' });
}, [botText]);

useEffect(() => {
const answer = text;
let curIdx = 0;

function write() {
if (!answer) return;

setBotText(answer.slice(0, curIdx));
if (curIdx++ === answer.length) {
curIdx = 0;
} else {
setTimeout(() => {
write();
}, 50);
}
}
write();
}, []);

return (
<div className={isUser ? messageUserWrapStyle : messageBotWrapStyle}>
<div className={isUser ? messageUserStyle : messageBotStyle}>{isUser ? text : botText}</div>
</div>
);
};

const MessageList = ({
messages,
isLoading,
scrollRef,
}: {
messages: IMessage[];
isLoading: boolean;
scrollRef: React.MutableRefObject<HTMLDivElement | null>;
}) => {
return (
<div className={messageListStyle}>
{messages.map((message, index) => (
<Message key={index} text={message.text} isUser={message.isUser} scrollRef={scrollRef} />
))}
{isLoading ? <LoadingMessage /> : <></>}
<div ref={scrollRef}></div>
</div>
);
};

const MessageInput = ({ onSendMessage }: { onSendMessage: (text: string) => void }) => {
const [text, setText] = useState('');

const handleSubmit = (e: FormEvent<HTMLFormElement>) => {
e.preventDefault();
if (text.trim() !== '') {
onSendMessage(text);
setText('');
}
};

return (
<form onSubmit={handleSubmit} className={messageFormStyle}>
<input
type='text'
placeholder='궁금한 점을 입력하세요'
value={text}
onChange={(e) => setText(e.target.value)}
className={messageInputStyle}
/>
<button type='submit' className={messageSubmitButtonStyle}>
전송
</button>
</form>
);
};

const LoadingMessage = () => {
return (
<div className={loadingMessageStyle}>
<div className={ellipsis1Style}></div>
<div className={ellipsis2Style}></div>
<div className={ellipsis3Style}></div>
<div className={ellipsis4Style}></div>
</div>
);
};

const ChatApp = ({ isShown }: { isShown: boolean }) => {
const scrollRef = useRef<HTMLDivElement | null>(null);
const [messages, setMessages] = useState<IMessage[]>([]);
const { mutate, isLoading } = useMutation<string, AxiosError, string>(commonApiWrapper.postChat, {
onSuccess: (answer) => {
setMessages((prev) => [...prev, { text: answer, isUser: false, scrollRef }]);
},
});

const handleSendMessage = (text: string) => {
setMessages((prev) => [...prev, { text, isUser: true, scrollRef }]);
mutate(text);
};

useEffect(() => {
scrollRef.current?.scrollIntoView({ behavior: 'smooth' });
}, [messages]);

return (
<div className={isShown ? chatAppStyle : displayNoneStyle}>
<p className={chatAppTitleStyle}>
<img src={chatgptImg} alt='chatGPT' width='20px' className={chatAppTitleImgStyle} />
<span>챗봇에게 궁금한 점을 물어보세요!</span>
</p>
<MessageList messages={messages} isLoading={isLoading} scrollRef={scrollRef} />

<MessageInput onSendMessage={handleSendMessage} />
</div>
);
};

const ChatBotTooltip = ({ isShown }: { isShown: boolean }) => {
return <div className={isShown ? chatBotTooltipStyle : displayNoneStyle}>로그인 후 이용가능</div>;
};

const ChatBot = () => {
const [isChatShown, setIsChatShow] = useState(false);
const [isTooltipShown, setIsTooltipShown] = useState(false);

return (
<>
<ChatApp isShown={isChatShown} />
<ChatBotTooltip isShown={isTooltipShown} />
<FloatingButton
onClick={() => {
const userInfo = getUserInfo();
if (!userInfo) return;
setIsChatShow((prev) => !prev);
}}
onMouseOver={() => {
const userInfo = getUserInfo();
if (userInfo) return;
setIsTooltipShown(true);
}}
onMouseOut={() => {
const userInfo = getUserInfo();
if (userInfo) return;
setIsTooltipShown(false);
}}
>
<img src={chatgptImg} alt='AI 챗봇' width='32px' />
</FloatingButton>
</>
);
};

export default ChatBot;
Loading

0 comments on commit ef3932b

Please sign in to comment.