redxican commited on
Commit
ee8d1a8
·
verified ·
1 Parent(s): c9333fe

Upload 3 files

Browse files
.gitattributes CHANGED
@@ -33,3 +33,4 @@ saved_model/**/* filter=lfs diff=lfs merge=lfs -text
33
  *.zip filter=lfs diff=lfs merge=lfs -text
34
  *.zst filter=lfs diff=lfs merge=lfs -text
35
  *tfevents* filter=lfs diff=lfs merge=lfs -text
 
 
33
  *.zip filter=lfs diff=lfs merge=lfs -text
34
  *.zst filter=lfs diff=lfs merge=lfs -text
35
  *tfevents* filter=lfs diff=lfs merge=lfs -text
36
+ turbo_air_db.sqlite filter=lfs diff=lfs merge=lfs -text
requirements_viewer.txt ADDED
@@ -0,0 +1,2 @@
 
 
 
1
+ streamlit==1.28.1
2
+ pandas==1.5.3
turbo_air_db.sqlite ADDED
@@ -0,0 +1,3 @@
 
 
 
 
1
+ version https://git-lfs.github.com/spec/v1
2
+ oid sha256:bee262ed2d48dd1378df9b44bc6f7114a02499e6c41550f4fd9e63714119b21e
3
+ size 279015424
turbo_air_viewer.py ADDED
@@ -0,0 +1,930 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ #!/usr/bin/env python3
2
+ """
3
+ Turbo Air Viewer - Equipment Specification Database Viewer
4
+ Deployment-ready version with automatic database download
5
+ """
6
+
7
+ import streamlit as st
8
+ import sqlite3
9
+ import json
10
+ from pathlib import Path
11
+ import pandas as pd
12
+ from datetime import datetime
13
+ import re
14
+ import time
15
+ import base64
16
+ import io
17
+ import os
18
+ import requests
19
+
20
+ # Streamlit page config MUST be first
21
+ st.set_page_config(
22
+ page_title="Turbo Air Equipment Viewer",
23
+ page_icon="❄️",
24
+ layout="wide"
25
+ )
26
+
27
+ # Configuration
28
+ DB_FILENAME = "turbo_air_db.sqlite"
29
+ # Use the exact URL you provided
30
+ DB_URL = "https://huggingface.co/spaces/TurboAir/turbo-air-viewer/resolve/main/turbo_air_db.sqlite"
31
+
32
+ def download_database():
33
+ """Download database from Hugging Face if not present or invalid"""
34
+ if os.path.exists(DB_FILENAME):
35
+ # Check if existing file is valid
36
+ try:
37
+ # Check file size first
38
+ file_size = os.path.getsize(DB_FILENAME)
39
+ if file_size < 1000000: # Less than 1MB, probably wrong
40
+ st.warning(f"Existing database file is too small ({file_size/1024/1024:.1f} MB), re-downloading...")
41
+ os.remove(DB_FILENAME)
42
+ else:
43
+ conn = sqlite3.connect(DB_FILENAME)
44
+ cursor = conn.cursor()
45
+ cursor.execute("SELECT name FROM sqlite_master WHERE type='table' LIMIT 1")
46
+ tables = cursor.fetchall()
47
+ conn.close()
48
+
49
+ if tables:
50
+ st.success(f"✅ Using existing database ({file_size/1024/1024:.1f} MB)")
51
+ return DB_FILENAME # Valid database exists
52
+ except:
53
+ st.warning("Existing database file is invalid, re-downloading...")
54
+ if os.path.exists(DB_FILENAME):
55
+ os.remove(DB_FILENAME)
56
+
57
+ # Download the database
58
+ st.info("🔄 Downloading database... This is a one-time download of 279 MB.")
59
+
60
+ try:
61
+ # Method 1: Try requests first
62
+ import requests
63
+
64
+ headers = {
65
+ 'User-Agent': 'Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36'
66
+ }
67
+
68
+ response = requests.get(DB_URL, headers=headers, stream=True, timeout=30, allow_redirects=True)
69
+
70
+ # Check if we got a valid response
71
+ if response.status_code == 200:
72
+ total_size = int(response.headers.get('content-length', 0))
73
+
74
+ # Only proceed if file size looks right (should be ~279 MB)
75
+ if total_size < 10000000: # Less than 10MB
76
+ st.error(f"Downloaded file too small ({total_size/1024/1024:.1f} MB). Expected ~279 MB.")
77
+ st.error("The database file may be a Git LFS pointer.")
78
+ raise Exception("File size mismatch")
79
+
80
+ progress_bar = st.progress(0)
81
+ status_text = st.empty()
82
+
83
+ with open(DB_FILENAME, 'wb') as f:
84
+ downloaded = 0
85
+ for chunk in response.iter_content(chunk_size=1024*1024): # 1MB chunks
86
+ if chunk:
87
+ f.write(chunk)
88
+ downloaded += len(chunk)
89
+ if total_size > 0:
90
+ progress = downloaded / total_size
91
+ progress_bar.progress(progress)
92
+ status_text.text(f"Downloaded {downloaded/1024/1024:.1f} MB / {total_size/1024/1024:.1f} MB")
93
+
94
+ progress_bar.empty()
95
+ status_text.empty()
96
+
97
+ # Verify the downloaded file
98
+ if os.path.getsize(DB_FILENAME) > 100000000: # At least 100MB
99
+ try:
100
+ conn = sqlite3.connect(DB_FILENAME)
101
+ cursor = conn.cursor()
102
+ cursor.execute("SELECT name FROM sqlite_master WHERE type='table' LIMIT 1")
103
+ tables = cursor.fetchall()
104
+ conn.close()
105
+
106
+ if tables:
107
+ st.success("✅ Database downloaded and verified successfully!")
108
+ return DB_FILENAME
109
+ except:
110
+ st.error("Downloaded file is not a valid SQLite database")
111
+
112
+ else:
113
+ st.error(f"Failed to download: HTTP {response.status_code}")
114
+
115
+ except requests.exceptions.RequestException as e:
116
+ st.error(f"Download failed: {str(e)}")
117
+ except ImportError:
118
+ st.error("requests library not installed. Please add 'requests' to requirements.txt")
119
+ except Exception as e:
120
+ st.error(f"Unexpected error: {str(e)}")
121
+
122
+ # If download failed, provide manual instructions
123
+ st.error("❌ Automatic download failed.")
124
+ st.markdown("""
125
+ ### Manual Download Instructions:
126
+
127
+ 1. **Download directly from this link:**
128
+ [Download turbo_air_db.sqlite (279 MB)](https://huggingface.co/spaces/TurboAir/turbo-air-viewer/resolve/main/turbo_air_db.sqlite)
129
+
130
+ 2. **Or use wget/curl:**
131
+ ```bash
132
+ wget https://huggingface.co/spaces/TurboAir/turbo-air-viewer/resolve/main/turbo_air_db.sqlite
133
+ # or
134
+ curl -L -o turbo_air_db.sqlite https://huggingface.co/spaces/TurboAir/turbo-air-viewer/resolve/main/turbo_air_db.sqlite
135
+ ```
136
+
137
+ 3. **Or clone with Git LFS:**
138
+ ```bash
139
+ git lfs install
140
+ git clone https://huggingface.co/spaces/TurboAir/turbo-air-viewer
141
+ ```
142
+
143
+ **Note:** The database file is 279 MB. Make sure you have a stable internet connection.
144
+ """)
145
+
146
+ return None
147
+
148
+ # Download or verify database
149
+ DB_PATH = download_database()
150
+
151
+ if DB_PATH is None:
152
+ st.error("❌ Unable to load database. Please refresh the page to try again.")
153
+ st.stop()
154
+
155
+ # Rest of your original code continues here...
156
+ # Product type mappings
157
+ PRODUCT_TYPES = {
158
+ 'TSR': 'Reach-In Refrigerators',
159
+ 'TSF': 'Reach-In Freezers',
160
+ 'TGM': 'Glass Door Merchandisers',
161
+ 'TOM': 'Open Display Merchandisers',
162
+ 'MUR': 'Undercounter Refrigerators',
163
+ 'MUF': 'Undercounter Freezers',
164
+ 'PRO': 'Prep Tables',
165
+ 'M3': 'M3 Series',
166
+ 'TBP': 'Back Bar Coolers',
167
+ 'CRT': 'Countertop Display',
168
+ 'TPR': 'Pizza Prep Tables',
169
+ 'MST': 'Sandwich/Salad Units',
170
+ 'J': 'J Series',
171
+ 'TUF': 'Undercounter Freezers',
172
+ 'TUR': 'Undercounter Refrigerators',
173
+ 'TGF': 'Glass Door Freezers',
174
+ 'TGR': 'Glass Door Refrigerators',
175
+ 'JUF': 'J Series Undercounter Freezers',
176
+ 'JUR': 'J Series Undercounter Refrigerators',
177
+ 'PST': 'Prep Station Tables'
178
+ }
179
+
180
+ # Enhanced dark theme
181
+ st.markdown("""
182
+ <style>
183
+ .stApp {
184
+ background-color: #1a1a1a;
185
+ }
186
+
187
+ .search-container {
188
+ padding: 25px;
189
+ margin: 20px 0;
190
+ }
191
+
192
+ .search-label {
193
+ color: #4CAF50;
194
+ font-size: 1.2em;
195
+ font-weight: bold;
196
+ margin-bottom: 10px;
197
+ display: block;
198
+ }
199
+
200
+ .stSelectbox > div > div {
201
+ background-color: #3d3d3d !important;
202
+ border: 1px solid #555 !important;
203
+ border-radius: 10px !important;
204
+ }
205
+
206
+ .specs-table {
207
+ padding: 10px;
208
+ }
209
+
210
+ .google-search-button {
211
+ width: 100%;
212
+ padding: 0.5rem;
213
+ background-color: #4CAF50;
214
+ color: white;
215
+ border: none;
216
+ border-radius: 5px;
217
+ cursor: pointer;
218
+ font-size: 16px;
219
+ }
220
+
221
+ .google-search-button:hover {
222
+ background-color: #45a049;
223
+ }
224
+
225
+ .pdf-container iframe {
226
+ border: 2px solid #444;
227
+ border-radius: 8px;
228
+ }
229
+
230
+ .main-title {
231
+ color: #4CAF50;
232
+ font-size: 2.5em;
233
+ font-weight: bold;
234
+ margin: 0;
235
+ }
236
+
237
+ .subtitle {
238
+ color: #888;
239
+ font-size: 1.1em;
240
+ margin-top: 5px;
241
+ }
242
+
243
+ .quality-badge {
244
+ display: inline-block;
245
+ padding: 2px 8px;
246
+ border-radius: 4px;
247
+ font-size: 0.85em;
248
+ margin-left: 5px;
249
+ font-weight: 600;
250
+ }
251
+
252
+ .quality-excellent {
253
+ background-color: #4CAF50;
254
+ color: white;
255
+ }
256
+
257
+ .quality-good {
258
+ background-color: #8BC34A;
259
+ color: white;
260
+ }
261
+
262
+ .quality-acceptable {
263
+ background-color: #FFC107;
264
+ color: black;
265
+ }
266
+
267
+ .quality-poor {
268
+ background-color: #FF5722;
269
+ color: white;
270
+ }
271
+ </style>
272
+ """, unsafe_allow_html=True)
273
+
274
+ # Initialize session state
275
+ if 'selected_model' not in st.session_state:
276
+ st.session_state.selected_model = None
277
+ if 'bookmarked_models' not in st.session_state:
278
+ st.session_state.bookmarked_models = []
279
+
280
+ # Cache functions
281
+ @st.cache_data
282
+ def get_all_models():
283
+ """Get all models from database - CACHED"""
284
+ if DB_PATH is None:
285
+ return []
286
+ conn = sqlite3.connect(DB_PATH)
287
+ cursor = conn.cursor()
288
+
289
+ all_models = []
290
+
291
+ try:
292
+ # First try the model_index table (faster)
293
+ cursor.execute("SELECT name FROM sqlite_master WHERE type='table' AND name='model_index'")
294
+ if cursor.fetchone():
295
+ cursor.execute("SELECT model FROM model_index ORDER BY model")
296
+ models = cursor.fetchall()
297
+ if models:
298
+ all_models = [m[0] for m in models]
299
+ conn.close()
300
+ return all_models
301
+
302
+ # Fallback to scanning documents
303
+ cursor.execute("SELECT full_data FROM documents")
304
+ rows = cursor.fetchall()
305
+
306
+ model_set = set()
307
+ for row in rows:
308
+ try:
309
+ data = json.loads(row[0])
310
+ models = data.get('models', [])
311
+ model_set.update(models)
312
+ except:
313
+ continue
314
+
315
+ all_models = sorted(list(model_set))
316
+
317
+ except Exception as e:
318
+ st.error(f"Database error: {e}")
319
+ all_models = []
320
+ finally:
321
+ conn.close()
322
+
323
+ return all_models
324
+
325
+ @st.cache_data
326
+ def get_model_data(model_name):
327
+ """Get data for specific model - CACHED"""
328
+ if DB_PATH is None:
329
+ return None
330
+ conn = sqlite3.connect(DB_PATH)
331
+ cursor = conn.cursor()
332
+
333
+ try:
334
+ # Search for the model in documents
335
+ cursor.execute("""
336
+ SELECT id, file_path, full_data, quality FROM documents
337
+ WHERE full_data LIKE ?
338
+ ORDER BY import_date DESC
339
+ LIMIT 1
340
+ """, (f'%"{model_name}"%',))
341
+
342
+ row = cursor.fetchone()
343
+ if row:
344
+ doc_id, file_path, full_data_str, quality = row
345
+
346
+ try:
347
+ data = json.loads(full_data_str)
348
+ except:
349
+ data = {'models': [], 'specs': {}, 'features': []}
350
+
351
+ filename = Path(file_path).name if file_path else 'Unknown'
352
+
353
+ return {
354
+ 'id': doc_id,
355
+ 'filename': filename,
356
+ 'file_path': file_path,
357
+ 'data': data,
358
+ 'quality': quality
359
+ }
360
+
361
+ except Exception as e:
362
+ st.error(f"Database error: {e}")
363
+ finally:
364
+ conn.close()
365
+
366
+ return None
367
+
368
+ def clean_spec_data(specs):
369
+ """Clean and validate specification data"""
370
+ cleaned_specs = {}
371
+
372
+ for key, value in specs.items():
373
+ if value and isinstance(value, str):
374
+ # Clean amperage values
375
+ if key == 'amperage':
376
+ if value.strip().upper() in ['A', 'AMP', 'AMPS', 'AMPERE', 'AMPERES']:
377
+ value = 'N/A'
378
+ else:
379
+ match = re.search(r'(\d+\.?\d*)\s*[Aa]', value)
380
+ if match:
381
+ value = f"{match.group(1)} A"
382
+ elif value.strip().upper() == 'A':
383
+ value = 'N/A'
384
+
385
+ # Clean voltage values
386
+ elif key == 'voltage':
387
+ if value.strip().upper() in ['V', 'VOLT', 'VOLTS']:
388
+ value = 'N/A'
389
+ else:
390
+ match = re.search(r'(\d+)\s*[Vv]', value)
391
+ if match:
392
+ value = f"{match.group(1)}V"
393
+
394
+ # Clean phase values
395
+ elif key == 'phase':
396
+ if value.strip() in ['1', 'Single', 'single', '1-phase', '1 phase']:
397
+ value = '1-Phase'
398
+ elif value.strip() in ['3', 'Three', 'three', '3-phase', '3 phase']:
399
+ value = '3-Phase'
400
+
401
+ # Clean frequency values
402
+ elif key == 'frequency':
403
+ if value.strip().upper() in ['HZ', 'HERTZ']:
404
+ value = 'N/A'
405
+ else:
406
+ match = re.search(r'(\d+)\s*[Hh][Zz]', value)
407
+ if match:
408
+ value = f"{match.group(1)} Hz"
409
+
410
+ cleaned_specs[key] = value
411
+
412
+ return cleaned_specs
413
+
414
+ def get_product_type(model):
415
+ """Determine product type from model number"""
416
+ for prefix, type_name in PRODUCT_TYPES.items():
417
+ if model.startswith(prefix):
418
+ return type_name
419
+
420
+ model_upper = model.upper()
421
+ if 'REFRIGERATOR' in model_upper or 'REF' in model_upper:
422
+ return "Refrigerator"
423
+ elif 'FREEZER' in model_upper or 'FRZ' in model_upper:
424
+ return "Freezer"
425
+ elif 'PREP' in model_upper:
426
+ return "Prep Table"
427
+ elif 'DISPLAY' in model_upper:
428
+ return "Display Case"
429
+ elif 'MERCHANDISER' in model_upper:
430
+ return "Merchandiser"
431
+
432
+ return "Equipment"
433
+
434
+ def format_model_option(model):
435
+ """Format model with product type for display"""
436
+ product_type = get_product_type(model)
437
+ return f"{model} - {product_type}"
438
+
439
+ def export_bookmarked_models():
440
+ """Export bookmarked models to CSV"""
441
+ if not st.session_state.bookmarked_models:
442
+ return None
443
+
444
+ export_data = []
445
+ for model in st.session_state.bookmarked_models:
446
+ model_data = get_model_data(model)
447
+ if model_data:
448
+ specs = model_data['data'].get('specs', {})
449
+ specs = clean_spec_data(specs)
450
+ features = model_data['data'].get('features', [])
451
+
452
+ # Parse dimensions
453
+ dimensions = specs.get('dimensions', 'N/A')
454
+ width, depth, height = 'N/A', 'N/A', 'N/A'
455
+
456
+ dim_patterns = [
457
+ r'(\d+\.?\d*)["\s]*[WwLl]\s*[xX×]\s*(\d+\.?\d*)["\s]*[DdWw]\s*[xX×]\s*(\d+\.?\d*)["\s]*[HhTt]',
458
+ r'(\d+\.?\d*)\s*[xX×]\s*(\d+\.?\d*)\s*[xX×]\s*(\d+\.?\d*)',
459
+ r'(\d+\.?\d*)"?\s*x\s*(\d+\.?\d*)"?\s*x\s*(\d+\.?\d*)"?'
460
+ ]
461
+
462
+ for pattern in dim_patterns:
463
+ dim_match = re.search(pattern, dimensions)
464
+ if dim_match:
465
+ width = f"{dim_match.group(1)}\""
466
+ depth = f"{dim_match.group(2)}\""
467
+ height = f"{dim_match.group(3)}\""
468
+ break
469
+
470
+ row = {
471
+ 'Model': model,
472
+ 'Product Type': get_product_type(model),
473
+ 'Voltage': specs.get('voltage', 'N/A'),
474
+ 'Amperage': specs.get('amperage', 'N/A'),
475
+ 'Phase': specs.get('phase', 'N/A'),
476
+ 'Frequency': specs.get('frequency', 'N/A'),
477
+ 'Width': width,
478
+ 'Depth': depth,
479
+ 'Height': height,
480
+ 'Capacity': specs.get('capacity', 'N/A'),
481
+ 'Weight': specs.get('weight', 'N/A'),
482
+ 'Refrigerant': specs.get('refrigerant', 'N/A'),
483
+ 'Temperature Range': specs.get('temperature_range', 'N/A'),
484
+ 'BTU': specs.get('btu', 'N/A'),
485
+ 'Compressor': specs.get('compressor', 'N/A'),
486
+ 'Features': '; '.join(features),
487
+ 'Source File': model_data['filename']
488
+ }
489
+ export_data.append(row)
490
+
491
+ df = pd.DataFrame(export_data)
492
+ return df.to_csv(index=False)
493
+
494
+ def display_pdf_preview(file_path, model_name):
495
+ """Display PDF preview inline"""
496
+ import os
497
+
498
+ if os.path.exists(file_path):
499
+ try:
500
+ with open(file_path, "rb") as f:
501
+ pdf_data = f.read()
502
+
503
+ file_size_mb = len(pdf_data) / (1024 * 1024)
504
+
505
+ # Create columns for controls
506
+ col1, col2, col3 = st.columns([2, 1, 1])
507
+
508
+ with col1:
509
+ st.download_button(
510
+ "📥 Download PDF",
511
+ data=pdf_data,
512
+ file_name=f"{model_name}_spec_sheet.pdf",
513
+ mime="application/pdf",
514
+ use_container_width=True,
515
+ type="primary"
516
+ )
517
+
518
+ with col2:
519
+ st.success(f"✅ PDF loaded ({file_size_mb:.1f} MB)")
520
+
521
+ with col3:
522
+ if st.button("❌ Close Preview", use_container_width=True):
523
+ st.session_state[f'show_pdf_{model_name}'] = False
524
+ st.rerun()
525
+
526
+ # Display PDF inline
527
+ st.markdown("---")
528
+
529
+ if file_size_mb < 10:
530
+ # Convert to base64
531
+ base64_pdf = base64.b64encode(pdf_data).decode('utf-8')
532
+
533
+ # Create HTML for inline PDF display
534
+ pdf_display = f"""
535
+ <iframe
536
+ src="data:application/pdf;base64,{base64_pdf}"
537
+ width="100%"
538
+ height="800px"
539
+ type="application/pdf"
540
+ style="border: 2px solid #4CAF50; border-radius: 8px;">
541
+ </iframe>
542
+ """
543
+
544
+ st.markdown(pdf_display, unsafe_allow_html=True)
545
+
546
+ else:
547
+ st.warning("PDF file is too large for inline viewing. Please use the download button.")
548
+
549
+ except Exception as e:
550
+ st.error(f"Error reading PDF: {str(e)}")
551
+ else:
552
+ st.error("PDF file not found at the stored location.")
553
+ st.info("The PDF file may have been moved or the database contains an old file path.")
554
+
555
+ def get_high_accuracy_models(limit=8):
556
+ """Get models with highest extraction quality"""
557
+ if DB_PATH is None:
558
+ return []
559
+ conn = sqlite3.connect(DB_PATH)
560
+ cursor = conn.cursor()
561
+
562
+ high_accuracy = []
563
+
564
+ try:
565
+ cursor.execute("""
566
+ SELECT full_data, quality FROM documents
567
+ WHERE quality IN ('excellent', 'good')
568
+ ORDER BY
569
+ CASE quality
570
+ WHEN 'excellent' THEN 1
571
+ WHEN 'good' THEN 2
572
+ ELSE 3
573
+ END,
574
+ import_date DESC
575
+ LIMIT 200
576
+ """)
577
+
578
+ rows = cursor.fetchall()
579
+ seen_models = set()
580
+
581
+ for row in rows:
582
+ if len(high_accuracy) >= limit:
583
+ break
584
+
585
+ try:
586
+ data = json.loads(row[0])
587
+ models = data.get('models', [])
588
+ specs = data.get('specs', {})
589
+
590
+ filled_specs = sum(1 for v in specs.values() if v and v != 'N/A')
591
+
592
+ if models and filled_specs >= 4:
593
+ for model in models:
594
+ if model not in seen_models:
595
+ product_type = get_product_type(model)
596
+
597
+ type_count = sum(1 for item in high_accuracy if get_product_type(item['model']) == product_type)
598
+ if type_count >= 2:
599
+ continue
600
+
601
+ seen_models.add(model)
602
+ high_accuracy.append({
603
+ 'model': model,
604
+ 'quality': row[1],
605
+ 'spec_count': filled_specs,
606
+ 'product_type': product_type
607
+ })
608
+
609
+ if len(high_accuracy) >= limit:
610
+ break
611
+ except:
612
+ continue
613
+
614
+ finally:
615
+ conn.close()
616
+
617
+ high_accuracy.sort(key=lambda x: (
618
+ 0 if x['quality'] == 'excellent' else 1,
619
+ -x['spec_count']
620
+ ))
621
+
622
+ return high_accuracy[:limit]
623
+
624
+ # MAIN UI
625
+ st.title("❄️ Turbo Air Equipment Viewer")
626
+ st.caption("Professional Equipment Specification Database")
627
+
628
+ # Get all models
629
+ all_models = get_all_models()
630
+
631
+ if not all_models:
632
+ st.error("⚠️ No data found in database. Please ensure turbo_air_db.sqlite is available.")
633
+ st.stop()
634
+
635
+ # Bookmarks section
636
+ if st.session_state.bookmarked_models:
637
+ st.markdown("### 📌 Bookmarked Models")
638
+
639
+ view_col1, view_col2 = st.columns([2, 1])
640
+ with view_col2:
641
+ st.markdown(f"**{len(st.session_state.bookmarked_models)} models selected**")
642
+
643
+ # List view
644
+ display_limit = 5
645
+ for idx, model in enumerate(st.session_state.bookmarked_models[:display_limit]):
646
+ col_model, col_remove = st.columns([5, 1])
647
+ with col_model:
648
+ st.text(f"• {model}")
649
+ with col_remove:
650
+ if st.button("❌", key=f"remove_bookmark_list_{idx}", help=f"Remove {model}"):
651
+ st.session_state.bookmarked_models.remove(model)
652
+ st.rerun()
653
+
654
+ if len(st.session_state.bookmarked_models) > display_limit:
655
+ with st.expander(f"Show all {len(st.session_state.bookmarked_models)} bookmarks"):
656
+ for idx, model in enumerate(st.session_state.bookmarked_models[display_limit:], display_limit):
657
+ col_model, col_remove = st.columns([5, 1])
658
+ with col_model:
659
+ st.text(f"• {model}")
660
+ with col_remove:
661
+ if st.button("❌", key=f"remove_bookmark_exp_{idx}", help=f"Remove {model}"):
662
+ st.session_state.bookmarked_models.remove(model)
663
+ st.rerun()
664
+
665
+ # Export section
666
+ st.markdown("---")
667
+ export_col1, export_col2 = st.columns(2)
668
+
669
+ with export_col1:
670
+ csv_data = export_bookmarked_models()
671
+ if csv_data:
672
+ st.download_button(
673
+ "📥 Export CSV",
674
+ data=csv_data,
675
+ file_name=f"turbo_air_selection_{datetime.now().strftime('%Y%m%d_%H%M')}.csv",
676
+ mime="text/csv",
677
+ use_container_width=True
678
+ )
679
+
680
+ with export_col2:
681
+ if st.button("🗑️ Clear All", use_container_width=True):
682
+ st.session_state.bookmarked_models = []
683
+ st.rerun()
684
+ else:
685
+ st.info("📌 No models bookmarked yet. Select models to create your custom list!")
686
+
687
+ # Main content area
688
+ col1, col2 = st.columns([1, 3])
689
+
690
+ with col1:
691
+ st.markdown("### 💡 Quick Tips")
692
+ st.write("• View PDF spec sheets")
693
+ st.write("• Bookmark models for lists")
694
+ st.write("• Google search finds prices")
695
+ st.write("• Export includes all specs")
696
+
697
+ with col2:
698
+ st.markdown('### 🔍 Model Search')
699
+ st.caption("Start typing the model number or browse all models")
700
+
701
+ # Group models by product type
702
+ grouped_models = {}
703
+ for model in all_models:
704
+ product_type = get_product_type(model)
705
+ if product_type not in grouped_models:
706
+ grouped_models[product_type] = []
707
+ grouped_models[product_type].append(model)
708
+
709
+ # Create formatted options
710
+ formatted_options = ['']
711
+ for product_type in sorted(grouped_models.keys()):
712
+ for model in sorted(grouped_models[product_type]):
713
+ formatted_options.append(model)
714
+
715
+ # Search selectbox
716
+ selected = st.selectbox(
717
+ "Select or type a model number:",
718
+ options=formatted_options,
719
+ format_func=lambda x: format_model_option(x) if x else "Select a model or start typing...",
720
+ key="model_search",
721
+ index=formatted_options.index(st.session_state.selected_model) if st.session_state.selected_model in formatted_options else 0,
722
+ help="Start typing to filter models"
723
+ )
724
+
725
+ if selected:
726
+ st.session_state.selected_model = selected
727
+
728
+ # High Accuracy Models
729
+ with st.expander("⭐ Example Models - Click to expand", expanded=False):
730
+ st.caption("Examples of models with comprehensive data")
731
+
732
+ high_accuracy = get_high_accuracy_models()
733
+ if high_accuracy:
734
+ cols = st.columns(4)
735
+ col_idx = 0
736
+
737
+ for item in high_accuracy:
738
+ with cols[col_idx % 4]:
739
+ if st.button(
740
+ f"{item['model']}",
741
+ key=f"ha_{item['model']}",
742
+ use_container_width=True,
743
+ help=f"{item['product_type']} - {item['spec_count']} specifications"
744
+ ):
745
+ st.session_state.selected_model = item['model']
746
+ st.rerun()
747
+
748
+ st.caption(f"{item['product_type']}")
749
+ quality_class = f"quality-{item['quality']}"
750
+ quality_label = "⭐ Excellent" if item['quality'] == 'excellent' else "✓ Good"
751
+ st.markdown(f'<span class="quality-badge {quality_class}">{quality_label}</span>',
752
+ unsafe_allow_html=True)
753
+
754
+ col_idx += 1
755
+
756
+ # Display selected model
757
+ if selected and selected != '':
758
+ st.markdown("---")
759
+
760
+ model_data = get_model_data(selected)
761
+
762
+ if model_data:
763
+ # Model header with bookmark
764
+ col1, col2 = st.columns([4, 1])
765
+ with col1:
766
+ st.markdown(f"## {selected}")
767
+ st.caption(f"Product Type: {get_product_type(selected)}")
768
+ if model_data.get('quality'):
769
+ quality_class = f"quality-{model_data['quality']}"
770
+ st.markdown(f'<span class="quality-badge {quality_class}">Data Quality: {model_data["quality"].title()}</span>',
771
+ unsafe_allow_html=True)
772
+
773
+ with col2:
774
+ is_bookmarked = selected in st.session_state.bookmarked_models
775
+ bookmark_label = "❌ Remove" if is_bookmarked else "📌 Bookmark"
776
+ if st.button(bookmark_label, key=f"bookmark_{selected}", use_container_width=True):
777
+ if is_bookmarked:
778
+ st.session_state.bookmarked_models.remove(selected)
779
+ st.success("Bookmark removed!")
780
+ else:
781
+ if len(st.session_state.bookmarked_models) >= 50:
782
+ st.error("Maximum 50 bookmarks allowed")
783
+ else:
784
+ st.session_state.bookmarked_models.append(selected)
785
+ st.success("Model bookmarked!")
786
+ time.sleep(0.5)
787
+ st.rerun()
788
+
789
+ # Specifications
790
+ specs = model_data['data'].get('specs', {})
791
+ specs = clean_spec_data(specs)
792
+
793
+ if specs:
794
+ st.markdown("### Technical Specifications")
795
+
796
+ spec_col1, spec_col2 = st.columns(2)
797
+
798
+ with spec_col1:
799
+ st.markdown("**Electrical Specifications:**")
800
+ if specs.get('voltage') and specs.get('voltage') != 'N/A':
801
+ st.write(f"Voltage: {specs['voltage']}")
802
+ if specs.get('amperage') and specs.get('amperage') != 'N/A':
803
+ st.write(f"Amperage: {specs['amperage']}")
804
+ if specs.get('phase') and specs.get('phase') != 'N/A':
805
+ st.write(f"Phase: {specs['phase']}")
806
+ if specs.get('frequency') and specs.get('frequency') != 'N/A':
807
+ st.write(f"Frequency: {specs['frequency']}")
808
+
809
+ st.markdown("**Physical Specifications:**")
810
+ if specs.get('dimensions') and specs.get('dimensions') != 'N/A':
811
+ st.write(f"Dimensions: {specs['dimensions']}")
812
+ if specs.get('weight') and specs.get('weight') != 'N/A':
813
+ st.write(f"Weight: {specs['weight']}")
814
+
815
+ with spec_col2:
816
+ st.markdown("**Performance Specifications:**")
817
+ if specs.get('refrigerant') and specs.get('refrigerant') != 'N/A':
818
+ st.write(f"Refrigerant: {specs['refrigerant']}")
819
+ if specs.get('temperature_range') and specs.get('temperature_range') != 'N/A':
820
+ st.write(f"Temperature: {specs['temperature_range']}")
821
+ if specs.get('compressor') and specs.get('compressor') != 'N/A':
822
+ st.write(f"Compressor: {specs['compressor']}")
823
+ if specs.get('btu') and specs.get('btu') != 'N/A':
824
+ st.write(f"BTU: {specs['btu']}")
825
+ if specs.get('capacity') and specs.get('capacity') != 'N/A':
826
+ st.write(f"Capacity: {specs['capacity']}")
827
+
828
+ # Features
829
+ features = model_data['data'].get('features', [])
830
+ if features:
831
+ st.markdown("### Features")
832
+ feature_cols = st.columns(3)
833
+ for idx, feature in enumerate(features):
834
+ with feature_cols[idx % 3]:
835
+ st.write(f"• {feature}")
836
+
837
+ # Certifications
838
+ certifications = model_data['data'].get('certifications', [])
839
+ if certifications:
840
+ st.markdown("### Certifications")
841
+ st.write(" • ".join(certifications))
842
+
843
+ # Description
844
+ description = model_data['data'].get('description', '')
845
+ if description:
846
+ st.markdown("### Description")
847
+ st.write(description)
848
+
849
+ # Use cases
850
+ use_cases = model_data['data'].get('use_cases', '')
851
+ if use_cases:
852
+ st.markdown("### Use Cases")
853
+ st.write(use_cases)
854
+
855
+ # Action buttons - View PDF and Search in Google
856
+ st.markdown("### Actions")
857
+ action_col1, action_col2 = st.columns(2)
858
+
859
+ with action_col1:
860
+ # PDF toggle button
861
+ pdf_key = f'show_pdf_{selected}'
862
+ button_text = "📄 Hide PDF" if st.session_state.get(pdf_key, False) else "📄 View PDF"
863
+ if st.button(button_text, use_container_width=True, key=f"view_pdf_{selected}"):
864
+ st.session_state[pdf_key] = not st.session_state.get(pdf_key, False)
865
+
866
+ with action_col2:
867
+ # Google search button
868
+ google_search = f"https://www.google.com/search?q=turboair+{selected.replace(' ', '+')}+price"
869
+ st.markdown(f'''
870
+ <a href="{google_search}" target="_blank" style="text-decoration: none;">
871
+ <button class="google-search-button">
872
+ 🔍 Search in Google
873
+ </button>
874
+ </a>
875
+ ''', unsafe_allow_html=True)
876
+
877
+ # Display PDF preview if requested
878
+ pdf_key = f'show_pdf_{selected}'
879
+ if st.session_state.get(pdf_key, False):
880
+ st.markdown("---")
881
+ st.markdown("### 📄 PDF Specification Sheet")
882
+
883
+ if 'file_path' in model_data and model_data['file_path']:
884
+ display_pdf_preview(model_data['file_path'], selected)
885
+ else:
886
+ st.error("❌ No PDF file path found for this model.")
887
+ st.info("PDF file may not be available.")
888
+
889
+ # Source info
890
+ st.markdown("---")
891
+ st.caption(f"Source: {model_data['filename']}")
892
+ else:
893
+ st.error(f"No data found for model {selected}")
894
+
895
+ # Stats at bottom
896
+ st.markdown("---")
897
+ col1, col2 = st.columns(2)
898
+
899
+ with col1:
900
+ st.markdown("### Models by Product Type")
901
+ type_counts = {}
902
+ for model in all_models:
903
+ ptype = get_product_type(model)
904
+ type_counts[ptype] = type_counts.get(ptype, 0) + 1
905
+
906
+ sorted_types = sorted(type_counts.items(), key=lambda x: x[1], reverse=True)
907
+ for ptype, count in sorted_types[:8]:
908
+ if ptype == "Equipment" and count < 20:
909
+ continue
910
+ st.write(f"{ptype}: {count}")
911
+
912
+ with col2:
913
+ st.markdown("### Database Info")
914
+ try:
915
+ conn = sqlite3.connect(DB_PATH)
916
+ cursor = conn.cursor()
917
+ cursor.execute("SELECT COUNT(*) FROM documents")
918
+ doc_count = cursor.fetchone()[0]
919
+ conn.close()
920
+
921
+ st.write(f"• Total Documents: {doc_count}")
922
+ st.write(f"• Total Models: {len(all_models)}")
923
+ st.write(f"• Database Size: {Path(DB_PATH).stat().st_size/1024/1024:.1f} MB")
924
+ except:
925
+ st.write("• Database info unavailable")
926
+
927
+ # Footer
928
+ st.markdown("---")
929
+ st.caption("Turbo Air Equipment Viewer - Professional Specification Database")
930
+ st.caption("💡 Tip: Use Google search button to find current prices and availability")