From de51c1b18de74c7c8329a0ee07820b59c93dd034 Mon Sep 17 00:00:00 2001 From: "andrii.dudar" Date: Wed, 4 Sep 2024 11:46:11 +0200 Subject: [PATCH] OPIK-20 [UX] Add simple keyboard shortcuts (#178) --- apps/opik-frontend/package-lock.json | 10 +++ apps/opik-frontend/package.json | 1 + .../DatasetCompareExperimentsPage.tsx | 10 ++- .../DatasetCompareExperimentsPanel.tsx | 4 + .../DatasetItemsPage/DatasetItemsPage.tsx | 1 + .../ResizableSidePanel/ResizableSidePanel.tsx | 90 +++++++++++++------ .../shared/TooltipWrapper/TooltipWrapper.tsx | 9 +- .../TraceAnnotateViewer/AnnotateRow.tsx | 4 +- .../TraceAnnotateViewer.tsx | 35 ++++++-- .../TraceDetailsPanel/TraceDetailsPanel.tsx | 2 + .../src/components/ui/tooltip.tsx | 31 +++++-- 11 files changed, 150 insertions(+), 47 deletions(-) diff --git a/apps/opik-frontend/package-lock.json b/apps/opik-frontend/package-lock.json index 71fa11f4f4..c6bbb686ea 100644 --- a/apps/opik-frontend/package-lock.json +++ b/apps/opik-frontend/package-lock.json @@ -47,6 +47,7 @@ "react-complex-tree": "^2.4.4", "react-day-picker": "^8.10.1", "react-dom": "^18.3.1", + "react-hotkeys-hook": "^4.5.1", "react-resizable-panels": "^2.0.20", "react18-json-view": "^0.2.8", "stylelint-scss": "^6.4.1", @@ -10559,6 +10560,15 @@ "react": "^18.3.1" } }, + "node_modules/react-hotkeys-hook": { + "version": "4.5.1", + "resolved": "https://registry.npmjs.org/react-hotkeys-hook/-/react-hotkeys-hook-4.5.1.tgz", + "integrity": "sha512-scAEJOh3Irm0g95NIn6+tQVf/OICCjsQsC9NBHfQws/Vxw4sfq1tDQut5fhTEvPraXhu/sHxRd9lOtxzyYuNAg==", + "peerDependencies": { + "react": ">=16.8.1", + "react-dom": ">=16.8.1" + } + }, "node_modules/react-is": { "version": "17.0.2", "resolved": "https://registry.npmjs.org/react-is/-/react-is-17.0.2.tgz", diff --git a/apps/opik-frontend/package.json b/apps/opik-frontend/package.json index d0cbb14e05..e2e813a875 100644 --- a/apps/opik-frontend/package.json +++ b/apps/opik-frontend/package.json @@ -64,6 +64,7 @@ "react-complex-tree": "^2.4.4", "react-day-picker": "^8.10.1", "react-dom": "^18.3.1", + "react-hotkeys-hook": "^4.5.1", "react-resizable-panels": "^2.0.20", "react18-json-view": "^0.2.8", "stylelint-scss": "^6.4.1", diff --git a/apps/opik-frontend/src/components/pages/DatasetCompareExperimentsPage/DatasetCompareExperimentsPage.tsx b/apps/opik-frontend/src/components/pages/DatasetCompareExperimentsPage/DatasetCompareExperimentsPage.tsx index a715d56f28..cb45398fbb 100644 --- a/apps/opik-frontend/src/components/pages/DatasetCompareExperimentsPage/DatasetCompareExperimentsPage.tsx +++ b/apps/opik-frontend/src/components/pages/DatasetCompareExperimentsPage/DatasetCompareExperimentsPage.tsx @@ -18,7 +18,7 @@ import IdCell from "@/components/shared/DataTableCells/IdCell"; import DataTableNoData from "@/components/shared/DataTableNoData/DataTableNoData"; import DataTableRowHeightSelector from "@/components/shared/DataTableRowHeightSelector/DataTableRowHeightSelector"; import useCompareExperimentsList from "@/api/datasets/useCompareExperimentsList"; -import { DatasetItem, ExperimentsCompare } from "@/types/datasets"; +import { ExperimentsCompare } from "@/types/datasets"; import Loader from "@/components/shared/Loader/Loader"; import useAppStore from "@/store/AppStore"; import { useDatasetIdFromURL } from "@/hooks/useDatasetIdFromURL"; @@ -38,6 +38,8 @@ import ColumnsButton from "@/components/shared/ColumnsButton/ColumnsButton"; import TraceDetailsPanel from "@/components/shared/TraceDetailsPanel/TraceDetailsPanel"; import useExperimentById from "@/api/datasets/useExperimentById"; +const getRowId = (d: ExperimentsCompare) => d.id; + const getRowHeightClass = (height: ROW_HEIGHT) => { switch (height) { case ROW_HEIGHT.small: @@ -65,6 +67,7 @@ export const DEFAULT_COLUMNS: ColumnData[] = [ label: "Input", size: 400, type: COLUMN_TYPE.string, + iconType: COLUMN_TYPE.dictionary, accessorFn: (row) => isObject(row.input) ? JSON.stringify(row.input, null, 2) @@ -227,7 +230,7 @@ const DatasetCompareExperimentsPage: React.FunctionComponent = () => { : `Compare (${experimentsIds.length})`; const handleRowClick = useCallback( - (row: DatasetItem) => { + (row: ExperimentsCompare) => { setActiveRowId((state) => (row.id === state ? "" : row.id)); }, [setActiveRowId], @@ -289,7 +292,9 @@ const DatasetCompareExperimentsPage: React.FunctionComponent = () => { columns={columns} data={rows} onRowClick={handleRowClick} + activeRowId={activeRowId ?? ""} resizeConfig={resizeConfig} + getRowId={getRowId} rowHeight={height as ROW_HEIGHT} getRowHeightClass={getRowHeightClass} noData={} @@ -312,6 +317,7 @@ const DatasetCompareExperimentsPage: React.FunctionComponent = () => { openTrace={setTraceId as OnChangeFn} onClose={handleClose} onRowChange={handleRowChange} + isTraceDetailsOpened={Boolean(traceId)} /> void; onRowChange?: (shift: number) => void; + isTraceDetailsOpened: boolean; }; const DatasetCompareExperimentsPanel: React.FunctionComponent< @@ -42,6 +43,7 @@ const DatasetCompareExperimentsPanel: React.FunctionComponent< hasNextRow, onClose, onRowChange, + isTraceDetailsOpened, }) => { const { toast } = useToast(); @@ -151,6 +153,7 @@ const DatasetCompareExperimentsPanel: React.FunctionComponent< return ( {renderContent()} diff --git a/apps/opik-frontend/src/components/pages/DatasetItemsPage/DatasetItemsPage.tsx b/apps/opik-frontend/src/components/pages/DatasetItemsPage/DatasetItemsPage.tsx index 7a0a1e4dee..3fd5a5756c 100644 --- a/apps/opik-frontend/src/components/pages/DatasetItemsPage/DatasetItemsPage.tsx +++ b/apps/opik-frontend/src/components/pages/DatasetItemsPage/DatasetItemsPage.tsx @@ -192,6 +192,7 @@ const DatasetItemsPage = () => { void; onRowChange?: (shift: number) => void; initialWidth?: number; + ignoreHotkeys?: boolean; }; const ResizableSidePanel: React.FunctionComponent = ({ panelId, children, + entity = "", headerContent, open = false, hasPreviousRow, @@ -31,6 +37,7 @@ const ResizableSidePanel: React.FunctionComponent = ({ onClose, onRowChange, initialWidth = INITIAL_WIDTH, + ignoreHotkeys = false, }) => { const localStorageKey = `${panelId}-side-panel-width`; @@ -46,7 +53,28 @@ const ResizableSidePanel: React.FunctionComponent = ({ resizeHandleRef.current.setAttribute("data-resize-handle-active", "true"); }, []); - React.useEffect(() => { + useHotkeys( + "ArrowUp,ArrowDown,Escape", + (keyboardEvent: KeyboardEvent) => { + if (!open) return; + keyboardEvent.stopPropagation(); + switch (keyboardEvent.code) { + case "ArrowUp": + isFunction(onRowChange) && hasPreviousRow && onRowChange(-1); + break; + case "ArrowDown": + isFunction(onRowChange) && hasNextRow && onRowChange(1); + break; + case "Escape": + onClose(); + break; + } + }, + { ignoreEventWhen: () => ignoreHotkeys }, + [onRowChange, onClose, open, ignoreHotkeys], + ); + + useEffect(() => { const handleMouseMove = (event: MouseEvent) => { if (resizeHandleRef.current) { leftRef.current = event.pageX / window.innerWidth; @@ -90,22 +118,26 @@ const ResizableSidePanel: React.FunctionComponent = ({ return ( <> - - + + + + + + ); }; @@ -124,14 +156,16 @@ const ResizableSidePanel: React.FunctionComponent = ({
- + + + {renderNavigation()}
{headerContent &&
{headerContent}
} diff --git a/apps/opik-frontend/src/components/shared/TooltipWrapper/TooltipWrapper.tsx b/apps/opik-frontend/src/components/shared/TooltipWrapper/TooltipWrapper.tsx index feb893c580..436beed51e 100644 --- a/apps/opik-frontend/src/components/shared/TooltipWrapper/TooltipWrapper.tsx +++ b/apps/opik-frontend/src/components/shared/TooltipWrapper/TooltipWrapper.tsx @@ -12,20 +12,27 @@ type TooltipWrapperProps = { content: string; children?: React.ReactNode; side?: "top" | "right" | "bottom" | "left"; + hotkey?: React.ReactNode; }; const TooltipWrapper: React.FunctionComponent = ({ content, children, side, + hotkey = null, }) => { return ( {children} - + {content} + {hotkey && ( +
+ {hotkey} +
+ )}
diff --git a/apps/opik-frontend/src/components/shared/TraceDetailsPanel/TraceAnnotateViewer/AnnotateRow.tsx b/apps/opik-frontend/src/components/shared/TraceDetailsPanel/TraceAnnotateViewer/AnnotateRow.tsx index be1e876874..be42340653 100644 --- a/apps/opik-frontend/src/components/shared/TraceDetailsPanel/TraceAnnotateViewer/AnnotateRow.tsx +++ b/apps/opik-frontend/src/components/shared/TraceDetailsPanel/TraceAnnotateViewer/AnnotateRow.tsx @@ -37,12 +37,12 @@ const AnnotateRow: React.FunctionComponent = ({ ); useEffect(() => { setCategoryName(feedbackScore?.category_name); - }, [feedbackScore?.category_name]); + }, [feedbackScore?.category_name, traceId, spanId]); const [value, setValue] = useState(feedbackScore?.value); useEffect(() => { setValue(feedbackScore?.value); - }, [feedbackScore?.value]); + }, [feedbackScore?.value, spanId, traceId]); const feedbackScoreDeleteMutation = useTraceFeedbackScoreDeleteMutation(); diff --git a/apps/opik-frontend/src/components/shared/TraceDetailsPanel/TraceAnnotateViewer/TraceAnnotateViewer.tsx b/apps/opik-frontend/src/components/shared/TraceDetailsPanel/TraceAnnotateViewer/TraceAnnotateViewer.tsx index 1bd91a6121..cdcec2cf9e 100644 --- a/apps/opik-frontend/src/components/shared/TraceDetailsPanel/TraceAnnotateViewer/TraceAnnotateViewer.tsx +++ b/apps/opik-frontend/src/components/shared/TraceDetailsPanel/TraceAnnotateViewer/TraceAnnotateViewer.tsx @@ -1,6 +1,7 @@ import React, { useMemo, useState } from "react"; import sortBy from "lodash/sortBy"; import { Plus } from "lucide-react"; +import { useHotkeys } from "react-hotkeys-hook"; import { FEEDBACK_SCORE_TYPE, @@ -12,6 +13,7 @@ import { Button } from "@/components/ui/button"; import useAppStore from "@/store/AppStore"; import useFeedbackDefinitionsList from "@/api/feedback-definitions/useFeedbackDefinitionsList"; import { FeedbackDefinition } from "@/types/feedback-definitions"; +import TooltipWrapper from "@/components/shared/TooltipWrapper/TooltipWrapper"; import AddFeedbackDefinitionDialog from "@/components/shared/AddFeedbackDefinitionDialog/AddFeedbackDefinitionDialog"; import AnnotateRow from "./AnnotateRow"; @@ -19,15 +21,30 @@ type TraceAnnotateViewerProps = { data: Trace | Span; spanId?: string; traceId: string; + annotateOpen: boolean; setAnnotateOpen: (open: boolean) => void; }; const TraceAnnotateViewer: React.FunctionComponent< TraceAnnotateViewerProps -> = ({ data, spanId, traceId, setAnnotateOpen }) => { +> = ({ data, spanId, traceId, annotateOpen, setAnnotateOpen }) => { const [feedbackDefinitionDialogOpen, setFeedbackDefinitionDialogOpen] = useState(false); + useHotkeys( + "Escape", + (keyboardEvent: KeyboardEvent) => { + if (!annotateOpen) return; + keyboardEvent.stopPropagation(); + switch (keyboardEvent.code) { + case "Escape": + setAnnotateOpen(false); + break; + } + }, + [annotateOpen], + ); + const workspaceName = useAppStore((state) => state.activeWorkspaceName); const { data: feedbackDefinitionsData } = useFeedbackDefinitionsList({ workspaceName, @@ -78,13 +95,15 @@ const TraceAnnotateViewer: React.FunctionComponent<
Annotate
- + + +
diff --git a/apps/opik-frontend/src/components/shared/TraceDetailsPanel/TraceDetailsPanel.tsx b/apps/opik-frontend/src/components/shared/TraceDetailsPanel/TraceDetailsPanel.tsx index ee2ae722f1..d12ba3b508 100644 --- a/apps/opik-frontend/src/components/shared/TraceDetailsPanel/TraceDetailsPanel.tsx +++ b/apps/opik-frontend/src/components/shared/TraceDetailsPanel/TraceDetailsPanel.tsx @@ -145,6 +145,7 @@ const TraceDetailsPanel: React.FunctionComponent = ({ data={dataToView} spanId={spanId} traceId={traceId} + annotateOpen={annotateOpen as boolean} setAnnotateOpen={setAnnotateOpen} /> @@ -195,6 +196,7 @@ const TraceDetailsPanel: React.FunctionComponent = ({ return ( , + VariantProps { + asChild?: boolean; +} + const TooltipContent = React.forwardRef< React.ElementRef, - React.ComponentPropsWithoutRef ->(({ className, sideOffset = 4, ...props }, ref) => ( + TooltipContentProps +>(({ className, variant, sideOffset = 4, ...props }, ref) => ( ));