Skip to content

Commit 2b2efd3

Browse files
kkkk666web3jenks
andauthored
feat(chat): implement privacy consent screen and related functionality (#405)
* feat(chat): implement privacy consent screen and related functionality - Added a privacy consent screen to the ChatWidget, requiring user agreement before accessing chat features. - Introduced state management for user consent, storing consent status in local storage. - Updated UI components to display consent options and handle user interactions for agreeing or declining consent. - Enhanced styling for the consent screen and buttons to improve user experience. This update ensures compliance with privacy standards and enhances user awareness regarding data handling. * update text '2 months' to 'two months'. --------- Co-authored-by: Jenks <jenks@babylonlabs.io>
1 parent 14e3903 commit 2b2efd3

2 files changed

Lines changed: 168 additions & 72 deletions

File tree

src/components/ChatWidget.css

Lines changed: 35 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -226,6 +226,41 @@
226226
box-shadow: 0 0 0 2px rgba(52, 143, 148, 0.2) !important; /* Subtle focus ring */
227227
}
228228

229+
/* Privacy Consent Screen */
230+
.chat-consent {
231+
text-align: center;
232+
}
233+
234+
.consent-icon-wrapper {
235+
display: flex;
236+
align-items: center;
237+
justify-content: center;
238+
width: 64px;
239+
height: 64px;
240+
border-radius: 50%;
241+
background: var(--ifm-color-emphasis-100);
242+
}
243+
244+
.consent-text-box {
245+
max-height: 240px;
246+
overflow-y: auto;
247+
scrollbar-width: thin;
248+
text-align: left;
249+
}
250+
251+
.consent-btn {
252+
cursor: pointer;
253+
transition: all 0.2s ease;
254+
}
255+
256+
.consent-btn-agree:hover {
257+
box-shadow: 0 2px 8px rgba(0, 0, 0, 0.15);
258+
}
259+
260+
.consent-btn-decline:hover {
261+
background: var(--ifm-color-emphasis-100);
262+
}
263+
229264
/* Markdown Styles Override inside Chat */
230265
.chat-messages .markdown-body p {
231266
margin-bottom: 0.5em;

src/components/ChatWidget.tsx

Lines changed: 133 additions & 72 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,5 @@
11
import React, { useState, useRef, useEffect } from 'react';
2-
import { MessageCircle, X, Send, Loader2, User, Bot, Plus, MessageSquare, Pencil, Check, Trash2, Minimize2, Maximize2 } from 'lucide-react';
2+
import { MessageCircle, X, Send, Loader2, User, Bot, Plus, MessageSquare, Pencil, Check, Trash2, Minimize2, Maximize2, ShieldCheck } from 'lucide-react';
33
import ReactMarkdown from 'react-markdown';
44
import rehypeSanitize from 'rehype-sanitize';
55
import { motion, AnimatePresence } from 'framer-motion';
@@ -39,6 +39,10 @@ interface ChatSession {
3939

4040
const STORAGE_KEY = 'babylon_ai_chat_sessions';
4141
const OLD_STORAGE_KEY = 'babylon_ai_chat_history'; // For migration
42+
const CONSENT_KEY = 'babylon_ai_chat_consent';
43+
44+
const PRIVACY_CONSENT_TEXT =
45+
'This chatbot is intended for technical and informational purposes only. Please do not provide any personal data, including information that can directly or indirectly identify an individual. Your chat history may be used for improving the bot\'s responses and will be permanently deleted after two months.';
4246

4347
// Helper to generate UUID using cryptographically secure random
4448
const generateUUID = () => {
@@ -65,7 +69,12 @@ export default function ChatWidget() {
6569
const [isExpanded, setIsExpanded] = useState(false);
6670
const [hasUserToggledExpand, setHasUserToggledExpand] = useState(false);
6771
const [isApiHealthy, setIsApiHealthy] = useState<boolean>(false);
68-
// Removed showSidebar state as it's now strictly tied to isExpanded
72+
const [hasConsented, setHasConsented] = useState<boolean>(() => {
73+
if (typeof window !== 'undefined') {
74+
return localStorage.getItem(CONSENT_KEY) === 'true';
75+
}
76+
return false;
77+
});
6978

7079
// State for sessions
7180
const [sessions, setSessions] = useState<ChatSession[]>(() => {
@@ -490,6 +499,20 @@ export default function ChatWidget() {
490499
}
491500
};
492501

502+
const handleConsent = () => {
503+
setHasConsented(true);
504+
if (typeof window !== 'undefined') {
505+
localStorage.setItem(CONSENT_KEY, 'true');
506+
}
507+
};
508+
509+
const handleDeclineConsent = () => {
510+
// Close the chat widget when the user declines
511+
setIsOpen(false);
512+
setIsExpanded(false);
513+
setHasUserToggledExpand(false);
514+
};
515+
493516
const handleClose = () => {
494517
abortControllerRef.current?.abort();
495518
setIsOpen(false);
@@ -529,8 +552,8 @@ export default function ChatWidget() {
529552
style={isExpanded ? { position: 'fixed' } : {}}
530553
>
531554
<div className="flex h-full w-full overflow-hidden">
532-
{/* Sidebar - ONLY shown when expanded */}
533-
{isExpanded && (
555+
{/* Sidebar - ONLY shown when expanded and consented */}
556+
{isExpanded && hasConsented && (
534557
<div className="chat-sidebar">
535558
<div className="p-3 border-b border-[var(--ifm-color-emphasis-200)] flex justify-between items-center bg-[var(--ifm-background-surface-color)]">
536559
<button
@@ -608,7 +631,12 @@ export default function ChatWidget() {
608631
<div className="flex-1 flex flex-col h-full relative bg-[var(--ifm-background-surface-color)]">
609632
{/* Header */}
610633
<div className="chat-header flex justify-between items-center p-4">
611-
{editingSessionId === currentSession.id ? (
634+
{!hasConsented ? (
635+
<div className="flex items-center gap-2 font-semibold text-[var(--ifm-color-content)]">
636+
<ShieldCheck className="w-5 h-5 text-[var(--ifm-color-primary)] shrink-0" />
637+
<span>Privacy Notice</span>
638+
</div>
639+
) : editingSessionId === currentSession.id ? (
612640
<div className={`flex items-center gap-2 flex-1 mr-2 ${isExpanded ? 'max-w-[280px]' : 'max-w-[160px]'}`} onClick={e => e.stopPropagation()}>
613641
<Bot className="w-5 h-5 text-[var(--ifm-color-primary)] shrink-0" />
614642
<input
@@ -650,17 +678,19 @@ export default function ChatWidget() {
650678
</div>
651679
)}
652680
<div className="flex items-center gap-2 shrink-0">
653-
<button
654-
onClick={() => {
655-
setHasUserToggledExpand(true);
656-
setIsExpanded(prev => !prev);
657-
}}
658-
title={isExpanded ? "Minimize" : "Expand"}
659-
className="header-control-btn"
660-
aria-label={isExpanded ? "Minimize chat" : "Expand chat"}
661-
>
662-
{isExpanded ? <Minimize2 className="w-4 h-4" /> : <Maximize2 className="w-4 h-4" />}
663-
</button>
681+
{hasConsented && (
682+
<button
683+
onClick={() => {
684+
setHasUserToggledExpand(true);
685+
setIsExpanded(prev => !prev);
686+
}}
687+
title={isExpanded ? "Minimize" : "Expand"}
688+
className="header-control-btn"
689+
aria-label={isExpanded ? "Minimize chat" : "Expand chat"}
690+
>
691+
{isExpanded ? <Minimize2 className="w-4 h-4" /> : <Maximize2 className="w-4 h-4" />}
692+
</button>
693+
)}
664694
<button
665695
onClick={handleClose}
666696
className="header-control-btn chat-close-btn"
@@ -672,65 +702,96 @@ export default function ChatWidget() {
672702
</div>
673703
</div>
674704

675-
{/* Messages */}
676-
<div className="chat-messages flex-1 p-4 overflow-y-auto bg-[var(--ifm-background-color)]">
677-
{messages.map((msg) => (
678-
<div
679-
key={msg.id}
680-
className={`flex gap-3 mb-4 ${msg.role === 'user' ? 'flex-row-reverse' : ''}`}
681-
>
682-
<div className={`w-8 h-8 rounded-full flex items-center justify-center shrink-0 ${
683-
msg.role === 'user'
684-
? 'bg-[var(--ifm-color-primary)] text-white'
685-
: 'bg-[var(--ifm-color-emphasis-200)] text-[var(--ifm-color-content)]'
686-
}`}>
687-
{msg.role === 'user' ? <User className="w-5 h-5" /> : <Bot className="w-5 h-5" />}
688-
</div>
689-
<div className={`max-w-[80%] rounded-2xl p-3 text-sm ${
690-
msg.role === 'user'
691-
? 'message-bubble-user'
692-
: 'message-bubble-ai'
693-
}`}>
694-
<div className="markdown-body">
695-
{msg.role === 'assistant' && msg.content === '' && isLoading ? (
696-
<Loader2 className="w-5 h-5 animate-spin opacity-60" />
697-
) : (
698-
<ReactMarkdown rehypePlugins={[rehypeSanitize]}>{msg.content}</ReactMarkdown>
699-
)}
705+
{/* Privacy Consent Screen */}
706+
{!hasConsented ? (
707+
<div className="chat-consent flex-1 flex flex-col items-center justify-center p-6 bg-[var(--ifm-background-color)]">
708+
<div className="consent-icon-wrapper mb-4">
709+
<ShieldCheck className="w-12 h-12 text-[var(--ifm-color-primary)]" />
710+
</div>
711+
<h3 className="text-base font-semibold text-[var(--ifm-color-content)] mb-3 text-center">
712+
Before You Begin
713+
</h3>
714+
<div className="consent-text-box rounded-lg p-4 mb-6 text-sm leading-relaxed text-[var(--ifm-color-content-secondary)] bg-[var(--ifm-color-emphasis-100)] border border-[var(--ifm-color-emphasis-200)]">
715+
{PRIVACY_CONSENT_TEXT}
716+
</div>
717+
<div className="flex gap-3 w-full">
718+
<button
719+
onClick={handleDeclineConsent}
720+
className="consent-btn consent-btn-decline flex-1 px-4 py-2.5 rounded-lg text-sm font-medium border border-[var(--ifm-color-emphasis-300)] text-[var(--ifm-color-content-secondary)] bg-transparent hover:bg-[var(--ifm-color-emphasis-100)] transition-colors"
721+
>
722+
Decline
723+
</button>
724+
<button
725+
onClick={handleConsent}
726+
className="consent-btn consent-btn-agree flex-1 px-4 py-2.5 rounded-lg text-sm font-medium bg-[var(--ifm-color-primary)] text-white hover:opacity-90 transition-opacity border-none"
727+
>
728+
I Agree
729+
</button>
730+
</div>
731+
</div>
732+
) : (
733+
<>
734+
{/* Messages */}
735+
<div className="chat-messages flex-1 p-4 overflow-y-auto bg-[var(--ifm-background-color)]">
736+
{messages.map((msg) => (
737+
<div
738+
key={msg.id}
739+
className={`flex gap-3 mb-4 ${msg.role === 'user' ? 'flex-row-reverse' : ''}`}
740+
>
741+
<div className={`w-8 h-8 rounded-full flex items-center justify-center shrink-0 ${
742+
msg.role === 'user'
743+
? 'bg-[var(--ifm-color-primary)] text-white'
744+
: 'bg-[var(--ifm-color-emphasis-200)] text-[var(--ifm-color-content)]'
745+
}`}>
746+
{msg.role === 'user' ? <User className="w-5 h-5" /> : <Bot className="w-5 h-5" />}
747+
</div>
748+
<div className={`max-w-[80%] rounded-2xl p-3 text-sm ${
749+
msg.role === 'user'
750+
? 'message-bubble-user'
751+
: 'message-bubble-ai'
752+
}`}>
753+
<div className="markdown-body">
754+
{msg.role === 'assistant' && msg.content === '' && isLoading ? (
755+
<Loader2 className="w-5 h-5 animate-spin opacity-60" />
756+
) : (
757+
<ReactMarkdown rehypePlugins={[rehypeSanitize]}>{msg.content}</ReactMarkdown>
758+
)}
759+
</div>
760+
</div>
700761
</div>
701-
</div>
762+
))}
763+
<div ref={messagesEndRef} />
702764
</div>
703-
))}
704-
<div ref={messagesEndRef} />
705-
</div>
706765

707-
{/* Input */}
708-
<form onSubmit={handleSubmit} className="chat-input p-4 flex gap-2">
709-
<div className="flex-1 flex flex-col gap-1">
710-
<input
711-
type="text"
712-
value={input}
713-
onChange={handleInputChange}
714-
placeholder="Ask a question..."
715-
className="flex-1 px-4 py-2 rounded-full"
716-
disabled={isLoading}
717-
/>
718-
{inputError && (
719-
<p className={`text-xs px-4 ${
720-
isInputTooLong ? 'text-red-500' : 'text-yellow-600'
721-
}`}>
722-
{inputError}
723-
</p>
724-
)}
725-
</div>
726-
<button
727-
type="submit"
728-
disabled={isLoading || !input.trim() || isInputTooLong}
729-
className="w-10 h-10 min-w-[40px] min-h-[40px] bg-[var(--ifm-color-primary)] text-white rounded-full disabled:opacity-50 hover:opacity-90 transition-opacity flex items-center justify-center shrink-0"
730-
>
731-
<Send className="w-5 h-5" />
732-
</button>
733-
</form>
766+
{/* Input */}
767+
<form onSubmit={handleSubmit} className="chat-input p-4 flex gap-2">
768+
<div className="flex-1 flex flex-col gap-1">
769+
<input
770+
type="text"
771+
value={input}
772+
onChange={handleInputChange}
773+
placeholder="Ask a question..."
774+
className="flex-1 px-4 py-2 rounded-full"
775+
disabled={isLoading}
776+
/>
777+
{inputError && (
778+
<p className={`text-xs px-4 ${
779+
isInputTooLong ? 'text-red-500' : 'text-yellow-600'
780+
}`}>
781+
{inputError}
782+
</p>
783+
)}
784+
</div>
785+
<button
786+
type="submit"
787+
disabled={isLoading || !input.trim() || isInputTooLong}
788+
className="w-10 h-10 min-w-[40px] min-h-[40px] bg-[var(--ifm-color-primary)] text-white rounded-full disabled:opacity-50 hover:opacity-90 transition-opacity flex items-center justify-center shrink-0"
789+
>
790+
<Send className="w-5 h-5" />
791+
</button>
792+
</form>
793+
</>
794+
)}
734795
</div>
735796
</div>
736797
</motion.div>

0 commit comments

Comments
 (0)