Enhance TUI and core functionality: add header rendering, improve message formatting, and refine provider/model handling logic. Update dependencies.
This commit is contained in:
@@ -17,6 +17,8 @@ async-trait = "0.1"
|
|||||||
textwrap = { workspace = true }
|
textwrap = { workspace = true }
|
||||||
toml = { workspace = true }
|
toml = { workspace = true }
|
||||||
shellexpand = { workspace = true }
|
shellexpand = { workspace = true }
|
||||||
|
regex = "1"
|
||||||
|
once_cell = "1.21.3"
|
||||||
|
|
||||||
[dev-dependencies]
|
[dev-dependencies]
|
||||||
tokio-test = { workspace = true }
|
tokio-test = { workspace = true }
|
||||||
|
|||||||
@@ -15,7 +15,7 @@ impl MessageFormatter {
|
|||||||
Self {
|
Self {
|
||||||
wrap_width: wrap_width.max(20),
|
wrap_width: wrap_width.max(20),
|
||||||
show_role_labels,
|
show_role_labels,
|
||||||
preserve_empty_lines: true,
|
preserve_empty_lines: false,
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -25,36 +25,51 @@ impl MessageFormatter {
|
|||||||
self
|
self
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Render a message to a list of visual lines ready for display
|
/// Update the wrap width
|
||||||
pub fn format_message(&self, message: &Message) -> Vec<String> {
|
pub fn set_wrap_width(&mut self, width: usize) {
|
||||||
let mut lines = Vec::new();
|
self.wrap_width = width.max(20);
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn format_message(&self, message: &Message) -> Vec<String> {
|
||||||
|
// 1) Normalize line breaks to '\n' (handles CR, NEL, LS, PS)
|
||||||
|
let normalized: String = message
|
||||||
|
.content
|
||||||
|
.chars()
|
||||||
|
.map(|ch| match ch {
|
||||||
|
'\r' | '\u{0085}' | '\u{2028}' | '\u{2029}' => '\n',
|
||||||
|
_ => ch,
|
||||||
|
})
|
||||||
|
.collect();
|
||||||
|
|
||||||
|
// 2) Collapse: remove whitespace-only lines; keep exactly one '\n' between content lines
|
||||||
|
let mut content = normalized
|
||||||
|
.split('\n')
|
||||||
|
.map(|l| l.trim_end()) // trim trailing spaces per line
|
||||||
|
.filter(|l| !l.trim().is_empty()) // drop blank/whitespace-only lines
|
||||||
|
.collect::<Vec<_>>()
|
||||||
|
.join("\n")
|
||||||
|
.trim() // trim leading/trailing whitespace
|
||||||
|
.to_string();
|
||||||
|
|
||||||
let mut content = message.content.trim_end().to_string();
|
|
||||||
if content.is_empty() && self.preserve_empty_lines {
|
if content.is_empty() && self.preserve_empty_lines {
|
||||||
content.push(' ');
|
content.push(' ');
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// 3) Wrap
|
||||||
let options = Options::new(self.wrap_width)
|
let options = Options::new(self.wrap_width)
|
||||||
.break_words(true)
|
.break_words(true)
|
||||||
.word_separator(textwrap::WordSeparator::UnicodeBreakProperties);
|
.word_separator(textwrap::WordSeparator::UnicodeBreakProperties);
|
||||||
|
|
||||||
let wrapped = wrap(&content, &options);
|
// 4) Post: rtrim each visual line; drop any whitespace-only lines
|
||||||
|
let mut lines: Vec<String> = wrap(&content, &options)
|
||||||
|
.into_iter()
|
||||||
|
.map(|s| s.trim_end().to_string())
|
||||||
|
.filter(|s| !s.trim().is_empty())
|
||||||
|
.collect();
|
||||||
|
|
||||||
if self.show_role_labels {
|
// 5) Belt & suspenders: remove leading/trailing blanks if any survived
|
||||||
let label = format!("{}:", message.role.to_string().to_uppercase());
|
while lines.first().map_or(false, |s| s.trim().is_empty()) { lines.remove(0); }
|
||||||
if let Some(first) = wrapped.first() {
|
while lines.last().map_or(false, |s| s.trim().is_empty()) { lines.pop(); }
|
||||||
lines.push(format!("{label} {first}"));
|
|
||||||
for line in wrapped.iter().skip(1) {
|
|
||||||
lines.push(format!("{:width$} {line}", "", width = label.len()));
|
|
||||||
}
|
|
||||||
} else {
|
|
||||||
lines.push(label);
|
|
||||||
}
|
|
||||||
} else {
|
|
||||||
for line in wrapped {
|
|
||||||
lines.push(line.into_owned());
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
lines
|
lines
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -87,6 +87,11 @@ impl SessionController {
|
|||||||
&self.formatter
|
&self.formatter
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/// Update the wrap width of the message formatter
|
||||||
|
pub fn set_formatter_wrap_width(&mut self, width: usize) {
|
||||||
|
self.formatter.set_wrap_width(width);
|
||||||
|
}
|
||||||
|
|
||||||
/// Access configuration
|
/// Access configuration
|
||||||
pub fn config(&self) -> &Config {
|
pub fn config(&self) -> &Config {
|
||||||
&self.config
|
&self.config
|
||||||
|
|||||||
@@ -104,7 +104,8 @@ impl ChatApp {
|
|||||||
}
|
}
|
||||||
|
|
||||||
pub fn models(&self) -> Vec<&ModelInfo> {
|
pub fn models(&self) -> Vec<&ModelInfo> {
|
||||||
self.models.iter()
|
self.models
|
||||||
|
.iter()
|
||||||
.filter(|m| m.provider == self.selected_provider)
|
.filter(|m| m.provider == self.selected_provider)
|
||||||
.collect()
|
.collect()
|
||||||
}
|
}
|
||||||
@@ -153,16 +154,24 @@ impl ChatApp {
|
|||||||
self.models = all_models;
|
self.models = all_models;
|
||||||
|
|
||||||
// Populate available_providers
|
// Populate available_providers
|
||||||
let mut providers = self.models.iter().map(|m| m.provider.clone()).collect::<HashSet<_>>();
|
let mut providers = self
|
||||||
|
.models
|
||||||
|
.iter()
|
||||||
|
.map(|m| m.provider.clone())
|
||||||
|
.collect::<HashSet<_>>();
|
||||||
self.available_providers = providers.into_iter().collect();
|
self.available_providers = providers.into_iter().collect();
|
||||||
self.available_providers.sort();
|
self.available_providers.sort();
|
||||||
|
|
||||||
// Set selected_provider based on config, or default to "ollama" if not found
|
// Set selected_provider based on config, or default to "ollama" if not found
|
||||||
self.selected_provider = self.available_providers.iter()
|
self.selected_provider = self
|
||||||
|
.available_providers
|
||||||
|
.iter()
|
||||||
.find(|&p| p == &config_model_provider)
|
.find(|&p| p == &config_model_provider)
|
||||||
.cloned()
|
.cloned()
|
||||||
.unwrap_or_else(|| "ollama".to_string());
|
.unwrap_or_else(|| "ollama".to_string());
|
||||||
self.selected_provider_index = self.available_providers.iter()
|
self.selected_provider_index = self
|
||||||
|
.available_providers
|
||||||
|
.iter()
|
||||||
.position(|p| p == &self.selected_provider)
|
.position(|p| p == &self.selected_provider)
|
||||||
.unwrap_or(0);
|
.unwrap_or(0);
|
||||||
|
|
||||||
@@ -174,7 +183,9 @@ impl ChatApp {
|
|||||||
let current_model_name = self.controller.selected_model().to_string();
|
let current_model_name = self.controller.selected_model().to_string();
|
||||||
let current_model_provider = self.controller.config().general.default_provider.clone();
|
let current_model_provider = self.controller.config().general.default_provider.clone();
|
||||||
|
|
||||||
if config_model_name.as_deref() != Some(¤t_model_name) || config_model_provider != current_model_provider {
|
if config_model_name.as_deref() != Some(¤t_model_name)
|
||||||
|
|| config_model_provider != current_model_provider
|
||||||
|
{
|
||||||
if let Err(err) = config::save_config(self.controller.config()) {
|
if let Err(err) = config::save_config(self.controller.config()) {
|
||||||
self.error = Some(format!("Failed to save config: {err}"));
|
self.error = Some(format!("Failed to save config: {err}"));
|
||||||
} else {
|
} else {
|
||||||
@@ -286,7 +297,9 @@ impl ChatApp {
|
|||||||
self.mode = InputMode::Normal;
|
self.mode = InputMode::Normal;
|
||||||
}
|
}
|
||||||
KeyCode::Enter => {
|
KeyCode::Enter => {
|
||||||
if let Some(provider) = self.available_providers.get(self.selected_provider_index) {
|
if let Some(provider) =
|
||||||
|
self.available_providers.get(self.selected_provider_index)
|
||||||
|
{
|
||||||
self.selected_provider = provider.clone();
|
self.selected_provider = provider.clone();
|
||||||
self.sync_selected_model_index(); // Update model selection based on new provider
|
self.sync_selected_model_index(); // Update model selection based on new provider
|
||||||
self.mode = InputMode::ModelSelection;
|
self.mode = InputMode::ModelSelection;
|
||||||
@@ -318,12 +331,15 @@ impl ChatApp {
|
|||||||
self.controller.set_model(model_id.clone());
|
self.controller.set_model(model_id.clone());
|
||||||
self.status = format!("Using model: {}", model_name);
|
self.status = format!("Using model: {}", model_name);
|
||||||
// Save the selected provider and model to config
|
// Save the selected provider and model to config
|
||||||
self.controller.config_mut().general.default_model = Some(model_id.clone());
|
self.controller.config_mut().general.default_model =
|
||||||
self.controller.config_mut().general.default_provider = self.selected_provider.clone();
|
Some(model_id.clone());
|
||||||
|
self.controller.config_mut().general.default_provider =
|
||||||
|
self.selected_provider.clone();
|
||||||
match config::save_config(self.controller.config()) {
|
match config::save_config(self.controller.config()) {
|
||||||
Ok(_) => self.error = None,
|
Ok(_) => self.error = None,
|
||||||
Err(err) => {
|
Err(err) => {
|
||||||
self.error = Some(format!("Failed to save config: {}", err));
|
self.error =
|
||||||
|
Some(format!("Failed to save config: {}", err));
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -394,16 +410,24 @@ impl ChatApp {
|
|||||||
self.models = all_models;
|
self.models = all_models;
|
||||||
|
|
||||||
// Populate available_providers
|
// Populate available_providers
|
||||||
let mut providers = self.models.iter().map(|m| m.provider.clone()).collect::<HashSet<_>>();
|
let mut providers = self
|
||||||
|
.models
|
||||||
|
.iter()
|
||||||
|
.map(|m| m.provider.clone())
|
||||||
|
.collect::<HashSet<_>>();
|
||||||
self.available_providers = providers.into_iter().collect();
|
self.available_providers = providers.into_iter().collect();
|
||||||
self.available_providers.sort();
|
self.available_providers.sort();
|
||||||
|
|
||||||
// Set selected_provider based on config, or default to "ollama" if not found
|
// Set selected_provider based on config, or default to "ollama" if not found
|
||||||
self.selected_provider = self.available_providers.iter()
|
self.selected_provider = self
|
||||||
|
.available_providers
|
||||||
|
.iter()
|
||||||
.find(|&p| p == &config_model_provider)
|
.find(|&p| p == &config_model_provider)
|
||||||
.cloned()
|
.cloned()
|
||||||
.unwrap_or_else(|| "ollama".to_string());
|
.unwrap_or_else(|| "ollama".to_string());
|
||||||
self.selected_provider_index = self.available_providers.iter()
|
self.selected_provider_index = self
|
||||||
|
.available_providers
|
||||||
|
.iter()
|
||||||
.position(|p| p == &self.selected_provider)
|
.position(|p| p == &self.selected_provider)
|
||||||
.unwrap_or(0);
|
.unwrap_or(0);
|
||||||
|
|
||||||
@@ -413,7 +437,9 @@ impl ChatApp {
|
|||||||
let current_model_name = self.controller.selected_model().to_string();
|
let current_model_name = self.controller.selected_model().to_string();
|
||||||
let current_model_provider = self.controller.config().general.default_provider.clone();
|
let current_model_provider = self.controller.config().general.default_provider.clone();
|
||||||
|
|
||||||
if config_model_name.as_deref() != Some(¤t_model_name) || config_model_provider != current_model_provider {
|
if config_model_name.as_deref() != Some(¤t_model_name)
|
||||||
|
|| config_model_provider != current_model_provider
|
||||||
|
{
|
||||||
if let Err(err) = config::save_config(self.controller.config()) {
|
if let Err(err) = config::save_config(self.controller.config()) {
|
||||||
self.error = Some(format!("Failed to save config: {err}"));
|
self.error = Some(format!("Failed to save config: {err}"));
|
||||||
} else {
|
} else {
|
||||||
@@ -480,7 +506,9 @@ impl ChatApp {
|
|||||||
|
|
||||||
fn sync_selected_model_index(&mut self) {
|
fn sync_selected_model_index(&mut self) {
|
||||||
let current_model_id = self.controller.selected_model().to_string();
|
let current_model_id = self.controller.selected_model().to_string();
|
||||||
let filtered_models: Vec<&ModelInfo> = self.models.iter()
|
let filtered_models: Vec<&ModelInfo> = self
|
||||||
|
.models
|
||||||
|
.iter()
|
||||||
.filter(|m| m.provider == self.selected_provider)
|
.filter(|m| m.provider == self.selected_provider)
|
||||||
.collect();
|
.collect();
|
||||||
|
|
||||||
@@ -501,7 +529,8 @@ impl ChatApp {
|
|||||||
self.controller.set_model(model.id.clone());
|
self.controller.set_model(model.id.clone());
|
||||||
// Also update the config with the new model and provider
|
// Also update the config with the new model and provider
|
||||||
self.controller.config_mut().general.default_model = Some(model.id.clone());
|
self.controller.config_mut().general.default_model = Some(model.id.clone());
|
||||||
self.controller.config_mut().general.default_provider = self.selected_provider.clone();
|
self.controller.config_mut().general.default_provider =
|
||||||
|
self.selected_provider.clone();
|
||||||
if let Err(err) = config::save_config(self.controller.config()) {
|
if let Err(err) = config::save_config(self.controller.config()) {
|
||||||
self.error = Some(format!("Failed to save config: {err}"));
|
self.error = Some(format!("Failed to save config: {err}"));
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -11,15 +11,17 @@ pub fn render_chat(frame: &mut Frame<'_>, app: &ChatApp) {
|
|||||||
let layout = Layout::default()
|
let layout = Layout::default()
|
||||||
.direction(Direction::Vertical)
|
.direction(Direction::Vertical)
|
||||||
.constraints([
|
.constraints([
|
||||||
Constraint::Min(8),
|
Constraint::Length(4), // Header
|
||||||
Constraint::Length(5),
|
Constraint::Min(8), // Messages
|
||||||
Constraint::Length(3),
|
Constraint::Length(3), // Input
|
||||||
|
Constraint::Length(3), // Status
|
||||||
])
|
])
|
||||||
.split(frame.area());
|
.split(frame.area());
|
||||||
|
|
||||||
render_messages(frame, layout[0], app);
|
render_header(frame, layout[0], app);
|
||||||
render_input(frame, layout[1], app);
|
render_messages(frame, layout[1], app);
|
||||||
render_status(frame, layout[2], app);
|
render_input(frame, layout[2], app);
|
||||||
|
render_status(frame, layout[3], app);
|
||||||
|
|
||||||
match app.mode() {
|
match app.mode() {
|
||||||
InputMode::ProviderSelection => render_provider_selector(frame, app),
|
InputMode::ProviderSelection => render_provider_selector(frame, app),
|
||||||
@@ -29,13 +31,49 @@ pub fn render_chat(frame: &mut Frame<'_>, app: &ChatApp) {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
fn render_header(frame: &mut Frame<'_>, area: Rect, app: &ChatApp) {
|
||||||
|
let title_span = Span::styled(
|
||||||
|
" 🦉 OWLEN - AI Assistant ",
|
||||||
|
Style::default()
|
||||||
|
.fg(Color::LightMagenta)
|
||||||
|
.add_modifier(Modifier::BOLD),
|
||||||
|
);
|
||||||
|
let model_span = Span::styled(
|
||||||
|
format!("Model: {}", app.selected_model()),
|
||||||
|
Style::default().fg(Color::LightBlue),
|
||||||
|
);
|
||||||
|
|
||||||
|
let header_block = Block::default()
|
||||||
|
.borders(Borders::ALL)
|
||||||
|
.border_style(Style::default().fg(Color::Rgb(95, 20, 135)))
|
||||||
|
.title(Line::from(vec![title_span]));
|
||||||
|
|
||||||
|
let inner_area = header_block.inner(area);
|
||||||
|
|
||||||
|
let header_text = vec![Line::from(""), Line::from(format!(" {model_span} "))];
|
||||||
|
|
||||||
|
let paragraph = Paragraph::new(header_text).alignment(Alignment::Left);
|
||||||
|
|
||||||
|
frame.render_widget(header_block, area);
|
||||||
|
frame.render_widget(paragraph, inner_area);
|
||||||
|
}
|
||||||
|
|
||||||
fn render_messages(frame: &mut Frame<'_>, area: Rect, app: &ChatApp) {
|
fn render_messages(frame: &mut Frame<'_>, area: Rect, app: &ChatApp) {
|
||||||
let conversation = app.conversation();
|
let conversation = app.conversation();
|
||||||
let formatter = app.formatter();
|
let mut formatter = app.formatter().clone();
|
||||||
|
|
||||||
|
// Reserve space for borders and the message indent so text fits within the block
|
||||||
|
let content_width = area.width.saturating_sub(4).max(20);
|
||||||
|
formatter.set_wrap_width(usize::from(content_width));
|
||||||
|
|
||||||
let mut lines: Vec<Line> = Vec::new();
|
let mut lines: Vec<Line> = Vec::new();
|
||||||
for message in &conversation.messages {
|
for (message_index, message) in conversation.messages.iter().enumerate() {
|
||||||
let color = role_color(message.role.clone());
|
let (prefix, color) = match message.role {
|
||||||
|
Role::User => ("👤 You:", Color::LightBlue),
|
||||||
|
Role::Assistant => ("🤖 Assistant:", Color::LightMagenta),
|
||||||
|
Role::System => ("⚙️ System:", Color::Cyan),
|
||||||
|
};
|
||||||
|
|
||||||
let mut formatted = formatter.format_message(message);
|
let mut formatted = formatter.format_message(message);
|
||||||
let is_streaming = message
|
let is_streaming = message
|
||||||
.metadata
|
.metadata
|
||||||
@@ -43,38 +81,24 @@ fn render_messages(frame: &mut Frame<'_>, area: Rect, app: &ChatApp) {
|
|||||||
.and_then(|v| v.as_bool())
|
.and_then(|v| v.as_bool())
|
||||||
.unwrap_or(false);
|
.unwrap_or(false);
|
||||||
|
|
||||||
if let Some(first) = formatted.first_mut() {
|
lines.push(Line::from(Span::styled(
|
||||||
if let Some((label, rest)) = first.split_once(':') {
|
prefix,
|
||||||
let mut spans = Vec::new();
|
Style::default().fg(color).add_modifier(Modifier::BOLD),
|
||||||
spans.push(Span::styled(
|
)));
|
||||||
format!("{label}:"),
|
|
||||||
color.add_modifier(Modifier::BOLD),
|
|
||||||
));
|
|
||||||
if !rest.trim().is_empty() {
|
|
||||||
spans.push(Span::raw(format!(" {}", rest.trim_start())));
|
|
||||||
}
|
|
||||||
if is_streaming {
|
|
||||||
spans.push(Span::styled(" ▌", Style::default().fg(Color::Magenta)));
|
|
||||||
}
|
|
||||||
lines.push(Line::from(spans));
|
|
||||||
} else {
|
|
||||||
let mut spans = vec![Span::raw(first.clone())];
|
|
||||||
if is_streaming {
|
|
||||||
spans.push(Span::styled(" ▌", Style::default().fg(Color::Magenta)));
|
|
||||||
}
|
|
||||||
lines.push(Line::from(spans));
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
for line in formatted.into_iter().skip(1) {
|
for (i, line) in formatted.iter().enumerate() {
|
||||||
let mut spans = vec![Span::raw(line)];
|
let mut spans = Vec::new();
|
||||||
if is_streaming {
|
spans.push(Span::raw(format!(" {}", line.clone())));
|
||||||
|
if i == formatted.len() - 1 && is_streaming {
|
||||||
spans.push(Span::styled(" ▌", Style::default().fg(Color::Magenta)));
|
spans.push(Span::styled(" ▌", Style::default().fg(Color::Magenta)));
|
||||||
}
|
}
|
||||||
lines.push(Line::from(spans));
|
lines.push(Line::from(spans));
|
||||||
}
|
}
|
||||||
|
// Add an empty line after each message, except the last one
|
||||||
|
if message_index < conversation.messages.len() - 1 {
|
||||||
lines.push(Line::from(""));
|
lines.push(Line::from(""));
|
||||||
}
|
}
|
||||||
|
}
|
||||||
|
|
||||||
if lines.is_empty() {
|
if lines.is_empty() {
|
||||||
lines.push(Line::from("No messages yet. Press 'i' to start typing."));
|
lines.push(Line::from("No messages yet. Press 'i' to start typing."));
|
||||||
@@ -83,12 +107,6 @@ fn render_messages(frame: &mut Frame<'_>, area: Rect, app: &ChatApp) {
|
|||||||
let mut paragraph = Paragraph::new(lines)
|
let mut paragraph = Paragraph::new(lines)
|
||||||
.block(
|
.block(
|
||||||
Block::default()
|
Block::default()
|
||||||
.title(Span::styled(
|
|
||||||
"Conversation",
|
|
||||||
Style::default()
|
|
||||||
.fg(Color::LightMagenta)
|
|
||||||
.add_modifier(Modifier::BOLD),
|
|
||||||
))
|
|
||||||
.borders(Borders::ALL)
|
.borders(Borders::ALL)
|
||||||
.border_style(Style::default().fg(Color::Rgb(95, 20, 135))),
|
.border_style(Style::default().fg(Color::Rgb(95, 20, 135))),
|
||||||
)
|
)
|
||||||
@@ -103,7 +121,7 @@ fn render_messages(frame: &mut Frame<'_>, area: Rect, app: &ChatApp) {
|
|||||||
fn render_input(frame: &mut Frame<'_>, area: Rect, app: &ChatApp) {
|
fn render_input(frame: &mut Frame<'_>, area: Rect, app: &ChatApp) {
|
||||||
let title = match app.mode() {
|
let title = match app.mode() {
|
||||||
InputMode::Editing => " Input (Enter=send · Shift+Enter/Ctrl+J=newline) ",
|
InputMode::Editing => " Input (Enter=send · Shift+Enter/Ctrl+J=newline) ",
|
||||||
_ => "Input",
|
_ => " Input (Press 'i' to start typing) ",
|
||||||
};
|
};
|
||||||
|
|
||||||
let input_block = Block::default()
|
let input_block = Block::default()
|
||||||
@@ -136,58 +154,46 @@ fn render_input(frame: &mut Frame<'_>, area: Rect, app: &ChatApp) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
fn render_status(frame: &mut Frame<'_>, area: Rect, app: &ChatApp) {
|
fn render_status(frame: &mut Frame<'_>, area: Rect, app: &ChatApp) {
|
||||||
let mut spans = Vec::new();
|
let (mode_text, mode_bg_color) = match app.mode() {
|
||||||
spans.push(Span::styled(
|
InputMode::Normal => (" NORMAL", Color::LightBlue),
|
||||||
" OWLEN ",
|
InputMode::Editing => (" INPUT", Color::LightGreen),
|
||||||
|
InputMode::ModelSelection => (" MODEL", Color::LightYellow),
|
||||||
|
InputMode::ProviderSelection => (" PROVIDER", Color::LightCyan),
|
||||||
|
InputMode::Help => (" HELP", Color::LightMagenta),
|
||||||
|
};
|
||||||
|
|
||||||
|
let status_message = if app.streaming_count() > 0 {
|
||||||
|
format!("Streaming... ({})", app.streaming_count())
|
||||||
|
} else if let Some(error) = app.error_message() {
|
||||||
|
format!("Error: {}", error)
|
||||||
|
} else {
|
||||||
|
"Ready".to_string()
|
||||||
|
};
|
||||||
|
|
||||||
|
let help_text = "i:Input m:Model c:Clear q:Quit";
|
||||||
|
|
||||||
|
let left_spans = vec![
|
||||||
|
Span::styled(
|
||||||
|
format!(" {} ", mode_text),
|
||||||
Style::default()
|
Style::default()
|
||||||
.fg(Color::Magenta)
|
.fg(Color::Black)
|
||||||
|
.bg(mode_bg_color)
|
||||||
.add_modifier(Modifier::BOLD),
|
.add_modifier(Modifier::BOLD),
|
||||||
));
|
),
|
||||||
spans.push(Span::raw(" "));
|
Span::raw(format!(" | {} ", status_message)),
|
||||||
spans.push(Span::styled(
|
];
|
||||||
format!("Model {} ({})", app.selected_model(), app.selected_provider),
|
|
||||||
Style::default().fg(Color::LightMagenta),
|
|
||||||
));
|
|
||||||
spans.push(Span::raw(" "));
|
|
||||||
spans.push(Span::styled(
|
|
||||||
format!("Mode {}", app.mode()),
|
|
||||||
Style::default()
|
|
||||||
.fg(Color::LightBlue)
|
|
||||||
.add_modifier(Modifier::ITALIC),
|
|
||||||
));
|
|
||||||
spans.push(Span::raw(" "));
|
|
||||||
spans.push(Span::styled(
|
|
||||||
format!("Msgs {}", app.message_count()),
|
|
||||||
Style::default().fg(Color::Cyan),
|
|
||||||
));
|
|
||||||
|
|
||||||
if app.streaming_count() > 0 {
|
let right_spans = vec![
|
||||||
spans.push(Span::raw(" "));
|
Span::raw(" Help: "),
|
||||||
spans.push(Span::styled(
|
Span::styled(help_text, Style::default().fg(Color::LightBlue)),
|
||||||
format!("⟳ {}", app.streaming_count()),
|
];
|
||||||
Style::default()
|
|
||||||
.fg(Color::LightMagenta)
|
|
||||||
.add_modifier(Modifier::BOLD),
|
|
||||||
));
|
|
||||||
}
|
|
||||||
|
|
||||||
spans.push(Span::raw(" "));
|
let layout = Layout::default()
|
||||||
spans.push(Span::styled(
|
.direction(Direction::Horizontal)
|
||||||
app.status_message(),
|
.constraints([Constraint::Percentage(50), Constraint::Percentage(50)])
|
||||||
Style::default().fg(Color::LightBlue),
|
.split(area);
|
||||||
));
|
|
||||||
|
|
||||||
if let Some(error) = app.error_message() {
|
let left_paragraph = Paragraph::new(Line::from(left_spans))
|
||||||
spans.push(Span::raw(" "));
|
|
||||||
spans.push(Span::styled(
|
|
||||||
error,
|
|
||||||
Style::default()
|
|
||||||
.fg(Color::LightRed)
|
|
||||||
.add_modifier(Modifier::BOLD),
|
|
||||||
));
|
|
||||||
}
|
|
||||||
|
|
||||||
let paragraph = Paragraph::new(Line::from(spans))
|
|
||||||
.alignment(Alignment::Left)
|
.alignment(Alignment::Left)
|
||||||
.block(
|
.block(
|
||||||
Block::default()
|
Block::default()
|
||||||
@@ -195,7 +201,16 @@ fn render_status(frame: &mut Frame<'_>, area: Rect, app: &ChatApp) {
|
|||||||
.border_style(Style::default().fg(Color::Rgb(95, 20, 135))),
|
.border_style(Style::default().fg(Color::Rgb(95, 20, 135))),
|
||||||
);
|
);
|
||||||
|
|
||||||
frame.render_widget(paragraph, area);
|
let right_paragraph = Paragraph::new(Line::from(right_spans))
|
||||||
|
.alignment(Alignment::Right)
|
||||||
|
.block(
|
||||||
|
Block::default()
|
||||||
|
.borders(Borders::ALL)
|
||||||
|
.border_style(Style::default().fg(Color::Rgb(95, 20, 135))),
|
||||||
|
);
|
||||||
|
|
||||||
|
frame.render_widget(left_paragraph, layout[0]);
|
||||||
|
frame.render_widget(right_paragraph, layout[1]);
|
||||||
}
|
}
|
||||||
|
|
||||||
fn render_provider_selector(frame: &mut Frame<'_>, app: &ChatApp) {
|
fn render_provider_selector(frame: &mut Frame<'_>, app: &ChatApp) {
|
||||||
|
|||||||
Reference in New Issue
Block a user