- Introduce `Theme` import and pass a cloned `theme` instance to UI helpers (e.g., `render_editable_textarea`). - Remove direct `Color` usage; UI now derives colors from `Theme` fields for placeholders, selections, ReAct components (thought, action, input, observation, final answer), status badges, operating mode badges, and model info panel. - Extend `Theme` with new color fields for agent ReAct stages, badge foreground/background, and operating mode colors. - Update rendering logic to apply these theme colors throughout the TUI (input panel, help text, status lines, model selection UI, etc.). - Adjust imports to drop unused `Color` references.
227 lines
6.4 KiB
Rust
227 lines
6.4 KiB
Rust
use owlen_core::model::DetailedModelInfo;
|
|
use owlen_core::theme::Theme;
|
|
use ratatui::{
|
|
Frame,
|
|
layout::Rect,
|
|
style::{Modifier, Style},
|
|
widgets::{Block, Borders, Paragraph, Wrap},
|
|
};
|
|
|
|
/// Dedicated panel for presenting detailed model information.
|
|
pub struct ModelInfoPanel {
|
|
info: Option<DetailedModelInfo>,
|
|
scroll_offset: usize,
|
|
total_lines: usize,
|
|
}
|
|
|
|
impl ModelInfoPanel {
|
|
pub fn new() -> Self {
|
|
Self {
|
|
info: None,
|
|
scroll_offset: 0,
|
|
total_lines: 0,
|
|
}
|
|
}
|
|
|
|
pub fn set_model_info(&mut self, info: DetailedModelInfo) {
|
|
self.info = Some(info);
|
|
self.scroll_offset = 0;
|
|
self.total_lines = 0;
|
|
}
|
|
|
|
pub fn clear(&mut self) {
|
|
self.info = None;
|
|
self.scroll_offset = 0;
|
|
self.total_lines = 0;
|
|
}
|
|
|
|
pub fn render(&mut self, frame: &mut Frame<'_>, area: Rect, theme: &Theme) {
|
|
let block = Block::default()
|
|
.title("Model Information")
|
|
.borders(Borders::ALL)
|
|
.style(Style::default().bg(theme.background).fg(theme.text))
|
|
.border_style(Style::default().fg(theme.focused_panel_border));
|
|
|
|
if let Some(info) = &self.info {
|
|
let body = self.format_info(info);
|
|
self.total_lines = body.lines().count();
|
|
let paragraph = Paragraph::new(body)
|
|
.block(block)
|
|
.style(Style::default().fg(theme.text))
|
|
.wrap(Wrap { trim: true })
|
|
.scroll((self.scroll_offset as u16, 0));
|
|
frame.render_widget(paragraph, area);
|
|
} else {
|
|
self.total_lines = 0;
|
|
let paragraph = Paragraph::new("Select a model to inspect its details.")
|
|
.block(block)
|
|
.style(
|
|
Style::default()
|
|
.fg(theme.placeholder)
|
|
.add_modifier(Modifier::ITALIC),
|
|
)
|
|
.wrap(Wrap { trim: true });
|
|
frame.render_widget(paragraph, area);
|
|
}
|
|
}
|
|
|
|
pub fn scroll_up(&mut self) {
|
|
if self.scroll_offset > 0 {
|
|
self.scroll_offset -= 1;
|
|
}
|
|
}
|
|
|
|
pub fn scroll_down(&mut self, viewport_height: usize) {
|
|
if viewport_height == 0 {
|
|
return;
|
|
}
|
|
let max_offset = self.total_lines.saturating_sub(viewport_height);
|
|
if self.scroll_offset < max_offset {
|
|
self.scroll_offset += 1;
|
|
}
|
|
}
|
|
|
|
pub fn reset_scroll(&mut self) {
|
|
self.scroll_offset = 0;
|
|
}
|
|
|
|
pub fn scroll_offset(&self) -> usize {
|
|
self.scroll_offset
|
|
}
|
|
|
|
pub fn total_lines(&self) -> usize {
|
|
self.total_lines
|
|
}
|
|
|
|
pub fn current_model_name(&self) -> Option<&str> {
|
|
self.info.as_ref().map(|info| info.name.as_str())
|
|
}
|
|
|
|
fn format_info(&self, info: &DetailedModelInfo) -> String {
|
|
let mut lines = Vec::new();
|
|
lines.push(format!("Name: {}", info.name));
|
|
lines.push(format!(
|
|
"Architecture: {}",
|
|
display_option(info.architecture.as_deref())
|
|
));
|
|
lines.push(format!(
|
|
"Parameters: {}",
|
|
display_option(info.parameters.as_deref())
|
|
));
|
|
lines.push(format!(
|
|
"Context Length: {}",
|
|
display_u64(info.context_length)
|
|
));
|
|
lines.push(format!(
|
|
"Embedding Length: {}",
|
|
display_u64(info.embedding_length)
|
|
));
|
|
lines.push(format!(
|
|
"Quantization: {}",
|
|
display_option(info.quantization.as_deref())
|
|
));
|
|
lines.push(format!(
|
|
"Family: {}",
|
|
display_option(info.family.as_deref())
|
|
));
|
|
if !info.families.is_empty() {
|
|
lines.push(format!("Families: {}", info.families.join(", ")));
|
|
}
|
|
lines.push(format!(
|
|
"Parameter Size: {}",
|
|
display_option(info.parameter_size.as_deref())
|
|
));
|
|
lines.push(format!("Size: {}", format_size(info.size)));
|
|
lines.push(format!(
|
|
"Modified: {}",
|
|
display_option(info.modified_at.as_deref())
|
|
));
|
|
lines.push(format!(
|
|
"License: {}",
|
|
display_option(info.license.as_deref())
|
|
));
|
|
lines.push(format!(
|
|
"Digest: {}",
|
|
display_option(info.digest.as_deref())
|
|
));
|
|
|
|
if let Some(template) = info.template.as_deref() {
|
|
lines.push(format!("Template: {}", snippet(template)));
|
|
}
|
|
|
|
if let Some(system) = info.system.as_deref() {
|
|
lines.push(format!("System Prompt: {}", snippet(system)));
|
|
}
|
|
|
|
if let Some(modelfile) = info.modelfile.as_deref() {
|
|
lines.push("Modelfile:".to_string());
|
|
lines.push(snippet_multiline(modelfile, 8));
|
|
}
|
|
|
|
lines.join("\n")
|
|
}
|
|
}
|
|
|
|
impl Default for ModelInfoPanel {
|
|
fn default() -> Self {
|
|
Self::new()
|
|
}
|
|
}
|
|
|
|
fn display_option(value: Option<&str>) -> String {
|
|
value
|
|
.map(|s| s.to_string())
|
|
.filter(|s| !s.trim().is_empty())
|
|
.unwrap_or_else(|| "N/A".to_string())
|
|
}
|
|
|
|
fn display_u64(value: Option<u64>) -> String {
|
|
value
|
|
.map(|v| v.to_string())
|
|
.unwrap_or_else(|| "N/A".to_string())
|
|
}
|
|
|
|
fn format_size(value: Option<u64>) -> String {
|
|
if let Some(bytes) = value {
|
|
if bytes >= 1_000_000_000 {
|
|
let human = bytes as f64 / 1_000_000_000_f64;
|
|
format!("{human:.2} GB ({} bytes)", bytes)
|
|
} else if bytes >= 1_000_000 {
|
|
let human = bytes as f64 / 1_000_000_f64;
|
|
format!("{human:.2} MB ({} bytes)", bytes)
|
|
} else if bytes >= 1_000 {
|
|
let human = bytes as f64 / 1_000_f64;
|
|
format!("{human:.2} KB ({} bytes)", bytes)
|
|
} else {
|
|
format!("{bytes} bytes")
|
|
}
|
|
} else {
|
|
"N/A".to_string()
|
|
}
|
|
}
|
|
|
|
fn snippet(text: &str) -> String {
|
|
const MAX_LEN: usize = 160;
|
|
if text.len() > MAX_LEN {
|
|
format!("{}…", text.chars().take(MAX_LEN).collect::<String>())
|
|
} else {
|
|
text.to_string()
|
|
}
|
|
}
|
|
|
|
fn snippet_multiline(text: &str, max_lines: usize) -> String {
|
|
let mut lines = Vec::new();
|
|
for (idx, line) in text.lines().enumerate() {
|
|
if idx >= max_lines {
|
|
lines.push("…".to_string());
|
|
break;
|
|
}
|
|
lines.push(snippet(line));
|
|
}
|
|
if lines.is_empty() {
|
|
"N/A".to_string()
|
|
} else {
|
|
lines.join("\n")
|
|
}
|
|
}
|