redxican commited on
Commit
be49f6a
·
verified ·
1 Parent(s): bdc687a

Update app.py

Browse files
Files changed (1) hide show
  1. app.py +600 -379
app.py CHANGED
@@ -2,6 +2,7 @@
2
  """
3
  Turbo Air Viewer - Equipment Specification Database Viewer
4
  Enhanced version with product image extraction and display
 
5
 
6
  Required dependencies (add to requirements.txt):
7
  - streamlit
@@ -13,6 +14,7 @@ Required dependencies (add to requirements.txt):
13
  """
14
 
15
  import streamlit as st
 
16
  import sqlite3
17
  import json
18
  from pathlib import Path
@@ -34,6 +36,17 @@ from reportlab.lib.styles import getSampleStyleSheet, ParagraphStyle
34
  from reportlab.lib.units import inch
35
  from reportlab.platypus import SimpleDocTemplate, Table, TableStyle, Paragraph, Spacer, Image as RLImage, PageBreak
36
  from reportlab.lib.enums import TA_CENTER
 
 
 
 
 
 
 
 
 
 
 
37
 
38
  # Streamlit page config MUST be first
39
  st.set_page_config(
@@ -42,131 +55,20 @@ st.set_page_config(
42
  layout="wide"
43
  )
44
 
45
- # Configuration
46
- DB_FILENAME = "turbo_air_db.sqlite"
47
- DB_URL = "https://huggingface.co/spaces/TurboAir/turbo-air-viewer/resolve/main/turbo_air_db.sqlite"
48
-
49
- def download_database():
50
- """Download database from Hugging Face if not present or invalid"""
51
- if os.path.exists(DB_FILENAME):
52
- # Check if existing file is valid
53
- try:
54
- # Check file size first
55
- file_size = os.path.getsize(DB_FILENAME)
56
- if file_size < 1000000: # Less than 1MB, probably wrong
57
- st.warning(f"Existing database file is too small ({file_size/1024/1024:.1f} MB), re-downloading...")
58
- os.remove(DB_FILENAME)
59
- else:
60
- conn = sqlite3.connect(DB_FILENAME)
61
- cursor = conn.cursor()
62
- cursor.execute("SELECT name FROM sqlite_master WHERE type='table' LIMIT 1")
63
- tables = cursor.fetchall()
64
- conn.close()
65
-
66
- if tables:
67
- # Don't show success message - just return
68
- return DB_FILENAME # Valid database exists
69
- except:
70
- st.warning("Existing database file is invalid, re-downloading...")
71
- if os.path.exists(DB_FILENAME):
72
- os.remove(DB_FILENAME)
73
-
74
- # Download the database
75
- st.info("🔄 Downloading database... This is a one-time download of 279 MB.")
76
-
77
- try:
78
- # Method 1: Try requests first
79
- import requests
80
-
81
- headers = {
82
- 'User-Agent': 'Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36'
83
- }
84
-
85
- response = requests.get(DB_URL, headers=headers, stream=True, timeout=30, allow_redirects=True)
86
-
87
- # Check if we got a valid response
88
- if response.status_code == 200:
89
- total_size = int(response.headers.get('content-length', 0))
90
-
91
- # Only proceed if file size looks right (should be ~279 MB)
92
- if total_size < 10000000: # Less than 10MB
93
- st.error(f"Downloaded file too small ({total_size/1024/1024:.1f} MB). Expected ~279 MB.")
94
- st.error("The database file may be a Git LFS pointer.")
95
- raise Exception("File size mismatch")
96
-
97
- progress_bar = st.progress(0)
98
- status_text = st.empty()
99
-
100
- with open(DB_FILENAME, 'wb') as f:
101
- downloaded = 0
102
- for chunk in response.iter_content(chunk_size=1024*1024): # 1MB chunks
103
- if chunk:
104
- f.write(chunk)
105
- downloaded += len(chunk)
106
- if total_size > 0:
107
- progress = downloaded / total_size
108
- progress_bar.progress(progress)
109
- status_text.text(f"Downloaded {downloaded/1024/1024:.1f} MB / {total_size/1024/1024:.1f} MB")
110
-
111
- progress_bar.empty()
112
- status_text.empty()
113
-
114
- # Verify the downloaded file
115
- if os.path.getsize(DB_FILENAME) > 100000000: # At least 100MB
116
- try:
117
- conn = sqlite3.connect(DB_FILENAME)
118
- cursor = conn.cursor()
119
- cursor.execute("SELECT name FROM sqlite_master WHERE type='table' LIMIT 1")
120
- tables = cursor.fetchall()
121
- conn.close()
122
-
123
- if tables:
124
- # Don't show success message - just return
125
- return DB_FILENAME
126
- except:
127
- st.error("Downloaded file is not a valid SQLite database")
128
-
129
- else:
130
- st.error(f"Failed to download: HTTP {response.status_code}")
131
-
132
- except requests.exceptions.RequestException as e:
133
- st.error(f"Download failed: {str(e)}")
134
- except ImportError:
135
- st.error("requests library not installed. Please add 'requests' to requirements.txt")
136
- except Exception as e:
137
- st.error(f"Unexpected error: {str(e)}")
138
-
139
- # If download failed, provide manual instructions
140
- st.error("❌ Automatic download failed.")
141
- st.markdown("""
142
- ### Manual Download Instructions:
143
-
144
- 1. **Download directly from this link:**
145
- [Download turbo_air_db.sqlite (279 MB)](https://huggingface.co/spaces/TurboAir/turbo-air-viewer/resolve/main/turbo_air_db.sqlite)
146
-
147
- 2. **Or use wget/curl:**
148
- ```bash
149
- wget https://huggingface.co/spaces/TurboAir/turbo-air-viewer/resolve/main/turbo_air_db.sqlite
150
- # or
151
- curl -L -o turbo_air_db.sqlite https://huggingface.co/spaces/TurboAir/turbo-air-viewer/resolve/main/turbo_air_db.sqlite
152
- ```
153
-
154
- 3. **Or clone with Git LFS:**
155
- ```bash
156
- git lfs install
157
- git clone https://huggingface.co/spaces/TurboAir/turbo-air-viewer
158
- ```
159
-
160
- **Note:** The database file is 279 MB. Make sure you have a stable internet connection.
161
- """)
162
-
163
- return None
164
 
165
- # Download or verify database
166
- DB_PATH = download_database()
 
 
167
 
168
- if DB_PATH is None:
169
- st.error("❌ Unable to load database. Please refresh the page to try again.")
 
 
 
170
  st.stop()
171
 
172
  # Product type mappings
@@ -243,6 +145,12 @@ st.markdown("""
243
  border-radius: 8px;
244
  }
245
 
 
 
 
 
 
 
246
  .main-title {
247
  color: #4CAF50;
248
  font-size: 2.5em;
@@ -291,14 +199,14 @@ st.markdown("""
291
  border: 1px solid #444;
292
  }
293
 
294
- .bookmark-image {
295
  border: 1px solid #444;
296
  border-radius: 4px;
297
  margin-bottom: 8px;
298
  transition: transform 0.2s ease;
299
  }
300
 
301
- .bookmark-image:hover {
302
  transform: scale(1.05);
303
  border-color: #4CAF50;
304
  }
@@ -323,6 +231,34 @@ st.markdown("""
323
  .stSelectbox input {
324
  cursor: text !important;
325
  }
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
326
  </style>
327
 
328
  <script>
@@ -344,14 +280,61 @@ document.addEventListener('DOMContentLoaded', function() {
344
  # Initialize session state
345
  if 'selected_model' not in st.session_state:
346
  st.session_state.selected_model = None
347
- if 'bookmarked_models' not in st.session_state:
348
- st.session_state.bookmarked_models = []
349
  if 'text_only_view' not in st.session_state:
350
  st.session_state.text_only_view = False
351
  if 'product_images' not in st.session_state:
352
  st.session_state.product_images = {}
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
353
 
354
- def extract_pdf_thumbnail(pdf_url, model_name, max_width=300, max_height=400):
355
  """Extract first page of PDF as thumbnail image"""
356
  cache_key = f"thumb_{model_name}"
357
 
@@ -360,68 +343,95 @@ def extract_pdf_thumbnail(pdf_url, model_name, max_width=300, max_height=400):
360
  return st.session_state.product_images[cache_key]
361
 
362
  try:
363
- # Download PDF to temporary file (silently)
364
- response = requests.get(pdf_url, timeout=30)
365
- if response.status_code == 200:
366
- with tempfile.NamedTemporaryFile(delete=False, suffix='.pdf') as tmp_file:
367
- tmp_file.write(response.content)
368
- tmp_path = tmp_file.name
 
 
 
 
 
 
 
369
 
370
- # Open PDF and extract first page
371
- pdf_document = fitz.open(tmp_path) # type: ignore
372
- first_page = pdf_document[0]
373
 
374
- # Render page as image (2x resolution for better quality)
375
- mat = fitz.Matrix(2, 2)
376
- # Try new API first, fallback to old API
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
377
  try:
378
- # New PyMuPDF API
379
- pix = first_page.get_pixmap(matrix=mat)
380
  except:
381
  try:
382
- # Alternative method
383
- pix = first_page.get_pixmap(mat)
384
  except:
385
- # Fallback without matrix for very old versions
386
- pix = first_page.get_pixmap()
387
-
388
- # Convert to PIL Image
389
- img_data = pix.tobytes("png")
390
- img = Image.open(io.BytesIO(img_data))
391
-
392
- # Calculate aspect ratio and resize
393
- width, height = img.size
394
- aspect_ratio = width / height
395
-
396
- if width > max_width:
397
- new_width = max_width
398
- new_height = int(new_width / aspect_ratio)
399
- else:
400
- new_width = width
401
- new_height = height
402
-
403
- if new_height > max_height:
404
- new_height = max_height
405
- new_width = int(new_height * aspect_ratio)
406
-
407
- img = img.resize((new_width, new_height), Image.Resampling.LANCZOS)
408
-
409
- # Convert to base64 for caching
410
- buffered = io.BytesIO()
411
- img.save(buffered, format="PNG")
412
- img_base64 = base64.b64encode(buffered.getvalue()).decode()
413
-
414
- # Cache in session state
415
- st.session_state.product_images[cache_key] = img_base64
416
-
417
- # Cleanup
418
- pdf_document.close()
419
- os.unlink(tmp_path)
420
-
421
- return img_base64
422
-
423
  except Exception as e:
424
- # Silently fail - don't show warnings
425
  return None
426
 
427
  # Cache functions
@@ -436,30 +446,11 @@ def get_all_models():
436
  all_models = []
437
 
438
  try:
439
- # First try the model_index table (faster)
440
- cursor.execute("SELECT name FROM sqlite_master WHERE type='table' AND name='model_index'")
441
- if cursor.fetchone():
442
- cursor.execute("SELECT model FROM model_index ORDER BY model")
443
- models = cursor.fetchall()
444
- if models:
445
- all_models = [m[0] for m in models]
446
- conn.close()
447
- return all_models
448
-
449
- # Fallback to scanning documents
450
- cursor.execute("SELECT full_data FROM documents")
451
- rows = cursor.fetchall()
452
-
453
- model_set = set()
454
- for row in rows:
455
- try:
456
- data = json.loads(row[0])
457
- models = data.get('models', [])
458
- model_set.update(models)
459
- except:
460
- continue
461
-
462
- all_models = sorted(list(model_set))
463
 
464
  except Exception as e:
465
  st.error(f"Database error: {e}")
@@ -471,38 +462,91 @@ def get_all_models():
471
 
472
  @st.cache_data
473
  def get_model_data(model_name):
474
- """Get data for specific model - CACHED"""
475
  if DB_PATH is None:
476
  return None
477
  conn = sqlite3.connect(DB_PATH)
478
  cursor = conn.cursor()
479
 
480
  try:
481
- # Search for the model in documents
482
- cursor.execute("""
483
- SELECT id, file_path, full_data, quality FROM documents
484
- WHERE full_data LIKE ?
485
- ORDER BY import_date DESC
486
- LIMIT 1
487
- """, (f'%"{model_name}"%',))
488
-
489
  row = cursor.fetchone()
 
490
  if row:
491
- doc_id, file_path, full_data_str, quality = row
 
492
 
493
- try:
494
- data = json.loads(full_data_str)
495
- except:
496
- data = {'models': [], 'specs': {}, 'features': []}
497
 
498
- filename = Path(file_path).name if file_path else 'Unknown'
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
499
 
500
  return {
501
- 'id': doc_id,
502
  'filename': filename,
503
  'file_path': file_path,
504
- 'data': data,
505
- 'quality': quality
 
 
 
 
 
 
 
 
 
506
  }
507
 
508
  except Exception as e:
@@ -583,64 +627,132 @@ def format_model_option(model):
583
  product_type = get_product_type(model)
584
  return f"{model} - {product_type}"
585
 
586
- def export_bookmarked_models():
587
- """Export bookmarked models to CSV"""
588
- if not st.session_state.bookmarked_models:
 
 
 
 
589
  return None
590
 
591
- export_data = []
592
- for model in st.session_state.bookmarked_models:
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
593
  model_data = get_model_data(model)
594
- if model_data:
595
- specs = model_data['data'].get('specs', {})
596
- specs = clean_spec_data(specs)
597
- features = model_data['data'].get('features', [])
598
 
599
- # Parse dimensions
600
- dimensions = specs.get('dimensions', 'N/A')
601
- width, depth, height = 'N/A', 'N/A', 'N/A'
 
 
 
 
 
602
 
603
- dim_patterns = [
604
- r'(\d+\.?\d*)["\s]*[WwLl]\s*[xX×]\s*(\d+\.?\d*)["\s]*[DdWw]\s*[xX×]\s*(\d+\.?\d*)["\s]*[HhTt]',
605
- r'(\d+\.?\d*)\s*[xX×]\s*(\d+\.?\d*)\s*[xX×]\s*(\d+\.?\d*)',
606
- r'(\d+\.?\d*)"?\s*x\s*(\d+\.?\d*)"?\s*x\s*(\d+\.?\d*)"?'
607
- ]
608
 
609
- for pattern in dim_patterns:
610
- dim_match = re.search(pattern, dimensions)
611
- if dim_match:
612
- width = f"{dim_match.group(1)}\""
613
- depth = f"{dim_match.group(2)}\""
614
- height = f"{dim_match.group(3)}\""
615
- break
616
 
617
- row = {
618
- 'Model': model,
619
- 'Product Type': get_product_type(model),
620
- 'Voltage': specs.get('voltage', 'N/A'),
621
- 'Amperage': specs.get('amperage', 'N/A'),
622
- 'Phase': specs.get('phase', 'N/A'),
623
- 'Frequency': specs.get('frequency', 'N/A'),
624
- 'Width': width,
625
- 'Depth': depth,
626
- 'Height': height,
627
- 'Capacity': specs.get('capacity', 'N/A'),
628
- 'Weight': specs.get('weight', 'N/A'),
629
- 'Refrigerant': specs.get('refrigerant', 'N/A'),
630
- 'Temperature Range': specs.get('temperature_range', 'N/A'),
631
- 'BTU': specs.get('btu', 'N/A'),
632
- 'Compressor': specs.get('compressor', 'N/A'),
633
- 'Features': '; '.join(features),
634
- 'Source File': model_data['filename']
635
- }
636
- export_data.append(row)
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
637
 
638
- df = pd.DataFrame(export_data)
639
- return df.to_csv(index=False)
 
 
 
640
 
641
- def export_bookmarked_models_pdf():
642
- """Export bookmarked models to PDF with images and specifications"""
643
- if not st.session_state.bookmarked_models:
644
  return None
645
 
646
  # Create PDF in memory
@@ -677,8 +789,8 @@ def export_bookmarked_models_pdf():
677
  styles['Normal']))
678
  elements.append(Spacer(1, 0.5*inch))
679
 
680
- # Process each bookmarked model
681
- for idx, model in enumerate(st.session_state.bookmarked_models):
682
  if idx > 0:
683
  elements.append(PageBreak())
684
 
@@ -691,15 +803,15 @@ def export_bookmarked_models_pdf():
691
 
692
  # Try to get product image
693
  if model_data.get('file_path'):
694
- pdf_filename = model_data['file_path'].replace('\\', '/').split('/')[-1]
695
- pdf_url = f"https://huggingface.co/spaces/TurboAir/TurboAirViewer/resolve/main/pdfs/{pdf_filename}"
696
 
697
  # Get cached image or extract it
698
  cache_key = f"thumb_{model}"
699
  img_base64 = st.session_state.product_images.get(cache_key)
700
 
701
  if not img_base64:
702
- img_base64 = extract_pdf_thumbnail(pdf_url, model, max_width=200, max_height=250)
703
 
704
  if img_base64:
705
  # Convert base64 to image for PDF
@@ -738,6 +850,11 @@ def export_bookmarked_models_pdf():
738
  if specs.get('btu') and specs.get('btu') != 'N/A':
739
  spec_data.append(['BTU', specs['btu']])
740
 
 
 
 
 
 
741
  if len(spec_data) > 1:
742
  # Create table
743
  spec_table = Table(spec_data, colWidths=[2.5*inch, 4*inch])
@@ -766,7 +883,7 @@ def export_bookmarked_models_pdf():
766
  elements.append(Spacer(1, 0.2*inch))
767
 
768
  # Source info
769
- elements.append(Paragraph(f"<i>Source: {model_data['filename']}</i>", styles['Normal']))
770
 
771
  # Build PDF
772
  doc.build(elements)
@@ -780,7 +897,7 @@ def display_pdf_preview(file_path, model_name):
780
  pdf_filename = file_path.replace('\\', '/').split('/')[-1]
781
 
782
  # Create the HuggingFace Space URL for the PDF
783
- pdf_url = f"https://huggingface.co/spaces/TurboAir/TurboAirViewer/resolve/main/pdfs/{pdf_filename}"
784
 
785
  # Control buttons row
786
  col1, col2, col3 = st.columns([2, 1, 1])
@@ -819,21 +936,20 @@ def display_pdf_preview(file_path, model_name):
819
  st.markdown("🔍 **Backup viewer** (if PDF doesn't display above):")
820
 
821
  # Use PDF.js viewer directly (most reliable for HuggingFace Spaces)
822
- import streamlit.components.v1 as components
823
  components.iframe(
824
  src=f"https://mozilla.github.io/pdf.js/web/viewer.html?file={quote(pdf_url, safe='')}",
825
- height=900,
826
  scrolling=True
827
  )
828
 
829
- def display_bookmarked_models():
830
- """Display bookmarked models section with optimized toggle"""
831
- if not st.session_state.bookmarked_models:
832
- st.info("📌 No models bookmarked yet. Select models to create your custom list!")
833
  return
834
 
835
  # Collapsible header
836
- with st.expander(f"📌 Bookmarked Models ({len(st.session_state.bookmarked_models)} selected)", expanded=True):
837
  view_col1, view_col2, view_col3 = st.columns([2, 1, 1])
838
 
839
  with view_col3:
@@ -842,56 +958,56 @@ def display_bookmarked_models():
842
  if st.button(toggle_label, key="toggle_view", use_container_width=True):
843
  st.session_state.text_only_view = not st.session_state.text_only_view
844
 
845
- # Display bookmarked models with or without images
846
  if st.session_state.text_only_view:
847
  # Text-only view (original compact list)
848
  display_limit = 5
849
- for idx, model in enumerate(st.session_state.bookmarked_models[:display_limit]):
850
  col_select, col_remove = st.columns([5, 1])
851
  with col_select:
852
  if st.button(f"• {model}", key=f"select_text_{idx}", use_container_width=True, help=f"Click to view {model}"):
853
  st.session_state.selected_model = model
854
  st.rerun()
855
  with col_remove:
856
- if st.button("❌", key=f"remove_bookmark_list_{idx}", help=f"Remove {model}"):
857
- st.session_state.bookmarked_models.remove(model)
858
  st.rerun()
859
 
860
- if len(st.session_state.bookmarked_models) > display_limit:
861
- with st.expander(f"Show all {len(st.session_state.bookmarked_models)} bookmarks"):
862
- for idx, model in enumerate(st.session_state.bookmarked_models[display_limit:], display_limit):
863
  col_select, col_remove = st.columns([5, 1])
864
  with col_select:
865
  if st.button(f"• {model}", key=f"select_text_exp_{idx}", use_container_width=True, help=f"Click to view {model}"):
866
  st.session_state.selected_model = model
867
  st.rerun()
868
  with col_remove:
869
- if st.button("❌", key=f"remove_bookmark_exp_{idx}", help=f"Remove {model}"):
870
- st.session_state.bookmarked_models.remove(model)
871
  st.rerun()
872
 
873
- else: # ← THIS IS THE KEY FIX - Added else block
874
  # Image view
875
  # Display in grid layout
876
  cols_per_row = 6 # Changed from 4 to 6 columns for narrower items
877
- for i in range(0, len(st.session_state.bookmarked_models), cols_per_row):
878
  cols = st.columns(cols_per_row)
879
 
880
  for j, col in enumerate(cols):
881
- if i + j < len(st.session_state.bookmarked_models):
882
- model = st.session_state.bookmarked_models[i + j]
883
  model_data = get_model_data(model)
884
 
885
  with col:
886
- # Container for each bookmarked item
887
- st.markdown('<div class="bookmark-item">', unsafe_allow_html=True)
888
 
889
- # Container for each bookmarked item
890
  with st.container():
891
  # Try to get and display thumbnail
892
  if model_data and model_data.get('file_path'):
893
- pdf_filename = model_data['file_path'].replace('\\', '/').split('/')[-1]
894
- pdf_url = f"https://huggingface.co/spaces/TurboAir/TurboAirViewer/resolve/main/pdfs/{pdf_filename}"
895
 
896
  # Check if image is already cached
897
  cache_key = f"thumb_{model}"
@@ -902,18 +1018,18 @@ def display_bookmarked_models():
902
  st.markdown(
903
  f'<img src="data:image/png;base64,{img_base64}" '
904
  f'style="width:100%; max-height:150px; object-fit:contain; cursor:pointer;" '
905
- f'class="bookmark-image">',
906
  unsafe_allow_html=True
907
  )
908
  else:
909
  # Extract image with minimal loading indication
910
- img_base64 = extract_pdf_thumbnail(pdf_url, model, max_width=200, max_height=250)
911
 
912
  if img_base64:
913
  st.markdown(
914
  f'<img src="data:image/png;base64,{img_base64}" '
915
  f'style="width:100%; max-height:200px; object-fit:contain; cursor:pointer;" '
916
- f'class="bookmark-image">',
917
  unsafe_allow_html=True
918
  )
919
  else:
@@ -935,44 +1051,118 @@ def display_bookmarked_models():
935
  st.rerun()
936
  with col_remove:
937
  if st.button("❌", key=f"remove_img_{i}_{j}", use_container_width=True, type="secondary"):
938
- st.session_state.bookmarked_models.remove(model)
939
  st.rerun()
940
 
941
  st.markdown('</div>', unsafe_allow_html=True)
942
 
943
- # Export section - MOVED OUTSIDE THE IF/ELSE AND PROPERLY INDENTED
944
  st.markdown("---")
945
- export_col1, export_col2, export_col3 = st.columns(3)
946
-
947
- with export_col1:
948
- csv_data = export_bookmarked_models()
949
- if csv_data:
950
- st.download_button(
951
- "📥 Export CSV",
952
- data=csv_data,
953
- file_name=f"turbo_air_selection_{datetime.now().strftime('%Y%m%d_%H%M')}.csv",
954
- mime="text/csv",
955
- use_container_width=True
956
- )
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
957
 
958
- with export_col2:
959
  # PDF export button
960
- pdf_data = export_bookmarked_models_pdf()
961
  if pdf_data:
962
  st.download_button(
963
- "📄 Export PDF",
964
  data=pdf_data,
965
  file_name=f"turbo_air_report_{datetime.now().strftime('%Y%m%d_%H%M')}.pdf",
966
  mime="application/pdf",
967
  use_container_width=True,
968
- type="primary"
969
  )
970
 
971
- with export_col3:
972
- if st.button("🗑️ Clear All", use_container_width=True):
973
- st.session_state.bookmarked_models = []
 
974
  st.session_state.product_images = {} # Clear image cache too
975
  st.rerun()
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
976
 
977
  # MAIN UI
978
  st.title("❄️ Turbo Air Equipment Viewer")
@@ -982,14 +1172,14 @@ st.caption("Professional Equipment Specification Database")
982
  all_models = get_all_models()
983
 
984
  if not all_models:
985
- st.error("⚠️ No data found in database. Please ensure turbo_air_db.sqlite is available.")
986
  st.stop()
987
 
988
- # Bookmarks section
989
- if st.session_state.bookmarked_models:
990
- display_bookmarked_models()
991
  else:
992
- st.info("📌 No models bookmarked yet. Select models to create your custom list!")
993
 
994
  # Main content area
995
  col1, col2 = st.columns([1, 3])
@@ -997,28 +1187,22 @@ col1, col2 = st.columns([1, 3])
997
  with col1:
998
  st.markdown("### 💡 Quick Tips")
999
  st.write("• View PDF spec sheets")
1000
- st.write("• Bookmark models for lists")
1001
  st.write("• Toggle image/text view")
1002
- st.write("• Export to CSV or PDF")
 
 
1003
  st.write("• Google search finds prices")
1004
 
1005
  with col2:
1006
  st.markdown('### 🔍 Model Search')
1007
  st.caption("Start typing the model number or browse all models")
1008
 
1009
- # Group models by product type
1010
- grouped_models = {}
1011
- for model in all_models:
1012
- product_type = get_product_type(model)
1013
- if product_type not in grouped_models:
1014
- grouped_models[product_type] = []
1015
- grouped_models[product_type].append(model)
1016
-
1017
  # Create formatted options with empty first option for easy typing
1018
  formatted_options = [''] # Empty first option
1019
- for product_type in sorted(grouped_models.keys()):
1020
- for model in sorted(grouped_models[product_type]):
1021
- formatted_options.append(model)
1022
 
1023
  # Search selectbox with clear typing experience
1024
  if st.session_state.selected_model and st.session_state.selected_model in formatted_options:
@@ -1029,7 +1213,7 @@ with col2:
1029
  selected = st.selectbox(
1030
  "Select or type a model number:",
1031
  options=formatted_options,
1032
- format_func=lambda x: format_model_option(x) if x else "↓ Click here and start typing model number...",
1033
  key="model_search",
1034
  index=default_index,
1035
  help="Click and start typing to search models"
@@ -1045,33 +1229,30 @@ if st.session_state.selected_model and st.session_state.selected_model != '':
1045
  model_data = get_model_data(st.session_state.selected_model)
1046
 
1047
  if model_data:
1048
- # Model header with bookmark
1049
  col1, col2 = st.columns([4, 1])
1050
  with col1:
1051
  st.markdown(f"## {st.session_state.selected_model}")
1052
  st.caption(f"Product Type: {get_product_type(st.session_state.selected_model)}")
1053
- if model_data.get('quality'):
1054
- quality_class = f"quality-{model_data['quality']}"
1055
- st.markdown(f'<span class="quality-badge {quality_class}">Data Quality: {model_data["quality"].title()}</span>',
1056
- unsafe_allow_html=True)
1057
 
1058
  with col2:
1059
- is_bookmarked = st.session_state.selected_model in st.session_state.bookmarked_models
1060
- bookmark_label = "❌ Remove" if is_bookmarked else "📌 Bookmark"
1061
- if st.button(bookmark_label, key=f"bookmark_{st.session_state.selected_model}", use_container_width=True):
1062
- if is_bookmarked:
1063
- st.session_state.bookmarked_models.remove(st.session_state.selected_model)
1064
- st.success("Bookmark removed!")
1065
  else:
1066
- st.session_state.bookmarked_models.append(st.session_state.selected_model)
1067
- st.success("Model bookmarked!")
1068
  time.sleep(0.5)
1069
  st.rerun()
1070
 
1071
  # Display product image if available
1072
  if model_data.get('file_path'):
1073
- pdf_filename = model_data['file_path'].replace('\\', '/').split('/')[-1]
1074
- pdf_url = f"https://huggingface.co/spaces/TurboAir/TurboAirViewer/resolve/main/pdfs/{pdf_filename}"
1075
 
1076
  # Create columns for image and specifications
1077
  img_col, _, spec_col = st.columns([1, 0.1, 2])
@@ -1085,7 +1266,7 @@ if st.session_state.selected_model and st.session_state.selected_model != '':
1085
  if not img_base64:
1086
  # Extract image if not cached
1087
  with st.spinner("Loading product image..."):
1088
- img_base64 = extract_pdf_thumbnail(pdf_url, st.session_state.selected_model, max_width=400, max_height=500)
1089
 
1090
  if img_base64:
1091
  st.markdown(
@@ -1096,6 +1277,11 @@ if st.session_state.selected_model and st.session_state.selected_model != '':
1096
  )
1097
  else:
1098
  st.info("📄 No preview available")
 
 
 
 
 
1099
 
1100
  with spec_col:
1101
  # Specifications
@@ -1132,6 +1318,26 @@ if st.session_state.selected_model and st.session_state.selected_model != '':
1132
  st.write(f"BTU: {specs['btu']}")
1133
  if specs.get('capacity') and specs.get('capacity') != 'N/A':
1134
  st.write(f"Capacity: {specs['capacity']}")
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1135
  else:
1136
  # No file path - show specifications in original two-column layout
1137
  specs = model_data['data'].get('specs', {})
@@ -1171,6 +1377,12 @@ if st.session_state.selected_model and st.session_state.selected_model != '':
1171
  st.write(f"BTU: {specs['btu']}")
1172
  if specs.get('capacity') and specs.get('capacity') != 'N/A':
1173
  st.write(f"Capacity: {specs['capacity']}")
 
 
 
 
 
 
1174
 
1175
  # Features
1176
  features = model_data['data'].get('features', [])
@@ -1211,8 +1423,9 @@ if st.session_state.selected_model and st.session_state.selected_model != '':
1211
  st.session_state[pdf_key] = not st.session_state.get(pdf_key, False)
1212
 
1213
  with action_col2:
1214
- # Google search button
1215
- google_search = f"https://www.google.com/search?q=turboair+{st.session_state.selected_model.replace(' ', '+')}+price"
 
1216
  st.markdown(f'''
1217
  <a href="{google_search}" target="_blank" style="text-decoration: none;">
1218
  <button class="google-search-button">
@@ -1258,18 +1471,26 @@ with col1:
1258
 
1259
  with col2:
1260
  st.markdown("### Database Info")
1261
- try:
1262
- conn = sqlite3.connect(DB_PATH)
1263
- cursor = conn.cursor()
1264
- cursor.execute("SELECT COUNT(*) FROM documents")
1265
- doc_count = cursor.fetchone()[0]
1266
- conn.close()
1267
-
1268
- st.write(f"• Total Documents: {doc_count}")
1269
- st.write(f" Total Models: {len(all_models)}")
1270
- st.write(f"• Database Size: {Path(DB_PATH).stat().st_size/1024/1024:.1f} MB")
1271
- except:
1272
- st.write("• Database info unavailable")
 
 
 
 
 
 
 
 
1273
 
1274
  # Footer
1275
  st.markdown("---")
 
2
  """
3
  Turbo Air Viewer - Equipment Specification Database Viewer
4
  Enhanced version with product image extraction and display
5
+ Modified to work with Excel-generated database structure
6
 
7
  Required dependencies (add to requirements.txt):
8
  - streamlit
 
14
  """
15
 
16
  import streamlit as st
17
+ import streamlit.components.v1 as components
18
  import sqlite3
19
  import json
20
  from pathlib import Path
 
36
  from reportlab.lib.units import inch
37
  from reportlab.platypus import SimpleDocTemplate, Table, TableStyle, Paragraph, Spacer, Image as RLImage, PageBreak
38
  from reportlab.lib.enums import TA_CENTER
39
+ import urllib.parse
40
+
41
+ # Try to import openpyxl for Excel export
42
+ try:
43
+ import openpyxl
44
+ from openpyxl.drawing.image import Image as XLImage
45
+ from openpyxl.styles import Alignment, Font, PatternFill
46
+ from openpyxl.utils import get_column_letter
47
+ EXCEL_AVAILABLE = True
48
+ except ImportError:
49
+ EXCEL_AVAILABLE = False
50
 
51
  # Streamlit page config MUST be first
52
  st.set_page_config(
 
55
  layout="wide"
56
  )
57
 
58
+ # Configuration - MODIFIED FOR HUGGING FACE
59
+ DB_FILENAME = "turbo_air_db_online.sqlite" # Local database in root
60
+ PDF_DIR = "pdfs" # Local PDF directory
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
61
 
62
+ # Create PDF directory if it doesn't exist
63
+ if not os.path.exists(PDF_DIR):
64
+ os.makedirs(PDF_DIR)
65
+ st.info(f"Created PDF directory: {PDF_DIR}")
66
 
67
+ # Check if database exists
68
+ if not os.path.exists(DB_FILENAME):
69
+ st.error(f"❌ Database file '{DB_FILENAME}' not found!")
70
+ st.info("Please ensure 'turbo_air_db_online.sqlite' is in the same directory as this script.")
71
+ st.write(f"Looking in: {os.path.abspath(DB_FILENAME)}")
72
  st.stop()
73
 
74
  # Product type mappings
 
145
  border-radius: 8px;
146
  }
147
 
148
+ /* Make PDF viewer take up more vertical space */
149
+ iframe[src*="pdf.js"] {
150
+ min-height: 85vh !important;
151
+ height: 85vh !important;
152
+ }
153
+
154
  .main-title {
155
  color: #4CAF50;
156
  font-size: 2.5em;
 
199
  border: 1px solid #444;
200
  }
201
 
202
+ .cart-image {
203
  border: 1px solid #444;
204
  border-radius: 4px;
205
  margin-bottom: 8px;
206
  transition: transform 0.2s ease;
207
  }
208
 
209
+ .cart-image:hover {
210
  transform: scale(1.05);
211
  border-color: #4CAF50;
212
  }
 
231
  .stSelectbox input {
232
  cursor: text !important;
233
  }
234
+
235
+ .email-button {
236
+ width: 100%;
237
+ padding: 0.5rem;
238
+ background-color: #4CAF50;
239
+ color: white;
240
+ border: none;
241
+ border-radius: 5px;
242
+ cursor: pointer;
243
+ font-size: 16px;
244
+ text-decoration: none;
245
+ display: inline-block;
246
+ text-align: center;
247
+ }
248
+
249
+ .email-button:hover {
250
+ background-color: #45a049;
251
+ color: white;
252
+ text-decoration: none;
253
+ }
254
+
255
+ /* Left-align text in cart text-only view buttons */
256
+ [data-testid="stButton"][id*="select_text_"] button,
257
+ [data-testid="stButton"][id*="select_text_exp_"] button {
258
+ text-align: left !important;
259
+ justify-content: flex-start !important;
260
+ padding-left: 10px !important;
261
+ }
262
  </style>
263
 
264
  <script>
 
280
  # Initialize session state
281
  if 'selected_model' not in st.session_state:
282
  st.session_state.selected_model = None
283
+ if 'cart_models' not in st.session_state:
284
+ st.session_state.cart_models = []
285
  if 'text_only_view' not in st.session_state:
286
  st.session_state.text_only_view = False
287
  if 'product_images' not in st.session_state:
288
  st.session_state.product_images = {}
289
+ if 'db_last_modified' not in st.session_state:
290
+ st.session_state.db_last_modified = None
291
+
292
+ # Check if database has been modified
293
+ def check_db_cache():
294
+ """Check if database has been modified and clear cache if needed"""
295
+ if DB_PATH and os.path.exists(DB_PATH):
296
+ current_mtime = os.path.getmtime(DB_PATH)
297
+ if st.session_state.db_last_modified is None:
298
+ st.session_state.db_last_modified = current_mtime
299
+ elif current_mtime != st.session_state.db_last_modified:
300
+ # Database has been modified, clear cache
301
+ st.session_state.product_images = {}
302
+ st.session_state.db_last_modified = current_mtime
303
+ st.cache_data.clear()
304
+ return True
305
+ return False
306
+
307
+ # MODIFIED: Check for local database
308
+ def check_database():
309
+ """Check if database exists locally"""
310
+ if os.path.exists(DB_FILENAME):
311
+ try:
312
+ conn = sqlite3.connect(DB_FILENAME)
313
+ cursor = conn.cursor()
314
+ cursor.execute("SELECT name FROM sqlite_master WHERE type='table' LIMIT 1")
315
+ tables = cursor.fetchall()
316
+ conn.close()
317
+
318
+ if tables:
319
+ return DB_FILENAME
320
+ except:
321
+ st.error("Database file exists but is invalid")
322
+ return None
323
+ else:
324
+ st.error(f"Database file '{DB_FILENAME}' not found in root directory")
325
+ return None
326
+
327
+ # Check database
328
+ DB_PATH = check_database()
329
+
330
+ if DB_PATH is None:
331
+ st.error("❌ Unable to load database. Please ensure 'turbo_air_db_online.sqlite' is in the root directory.")
332
+ st.stop()
333
+
334
+ # Check if database was modified
335
+ check_db_cache()
336
 
337
+ def extract_pdf_thumbnail(pdf_path, model_name, max_width=300, max_height=400):
338
  """Extract first page of PDF as thumbnail image"""
339
  cache_key = f"thumb_{model_name}"
340
 
 
343
  return st.session_state.product_images[cache_key]
344
 
345
  try:
346
+ # Check if file exists
347
+ if not os.path.exists(pdf_path):
348
+ # Try alternate naming conventions in PDF_DIR
349
+ alt_paths = [
350
+ os.path.join(PDF_DIR, f"{model_name}.pdf"),
351
+ os.path.join(PDF_DIR, f"{model_name.upper()}.pdf"),
352
+ os.path.join(PDF_DIR, f"{model_name.lower()}.pdf"),
353
+ os.path.join(PDF_DIR, f"{model_name.replace('-', '_')}.pdf"),
354
+ os.path.join(PDF_DIR, f"{model_name.replace('-', '')}.pdf"),
355
+ # Also try with parentheses removed
356
+ os.path.join(PDF_DIR, f"{model_name.replace('(', '').replace(')', '')}.pdf"),
357
+ os.path.join(PDF_DIR, f"{model_name.split('(')[0].strip()}.pdf"),
358
+ ]
359
 
360
+ # Debug: Show what files we're looking for
361
+ print(f"Looking for PDF for model {model_name}")
362
+ print(f"Primary path: {pdf_path}")
363
 
364
+ for alt_path in alt_paths:
365
+ if os.path.exists(alt_path):
366
+ print(f"Found PDF at: {alt_path}")
367
+ pdf_path = alt_path
368
+ break
369
+ else:
370
+ print(f"No PDF found for {model_name}")
371
+ # List available PDFs in the directory for debugging
372
+ if os.path.exists(PDF_DIR):
373
+ available_pdfs = [f for f in os.listdir(PDF_DIR) if f.endswith('.pdf')]
374
+ print(f"Available PDFs in {PDF_DIR}: {available_pdfs[:5]}...") # Show first 5
375
+ return None
376
+
377
+ # Open PDF and extract first page
378
+ pdf_document = fitz.open(pdf_path) # type: ignore
379
+ first_page = pdf_document[0]
380
+
381
+ # Render page as image (2x resolution for better quality)
382
+ mat = fitz.Matrix(2, 2)
383
+ # Fixed: Use getPixmap for older PyMuPDF versions or get_pixmap for newer
384
+ try:
385
+ # Try newer API first
386
+ pix = first_page.get_pixmap(matrix=mat) # type: ignore
387
+ except AttributeError:
388
  try:
389
+ # Try older API with matrix parameter
390
+ pix = first_page.getPixmap(matrix=mat) # type: ignore
391
  except:
392
  try:
393
+ # Try older API with mat parameter
394
+ pix = first_page.getPixmap(mat) # type: ignore
395
  except:
396
+ # Fallback to no matrix
397
+ pix = first_page.getPixmap() # type: ignore
398
+
399
+ # Convert to PIL Image
400
+ img_data = pix.tobytes("png")
401
+ img = Image.open(io.BytesIO(img_data))
402
+
403
+ # Calculate aspect ratio and resize
404
+ width, height = img.size
405
+ aspect_ratio = width / height
406
+
407
+ if width > max_width:
408
+ new_width = max_width
409
+ new_height = int(new_width / aspect_ratio)
410
+ else:
411
+ new_width = width
412
+ new_height = height
413
+
414
+ if new_height > max_height:
415
+ new_height = max_height
416
+ new_width = int(new_height * aspect_ratio)
417
+
418
+ img = img.resize((new_width, new_height), Image.Resampling.LANCZOS)
419
+
420
+ # Convert to base64 for caching
421
+ buffered = io.BytesIO()
422
+ img.save(buffered, format="PNG")
423
+ img_base64 = base64.b64encode(buffered.getvalue()).decode()
424
+
425
+ # Cache in session state
426
+ st.session_state.product_images[cache_key] = img_base64
427
+
428
+ # Cleanup
429
+ pdf_document.close()
430
+
431
+ return img_base64
432
+
 
433
  except Exception as e:
434
+ print(f"Error extracting thumbnail: {e}")
435
  return None
436
 
437
  # Cache functions
 
446
  all_models = []
447
 
448
  try:
449
+ # Try products table from scanner database
450
+ cursor.execute("SELECT model FROM products ORDER BY model")
451
+ models = cursor.fetchall()
452
+ if models:
453
+ all_models = [m[0] for m in models]
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
454
 
455
  except Exception as e:
456
  st.error(f"Database error: {e}")
 
462
 
463
  @st.cache_data
464
  def get_model_data(model_name):
465
+ """Get data for specific model - CACHED - MODIFIED FOR EXCEL STRUCTURE"""
466
  if DB_PATH is None:
467
  return None
468
  conn = sqlite3.connect(DB_PATH)
469
  cursor = conn.cursor()
470
 
471
  try:
472
+ # Get from products table
473
+ cursor.execute("SELECT * FROM products WHERE model = ?", (model_name,))
 
 
 
 
 
 
474
  row = cursor.fetchone()
475
+
476
  if row:
477
+ # Get column names
478
+ columns = [description[0] for description in cursor.description]
479
 
480
+ # Create dictionary from row data
481
+ data = dict(zip(columns, row))
 
 
482
 
483
+ # Build file path from source_file
484
+ filename = 'Unknown'
485
+ file_path = None
486
+ if data.get('source_file'):
487
+ # Extract just the filename from the full path
488
+ source_path = data['source_file']
489
+ # Handle both Windows and Unix paths
490
+ filename = source_path.replace('\\', '/').split('/')[-1]
491
+ # Remove any file extension and add .pdf if needed
492
+ if not filename.lower().endswith('.pdf'):
493
+ filename = filename.split('.')[0] + '.pdf'
494
+ file_path = filename # Store just the filename
495
+
496
+ # Create specs dictionary
497
+ specs = {
498
+ 'voltage': data.get('Voltage', 'N/A'), # Changed from hardcoded 'N/A'
499
+ 'amperage': f"{data.get('amps', 'N/A')} A" if data.get('amps') else 'N/A',
500
+ 'phase': data.get('phase', 'N/A'), # Now available from Excel
501
+ 'frequency': data.get('frequency', 'N/A'), # Now available from Excel
502
+ 'dimensions': data.get('Dimensions', 'N/A'), # Direct from Excel
503
+ 'weight': data.get('Weight', 'N/A'), # Changed from weight_lbs
504
+ 'capacity': data.get('Capacity', 'N/A'), # Changed from capacity_cuft
505
+ 'refrigerant': data.get('refrigerant', 'N/A'),
506
+ 'temperature_range': data.get('temperature_range', 'N/A'), # Now available from Excel
507
+ 'compressor': data.get('Compressor', 'N/A'), # Changed from hp
508
+ 'btu': 'N/A', # Not in products table
509
+ 'doors': str(data.get('doors', 'N/A')) if data.get('doors') else 'N/A',
510
+ 'shelves': str(data.get('shelves', 'N/A')) if data.get('shelves') else 'N/A',
511
+ 'pans': str(data.get('pans', 'N/A')) if data.get('pans') else 'N/A',
512
+ }
513
+
514
+ # Format dimensions properly if available from individual columns
515
+ if data.get('length_in') and data.get('depth_in') and data.get('height_in'):
516
+ specs['dimensions'] = f"{data['length_in']}\" x {data['depth_in']}\" x {data['height_in']}\""
517
+
518
+ # Add voltage if we have plug_type and Voltage is not available
519
+ if specs['voltage'] == 'N/A' and data.get('plug_type'):
520
+ # Extract voltage from plug type (e.g., "NEMA 5-15P" might be 115V)
521
+ plug = str(data['plug_type'])
522
+ if '5-15' in plug:
523
+ specs['voltage'] = '115V'
524
+ elif '5-20' in plug:
525
+ specs['voltage'] = '115V'
526
+ elif '6-20' in plug:
527
+ specs['voltage'] = '208-230V'
528
+ elif '6-30' in plug:
529
+ specs['voltage'] = '208-230V'
530
+ elif '6-50' in plug:
531
+ specs['voltage'] = '208-230V'
532
+ else:
533
+ specs['voltage'] = 'See specifications'
534
 
535
  return {
536
+ 'id': model_name,
537
  'filename': filename,
538
  'file_path': file_path,
539
+ 'data': {
540
+ 'models': [model_name],
541
+ 'specs': specs,
542
+ 'features': data.get('features', '').split(', ') if data.get('features') else [], # Now available from Excel
543
+ 'certifications': data.get('certifications', '').split(', ') if data.get('certifications') else [], # Now available from Excel
544
+ 'description': data.get('description', ''), # Now available from Excel
545
+ 'use_cases': data.get('use_cases', ''), # Now available from Excel
546
+ },
547
+ 'quality': 'good', # Default quality since no confidence score
548
+ 'price': data.get('Price', 'N/A'), # Single price field from Excel
549
+ 'model_no_dashes': model_name.replace('-', '') # Generate on the fly
550
  }
551
 
552
  except Exception as e:
 
627
  product_type = get_product_type(model)
628
  return f"{model} - {product_type}"
629
 
630
+ def export_cart_models_excel():
631
+ """Export cart models to Excel with thumbnail images"""
632
+ if not st.session_state.cart_models:
633
+ return None
634
+
635
+ if not EXCEL_AVAILABLE:
636
+ st.error("Excel export requires openpyxl. Please ensure it's installed.")
637
  return None
638
 
639
+ # Create workbook and worksheet
640
+ wb = openpyxl.Workbook() # type: ignore
641
+ ws = wb.active
642
+ if ws is None: # Fixed: Check if worksheet is None
643
+ st.error("Failed to create Excel worksheet")
644
+ return None
645
+
646
+ ws.title = "Turbo Air Equipment"
647
+
648
+ # Set up headers
649
+ headers = [
650
+ 'Image', 'Model', 'Product Type', 'Voltage', 'Amperage',
651
+ 'Dimensions', 'Weight', 'Capacity', 'Refrigerant', 'Compressor',
652
+ 'Doors', 'Shelves', 'Pans', 'Price'
653
+ ]
654
+
655
+ # Style for headers
656
+ header_font = Font(bold=True, color="FFFFFF") # type: ignore
657
+ header_fill = PatternFill(start_color="4CAF50", end_color="4CAF50", fill_type="solid") # type: ignore
658
+
659
+ # Write headers
660
+ for col, header in enumerate(headers, 1):
661
+ cell = ws.cell(row=1, column=col, value=header)
662
+ cell.font = header_font
663
+ cell.fill = header_fill
664
+ cell.alignment = Alignment(horizontal='center', vertical='center') # type: ignore
665
+
666
+ # Process each cart model
667
+ progress_bar = st.progress(0)
668
+ status_text = st.empty()
669
+
670
+ for idx, model in enumerate(st.session_state.cart_models):
671
+ # Update progress
672
+ progress = (idx + 1) / len(st.session_state.cart_models)
673
+ progress_bar.progress(progress)
674
+ status_text.text(f"Processing {model}... ({idx + 1}/{len(st.session_state.cart_models)})")
675
+
676
  model_data = get_model_data(model)
677
+ if not model_data:
678
+ continue
 
 
679
 
680
+ row = idx + 2 # Start from row 2 (after headers)
681
+ specs = model_data['data'].get('specs', {})
682
+ specs = clean_spec_data(specs)
683
+
684
+ # Column A: Image
685
+ if model_data.get('file_path'):
686
+ pdf_filename = model_data['file_path']
687
+ pdf_path = os.path.join(PDF_DIR, pdf_filename)
688
 
689
+ # Get or extract thumbnail
690
+ cache_key = f"thumb_{model}"
691
+ img_base64 = st.session_state.product_images.get(cache_key)
 
 
692
 
693
+ if not img_base64:
694
+ img_base64 = extract_pdf_thumbnail(pdf_path, model, max_width=150, max_height=200)
 
 
 
 
 
695
 
696
+ if img_base64:
697
+ # Convert base64 to image file for Excel
698
+ img_data = base64.b64decode(img_base64)
699
+ img = Image.open(io.BytesIO(img_data))
700
+
701
+ # Save to temporary file
702
+ temp_img = io.BytesIO()
703
+ img.save(temp_img, format='PNG')
704
+ temp_img.seek(0)
705
+
706
+ # Add to Excel
707
+ xl_img = XLImage(temp_img) # type: ignore
708
+ xl_img.width = 150
709
+ xl_img.height = 200
710
+ ws.add_image(xl_img, f'A{row}')
711
+
712
+ # Set row height to accommodate image
713
+ ws.row_dimensions[row].height = 150
714
+
715
+ # Column B onwards: Data
716
+ ws.cell(row=row, column=2, value=model)
717
+ ws.cell(row=row, column=3, value=get_product_type(model))
718
+ ws.cell(row=row, column=4, value=specs.get('voltage', 'N/A'))
719
+ ws.cell(row=row, column=5, value=specs.get('amperage', 'N/A'))
720
+ ws.cell(row=row, column=6, value=specs.get('dimensions', 'N/A'))
721
+ ws.cell(row=row, column=7, value=specs.get('weight', 'N/A'))
722
+ ws.cell(row=row, column=8, value=specs.get('capacity', 'N/A'))
723
+ ws.cell(row=row, column=9, value=specs.get('refrigerant', 'N/A'))
724
+ ws.cell(row=row, column=10, value=specs.get('compressor', 'N/A'))
725
+ ws.cell(row=row, column=11, value=specs.get('doors', 'N/A'))
726
+ ws.cell(row=row, column=12, value=specs.get('shelves', 'N/A'))
727
+ ws.cell(row=row, column=13, value=specs.get('pans', 'N/A'))
728
+
729
+ # Price information - MODIFIED FOR SINGLE PRICE
730
+ price = model_data.get('price', 'N/A')
731
+ ws.cell(row=row, column=14, value=price)
732
+
733
+ # Center align all cells
734
+ for col in range(2, 15):
735
+ ws.cell(row=row, column=col).alignment = Alignment(vertical='center') # type: ignore
736
+
737
+ # Adjust column widths
738
+ ws.column_dimensions['A'].width = 25 # Image column
739
+ for col in range(2, 15):
740
+ ws.column_dimensions[get_column_letter(col)].width = 15 # type: ignore
741
+
742
+ # Save to BytesIO
743
+ output = io.BytesIO()
744
+ wb.save(output)
745
+ output.seek(0)
746
 
747
+ # Clear progress
748
+ progress_bar.empty()
749
+ status_text.empty()
750
+
751
+ return output.getvalue()
752
 
753
+ def export_cart_models_pdf():
754
+ """Export cart models to PDF with images and specifications"""
755
+ if not st.session_state.cart_models:
756
  return None
757
 
758
  # Create PDF in memory
 
789
  styles['Normal']))
790
  elements.append(Spacer(1, 0.5*inch))
791
 
792
+ # Process each cart model
793
+ for idx, model in enumerate(st.session_state.cart_models):
794
  if idx > 0:
795
  elements.append(PageBreak())
796
 
 
803
 
804
  # Try to get product image
805
  if model_data.get('file_path'):
806
+ pdf_filename = model_data['file_path']
807
+ pdf_path = os.path.join(PDF_DIR, pdf_filename)
808
 
809
  # Get cached image or extract it
810
  cache_key = f"thumb_{model}"
811
  img_base64 = st.session_state.product_images.get(cache_key)
812
 
813
  if not img_base64:
814
+ img_base64 = extract_pdf_thumbnail(pdf_path, model, max_width=200, max_height=250)
815
 
816
  if img_base64:
817
  # Convert base64 to image for PDF
 
850
  if specs.get('btu') and specs.get('btu') != 'N/A':
851
  spec_data.append(['BTU', specs['btu']])
852
 
853
+ # Add price information - MODIFIED FOR SINGLE PRICE
854
+ price = model_data.get('price', 'N/A')
855
+ if price and price != 'N/A':
856
+ spec_data.append(['Price', price])
857
+
858
  if len(spec_data) > 1:
859
  # Create table
860
  spec_table = Table(spec_data, colWidths=[2.5*inch, 4*inch])
 
883
  elements.append(Spacer(1, 0.2*inch))
884
 
885
  # Source info
886
+ elements.append(Paragraph(f"<i>Source: {model_data.get('filename', 'Unknown')}</i>", styles['Normal']))
887
 
888
  # Build PDF
889
  doc.build(elements)
 
897
  pdf_filename = file_path.replace('\\', '/').split('/')[-1]
898
 
899
  # Create the HuggingFace Space URL for the PDF
900
+ pdf_url = f"https://huggingface.co/spaces/redxican/TurboAirViewer2.0/resolve/main/pdfs/{pdf_filename}"
901
 
902
  # Control buttons row
903
  col1, col2, col3 = st.columns([2, 1, 1])
 
936
  st.markdown("🔍 **Backup viewer** (if PDF doesn't display above):")
937
 
938
  # Use PDF.js viewer directly (most reliable for HuggingFace Spaces)
 
939
  components.iframe(
940
  src=f"https://mozilla.github.io/pdf.js/web/viewer.html?file={quote(pdf_url, safe='')}",
941
+ height=1200,
942
  scrolling=True
943
  )
944
 
945
+ def display_cart_models():
946
+ """Display cart models section with optimized toggle"""
947
+ if not st.session_state.cart_models:
948
+ st.info("🛒 Your cart is empty. Add models to create your custom quote!")
949
  return
950
 
951
  # Collapsible header
952
+ with st.expander(f"🛒 Shopping Cart ({len(st.session_state.cart_models)} items)", expanded=True):
953
  view_col1, view_col2, view_col3 = st.columns([2, 1, 1])
954
 
955
  with view_col3:
 
958
  if st.button(toggle_label, key="toggle_view", use_container_width=True):
959
  st.session_state.text_only_view = not st.session_state.text_only_view
960
 
961
+ # Display cart models with or without images
962
  if st.session_state.text_only_view:
963
  # Text-only view (original compact list)
964
  display_limit = 5
965
+ for idx, model in enumerate(st.session_state.cart_models[:display_limit]):
966
  col_select, col_remove = st.columns([5, 1])
967
  with col_select:
968
  if st.button(f"• {model}", key=f"select_text_{idx}", use_container_width=True, help=f"Click to view {model}"):
969
  st.session_state.selected_model = model
970
  st.rerun()
971
  with col_remove:
972
+ if st.button("❌", key=f"remove_cart_list_{idx}", help=f"Remove {model} from cart"):
973
+ st.session_state.cart_models.remove(model)
974
  st.rerun()
975
 
976
+ if len(st.session_state.cart_models) > display_limit:
977
+ with st.expander(f"Show all {len(st.session_state.cart_models)} items"):
978
+ for idx, model in enumerate(st.session_state.cart_models[display_limit:], display_limit):
979
  col_select, col_remove = st.columns([5, 1])
980
  with col_select:
981
  if st.button(f"• {model}", key=f"select_text_exp_{idx}", use_container_width=True, help=f"Click to view {model}"):
982
  st.session_state.selected_model = model
983
  st.rerun()
984
  with col_remove:
985
+ if st.button("❌", key=f"remove_cart_exp_{idx}", help=f"Remove {model} from cart"):
986
+ st.session_state.cart_models.remove(model)
987
  st.rerun()
988
 
989
+ else:
990
  # Image view
991
  # Display in grid layout
992
  cols_per_row = 6 # Changed from 4 to 6 columns for narrower items
993
+ for i in range(0, len(st.session_state.cart_models), cols_per_row):
994
  cols = st.columns(cols_per_row)
995
 
996
  for j, col in enumerate(cols):
997
+ if i + j < len(st.session_state.cart_models):
998
+ model = st.session_state.cart_models[i + j]
999
  model_data = get_model_data(model)
1000
 
1001
  with col:
1002
+ # Container for each cart item
1003
+ st.markdown('<div class="cart-item">', unsafe_allow_html=True)
1004
 
1005
+ # Container for each cart item
1006
  with st.container():
1007
  # Try to get and display thumbnail
1008
  if model_data and model_data.get('file_path'):
1009
+ pdf_filename = model_data['file_path']
1010
+ pdf_path = os.path.join(PDF_DIR, pdf_filename)
1011
 
1012
  # Check if image is already cached
1013
  cache_key = f"thumb_{model}"
 
1018
  st.markdown(
1019
  f'<img src="data:image/png;base64,{img_base64}" '
1020
  f'style="width:100%; max-height:150px; object-fit:contain; cursor:pointer;" '
1021
+ f'class="cart-image">',
1022
  unsafe_allow_html=True
1023
  )
1024
  else:
1025
  # Extract image with minimal loading indication
1026
+ img_base64 = extract_pdf_thumbnail(pdf_path, model, max_width=200, max_height=250)
1027
 
1028
  if img_base64:
1029
  st.markdown(
1030
  f'<img src="data:image/png;base64,{img_base64}" '
1031
  f'style="width:100%; max-height:200px; object-fit:contain; cursor:pointer;" '
1032
+ f'class="cart-image">',
1033
  unsafe_allow_html=True
1034
  )
1035
  else:
 
1051
  st.rerun()
1052
  with col_remove:
1053
  if st.button("❌", key=f"remove_img_{i}_{j}", use_container_width=True, type="secondary"):
1054
+ st.session_state.cart_models.remove(model)
1055
  st.rerun()
1056
 
1057
  st.markdown('</div>', unsafe_allow_html=True)
1058
 
1059
+ # Export section
1060
  st.markdown("---")
1061
+ st.markdown("### 📊 Export & Share")
1062
+
1063
+ # Initialize email visibility state
1064
+ if 'show_email_form' not in st.session_state:
1065
+ st.session_state.show_email_form = False
1066
+
1067
+ # Export buttons row - all in one line
1068
+ button_col1, button_col2, button_col3, button_col4 = st.columns([1.2, 1.2, 1.2, 1])
1069
+
1070
+ with button_col1:
1071
+ # Email toggle button (moved to first position)
1072
+ email_btn_text = "📧 Email Excel ▲" if st.session_state.show_email_form else "📧 Email Excel ▼"
1073
+ if st.button(email_btn_text, use_container_width=True, key="toggle_email"):
1074
+ st.session_state.show_email_form = not st.session_state.show_email_form
1075
+
1076
+ with button_col2:
1077
+ # Excel export button
1078
+ if EXCEL_AVAILABLE:
1079
+ excel_data = export_cart_models_excel()
1080
+ if excel_data:
1081
+ st.download_button(
1082
+ "📊 Download Excel",
1083
+ data=excel_data,
1084
+ file_name=f"turbo_air_cart_{datetime.now().strftime('%Y%m%d_%H%M')}.xlsx",
1085
+ mime="application/vnd.openxmlformats-officedocument.spreadsheetml.sheet",
1086
+ use_container_width=True,
1087
+ type="primary",
1088
+ key="download_excel_btn"
1089
+ )
1090
+ else:
1091
+ st.error("Excel export unavailable - openpyxl not installed")
1092
 
1093
+ with button_col3:
1094
  # PDF export button
1095
+ pdf_data = export_cart_models_pdf()
1096
  if pdf_data:
1097
  st.download_button(
1098
+ "📄 Download PDF",
1099
  data=pdf_data,
1100
  file_name=f"turbo_air_report_{datetime.now().strftime('%Y%m%d_%H%M')}.pdf",
1101
  mime="application/pdf",
1102
  use_container_width=True,
1103
+ key="download_pdf_btn"
1104
  )
1105
 
1106
+ with button_col4:
1107
+ # Clear cart button (moved to last position)
1108
+ if st.button("🗑️ Clear Cart", use_container_width=True):
1109
+ st.session_state.cart_models = []
1110
  st.session_state.product_images = {} # Clear image cache too
1111
  st.rerun()
1112
+
1113
+ # Email form (only shown when toggled)
1114
+ if st.session_state.show_email_form:
1115
+ st.markdown("---")
1116
+
1117
+ # Initialize session state for download tracking
1118
+ if 'excel_downloaded' not in st.session_state:
1119
+ st.session_state.excel_downloaded = False
1120
+
1121
+ # Email form
1122
+ email_col1, email_col2, email_col3 = st.columns([3, 1, 1])
1123
+
1124
+ with email_col1:
1125
+ receiver_email = st.text_input("To:", placeholder="Customer email address", key="receiver_email", label_visibility="collapsed")
1126
+
1127
+ if receiver_email and EXCEL_AVAILABLE:
1128
+ excel_data = export_cart_models_excel()
1129
+ if excel_data:
1130
+ filename = f"turbo_air_cart_{datetime.now().strftime('%Y%m%d_%H%M')}.xlsx"
1131
+
1132
+ with email_col2:
1133
+ # Step 1: Download Excel
1134
+ downloaded = st.download_button(
1135
+ label="📥 Step 1: Download",
1136
+ data=excel_data,
1137
+ file_name=filename,
1138
+ mime="application/vnd.openxmlformats-officedocument.spreadsheetml.sheet",
1139
+ use_container_width=True,
1140
+ key="download_excel_for_email"
1141
+ )
1142
+ if downloaded:
1143
+ st.session_state.excel_downloaded = True
1144
+
1145
+ with email_col3:
1146
+ # Step 2: Open Email (only enabled after download)
1147
+ mailto_link = f"mailto:{receiver_email}"
1148
+
1149
+ if st.session_state.excel_downloaded:
1150
+ st.markdown(f'''
1151
+ <a href="{mailto_link}" class="email-button" target="_blank" style="width: 100%; padding: 0.5rem; display: inline-block; text-align: center;">
1152
+ 📧 Step 2: Email
1153
+ </a>
1154
+ ''', unsafe_allow_html=True)
1155
+ else:
1156
+ st.button("📧 Step 2: Email", use_container_width=True, disabled=True, key="email_disabled")
1157
+
1158
+ # Instructions
1159
+ if st.session_state.excel_downloaded:
1160
+ st.info(f"✅ File downloaded: **{filename}** → Now click 'Step 2: Email' to compose your message and attach the file from your Downloads folder.")
1161
+ else:
1162
+ with email_col2:
1163
+ st.button("📥 Step 1: Download", use_container_width=True, disabled=True)
1164
+ with email_col3:
1165
+ st.button("📧 Step 2: Email", use_container_width=True, disabled=True)
1166
 
1167
  # MAIN UI
1168
  st.title("❄️ Turbo Air Equipment Viewer")
 
1172
  all_models = get_all_models()
1173
 
1174
  if not all_models:
1175
+ st.error("⚠️ No data found in database. Please ensure turbo_air_db_online.sqlite is available.")
1176
  st.stop()
1177
 
1178
+ # Cart section
1179
+ if st.session_state.cart_models:
1180
+ display_cart_models()
1181
  else:
1182
+ st.info("🛒 Your cart is empty. Add models to create your custom quote!")
1183
 
1184
  # Main content area
1185
  col1, col2 = st.columns([1, 3])
 
1187
  with col1:
1188
  st.markdown("### 💡 Quick Tips")
1189
  st.write("• View PDF spec sheets")
1190
+ st.write("• Add models to cart")
1191
  st.write("• Toggle image/text view")
1192
+ st.write("• Export to Excel with images")
1193
+ st.write("• Export to PDF report")
1194
+ st.write("• Email quotes to customers")
1195
  st.write("• Google search finds prices")
1196
 
1197
  with col2:
1198
  st.markdown('### 🔍 Model Search')
1199
  st.caption("Start typing the model number or browse all models")
1200
 
 
 
 
 
 
 
 
 
1201
  # Create formatted options with empty first option for easy typing
1202
  formatted_options = [''] # Empty first option
1203
+ # Add all models sorted alphabetically
1204
+ for model in sorted(all_models):
1205
+ formatted_options.append(model)
1206
 
1207
  # Search selectbox with clear typing experience
1208
  if st.session_state.selected_model and st.session_state.selected_model in formatted_options:
 
1213
  selected = st.selectbox(
1214
  "Select or type a model number:",
1215
  options=formatted_options,
1216
+ format_func=lambda x: x if x else "↓ Click here and start typing model number...",
1217
  key="model_search",
1218
  index=default_index,
1219
  help="Click and start typing to search models"
 
1229
  model_data = get_model_data(st.session_state.selected_model)
1230
 
1231
  if model_data:
1232
+ # Model header with cart button
1233
  col1, col2 = st.columns([4, 1])
1234
  with col1:
1235
  st.markdown(f"## {st.session_state.selected_model}")
1236
  st.caption(f"Product Type: {get_product_type(st.session_state.selected_model)}")
1237
+ # Removed quality badge display since no confidence score
 
 
 
1238
 
1239
  with col2:
1240
+ is_in_cart = st.session_state.selected_model in st.session_state.cart_models
1241
+ cart_label = "❌ Remove from Cart" if is_in_cart else "🛒 Add to Cart"
1242
+ if st.button(cart_label, key=f"cart_{st.session_state.selected_model}", use_container_width=True):
1243
+ if is_in_cart:
1244
+ st.session_state.cart_models.remove(st.session_state.selected_model)
1245
+ st.success("Removed from cart!")
1246
  else:
1247
+ st.session_state.cart_models.append(st.session_state.selected_model)
1248
+ st.success("Added to cart!")
1249
  time.sleep(0.5)
1250
  st.rerun()
1251
 
1252
  # Display product image if available
1253
  if model_data.get('file_path'):
1254
+ pdf_filename = model_data['file_path']
1255
+ pdf_path = os.path.join(PDF_DIR, pdf_filename)
1256
 
1257
  # Create columns for image and specifications
1258
  img_col, _, spec_col = st.columns([1, 0.1, 2])
 
1266
  if not img_base64:
1267
  # Extract image if not cached
1268
  with st.spinner("Loading product image..."):
1269
+ img_base64 = extract_pdf_thumbnail(pdf_path, st.session_state.selected_model, max_width=400, max_height=500)
1270
 
1271
  if img_base64:
1272
  st.markdown(
 
1277
  )
1278
  else:
1279
  st.info("📄 No preview available")
1280
+ # Show debug info in expander
1281
+ with st.expander("Debug Info"):
1282
+ st.write(f"PDF filename: {pdf_filename}")
1283
+ st.write(f"Looking for: {pdf_path}")
1284
+ st.write(f"File exists: {os.path.exists(pdf_path)}")
1285
 
1286
  with spec_col:
1287
  # Specifications
 
1318
  st.write(f"BTU: {specs['btu']}")
1319
  if specs.get('capacity') and specs.get('capacity') != 'N/A':
1320
  st.write(f"Capacity: {specs['capacity']}")
1321
+
1322
+ # Configuration details
1323
+ config_items = []
1324
+ if specs.get('doors') and specs.get('doors') != 'N/A':
1325
+ config_items.append(f"Doors: {specs['doors']}")
1326
+ if specs.get('shelves') and specs.get('shelves') != 'N/A':
1327
+ config_items.append(f"Shelves: {specs['shelves']}")
1328
+ if specs.get('pans') and specs.get('pans') != 'N/A':
1329
+ config_items.append(f"Pans: {specs['pans']}")
1330
+
1331
+ if config_items:
1332
+ st.markdown("**Configuration:**")
1333
+ for item in config_items:
1334
+ st.write(f"{item}")
1335
+
1336
+ # Price information - MODIFIED FOR SINGLE PRICE
1337
+ price = model_data.get('price', 'N/A')
1338
+ if price and price != 'N/A':
1339
+ st.markdown("### Price")
1340
+ st.markdown(f'<p style="font-size: 1.2em; color: #ff0000; font-weight: bold; margin: 0;">{price}</p>', unsafe_allow_html=True)
1341
  else:
1342
  # No file path - show specifications in original two-column layout
1343
  specs = model_data['data'].get('specs', {})
 
1377
  st.write(f"BTU: {specs['btu']}")
1378
  if specs.get('capacity') and specs.get('capacity') != 'N/A':
1379
  st.write(f"Capacity: {specs['capacity']}")
1380
+
1381
+ # Price information - MODIFIED FOR SINGLE PRICE
1382
+ price = model_data.get('price', 'N/A')
1383
+ if price and price != 'N/A':
1384
+ st.markdown("### Price")
1385
+ st.markdown(f'<p style="font-size: 1.2em; color: #ff0000; font-weight: bold; margin: 0;">{price}</p>', unsafe_allow_html=True)
1386
 
1387
  # Features
1388
  features = model_data['data'].get('features', [])
 
1423
  st.session_state[pdf_key] = not st.session_state.get(pdf_key, False)
1424
 
1425
  with action_col2:
1426
+ # Google search button - use model_no_dashes from database
1427
+ search_model = model_data.get('model_no_dashes', st.session_state.selected_model.replace(' ', '+'))
1428
+ google_search = f"https://www.google.com/search?q=turboair+{search_model}+price"
1429
  st.markdown(f'''
1430
  <a href="{google_search}" target="_blank" style="text-decoration: none;">
1431
  <button class="google-search-button">
 
1471
 
1472
  with col2:
1473
  st.markdown("### Database Info")
1474
+ if DB_PATH:
1475
+ try:
1476
+ conn = sqlite3.connect(DB_PATH)
1477
+ cursor = conn.cursor()
1478
+ cursor.execute("SELECT COUNT(*) FROM products")
1479
+ product_count = cursor.fetchone()[0]
1480
+
1481
+ # Get products with prices - MODIFIED FOR SINGLE PRICE FIELD
1482
+ cursor.execute("SELECT COUNT(*) FROM products WHERE Price IS NOT NULL AND Price != 'N/A'")
1483
+ priced_count = cursor.fetchone()[0]
1484
+
1485
+ conn.close()
1486
+
1487
+ st.write(f"• Total Products: {product_count}")
1488
+ st.write(f"• Products with Prices: {priced_count}")
1489
+ st.write(f"• Database Size: {Path(DB_PATH).stat().st_size/1024/1024:.1f} MB")
1490
+ except:
1491
+ st.write("• Database info unavailable")
1492
+ else:
1493
+ st.write("• Database not loaded")
1494
 
1495
  # Footer
1496
  st.markdown("---")