diff --git a/api.ts b/api.ts index ee28e72..1b076e3 100644 --- a/api.ts +++ b/api.ts @@ -64,4 +64,7 @@ export const API = { UPDATE_STATUS: (id: number | string) => `${API_URL_ROOT}/submissions/${id}/status`, DELETE: (id: number | string) => `${API_URL_ROOT}/submissions/${id}`, }, + Chatbot:{ + CHAT: `${API_URL_ROOT}/chatbot/chat`, + } } diff --git a/src/app/user/layout.tsx b/src/app/user/layout.tsx index 239c85c..cbca4bb 100644 --- a/src/app/user/layout.tsx +++ b/src/app/user/layout.tsx @@ -8,6 +8,7 @@ import { apiGetCurrentUser } from "@/service/auth"; import { setUserData } from "@/store/features/userSlice"; import React, { useEffect } from "react"; import { useDispatch } from "react-redux"; +import ChatbotWidget from "@/components/ui/chat/ChatbotWidget"; export default function AdminLayout({ children, @@ -51,6 +52,7 @@ export default function AdminLayout({ {/* Page Content */}
{children}
+ ); } diff --git a/src/app/user/projects/page.tsx b/src/app/user/projects/page.tsx index ad0704e..619dd50 100644 --- a/src/app/user/projects/page.tsx +++ b/src/app/user/projects/page.tsx @@ -342,7 +342,7 @@ export default function ProjectsPage() { variant="outline" disabled={isExportingProjectId === String(project.id)} onClick={() => handleExportHeadSnapshot(project)} - title="Export head commit snapshot_json" + // title="Export head commit snapshot_json" > ExportJSON @@ -484,7 +484,7 @@ export default function ProjectsPage() { disabled={isSubmitting} className="bg-gray-900 hover:bg-gray-800 text-white" onClick={handleCreateProjectWithJson} - title="Tạo dự án và tạo commit đầu tiên từ JSON snapshot" + // title="Tạo dự án và tạo commit đầu tiên từ JSON snapshot" > Tạo với JSON @@ -494,4 +494,4 @@ export default function ProjectsPage() { ); -} +} \ No newline at end of file diff --git a/src/components/ui/chat/ChatbotWidget.tsx b/src/components/ui/chat/ChatbotWidget.tsx new file mode 100644 index 0000000..33d860c --- /dev/null +++ b/src/components/ui/chat/ChatbotWidget.tsx @@ -0,0 +1,223 @@ +"use client"; + +import React, { useState, useRef, useEffect } from "react"; +import { ChatbotPayload } from "@/interface/chatbot"; +import { apiChatbot } from "@/service/chatbotService"; + +type Message = { + id: string; + sender: "user" | "bot"; + text: string; +}; + +export default function ChatbotWidget({ + projectId = "", +}: { + projectId?: string; +}) { + const [isOpen, setIsOpen] = useState(false); + const [messages, setMessages] = useState([ + { + id: "init", + sender: "bot", + text: "Xin chào! Tôi là trợ lý lịch sử thân thiện. Tôi có thể giúp gì cho bạn?", + }, + ]); + const [input, setInput] = useState(""); + const [isLoading, setIsLoading] = useState(false); + const messagesEndRef = useRef(null); + + const scrollToBottom = () => { + messagesEndRef.current?.scrollIntoView({ behavior: "smooth" }); + }; + + useEffect(() => { + if (isOpen) { + scrollToBottom(); + } + }, [messages, isOpen]); + + const handleSend = async () => { + if (!input.trim()) return; + + const userMessage: Message = { + id: Date.now().toString(), + sender: "user", + text: input.trim(), + }; + + setMessages((prev) => [...prev, userMessage]); + setInput(""); + setIsLoading(true); + + try { + const payload: ChatbotPayload = { + project_id: projectId, + question: userMessage.text, + }; + + const res = await apiChatbot(payload); + + const botMessage: Message = { + id: (Date.now() + 1).toString(), + sender: "bot", + text: res?.status + ? res?.data + : "Xin lỗi, tôi không thể trả lời lúc này.", + }; + + setMessages((prev) => [...prev, botMessage]); + } catch (error: any) { + const errorMessage: Message = { + id: (Date.now() + 1).toString(), + sender: "bot", + text: + error?.response?.data?.message || + "Có lỗi xảy ra khi kết nối. Vui lòng thử lại sau.", + }; + setMessages((prev) => [...prev, errorMessage]); + } finally { + setIsLoading(false); + } + }; + + const handleKeyDown = (e: React.KeyboardEvent) => { + if (e.key === "Enter") { + handleSend(); + } + }; + + return ( +
+ {!isOpen && ( + + )} + + {/* Khung Chat */} + {isOpen && ( +
+ {/* Header */} +
+
+ + + + Trợ lý lịch sử. +
+ +
+ + {/* Nội dung Chat */} +
+ {messages.map((msg) => ( +
+ {msg.text} +
+ ))} + {isLoading && ( +
+
+
+
+
+ )} +
+
+ + {/* Khu vực Nhập Input */} +
+
+ setInput(e.target.value)} + onKeyDown={handleKeyDown} + disabled={isLoading} + className="flex-1 bg-gray-100 dark:bg-gray-800 border-transparent focus:border-brand-500 focus:bg-white dark:focus:bg-gray-900 focus:ring-1 focus:ring-brand-500/20 rounded-full px-4 py-2.5 text-sm outline-none transition-all" + /> + +
+
+
+ )} +
+ ); +} diff --git a/src/interface/chatbot.ts b/src/interface/chatbot.ts new file mode 100644 index 0000000..9622e70 --- /dev/null +++ b/src/interface/chatbot.ts @@ -0,0 +1,9 @@ +export interface ChatbotPayload { + project_id?: string; + question: string; +} + +export interface ChatbotResponse { + status: boolean; + data: string; +} \ No newline at end of file diff --git a/src/service/chatbotService.ts b/src/service/chatbotService.ts new file mode 100644 index 0000000..885d0f6 --- /dev/null +++ b/src/service/chatbotService.ts @@ -0,0 +1,8 @@ +import api from "@/config/config"; +import { API } from "../../api"; +import { ChatbotPayload, ChatbotResponse } from "@/interface/chatbot"; + +export const apiChatbot = async (payload: ChatbotPayload): Promise => { + const response = await api.post(API.Chatbot.CHAT,payload); + return await response?.data; +}; \ No newline at end of file