This commit is contained in:
Vassshhh
2025-08-07 00:10:38 +07:00
parent 7abdf68372
commit c1a9f37888
4 changed files with 119 additions and 81 deletions

View File

@@ -9,17 +9,13 @@ const ChatBot = ({ existingConversation }) => {
sender: 'bot', sender: 'bot',
text: 'Hai! Saya Alle, asisten virtual Dermalounge Clinic. Ada yang bisa Alle bantu hari ini?', text: 'Hai! Saya Alle, asisten virtual Dermalounge Clinic. Ada yang bisa Alle bantu hari ini?',
time: getTime(), time: getTime(),
quickReplies: [ quickReplies: ['Konsultasi Estetik', 'Konsultasi Kulit & Kelamin'],
'Konsultasi Estetik',
'Konsultasi Kulit & Kelamin'
],
}, },
]); ]);
const [searchParams, setSearchParams] = useSearchParams(); const [searchParams, setSearchParams] = useSearchParams();
const navigate = useNavigate(); const navigate = useNavigate();
const isOpenCamera = searchParams.get('camera') === 'open';
const isOpenCamera = searchParams.get('camera') === 'open';
const [input, setInput] = useState(''); const [input, setInput] = useState('');
const [isLoading, setIsLoading] = useState(''); const [isLoading, setIsLoading] = useState('');
@@ -45,8 +41,7 @@ const isOpenCamera = searchParams.get('camera') === 'open';
const sessionId = generateUUID(); const sessionId = generateUUID();
const dateNow = new Date().toISOString(); const dateNow = new Date().toISOString();
sessionStorage.setItem('session', JSON.stringify({ sessionId, lastSeen: dateNow }));
sessionStorage.setItem('session', JSON.stringify({ sessionId: sessionId, lastSeen: dateNow }))
} }
}, []); }, []);
@@ -56,11 +51,9 @@ const isOpenCamera = searchParams.get('camera') === 'open';
const bstr = atob(arr[1]); const bstr = atob(arr[1]);
let n = bstr.length; let n = bstr.length;
const u8arr = new Uint8Array(n); const u8arr = new Uint8Array(n);
while (n--) { while (n--) {
u8arr[n] = bstr.charCodeAt(n); u8arr[n] = bstr.charCodeAt(n);
} }
return new File([u8arr], filename, { type: mime }); return new File([u8arr], filename, { type: mime });
} }
@@ -70,7 +63,6 @@ const isOpenCamera = searchParams.get('camera') === 'open';
let body; let body;
let headers; let headers;
const isBase64Image = type === 'image' && typeof content === 'string' && content.startsWith('data:image/'); const isBase64Image = type === 'image' && typeof content === 'string' && content.startsWith('data:image/');
if (isBase64Image) { if (isBase64Image) {
@@ -82,7 +74,6 @@ const isOpenCamera = searchParams.get('camera') === 'open';
formData.append('phoneNumber', session.phoneNumber || ''); formData.append('phoneNumber', session.phoneNumber || '');
formData.append('type', type); formData.append('type', type);
formData.append('image', file); formData.append('image', file);
body = formData; body = formData;
headers = {}; headers = {};
} else { } else {
@@ -103,7 +94,6 @@ const isOpenCamera = searchParams.get('camera') === 'open';
headers, headers,
body, body,
}); });
const data = await response.json(); const data = await response.json();
return data; return data;
} catch (error) { } catch (error) {
@@ -119,7 +109,14 @@ const isOpenCamera = searchParams.get('camera') === 'open';
}; };
const handleUploadImage = async (img) => { const handleUploadImage = async (img) => {
setSearchParams({ }) const session = JSON.parse(sessionStorage.getItem('session'));
if (!session?.name || !session?.phoneNumber) {
setIsPoppedUp(img); // simpan gambar untuk dikirim setelah user isi data
setSearchParams({});
return;
}
setSearchParams({});
const newMessages = [ const newMessages = [
...messages, ...messages,
{ sender: 'user', img: img, time: getTime() }, { sender: 'user', img: img, time: getTime() },
@@ -128,14 +125,12 @@ const isOpenCamera = searchParams.get('camera') === 'open';
setIsLoading('Menganalisa gambar anda...'); setIsLoading('Menganalisa gambar anda...');
const data = await askToBot({ type: 'image', content: img }); const data = await askToBot({ type: 'image', content: img });
const botAnswer = data.jawaban || 'Maaf, saya tidak bisa menganalisis gambar tersebut.'; const botAnswer = data.jawaban || 'Maaf, saya tidak bisa menganalisis gambar tersebut.';
setMessages((prev) => [ setMessages((prev) => [
...prev, ...prev,
{ sender: 'bot', text: botAnswer, time: getTime() }, { sender: 'bot', text: botAnswer, time: getTime() },
]); ]);
setIsLoading(''); setIsLoading('');
}; };
@@ -150,25 +145,19 @@ const isOpenCamera = searchParams.get('camera') === 'open';
return; return;
} }
const newMessages = [ const newMessages = [...messages, { sender: 'user', text: message, time: getTime() }];
...messages,
{ sender: 'user', text: message, time: getTime() },
];
setMessages(newMessages); setMessages(newMessages);
setInput(''); setInput('');
setIsLoading('Mengetik...'); setIsLoading('Mengetik...');
try { try {
const data = await askToBot({ type: 'text', content: message, tryCount }); const data = await askToBot({ type: 'text', content: message, tryCount });
const botAnswer = data.jawaban || 'Maaf saya sedang tidak tersedia sekarang, coba lagi nanti'; const botAnswer = data.jawaban || 'Maaf saya sedang tidak tersedia sekarang, coba lagi nanti';
setMessages(prev => [ setMessages(prev => [
...prev, ...prev,
{ sender: 'bot', text: botAnswer, time: getTime() }, { sender: 'bot', text: botAnswer, time: getTime() },
]); ]);
setIsLoading(''); setIsLoading('');
} catch (error) { } catch (error) {
console.error('Error sending message:', error); console.error('Error sending message:', error);
@@ -186,10 +175,8 @@ const isOpenCamera = searchParams.get('camera') === 'open';
function formatBoldText(text) { function formatBoldText(text) {
const parts = text.split(/(\*\*[^\*]+\*\*)/g); const parts = text.split(/(\*\*[^\*]+\*\*)/g);
return parts.flatMap((part, index) => { return parts.flatMap((part, index) => {
const elements = []; const elements = [];
if (part.startsWith('**') && part.endsWith('**')) { if (part.startsWith('**') && part.endsWith('**')) {
part = part.slice(2, -2); part = part.slice(2, -2);
part.split('\n').forEach((line, i) => { part.split('\n').forEach((line, i) => {
@@ -202,7 +189,6 @@ const isOpenCamera = searchParams.get('camera') === 'open';
elements.push(<span key={`text-${index}-${i}`}>{line}</span>); elements.push(<span key={`text-${index}-${i}`}>{line}</span>);
}); });
} }
return elements; return elements;
}); });
} }
@@ -215,7 +201,7 @@ const isOpenCamera = searchParams.get('camera') === 'open';
</div> </div>
<div className={styles.chatBody}> <div className={styles.chatBody}>
{isLoading != '' && ( {isLoading !== '' && (
<div className={`${styles.messageRow} ${styles.bot}`}> <div className={`${styles.messageRow} ${styles.bot}`}>
<div className={`${styles.message} ${styles.bot}`}> <div className={`${styles.message} ${styles.bot}`}>
<em>{isLoading}</em> <em>{isLoading}</em>
@@ -223,39 +209,30 @@ const isOpenCamera = searchParams.get('camera') === 'open';
</div> </div>
)} )}
{messages.slice().reverse().map((msg, index) => ( {messages.slice().reverse().map((msg, index) => (
<div <div key={index} className={`${styles.messageRow} ${styles[msg.sender]}`}>
key={index}
className={`${styles.messageRow} ${styles[msg.sender]}`}
>
<div className={`${styles.message} ${styles[msg.sender]}`}> <div className={`${styles.message} ${styles[msg.sender]}`}>
{msg.sender !== 'bot' {msg.sender !== 'bot' ? (
? (msg.text ? msg.text ? msg.text : <img style={{ maxHeight: '300px', maxWidth: '240px', borderRadius: '12px' }} src={msg.img} />
msg.text ) : (
: (() => {
<img style={{ maxHeight: '300px', maxWidth: '240px', borderRadius: '12px' }} src={msg.img} />
)
: (() => {
try { try {
return formatBoldText(msg.text); return formatBoldText(msg.text);
} catch (e) { } catch (e) {
return msg.text; return msg.text;
} }
})()} })()
)}
{msg.quickReplies && ( {msg.quickReplies && (
<div className={styles.quickReplies}> <div className={styles.quickReplies}>
{msg.quickReplies.map((reply, i) => ( {msg.quickReplies.map((reply, i) => (
<div <div key={i} className={styles.quickReply} onClick={() => sendMessage(reply)}>
key={i}
className={styles.quickReply}
onClick={() => sendMessage(reply)}
>
{reply} {reply}
</div> </div>
))} ))}
<div <div
className={styles.quickReply} className={styles.quickReply}
onClick={() => setSearchParams({ camera: 'open' })} onClick={() => setSearchParams({ camera: 'open' })}
style={{ color: 'white', backgroundColor: '#075e54', display: 'flex', flexDirection: 'row', alignItems: 'center' }} style={{ color: 'white', backgroundColor: '#075e54', display: 'flex', alignItems: 'center' }}
> >
<img style={{ marginRight: '5px', height: '14px', filter: 'invert(1)' }} src={'/camera.png'} /> <img style={{ marginRight: '5px', height: '14px', filter: 'invert(1)' }} src={'/camera.png'} />
Analisa Gambar Analisa Gambar
@@ -275,27 +252,17 @@ const isOpenCamera = searchParams.get('camera') === 'open';
value={input} value={input}
onChange={(e) => setInput(e.target.value)} onChange={(e) => setInput(e.target.value)}
onKeyDown={(e) => e.key === 'Enter' && sendMessage()} onKeyDown={(e) => e.key === 'Enter' && sendMessage()}
disabled={isLoading != ''} disabled={isLoading !== ''}
/> />
<button onClick={() => sendMessage()} style={{ marginLeft: '-40px' }} disabled={isLoading !== ''}>
<button onClick={() => sendMessage()} style={{marginLeft: '-40px'}}disabled={isLoading!=''}> <img src="/send.png" alt="Kirim" style={{ height: '20px', filter: 'invert(1)' }} />
<img
src="/send.png"
alt="Kirim"
style={{ height: '20px', filter: 'invert(1)' }}
/>
</button> </button>
<button onClick={() => setSearchParams({ camera: 'open' })} disabled={isLoading !== ''}>
<button onClick={() => setSearchParams({ camera: 'open' })} disabled={isLoading!=''}> <img src="/camera.png" alt="Kamera" style={{ height: '18px', filter: 'invert(1)' }} />
<img
src="/camera.png"
alt="Kamera"
style={{ height: '18px', filter: 'invert(1)' }}
/>
</button> </button>
</div> </div>
{isPoppedUp !== '' && {isPoppedUp !== '' && (
<div className={styles.PopUp}> <div className={styles.PopUp}>
<div className={`${styles.message} ${styles['bot']}`}> <div className={`${styles.message} ${styles['bot']}`}>
Untuk bisa membantu Anda lebih jauh, boleh saya tahu nama dan nomor telepon Anda? Untuk bisa membantu Anda lebih jauh, boleh saya tahu nama dan nomor telepon Anda?
@@ -326,7 +293,6 @@ const isOpenCamera = searchParams.get('camera') === 'open';
}} }}
/> />
</div> </div>
<div <div
className={styles.nextButton} className={styles.nextButton}
onClick={() => { onClick={() => {
@@ -336,7 +302,11 @@ const isOpenCamera = searchParams.get('camera') === 'open';
sessionData.phoneNumber = phoneNumber; sessionData.phoneNumber = phoneNumber;
sessionStorage.setItem('session', JSON.stringify(sessionData)); sessionStorage.setItem('session', JSON.stringify(sessionData));
setIsPoppedUp(''); setIsPoppedUp('');
sendMessage(isPoppedUp); if (typeof isPoppedUp === 'string' && isPoppedUp.startsWith('data:image/')) {
handleUploadImage(isPoppedUp);
} else {
sendMessage(isPoppedUp);
}
} }
}} }}
> >
@@ -345,7 +315,7 @@ const isOpenCamera = searchParams.get('camera') === 'open';
</div> </div>
</div> </div>
</div> </div>
} )}
{isOpenCamera && ( {isOpenCamera && (
<Camera <Camera

View File

@@ -22,6 +22,7 @@ const Dashboard = () => {
const [weeklyData, setWeeklyData] = useState([]); const [weeklyData, setWeeklyData] = useState([]);
const [allTimeData, setAllTimeData] = useState([]); const [allTimeData, setAllTimeData] = useState([]);
const [faceAnalystList, setFaceAnalystList] = useState([]);
const [conversations, setConversations] = useState([]); const [conversations, setConversations] = useState([]);
const [followUps, setFollowUps] = useState([]); const [followUps, setFollowUps] = useState([]);
@@ -318,6 +319,8 @@ function parseGraphData(graph) {
try{ try{
console.log(data); console.log(data);
setDiscussedTopics(data[0]?.graph[0]?.json?.result?.topics) setDiscussedTopics(data[0]?.graph[0]?.json?.result?.topics)
setFaceAnalystList(data[0]?.graph[0]?.json?.result?.analyst_counter);
setFollowUps(data[0]?.graph[0]?.json?.result?.interested_users) setFollowUps(data[0]?.graph[0]?.json?.result?.interested_users)
setFileList(data[0]?.files) setFileList(data[0]?.files)
setUpdateDetected(data[1]?.updateDetected) setUpdateDetected(data[1]?.updateDetected)
@@ -437,7 +440,7 @@ function parseGraphData(graph) {
}; };
const openTopicsModal = () => { const openTopicsModal = () => {
setModalContent(<DiscussedTopics topics={discussedTopics} />); setModalContent(<DiscussedTopics topics={discussedTopics} faceAnalystList={faceAnalystList} />);
}; };
const handleDeleteFile = async (key) => { const handleDeleteFile = async (key) => {

View File

@@ -1,32 +1,66 @@
// DiscussedTopics.js // DiscussedTopics.js
import React from 'react'; import React, { useState } from 'react';
import styles from './DiscussedTopics.module.css'; import styles from './DiscussedTopics.module.css';
const DiscussedTopics = ({ topics }) => { const DiscussedTopics = ({ topics, faceAnalystList }) => {
const [activeTab, setActiveTab] = useState('topics'); // 'topics' or 'face'
return ( return (
<div className={styles.container}> <div className={styles.container}>
<div className={styles.header}> <div className={styles.header}>
<h2 className={styles.title}>Top Topic</h2> <div className={styles.tabButtons}>
<button
className={`${styles.tabButton} ${activeTab === 'topics' ? styles.activeTab : ''}`}
onClick={() => setActiveTab('topics')}
>
Top Topic
</button>
<button
className={`${styles.tabButton} ${activeTab === 'face' ? styles.activeTab : ''}`}
onClick={() => setActiveTab('face')}
>
Face Analyst Report
</button>
</div>
<div className={styles.resultCount}> <div className={styles.resultCount}>
{topics.length} topik {activeTab === 'topics'
? `${topics.length} topik`
: `${faceAnalystList.length} report`}
</div> </div>
</div> </div>
<div className={styles.grid}> <div className={styles.grid}>
{topics.length === 0 ? ( {activeTab === 'topics' ? (
topics.length === 0 ? (
<div className={styles.emptyState}>
<div className={styles.emptyIcon}>💬</div>
<p>Discussed Topic Is Empty</p>
</div>
) : (
topics.map((topic, idx) => (
<div key={idx} className={styles.card}>
<div className={styles.cardContent}>
<h3 className={styles.topicName}>{topic.topic}</h3>
<div className={styles.countBadge}>
<span className={styles.countValue}>{topic.count}</span>
<span className={styles.countLabel}>times</span>
</div>
</div>
</div>
))
)
) : faceAnalystList.length === 0 ? (
<div className={styles.emptyState}> <div className={styles.emptyState}>
<div className={styles.emptyIcon}>💬</div> <div className={styles.emptyIcon}>🧑</div>
<p>Discussed Topic Is Empty</p> <p>No Face Analyst Report</p>
</div> </div>
) : ( ) : (
topics.map((topic, idx) => ( faceAnalystList.map((item, idx) => (
<div key={idx} className={styles.card}> <div key={idx} className={styles.card}>
<div className={styles.cardContent}> <div className={styles.cardContent}>
<h3 className={styles.topicName}>{topic.topic}</h3> <h3 className={styles.topicName}>{item.name}</h3>
<div className={styles.countBadge}> <p className={styles.description}>{item.description}</p>
<span className={styles.countValue}>{topic.count}</span> <p className={styles.phone}>📞 {item.phone_number}</p>
<span className={styles.countLabel}>times</span>
</div>
</div> </div>
</div> </div>
)) ))
@@ -36,4 +70,4 @@ const DiscussedTopics = ({ topics }) => {
); );
}; };
export default DiscussedTopics; export default DiscussedTopics;

View File

@@ -184,4 +184,35 @@
.countLabel { .countLabel {
font-size: 11px; font-size: 11px;
} }
} }
.tabButtons {
display: flex;
gap: 10px;
}
.tabButton {
background-color: #eee;
border: none;
padding: 6px 12px;
border-radius: 8px;
cursor: pointer;
font-weight: 500;
}
.activeTab {
background-color: #007bff;
color: white;
}
.description {
margin-top: 6px;
font-size: 0.95rem;
color: #444;
}
.phone {
margin-top: 4px;
font-size: 0.85rem;
color: #666;
}