diff --git a/.gitignore b/.gitignore index e998c1a..2a04a2a 100644 --- a/.gitignore +++ b/.gitignore @@ -9,9 +9,10 @@ lerna-debug.log* node_modules dist +build dist-ssr *.local -pnpm-lock.yaml +.eslintcache # Editor directories and files .idea @@ -23,3 +24,4 @@ pnpm-lock.yaml *.sw? .firebase +.env diff --git a/src/components/Chatbot/Chatbot.tsx b/src/components/Chatbot/Chatbot.tsx deleted file mode 100644 index 50a8ae1..0000000 --- a/src/components/Chatbot/Chatbot.tsx +++ /dev/null @@ -1,118 +0,0 @@ -import { CustomDialog } from '@/components/common' -import SendIcon from '@mui/icons-material/Send' -import { Box, IconButton, TextField } from '@mui/material' -import { useState } from 'react' - -interface ChatbotProps { - open: boolean - onClose: () => void -} - -interface Message { - id: number - text: string - sender: 'user' | 'bot' - timestamp: Date -} - -export const Chatbot = ({ open, onClose }: ChatbotProps) => { - const [messages, setMessages] = useState([]) - const [input, setInput] = useState('') - const [messageId, setMessageId] = useState(1) - - const addMessage = (text: string, sender: 'user' | 'bot') => { - setMessages(prev => [ - ...prev, - { id: messageId, text, sender, timestamp: new Date() }, - ]) - setMessageId(prev => prev + 1) - } - - const handleSend = () => { - if (input.trim()) { - addMessage(input, 'user') - setInput('') - - // Simulate bot response - setTimeout(() => { - addMessage('This is a sample response from PlanifyAI', 'bot') - }, 1000) - } - } - - const handleInputKeyDown = (event_: React.KeyboardEvent) => { - if (event_.key === 'Enter') { - event_.preventDefault() - handleSend() - } - } - - return ( - - - - {messages.map(({ id, text, sender, timestamp }) => ( - - {text} - - {timestamp.toLocaleTimeString()} - - - ))} - - - - - setInput(e.target.value)} - onKeyDown={handleInputKeyDown} - /> - - - - - - - - ) -} diff --git a/src/components/Chatbot/MessageInput.tsx b/src/components/Chatbot/MessageInput.tsx new file mode 100644 index 0000000..d63c476 --- /dev/null +++ b/src/components/Chatbot/MessageInput.tsx @@ -0,0 +1,53 @@ +import SendIcon from '@mui/icons-material/Send' +import { Box, CircularProgress, IconButton, TextField } from '@mui/material' +import { useCallback, useState } from 'react' + +interface MessageInputProps { + onSend: (message: string) => Promise + isLoading: boolean +} + +const MessageInput = ({ onSend, isLoading }: MessageInputProps) => { + const [input, setInput] = useState('') + + const handleSendClick = useCallback(async () => { + if (input.trim()) { + await onSend(input.trim()) + setInput('') + } + }, [input, onSend]) + + const handleInputKeyDown = useCallback( + async (event_: React.KeyboardEvent) => { + if (event_.key === 'Enter') { + event_.preventDefault() + await handleSendClick() + } + }, + [handleSendClick], + ) + + return ( + + + setInput(event_.target.value)} + onKeyDown={handleInputKeyDown} + /> + + {isLoading ? : } + + + + ) +} + +export default MessageInput diff --git a/src/components/Chatbot/MessageList.tsx b/src/components/Chatbot/MessageList.tsx new file mode 100644 index 0000000..0407816 --- /dev/null +++ b/src/components/Chatbot/MessageList.tsx @@ -0,0 +1,57 @@ +import { Box } from '@mui/material' +import { memo, useEffect, useRef } from 'react' + +const MessageList = memo(({ messages }: { messages: Message[] }) => { + const lastMessageRef = useRef(null) + + useEffect(() => { + if (lastMessageRef.current) { + lastMessageRef.current.scrollIntoView({ behavior: 'smooth' }) + } + }, [messages]) + + return ( + + {messages.map(({ id, text, sender, timestamp }, index) => ( + + {text} + + {timestamp.toLocaleTimeString()} + + + ))} + + ) +}) + +export default MessageList diff --git a/src/components/Chatbot/index.ts b/src/components/Chatbot/index.ts deleted file mode 100644 index 5122abe..0000000 --- a/src/components/Chatbot/index.ts +++ /dev/null @@ -1 +0,0 @@ -export { Chatbot } from './Chatbot' diff --git a/src/components/Chatbot/index.tsx b/src/components/Chatbot/index.tsx new file mode 100644 index 0000000..11d67ce --- /dev/null +++ b/src/components/Chatbot/index.tsx @@ -0,0 +1,25 @@ +import { CustomDialog } from '@/components/common' +import { useChatbot } from '@/hooks' +import { Box } from '@mui/material' +import MessageInput from './MessageInput' +import MessageList from './MessageList' + +interface ChatbotProps { + open: boolean + onClose: () => void +} + +const Chatbot = ({ open, onClose }: ChatbotProps) => { + const { messages, handleSend, isLoading } = useChatbot() + + return ( + + + + + + + ) +} + +export default Chatbot diff --git a/src/hooks/index.ts b/src/hooks/index.ts index e69de29..59dfe3f 100644 --- a/src/hooks/index.ts +++ b/src/hooks/index.ts @@ -0,0 +1 @@ +export { default as useChatbot } from './useChatbot' diff --git a/src/hooks/useChatbot.ts b/src/hooks/useChatbot.ts new file mode 100644 index 0000000..891cb2d --- /dev/null +++ b/src/hooks/useChatbot.ts @@ -0,0 +1,79 @@ +import { useCallback, useReducer, useState } from 'react' + +interface MessageAction { + type: 'add' + payload: Message +} + +const messageReducer = (state: Message[], action: MessageAction) => { + switch (action.type) { + case 'add': + return [...state, action.payload] + default: + return state + } +} + +const useChatbot = () => { + const [messages, dispatch] = useReducer(messageReducer, []) + const [isLoading, setIsLoading] = useState(false) + + const DEEPSEEK_API_URL = 'https://api.deepseek.com/v1/chat/completions' + const DEEPSEEK_API_KEY = import.meta.env.VITE_API_KEY as string + + const addMessage = useCallback((text: string, sender: 'user' | 'bot') => { + dispatch({ + type: 'add', + payload: { + id: Date.now(), + text, + sender, + timestamp: new Date(), + }, + }) + }, []) + + const handleSend = useCallback( + async (input: string) => { + addMessage(input, 'user') + setIsLoading(true) + + try { + const response = await fetch(DEEPSEEK_API_URL, { + method: 'POST', + headers: { + 'Content-Type': 'application/json', + 'Authorization': `Bearer ${DEEPSEEK_API_KEY}`, + }, + body: JSON.stringify({ + model: 'deepseek-chat', + messages: [{ role: 'user', content: input }], + temperature: 0.7, + max_tokens: 1000, + }), + }) + + if (!response.ok) { + throw new Error('API Error') + } + + const data = await response.json() as DeepSeekResponse + const botMessage + = data?.choices?.[0]?.message?.content ?? 'No response from the bot.' + addMessage(botMessage, 'bot') + } + catch (error) { + console.error('Error:', error) + addMessage('An error occurred. Please try again later.', 'bot') + } + finally { + setIsLoading(false) + } + }, + [addMessage, DEEPSEEK_API_URL, DEEPSEEK_API_KEY], + ) + + return { messages, handleSend, isLoading } +} + +export default useChatbot diff --git a/src/pages/Home.tsx b/src/pages/Home.tsx index aa97e21..d698223 100644 --- a/src/pages/Home.tsx +++ b/src/pages/Home.tsx @@ -1,4 +1,4 @@ -import { Chatbot } from '@/components/Chatbot' +import Chatbot from '@/components/Chatbot' import { Calendar } from '@/components/Home' import ChatIcon from '@mui/icons-material/Chat' import { Box, Fab } from '@mui/material' diff --git a/src/types/chatBot.d.ts b/src/types/chatBot.d.ts new file mode 100644 index 0000000..a471e8c --- /dev/null +++ b/src/types/chatBot.d.ts @@ -0,0 +1,23 @@ +interface DeepSeekError { + error?: { + message?: string + } +} + +interface DeepSeekMessage { + role: string + content: string +} + +interface DeepSeekResponse { + choices?: Array<{ + message?: DeepSeekMessage + }> +} + +interface Message { + id: number + text: string + sender: 'user' | 'bot' + timestamp: Date +}