hvac_fable / FASTAPI_HTML_CANVAS_1DAY_GUIDE.md
Rachel7's picture
Deploy Fable B-test analyzer
dac3297 verified
|
Raw
History Blame Contribute Delete
41.9 kB

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

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

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

pip install -r requirements.txt

1.4 Create .env File

# 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

# 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

# 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)

# 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

<!DOCTYPE html>
<html lang="en">
<head>
    <meta charset="UTF-8">
    <meta name="viewport" content="width=device-width, initial-scale=1.0">
    <title>HVAC Blueprint Analyzer</title>
    <link rel="stylesheet" href="/static/style.css">
</head>
<body>
    <div class="container">
        <header>
            <h1>🏒 HVAC Blueprint Analyzer</h1>
            <p>Analyze HVAC systems from architectural blueprints</p>
        </header>

        <main>
            <!-- Upload Section -->
            <section class="card upload-section">
                <h2>πŸ“€ Upload Blueprint</h2>
                
                <div class="upload-area" id="uploadArea">
                    <svg class="upload-icon" viewBox="0 0 24 24" fill="none" stroke="currentColor">
                        <path d="M21 15v4a2 2 0 0 1-2 2H5a2 2 0 0 1-2-2v-4"></path>
                        <polyline points="17 8 12 3 7 8"></polyline>
                        <line x1="12" y1="3" x2="12" y2="15"></line>
                    </svg>
                    <p>Drag & drop PDF here or click to browse</p>
                    <input type="file" id="fileInput" accept=".pdf" style="display:none;">
                </div>

                <div class="file-info" id="fileInfo" style="display:none;">
                    <span id="fileName"></span>
                    <span id="fileSize"></span>
                </div>

                <div class="config-grid">
                    <div class="config-item">
                        <label>AI Model:</label>
                        <div class="radio-group">
                            <input type="radio" id="gemini" name="model" value="gemini" checked>
                            <label for="gemini">Gemini</label>
                            <input type="radio" id="openai" name="model" value="openai">
                            <label for="openai">OpenAI</label>
                        </div>
                    </div>

                    <div class="config-item">
                        <label>Schedule Mode:</label>
                        <div class="radio-group">
                            <input type="radio" id="auto" name="schedule" value="auto" checked>
                            <label for="auto">Auto-Detect</label>
                            <input type="radio" id="manual" name="schedule" value="manual">
                            <label for="manual">Upload Schedule</label>
                        </div>
                    </div>
                </div>

                <button id="analyzeBtn" class="btn btn-primary" disabled>
                    πŸš€ Analyze Blueprint
                </button>
            </section>

            <!-- Loading Section -->
            <section class="card loading-section" id="loadingSection" style="display:none;">
                <div class="spinner"></div>
                <p id="loadingText">Processing blueprint...</p>
                <div class="progress-bar">
                    <div class="progress-fill" id="progressFill"></div>
                </div>
            </section>

            <!-- Results Section -->
            <section class="card results-section" id="resultsSection" style="display:none;">
                <div class="tabs">
                    <button class="tab-btn active" data-tab="summary">πŸ“Š Summary</button>
                    <button class="tab-btn" data-tab="floors">πŸ—οΈ Floors & Units</button>
                    <button class="tab-btn" data-tab="canvas">🎨 Floor Plan</button>
                    <button class="tab-btn" data-tab="downloads">⬇️ Downloads</button>
                </div>

                <!-- Summary Tab -->
                <div class="tab-content active" id="summary-tab">
                    <div class="stats-grid">
                        <div class="stat-card">
                            <div class="stat-label">Total Floors</div>
                            <div class="stat-value" id="totalFloors">-</div>
                        </div>
                        <div class="stat-card">
                            <div class="stat-label">Total Units</div>
                            <div class="stat-value" id="totalUnits">-</div>
                        </div>
                        <div class="stat-card">
                            <div class="stat-label">Total Capacity</div>
                            <div class="stat-value" id="totalCapacity">-</div>
                        </div>
                        <div class="stat-card">
                            <div class="stat-label">Processing Time</div>
                            <div class="stat-value" id="processingTime">-</div>
                        </div>
                    </div>

                    <div class="info-box">
                        <p><strong>Model Used:</strong> <span id="modelUsed">-</span></p>
                        <p><strong>File:</strong> <span id="fileName2">-</span></p>
                        <p><strong>Cost:</strong> <span id="costUsed">-</span></p>
                    </div>
                </div>

                <!-- Floors & Units Tab -->
                <div class="tab-content" id="floors-tab">
                    <h3>HVAC Units by Floor</h3>
                    <div class="table-container">
                        <table id="floorsTable">
                            <thead>
                                <tr>
                                    <th>Floor</th>
                                    <th>Unit ID</th>
                                    <th>Type</th>
                                    <th>Qty</th>
                                    <th>Capacity (BTU)</th>
                                    <th>Capacity (kW)</th>
                                    <th>Location</th>
                                </tr>
                            </thead>
                            <tbody id="floorsTableBody">
                            </tbody>
                        </table>
                    </div>
                </div>

                <!-- Canvas/Floor Plan Tab -->
                <div class="tab-content" id="canvas-tab">
                    <h3>Floor Plan Visualization</h3>
                    <canvas id="floorCanvas" width="800" height="600" style="border:1px solid #ddd;"></canvas>
                </div>

                <!-- Downloads Tab -->
                <div class="tab-content" id="downloads-tab">
                    <h3>Download Results</h3>
                    <div class="downloads-grid">
                        <button class="btn btn-download" id="downloadFloors">
                            πŸ“„ Floors & Units (CSV)
                        </button>
                        <button class="btn btn-download" id="downloadSchedule">
                            πŸ“‹ Equipment Schedule (CSV)
                        </button>
                        <button class="btn btn-download" id="downloadReconciliation">
                            βœ”οΈ Reconciliation (CSV)
                        </button>
                        <button class="btn btn-download" id="downloadZip">
                            πŸ“¦ Download All (ZIP)
                        </button>
                    </div>
                </div>
            </section>

            <!-- Error Section -->
            <section class="card error-section" id="errorSection" style="display:none;">
                <h3>❌ Error</h3>
                <p id="errorMessage"></p>
                <button class="btn btn-secondary" onclick="location.reload()">Try Again</button>
            </section>
        </main>
    </div>

    <script src="/static/script.js"></script>
</body>
</html>

3.2 Create public/style.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

// 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 = `
                <td>${floor.floor_number}</td>
                <td><strong>${unit.tag}</strong></td>
                <td>${unit.type}</td>
                <td>${unit.quantity}</td>
                <td>${unit.capacity_btu || '-'}</td>
                <td>${unit.capacity_kw || '-'}</td>
                <td>${unit.location || '-'}</td>
            `;
        });
    });
}

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:

# 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

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

5.3 Common Fixes

# 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:

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)

# 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

# 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

  • FastAPI server running
  • HTML UI loads
  • File upload works
  • Analysis runs (with mock data)
  • Results display
  • CSV exports work
  • Canvas draws floor plan
  • Download buttons functional
  • Error handling in place
  • 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!