asterai 1.0.0-alpha.13

CLI for asterai - the portable AI compute platform
use crate::tui::Tty;
use crate::tui::app::{App, AuthState, PickerState, Screen};
use crate::tui::ops;
use crate::tui::views::picker;
use crossterm::event::{Event, KeyCode, KeyEvent};
use ratatui::prelude::*;
use ratatui::widgets::{Block, Borders, Paragraph};

pub fn render(f: &mut Frame, state: &AuthState) {
    let area = centered_rect(60, 12, f.area());
    let block = Block::default()
        .title(" asterai agents ")
        .borders(Borders::ALL)
        .border_style(Style::default().fg(Color::Cyan));
    let inner = block.inner(area);
    f.render_widget(block, area);
    match state {
        AuthState::Checking => {
            let text = Paragraph::new("Checking login...")
                .style(Style::default().fg(Color::DarkGray))
                .alignment(Alignment::Center);
            f.render_widget(text, inner);
        }
        AuthState::LoggingIn => {
            let text = Paragraph::new("Logging in...")
                .style(Style::default().fg(Color::DarkGray))
                .alignment(Alignment::Center);
            f.render_widget(text, inner);
        }
        AuthState::NeedLogin { input, error } => {
            let mut lines = vec![
                Line::from(""),
                Line::from(Span::styled(
                    "You need an asterai account to continue.",
                    Style::default().bold(),
                )),
                Line::from(""),
                Line::from(vec![
                    Span::styled("Sign up:   ", Style::default().fg(Color::DarkGray)),
                    Span::styled("https://asterai.io/login", Style::default().fg(Color::Cyan)),
                ]),
                Line::from(vec![
                    Span::styled("API keys:  ", Style::default().fg(Color::DarkGray)),
                    Span::styled(
                        "https://asterai.io/settings/api-keys",
                        Style::default().fg(Color::Cyan),
                    ),
                ]),
                Line::from(""),
            ];
            if let Some(err) = error {
                lines.push(Line::from(Span::styled(
                    err.as_str(),
                    Style::default().fg(Color::Red),
                )));
                lines.push(Line::from(""));
            }
            let masked: String = "*".repeat(input.len());
            lines.push(Line::from(vec![
                Span::styled("API key: ", Style::default().bold()),
                Span::styled(masked, Style::default().fg(Color::Yellow)),
                Span::styled("_", Style::default().fg(Color::DarkGray)),
            ]));
            let text = Paragraph::new(lines).alignment(Alignment::Center);
            f.render_widget(text, inner);
        }
    }
}

pub async fn handle_event(
    app: &mut App,
    event: Event,
    terminal: &mut Terminal<CrosstermBackend<Tty>>,
) -> eyre::Result<()> {
    let Event::Key(KeyEvent { code, .. }) = event else {
        return Ok(());
    };
    let Screen::Auth(state) = &mut app.screen else {
        return Ok(());
    };
    if let AuthState::NeedLogin { input, error } = state {
        match code {
            KeyCode::Char(c) => {
                input.push(c);
                *error = None;
            }
            KeyCode::Backspace => {
                input.pop();
            }
            KeyCode::Enter => {
                let key = input.trim().to_string();
                if key.is_empty() {
                    *error = Some("API key is required.".to_string());
                    return Ok(());
                }
                app.screen = Screen::Auth(AuthState::LoggingIn);
                terminal.draw(|f| super::render(f, app))?;
                match ops::login(&key).await {
                    Ok(_) => {
                        app.screen = Screen::Picker(PickerState {
                            error: None,
                            agents: Vec::new(),
                            selected: 0,
                            loading: true,
                        });
                        terminal.draw(|f| super::render(f, app))?;
                        picker::discover_agents(app).await;
                    }
                    Err(e) => {
                        app.screen = Screen::Auth(AuthState::NeedLogin {
                            input: String::new(),
                            error: Some(format!("Login failed: {e}")),
                        });
                    }
                }
            }
            KeyCode::Esc => {
                app.should_quit = true;
            }
            _ => {}
        }
    }
    Ok(())
}

fn centered_rect(width_pct: u16, height: u16, area: Rect) -> Rect {
    let popup_width = area.width * width_pct / 100;
    let x = (area.width.saturating_sub(popup_width)) / 2;
    let y = (area.height.saturating_sub(height)) / 2;
    Rect::new(x, y, popup_width.min(area.width), height.min(area.height))
}