Spaces:
Sleeping
Sleeping
Almaz K commited on
Commit Β·
e4c90f5
1
Parent(s): 79a2170
added phone numbers to results
Browse files- app.py +6 -3
- core/__init__.py +24 -1
- core/matcher.py +359 -69
- ui/__init__.py +41 -1
- ui/interface.py +295 -53
- ui/tabs/__init__.py +20 -1
- ui/tabs/range_search_tab.py +174 -0
- ui/tabs/simple_tab.py +1 -1
- utils/__init__.py +21 -1
- utils/helpers.py +140 -2
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 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 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 |
-
|
|
|
|
|
|
|
|
|
|
| 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 |
-
|
| 593 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 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
|
| 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 |
-
|
| 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 |
-
#
|
| 626 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 627 |
|
| 628 |
-
|
| 629 |
-
|
| 630 |
-
|
| 631 |
-
|
| 632 |
-
|
| 633 |
-
|
| 634 |
-
|
|
|
|
| 635 |
|
| 636 |
-
|
| 637 |
-
|
| 638 |
-
|
| 639 |
-
|
| 640 |
-
|
| 641 |
-
|
| 642 |
-
|
| 643 |
-
|
| 644 |
-
|
| 645 |
-
|
| 646 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 647 |
|
| 648 |
-
#
|
| 649 |
-
|
| 650 |
-
|
| 651 |
-
|
| 652 |
-
|
| 653 |
-
|
| 654 |
-
|
| 655 |
-
odometer_str = "Unknown km"
|
| 656 |
|
| 657 |
-
#
|
| 658 |
-
|
| 659 |
-
|
| 660 |
-
|
| 661 |
-
|
| 662 |
-
|
| 663 |
-
else:
|
| 664 |
-
price_str = "Price on request"
|
| 665 |
|
| 666 |
-
|
| 667 |
-
|
| 668 |
-
|
| 669 |
-
|
| 670 |
-
|
| 671 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 672 |
|
| 673 |
-
#
|
| 674 |
-
if
|
| 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 |
-
|
| 682 |
-
|
| 683 |
-
|
| 684 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 685 |
|
| 686 |
-
|
| 687 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 688 |
|
| 689 |
except Exception as e:
|
| 690 |
-
logger.error(f"β
|
| 691 |
-
return
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 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 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 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 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 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
|
| 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 |
-
|
| 145 |
-
|
| 146 |
-
|
| 147 |
-
|
| 148 |
-
|
| 149 |
-
|
| 150 |
-
|
| 151 |
-
|
| 152 |
-
|
| 153 |
-
|
| 154 |
-
|
| 155 |
-
|
| 156 |
-
|
| 157 |
-
|
| 158 |
-
|
| 159 |
-
|
| 160 |
-
|
| 161 |
-
|
|
|
|
| 162 |
|
| 163 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 164 |
else:
|
| 165 |
-
#
|
| 166 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 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 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 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 |
-
|
| 203 |
-
|
| 204 |
-
|
| 205 |
-
|
| 206 |
-
|
| 207 |
-
|
| 208 |
-
|
| 209 |
-
|
| 210 |
-
|
| 211 |
-
|
| 212 |
-
|
| 213 |
-
else
|
| 214 |
-
|
| 215 |
-
|
| 216 |
-
|
| 217 |
-
|
| 218 |
-
|
| 219 |
-
|
| 220 |
-
|
| 221 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 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 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 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=
|
| 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 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 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
|