From 446098cc61866d484c38db058165dccda98f52e1 Mon Sep 17 00:00:00 2001 From: dwang5460 <69324600+dwang5460@users.noreply.github.com> Date: Tue, 14 Jan 2025 19:24:49 -0600 Subject: [PATCH 1/3] api-integration added --- src/components/Chatbot/Chatbot.tsx | 91 +++++++++++++++++++++++++----- 1 file changed, 78 insertions(+), 13 deletions(-) diff --git a/src/components/Chatbot/Chatbot.tsx b/src/components/Chatbot/Chatbot.tsx index 50a8ae1..e62bc5e 100644 --- a/src/components/Chatbot/Chatbot.tsx +++ b/src/components/Chatbot/Chatbot.tsx @@ -1,6 +1,6 @@ import { CustomDialog } from '@/components/common' import SendIcon from '@mui/icons-material/Send' -import { Box, IconButton, TextField } from '@mui/material' +import { Box, CircularProgress, IconButton, TextField } from '@mui/material' import { useState } from 'react' interface ChatbotProps { @@ -15,10 +15,31 @@ interface Message { timestamp: Date } +interface DeepSeekError { + error?: { + message?: string + } +} + +interface DeepSeekMessage { + role: string + content: string +} + +interface DeepSeekResponse { + choices?: Array<{ + message?: DeepSeekMessage + }> +} + export const Chatbot = ({ open, onClose }: ChatbotProps) => { const [messages, setMessages] = useState([]) - const [input, setInput] = useState('') const [messageId, setMessageId] = useState(1) + const [input, setInput] = useState('') + const [isLoading, setIsLoading] = useState(false) + + const DEEPSEEK_API_URL = 'https://api.deepseek.com/v1/chat/completions' + const DEEPSEEK_API_KEY = 'API_KEY' const addMessage = (text: string, sender: 'user' | 'bot') => { setMessages(prev => [ @@ -28,22 +49,64 @@ export const Chatbot = ({ open, onClose }: ChatbotProps) => { setMessageId(prev => prev + 1) } - const handleSend = () => { + const handleSend = async () => { if (input.trim()) { addMessage(input, 'user') setInput('') + 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) { + const errorData: DeepSeekError = (await response.json()) as DeepSeekError - // Simulate bot response - setTimeout(() => { - addMessage('This is a sample response from PlanifyAI', 'bot') - }, 1000) + let errorMessage = 'Unknown error' + if (errorData.error && typeof errorData.error.message === 'string') { + const trimmedMessage = errorData.error.message.trim() + if (trimmedMessage.length > 0) { + errorMessage = trimmedMessage + } + } + + throw new Error(`API Error: ${errorMessage}`) + } + + const data: DeepSeekResponse = (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) + } } } - const handleInputKeyDown = (event_: React.KeyboardEvent) => { + const handleInputKeyDown = async (event_: React.KeyboardEvent) => { if (event_.key === 'Enter') { event_.preventDefault() - handleSend() + await handleSend() } } @@ -82,7 +145,10 @@ export const Chatbot = ({ open, onClose }: ChatbotProps) => { { ))} - { - + {isLoading ? : } From 65c906c5dc5a0ca0ca67a9bbfd9a1af51eb1c833 Mon Sep 17 00:00:00 2001 From: ZL Asica <40444637+ZL-Asica@users.noreply.github.com> Date: Wed, 15 Jan 2025 10:45:54 -0600 Subject: [PATCH 2/3] chore: code clean up and modularize --- .gitignore | 4 +- src/components/Chatbot/Chatbot.tsx | 183 ------------------------ src/components/Chatbot/MessageInput.tsx | 53 +++++++ src/components/Chatbot/MessageList.tsx | 48 +++++++ src/components/Chatbot/index.ts | 1 - src/components/Chatbot/index.tsx | 25 ++++ src/hooks/index.ts | 1 + src/hooks/useChatbot.ts | 79 ++++++++++ src/pages/Home.tsx | 2 +- src/types/chatBot.d.ts | 23 +++ 10 files changed, 233 insertions(+), 186 deletions(-) delete mode 100644 src/components/Chatbot/Chatbot.tsx create mode 100644 src/components/Chatbot/MessageInput.tsx create mode 100644 src/components/Chatbot/MessageList.tsx delete mode 100644 src/components/Chatbot/index.ts create mode 100644 src/components/Chatbot/index.tsx create mode 100644 src/hooks/useChatbot.ts create mode 100644 src/types/chatBot.d.ts 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 e62bc5e..0000000 --- a/src/components/Chatbot/Chatbot.tsx +++ /dev/null @@ -1,183 +0,0 @@ -import { CustomDialog } from '@/components/common' -import SendIcon from '@mui/icons-material/Send' -import { Box, CircularProgress, 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 -} - -interface DeepSeekError { - error?: { - message?: string - } -} - -interface DeepSeekMessage { - role: string - content: string -} - -interface DeepSeekResponse { - choices?: Array<{ - message?: DeepSeekMessage - }> -} - -export const Chatbot = ({ open, onClose }: ChatbotProps) => { - const [messages, setMessages] = useState([]) - const [messageId, setMessageId] = useState(1) - const [input, setInput] = useState('') - const [isLoading, setIsLoading] = useState(false) - - const DEEPSEEK_API_URL = 'https://api.deepseek.com/v1/chat/completions' - const DEEPSEEK_API_KEY = 'API_KEY' - - const addMessage = (text: string, sender: 'user' | 'bot') => { - setMessages(prev => [ - ...prev, - { id: messageId, text, sender, timestamp: new Date() }, - ]) - setMessageId(prev => prev + 1) - } - - const handleSend = async () => { - if (input.trim()) { - addMessage(input, 'user') - setInput('') - 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) { - const errorData: DeepSeekError = (await response.json()) as DeepSeekError - - let errorMessage = 'Unknown error' - if (errorData.error && typeof errorData.error.message === 'string') { - const trimmedMessage = errorData.error.message.trim() - if (trimmedMessage.length > 0) { - errorMessage = trimmedMessage - } - } - - throw new Error(`API Error: ${errorMessage}`) - } - - const data: DeepSeekResponse = (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) - } - } - } - - const handleInputKeyDown = async (event_: React.KeyboardEvent) => { - if (event_.key === 'Enter') { - event_.preventDefault() - await handleSend() - } - } - - return ( - - - - {messages.map(({ id, text, sender, timestamp }) => ( - - {text} - - {timestamp.toLocaleTimeString()} - - - ))} - - - - setInput(e.target.value)} - onKeyDown={handleInputKeyDown} - /> - - {isLoading ? : } - - - - - - ) -} 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..cabdbe6 --- /dev/null +++ b/src/components/Chatbot/MessageList.tsx @@ -0,0 +1,48 @@ +import { Box } from '@mui/material' +import { memo } from 'react' + +const MessageList = memo(({ messages }: { messages: Message[] }) => { + return ( + + {messages.map(({ id, text, sender, timestamp }) => ( + + {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 +} From 4ba791487410c75d93358762b69e56c85cd6390a Mon Sep 17 00:00:00 2001 From: ZL Asica <40444637+ZL-Asica@users.noreply.github.com> Date: Wed, 15 Jan 2025 10:46:18 -0600 Subject: [PATCH 3/3] fix: cannot auto focus when message update --- src/components/Chatbot/MessageList.tsx | 13 +++++++++++-- 1 file changed, 11 insertions(+), 2 deletions(-) diff --git a/src/components/Chatbot/MessageList.tsx b/src/components/Chatbot/MessageList.tsx index cabdbe6..0407816 100644 --- a/src/components/Chatbot/MessageList.tsx +++ b/src/components/Chatbot/MessageList.tsx @@ -1,7 +1,15 @@ import { Box } from '@mui/material' -import { memo } from 'react' +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 ( { gap: 2, }} > - {messages.map(({ id, text, sender, timestamp }) => ( + {messages.map(({ id, text, sender, timestamp }, index) => (