""" CSV to JSON Parser - Generate Navigation, Products, and Accessories JSON Files Based on AI_PARSING_INSTRUCTIONS.md This script reads data/products.csv and generates: 1. data/navigation.json - Question flow with conditional logic 2. data/products.json - Product catalog 3. data/accessories.json - Accessory options with compatibility rules Run: python parse_csv_to_json.py """ import csv import json from collections import defaultdict from typing import Dict, List, Set, Tuple def parse_bool(value): """Parse TRUE/FALSE string to boolean""" if isinstance(value, str): return value.strip().upper() == 'TRUE' return bool(value) def clean_string(value): """Clean and strip string values""" if isinstance(value, str): return value.strip() return value or "" class CSVParser: def __init__(self, csv_path: str): self.csv_path = csv_path self.products = [] self.accessories = [] self.base_types = set() self.door_subtypes = defaultdict(list) # subtype -> [products] self.window_subtypes = defaultdict(list) self.material_counts = defaultdict(lambda: {'aluminum': 0, 'vinyl': 0}) self.color_counts = defaultdict(lambda: defaultdict(int)) # subtype -> {color: count} self.color_by_material = defaultdict(lambda: defaultdict(set)) # subtype -> {material: {colors}} self.detailed_products = {} # Load from questions.json def load_detailed_product_info(self): """Load detailed product info from questions.json""" try: # Load the code mapping try: with open('data/product_code_mapping.json', 'r', encoding='utf-8') as f: code_mapping = json.load(f) except FileNotFoundError: code_mapping = {} with open('data/questions.json', 'r', encoding='utf-8') as f: questions_data = json.load(f) # Extract all product nodes for key, value in questions_data.items(): if key.startswith('prod-') and value.get('type') == 'product': code = value.get('code') if code: detailed_info = { 'title': value.get('title'), 'detailedDescription': value.get('description'), 'features': value.get('features', []), 'image': value.get('image'), 'url': value.get('url'), 'brochure': value.get('brochure'), 'measureGuide': value.get('measureGuide') } # Store under the simplified code self.detailed_products[code] = detailed_info # Also store under all mapped codes for csv_code, simple_code in code_mapping.items(): if simple_code == code: self.detailed_products[csv_code] = detailed_info print(f"βœ“ Loaded {len(self.detailed_products)} detailed product descriptions") except FileNotFoundError: print("⚠ questions.json not found - skipping detailed product info") except Exception as e: print(f"⚠ Error loading product details: {e}") def parse_csv(self): """Parse the CSV file and separate products from accessories""" print("πŸ“– Reading CSV file...") with open(self.csv_path, 'r', encoding='utf-8', errors='ignore') as f: reader = csv.reader(f) # Skip first two header rows next(reader) # Row 1: Category headers header_row = next(reader) # Row 2: Column names row_count = 0 for row in reader: if len(row) < 11: # Need at least up to column K continue row_count += 1 # Skip discontinued items if parse_bool(row[0]): # DISCONT column continue # Extract basic info prod_code = clean_string(row[2]) # PROD_CODE category = clean_string(row[3]) # CATEGORY description = clean_string(row[5]) # DESCRIPTION location = clean_string(row[1]) # LOC_CODE base_type = clean_string(row[6]) if len(row) > 6 else "" # Base Type (G) door_subtype = clean_string(row[7]) if len(row) > 7 else "" # Sub-type Door (H) window_subtype = clean_string(row[8]) if len(row) > 8 else "" # Sub-type Window (I) is_accessory = parse_bool(row[9]) if len(row) > 9 else False # Accessory Yes (J) specific_item = parse_bool(row[10]) if len(row) > 10 else False # This Item (K) # Parse materials (L, M) materials = [] if len(row) > 11 and parse_bool(row[11]): materials.append('Aluminum') if len(row) > 12 and parse_bool(row[12]): materials.append('Vinyl') # Parse colors (N through S+) colors = [] color_columns = [ (13, 'Black'), (14, 'White'), (15, 'Bronze'), (16, 'Tan'), (17, 'Mill'), (18, 'Sandstone') ] for col_idx, color_name in color_columns: if len(row) > col_idx and parse_bool(row[col_idx]): colors.append(color_name) # Build item data item_data = { 'id': prod_code, 'productCode': prod_code, 'category': category, 'description': description, 'discontinued': False, 'location': location, 'baseType': base_type, 'subType': { 'door': door_subtype if door_subtype else None, 'window': window_subtype if window_subtype else None }, 'materials': materials, 'colors': colors } # Separate accessories from products if is_accessory: # Build accessory-specific data accessory = { **item_data, 'accessoryCode': prod_code, 'compatibilityRules': self._build_compatibility_rules( door_subtype, window_subtype, category, specific_item ), 'optionType': self._infer_option_type(description), 'metadata': { 'specificItemLink': specific_item } } self.accessories.append(accessory) else: # Build product-specific data product = { **item_data, 'isAccessory': False, 'compatibleAccessories': [] # Will be populated later } # Merge detailed info if available if prod_code in self.detailed_products: detailed = self.detailed_products[prod_code] product.update({ 'title': detailed['title'], 'detailedDescription': detailed['detailedDescription'], 'features': detailed['features'], 'image': detailed['image'], 'url': detailed['url'], 'brochure': detailed.get('brochure'), 'measureGuide': detailed.get('measureGuide') }) self.products.append(product) # Track for navigation building if base_type: self.base_types.add(base_type) # Track subtypes per type if door_subtype: self.door_subtypes[door_subtype].append(product) if window_subtype: self.window_subtypes[window_subtype].append(product) # Count materials per subtype subtype_key = door_subtype or window_subtype or "none" if 'Aluminum' in materials: self.material_counts[subtype_key]['aluminum'] += 1 # Track colors per material for color in colors: self.color_by_material[subtype_key]['Aluminum'].add(color) if 'Vinyl' in materials: self.material_counts[subtype_key]['vinyl'] += 1 # Track colors per material for color in colors: self.color_by_material[subtype_key]['Vinyl'].add(color) # Count colors per subtype (overall) for color in colors: self.color_counts[subtype_key][color] += 1 print(f"βœ“ Parsed {row_count} rows") print(f"βœ“ Found {len(self.products)} products") print(f"βœ“ Found {len(self.accessories)} accessories") def _build_compatibility_rules(self, door_subtype, window_subtype, category, specific_item): """Build compatibility rules for accessories""" rules = { 'type': 'category', # Default 'subTypeDoor': door_subtype if door_subtype else None, 'subTypeWindow': window_subtype if window_subtype else None, 'categories': [category] if category else [], 'specificProducts': [], 'requiresMatch': { 'material': True, # Usually accessories need matching material 'color': False # Colors usually don't need to match } } # Determine rule type if specific_item: rules['type'] = 'specific' elif door_subtype or window_subtype: rules['type'] = 'subType' return rules def _infer_option_type(self, description): """Infer option type from description""" desc_upper = description.upper() if 'HANDLE' in desc_upper: return 'handle' elif 'INSERT' in desc_upper or 'GLASS' in desc_upper: return 'insert' elif 'HARDWARE' in desc_upper: return 'hardware' elif 'SCREEN' in desc_upper: return 'screen' else: return 'option' def link_accessories_to_products(self): """Link compatible accessories to products""" print("πŸ”— Linking accessories to products...") linked_count = 0 for product in self.products: compatible = [] for accessory in self.accessories: rules = accessory['compatibilityRules'] # Check by subtype if rules['type'] == 'subType': prod_door = product['subType']['door'] prod_window = product['subType']['window'] acc_door = rules['subTypeDoor'] acc_window = rules['subTypeWindow'] if (prod_door and prod_door == acc_door) or \ (prod_window and prod_window == acc_window): # Check material compatibility if required if rules['requiresMatch']['material']: # Check if they share any materials if any(m in product['materials'] for m in accessory['materials']): compatible.append(accessory['id']) linked_count += 1 else: compatible.append(accessory['id']) linked_count += 1 # Check by category elif rules['type'] == 'category': if product['category'] in rules['categories']: compatible.append(accessory['id']) linked_count += 1 product['compatibleAccessories'] = compatible print(f"βœ“ Created {linked_count} product-accessory links") def build_navigation(self): """Build navigation JSON with conditional logic""" print("πŸ—ΊοΈ Building navigation flow...") navigation = {} # Question 1: Base Type Selection navigation['start'] = { 'type': 'question', 'inputType': 'button', 'title': 'What are you looking for?', 'subtitle': 'Select the product category', 'answers': [] } # Add base type options sorted_types = sorted(self.base_types) for base_type in sorted_types: emoji = 'πŸšͺ' if 'door' in base_type.lower() else 'πŸͺŸ' next_key = f'q-{base_type.lower()}-type' navigation['start']['answers'].append({ 'caption': base_type, 'image': emoji, 'next': next_key, 'filter': { 'baseType': base_type } }) # Question 2: Sub-type Selection for Doors if self.door_subtypes: door_subtypes = sorted(self.door_subtypes.keys()) navigation['q-door-type'] = { 'type': 'question', 'inputType': 'button', 'title': 'What type of door?', 'subtitle': 'Select the door category', 'answers': [] } for subtype in door_subtypes: products = self.door_subtypes[subtype] # Check if material question is needed needs_material = self._needs_material_question(subtype) # Determine next question if needs_material: next_key = f'q-material-{subtype.lower().replace(" ", "-")}' else: # No material choice needed - determine single material and check colors single_material = 'Aluminum' if self.material_counts[subtype]['aluminum'] > 0 else 'Vinyl' needs_color = self._needs_color_question(subtype, single_material) next_key = f'q-color-{subtype.lower().replace(" ", "-")}-{single_material.lower()}' if needs_color else 'q-dimensions' # Auto-apply material if only one filter_obj = { 'baseType': 'Door', 'subType': subtype } if not needs_material: # Auto-apply the single material if self.material_counts[subtype]['aluminum'] > 0: filter_obj['material'] = 'Aluminum' elif self.material_counts[subtype]['vinyl'] > 0: filter_obj['material'] = 'Vinyl' # If no color question either, auto-apply single color single_material = filter_obj['material'] if not self._needs_color_question(subtype, single_material): colors = list(self.color_by_material[subtype][single_material]) if len(colors) == 1: filter_obj['color'] = colors[0] navigation['q-door-type']['answers'].append({ 'caption': subtype, 'image': 'πŸšͺ', 'next': next_key, 'filter': filter_obj }) # Create material question if needed if needs_material: self._create_material_question(navigation, subtype, 'Door') # Create color question if material skipped but color needed elif next_key.startswith('q-color-'): single_material = 'Aluminum' if self.material_counts[subtype]['aluminum'] > 0 else 'Vinyl' self._create_color_question(navigation, subtype, single_material, 'Door') # Question 2: Sub-type Selection for Windows if self.window_subtypes: window_subtypes = sorted(self.window_subtypes.keys()) navigation['q-window-type'] = { 'type': 'question', 'inputType': 'button', 'title': 'What type of window?', 'subtitle': 'Select the window category', 'answers': [] } for subtype in window_subtypes: needs_material = self._needs_material_question(subtype) # Determine next question if needs_material: next_key = f'q-material-{subtype.lower().replace(" ", "-")}' else: # No material choice needed - determine single material and check colors single_material = 'Aluminum' if self.material_counts[subtype]['aluminum'] > 0 else 'Vinyl' needs_color = self._needs_color_question(subtype, single_material) next_key = f'q-color-{subtype.lower().replace(" ", "-")}-{single_material.lower()}' if needs_color else 'q-dimensions' filter_obj = { 'baseType': 'Window', 'subType': subtype } if not needs_material: if self.material_counts[subtype]['aluminum'] > 0: filter_obj['material'] = 'Aluminum' elif self.material_counts[subtype]['vinyl'] > 0: filter_obj['material'] = 'Vinyl' # If no color question either, auto-apply single color single_material = filter_obj['material'] if not self._needs_color_question(subtype, single_material): colors = list(self.color_by_material[subtype][single_material]) if len(colors) == 1: filter_obj['color'] = colors[0] navigation['q-window-type']['answers'].append({ 'caption': subtype, 'image': 'πŸͺŸ', 'next': next_key, 'filter': filter_obj }) if needs_material: self._create_material_question(navigation, subtype, 'Window') # Create color question if material skipped but color needed elif next_key.startswith('q-color-'): single_material = 'Aluminum' if self.material_counts[subtype]['aluminum'] > 0 else 'Vinyl' self._create_color_question(navigation, subtype, single_material, 'Window') # Final Question: Dimensions navigation['q-dimensions'] = { 'type': 'question', 'inputType': 'form', 'title': 'Enter Product Dimensions', 'subtitle': 'Please provide the measurements', 'fields': [ { 'name': 'width', 'label': 'Width (inches)', 'type': 'number', 'required': True, 'placeholder': 'e.g., 36' }, { 'name': 'height', 'label': 'Height (inches)', 'type': 'number', 'required': True, 'placeholder': 'e.g., 80' } ], 'next': 'results' } print(f"βœ“ Created {len(navigation)} navigation nodes") return navigation def _needs_material_question(self, subtype): """Check if a subtype needs a material selection question""" aluminum = self.material_counts[subtype]['aluminum'] vinyl = self.material_counts[subtype]['vinyl'] # Show material question if both materials exist return aluminum > 0 and vinyl > 0 def _create_material_question(self, navigation, subtype, base_type): """Create a material selection question""" question_key = f'q-material-{subtype.lower().replace(" ", "-")}' navigation[question_key] = { 'type': 'question', 'inputType': 'button', 'title': 'Select Material', 'subtitle': f'Choose your preferred material for {subtype}', 'conditional': { 'type': 'material', 'fallbackNext': 'q-dimensions' }, 'answers': [] } # Add material options if self.material_counts[subtype]['aluminum'] > 0: # Check if color question needed after aluminum selection needs_color = self._needs_color_question(subtype, 'Aluminum') next_key_al = f'q-color-{subtype.lower().replace(" ", "-")}-aluminum' if needs_color else 'q-dimensions' navigation[question_key]['answers'].append({ 'caption': 'Aluminum', 'image': 'πŸ”©', 'next': next_key_al, 'filter': { 'baseType': base_type, 'subType': subtype, 'material': 'Aluminum' } }) if self.material_counts[subtype]['vinyl'] > 0: # Check if color question needed after vinyl selection needs_color = self._needs_color_question(subtype, 'Vinyl') next_key_vin = f'q-color-{subtype.lower().replace(" ", "-")}-vinyl' if needs_color else 'q-dimensions' navigation[question_key]['answers'].append({ 'caption': 'Vinyl', 'image': 'πŸͺŸ', 'next': next_key_vin, 'filter': { 'baseType': base_type, 'subType': subtype, 'material': 'Vinyl' } }) # Create color questions for each material option if self.material_counts[subtype]['aluminum'] > 0 and self._needs_color_question(subtype, 'Aluminum'): self._create_color_question(navigation, subtype, 'Aluminum', base_type) if self.material_counts[subtype]['vinyl'] > 0 and self._needs_color_question(subtype, 'Vinyl'): self._create_color_question(navigation, subtype, 'Vinyl', base_type) def _needs_color_question(self, subtype, material): """Check if a subtype+material combination needs a color selection question""" colors = self.color_by_material[subtype][material] # Show color question if multiple colors exist return len(colors) > 1 def _create_color_question(self, navigation, subtype, material, base_type): """Create a color selection question""" question_key = f'q-color-{subtype.lower().replace(" ", "-")}-{material.lower()}' navigation[question_key] = { 'type': 'question', 'inputType': 'button', 'title': 'Select Color', 'subtitle': f'Choose your preferred color for {material} {subtype}', 'conditional': { 'type': 'color', 'fallbackNext': 'q-dimensions' }, 'answers': [] } # Get available colors for this subtype+material colors = sorted(self.color_by_material[subtype][material]) # Color emoji mapping color_emojis = { 'Black': '⬛', 'White': '⬜', 'Bronze': '🟫', 'Tan': '🟀', 'Mill': 'βšͺ', 'Sandstone': '🟨' } for color in colors: navigation[question_key]['answers'].append({ 'caption': color, 'image': color_emojis.get(color, '🎨'), 'next': 'q-dimensions', 'filter': { 'baseType': base_type, 'subType': subtype, 'material': material, 'color': color } }) def save_json_files(self, navigation): """Save all three JSON files""" print("πŸ’Ύ Saving JSON files...") # Save products.json with open('data/products.json', 'w', encoding='utf-8') as f: json.dump(self.products, f, indent=2) print(f"βœ“ Saved data/products.json ({len(self.products)} products)") # Save accessories.json with open('data/accessories.json', 'w', encoding='utf-8') as f: json.dump(self.accessories, f, indent=2) print(f"βœ“ Saved data/accessories.json ({len(self.accessories)} accessories)") # Save navigation.json with open('data/navigation.json', 'w', encoding='utf-8') as f: json.dump(navigation, f, indent=2) print(f"βœ“ Saved data/navigation.json ({len(navigation)} nodes)") def print_statistics(self): """Print parsing statistics""" print("\n" + "="*60) print("PARSING STATISTICS") print("="*60) print(f"Total Products: {len(self.products)}") print(f"Total Accessories: {len(self.accessories)}") print(f"Base Types: {', '.join(sorted(self.base_types))}") print(f"Door Subtypes: {len(self.door_subtypes)}") print(f"Window Subtypes: {len(self.window_subtypes)}") print() print("Material Distribution:") for subtype, counts in sorted(self.material_counts.items()): if counts['aluminum'] > 0 or counts['vinyl'] > 0: needs_q = "βœ“ Material Q" if (counts['aluminum'] > 0 and counts['vinyl'] > 0) else "βœ— Skip" print(f" {subtype:20} - Al:{counts['aluminum']:3} Vinyl:{counts['vinyl']:3} [{needs_q}]") print("="*60) def main(): csv_path = 'data/products.csv' print("πŸš€ CSV to JSON Parser") print("="*60) try: parser = CSVParser(csv_path) parser.load_detailed_product_info() parser.parse_csv() parser.link_accessories_to_products() navigation = parser.build_navigation() parser.save_json_files(navigation) parser.print_statistics() print("\nβœ… All JSON files generated successfully!") print("\nNext steps:") print("1. Review data/navigation.json for question flow") print("2. Review data/products.json for product data") print("3. Review data/accessories.json for accessory compatibility") print("4. Test the application in your browser") except FileNotFoundError: print(f"❌ Error: Could not find {csv_path}") print(" Make sure products.csv exists in the data/ folder") except Exception as e: print(f"❌ Error: {e}") import traceback traceback.print_exc() if __name__ == '__main__': main()