use ratatui::{
Frame,
layout::Rect,
style::{Color, Modifier, Style},
text::{Line, Span},
widgets::{Block, Borders, Paragraph},
};
use crate::models::{ModelState, strip_gguf};
use crate::tui::app::{App, LoadingPhase};
use crate::tui::format_size;
pub fn render(f: &mut Frame, area: Rect, app: &mut App) {
let mut title_spans = if app.is_panel_visible(4) {
vec![Span::raw(" Active Model (F5) ")]
} else {
vec![Span::raw(" Active Model(s) (F5) ")]
};
if app.metrics.total_vram_used > 0 {
title_spans.push(Span::styled("[ ", Style::default().fg(Color::White)));
title_spans.push(Span::styled(
"Total VRAM: ",
Style::default().fg(Color::Yellow),
));
title_spans.push(Span::styled(
format_size(app.metrics.total_vram_used),
Style::default().fg(Color::Cyan),
));
title_spans.push(Span::styled(" / ", Style::default().fg(Color::White)));
title_spans.push(Span::styled(
format_size(app.metrics.gpu_mem_total),
Style::default().fg(Color::Cyan),
));
title_spans.push(Span::styled(" ]", Style::default().fg(Color::White)));
}
let block = Block::default()
.title(Line::from(title_spans))
.borders(Borders::ALL)
.border_style(Style::default().fg(
if app.ui.active_panel == crate::tui::app::ActivePanel::ActiveModel {
Color::Green
} else {
Color::DarkGray
},
));
let mut lines = Vec::new();
let mut loaded_models = Vec::new();
for (name, state) in &app.model_states {
if !matches!(state, ModelState::Available) {
loaded_models.push((name.clone(), state.clone()));
}
}
if loaded_models.is_empty()
&& let Some(m) = app.selected_model()
&& let Some(state) = app.model_states.get(&m.display_name)
&& !matches!(state, ModelState::Available) {
loaded_models.push((m.display_name.clone(), state.clone()));
}
if app.bench_tune.bench_tune_running {
let display_name = if let Some(m) = app.selected_model() {
strip_gguf(&m.name).to_string()
} else {
"Benchmarking".to_string()
};
lines.push(Line::from(vec![
Span::styled(" Model: ", Style::default().fg(Color::Yellow)),
Span::styled(
display_name,
Style::default()
.fg(Color::White)
.add_modifier(Modifier::BOLD),
),
]));
lines.push(Line::from(vec![
Span::styled(" Status: ", Style::default().fg(Color::Yellow)),
Span::styled(
"BENCHMARKING",
Style::default()
.fg(Color::Yellow)
.add_modifier(Modifier::BOLD),
),
]));
if let Some(progress) = &app.bench_tune.bench_tune_progress {
match progress {
crate::models::BenchTuneProgress::Running {
current,
total,
progress: p,
current_params,
} => {
let label = " Progress: ";
let overhead = label.len() + 2 + 6;
let bar_width = area.width.saturating_sub(overhead as u16 + 2) as usize;
let filled = (*p as f64 / 100.0 * bar_width as f64) as usize;
let bar = format!(
"[{}{}] {:.0}%",
"â–ˆ".repeat(filled),
"â–‘".repeat(bar_width.saturating_sub(filled)),
p
);
lines.push(Line::from(vec![
Span::styled(label, Style::default().fg(Color::Yellow)),
Span::styled(bar, Style::default().fg(Color::Yellow)),
]));
lines.push(Line::from(vec![
Span::styled(" Test: ", Style::default().fg(Color::Yellow)),
Span::styled(
format!("{}/{}", current, total),
Style::default().fg(Color::White),
),
]));
let p_str = crate::tui::format_bench_params(current_params, false).join(", ");
lines.push(Line::from(vec![
Span::styled(" Current: ", Style::default().fg(Color::Yellow)),
Span::styled(p_str, Style::default().fg(Color::Cyan)),
]));
}
crate::models::BenchTuneProgress::Completed {
total_tests,
successful_tests,
elapsed,
} => {
let elapsed_str = format!("{}s", elapsed.as_secs());
lines.push(Line::from(vec![
Span::styled(" Results: ", Style::default().fg(Color::Yellow)),
Span::styled(
format!("{}/{} tests successful", successful_tests, total_tests),
Style::default().fg(Color::White),
),
]));
lines.push(Line::from(vec![
Span::styled(" Total Time: ", Style::default().fg(Color::Yellow)),
Span::styled(elapsed_str, Style::default().fg(Color::White)),
]));
}
crate::models::BenchTuneProgress::PartiallyCompleted {
total_tests,
successful_tests,
failed_tests,
elapsed,
} => {
let elapsed_str = format!("{}s", elapsed.as_secs());
lines.push(Line::from(vec![
Span::styled(" Results: ", Style::default().fg(Color::Yellow)),
Span::styled(
format!("{}/{} tests successful", successful_tests, total_tests),
Style::default().fg(Color::Yellow),
),
]));
lines.push(Line::from(vec![
Span::styled(" Failed: ", Style::default().fg(Color::Yellow)),
Span::styled(
format!("{} test(s)", failed_tests),
Style::default().fg(Color::Red),
),
]));
lines.push(Line::from(vec![
Span::styled(" Total Time: ", Style::default().fg(Color::Yellow)),
Span::styled(elapsed_str, Style::default().fg(Color::White)),
]));
}
crate::models::BenchTuneProgress::Cancelled {
total_tests,
successful_tests,
failed_tests,
elapsed,
} => {
let elapsed_str = format!("{}s", elapsed.as_secs());
lines.push(Line::from(vec![
Span::styled(" Results: ", Style::default().fg(Color::Yellow)),
Span::styled(
format!("{}/{} tests successful", successful_tests, total_tests),
Style::default().fg(Color::Yellow),
),
]));
lines.push(Line::from(vec![
Span::styled(" Failed: ", Style::default().fg(Color::Yellow)),
Span::styled(
format!("{} test(s)", failed_tests),
Style::default().fg(Color::Red),
),
]));
lines.push(Line::from(vec![
Span::styled(" Total Time: ", Style::default().fg(Color::Yellow)),
Span::styled(elapsed_str, Style::default().fg(Color::White)),
]));
}
crate::models::BenchTuneProgress::Error { error } => {
lines.push(Line::from(vec![
Span::styled(" Error: ", Style::default().fg(Color::Red)),
Span::styled(error, Style::default().fg(Color::White)),
]));
}
}
} else {
lines.push(Line::from(vec![
Span::styled(" Info: ", Style::default().fg(Color::Yellow)),
Span::styled(
"Starting first test...",
Style::default().fg(Color::DarkGray),
),
]));
}
} else if let Some((name, state)) = loaded_models.first() {
match state {
ModelState::Loaded { .. } => {
let display_used = app.metrics.ctx_used.max(2049);
let pct = if app.metrics.ctx_max > 0 {
(display_used as f64 / app.metrics.ctx_max as f64 * 100.0).ceil() as usize
} else {
0
};
let bar_width = 20usize;
let filled = (pct as f64 / 100.0 * bar_width as f64) as usize;
let bar_only = format!(
"{}{}",
"â–ˆ".repeat(filled),
"â–‘".repeat(bar_width.saturating_sub(filled)),
);
let token_str = format!(
"{}/{} ({:.0}%)",
display_used, app.metrics.ctx_max, pct as f64
);
lines.push(Line::from(vec![
Span::styled(" Model: ", Style::default().fg(Color::Yellow)),
Span::styled(
strip_gguf(name),
Style::default()
.fg(Color::White)
.add_modifier(Modifier::BOLD),
),
]));
let tps_style = if app.metrics.tps > 30.0 {
Style::default().fg(Color::Green)
} else if app.metrics.tps > 15.0 {
Style::default().fg(Color::Yellow)
} else if app.metrics.tps > 0.0 {
Style::default().fg(Color::Red)
} else {
Style::default().fg(Color::DarkGray)
};
let prompt_style = if app.metrics.prompt_tps > 100.0 {
Style::default().fg(Color::Green)
} else if app.metrics.prompt_tps > 50.0 {
Style::default().fg(Color::Yellow)
} else if app.metrics.prompt_tps > 0.0 {
Style::default().fg(Color::Red)
} else {
Style::default().fg(Color::DarkGray)
};
let tps_str = format!("{:.1}", app.metrics.tps);
let prompt_str = format!("{:.1}", app.metrics.prompt_tps);
let latency_str = if app.metrics.latency_per_token_ms > 0.0 {
format!(" {:.0}ms/tok", app.metrics.latency_per_token_ms)
} else {
String::new()
};
let gen_tps_style = if app.metrics.gen_tps > 30.0 {
Style::default().fg(Color::Green)
} else if app.metrics.gen_tps > 15.0 {
Style::default().fg(Color::Yellow)
} else if app.metrics.gen_tps > 0.0 {
Style::default().fg(Color::Red)
} else {
Style::default().fg(Color::DarkGray)
};
let tps_parts = vec![
Span::styled(" [ ", Style::default().fg(Color::White)),
Span::styled("Tokens/s: ", Style::default().fg(Color::Yellow)),
Span::styled(tps_str, tps_style),
if !latency_str.is_empty() {
Span::styled(latency_str, Style::default().fg(Color::DarkGray))
} else {
Span::styled(" ".repeat(10), Style::default().fg(Color::DarkGray))
},
Span::styled(" (prompt: ", Style::default().fg(Color::DarkGray)),
Span::styled(prompt_str, prompt_style),
Span::styled(")", Style::default().fg(Color::DarkGray)),
Span::styled(" ]", Style::default().fg(Color::White)),
Span::styled(" [ ", Style::default().fg(Color::White)),
Span::styled("Decoded: ", Style::default().fg(Color::Yellow)),
Span::styled(format!("{}", app.metrics.decoded_tokens), Style::default().fg(Color::Cyan)),
Span::styled(" ", Style::default().fg(Color::White)),
Span::styled("Gen: ", Style::default().fg(Color::Yellow)),
Span::styled(format!("{:.1}", app.metrics.gen_tps), gen_tps_style),
Span::styled(" t/s", Style::default().fg(Color::DarkGray)),
Span::styled(" ]", Style::default().fg(Color::White)),
];
lines.push(Line::from(tps_parts));
let context_parts = vec![
Span::styled(" [ ", Style::default().fg(Color::White)),
Span::styled("Context: ", Style::default().fg(Color::Yellow)),
Span::styled(bar_only, Style::default().fg(Color::Cyan)),
Span::styled(" ", Style::default().fg(Color::Cyan)),
Span::styled(token_str, Style::default().fg(Color::Cyan)),
Span::styled(" ]", Style::default().fg(Color::White)),
];
lines.push(Line::from(context_parts));
lines.push(Line::from(vec![
Span::styled(" [ ", Style::default().fg(Color::White)),
Span::styled("CPU: ", Style::default().fg(Color::Yellow)),
Span::styled(
format!("{:.1}%", app.metrics.cpu_usage),
Style::default().fg(Color::Cyan),
),
Span::styled(" ]", Style::default().fg(Color::White)),
Span::styled(" [ ", Style::default().fg(Color::White)),
Span::styled("RAM: ", Style::default().fg(Color::Yellow)),
Span::styled(
format_size(app.metrics.ram_used),
Style::default().fg(Color::Cyan),
),
Span::styled(" ]", Style::default().fg(Color::White)),
Span::styled(" [ ", Style::default().fg(Color::White)),
Span::styled("VRAM: ", Style::default().fg(Color::Yellow)),
Span::styled(
format_size(app.metrics.gpu_mem_used),
Style::default().fg(Color::Cyan),
),
Span::styled(" / ", Style::default().fg(Color::White)),
Span::styled(
format_size(app.metrics.gpu_mem_total),
Style::default().fg(Color::Cyan),
),
Span::styled(" ]", Style::default().fg(Color::White)),
]));
}
ModelState::Benchmarking => {
lines.push(Line::from(vec![
Span::styled(" Model: ", Style::default().fg(Color::Yellow)),
Span::styled(
strip_gguf(name),
Style::default()
.fg(Color::White)
.add_modifier(Modifier::BOLD),
),
]));
lines.push(Line::from(vec![
Span::styled(" Status: ", Style::default().fg(Color::Yellow)),
Span::styled(
"BENCHMARKING",
Style::default()
.fg(Color::Yellow)
.add_modifier(Modifier::BOLD),
),
]));
}
ModelState::Loading => {
lines.push(Line::from(vec![
Span::styled(" Model: ", Style::default().fg(Color::Yellow)),
Span::styled(
strip_gguf(name),
Style::default()
.fg(Color::White)
.add_modifier(Modifier::BOLD),
),
]));
let status_content = if app.loading.loading_progress <= 0.0 {
let spinners = ["â ‹", "â ™", "â ¹", "â ¸"];
format!("LOADING {}", spinners[app.loading.loading_spinner])
} else {
"LOADING".to_string()
};
lines.push(Line::from(vec![
Span::styled(" Status: ", Style::default().fg(Color::Yellow)),
Span::styled(status_content, Style::default().fg(Color::Yellow)),
]));
let overhead = 2 + 5;
let bar_width = area.width.saturating_sub(overhead as u16 + 2) as usize;
if app.loading.loading_progress > 0.0 && app.loading.loading_progress <= 1.0 {
let filled = (app.loading.loading_progress * bar_width as f32) as usize;
let bar = format!(
"[{}{}] {:.0}%",
"â–ˆ".repeat(filled),
"â–‘".repeat(bar_width.saturating_sub(filled)),
app.loading.loading_progress * 100.0
);
lines.push(Line::from(vec![Span::styled(
bar,
Style::default().fg(Color::Yellow),
)]));
} else {
let bar = format!("[{}] 0%", "â–‘".repeat(bar_width));
lines.push(Line::from(vec![Span::styled(
bar,
Style::default().fg(Color::DarkGray),
)]));
}
let mut detail_parts = Vec::new();
if let (Some(loaded), Some(total)) = (
app.loading.load_progress.layers_loaded,
app.loading.load_progress.layers_total,
) {
detail_parts.push(format!("({}/{})", loaded, total));
}
if app.loading.load_progress.tensors_loaded > 0 {
detail_parts.push(format!(
"{} tensors",
app.loading.load_progress.tensors_loaded
));
}
let total_gpu: f64 = app
.loading
.load_progress
.buffers
.iter()
.filter(|b| b.device != "CPU_Mapped" && b.device != "CPU_Cached")
.map(|b| b.buffer_size_mib)
.sum();
if total_gpu > 0.0 {
detail_parts.push(format!(
"{} VRAM",
format_size((total_gpu * 1024.0 * 1024.0) as u64)
));
}
let phase = app
.loading
.loading_phases
.iter()
.next()
.map(|p: &LoadingPhase| p.label())
.unwrap_or("Loading...");
let detail = detail_parts.join(", ");
lines.push(Line::from(vec![
Span::styled(" ", Style::default()),
Span::styled(phase, Style::default().fg(Color::Cyan)),
Span::raw(" "),
Span::styled(detail, Style::default().fg(Color::Magenta)),
]));
}
ModelState::Failed { error } => {
lines.push(Line::from(vec![
Span::styled(" Model: ", Style::default().fg(Color::Yellow)),
Span::styled(
strip_gguf(name),
Style::default()
.fg(Color::White)
.add_modifier(Modifier::BOLD),
),
]));
lines.push(Line::from(vec![
Span::styled(" Status: ", Style::default().fg(Color::Yellow)),
Span::styled(
"FAILED",
Style::default().fg(Color::Red).add_modifier(Modifier::BOLD),
),
]));
lines.push(Line::from(vec![
Span::styled(" Error: ", Style::default().fg(Color::Red)),
Span::styled(error, Style::default().fg(Color::White)),
]));
}
ModelState::Available => {
}
}
} else {
if app.server.server_handle.is_some() {
lines.push(Line::from(vec![
Span::styled(" Model: ", Style::default().fg(Color::Yellow)),
Span::styled(
"llama-server",
Style::default()
.fg(Color::White)
.add_modifier(Modifier::BOLD),
),
Span::styled(
" (no active model loaded)",
Style::default().fg(Color::DarkGray),
),
]));
} else {
lines.push(Line::from(vec![Span::styled(
" No active model ",
Style::default().fg(Color::DarkGray),
)]));
lines.push(Line::from(vec![Span::styled(
" (select a model and press Enter to load)",
Style::default().fg(Color::DarkGray),
)]));
}
}
let paragraph = Paragraph::new(lines).block(block);
f.render_widget(paragraph, area);
}