feat: implement interactive lesson management and update command handling

This commit is contained in:
asepharyana
2026-07-13 02:53:30 +07:00
parent ab8b098a57
commit dc0f1c7647
10 changed files with 288 additions and 209 deletions
+122 -63
View File
@@ -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 ");