ok
This commit is contained in:
106
package-lock.json
generated
106
package-lock.json
generated
@@ -12,8 +12,11 @@
|
|||||||
"@testing-library/jest-dom": "^6.6.3",
|
"@testing-library/jest-dom": "^6.6.3",
|
||||||
"@testing-library/react": "^16.3.0",
|
"@testing-library/react": "^16.3.0",
|
||||||
"@testing-library/user-event": "^13.5.0",
|
"@testing-library/user-event": "^13.5.0",
|
||||||
|
"axios": "^1.9.0",
|
||||||
|
"chart.js": "^4.4.9",
|
||||||
"react": "^19.1.0",
|
"react": "^19.1.0",
|
||||||
"react-dom": "^19.1.0",
|
"react-dom": "^19.1.0",
|
||||||
|
"react-router-dom": "^7.6.1",
|
||||||
"react-scripts": "5.0.1",
|
"react-scripts": "5.0.1",
|
||||||
"web-vitals": "^2.1.4"
|
"web-vitals": "^2.1.4"
|
||||||
}
|
}
|
||||||
@@ -2957,6 +2960,12 @@
|
|||||||
"@jridgewell/sourcemap-codec": "^1.4.14"
|
"@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": {
|
"node_modules/@leichtgewicht/ip-codec": {
|
||||||
"version": "2.0.5",
|
"version": "2.0.5",
|
||||||
"resolved": "https://registry.npmjs.org/@leichtgewicht/ip-codec/-/ip-codec-2.0.5.tgz",
|
"resolved": "https://registry.npmjs.org/@leichtgewicht/ip-codec/-/ip-codec-2.0.5.tgz",
|
||||||
@@ -4894,6 +4903,32 @@
|
|||||||
"node": ">=4"
|
"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": {
|
"node_modules/axobject-query": {
|
||||||
"version": "4.1.0",
|
"version": "4.1.0",
|
||||||
"resolved": "https://registry.npmjs.org/axobject-query/-/axobject-query-4.1.0.tgz",
|
"resolved": "https://registry.npmjs.org/axobject-query/-/axobject-query-4.1.0.tgz",
|
||||||
@@ -5528,6 +5563,18 @@
|
|||||||
"node": ">=10"
|
"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": {
|
"node_modules/check-types": {
|
||||||
"version": "11.2.3",
|
"version": "11.2.3",
|
||||||
"resolved": "https://registry.npmjs.org/check-types/-/check-types-11.2.3.tgz",
|
"resolved": "https://registry.npmjs.org/check-types/-/check-types-11.2.3.tgz",
|
||||||
@@ -13626,6 +13673,12 @@
|
|||||||
"node": ">= 0.10"
|
"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": {
|
"node_modules/psl": {
|
||||||
"version": "1.15.0",
|
"version": "1.15.0",
|
||||||
"resolved": "https://registry.npmjs.org/psl/-/psl-1.15.0.tgz",
|
"resolved": "https://registry.npmjs.org/psl/-/psl-1.15.0.tgz",
|
||||||
@@ -13917,6 +13970,53 @@
|
|||||||
"node": ">=0.10.0"
|
"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": {
|
"node_modules/react-scripts": {
|
||||||
"version": "5.0.1",
|
"version": "5.0.1",
|
||||||
"resolved": "https://registry.npmjs.org/react-scripts/-/react-scripts-5.0.1.tgz",
|
"resolved": "https://registry.npmjs.org/react-scripts/-/react-scripts-5.0.1.tgz",
|
||||||
@@ -14823,6 +14923,12 @@
|
|||||||
"node": ">= 0.8.0"
|
"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": {
|
"node_modules/set-function-length": {
|
||||||
"version": "1.2.2",
|
"version": "1.2.2",
|
||||||
"resolved": "https://registry.npmjs.org/set-function-length/-/set-function-length-1.2.2.tgz",
|
"resolved": "https://registry.npmjs.org/set-function-length/-/set-function-length-1.2.2.tgz",
|
||||||
|
|||||||
@@ -7,8 +7,11 @@
|
|||||||
"@testing-library/jest-dom": "^6.6.3",
|
"@testing-library/jest-dom": "^6.6.3",
|
||||||
"@testing-library/react": "^16.3.0",
|
"@testing-library/react": "^16.3.0",
|
||||||
"@testing-library/user-event": "^13.5.0",
|
"@testing-library/user-event": "^13.5.0",
|
||||||
|
"axios": "^1.9.0",
|
||||||
|
"chart.js": "^4.4.9",
|
||||||
"react": "^19.1.0",
|
"react": "^19.1.0",
|
||||||
"react-dom": "^19.1.0",
|
"react-dom": "^19.1.0",
|
||||||
|
"react-router-dom": "^7.6.1",
|
||||||
"react-scripts": "5.0.1",
|
"react-scripts": "5.0.1",
|
||||||
"web-vitals": "^2.1.4"
|
"web-vitals": "^2.1.4"
|
||||||
},
|
},
|
||||||
|
|||||||
@@ -1,5 +1,8 @@
|
|||||||
.App {
|
.App {
|
||||||
text-align: center;
|
height: 100vh;
|
||||||
|
display: flex;
|
||||||
|
align-items: end;
|
||||||
|
justify-content: center;
|
||||||
}
|
}
|
||||||
|
|
||||||
.App-logo {
|
.App-logo {
|
||||||
|
|||||||
60
src/App.js
60
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';
|
import './App.css';
|
||||||
|
|
||||||
function App() {
|
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 (
|
return (
|
||||||
<div className="App">
|
<div className='App'>
|
||||||
<header className="App-header">
|
<BrowserRouter>
|
||||||
<img src={logo} className="App-logo" alt="logo" />
|
<Routes>
|
||||||
<p>
|
<Route path="/" element={<Dashboard />} />
|
||||||
Edit <code>src/App.js</code> and save to reload.
|
<Route path="/dashboard" element={<TenantDashboard />} />
|
||||||
</p>
|
<Route path="/:tenantId" element={<ChatBotWrapper />} />
|
||||||
<a
|
</Routes>
|
||||||
className="App-link"
|
</BrowserRouter>
|
||||||
href="https://reactjs.org"
|
|
||||||
target="_blank"
|
|
||||||
rel="noopener noreferrer"
|
|
||||||
>
|
|
||||||
Learn React
|
|
||||||
</a>
|
|
||||||
</header>
|
|
||||||
</div>
|
</div>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|||||||
152
src/ChatBot.js
Normal file
152
src/ChatBot.js
Normal 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
134
src/ChatBot.module.css
Normal 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
91
src/Dashboard.js
Normal 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}>
|
||||||
|
© 2025 Kloowear AI - Admin Panel
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
};
|
||||||
|
|
||||||
|
export default Dashboard;
|
||||||
64
src/Dashboard.module.css
Normal file
64
src/Dashboard.module.css
Normal 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
91
src/TenantDashboard.js
Normal 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}>
|
||||||
|
© 2025 Kloowear AI - Admin Panel
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
};
|
||||||
|
|
||||||
|
export default TenantDashboard;
|
||||||
Reference in New Issue
Block a user