ok
This commit is contained in:
@@ -5,6 +5,7 @@ html, body, #root {
|
||||
|
||||
.App {
|
||||
height: 100%;
|
||||
overflow: auto;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
background: #fff;
|
||||
|
||||
@@ -1,19 +1,20 @@
|
||||
import React, { useState, useEffect } from 'react';
|
||||
import styles from './ChatBot.module.css';
|
||||
|
||||
const ChatBot = ({ existingConversation, readOnly, hh }) => {
|
||||
const ChatBot = ({ existingConversation }) => {
|
||||
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?',
|
||||
text: 'Hai! Saya Alle, asisten virtual Dermalounge Clinic. Ada yang bisa Alle bantu hari ini?',
|
||||
time: getTime(),
|
||||
quickReplies: [
|
||||
'Konsultasi estetik',
|
||||
'Konsultasi kulit dan kelamin'
|
||||
'Konsultasi Estetik',
|
||||
'Konsultasi Kulit & Kelamin'
|
||||
],
|
||||
},
|
||||
]);
|
||||
|
||||
|
||||
const [input, setInput] = useState('');
|
||||
const [isLoading, setIsLoading] = useState(false);
|
||||
|
||||
@@ -231,8 +232,7 @@ function formatBoldText(text) {
|
||||
</div>
|
||||
|
||||
<div
|
||||
className={styles.quickReply}
|
||||
style={{ color: name.length > 2 && phoneNumber.length >= 10 ? 'black' : '#ccc' }}
|
||||
className={styles.nextButton}
|
||||
onClick={() => {
|
||||
if (name.length > 2 && phoneNumber.length >= 10) {
|
||||
const sessionData = JSON.parse(localStorage.getItem('session')) || {};
|
||||
|
||||
@@ -152,16 +152,30 @@
|
||||
transition: all 0.2s ease;
|
||||
}
|
||||
|
||||
.quickReply:hover {
|
||||
/* .quickReply:hover {
|
||||
background: #075e54;
|
||||
color: white;
|
||||
border-color: #075e54;
|
||||
}
|
||||
} */
|
||||
|
||||
.quickReply:hover::placeholder {
|
||||
/* .quickReply:hover::placeholder {
|
||||
color: white;
|
||||
} */
|
||||
|
||||
.nextButton {
|
||||
background: #075e54;
|
||||
border: 1px solid #ccc;
|
||||
padding: 8px 14px;
|
||||
border-radius: 20px;
|
||||
font-size: 13px;
|
||||
cursor: pointer;
|
||||
transition: all 0.2s ease;
|
||||
color: white;
|
||||
}
|
||||
|
||||
.nextButton:hover::placeholder {
|
||||
color: #ccc;
|
||||
}
|
||||
.quickReply2 {
|
||||
width: 100%;
|
||||
background: #fff;
|
||||
@@ -174,13 +188,13 @@
|
||||
transition: all 0.2s ease;
|
||||
}
|
||||
|
||||
.quickReply2:hover {
|
||||
/* .quickReply2:hover {
|
||||
background: #075e54;
|
||||
color: white;
|
||||
border-color: #075e54;
|
||||
border: 1px solid #075e54;
|
||||
}
|
||||
} */
|
||||
|
||||
.quickReply2:hover::placeholder {
|
||||
/* .quickReply2:hover::placeholder {
|
||||
color: white;
|
||||
}
|
||||
} */
|
||||
242
src/Dashboard.js
242
src/Dashboard.js
@@ -22,6 +22,9 @@ const Dashboard = () => {
|
||||
const [modalContent, setModalContent] = useState(null);
|
||||
const [rawData, setRawData] = useState([]);
|
||||
const [loading, setLoading] = useState(true); // ⬅️ Tambahkan state loading
|
||||
const [fileList, setFileList] = useState([]);
|
||||
const [selectedKeys, setSelectedKeys] = useState([]);
|
||||
|
||||
|
||||
const [stats, setStats] = useState({
|
||||
totalChats: 0,
|
||||
@@ -29,17 +32,21 @@ const Dashboard = () => {
|
||||
});
|
||||
|
||||
const [isDragging, setIsDragging] = useState(false);
|
||||
const [selectedFile, setSelectedFile] = useState(null);
|
||||
const [selectedFiles, setSelectedFiles] = useState([]);
|
||||
|
||||
const navigate = useNavigate();
|
||||
|
||||
const handleFile = (file) => {
|
||||
if (file) {
|
||||
setSelectedFile(file);
|
||||
}
|
||||
const handleFiles = (files) => {
|
||||
const newFiles = files.filter(file => {
|
||||
// Hindari duplikat berdasarkan nama (atau bisa pakai hash/md5 jika perlu)
|
||||
return !selectedFiles.some(f => f.name === file.name);
|
||||
});
|
||||
|
||||
setSelectedFiles((prev) => [...prev, ...newFiles]);
|
||||
};
|
||||
|
||||
|
||||
|
||||
const handleLogout = () => {
|
||||
localStorage.removeItem('token');
|
||||
localStorage.removeItem('user');
|
||||
@@ -90,16 +97,17 @@ const Dashboard = () => {
|
||||
return;
|
||||
}
|
||||
|
||||
if (!response.ok) {
|
||||
throw new Error('Fetch gagal dengan status: ' + response.status);
|
||||
}
|
||||
// if (!response.ok) {
|
||||
// throw new Error('Fetch gagal dengan status: ' + response.status);
|
||||
// }
|
||||
|
||||
const data = await response.json();
|
||||
console.log(data);
|
||||
setDiscussedTopics(data?.result?.topics)
|
||||
setFollowUps(data?.result?.interested_users)
|
||||
|
||||
const graphObj = data.result.graph;
|
||||
setDiscussedTopics(data?.graph[0]?.json?.result?.topics)
|
||||
setFollowUps(data?.graph[0]?.json?.result?.interested_users)
|
||||
setFileList(data?.files)
|
||||
const graphObj = data?.graph[0]?.json?.result?.graph;
|
||||
console.log(graphObj)
|
||||
const rawDataArray = Object.entries(graphObj).map(([hour, sesi]) => ({
|
||||
hour,
|
||||
sesi,
|
||||
@@ -298,6 +306,109 @@ const subscribeUser = async () => {
|
||||
});
|
||||
}, [rawData]);
|
||||
|
||||
const handleDeleteFile = async (key) => {
|
||||
if (!window.confirm(`Yakin ingin menghapus "${key}"?`)) return;
|
||||
const token = localStorage.getItem('token');
|
||||
|
||||
try {
|
||||
await fetch("https://bot.kediritechnopark.com/webhook/files/delete", {
|
||||
method: "POST",
|
||||
headers: {
|
||||
"Content-Type": "application/json",
|
||||
"Authorization": `Bearer ${token}`,
|
||||
},
|
||||
body: JSON.stringify({ key }),
|
||||
});
|
||||
|
||||
// fetchFiles(); // Refresh list
|
||||
} catch (error) {
|
||||
console.error("Gagal menghapus file:", error);
|
||||
}
|
||||
};
|
||||
|
||||
const handleBatchDownload = async () => {
|
||||
const token = localStorage.getItem('token');
|
||||
for (const key of selectedKeys) {
|
||||
try {
|
||||
const response = await fetch(
|
||||
`https://bot.kediritechnopark.com/webhook/files/download?key=${encodeURIComponent(key)}`,
|
||||
{
|
||||
method: 'GET',
|
||||
headers: {
|
||||
'Authorization': `Bearer ${token}`
|
||||
}
|
||||
}
|
||||
);
|
||||
|
||||
if (!response.ok) throw new Error('Gagal download');
|
||||
|
||||
const blob = await response.blob();
|
||||
|
||||
// Coba ambil nama file dari header Content-Disposition (jika tersedia)
|
||||
let filename = key;
|
||||
const disposition = response.headers.get('Content-Disposition');
|
||||
if (disposition && disposition.includes('filename=')) {
|
||||
const match = disposition.match(/filename="?(.+?)"?$/);
|
||||
if (match) filename = match[1];
|
||||
}
|
||||
|
||||
// Buat URL dan download
|
||||
const url = window.URL.createObjectURL(blob);
|
||||
const a = document.createElement('a');
|
||||
a.href = url;
|
||||
a.download = filename;
|
||||
a.click();
|
||||
window.URL.revokeObjectURL(url);
|
||||
} catch (err) {
|
||||
console.error(`Gagal download ${key}:`, err);
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
const handleBatchUpload = async () => {
|
||||
const token = localStorage.getItem('token');
|
||||
|
||||
for (const file of selectedFiles) {
|
||||
const formData = new FormData();
|
||||
formData.append('file', file); // Kirim satu per satu file
|
||||
|
||||
const response = await fetch('https://bot.kediritechnopark.com/webhook/files/upload', {
|
||||
method: 'POST',
|
||||
headers: {
|
||||
'Authorization': `Bearer ${token}`
|
||||
// ❗Jangan set 'Content-Type' untuk FormData, biarkan browser mengaturnya
|
||||
},
|
||||
body: formData
|
||||
});
|
||||
|
||||
if (!response.ok) {
|
||||
console.error(`Upload gagal untuk file ${file.name}`);
|
||||
}
|
||||
}
|
||||
|
||||
alert('Upload selesai');
|
||||
setSelectedFiles([]); // Kosongkan setelah selesai upload
|
||||
};
|
||||
|
||||
const handleBatchDelete = async () => {
|
||||
if (!window.confirm(`Yakin ingin menghapus ${selectedKeys.length} file?`)) return;
|
||||
const token = localStorage.getItem('token');
|
||||
|
||||
for (const key of selectedKeys) {
|
||||
const response = await fetch(
|
||||
`https://bot.kediritechnopark.com/webhook/files/delete?key=${encodeURIComponent(key)}`,
|
||||
{
|
||||
method: 'DELETE',
|
||||
headers: {
|
||||
'Authorization': `Bearer ${token}`
|
||||
}
|
||||
}
|
||||
);
|
||||
}
|
||||
|
||||
setSelectedKeys([]);
|
||||
};
|
||||
|
||||
// ⬇️ Jika masih loading, tampilkan full white screen
|
||||
if (loading) {
|
||||
return <div style={{ backgroundColor: 'white', width: '100vw', height: '100vh' }} />;
|
||||
@@ -354,53 +465,126 @@ const subscribeUser = async () => {
|
||||
<h2 className={styles.chartTitle}>Interactions</h2>
|
||||
<canvas ref={chartRef}></canvas>
|
||||
</div>
|
||||
|
||||
<div className={styles.chartSection}>
|
||||
<h2 className={styles.chartTitle}>Update data</h2>
|
||||
|
||||
{/* ✅ TOMBOL AKSI */}
|
||||
{selectedKeys.length > 0 && (
|
||||
<div className={styles.actionBar}>
|
||||
<button onClick={handleBatchDownload}>⬇️ Download</button>
|
||||
<button onClick={handleBatchDelete}>🗑️ Hapus</button>
|
||||
</div>
|
||||
)}
|
||||
|
||||
{/* ✅ AREA UPLOAD */}
|
||||
<div
|
||||
className={`${styles.uploadContainer} ${isDragging ? styles.dragActive : ""}`}
|
||||
onClick={() => selectedFile ? null : document.getElementById("fileInput").click()}
|
||||
onDragOver={(e) => {
|
||||
e.preventDefault();
|
||||
setIsDragging(true);
|
||||
}}
|
||||
onDragLeave={() => setIsDragging(false)}
|
||||
|
||||
onDrop={(e) => {
|
||||
e.preventDefault();
|
||||
setIsDragging(false);
|
||||
const file = e.dataTransfer.files[0];
|
||||
handleFile(file);
|
||||
const files = Array.from(e.dataTransfer.files);
|
||||
handleFiles(files);
|
||||
}}
|
||||
>
|
||||
<p className={styles.desktopText}>
|
||||
Drop file here, or <span className={styles.uploadLink}>Click to upload</span>
|
||||
</p>
|
||||
<p className={styles.mobileText}>Click to upload</p>
|
||||
|
||||
{selectedFile && (
|
||||
<>
|
||||
<div className={styles.fileInfo}>
|
||||
<strong>{selectedFile.name}</strong>
|
||||
{/* ✅ TABEL FILE */}
|
||||
<table className={styles.fileTable}>
|
||||
<thead>
|
||||
<tr>
|
||||
<th>
|
||||
<input
|
||||
type="checkbox"
|
||||
checked={fileList.length > 0 && selectedKeys.length === fileList.length}
|
||||
onChange={(e) => {
|
||||
if (e.target.checked) {
|
||||
setSelectedKeys(fileList.map((f) => f.json.Key));
|
||||
} else {
|
||||
setSelectedKeys([]);
|
||||
}
|
||||
}}
|
||||
/>
|
||||
</th>
|
||||
<th>select all</th>
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody>
|
||||
{fileList.map((file, index) => (
|
||||
<tr key={index}>
|
||||
<td>
|
||||
<input
|
||||
type="checkbox"
|
||||
checked={selectedKeys.includes(file.json.Key)}
|
||||
onChange={() => {
|
||||
setSelectedKeys((prev) =>
|
||||
prev.includes(file.json.Key)
|
||||
? prev.filter((key) => key !== file.json.Key)
|
||||
: [...prev, file.json.Key]
|
||||
);
|
||||
}}
|
||||
/>
|
||||
</td>
|
||||
<td>{file.json.Key}</td>
|
||||
</tr>
|
||||
))}
|
||||
</tbody>
|
||||
</table>
|
||||
|
||||
<p className={styles.desktopText}>
|
||||
Drop file here, or <span onClick={() => document.getElementById("fileInput").click()} className={styles.uploadLink}>Click to upload</span>
|
||||
</p>
|
||||
<p className={styles.mobileText} onClick={() => document.getElementById("fileInput").click()}>Click to upload</p>
|
||||
|
||||
|
||||
<div>
|
||||
|
||||
{selectedFiles.length > 0 &&
|
||||
selectedFiles.map((file, index) => (
|
||||
<div>
|
||||
<div key={index} className={styles.fileInfo}>
|
||||
<strong>{file.name}</strong>
|
||||
</div>
|
||||
<div className={styles.fileInfoClose} onClick={() => setSelectedFile(null)}>
|
||||
<div
|
||||
className={styles.fileInfoClose}
|
||||
onClick={() =>
|
||||
setSelectedFiles((prev) => prev.filter((_, i) => i !== index))
|
||||
}
|
||||
>
|
||||
X
|
||||
</div>
|
||||
</>
|
||||
)}
|
||||
</div>
|
||||
))}
|
||||
|
||||
{selectedFiles.length > 0 &&
|
||||
<div>
|
||||
<div onClick={()=>handleBatchUpload()} className={styles.fileUpload}>
|
||||
<strong>Upload</strong>
|
||||
</div>
|
||||
</div>
|
||||
}
|
||||
</div>
|
||||
<input
|
||||
id="fileInput"
|
||||
type="file"
|
||||
multiple
|
||||
style={{ display: "none" }}
|
||||
onChange={(e) => {
|
||||
const file = e.target.files[0];
|
||||
handleFile(file);
|
||||
const files = Array.from(e.target.files);
|
||||
handleFiles(files);
|
||||
}}
|
||||
/>
|
||||
|
||||
</div>
|
||||
</div>
|
||||
|
||||
|
||||
|
||||
|
||||
<div className={styles.footer}>
|
||||
© 2025 Kediri Technopark
|
||||
</div>
|
||||
|
||||
@@ -4,13 +4,14 @@
|
||||
}
|
||||
|
||||
.uploadContainer {
|
||||
overflow: hidden;
|
||||
margin-top: 16px;
|
||||
border: 2px dashed #ccc;
|
||||
border-radius: 12px;
|
||||
padding: 32px;
|
||||
text-align: center;
|
||||
cursor: pointer;
|
||||
transition: border-color 0.3s;
|
||||
padding-bottom: 30px;
|
||||
}
|
||||
|
||||
.uploadContainer:hover {
|
||||
@@ -96,6 +97,7 @@
|
||||
|
||||
.desktopText {
|
||||
display: block;
|
||||
/* padding: 32px; */
|
||||
}
|
||||
|
||||
.mobileText {
|
||||
@@ -115,10 +117,12 @@
|
||||
padding: 8px 12px;
|
||||
border-radius: 8px 0 0 8px;
|
||||
display: inline-block;
|
||||
margin-bottom: 10px;
|
||||
}
|
||||
|
||||
.fileInfoClose {
|
||||
margin-left: 2px;
|
||||
margin-right: 6px;
|
||||
margin-top: 16px;
|
||||
font-size: 14px;
|
||||
color: #ff0000;
|
||||
@@ -130,6 +134,17 @@
|
||||
|
||||
}
|
||||
|
||||
.fileUpload {
|
||||
margin-top: 16px;
|
||||
font-size: 14px;
|
||||
color: white;
|
||||
background: #2bb438;
|
||||
padding: 8px 12px;
|
||||
border-radius: 8px;
|
||||
display: inline-block;
|
||||
margin-bottom: 10px;
|
||||
}
|
||||
|
||||
/* Mobile styles */
|
||||
@media (max-width: 768px) {
|
||||
.h1 {
|
||||
@@ -243,3 +258,41 @@ position: absolute;
|
||||
.dropdownItem:hover {
|
||||
background-color: #f0f0f0;
|
||||
}
|
||||
|
||||
.fileTable {
|
||||
width: 100%;
|
||||
border-collapse: collapse;
|
||||
margin-bottom: 40px;
|
||||
}
|
||||
|
||||
.fileTable th,
|
||||
.fileTable td {
|
||||
border-bottom: 1px solid #ccc;
|
||||
padding: 10px;
|
||||
text-align: left;
|
||||
}
|
||||
|
||||
.fileTable th {
|
||||
background-color: #f2f2f2;
|
||||
}
|
||||
|
||||
.actionBar {
|
||||
margin-bottom: 12px;
|
||||
display: flex;
|
||||
gap: 10px;
|
||||
}
|
||||
|
||||
.actionBar button {
|
||||
padding: 6px 12px;
|
||||
background-color: #00adef;
|
||||
color: white;
|
||||
font-weight: bold;
|
||||
border: none;
|
||||
border-radius: 6px;
|
||||
cursor: pointer;
|
||||
}
|
||||
|
||||
.actionBar button:hover {
|
||||
background-color: #0095cc;
|
||||
}
|
||||
|
||||
|
||||
131
src/FileManager.js
Normal file
131
src/FileManager.js
Normal file
@@ -0,0 +1,131 @@
|
||||
// import React, { useEffect, useState } from 'react';
|
||||
|
||||
// const FileManager = ({ token }) => {
|
||||
// const [files, setFiles] = useState([]);
|
||||
// const [uploadFile, setUploadFile] = useState(null);
|
||||
|
||||
// const API_BASE = 'https://bot.kediritechnopark.com/webhook/files';
|
||||
|
||||
// // Ambil daftar file
|
||||
// const fetchFiles = async () => {
|
||||
// try {
|
||||
// const response = await fetch(`${API_BASE}/list`, {
|
||||
// method: 'POST',
|
||||
// headers: {
|
||||
// 'Content-Type': 'application/json',
|
||||
// 'Authorization': `Bearer ${token}`,
|
||||
// }
|
||||
// });
|
||||
|
||||
// const data = await response.json();
|
||||
// setFiles(data);
|
||||
// } catch (error) {
|
||||
// console.error('Gagal mengambil daftar file:', error);
|
||||
// }
|
||||
// };
|
||||
|
||||
// // Hapus file
|
||||
// const deleteFile = async (key) => {
|
||||
// if (!window.confirm(`Yakin ingin hapus "${key}"?`)) return;
|
||||
// try {
|
||||
// await fetch(`${API_BASE}/delete`, {
|
||||
// method: 'POST',
|
||||
// headers: {
|
||||
// 'Content-Type': 'application/json',
|
||||
// 'Authorization': `Bearer ${token}`,
|
||||
// },
|
||||
// body: JSON.stringify({ key }),
|
||||
// });
|
||||
|
||||
// fetchFiles();
|
||||
// } catch (error) {
|
||||
// console.error('Gagal menghapus file:', error);
|
||||
// }
|
||||
// };
|
||||
|
||||
// // Upload file
|
||||
// const handleUpload = async () => {
|
||||
// if (!uploadFile) return;
|
||||
|
||||
// const formData = new FormData();
|
||||
// formData.append('file', uploadFile);
|
||||
|
||||
// try {
|
||||
// await fetch(`${API_BASE}/upload`, {
|
||||
// method: 'POST',
|
||||
// headers: {
|
||||
// 'Authorization': `Bearer ${token}`,
|
||||
// },
|
||||
// body: formData,
|
||||
// });
|
||||
|
||||
// setUploadFile(null);
|
||||
// fetchFiles();
|
||||
// } catch (error) {
|
||||
// console.error('Gagal mengunggah file:', error);
|
||||
// }
|
||||
// };
|
||||
|
||||
// // Download file
|
||||
// const downloadFile = async (key) => {
|
||||
// try {
|
||||
// const response = await fetch(`${API_BASE}/download?key=${encodeURIComponent(key)}`, {
|
||||
// method: 'GET',
|
||||
// headers: {
|
||||
// 'Authorization': `Bearer ${token}`,
|
||||
// },
|
||||
// });
|
||||
|
||||
// if (!response.ok) throw new Error('Download gagal');
|
||||
|
||||
// const blob = await response.blob();
|
||||
// const url = window.URL.createObjectURL(blob);
|
||||
|
||||
// const a = document.createElement('a');
|
||||
// a.href = url;
|
||||
// a.download = key;
|
||||
// document.body.appendChild(a);
|
||||
// a.click();
|
||||
// a.remove();
|
||||
|
||||
// window.URL.revokeObjectURL(url);
|
||||
// } catch (error) {
|
||||
// console.error('Gagal mengunduh file:', error);
|
||||
// }
|
||||
// };
|
||||
|
||||
// useEffect(() => {
|
||||
// fetchFiles();
|
||||
// }, []);
|
||||
|
||||
// return (
|
||||
// <div style={{ padding: 20, fontFamily: 'Arial' }}>
|
||||
// <h2>📁 Manajemen File</h2>
|
||||
|
||||
// <ul>
|
||||
// {files.map(file => (
|
||||
// <li key={file.Key} style={{ marginBottom: 8 }}>
|
||||
// <strong>{file.Key}</strong>{' '}
|
||||
// <button onClick={() => downloadFile(file.Key)}>⬇️ Download</button>{' '}
|
||||
// <button onClick={() => deleteFile(file.Key)}>🗑️ Hapus</button>
|
||||
// </li>
|
||||
// ))}
|
||||
// </ul>
|
||||
|
||||
// <hr />
|
||||
|
||||
// <div>
|
||||
// <h3>📤 Upload File</h3>
|
||||
// <input
|
||||
// type="file"
|
||||
// onChange={e => setUploadFile(e.target.files[0])}
|
||||
// />
|
||||
// <button onClick={handleUpload} disabled={!uploadFile}>
|
||||
// Upload
|
||||
// </button>
|
||||
// </div>
|
||||
// </div>
|
||||
// );
|
||||
// };
|
||||
|
||||
// export default FileManager;
|
||||
Reference in New Issue
Block a user