use crate::theme::Theme;
use ratatui::{
layout::Rect,
text::{Line, Span},
widgets::{Block, Borders, Clear, Paragraph, Wrap},
Frame,
};
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub enum OnboardingStep {
Welcome,
AskName,
AskAI,
AskAIProvider,
Complete,
}
impl OnboardingStep {
pub fn next(self) -> Self {
match self {
Self::Welcome => Self::AskName,
Self::AskName => Self::AskAI,
Self::AskAI => Self::AskAIProvider,
Self::AskAIProvider => Self::Complete,
Self::Complete => Self::Complete,
}
}
pub fn previous(self) -> Self {
match self {
Self::Welcome => Self::Welcome,
Self::AskName => Self::Welcome,
Self::AskAI => Self::AskName,
Self::AskAIProvider => Self::AskAI,
Self::Complete => Self::AskAIProvider,
}
}
}
pub struct OnboardingWizard {
pub step: OnboardingStep,
pub user_name: String,
pub ai_enabled: Option<bool>,
pub ai_provider: Option<String>,
pub selected_option: usize,
}
impl OnboardingWizard {
pub fn new() -> Self {
Self {
step: OnboardingStep::Welcome,
user_name: String::new(),
ai_enabled: None,
ai_provider: None,
selected_option: 0,
}
}
pub fn handle_char(&mut self, c: char) {
if self.step == OnboardingStep::AskName {
self.user_name.push(c);
}
}
pub fn handle_backspace(&mut self) {
if self.step == OnboardingStep::AskName {
self.user_name.pop();
}
}
pub fn handle_up(&mut self) {
if self.selected_option > 0 {
self.selected_option -= 1;
}
}
pub fn handle_down(&mut self, max_options: usize) {
if self.selected_option < max_options - 1 {
self.selected_option += 1;
}
}
pub fn handle_enter(&mut self) {
match self.step {
OnboardingStep::Welcome => {
self.step = self.step.next();
}
OnboardingStep::AskName => {
if !self.user_name.trim().is_empty() {
self.step = self.step.next();
self.selected_option = 0;
}
}
OnboardingStep::AskAI => {
match self.selected_option {
0 => {
self.ai_enabled = Some(true);
self.step = self.step.next();
self.selected_option = 0;
}
1 => {
self.ai_enabled = Some(false);
self.step = OnboardingStep::Complete;
}
2 => {
self.ai_enabled = Some(false);
self.step = OnboardingStep::Complete;
}
_ => {}
}
}
OnboardingStep::AskAIProvider => {
match self.selected_option {
0 => {
self.ai_provider = Some("claude-code".to_string());
self.step = OnboardingStep::Complete;
}
1 => {
self.ai_provider = Some("own".to_string());
self.step = OnboardingStep::Complete;
}
2 => {
self.ai_provider = Some("managed".to_string());
self.step = OnboardingStep::Complete;
}
_ => {}
}
}
OnboardingStep::Complete => {}
}
}
pub fn render(&self, frame: &mut Frame, theme: &Theme) {
let area = frame.size();
let modal_width = 70.min(area.width - 4);
let modal_height = 20.min(area.height - 4);
let horizontal_margin = (area.width.saturating_sub(modal_width)) / 2;
let vertical_margin = (area.height.saturating_sub(modal_height)) / 2;
let modal_area = Rect {
x: horizontal_margin,
y: vertical_margin,
width: modal_width,
height: modal_height,
};
frame.render_widget(Clear, modal_area);
let block = Block::default()
.title(" Arc Academy Terminal - Setup ")
.borders(Borders::ALL)
.border_style(theme.style_border_focused())
.style(theme.style_block());
let inner = block.inner(modal_area);
frame.render_widget(block, modal_area);
match self.step {
OnboardingStep::Welcome => self.render_welcome(frame, inner, theme),
OnboardingStep::AskName => self.render_ask_name(frame, inner, theme),
OnboardingStep::AskAI => self.render_ask_ai(frame, inner, theme),
OnboardingStep::AskAIProvider => self.render_ask_provider(frame, inner, theme),
OnboardingStep::Complete => self.render_complete(frame, inner, theme),
}
}
fn render_welcome(&self, frame: &mut Frame, area: Rect, theme: &Theme) {
let lines = vec![
Line::from(""),
Line::from(vec![
Span::styled(" Welcome to ", theme.style_normal()),
Span::styled("Arc Academy Terminal", theme.style_accent()),
Span::styled("!", theme.style_normal()),
]),
Line::from(""),
Line::from(vec![
Span::styled(" Learn shell commands interactively with AI-powered", theme.style_normal()),
]),
Line::from(vec![
Span::styled(" explanations and real-time help.", theme.style_normal()),
]),
Line::from(""),
Line::from(""),
Line::from(vec![
Span::styled(" Let's get you set up in just a few steps.", theme.style_dim()),
]),
Line::from(""),
Line::from(""),
Line::from(""),
Line::from(vec![
Span::styled(" Press ", theme.style_dim()),
Span::styled("Enter", theme.style_accent()),
Span::styled(" to continue", theme.style_dim()),
]),
];
let paragraph = Paragraph::new(lines).wrap(Wrap { trim: false });
frame.render_widget(paragraph, area);
}
fn render_ask_name(&self, frame: &mut Frame, area: Rect, theme: &Theme) {
let lines = vec![
Line::from(""),
Line::from(vec![
Span::styled(" What's your name?", theme.style_accent()),
]),
Line::from(""),
Line::from(vec![
Span::styled(" This helps us personalize your experience.", theme.style_dim()),
]),
Line::from(""),
Line::from(""),
Line::from(vec![
Span::styled(" Name: ", theme.style_normal()),
Span::styled(&self.user_name, theme.style_accent()),
Span::styled("█", theme.style_accent()),
]),
Line::from(""),
Line::from(""),
Line::from(""),
Line::from(vec![
Span::styled(" Press ", theme.style_dim()),
Span::styled("Enter", theme.style_accent()),
Span::styled(" to continue", theme.style_dim()),
]),
];
let paragraph = Paragraph::new(lines).wrap(Wrap { trim: false });
frame.render_widget(paragraph, area);
}
fn render_ask_ai(&self, frame: &mut Frame, area: Rect, theme: &Theme) {
let options = vec![
("Yes, enable AI assistant", 0),
("No, not right now", 1),
("Maybe later", 2),
];
let mut lines = vec![
Line::from(""),
Line::from(vec![
Span::styled(" Would you like to enable the AI assistant?", theme.style_accent()),
]),
Line::from(""),
Line::from(vec![
Span::styled(" The AI can help explain commands, suggest alternatives,", theme.style_dim()),
]),
Line::from(vec![
Span::styled(" and answer your shell questions.", theme.style_dim()),
]),
Line::from(""),
Line::from(""),
];
for (i, (option, _)) in options.iter().enumerate() {
let prefix = if i == self.selected_option {
Span::styled(" ▶ ", theme.style_accent())
} else {
Span::styled(" ", theme.style_dim())
};
let style = if i == self.selected_option {
theme.style_accent()
} else {
theme.style_normal()
};
lines.push(Line::from(vec![prefix, Span::styled(*option, style)]));
}
lines.push(Line::from(""));
lines.push(Line::from(""));
lines.push(Line::from(vec![
Span::styled(" Use ", theme.style_dim()),
Span::styled("↑/↓", theme.style_accent()),
Span::styled(" to select, ", theme.style_dim()),
Span::styled("Enter", theme.style_accent()),
Span::styled(" to continue", theme.style_dim()),
]));
let paragraph = Paragraph::new(lines).wrap(Wrap { trim: false });
frame.render_widget(paragraph, area);
}
fn render_ask_provider(&self, frame: &mut Frame, area: Rect, theme: &Theme) {
let options = vec![
("I have Claude Code with Max subscription", 0),
("I have my own API key (Anthropic, OpenAI, or local LLM)", 1),
("Use Arc Academy managed AI (Coming Soon)", 2),
];
let mut lines = vec![
Line::from(""),
Line::from(vec![
Span::styled(" How would you like to use AI?", theme.style_accent()),
]),
Line::from(""),
Line::from(""),
];
for (i, (option, _)) in options.iter().enumerate() {
let prefix = if i == self.selected_option {
Span::styled(" ▶ ", theme.style_accent())
} else {
Span::styled(" ", theme.style_dim())
};
let mut style = if i == self.selected_option {
theme.style_accent()
} else {
theme.style_normal()
};
if i == 2 {
style = theme.style_dim();
}
lines.push(Line::from(vec![prefix, Span::styled(*option, style)]));
}
lines.push(Line::from(""));
lines.push(Line::from(""));
lines.push(Line::from(vec![
Span::styled(" Note: Claude Code option uses ", theme.style_dim()),
Span::styled("`claude`", theme.style_accent()),
Span::styled(" CLI - no API key needed!", theme.style_dim()),
]));
lines.push(Line::from(""));
lines.push(Line::from(vec![
Span::styled(" Use ", theme.style_dim()),
Span::styled("↑/↓", theme.style_accent()),
Span::styled(" to select, ", theme.style_dim()),
Span::styled("Enter", theme.style_accent()),
Span::styled(" to continue", theme.style_dim()),
]));
let paragraph = Paragraph::new(lines).wrap(Wrap { trim: false });
frame.render_widget(paragraph, area);
}
fn render_complete(&self, frame: &mut Frame, area: Rect, theme: &Theme) {
let name = if !self.user_name.is_empty() {
&self.user_name
} else {
"there"
};
let lines = vec![
Line::from(""),
Line::from(vec![
Span::styled(" All set, ", theme.style_normal()),
Span::styled(name, theme.style_accent()),
Span::styled("!", theme.style_normal()),
]),
Line::from(""),
Line::from(""),
Line::from(vec![
Span::styled(" Quick tips:", theme.style_header()),
]),
Line::from(vec![
Span::styled(" • Press ", theme.style_dim()),
Span::styled("?", theme.style_accent()),
Span::styled(" for help anytime", theme.style_dim()),
]),
Line::from(vec![
Span::styled(" • Press ", theme.style_dim()),
Span::styled("Ctrl+A", theme.style_accent()),
Span::styled(" to toggle AI assistant", theme.style_dim()),
]),
Line::from(vec![
Span::styled(" • Press ", theme.style_dim()),
Span::styled("Ctrl+S", theme.style_accent()),
Span::styled(" to open settings", theme.style_dim()),
]),
Line::from(vec![
Span::styled(" • Press ", theme.style_dim()),
Span::styled("Tab", theme.style_accent()),
Span::styled(" to switch panels", theme.style_dim()),
]),
Line::from(""),
Line::from(""),
Line::from(vec![
Span::styled(" Press ", theme.style_dim()),
Span::styled("Enter", theme.style_accent()),
Span::styled(" to start learning!", theme.style_dim()),
]),
];
let paragraph = Paragraph::new(lines).wrap(Wrap { trim: false });
frame.render_widget(paragraph, area);
}
}
impl Default for OnboardingWizard {
fn default() -> Self {
Self::new()
}
}