Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

[USER] Feat: type store #22

Merged
merged 21 commits into from
Aug 4, 2024
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
38 changes: 37 additions & 1 deletion package-lock.json

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

3 changes: 2 additions & 1 deletion package.json
Original file line number Diff line number Diff line change
Expand Up @@ -28,7 +28,8 @@
"swr": "^2.2.5",
"tailwind-merge": "^2.3.0",
"typescript": "^4.9.5",
"web-vitals": "^2.1.4"
"web-vitals": "^2.1.4",
"zustand": "^4.5.4"
},
"scripts": {
"start": "react-scripts start",
Expand Down
2 changes: 1 addition & 1 deletion src/api/post-question.ts
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
import { server_axiosInstance } from '../utils/axios';

export async function postQuestion(category: string, type: string, content: string): Promise<any> {
export async function postQuestion(category: string | undefined, type: string, content: string): Promise<any> {
const data = {
type,
category,
Expand Down
2 changes: 1 addition & 1 deletion src/routes/admin-routes.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -2,7 +2,7 @@ import React from 'react';
import { Route, Routes } from 'react-router-dom';
import PrivateRoute from './private-route';
import FileList from '../ui/pages/admin/file-list';
import AdminLayout from '../ui/components/admin/Layout/admin-layout';
import AdminLayout from '../ui/components/molecule/admin/Layout/admin-layout';
import Login from '../ui/pages/admin/login';

const AdminRoutes: React.FC = () => {
Expand Down
23 changes: 23 additions & 0 deletions src/store/chat-store.ts
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

이 부분 설명한번 가능할까요,,?

Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

해당 부분 다음 pr올릴때 자세한 설명 덧붙여 말씀드리겠습니다.

Original file line number Diff line number Diff line change
@@ -0,0 +1,23 @@
import { create } from 'zustand';

interface ChatState {
messages: { content: string; role: 'user' | 'system' }[];
loading: boolean;
addMessage: (message: { content: string; role: 'user' | 'system' }) => void;
setLoading: (loading: boolean) => void;
updateLastMessage: (content: string) => void;
}

const useChatStore = create<ChatState>((set) => ({
messages: [],
loading: false,
addMessage: (message) => set((state) => ({ messages: [...state.messages, message] })),
setLoading: (loading) => set({ loading }),
updateLastMessage: (content) =>
set((state) => {
const updatedMessages = [...state.messages];
updatedMessages[updatedMessages.length - 1].content = content;
return { messages: updatedMessages };
}),
}));
export default useChatStore;
13 changes: 13 additions & 0 deletions src/store/type-store.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,13 @@
import { create } from 'zustand';

interface TypeState {
type: null | 'SUSI' | 'PYEONIP' | 'JEONGSI';
setSelectedType: (button: TypeState['type']) => void;
}

const useTypeStore = create<TypeState>()((set) => ({
type: null,
setSelectedType: (button) => set({ type: button }),
}));

export default useTypeStore;
28 changes: 15 additions & 13 deletions src/ui/components/atom/chat-card/chat-card.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -2,28 +2,30 @@ import React from 'react';
import ReactMarkdown from 'react-markdown';
import remarkGfm from 'remark-gfm';
import { cn } from '../../../../utils/style';

import maru from '../../../../assets/maru-egg.png';
interface ChatCardProps {
content: string;
role: 'user' | 'system';
}

const ChatCard = ({ content, role }: ChatCardProps) => {
return (
<div
className={cn('flex h-auto flex-col-reverse', {
'items-end justify-end': role === 'user',
'items-start justify-start': role !== 'user',
})}
>
<div>
{role === 'user' ? null : <img src={maru} className="mb-2 h-8 w-8" />}
<div
className={cn('flex w-auto max-w-72 rounded-md bg-white px-5 py-3 text-black', {
'justify-end bg-primary-blue text-white': role === 'user',
'justify-start text-left': role !== 'user',
className={cn('flex h-auto flex-col-reverse py-3', {
'items-end justify-end': role === 'user',
'items-start justify-start': role !== 'user',
})}
>
<div className="px-4 py-3 text-body1">
<ReactMarkdown remarkPlugins={[remarkGfm]}>{content}</ReactMarkdown>
<div
className={cn('flex w-auto rounded-md bg-white px-5 py-3 text-black', {
'justify-end bg-primary-blue text-white': role === 'user',
'justify-start text-left': role !== 'user',
})}
>
<div className="px-4 py-3 text-body1">
<ReactMarkdown remarkPlugins={[remarkGfm]}>{content}</ReactMarkdown>
</div>
</div>
</div>
</div>
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -10,8 +10,7 @@ const meta = {
layout: 'centered',
},
args: {
type: 'question',
category: 'general',
type: 'SUSI',
},
} satisfies Meta<typeof ChatForm>;

Expand All @@ -20,7 +19,7 @@ type Story = StoryObj<typeof meta>;

export const Default: Story = {
args: {
type: 'question',
category: 'general',
type: 'SUSI',
category: 'PAST_QUESTIONS',
},
};
21 changes: 12 additions & 9 deletions src/ui/components/molecule/user/chat-form/chat-form.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -4,10 +4,11 @@ import IconButton from '../../../atom/icon/icon-button';
import { ReactComponent as SendIcon } from '../../../../../assets/Send.svg';
import { postQuestion } from '../../../../../api/post-question';
import { cn } from '../../../../../utils/style';
import useChatStore from '../../../../../store/chat-store';

interface ChatFormProps {
type: string;
category: string;
type: 'SUSI' | 'PYEONIP' | 'JEONGSI';
category?: 'PAST_QUESTIONS' | 'INTERVIEW_PRACTICAL_TEST' | 'PASSING_RESULT' | 'ADMISSION_GUIDELINE';
}

const ChatForm = ({ type, category }: ChatFormProps) => {
Expand All @@ -22,20 +23,22 @@ const ChatForm = ({ type, category }: ChatFormProps) => {
const handleSubmit = async (e: React.FormEvent<HTMLFormElement>) => {
e.preventDefault();
try {
const response = await postQuestion(category, type, content);
console.log('전송 성공:', response);
useChatStore.getState().addMessage({ content, role: 'user' });
useChatStore.getState().addMessage({ content: '답변을 생성중입니다...', role: 'system' });
useChatStore.getState().setLoading(true);
setContent('');
const response = await postQuestion(category, type, content);
useChatStore.getState().updateLastMessage(response.answer.content);
useChatStore.getState().setLoading(false);
setDisabled(true);
} catch (error) {
console.error('전송 실패:', error);
useChatStore.getState().setLoading(false);
useChatStore.getState().updateLastMessage('답변 생성에 실패했습니다. 새로고침해주세요');
}
};

return (
<form
className={cn('flex w-96 flex-nowrap rounded-2xl px-4 py-2', 'bg-background-default')}
onSubmit={handleSubmit}
>
<form className={cn('flex flex-nowrap rounded-2xl px-4 py-2', 'bg-background-default')} onSubmit={handleSubmit}>
<TextInput value={content} onValueChange={handleChange} placeholder="메시지를 입력해주세요." />
<IconButton type="submit" disabled={disabled}>
<SendIcon />
Expand Down
27 changes: 15 additions & 12 deletions src/ui/components/molecule/user/header/header.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -6,15 +6,14 @@ import maruEgg from '../../../../../assets/maru-egg.png';
import IconButton from '../../../atom/icon/icon-button';

interface HeaderProps {
type: string;
type: null | 'SUSI' | 'PYEONIP' | 'JEONGSI';
}

const Header = ({ type }: HeaderProps) => {
const navigate = useNavigate();
const [menuOpen, setMenuOpen] = useState(false);

const handleRefreshClick = () => {
navigate('/');
window.location.reload();
console.log('새로고침');
};

Expand All @@ -23,21 +22,25 @@ const Header = ({ type }: HeaderProps) => {
};

return (
<div className="flex items-center justify-between px-3 py-2">
<div className="fixed flex w-full items-center justify-between bg-white px-3 py-2">
<IconButton onClick={handleRefreshClick}>
<RefreshIcon />
</IconButton>
<div className="flex items-center">
<img className="mr-2 h-8 w-8" src={maruEgg} alt="마루에그 캐릭터" />
<div className="mr-8 text-title text-primary-blue">명지대학교 입학처 챗봇</div>
<ul role="list" className="flex list-disc items-center marker:text-primary-blue">
<li className="pl-0">
<div className="flex text-body2">
<p className="text-primary-blue">{type}&nbsp;</p>
<p>질문중</p>
</div>
</li>
</ul>
{type && (
<ul role="list" className="flex list-disc items-center marker:text-primary-blue">
<li className="pl-0">
<div className="flex text-body2">
<p className="text-primary-blue">
{type === 'SUSI' ? '수시' : type === 'JEONGSI' ? '정시' : '편입'}&nbsp;
</p>
<p>질문중</p>
</div>
</li>
</ul>
)}
</div>
<IconButton onClick={handleMenuClick}>
<MenuIcon />
Expand Down
10 changes: 4 additions & 6 deletions src/ui/pages/admin/login.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -12,7 +12,6 @@ export default function Login() {
const handleSubmit = async (e: React.FormEvent) => {
e.preventDefault();
setError('');

try {
await handleLogin(email, password);
} catch (err: any) {
Expand All @@ -28,7 +27,6 @@ export default function Login() {
Sign in to your account
</h2>
</div>

<div className="mt-10 sm:mx-auto sm:w-full sm:max-w-sm">
<form className="space-y-6" onSubmit={handleSubmit}>
<div>
Expand All @@ -44,7 +42,7 @@ export default function Login() {
required
value={email}
onChange={(e) => setEmail(e.target.value)}
className="block w-full rounded-md border-0 py-1.5 px-2 text-gray-900 shadow-sm ring-1 ring-inset ring-gray-300 placeholder:text-gray-400 focus:ring-2 focus:ring-inset focus:ring-indigo-600 sm:text-lg sm:leading-6"
className="block w-full rounded-md border-0 px-2 py-1.5 text-gray-900 shadow-sm ring-1 ring-inset ring-gray-300 placeholder:text-gray-400 focus:ring-2 focus:ring-inset focus:ring-indigo-600 sm:text-lg sm:leading-6"
/>
</div>
</div>
Expand All @@ -64,7 +62,7 @@ export default function Login() {
required
value={password}
onChange={(e) => setPassword(e.target.value)}
className="block w-full rounded-md border-0 py-1.5 px-2 text-gray-900 shadow-sm ring-1 ring-inset ring-gray-300 placeholder:text-gray-400 focus:ring-2 focus:ring-inset focus:ring-indigo-600 sm:text-lg sm:leading-6"
className="block w-full rounded-md border-0 px-2 py-1.5 text-gray-900 shadow-sm ring-1 ring-inset ring-gray-300 placeholder:text-gray-400 focus:ring-2 focus:ring-inset focus:ring-indigo-600 sm:text-lg sm:leading-6"
/>
</div>
</div>
Expand All @@ -75,9 +73,9 @@ export default function Login() {
className="flex w-full justify-center rounded-md bg-indigo-600 px-3 py-1.5 text-lg font-semibold leading-6 text-white shadow-sm hover:bg-indigo-500 focus-visible:outline focus-visible:outline-2 focus-visible:outline-offset-2 focus-visible:outline-indigo-600"
>
{isLoading ? (
'Sign in'
) : (
<Spin indicator={<LoadingOutlined style={{ color: 'white' }} spin />} size="large" />
) : (
'Sign in'
)}
</button>
</div>
Expand Down
47 changes: 43 additions & 4 deletions src/ui/pages/maru-egg.tsx
Original file line number Diff line number Diff line change
@@ -1,12 +1,51 @@
import React from 'react';

import Header from '../components/molecule/user/header/header';
import useTypeStore from '../../store/type-store';
import ChatCard from '../components/atom/chat-card/chat-card';
import ChatForm from '../components/molecule/user/chat-form/chat-form';
import useChatStore from '../../store/chat-store';

const MaruEgg: React.FC = () => {
const { setSelectedType, type } = useTypeStore();
const { messages } = useChatStore();
const handleButtonClick = (selectedType: 'SUSI' | 'PYEONIP' | 'JEONGSI') => {
setSelectedType(selectedType);
};

return (
<div className="min-w-[360px]">
<div className="bg-background-default"></div>
<Header type="수시" />
<div className="h-full min-w-[360px] bg-background-default">
<Header type={type} />

<div className="w-full px-4 pb-24 pt-16">
<ChatCard
content={`안녕하세요 입학처 챗봇 MARU-EGG입니다!
궁금하신 내용 안내 도와드리겠습니다.
알아보고 싶은 전형을 선택해주세요!`}
role="system"
/>
<button onClick={() => handleButtonClick('SUSI')}>Select SUSI</button>
<button onClick={() => handleButtonClick('PYEONIP')}>Select PYEONIP</button>
<button onClick={() => handleButtonClick('JEONGSI')}>Select JEONGSI</button>
{type !== null && (
<ChatCard role="user" content={type === 'SUSI' ? '수시' : type === 'JEONGSI' ? '정시' : '편입'} />
)}
{type !== null && (
<ChatCard
role="system"
content={`안녕하세요 입학처 챗봇 MARU-EGG입니다!
궁금하신 내용 안내 도와드리겠습니다.
`}
/>
)}
{messages.map((msg, index) => {
return <ChatCard key={index} content={msg.content} role={msg.role} />;
})}
</div>
{type !== null && (
<div className="fixed bottom-0 w-full bg-white px-4 py-3">
<ChatForm type={type} />
</div>
)}
</div>
);
};
Expand Down
Loading
Loading