use crate::api::{BankProfileResponse, RecallResult, RecallResponse, ReflectResponse}; use colored::*; use hindsight_client::types::ChunkData; use indicatif::{ProgressBar, ProgressStyle}; use std::io::{self, Write}; /// The logo as ANSI-colored text, generated by test-logo.py const LOGO: &str = include_str!("logo.ansi"); // Gradient colors: #0074d9 -> #009296 const GRADIENT_START: (u8, u8, u8) = (0, 116, 217); // #0074d9 const GRADIENT_END: (u8, u8, u8) = (0, 146, 150); // #009296 /// Interpolate between two RGB colors fn interpolate_color(start: (u8, u8, u8), end: (u8, u8, u8), t: f32) -> (u8, u8, u8) { ( (start.0 as f32 + (end.0 as f32 - start.0 as f32) * t) as u8, (start.1 as f32 + (end.1 as f32 - start.1 as f32) * t) as u8, (start.2 as f32 + (end.2 as f32 - start.2 as f32) * t) as u8, ) } /// Color text using gradient position (0.0 = start, 1.0 = end) pub fn gradient(text: &str, t: f32) -> String { let (r, g, b) = interpolate_color(GRADIENT_START, GRADIENT_END, t); format!("\x1b[38;2;{};{};{}m{}\x1b[0m", r, g, b, text) } /// Color text with gradient start color (#0074d9) pub fn gradient_start(text: &str) -> String { gradient(text, 0.0) } /// Color text with gradient end color (#009296) pub fn gradient_end(text: &str) -> String { gradient(text, 1.0) } /// Color text with gradient middle color pub fn gradient_mid(text: &str) -> String { gradient(text, 0.5) } /// Apply gradient across entire text string pub fn gradient_text(text: &str) -> String { let chars: Vec = text.chars().collect(); let len = chars.len(); if len == 0 { return String::new(); } let mut result = String::new(); for (i, ch) in chars.iter().enumerate() { if *ch == ' ' { result.push(' '); } else { let t = i as f32 / (len - 1).max(1) as f32; let (r, g, b) = interpolate_color(GRADIENT_START, GRADIENT_END, t); result.push_str(&format!("\x1b[38;2;{};{};{}m{}", r, g, b, ch)); } } result.push_str("\x1b[0m"); result } /// Dim/gray text pub fn dim(text: &str) -> String { format!("\x1b[38;2;128;128;128m{}\x1b[0m", text) } pub fn get_logo() -> &'static str { LOGO } pub fn print_section_header(title: &str) { println!(); println!("{}", gradient_text(&format!("━━━ {} ━━━", title))); println!(); } pub fn print_fact(fact: &RecallResult, _show_activation: bool) { let fact_type = fact.type_.as_deref().unwrap_or("unknown"); // Use gradient positions for different fact types let type_t = match fact_type { "world" => 0.0, "agent" => 0.5, "opinion" => 1.0, _ => 0.5, }; println!("{}", gradient(&format!("[{}]", fact_type.to_uppercase()), type_t)); println!(" {}", fact.text); // Show context if available if let Some(context) = &fact.context { println!(" {} {}", dim("context:"), dim(context)); } // Show temporal information if let Some(occurred_start) = &fact.occurred_start { if let Some(occurred_end) = &fact.occurred_end { println!(" {} {} - {}", dim("date:"), dim(occurred_start), dim(occurred_end)); } else { println!(" {} {}", dim("date:"), dim(occurred_start)); } } // Show document ID if available if let Some(document_id) = &fact.document_id { println!(" {} {}", dim("document:"), dim(document_id)); } println!(); } pub fn print_chunk(chunk: &ChunkData) { println!(" {}", gradient_mid("─── Source Chunk ───")); // Split text into lines and indent each line for line in chunk.text.lines() { println!(" {}", line); } if chunk.truncated { println!(" {}", gradient_end("[Truncated due to token limit]")); } println!(" {} {} | {} {}", dim("Chunk ID:"), dim(&chunk.id), dim("Index:"), dim(&chunk.chunk_index.to_string()) ); println!(); } pub fn print_search_results(response: &RecallResponse, show_trace: bool, show_chunks: bool) { let results = &response.results; print_section_header(&format!("Search Results ({})", results.len())); if results.is_empty() { println!(" {}", dim("No results found.")); } else { for (i, fact) in results.iter().enumerate() { println!(" {}", dim(&format!("Result #{}", i + 1))); print_fact(fact, true); // Show chunk if available and requested if show_chunks { if let Some(chunk_id) = &fact.chunk_id { if let Some(chunks) = &response.chunks { if let Some(chunk) = chunks.get(chunk_id) { print_chunk(chunk); } } } } } } if show_trace { if let Some(trace) = &response.trace { print_trace_info(trace); } } } pub fn print_think_response(response: &ReflectResponse) { print_section_header("Reflection"); println!("{}", response.text); println!(); if let Some(based_on) = &response.based_on { let count = based_on.memories.len(); if count > 0 { println!("{}", dim(&format!("Based on {} memory units", count))); } } // Display structured output if present if let Some(structured) = &response.structured_output { println!(); println!("{}", gradient_text("─── Structured Output ───")); println!(); if let Ok(json) = serde_json::to_string_pretty(structured) { println!("{}", json); } } } pub fn print_trace_info(trace: &serde_json::Map) { print_section_header("Trace"); if let Some(time) = trace.get("total_time").and_then(|v| v.as_f64()) { println!(" {} {}", dim("total time:"), gradient_start(&format!("{:.2}ms", time))); } if let Some(count) = trace.get("activation_count").and_then(|v| v.as_i64()) { println!(" {} {}", dim("activation count:"), gradient_end(&count.to_string())); } println!(); } pub fn print_success(message: &str) { println!("{}", gradient_start(message)); } pub fn print_error(message: &str) { eprintln!("{} {}", "error:".bright_red().bold(), message.bright_red()); } pub fn print_warning(message: &str) { println!("{} {}", gradient_end("warning:"), message); } pub fn print_info(message: &str) { println!("{}", gradient_start(message)); } /// Animated gradient spinner that shows text with moving gradient colors pub struct GradientSpinner { message: String, running: std::sync::Arc, handle: Option>, } impl GradientSpinner { pub fn new(message: &str) -> Self { let message = message.to_string(); let running = std::sync::Arc::new(std::sync::atomic::AtomicBool::new(true)); let msg_clone = message.clone(); let running_clone = running.clone(); let handle = std::thread::spawn(move || { let chars: Vec = msg_clone.chars().collect(); let len = chars.len(); let num_frames = 30; let mut current_frame = 0usize; while running_clone.load(std::sync::atomic::Ordering::Relaxed) { current_frame = (current_frame + 1) % num_frames; let offset = current_frame as f32 / num_frames as f32; // Build the gradient string let mut result = String::from("\r"); for (i, ch) in chars.iter().enumerate() { if *ch == ' ' { result.push(' '); } else { let base_t = if len > 1 { i as f32 / (len - 1) as f32 } else { 0.0 }; let t = (base_t + offset) % 1.0; let (r, g, b) = interpolate_color(GRADIENT_START, GRADIENT_END, t); result.push_str(&format!("\x1b[38;2;{};{};{}m{}", r, g, b, ch)); } } result.push_str("\x1b[0m"); print!("{}", result); let _ = io::stdout().flush(); std::thread::sleep(std::time::Duration::from_millis(80)); } }); Self { message, running, handle: Some(handle), } } pub fn finish(&mut self) { self.running.store(false, std::sync::atomic::Ordering::Relaxed); if let Some(handle) = self.handle.take() { let _ = handle.join(); } // Clear the line print!("\r{}\r", " ".repeat(self.message.len() + 10)); let _ = io::stdout().flush(); } } impl Drop for GradientSpinner { fn drop(&mut self) { if self.running.load(std::sync::atomic::Ordering::Relaxed) { self.finish(); } } } pub fn create_spinner(message: &str) -> GradientSpinner { GradientSpinner::new(message) } pub fn create_progress_bar(total: u64, message: &str) -> ProgressBar { let pb = ProgressBar::new(total); pb.set_style( ProgressStyle::default_bar() .template("{msg} [{bar:40.cyan/blue}] {pos}/{len} ({percent}%)") .unwrap() .progress_chars("█▓▒░ "), ); pb.set_message(message.to_string()); pb } pub fn prompt_confirmation(message: &str) -> io::Result { print!("{} [y/N]: ", gradient_start(message)); io::stdout().flush()?; let mut input = String::new(); io::stdin().read_line(&mut input)?; Ok(input.trim().eq_ignore_ascii_case("y") || input.trim().eq_ignore_ascii_case("yes")) } pub fn print_disposition(profile: &BankProfileResponse) { print_section_header(&format!("Disposition: {}", profile.bank_id)); // Print name println!("{} {}", dim("Name:"), gradient_start(&profile.name)); println!(); // Print mission if available if !profile.mission.is_empty() { println!("{}", gradient_mid("Mission:")); for line in profile.mission.lines() { println!("{}", line); } println!(); } // Print disposition traits println!("{}", gradient_text("─── Disposition Traits ───")); println!(); // New 3-trait disposition system (values 1-5) let traits: [(_, i64, f32, _); 3] = [ ("Skepticism", profile.disposition.skepticism.get() as i64, 0.0, "1=trusting, 5=skeptical"), ("Literalism", profile.disposition.literalism.get() as i64, 0.5, "1=flexible, 5=literal"), ("Empathy", profile.disposition.empathy.get() as i64, 1.0, "1=detached, 5=empathetic"), ]; for (name, value, t, desc) in &traits { // Scale 1-5 to bar visualization (each point = 8 chars, total 40) let bar_length = 40; let filled = ((*value - 1) * 10) as usize; // 1->0, 2->10, 3->20, 4->30, 5->40 let empty = bar_length - filled; let bar = format!("{}{}", "█".repeat(filled), "░".repeat(empty)); println!(" {:<12} [{}] {}/5", name, gradient(&bar, *t), value ); println!(" {}", dim(desc)); } println!(); }