This commit is contained in:
3
api.ts
3
api.ts
@@ -64,4 +64,7 @@ export const API = {
|
|||||||
UPDATE_STATUS: (id: number | string) => `${API_URL_ROOT}/submissions/${id}/status`,
|
UPDATE_STATUS: (id: number | string) => `${API_URL_ROOT}/submissions/${id}/status`,
|
||||||
DELETE: (id: number | string) => `${API_URL_ROOT}/submissions/${id}`,
|
DELETE: (id: number | string) => `${API_URL_ROOT}/submissions/${id}`,
|
||||||
},
|
},
|
||||||
|
Chatbot:{
|
||||||
|
CHAT: `${API_URL_ROOT}/chatbot/chat`,
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -8,6 +8,7 @@ import { apiGetCurrentUser } from "@/service/auth";
|
|||||||
import { setUserData } from "@/store/features/userSlice";
|
import { setUserData } from "@/store/features/userSlice";
|
||||||
import React, { useEffect } from "react";
|
import React, { useEffect } from "react";
|
||||||
import { useDispatch } from "react-redux";
|
import { useDispatch } from "react-redux";
|
||||||
|
import ChatbotWidget from "@/components/ui/chat/ChatbotWidget";
|
||||||
|
|
||||||
export default function AdminLayout({
|
export default function AdminLayout({
|
||||||
children,
|
children,
|
||||||
@@ -51,6 +52,7 @@ export default function AdminLayout({
|
|||||||
{/* Page Content */}
|
{/* Page Content */}
|
||||||
<div className="p-4 mx-auto max-w-(--breakpoint-2xl) md:p-6">{children}</div>
|
<div className="p-4 mx-auto max-w-(--breakpoint-2xl) md:p-6">{children}</div>
|
||||||
</div>
|
</div>
|
||||||
|
<ChatbotWidget />
|
||||||
</div>
|
</div>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -342,7 +342,7 @@ export default function ProjectsPage() {
|
|||||||
variant="outline"
|
variant="outline"
|
||||||
disabled={isExportingProjectId === String(project.id)}
|
disabled={isExportingProjectId === String(project.id)}
|
||||||
onClick={() => handleExportHeadSnapshot(project)}
|
onClick={() => handleExportHeadSnapshot(project)}
|
||||||
title="Export head commit snapshot_json"
|
// title="Export head commit snapshot_json"
|
||||||
>
|
>
|
||||||
ExportJSON
|
ExportJSON
|
||||||
</Button>
|
</Button>
|
||||||
@@ -484,7 +484,7 @@ export default function ProjectsPage() {
|
|||||||
disabled={isSubmitting}
|
disabled={isSubmitting}
|
||||||
className="bg-gray-900 hover:bg-gray-800 text-white"
|
className="bg-gray-900 hover:bg-gray-800 text-white"
|
||||||
onClick={handleCreateProjectWithJson}
|
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
|
Tạo với JSON
|
||||||
</Button>
|
</Button>
|
||||||
@@ -494,4 +494,4 @@ export default function ProjectsPage() {
|
|||||||
</Modal>
|
</Modal>
|
||||||
</div>
|
</div>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
223
src/components/ui/chat/ChatbotWidget.tsx
Normal file
223
src/components/ui/chat/ChatbotWidget.tsx
Normal file
@@ -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<Message[]>([
|
||||||
|
{
|
||||||
|
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<HTMLDivElement>(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<HTMLInputElement>) => {
|
||||||
|
if (e.key === "Enter") {
|
||||||
|
handleSend();
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
return (
|
||||||
|
<div className="fixed bottom-6 right-6 z-50">
|
||||||
|
{!isOpen && (
|
||||||
|
<button
|
||||||
|
onClick={() => setIsOpen(true)}
|
||||||
|
className="w-14 h-14 bg-brand-500 hover:bg-brand-600 text-white rounded-full flex items-center justify-center shadow-[0_4px_14px_rgba(0,0,0,0.25)] transition-transform hover:scale-105"
|
||||||
|
>
|
||||||
|
<svg
|
||||||
|
xmlns="http://www.w3.org/2000/svg"
|
||||||
|
fill="none"
|
||||||
|
viewBox="0 0 24 24"
|
||||||
|
strokeWidth={1.5}
|
||||||
|
stroke="currentColor"
|
||||||
|
className="w-6 h-6"
|
||||||
|
>
|
||||||
|
<path
|
||||||
|
strokeLinecap="round"
|
||||||
|
strokeLinejoin="round"
|
||||||
|
d="M7.5 8.25h9m-9 3H12m-9.75 1.51c0 1.6 1.123 2.994 2.707 3.227 1.129.166 2.27.293 3.423.379.35.026.67.21.865.501L12 21l2.755-4.133a1.14 1.14 0 01.865-.501 48.172 48.172 0 003.423-.379c1.584-.233 2.707-1.626 2.707-3.228V6.741c0-1.602-1.123-2.995-2.707-3.228A48.394 48.394 0 0012 3c-2.392 0-4.744.175-7.043.513C3.373 3.746 2.25 5.14 2.25 6.741v6.018z"
|
||||||
|
/>
|
||||||
|
</svg>
|
||||||
|
</button>
|
||||||
|
)}
|
||||||
|
|
||||||
|
{/* Khung Chat */}
|
||||||
|
{isOpen && (
|
||||||
|
<div className="w-[360px] h-[520px] bg-white dark:bg-gray-900 rounded-2xl shadow-2xl flex flex-col border border-gray-200 dark:border-gray-800 overflow-hidden animate-in slide-in-from-bottom-4 duration-300">
|
||||||
|
{/* Header */}
|
||||||
|
<div className="px-4 py-3 bg-brand-500 text-white flex items-center justify-between shadow-sm z-10">
|
||||||
|
<div className="font-semibold flex items-center gap-2">
|
||||||
|
<svg
|
||||||
|
xmlns="http://www.w3.org/2000/svg"
|
||||||
|
fill="none"
|
||||||
|
viewBox="0 0 24 24"
|
||||||
|
strokeWidth={1.5}
|
||||||
|
stroke="currentColor"
|
||||||
|
className="w-5 h-5"
|
||||||
|
>
|
||||||
|
<path
|
||||||
|
strokeLinecap="round"
|
||||||
|
strokeLinejoin="round"
|
||||||
|
d="M9.75 3.104v5.714a2.25 2.25 0 01-.659 1.591L5 14.5M9.75 3.104c-.251.023-.501.05-.75.082m.75-.082a24.301 24.301 0 014.5 0m0 0v5.714c0 .597.237 1.17.659 1.591L19.8 15.3M14.25 3.104c.251.023.501.05.75.082M19.8 15.3l-1.57.393A9.065 9.065 0 0112 15a9.065 9.065 0 00-6.23-.693L5 14.5m14.8.8l1.402 1.402c1.232 1.232.65 3.318-1.067 3.611A48.309 48.309 0 0112 21c-2.773 0-5.491-.235-8.135-.687-1.718-.293-2.3-2.379-1.067-3.61L5 14.5"
|
||||||
|
/>
|
||||||
|
</svg>
|
||||||
|
Trợ lý lịch sử.
|
||||||
|
</div>
|
||||||
|
<button
|
||||||
|
onClick={() => setIsOpen(false)}
|
||||||
|
className="text-white hover:text-gray-200 transition-colors"
|
||||||
|
>
|
||||||
|
<svg
|
||||||
|
xmlns="http://www.w3.org/2000/svg"
|
||||||
|
fill="none"
|
||||||
|
viewBox="0 0 24 24"
|
||||||
|
strokeWidth={2}
|
||||||
|
stroke="currentColor"
|
||||||
|
className="w-5 h-5"
|
||||||
|
>
|
||||||
|
<path
|
||||||
|
strokeLinecap="round"
|
||||||
|
strokeLinejoin="round"
|
||||||
|
d="M6 18L18 6M6 6l12 12"
|
||||||
|
/>
|
||||||
|
</svg>
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
{/* Nội dung Chat */}
|
||||||
|
<div className="flex-1 p-4 overflow-y-auto flex flex-col gap-3 bg-gray-50 dark:bg-[#0d1117] text-sm">
|
||||||
|
{messages.map((msg) => (
|
||||||
|
<div
|
||||||
|
key={msg.id}
|
||||||
|
className={`max-w-[85%] rounded-2xl px-4 py-2 shadow-sm ${
|
||||||
|
msg.sender === "user"
|
||||||
|
? "bg-brand-500 text-white self-end rounded-br-sm"
|
||||||
|
: "bg-white dark:bg-gray-800 text-gray-800 dark:text-gray-200 border border-gray-100 dark:border-gray-700 self-start rounded-bl-sm"
|
||||||
|
}`}
|
||||||
|
>
|
||||||
|
{msg.text}
|
||||||
|
</div>
|
||||||
|
))}
|
||||||
|
{isLoading && (
|
||||||
|
<div className="bg-white dark:bg-gray-800 border border-gray-100 dark:border-gray-700 self-start rounded-2xl rounded-bl-sm px-4 py-3 shadow-sm flex items-center gap-1.5 max-w-[80%]">
|
||||||
|
<div className="w-2 h-2 bg-gray-400 rounded-full animate-bounce"></div>
|
||||||
|
<div
|
||||||
|
className="w-2 h-2 bg-gray-400 rounded-full animate-bounce"
|
||||||
|
style={{ animationDelay: "0.2s" }}
|
||||||
|
></div>
|
||||||
|
<div
|
||||||
|
className="w-2 h-2 bg-gray-400 rounded-full animate-bounce"
|
||||||
|
style={{ animationDelay: "0.4s" }}
|
||||||
|
></div>
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
|
<div ref={messagesEndRef} />
|
||||||
|
</div>
|
||||||
|
|
||||||
|
{/* Khu vực Nhập Input */}
|
||||||
|
<div className="p-3 bg-white dark:bg-gray-900 border-t border-gray-200 dark:border-gray-800">
|
||||||
|
<div className="flex items-center gap-2">
|
||||||
|
<input
|
||||||
|
type="text"
|
||||||
|
placeholder="Nhập câu hỏi..."
|
||||||
|
value={input}
|
||||||
|
onChange={(e) => 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"
|
||||||
|
/>
|
||||||
|
<button
|
||||||
|
onClick={handleSend}
|
||||||
|
disabled={!input.trim() || isLoading}
|
||||||
|
className={`p-2.5 rounded-full transition-colors flex shrink-0 items-center justify-center ${
|
||||||
|
!input.trim() || isLoading
|
||||||
|
? "text-gray-400 bg-gray-100 dark:bg-gray-800 cursor-not-allowed"
|
||||||
|
: "bg-brand-500 text-white hover:bg-brand-600"
|
||||||
|
}`}
|
||||||
|
>
|
||||||
|
<svg
|
||||||
|
xmlns="http://www.w3.org/2000/svg"
|
||||||
|
viewBox="0 0 24 24"
|
||||||
|
fill="currentColor"
|
||||||
|
className="w-5 h-5"
|
||||||
|
>
|
||||||
|
<path d="M3.478 2.405a.75.75 0 00-.926.94l2.432 7.905H13.5a.75.75 0 010 1.5H4.984l-2.432 7.905a.75.75 0 00.926.94 60.519 60.519 0 0018.445-8.986.75.75 0 000-1.218A60.517 60.517 0 003.478 2.405z" />
|
||||||
|
</svg>
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
}
|
||||||
9
src/interface/chatbot.ts
Normal file
9
src/interface/chatbot.ts
Normal file
@@ -0,0 +1,9 @@
|
|||||||
|
export interface ChatbotPayload {
|
||||||
|
project_id?: string;
|
||||||
|
question: string;
|
||||||
|
}
|
||||||
|
|
||||||
|
export interface ChatbotResponse {
|
||||||
|
status: boolean;
|
||||||
|
data: string;
|
||||||
|
}
|
||||||
8
src/service/chatbotService.ts
Normal file
8
src/service/chatbotService.ts
Normal file
@@ -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<ChatbotResponse> => {
|
||||||
|
const response = await api.post(API.Chatbot.CHAT,payload);
|
||||||
|
return await response?.data;
|
||||||
|
};
|
||||||
Reference in New Issue
Block a user