feat: implement interactive lesson management and update command handling
This commit is contained in:
+122
-63
@@ -372,69 +372,128 @@ fn render_overlay(frame: &mut Frame, area: Rect, overlay: crate::app::state::typ
|
||||
frame.render_widget(paragraph, overlay_area);
|
||||
}
|
||||
crate::app::state::types::Overlay::Learning => {
|
||||
let block = block.title(" Learning ");
|
||||
let (total, by_user, by_fb, by_proj, by_ref, act, stale, contra, human, verified, unverified) = state.session_runtime.as_ref().map(|r| {
|
||||
(r.lesson_count, r.lessons_user, r.lessons_feedback, r.lessons_project, r.lessons_reference, r.lessons_active, r.lessons_stale, r.lessons_contradicted, r.lessons_human, r.lessons_verified, r.lessons_unverified)
|
||||
}).unwrap_or_default();
|
||||
let lines = vec![
|
||||
Line::from(Span::styled(
|
||||
"Lessons Dashboard",
|
||||
Style::default().fg(Theme::TEXT).add_modifier(Modifier::BOLD),
|
||||
)),
|
||||
Line::from(Span::styled("", Style::default())),
|
||||
Line::from(Span::styled(
|
||||
format!("Total lessons: {}", total),
|
||||
Style::default().fg(Theme::INFO),
|
||||
)),
|
||||
Line::from(Span::styled("", Style::default())),
|
||||
Line::from(Span::styled("By Type:", Style::default().fg(Theme::DIM))),
|
||||
Line::from(Span::styled(
|
||||
format!(" User: {}", by_user),
|
||||
Style::default().fg(Theme::TEXT),
|
||||
)),
|
||||
Line::from(Span::styled(
|
||||
format!(" Feedback: {}", by_fb),
|
||||
Style::default().fg(Theme::TEXT),
|
||||
)),
|
||||
Line::from(Span::styled(
|
||||
format!(" Project: {}", by_proj),
|
||||
Style::default().fg(Theme::TEXT),
|
||||
)),
|
||||
Line::from(Span::styled(
|
||||
format!(" Reference: {}", by_ref),
|
||||
Style::default().fg(Theme::TEXT),
|
||||
)),
|
||||
Line::from(Span::styled("", Style::default())),
|
||||
Line::from(Span::styled("Lifecycle:", Style::default().fg(Theme::DIM))),
|
||||
Line::from(Span::styled(
|
||||
format!(" Active: {}", act),
|
||||
Style::default().fg(Theme::TEXT),
|
||||
)),
|
||||
Line::from(Span::styled(
|
||||
format!(" Stale: {}", stale),
|
||||
Style::default().fg(Theme::TEXT),
|
||||
)),
|
||||
Line::from(Span::styled(
|
||||
format!(" Contradicted: {}", contra),
|
||||
Style::default().fg(Theme::TEXT),
|
||||
)),
|
||||
Line::from(Span::styled("", Style::default())),
|
||||
Line::from(Span::styled("Confidence:", Style::default().fg(Theme::DIM))),
|
||||
Line::from(Span::styled(
|
||||
format!(" Human: {}", human),
|
||||
Style::default().fg(Theme::TEXT),
|
||||
)),
|
||||
Line::from(Span::styled(
|
||||
format!(" Verified: {}", verified),
|
||||
Style::default().fg(Theme::TEXT),
|
||||
)),
|
||||
Line::from(Span::styled(
|
||||
format!(" Unverified: {}", unverified),
|
||||
Style::default().fg(Theme::TEXT),
|
||||
)),
|
||||
];
|
||||
let paragraph = Paragraph::new(lines).block(block);
|
||||
frame.render_widget(paragraph, overlay_area);
|
||||
let h_chunks = Layout::default()
|
||||
.direction(Direction::Horizontal)
|
||||
.constraints([
|
||||
Constraint::Percentage(40), // Left: List
|
||||
Constraint::Percentage(60), // Right: Details
|
||||
])
|
||||
.split(overlay_area);
|
||||
|
||||
let left_block = Block::default()
|
||||
.title(" Lessons ")
|
||||
.borders(Borders::ALL)
|
||||
.border_style(Style::default().fg(Theme::PRIMARY))
|
||||
.style(Style::default().bg(Theme::BG));
|
||||
|
||||
let right_block = Block::default()
|
||||
.title(" Lesson Details ")
|
||||
.borders(Borders::ALL)
|
||||
.border_style(Style::default().fg(Theme::PRIMARY))
|
||||
.style(Style::default().bg(Theme::BG));
|
||||
|
||||
let items = crate::app::mode::learning::get_learning_items(state);
|
||||
let mut left_lines = Vec::new();
|
||||
if items.is_empty() {
|
||||
left_lines.push(Line::from(Span::styled(
|
||||
"No lessons found.",
|
||||
Style::default().fg(Theme::DIM),
|
||||
)));
|
||||
} else {
|
||||
for (i, item) in items.iter().enumerate() {
|
||||
let is_selected = i == state.misc.selected_index;
|
||||
let prefix = if is_selected { "▸ " } else { " " };
|
||||
let (label, style) = match item {
|
||||
crate::app::mode::learning::LearningItem::Pending { name, .. } => {
|
||||
(
|
||||
format!("{}[Pending] {}", prefix, name),
|
||||
if is_selected {
|
||||
Style::default().fg(Theme::WARNING).bg(Theme::HIGHLIGHT).add_modifier(Modifier::BOLD)
|
||||
} else {
|
||||
Style::default().fg(Theme::WARNING)
|
||||
}
|
||||
)
|
||||
}
|
||||
crate::app::mode::learning::LearningItem::Stored { name, lifecycle, .. } => {
|
||||
let status = if lifecycle == "stale" { "Stale" } else { "Active" };
|
||||
(
|
||||
format!("{}[{}] {}", prefix, status, name),
|
||||
if is_selected {
|
||||
Style::default().fg(Theme::TEXT).bg(Theme::HIGHLIGHT).add_modifier(Modifier::BOLD)
|
||||
} else {
|
||||
Style::default().fg(Theme::TEXT)
|
||||
}
|
||||
)
|
||||
}
|
||||
};
|
||||
left_lines.push(Line::from(Span::styled(label, style)));
|
||||
}
|
||||
}
|
||||
|
||||
// Scroll the left list so the selected index is always visible
|
||||
let max_lines = h_chunks[0].height.saturating_sub(2) as usize;
|
||||
let selected = state.misc.selected_index;
|
||||
let start_idx = if selected >= max_lines {
|
||||
selected - max_lines + 1
|
||||
} else {
|
||||
0
|
||||
};
|
||||
let end_idx = (start_idx + max_lines).min(left_lines.len());
|
||||
let visible_lines = if left_lines.is_empty() {
|
||||
Vec::new()
|
||||
} else {
|
||||
left_lines[start_idx..end_idx].to_vec()
|
||||
};
|
||||
|
||||
let left_paragraph = Paragraph::new(visible_lines).block(left_block);
|
||||
frame.render_widget(left_paragraph, h_chunks[0]);
|
||||
|
||||
let mut right_lines = Vec::new();
|
||||
if let Some(item) = items.get(selected) {
|
||||
match item {
|
||||
crate::app::mode::learning::LearningItem::Pending { name, content, scope, confidence } => {
|
||||
right_lines.push(Line::from(Span::styled("Name:", Style::default().fg(Theme::DIM))));
|
||||
right_lines.push(Line::from(Span::styled(name, Style::default().fg(Theme::TEXT).add_modifier(Modifier::BOLD))));
|
||||
right_lines.push(Line::from(""));
|
||||
right_lines.push(Line::from(Span::styled("Status: Pending Approval", Style::default().fg(Theme::WARNING))));
|
||||
right_lines.push(Line::from(Span::styled(format!("Scope: {}", scope), Style::default().fg(Theme::TEXT))));
|
||||
right_lines.push(Line::from(Span::styled(format!("Confidence: {}", confidence), Style::default().fg(Theme::TEXT))));
|
||||
right_lines.push(Line::from(""));
|
||||
right_lines.push(Line::from(Span::styled("Content:", Style::default().fg(Theme::DIM))));
|
||||
for line in content.lines() {
|
||||
right_lines.push(Line::from(Span::styled(line, Style::default().fg(Theme::TEXT))));
|
||||
}
|
||||
right_lines.push(Line::from(""));
|
||||
right_lines.push(Line::from(Span::styled("Keys:", Style::default().fg(Theme::DIM))));
|
||||
right_lines.push(Line::from(Span::styled(" [Enter] or [a] to Accept", Style::default().fg(Theme::SUCCESS))));
|
||||
right_lines.push(Line::from(Span::styled(" [Backspace]/[Delete]/[r] to Reject", Style::default().fg(Theme::ERROR))));
|
||||
}
|
||||
crate::app::mode::learning::LearningItem::Stored { name, content, lifecycle, scope, description } => {
|
||||
right_lines.push(Line::from(Span::styled("Name:", Style::default().fg(Theme::DIM))));
|
||||
right_lines.push(Line::from(Span::styled(name, Style::default().fg(Theme::TEXT).add_modifier(Modifier::BOLD))));
|
||||
right_lines.push(Line::from(""));
|
||||
let status_color = if lifecycle == "stale" { Theme::WARNING } else { Theme::SUCCESS };
|
||||
right_lines.push(Line::from(Span::styled(format!("Status: {}", lifecycle), Style::default().fg(status_color))));
|
||||
right_lines.push(Line::from(Span::styled(format!("Scope: {}", scope), Style::default().fg(Theme::TEXT))));
|
||||
right_lines.push(Line::from(Span::styled(format!("Description: {}", description), Style::default().fg(Theme::TEXT))));
|
||||
right_lines.push(Line::from(""));
|
||||
right_lines.push(Line::from(Span::styled("Content:", Style::default().fg(Theme::DIM))));
|
||||
for line in content.lines() {
|
||||
right_lines.push(Line::from(Span::styled(line, Style::default().fg(Theme::TEXT))));
|
||||
}
|
||||
right_lines.push(Line::from(""));
|
||||
right_lines.push(Line::from(Span::styled("Keys:", Style::default().fg(Theme::DIM))));
|
||||
right_lines.push(Line::from(Span::styled(" [Backspace]/[Delete]/[d] to Delete Lesson", Style::default().fg(Theme::ERROR))));
|
||||
}
|
||||
}
|
||||
} else {
|
||||
right_lines.push(Line::from(Span::styled(
|
||||
"Select a lesson on the left to see details.",
|
||||
Style::default().fg(Theme::DIM),
|
||||
)));
|
||||
}
|
||||
let right_paragraph = Paragraph::new(right_lines).block(right_block).wrap(Wrap { trim: false });
|
||||
frame.render_widget(right_paragraph, h_chunks[1]);
|
||||
}
|
||||
crate::app::state::types::Overlay::Usage => {
|
||||
let block = block.title(" Usage ");
|
||||
|
||||
Reference in New Issue
Block a user