Huyen My commited on
Commit
4e6c4ab
·
1 Parent(s): b977c43

update frontend

Browse files
ChatBot/__pycache__/app.cpython-311.pyc CHANGED
Binary files a/ChatBot/__pycache__/app.cpython-311.pyc and b/ChatBot/__pycache__/app.cpython-311.pyc differ
 
ChatBot/__pycache__/chatbot.cpython-311.pyc CHANGED
Binary files a/ChatBot/__pycache__/chatbot.cpython-311.pyc and b/ChatBot/__pycache__/chatbot.cpython-311.pyc differ
 
ChatBot/__pycache__/query_transformation.cpython-311.pyc CHANGED
Binary files a/ChatBot/__pycache__/query_transformation.cpython-311.pyc and b/ChatBot/__pycache__/query_transformation.cpython-311.pyc differ
 
ChatBot/__pycache__/retrieval.cpython-311.pyc CHANGED
Binary files a/ChatBot/__pycache__/retrieval.cpython-311.pyc and b/ChatBot/__pycache__/retrieval.cpython-311.pyc differ
 
ChatBot/app.py CHANGED
@@ -3,6 +3,9 @@ from pydantic import BaseModel
3
  from chatbot import ChatBot
4
  import warnings
5
  from fastapi.middleware.cors import CORSMiddleware
 
 
 
6
  # Tắt các cảnh báo
7
  warnings.filterwarnings("ignore")
8
 
@@ -32,12 +35,15 @@ async def new_chat():
32
  return {"message": "ChatBot đã được khởi tạo lại."}
33
 
34
  # Endpoint để xử lý câu hỏi
35
- @app.post("/process_query/")
36
- async def process_query(request: QueryRequest):
37
  global chatbot
38
-
39
- raw_query = request.query # Lấy query từ người dùng
40
- # Xử truy vấn bằng chatbot
41
- response = chatbot.process_query(raw_query)
42
- # Trả về câu trả lời từ chatbot
43
- return {"response": response}
 
 
 
 
3
  from chatbot import ChatBot
4
  import warnings
5
  from fastapi.middleware.cors import CORSMiddleware
6
+ from fastapi import FastAPI, HTTPException
7
+ from fastapi.responses import StreamingResponse
8
+ import time
9
  # Tắt các cảnh báo
10
  warnings.filterwarnings("ignore")
11
 
 
35
  return {"message": "ChatBot đã được khởi tạo lại."}
36
 
37
  # Endpoint để xử lý câu hỏi
38
+ @app.post("/process_query_stream/")
39
+ async def process_query_stream(request: QueryRequest):
40
  global chatbot
41
+ try:
42
+ raw_query = request.query
43
+ # Sử dụng generator để trả về từng phần của câu trả lời
44
+ def generate():
45
+ for chunk in chatbot.process_query_stream(raw_query):
46
+ yield chunk
47
+ return StreamingResponse(generate(), media_type="text/plain")
48
+ except Exception as e:
49
+ raise HTTPException(status_code=500, detail=str(e))
ChatBot/chatbot.py CHANGED
@@ -11,6 +11,7 @@ from dotenv import load_dotenv
11
  from langchain.chains import LLMChain
12
  from langchain.prompts import PromptTemplate
13
  from langchain.memory import ConversationBufferWindowMemory
 
14
 
15
 
16
  class ChatBot:
@@ -54,9 +55,9 @@ class ChatBot:
54
 
55
  - Với `question_type = 0|3|5|7`: *Trả về ngữ cảnh y như nó được cung cấp, không thêm bớt bất kỳ thông tin nào khác.*
56
 
57
- Mỗi câu hỏi đã được làm rõ (converted_query[i]) tương ứng với `context[i]` và `question_type[i]`. Trả lời lần lượt từng câu hỏi theo các quy tắc trên. **KHÔNG ĐƯỢC sử dụng các cụm từ tương tự như "Dựa vào thông tin được cung cấp", "Dựa vào ngữ cảnh", "không được nêu rõ trong ngữ cảnh".**
58
 
59
- **Cách trình bày câu trả lời:** Tách các thông tin trả lời một cách rõ ràn, không nhắc lại câu hỏi
60
 
61
  Dữ liệu cung cấp:
62
  - Loại câu hỏi: {question_type}
@@ -75,7 +76,7 @@ class ChatBot:
75
  # LLMChain kết hợp LLM và Prompt
76
  self.chain = LLMChain(llm=self.llm, prompt=self.prompt, memory=self.memory)
77
 
78
- def process_query(self, raw_query):
79
  print ('QUERY GỐC: ', raw_query, '\n---------------------------------------------------------')
80
  # Bước 1: Biến đổi câu truy vấn dựa trên lịch sử
81
  converted_queries = self.query_transform.transform(raw_query, self.memory.load_memory_variables({})["history"])
@@ -91,4 +92,8 @@ class ChatBot:
91
  # Bước 4: Tạo câu trả lời bằng LangChain
92
  response = self.chain.run(question_type=query_types, context=context, query=raw_query, converted_query=converted_queries)
93
  print ('CÂU TRẢ LỜI: ', response, '\n___________________________________________________________')
94
- return response
 
 
 
 
 
11
  from langchain.chains import LLMChain
12
  from langchain.prompts import PromptTemplate
13
  from langchain.memory import ConversationBufferWindowMemory
14
+ import time
15
 
16
 
17
  class ChatBot:
 
55
 
56
  - Với `question_type = 0|3|5|7`: *Trả về ngữ cảnh y như nó được cung cấp, không thêm bớt bất kỳ thông tin nào khác.*
57
 
58
+ Mỗi câu hỏi đã được làm rõ (converted_query[i]) tương ứng với `context[i]` và `question_type[i]`. Trả lời lần lượt từng câu hỏi theo các quy tắc trên. **KHÔNG ĐƯỢC sử dụng các cụm từ tương tự như "Dựa vào thông tin được cung cấp", "Dựa vào ngữ cảnh", "không được nêu rõ trong ngữ cảnh", "trong văn bản được cung cấp".**
59
 
60
+ **Cách trình bày câu trả lời:** Tách các thông tin trả lời một cách rõ ràn, không nhắc lại câu hỏi.
61
 
62
  Dữ liệu cung cấp:
63
  - Loại câu hỏi: {question_type}
 
76
  # LLMChain kết hợp LLM và Prompt
77
  self.chain = LLMChain(llm=self.llm, prompt=self.prompt, memory=self.memory)
78
 
79
+ def process_query_stream(self, raw_query):
80
  print ('QUERY GỐC: ', raw_query, '\n---------------------------------------------------------')
81
  # Bước 1: Biến đổi câu truy vấn dựa trên lịch sử
82
  converted_queries = self.query_transform.transform(raw_query, self.memory.load_memory_variables({})["history"])
 
92
  # Bước 4: Tạo câu trả lời bằng LangChain
93
  response = self.chain.run(question_type=query_types, context=context, query=raw_query, converted_query=converted_queries)
94
  print ('CÂU TRẢ LỜI: ', response, '\n___________________________________________________________')
95
+
96
+ # Trả về từng phần của câu trả lời
97
+ for chunk in response.split(): # Tách câu trả lời thành các từ hoặc cụm từ
98
+ yield chunk + " " # Trả về từng phần với khoảng trắng
99
+ time.sleep(0.05) # Thêm độ trễ để mô phỏng quá trình streaming
ChatBot/indexing.py CHANGED
@@ -49,8 +49,8 @@ vectorstore = QdrantVectorStore.from_documents(
49
  embedding_model,
50
  sparse_embedding=sparse_embeddings,
51
  retrieval_mode=RetrievalMode.HYBRID,
52
- url=os.getenv("QDRANT_URL"),
53
- api_key=os.getenv("QDRANT_API_KEY"),
54
  collection_name="LAWDATA",
55
  distance=models.Distance.COSINE
56
  )
 
49
  embedding_model,
50
  sparse_embedding=sparse_embeddings,
51
  retrieval_mode=RetrievalMode.HYBRID,
52
+ url="https://ee4decd4-2b54-4960-92f5-615ba47f3e04.us-east4-0.gcp.cloud.qdrant.io",
53
+ api_key="JJ9wyq9OVLta3RODIM5_WX6v5CqpwMHhk-ednmmdrj44-8f_7m40ow",
54
  collection_name="LAWDATA",
55
  distance=models.Distance.COSINE
56
  )
ChatBot/query_transformation.py CHANGED
@@ -13,8 +13,8 @@ class QueryTransform:
13
  4. NẾU CÂU HỎI chứa **SỐ ĐIỀU/SỐ CHƯƠNG cụ thể** và NẾU trong lịch sử trò chuyện gần nhất có chứa:
14
  + "Vui lòng chọn Điều cụ thể.", thì ý định của câu hỏi là: **Khoản <số Khoản trong lịch sử> Điều <SỐ/SỐ ĐIỀU trong câu hỏi>. Ngược lại ý định là: Điều <SỐ/SỐ ĐIỀU trong câu hỏi> **
15
  + "Vui lòng chọn Chương cụ thể", thì ý định của câu hỏi là: **Mục <số Mục trong lịch sử> Chương <SỐ/SỐ CHƯƠNG trong câu hỏi>. Ngược lại ý định là: Chương <SỐ/SỐ CHƯƠNG trong câu hỏi> **
16
- 5. **NẾU CÂU HỎI KHÔNG RÕ RÀNG HOẶC CHƯA ĐẦY ĐỦ**, hãy sử dụng lịch sử trò chuyện được cung cấp **CHỈ KHI LỊCH SỬ LIÊN QUAN TRỰC TIẾP ĐẾN CÂU HỎI** để làm rõ ý nghĩa và ngữ cảnh truy vấn của người dùng. **KHÔNG ĐƯỢC THÊM THÔNG TIN KHÔNG LIÊN QUAN VÀO CÂU HỎI**.
17
- 6. Suy ra nội dung chính của câu hỏi (hoặc câu khẳng định) thật đơn giản, rõ ràng và dễ hiểu (dùng các từ ngữ trong luật Hôn nhân và Gia đình nếu có thể), **BỎ QUA ĐẠI TỪ DANH XƯNG NẾU CÓ THỂ**. Ví dụ: Tôi là nữ 16 tuổi thì có lấy chồng được không? -> Nữ 16 tuổi có đủ điều kiện kết hôn không?, Người bị điên có lấy vợ được không? -> Người mất hành vi dân sự có được phép kết hôn không?
18
 
19
  Chỉ cung cấp kết quả **theo định dạng sau** và không thêm bất kỳ văn bản, giải thích hoặc bình luận nào khác:
20
  Kết quả: <nội dung chính của câu hỏi 1>|<nội dung chính của câu hỏi 2>...
@@ -23,7 +23,7 @@ class QueryTransform:
23
  Câu hỏi gốc: {query}
24
  """)
25
 
26
- self.model = ChatGoogleGenerativeAI(model=model, temperature=temperature, api_key= "AIzaSyAZ9e7fpVwT_Ao0c2q5IuvZIvuSpN-EXG0")
27
  self.parser = StrOutputParser()
28
 
29
  def transform(self, raw_query, history):
 
13
  4. NẾU CÂU HỎI chứa **SỐ ĐIỀU/SỐ CHƯƠNG cụ thể** và NẾU trong lịch sử trò chuyện gần nhất có chứa:
14
  + "Vui lòng chọn Điều cụ thể.", thì ý định của câu hỏi là: **Khoản <số Khoản trong lịch sử> Điều <SỐ/SỐ ĐIỀU trong câu hỏi>. Ngược lại ý định là: Điều <SỐ/SỐ ĐIỀU trong câu hỏi> **
15
  + "Vui lòng chọn Chương cụ thể", thì ý định của câu hỏi là: **Mục <số Mục trong lịch sử> Chương <SỐ/SỐ CHƯƠNG trong câu hỏi>. Ngược lại ý định là: Chương <SỐ/SỐ CHƯƠNG trong câu hỏi> **
16
+ 5. **NẾU CÂU HỎI KHÔNG RÕ RÀNG HOẶC CHƯA ĐẦY ĐỦ**, hãy sử dụng lịch sử trò chuyện được cung cấp **CHỈ KHI LỊCH SỬ LIÊN QUAN TRỰC TIẾP ĐẾN CÂU HỎI** để làm rõ ý nghĩa và ngữ cảnh truy vấn của người dùng. **KHÔNG ĐƯỢC THAY ĐỔI NỘI DUNG CHÍNH CỦA CÂU HỎI**.
17
+ 6. Suy ra nội dung chính của câu hỏi (hoặc câu khẳng định) thật đơn giản, rõ ràng và dễ hiểu (dùng các từ ngữ trong luật Hôn nhân và Gia đình nếu có thể), **BỎ QUA ĐẠI TỪ DANH XƯNG NẾU CÓ THỂ**. Ví dụ: Tôi là nữ 16 tuổi thì có lấy chồng được không? -> Nữ 16 tuổi có đủ điều kiện kết hôn không?
18
 
19
  Chỉ cung cấp kết quả **theo định dạng sau** và không thêm bất kỳ văn bản, giải thích hoặc bình luận nào khác:
20
  Kết quả: <nội dung chính của câu hỏi 1>|<nội dung chính của câu hỏi 2>...
 
23
  Câu hỏi gốc: {query}
24
  """)
25
 
26
+ self.model = ChatGoogleGenerativeAI(model=model, temperature=temperature, api_key= "AIzaSyB7CqaOvl9gRIhD7ZD61MRKsS_vS5v5VUk")
27
  self.parser = StrOutputParser()
28
 
29
  def transform(self, raw_query, history):
ChatBot/retrieval.py CHANGED
@@ -81,8 +81,7 @@ class Retriever:
81
  docs = [doc.page_content for doc in results]
82
  docs = self.rerank(query, docs)
83
  results = [results[i] for i in docs]
84
- for r in results:
85
- print (r)
86
  res = {}
87
  refers = set()
88
  for doc in results[:top_n]:
 
81
  docs = [doc.page_content for doc in results]
82
  docs = self.rerank(query, docs)
83
  results = [results[i] for i in docs]
84
+
 
85
  res = {}
86
  refers = set()
87
  for doc in results[:top_n]:
FrontEnd/src/ChatBot.css CHANGED
@@ -222,4 +222,4 @@
222
  display: inline-block;
223
  max-width: 80%;
224
  /* box-shadow: 0 2px 10px rgba(0, 0, 0, 0.1); */
225
- }
 
222
  display: inline-block;
223
  max-width: 80%;
224
  /* box-shadow: 0 2px 10px rgba(0, 0, 0, 0.1); */
225
+ }
FrontEnd/src/ChatBot.jsx CHANGED
@@ -8,59 +8,86 @@ const ChatBot = () => {
8
  const [isLoading, setIsLoading] = useState(false);
9
  const chatWindowRef = useRef(null);
10
 
 
11
  useEffect(() => {
12
  if (chatWindowRef.current) {
13
  chatWindowRef.current.scrollTop = chatWindowRef.current.scrollHeight;
14
  }
15
  }, [messages]);
16
 
17
- const sendQueryToBackend = async (query) => {
 
 
 
 
 
 
 
 
 
 
 
 
 
18
  try {
19
- const response = await fetch("http://127.0.0.1:8000/process_query", {
20
  method: "POST",
21
  headers: {
22
  "Content-Type": "application/json",
23
  },
24
- body: JSON.stringify({ query }),
25
  });
26
 
27
  if (!response.ok) {
28
  throw new Error("Lỗi khi gửi yêu cầu");
29
  }
30
 
31
- const data = await response.json();
32
- return data.response;
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
33
  } catch (error) {
34
  console.error("Lỗi:", error);
35
- return "Đã xảy ra lỗi khi kết nối với chatbot.";
 
 
 
 
 
36
  }
37
  };
38
 
39
- const handleSend = async () => {
40
- if (userInput.trim() === "") return;
41
-
42
- setMessages((prevMessages) => [
43
- ...prevMessages,
44
- { sender: "user", text: userInput },
45
- ]);
46
-
47
- setUserInput("");
48
- setIsLoading(true);
49
-
50
- const botResponse = await sendQueryToBackend(userInput);
51
-
52
- setMessages((prevMessages) => [
53
- ...prevMessages.filter((msg) => msg.text !== "..."),
54
- { sender: "bot", text: botResponse },
55
- ]);
56
-
57
- setIsLoading(false);
58
- };
59
-
60
  const handleNewChat = async () => {
61
- // setIsLoading(true);
62
- // Reset messages về mảng rỗng
63
- setMessages([]);
64
  try {
65
  const response = await fetch("http://127.0.0.1:8000/newchat/", {
66
  method: "GET",
@@ -72,25 +99,12 @@ const ChatBot = () => {
72
 
73
  const data = await response.json();
74
  console.log(data.message); // Chỉ log ra console, không thêm vào tin nhắn
75
-
76
  } catch (error) {
77
  console.error("Lỗi:", error);
78
- // Nếu có lỗi, hiển thị thông báo lỗi trong console, không thêm vào tin nhắn
79
  }
80
- // finally {
81
- // setIsLoading(false);
82
- // }
83
  };
84
 
85
- useEffect(() => {
86
- if (isLoading) {
87
- setMessages((prevMessages) => [
88
- ...prevMessages,
89
- { sender: "bot", text: "..." },
90
- ]);
91
- }
92
- }, [isLoading]);
93
-
94
  const formatBotResponse = (text) => {
95
  return text.split("\n").map((line, index) => (
96
  <p key={index}>
@@ -111,6 +125,7 @@ const ChatBot = () => {
111
  return (
112
  <div className="chatbot-container">
113
  <div className="chat-header">
 
114
  <h1>CHATBOT HỎI ĐÁP VỀ LUẬT HÔN NHÂN VÀ GIA ĐÌNH VIỆT NAM</h1>
115
  <button className="new-chat-btn" onClick={handleNewChat} title="Bắt đầu cuộc trò chuyện mới">
116
  <FaCommentDots size={24} color="white" />
@@ -132,8 +147,7 @@ const ChatBot = () => {
132
  {messages.map((message, index) => (
133
  <div
134
  key={index}
135
- className={`message-container ${message.sender === "user" ? "user" : "bot"
136
- }`}
137
  >
138
  {message.sender === "user" && (
139
  <div className="icon-container user-icon">
@@ -141,8 +155,7 @@ const ChatBot = () => {
141
  </div>
142
  )}
143
  <div
144
- className={`message ${message.sender} ${message.text === "..." ? "typing" : ""
145
- }`}
146
  >
147
  {message.sender === "bot" && message.text === "..." ? (
148
  <div className="typing-indicator">
@@ -172,12 +185,16 @@ const ChatBot = () => {
172
  value={userInput}
173
  onChange={(e) => setUserInput(e.target.value)}
174
  placeholder="Nhập câu hỏi..."
175
- onKeyPress={(e) => e.key === "Enter" && handleSend()}
 
 
 
 
176
  />
177
  <button
178
  onClick={handleSend}
179
- disabled={userInput.trim() === ""}
180
- className={`send-button ${userInput.trim() === "" ? "disabled" : ""}`}
181
  title="Gửi"
182
  >
183
  <FaPaperPlane size={20} color="white" />
 
8
  const [isLoading, setIsLoading] = useState(false);
9
  const chatWindowRef = useRef(null);
10
 
11
+ // Tự động cuộn xuống dưới cùng khi có tin nhắn mới
12
  useEffect(() => {
13
  if (chatWindowRef.current) {
14
  chatWindowRef.current.scrollTop = chatWindowRef.current.scrollHeight;
15
  }
16
  }, [messages]);
17
 
18
+ // Xử gửi tin nhắn
19
+ const handleSend = async () => {
20
+ if (userInput.trim() === "" || isLoading) return; // Ngăn chặn gửi tin nhắn nếu đang tải
21
+
22
+ // Thêm tin nhắn của người dùng và một dấu ba chấm của bot
23
+ setMessages((prevMessages) => [
24
+ ...prevMessages,
25
+ { sender: "user", text: userInput },
26
+ { sender: "bot", text: "..." }, // Chỉ thêm một dấu ba chấm
27
+ ]);
28
+
29
+ setUserInput("");
30
+ setIsLoading(true);
31
+
32
  try {
33
+ const response = await fetch("http://127.0.0.1:8000/process_query_stream/", {
34
  method: "POST",
35
  headers: {
36
  "Content-Type": "application/json",
37
  },
38
+ body: JSON.stringify({ query: userInput }),
39
  });
40
 
41
  if (!response.ok) {
42
  throw new Error("Lỗi khi gửi yêu cầu");
43
  }
44
 
45
+ const reader = response.body.getReader();
46
+ const decoder = new TextDecoder();
47
+ let botResponse = "";
48
+ let isFirstChunk = true; // Biến để kiểm tra chunk đầu tiên
49
+
50
+ while (true) {
51
+ const { done, value } = await reader.read();
52
+ if (done) break;
53
+
54
+ // Giải mã và cập nhật câu trả lời từng phần
55
+ const chunk = decoder.decode(value);
56
+ botResponse += chunk;
57
+
58
+ // Nếu là chunk đầu tiên, thay thế dấu ba chấm bằng chunk đầu tiên
59
+ if (isFirstChunk) {
60
+ setMessages((prevMessages) => [
61
+ ...prevMessages.filter((msg) => msg.text !== "..."), // Xóa dấu ba chấm
62
+ { sender: "bot", text: botResponse }, // Thêm chunk đầu tiên
63
+ ]);
64
+ isFirstChunk = false; // Đánh dấu đã xử lý chunk đầu tiên
65
+ } else {
66
+ // Cập nhật tin nhắn cuối cùng của bot với chunk tiếp theo
67
+ setMessages((prevMessages) => {
68
+ const newMessages = [...prevMessages];
69
+ const lastMessageIndex = newMessages.length - 1;
70
+ if (newMessages[lastMessageIndex].sender === "bot") {
71
+ newMessages[lastMessageIndex].text = botResponse;
72
+ }
73
+ return newMessages;
74
+ });
75
+ }
76
+ }
77
  } catch (error) {
78
  console.error("Lỗi:", error);
79
+ setMessages((prevMessages) => [
80
+ ...prevMessages.filter((msg) => msg.text !== "..."), // Xóa dấu ba chấm
81
+ { sender: "bot", text: "Đã xảy ra lỗi khi kết nối với chatbot." }, // Thêm thông báo lỗi
82
+ ]);
83
+ } finally {
84
+ setIsLoading(false);
85
  }
86
  };
87
 
88
+ // Xử bắt đầu cuộc trò chuyện mới
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
89
  const handleNewChat = async () => {
90
+ setMessages([]); // Reset messages về mảng rỗng
 
 
91
  try {
92
  const response = await fetch("http://127.0.0.1:8000/newchat/", {
93
  method: "GET",
 
99
 
100
  const data = await response.json();
101
  console.log(data.message); // Chỉ log ra console, không thêm vào tin nhắn
 
102
  } catch (error) {
103
  console.error("Lỗi:", error);
 
104
  }
 
 
 
105
  };
106
 
107
+ // Định dạng câu trả lời của bot (in đậm các phần quan trọng)
 
 
 
 
 
 
 
 
108
  const formatBotResponse = (text) => {
109
  return text.split("\n").map((line, index) => (
110
  <p key={index}>
 
125
  return (
126
  <div className="chatbot-container">
127
  <div className="chat-header">
128
+ <FaRobot size={30} style={{ marginRight: "10px" }} /> {/* Thêm icon chatbot */}
129
  <h1>CHATBOT HỎI ĐÁP VỀ LUẬT HÔN NHÂN VÀ GIA ĐÌNH VIỆT NAM</h1>
130
  <button className="new-chat-btn" onClick={handleNewChat} title="Bắt đầu cuộc trò chuyện mới">
131
  <FaCommentDots size={24} color="white" />
 
147
  {messages.map((message, index) => (
148
  <div
149
  key={index}
150
+ className={`message-container ${message.sender === "user" ? "user" : "bot"}`}
 
151
  >
152
  {message.sender === "user" && (
153
  <div className="icon-container user-icon">
 
155
  </div>
156
  )}
157
  <div
158
+ className={`message ${message.sender} ${message.text === "..." ? "typing" : ""}`}
 
159
  >
160
  {message.sender === "bot" && message.text === "..." ? (
161
  <div className="typing-indicator">
 
185
  value={userInput}
186
  onChange={(e) => setUserInput(e.target.value)}
187
  placeholder="Nhập câu hỏi..."
188
+ onKeyPress={(e) => {
189
+ if (e.key === "Enter" && !isLoading) {
190
+ handleSend();
191
+ }
192
+ }}
193
  />
194
  <button
195
  onClick={handleSend}
196
+ disabled={userInput.trim() === "" || isLoading} // Vô hiệu hóa nút gửi khi đang tải
197
+ className={`send-button ${userInput.trim() === "" || isLoading ? "disabled" : ""}`}
198
  title="Gửi"
199
  >
200
  <FaPaperPlane size={20} color="white" />