diff --git a/lumni/src/apps/builtin/llm/prompt/src/chat/db/user_profile/provider_config.rs b/lumni/src/apps/builtin/llm/prompt/src/chat/db/user_profile/provider_config.rs index 472d302..2cdd4c1 100644 --- a/lumni/src/apps/builtin/llm/prompt/src/chat/db/user_profile/provider_config.rs +++ b/lumni/src/apps/builtin/llm/prompt/src/chat/db/user_profile/provider_config.rs @@ -1,4 +1,3 @@ -use std::collections::HashMap; use std::path::PathBuf; use super::*; @@ -200,4 +199,13 @@ impl UserProfileDbHandler { }) .map_err(ApplicationError::from) } + + pub async fn rename_provider_config( + &self, + profile: &ProviderConfig, + new_name: &str, + ) -> Result<(), ApplicationError> { + eprintln!("TODO: Implement rename_provider_config"); + Ok(()) + } } diff --git a/lumni/src/apps/builtin/llm/prompt/src/tui/events/handle_prompt_window.rs b/lumni/src/apps/builtin/llm/prompt/src/tui/events/handle_prompt_window.rs index 64cf130..5237acc 100644 --- a/lumni/src/apps/builtin/llm/prompt/src/tui/events/handle_prompt_window.rs +++ b/lumni/src/apps/builtin/llm/prompt/src/tui/events/handle_prompt_window.rs @@ -7,7 +7,7 @@ use lumni::api::error::ApplicationError; use super::key_event::KeyTrack; use super::text_window_event::handle_text_window_event; use super::{ - AppUi, LineType, PromptAction, TextArea, TextWindowTrait, WindowEvent, + AppUi, LineType, PromptAction, PromptWindow, TextWindowTrait, WindowEvent, }; use crate::apps::builtin::llm::prompt::src::tui::WindowKind; pub use crate::external as lumni; @@ -110,7 +110,7 @@ pub fn handle_prompt_window_event( handle_text_window_event(key_track, &mut app_ui.prompt, is_running) } -fn is_closed_block(prompt_window: &mut TextArea) -> Option { +fn is_closed_block(prompt_window: &mut PromptWindow) -> Option { // return None if not inside a block // return Some(true) if block is closed, else return Some(false) let code_block = prompt_window.current_code_block(); @@ -121,7 +121,7 @@ fn is_closed_block(prompt_window: &mut TextArea) -> Option { } fn ensure_closed_block( - prompt_window: &mut TextArea, + prompt_window: &mut PromptWindow, ) -> Result<(), ApplicationError> { if let Some(closed_block) = is_closed_block(prompt_window) { if !closed_block { @@ -132,7 +132,7 @@ fn ensure_closed_block( Ok(()) } -fn in_editing_block(prompt_window: &mut TextArea) -> bool { +fn in_editing_block(prompt_window: &mut PromptWindow) -> bool { let line_type = prompt_window.current_line_type().unwrap_or(LineType::Text); match line_type { LineType::Code(block_line) => !block_line.is_end(), diff --git a/lumni/src/apps/builtin/llm/prompt/src/tui/events/key_event.rs b/lumni/src/apps/builtin/llm/prompt/src/tui/events/key_event.rs index e39bf5e..fbcf903 100644 --- a/lumni/src/apps/builtin/llm/prompt/src/tui/events/key_event.rs +++ b/lumni/src/apps/builtin/llm/prompt/src/tui/events/key_event.rs @@ -15,7 +15,7 @@ use super::{ pub struct KeyTrack { previous_key_str: Option, numeric_input: NumericInput, - current_key: KeyEvent, + pub current_key: KeyEvent, leader_key_set: bool, } @@ -29,6 +29,14 @@ impl KeyTrack { } } + pub fn process_key(&mut self, key_event: KeyEvent) { + if !self.leader_key_set { + self.update_previous_key(key_event); + } else { + self.update_previous_key_with_leader(key_event); + } + } + pub fn previous_key_str(&self) -> Option<&str> { self.previous_key_str.as_deref() } @@ -37,7 +45,7 @@ impl KeyTrack { self.current_key } - pub fn update_previous_key(&mut self, key_event: KeyEvent) { + fn update_previous_key(&mut self, key_event: KeyEvent) { if let KeyCode::Char(c) = self.current_key.code { // copy previous key_event to previous_char self.previous_key_str = Some(c.to_string()); @@ -56,7 +64,7 @@ impl KeyTrack { } } - pub fn update_previous_key_with_leader( + fn update_previous_key_with_leader( &mut self, key_event: KeyEvent, ) -> Option<&str> { @@ -163,15 +171,7 @@ impl KeyEventHandler { is_running: Arc, handler: &mut ConversationDbHandler, ) -> Result { - if !self.key_track.leader_key_set() - || self - .key_track - .update_previous_key_with_leader(key_event) - .is_none() - { - // leader key not set or updating leader is un-successful - self.key_track.update_previous_key(key_event); - } + self.key_track.process_key(key_event); // try to catch Shift+Enter key press in prompt window match current_mode { diff --git a/lumni/src/apps/builtin/llm/prompt/src/tui/events/mod.rs b/lumni/src/apps/builtin/llm/prompt/src/tui/events/mod.rs index 9fbb106..6e7285e 100644 --- a/lumni/src/apps/builtin/llm/prompt/src/tui/events/mod.rs +++ b/lumni/src/apps/builtin/llm/prompt/src/tui/events/mod.rs @@ -12,7 +12,7 @@ use super::clipboard::ClipboardProvider; use super::modals::{ModalAction, ModalWindowType}; use super::ui::AppUi; use super::window::{ - LineType, MoveCursor, TextArea, TextDocumentTrait, TextWindowTrait, + LineType, MoveCursor, PromptWindow, TextDocumentTrait, TextWindowTrait, WindowKind, }; use super::{ConversationDbHandler, NewConversation, ThreadedChatSession}; diff --git a/lumni/src/apps/builtin/llm/prompt/src/tui/mod.rs b/lumni/src/apps/builtin/llm/prompt/src/tui/mod.rs index 0ea1590..e4a2403 100644 --- a/lumni/src/apps/builtin/llm/prompt/src/tui/mod.rs +++ b/lumni/src/apps/builtin/llm/prompt/src/tui/mod.rs @@ -17,8 +17,8 @@ use lumni::api::error::ApplicationError; pub use modals::{ModalAction, ModalWindowTrait, ModalWindowType}; pub use ui::AppUi; pub use window::{ - CommandLine, ReadDocument, ReadWriteDocument, ResponseWindow, SimpleString, - TextArea, TextBuffer, TextDocumentTrait, TextLine, TextSegment, + CommandLine, PromptWindow, ReadDocument, ReadWriteDocument, ResponseWindow, + SimpleString, TextBuffer, TextDocumentTrait, TextLine, TextSegment, TextWindowTrait, WindowKind, }; diff --git a/lumni/src/apps/builtin/llm/prompt/src/tui/modals/conversations/mod.rs b/lumni/src/apps/builtin/llm/prompt/src/tui/modals/conversations/mod.rs index 611a9a3..055f63a 100644 --- a/lumni/src/apps/builtin/llm/prompt/src/tui/modals/conversations/mod.rs +++ b/lumni/src/apps/builtin/llm/prompt/src/tui/modals/conversations/mod.rs @@ -18,7 +18,7 @@ use ratatui::Frame; use super::{ ApplicationError, Conversation, ConversationDbHandler, ConversationStatus, KeyTrack, ModalAction, ModalWindowTrait, ModalWindowType, - PromptInstruction, TextArea, TextWindowTrait, ThreadedChatSession, + PromptInstruction, PromptWindow, TextWindowTrait, ThreadedChatSession, UserEvent, WindowEvent, }; use crate::apps::builtin::llm::prompt::src::chat::db::ConversationId; @@ -31,7 +31,7 @@ pub struct ConversationListModal<'a> { conversations: Vec, current_tab: ConversationStatus, tab_indices: HashMap, - edit_name_line: Option>, + edit_name_line: Option>, editing_index: Option, last_selected_conversation_id: Option, } @@ -234,7 +234,7 @@ impl<'a> ConversationListModal<'a> { async fn edit_conversation_name(&mut self) -> Result<(), ApplicationError> { if let Some(conversation) = self.get_current_conversation() { - let mut command_line = TextArea::new(); + let mut command_line = PromptWindow::new(); command_line.text_set(&conversation.name, None)?; command_line.set_status_insert(); self.edit_name_line = Some(command_line); diff --git a/lumni/src/apps/builtin/llm/prompt/src/tui/modals/filebrowser/mod.rs b/lumni/src/apps/builtin/llm/prompt/src/tui/modals/filebrowser/mod.rs index 22994d1..9760502 100644 --- a/lumni/src/apps/builtin/llm/prompt/src/tui/modals/filebrowser/mod.rs +++ b/lumni/src/apps/builtin/llm/prompt/src/tui/modals/filebrowser/mod.rs @@ -9,10 +9,10 @@ use ratatui::text::{Line, Span}; use ratatui::widgets::{Block, Borders, Clear, Paragraph}; use ratatui::Frame; +use super::widgets::{FileBrowserState, FileBrowserWidget}; use super::{ - ApplicationError, ConversationDbHandler, FileBrowserState, - FileBrowserWidget, KeyTrack, ModalAction, ModalWindowTrait, - ModalWindowType, ThreadedChatSession, WindowEvent, + ApplicationError, ConversationDbHandler, KeyTrack, ModalAction, + ModalWindowTrait, ModalWindowType, ThreadedChatSession, WindowEvent, }; pub use crate::external as lumni; diff --git a/lumni/src/apps/builtin/llm/prompt/src/tui/modals/mod.rs b/lumni/src/apps/builtin/llm/prompt/src/tui/modals/mod.rs index 21809ca..5405465 100644 --- a/lumni/src/apps/builtin/llm/prompt/src/tui/modals/mod.rs +++ b/lumni/src/apps/builtin/llm/prompt/src/tui/modals/mod.rs @@ -8,15 +8,15 @@ pub use filebrowser::FileBrowserModal; use ratatui::layout::Rect; use ratatui::Frame; pub use settings::SettingsModal; -use widgets::{FileBrowserState, FileBrowserWidget}; pub use super::widgets; use super::{ ApplicationError, Conversation, ConversationDbHandler, ConversationStatus, KeyTrack, MaskMode, ModelServer, ModelSpec, PromptInstruction, - ProviderConfig, ProviderConfigOptions, ServerTrait, SimpleString, TextArea, - TextLine, TextWindowTrait, ThreadedChatSession, UserEvent, UserProfile, - UserProfileDbHandler, WindowEvent, SUPPORTED_MODEL_ENDPOINTS, + PromptWindow, ProviderConfig, ProviderConfigOptions, ReadDocument, + ServerTrait, SimpleString, TextLine, TextWindowTrait, ThreadedChatSession, + UserEvent, UserProfile, UserProfileDbHandler, WindowEvent, + SUPPORTED_MODEL_ENDPOINTS, }; #[derive(Debug, Clone, PartialEq)] diff --git a/lumni/src/apps/builtin/llm/prompt/src/tui/modals/settings/list.rs b/lumni/src/apps/builtin/llm/prompt/src/tui/modals/settings/list.rs index 8be08e1..27af0d2 100644 --- a/lumni/src/apps/builtin/llm/prompt/src/tui/modals/settings/list.rs +++ b/lumni/src/apps/builtin/llm/prompt/src/tui/modals/settings/list.rs @@ -1,6 +1,6 @@ use super::*; -pub trait ListItem: Clone { +pub trait ListItemTrait: Clone { fn name(&self) -> &str; fn id(&self) -> i64; fn with_new_name(&self, new_name: String) -> Self; @@ -9,13 +9,13 @@ pub trait ListItem: Clone { Self: Sized; } -pub struct SettingsList { +pub struct SettingsList { items: Vec, selected_index: usize, pub default_item: Option, } -impl SettingsList { +impl SettingsList { pub fn new(items: Vec, default_item: Option) -> Self { let mut list = SettingsList { items, @@ -106,21 +106,30 @@ impl SettingsList { } } -impl SettingsListTrait for SettingsList { +impl SettingsListTrait for SettingsList { + type Item = T; + fn get_items(&self) -> Vec { self.get_items() } + fn get_selected_index(&self) -> usize { self.selected_index } + + fn get_selected_item(&self) -> Option<&Self::Item> { + self.items.get(self.selected_index) + } } pub trait SettingsListTrait { + type Item: ListItemTrait + SettingsItem; fn get_items(&self) -> Vec; fn get_selected_index(&self) -> usize; + fn get_selected_item(&self) -> Option<&Self::Item>; } -impl ListItem for UserProfile { +impl ListItemTrait for UserProfile { fn name(&self) -> &str { &self.name } @@ -141,7 +150,7 @@ impl ListItem for UserProfile { } } -impl ListItem for ProviderConfig { +impl ListItemTrait for ProviderConfig { fn name(&self) -> &str { &self.name } diff --git a/lumni/src/apps/builtin/llm/prompt/src/tui/modals/settings/manager.rs b/lumni/src/apps/builtin/llm/prompt/src/tui/modals/settings/manager.rs index 2971cdd..053e0cb 100644 --- a/lumni/src/apps/builtin/llm/prompt/src/tui/modals/settings/manager.rs +++ b/lumni/src/apps/builtin/llm/prompt/src/tui/modals/settings/manager.rs @@ -4,7 +4,6 @@ use async_trait::async_trait; use ratatui::prelude::*; use serde_json::{json, Value as JsonValue}; -use super::list::{ListItem, SettingsList}; use super::profile::{ ProfileCreationStep, ProfileCreator, SubPartCreationState, }; @@ -12,7 +11,7 @@ use super::provider::{ProviderCreationStep, ProviderCreator}; use super::*; #[async_trait] -pub trait ManagedItem: Clone + Send + Sync + ListItem { +pub trait ManagedItem: Clone + Send + Sync + ListItemTrait { async fn save( &self, db_handler: &mut UserProfileDbHandler, @@ -124,7 +123,7 @@ pub trait Creator: Send + Sync + 'static { &mut self, input: KeyEvent, ) -> Result, ApplicationError>; - fn render(&self, f: &mut Frame, area: Rect); + fn render(&mut self, f: &mut Frame, area: Rect); async fn create_item( &mut self, ) -> Result, ApplicationError>; @@ -143,8 +142,8 @@ pub struct SettingsManager { pub list: SettingsList, pub settings_editor: SettingsEditor, pub creator: Option>>, - rename_buffer: Option, - db_handler: UserProfileDbHandler, + pub rename_buffer: Option, + pub db_handler: UserProfileDbHandler, pub tab_focus: TabFocus, } @@ -233,11 +232,12 @@ impl Ok(WindowEvent::Modal(ModalAction::UpdateUI)) } KeyCode::Enter => { - if self.list.is_new_item_selected() { + if self.rename_buffer.is_some() { + // ensure this is matched before any other action + self.confirm_rename_item().await?; + } else if self.list.is_new_item_selected() { self.start_item_creation().await?; *tab_focus = TabFocus::Creation; - } else if self.rename_buffer.is_some() { - self.confirm_rename_item().await?; } else { *tab_focus = TabFocus::Settings; } @@ -259,16 +259,23 @@ impl Ok(WindowEvent::Modal(ModalAction::UpdateUI)) } } - KeyCode::Char('r') | KeyCode::Char('R') => { - self.start_item_renaming(); + KeyCode::Backspace if self.get_rename_buffer().is_some() => { + if let Some(buffer) = &mut self.rename_buffer { + buffer.pop(); + } Ok(WindowEvent::Modal(ModalAction::UpdateUI)) } KeyCode::Char(c) if self.rename_buffer.is_some() => { + // ensure this is matched before any other char if let Some(buffer) = &mut self.rename_buffer { buffer.push(c); } Ok(WindowEvent::Modal(ModalAction::UpdateUI)) } + KeyCode::Char('r') | KeyCode::Char('R') => { + self.start_item_renaming(); + Ok(WindowEvent::Modal(ModalAction::UpdateUI)) + } KeyCode::Backspace if self.rename_buffer.is_some() => { if let Some(buffer) = &mut self.rename_buffer { buffer.pop(); @@ -412,7 +419,7 @@ impl Ok(()) } - fn start_item_renaming(&mut self) { + pub fn start_item_renaming(&mut self) { if let Some(item) = self.list.get_selected_item() { self.rename_buffer = Some(item.name().to_string()); } @@ -423,8 +430,8 @@ impl (&self.rename_buffer, self.list.get_selected_item()) { if !new_name.is_empty() { - let updated_item = item.with_new_name(new_name.clone()); - updated_item.save(&mut self.db_handler).await?; + // Use type-specific renaming method + self.rename_item(item, new_name).await?; self.list.rename_selected_item(new_name.clone()); } } @@ -432,7 +439,25 @@ impl Ok(()) } - fn cancel_rename_item(&mut self) { + async fn rename_item( + &self, + item: &T, + new_name: &str, + ) -> Result<(), ApplicationError> { + if let Some(profile) = (item as &dyn Any).downcast_ref::() + { + self.db_handler.rename_profile(profile, new_name).await?; + } else if let Some(provider) = + (item as &dyn Any).downcast_ref::() + { + self.db_handler + .rename_provider_config(provider, new_name) + .await?; + } + Ok(()) + } + + pub fn cancel_rename_item(&mut self) { self.rename_buffer = None; } @@ -637,7 +662,7 @@ impl Creator for ProfileCreator { self.handle_key_event(input).await } - fn render(&self, f: &mut Frame, area: Rect) { + fn render(&mut self, f: &mut Frame, area: Rect) { match self.sub_part_creation_state { SubPartCreationState::NotCreating => match self.creation_step { ProfileCreationStep::EnterName => { @@ -653,7 +678,7 @@ impl Creator for ProfileCreator { self.render_creating_profile(f, area) } }, - SubPartCreationState::CreatingProvider(ref creator) => { + SubPartCreationState::CreatingProvider(ref mut creator) => { creator.render(f, area); } } @@ -679,7 +704,7 @@ impl Creator for ProviderCreator { self.handle_key_event(input).await } - fn render(&self, f: &mut Frame, area: Rect) { + fn render(&mut self, f: &mut Frame, area: Rect) { match self.current_step { ProviderCreationStep::EnterName => self.render_enter_name(f, area), ProviderCreationStep::SelectProviderType => { diff --git a/lumni/src/apps/builtin/llm/prompt/src/tui/modals/settings/mod.rs b/lumni/src/apps/builtin/llm/prompt/src/tui/modals/settings/mod.rs index 241b79c..bdf07eb 100644 --- a/lumni/src/apps/builtin/llm/prompt/src/tui/modals/settings/mod.rs +++ b/lumni/src/apps/builtin/llm/prompt/src/tui/modals/settings/mod.rs @@ -2,24 +2,26 @@ mod list; mod manager; mod profile; mod provider; -mod renderer; mod settings_editor; use async_trait::async_trait; use crossterm::event::{KeyCode, KeyEvent}; -use list::SettingsListTrait; +use list::{ListItemTrait, SettingsList, SettingsListTrait}; use manager::{Creator, CreatorAction, SettingsManager}; -use ratatui::layout::Rect; -use ratatui::widgets::Clear; +use ratatui::layout::{Alignment, Constraint, Direction, Layout, Rect}; +use ratatui::style::{Color, Modifier, Style}; +use ratatui::text::{Line, Span}; +use ratatui::widgets::{ + Block, Borders, Clear, List, ListItem, ListState, Paragraph, Tabs, +}; use ratatui::Frame; -use renderer::SettingsRenderer; use settings_editor::{SettingsAction, SettingsEditor}; -use super::widgets::{TextAreaState, TextAreaWidget}; +use super::widgets::TextArea; use super::{ ApplicationError, ConversationDbHandler, KeyTrack, MaskMode, ModalAction, ModalWindowTrait, ModalWindowType, ModelServer, ModelSpec, ProviderConfig, - ProviderConfigOptions, ServerTrait, SimpleString, TextLine, + ProviderConfigOptions, ReadDocument, ServerTrait, SimpleString, TextLine, ThreadedChatSession, UserProfile, UserProfileDbHandler, WindowEvent, SUPPORTED_MODEL_ENDPOINTS, }; @@ -49,12 +51,60 @@ pub enum EditTab { Profiles, Providers, } + +pub enum SettingsManagerEnum { + Profile(SettingsManager), + Provider(SettingsManager), +} + +impl SettingsManagerEnum { + fn get_selected_item(&self) -> Option<&dyn SettingsItem> { + match self { + SettingsManagerEnum::Profile(manager) => manager + .list + .get_selected_item() + .map(|item| item as &dyn SettingsItem), + SettingsManagerEnum::Provider(manager) => manager + .list + .get_selected_item() + .map(|item| item as &dyn SettingsItem), + } + } + + fn get_settings_editor(&self) -> &SettingsEditor { + match self { + SettingsManagerEnum::Profile(manager) => &manager.settings_editor, + SettingsManagerEnum::Provider(manager) => &manager.settings_editor, + } + } + + fn get_rename_buffer(&self) -> Option<&String> { + match self { + SettingsManagerEnum::Profile(manager) => { + manager.rename_buffer.as_ref() + } + SettingsManagerEnum::Provider(manager) => { + manager.rename_buffer.as_ref() + } + } + } + + fn cancel_rename_item(&mut self) { + match self { + SettingsManagerEnum::Profile(manager) => { + manager.cancel_rename_item() + } + SettingsManagerEnum::Provider(manager) => { + manager.cancel_rename_item() + } + } + } +} + pub struct SettingsModal { pub current_tab: EditTab, pub tab_focus: TabFocus, - pub profile_manager: SettingsManager, - pub provider_manager: SettingsManager, - renderer: SettingsRenderer, + pub manager: SettingsManagerEnum, } impl SettingsModal { @@ -64,9 +114,9 @@ impl SettingsModal { Ok(Self { current_tab: EditTab::Profiles, tab_focus: TabFocus::List, - profile_manager: SettingsManager::new(db_handler.clone()).await?, - provider_manager: SettingsManager::new(db_handler.clone()).await?, - renderer: SettingsRenderer::new(), + manager: SettingsManagerEnum::Profile( + SettingsManager::new(db_handler).await?, + ), }) } @@ -74,7 +124,6 @@ impl SettingsModal { &mut self, key_event: KeyEvent, ) -> Result { - // Handle common cases for List and Settings tab focus if matches!(self.tab_focus, TabFocus::List | TabFocus::Settings) { match key_event.code { KeyCode::Tab => { @@ -85,7 +134,7 @@ impl SettingsModal { if self.tab_focus == TabFocus::Settings { self.tab_focus = TabFocus::List; return Ok(WindowEvent::Modal(ModalAction::UpdateUI)); - } else { + } else if !self.manager.get_rename_buffer().is_some() { return Ok(WindowEvent::PromptWindow(None)); } } @@ -93,15 +142,14 @@ impl SettingsModal { } } - // Pass all other key events to the respective manager - match self.current_tab { - EditTab::Profiles => { - self.profile_manager + match &mut self.manager { + SettingsManagerEnum::Profile(manager) => { + manager .handle_key_event(key_event, &mut self.tab_focus) .await } - EditTab::Providers => { - self.provider_manager + SettingsManagerEnum::Provider(manager) => { + manager .handle_key_event(key_event, &mut self.tab_focus) .await } @@ -109,81 +157,303 @@ impl SettingsModal { } async fn switch_tab(&mut self) -> Result<(), ApplicationError> { - self.current_tab = match self.current_tab { - EditTab::Profiles => EditTab::Providers, - EditTab::Providers => EditTab::Profiles, + let db_handler = self.get_db_handler().clone(); + self.manager = match self.current_tab { + EditTab::Profiles => { + self.current_tab = EditTab::Providers; + SettingsManagerEnum::Provider( + SettingsManager::new(db_handler).await?, + ) + } + EditTab::Providers => { + self.current_tab = EditTab::Profiles; + SettingsManagerEnum::Profile( + SettingsManager::new(db_handler).await?, + ) + } }; self.tab_focus = TabFocus::List; self.refresh_list().await?; Ok(()) } - pub fn get_current_list(&self) -> &dyn SettingsListTrait { - match self.current_tab { - EditTab::Profiles => &self.profile_manager.list, - EditTab::Providers => &self.provider_manager.list, - } - } - - pub fn get_current_settings_editor(&self) -> &SettingsEditor { - match self.current_tab { - EditTab::Profiles => &self.profile_manager.settings_editor, - EditTab::Providers => &self.provider_manager.settings_editor, + fn get_db_handler(&self) -> &UserProfileDbHandler { + match &self.manager { + SettingsManagerEnum::Profile(m) => &m.db_handler, + SettingsManagerEnum::Provider(m) => &m.db_handler, } } pub async fn refresh_list( &mut self, ) -> Result { - match self.current_tab { - EditTab::Profiles => self.profile_manager.refresh_list().await, - EditTab::Providers => self.provider_manager.refresh_list().await, + match &mut self.manager { + SettingsManagerEnum::Profile(manager) => { + manager.refresh_list().await + } + SettingsManagerEnum::Provider(manager) => { + manager.refresh_list().await + } } } - pub fn get_rename_buffer(&self) -> Option<&String> { - match self.current_tab { - EditTab::Profiles => self.profile_manager.get_rename_buffer(), - EditTab::Providers => self.provider_manager.get_rename_buffer(), - } + fn render_tab_bar(&self, f: &mut Frame, area: Rect) { + let tabs = vec!["Profiles", "Providers"]; + let tab_index = match self.current_tab { + EditTab::Profiles => 0, + EditTab::Providers => 1, + }; + let tabs = Tabs::new(tabs) + .block(Block::default().borders(Borders::ALL)) + .select(tab_index) + .style(Style::default().fg(Color::Cyan)) + .highlight_style(Style::default().fg(Color::Yellow)); + f.render_widget(tabs, area); } - fn render_settings(&self, f: &mut Frame, area: Rect) { - match self.current_tab { - EditTab::Profiles => self.renderer.render_settings( - f, - area, - self.profile_manager.list.get_selected_item(), - &self.profile_manager.settings_editor, - ), - EditTab::Providers => self.renderer.render_settings( - f, - area, - self.provider_manager.list.get_selected_item(), - &self.provider_manager.settings_editor, - ), - } + fn render_list(&self, f: &mut Frame, area: Rect) { + let items = match &self.manager { + SettingsManagerEnum::Profile(manager) => { + self.render_list_items(&manager.list) + } + SettingsManagerEnum::Provider(manager) => { + self.render_list_items(&manager.list) + } + }; + + let title = match self.current_tab { + EditTab::Profiles => "Profiles", + EditTab::Providers => "Providers", + }; + + let list = List::new(items) + .block(Block::default().borders(Borders::ALL).title(title)) + .highlight_style(Style::default().add_modifier(Modifier::BOLD)) + .highlight_symbol("> "); + + let mut list_state = ListState::default(); + list_state.select(Some(self.get_selected_index())); + + f.render_stateful_widget(list, area, &mut list_state); + } + + fn render_list_items( + &self, + list: &SettingsList, + ) -> Vec { + list.get_items() + .iter() + .enumerate() + .map(|(i, item)| { + let content = if i == list.get_selected_index() + && self.manager.get_rename_buffer().is_some() + { + self.manager.get_rename_buffer().unwrap().clone() + } else { + item.to_string() + }; + + let style = if i == list.get_selected_index() { + Style::default().bg(Color::Rgb(40, 40, 40)).fg(Color::White) + } else if i == list.get_items().len() - 1 { + Style::default().fg(Color::Green) + } else if item.ends_with("(default)") { + Style::default().fg(Color::Yellow) + } else { + Style::default().fg(Color::Cyan) + }; + ListItem::new(Span::styled(content, style)) + }) + .collect() } - fn render_content(f: &mut Frame, area: Rect, modal: &SettingsModal) { - match modal.tab_focus { + fn render_content(&mut self, f: &mut Frame, area: Rect) { + match self.tab_focus { TabFocus::Settings | TabFocus::List => { - modal.render_settings(f, area); + self.render_settings(f, area); } - TabFocus::Creation => match modal.current_tab { - EditTab::Profiles => { - if let Some(creator) = &modal.profile_manager.creator { + TabFocus::Creation => match &mut self.manager { + SettingsManagerEnum::Profile(manager) => { + if let Some(creator) = &mut manager.creator { creator.render(f, area); } } - EditTab::Providers => { - if let Some(creator) = &modal.provider_manager.creator { + SettingsManagerEnum::Provider(manager) => { + if let Some(creator) = &mut manager.creator { creator.render(f, area); } } }, } } + + fn render_settings(&self, f: &mut Frame, area: Rect) { + let item = self.manager.get_selected_item(); + let settings_editor = self.manager.get_settings_editor(); + + if let Some(item) = item { + let settings = settings_editor.get_settings(); + let mut items: Vec = settings + .as_object() + .unwrap() + .iter() + .enumerate() + .map(|(i, (key, value))| { + let is_editable = !key.starts_with("__"); + let display_value = + settings_editor.get_display_value(value); + + let content = if settings_editor.edit_mode + == EditMode::EditingValue + && i == settings_editor.get_current_field() + && is_editable + { + format!( + "{}: {}", + key, + settings_editor.get_edit_buffer() + ) + } else { + format!("{}: {}", key, display_value) + }; + + let style = if i == settings_editor.get_current_field() { + Style::default() + .bg(Color::Rgb(40, 40, 40)) + .fg(Color::White) + } else if is_editable { + Style::default().bg(Color::Black).fg(Color::Cyan) + } else { + Style::default().bg(Color::Black).fg(Color::DarkGray) + }; + ListItem::new(Line::from(vec![Span::styled( + content, style, + )])) + }) + .collect(); + + // Add new key input field if in AddingNewKey mode + if settings_editor.edit_mode == EditMode::AddingNewKey { + let secure_indicator = if settings_editor.is_new_value_secure() + { + "🔒 " + } else { + "" + }; + items.push(ListItem::new(Line::from(vec![Span::styled( + format!( + "{}New key: {}", + secure_indicator, + settings_editor.get_new_key_buffer() + ), + Style::default() + .bg(Color::Rgb(40, 40, 40)) + .fg(Color::White), + )]))); + } + + // Add new value input field if in AddingNewValue mode + if settings_editor.edit_mode == EditMode::AddingNewValue { + let secure_indicator = if settings_editor.is_new_value_secure() + { + "🔒 " + } else { + "" + }; + items.push(ListItem::new(Line::from(vec![Span::styled( + format!( + "{}{}: {}", + secure_indicator, + settings_editor.get_new_key_buffer(), + settings_editor.get_edit_buffer() + ), + Style::default() + .bg(Color::Rgb(40, 40, 40)) + .fg(Color::White), + )]))); + } + + let list = List::new(items) + .block(Block::default().borders(Borders::ALL).title(format!( + "{} Settings: {}", + item.item_type(), + item.name() + ))) + .highlight_style(Style::default().add_modifier(Modifier::BOLD)) + .highlight_symbol(">> "); + + let mut state = ListState::default(); + state.select(Some(settings_editor.get_current_field())); + + f.render_stateful_widget(list, area, &mut state); + } else { + let paragraph = Paragraph::new("No item selected").block( + Block::default().borders(Borders::ALL).title("Settings"), + ); + f.render_widget(paragraph, area); + } + } + + fn render_instructions(&self, f: &mut Frame, area: Rect) { + let instructions = match (self.current_tab, self.tab_focus) { + (EditTab::Profiles, TabFocus::List) => { + "↑↓: Navigate | Enter: Select | R: Rename | D: Delete | Space: \ + Set Default | Tab: Switch Tab" + } + (EditTab::Providers, TabFocus::List) => { + "↑↓: Navigate | Enter: Select | R: Rename | D: Delete | Tab: \ + Switch Tab" + } + (_, TabFocus::Settings) => { + let settings_editor = match &self.manager { + SettingsManagerEnum::Profile(m) => &m.settings_editor, + SettingsManagerEnum::Provider(m) => &m.settings_editor, + }; + match settings_editor.edit_mode { + EditMode::NotEditing => { + "↑↓: Navigate | Enter: Edit | n: New | N: New Secure | \ + D: Delete | C: Clear | S: Show/Hide Secure | \ + ←/Tab/q/Esc: Back to List" + } + EditMode::EditingValue => "Enter: Save | Esc: Cancel", + EditMode::AddingNewKey => { + "Enter: Confirm Key | Esc: Cancel" + } + EditMode::AddingNewValue => { + "Enter: Save New Value | Esc: Cancel" + } + } + } + (_, TabFocus::Creation) => "Enter: Create | Esc: Cancel", + }; + + let simple_string = SimpleString::from(instructions); + let wrapped_spans = simple_string.wrapped_spans( + area.width as usize - 2, // Subtract 2 for left and right borders + Some(Style::default().fg(Color::Cyan)), + Some(" | "), + ); + + let wrapped_text: Vec = + wrapped_spans.into_iter().map(Line::from).collect(); + + let paragraph = Paragraph::new(wrapped_text) + .alignment(Alignment::Center) + .block(Block::default().borders(Borders::TOP)); + + f.render_widget(paragraph, area); + } + + fn get_selected_index(&self) -> usize { + match &self.manager { + SettingsManagerEnum::Profile(manager) => { + manager.list.get_selected_index() + } + SettingsManagerEnum::Provider(manager) => { + manager.list.get_selected_index() + } + } + } } #[async_trait] @@ -194,24 +464,44 @@ impl ModalWindowTrait for SettingsModal { fn render_on_frame(&mut self, frame: &mut Frame, area: Rect) { frame.render_widget(Clear, area); - self.renderer - .render_layout(frame, area, self, &Self::render_content); + + let chunks = Layout::default() + .direction(Direction::Vertical) + .constraints([ + Constraint::Length(3), // Tab bar + Constraint::Min(1), // Main content + Constraint::Length(3), // Instructions + ]) + .split(area); + + self.render_tab_bar(frame, chunks[0]); + + let main_chunks = Layout::default() + .direction(Direction::Horizontal) + .constraints([ + Constraint::Percentage(30), + Constraint::Percentage(70), + ]) + .split(chunks[1]); + + self.render_list(frame, main_chunks[0]); + self.render_content(frame, main_chunks[1]); + + self.render_instructions(frame, chunks[2]); } async fn poll_background_task( &mut self, ) -> Result { - match self.current_tab { - EditTab::Profiles => { + match &mut self.manager { + SettingsManagerEnum::Profile(manager) => { if let TabFocus::Creation = self.tab_focus { - if let Some(creator) = &mut self.profile_manager.creator { + if let Some(creator) = &mut manager.creator { if let Some(action) = creator.poll_background_task() { match action { CreatorAction::Finish(new_profile) => { - self.profile_manager - .list - .add_item(new_profile); - self.profile_manager.creator = None; + manager.list.add_item(new_profile); + manager.creator = None; self.tab_focus = TabFocus::List; return Ok(WindowEvent::Modal( ModalAction::PollBackGroundTask, @@ -228,7 +518,7 @@ impl ModalWindowTrait for SettingsModal { } } } - EditTab::Providers => { + SettingsManagerEnum::Provider(_) => { // Provider creation is instant and does not have background tasks } } @@ -241,6 +531,31 @@ impl ModalWindowTrait for SettingsModal { _tab_chat: Option<&'b mut ThreadedChatSession>, _handler: &mut ConversationDbHandler, ) -> Result { - self.handle_key_event(key_event.current_key().clone()).await + self.handle_key_event(key_event.current_key()).await + } +} + +pub trait SettingsItem { + fn name(&self) -> &str; + fn item_type(&self) -> &'static str; +} + +impl SettingsItem for UserProfile { + fn name(&self) -> &str { + &self.name + } + + fn item_type(&self) -> &'static str { + "Profile" + } +} + +impl SettingsItem for ProviderConfig { + fn name(&self) -> &str { + &self.name + } + + fn item_type(&self) -> &'static str { + "Provider" } } diff --git a/lumni/src/apps/builtin/llm/prompt/src/tui/modals/settings/profile/creator.rs b/lumni/src/apps/builtin/llm/prompt/src/tui/modals/settings/profile/creator.rs index 9a5dc11..8d4e222 100644 --- a/lumni/src/apps/builtin/llm/prompt/src/tui/modals/settings/profile/creator.rs +++ b/lumni/src/apps/builtin/llm/prompt/src/tui/modals/settings/profile/creator.rs @@ -1,8 +1,6 @@ use std::time::Instant; -use ratatui::layout::{Alignment, Constraint, Direction, Layout, Margin}; -use ratatui::style::{Color, Modifier, Style}; -use ratatui::widgets::{Block, Borders, List, ListItem, ListState, Paragraph}; +use ratatui::layout::Margin; use serde_json::json; use tokio::sync::mpsc; @@ -33,6 +31,7 @@ pub struct ProfileCreator { provider_configs: Vec, selected_provider_index: usize, pub sub_part_creation_state: SubPartCreationState, + text_area: Option>, } impl ProfileCreator { @@ -52,6 +51,7 @@ impl ProfileCreator { provider_configs, selected_provider_index: 0, sub_part_creation_state: SubPartCreationState::NotCreating, + text_area: None, }) } @@ -184,6 +184,7 @@ impl ProfileCreator { .clone(), ); self.creation_step = ProfileCreationStep::ConfirmCreate; + self.initialize_confirm_create_state(); } } KeyCode::Esc | KeyCode::Backspace => { @@ -194,16 +195,32 @@ impl ProfileCreator { Ok(CreatorAction::Continue) } + fn initialize_confirm_create_state(&mut self) { + let text_lines = self.create_confirm_details(); + self.text_area = Some(TextArea::with_read_document(Some(text_lines))); + } + pub fn handle_confirm_create( &mut self, input: KeyEvent, ) -> Result, ApplicationError> { match input.code { KeyCode::Enter => { + self.text_area = None; self.creation_step = ProfileCreationStep::CreatingProfile; Ok(CreatorAction::CreateItem) } - _ => Ok(CreatorAction::Continue), + KeyCode::Esc => { + self.text_area = None; + self.go_to_previous_step() + } + _ => { + // Forward all other key events to the TextAreaState + if let Some(text_area) = &mut self.text_area { + text_area.handle_key_event(input); + } + Ok(CreatorAction::Continue) + } } } @@ -329,7 +346,7 @@ impl ProfileCreator { f.render_stateful_widget(list, area, &mut state); } - pub fn render_confirm_create(&self, f: &mut Frame, area: Rect) { + pub fn render_confirm_create(&mut self, f: &mut Frame, area: Rect) { let chunks = Layout::default() .direction(Direction::Vertical) .constraints([Constraint::Min(1), Constraint::Length(3)]) @@ -342,20 +359,20 @@ impl ProfileCreator { ) .split(chunks[0]); - let text_lines = self.create_confirm_details(); - let text_area_widget = TextAreaWidget::new(); - let mut text_area_state = - TextAreaState::with_read_document(Some(text_lines)); - let text_area_block = Block::default() .borders(Borders::ALL) .title("Profile Details"); - f.render_stateful_widget( - &text_area_widget, - content_area[0].inner(Margin::new(1, 1)), - &mut text_area_state, - ); + if let Some(text_area) = &mut self.text_area { + text_area.render(f, content_area[0].inner(Margin::new(1, 1))); + } else { + let fallback_text = Paragraph::new("No profile details available.") + .style(Style::default().fg(Color::Red)); + f.render_widget( + fallback_text, + content_area[0].inner(Margin::new(1, 1)), + ); + } f.render_widget(text_area_block, content_area[0]); diff --git a/lumni/src/apps/builtin/llm/prompt/src/tui/modals/settings/provider/creator.rs b/lumni/src/apps/builtin/llm/prompt/src/tui/modals/settings/provider/creator.rs index 417bfee..1c16136 100644 --- a/lumni/src/apps/builtin/llm/prompt/src/tui/modals/settings/provider/creator.rs +++ b/lumni/src/apps/builtin/llm/prompt/src/tui/modals/settings/provider/creator.rs @@ -1,9 +1,8 @@ use std::collections::HashMap; -use ratatui::layout::{Alignment, Constraint, Direction, Layout, Margin}; -use ratatui::style::{Color, Modifier, Style}; -use ratatui::text::{Line, Span, Text}; -use ratatui::widgets::{Block, Borders, List, ListItem, ListState, Paragraph}; +use hmac::crypto_mac::Key; +use ratatui::layout::Margin; +use ratatui::text::Text; use serde_json::Value as JsonValue; use super::*; @@ -32,6 +31,7 @@ pub struct ProviderCreator { edit_buffer: String, is_editing: bool, model_fetch_error: Option, + text_area: Option>, } impl ProviderCreator { @@ -51,10 +51,11 @@ impl ProviderCreator { edit_buffer: String::new(), is_editing: false, model_fetch_error: None, + text_area: None, }) } - pub fn render(&self, f: &mut Frame, area: Rect) { + pub fn render(&mut self, f: &mut Frame, area: Rect) { match self.current_step { ProviderCreationStep::EnterName => self.render_enter_name(f, area), ProviderCreationStep::SelectProviderType => { @@ -75,7 +76,7 @@ impl ProviderCreator { } } - pub fn render_confirm_create(&self, f: &mut Frame, area: Rect) { + pub fn render_confirm_create(&mut self, f: &mut Frame, area: Rect) { let chunks = Layout::default() .direction(Direction::Vertical) .constraints([Constraint::Min(1), Constraint::Length(3)]) @@ -86,20 +87,21 @@ impl ProviderCreator { .constraints([Constraint::Min(1), Constraint::Length(1)]) .split(chunks[0]); - let text_lines = self.create_confirm_details(); - let text_area_widget = TextAreaWidget::new(); - let mut text_area_state = - TextAreaState::with_read_document(Some(text_lines)); - let text_area_block = Block::default() .borders(Borders::ALL) .title("Provider Details"); - f.render_stateful_widget( - &text_area_widget, - content_area[0].inner(Margin::new(1, 1)), - &mut text_area_state, - ); + if let Some(text_area) = &mut self.text_area { + text_area.render(f, content_area[0].inner(Margin::new(1, 1))); + } else { + let fallback_text = + Paragraph::new("No provider details available.") + .style(Style::default().fg(Color::Red)); + f.render_widget( + fallback_text, + content_area[0].inner(Margin::new(1, 1)), + ); + } f.render_widget(text_area_block, content_area[0]); @@ -126,10 +128,14 @@ impl ProviderCreator { f.render_widget(create_button, button_chunks[1]); } + fn initialize_confirm_create_state(&mut self) { + let text_lines = self.create_confirm_details(); + self.text_area = Some(TextArea::with_read_document(Some(text_lines))); + } + fn create_confirm_details(&self) -> Vec { let mut lines = Vec::new(); - // Name let mut name_line = TextLine::new(); name_line.add_segment( "Name:", @@ -213,17 +219,23 @@ impl ProviderCreator { input: KeyEvent, ) -> Result, ApplicationError> { match input.code { - KeyCode::Esc => return self.go_to_previous_step(), - KeyCode::Backspace => { - if self.current_step == ProviderCreationStep::EnterName - && !self.name.is_empty() - { + KeyCode::Esc => { + if self.current_step == ProviderCreationStep::ConfirmCreate { + self.text_area = None; + } + return self.go_to_previous_step(); + } + KeyCode::Backspace => match self.current_step { + ProviderCreationStep::EnterName if !self.name.is_empty() => { self.name.pop(); return Ok(CreatorAction::Continue); - } else { + } + ProviderCreationStep::ConfirmCreate => { + self.text_area = None; return self.go_to_previous_step(); } - } + _ => return self.go_to_previous_step(), + }, _ => {} } @@ -594,6 +606,7 @@ impl ProviderCreator { self.is_editing = false; if self.is_last_setting() { self.current_step = ProviderCreationStep::ConfirmCreate; + self.initialize_confirm_create_state(); return Ok(CreatorAction::Continue); } else { self.move_setting_selection(1); @@ -607,6 +620,7 @@ impl ProviderCreator { self.save_current_setting(); } self.current_step = ProviderCreationStep::ConfirmCreate; + self.initialize_confirm_create_state(); return Ok(CreatorAction::Continue); } KeyCode::Char(c) => { @@ -676,7 +690,17 @@ impl ProviderCreator { let new_config = self.create_provider().await?; Ok(CreatorAction::Finish(new_config)) } - _ => Ok(CreatorAction::Continue), + KeyCode::Esc => { + self.text_area = None; + self.go_to_previous_step() + } + _ => { + // Forward other key events to the TextArea + if let Some(text_area) = &mut self.text_area { + text_area.handle_key_event(input); + } + Ok(CreatorAction::Continue) + } } } diff --git a/lumni/src/apps/builtin/llm/prompt/src/tui/modals/settings/renderer.rs b/lumni/src/apps/builtin/llm/prompt/src/tui/modals/settings/renderer.rs deleted file mode 100644 index 04901c1..0000000 --- a/lumni/src/apps/builtin/llm/prompt/src/tui/modals/settings/renderer.rs +++ /dev/null @@ -1,302 +0,0 @@ -use ratatui::prelude::*; -use ratatui::widgets::{ - Block, Borders, List, ListItem, ListState, Paragraph, Tabs, -}; - -use super::settings_editor::SettingsEditor; -use super::{ - EditMode, EditTab, ProviderConfig, SettingsModal, SimpleString, TabFocus, - UserProfile, -}; - -pub struct SettingsRenderer; - -impl SettingsRenderer { - pub fn new() -> Self { - SettingsRenderer - } - - pub fn render_layout( - &self, - f: &mut Frame, - area: Rect, - modal: &SettingsModal, - content_renderer: &dyn Fn(&mut Frame, Rect, &SettingsModal), - ) { - let chunks = Layout::default() - .direction(Direction::Vertical) - .constraints([ - Constraint::Length(3), // Tab bar - Constraint::Min(1), // Main content - Constraint::Length(3), // Instructions - ]) - .split(area); - - self.render_tab_bar(f, chunks[0], modal.current_tab); - - let main_chunks = Layout::default() - .direction(Direction::Horizontal) - .constraints([ - Constraint::Percentage(30), - Constraint::Percentage(70), - ]) - .split(chunks[1]); - - self.render_list(f, main_chunks[0], modal); - content_renderer(f, main_chunks[1], modal); - - self.render_instructions(f, chunks[2], modal); - } - - fn render_tab_bar(&self, f: &mut Frame, area: Rect, current_tab: EditTab) { - let tabs = vec!["Profiles", "Providers"]; - let tab_index = match current_tab { - EditTab::Profiles => 0, - EditTab::Providers => 1, - }; - let tabs = Tabs::new(tabs) - .block(Block::default().borders(Borders::ALL)) - .select(tab_index) - .style(Style::default().fg(Color::Cyan)) - .highlight_style(Style::default().fg(Color::Yellow)); - f.render_widget(tabs, area); - } - - fn render_list(&self, f: &mut Frame, area: Rect, modal: &SettingsModal) { - let items: Vec = modal - .get_current_list() - .get_items() - .into_iter() - .enumerate() - .map(|(i, item)| { - // check if item is default - let content = if i - == modal.get_current_list().get_selected_index() - && modal.get_rename_buffer().is_some() - { - format!("{}", modal.get_rename_buffer().unwrap()) - } else { - item.clone() - }; - - let style = if i - == modal.get_current_list().get_selected_index() - { - Style::default().bg(Color::Rgb(40, 40, 40)).fg(Color::White) - } else if i == modal.get_current_list().get_items().len() - 1 { - Style::default().fg(Color::Green) - } else if item.ends_with("(default)") { - // Special style for default profile - Style::default().fg(Color::Yellow) - } else { - Style::default().fg(Color::Cyan) - }; - ListItem::new(Span::styled(content, style)) - }) - .collect(); - - let title = match modal.current_tab { - EditTab::Profiles => "Profiles", - EditTab::Providers => "Providers", - }; - - let list = List::new(items) - .block(Block::default().borders(Borders::ALL).title(title)) - .highlight_style(Style::default().add_modifier(Modifier::BOLD)) - .highlight_symbol("> "); - - let mut list_state = ListState::default(); - list_state.select(Some(modal.get_current_list().get_selected_index())); - - f.render_stateful_widget(list, area, &mut list_state); - } - - pub fn render_settings( - &self, - f: &mut Frame, - area: Rect, - item: Option<&T>, - settings_editor: &SettingsEditor, - ) { - if let Some(item) = item { - let settings = settings_editor.get_settings(); - let mut items: Vec = settings - .as_object() - .unwrap() - .iter() - .enumerate() - .map(|(i, (key, value))| { - let is_editable = !key.starts_with("__"); - let display_value = - settings_editor.get_display_value(value); - - let content = if settings_editor.edit_mode - == EditMode::EditingValue - && i == settings_editor.get_current_field() - && is_editable - { - format!( - "{}: {}", - key, - settings_editor.get_edit_buffer() - ) - } else { - format!("{}: {}", key, display_value) - }; - - let style = if i == settings_editor.get_current_field() { - Style::default() - .bg(Color::Rgb(40, 40, 40)) - .fg(Color::White) - } else if is_editable { - Style::default().bg(Color::Black).fg(Color::Cyan) - } else { - Style::default().bg(Color::Black).fg(Color::DarkGray) - }; - ListItem::new(Line::from(vec![Span::styled( - content, style, - )])) - }) - .collect(); - - // Add new key input field if in AddingNewKey mode - if settings_editor.edit_mode == EditMode::AddingNewKey { - let secure_indicator = if settings_editor.is_new_value_secure() - { - "🔒 " - } else { - "" - }; - items.push(ListItem::new(Line::from(vec![Span::styled( - format!( - "{}New key: {}", - secure_indicator, - settings_editor.get_new_key_buffer() - ), - Style::default() - .bg(Color::Rgb(40, 40, 40)) - .fg(Color::White), - )]))); - } - - // Add new value input field if in AddingNewValue mode - if settings_editor.edit_mode == EditMode::AddingNewValue { - let secure_indicator = if settings_editor.is_new_value_secure() - { - "🔒 " - } else { - "" - }; - items.push(ListItem::new(Line::from(vec![Span::styled( - format!( - "{}{}: {}", - secure_indicator, - settings_editor.get_new_key_buffer(), - settings_editor.get_edit_buffer() - ), - Style::default() - .bg(Color::Rgb(40, 40, 40)) - .fg(Color::White), - )]))); - } - - let list = List::new(items) - .block(Block::default().borders(Borders::ALL).title(format!( - "{} Settings: {}", - T::item_type(), - item.name() - ))) - .highlight_style(Style::default().add_modifier(Modifier::BOLD)) - .highlight_symbol(">> "); - - let mut state = ListState::default(); - state.select(Some(settings_editor.get_current_field())); - - f.render_stateful_widget(list, area, &mut state); - } else { - let paragraph = - Paragraph::new(format!("No {} selected", T::item_type())) - .block( - Block::default() - .borders(Borders::ALL) - .title(format!("{} Settings", T::item_type())), - ); - f.render_widget(paragraph, area); - } - } - - fn render_instructions( - &self, - f: &mut Frame, - area: Rect, - modal: &SettingsModal, - ) { - let instructions = match (modal.current_tab, modal.tab_focus) { - (EditTab::Profiles, TabFocus::List) => { - "↑↓: Navigate | Enter: Select | R: Rename | D: Delete | Space: \ - Set Default | Tab: Switch Tab" - } - (EditTab::Providers, TabFocus::List) => { - "↑↓: Navigate | Enter: Select | R: Rename | D: Delete | Tab: \ - Switch Tab" - } - (_, TabFocus::Settings) => match modal - .get_current_settings_editor() - .edit_mode - { - EditMode::NotEditing => { - "↑↓: Navigate | Enter: Edit | n: New | N: New Secure | D: \ - Delete | C: Clear | S: Show/Hide Secure | ←/Tab/q/Esc: \ - Back to List" - } - EditMode::EditingValue => "Enter: Save | Esc: Cancel", - EditMode::AddingNewKey => "Enter: Confirm Key | Esc: Cancel", - EditMode::AddingNewValue => { - "Enter: Save New Value | Esc: Cancel" - } - }, - (_, TabFocus::Creation) => "Enter: Create | Esc: Cancel", - }; - - let simple_string = SimpleString::from(instructions); - let wrapped_spans = simple_string.wrapped_spans( - area.width as usize - 2, // Subtract 2 for left and right borders - Some(Style::default().fg(Color::Cyan)), - Some(" | "), - ); - - let wrapped_text: Vec = - wrapped_spans.into_iter().map(Line::from).collect(); - - let paragraph = Paragraph::new(wrapped_text) - .alignment(Alignment::Center) - .block(Block::default().borders(Borders::TOP)); - - f.render_widget(paragraph, area); - } -} - -pub trait SettingsItem { - fn name(&self) -> &str; - fn item_type() -> &'static str; -} - -impl SettingsItem for UserProfile { - fn name(&self) -> &str { - &self.name - } - - fn item_type() -> &'static str { - "Profile" - } -} - -impl SettingsItem for ProviderConfig { - fn name(&self) -> &str { - &self.name - } - - fn item_type() -> &'static str { - "Provider" - } -} diff --git a/lumni/src/apps/builtin/llm/prompt/src/tui/ui.rs b/lumni/src/apps/builtin/llm/prompt/src/tui/ui.rs index 9248449..bb15f65 100644 --- a/lumni/src/apps/builtin/llm/prompt/src/tui/ui.rs +++ b/lumni/src/apps/builtin/llm/prompt/src/tui/ui.rs @@ -6,13 +6,13 @@ use ratatui::widgets::Borders; use super::modals::{ConversationListModal, FileBrowserModal, SettingsModal}; use super::{ CommandLine, ConversationDatabase, ConversationId, ModalWindowTrait, - ModalWindowType, ResponseWindow, TextArea, TextLine, TextWindowTrait, + ModalWindowType, PromptWindow, ResponseWindow, TextLine, TextWindowTrait, WindowEvent, WindowKind, }; pub use crate::external as lumni; pub struct AppUi<'a> { - pub prompt: TextArea<'a>, + pub prompt: PromptWindow<'a>, pub response: ResponseWindow<'a>, pub command_line: CommandLine<'a>, pub primary_window: WindowKind, @@ -22,7 +22,7 @@ pub struct AppUi<'a> { impl AppUi<'_> { pub fn new(conversation_text: Option>) -> Self { Self { - prompt: TextArea::new().with_borders(Borders::ALL), + prompt: PromptWindow::new().with_borders(Borders::ALL), response: ResponseWindow::new(conversation_text), command_line: CommandLine::new(), primary_window: WindowKind::ResponseWindow, diff --git a/lumni/src/apps/builtin/llm/prompt/src/tui/widgets/filebrowser.rs b/lumni/src/apps/builtin/llm/prompt/src/tui/widgets/filebrowser.rs index a84de8e..bc5e7e3 100644 --- a/lumni/src/apps/builtin/llm/prompt/src/tui/widgets/filebrowser.rs +++ b/lumni/src/apps/builtin/llm/prompt/src/tui/widgets/filebrowser.rs @@ -22,7 +22,7 @@ use ratatui::widgets::{ }; use tokio::sync::mpsc; -use super::{KeyTrack, ModalAction, TextArea, TextWindowTrait}; +use super::{KeyTrack, ModalAction, PromptWindow, TextWindowTrait}; pub use crate::external as lumni; // TODO notes: @@ -88,7 +88,7 @@ pub enum BackgroundTaskResult { } pub struct FileBrowserState<'a> { - path_input: TextArea<'a>, + path_input: PromptWindow<'a>, selected_index: usize, displayed_index: usize, scroll_offset: usize, @@ -99,7 +99,7 @@ pub struct FileBrowserState<'a> { impl<'a> Default for FileBrowserState<'a> { fn default() -> Self { - let mut path_input = TextArea::new(); + let mut path_input = PromptWindow::new(); path_input.text_set("", None).unwrap(); Self { path_input, @@ -148,7 +148,7 @@ impl FileBrowserWidget { }); }); - let mut path_input = TextArea::new(); + let mut path_input = PromptWindow::new(); path_input.text_set("", None).unwrap(); let widget = Self { diff --git a/lumni/src/apps/builtin/llm/prompt/src/tui/widgets/mod.rs b/lumni/src/apps/builtin/llm/prompt/src/tui/widgets/mod.rs index 7c44475..1c7ae5a 100644 --- a/lumni/src/apps/builtin/llm/prompt/src/tui/widgets/mod.rs +++ b/lumni/src/apps/builtin/llm/prompt/src/tui/widgets/mod.rs @@ -2,9 +2,9 @@ mod filebrowser; mod textarea; pub use filebrowser::{FileBrowserState, FileBrowserWidget}; -pub use textarea::{TextAreaState, TextAreaWidget}; +pub use textarea::{TextArea, TextAreaState, TextAreaWidget}; use super::{ - KeyTrack, ModalAction, ReadDocument, ReadWriteDocument, TextArea, + KeyTrack, ModalAction, PromptWindow, ReadDocument, ReadWriteDocument, TextBuffer, TextDocumentTrait, TextLine, TextWindowTrait, }; diff --git a/lumni/src/apps/builtin/llm/prompt/src/tui/widgets/textarea.rs b/lumni/src/apps/builtin/llm/prompt/src/tui/widgets/textarea.rs index 63de2d1..0bb7990 100644 --- a/lumni/src/apps/builtin/llm/prompt/src/tui/widgets/textarea.rs +++ b/lumni/src/apps/builtin/llm/prompt/src/tui/widgets/textarea.rs @@ -1,20 +1,69 @@ +use crossterm::event::{KeyCode, KeyEvent, KeyModifiers}; use ratatui::buffer::Buffer; use ratatui::layout::Rect; use ratatui::style::Style; use ratatui::widgets::{ - Paragraph, Scrollbar, ScrollbarOrientation, ScrollbarState, - StatefulWidget, StatefulWidgetRef, Widget, + Paragraph, Scrollbar, ScrollbarOrientation, ScrollbarState, StatefulWidget, + StatefulWidgetRef, Widget, }; +use ratatui::Frame; use super::{ KeyTrack, ReadDocument, ReadWriteDocument, TextBuffer, TextDocumentTrait, TextLine, }; +#[derive(Debug, Clone)] +pub struct TextArea { + widget: TextAreaWidget, + state: TextAreaState<'static, T>, +} + +impl TextArea { + pub fn new() -> Self + where + T: Default, + { + Self { + widget: TextAreaWidget::new(), + state: TextAreaState::new(T::default()), + } + } + + pub fn with_state(state: TextAreaState<'static, T>) -> Self { + Self { + widget: TextAreaWidget::new(), + state, + } + } + + pub fn handle_key_event(&mut self, key_event: KeyEvent) { + self.state.handle_key_event(key_event); + } + + pub fn render(&mut self, f: &mut Frame, area: Rect) { + f.render_stateful_widget(&self.widget, area, &mut self.state); + } +} + +impl TextArea { + pub fn with_read_document(text: Option>) -> Self { + Self::with_state(TextAreaState::with_read_document(text)) + } +} + +impl TextArea { + pub fn with_read_write_document(text: Option>) -> Self { + Self::with_state(TextAreaState::with_read_write_document(text)) + } +} +#[derive(Debug, Clone)] pub struct TextAreaWidget(std::marker::PhantomData); +#[derive(Debug, Clone)] pub struct TextAreaState<'a, T: TextDocumentTrait> { text_buffer: TextBuffer<'a, T>, + key_track: KeyTrack, scroll_offset: usize, } @@ -28,6 +77,7 @@ impl<'a, T: TextDocumentTrait> TextAreaState<'a, T> { pub fn new(document: T) -> Self { Self { text_buffer: TextBuffer::new(document), + key_track: KeyTrack::new(), scroll_offset: 0, } } @@ -40,9 +90,14 @@ impl<'a, T: TextDocumentTrait> TextAreaState<'a, T> { &mut self.text_buffer } - pub fn handle_key_event(&mut self, key_event: &KeyTrack) { + pub fn handle_key_event(&mut self, key_event: KeyEvent) { + self.key_track.process_key(key_event); + // For now, just print the received key events - eprintln!("Received key event: {:?}", key_event.current_key()); + eprintln!( + "Received key event for TextArea: {:?}", + self.key_track.current_key() + ); } } diff --git a/lumni/src/apps/builtin/llm/prompt/src/tui/window/mod.rs b/lumni/src/apps/builtin/llm/prompt/src/tui/window/mod.rs index 38e0779..ce43293 100644 --- a/lumni/src/apps/builtin/llm/prompt/src/tui/window/mod.rs +++ b/lumni/src/apps/builtin/llm/prompt/src/tui/window/mod.rs @@ -72,17 +72,17 @@ impl RectArea { } } -pub struct TextArea<'a> { +pub struct PromptWindow<'a> { base: TextWindow<'a, ReadWriteDocument>, } -impl<'a> TextWindowTrait<'a, ReadWriteDocument> for TextArea<'a> { +impl<'a> TextWindowTrait<'a, ReadWriteDocument> for PromptWindow<'a> { fn base(&mut self) -> &mut TextWindow<'a, ReadWriteDocument> { &mut self.base } } -impl TextArea<'_> { +impl PromptWindow<'_> { pub fn new() -> Self { let mut window_type = WindowConfig::new(WindowKind::EditorWindow); window_type.set_window_status(WindowStatus::InActive); diff --git a/lumni/src/apps/builtin/llm/prompt/src/tui/window/text_display/mod.rs b/lumni/src/apps/builtin/llm/prompt/src/tui/window/text_display/mod.rs index 7c43be9..70898a0 100644 --- a/lumni/src/apps/builtin/llm/prompt/src/tui/window/text_display/mod.rs +++ b/lumni/src/apps/builtin/llm/prompt/src/tui/window/text_display/mod.rs @@ -1,6 +1,6 @@ mod text_render; -use ratatui::style::{Color, Style}; +use ratatui::style::Color; use ratatui::text::{Line, Span}; use text_render::DisplayWindowRenderer; diff --git a/lumni/src/apps/builtin/llm/prompt/src/tui/window/text_document/read_document.rs b/lumni/src/apps/builtin/llm/prompt/src/tui/window/text_document/read_document.rs index 07642d3..766e47b 100644 --- a/lumni/src/apps/builtin/llm/prompt/src/tui/window/text_document/read_document.rs +++ b/lumni/src/apps/builtin/llm/prompt/src/tui/window/text_document/read_document.rs @@ -5,7 +5,7 @@ use super::text_line::TextLine; use super::TextDocumentTrait; use crate::external as lumni; -#[derive(Debug)] +#[derive(Debug, Clone)] pub struct ReadDocument { lines: Vec, modified: bool,