diff --git a/apps/opik-frontend/src/api/debug/useIsAlive.ts b/apps/opik-frontend/src/api/debug/useIsAlive.ts new file mode 100644 index 00000000000..ad44a3a0e82 --- /dev/null +++ b/apps/opik-frontend/src/api/debug/useIsAlive.ts @@ -0,0 +1,42 @@ +import api from "@/api/api"; +import { useQuery } from "@tanstack/react-query"; + +const PING_FETCHING_TIMEOUT_SECONDS = 5; +const CONNECTED_PING_REFETCH_INTERVAL_SECONDS = 10; +const DISCONNECTED_PING_REFETCH_INTERVAL_SECONDS = 5; + +interface IsAlivePingResponse { + healthy: boolean; + rtt: number; +} + +const getPing = async (): Promise => { + const startTime = performance.now(); + + const { data } = await api.get("/is-alive/ping", { + timeout: PING_FETCHING_TIMEOUT_SECONDS * 1000, + }); + + const endTime = performance.now(); + + const rtt = endTime - startTime; + + return { ...data, rtt }; +}; + +export const usePingBackend = (isNetworkOnline: boolean) => + useQuery({ + queryKey: ["backend-ping"], + queryFn: getPing, + enabled: isNetworkOnline, + retryDelay: 1000, + refetchInterval: (query) => { + const { error: isError, data } = query.state; + const isConnected = !isError && data?.healthy; + + return isConnected + ? CONNECTED_PING_REFETCH_INTERVAL_SECONDS * 1000 + : DISCONNECTED_PING_REFETCH_INTERVAL_SECONDS * 1000; + }, + refetchOnReconnect: true, + }); diff --git a/apps/opik-frontend/src/components/layout/AppDebugInfo/AppDebugInfo.tsx b/apps/opik-frontend/src/components/layout/AppDebugInfo/AppDebugInfo.tsx new file mode 100644 index 00000000000..4e704cce742 --- /dev/null +++ b/apps/opik-frontend/src/components/layout/AppDebugInfo/AppDebugInfo.tsx @@ -0,0 +1,58 @@ +import { useState } from "react"; +import { useHotkeys } from "react-hotkeys-hook"; +import { toast } from "@/components/ui/use-toast"; +import { APP_VERSION } from "@/constants/app"; +import AppNetworkStatus from "@/components/layout/AppNetworkStatus/AppNetworkStatus"; +import OpikIcon from "@/icons/opik.svg?react"; +import copy from "clipboard-copy"; +import { Copy } from "lucide-react"; +import { modifierKey } from "@/lib/utils"; + +const DEBUGGER_MODE_KEY = "comet-debugger-mode"; // Same key used in EM for consistency + +const AppDebugInfo = () => { + const [showAppDebugInfo, setShowAppDebugInfo] = useState( + () => localStorage.getItem(DEBUGGER_MODE_KEY) === "true", + ); + + // Keyboard shortcut handler for debugger mode: Meta/Ctrl + Shift + . (all pressed simultaneously) + useHotkeys(`${modifierKey}+shift+period`, (keyboardEvent: KeyboardEvent) => { + keyboardEvent.preventDefault(); + keyboardEvent.stopPropagation(); + + // Toggle debugger mode + setShowAppDebugInfo((prev) => { + const newValue = !prev; + localStorage.setItem(DEBUGGER_MODE_KEY, String(newValue)); + return newValue; + }); + }); + + return ( + showAppDebugInfo && ( + <> +
+ +
+ + {APP_VERSION && ( +
{ + copy(APP_VERSION); + toast({ description: "Successfully copied version" }); + }} + > + + + OPIK VERSION {APP_VERSION} + + +
+ )} + + ) + ); +}; + +export default AppDebugInfo; diff --git a/apps/opik-frontend/src/components/layout/AppNetworkStatus/AppNetworkStatus.tsx b/apps/opik-frontend/src/components/layout/AppNetworkStatus/AppNetworkStatus.tsx new file mode 100644 index 00000000000..861a63b2273 --- /dev/null +++ b/apps/opik-frontend/src/components/layout/AppNetworkStatus/AppNetworkStatus.tsx @@ -0,0 +1,73 @@ +import { cn } from "@/lib/utils"; + +import OpikIcon from "@/icons/opik.svg?react"; +import { usePingBackend } from "@/api/debug/useIsAlive"; +import useIsNetworkOnline from "@/hooks/useIsNetworkOnline"; +import { WifiOffIcon, WifiIcon, SatelliteDishIcon } from "lucide-react"; +import TooltipWrapper from "@/components/shared/TooltipWrapper/TooltipWrapper"; + +const AppNetworkStatus = () => { + const isNetworkOnline = useIsNetworkOnline(); + const { data: pingResponse, isError } = usePingBackend(isNetworkOnline); + const rtt = pingResponse?.rtt; + const rttInSeconds = rtt ? (rtt / 1000).toFixed(2) : null; + const isConnectedToBackend = + isNetworkOnline && !isError && pingResponse?.healthy; + + return ( +
+ {isConnectedToBackend && ( +
+ + + RTT: {rttInSeconds}s + +
+ )} + {isNetworkOnline && ( +
+
+ + + + + +
+ )} +
+
+ + {isNetworkOnline ? ( + + ) : ( + + )} + +
+
+ ); +}; + +export default AppNetworkStatus; diff --git a/apps/opik-frontend/src/components/layout/PartialPageLayout/PartialPageLayout.tsx b/apps/opik-frontend/src/components/layout/PartialPageLayout/PartialPageLayout.tsx index 7a6f2db5a7c..37f0757da20 100644 --- a/apps/opik-frontend/src/components/layout/PartialPageLayout/PartialPageLayout.tsx +++ b/apps/opik-frontend/src/components/layout/PartialPageLayout/PartialPageLayout.tsx @@ -3,6 +3,7 @@ import useAppStore from "@/store/AppStore"; import usePluginsStore from "@/store/PluginsStore"; import { Link, Outlet } from "@tanstack/react-router"; import Logo from "@/components/layout/Logo/Logo"; +import AppDebugInfo from "@/components/layout/AppDebugInfo/AppDebugInfo"; import ThemeToggle from "@/components/layout/ThemeToggle/ThemeToggle"; export const PartialPageLayout = ({ @@ -30,6 +31,7 @@ export const PartialPageLayout = ({
+ {UserMenu ? : } diff --git a/apps/opik-frontend/src/components/layout/TopBar/TopBar.tsx b/apps/opik-frontend/src/components/layout/TopBar/TopBar.tsx index c681149bb36..5451dd12bc5 100644 --- a/apps/opik-frontend/src/components/layout/TopBar/TopBar.tsx +++ b/apps/opik-frontend/src/components/layout/TopBar/TopBar.tsx @@ -1,6 +1,6 @@ -import React from "react"; import Breadcrumbs from "@/components/layout/Breadcrumbs/Breadcrumbs"; import usePluginsStore from "@/store/PluginsStore"; +import AppDebugInfo from "@/components/layout/AppDebugInfo/AppDebugInfo"; import ThemeToggle from "../ThemeToggle/ThemeToggle"; const TopBar = () => { @@ -12,6 +12,7 @@ const TopBar = () => {
+ {UserMenu ? : } ); diff --git a/apps/opik-frontend/src/components/pages/SMEFlowPage/hotkeys.ts b/apps/opik-frontend/src/components/pages/SMEFlowPage/hotkeys.ts index 39f84b96006..5edf0844e7a 100644 --- a/apps/opik-frontend/src/components/pages/SMEFlowPage/hotkeys.ts +++ b/apps/opik-frontend/src/components/pages/SMEFlowPage/hotkeys.ts @@ -1,3 +1,5 @@ +import { modifierKey, isMac } from "@/lib/utils"; + export enum SME_ACTION { PREVIOUS = "previous", NEXT = "next", @@ -7,11 +9,6 @@ export enum SME_ACTION { FOCUS_FEEDBACK_SCORES = "focus_feedback_scores", } -const isMac = - typeof navigator !== "undefined" && - navigator.platform.toUpperCase().indexOf("MAC") >= 0; -const modifierKey = isMac ? "meta" : "ctrl"; - export const SME_HOTKEYS = { [SME_ACTION.PREVIOUS]: { key: "p", diff --git a/apps/opik-frontend/src/components/ui/switch.tsx b/apps/opik-frontend/src/components/ui/switch.tsx index 80868cf1d41..af960ff0efe 100644 --- a/apps/opik-frontend/src/components/ui/switch.tsx +++ b/apps/opik-frontend/src/components/ui/switch.tsx @@ -11,6 +11,7 @@ const switchVariants = cva( size: { default: "h-6 w-11", sm: "h-5 w-9", + xs: "h-4 w-7", }, }, defaultVariants: { @@ -27,6 +28,7 @@ const switchThumbVariants = cva( default: "size-5 data-[state=checked]:translate-x-5 data-[state=unchecked]:translate-x-0", sm: "size-4 data-[state=checked]:translate-x-4 data-[state=unchecked]:translate-x-0", + xs: "size-3 data-[state=checked]:translate-x-3 data-[state=unchecked]:translate-x-0", }, }, defaultVariants: { diff --git a/apps/opik-frontend/src/hooks/useIsNetworkOnline.ts b/apps/opik-frontend/src/hooks/useIsNetworkOnline.ts new file mode 100644 index 00000000000..49e64939210 --- /dev/null +++ b/apps/opik-frontend/src/hooks/useIsNetworkOnline.ts @@ -0,0 +1,21 @@ +import { useState, useEffect } from "react"; + +const useIsNetworkOnline = () => { + const [isNetworkOnline, setIsNetworkOnline] = useState(navigator.onLine); + + useEffect(() => { + const updateNetworkStatus = () => setIsNetworkOnline(navigator.onLine); + + window.addEventListener("online", updateNetworkStatus); + window.addEventListener("offline", updateNetworkStatus); + + return () => { + window.removeEventListener("online", updateNetworkStatus); + window.removeEventListener("offline", updateNetworkStatus); + }; + }, []); + + return isNetworkOnline; +}; + +export default useIsNetworkOnline; diff --git a/apps/opik-frontend/src/icons/comet.svg b/apps/opik-frontend/src/icons/comet.svg new file mode 100644 index 00000000000..f289c18d2a1 --- /dev/null +++ b/apps/opik-frontend/src/icons/comet.svg @@ -0,0 +1 @@ + \ No newline at end of file diff --git a/apps/opik-frontend/src/icons/opik.svg b/apps/opik-frontend/src/icons/opik.svg new file mode 100644 index 00000000000..1b3a57c1cad --- /dev/null +++ b/apps/opik-frontend/src/icons/opik.svg @@ -0,0 +1,9 @@ + + + + + + + + + diff --git a/apps/opik-frontend/src/lib/utils.ts b/apps/opik-frontend/src/lib/utils.ts index 36babbe97cc..a997f0afd72 100644 --- a/apps/opik-frontend/src/lib/utils.ts +++ b/apps/opik-frontend/src/lib/utils.ts @@ -238,3 +238,9 @@ export const updateTextAreaHeight = ( export const capitalizeFirstLetter = (str?: string | null) => str ? str.charAt(0).toUpperCase() + str.slice(1) : ""; + +export const isMac = + typeof navigator !== "undefined" && + navigator.platform.toUpperCase().includes("MAC"); + +export const modifierKey = isMac ? "meta" : "ctrl"; diff --git a/apps/opik-frontend/src/plugins/comet/UserMenu.tsx b/apps/opik-frontend/src/plugins/comet/UserMenu.tsx index b6d8cef38a3..767e28af89a 100644 --- a/apps/opik-frontend/src/plugins/comet/UserMenu.tsx +++ b/apps/opik-frontend/src/plugins/comet/UserMenu.tsx @@ -417,7 +417,7 @@ const UserMenu = () => { Logout - {APP_VERSION ? ( + {APP_VERSION && ( <> { - ) : null} + )} );