use ratatui::Frame;
use ratatui::layout::{Constraint, Direction, Layout, Rect};
use ratatui::style::Style;
use ratatui::text::{Line, Span};
use ratatui::widgets::{Block, Borders, Paragraph};
use crate::app::App;
use crate::theme;
use crate::types::Panel;
use super::panels::{
render_activity_log, render_focused_framework_gauge, render_framework_cards, render_info_panel,
render_score_gauge,
};
pub(super) fn render_dashboard_content(frame: &mut Frame, area: Rect, app: &App) {
use crate::components::zoom::ZoomedWidget;
if let Some(zoomed) = app.zoom.zoomed {
match zoomed {
ZoomedWidget::ScoreGauge => render_score_gauge(frame, area, app),
ZoomedWidget::DeadlineCountdown => {
super::panels::render_deadline_countdown(frame, area);
}
ZoomedWidget::ActivityLog => render_activity_log(frame, area, app),
ZoomedWidget::ScoreSparkline => render_score_history_line(frame, area, app),
}
return;
}
let has_agents = !app.passport_view.loaded_passports.is_empty();
let top_split = if has_agents {
Layout::default()
.direction(Direction::Vertical)
.constraints([
Constraint::Length(3),
Constraint::Length(3),
Constraint::Min(8),
])
.split(area)
} else {
Layout::default()
.direction(Direction::Vertical)
.constraints([Constraint::Length(3), Constraint::Min(8)])
.split(area)
};
if let Some(ref fs) = app.framework_scores {
if fs.frameworks.len() > 1 {
if let Some(idx) = app.focused_framework {
if let Some(fw) = fs.frameworks.get(idx) {
render_focused_framework_gauge(frame, top_split[0], fw);
} else {
render_framework_cards(frame, top_split[0], app);
}
} else {
render_framework_cards(frame, top_split[0], app);
}
} else {
render_score_gauge(frame, top_split[0], app);
}
} else {
render_score_gauge(frame, top_split[0], app);
}
if has_agents {
render_agent_strip(frame, top_split[1], app);
}
let content_area = if has_agents {
top_split[2]
} else {
top_split[1]
};
let h_split = Layout::default()
.direction(Direction::Horizontal)
.constraints([Constraint::Percentage(60), Constraint::Percentage(40)])
.split(content_area);
let left_col = Layout::default()
.direction(Direction::Vertical)
.constraints([Constraint::Percentage(70), Constraint::Percentage(30)])
.split(h_split[0]);
super::super::chat::render_chat(frame, left_col[0], app, app.active_panel == Panel::Chat);
render_score_history_line(frame, left_col[1], app);
render_info_panel(frame, h_split[1], app);
}
fn render_agent_strip(frame: &mut Frame, area: Rect, app: &App) {
let t = theme::theme();
let count = app.passport_view.loaded_passports.len();
let block = Block::default()
.title(format!(" Agents ({count}) "))
.title_style(theme::title_style())
.borders(Borders::ALL)
.border_style(Style::default().fg(t.border));
let inner = block.inner(area);
frame.render_widget(block, area);
let mut spans: Vec<Span> = Vec::new();
for (i, passport) in app.passport_view.loaded_passports.iter().enumerate() {
let name = passport
.get("name")
.and_then(|v| v.as_str())
.unwrap_or("unknown");
let autonomy = passport
.get("autonomy_level")
.and_then(|v| v.as_str())
.unwrap_or("?");
let score = passport
.get("compliance")
.and_then(|c| c.get("complior_score"))
.and_then(serde_json::Value::as_f64)
.unwrap_or(0.0);
let score_color = crate::views::score_zone_color(score, &t);
if i > 0 {
spans.push(Span::styled(" \u{2022} ", Style::default().fg(t.muted)));
}
spans.push(Span::styled(
format!("{name} {autonomy} "),
Style::default().fg(t.fg),
));
spans.push(Span::styled(
format!("{score:.0}"),
Style::default().fg(score_color),
));
}
frame.render_widget(Paragraph::new(Line::from(spans)), inner);
}
pub(super) fn render_score_history_line(frame: &mut Frame, area: Rect, app: &App) {
let t = theme::theme();
let block = Block::default()
.title(" Score History ")
.title_style(theme::title_style())
.borders(Borders::ALL)
.border_style(Style::default().fg(t.border));
let inner = block.inner(area);
frame.render_widget(block, area);
if app.score_history.is_empty() {
frame.render_widget(
Paragraph::new(Line::from(Span::styled(
" No history yet \u{2014} run /scan",
Style::default().fg(t.muted),
))),
inner,
);
return;
}
let sparkline_chars = [
'\u{2581}', '\u{2582}', '\u{2583}', '\u{2584}', '\u{2585}', '\u{2586}', '\u{2587}',
'\u{2588}',
];
let sparkline: String = app
.score_history
.iter()
.map(|&score| {
#[allow(clippy::cast_possible_truncation, clippy::cast_sign_loss)]
let idx = ((score / 100.0) * 7.0).clamp(0.0, 7.0) as usize;
sparkline_chars[idx]
})
.collect();
let last_score = app.score_history.last().copied().unwrap_or(0.0);
let color = crate::views::score_zone_color(last_score, &t);
let lines = vec![
Line::from(Span::styled(
format!(" {sparkline}"),
Style::default().fg(color),
)),
Line::from(Span::styled(
format!(
" Latest: {last_score:.0}/100 ({} scans)",
app.score_history.len()
),
Style::default().fg(t.muted),
)),
];
frame.render_widget(Paragraph::new(lines), inner);
}