@@ -67,6 +67,43 @@ pub enum AgentMessage {
6767 CloudSynthesisComplete ( Result < AtomicNote , CloudError > ) ,
6868}
6969
70+ #[ derive( Debug , Clone , Copy , PartialEq , Eq ) ]
71+ pub enum RuixenState {
72+ Resting , // 😴💤🌙 - Waiting for input, peaceful state
73+ Curious , // 🤨🧠💭 - Analyzing user query, thinking
74+ Working , // 😤💦📝 - Processing complex query, working hard
75+ Searching , // 🔍☁️⚡ - Cloud processing, searching for answers
76+ Celebrating , // 💎🚀🎯 - Successful synthesis, celebration
77+ Confused , // 😅🤦♂️📝 - Error state, but learning from it
78+ }
79+
80+ impl RuixenState {
81+ pub fn emoji_expression ( & self ) -> & ' static str {
82+ match self {
83+ RuixenState :: Resting => "😴 💤 🌙" ,
84+ RuixenState :: Curious => "🤨 🧠 💭" ,
85+ RuixenState :: Working => "😤 💦 📝" ,
86+ RuixenState :: Searching => "🔍 ☁️ ⚡" ,
87+ RuixenState :: Celebrating => "💎 🚀 🎯" ,
88+ RuixenState :: Confused => "😅 🤦♂️ 📝" ,
89+ }
90+ }
91+
92+ pub fn from_agent_status ( status : AgentStatus ) -> Self {
93+ match status {
94+ AgentStatus :: Ready => RuixenState :: Resting ,
95+ AgentStatus :: Orchestrating => RuixenState :: Curious ,
96+ AgentStatus :: Searching => RuixenState :: Searching ,
97+ AgentStatus :: Complete => RuixenState :: Celebrating ,
98+ AgentStatus :: LocalEndpointError | AgentStatus :: CloudEndpointError => {
99+ RuixenState :: Confused
100+ }
101+ AgentStatus :: ValidatingLocal | AgentStatus :: ValidatingCloud => RuixenState :: Working ,
102+ _ => RuixenState :: Resting ,
103+ }
104+ }
105+ }
106+
70107#[ derive( Debug , Clone , Copy , PartialEq , Eq , Default ) ]
71108pub enum SettingsSelection {
72109 #[ default]
@@ -124,11 +161,14 @@ pub struct App {
124161 final_prompt : String ,
125162 cloud_response : Option < AtomicNote > ,
126163 synthesis_scroll : u16 ,
164+ about_scroll : u16 ,
127165 coaching_tip : ( String , String ) ,
128166 local_tokens_used : u32 , // Token count for current local request
129167 cloud_tokens_used : u32 , // Token count for current cloud request
130168 show_autocomplete : bool ,
131169 autocomplete_index : usize ,
170+ ruixen_reaction_state : Option < RuixenState > , // Temporary reaction state
171+ reaction_timer : Option < std:: time:: Instant > , // When reaction started,
132172}
133173
134174impl App {
@@ -157,11 +197,82 @@ impl App {
157197 final_prompt : String :: new ( ) ,
158198 cloud_response : None ,
159199 synthesis_scroll : 0 ,
200+ about_scroll : 0 ,
160201 coaching_tip : ( String :: new ( ) , String :: new ( ) ) ,
161202 local_tokens_used : 0 ,
162203 cloud_tokens_used : 0 ,
163204 show_autocomplete : false ,
164205 autocomplete_index : 0 ,
206+ ruixen_reaction_state : None ,
207+ reaction_timer : None ,
208+ }
209+ }
210+
211+ fn get_current_ruixen_emoji ( & self ) -> & ' static str {
212+ // Check if we have a temporary reaction that should expire
213+ if let ( Some ( reaction) , Some ( timer) ) = ( & self . ruixen_reaction_state , & self . reaction_timer ) {
214+ if timer. elapsed ( ) <= std:: time:: Duration :: from_millis ( 2000 ) {
215+ // 2 second reactions
216+ return reaction. emoji_expression ( ) ;
217+ }
218+ }
219+
220+ // Default to agent status-based emoji
221+ RuixenState :: from_agent_status ( self . agent_status ) . emoji_expression ( )
222+ }
223+
224+ fn set_ruixen_reaction ( & mut self , reaction : RuixenState ) {
225+ self . ruixen_reaction_state = Some ( reaction) ;
226+ self . reaction_timer = Some ( std:: time:: Instant :: now ( ) ) ;
227+ }
228+
229+ fn cleanup_expired_reactions ( & mut self ) {
230+ if let ( Some ( _) , Some ( timer) ) = ( & self . ruixen_reaction_state , & self . reaction_timer ) {
231+ if timer. elapsed ( ) > std:: time:: Duration :: from_millis ( 2000 ) {
232+ self . ruixen_reaction_state = None ;
233+ self . reaction_timer = None ;
234+ }
235+ }
236+ }
237+
238+ fn analyze_query_complexity ( & self , query : & str ) -> RuixenState {
239+ let word_count = query. split_whitespace ( ) . count ( ) ;
240+ let has_questions = query. contains ( '?' ) ;
241+ let has_complex_words = query. split_whitespace ( ) . any ( |word| word. len ( ) > 10 ) ;
242+ let is_philosophical = query. to_lowercase ( ) . contains ( "why" )
243+ || query. to_lowercase ( ) . contains ( "how" )
244+ || query. to_lowercase ( ) . contains ( "what if" ) ;
245+
246+ // Determine Ruixen's initial reaction based on query complexity
247+ if word_count < 5 && !has_questions {
248+ RuixenState :: Curious // 🤨🧠💭 - Simple query, just curious
249+ } else if ( word_count > 15 ) || has_complex_words || is_philosophical {
250+ RuixenState :: Working // 😤💦📝 - Complex query, need to work hard
251+ } else {
252+ RuixenState :: Curious // 🤨🧠💭 - Standard query, thinking
253+ }
254+ }
255+
256+ fn analyze_synthesis_quality ( & self , response : & AtomicNote ) -> RuixenState {
257+ let body_length = response. body_text . len ( ) ;
258+ let has_insights = response. body_text . to_lowercase ( ) . contains ( "insight" )
259+ || response. body_text . to_lowercase ( ) . contains ( "reveals" )
260+ || response. body_text . to_lowercase ( ) . contains ( "understanding" ) ;
261+ let has_technical_terms = response
262+ . body_text
263+ . split_whitespace ( )
264+ . any ( |word| word. len ( ) > 12 || word. contains ( "ology" ) || word. contains ( "tion" ) ) ;
265+ let tag_count = response. header_tags . len ( ) ;
266+
267+ // Determine Ruixen's reaction to the synthesis quality
268+ if body_length > 800 && has_insights && tag_count > 3 {
269+ RuixenState :: Celebrating // 💎🚀🎯 - Excellent synthesis, celebration!
270+ } else if body_length > 400 && ( has_insights || has_technical_terms) {
271+ RuixenState :: Resting // 😴💤🌙 - Good synthesis, satisfied
272+ } else if body_length < 200 {
273+ RuixenState :: Confused // 😅🤦♂️📝 - Short response, maybe didn't work well
274+ } else {
275+ RuixenState :: Curious // 🤨🧠💭 - Decent response, still thinking
165276 }
166277 }
167278
@@ -173,7 +284,7 @@ impl App {
173284 } ;
174285
175286 let block = Block :: default ( )
176- . title ( " Synthesize Knowledge " )
287+ . title ( format ! ( " {} " , self . get_current_ruixen_emoji ( ) ) )
177288 . borders ( Borders :: ALL )
178289 . style ( self . theme . ratatui_style ( Element :: Active ) ) ;
179290
@@ -271,34 +382,45 @@ impl App {
271382 let inner_area = block. inner ( area) ;
272383 frame. render_widget ( block, area) ;
273384
274- // Split area: message + tips
385+ // Split area: message + navigation footer
275386 let chunks = Layout :: default ( )
276387 . direction ( Direction :: Vertical )
277388 . constraints ( [
278389 Constraint :: Min ( 5 ) , // Main message (flexible)
279- Constraint :: Length ( 3 ) , // Tips footer
390+ Constraint :: Length ( 1 ) , // Navigation footer - single line like settings
280391 ] )
281392 . split ( inner_area) ;
282393
283- let message = Paragraph :: new ( message. as_str ( ) )
284- . alignment ( Alignment :: Center )
394+ let mut message = Paragraph :: new ( message. as_str ( ) )
395+ . alignment ( Alignment :: Left ) // Use Left alignment for better scrolling readability
285396 . style ( self . theme . ratatui_style ( Element :: Text ) )
286397 . wrap ( Wrap { trim : true } ) ;
287398
399+ // Apply scrolling only for About pages
400+ if title. contains ( "About RuixenOS" ) {
401+ message = message. scroll ( ( self . about_scroll , 0 ) ) ;
402+ }
403+
288404 frame. render_widget ( message, chunks[ 0 ] ) ;
289405
290- // Navigation footer
291- let footer_text = "Press [ESC] to return." ;
406+ // Navigation footer - show scroll controls for About page
407+ let footer_text = if title. contains ( "About RuixenOS" ) {
408+ "[←] [→] Scroll | [ESC] Return"
409+ } else {
410+ "Press [ESC] to return."
411+ } ;
292412 let footer = Paragraph :: new ( footer_text)
293413 . alignment ( Alignment :: Center )
294- . style ( self . theme . ratatui_style ( Element :: Inactive ) )
295- . wrap ( Wrap { trim : true } ) ;
414+ . style ( self . theme . ratatui_style ( Element :: Inactive ) ) ;
296415
297416 frame. render_widget ( footer, chunks[ 1 ] ) ;
298417 }
299418
300419 pub async fn run ( & mut self , terminal : & mut Terminal < CrosstermBackend < Stdout > > ) -> Result < ( ) > {
301420 while !self . should_quit {
421+ // Clean up expired reactions
422+ self . cleanup_expired_reactions ( ) ;
423+
302424 self . draw ( terminal) ?;
303425
304426 // Handle validation messages from background tasks
@@ -496,7 +618,7 @@ impl App {
496618 ) ;
497619
498620 let block = Block :: default ( )
499- . title ( " Synthesis Complete " )
621+ . title ( format ! ( " {} " , self . get_current_ruixen_emoji ( ) ) )
500622 . borders ( Borders :: ALL )
501623 . style ( self . theme . ratatui_style ( Element :: Active ) ) ;
502624
@@ -519,6 +641,7 @@ impl App {
519641 commands : & self . get_filtered_slash_commands ( ) ,
520642 selected_index : self . autocomplete_index ,
521643 } ,
644+ self . get_current_ruixen_emoji ( ) ,
522645 ) ;
523646 }
524647 } ) ?;
@@ -638,6 +761,10 @@ impl App {
638761 self . agent_status = AgentStatus :: Ready ;
639762 }
640763 AgentMessage :: CloudSynthesisComplete ( Ok ( response) ) => {
764+ // Analyze the synthesis quality and show reaction
765+ let reaction = self . analyze_synthesis_quality ( & response) ;
766+ self . set_ruixen_reaction ( reaction) ;
767+
641768 self . cloud_response = Some ( response) ;
642769 self . mode = AppMode :: Complete ;
643770 self . agent_status = AgentStatus :: Complete ;
@@ -724,7 +851,7 @@ impl App {
724851 // Show About modal - same as /about command
725852 self . coaching_tip = (
726853 "About RuixenOS v0.1.0" . to_string ( ) ,
727- "🎯 The Curiosity Machine\n Transforming queries into thoughtful Ruixen inquiries since 2025.\n Built with Rust, ratatui, and endless wonder." . to_string ( ) ,
854+ "🎯 The Curiosity Machine\n Transforming queries into thoughtful Ruixen inquiries since 2025.\n Built with Rust, ratatui, and endless wonder.\n \n 💝 Builder's Note: \n This app was crafted with constitutional Rust patterns, following the RuixenOS workspace architecture. Every emoji expression, every token counted, every error handled gracefully. It's been an absolute joy building something that turns simple questions into profound explorations. The curiosity machine doesn't just process queries - it awakens wonder. \n \n 🤝 Co-built with love by humans and AI agents working in harmony. " . to_string ( ) ,
728855 ) ;
729856 self . mode = AppMode :: CoachingTip ;
730857 }
@@ -1050,7 +1177,48 @@ impl App {
10501177 _ => { }
10511178 } ,
10521179 AppMode :: CoachingTip => match key. code {
1180+ KeyCode :: Left => {
1181+ // Scroll up through About content (only for About page)
1182+ if self . coaching_tip . 0 . contains ( "About RuixenOS" )
1183+ && self . about_scroll > 0
1184+ {
1185+ self . about_scroll -= 1 ;
1186+ }
1187+ }
1188+ KeyCode :: Right => {
1189+ // Scroll down through About content (only for About page)
1190+ if self . coaching_tip . 0 . contains ( "About RuixenOS" ) {
1191+ // Calculate max scroll based on content length
1192+ let content = & self . coaching_tip . 1 ;
1193+ let approx_usable_width = 50u16 ; // Conservative estimate for modal width
1194+ let approx_display_height = 8u16 ; // Conservative estimate (modal height - borders)
1195+
1196+ let lines: Vec < & str > = content. lines ( ) . collect ( ) ;
1197+ let total_wrapped_lines: u16 = lines
1198+ . iter ( )
1199+ . map ( |line| {
1200+ if line. is_empty ( ) {
1201+ 1 // Empty lines still take space
1202+ } else {
1203+ ( ( line. len ( ) as f32 / approx_usable_width as f32 )
1204+ . ceil ( )
1205+ as u16 )
1206+ . max ( 1 )
1207+ }
1208+ } )
1209+ . sum ( ) ;
1210+
1211+ let max_scroll =
1212+ total_wrapped_lines. saturating_sub ( approx_display_height) ;
1213+
1214+ if max_scroll > 0 && self . about_scroll < max_scroll {
1215+ self . about_scroll += 1 ;
1216+ }
1217+ }
1218+ }
10531219 KeyCode :: Enter | KeyCode :: Esc => {
1220+ // Reset scroll when closing and return to appropriate mode
1221+ self . about_scroll = 0 ;
10541222 // About modal should return to main menu, errors return to chat
10551223 if self . coaching_tip . 0 . contains ( "About RuixenOS" ) {
10561224 self . mode = AppMode :: Normal ;
@@ -1079,6 +1247,10 @@ impl App {
10791247 // Store the original user query for metadata
10801248 self . original_user_query = message. clone ( ) ;
10811249
1250+ // Analyze query complexity and show brief reaction
1251+ let reaction = self . analyze_query_complexity ( & message) ;
1252+ self . set_ruixen_reaction ( reaction) ;
1253+
10821254 // Estimate tokens for local request (rough: chars/4 + prompt overhead)
10831255 self . local_tokens_used = ( message. len ( ) / 4 ) as u32 + 500 ; // ~500 tokens for prompt template
10841256 self . cloud_tokens_used = 0 ; // Reset cloud tokens for new session
0 commit comments