Almaz K commited on
Commit
e4c90f5
Β·
1 Parent(s): 79a2170

added phone numbers to results

Browse files
app.py CHANGED
@@ -13,12 +13,13 @@ from core.config import DETAILED_MODEL_PATH, SIMPLE_MODEL_PATH, UI_CONFIG, GRADI
13
  # Import UI components
14
  from ui.interface import (
15
  update_models, predict_dealers_interface, simple_predict_dealers_interface,
16
- search_data_interface, simple_search_data_interface
17
  )
18
  from ui.tabs.simple_tab import create_simple_tab
19
  from ui.tabs.detailed_tab import create_detailed_tab
20
  from ui.tabs.traditional_tab import create_traditional_tab
21
  from ui.tabs.simple_search_tab import create_simple_search_tab
 
22
 
23
  # Import utilities
24
  from utils.helpers import get_model_status_info, get_app_header, get_app_description, setup_event_handlers
@@ -50,6 +51,7 @@ def create_app():
50
  # Create tabs
51
  with gr.Tabs():
52
  simple_search_tab = create_simple_search_tab(matcher)
 
53
  traditional_tab = create_traditional_tab(matcher)
54
  simple_tab = create_simple_tab(simple_matcher)
55
  detailed_tab = create_detailed_tab(matcher)
@@ -64,10 +66,11 @@ def create_app():
64
  'predict': predict_dealers_interface,
65
  'simple_predict': simple_predict_dealers_interface,
66
  'search': search_data_interface,
67
- 'simple_search': simple_search_data_interface
 
68
  }
69
 
70
- tabs_data = (simple_tab, detailed_tab, traditional_tab, simple_search_tab)
71
  matchers = (matcher, simple_matcher)
72
 
73
  setup_event_handlers(tabs_data, interface_functions, matchers)
 
13
  # Import UI components
14
  from ui.interface import (
15
  update_models, predict_dealers_interface, simple_predict_dealers_interface,
16
+ search_data_interface, simple_search_data_interface, range_search_data_interface
17
  )
18
  from ui.tabs.simple_tab import create_simple_tab
19
  from ui.tabs.detailed_tab import create_detailed_tab
20
  from ui.tabs.traditional_tab import create_traditional_tab
21
  from ui.tabs.simple_search_tab import create_simple_search_tab
22
+ from ui.tabs.range_search_tab import create_range_search_tab
23
 
24
  # Import utilities
25
  from utils.helpers import get_model_status_info, get_app_header, get_app_description, setup_event_handlers
 
51
  # Create tabs
52
  with gr.Tabs():
53
  simple_search_tab = create_simple_search_tab(matcher)
54
+ range_search_tab = create_range_search_tab(matcher)
55
  traditional_tab = create_traditional_tab(matcher)
56
  simple_tab = create_simple_tab(simple_matcher)
57
  detailed_tab = create_detailed_tab(matcher)
 
66
  'predict': predict_dealers_interface,
67
  'simple_predict': simple_predict_dealers_interface,
68
  'search': search_data_interface,
69
+ 'simple_search': simple_search_data_interface,
70
+ 'simple_range_search': range_search_data_interface
71
  }
72
 
73
+ tabs_data = (simple_tab, detailed_tab, traditional_tab, simple_search_tab, range_search_tab)
74
  matchers = (matcher, simple_matcher)
75
 
76
  setup_event_handlers(tabs_data, interface_functions, matchers)
core/__init__.py CHANGED
@@ -1 +1,24 @@
1
- # Core modules for the Swiper Match application
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ """
2
+ Core modules for the Swiper Match application
3
+ Contains the main business logic and configuration
4
+ """
5
+
6
+ # Import core components
7
+ from .matcher import CarDealerMatcher
8
+ from .config import (
9
+ DETAILED_MODEL_PATH,
10
+ SIMPLE_MODEL_PATH,
11
+ UI_CONFIG,
12
+ GRADIO_CSS,
13
+ MAKE_MODEL_DATA
14
+ )
15
+
16
+ # Define what gets exported when using "from core import *"
17
+ __all__ = [
18
+ 'CarDealerMatcher',
19
+ 'DETAILED_MODEL_PATH',
20
+ 'SIMPLE_MODEL_PATH',
21
+ 'UI_CONFIG',
22
+ 'GRADIO_CSS',
23
+ 'MAKE_MODEL_DATA'
24
+ ]
core/matcher.py CHANGED
@@ -258,15 +258,32 @@ class CarDealerMatcher:
258
  # Sort by probability and get top 5
259
  sorted_dealers = sorted(proba_dict.items(), key=lambda x: x[1], reverse=True)[:5]
260
 
 
 
 
 
261
  # Create structured JSON response
262
  dealer_predictions = []
263
  for i, (dealer, prob) in enumerate(sorted_dealers, 1):
264
- dealer_predictions.append({
 
 
 
265
  "rank": i,
266
  "dealer_name": dealer,
267
  "confidence_score": round(prob, 4),
268
  "confidence_percentage": f"{prob:.2%}"
269
- })
 
 
 
 
 
 
 
 
 
 
270
 
271
  # Create vehicle input summary
272
  vehicle_input = {
@@ -291,14 +308,27 @@ class CarDealerMatcher:
291
  # Remove None values from vehicle input
292
  vehicle_input = {k: v for k, v in vehicle_input.items() if v is not None}
293
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
294
  return {
295
  "success": True,
296
  "data": {
297
- "top_dealer": {
298
- "name": sorted_dealers[0][0],
299
- "confidence_score": round(sorted_dealers[0][1], 4),
300
- "confidence_percentage": f"{sorted_dealers[0][1]:.2%}"
301
- },
302
  "dealer_rankings": dealer_predictions,
303
  "model_info": {
304
  "model_used": model_used,
@@ -589,8 +619,17 @@ class CarDealerMatcher:
589
 
590
  # Create dealer ranking summary
591
  if show_dealer_stats and 'dealer_trading_name' in df.columns:
592
- dealer_summary = self._create_dealer_summary(df)
593
- return f"βœ… Found {filtered_count:,} matches from {original_count:,} records in {file_to_search}", dealer_summary
 
 
 
 
 
 
 
 
 
594
  else:
595
  # Return basic summary without dealer rankings
596
  summary_df = pd.DataFrame({
@@ -604,7 +643,7 @@ class CarDealerMatcher:
604
  return f"❌ Error searching data: {str(e)}", pd.DataFrame()
605
 
606
  def _create_dealer_summary(self, df):
607
- """Create dealer ranking summary showing top dealers by car count with expandable car lists"""
608
  try:
609
  logger.info(f"πŸ“Š Creating dealer summary from {len(df)} matching cars...")
610
 
@@ -614,78 +653,329 @@ class CarDealerMatcher:
614
  # Get top 10 dealers (increased from 5 to show more)
615
  top_dealers = dealer_counts.head(10)
616
 
617
- # Create HTML with expandable sections for each dealer
618
- html_content = ""
619
 
620
  # Add dealer sections
621
  for rank, (dealer_name, car_count) in enumerate(top_dealers.items(), 1):
622
  # Get cars for this dealer
623
  dealer_cars = df[df['dealer_trading_name'] == dealer_name]
624
 
625
- # Create rank emoji
626
- rank_emoji = "πŸ₯‡" if rank == 1 else "πŸ₯ˆ" if rank == 2 else "πŸ₯‰" if rank == 3 else f"#{rank}"
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
627
 
628
- html_content += f"""
629
- <details style="margin-bottom: 10px; border: 1px solid #ddd; padding: 5px; border-radius: 5px;">
630
- <summary style="font-weight: bold; cursor: pointer; padding: 5px;">
631
- {rank_emoji} {dealer_name} ({car_count} cars)
632
- </summary>
633
- <div style="margin-top: 10px; max-height: 300px; overflow-y: auto;">
634
- """
 
635
 
636
- # Add individual cars
637
- for idx, (_, car) in enumerate(dealer_cars.head(20).iterrows()): # Limit to 20 cars per dealer
638
- # Extract car details safely
639
- make = car.get('make', 'Unknown')
640
- model = car.get('model', 'Unknown')
641
- year = car.get('manu_year', 'Unknown')
642
- odometer = car.get('odometer', 'Unknown')
643
- price = car.get('advertised_price', 'Unknown')
644
- body_type = car.get('vehicle_body_type', 'Unknown')
645
- fuel_type = car.get('vehicle_fuel_type', 'Unknown')
646
- transmission = car.get('vehicle_transmission_type', 'Unknown')
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
647
 
648
- # Format odometer
649
- if odometer != 'Unknown' and pd.notna(odometer):
650
- try:
651
- odometer_str = f"{int(float(odometer)):,} km"
652
- except:
653
- odometer_str = str(odometer)
654
- else:
655
- odometer_str = "Unknown km"
656
 
657
- # Format price
658
- if price != 'Unknown' and pd.notna(price):
659
- try:
660
- price_str = f"${int(float(price)):,}"
661
- except:
662
- price_str = str(price)
663
- else:
664
- price_str = "Price on request"
665
 
666
- html_content += f"""
667
- <div style="padding: 8px; margin: 4px 0; background: #f9f9f9; border-radius: 3px; border-left: 3px solid #007bff;">
668
- <strong>{year} {make} {model}</strong> - {price_str}<br>
669
- <small style="color: #666;">{body_type} β€’ {fuel_type} β€’ {transmission} β€’ {odometer_str}</small>
670
- </div>
671
- """
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
672
 
673
- # Add "show more" if there are more than 20 cars
674
- if len(dealer_cars) > 20:
675
- html_content += f"""
676
- <div style="padding: 8px; margin: 4px 0; background: #e9ecef; border-radius: 3px; text-align: center; font-style: italic;">
677
- ... and {len(dealer_cars) - 20} more cars
678
- </div>
679
- """
680
 
681
- html_content += """
682
- </div>
683
- </details>
684
- """
 
 
 
 
 
 
685
 
686
- logger.info(f"βœ… Created dealer summary. Top dealer: {top_dealers.index[0]} with {top_dealers.iloc[0]} cars")
687
- return html_content
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
688
 
689
  except Exception as e:
690
- logger.error(f"❌ Error creating dealer summary: {e}")
691
- return f"<div style='color: red; padding: 20px;'>❌ Error creating dealer summary: {str(e)}</div>"
 
 
 
 
 
258
  # Sort by probability and get top 5
259
  sorted_dealers = sorted(proba_dict.items(), key=lambda x: x[1], reverse=True)[:5]
260
 
261
+ # Get all dealer names for batch contact lookup
262
+ dealer_names = [dealer[0] for dealer in sorted_dealers]
263
+ all_contacts = self.get_dealers_contact_info_batch(dealer_names)
264
+
265
  # Create structured JSON response
266
  dealer_predictions = []
267
  for i, (dealer, prob) in enumerate(sorted_dealers, 1):
268
+ # Get contact information from batch lookup
269
+ contact_info = all_contacts.get(dealer)
270
+
271
+ dealer_prediction = {
272
  "rank": i,
273
  "dealer_name": dealer,
274
  "confidence_score": round(prob, 4),
275
  "confidence_percentage": f"{prob:.2%}"
276
+ }
277
+
278
+ # Add contact information if available
279
+ if contact_info:
280
+ dealer_prediction["contact"] = {
281
+ "phone": contact_info.get('phone'),
282
+ "city": contact_info.get('city'),
283
+ "state": contact_info.get('state')
284
+ }
285
+
286
+ dealer_predictions.append(dealer_prediction)
287
 
288
  # Create vehicle input summary
289
  vehicle_input = {
 
308
  # Remove None values from vehicle input
309
  vehicle_input = {k: v for k, v in vehicle_input.items() if v is not None}
310
 
311
+ # Get contact info for top dealer from batch lookup
312
+ top_dealer_contact = all_contacts.get(sorted_dealers[0][0])
313
+
314
+ top_dealer_info = {
315
+ "name": sorted_dealers[0][0],
316
+ "confidence_score": round(sorted_dealers[0][1], 4),
317
+ "confidence_percentage": f"{sorted_dealers[0][1]:.2%}"
318
+ }
319
+
320
+ # Add contact information for top dealer
321
+ if top_dealer_contact:
322
+ top_dealer_info["contact"] = {
323
+ "phone": top_dealer_contact.get('phone'),
324
+ "city": top_dealer_contact.get('city'),
325
+ "state": top_dealer_contact.get('state')
326
+ }
327
+
328
  return {
329
  "success": True,
330
  "data": {
331
+ "top_dealer": top_dealer_info,
 
 
 
 
332
  "dealer_rankings": dealer_predictions,
333
  "model_info": {
334
  "model_used": model_used,
 
619
 
620
  # Create dealer ranking summary
621
  if show_dealer_stats and 'dealer_trading_name' in df.columns:
622
+ dealer_summary_data = self._create_dealer_summary(df)
623
+
624
+ # Import the HTML formatting utility here to avoid circular imports
625
+ try:
626
+ from utils import format_dealer_results_as_html
627
+ dealer_summary_html = format_dealer_results_as_html(dealer_summary_data)
628
+ return f"βœ… Found {filtered_count:,} matches from {original_count:,} records in {file_to_search}", dealer_summary_html
629
+ except ImportError:
630
+ # Fallback if import fails
631
+ logger.warning("Could not import HTML formatting utility, returning raw data")
632
+ return f"βœ… Found {filtered_count:,} matches from {original_count:,} records in {file_to_search}", dealer_summary_data
633
  else:
634
  # Return basic summary without dealer rankings
635
  summary_df = pd.DataFrame({
 
643
  return f"❌ Error searching data: {str(e)}", pd.DataFrame()
644
 
645
  def _create_dealer_summary(self, df):
646
+ """Create dealer ranking summary with structured data (API-friendly)"""
647
  try:
648
  logger.info(f"πŸ“Š Creating dealer summary from {len(df)} matching cars...")
649
 
 
653
  # Get top 10 dealers (increased from 5 to show more)
654
  top_dealers = dealer_counts.head(10)
655
 
656
+ dealer_results = []
 
657
 
658
  # Add dealer sections
659
  for rank, (dealer_name, car_count) in enumerate(top_dealers.items(), 1):
660
  # Get cars for this dealer
661
  dealer_cars = df[df['dealer_trading_name'] == dealer_name]
662
 
663
+ # Extract contact information directly from the first car record for this dealer
664
+ first_car = dealer_cars.iloc[0]
665
+ contact_info = {
666
+ 'phone': None,
667
+ 'city': first_car.get('dealer_city', 'Unknown'),
668
+ 'state': first_car.get('dealer_state', 'Unknown')
669
+ }
670
+
671
+ # Try to get phone number from available phone fields in the data
672
+ phone_fields = ['dealer_phone_lead', 'dealer_phone_lead_at', 'dealer_phone_lead_cg']
673
+ for field in phone_fields:
674
+ if field in first_car and pd.notna(first_car[field]):
675
+ contact_info['phone'] = str(first_car[field]).strip()
676
+ break
677
+
678
+ # Convert cars to structured format (limit to 20 for performance)
679
+ cars_list = []
680
+ for _, car in dealer_cars.head(20).iterrows():
681
+ car_data = {
682
+ 'make': car.get('make', 'Unknown'),
683
+ 'model': car.get('model', 'Unknown'),
684
+ 'year': car.get('manu_year', 'Unknown'),
685
+ 'price': car.get('advertised_price', 'Unknown'),
686
+ 'odometer': car.get('odometer', 'Unknown'),
687
+ 'body_type': car.get('vehicle_body_type', 'Unknown'),
688
+ 'fuel_type': car.get('vehicle_fuel_type', 'Unknown'),
689
+ 'transmission': car.get('vehicle_transmission_type', 'Unknown')
690
+ }
691
+ cars_list.append(car_data)
692
 
693
+ dealer_result = {
694
+ 'rank': rank,
695
+ 'dealer_name': dealer_name,
696
+ 'car_count': int(car_count),
697
+ 'contact': contact_info,
698
+ 'cars': cars_list,
699
+ 'total_cars': len(dealer_cars) # Include total count for "show more" logic
700
+ }
701
 
702
+ dealer_results.append(dealer_result)
703
+
704
+ logger.info(f"βœ… Created dealer summary. Top dealer: {top_dealers.index[0]} with {top_dealers.iloc[0]} cars")
705
+ return dealer_results
706
+
707
+ except Exception as e:
708
+ logger.error(f"❌ Error creating dealer summary: {e}")
709
+ return []
710
+
711
+ def get_dealer_contact_info(self, dealer_name):
712
+ """Look up dealer contact information from CSV data files"""
713
+ try:
714
+ if not self.data_files:
715
+ return None
716
+
717
+ # Use the first available data file (Combined file should have all dealer info)
718
+ file_to_search = self.data_files[0]
719
+ file_path = os.path.join(DATA_DIR, file_to_search)
720
+
721
+ # Read CSV with error handling for different encodings
722
+ try:
723
+ df = pd.read_csv(file_path, encoding='utf-8')
724
+ except UnicodeDecodeError:
725
+ try:
726
+ df = pd.read_csv(file_path, encoding='latin-1')
727
+ except:
728
+ df = pd.read_csv(file_path, encoding='cp1252')
729
+
730
+ # Convert column names to lowercase for easier matching
731
+ df.columns = df.columns.str.lower().str.strip()
732
+
733
+ # Find dealer by name
734
+ if 'dealer_trading_name' in df.columns:
735
+ dealer_matches = df[df['dealer_trading_name'].astype(str).str.contains(dealer_name, case=False, na=False)]
736
+
737
+ if not dealer_matches.empty:
738
+ # Get the first match
739
+ dealer_info = dealer_matches.iloc[0]
740
 
741
+ # Extract contact information
742
+ contact_info = {
743
+ 'dealer_name': dealer_info.get('dealer_trading_name', dealer_name),
744
+ 'phone': None,
745
+ 'city': dealer_info.get('dealer_city', 'Unknown'),
746
+ 'state': dealer_info.get('dealer_state', 'Unknown')
747
+ }
 
748
 
749
+ # Try to get phone number from available phone fields
750
+ phone_fields = ['dealer_phone_lead', 'dealer_phone_lead_at', 'dealer_phone_lead_cg']
751
+ for field in phone_fields:
752
+ if field in dealer_info and pd.notna(dealer_info[field]):
753
+ contact_info['phone'] = str(dealer_info[field]).strip()
754
+ break
 
 
755
 
756
+ return contact_info
757
+
758
+ return None
759
+
760
+ except Exception as e:
761
+ logger.error(f"❌ Error looking up dealer contact info: {e}")
762
+ return None
763
+
764
+ def get_dealers_contact_info_batch(self, dealer_names):
765
+ """Look up contact information for multiple dealers efficiently in one pass"""
766
+ try:
767
+ if not self.data_files or not dealer_names:
768
+ return {}
769
+
770
+ # Use the first available data file (Combined file should have all dealer info)
771
+ file_to_search = self.data_files[0]
772
+ file_path = os.path.join(DATA_DIR, file_to_search)
773
+
774
+ # Read CSV with error handling for different encodings (only once)
775
+ try:
776
+ df = pd.read_csv(file_path, encoding='utf-8')
777
+ except UnicodeDecodeError:
778
+ try:
779
+ df = pd.read_csv(file_path, encoding='latin-1')
780
+ except:
781
+ df = pd.read_csv(file_path, encoding='cp1252')
782
+
783
+ # Convert column names to lowercase for easier matching
784
+ df.columns = df.columns.str.lower().str.strip()
785
+
786
+ contact_info_dict = {}
787
+
788
+ # Find dealers by name in batch
789
+ if 'dealer_trading_name' in df.columns:
790
+ for dealer_name in dealer_names:
791
+ dealer_matches = df[df['dealer_trading_name'].astype(str).str.contains(dealer_name, case=False, na=False)]
792
+
793
+ if not dealer_matches.empty:
794
+ # Get the first match
795
+ dealer_info = dealer_matches.iloc[0]
796
+
797
+ # Extract contact information
798
+ contact_info = {
799
+ 'dealer_name': dealer_info.get('dealer_trading_name', dealer_name),
800
+ 'phone': None,
801
+ 'city': dealer_info.get('dealer_city', 'Unknown'),
802
+ 'state': dealer_info.get('dealer_state', 'Unknown')
803
+ }
804
+
805
+ # Try to get phone number from available phone fields
806
+ phone_fields = ['dealer_phone_lead', 'dealer_phone_lead_at', 'dealer_phone_lead_cg']
807
+ for field in phone_fields:
808
+ if field in dealer_info and pd.notna(dealer_info[field]):
809
+ contact_info['phone'] = str(dealer_info[field]).strip()
810
+ break
811
+
812
+ contact_info_dict[dealer_name] = contact_info
813
+
814
+ return contact_info_dict
815
+
816
+ except Exception as e:
817
+ logger.error(f"❌ Error looking up dealers contact info: {e}")
818
+ return {}
819
+
820
+ def search_data_files_api(self, keyword_search=None, make=None, model=None, year_min=None, year_max=None,
821
+ year_from=None, year_to=None,
822
+ body_type=None, fuel_type=None, max_odometer=None,
823
+ odometer_from=None, odometer_to=None,
824
+ max_price=None, selected_file=None, max_results=100):
825
+ """
826
+ Search through CSV data files and return clean structured data (API-friendly)
827
+
828
+ Returns:
829
+ dict: {
830
+ "success": bool,
831
+ "message": str,
832
+ "data": {
833
+ "search_summary": {
834
+ "total_matches": int,
835
+ "original_count": int,
836
+ "file_searched": str
837
+ },
838
+ "dealers": [dealer_results_list]
839
+ }
840
+ }
841
+ """
842
+ try:
843
+ if not self.data_files:
844
+ return {
845
+ "success": False,
846
+ "message": "No CSV data files available",
847
+ "data": None
848
+ }
849
+
850
+ # Use selected file or default to first available
851
+ if selected_file and selected_file in self.data_files:
852
+ file_to_search = selected_file
853
+ else:
854
+ file_to_search = self.data_files[0] if self.data_files else None
855
+
856
+ if not file_to_search:
857
+ return {
858
+ "success": False,
859
+ "message": "No valid file selected",
860
+ "data": None
861
+ }
862
+
863
+ file_path = os.path.join(DATA_DIR, file_to_search)
864
+
865
+ # Load the CSV file
866
+ logger.info(f"πŸ” Loading data from: {file_to_search}")
867
+
868
+ # Read CSV with error handling for different encodings
869
+ try:
870
+ df = pd.read_csv(file_path, encoding='utf-8')
871
+ except UnicodeDecodeError:
872
+ try:
873
+ df = pd.read_csv(file_path, encoding='latin-1')
874
+ except:
875
+ df = pd.read_csv(file_path, encoding='cp1252')
876
+
877
+ # Convert column names to lowercase for easier matching
878
+ df.columns = df.columns.str.lower().str.strip()
879
+
880
+ original_count = len(df)
881
+
882
+ # Apply the same filtering logic as search_data_files
883
+ # [Apply all the same filters here - keyword_search, make, model, etc.]
884
+ # ... (same filtering logic as in search_data_files)
885
+
886
+ # Apply keyword search filter first (searches across multiple text columns)
887
+ if keyword_search and keyword_search.strip():
888
+ keyword = keyword_search.strip().lower()
889
+
890
+ # Define text columns to search across
891
+ text_columns = ['make', 'model', 'dealer_trading_name', 'dealer_city', 'dealer_state',
892
+ 'vehicle_body_type', 'vehicle_fuel_type', 'vehicle_transmission_type',
893
+ 'series', 'variant', 'vehicle_segment', 'vehicle_drive_type']
894
 
895
+ # Filter to only existing columns
896
+ existing_text_columns = [col for col in text_columns if col in df.columns]
 
 
 
 
 
897
 
898
+ if existing_text_columns:
899
+ # Create a combined search across all text columns
900
+ # Convert to string first to handle non-string values
901
+ keyword_mask = df[existing_text_columns[0]].astype(str).str.contains(keyword, case=False, na=False)
902
+
903
+ for col in existing_text_columns[1:]:
904
+ keyword_mask = keyword_mask | df[col].astype(str).str.contains(keyword, case=False, na=False)
905
+
906
+ df = df[keyword_mask]
907
+ logger.info(f"πŸ” Keyword search '{keyword}' filtered to {len(df)} records")
908
 
909
+ # Apply specific filters using correct column names from the dataset
910
+ if make and 'make' in df.columns:
911
+ df = df[df['make'].astype(str).str.contains(make, case=False, na=False)]
912
+
913
+ if model and 'model' in df.columns:
914
+ df = df[df['model'].astype(str).str.contains(model, case=False, na=False)]
915
+
916
+ # Handle year filtering
917
+ if year_from is not None and year_to is not None and 'manu_year' in df.columns:
918
+ year_numeric = pd.to_numeric(df['manu_year'], errors='coerce')
919
+ df = df[(year_numeric >= year_from) & (year_numeric <= year_to)]
920
+ elif year_min and 'manu_year' in df.columns:
921
+ df = df[pd.to_numeric(df['manu_year'], errors='coerce') >= year_min]
922
+ elif year_max and 'manu_year' in df.columns:
923
+ df = df[pd.to_numeric(df['manu_year'], errors='coerce') <= year_max]
924
+
925
+ if body_type and 'vehicle_body_type' in df.columns:
926
+ df = df[df['vehicle_body_type'].astype(str).str.contains(body_type, case=False, na=False)]
927
+
928
+ if fuel_type and 'vehicle_fuel_type' in df.columns:
929
+ df = df[df['vehicle_fuel_type'].astype(str).str.contains(fuel_type, case=False, na=False)]
930
+
931
+ # Handle odometer filtering
932
+ if odometer_from is not None and odometer_to is not None and 'odometer' in df.columns:
933
+ odometer_numeric = pd.to_numeric(df['odometer'], errors='coerce')
934
+ df = df[(odometer_numeric >= odometer_from) & (odometer_numeric <= odometer_to)]
935
+ elif max_odometer and 'odometer' in df.columns:
936
+ df = df[pd.to_numeric(df['odometer'], errors='coerce') <= max_odometer]
937
+
938
+ if max_price and 'advertised_price' in df.columns:
939
+ df = df[pd.to_numeric(df['advertised_price'], errors='coerce') <= max_price]
940
+
941
+ filtered_count = len(df)
942
+
943
+ if df.empty:
944
+ return {
945
+ "success": True,
946
+ "message": f"Searched {file_to_search} ({original_count:,} records) - No matches found",
947
+ "data": {
948
+ "search_summary": {
949
+ "total_matches": 0,
950
+ "original_count": original_count,
951
+ "file_searched": file_to_search
952
+ },
953
+ "dealers": []
954
+ }
955
+ }
956
+
957
+ # Create dealer ranking summary (structured data)
958
+ dealer_results = []
959
+ if 'dealer_trading_name' in df.columns:
960
+ dealer_results = self._create_dealer_summary(df)
961
+
962
+ return {
963
+ "success": True,
964
+ "message": f"Found {filtered_count:,} matches from {original_count:,} records in {file_to_search}",
965
+ "data": {
966
+ "search_summary": {
967
+ "total_matches": filtered_count,
968
+ "original_count": original_count,
969
+ "file_searched": file_to_search
970
+ },
971
+ "dealers": dealer_results
972
+ }
973
+ }
974
 
975
  except Exception as e:
976
+ logger.error(f"❌ API search error: {e}")
977
+ return {
978
+ "success": False,
979
+ "message": f"Error searching data: {str(e)}",
980
+ "data": None
981
+ }
ui/__init__.py CHANGED
@@ -1 +1,41 @@
1
- # UI components for the Swiper Match Gradio application
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ """
2
+ UI components for the Swiper Match Gradio application
3
+ """
4
+
5
+ # Import core interface functions
6
+ from .interface import (
7
+ update_models,
8
+ predict_dealers_interface,
9
+ simple_predict_dealers_interface,
10
+ search_data_interface,
11
+ simple_search_data_interface,
12
+ range_search_data_interface,
13
+ range_search_data_interface_api,
14
+ search_data_interface_api,
15
+ simple_search_data_interface_api
16
+ )
17
+
18
+ # Import tab creation functions
19
+ from .tabs.simple_tab import create_simple_tab
20
+ from .tabs.detailed_tab import create_detailed_tab
21
+ from .tabs.traditional_tab import create_traditional_tab
22
+ from .tabs.simple_search_tab import create_simple_search_tab
23
+ from .tabs.range_search_tab import create_range_search_tab
24
+
25
+ # Define what gets exported when using "from ui import *"
26
+ __all__ = [
27
+ 'update_models',
28
+ 'predict_dealers_interface',
29
+ 'simple_predict_dealers_interface',
30
+ 'search_data_interface',
31
+ 'simple_search_data_interface',
32
+ 'range_search_data_interface',
33
+ 'range_search_data_interface_api',
34
+ 'search_data_interface_api',
35
+ 'simple_search_data_interface_api',
36
+ 'create_simple_tab',
37
+ 'create_detailed_tab',
38
+ 'create_traditional_tab',
39
+ 'create_simple_search_tab',
40
+ 'create_range_search_tab'
41
+ ]
ui/interface.py CHANGED
@@ -80,6 +80,23 @@ def simple_predict_dealers_interface(matcher, simple_make, simple_model, simple_
80
  top_dealer = data["top_dealer"]["name"]
81
  confidence = data["top_dealer"]["confidence_percentage"]
82
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
83
  # Create user-friendly display with structured data
84
  display_text = f"""
85
  ## 🎯 **Prediction Results**
@@ -87,12 +104,34 @@ def simple_predict_dealers_interface(matcher, simple_make, simple_model, simple_
87
  ### πŸ† **Top Dealer**
88
  **{top_dealer}** - {confidence} confidence
89
 
90
- ### πŸ“Š **All Dealer Rankings**
91
  """
92
 
 
 
 
 
 
 
 
 
 
 
 
 
 
93
  for dealer in data["dealer_rankings"]:
94
  rank_emoji = "πŸ₯‡" if dealer["rank"] == 1 else "πŸ₯ˆ" if dealer["rank"] == 2 else "πŸ₯‰" if dealer["rank"] == 3 else "πŸ”Έ"
95
- display_text += f"{rank_emoji} **{dealer['rank']}. {dealer['dealer_name']}** - {dealer['confidence_percentage']}\n"
 
 
 
 
 
 
 
 
 
 
96
 
97
  display_text += f"""
98
 
@@ -128,7 +167,7 @@ def simple_predict_dealers_interface(matcher, simple_make, simple_model, simple_
128
  - Error handling: `success` and `error` fields
129
  """
130
 
131
- return f"🎯 **Best Match: {top_dealer}**", confidence, display_text
132
 
133
 
134
  def search_data_interface(matcher, keyword_search, make, model, year_from, year_to, body_type, fuel_type,
@@ -141,29 +180,51 @@ def search_data_interface(matcher, keyword_search, make, model, year_from, year_
141
  processed_fuel_type = None if fuel_type == "Select Fuel Type" else fuel_type
142
  processed_selected_file = None if selected_file == "Select File" else selected_file
143
 
144
- message, result_data = matcher.search_data_files(keyword_search=keyword_search, make=processed_make,
145
- model=processed_model, year_min=None, year_max=None,
146
- year_from=year_from, year_to=year_to,
147
- body_type=processed_body_type, fuel_type=processed_fuel_type,
148
- max_odometer=None, odometer_from=odometer_from,
149
- odometer_to=odometer_to, max_price=max_price,
150
- selected_file=processed_selected_file,
151
- max_results=max_results, show_dealer_stats=show_dealer_stats)
152
-
153
- if isinstance(result_data, pd.DataFrame) and result_data.empty:
154
- return message, "No results to display", ""
155
- elif isinstance(result_data, str): # HTML from _create_dealer_summary
156
- # Extract stats for info display
157
- if "Top dealer:" in message:
158
- info_text = f"**πŸ† Dealer Rankings with Expandable Car Lists**\n\n"
159
- info_text += "Click on any dealer name to see their individual car listings with prices and specifications."
160
- else:
161
- info_text = f"**πŸ“Š Search completed successfully**\n\n"
 
162
 
163
- return message, info_text, result_data
 
 
 
 
 
 
 
 
 
 
 
 
164
  else:
165
- # Handle DataFrame case (when show_dealer_stats=False)
166
- if hasattr(result_data, 'empty') and not result_data.empty:
 
 
 
 
 
 
 
 
 
167
  info_text = f"**πŸ“Š Search completed successfully**\n\n"
168
  html_table = result_data.to_html(classes='table table-striped',
169
  table_id='search-results', escape=False, index=False)
@@ -180,42 +241,223 @@ def simple_search_data_interface(matcher, keyword_search, make, model, year_from
180
  processed_model = None if (not model or model.strip() == "" or model == "ALL") else model.strip()
181
  processed_selected_file = None if selected_file == "Select File" else selected_file
182
 
183
- message, result_data = matcher.search_data_files(
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
184
  keyword_search=keyword_search,
185
  make=processed_make,
186
  model=processed_model,
187
- year_min=None,
188
- year_max=None,
189
  year_from=year_from,
190
  year_to=year_to,
191
- body_type=None, # Not used in simple search
192
- fuel_type=None, # Not used in simple search
193
- max_odometer=None, # Not using max_odometer anymore
194
  odometer_from=odometer_from,
195
  odometer_to=odometer_to,
196
- max_price=None, # Not used in simple search
197
  selected_file=processed_selected_file,
198
- max_results=max_results,
199
- show_dealer_stats=show_dealer_stats
200
  )
201
 
202
- if isinstance(result_data, pd.DataFrame) and result_data.empty:
203
- return message, "No results to display", ""
204
- elif isinstance(result_data, str): # HTML from _create_dealer_summary
205
- # Extract stats for info display
206
- if "Top dealer:" in message:
207
- info_text = f"**πŸ† Dealer Rankings with Expandable Car Lists**\n\n"
208
- info_text += "Click on any dealer name to see their individual car listings with prices and specifications."
209
- else:
210
- info_text = f"**πŸ“Š Search completed successfully**\n\n"
211
-
212
- return message, info_text, result_data
213
- else:
214
- # Handle DataFrame case (when show_dealer_stats=False)
215
- if hasattr(result_data, 'empty') and not result_data.empty:
216
- info_text = f"**πŸ“Š Search completed successfully**\n\n"
217
- html_table = result_data.to_html(classes='table table-striped',
218
- table_id='search-results', escape=False, index=False)
219
- return message, info_text, html_table
220
- else:
221
- return message, "No results to display", ""
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
80
  top_dealer = data["top_dealer"]["name"]
81
  confidence = data["top_dealer"]["confidence_percentage"]
82
 
83
+ # Get all dealer names for batch contact lookup
84
+ all_dealer_names = [dealer['dealer_name'] for dealer in data["dealer_rankings"]]
85
+
86
+ # Batch lookup contact information for all dealers
87
+ all_contacts = matcher.get_dealers_contact_info_batch(all_dealer_names)
88
+
89
+ # Get contact info for top dealer
90
+ top_dealer_contact = all_contacts.get(top_dealer)
91
+
92
+ # Format top dealer info with contact details
93
+ if top_dealer_contact and top_dealer_contact.get('phone'):
94
+ top_dealer_display = f"🎯 **Best Match: {top_dealer}**\nπŸ“ž **Phone:** {top_dealer_contact['phone']}"
95
+ if top_dealer_contact.get('city') != 'Unknown' and top_dealer_contact.get('state') != 'Unknown':
96
+ top_dealer_display += f"\nπŸ“ **Location:** {top_dealer_contact['city']}, {top_dealer_contact['state']}"
97
+ else:
98
+ top_dealer_display = f"🎯 **Best Match: {top_dealer}**\nπŸ“ž **Phone:** Contact dealer directly"
99
+
100
  # Create user-friendly display with structured data
101
  display_text = f"""
102
  ## 🎯 **Prediction Results**
 
104
  ### πŸ† **Top Dealer**
105
  **{top_dealer}** - {confidence} confidence
106
 
 
107
  """
108
 
109
+ # Add contact information for top dealer
110
+ if top_dealer_contact:
111
+ display_text += f"""**πŸ“ž Phone:** {top_dealer_contact.get('phone', 'Contact dealer directly')}
112
+ **πŸ“ Location:** {top_dealer_contact.get('city', 'Unknown')}, {top_dealer_contact.get('state', 'Unknown')}
113
+
114
+ """
115
+ else:
116
+ display_text += "**πŸ“ž Phone:** Contact dealer directly\n\n"
117
+
118
+ display_text += """### πŸ“Š **All Dealer Rankings**
119
+ """
120
+
121
+ # Add dealer rankings with contact info using batch lookup results
122
  for dealer in data["dealer_rankings"]:
123
  rank_emoji = "πŸ₯‡" if dealer["rank"] == 1 else "πŸ₯ˆ" if dealer["rank"] == 2 else "πŸ₯‰" if dealer["rank"] == 3 else "πŸ”Έ"
124
+ dealer_name = dealer['dealer_name']
125
+
126
+ # Get contact info from batch lookup
127
+ contact_info = all_contacts.get(dealer_name)
128
+
129
+ if contact_info and contact_info.get('phone'):
130
+ phone_info = f" β€’ πŸ“ž {contact_info['phone']}"
131
+ else:
132
+ phone_info = " β€’ πŸ“ž Contact dealer"
133
+
134
+ display_text += f"{rank_emoji} **{dealer['rank']}. {dealer_name}** - {dealer['confidence_percentage']}{phone_info}\n"
135
 
136
  display_text += f"""
137
 
 
167
  - Error handling: `success` and `error` fields
168
  """
169
 
170
+ return top_dealer_display, confidence, display_text
171
 
172
 
173
  def search_data_interface(matcher, keyword_search, make, model, year_from, year_to, body_type, fuel_type,
 
180
  processed_fuel_type = None if fuel_type == "Select Fuel Type" else fuel_type
181
  processed_selected_file = None if selected_file == "Select File" else selected_file
182
 
183
+ if show_dealer_stats:
184
+ # Use API method that returns clean structured data
185
+ api_result = matcher.search_data_files_api(
186
+ keyword_search=keyword_search,
187
+ make=processed_make,
188
+ model=processed_model,
189
+ year_from=year_from,
190
+ year_to=year_to,
191
+ body_type=processed_body_type,
192
+ fuel_type=processed_fuel_type,
193
+ odometer_from=odometer_from,
194
+ odometer_to=odometer_to,
195
+ max_price=max_price,
196
+ selected_file=processed_selected_file,
197
+ max_results=max_results
198
+ )
199
+
200
+ if not api_result["success"]:
201
+ return f"❌ {api_result['message']}", "Error occurred", ""
202
 
203
+ # Apply HTML formatting for UI display
204
+ try:
205
+ from utils import format_dealer_results_as_html
206
+ dealer_html = format_dealer_results_as_html(api_result['data']['dealers'])
207
+
208
+ summary = api_result['data']['search_summary']
209
+ message = f"βœ… Found {summary['total_matches']:,} matches from {summary['original_count']:,} records in {summary['file_searched']}"
210
+ info_text = "**πŸ† Dealer Rankings with Expandable Car Lists**\n\nClick on any dealer name to see their individual car listings with prices and specifications."
211
+
212
+ return message, info_text, dealer_html
213
+ except ImportError:
214
+ # Fallback if HTML formatting fails
215
+ return api_result['message'], "API data available but HTML formatting failed", str(api_result['data'])
216
  else:
217
+ # For non-dealer stats, use the original method that returns DataFrame
218
+ message, result_data = matcher.search_data_files(keyword_search=keyword_search, make=processed_make,
219
+ model=processed_model, year_min=None, year_max=None,
220
+ year_from=year_from, year_to=year_to,
221
+ body_type=processed_body_type, fuel_type=processed_fuel_type,
222
+ max_odometer=None, odometer_from=odometer_from,
223
+ odometer_to=odometer_to, max_price=max_price,
224
+ selected_file=processed_selected_file,
225
+ max_results=max_results, show_dealer_stats=False)
226
+
227
+ if isinstance(result_data, pd.DataFrame) and not result_data.empty:
228
  info_text = f"**πŸ“Š Search completed successfully**\n\n"
229
  html_table = result_data.to_html(classes='table table-striped',
230
  table_id='search-results', escape=False, index=False)
 
241
  processed_model = None if (not model or model.strip() == "" or model == "ALL") else model.strip()
242
  processed_selected_file = None if selected_file == "Select File" else selected_file
243
 
244
+ if show_dealer_stats:
245
+ # Use API method that returns clean structured data
246
+ api_result = matcher.search_data_files_api(
247
+ keyword_search=keyword_search,
248
+ make=processed_make,
249
+ model=processed_model,
250
+ year_from=year_from,
251
+ year_to=year_to,
252
+ odometer_from=odometer_from,
253
+ odometer_to=odometer_to,
254
+ selected_file=processed_selected_file,
255
+ max_results=max_results
256
+ )
257
+
258
+ if not api_result["success"]:
259
+ return f"❌ {api_result['message']}", "Error occurred", ""
260
+
261
+ # Apply HTML formatting for UI display
262
+ try:
263
+ from utils import format_dealer_results_as_html
264
+ dealer_html = format_dealer_results_as_html(api_result['data']['dealers'])
265
+
266
+ summary = api_result['data']['search_summary']
267
+ message = f"βœ… Found {summary['total_matches']:,} matches from {summary['original_count']:,} records in {summary['file_searched']}"
268
+ info_text = "**πŸ† Dealer Rankings with Expandable Car Lists**\n\nClick on any dealer name to see their individual car listings with prices and specifications."
269
+
270
+ return message, info_text, dealer_html
271
+ except ImportError:
272
+ # Fallback if HTML formatting fails
273
+ return api_result['message'], "API data available but HTML formatting failed", str(api_result['data'])
274
+ else:
275
+ # For non-dealer stats, use original method that returns DataFrame
276
+ message, result_data = matcher.search_data_files(
277
+ keyword_search=keyword_search,
278
+ make=processed_make,
279
+ model=processed_model,
280
+ year_min=None,
281
+ year_max=None,
282
+ year_from=year_from,
283
+ year_to=year_to,
284
+ body_type=None, # Not used in simple search
285
+ fuel_type=None, # Not used in simple search
286
+ max_odometer=None, # Not using max_odometer anymore
287
+ odometer_from=odometer_from,
288
+ odometer_to=odometer_to,
289
+ max_price=None, # Not used in simple search
290
+ selected_file=processed_selected_file,
291
+ max_results=max_results,
292
+ show_dealer_stats=False
293
+ )
294
+
295
+ if isinstance(result_data, pd.DataFrame) and not result_data.empty:
296
+ info_text = f"**πŸ“Š Search completed successfully**\n\n"
297
+ html_table = result_data.to_html(classes='table table-striped',
298
+ table_id='search-results', escape=False, index=False)
299
+ return message, info_text, html_table
300
+ else:
301
+ return message, "No results to display", ""
302
+
303
+
304
+ def range_search_data_interface(matcher, keyword_search, make, model, year_value, year_range, odometer_value, odometer_range,
305
+ selected_file, max_results, show_dealer_stats):
306
+ """Interface function for simple range-based data search with slider-controlled ranges and manual text input"""
307
+ # Handle text inputs - convert empty strings to None for backend processing
308
+ processed_make = None if (not make or make.strip() == "") else make.strip()
309
+ processed_model = None if (not model or model.strip() == "") else model.strip()
310
+ processed_selected_file = None if selected_file == "Select File" else selected_file
311
+
312
+ # Convert targetΒ±range to from/to values for the matcher
313
+ year_from = year_value - year_range if year_value and year_range is not None else None
314
+ year_to = year_value + year_range if year_value and year_range is not None else None
315
+
316
+ odometer_from = odometer_value - odometer_range if odometer_value and odometer_range is not None else None
317
+ odometer_to = odometer_value + odometer_range if odometer_value and odometer_range is not None else None
318
+
319
+ if show_dealer_stats:
320
+ # Use API method that returns clean structured data
321
+ api_result = matcher.search_data_files_api(
322
+ keyword_search=keyword_search,
323
+ make=processed_make,
324
+ model=processed_model,
325
+ year_from=year_from,
326
+ year_to=year_to,
327
+ odometer_from=odometer_from,
328
+ odometer_to=odometer_to,
329
+ selected_file=processed_selected_file,
330
+ max_results=max_results
331
+ )
332
+
333
+ if not api_result["success"]:
334
+ return f"❌ {api_result['message']}", "Error occurred", ""
335
+
336
+ # Apply HTML formatting for UI display
337
+ try:
338
+ from utils import format_dealer_results_as_html
339
+ dealer_html = format_dealer_results_as_html(api_result['data']['dealers'])
340
+
341
+ summary = api_result['data']['search_summary']
342
+ message = f"βœ… Found {summary['total_matches']:,} matches from {summary['original_count']:,} records in {summary['file_searched']}"
343
+ info_text = "**πŸ† Dealer Rankings with Expandable Car Lists**\n\nClick on any dealer name to see their individual car listings with prices and specifications."
344
+
345
+ return message, info_text, dealer_html
346
+ except ImportError:
347
+ # Fallback if HTML formatting fails
348
+ return api_result['message'], "API data available but HTML formatting failed", str(api_result['data'])
349
+ else:
350
+ # For non-dealer stats, use original method that returns DataFrame
351
+ message, result_data = matcher.search_data_files(
352
+ keyword_search=keyword_search,
353
+ make=processed_make,
354
+ model=processed_model,
355
+ year_min=None,
356
+ year_max=None,
357
+ year_from=year_from,
358
+ year_to=year_to,
359
+ body_type=None, # Not used in range search
360
+ fuel_type=None, # Not used in range search
361
+ max_odometer=None, # Not using max_odometer anymore
362
+ odometer_from=odometer_from,
363
+ odometer_to=odometer_to,
364
+ max_price=None, # Not used in range search
365
+ selected_file=processed_selected_file,
366
+ max_results=max_results,
367
+ show_dealer_stats=False
368
+ )
369
+
370
+ if isinstance(result_data, pd.DataFrame) and not result_data.empty:
371
+ info_text = f"**πŸ“Š Search completed successfully**\n\n"
372
+ html_table = result_data.to_html(classes='table table-striped',
373
+ table_id='search-results', escape=False, index=False)
374
+ return message, info_text, html_table
375
+ else:
376
+ return message, "No results to display", ""
377
+
378
+
379
+ def range_search_data_interface_api(matcher, keyword_search, make, model, year_value, year_range, odometer_value, odometer_range,
380
+ selected_file, max_results, show_dealer_stats):
381
+ """Pure API interface function that returns clean JSON data without HTML formatting"""
382
+ # Handle text inputs - convert empty strings to None for backend processing
383
+ processed_make = None if (not make or make.strip() == "") else make.strip()
384
+ processed_model = None if (not model or model.strip() == "") else model.strip()
385
+ processed_selected_file = None if selected_file == "Select File" else selected_file
386
+
387
+ # Convert targetΒ±range to from/to values for the matcher
388
+ year_from = year_value - year_range if year_value and year_range is not None else None
389
+ year_to = year_value + year_range if year_value and year_range is not None else None
390
+
391
+ odometer_from = odometer_value - odometer_range if odometer_value and odometer_range is not None else None
392
+ odometer_to = odometer_value + odometer_range if odometer_value and odometer_range is not None else None
393
+
394
+ # Always use API method that returns clean structured data
395
+ api_result = matcher.search_data_files_api(
396
  keyword_search=keyword_search,
397
  make=processed_make,
398
  model=processed_model,
 
 
399
  year_from=year_from,
400
  year_to=year_to,
 
 
 
401
  odometer_from=odometer_from,
402
  odometer_to=odometer_to,
 
403
  selected_file=processed_selected_file,
404
+ max_results=max_results
 
405
  )
406
 
407
+ # Return clean JSON data directly - NO HTML formatting
408
+ return api_result
409
+
410
+
411
+ def search_data_interface_api(matcher, keyword_search, make, model, year_from, year_to, body_type, fuel_type,
412
+ odometer_from, odometer_to, max_price, selected_file, max_results, show_dealer_stats):
413
+ """Pure API interface function that returns clean JSON data without HTML formatting"""
414
+ # Handle text inputs - convert empty strings to None for backend processing
415
+ processed_make = None if (not make or make.strip() == "" or make == "Select Make") else make.strip()
416
+ processed_model = None if (not model or model.strip() == "" or model == "ALL") else model.strip()
417
+ processed_body_type = None if body_type == "Select Body Type" else body_type
418
+ processed_fuel_type = None if fuel_type == "Select Fuel Type" else fuel_type
419
+ processed_selected_file = None if selected_file == "Select File" else selected_file
420
+
421
+ # Always use API method that returns clean structured data
422
+ api_result = matcher.search_data_files_api(
423
+ keyword_search=keyword_search,
424
+ make=processed_make,
425
+ model=processed_model,
426
+ year_from=year_from,
427
+ year_to=year_to,
428
+ body_type=processed_body_type,
429
+ fuel_type=processed_fuel_type,
430
+ odometer_from=odometer_from,
431
+ odometer_to=odometer_to,
432
+ max_price=max_price,
433
+ selected_file=processed_selected_file,
434
+ max_results=max_results
435
+ )
436
+
437
+ # Return clean JSON data directly - NO HTML formatting
438
+ return api_result
439
+
440
+
441
+ def simple_search_data_interface_api(matcher, keyword_search, make, model, year_from, year_to, odometer_from, odometer_to, selected_file,
442
+ max_results, show_dealer_stats):
443
+ """Pure API interface function that returns clean JSON data without HTML formatting"""
444
+ # Handle text inputs - convert empty strings to None for backend processing
445
+ processed_make = None if (not make or make.strip() == "" or make == "Select Make") else make.strip()
446
+ processed_model = None if (not model or model.strip() == "" or model == "ALL") else model.strip()
447
+ processed_selected_file = None if selected_file == "Select File" else selected_file
448
+
449
+ # Always use API method that returns clean structured data
450
+ api_result = matcher.search_data_files_api(
451
+ keyword_search=keyword_search,
452
+ make=processed_make,
453
+ model=processed_model,
454
+ year_from=year_from,
455
+ year_to=year_to,
456
+ odometer_from=odometer_from,
457
+ odometer_to=odometer_to,
458
+ selected_file=processed_selected_file,
459
+ max_results=max_results
460
+ )
461
+
462
+ # Return clean JSON data directly - NO HTML formatting
463
+ return api_result
ui/tabs/__init__.py CHANGED
@@ -1 +1,20 @@
1
- # Tab components for the Gradio interface
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ """
2
+ UI tab components for the Swiper Match Gradio application
3
+ Contains all tab creation functions for different search interfaces
4
+ """
5
+
6
+ # Import all tab creation functions
7
+ from .simple_tab import create_simple_tab
8
+ from .detailed_tab import create_detailed_tab
9
+ from .traditional_tab import create_traditional_tab
10
+ from .simple_search_tab import create_simple_search_tab
11
+ from .range_search_tab import create_range_search_tab
12
+
13
+ # Define what gets exported when using "from ui.tabs import *"
14
+ __all__ = [
15
+ 'create_simple_tab',
16
+ 'create_detailed_tab',
17
+ 'create_traditional_tab',
18
+ 'create_simple_search_tab',
19
+ 'create_range_search_tab'
20
+ ]
ui/tabs/range_search_tab.py ADDED
@@ -0,0 +1,174 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ """
2
+ Range search tab component for advanced CSV data search with range filtering
3
+ """
4
+
5
+ import gradio as gr
6
+ from core.config import CAR_MAKES, MAKE_MODEL_DATA, DEFAULT_VALUES
7
+
8
+
9
+ def create_range_search_tab(matcher):
10
+ """Create the range SQL search tab"""
11
+
12
+ with gr.Tab("3️⃣ Simple Range Search"):
13
+ gr.Markdown("### Simple Range-Based CSV Data Search")
14
+ gr.Markdown("Easy search through car listing CSV files with manual text input and flexible range-based filters")
15
+
16
+ with gr.Row():
17
+ with gr.Column(scale=1):
18
+ gr.Markdown("### πŸ—‚οΈ File & Options")
19
+ file_choices = ["Select File"] + matcher.data_files
20
+ range_search_file = gr.Dropdown(
21
+ choices=file_choices,
22
+ label="Select CSV File",
23
+ value="Select File",
24
+ info=f"Available files: {len(matcher.data_files)}"
25
+ )
26
+
27
+ range_max_results = gr.Number(
28
+ label="Max Results",
29
+ value=DEFAULT_VALUES['max_results'],
30
+ minimum=1,
31
+ maximum=1000,
32
+ info="Limit number of results returned"
33
+ )
34
+
35
+ range_show_dealer_stats = gr.Checkbox(
36
+ label="πŸ“Š Show Dealer Rankings",
37
+ value=True,
38
+ info="Rank dealers by number of matching cars"
39
+ )
40
+
41
+ with gr.Column(scale=2):
42
+ gr.Markdown("### πŸ” Advanced Range Search Filters")
43
+
44
+ # General keyword search
45
+ range_keyword_search = gr.Textbox(
46
+ label="πŸ” Keyword Search",
47
+ placeholder="Search across all fields (make, model, dealer, location, etc.)",
48
+ info="General search across multiple columns"
49
+ )
50
+
51
+ gr.Markdown("#### Range-Based Filters")
52
+
53
+ with gr.Row():
54
+ range_search_make = gr.Textbox(
55
+ label="Make",
56
+ placeholder="e.g., Toyota, Ford, Honda",
57
+ info="Type car manufacturer name"
58
+ )
59
+ range_search_model = gr.Textbox(
60
+ label="Model",
61
+ placeholder="e.g., Camry, Focus, Civic",
62
+ info="Type car model name"
63
+ )
64
+
65
+ with gr.Row():
66
+ range_year_value = gr.Number(
67
+ label="Target Year",
68
+ value=DEFAULT_VALUES['year'],
69
+ minimum=1990,
70
+ maximum=2025,
71
+ info="Target manufacturing year"
72
+ )
73
+ range_year_range = gr.Slider(
74
+ minimum=0,
75
+ maximum=10,
76
+ value=2,
77
+ step=1,
78
+ label="Year Range (Β±years)",
79
+ info="Range around target year"
80
+ )
81
+
82
+ with gr.Row():
83
+ range_odometer_value = gr.Number(
84
+ label="Target Odometer (km)",
85
+ value=65000,
86
+ minimum=0,
87
+ info="Target odometer reading"
88
+ )
89
+ range_odometer_range = gr.Slider(
90
+ minimum=0,
91
+ maximum=100000,
92
+ value=25000,
93
+ step=1000,
94
+ label="Odometer Range (Β±km)",
95
+ info="Range around target (Β±km)"
96
+ )
97
+
98
+
99
+
100
+ # Simple range search button
101
+ range_search_btn = gr.Button(
102
+ "πŸ” Search CSV Data (Simple Range)",
103
+ variant="primary",
104
+ size="lg"
105
+ )
106
+
107
+ with gr.Row():
108
+ # Reset button
109
+ range_reset_btn = gr.Button(
110
+ "πŸ”„ Reset to Defaults",
111
+ variant="secondary",
112
+ size="sm"
113
+ )
114
+
115
+ # Range Search Results Section
116
+ with gr.Row():
117
+ range_search_status = gr.Textbox(
118
+ label="πŸ” Search Status",
119
+ interactive=False,
120
+ lines=2
121
+ )
122
+
123
+ with gr.Row():
124
+ range_search_info = gr.Markdown(
125
+ label="πŸ“Š Results Info",
126
+ value="Click 'Search CSV Data (Simple Range)' to start searching..."
127
+ )
128
+
129
+ with gr.Row():
130
+ range_search_results_table = gr.HTML(
131
+ label="πŸ“‹ Search Results",
132
+ value="<p>No search performed yet</p>"
133
+ )
134
+
135
+ gr.Markdown("""
136
+ ---
137
+ ### 🎯 Simple Range Search Features
138
+
139
+ **πŸ” Keyword Search:** General search across all text fields (make, model, dealer name, location, etc.)
140
+ **✍️ Manual Input:** Type make and model names directly - no dropdown limitations
141
+ **πŸ“Š Simple Ranges:** Set target values with flexible range controls using sliders
142
+ **🎯 Year Range:** Set target year with ± range (e.g., 2020 ± 2 years = 2018-2022)
143
+ **🎯 Odometer Range:** Set target odometer reading with ± range (e.g., 65,000 ± 25,000 km)
144
+ **πŸš€ Easy to Use:** Simplified interface with text inputs for make and model
145
+ **Dealer Ranking:** Get dealers ranked by inventory matching your criteria
146
+ **Flexible Input:** Type any make/model combination, not limited to predefined lists
147
+ **πŸ”„ Reset Button:** Quickly restore all fields to their default values
148
+ **Flexible Filtering:** Combine multiple range filters for highly targeted results
149
+ """)
150
+
151
+ def reset_range_search():
152
+ """Reset all range search fields to default values"""
153
+ return (
154
+ "", # range_keyword_search
155
+ "", # range_search_make
156
+ "", # range_search_model
157
+ DEFAULT_VALUES['year'], # range_year_value
158
+ 2, # range_year_range
159
+ 65000, # range_odometer_value
160
+ 25000, # range_odometer_range
161
+ "Select File", # range_search_file
162
+ DEFAULT_VALUES['max_results'], # range_max_results
163
+ True, # range_show_dealer_stats
164
+ )
165
+
166
+ return {
167
+ 'inputs': [range_keyword_search, range_search_make, range_search_model, range_year_value, range_year_range,
168
+ range_odometer_value, range_odometer_range, range_search_file,
169
+ range_max_results, range_show_dealer_stats],
170
+ 'outputs': [range_search_status, range_search_info, range_search_results_table],
171
+ 'button': range_search_btn,
172
+ 'reset_button': range_reset_btn,
173
+ 'reset_function': reset_range_search
174
+ }
ui/tabs/simple_tab.py CHANGED
@@ -60,7 +60,7 @@ def create_simple_tab(simple_matcher):
60
  simple_top_dealer = gr.Textbox(
61
  label="πŸ† Top Recommended Dealer",
62
  interactive=False,
63
- lines=2
64
  )
65
  simple_confidence = gr.Textbox(
66
  label="🎯 Confidence Score",
 
60
  simple_top_dealer = gr.Textbox(
61
  label="πŸ† Top Recommended Dealer",
62
  interactive=False,
63
+ lines=3
64
  )
65
  simple_confidence = gr.Textbox(
66
  label="🎯 Confidence Score",
utils/__init__.py CHANGED
@@ -1 +1,21 @@
1
- # Utility functions for the Swiper Match application
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ """
2
+ Utility functions for the Swiper Match application
3
+ """
4
+
5
+ # Import all helper functions and make them available at package level
6
+ from .helpers import (
7
+ get_model_status_info,
8
+ get_app_header,
9
+ get_app_description,
10
+ setup_event_handlers,
11
+ format_dealer_results_as_html
12
+ )
13
+
14
+ # Define what gets exported when using "from utils import *"
15
+ __all__ = [
16
+ 'get_model_status_info',
17
+ 'get_app_header',
18
+ 'get_app_description',
19
+ 'setup_event_handlers',
20
+ 'format_dealer_results_as_html'
21
+ ]
utils/helpers.py CHANGED
@@ -57,7 +57,7 @@ def get_app_description():
57
 
58
  def setup_event_handlers(tabs_data, interface_functions, matchers):
59
  """Setup event handlers for all tabs"""
60
- simple_tab, detailed_tab, traditional_tab, simple_search_tab = tabs_data
61
  matcher, simple_matcher = matchers
62
 
63
  # Simple tab click event
@@ -88,6 +88,13 @@ def setup_event_handlers(tabs_data, interface_functions, matchers):
88
  outputs=simple_search_tab['outputs']
89
  )
90
 
 
 
 
 
 
 
 
91
  # Set up dynamic model updating based on make selection (only for tabs with dropdowns)
92
  if 'make_dropdown' in detailed_tab and 'model_dropdown' in detailed_tab:
93
  detailed_tab['make_dropdown'].change(
@@ -104,6 +111,8 @@ def setup_event_handlers(tabs_data, interface_functions, matchers):
104
  outputs=simple_search_tab['model_dropdown']
105
  )
106
 
 
 
107
  # Reset buttons
108
  if 'reset_button' in simple_search_tab:
109
  simple_search_tab['reset_button'].click(
@@ -117,4 +126,133 @@ def setup_event_handlers(tabs_data, interface_functions, matchers):
117
  fn=traditional_tab['reset_function'],
118
  inputs=[],
119
  outputs=traditional_tab['inputs']
120
- )
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
57
 
58
  def setup_event_handlers(tabs_data, interface_functions, matchers):
59
  """Setup event handlers for all tabs"""
60
+ simple_tab, detailed_tab, traditional_tab, simple_search_tab, range_search_tab = tabs_data
61
  matcher, simple_matcher = matchers
62
 
63
  # Simple tab click event
 
88
  outputs=simple_search_tab['outputs']
89
  )
90
 
91
+ # Range search click event
92
+ range_search_tab['button'].click(
93
+ fn=lambda *args: interface_functions['simple_range_search'](matcher, *args),
94
+ inputs=range_search_tab['inputs'],
95
+ outputs=range_search_tab['outputs']
96
+ )
97
+
98
  # Set up dynamic model updating based on make selection (only for tabs with dropdowns)
99
  if 'make_dropdown' in detailed_tab and 'model_dropdown' in detailed_tab:
100
  detailed_tab['make_dropdown'].change(
 
111
  outputs=simple_search_tab['model_dropdown']
112
  )
113
 
114
+ # Range search now uses text inputs instead of dropdowns, so no model update needed
115
+
116
  # Reset buttons
117
  if 'reset_button' in simple_search_tab:
118
  simple_search_tab['reset_button'].click(
 
126
  fn=traditional_tab['reset_function'],
127
  inputs=[],
128
  outputs=traditional_tab['inputs']
129
+ )
130
+
131
+ if 'reset_button' in range_search_tab:
132
+ range_search_tab['reset_button'].click(
133
+ fn=range_search_tab['reset_function'],
134
+ inputs=[],
135
+ outputs=range_search_tab['inputs']
136
+ )
137
+
138
+
139
+ def format_dealer_results_as_html(dealer_results):
140
+ """
141
+ Format dealer search results as HTML for UI display
142
+
143
+ Args:
144
+ dealer_results: List of dealer dictionaries with structure:
145
+ [
146
+ {
147
+ "rank": 1,
148
+ "dealer_name": "Toyota City",
149
+ "car_count": 25,
150
+ "contact": {
151
+ "phone": "(08) 9999 1234",
152
+ "city": "Perth",
153
+ "state": "WA"
154
+ },
155
+ "cars": [list of car dictionaries]
156
+ }
157
+ ]
158
+
159
+ Returns:
160
+ str: HTML formatted string for display
161
+ """
162
+ if not dealer_results:
163
+ return "<p>No dealer results to display</p>"
164
+
165
+ html_content = ""
166
+
167
+ for dealer in dealer_results:
168
+ rank = dealer.get('rank', 0)
169
+ dealer_name = dealer.get('dealer_name', 'Unknown Dealer')
170
+ car_count = dealer.get('car_count', 0)
171
+ contact = dealer.get('contact', {})
172
+ cars = dealer.get('cars', [])
173
+
174
+ # Create rank emoji
175
+ rank_emoji = "πŸ₯‡" if rank == 1 else "πŸ₯ˆ" if rank == 2 else "πŸ₯‰" if rank == 3 else f"#{rank}"
176
+
177
+ # Format dealer header with contact info
178
+ dealer_header = f"{rank_emoji} {dealer_name} ({car_count} cars)"
179
+ if contact:
180
+ if contact.get('phone'):
181
+ dealer_header += f" β€’ πŸ“ž {contact['phone']}"
182
+ if contact.get('city') != 'Unknown' and contact.get('state') != 'Unknown':
183
+ dealer_header += f" β€’ πŸ“ {contact['city']}, {contact['state']}"
184
+
185
+ html_content += f"""
186
+ <details style="margin-bottom: 10px; border: 1px solid #ddd; padding: 5px; border-radius: 5px;">
187
+ <summary style="font-weight: bold; cursor: pointer; padding: 5px;">
188
+ {dealer_header}
189
+ </summary>
190
+ <div style="margin-top: 10px; max-height: 300px; overflow-y: auto;">
191
+ """
192
+
193
+ # Add contact information section at the top if available
194
+ if contact and (contact.get('phone') or
195
+ (contact.get('city') != 'Unknown' or contact.get('state') != 'Unknown')):
196
+ html_content += f"""
197
+ <div style="padding: 8px; margin: 4px 0; background: #e8f4f8; border-radius: 3px; border-left: 3px solid #007bff;">
198
+ <strong>πŸ“ž Contact Information</strong><br>
199
+ """
200
+ if contact.get('phone'):
201
+ html_content += f"<small>Phone: {contact['phone']}</small><br>"
202
+ if contact.get('city') != 'Unknown' or contact.get('state') != 'Unknown':
203
+ html_content += f"<small>Location: {contact.get('city', 'Unknown')}, {contact.get('state', 'Unknown')}</small>"
204
+ html_content += "</div>"
205
+
206
+ # Add individual cars (limit to first 20)
207
+ displayed_cars = cars[:20]
208
+ for car in displayed_cars:
209
+ make = car.get('make', 'Unknown')
210
+ model = car.get('model', 'Unknown')
211
+ year = car.get('year', 'Unknown')
212
+ price = car.get('price', 'Unknown')
213
+ odometer = car.get('odometer', 'Unknown')
214
+ body_type = car.get('body_type', 'Unknown')
215
+ fuel_type = car.get('fuel_type', 'Unknown')
216
+ transmission = car.get('transmission', 'Unknown')
217
+
218
+ # Format price
219
+ if price != 'Unknown' and price is not None:
220
+ try:
221
+ price_str = f"${int(float(price)):,}"
222
+ except:
223
+ price_str = "Price on request"
224
+ else:
225
+ price_str = "Price on request"
226
+
227
+ # Format odometer
228
+ if odometer != 'Unknown' and odometer is not None:
229
+ try:
230
+ odometer_str = f"{int(float(odometer)):,} km"
231
+ except:
232
+ odometer_str = "Unknown km"
233
+ else:
234
+ odometer_str = "Unknown km"
235
+
236
+ html_content += f"""
237
+ <div style="padding: 8px; margin: 4px 0; background: #f9f9f9; border-radius: 3px; border-left: 3px solid #007bff;">
238
+ <strong>{year} {make} {model}</strong> - {price_str}<br>
239
+ <small style="color: #666;">{body_type} β€’ {fuel_type} β€’ {transmission} β€’ {odometer_str}</small>
240
+ </div>
241
+ """
242
+
243
+ # Add "show more" if there are more cars than displayed
244
+ total_cars = dealer.get('total_cars', len(cars))
245
+ if total_cars > len(cars):
246
+ remaining_cars = total_cars - len(cars)
247
+ html_content += f"""
248
+ <div style="padding: 8px; margin: 4px 0; background: #e9ecef; border-radius: 3px; text-align: center; font-style: italic;">
249
+ ... and {remaining_cars} more cars
250
+ </div>
251
+ """
252
+
253
+ html_content += """
254
+ </div>
255
+ </details>
256
+ """
257
+
258
+ return html_content