# HVAC Blueprint Analyzer - FastAPI + HTML Canvas - 1 Day Build Guide **Timeline**: 6-8 hours **Stack**: FastAPI (backend) + HTML/Canvas (frontend) **Deliverable**: Working demo with file upload, analysis, CSV export, and visual floor plan --- ## โฐ Timeline Breakdown | Time | Task | Duration | |------|------|----------| | 0:00-0:30 | Project setup & dependencies | 30 min | | 0:30-1:30 | FastAPI server scaffold | 1 hour | | 1:30-3:00 | HTML UI & Canvas setup | 1.5 hours | | 3:00-4:30 | CSV exporter integration | 1.5 hours | | 4:30-5:30 | Testing & bug fixes | 1 hour | | 5:30-6:00 | Final polish & deployment | 30 min | | **Total** | | **6 hours** | --- ## ๐Ÿ“ Project Structure ``` hvac_project/ โ”œโ”€โ”€ server.py # FastAPI main app โ”œโ”€โ”€ requirements.txt # Dependencies โ”œโ”€โ”€ public/ # Static files โ”‚ โ”œโ”€โ”€ index.html # Main UI โ”‚ โ”œโ”€โ”€ style.css # Styling โ”‚ โ”œโ”€โ”€ script.js # Canvas & interactions โ”‚ โ””โ”€โ”€ favicon.ico โ”œโ”€โ”€ modules/ โ”‚ โ”œโ”€โ”€ analyzer.py # Analysis logic (use existing code) โ”‚ โ”œโ”€โ”€ csv_exporter.py # CSV generation โ”‚ โ””โ”€โ”€ models.py # Pydantic models โ”œโ”€โ”€ outputs/ # Generated files (CSVs, PDFs) โ””โ”€โ”€ .env # API keys (Gemini, OpenAI) ``` --- ## ๐Ÿš€ STEP 1: Project Setup (30 min) ### 1.1 Create Virtual Environment ```bash cd C:\Users\ruchy\OneDrive\Documents\hvac_project # Create venv python -m venv venv # Activate venv\Scripts\activate # On Mac/Linux: # source venv/bin/activate ``` ### 1.2 Create requirements.txt ```txt fastapi==0.104.1 uvicorn[standard]==0.24.0 python-multipart==0.0.6 pydantic==2.5.0 python-dotenv==1.0.0 pandas==2.1.3 openpyxl==3.11.0 fitz==0.0.1.dev2 PyPDF2==3.17.1 pdfplumber==0.10.3 pytesseract==0.3.10 pillow==10.1.0 aiofiles==23.2.1 requests==2.31.0 reportlab==4.0.7 # Add your existing HVAC dependencies google-generativeai==0.3.0 # For Gemini openai==1.3.0 # For OpenAI ``` ### 1.3 Install Dependencies ```bash pip install -r requirements.txt ``` ### 1.4 Create .env File ```env # Google Gemini GEMINI_API_KEY=your_gemini_key_here # OpenAI OPENAI_API_KEY=your_openai_key_here # FastAPI DEBUG=True HOST=127.0.0.1 PORT=8000 ``` --- ## ๐Ÿ–ฅ๏ธ STEP 2: FastAPI Server (1 hour) ### 2.1 Create models.py ```python # modules/models.py from pydantic import BaseModel from typing import List, Optional, Dict, Any from datetime import datetime class Unit(BaseModel): tag: str type: str quantity: int capacity_btu: Optional[float] = None capacity_kw: Optional[float] = None location: Optional[str] = None detection_status: str = "Detected" notes: Optional[str] = None class Floor(BaseModel): floor_number: int floor_type: str = "residential" units: List[Unit] = [] unit_count: Optional[int] = None class AnalysisResult(BaseModel): status: str file_name: str model_used: str processing_time_seconds: float cost_usd: float summary: Dict[str, Any] floors: List[Floor] equipment_schedule: List[Dict[str, Any]] reconciliation: Dict[str, Any] class AnalysisRequest(BaseModel): model_choice: str = "gemini" # or "openai" schedule_mode: str = "auto" # or "manual" confidence_threshold: float = 0.7 class ScheduleUpload(BaseModel): floor: int unit_id: str unit_type: str quantity: int capacity_btu: Optional[float] = None capacity_kw: Optional[float] = None ``` ### 2.2 Create csv_exporter.py ```python # modules/csv_exporter.py import pandas as pd from pathlib import Path from typing import Dict, List, Any from datetime import datetime class HVACCSVExporter: """Export HVAC analysis to CSV files""" def __init__(self, output_dir: str = "./outputs"): self.output_dir = Path(output_dir) self.output_dir.mkdir(exist_ok=True) def export_all(self, analysis_data: Dict, task_id: str) -> Dict[str, Path]: """Export all CSV files""" return { 'floors_units': self.export_floors_units(analysis_data, task_id), 'equipment_schedule': self.export_equipment_schedule(analysis_data, task_id), 'reconciliation': self.export_reconciliation(analysis_data, task_id), } def export_floors_units(self, data: Dict, task_id: str) -> Path: """Export floor and units breakdown""" rows = [] for floor in data.get('floors', []): for unit in floor.get('units', []): rows.append({ 'Floor': floor['floor_number'], 'Unit_ID': unit['tag'], 'Unit_Type': unit['type'], 'Quantity': unit['quantity'], 'Capacity_BTU': unit.get('capacity_btu', ''), 'Capacity_kW': unit.get('capacity_kw', ''), 'Status': unit.get('detection_status', 'Detected'), 'Location': unit.get('location', ''), 'Notes': unit.get('notes', ''), }) if not rows: rows = [{'Floor': '', 'Unit_ID': '', 'Unit_Type': ''}] df = pd.DataFrame(rows) path = self.output_dir / f"{task_id}_floors_units.csv" df.to_csv(path, index=False) return path def export_equipment_schedule(self, data: Dict, task_id: str) -> Path: """Export equipment schedule""" schedule = data.get('equipment_schedule', []) if not schedule: schedule = [{'Equipment_ID': '', 'Manufacturer': ''}] df = pd.DataFrame(schedule) path = self.output_dir / f"{task_id}_equipment_schedule.csv" df.to_csv(path, index=False) return path def export_reconciliation(self, data: Dict, task_id: str) -> Path: """Export reconciliation report""" issues = data.get('reconciliation', {}).get('issues', []) if not issues: issues = [{'Issue_Type': 'No issues', 'Severity': 'None'}] df = pd.DataFrame(issues) path = self.output_dir / f"{task_id}_reconciliation.csv" df.to_csv(path, index=False) return path # Quick function for FastAPI def create_exporter() -> HVACCSVExporter: return HVACCSVExporter() ``` ### 2.3 Create server.py (Main FastAPI App) ```python # server.py import os import uuid import time from pathlib import Path from typing import Dict, Any from datetime import datetime from fastapi import FastAPI, UploadFile, File, HTTPException from fastapi.staticfiles import StaticFiles from fastapi.responses import FileResponse, JSONResponse, StreamingResponse from fastapi.middleware.cors import CORSMiddleware import aiofiles import zipfile from io import BytesIO from dotenv import load_dotenv # Import your existing analysis module # from python_files.read_mechanical_plansvs1_4 import analyze_blueprint from modules.csv_exporter import HVACCSVExporter from modules.models import AnalysisResult, AnalysisRequest load_dotenv() app = FastAPI( title="HVAC Blueprint Analyzer", description="Analyze HVAC systems from architectural blueprints", version="1.0.0" ) # CORS middleware app.add_middleware( CORSMiddleware, allow_origins=["*"], allow_credentials=True, allow_methods=["*"], allow_headers=["*"], ) # Serve static files app.mount("/static", StaticFiles(directory="public"), name="static") # Initialize exporter exporter = HVACCSVExporter() # Storage for active analyses (in-memory for now) active_analyses = {} @app.get("/") async def root(): """Serve main HTML page""" return FileResponse("public/index.html") @app.post("/api/analyze") async def analyze( file: UploadFile = File(...), model_choice: str = "gemini", schedule_mode: str = "auto" ): """ Analyze uploaded blueprint Returns task_id for tracking """ if not file.filename.endswith('.pdf'): raise HTTPException(status_code=400, detail="Only PDF files allowed") # Generate task ID task_id = str(uuid.uuid4()) # Save uploaded file upload_dir = Path("./uploads") upload_dir.mkdir(exist_ok=True) file_path = upload_dir / f"{task_id}_{file.filename}" try: # Save file async with aiofiles.open(file_path, 'wb') as f: contents = await file.read() await f.write(contents) # Start analysis (mock for now - replace with your actual analysis) start_time = time.time() analysis_result = await analyze_blueprint_async( file_path, model_choice, schedule_mode ) processing_time = time.time() - start_time # Add metadata analysis_result['task_id'] = task_id analysis_result['processing_time_seconds'] = processing_time analysis_result['model_used'] = model_choice analysis_result['file_name'] = file.filename # Generate CSVs csv_files = exporter.export_all(analysis_result, task_id) analysis_result['csv_files'] = { name: f"/api/download/csv/{task_id}/{name}" for name in csv_files.keys() } # Store result active_analyses[task_id] = analysis_result return { "status": "success", "task_id": task_id, "result": analysis_result } except Exception as e: return { "status": "error", "task_id": task_id, "error": str(e) } @app.get("/api/results/{task_id}") async def get_results(task_id: str): """Get analysis results by task ID""" if task_id not in active_analyses: raise HTTPException(status_code=404, detail="Task not found") return active_analyses[task_id] @app.get("/api/download/csv/{task_id}/{csv_type}") async def download_csv(task_id: str, csv_type: str): """Download specific CSV file""" file_path = Path("./outputs") / f"{task_id}_{csv_type}.csv" if not file_path.exists(): raise HTTPException(status_code=404, detail="File not found") return FileResponse( file_path, filename=f"hvac_{csv_type}.csv", media_type="text/csv" ) @app.get("/api/download/zip/{task_id}") async def download_all_zip(task_id: str): """Download all outputs as ZIP""" if task_id not in active_analyses: raise HTTPException(status_code=404, detail="Task not found") # Create ZIP buffer zip_buffer = BytesIO() with zipfile.ZipFile(zip_buffer, 'w', zipfile.ZIP_DEFLATED) as zf: # Add all CSVs for csv_type in ['floors_units', 'equipment_schedule', 'reconciliation']: file_path = Path("./outputs") / f"{task_id}_{csv_type}.csv" if file_path.exists(): zf.write(file_path, arcname=file_path.name) # Add JSON results import json json_data = json.dumps(active_analyses[task_id], indent=2, default=str) zf.writestr(f"{task_id}_results.json", json_data) zip_buffer.seek(0) return StreamingResponse( iter([zip_buffer.getvalue()]), media_type="application/zip", headers={"Content-Disposition": f"attachment; filename=hvac_{task_id}.zip"} ) @app.get("/api/health") async def health(): """Health check endpoint""" return {"status": "ok"} # ============================================================ # MOCK ANALYSIS FUNCTION - REPLACE WITH YOUR ACTUAL CODE # ============================================================ async def analyze_blueprint_async( file_path: Path, model_choice: str = "gemini", schedule_mode: str = "auto" ) -> Dict[str, Any]: """ Mock analysis function Replace this with your actual HVAC analysis code """ # TODO: Replace with actual analysis from your existing code # For now, returning mock data return { "status": "success", "summary": { "total_floors": 3, "total_units": 8, "total_capacity_btu": 256000, "total_capacity_kw": 75.0 }, "floors": [ { "floor_number": 1, "floor_type": "residential", "units": [ { "tag": "AC-A", "type": "Ceiling Concealed Ducted", "quantity": 1, "capacity_btu": 36000, "capacity_kw": 10.5, "location": "Living area", "detection_status": "Detected" }, { "tag": "AC-B", "type": "Wall Mounted", "quantity": 1, "capacity_btu": 24000, "capacity_kw": 7.0, "location": "Bedroom", "detection_status": "Detected" } ] }, { "floor_number": 2, "floor_type": "residential", "units": [ { "tag": "AC-C", "type": "Ceiling Concealed Ducted", "quantity": 1, "capacity_btu": 36000, "capacity_kw": 10.5, "location": "Master bedroom", "detection_status": "Detected" }, { "tag": "AC-D", "type": "Wall Mounted", "quantity": 2, "capacity_btu": 24000, "capacity_kw": 7.0, "location": "Living areas", "detection_status": "Detected" } ] }, { "floor_number": 3, "floor_type": "commercial", "units": [ { "tag": "AC-E", "type": "Ceiling Concealed Ducted", "quantity": 1, "capacity_btu": 48000, "capacity_kw": 14.0, "location": "Office zone", "detection_status": "Detected" } ] } ], "equipment_schedule": [ { "id": 1, "manufacturer": "Carrier", "model": "25HNE024A03", "type": "Ceiling Cassette", "capacity_btu": 24000, "voltage": "208-230V", "phase": 1, "frequency_hz": 60, "quantity": 1, "floor": 1, "unit_tag": "AC-A" } ], "reconciliation": { "issues_found": 0, "high_severity": 0, "medium_severity": 0, "low_severity": 0, "issues": [] }, "cost_usd": 0.08 } # ============================================================ if __name__ == "__main__": import uvicorn uvicorn.run( "server:app", host=os.getenv("HOST", "127.0.0.1"), port=int(os.getenv("PORT", 8000)), reload=os.getenv("DEBUG", True) ) ``` --- ## ๐ŸŽจ STEP 3: HTML UI & Canvas (1.5 hours) ### 3.1 Create public/index.html ```html HVAC Blueprint Analyzer

๐Ÿข HVAC Blueprint Analyzer

Analyze HVAC systems from architectural blueprints

๐Ÿ“ค Upload Blueprint

Drag & drop PDF here or click to browse

``` ### 3.2 Create public/style.css ```css /* style.css */ :root { --primary: #2563eb; --primary-hover: #1d4ed8; --secondary: #64748b; --success: #16a34a; --error: #dc2626; --warning: #f59e0b; --bg: #f8fafc; --card: #ffffff; --border: #e2e8f0; --text: #1e293b; --text-light: #64748b; } * { margin: 0; padding: 0; box-sizing: border-box; } body { font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, Oxygen, Ubuntu, Cantarell, sans-serif; background: var(--bg); color: var(--text); line-height: 1.6; } .container { max-width: 1200px; margin: 0 auto; padding: 20px; } header { text-align: center; margin-bottom: 40px; } header h1 { font-size: 2.5em; color: var(--primary); margin-bottom: 10px; } header p { color: var(--text-light); font-size: 1.1em; } /* Cards */ .card { background: var(--card); border-radius: 12px; padding: 30px; margin-bottom: 20px; box-shadow: 0 1px 3px rgba(0, 0, 0, 0.1); } .card h2, .card h3 { margin-bottom: 20px; color: var(--text); } /* Upload Area */ .upload-area { border: 2px dashed var(--primary); border-radius: 8px; padding: 40px; text-align: center; cursor: pointer; transition: all 0.3s; margin-bottom: 20px; background: #f0f9ff; } .upload-area:hover { border-color: var(--primary-hover); background: #e0f2fe; } .upload-area.dragover { border-color: var(--success); background: #f0fdf4; } .upload-icon { width: 60px; height: 60px; color: var(--primary); margin-bottom: 10px; } .upload-area p { color: var(--text-light); margin-bottom: 10px; } .file-info { background: #f1f5f9; padding: 15px; border-radius: 6px; margin-bottom: 20px; display: flex; justify-content: space-between; align-items: center; } /* Config Grid */ .config-grid { display: grid; grid-template-columns: 1fr 1fr; gap: 20px; margin-bottom: 20px; } .config-item label { display: block; font-weight: 500; margin-bottom: 10px; color: var(--text); } .radio-group { display: flex; gap: 20px; flex-wrap: wrap; } .radio-group input[type="radio"] { margin-right: 5px; } .radio-group label { display: inline; margin: 0; font-weight: 400; } /* Buttons */ .btn { padding: 12px 24px; border: none; border-radius: 6px; font-size: 1em; cursor: pointer; transition: all 0.3s; font-weight: 500; } .btn-primary { background: var(--primary); color: white; width: 100%; } .btn-primary:hover { background: var(--primary-hover); transform: translateY(-2px); box-shadow: 0 4px 12px rgba(37, 99, 235, 0.3); } .btn-primary:disabled { background: var(--border); cursor: not-allowed; transform: none; } .btn-secondary { background: var(--secondary); color: white; } .btn-secondary:hover { background: #475569; } .btn-download { background: var(--success); color: white; width: 100%; margin-bottom: 10px; } .btn-download:hover { background: #15803d; } /* Loading */ .spinner { border: 4px solid var(--border); border-top: 4px solid var(--primary); border-radius: 50%; width: 40px; height: 40px; animation: spin 1s linear infinite; margin: 20px auto; } @keyframes spin { 0% { transform: rotate(0deg); } 100% { transform: rotate(360deg); } } .progress-bar { width: 100%; height: 8px; background: var(--border); border-radius: 4px; overflow: hidden; margin-top: 15px; } .progress-fill { height: 100%; background: var(--primary); width: 0%; transition: width 0.3s; } /* Tabs */ .tabs { display: flex; gap: 10px; border-bottom: 2px solid var(--border); margin-bottom: 20px; flex-wrap: wrap; } .tab-btn { padding: 12px 20px; background: none; border: none; color: var(--text-light); cursor: pointer; font-weight: 500; border-bottom: 3px solid transparent; transition: all 0.3s; } .tab-btn.active { color: var(--primary); border-bottom-color: var(--primary); } .tab-btn:hover { color: var(--text); } .tab-content { display: none; } .tab-content.active { display: block; } /* Stats Grid */ .stats-grid { display: grid; grid-template-columns: repeat(auto-fit, minmax(200px, 1fr)); gap: 15px; margin-bottom: 20px; } .stat-card { background: linear-gradient(135deg, #667eea 0%, #764ba2 100%); color: white; padding: 20px; border-radius: 8px; text-align: center; } .stat-label { font-size: 0.9em; opacity: 0.9; margin-bottom: 10px; } .stat-value { font-size: 1.8em; font-weight: bold; } /* Info Box */ .info-box { background: #f8fafc; padding: 15px; border-radius: 6px; border-left: 4px solid var(--primary); } .info-box p { margin: 8px 0; font-size: 0.95em; } /* Table */ .table-container { overflow-x: auto; margin-bottom: 20px; } table { width: 100%; border-collapse: collapse; } th { background: #f1f5f9; padding: 12px; text-align: left; font-weight: 600; border-bottom: 2px solid var(--border); } td { padding: 12px; border-bottom: 1px solid var(--border); } tr:hover { background: #f8fafc; } /* Canvas */ canvas { max-width: 100%; background: white; border-radius: 6px; } /* Downloads Grid */ .downloads-grid { display: grid; grid-template-columns: 1fr; gap: 10px; } /* Error */ .error-section { border-left: 4px solid var(--error); } .error-section h3 { color: var(--error); } /* Responsive */ @media (max-width: 768px) { .config-grid { grid-template-columns: 1fr; } .stats-grid { grid-template-columns: 1fr 1fr; } .tabs { flex-direction: column; } .tab-btn { width: 100%; border-bottom: none; border-left: 3px solid transparent; } .tab-btn.active { border-left-color: var(--primary); border-bottom: none; } } ``` ### 3.3 Create public/script.js ```javascript // script.js const API_BASE = '/api'; let currentTaskId = null; let currentAnalysis = null; // DOM Elements const uploadArea = document.getElementById('uploadArea'); const fileInput = document.getElementById('fileInput'); const fileInfo = document.getElementById('fileInfo'); const analyzeBtn = document.getElementById('analyzeBtn'); const loadingSection = document.getElementById('loadingSection'); const resultsSection = document.getElementById('resultsSection'); const errorSection = document.getElementById('errorSection'); const tabs = document.querySelectorAll('.tab-btn'); const tabContents = document.querySelectorAll('.tab-content'); // ===== FILE UPLOAD ===== uploadArea.addEventListener('click', () => fileInput.click()); uploadArea.addEventListener('dragover', (e) => { e.preventDefault(); uploadArea.classList.add('dragover'); }); uploadArea.addEventListener('dragleave', () => { uploadArea.classList.remove('dragover'); }); uploadArea.addEventListener('drop', (e) => { e.preventDefault(); uploadArea.classList.remove('dragover'); handleFiles(e.dataTransfer.files); }); fileInput.addEventListener('change', (e) => { handleFiles(e.target.files); }); function handleFiles(files) { const file = files[0]; if (!file) return; if (!file.name.endsWith('.pdf')) { showError('Please upload a PDF file'); return; } // Show file info fileInfo.style.display = 'block'; document.getElementById('fileName').textContent = file.name; document.getElementById('fileSize').textContent = `(${(file.size / 1024 / 1024).toFixed(2)} MB)`; // Enable analyze button analyzeBtn.disabled = false; analyzeBtn.onclick = () => analyzeBlueprint(file); } // ===== ANALYSIS ===== async function analyzeBlueprint(file) { const modelChoice = document.querySelector('input[name="model"]:checked').value; const scheduleMode = document.querySelector('input[name="schedule"]:checked').value; // Prepare form data const formData = new FormData(); formData.append('file', file); formData.append('model_choice', modelChoice); formData.append('schedule_mode', scheduleMode); // Show loading resultsSection.style.display = 'none'; errorSection.style.display = 'none'; loadingSection.style.display = 'block'; // Simulate progress let progress = 0; const progressInterval = setInterval(() => { progress = Math.min(progress + Math.random() * 30, 90); document.getElementById('progressFill').style.width = progress + '%'; }, 200); try { const response = await fetch(`${API_BASE}/analyze`, { method: 'POST', body: formData }); clearInterval(progressInterval); if (!response.ok) { const error = await response.json(); throw new Error(error.detail || 'Analysis failed'); } document.getElementById('progressFill').style.width = '100%'; const data = await response.json(); if (data.status === 'error') { throw new Error(data.error); } currentTaskId = data.task_id; currentAnalysis = data.result; // Display results loadingSection.style.display = 'none'; displayResults(data.result); resultsSection.style.display = 'block'; } catch (error) { clearInterval(progressInterval); loadingSection.style.display = 'none'; showError(error.message); } } // ===== DISPLAY RESULTS ===== function displayResults(result) { // Summary tab document.getElementById('totalFloors').textContent = result.summary.total_floors; document.getElementById('totalUnits').textContent = result.summary.total_units; document.getElementById('totalCapacity').textContent = `${(result.summary.total_capacity_btu / 1000).toFixed(0)}k BTU`; document.getElementById('processingTime').textContent = `${result.processing_time_seconds.toFixed(2)}s`; document.getElementById('modelUsed').textContent = result.model_used; document.getElementById('fileName2').textContent = result.file_name; document.getElementById('costUsed').textContent = `$${result.cost_usd.toFixed(4)}`; // Floors table populateFloorsTable(result.floors); // Canvas drawFloorPlan(result.floors); // Download buttons setupDownloadButtons(result.csv_files); } function populateFloorsTable(floors) { const tbody = document.getElementById('floorsTableBody'); tbody.innerHTML = ''; floors.forEach(floor => { floor.units.forEach(unit => { const row = tbody.insertRow(); row.innerHTML = ` ${floor.floor_number} ${unit.tag} ${unit.type} ${unit.quantity} ${unit.capacity_btu || '-'} ${unit.capacity_kw || '-'} ${unit.location || '-'} `; }); }); } function drawFloorPlan(floors) { const canvas = document.getElementById('floorCanvas'); const ctx = canvas.getContext('2d'); // Clear canvas ctx.fillStyle = '#ffffff'; ctx.fillRect(0, 0, canvas.width, canvas.height); // Draw background ctx.strokeStyle = '#e2e8f0'; ctx.lineWidth = 1; for (let i = 0; i < canvas.width; i += 50) { ctx.beginPath(); ctx.moveTo(i, 0); ctx.lineTo(i, canvas.height); ctx.stroke(); } // Draw floors const floorHeight = canvas.height / (floors.length + 1); floors.forEach((floor, floorIndex) => { const y = 50 + floorIndex * floorHeight; // Floor label ctx.fillStyle = '#1e293b'; ctx.font = 'bold 16px Arial'; ctx.fillText(`Floor ${floor.floor_number}`, 20, y); // Units let unitX = 150; floor.units.forEach(unit => { drawUnit(ctx, unitX, y, unit); unitX += 120; }); // Floor line ctx.strokeStyle = '#cbd5e1'; ctx.lineWidth = 2; ctx.beginPath(); ctx.moveTo(0, y + 40); ctx.lineTo(canvas.width, y + 40); ctx.stroke(); }); } function drawUnit(ctx, x, y, unit) { // Unit box ctx.fillStyle = '#dbeafe'; ctx.fillRect(x, y, 100, 30); ctx.strokeStyle = '#2563eb'; ctx.lineWidth = 2; ctx.strokeRect(x, y, 100, 30); // Unit text ctx.fillStyle = '#1e293b'; ctx.font = 'bold 12px Arial'; ctx.textAlign = 'center'; ctx.fillText(unit.tag, x + 50, y + 20); // Capacity below ctx.font = '10px Arial'; ctx.fillStyle = '#64748b'; ctx.fillText(`${unit.quantity}x${Math.round(unit.capacity_btu / 1000)}k`, x + 50, y + 35); } function setupDownloadButtons(csvFiles) { document.getElementById('downloadFloors').onclick = () => downloadFile(csvFiles.floors_units, 'hvac_floors_units.csv'); document.getElementById('downloadSchedule').onclick = () => downloadFile(csvFiles.equipment_schedule, 'hvac_equipment_schedule.csv'); document.getElementById('downloadReconciliation').onclick = () => downloadFile(csvFiles.reconciliation, 'hvac_reconciliation.csv'); document.getElementById('downloadZip').onclick = () => downloadFile(`${API_BASE}/download/zip/${currentTaskId}`, 'hvac_analysis.zip'); } function downloadFile(url, filename) { const a = document.createElement('a'); a.href = url; a.download = filename; a.click(); } // ===== TABS ===== tabs.forEach(tab => { tab.addEventListener('click', () => { // Remove active from all tabs.forEach(t => t.classList.remove('active')); tabContents.forEach(c => c.classList.remove('active')); // Add active to clicked tab.classList.add('active'); const tabId = tab.dataset.tab + '-tab'; document.getElementById(tabId).classList.add('active'); }); }); // ===== ERROR HANDLING ===== function showError(message) { loadingSection.style.display = 'none'; resultsSection.style.display = 'none'; errorSection.style.display = 'block'; document.getElementById('errorMessage').textContent = message; } ``` --- ## ๐Ÿงช STEP 4: CSV Integration (1.5 hours) Already done in `server.py`! The exporter is integrated into the `/api/analyze` endpoint. **Tests to run:** ```bash # Test CSV generation curl -X POST -F "file=@test.pdf" \ -F "model_choice=gemini" \ http://localhost:8000/api/analyze # Download CSV curl http://localhost:8000/api/download/csv/{task_id}/floors_units \ -o floors_units.csv ``` --- ## ๐Ÿš€ STEP 5: Testing & Bug Fixes (1 hour) ### 5.1 Run FastAPI Server ```bash cd C:\Users\ruchy\OneDrive\Documents\hvac_project # Activate venv venv\Scripts\activate # Run server python server.py ``` **Expected output:** ``` INFO: Uvicorn running on http://127.0.0.1:8000 INFO: Application startup complete ``` ### 5.2 Test in Browser - Open: http://127.0.0.1:8000 - Upload test PDF - Click "Analyze" - Check results display - Download CSVs ### 5.3 Common Fixes ```python # If upload fails: # 1. Check /uploads folder exists # 2. Check file permissions # 3. Check PDF is valid # If CSV not generated: # 1. Check /outputs folder exists # 2. Check Pandas is installed # 3. Check data structure matches # If canvas not displaying: # 1. Check browser console for JS errors # 2. Ensure floor data structure is correct # 3. Check canvas size ``` --- ## ๐Ÿ“ฆ STEP 6: Polish & Deployment (30 min) ### 6.1 Replace Mock Analysis In `server.py`, replace the `analyze_blueprint_async()` function with your actual code: ```python async def analyze_blueprint_async(file_path, model_choice, schedule_mode): # Import your actual analyzer from python_files.read_mechanical_plansvs1_4 import extract_blueprint_data # Run analysis result = extract_blueprint_data(str(file_path), model_choice) return result ``` ### 6.2 Create .gitignore ``` venv/ __pycache__/ *.pyc .env outputs/ uploads/ .DS_Store .idea/ *.log ``` ### 6.3 Deploy to Cloud (Choose One) **Option A: Heroku (Easiest)** ```bash # Create requirements.txt pip freeze > requirements.txt # Create Procfile echo "web: uvicorn server:app --host 0.0.0.0 --port $PORT" > Procfile # Deploy heroku create hvac-analyzer git push heroku main ``` **Option B: Google Cloud Run** ```bash # Create Dockerfile # Create cloudbuild.yaml # Deploy via gcloud CLI gcloud run deploy hvac-analyzer --source . ``` **Option C: Local/VPS** - Use systemd service - Run with Gunicorn: `gunicorn -w 4 -k uvicorn.workers.UvicornWorker server:app` - Configure Nginx reverse proxy --- ## โœ… FINAL CHECKLIST - [x] FastAPI server running - [x] HTML UI loads - [x] File upload works - [x] Analysis runs (with mock data) - [x] Results display - [x] CSV exports work - [x] Canvas draws floor plan - [x] Download buttons functional - [x] Error handling in place - [x] Responsive on mobile --- ## ๐ŸŽฏ Next Steps (After 1 Day) 1. **Replace mock analysis** with your actual HVAC code 2. **Add Gemini API calls** (if not already done) 3. **Implement OpenAI fallback** 4. **Add A/B testing comparison** tab 5. **Create schedule upload** functionality 6. **Deploy to production** URL 7. **Add monitoring & logging** --- ## ๐Ÿ“ Project Layout After Build ``` hvac_project/ โ”œโ”€โ”€ server.py โœ… โ”œโ”€โ”€ requirements.txt โœ… โ”œโ”€โ”€ .env โœ… โ”œโ”€โ”€ .gitignore โœ… โ”œโ”€โ”€ public/ โ”‚ โ”œโ”€โ”€ index.html โœ… โ”‚ โ”œโ”€โ”€ style.css โœ… โ”‚ โ””โ”€โ”€ script.js โœ… โ”œโ”€โ”€ modules/ โ”‚ โ”œโ”€โ”€ csv_exporter.py โœ… โ”‚ โ””โ”€โ”€ models.py โœ… โ”œโ”€โ”€ uploads/ (auto-created) โ”œโ”€โ”€ outputs/ (auto-created) โ””โ”€โ”€ python_files/ (your existing code) ``` --- ## ๐ŸŽ‰ Success = Working Demo in 6 Hours! Once done, you'll have: - โœ… FastAPI backend running - โœ… Professional HTML/Canvas UI - โœ… File upload & processing - โœ… CSV export (3 files) - โœ… Floor plan visualization - โœ… Deployed to cloud **Ready to show investors/clients!**