ok
This commit is contained in:
13
src/App.css
13
src/App.css
@@ -1,7 +1,14 @@
|
||||
html, body, #root {
|
||||
height: 100%;
|
||||
margin: 0;
|
||||
}
|
||||
|
||||
.App {
|
||||
display: flex;
|
||||
align-items: end;
|
||||
justify-content: center;
|
||||
height: 100%;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
background: #fff;
|
||||
align-items: center;
|
||||
}
|
||||
|
||||
.App-logo {
|
||||
|
||||
393
src/ChatBot.js
393
src/ChatBot.js
@@ -1,220 +1,203 @@
|
||||
import React, { useState, useEffect } from 'react';
|
||||
import styles from './ChatBot.module.css';
|
||||
import React, { useState, useEffect } from 'react';
|
||||
import styles from './ChatBot.module.css';
|
||||
|
||||
const ChatBot = ({ existingConversation, readOnly, hh }) => {
|
||||
const [messages, setMessages] = useState([
|
||||
{
|
||||
sender: 'bot',
|
||||
text: 'Hai Dermalovers! 👋 Saya siap membantu anda tampil lebih percaya diri. Ada pertanyaan seputar perawatan kulit atau kecantikan hari ini?',
|
||||
time: getTime(),
|
||||
quickReplies: [
|
||||
'List harga layanan Dermalounge',
|
||||
'Beri saya info jadwal dokter',
|
||||
'Apa saja layanan disini',
|
||||
],
|
||||
},
|
||||
]);
|
||||
const ChatBot = ({ existingConversation, readOnly, hh }) => {
|
||||
const [messages, setMessages] = useState([
|
||||
{
|
||||
sender: 'bot',
|
||||
text: 'Hai Dermalovers! 👋 Saya siap membantu anda tampil lebih percaya diri. Ada pertanyaan seputar perawatan kulit atau kecantikan hari ini?',
|
||||
time: getTime(),
|
||||
quickReplies: [
|
||||
'Info layanan Dermalounge',
|
||||
'Apa perawatan wajah recommended',
|
||||
'Saya ingin konsultasi masalah kulit',
|
||||
'Info lokasi & cara booking',
|
||||
],
|
||||
},
|
||||
]);
|
||||
|
||||
const [input, setInput] = useState('');
|
||||
const [isLoading, setIsLoading] = useState(false);
|
||||
useEffect(() => {
|
||||
const [input, setInput] = useState('');
|
||||
const [isLoading, setIsLoading] = useState(false);
|
||||
useEffect(() => {
|
||||
|
||||
if (existingConversation && existingConversation.length > 0) {
|
||||
setMessages(existingConversation);
|
||||
}
|
||||
}, [existingConversation])
|
||||
useEffect(() => {
|
||||
if (!localStorage.getItem('session')) {
|
||||
function generateUUID() {
|
||||
return 'xxxxxxxx-xxxx-4xxx-yxxx-xxxxxxxxxxxx'.replace(/[xy]/g, function (c) {
|
||||
const r = Math.random() * 16 | 0;
|
||||
const v = c === 'x' ? r : (r & 0x3 | 0x8);
|
||||
return v.toString(16);
|
||||
});
|
||||
}
|
||||
|
||||
const sessionId = generateUUID();
|
||||
const dateNow = new Date().toISOString();
|
||||
|
||||
localStorage.setItem('session', JSON.stringify({ sessionId: sessionId, lastSeen: dateNow }))
|
||||
}
|
||||
}, []);
|
||||
|
||||
const sendMessage = async (textOverride = null) => {
|
||||
const message = textOverride || input.trim();
|
||||
if (message === '') return;
|
||||
|
||||
// Show user's message immediately
|
||||
const newMessages = [
|
||||
...messages,
|
||||
{ sender: 'user', text: message, time: getTime() },
|
||||
];
|
||||
|
||||
setMessages(newMessages);
|
||||
setInput('');
|
||||
|
||||
setIsLoading(true);
|
||||
try {
|
||||
// Send to backend
|
||||
const response = await fetch('https://bot.kediritechnopark.com/webhook/master-agent/ask', {
|
||||
method: 'POST',
|
||||
headers: { 'Content-Type': 'application/json' },
|
||||
body: JSON.stringify({ pertanyaan: message, sessionId: JSON.parse(localStorage.getItem('session')).sessionId, lastSeen: new Date().toISOString() }),
|
||||
if (existingConversation && existingConversation.length > 0) {
|
||||
setMessages(existingConversation);
|
||||
}
|
||||
}, [existingConversation])
|
||||
useEffect(() => {
|
||||
if (!localStorage.getItem('session')) {
|
||||
function generateUUID() {
|
||||
return 'xxxxxxxx-xxxx-4xxx-yxxx-xxxxxxxxxxxx'.replace(/[xy]/g, function (c) {
|
||||
const r = Math.random() * 16 | 0;
|
||||
const v = c === 'x' ? r : (r & 0x3 | 0x8);
|
||||
return v.toString(16);
|
||||
});
|
||||
}
|
||||
|
||||
const data = await response.json();
|
||||
console.log(data)
|
||||
// Assuming your backend sends back something like: { answer: "text" }
|
||||
// Adjust this according to your actual response shape
|
||||
const botAnswer = data[0].output[0].text || data[0].output || 'Maaf, saya tidak mengerti.';
|
||||
const sessionId = generateUUID();
|
||||
const dateNow = new Date().toISOString();
|
||||
|
||||
// Add bot's reply
|
||||
localStorage.setItem('session', JSON.stringify({ sessionId: sessionId, lastSeen: dateNow }))
|
||||
}
|
||||
}, []);
|
||||
|
||||
const sendMessage = async (textOverride = null, tryCount = 0) => {
|
||||
const message = textOverride || input.trim();
|
||||
if (message === '') return;
|
||||
|
||||
// Show user's message immediately
|
||||
const newMessages = [
|
||||
...messages,
|
||||
{ sender: 'user', text: message, time: getTime() },
|
||||
];
|
||||
|
||||
setMessages(newMessages);
|
||||
setInput('');
|
||||
|
||||
setIsLoading(true);
|
||||
try {
|
||||
// Send to backend
|
||||
const response = await fetch('https://bot.kediritechnopark.com/webhook/master-agent/ask/dev', {
|
||||
method: 'POST',
|
||||
headers: { 'Content-Type': 'application/json' },
|
||||
body: JSON.stringify({ pertanyaan: message, sessionId: JSON.parse(localStorage.getItem('session')).sessionId, lastSeen: new Date().toISOString() }),
|
||||
});
|
||||
|
||||
const data = await response.json();
|
||||
console.log(data)
|
||||
// Assuming your backend sends back something like: { answer: "text" }
|
||||
// Adjust this according to your actual response shape
|
||||
const botAnswer = data[0].output[0].text || data[0].output || 'Maaf, saya tidak mengerti.';
|
||||
|
||||
// Add bot's reply
|
||||
setMessages(prev => [
|
||||
...prev,
|
||||
{ sender: 'bot', text: botAnswer, time: getTime() },
|
||||
]);
|
||||
|
||||
setIsLoading(false);
|
||||
} catch (error) {
|
||||
console.log(tryCount)
|
||||
if (tryCount > 3) {
|
||||
// Add bot's error reply
|
||||
setMessages(prev => [
|
||||
...prev,
|
||||
{ sender: 'bot', text: botAnswer, time: getTime() },
|
||||
{ sender: 'bot', text: 'Maaf saya sedang tidak tersedia sekarang, coba lagi nanti', time: getTime() },
|
||||
]);
|
||||
|
||||
setIsLoading(false);
|
||||
} catch (error) {
|
||||
sendMessage('gimana')
|
||||
console.error('Fetch error:', error);
|
||||
} finally {
|
||||
setIsLoading(false);
|
||||
return;
|
||||
}
|
||||
};
|
||||
setTimeout(() => sendMessage(message, tryCount + 1), 3000);
|
||||
|
||||
return (
|
||||
<div className={styles.chatContainer} style={{ height: hh || '100vh' }}>
|
||||
<div className={styles.chatHeader}>
|
||||
<img src="/dermalounge.jpg" alt="Bot Avatar" />
|
||||
<strong>DERMALOUNGE</strong>
|
||||
</div>
|
||||
|
||||
<div className={styles.chatBody}>
|
||||
|
||||
{isLoading && (
|
||||
<div className={`${styles.messageRow} ${styles.bot}`}>
|
||||
<div className={`${styles.message} ${styles.bot}`}>
|
||||
<em>Mengetik...</em>
|
||||
</div>
|
||||
</div>
|
||||
)}
|
||||
{messages.slice().reverse().map((msg, index) => (
|
||||
<div
|
||||
key={index}
|
||||
className={`${styles.messageRow} ${styles[msg.sender]}`}
|
||||
>
|
||||
<div className={`${styles.message} ${styles[msg.sender]}`}>
|
||||
{msg.sender !== 'bot'
|
||||
? msg.text
|
||||
: (() => {
|
||||
try {let cleanText = msg.text.replace(/`/g, ''); // Remove backticks
|
||||
cleanText = cleanText.substring(4); // Remove first 4 characters
|
||||
let parsedObj = JSON.parse(cleanText);
|
||||
|
||||
return parsedObj.jawaban;
|
||||
} catch (e) {
|
||||
return msg.text; // Return an empty string if there is an error
|
||||
}
|
||||
|
||||
})()}
|
||||
{msg.quickReplies && (
|
||||
<div className={styles.quickReplies}>
|
||||
{msg.quickReplies.map((reply, i) => (
|
||||
<div
|
||||
key={i}
|
||||
className={styles.quickReply}
|
||||
onClick={() => sendMessage(reply)}
|
||||
>
|
||||
{reply}
|
||||
</div>
|
||||
))}
|
||||
</div>
|
||||
)}
|
||||
<div className={styles.timestamp}>{msg.time}</div>
|
||||
</div>
|
||||
</div>
|
||||
))}
|
||||
</div>
|
||||
|
||||
<div className={styles.chatInput} style={{ visibility: readOnly ? 'hidden' : 'visible', marginTop: readOnly ? '-59px' : '0px' }}>
|
||||
<input
|
||||
type="text"
|
||||
placeholder="Ketik pesan..."
|
||||
value={input}
|
||||
onChange={(e) => setInput(e.target.value)}
|
||||
onKeyDown={(e) => e.key === 'Enter' && sendMessage()}
|
||||
disabled={isLoading}
|
||||
/>
|
||||
<button onClick={() => sendMessage()} disabled={isLoading}>
|
||||
Kirim
|
||||
</button>
|
||||
</div>
|
||||
<div style={{width: '96.6%'}}>
|
||||
<div style={{
|
||||
backgroundColor: '#f0f0f0',
|
||||
width: '100%',
|
||||
display: 'flex',
|
||||
flexDirection: 'row',
|
||||
overflowX: 'auto',
|
||||
padding: '8px',
|
||||
scrollbarWidth: 'none'
|
||||
}}>
|
||||
<div
|
||||
style={{
|
||||
flexShrink: 0,
|
||||
background: '#fff',
|
||||
border: '1px solid #ccc',
|
||||
padding: '8px 12px',
|
||||
borderRadius: '20px',
|
||||
fontSize: '13px',
|
||||
cursor: 'pointer',
|
||||
margin: '3px'
|
||||
}}
|
||||
onClick={() => sendMessage('Dapatkah bopeng dihilangkan?')}
|
||||
>
|
||||
Dapatkah bopeng dihilangkan?
|
||||
</div>
|
||||
<div
|
||||
style={{
|
||||
flexShrink: 0,
|
||||
background: '#fff',
|
||||
border: '1px solid #ccc',
|
||||
padding: '8px 12px',
|
||||
borderRadius: '20px',
|
||||
fontSize: '13px',
|
||||
cursor: 'pointer',
|
||||
margin: '3px'
|
||||
}}
|
||||
onClick={() => sendMessage('Bisa booking treatment untuk besok?')}
|
||||
>
|
||||
Bisa booking treatment untuk besok?
|
||||
</div>
|
||||
<div
|
||||
style={{
|
||||
flexShrink: 0,
|
||||
background: '#fff',
|
||||
border: '1px solid #ccc',
|
||||
padding: '8px 12px',
|
||||
borderRadius: '20px',
|
||||
fontSize: '13px',
|
||||
cursor: 'pointer',
|
||||
margin: '3px'
|
||||
}}
|
||||
onClick={() => sendMessage('Bisa booking treatment untuk besok?')}
|
||||
>
|
||||
Ada treatment untuk jerawat?
|
||||
</div>
|
||||
</div>
|
||||
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
console.error('Fetch error:', error);
|
||||
}
|
||||
};
|
||||
|
||||
function getTime() {
|
||||
const now = new Date();
|
||||
return now.toLocaleTimeString([], { hour: '2-digit', minute: '2-digit' });
|
||||
}
|
||||
return (
|
||||
<div className={styles.chatContainer} >
|
||||
<div className={styles.chatHeader}>
|
||||
<img src="/dermalounge.jpg" alt="Bot Avatar" />
|
||||
<strong>DERMALOUNGE</strong>
|
||||
</div>
|
||||
|
||||
export default ChatBot;
|
||||
<div className={styles.chatBody}>
|
||||
|
||||
{isLoading && (
|
||||
<div className={`${styles.messageRow} ${styles.bot}`}>
|
||||
<div className={`${styles.message} ${styles.bot}`}>
|
||||
<em>Mengetik...</em>
|
||||
</div>
|
||||
</div>
|
||||
)}
|
||||
{messages.slice().reverse().map((msg, index) => (
|
||||
<div
|
||||
key={index}
|
||||
className={`${styles.messageRow} ${styles[msg.sender]}`}
|
||||
>
|
||||
<div className={`${styles.message} ${styles[msg.sender]}`}>
|
||||
{msg.sender !== 'bot'
|
||||
? msg.text
|
||||
: (() => {
|
||||
try {
|
||||
let cleanText = msg.text.replace(/`/g, ''); // Remove backticks
|
||||
cleanText = cleanText.substring(4); // Remove first 4 characters
|
||||
let parsedObj = JSON.parse(cleanText);
|
||||
|
||||
return parsedObj.jawaban;
|
||||
} catch (e) {
|
||||
return msg.text; // Return an empty string if there is an error
|
||||
}
|
||||
|
||||
})()}
|
||||
{msg.quickReplies && (
|
||||
<div className={styles.quickReplies}>
|
||||
{msg.quickReplies.map((reply, i) => (
|
||||
<div
|
||||
key={i}
|
||||
className={styles.quickReply}
|
||||
onClick={() => sendMessage(reply)}
|
||||
>
|
||||
{reply}
|
||||
</div>
|
||||
))}
|
||||
</div>
|
||||
)}
|
||||
<div className={styles.timestamp}>{msg.time}</div>
|
||||
</div>
|
||||
</div>
|
||||
))}
|
||||
</div>
|
||||
|
||||
<div className={styles.chatInput} /*/style={{ visibility: readOnly ? 'hidden' : 'visible'}}/*/>
|
||||
<input
|
||||
type="text"
|
||||
placeholder="Ketik pesan..."
|
||||
value={input}
|
||||
onChange={(e) => setInput(e.target.value)}
|
||||
onKeyDown={(e) => e.key === 'Enter' && sendMessage()}
|
||||
disabled={isLoading}
|
||||
/>
|
||||
<button onClick={() => sendMessage()} disabled={isLoading}>
|
||||
Kirim
|
||||
</button>
|
||||
</div>
|
||||
<div className={styles.PopUp}>
|
||||
|
||||
<div className={`${styles.message} ${styles['bot']}`}>
|
||||
Untuk bisa membantu Anda lebih jauh, boleh saya tahu nama dan nomor telepon Anda?
|
||||
Informasi ini juga membantu tim admin kami jika perlu melakukan follow-up nantinya 😊
|
||||
<div className={styles.quickReplies} style={{ flexDirection: 'column' }}>
|
||||
<input
|
||||
className={styles.quickReply}
|
||||
placeholder="Nama Lengkapmu"
|
||||
onFocus={() => console.log('Nama focused')}
|
||||
/>
|
||||
<div className={styles.inputGroup}>
|
||||
<span className={styles.prefix}>+62</span>
|
||||
<input
|
||||
type="tel"
|
||||
className={styles.quickReply}
|
||||
placeholder="Nomor HP"
|
||||
onFocus={() => console.log('Telepon focused')}
|
||||
style={{border: 0, width: '100%'}}
|
||||
/>
|
||||
</div>
|
||||
|
||||
<div
|
||||
className={styles.quickReply}
|
||||
>
|
||||
Lanjut
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
};
|
||||
|
||||
function getTime() {
|
||||
const now = new Date();
|
||||
return now.toLocaleTimeString([], { hour: '2-digit', minute: '2-digit' });
|
||||
}
|
||||
|
||||
export default ChatBot;
|
||||
|
||||
@@ -1,14 +1,23 @@
|
||||
.chatContainer {
|
||||
max-width: 500px;
|
||||
background: #fff;
|
||||
border-radius: 10px;
|
||||
box-shadow: 0 5px 15px rgba(0, 0, 0, 0.2);
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
height: 100vh;
|
||||
overflow: auto;
|
||||
height: 100%;
|
||||
background: #fff;
|
||||
border-radius: 10px;
|
||||
box-shadow: 0 5px 15px #0003;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
max-width: 500px;
|
||||
overflow: auto;
|
||||
position: relative;
|
||||
}
|
||||
|
||||
.PopUp{
|
||||
background-color: #555454ab;
|
||||
position: absolute;
|
||||
height: 100%;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
}
|
||||
.chatHeader {
|
||||
background: #075e54;
|
||||
color: #fff;
|
||||
@@ -117,6 +126,22 @@
|
||||
gap: 8px;
|
||||
margin: 10px 0 0;
|
||||
}
|
||||
.inputGroup {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
border: 1px solid #ccc;
|
||||
border-radius: 20px;
|
||||
overflow: hidden;
|
||||
width: 100%;
|
||||
}
|
||||
|
||||
.prefix {
|
||||
background-color: #f0f0f0;
|
||||
padding: 0.5rem 0.75rem;
|
||||
border-right: 1px solid #ccc;
|
||||
font-size: 12px;
|
||||
color: #555;
|
||||
}
|
||||
|
||||
.quickReply {
|
||||
background: #fff;
|
||||
|
||||
@@ -9,6 +9,8 @@ import DiscussedTopics from './DiscussedTopics';
|
||||
import Chart from 'chart.js/auto';
|
||||
|
||||
const Dashboard = () => {
|
||||
const [isMenuOpen, setIsMenuOpen] = useState(false);
|
||||
|
||||
const chartRef = useRef(null);
|
||||
const chartInstanceRef = useRef(null);
|
||||
const [conversations, setConversations] = useState([]);
|
||||
@@ -46,6 +48,22 @@ const Dashboard = () => {
|
||||
window.location.reload();
|
||||
};
|
||||
|
||||
const menuRef = useRef(null);
|
||||
|
||||
// Close dropdown if click outside
|
||||
useEffect(() => {
|
||||
const handleClickOutside = (event) => {
|
||||
if (menuRef.current && !menuRef.current.contains(event.target)) {
|
||||
setIsMenuOpen(false);
|
||||
}
|
||||
};
|
||||
|
||||
document.addEventListener('mousedown', handleClickOutside);
|
||||
return () => {
|
||||
document.removeEventListener('mousedown', handleClickOutside);
|
||||
};
|
||||
}, []);
|
||||
|
||||
useEffect(() => {
|
||||
const fetchData = async () => {
|
||||
const token = localStorage.getItem('token');
|
||||
@@ -98,7 +116,7 @@ const Dashboard = () => {
|
||||
userMessages,
|
||||
botMessages,
|
||||
});
|
||||
|
||||
|
||||
setLoading(false); // ⬅️ Setelah berhasil, hilangkan loading
|
||||
} catch (error) {
|
||||
console.error('Error:', error);
|
||||
@@ -206,14 +224,33 @@ const Dashboard = () => {
|
||||
<div className={styles.dashboardContainer}>
|
||||
<div className={styles.dashboardHeader}>
|
||||
{isLoggedIn ? (
|
||||
<button onClick={handleLogout} className={styles.logoutButton}>Logout</button>
|
||||
|
||||
<div className={styles.dropdownContainer}>
|
||||
<button
|
||||
onClick={() => setIsMenuOpen(!isMenuOpen)}
|
||||
className={styles.dropdownToggle}
|
||||
ref={menuRef}
|
||||
>
|
||||
☰ Menu
|
||||
</button>
|
||||
|
||||
{isMenuOpen && (
|
||||
<div className={styles.dropdownMenu}>
|
||||
<button onClick={handleLogout} className={styles.dropdownItem}>
|
||||
Ganti Password
|
||||
</button>
|
||||
<button onClick={handleLogout} className={styles.dropdownItem}>
|
||||
Logout
|
||||
</button>
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
) : (
|
||||
<a href="/login" className={styles.loginButton}>Login</a>
|
||||
)}
|
||||
<img src="/dermalounge.jpg" alt="Bot Avatar" />
|
||||
<div>
|
||||
<h1 className={styles.h1}>Dermalounge AI Admin Dashboard</h1>
|
||||
<p>Statistik penggunaan chatbot secara real-time</p>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
|
||||
@@ -43,8 +43,8 @@
|
||||
}
|
||||
|
||||
.dashboardHeader img {
|
||||
width: 50px;
|
||||
height: 50px;
|
||||
width: 75px;
|
||||
height: 75px;
|
||||
border-radius: 50%;
|
||||
}
|
||||
|
||||
@@ -191,4 +191,46 @@ position: absolute;
|
||||
|
||||
.logoutButton:hover {
|
||||
background-color: #cb0f0f;
|
||||
}
|
||||
}
|
||||
.dropdownContainer {
|
||||
position: relative;
|
||||
display: inline-block;
|
||||
|
||||
position: absolute;
|
||||
top: 10px;
|
||||
right: 10px;
|
||||
}
|
||||
|
||||
.dropdownToggle {
|
||||
background-color: #007bff;
|
||||
color: white;
|
||||
padding: 8px 12px;
|
||||
border: none;
|
||||
border-radius: 4px;
|
||||
cursor: pointer;
|
||||
}
|
||||
|
||||
.dropdownMenu {
|
||||
position: absolute;
|
||||
right: 0;
|
||||
top: 100%;
|
||||
background-color: white;
|
||||
border: 1px solid #ccc;
|
||||
min-width: 160px;
|
||||
box-shadow: 0 8px 16px rgba(0,0,0,0.1);
|
||||
z-index: 1;
|
||||
border-radius: 4px;
|
||||
}
|
||||
|
||||
.dropdownItem {
|
||||
padding: 10px 16px;
|
||||
text-align: left;
|
||||
width: 100%;
|
||||
background: none;
|
||||
border: none;
|
||||
cursor: pointer;
|
||||
}
|
||||
|
||||
.dropdownItem:hover {
|
||||
background-color: #f0f0f0;
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user