This commit is contained in:
everythingonblack
2025-05-28 20:29:10 +07:00
parent 9a62c7b4d0
commit 6afcbd8847
9 changed files with 689 additions and 17 deletions

106
package-lock.json generated
View File

@@ -12,8 +12,11 @@
"@testing-library/jest-dom": "^6.6.3",
"@testing-library/react": "^16.3.0",
"@testing-library/user-event": "^13.5.0",
"axios": "^1.9.0",
"chart.js": "^4.4.9",
"react": "^19.1.0",
"react-dom": "^19.1.0",
"react-router-dom": "^7.6.1",
"react-scripts": "5.0.1",
"web-vitals": "^2.1.4"
}
@@ -2957,6 +2960,12 @@
"@jridgewell/sourcemap-codec": "^1.4.14"
}
},
"node_modules/@kurkle/color": {
"version": "0.3.4",
"resolved": "https://registry.npmjs.org/@kurkle/color/-/color-0.3.4.tgz",
"integrity": "sha512-M5UknZPHRu3DEDWoipU6sE8PdkZ6Z/S+v4dD+Ke8IaNlpdSQah50lz1KtcFBa2vsdOnwbbnxJwVM4wty6udA5w==",
"license": "MIT"
},
"node_modules/@leichtgewicht/ip-codec": {
"version": "2.0.5",
"resolved": "https://registry.npmjs.org/@leichtgewicht/ip-codec/-/ip-codec-2.0.5.tgz",
@@ -4894,6 +4903,32 @@
"node": ">=4"
}
},
"node_modules/axios": {
"version": "1.9.0",
"resolved": "https://registry.npmjs.org/axios/-/axios-1.9.0.tgz",
"integrity": "sha512-re4CqKTJaURpzbLHtIi6XpDv20/CnpXOtjRY5/CU32L8gU8ek9UIivcfvSWvmKEngmVbrUtPpdDwWDWL7DNHvg==",
"license": "MIT",
"dependencies": {
"follow-redirects": "^1.15.6",
"form-data": "^4.0.0",
"proxy-from-env": "^1.1.0"
}
},
"node_modules/axios/node_modules/form-data": {
"version": "4.0.2",
"resolved": "https://registry.npmjs.org/form-data/-/form-data-4.0.2.tgz",
"integrity": "sha512-hGfm/slu0ZabnNt4oaRZ6uREyfCj6P4fT/n6A1rGV+Z0VdGXjfOhVUpkn6qVQONHGIFwmveGXyDs75+nr6FM8w==",
"license": "MIT",
"dependencies": {
"asynckit": "^0.4.0",
"combined-stream": "^1.0.8",
"es-set-tostringtag": "^2.1.0",
"mime-types": "^2.1.12"
},
"engines": {
"node": ">= 6"
}
},
"node_modules/axobject-query": {
"version": "4.1.0",
"resolved": "https://registry.npmjs.org/axobject-query/-/axobject-query-4.1.0.tgz",
@@ -5528,6 +5563,18 @@
"node": ">=10"
}
},
"node_modules/chart.js": {
"version": "4.4.9",
"resolved": "https://registry.npmjs.org/chart.js/-/chart.js-4.4.9.tgz",
"integrity": "sha512-EyZ9wWKgpAU0fLJ43YAEIF8sr5F2W3LqbS40ZJyHIner2lY14ufqv2VMp69MAiZ2rpwxEUxEhIH/0U3xyRynxg==",
"license": "MIT",
"dependencies": {
"@kurkle/color": "^0.3.0"
},
"engines": {
"pnpm": ">=8"
}
},
"node_modules/check-types": {
"version": "11.2.3",
"resolved": "https://registry.npmjs.org/check-types/-/check-types-11.2.3.tgz",
@@ -13626,6 +13673,12 @@
"node": ">= 0.10"
}
},
"node_modules/proxy-from-env": {
"version": "1.1.0",
"resolved": "https://registry.npmjs.org/proxy-from-env/-/proxy-from-env-1.1.0.tgz",
"integrity": "sha512-D+zkORCbA9f1tdWRK0RaCR3GPv50cMxcrz4X8k5LTSUD1Dkw47mKJEZQNunItRTkWwgtaUSo1RVFRIG9ZXiFYg==",
"license": "MIT"
},
"node_modules/psl": {
"version": "1.15.0",
"resolved": "https://registry.npmjs.org/psl/-/psl-1.15.0.tgz",
@@ -13917,6 +13970,53 @@
"node": ">=0.10.0"
}
},
"node_modules/react-router": {
"version": "7.6.1",
"resolved": "https://registry.npmjs.org/react-router/-/react-router-7.6.1.tgz",
"integrity": "sha512-hPJXXxHJZEsPFNVbtATH7+MMX43UDeOauz+EAU4cgqTn7ojdI9qQORqS8Z0qmDlL1TclO/6jLRYUEtbWidtdHQ==",
"license": "MIT",
"dependencies": {
"cookie": "^1.0.1",
"set-cookie-parser": "^2.6.0"
},
"engines": {
"node": ">=20.0.0"
},
"peerDependencies": {
"react": ">=18",
"react-dom": ">=18"
},
"peerDependenciesMeta": {
"react-dom": {
"optional": true
}
}
},
"node_modules/react-router-dom": {
"version": "7.6.1",
"resolved": "https://registry.npmjs.org/react-router-dom/-/react-router-dom-7.6.1.tgz",
"integrity": "sha512-vxU7ei//UfPYQ3iZvHuO1D/5fX3/JOqhNTbRR+WjSBWxf9bIvpWK+ftjmdfJHzPOuMQKe2fiEdG+dZX6E8uUpA==",
"license": "MIT",
"dependencies": {
"react-router": "7.6.1"
},
"engines": {
"node": ">=20.0.0"
},
"peerDependencies": {
"react": ">=18",
"react-dom": ">=18"
}
},
"node_modules/react-router/node_modules/cookie": {
"version": "1.0.2",
"resolved": "https://registry.npmjs.org/cookie/-/cookie-1.0.2.tgz",
"integrity": "sha512-9Kr/j4O16ISv8zBBhJoi4bXOYNTkFLOqSL3UDB0njXxCXNezjeyVrJyGOWtgfs/q2km1gwBcfH8q1yEGoMYunA==",
"license": "MIT",
"engines": {
"node": ">=18"
}
},
"node_modules/react-scripts": {
"version": "5.0.1",
"resolved": "https://registry.npmjs.org/react-scripts/-/react-scripts-5.0.1.tgz",
@@ -14823,6 +14923,12 @@
"node": ">= 0.8.0"
}
},
"node_modules/set-cookie-parser": {
"version": "2.7.1",
"resolved": "https://registry.npmjs.org/set-cookie-parser/-/set-cookie-parser-2.7.1.tgz",
"integrity": "sha512-IOc8uWeOZgnb3ptbCURJWNjWUPcO3ZnTTdzsurqERrP6nPyv+paC55vJM0LpOlT2ne+Ix+9+CRG1MNLlyZ4GjQ==",
"license": "MIT"
},
"node_modules/set-function-length": {
"version": "1.2.2",
"resolved": "https://registry.npmjs.org/set-function-length/-/set-function-length-1.2.2.tgz",

View File

@@ -7,8 +7,11 @@
"@testing-library/jest-dom": "^6.6.3",
"@testing-library/react": "^16.3.0",
"@testing-library/user-event": "^13.5.0",
"axios": "^1.9.0",
"chart.js": "^4.4.9",
"react": "^19.1.0",
"react-dom": "^19.1.0",
"react-router-dom": "^7.6.1",
"react-scripts": "5.0.1",
"web-vitals": "^2.1.4"
},

View File

@@ -1,5 +1,8 @@
.App {
text-align: center;
height: 100vh;
display: flex;
align-items: end;
justify-content: center;
}
.App-logo {

View File

@@ -1,23 +1,51 @@
import logo from './logo.svg';
import React, { useState, useEffect } from 'react';
import { BrowserRouter, Routes, Route, useParams } from 'react-router-dom';
import axios from 'axios';
import Dashboard from './Dashboard';
import TenantDashboard from './TenantDashboard';
import ChatBot from './ChatBot';
import './App.css';
function App() {
function ChatBotWrapper() {
const { agentId } = useParams();
const [agentDetails, setAgentDetails] = useState(null);
const [loading, setLoading] = useState(true);
useEffect(() => {
const fetchAgent = async () => {
try {
// const response = await axios.get(
// `https://n8n.kediritechnopark.my.id/webhook/get-agent?id=${agentId}`
// );
// if (response.data && response.data.length > 0) {
// setAgentDetails(response.data[0].data);
// }
} catch (error) {
console.error("Error fetching certificate:", error);
} finally {
setLoading(false);
}
};
fetchAgent();
}, [agentId]);
// if (loading) return <div>Loading...</div>;
// if (!agentDetails) return <div>No agent found</div>;
return <ChatBot agentId={agentId} />;
}
return (
<div className="App">
<header className="App-header">
<img src={logo} className="App-logo" alt="logo" />
<p>
Edit <code>src/App.js</code> and save to reload.
</p>
<a
className="App-link"
href="https://reactjs.org"
target="_blank"
rel="noopener noreferrer"
>
Learn React
</a>
</header>
<div className='App'>
<BrowserRouter>
<Routes>
<Route path="/" element={<Dashboard />} />
<Route path="/dashboard" element={<TenantDashboard />} />
<Route path="/:tenantId" element={<ChatBotWrapper />} />
</Routes>
</BrowserRouter>
</div>
);
}

152
src/ChatBot.js Normal file
View File

@@ -0,0 +1,152 @@
import React, { useState } from 'react';
import styles from './ChatBot.module.css';
const ChatBot = () => {
const [messages, setMessages] = useState([
{
sender: 'bot',
text: 'Halo 👋 Saya Kloowear AI! Ada yang bisa saya bantu?',
time: getTime(),
quickReplies: [
'Saya ingin beli gelang custom',
'Ada katalog produk terbaru?',
'Gelang cocok untuk hadiah?',
],
},
]);
const [input, setInput] = useState('');
const [isLoading, setIsLoading] = useState(false);
const sendMessage = async (textOverride = null) => {
const message = textOverride || input.trim();
if (message === '') return;
// Show user's message immediately
const newMessages = [
{ sender: 'user', text: message, time: getTime() },
...messages,
];
setMessages(newMessages);
setInput('');
setIsLoading(true);
try {
// Send to backend
const response = await fetch('https://n8n.kediritechnopark.my.id/webhook/master-agent/ask' , {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify({ pertanyaan: newMessages }),
});
if (!response.ok) throw new Error('Network response was not ok');
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 || 'Maaf, saya tidak mengerti.';
// Add bot's reply
setMessages(prev => [
{ sender: 'bot', text: botAnswer, time: getTime() },
...prev,
]);
} catch (error) {
setMessages(prev => [
{
sender: 'bot',
text: 'Maaf, terjadi kesalahan pada server. Silakan coba lagi nanti.',
time: getTime(),
},
...prev,
]);
console.error('Fetch error:', error);
} finally {
setIsLoading(false);
}
};
return (
<div className={styles.chatContainer}>
<div className={styles.chatHeader}>
<img src="https://i.ibb.co/YXxXr72/bot-avatar.png" alt="Bot Avatar" />
<strong>Kloowear AI Assistant</strong>
</div>
<div className={styles.chatBody}>
{isLoading && (
<div className={`${styles.messageRow} ${styles.bot}`}>
<img
src="https://i.ibb.co/YXxXr72/bot-avatar.png"
alt="Bot"
className={styles.avatar}
/>
<div className={`${styles.message} ${styles.bot}`}>
<em>Mengetik...</em>
</div>
</div>
)}
{messages.map((msg, index) => (
<div
key={index}
className={`${styles.messageRow} ${styles[msg.sender]}`}
>
{msg.sender === 'bot' && (
<img
src="https://i.ibb.co/YXxXr72/bot-avatar.png"
alt="Bot"
className={styles.avatar}
/>
)}
<div className={`${styles.message} ${styles[msg.sender]}`}>
{msg.text}
<div className={styles.timestamp}>{msg.time}</div>
{msg.quickReplies && (
<div className={styles.quickReplies}>
{msg.quickReplies.map((reply, i) => (
<div
key={i}
className={styles.quickReply}
onClick={() => sendMessage(reply)}
>
{reply}
</div>
))}
</div>
)}
</div>
{msg.sender === 'user' && (
<img
src="https://i.ibb.co/4pDNDk1/user-avatar.png"
alt="User"
className={styles.avatar}
/>
)}
</div>
))}
</div>
<div className={styles.chatInput}>
<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>
);
};
function getTime() {
const now = new Date();
return now.toLocaleTimeString([], { hour: '2-digit', minute: '2-digit' });
}
export default ChatBot;

134
src/ChatBot.module.css Normal file
View File

@@ -0,0 +1,134 @@
.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: 71vh;
overflow: hidden;
}
.chatHeader {
background: #075e54;
color: #fff;
padding: 15px;
display: flex;
align-items: center;
gap: 10px;
border-top-left-radius: 10px;
border-top-right-radius: 10px;
}
.chatHeader img {
width: 45px;
height: 45px;
border-radius: 50%;
}
.chatBody {
flex: 1;
overflow-y: auto;
padding: 15px;
background: #ece5dd;
display: flex;
flex-direction: column-reverse;
}
.messageRow {
display: flex;
margin-bottom: 12px;
align-items: flex-end;
}
.bot {
justify-content: flex-start;
}
.user {
justify-content: flex-end;
}
.message {
max-width: 70%;
padding: 12px 16px;
border-radius: 15px;
font-size: 14px;
position: relative;
line-height: 1.4;
}
.message.bot {
background: #ffffff;
border: 1px solid #ccc;
}
.message.user {
background: #dcf8c6;
}
.timestamp {
font-size: 10px;
color: #555;
margin-top: 4px;
text-align: right;
}
.avatar {
width: 36px;
height: 36px;
border-radius: 50%;
margin: 0 8px;
}
.chatInput {
display: flex;
padding: 10px;
border-top: 1px solid #ddd;
background: #f0f0f0;
}
.chatInput input {
flex: 1;
padding: 10px;
border-radius: 20px;
border: 1px solid #ccc;
font-size: 14px;
}
.chatInput button {
background: #075e54;
color: #fff;
border: none;
border-radius: 20px;
padding: 0 20px;
margin-left: 10px;
cursor: pointer;
}
.chatInput button:hover {
background: #0b7366;
}
.quickReplies {
display: flex;
flex-wrap: wrap;
gap: 8px;
margin: 10px 0 0;
}
.quickReply {
background: #fff;
border: 1px solid #ccc;
padding: 8px 14px;
border-radius: 20px;
font-size: 13px;
cursor: pointer;
transition: all 0.2s ease;
}
.quickReply:hover {
background: #075e54;
color: white;
border-color: #075e54;
}

91
src/Dashboard.js Normal file
View File

@@ -0,0 +1,91 @@
import React, { useEffect, useRef } from 'react';
import styles from './Dashboard.module.css';
import { Chart, LineElement, PointElement, LineController, CategoryScale, LinearScale, Title, Tooltip, Legend } from 'chart.js';
Chart.register(LineElement, PointElement, LineController, CategoryScale, LinearScale, Title, Tooltip, Legend);
const Dashboard = () => {
const chartRef = useRef(null);
const chartInstanceRef = useRef(null);
useEffect(() => {
const stats = {
totalChats: 120,
userMessages: 210,
botMessages: 220,
activeNow: 5,
};
// Inject stats manually
document.getElementById("totalChats").textContent = stats.totalChats;
document.getElementById("userMessages").textContent = stats.userMessages;
document.getElementById("botMessages").textContent = stats.botMessages;
document.getElementById("activeNow").textContent = stats.activeNow;
// Get canvas from ref
const ctx = chartRef.current.getContext("2d");
if (chartInstanceRef.current) {
chartInstanceRef.current.destroy();
}
chartInstanceRef.current = new Chart(ctx, {
type: 'line',
data: {
labels: ["08:00", "10:00", "12:00", "14:00", "16:00", "18:00"],
datasets: [{
label: "Pesan Masuk",
data: [10, 25, 45, 30, 50, 60],
borderColor: "#075e54",
backgroundColor: "rgba(7, 94, 84, 0.2)",
fill: true,
tension: 0.3,
}]
},
options: {
responsive: true,
plugins: {
legend: {
display: true,
position: 'bottom'
}
},
scales: {
y: {
beginAtZero: true
}
}
}
});
}, []);
return (
<div className={styles.dashboardContainer}>
<div className={styles.dashboardHeader}>
<img src="https://i.ibb.co/YXxXr72/bot-avatar.png" alt="Bot Avatar" />
<div>
<h1>Kloowear AI Admin Dashboard</h1>
<p>Statistik penggunaan chatbot secara real-time</p>
</div>
</div>
<div className={styles.statsGrid}>
<div className={styles.statCard}><h2 id="totalChats">0</h2><p>Total Percakapan Hari Ini</p></div>
<div className={styles.statCard}><h2 id="userMessages">0</h2><p>Pesan dari Pengguna</p></div>
<div className={styles.statCard}><h2 id="botMessages">0</h2><p>Respons Bot</p></div>
<div className={styles.statCard}><h2 id="activeNow">0</h2><p>Pengguna Aktif Sekarang</p></div>
</div>
<div className={styles.chartSection}>
<h2 className={styles.chartTitle}>Grafik Interaksi (Simulasi)</h2>
<canvas ref={chartRef}></canvas>
</div>
<div className={styles.footer}>
&copy; 2025 Kloowear AI - Admin Panel
</div>
</div>
);
};
export default Dashboard;

64
src/Dashboard.module.css Normal file
View File

@@ -0,0 +1,64 @@
.dashboardContainer {
max-width: 900px;
margin: 30px auto;
background: #fff;
border-radius: 10px;
box-shadow: 0 5px 15px rgba(0, 0, 0, 0.2);
padding: 20px;
}
.dashboardHeader {
display: flex;
align-items: center;
gap: 15px;
background: #075e54;
color: white;
padding: 20px;
border-radius: 10px 10px 0 0;
}
.dashboardHeader img {
width: 50px;
height: 50px;
border-radius: 50%;
}
.statsGrid {
display: grid;
grid-template-columns: repeat(auto-fit, minmax(200px, 1fr));
gap: 20px;
margin-top: 20px;
}
.statCard {
background: #ece5dd;
border-radius: 10px;
padding: 20px;
box-shadow: inset 0 0 5px rgba(0, 0, 0, 0.05);
}
.statCard h2 {
margin: 0;
font-size: 28px;
color: #075e54;
}
.statCard p {
margin: 5px 0 0;
font-size: 14px;
}
.chartSection {
margin-top: 30px;
}
.chartTitle {
color: #075e54;
}
.footer {
text-align: center;
margin-top: 30px;
font-size: 13px;
color: #777;
}

91
src/TenantDashboard.js Normal file
View File

@@ -0,0 +1,91 @@
import React, { useEffect, useRef } from 'react';
import styles from './Dashboard.module.css';
import { Chart, LineElement, PointElement, LineController, CategoryScale, LinearScale, Title, Tooltip, Legend } from 'chart.js';
Chart.register(LineElement, PointElement, LineController, CategoryScale, LinearScale, Title, Tooltip, Legend);
const TenantDashboard = () => {
const chartRef = useRef(null);
const chartInstanceRef = useRef(null);
useEffect(() => {
const stats = {
totalChats: 120,
userMessages: 210,
botMessages: 220,
activeNow: 5,
};
// Inject stats manually
document.getElementById("totalChats").textContent = stats.totalChats;
document.getElementById("userMessages").textContent = stats.userMessages;
document.getElementById("botMessages").textContent = stats.botMessages;
document.getElementById("activeNow").textContent = stats.activeNow;
// Get canvas from ref
const ctx = chartRef.current.getContext("2d");
if (chartInstanceRef.current) {
chartInstanceRef.current.destroy();
}
chartInstanceRef.current = new Chart(ctx, {
type: 'line',
data: {
labels: ["08:00", "10:00", "12:00", "14:00", "16:00", "18:00"],
datasets: [{
label: "Pesan Masuk",
data: [10, 25, 45, 30, 50, 60],
borderColor: "#075e54",
backgroundColor: "rgba(7, 94, 84, 0.2)",
fill: true,
tension: 0.3,
}]
},
options: {
responsive: true,
plugins: {
legend: {
display: true,
position: 'bottom'
}
},
scales: {
y: {
beginAtZero: true
}
}
}
});
}, []);
return (
<div className={styles.dashboardContainer}>
<div className={styles.dashboardHeader}>
<img src="https://i.ibb.co/YXxXr72/bot-avatar.png" alt="Bot Avatar" />
<div>
<h1>Kloowear AI Admin Dashboard</h1>
<p>Statistik penggunaan chatbot secara real-time</p>
</div>
</div>
<div className={styles.statsGrid}>
<div className={styles.statCard}><h2 id="totalChats">0</h2><p>Total Percakapan Hari Ini</p></div>
<div className={styles.statCard}><h2 id="userMessages">0</h2><p>Pesan dari Pengguna</p></div>
<div className={styles.statCard}><h2 id="botMessages">0</h2><p>Respons Bot</p></div>
<div className={styles.statCard}><h2 id="activeNow">0</h2><p>Pengguna Aktif Sekarang</p></div>
</div>
<div className={styles.chartSection}>
<h2 className={styles.chartTitle}>Grafik Interaksi (Simulasi)</h2>
<canvas ref={chartRef}></canvas>
</div>
<div className={styles.footer}>
&copy; 2025 Kloowear AI - Admin Panel
</div>
</div>
);
};
export default TenantDashboard;