diff --git a/public/back.png b/public/back.png
new file mode 100644
index 0000000..77c050b
Binary files /dev/null and b/public/back.png differ
diff --git a/public/camera.png b/public/camera.png
new file mode 100644
index 0000000..83f5c3a
Binary files /dev/null and b/public/camera.png differ
diff --git a/public/send.png b/public/send.png
new file mode 100644
index 0000000..66cdf58
Binary files /dev/null and b/public/send.png differ
diff --git a/public/upload.png b/public/upload.png
new file mode 100644
index 0000000..1844373
Binary files /dev/null and b/public/upload.png differ
diff --git a/src/Camera.js b/src/Camera.js
index dd775aa..a498fbb 100644
--- a/src/Camera.js
+++ b/src/Camera.js
@@ -5,11 +5,11 @@ const CameraPage = ({ handleClose, handleUploadImage }) => {
const [isCameraActive, setIsCameraActive] = useState(true);
const [uploading, setUploading] = useState(false);
const [isMobile, setIsMobile] = useState(false);
+ const [isUploadedFile, setIsUploadedFile] = useState(false); // ✅ NEW STATE
const videoRef = useRef(null);
const canvasRef = useRef(null);
const fileInputRef = useRef(null);
- // Check if device is mobile
useEffect(() => {
const checkMobile = () => {
setIsMobile(window.innerWidth <= 768);
@@ -21,7 +21,6 @@ const CameraPage = ({ handleClose, handleUploadImage }) => {
return () => window.removeEventListener('resize', checkMobile);
}, []);
- // Start the camera when the component mounts
useEffect(() => {
const startCamera = async () => {
try {
@@ -37,7 +36,6 @@ const CameraPage = ({ handleClose, handleUploadImage }) => {
};
startCamera();
- // Cleanup camera stream when component unmounts
return () => {
if (videoRef.current && videoRef.current.srcObject) {
const tracks = videoRef.current.srcObject.getTracks();
@@ -46,7 +44,6 @@ const CameraPage = ({ handleClose, handleUploadImage }) => {
};
}, []);
- // Capture the image from the video stream
const captureImage = () => {
const canvas = canvasRef.current;
const video = videoRef.current;
@@ -61,10 +58,10 @@ const CameraPage = ({ handleClose, handleUploadImage }) => {
const capturedImage = canvas.toDataURL('image/jpeg');
setImage(capturedImage);
setIsCameraActive(false);
+ setIsUploadedFile(false); // ✅ from camera
}
};
- // Handle image upload from file input
const handleFileUpload = e => {
const file = e.target.files[0];
if (file) {
@@ -72,15 +69,17 @@ const CameraPage = ({ handleClose, handleUploadImage }) => {
reader.onloadend = () => {
setImage(reader.result);
setIsCameraActive(false);
+ setIsUploadedFile(true); // ✅ from file
};
reader.readAsDataURL(file);
}
};
- // Cancel the image capture or file upload and restart the camera
const cancelCapture = () => {
setImage(null);
setIsCameraActive(true);
+ setIsUploadedFile(false); // ✅ reset
+
const startCamera = async () => {
try {
const stream = await navigator.mediaDevices.getUserMedia({
@@ -96,71 +95,96 @@ const CameraPage = ({ handleClose, handleUploadImage }) => {
startCamera();
};
- // Trigger file input click
const triggerFileInput = () => {
fileInputRef.current?.click();
};
const mainContent = (
- {/* Camera/Image Display Area */}
{isCameraActive && (
)}
{!isCameraActive && image && (
-
+
)}
- {/* Floating Controls */}
{isCameraActive ? (
<>
{
+ handleClose();
+ }}
+ style={baseButtonStyle}
aria-label="Cancel and retake"
>
- ❌
+
- 📸
+
- 📁
+
>
) : (
<>
- ❌
+
handleUploadImage(image)} // Pass image data here
- style={confirmButtonStyle}
+ onClick={() => handleUploadImage(image)}
+ style={baseButtonStyle}
disabled={uploading}
aria-label={uploading ? 'Uploading...' : 'Confirm upload'}
>
- {uploading ? '⏳' : '✅'}
-
+
>
)}
- {/* Hidden file input */}
{
/>
- {/* Hidden canvas element for capturing the image */}
);
- // Desktop layout with left and right sidebars
if (!isMobile) {
return (
- {/* Left Sidebar */}
-
- {/* Main content */}
{mainContent}
-
- {/* Right Sidebar */}
);
}
- // Mobile layout (full screen)
return mainContent;
};
@@ -222,7 +238,6 @@ const videoStyle = {
const imageStyle = {
width: '100%',
height: '100%',
- objectFit: 'cover',
};
const controlsStyle = {
@@ -249,23 +264,6 @@ const baseButtonStyle = {
transition: 'all 0.2s ease',
};
-const captureButtonStyle = {
- ...baseButtonStyle,
- backgroundColor: '#fff',
- color: '#000',
-};
-
-const uploadButtonStyle = {
- ...baseButtonStyle,
- backgroundColor: '#4CAF50',
- color: '#fff',
-};
-
-const cancelButtonStyle = {
- ...baseButtonStyle,
- backgroundColor: '#f44336',
- color: '#fff',
-};
const confirmButtonStyle = {
...baseButtonStyle,
@@ -273,7 +271,6 @@ const confirmButtonStyle = {
color: '#fff',
};
-// Desktop styles
const desktopLayoutStyle = {
display: 'flex',
height: '100vh',
@@ -289,26 +286,6 @@ const sidebarStyle = {
borderRight: '1px solid #e0e0e0',
};
-const sidebarTitleStyle = {
- margin: '0 0 30px 0',
- fontSize: '24px',
- fontWeight: 'bold',
- color: '#333',
-};
-
-const sidebarContentStyle = {
- display: 'flex',
- flexDirection: 'column',
- gap: '15px',
-};
-
-const sidebarTextStyle = {
- margin: '0',
- fontSize: '16px',
- lineHeight: '1.4',
- color: '#666',
-};
-
const mainContentStyle = {
flex: 1,
position: 'relative',
diff --git a/src/ChatBot.js b/src/ChatBot.js
index 6c6c6ff..7d96478 100644
--- a/src/ChatBot.js
+++ b/src/ChatBot.js
@@ -1,6 +1,6 @@
import React, { useState, useEffect } from 'react';
import styles from './ChatBot.module.css';
-import Camera from './Camera'
+import Camera from './Camera';
const ChatBot = ({ existingConversation }) => {
const [messages, setMessages] = useState([
@@ -15,22 +15,19 @@ const ChatBot = ({ existingConversation }) => {
},
]);
-
const [input, setInput] = useState('');
- const [isLoading, setIsLoading] = useState(false);
-
-
+ const [isLoading, setIsLoading] = useState('');
const [isPoppedUp, setIsPoppedUp] = useState('');
const [name, setName] = useState('');
const [phoneNumber, setPhoneNumber] = useState('');
-
const [isOpenCamera, setIsOpenCamera] = useState(false);
- useEffect(() => {
+ useEffect(() => {
if (existingConversation && existingConversation.length > 0) {
setMessages(existingConversation);
}
- }, [existingConversation])
+ }, [existingConversation]);
+
useEffect(() => {
if (!localStorage.getItem('session')) {
function generateUUID() {
@@ -48,19 +45,107 @@ const ChatBot = ({ existingConversation }) => {
}
}, []);
+ function base64ToFile(base64Data, filename) {
+ const arr = base64Data.split(',');
+ const mime = arr[0].match(/:(.*?);/)[1];
+ const bstr = atob(arr[1]);
+ let n = bstr.length;
+ const u8arr = new Uint8Array(n);
+
+ while (n--) {
+ u8arr[n] = bstr.charCodeAt(n);
+ }
+
+ return new File([u8arr], filename, { type: mime });
+ }
+
+ const askToBot = async ({ type = 'text', content, tryCount = 0 }) => {
+ const session = JSON.parse(localStorage.getItem('session'));
+ if (!session || !session.sessionId) return;
+
+ let body;
+ let headers;
+
+ const isBase64Image = type === 'image' && typeof content === 'string' && content.startsWith('data:image/');
+
+ if (isBase64Image) {
+ const file = base64ToFile(content, 'photo.jpg');
+ const formData = new FormData();
+ formData.append('sessionId', session.sessionId);
+ formData.append('lastSeen', new Date().toISOString());
+ formData.append('name', session.name || '');
+ formData.append('phoneNumber', session.phoneNumber || '');
+ formData.append('type', type);
+ formData.append('image', file);
+
+ body = formData;
+ headers = {};
+ } else {
+ body = JSON.stringify({
+ sessionId: session.sessionId,
+ lastSeen: new Date().toISOString(),
+ name: session.name,
+ phoneNumber: session.phoneNumber,
+ pertanyaan: content,
+ type: type,
+ });
+ headers = { 'Content-Type': 'application/json' };
+ }
+
+ try {
+ const response = await fetch('https://bot.kediritechnopark.com/webhook/master-agent/ask', {
+ method: 'POST',
+ headers,
+ body,
+ });
+
+ const data = await response.json();
+ return data;
+ } catch (error) {
+ if (tryCount < 3) {
+ return new Promise((resolve) =>
+ setTimeout(() => resolve(askToBot({ type, content, tryCount: tryCount + 1 })), 3000)
+ );
+ } else {
+ console.error('Bot unavailable:', error);
+ return { jawaban: 'Maaf saya sedang tidak tersedia sekarang, coba lagi nanti' };
+ }
+ }
+ };
+
+ const handleUploadImage = async (img) => {
+ setIsOpenCamera(false);
+
+ const newMessages = [
+ ...messages,
+ { sender: 'user', img: img, time: getTime() },
+ ];
+ setMessages(newMessages);
+ setIsLoading('Menganalisa gambar anda...');
+
+ const data = await askToBot({ type: 'image', content: img });
+
+ const botAnswer = data.jawaban || 'Maaf, saya tidak bisa menganalisis gambar tersebut.';
+
+ setMessages((prev) => [
+ ...prev,
+ { sender: 'bot', text: botAnswer, time: getTime() },
+ ]);
+
+ setIsLoading('');
+ };
+
const sendMessage = async (textOverride = null, name, phoneNumber, tryCount = 0) => {
const message = textOverride || input.trim();
if (message === '') return;
const session = JSON.parse(localStorage.getItem('session'));
-
- if ((!session || !session.name || !session.phoneNumber) && messages.length > 2) {
- setIsPoppedUp(message); // munculkan form input
+ if ((!session || !session.name || !session.phoneNumber) && messages.length > 2) {
+ setIsPoppedUp(message);
setInput('');
return;
}
- // Show user's message immediately
const newMessages = [
...messages,
{ sender: 'user', text: message, time: getTime() },
@@ -68,87 +153,68 @@ const ChatBot = ({ existingConversation }) => {
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(), name: JSON.parse(localStorage.getItem('session')).name, phoneNumber: JSON.parse(localStorage.getItem('session')).phoneNumber }),
- });
+ setIsLoading('Mengetik...');
+
+ try {
+ const data = await askToBot({ type: 'text', content: message, tryCount });
- 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.jawaban || 'Maaf saya sedang tidak tersedia sekarang, coba lagi nanti';
- // Add bot's reply
setMessages(prev => [
...prev,
{ sender: 'bot', text: botAnswer, time: getTime() },
]);
- setIsLoading(false);
+ setIsLoading('');
} catch (error) {
- console.log(tryCount)
- if (tryCount > 3) {
- // Add bot's error reply
+ console.error('Error sending message:', error);
+ if (tryCount >= 3) {
setMessages(prev => [
...prev,
{ sender: 'bot', text: 'Maaf saya sedang tidak tersedia sekarang, coba lagi nanti', time: getTime() },
]);
- setIsLoading(false);
- return;
+ setIsLoading('');
+ } else {
+ setTimeout(() => sendMessage(message, name, phoneNumber, tryCount + 1), 3000);
}
- setTimeout(() => sendMessage(message, name, phoneNumber, tryCount + 1), 3000);
-
- console.error('Fetch error:', error);
}
};
-function formatBoldText(text) {
- const parts = text.split(/(\*\*[^\*]+\*\*)/g);
- return parts.flatMap((part, index) => {
- const elements = [];
+ function formatBoldText(text) {
+ const parts = text.split(/(\*\*[^\*]+\*\*)/g);
- if (part.startsWith('**') && part.endsWith('**')) {
- // Bold text
- part = part.slice(2, -2);
- part.split('\n').forEach((line, i) => {
- if (i > 0) elements.push( );
- elements.push({line} );
- });
- } else {
- // Normal text
- part.split('\n').forEach((line, i) => {
- if (i > 0) elements.push( );
- elements.push({line} );
- });
- }
+ return parts.flatMap((part, index) => {
+ const elements = [];
- return elements;
- });
-}
+ if (part.startsWith('**') && part.endsWith('**')) {
+ part = part.slice(2, -2);
+ part.split('\n').forEach((line, i) => {
+ if (i > 0) elements.push( );
+ elements.push({line} );
+ });
+ } else {
+ part.split('\n').forEach((line, i) => {
+ if (i > 0) elements.push( );
+ elements.push({line} );
+ });
+ }
-const handleUploadImage = (e) => {
- console.log(e)
-}
+ return elements;
+ });
+ }
return (
-
+
DERMALOUNGE
-
- {isLoading && (
+ {isLoading != '' && (
- Mengetik...
+ {isLoading}
)}
@@ -159,15 +225,18 @@ const handleUploadImage = (e) => {
>
{msg.sender !== 'bot'
- ? msg.text
- : (() => {
- try {
- return formatBoldText(msg.text); // Apply formatting here
- } catch (e) {
- return msg.text;
- }
- })()}
-
+ ? (msg.text ?
+ msg.text
+ :
+
+ )
+ : (() => {
+ try {
+ return formatBoldText(msg.text);
+ } catch (e) {
+ return msg.text;
+ }
+ })()}
{msg.quickReplies && (
{msg.quickReplies.map((reply, i) => (
@@ -179,14 +248,14 @@ const handleUploadImage = (e) => {
{reply}
))}
-
setIsOpenCamera(true)}
- style={{color: 'white', backgroundColor: '#075e54', display: 'flex', flexDirection: 'row', alignItems:'center'}}
- >
-
- Analisa Kulit
-
+
setIsOpenCamera(true)}
+ style={{ color: 'white', backgroundColor: '#075e54', display: 'flex', flexDirection: 'row', alignItems: 'center' }}
+ >
+
+ Analisa Gambar
+
)}
{msg.time}
@@ -195,21 +264,34 @@ const handleUploadImage = (e) => {
))}
-
+
- {isPoppedUp != '' &&
+
+ {isPoppedUp !== '' &&
Untuk bisa membantu Anda lebih jauh, boleh saya tahu nama dan nomor telepon Anda?
@@ -218,7 +300,6 @@ const handleUploadImage = (e) => {
console.log('Nama focused')}
value={name}
onChange={(e) => setName(e.target.value)}
maxLength={40}
@@ -235,14 +316,11 @@ const handleUploadImage = (e) => {
value={phoneNumber}
onChange={(e) => {
const value = e.target.value;
- // Hanya angka, maksimal 11 karakter
if (/^\d{0,11}$/.test(value)) {
setPhoneNumber(value);
}
}}
- onFocus={() => console.log('Telepon focused')}
/>
-
{
onClick={() => {
if (name.length > 2 && phoneNumber.length >= 10) {
const sessionData = JSON.parse(localStorage.getItem('session')) || {};
-
sessionData.name = name;
sessionData.phoneNumber = phoneNumber;
-
localStorage.setItem('session', JSON.stringify(sessionData));
- setIsPoppedUp('')
- sendMessage(isPoppedUp)
+ setIsPoppedUp('');
+ sendMessage(isPoppedUp);
}
}}
>
@@ -266,7 +342,13 @@ const handleUploadImage = (e) => {
}
- {isOpenCamera &&
setIsOpenCamera(false)} handleUploadImage={(e)=>handleUploadImage(e)}/>}
+
+ {isOpenCamera && (
+ setIsOpenCamera(false)}
+ handleUploadImage={(e) => handleUploadImage(e)}
+ />
+ )}
);
};