Remove backup file causing build error
Some checks are pending
CI / Engine — lint / typecheck / test (push) Waiting to run
CI / API — lint / typecheck / test (push) Waiting to run
CI / Web — typecheck / lint / build (push) Waiting to run

This commit is contained in:
Manohar Gupta 2026-05-23 19:07:47 +05:30
parent 351229b0a9
commit a850822e0a

View file

@ -1,632 +0,0 @@
"use client";
import { useState, useCallback, useEffect } from "react";
import { useQuery } from "@tanstack/react-query";
import {
BarChart,
Bar,
XAxis,
YAxis,
Tooltip,
ResponsiveContainer,
ReferenceLine,
LineChart,
Line,
CartesianGrid,
Legend,
} from "recharts";
import {
getAvailableProfiles,
getProfileStats,
getSolarProfile,
getWindProfile,
profileCsvUrl,
type ProfileStats,
} from "@/lib/api";
interface ProfileData {
solar: Record<string, number[]>; // { locationId: [8760 values] }
wind: Record<string, number[]>;
stats: Record<string, { avg: number; max: number; min: number; median: number }>;
}
const LOCATIONS = [
{ id: "GJ", name: "Gujarat" },
{ id: "KA", name: "Karnataka" },
{ id: "RJ", name: "Rajasthan" },
];
const SOLAR_COLOR = "#f97316"; // orange-500
const WIND_COLOR = "#3b82f6"; // blue-500
const UPLOAD_COLOR = "#8b5cf6"; // violet-500
interface StatsCardProps {
label: string;
value: string;
color?: string;
}
function StatsCard({ label, value, color }: StatsCardProps) {
return (
<div className="bg-card border rounded-lg p-3 text-center">
<div className="text-xs text-muted-foreground uppercase tracking-wider mb-1">{label}</div>
<div className="text-lg font-semibold tabular-nums" style={{ color: color || "inherit" }}>
{value}
</div>
</div>
);
}
// Custom tooltip for charts
function CustomTooltip({ active, payload, label }: { active?: boolean; payload?: { value: number; name: string }[]; label?: string }) {
if (!active || !payload?.length) return null;
return (
<div className="bg-background border rounded-lg shadow-lg p-2 text-xs">
<p className="font-medium mb-1">{label}</p>
{payload.map((entry, i) => (
<div key={i} className="flex items-center gap-2">
<span
className="w-2 h-2 rounded-full"
style={{ backgroundColor: entry.name === "Solar" ? SOLAR_COLOR : WIND_COLOR }}
/>
<span className="text-muted-foreground">{entry.name}:</span>
<span className="font-medium">{(entry.value * 100).toFixed(2)}%</span>
</div>
))}
</div>
);
}
export function ProfileViewer() {
const [isOpen, setIsOpen] = useState(false);
const [activeTab, setActiveTab] = useState<"solar" | "wind" | "compare">("solar");
const [uploadedProfiles, setUploadedProfiles] = useState<{
solar?: number[];
wind?: number[];
filename?: string;
}>({});
const [isUploading, setIsUploading] = useState(false);
const [uploadError, setUploadError] = useState<string | null>(null);
const [selectedFile, setSelectedFile] = useState<{
name: string;
path: string;
data: string[][];
headers: string[];
} | null>(null);
const [isLoadingFile, setIsLoadingFile] = useState(false);
const API_BASE = typeof window !== 'undefined'
? ((window as any).NEXT_PUBLIC_API_URL || 'http://localhost:8000')
: 'http://localhost:8000';
async function fetchAndDisplayFile(path: string, name: string) {
setIsLoadingFile(true);
try {
const res = await fetch(`${API_BASE}${path}`);
const text = await res.text();
const lines = text.trim().split('\n');
const headers = lines[0].split(',');
const data = lines.slice(1).map(line => line.split(','));
setSelectedFile({ name, path, data, headers });
} catch (err) {
console.error('Failed to fetch file:', err);
} finally {
setIsLoadingFile(false);
}
}
// Calculate stats for a profile
const calculateStats = (data: number[]) => {
const sorted = [...data].sort((a, b) => a - b);
const sum = data.reduce((a, b) => a + b, 0);
return {
avg: sum / data.length,
max: Math.max(...data),
min: Math.min(...data),
median: sorted[Math.floor(sorted.length / 2)],
};
};
// Handle file upload
const handleFileUpload = useCallback(async (event: React.ChangeEvent<HTMLInputElement>) => {
const file = event.target.files?.[0];
if (!file) return;
setIsUploading(true);
setUploadError(null);
try {
const text = await file.text();
const lines = text.trim().split("\n");
if (lines.length < 2) {
throw new Error("File must have at least a header row and one data row");
}
// Try to detect format
const header = lines[0].toLowerCase();
let parsedData: number[] = [];
if (header.includes("%_generation")) {
// New format: find the %_generation column
const cols = header.split(",");
const colIdx = cols.indexOf("%_generation");
if (colIdx === -1) throw new Error("Could not find %_generation column");
parsedData = lines.slice(1).map((line) => {
const vals = line.split(",");
let val = parseFloat(vals[colIdx]);
if (isNaN(val)) return 0;
if (val > 1) val = val / 100; // Convert % to fraction
return val;
});
} else if (header.includes("irradiance") || header.includes("wind_speed") || header.includes("e_grid")) {
// Old format: usually second column
parsedData = lines.slice(1).map((line) => {
const vals = line.split(",");
let val = parseFloat(vals[1]);
if (isNaN(val)) return 0;
// Handle different scales
if (val > 1) val = val / 1000; // Convert kW to MW
if (val > 1) val = val / 100; // Convert % to fraction
return val;
});
} else {
// Generic: just take all numeric values from first column
parsedData = lines.slice(1).map((line) => {
const vals = line.split(",");
let val = parseFloat(vals[0]);
if (isNaN(val)) return 0;
return val;
});
}
// Validate 8760 values
if (parsedData.length !== 8760) {
throw new Error(`Expected 8760 values, got ${parsedData.length}`);
}
// Determine if solar or wind based on average value (solar typically higher during day)
const isSolar = parsedData.reduce((a, b, i) => {
// Solar has high values during midday hours (indices 6-18 of each day)
const hourOfDay = i % 24;
if (hourOfDay >= 6 && hourOfDay <= 18) return a + b;
return a;
}, 0) / parsedData.reduce((a, b) => a + b, 0) > 0.7;
setUploadedProfiles({
solar: isSolar ? parsedData : undefined,
wind: !isSolar ? parsedData : undefined,
filename: file.name,
});
} catch (err) {
setUploadError(err instanceof Error ? err.message : "Failed to parse file");
} finally {
setIsUploading(false);
}
}, []);
// Generate monthly averages for chart
const getMonthlyAverages = (data: number[], isSolar: boolean) => {
const MONTH_DAYS = [31, 28, 31, 30, 31, 30, 31, 31, 30, 31, 30, 31];
const monthNames = ["Apr", "May", "Jun", "Jul", "Aug", "Sep", "Oct", "Nov", "Dec", "Jan", "Feb", "Mar"];
const monthPositions = [4, 5, 6, 7, 8, 9, 10, 11, 12, 1, 2, 3];
return monthNames.map((name, i) => {
const month = monthPositions[i];
const start = MONTH_DAYS.slice(0, month - 1).reduce((a, d) => a + d, 0) * 24;
const days = MONTH_DAYS[month - 1];
const monthData = data.slice(start, start + days * 24);
const avg = monthData.reduce((a, b) => a + b, 0) / monthData.length;
return { name, avg: avg * 100 };
});
};
// Generate hourly average profile for a typical day
const getHourlyAverage = (data: number[]) => {
const hourly = new Array(24).fill(0);
for (let i = 0; i < 8760; i++) {
hourly[i % 24] += data[i];
}
return hourly.map((v) => v / 365);
};
return (
<>
{/* Button to open the modal */}
<button
onClick={() => setIsOpen(true)}
className="inline-flex items-center gap-2 px-3 py-2 text-sm bg-primary text-primary-foreground rounded-lg hover:bg-primary/90 transition-colors"
>
<svg
xmlns="http://www.w3.org/2000/svg"
width="16"
height="16"
viewBox="0 0 24 24"
fill="none"
stroke="currentColor"
strokeWidth="2"
strokeLinecap="round"
strokeLinejoin="round"
>
<path d="M14.5 2H6a2 2 0 0 0-2 2v16a2 2 0 0 0 2 2h12a2 2 0 0 0 2-2V7.5L14.5 2z" />
<polyline points="14 2 14 8 20 8" />
<line x1="16" y1="13" x2="8" y2="13" />
<line x1="16" y1="17" x2="8" y2="17" />
<line x1="10" y1="9" x2="8" y2="9" />
</svg>
View Files
</button>
{/* Modal */}
{isOpen && (
<div className="fixed inset-0 z-50 flex items-center justify-center">
{/* Backdrop */}
<div
className="absolute inset-0 bg-black/50 backdrop-blur-sm"
onClick={() => setIsOpen(false)}
/>
{/* Modal content */}
<div className="relative w-full max-w-5xl max-h-[90vh] mx-4 bg-background rounded-xl shadow-2xl overflow-hidden flex flex-col">
{/* Header */}
<div className="flex items-center justify-between p-4 border-b">
<h2 className="text-lg font-semibold">File Reference Library</h2>
<button
onClick={() => setIsOpen(false)}
className="p-1 hover:bg-accent rounded-md transition-colors"
>
<svg
xmlns="http://www.w3.org/2000/svg"
width="20"
height="20"
viewBox="0 0 24 24"
fill="none"
stroke="currentColor"
strokeWidth="2"
strokeLinecap="round"
strokeLinejoin="round"
>
<line x1="18" y1="6" x2="6" y2="18" />
<line x1="6" y1="6" x2="18" y2="18" />
</svg>
</button>
</div>
{/* Tabs */}
<div className="flex gap-1 p-2 border-b bg-muted/30">
<button
onClick={() => setActiveTab("solar")}
className={`px-4 py-1.5 text-sm rounded-md transition-colors ${
activeTab === "solar"
? "bg-background shadow-sm font-medium"
: "hover:bg-background/50"
}`}
>
Solar
</button>
<button
onClick={() => setActiveTab("wind")}
className={`px-4 py-1.5 text-sm rounded-md transition-colors ${
activeTab === "wind"
? "bg-background shadow-sm font-medium"
: "hover:bg-background/50"
}`}
>
💨 Wind
</button>
<button
onClick={() => setActiveTab("compare")}
className={`px-4 py-1.5 text-sm rounded-md transition-colors ${
activeTab === "compare"
? "bg-background shadow-sm font-medium"
: "hover:bg-background/50"
}`}
>
📊 Compare
</button>
</div>
{/* Upload section */}
<div className="p-4 border-b bg-muted/10">
<div className="flex items-center gap-4">
<label className="flex-1">
<span className="text-sm text-muted-foreground block mb-1">
Upload custom profile (CSV with 8760 hourly values):
</span>
<div className="flex items-center gap-2">
<input
type="file"
accept=".csv"
onChange={handleFileUpload}
disabled={isUploading}
className="hidden"
id="profile-upload"
/>
<label
htmlFor="profile-upload"
className={`px-3 py-1.5 text-sm border rounded-md cursor-pointer hover:bg-accent transition-colors ${
isUploading ? "opacity-50" : ""
}`}
>
{isUploading ? "Parsing..." : "Choose File"}
</label>
{uploadedProfiles.filename && (
<span className="text-sm text-muted-foreground">
{uploadedProfiles.filename} (
{uploadedProfiles.solar ? "Solar" : "Wind"})
</span>
)}
</div>
</label>
{uploadError && (
<span className="text-sm text-destructive">{uploadError}</span>
)}
</div>
</div>
{/* Content */}
<div className="flex-1 overflow-y-auto p-4">
{/* File preview section - ALWAYS SHOWS when file is selected */}
{selectedFile ? (
<div className="mb-6 border rounded-lg overflow-hidden bg-background">
<div className="flex items-center justify-between p-4 bg-blue-50 border-b">
<div className="flex items-center gap-3">
<span className="text-2xl">{selectedFile.name.includes('Solar') ? '☀️' : '💨'}</span>
<div>
<h3 className="text-base font-semibold">{selectedFile.name}</h3>
<p className="text-xs text-muted-foreground">{selectedFile.data.length} rows of data</p>
</div>
</div>
<div className="flex items-center gap-2">
<a
href={`${typeof window !== 'undefined' ? (window as any).NEXT_PUBLIC_API_URL || 'http://localhost:8000' : 'http://localhost:8000'}${selectedFile.path}?as_csv=true`}
download
className="px-3 py-1.5 text-sm bg-primary text-primary-foreground rounded hover:bg-primary/90"
>
Download CSV
</a>
<button
onClick={() => setSelectedFile(null)}
className="px-3 py-1.5 text-sm border rounded hover:bg-accent"
>
Close Preview
</button>
</div>
</div>
<div className="max-h-[400px] overflow-auto bg-background">
<table className="w-full text-xs">
<thead className="bg-muted border-b sticky top-0">
<tr>
<th className="px-4 py-3 text-left font-semibold text-muted-foreground">Hour</th>
<th className="px-4 py-3 text-left font-semibold text-muted-foreground">Date</th>
<th className="px-4 py-3 text-left font-semibold text-muted-foreground">Month</th>
<th className="px-4 py-3 text-left font-semibold text-muted-foreground">Day</th>
<th className="px-4 py-3 text-left font-semibold text-muted-foreground">Hour of Day</th>
<th className="px-4 py-3 text-right font-semibold text-muted-foreground">% Generation</th>
</tr>
</thead>
<tbody>
{selectedFile.data.map((row: string[], i: number) => (
<tr key={i} className="border-b border-border/30 hover:bg-blue-50/50">
<td className="px-4 py-2 font-medium">{row[0]}</td>
<td className="px-4 py-2">{row[1]}</td>
<td className="px-4 py-2">{row[2]}</td>
<td className="px-4 py-2">{row[3]}</td>
<td className="px-4 py-2">{row[4]}</td>
<td className="px-4 py-2 text-right font-semibold text-blue-600">{row[5]}</td>
</tr>
))}
</tbody>
</table>
</div>
</div>
) : (
<div className="space-y-6">
{/* Bundled profiles section - shown when no file selected */}
<div className="mb-6">
<h3 className="text-sm font-semibold mb-4 text-foreground">📂 Click on any file below to view its contents:</h3>
<div className="grid grid-cols-1 md:grid-cols-2 lg:grid-cols-3 gap-4">
{[
{ name: "Solar - Gujarat", path: "/api/profiles/solar/GJ", type: "solar", id: "solar_gj" },
{ name: "Solar - Karnataka", path: "/api/profiles/solar/KA", type: "solar", id: "solar_ka" },
{ name: "Solar - Rajasthan", path: "/api/profiles/solar/RJ", type: "solar", id: "solar_rj" },
{ name: "Wind - Gujarat", path: "/api/profiles/wind/GJ", type: "wind", id: "wind_gj" },
{ name: "Wind - Karnataka", path: "/api/profiles/wind/KA", type: "wind", id: "wind_ka" },
{ name: "Wind - Rajasthan", path: "/api/profiles/wind/RJ", type: "wind", id: "wind_rj" },
].map((profile) => (
<button
key={profile.id}
onClick={() => fetchAndDisplayFile(profile.path, profile.name)}
className="flex items-center gap-3 p-4 border-2 border-dashed border-border rounded-xl hover:border-primary hover:bg-primary/5 transition-all cursor-pointer text-left group"
style={{ cursor: 'pointer' }}
>
<span className="text-2xl group-hover:scale-110 transition-transform">{profile.type === "solar" ? "☀️" : "💨"}</span>
<div className="flex-1">
<div className="text-sm font-semibold group-hover:text-primary transition-colors">{profile.name}</div>
<div className="text-xs text-muted-foreground mt-1">Click to view 8760 hourly values</div>
</div>
<div className="px-2 py-1 bg-primary/10 rounded text-xs font-medium text-primary group-hover:bg-primary group-hover:text-primary-foreground transition-colors">
VIEW
</div>
</button>
))}
</div>
</div>
{/* Charts section - hidden when file is selected */}
{!selectedFile && activeTab === "solar" && (
<div className="space-y-6">
<div>
<h3 className="text-sm font-medium mb-3 text-muted-foreground">
Monthly Average Solar Irradiance
</h3>
<div className="h-64">
<ResponsiveContainer width="100%" height="100%">
<BarChart data={LOCATIONS.map((loc) => {
// Mock data for demonstration - in real app this would come from API
const mockData = Array(8760).fill(0).map((_, i) => {
const hour = i % 24;
const dayOfYear = Math.floor(i / 24);
// Simple sinusoidal approximation
const solarNoon = Math.sin((hour - 6) * Math.PI / 12);
const seasonFactor = Math.sin((dayOfYear - 80) * 2 * Math.PI / 365);
return Math.max(0, solarNoon * (0.8 + 0.2 * seasonFactor));
});
const monthly = getMonthlyAverages(mockData, true);
const idx = ["GJ", "KA", "RJ"].indexOf(loc.id);
return {
name: loc.name,
Jan: monthly[9]?.avg || 0,
Feb: monthly[10]?.avg || 0,
Mar: monthly[11]?.avg || 0,
Apr: monthly[0]?.avg || 0,
May: monthly[1]?.avg || 0,
Jun: monthly[2]?.avg || 0,
Jul: monthly[3]?.avg || 0,
Aug: monthly[4]?.avg || 0,
Sep: monthly[5]?.avg || 0,
Oct: monthly[6]?.avg || 0,
Nov: monthly[7]?.avg || 0,
Dec: monthly[8]?.avg || 0,
[idx]: 0,
};
})}>
<XAxis dataKey="name" tick={{ fontSize: 12 }} />
<YAxis tick={{ fontSize: 12 }} domain={[0, 100]} unit="%" />
<Tooltip content={<CustomTooltip />} />
<ReferenceLine y={20} stroke="#94a3b8" strokeDasharray="3 3" label="20%" />
<Bar dataKey="GJ" fill={SOLAR_COLOR} name="Gujarat" />
<Bar dataKey="KA" fill="#ea580c" name="Karnataka" />
<Bar dataKey="RJ" fill="#c2410c" name="Rajasthan" />
</BarChart>
</ResponsiveContainer>
</div>
</div>
{uploadedProfiles.solar && (
<div>
<h3 className="text-sm font-medium mb-3 text-muted-foreground">
Your Uploaded Solar Profile
</h3>
<div className="grid grid-cols-4 gap-2">
{(() => {
const stats = calculateStats(uploadedProfiles.solar!);
return (
<>
<StatsCard label="Avg CUF" value={`${(stats.avg * 100).toFixed(2)}%`} color={SOLAR_COLOR} />
<StatsCard label="Max" value={`${(stats.max * 100).toFixed(1)}%`} />
<StatsCard label="Min" value={`${(stats.min * 100).toFixed(1)}%`} />
<StatsCard label="Median" value={`${(stats.median * 100).toFixed(1)}%`} />
</>
);
})()}
</div>
</div>
)}
</div>
)}
{activeTab === "wind" && (
<div className="space-y-6">
<div>
<h3 className="text-sm font-medium mb-3 text-muted-foreground">
Monthly Average Wind Speed (m/s)
</h3>
<div className="h-64">
<ResponsiveContainer width="100%" height="100%">
<BarChart data={[
{ name: "Gujarat", avg: 6.5, max: 12, min: 3 },
{ name: "Karnataka", avg: 7.2, max: 14, min: 4 },
{ name: "Rajasthan", avg: 5.8, max: 11, min: 2 },
]}>
<XAxis dataKey="name" tick={{ fontSize: 12 }} />
<YAxis tick={{ fontSize: 12 }} />
<Tooltip />
<Bar dataKey="avg" fill={WIND_COLOR} name="Avg Speed" />
</BarChart>
</ResponsiveContainer>
</div>
</div>
{uploadedProfiles.wind && (
<div>
<h3 className="text-sm font-medium mb-3 text-muted-foreground">
Your Uploaded Wind Profile
</h3>
<div className="grid grid-cols-4 gap-2">
{(() => {
const stats = calculateStats(uploadedProfiles.wind!);
return (
<>
<StatsCard label="Avg Speed" value={`${(stats.avg * 10).toFixed(2)} m/s`} color={WIND_COLOR} />
<StatsCard label="Max" value={`${(stats.max * 10).toFixed(1)} m/s`} />
<StatsCard label="Min" value={`${(stats.min * 10).toFixed(1)} m/s`} />
<StatsCard label="Median" value={`${(stats.median * 10).toFixed(1)} m/s`} />
</>
);
})()}
</div>
</div>
)}
</div>
)}
{!selectedFile && activeTab === "compare" && (
<div className="space-y-6">
<div>
<h3 className="text-sm font-medium mb-3 text-muted-foreground">
Solar vs Wind CUF (Typical Daily Pattern)
</h3>
<div className="h-64">
<ResponsiveContainer width="100%" height="100%">
<LineChart data={getHourlyAverage(
Array(8760).fill(0).map((_, i) => {
const hour = i % 24;
const solarValue = Math.max(0, Math.sin((hour - 6) * Math.PI / 12));
const windValue = 0.3 + 0.2 * Math.sin(i * Math.PI / 12) + 0.1 * Math.random();
return (solarValue + windValue) / 2;
})
).map((v, i) => ({ hour: `${String(i).padStart(2, '0')}:00`, Solar: v, Wind: 0.3 + 0.1 * Math.sin(i * Math.PI / 12) }))}>
<CartesianGrid strokeDasharray="3 3" stroke="#94a3b8" />
<XAxis dataKey="hour" tick={{ fontSize: 10 }} interval={3} />
<YAxis tick={{ fontSize: 12 }} domain={[0, 1]} tickFormatter={(v) => `${(v * 100).toFixed(0)}%`} />
<Tooltip content={<CustomTooltip />} />
<Line type="monotone" dataKey="Solar" stroke={SOLAR_COLOR} dot={false} name="Solar" />
<Line type="monotone" dataKey="Wind" stroke={WIND_COLOR} dot={false} name="Wind" />
<Legend />
</LineChart>
</ResponsiveContainer>
</div>
</div>
{uploadedProfiles.solar && uploadedProfiles.wind && (
<div>
<h3 className="text-sm font-medium mb-3 text-muted-foreground">
Your Uploaded Profiles Comparison
</h3>
<div className="grid grid-cols-4 gap-2">
{(() => {
const solarStats = calculateStats(uploadedProfiles.solar!);
const windStats = calculateStats(uploadedProfiles.wind!);
return (
<>
<StatsCard label="Solar CUF" value={`${(solarStats.avg * 100).toFixed(2)}%`} color={SOLAR_COLOR} />
<StatsCard label="Wind CUF" value={`${(windStats.avg * 100).toFixed(2)}%`} color={WIND_COLOR} />
<StatsCard label="Combined" value={`${((solarStats.avg + windStats.avg) * 50).toFixed(2)}%`} />
<StatsCard label="Upload" value={uploadedProfiles.filename || "-"} />
</>
);
})()}
</div>
</div>
)}
</div>
)}
</div>
</div>
</div>
)}
</>
);
}