* chore: run benchmarks with reflect mode * chore: run benchmarks with reflect mode * fixes * new mm * bunch of fixes * initial commit * fixes * fixes * fixes * fix: sometimes memories gets extracted in the wrong language
365 lines
11 KiB
Rust
365 lines
11 KiB
Rust
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<char> = 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<String, serde_json::Value>) {
|
|
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<std::sync::atomic::AtomicBool>,
|
|
handle: Option<std::thread::JoinHandle<()>>,
|
|
}
|
|
|
|
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<char> = 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<bool> {
|
|
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!();
|
|
}
|