11import 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' ;
33import ReactMarkdown from 'react-markdown' ;
44import rehypeSanitize from 'rehype-sanitize' ;
55import { motion , AnimatePresence } from 'framer-motion' ;
@@ -39,6 +39,10 @@ interface ChatSession {
3939
4040const STORAGE_KEY = 'babylon_ai_chat_sessions' ;
4141const 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
4448const 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