diff --git a/package-lock.json b/package-lock.json
index 534e028..33602d0 100644
--- a/package-lock.json
+++ b/package-lock.json
@@ -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",
diff --git a/package.json b/package.json
index 2f82861..38d2d30 100644
--- a/package.json
+++ b/package.json
@@ -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"
},
diff --git a/src/App.css b/src/App.css
index 74b5e05..66ad3fb 100644
--- a/src/App.css
+++ b/src/App.css
@@ -1,5 +1,8 @@
.App {
- text-align: center;
+ height: 100vh;
+ display: flex;
+ align-items: end;
+ justify-content: center;
}
.App-logo {
diff --git a/src/App.js b/src/App.js
index 3784575..95c95a7 100644
--- a/src/App.js
+++ b/src/App.js
@@ -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
Loading...
;
+ // if (!agentDetails) return No agent found
;
+
+ return ;
+}
return (
-
-
+
+
+
+ } />
+ } />
+ } />
+
+
);
}
diff --git a/src/ChatBot.js b/src/ChatBot.js
new file mode 100644
index 0000000..f70f64e
--- /dev/null
+++ b/src/ChatBot.js
@@ -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 (
+
+
+

+
Kloowear AI Assistant
+
+
+
+
+ {isLoading && (
+
+

+
+ Mengetik...
+
+
+ )}
+ {messages.map((msg, index) => (
+
+ {msg.sender === 'bot' && (
+

+ )}
+
+ {msg.text}
+
{msg.time}
+ {msg.quickReplies && (
+
+ {msg.quickReplies.map((reply, i) => (
+
sendMessage(reply)}
+ >
+ {reply}
+
+ ))}
+
+ )}
+
+ {msg.sender === 'user' && (
+

+ )}
+
+ ))}
+
+
+
+ setInput(e.target.value)}
+ onKeyDown={(e) => e.key === 'Enter' && sendMessage()}
+ disabled={isLoading}
+ />
+
+
+
+ );
+};
+
+function getTime() {
+ const now = new Date();
+ return now.toLocaleTimeString([], { hour: '2-digit', minute: '2-digit' });
+}
+
+export default ChatBot;
diff --git a/src/ChatBot.module.css b/src/ChatBot.module.css
new file mode 100644
index 0000000..8409aaf
--- /dev/null
+++ b/src/ChatBot.module.css
@@ -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;
+}
diff --git a/src/Dashboard.js b/src/Dashboard.js
new file mode 100644
index 0000000..48aadd7
--- /dev/null
+++ b/src/Dashboard.js
@@ -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 (
+
+
+

+
+
Kloowear AI Admin Dashboard
+
Statistik penggunaan chatbot secara real-time
+
+
+
+
+
0
Total Percakapan Hari Ini
+
+
+
+
+
+
+
Grafik Interaksi (Simulasi)
+
+
+
+
+ © 2025 Kloowear AI - Admin Panel
+
+
+ );
+};
+
+export default Dashboard;
diff --git a/src/Dashboard.module.css b/src/Dashboard.module.css
new file mode 100644
index 0000000..92f45c7
--- /dev/null
+++ b/src/Dashboard.module.css
@@ -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;
+}
diff --git a/src/TenantDashboard.js b/src/TenantDashboard.js
new file mode 100644
index 0000000..45f31d6
--- /dev/null
+++ b/src/TenantDashboard.js
@@ -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 (
+
+
+

+
+
Kloowear AI Admin Dashboard
+
Statistik penggunaan chatbot secara real-time
+
+
+
+
+
0
Total Percakapan Hari Ini
+
+
+
+
+
+
+
Grafik Interaksi (Simulasi)
+
+
+
+
+ © 2025 Kloowear AI - Admin Panel
+
+
+ );
+};
+
+export default TenantDashboard;