Initial Code Commit
This commit is contained in:
415
Project/components/uploadDataModal.tsx
Executable file
415
Project/components/uploadDataModal.tsx
Executable file
@@ -0,0 +1,415 @@
|
||||
// components/uploadDataModal.tsx
|
||||
|
||||
import { useState, useEffect } from "react";
|
||||
import { Button, Modal, ModalContent, ModalHeader, ModalBody, ModalFooter } from "@nextui-org/react";
|
||||
import { Accordion, AccordionItem } from "@nextui-org/react";
|
||||
import { AzureKeyCredential, DocumentAnalysisClient } from "@azure/ai-form-recognizer";
|
||||
import { useRouter } from 'next/navigation';
|
||||
|
||||
import { ElectricityDataPoint, NaturalGasDataPoint } from "../lib/useBuildingData";
|
||||
|
||||
interface UploadDataModalProps {
|
||||
isOpen: boolean;
|
||||
onClose: () => void;
|
||||
buildingid: string;
|
||||
updateBuilding: (newData: any) => void;
|
||||
}
|
||||
|
||||
const EMISSIONS_FACTOR = 0.5;
|
||||
const key = process.env.NEXT_PUBLIC_FORM_RECOGNIZER_KEY;
|
||||
const endpoint = process.env.NEXT_PUBLIC_FORM_RECOGNIZER_ENDPOINT;
|
||||
|
||||
export function UploadDataModal({ isOpen, onClose, buildingid, updateBuilding }: UploadDataModalProps) {
|
||||
const [isSubmitted, setIsSubmitted] = useState(false);
|
||||
const [gasFile, setGasFile] = useState<File | null>(null);
|
||||
const [electricityFile, setElectricityFile] = useState<File | null>(null);
|
||||
const [gasFileUrl, setGasFileUrl] = useState<string | null>(null);
|
||||
const [electricityFileUrl, setElectricityFileUrl] = useState<string | null>(null);
|
||||
const [extractionStatus, setExtractionStatus] = useState<'idle' | 'loading' | 'complete'>('idle');
|
||||
const [aiExtractionStatus, setAiExtractionStatus] = useState<'idle' | 'loading' | 'complete'>('idle');
|
||||
const [dataPreview, setDataPreview] = useState<any>(null);
|
||||
const router = useRouter();
|
||||
|
||||
const handleFileUpload = (type: 'gas' | 'electricity', file: File) => {
|
||||
if (type === 'gas') {
|
||||
setGasFile(file);
|
||||
setGasFileUrl(URL.createObjectURL(file));
|
||||
} else if (type === 'electricity') {
|
||||
setElectricityFile(file);
|
||||
setElectricityFileUrl(URL.createObjectURL(file));
|
||||
}
|
||||
setIsSubmitted(true);
|
||||
};
|
||||
|
||||
useEffect(() => {
|
||||
return () => {
|
||||
if (gasFileUrl) URL.revokeObjectURL(gasFileUrl);
|
||||
if (electricityFileUrl) URL.revokeObjectURL(electricityFileUrl);
|
||||
};
|
||||
}, [gasFileUrl, electricityFileUrl]);
|
||||
|
||||
const extractDataFromPDF = async (file: File, type: 'gas' | 'electricity') => {
|
||||
const client = new DocumentAnalysisClient(endpoint!, new AzureKeyCredential(key!));
|
||||
|
||||
const arrayBuffer = await file.arrayBuffer();
|
||||
const poller = await client.beginAnalyzeDocument("prebuilt-document", arrayBuffer);
|
||||
const { keyValuePairs } = await poller.pollUntilDone();
|
||||
|
||||
if (!keyValuePairs) return [];
|
||||
|
||||
const dataPoints: (ElectricityDataPoint | NaturalGasDataPoint)[] = [];
|
||||
let extractedDate: Date | null = null;
|
||||
|
||||
const monthMap: { [key: string]: number } = {
|
||||
'jan': 0, 'january': 0, 'feb': 1, 'february': 1, 'mar': 2, 'march': 2,
|
||||
'apr': 3, 'april': 3, 'may': 4, 'jun': 5, 'june': 5, 'jul': 6, 'july': 6,
|
||||
'aug': 7, 'august': 7, 'sep': 8, 'september': 8, 'oct': 9, 'october': 9,
|
||||
'nov': 10, 'november': 10, 'dec': 11, 'december': 11
|
||||
};
|
||||
|
||||
for (const { key, value } of keyValuePairs) {
|
||||
console.log("KEY:", key.content, "VALUE:", value?.content);
|
||||
if (!value) continue;
|
||||
|
||||
const keyLower = key.content.toLowerCase();
|
||||
const valueLower = value.content.toLowerCase();
|
||||
|
||||
// Extract date information
|
||||
if (keyLower.includes('date') || keyLower.includes('period')) {
|
||||
console.log("DATE IDENTIFIED:", valueLower);
|
||||
const dateMatch = valueLower.match(/(\d{1,2})\s*(?:st|nd|rd|th)?\s*(?:of)?\s*([a-z]+)?\s*(\d{4})?/i);
|
||||
|
||||
console.log("DATE MATCH:", dateMatch);
|
||||
|
||||
if (dateMatch) {
|
||||
const day = 1; // Always assume 1st of the month
|
||||
const month = dateMatch[2] ? monthMap[dateMatch[2].toLowerCase()] : new Date().getMonth();
|
||||
const year = dateMatch[3] ? parseInt(dateMatch[3]) : new Date().getFullYear();
|
||||
|
||||
if (year >= 1900 && year <= 2100) {
|
||||
extractedDate = new Date(year, month, day);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
if (type === 'electricity' && keyLower.includes('kwh')) {
|
||||
const kwh = parseFloat(value.content || '0');
|
||||
|
||||
if (kwh !== 0) {
|
||||
const timestamp = extractedDate || new Date();
|
||||
|
||||
timestamp.setHours(0, 0, 0, 0); // Set to midnight
|
||||
|
||||
const existingDataIndex = dataPoints.findIndex(point =>
|
||||
point.timestamp.seconds === timestamp.getTime() / 1000
|
||||
);
|
||||
|
||||
if (existingDataIndex === -1) {
|
||||
dataPoints.push({
|
||||
timestamp: { seconds: timestamp.getTime() / 1000, nanoseconds: 0 },
|
||||
kwh: kwh,
|
||||
emissions: kwh * EMISSIONS_FACTOR / 1000,
|
||||
});
|
||||
} else {
|
||||
dataPoints[existingDataIndex] = {
|
||||
...dataPoints[existingDataIndex],
|
||||
kwh: kwh,
|
||||
emissions: kwh * EMISSIONS_FACTOR / 1000,
|
||||
};
|
||||
}
|
||||
}
|
||||
} else if (type === 'gas' && keyLower.includes('therm')) {
|
||||
const therms = parseFloat(value.content || '0');
|
||||
|
||||
if (therms !== 0) {
|
||||
const timestamp = extractedDate || new Date();
|
||||
|
||||
timestamp.setHours(0, 0, 0, 0); // Set to midnight
|
||||
|
||||
const existingDataIndex = dataPoints.findIndex(point =>
|
||||
point.timestamp.seconds === timestamp.getTime() / 1000
|
||||
);
|
||||
|
||||
if (existingDataIndex === -1) {
|
||||
dataPoints.push({
|
||||
timestamp: { seconds: timestamp.getTime() / 1000, nanoseconds: 0 },
|
||||
therms: therms,
|
||||
emissions: therms * 5.3 / 1000, // approx CO2 emissions for natural gas (5.3 kg CO2 per therm, measured in tons)
|
||||
});
|
||||
} else {
|
||||
dataPoints[existingDataIndex] = {
|
||||
...dataPoints[existingDataIndex],
|
||||
therms: therms,
|
||||
emissions: therms * 5.3 / 1000,
|
||||
};
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return dataPoints;
|
||||
};
|
||||
|
||||
const handleExtraction = async () => {
|
||||
setExtractionStatus('loading');
|
||||
try {
|
||||
let newData: any = {};
|
||||
|
||||
if (gasFile) {
|
||||
const gasData = await extractDataFromPDF(gasFile, 'gas');
|
||||
|
||||
console.log("Gas data:");
|
||||
gasData.forEach(dataPoint => {
|
||||
console.log("Date:", new Date(dataPoint.timestamp.seconds * 1000).toLocaleDateString(), "Therms:", (dataPoint as NaturalGasDataPoint).therms);
|
||||
});
|
||||
newData.naturalGasUsage = gasData;
|
||||
}
|
||||
|
||||
if (electricityFile) {
|
||||
const electricityData = await extractDataFromPDF(electricityFile, 'electricity');
|
||||
|
||||
console.log("Electricity data:");
|
||||
electricityData.forEach(dataPoint => {
|
||||
console.log("Date:", new Date(dataPoint.timestamp.seconds * 1000).toLocaleDateString(), "kWh:", (dataPoint as ElectricityDataPoint).kwh);
|
||||
});
|
||||
newData.electricityUsage = electricityData;
|
||||
}
|
||||
|
||||
setDataPreview(newData);
|
||||
setExtractionStatus('complete');
|
||||
|
||||
// Update the building data
|
||||
updateBuilding(newData);
|
||||
} catch (error) {
|
||||
console.error("Error during extraction:", error);
|
||||
setExtractionStatus('idle');
|
||||
}
|
||||
};
|
||||
|
||||
const handleAIExtraction = async () => {
|
||||
setAiExtractionStatus('loading');
|
||||
try {
|
||||
let newData: any = {};
|
||||
|
||||
if (gasFile) {
|
||||
const gasData = await extractDataUsingAI(gasFile, 'gas');
|
||||
newData.naturalGasUsage = gasData;
|
||||
}
|
||||
|
||||
if (electricityFile) {
|
||||
const electricityData = await extractDataUsingAI(electricityFile, 'electricity');
|
||||
newData.electricityUsage = electricityData;
|
||||
}
|
||||
|
||||
setDataPreview(newData);
|
||||
setAiExtractionStatus('complete');
|
||||
|
||||
// Update the building data
|
||||
updateBuilding(newData);
|
||||
} catch (error) {
|
||||
console.error("Error during AI extraction:", error);
|
||||
setAiExtractionStatus('idle');
|
||||
}
|
||||
};
|
||||
|
||||
const extractDataUsingAI = async (file: File, type: 'gas' | 'electricity') => {
|
||||
// Step 1: Convert PDF to image
|
||||
const formData = new FormData();
|
||||
|
||||
formData.append('pdf', file, file.name);
|
||||
formData.append('type', type);
|
||||
|
||||
const pdfToImageResponse = await fetch('/api/pdf-to-image', {
|
||||
method: 'POST',
|
||||
body: formData,
|
||||
});
|
||||
|
||||
if (!pdfToImageResponse.ok) {
|
||||
throw new Error('Failed to convert PDF to image');
|
||||
}
|
||||
|
||||
const { response } = await pdfToImageResponse.json();
|
||||
console.log("PDF TO IMAGE RESPONSE", response);
|
||||
|
||||
// Parse the JSON response
|
||||
const parsedData: string = response.response;
|
||||
|
||||
//Trim the string to remove the "anything before first {" and "and after last }"
|
||||
const trimmedData = parsedData.replace(/^[^{]*|[^}]*$/g, '');
|
||||
|
||||
const parsedTrimmedData = JSON.parse(trimmedData);
|
||||
console.log("PARSED TRIMMED DATA", parsedTrimmedData);
|
||||
|
||||
// Convert the parsed data to the format expected by the application
|
||||
return parsedTrimmedData.dataPoints.map((point: any) => ({
|
||||
timestamp: {
|
||||
seconds: new Date(point.date).getTime() / 1000,
|
||||
nanoseconds: 0
|
||||
},
|
||||
[type === 'gas' ? 'therms' : 'kwh']: point.usage,
|
||||
emissions: point.usage * (type === 'gas' ? 5.3 : EMISSIONS_FACTOR) / 1000,
|
||||
}));
|
||||
};
|
||||
|
||||
return (
|
||||
<Modal backdrop="blur" isOpen={isOpen} size="xl" onClose={onClose}>
|
||||
<ModalContent>
|
||||
{!isSubmitted ? (
|
||||
<>
|
||||
<ModalHeader>Upload New Data</ModalHeader>
|
||||
<ModalBody>
|
||||
<div className="flex space-x-6">
|
||||
<div
|
||||
aria-label="Upload gas data"
|
||||
className="w-full h-40 border-2 border-dashed border-gray-300 rounded-lg flex items-center justify-center cursor-pointer"
|
||||
role="button"
|
||||
tabIndex={0}
|
||||
onClick={() => document.getElementById('gas-upload')?.click()}
|
||||
onDragOver={(e) => e.preventDefault()}
|
||||
onDrop={(e) => {
|
||||
e.preventDefault();
|
||||
const file = e.dataTransfer.files[0];
|
||||
|
||||
if (file) handleFileUpload('gas', file);
|
||||
}}
|
||||
onKeyDown={(e) => {
|
||||
if ((e.key === 'Enter' || e.key === ' ') && gasFile) {
|
||||
e.preventDefault();
|
||||
}
|
||||
}}
|
||||
>
|
||||
<p className="text-center p-4">Click or drag to upload gas bill PDF</p>
|
||||
<input
|
||||
accept=".pdf"
|
||||
className="hidden"
|
||||
id="gas-upload"
|
||||
type="file"
|
||||
onChange={(e) => {
|
||||
const file = e.target.files?.[0];
|
||||
|
||||
if (file) handleFileUpload('gas', file);
|
||||
}}
|
||||
/>
|
||||
</div>
|
||||
<div
|
||||
className="w-full h-40 border-2 border-dashed border-gray-300 rounded-lg flex items-center justify-center cursor-pointer"
|
||||
role="button"
|
||||
tabIndex={0}
|
||||
onClick={() => document.getElementById('electricity-upload')?.click()}
|
||||
onDragOver={(e) => e.preventDefault()}
|
||||
onDrop={(e) => {
|
||||
e.preventDefault();
|
||||
const file = e.dataTransfer.files[0];
|
||||
|
||||
if (file) handleFileUpload('electricity', file);
|
||||
}}
|
||||
onKeyDown={(e) => {
|
||||
if ((e.key === 'Enter' || e.key === ' ') && electricityFile) {
|
||||
e.preventDefault();
|
||||
}
|
||||
}}
|
||||
>
|
||||
<p className="text-center p-4">Click or drag to upload electricity bill PDF</p>
|
||||
<input
|
||||
accept=".pdf"
|
||||
className="hidden"
|
||||
id="electricity-upload"
|
||||
type="file"
|
||||
onChange={(e) => {
|
||||
const file = e.target.files?.[0];
|
||||
|
||||
if (file) handleFileUpload('electricity', file);
|
||||
}}
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
</ModalBody>
|
||||
<ModalFooter />
|
||||
</>
|
||||
) : (
|
||||
<>
|
||||
<ModalHeader>Data Uploaded</ModalHeader>
|
||||
<ModalBody>
|
||||
<div className="flex flex-col items-center">
|
||||
<p className="text-4xl">✅</p>
|
||||
<p className="text-center mt-4">
|
||||
Your file has been successfully uploaded! Please wait while we extract the data.
|
||||
</p>
|
||||
{(gasFile || electricityFile) && (
|
||||
<Accordion className="w-full mt-4">
|
||||
<AccordionItem key="1" aria-label="File Preview" title="File Preview">
|
||||
{gasFile && gasFileUrl && (
|
||||
<div>
|
||||
<p>Gas Bill:</p>
|
||||
<p>Name: {gasFile.name}</p>
|
||||
<p>Type: {gasFile.type}</p>
|
||||
<p>Size: {(gasFile.size / 1024).toFixed(2)} KB</p>
|
||||
<embed
|
||||
className="mt-2"
|
||||
height="500px"
|
||||
src={gasFileUrl}
|
||||
type="application/pdf"
|
||||
width="100%"
|
||||
/>
|
||||
</div>
|
||||
)}
|
||||
{electricityFile && electricityFileUrl && (
|
||||
<div className="mt-4">
|
||||
<p>Electricity Bill:</p>
|
||||
<p>Name: {electricityFile.name}</p>
|
||||
<p>Type: {electricityFile.type}</p>
|
||||
<p>Size: {(electricityFile.size / 1024).toFixed(2)} KB</p>
|
||||
<embed
|
||||
className="mt-2"
|
||||
height="500px"
|
||||
src={electricityFileUrl}
|
||||
type="application/pdf"
|
||||
width="100%"
|
||||
/>
|
||||
</div>
|
||||
)}
|
||||
</AccordionItem>
|
||||
<AccordionItem key="2" aria-label="Data Extraction" title="Data Extraction">
|
||||
{extractionStatus === 'idle' && aiExtractionStatus === 'idle' && (
|
||||
<div className="flex space-x-4">
|
||||
<Button
|
||||
color="primary"
|
||||
onPress={handleExtraction}
|
||||
>
|
||||
Start Form Recognizer Extraction
|
||||
</Button>
|
||||
<Button
|
||||
color="secondary"
|
||||
onPress={handleAIExtraction}
|
||||
>
|
||||
Start AI-Powered Extraction
|
||||
</Button>
|
||||
</div>
|
||||
)}
|
||||
{extractionStatus === 'loading' && <p>Extracting data using Form Recognizer...</p>}
|
||||
{aiExtractionStatus === 'loading' && <p>Extracting data using AI...</p>}
|
||||
{extractionStatus === 'complete' && <p>Form Recognizer extraction complete!</p>}
|
||||
{aiExtractionStatus === 'complete' && <p>AI-powered extraction complete!</p>}
|
||||
</AccordionItem>
|
||||
<AccordionItem key="3" aria-label="Data Preview" title="Data Preview">
|
||||
{dataPreview ? (
|
||||
<pre>{JSON.stringify(dataPreview, null, 2)}</pre>
|
||||
) : (
|
||||
<p>No data available. Please complete extraction first.</p>
|
||||
)}
|
||||
</AccordionItem>
|
||||
</Accordion>
|
||||
)}
|
||||
</div>
|
||||
</ModalBody>
|
||||
<ModalFooter>
|
||||
<Button color="primary" onPress={onClose}>
|
||||
Close
|
||||
</Button>
|
||||
</ModalFooter>
|
||||
</>
|
||||
)}
|
||||
</ModalContent>
|
||||
</Modal>
|
||||
);
|
||||
}
|
||||
Reference in New Issue
Block a user