653 lines
28 KiB
Python
653 lines
28 KiB
Python
"""
|
|
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()
|