@@ -101,12 +101,78 @@ const MESSAGE_TIMINGS = {
101101 DEMO_ANIMATION_STEP : 500 , // Time between demo animation steps
102102} as const ;
103103
104+ // Sleep persistence configuration
105+ const SLEEP_CONFIG = {
106+ STORAGE_KEY : 'thinkred_assistant_sleep_state' ,
107+ DEFAULT_SLEEP_DURATION : 10 * 60 * 1000 , // 10 minutes in milliseconds
108+ MIN_SLEEP_DURATION : 5 * 60 * 1000 , // 5 minutes minimum
109+ MAX_SLEEP_DURATION : 30 * 60 * 1000 , // 30 minutes maximum
110+ QUICK_SLEEP_DURATION : 5 * 60 * 1000 , // 5 minutes for quick sleep
111+ LONG_SLEEP_DURATION : 20 * 60 * 1000 , // 20 minutes for long sleep
112+ } as const ;
113+
114+ // Sleep state interface
115+ interface SleepState {
116+ isAsleep : boolean ;
117+ sleepStartTime : number ;
118+ sleepDuration : number ;
119+ reason : 'manual' | 'automatic' ;
120+ }
121+
122+ // Sleep storage utilities
123+ const sleepStorage = {
124+ set : ( state : SleepState ) : void => {
125+ try {
126+ localStorage . setItem ( SLEEP_CONFIG . STORAGE_KEY , JSON . stringify ( state ) ) ;
127+ } catch {
128+ // Silently fail if localStorage is not available
129+ }
130+ } ,
131+
132+ get : ( ) : SleepState | null => {
133+ try {
134+ const storedState = localStorage . getItem ( SLEEP_CONFIG . STORAGE_KEY ) ;
135+ if ( ! storedState ) return null ;
136+
137+ const state = JSON . parse ( storedState ) as SleepState ;
138+
139+ // Validate the stored state structure
140+ if (
141+ typeof state . isAsleep !== 'boolean' ||
142+ typeof state . sleepStartTime !== 'number' ||
143+ typeof state . sleepDuration !== 'number'
144+ ) {
145+ sleepStorage . clear ( ) ;
146+ return null ;
147+ }
148+
149+ return state ;
150+ } catch {
151+ sleepStorage . clear ( ) ;
152+ return null ;
153+ }
154+ } ,
155+
156+ clear : ( ) : void => {
157+ try {
158+ localStorage . removeItem ( SLEEP_CONFIG . STORAGE_KEY ) ;
159+ } catch {
160+ // Silently fail if localStorage is not available
161+ }
162+ } ,
163+
164+ isExpired : ( state : SleepState ) : boolean => {
165+ return Date . now ( ) > state . sleepStartTime + state . sleepDuration ;
166+ } ,
167+ } ;
168+
104169const AvatarAssistant = ( ) => {
105170 const navigate = useNavigate ( ) ;
106171 const location = useLocation ( ) ;
107172 const [ isVisible , setIsVisible ] = useState ( true ) ;
108173 const [ isSleeping , setIsSleeping ] = useState ( false ) ;
109174 const [ isManualSleep , setIsManualSleep ] = useState ( false ) ;
175+ const [ persistentSleep , setPersistentSleep ] = useState < SleepState | null > ( null ) ;
110176 const [ isGoingToSleep , setIsGoingToSleep ] = useState ( false ) ;
111177 const [ isWakingUp , setIsWakingUp ] = useState ( false ) ;
112178 const [ justWokeUp , setJustWokeUp ] = useState ( false ) ;
@@ -164,6 +230,7 @@ const AvatarAssistant = () => {
164230 const messageRef = useRef < HTMLDivElement > ( null ) ;
165231 const hasBeenRenderedRef = useRef ( false ) ;
166232 const isUnmountedRef = useRef ( false ) ; // Track if component is unmounted
233+ const [ sleepTimeoutId , setSleepTimeoutId ] = useState < NodeJS . Timeout | null > ( null ) ;
167234
168235 // Enhanced cleanup function
169236 const cleanupTimers = useCallback ( ( ) => {
@@ -186,6 +253,48 @@ const AvatarAssistant = () => {
186253 } ;
187254 } , [ cleanupTimers ] ) ;
188255
256+ // Initialize persistent sleep state on component mount
257+ useEffect ( ( ) => {
258+ const storedSleepState = sleepStorage . get ( ) ;
259+
260+ if ( storedSleepState && storedSleepState . isAsleep ) {
261+ if ( sleepStorage . isExpired ( storedSleepState ) ) {
262+ // Sleep duration has expired, clear storage and wake up
263+ sleepStorage . clear ( ) ;
264+ setPersistentSleep ( null ) ;
265+ } else {
266+ // Still within sleep duration, restore sleep state
267+ setPersistentSleep ( storedSleepState ) ;
268+ setIsSleeping ( true ) ;
269+ setIsManualSleep ( storedSleepState . reason === 'manual' ) ;
270+ setIsVisible ( false ) ;
271+
272+ // Set timeout for remaining sleep duration
273+ const remainingTime = storedSleepState . sleepStartTime + storedSleepState . sleepDuration - Date . now ( ) ;
274+ const timeoutId = setTimeout ( ( ) => {
275+ // Auto wake up when sleep duration expires
276+ sleepStorage . clear ( ) ;
277+ setPersistentSleep ( null ) ;
278+ // Wake up by resetting sleep states
279+ setIsSleeping ( false ) ;
280+ setIsManualSleep ( false ) ;
281+ setIsVisible ( true ) ;
282+ } , remainingTime ) ;
283+
284+ setSleepTimeoutId ( timeoutId ) ;
285+ }
286+ }
287+ } , [ ] ) ;
288+
289+ // Clean up sleep timeout on unmount
290+ useEffect ( ( ) => {
291+ return ( ) => {
292+ if ( sleepTimeoutId ) {
293+ clearTimeout ( sleepTimeoutId ) ;
294+ }
295+ } ;
296+ } , [ sleepTimeoutId ] ) ;
297+
189298 // Function to change message with shrink-then-grow animation - Enhanced with error handling
190299 const changeMessageWithAnimation = useCallback (
191300 ( newMessage : string ) => {
@@ -689,7 +798,7 @@ const AvatarAssistant = () => {
689798 } , 150 ) ;
690799 } , 100 ) ; // Small debounce to prevent rapid state changes
691800 } else if ( scrollY <= sleepScrollDistance && ( isSleeping || isGoingToSleep ) && ! isManualSleep && ! isWakingUp ) {
692- // Debounce the wake transition
801+ // Debounce the wake up transition
693802 scrollTimeout = setTimeout ( ( ) => {
694803 if ( isUnmountedRef . current ) return ;
695804 setIsGoingToSleep ( false ) ; // Reset transition state
@@ -834,6 +943,35 @@ const AvatarAssistant = () => {
834943 showPageWelcome ( ) ;
835944 } , [ location . pathname , isVisible , isSleeping , changeMessageWithAnimation ] ) ;
836945
946+ // Function to determine appropriate sleep duration based on user behavior
947+ const getAdaptiveSleepDuration = ( ) : number => {
948+ // If user has been very active, use shorter sleep duration
949+ if ( userInteractionCount > 10 ) {
950+ return SLEEP_CONFIG . QUICK_SLEEP_DURATION ;
951+ }
952+
953+ // If user has been idle for a long time, use longer sleep duration
954+ const timeSinceLastInteraction = Date . now ( ) - lastInteractionTime ;
955+ if ( timeSinceLastInteraction > 5 * 60 * 1000 ) {
956+ // 5 minutes of inactivity
957+ return SLEEP_CONFIG . LONG_SLEEP_DURATION ;
958+ }
959+
960+ // Default duration for most cases
961+ return SLEEP_CONFIG . DEFAULT_SLEEP_DURATION ;
962+ } ;
963+
964+ // Function to get remaining sleep time for display
965+ const getRemainingSleepaTime = ( ) => {
966+ if ( ! persistentSleep ) return null ;
967+
968+ const remaining = persistentSleep . sleepStartTime + persistentSleep . sleepDuration - Date . now ( ) ;
969+ if ( remaining <= 0 ) return null ;
970+
971+ const minutes = Math . ceil ( remaining / ( 60 * 1000 ) ) ;
972+ return minutes ;
973+ } ;
974+
837975 // Unified function to get avatar animation classes - prevents conflicts
838976 const getAvatarAnimationClass = ( ) => {
839977 // Priority order to prevent conflicts (highest to lowest priority):
@@ -1249,6 +1387,12 @@ const AvatarAssistant = () => {
12491387
12501388 // Put assistant to sleep (user-initiated hide)
12511389 const putAssistantToSleep = ( ) => {
1390+ // Clear any existing sleep timeout
1391+ if ( sleepTimeoutId ) {
1392+ clearTimeout ( sleepTimeoutId ) ;
1393+ setSleepTimeoutId ( null ) ;
1394+ }
1395+
12521396 // Clear any pending timeouts that might interfere first
12531397 if ( timeoutRef . current ) {
12541398 clearTimeout ( timeoutRef . current ) ;
@@ -1257,6 +1401,28 @@ const AvatarAssistant = () => {
12571401 clearInterval ( intervalRef . current ) ;
12581402 }
12591403
1404+ // Create persistent sleep state with adaptive duration
1405+ const sleepState : SleepState = {
1406+ isAsleep : true ,
1407+ sleepStartTime : Date . now ( ) ,
1408+ sleepDuration : getAdaptiveSleepDuration ( ) ,
1409+ reason : 'manual' ,
1410+ } ;
1411+
1412+ // Store sleep state in localStorage
1413+ sleepStorage . set ( sleepState ) ;
1414+ setPersistentSleep ( sleepState ) ;
1415+
1416+ // Set timeout to automatically wake up after sleep duration
1417+ const timeoutId = setTimeout ( ( ) => {
1418+ sleepStorage . clear ( ) ;
1419+ setPersistentSleep ( null ) ;
1420+ // Auto wake up by calling the wake up function
1421+ wakeUpAssistant ( ) ;
1422+ } , sleepState . sleepDuration ) ;
1423+
1424+ setSleepTimeoutId ( timeoutId ) ;
1425+
12601426 // Step 1: Close message bubble and start transition simultaneously
12611427 setIsExpanded ( false ) ;
12621428 setShowContextualOptions ( false ) ;
@@ -1281,6 +1447,16 @@ const AvatarAssistant = () => {
12811447
12821448 // Wake up assistant (user-initiated show)
12831449 const wakeUpAssistant = ( ) => {
1450+ // Clear any existing sleep timeout
1451+ if ( sleepTimeoutId ) {
1452+ clearTimeout ( sleepTimeoutId ) ;
1453+ setSleepTimeoutId ( null ) ;
1454+ }
1455+
1456+ // Clear persistent sleep state
1457+ sleepStorage . clear ( ) ;
1458+ setPersistentSleep ( null ) ;
1459+
12841460 // Track user interaction
12851461 setUserInteractionCount ( prev => prev + 1 ) ;
12861462 setLastInteractionTime ( Date . now ( ) ) ;
@@ -1371,7 +1547,7 @@ const AvatarAssistant = () => {
13711547 < div
13721548 className = "sleeping-avatar-size cursor-pointer transition-opacity duration-300 ease-in-out pointer-events-auto"
13731549 onClick = { wakeUpAssistant }
1374- title = " Click to wake up your assistant"
1550+ title = { ` Click to wake up your assistant${ getRemainingSleepaTime ( ) ? ` ( ${ getRemainingSleepaTime ( ) } min remaining)` : '' } ` }
13751551 >
13761552 < img
13771553 src = "/assets/avatars/assistant-red-sleeping.png"
0 commit comments