Remodel/packages/web/components/ProfileViewer_backup.tsx
Manohar Gupta 351229b0a9
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
Rewrite ProfileViewer to fix syntax error
2026-05-23 19:03:30 +05:30

632 lines
27 KiB
TypeScript
Raw Blame History

This file contains invisible Unicode characters

This file contains invisible Unicode characters that are indistinguishable to humans but may be processed differently by a computer. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

"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>
)}
</>
);
}