# 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
๐ค Upload Blueprint
Model Used: -
File: -
Cost: -
HVAC Units by Floor
| Floor |
Unit ID |
Type |
Qty |
Capacity (BTU) |
Capacity (kW) |
Location |
Floor Plan Visualization
Download Results
```
### 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!**