Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
3 changes: 3 additions & 0 deletions packages/client/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -22,8 +22,10 @@
"@react-three/fiber": "9.0.0-alpha.8",
"@react-three/postprocessing": "^2.16.0",
"@shared/helios-types": "*",
"@tanstack/react-query": "^5.90.20",
"@tanstack/react-table": "^8.20.5",
"@types/three": "^0.161.2",
"axios": "^1.13.4",
"chart.js": "^4.4.8",
"dayjs": "^1.11.13",
"framer-motion": "^11.0.6",
Expand All @@ -44,6 +46,7 @@
"three": "^0.161.0"
},
"devDependencies": {
"@tanstack/react-query-devtools": "^5.91.3",
"@trivago/prettier-plugin-sort-imports": "^4.3.0",
"@types/eslint": "^8.56.10",
"@types/google.maps": "^3.55.11",
Expand Down
105 changes: 59 additions & 46 deletions packages/client/src/components/containers/MLContainer.tsx
Original file line number Diff line number Diff line change
@@ -1,73 +1,86 @@
"use client";

import { useTheme } from "next-themes";
import { useEffect, useState } from "react";
import type { PlotParams } from "react-plotly.js";

import { API_ROUTES } from "@/constants/apiRoutes";
import useWindowDimensions from "@/hooks/PIS/useWindowDimensions";
import {
type PlotTypes,
useMLCorrelationMatrix,
} from "@/hooks/useMLCorrelationMatrix";

import Plotly from "./Plotly";

const SMALL_SCREEN = 380;

type PlotTypes =
| "/api/getPacketCorrelationMatrix"
| "/api/getLapCorrelationMatrix";

export default function MLContainer({
plotType = "/api/getPacketCorrelationMatrix",
plotType = API_ROUTES.ml.packetCorrelationMatrix,
}: {
plotType?: PlotTypes;
}) {
const [plot, setPlot] = useState<PlotParams | { error: boolean }>();
const { width } = useWindowDimensions();
const { resolvedTheme } = useTheme();

useEffect(() => {
const fetchPlot = async () => {
try {
const response = await fetch(plotType);
const graph = await response.json();
const data = JSON.parse(graph) as PlotParams;
const layout: PlotParams["layout"] = {
autosize: true,
font: {
color: resolvedTheme
? resolvedTheme === "dark"
? "white"
: "black"
: "black",
},
margin: { l: 175, t: 75 },
paper_bgcolor: "rgba(0,0,0,0)",
title: data.layout.title,
};
data.layout = layout;

setPlot(data);
} catch (e) {
setPlot({ error: true });
}
};
fetchPlot();
}, [plotType, resolvedTheme]);
// Fetch and transform correlation matrix data using TanStack Query
// The hook handles both fetching and theme-aware layout transformation
const {
data: plotData,
error,
isFetching,
isLoading,
refetch,
} = useMLCorrelationMatrix({
plotType,
});

if (!plot || !resolvedTheme || resolvedTheme === undefined) {
// Loading state
if (isLoading) {
return (
<div className="flex h-72 items-center justify-center rounded-lg bg-white p-2 text-3xl font-bold dark:bg-arsenic dark:text-white">
Loading...
{isFetching ? "Fetching data..." : "Loading..."}
</div>
);
}
if ("error" in plot || !resolvedTheme || resolvedTheme === undefined) {

// Error state with retry button
if (error) {
return (
<div className="flex h-72 flex-col items-center justify-center gap-4 rounded-lg bg-white p-2 dark:bg-arsenic dark:text-white">
<div className="text-xl font-bold text-red-600 dark:text-red-400">
Error loading data
</div>
<div className="text-gray-600 dark:text-gray-400 text-sm">
{error instanceof Error ? error.message : "Unknown error occurred"}
</div>
<button
className="rounded-lg bg-blue-600 px-4 py-2 text-sm font-semibold text-white transition-colors hover:bg-blue-700 dark:bg-blue-500 dark:hover:bg-blue-600"
onClick={() => refetch()}
>
Retry
</button>
</div>
);
}

// No data state (shouldn't happen if query is enabled correctly)
if (!plotData) {
return (
<div className="flex h-72 items-center justify-center rounded-lg bg-white p-2 text-3xl font-bold dark:bg-arsenic dark:text-white">
Error loading data
No data available
</div>
);
}

// Success state - render the plot
return (
<div className="flex h-72 items-center gap-4 overflow-x-auto overflow-y-hidden rounded-lg bg-white p-2 text-3xl font-bold dark:bg-arsenic dark:text-white sm:justify-center">
<div className="relative flex h-72 items-center gap-4 overflow-x-auto overflow-y-hidden rounded-lg bg-white p-2 text-3xl font-bold dark:bg-arsenic dark:text-white sm:justify-center">
{/* Refresh button - positioned in top-right corner */}
<button
className="text-gray-700 dark:text-gray-200 absolute right-2 top-2 z-10 rounded-md bg-gray-200 px-3 py-1 text-xs font-semibold transition-colors hover:bg-gray-300 dark:bg-gray-700 dark:hover:bg-gray-600"
disabled={isFetching}
onClick={() => refetch()}
title="Refresh data"
>
{isFetching ? "Refreshing..." : "Refresh"}
</button>

<Plotly
className="h-72 w-max bg-inherit"
config={{
Expand All @@ -82,8 +95,8 @@ export default function MLContainer({
scrollZoom: true,
staticPlot: width < SMALL_SCREEN,
}}
data={plot.data}
layout={plot.layout}
data={plotData.data}
layout={plotData.layout}
/>
</div>
);
Expand Down
97 changes: 97 additions & 0 deletions packages/client/src/constants/apiRoutes.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,97 @@
/**
* API Route Constants for Helios Telemetry Client
*
* This file defines all API endpoints used in the application in a structured format.
* Routes are organized by feature/domain for better maintainability.
*
* Usage:
* ```typescript
* import { API_ROUTES } from '@/constants/apiRoutes';
* const response = await api.get(API_ROUTES.ml.packetCorrelationMatrix);
* ```
*/

/**
* Next.js API Routes (client-side proxies)
* These routes are handled by Next.js API handlers in /pages/api/
*/
export const API_ROUTES = {
/**
* Authentication/Security endpoints
*/
auth: {
/** Check MQTT password for driver updates */
checkMQTTPassword: "/api/checkMQTTPassword",
},

/**
* Health check endpoint
*/
health: {
/** Basic health check */
hello: "/api/hello",
},

/**
* Machine Learning endpoints
*/
ml: {
/** Get lap correlation matrix data */
lapCorrelationMatrix: "/api/getLapCorrelationMatrix",
/** Get packet correlation matrix data */
packetCorrelationMatrix: "/api/getPacketCorrelationMatrix",
},
} as const;

/**
* Backend API Routes (direct server calls)
* These routes connect directly to the backend server (prodURL)
* Used when bypassing Next.js API routes
*/
export const BACKEND_ROUTES = {
/**
* Driver endpoints
*/
drivers: {
/** Get all drivers */
base: "/drivers",
/** Get driver by RFID */
byRfid: (rfid: number) => `/driver/${rfid}`,
},

/**
* Lap data endpoints
*/
laps: {
/** Get all laps */
base: "/laps",
},

/**
* Machine Learning endpoints (backend)
*/
ml: {
/** ML health check */
health: "/ml/health",
/** Invalidate ML cache */
invalidateCache: "/ml/invalidate",
/** Get lap correlation matrix */
lapCorrelationMatrix: "/ml/correlation-matrix/lap",
/** Get packet correlation matrix */
packetCorrelationMatrix: "/ml/correlation-matrix/packet",
},

/**
* Playback endpoints
*/
playback: {
/** Get packets between time range */
packetsBetween: "/packetsBetween",
},
} as const;

/**
* Type helper to extract route values
*/
export type ApiRoute = (typeof API_ROUTES)[keyof typeof API_ROUTES];
export type BackendRoute = (typeof BACKEND_ROUTES)[keyof typeof BACKEND_ROUTES];
100 changes: 100 additions & 0 deletions packages/client/src/hooks/useMLCorrelationMatrix.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,100 @@
import { useTheme } from "next-themes";
import { useMemo } from "react";
import type { PlotParams } from "react-plotly.js";

import { API_ROUTES } from "@/constants/apiRoutes";
import { api } from "@/lib/api";
import { useQuery } from "@tanstack/react-query";

export type PlotTypes =
| typeof API_ROUTES.ml.packetCorrelationMatrix
| typeof API_ROUTES.ml.lapCorrelationMatrix;

interface UseMLCorrelationMatrixOptions {
plotType: PlotTypes;
}

/**
* Fetches correlation matrix data from the API with a 30-second timeout.
*
* Uses the configured axios instance from @/lib/api which includes:
* - 30-second timeout to prevent hanging requests
* - Standard JSON headers
* - Centralized error handling
*
* @param plotType - The API endpoint to fetch from
* @returns Promise resolving to PlotParams data
* @throws Error if the request fails or times out
*/
async function fetchCorrelationMatrix(
plotType: PlotTypes,
): Promise<PlotParams> {
const response = await api.get<string>(plotType);

// Parse the JSON string response
const rawData = JSON.parse(response.data) as PlotParams;

return rawData;
}

/**
* Custom hook to fetch and cache ML correlation matrix data using TanStack Query.
*
* Features:
* - 1-hour cache TTL (matches backend cache)
* - Automatic retry on failure (3 attempts)
* - No refetch on window focus (expensive ML data)
* - Theme-aware layout transformation (doesn't refetch on theme change)
* - 30-second timeout to prevent hanging requests
*
* @param options - Configuration options
* @param options.plotType - The API endpoint to fetch from
* @returns Query result with theme-transformed plot data
*/
export function useMLCorrelationMatrix({
plotType,
}: UseMLCorrelationMatrixOptions) {
const { resolvedTheme } = useTheme();
// Fetch raw data from API
const query = useQuery({
// Only enable query when theme is resolved
// This prevents unnecessary fetches before theme is ready
enabled: !!resolvedTheme,

// Fetch function - uses axios with 30s timeout
queryFn: () => fetchCorrelationMatrix(plotType),

// Query key: ['ml', 'correlation-matrix', plotType]
// Note: theme is NOT in the key to avoid separate cache entries per theme
queryKey: ["ml", "correlation-matrix", plotType] as const,

// Throw errors to error boundary (optional, can be removed if you prefer error state)
throwOnError: false,
});
// Transform layout based on current theme (memoized to avoid unnecessary recalculations)
const transformedData = useMemo(() => {
if (!query.data) return null;

const layout: PlotParams["layout"] = {
autosize: true,
font: {
color: resolvedTheme === "dark" ? "white" : "black",
},
margin: { l: 175, t: 75 },
paper_bgcolor: "rgba(0,0,0,0)",
title: query.data.layout.title,
};

return {
...query.data,
layout,
};
}, [query.data, resolvedTheme]);

// Return query state with transformed data
return {
...query,
data: transformedData,
isLoading: query.isLoading || !resolvedTheme,
};
}
Loading