use ratatui::{ buffer::Buffer, layout::Rect, style::{Color, Modifier, Style}, widgets::{Block, Borders, Clear, Widget}, }; use crate::import::analyzer::{DateComponent, FieldKind}; use crate::import::wizard::{ImportWizard, WizardStep}; pub struct ImportWizardWidget<'a> { pub wizard: &'a ImportWizard, } impl<'a> ImportWizardWidget<'a> { pub fn new(wizard: &'a ImportWizard) -> Self { Self { wizard } } } impl<'a> Widget for ImportWizardWidget<'a> { fn render(self, area: Rect, buf: &mut Buffer) { let popup_w = area.width.min(80); let popup_h = area.height.min(30); let x = area.x + area.width.saturating_sub(popup_w) / 2; let y = area.y + area.height.saturating_sub(popup_h) / 2; let popup_area = Rect::new(x, y, popup_w, popup_h); Clear.render(popup_area, buf); let title = match self.wizard.step { WizardStep::Preview => " Import Wizard — Preview ", WizardStep::SelectArrayPath => " Import Wizard — Select Array ", WizardStep::ReviewProposals => " Import Wizard — Review Fields ", WizardStep::ConfigureDates => " Import Wizard — Date Components ", WizardStep::DefineFormulas => " Import Wizard — Formulas ", WizardStep::NameModel => " Import Wizard — Name Model ", WizardStep::Done => " Import Wizard — Done ", }; let block = Block::default() .borders(Borders::ALL) .border_style(Style::default().fg(Color::Magenta)) .title(title); let inner = block.inner(popup_area); block.render(popup_area, buf); let mut y = inner.y; let x = inner.x; let w = inner.width as usize; match &self.wizard.step { WizardStep::Preview => { let summary = self.wizard.pipeline.preview_summary(); buf.set_string(x, y, truncate(&summary, w), Style::default()); y += 2; buf.set_string( x, y, "Press Enter to continue\u{2026}", Style::default().fg(Color::Yellow), ); } WizardStep::SelectArrayPath => { buf.set_string( x, y, "Select the path containing records:", Style::default().fg(Color::Yellow), ); y += 1; for (i, path) in self.wizard.pipeline.array_paths.iter().enumerate() { if y >= inner.y + inner.height { break; } let is_sel = i == self.wizard.cursor; let style = if is_sel { Style::default() .fg(Color::Black) .bg(Color::Magenta) .add_modifier(Modifier::BOLD) } else { Style::default() }; let label = format!(" {}", if path.is_empty() { "(root)" } else { path }); buf.set_string(x, y, truncate(&label, w), style); y += 1; } y += 1; buf.set_string( x, y, "\u{2191}\u{2193} select Enter confirm", Style::default().fg(Color::DarkGray), ); } WizardStep::ReviewProposals => { buf.set_string( x, y, "Review field proposals (Space toggle, c cycle kind):", Style::default().fg(Color::Yellow), ); y += 1; let header = format!(" {:<20} {:<22} {:<6}", "Field", "Kind", "Accept"); buf.set_string( x, y, truncate(&header, w), Style::default() .fg(Color::Gray) .add_modifier(Modifier::UNDERLINED), ); y += 1; for (i, proposal) in self.wizard.pipeline.proposals.iter().enumerate() { if y >= inner.y + inner.height - 2 { break; } let is_sel = i == self.wizard.cursor; let kind_color = match proposal.kind { FieldKind::Category => Color::Green, FieldKind::Measure => Color::Cyan, FieldKind::TimeCategory => Color::Magenta, FieldKind::Label => Color::DarkGray, }; let accept_str = if proposal.accepted { "[\u{2713}]" } else { "[ ]" }; let row = format!( " {:<20} {:<22} {}", truncate(&proposal.field, 20), truncate(proposal.kind_label(), 22), accept_str ); let style = if is_sel { Style::default() .fg(Color::Black) .bg(Color::Cyan) .add_modifier(Modifier::BOLD) } else if proposal.accepted { Style::default().fg(kind_color) } else { Style::default().fg(Color::DarkGray) }; buf.set_string(x, y, truncate(&row, w), style); y += 1; } let hint_y = inner.y + inner.height - 1; buf.set_string( x, hint_y, "Enter: next Space: toggle c: cycle kind Esc: cancel", Style::default().fg(Color::DarkGray), ); } WizardStep::ConfigureDates => { buf.set_string( x, y, "Select date components to extract (Space toggle):", Style::default().fg(Color::Yellow), ); y += 1; let tc_proposals = self.wizard.time_category_proposals(); let mut item_idx = 0; for proposal in &tc_proposals { if y >= inner.y + inner.height - 2 { break; } let fmt_str = proposal.date_format.as_deref().unwrap_or("?"); let header = format!(" {} (format: {})", proposal.field, fmt_str); buf.set_string( x, y, truncate(&header, w), Style::default() .fg(Color::Magenta) .add_modifier(Modifier::BOLD), ); y += 1; for component in &[ DateComponent::Year, DateComponent::Month, DateComponent::Quarter, ] { if y >= inner.y + inner.height - 2 { break; } let enabled = proposal.date_components.contains(component); let check = if enabled { "[\u{2713}]" } else { "[ ]" }; let label = match component { DateComponent::Year => "Year", DateComponent::Month => "Month", DateComponent::Quarter => "Quarter", }; let row = format!(" {} {}", check, label); let is_sel = item_idx == self.wizard.cursor; let style = if is_sel { Style::default() .fg(Color::Black) .bg(Color::Cyan) .add_modifier(Modifier::BOLD) } else if enabled { Style::default().fg(Color::Green) } else { Style::default().fg(Color::DarkGray) }; buf.set_string(x, y, truncate(&row, w), style); y += 1; item_idx += 1; } } let hint_y = inner.y + inner.height - 1; buf.set_string( x, hint_y, "Space: toggle Enter: next Esc: cancel", Style::default().fg(Color::DarkGray), ); } WizardStep::DefineFormulas => { buf.set_string( x, y, "Define formulas (optional):", Style::default().fg(Color::Yellow), ); y += 1; // Show existing formulas if self.wizard.pipeline.formulas.is_empty() && !self.wizard.formula_editing { buf.set_string( x, y, " (no formulas yet)", Style::default().fg(Color::DarkGray), ); y += 1; } for (i, formula) in self.wizard.pipeline.formulas.iter().enumerate() { if y >= inner.y + inner.height - 5 { break; } let is_sel = i == self.wizard.cursor && !self.wizard.formula_editing; let style = if is_sel { Style::default() .fg(Color::Black) .bg(Color::Cyan) .add_modifier(Modifier::BOLD) } else { Style::default().fg(Color::Green) }; buf.set_string(x, y, truncate(&format!(" {}", formula), w), style); y += 1; } // Formula input area if self.wizard.formula_editing { y += 1; buf.set_string( x, y, "Formula (e.g., Profit = Revenue - Cost):", Style::default().fg(Color::Yellow), ); y += 1; let input = format!("> {}\u{2588}", self.wizard.formula_buffer); buf.set_string(x, y, truncate(&input, w), Style::default().fg(Color::Green)); y += 1; } // Sample formulas let samples = self.wizard.sample_formulas(); if !samples.is_empty() { y += 1; buf.set_string(x, y, "Examples:", Style::default().fg(Color::DarkGray)); y += 1; for sample in &samples { if y >= inner.y + inner.height - 1 { break; } buf.set_string( x, y, truncate(&format!(" {}", sample), w), Style::default().fg(Color::DarkGray), ); y += 1; } } let hint_y = inner.y + inner.height - 1; let hint = if self.wizard.formula_editing { "Enter: add Esc: cancel" } else { "n: new formula d: delete Enter: next Esc: cancel" }; buf.set_string(x, hint_y, hint, Style::default().fg(Color::DarkGray)); } WizardStep::NameModel => { buf.set_string(x, y, "Model name:", Style::default().fg(Color::Yellow)); y += 1; let name_str = format!("> {}\u{2588}", self.wizard.pipeline.model_name); buf.set_string( x, y, truncate(&name_str, w), Style::default().fg(Color::Green), ); y += 2; buf.set_string( x, y, "Enter to import, Esc to cancel", Style::default().fg(Color::DarkGray), ); if let Some(msg) = &self.wizard.message { let msg_y = inner.y + inner.height - 1; buf.set_string(x, msg_y, truncate(msg, w), Style::default().fg(Color::Red)); } } WizardStep::Done => { buf.set_string(x, y, "Import complete!", Style::default().fg(Color::Green)); } } } } fn truncate(s: &str, max: usize) -> String { if s.len() <= max { s.to_string() } else if max > 1 { format!("{}\u{2026}", &s[..max - 1]) } else { s[..max].to_string() } }