feat: rebrand to HutMind, adjust bot response width, add copy button
Browse files- FrontEnd/package-lock.json +12 -0
- FrontEnd/package.json +1 -0
- FrontEnd/src/ChatBot.css +43 -1
- FrontEnd/src/ChatBot.jsx +49 -24
FrontEnd/package-lock.json
CHANGED
|
@@ -8,6 +8,7 @@
|
|
| 8 |
"name": "frontend",
|
| 9 |
"version": "0.0.0",
|
| 10 |
"dependencies": {
|
|
|
|
| 11 |
"react": "^18.3.1",
|
| 12 |
"react-dom": "^18.3.1",
|
| 13 |
"react-icons": "^5.4.0",
|
|
@@ -943,6 +944,16 @@
|
|
| 943 |
"@jridgewell/sourcemap-codec": "^1.4.14"
|
| 944 |
}
|
| 945 |
},
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 946 |
"node_modules/@rollup/rollup-android-arm-eabi": {
|
| 947 |
"version": "4.30.1",
|
| 948 |
"resolved": "https://registry.npmjs.org/@rollup/rollup-android-arm-eabi/-/rollup-android-arm-eabi-4.30.1.tgz",
|
|
@@ -4337,6 +4348,7 @@
|
|
| 4337 |
"version": "18.3.1",
|
| 4338 |
"resolved": "https://registry.npmjs.org/react-dom/-/react-dom-18.3.1.tgz",
|
| 4339 |
"integrity": "sha512-5m4nQKp+rZRb09LNH59GM4BxTh9251/ylbKIbpe7TpGxfJ+9kv6BLkLBXIjjspbgbnIBNqlI23tRnTWT0snUIw==",
|
|
|
|
| 4340 |
"dependencies": {
|
| 4341 |
"loose-envify": "^1.1.0",
|
| 4342 |
"scheduler": "^0.23.2"
|
|
|
|
| 8 |
"name": "frontend",
|
| 9 |
"version": "0.0.0",
|
| 10 |
"dependencies": {
|
| 11 |
+
"@react-oauth/google": "^0.13.4",
|
| 12 |
"react": "^18.3.1",
|
| 13 |
"react-dom": "^18.3.1",
|
| 14 |
"react-icons": "^5.4.0",
|
|
|
|
| 944 |
"@jridgewell/sourcemap-codec": "^1.4.14"
|
| 945 |
}
|
| 946 |
},
|
| 947 |
+
"node_modules/@react-oauth/google": {
|
| 948 |
+
"version": "0.13.4",
|
| 949 |
+
"resolved": "https://registry.npmjs.org/@react-oauth/google/-/google-0.13.4.tgz",
|
| 950 |
+
"integrity": "sha512-hGKyNEH+/PK8M0sFEuo3MAEk0txtHpgs94tDQit+s2LXg7b6z53NtzHfqDvoB2X8O6lGB+FRg80hY//X6hfD+w==",
|
| 951 |
+
"license": "MIT",
|
| 952 |
+
"peerDependencies": {
|
| 953 |
+
"react": ">=16.8.0",
|
| 954 |
+
"react-dom": ">=16.8.0"
|
| 955 |
+
}
|
| 956 |
+
},
|
| 957 |
"node_modules/@rollup/rollup-android-arm-eabi": {
|
| 958 |
"version": "4.30.1",
|
| 959 |
"resolved": "https://registry.npmjs.org/@rollup/rollup-android-arm-eabi/-/rollup-android-arm-eabi-4.30.1.tgz",
|
|
|
|
| 4348 |
"version": "18.3.1",
|
| 4349 |
"resolved": "https://registry.npmjs.org/react-dom/-/react-dom-18.3.1.tgz",
|
| 4350 |
"integrity": "sha512-5m4nQKp+rZRb09LNH59GM4BxTh9251/ylbKIbpe7TpGxfJ+9kv6BLkLBXIjjspbgbnIBNqlI23tRnTWT0snUIw==",
|
| 4351 |
+
"peer": true,
|
| 4352 |
"dependencies": {
|
| 4353 |
"loose-envify": "^1.1.0",
|
| 4354 |
"scheduler": "^0.23.2"
|
FrontEnd/package.json
CHANGED
|
@@ -10,6 +10,7 @@
|
|
| 10 |
"preview": "vite preview"
|
| 11 |
},
|
| 12 |
"dependencies": {
|
|
|
|
| 13 |
"react": "^18.3.1",
|
| 14 |
"react-dom": "^18.3.1",
|
| 15 |
"react-icons": "^5.4.0",
|
|
|
|
| 10 |
"preview": "vite preview"
|
| 11 |
},
|
| 12 |
"dependencies": {
|
| 13 |
+
"@react-oauth/google": "^0.13.4",
|
| 14 |
"react": "^18.3.1",
|
| 15 |
"react-dom": "^18.3.1",
|
| 16 |
"react-icons": "^5.4.0",
|
FrontEnd/src/ChatBot.css
CHANGED
|
@@ -184,7 +184,8 @@
|
|
| 184 |
}
|
| 185 |
|
| 186 |
.message-content {
|
| 187 |
-
|
|
|
|
| 188 |
overflow-wrap: break-word;
|
| 189 |
font-size: 16px;
|
| 190 |
line-height: 1.6;
|
|
@@ -193,6 +194,7 @@
|
|
| 193 |
.message-row.user .message-content {
|
| 194 |
display: flex;
|
| 195 |
justify-content: flex-end;
|
|
|
|
| 196 |
}
|
| 197 |
|
| 198 |
.user-text {
|
|
@@ -204,9 +206,49 @@
|
|
| 204 |
box-shadow: 0 2px 8px rgba(227, 24, 55, 0.15);
|
| 205 |
}
|
| 206 |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 207 |
/* React Markdown Styles */
|
| 208 |
.markdown-body {
|
| 209 |
color: #374151;
|
|
|
|
| 210 |
}
|
| 211 |
|
| 212 |
.markdown-body p {
|
|
|
|
| 184 |
}
|
| 185 |
|
| 186 |
.message-content {
|
| 187 |
+
width: 100%;
|
| 188 |
+
max-width: 100%;
|
| 189 |
overflow-wrap: break-word;
|
| 190 |
font-size: 16px;
|
| 191 |
line-height: 1.6;
|
|
|
|
| 194 |
.message-row.user .message-content {
|
| 195 |
display: flex;
|
| 196 |
justify-content: flex-end;
|
| 197 |
+
max-width: 80%;
|
| 198 |
}
|
| 199 |
|
| 200 |
.user-text {
|
|
|
|
| 206 |
box-shadow: 0 2px 8px rgba(227, 24, 55, 0.15);
|
| 207 |
}
|
| 208 |
|
| 209 |
+
/* Bot message wrapper for copy button positioning */
|
| 210 |
+
.bot-message-wrapper {
|
| 211 |
+
position: relative;
|
| 212 |
+
width: 100%;
|
| 213 |
+
}
|
| 214 |
+
|
| 215 |
+
.copy-btn {
|
| 216 |
+
background-color: transparent;
|
| 217 |
+
color: #9ca3af; /* Gray icon */
|
| 218 |
+
margin-top: 12px;
|
| 219 |
+
margin-left: -8px; /* Alignment with text */
|
| 220 |
+
border: none;
|
| 221 |
+
cursor: pointer;
|
| 222 |
+
padding: 8px;
|
| 223 |
+
display: flex;
|
| 224 |
+
align-items: center;
|
| 225 |
+
justify-content: center;
|
| 226 |
+
transition: all 0.2s ease;
|
| 227 |
+
border-radius: 50%; /* Circular */
|
| 228 |
+
opacity: 0; /* Hidden by default */
|
| 229 |
+
width: 36px;
|
| 230 |
+
height: 36px;
|
| 231 |
+
}
|
| 232 |
+
|
| 233 |
+
/* Always show icon for the latest response */
|
| 234 |
+
.bot-message-wrapper.latest .copy-btn {
|
| 235 |
+
opacity: 1;
|
| 236 |
+
}
|
| 237 |
+
|
| 238 |
+
/* Show icon on hover for all responses */
|
| 239 |
+
.bot-message-wrapper:hover .copy-btn {
|
| 240 |
+
opacity: 1;
|
| 241 |
+
}
|
| 242 |
+
|
| 243 |
+
.copy-btn:hover {
|
| 244 |
+
background-color: #f3f4f6; /* Light circular hover */
|
| 245 |
+
color: #374151; /* Darker on hover */
|
| 246 |
+
}
|
| 247 |
+
|
| 248 |
/* React Markdown Styles */
|
| 249 |
.markdown-body {
|
| 250 |
color: #374151;
|
| 251 |
+
width: 100%;
|
| 252 |
}
|
| 253 |
|
| 254 |
.markdown-body p {
|
FrontEnd/src/ChatBot.jsx
CHANGED
|
@@ -1,5 +1,6 @@
|
|
| 1 |
import { useState, useEffect, useRef } from "react";
|
| 2 |
import { FaUser, FaRobot, FaPaperPlane, FaCircle, FaPlus, FaPizzaSlice } from "react-icons/fa";
|
|
|
|
| 3 |
import ReactMarkdown from "react-markdown";
|
| 4 |
import "./ChatBot.css";
|
| 5 |
|
|
@@ -9,6 +10,7 @@ const ChatBot = () => {
|
|
| 9 |
const [messages, setMessages] = useState([]);
|
| 10 |
const [userInput, setUserInput] = useState("");
|
| 11 |
const [isLoading, setIsLoading] = useState(false);
|
|
|
|
| 12 |
const chatWindowRef = useRef(null);
|
| 13 |
const textareaRef = useRef(null);
|
| 14 |
|
|
@@ -115,14 +117,20 @@ const ChatBot = () => {
|
|
| 115 |
}
|
| 116 |
};
|
| 117 |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 118 |
return (
|
| 119 |
<div className="chatbot-container">
|
| 120 |
<div className="chat-header">
|
| 121 |
<div className="header-logo" onClick={handleNewChat} title="Quay về trang chủ">
|
| 122 |
<img src="/pizzahut.png" alt="Pizza Hut Logo" width={50} height={50} />
|
| 123 |
<div className="header-title">
|
| 124 |
-
<h1>HutMind
|
| 125 |
-
<span>Powered by Tan Mai</span>
|
| 126 |
</div>
|
| 127 |
</div>
|
| 128 |
<button className="new-chat-btn" onClick={handleNewChat} title="Cuộc trò chuyện mới">
|
|
@@ -162,28 +170,45 @@ const ChatBot = () => {
|
|
| 162 |
)}
|
| 163 |
|
| 164 |
<div className="messages-list">
|
| 165 |
-
{messages.map((message, index) =>
|
| 166 |
-
|
| 167 |
-
|
| 168 |
-
|
| 169 |
-
|
| 170 |
-
|
| 171 |
-
|
| 172 |
-
|
| 173 |
-
|
| 174 |
-
|
| 175 |
-
|
| 176 |
-
|
| 177 |
-
|
| 178 |
-
|
| 179 |
-
|
| 180 |
-
|
| 181 |
-
|
| 182 |
-
|
| 183 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 184 |
</div>
|
| 185 |
-
|
| 186 |
-
)
|
| 187 |
</div>
|
| 188 |
</div>
|
| 189 |
|
|
@@ -213,7 +238,7 @@ const ChatBot = () => {
|
|
| 213 |
</button>
|
| 214 |
</div>
|
| 215 |
<div className="footer-text">
|
| 216 |
-
|
| 217 |
</div>
|
| 218 |
</div>
|
| 219 |
</div>
|
|
|
|
| 1 |
import { useState, useEffect, useRef } from "react";
|
| 2 |
import { FaUser, FaRobot, FaPaperPlane, FaCircle, FaPlus, FaPizzaSlice } from "react-icons/fa";
|
| 3 |
+
import { MdOutlineContentCopy, MdCheck } from "react-icons/md";
|
| 4 |
import ReactMarkdown from "react-markdown";
|
| 5 |
import "./ChatBot.css";
|
| 6 |
|
|
|
|
| 10 |
const [messages, setMessages] = useState([]);
|
| 11 |
const [userInput, setUserInput] = useState("");
|
| 12 |
const [isLoading, setIsLoading] = useState(false);
|
| 13 |
+
const [copiedIndex, setCopiedIndex] = useState(null);
|
| 14 |
const chatWindowRef = useRef(null);
|
| 15 |
const textareaRef = useRef(null);
|
| 16 |
|
|
|
|
| 117 |
}
|
| 118 |
};
|
| 119 |
|
| 120 |
+
const copyToClipboard = (text, index) => {
|
| 121 |
+
navigator.clipboard.writeText(text).then(() => {
|
| 122 |
+
setCopiedIndex(index);
|
| 123 |
+
setTimeout(() => setCopiedIndex(null), 2000);
|
| 124 |
+
});
|
| 125 |
+
};
|
| 126 |
+
|
| 127 |
return (
|
| 128 |
<div className="chatbot-container">
|
| 129 |
<div className="chat-header">
|
| 130 |
<div className="header-logo" onClick={handleNewChat} title="Quay về trang chủ">
|
| 131 |
<img src="/pizzahut.png" alt="Pizza Hut Logo" width={50} height={50} />
|
| 132 |
<div className="header-title">
|
| 133 |
+
<h1>HutMind</h1>
|
|
|
|
| 134 |
</div>
|
| 135 |
</div>
|
| 136 |
<button className="new-chat-btn" onClick={handleNewChat} title="Cuộc trò chuyện mới">
|
|
|
|
| 170 |
)}
|
| 171 |
|
| 172 |
<div className="messages-list">
|
| 173 |
+
{messages.map((message, index) => {
|
| 174 |
+
const lastBotIndex = [...messages].reverse().findIndex(m => m.sender === 'bot');
|
| 175 |
+
const actualLastBotIndex = lastBotIndex !== -1 ? messages.length - 1 - lastBotIndex : -1;
|
| 176 |
+
const isLatestBotMessage = index === actualLastBotIndex;
|
| 177 |
+
|
| 178 |
+
return (
|
| 179 |
+
<div
|
| 180 |
+
key={index}
|
| 181 |
+
className={`message-row ${message.sender}`}
|
| 182 |
+
>
|
| 183 |
+
<div className="message-content">
|
| 184 |
+
{message.sender === "bot" && message.text === "..." ? (
|
| 185 |
+
<div className="typing-indicator">
|
| 186 |
+
<FaCircle className="dot dot1" />
|
| 187 |
+
<FaCircle className="dot dot2" />
|
| 188 |
+
<FaCircle className="dot dot3" />
|
| 189 |
+
</div>
|
| 190 |
+
) : message.sender === "bot" ? (
|
| 191 |
+
<div className={`bot-message-wrapper ${isLatestBotMessage ? 'latest' : ''}`}>
|
| 192 |
+
<div className="markdown-body">
|
| 193 |
+
<ReactMarkdown>{message.text}</ReactMarkdown>
|
| 194 |
+
</div>
|
| 195 |
+
{message.text !== "..." && (
|
| 196 |
+
<button
|
| 197 |
+
className="copy-btn"
|
| 198 |
+
onClick={() => copyToClipboard(message.text, index)}
|
| 199 |
+
title="Sao chép"
|
| 200 |
+
>
|
| 201 |
+
{copiedIndex === index ? <MdCheck size={18} /> : <MdOutlineContentCopy size={18} />}
|
| 202 |
+
</button>
|
| 203 |
+
)}
|
| 204 |
+
</div>
|
| 205 |
+
) : (
|
| 206 |
+
<div className="user-text">{message.text}</div>
|
| 207 |
+
)}
|
| 208 |
+
</div>
|
| 209 |
</div>
|
| 210 |
+
);
|
| 211 |
+
})}
|
| 212 |
</div>
|
| 213 |
</div>
|
| 214 |
|
|
|
|
| 238 |
</button>
|
| 239 |
</div>
|
| 240 |
<div className="footer-text">
|
| 241 |
+
HutMind có thể mắc lỗi. Vui lòng kiểm tra lại các thông tin quan trọng.
|
| 242 |
</div>
|
| 243 |
</div>
|
| 244 |
</div>
|