From 8fcd28832d72057de80e43c1e44a00c500b4872b Mon Sep 17 00:00:00 2001 From: filipriec Date: Mon, 16 Jun 2025 11:14:04 +0200 Subject: [PATCH] better answer parsing --- client/src/components/common/autocomplete.rs | 109 ++++++++++++------- 1 file changed, 69 insertions(+), 40 deletions(-) diff --git a/client/src/components/common/autocomplete.rs b/client/src/components/common/autocomplete.rs index dd3c6e2..fca7f60 100644 --- a/client/src/components/common/autocomplete.rs +++ b/client/src/components/common/autocomplete.rs @@ -1,8 +1,6 @@ // src/components/common/autocomplete.rs use common::proto::multieko2::search::search_response::Hit; -use serde::Deserialize; -// Keep all existing imports use crate::config::colors::themes::Theme; use ratatui::{ layout::Rect, @@ -10,14 +8,23 @@ use ratatui::{ widgets::{Block, List, ListItem, ListState}, Frame, }; +use std::collections::HashMap; use unicode_width::UnicodeWidthStr; -// Helper struct for parsing the JSON inside a Hit -#[derive(Deserialize)] -struct SuggestionContent { - name: String, +/// Converts a serde_json::Value into a displayable String. +/// Handles String, Number, and Bool variants. Returns an empty string for Null and others. +fn json_value_to_string(value: &serde_json::Value) -> String { + match value { + serde_json::Value::String(s) => s.clone(), + serde_json::Value::Number(n) => n.to_string(), + serde_json::Value::Bool(b) => b.to_string(), + // Return an empty string for Null, Array, or Object so we can filter them out. + _ => String::new(), + } } -/// Renders an opaque dropdown list for autocomplete suggestions. + +/// Renders an opaque dropdown list for simple string-based suggestions. +/// This function remains unchanged. pub fn render_autocomplete_dropdown( f: &mut Frame, input_rect: Rect, @@ -29,39 +36,32 @@ pub fn render_autocomplete_dropdown( if suggestions.is_empty() { return; } - // --- Calculate Dropdown Size & Position --- - let max_suggestion_width = suggestions.iter().map(|s| s.width()).max().unwrap_or(0) as u16; + let max_suggestion_width = + suggestions.iter().map(|s| s.width()).max().unwrap_or(0) as u16; let horizontal_padding: u16 = 2; let dropdown_width = (max_suggestion_width + horizontal_padding).max(10); let dropdown_height = (suggestions.len() as u16).min(5); let mut dropdown_area = Rect { - x: input_rect.x, // Align horizontally with input - y: input_rect.y + 1, // Position directly below input + x: input_rect.x, + y: input_rect.y + 1, width: dropdown_width, height: dropdown_height, }; - // --- Clamping Logic (prevent rendering off-screen) --- - // Clamp vertically (if it goes below the frame) if dropdown_area.bottom() > frame_area.height { - dropdown_area.y = input_rect.y.saturating_sub(dropdown_height); // Try rendering above + dropdown_area.y = input_rect.y.saturating_sub(dropdown_height); } - // Clamp horizontally (if it goes past the right edge) if dropdown_area.right() > frame_area.width { dropdown_area.x = frame_area.width.saturating_sub(dropdown_width); } - // Ensure x is not negative (if clamping pushes it left) dropdown_area.x = dropdown_area.x.max(0); - // Ensure y is not negative (if clamping pushes it up) dropdown_area.y = dropdown_area.y.max(0); - // --- End Clamping --- - // Render a solid background block first to ensure opacity - let background_block = Block::default().style(Style::default().bg(Color::DarkGray)); + let background_block = + Block::default().style(Style::default().bg(Color::DarkGray)); f.render_widget(background_block, dropdown_area); - // Create list items, ensuring each has a defined background let items: Vec = suggestions .iter() .enumerate() @@ -69,54 +69,84 @@ pub fn render_autocomplete_dropdown( let is_selected = selected_index == Some(i); let s_width = s.width() as u16; let padding_needed = dropdown_width.saturating_sub(s_width); - let padded_s = format!("{}{}", s, " ".repeat(padding_needed as usize)); + let padded_s = + format!("{}{}", s, " ".repeat(padding_needed as usize)); ListItem::new(padded_s).style(if is_selected { Style::default() - .fg(theme.bg) // Text color on highlight - .bg(theme.highlight) // Highlight background + .fg(theme.bg) + .bg(theme.highlight) .add_modifier(Modifier::BOLD) } else { - // Style for non-selected items (matching background block) - Style::default() - .fg(theme.fg) // Text color on gray - .bg(Color::DarkGray) // Explicit gray background + Style::default().fg(theme.fg).bg(Color::DarkGray) }) }) .collect(); - // Create the list widget (without its own block) let list = List::new(items); - - // State for managing selection highlight (still needed for logic) let mut profile_list_state = ListState::default(); profile_list_state.select(selected_index); - // Render the list statefully *over* the background block f.render_stateful_widget(list, dropdown_area, &mut profile_list_state); } -// --- NEW FUNCTION FOR RICH SUGGESTIONS --- +// --- MODIFIED FUNCTION FOR RICH SUGGESTIONS --- /// Renders an opaque dropdown list for rich `Hit`-based suggestions. +/// Displays the value of the first meaningful column, followed by the Hit ID. pub fn render_rich_autocomplete_dropdown( f: &mut Frame, input_rect: Rect, frame_area: Rect, theme: &Theme, - suggestions: &[Hit], // <-- Accepts &[Hit] + suggestions: &[Hit], selected_index: Option, ) { if suggestions.is_empty() { return; } - // --- Get display names from Hits, with a fallback for parsing errors --- let display_names: Vec = suggestions .iter() .map(|hit| { - serde_json::from_str::(&hit.content_json) - .map(|content| content.name) - .unwrap_or_else(|_| format!("ID: {}", hit.id)) // Fallback display + // Use serde_json::Value to handle mixed types (string, null, etc.) + if let Ok(content_map) = + serde_json::from_str::>( + &hit.content_json, + ) + { + // Define keys to ignore for a cleaner display + const IGNORED_KEYS: &[&str] = &["id", "deleted", "created_at"]; + + // Get keys, filter out ignored ones, and sort for consistency + let mut keys: Vec<_> = content_map + .keys() + .filter(|k| !IGNORED_KEYS.contains(&k.as_str())) + .cloned() + .collect(); + keys.sort(); + + // Get only the first non-empty value from the sorted keys + let values: Vec<_> = keys + .iter() + .map(|key| { + content_map + .get(key) + .map(json_value_to_string) + .unwrap_or_default() + }) + .filter(|s| !s.is_empty()) // Filter out null/empty values + .take(1) // Changed from take(2) to take(1) + .collect(); + + let display_part = values.first().cloned().unwrap_or_default(); // Get the first value + if display_part.is_empty() { + format!("ID: {}", hit.id) + } else { + format!("{} | ID: {}", display_part, hit.id) // ID at the end + } + } else { + format!("ID: {} (parse error)", hit.id) + } }) .collect(); @@ -134,7 +164,7 @@ pub fn render_rich_autocomplete_dropdown( height: dropdown_height, }; - // --- Clamping Logic (prevent rendering off-screen) --- + // --- Clamping Logic --- if dropdown_area.bottom() > frame_area.height { dropdown_area.y = input_rect.y.saturating_sub(dropdown_height); } @@ -176,4 +206,3 @@ pub fn render_rich_autocomplete_dropdown( f.render_stateful_widget(list, dropdown_area, &mut list_state); } -