use crate::app::commands::CommandHandler;
use crate::app::models::ModelManager;
use crate::app::state::{App, AppState};
use crate::ui::components::*;
use crate::ui::styles::AppStyles;
use ratatui::{
layout::{Alignment, Constraint, Direction, Layout, Rect},
style::{Color, Modifier, Style},
text::{Line, Span},
widgets::{Block, Borders, Padding, Paragraph},
Frame,
};
fn add_prompt(f: &mut Frame, area: Rect, prompt_text: &str) {
let prompt = Span::styled(prompt_text, AppStyles::prompt_symbol());
let prompt_para = Paragraph::new(Line::from(vec![prompt]));
let prompt_area = Rect {
x: area.x + 1,
y: area.y + 1,
width: prompt_text.len() as u16,
height: 1,
};
f.render_widget(prompt_para, prompt_area);
}
pub fn ui(f: &mut Frame, app: &mut App) {
let _animation_active =
app.last_message_time.elapsed() < std::time::Duration::from_millis(1000);
if app.tool_execution_in_progress || app.agent_progress_rx.is_some() {
app.last_message_time = std::time::Instant::now();
}
match app.state {
AppState::Setup => draw_setup(f, app),
AppState::ApiKeyInput => draw_api_key_input(f, app),
AppState::Chat => draw_chat(f, app),
AppState::Error(ref error_msg) => draw_error(f, error_msg),
}
if app.permission_required && app.pending_tool.is_some() {
draw_permission_dialog(f, app);
}
}
pub fn draw_setup(f: &mut Frame, app: &mut App) {
let chunks = Layout::default()
.direction(Direction::Vertical)
.margin(3)
.constraints([
Constraint::Length(3),
Constraint::Min(5),
Constraint::Length(3),
])
.split(f.area());
let version = env!("CARGO_PKG_VERSION");
let title = Paragraph::new(format!("oli v{} Setup Assistant", version))
.style(AppStyles::title())
.alignment(Alignment::Center);
f.render_widget(title, chunks[0]);
let models_list = create_model_list(app);
f.render_widget(models_list, chunks[1]);
let progress_bar = create_progress_display(app);
f.render_widget(progress_bar, chunks[2]);
}
pub fn draw_api_key_input(f: &mut Frame, app: &mut App) {
let chunks = Layout::default()
.direction(Direction::Vertical)
.margin(3)
.constraints([
Constraint::Length(3),
Constraint::Min(5),
Constraint::Length(3),
])
.split(f.area());
let version = env!("CARGO_PKG_VERSION");
let title_text = match app.current_model().name.as_str() {
"GPT-4o" => format!("Oli v{} - OpenAI API Key Setup", version),
name if name.contains("Local") => format!("Oli v{} - Local Model Setup", version),
_ => format!("Oli v{} - Anthropic API Key Setup", version),
};
let title = Paragraph::new(title_text)
.style(AppStyles::title())
.alignment(Alignment::Center);
f.render_widget(title, chunks[0]);
let info = create_api_key_info(app);
f.render_widget(info, chunks[1]);
let input_block = Block::default()
.borders(Borders::ALL)
.title(" API Key ")
.title_alignment(Alignment::Left)
.border_style(AppStyles::border());
app.textarea.set_block(input_block);
app.textarea.set_mask_char('*');
let input_block_with_padding = Block::default()
.borders(Borders::ALL)
.title(" API Key ")
.title_alignment(Alignment::Left)
.border_style(AppStyles::border())
.padding(Padding::new(3, 1, 0, 0));
app.textarea.set_block(input_block_with_padding);
f.render_widget(&app.textarea, chunks[2]);
add_prompt(f, chunks[2], ">");
}
pub fn draw_chat(f: &mut Frame, app: &mut App) {
let horizontal_split = Layout::default()
.direction(Direction::Horizontal)
.margin(1)
.constraints([
Constraint::Percentage(25), Constraint::Percentage(75), ])
.split(f.area());
let tasks_area = horizontal_split[0];
let task_list = create_task_list(app, tasks_area);
f.render_widget(task_list, tasks_area);
let chat_area = horizontal_split[1];
let line_count = app.textarea.lines().len();
let terminal_width = chat_area.width.saturating_sub(4); let wrapped_line_count = app
.textarea
.lines()
.iter()
.map(|line| {
if line.is_empty() {
1 } else {
(line.len() as u16)
.saturating_add(terminal_width)
.saturating_sub(1)
/ terminal_width
}
})
.sum::<u16>() as usize;
let _effective_line_count = line_count.max(wrapped_line_count);
let estimated_available_height = chat_area.height.saturating_sub(4);
let max_input_height = estimated_available_height / 5; let base_input_height = 3.max(max_input_height as usize);
let input_height = if app.show_command_menu {
let cmd_count = app.filtered_commands().len();
base_input_height + cmd_count.min(5)
} else {
base_input_height };
let shortcuts_height = if app.textarea.is_empty() {
if app.show_detailed_shortcuts {
4 } else if app.show_shortcuts_hint {
1 } else {
0 }
} else {
0 };
let chunks = Layout::default()
.direction(Direction::Vertical)
.constraints([
Constraint::Length(1), Constraint::Min(5), Constraint::Length(input_height as u16), Constraint::Length(shortcuts_height), ])
.split(chat_area);
let status_bar = create_status_bar(app);
let status_bar_widget = Paragraph::new(status_bar).style(Style::default());
f.render_widget(status_bar_widget, chunks[0]);
if app.show_logs {
let logs_widget = create_log_list(app, chunks[1]);
f.render_widget(logs_widget, chunks[1]);
} else {
let messages_widget = create_message_list(app, chunks[1]);
f.render_widget(messages_widget, chunks[1]);
}
if app.show_command_menu {
let input_chunks = Layout::default()
.direction(Direction::Vertical)
.constraints([
Constraint::Length(3), Constraint::Length(app.filtered_commands().len().min(5) as u16), ])
.split(chunks[2]);
let input_block = Block::default()
.borders(Borders::ALL)
.title(" Input (Type / for commands) ")
.title_alignment(Alignment::Left)
.border_style(AppStyles::border())
.padding(Padding::new(3, 1, 0, 0));
app.textarea.set_block(input_block);
f.render_widget(&app.textarea, input_chunks[0]);
add_prompt(f, input_chunks[0], ">");
let commands_list = create_command_menu(app);
f.render_widget(commands_list, input_chunks[1]);
} else {
let input_block = Block::default()
.borders(Borders::ALL)
.title(" Input (Type / for commands) ")
.title_alignment(Alignment::Left)
.border_style(AppStyles::border())
.padding(Padding::new(2, 1, 0, 0));
app.textarea.set_block(input_block);
f.render_widget(&app.textarea, chunks[2]);
add_prompt(f, chunks[2], ">");
}
if shortcuts_height > 0 {
let shortcuts_panel = create_shortcuts_panel(app);
f.render_widget(shortcuts_panel, chunks[3]);
}
}
pub fn draw_error(f: &mut Frame, error_msg: &str) {
let chunks = Layout::default()
.direction(Direction::Vertical)
.margin(3)
.constraints([
Constraint::Length(3),
Constraint::Min(5),
Constraint::Length(3),
])
.split(f.area());
let title = Paragraph::new("Error Occurred")
.style(Style::default().fg(Color::Red).add_modifier(Modifier::BOLD))
.alignment(Alignment::Center);
f.render_widget(title, chunks[0]);
let error_text = Paragraph::new(error_msg)
.block(
Block::default()
.borders(Borders::ALL)
.title(" Error Details ")
.title_alignment(Alignment::Left)
.border_style(Style::default().fg(Color::Red))
.padding(Padding::new(1, 1, 0, 0)),
)
.style(Style::default().fg(Color::Red))
.wrap(ratatui::widgets::Wrap { trim: true });
f.render_widget(error_text, chunks[1]);
let instruction = Paragraph::new("Press Enter to return to setup or Esc to exit")
.block(
Block::default()
.borders(Borders::ALL)
.border_style(Style::default().fg(Color::Yellow))
.padding(Padding::new(0, 0, 0, 0)),
)
.style(Style::default().fg(Color::Yellow))
.alignment(Alignment::Center);
f.render_widget(instruction, chunks[2]);
}
pub fn draw_permission_dialog(f: &mut Frame, app: &App) {
let area = f.area();
let width = std::cmp::min(72, area.width.saturating_sub(8));
let height = 10;
let x = (area.width - width) / 2;
let y = (area.height - height) / 2;
let dialog_area = Rect::new(x, y, width, height);
let dialog = create_permission_dialog(app, dialog_area);
f.render_widget(dialog, dialog_area);
let inner_area = Rect {
x: dialog_area.x + 1,
y: dialog_area.y + 1,
width: dialog_area.width.saturating_sub(2),
height: dialog_area.height.saturating_sub(2),
};
let info = create_permission_content(app);
f.render_widget(info, inner_area);
}