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 && (
- {/* 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 ( -
+
Bot Avatar 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) => { ))}
-
+
setInput(e.target.value)} onKeyDown={(e) => e.key === 'Enter' && sendMessage()} - disabled={isLoading} + disabled={isLoading != ''} /> - + +
- {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)} + /> + )}
); };