Skip to content

Commit 677ca84

Browse files
Merge pull request #54 from gitcoder89431/core-ruixen-01
Core ruixen 01
2 parents 2e5e367 + 8a7ab0f commit 677ca84

File tree

4 files changed

+219
-37
lines changed

4 files changed

+219
-37
lines changed

.idea/agentic.iml

Lines changed: 3 additions & 0 deletions
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

.idea/workspace.xml

Lines changed: 30 additions & 25 deletions
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

crates/agentic-tui/src/ui/app.rs

Lines changed: 183 additions & 11 deletions
Original file line numberDiff line numberDiff line change
@@ -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)]
71108
pub 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

134174
impl 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\nTransforming queries into thoughtful Ruixen inquiries since 2025.\nBuilt with Rust, ratatui, and endless wonder.".to_string(),
854+
"🎯 The Curiosity Machine\nTransforming queries into thoughtful Ruixen inquiries since 2025.\nBuilt with Rust, ratatui, and endless wonder.\n\n💝 Builder's Note:\nThis 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

Comments
 (0)