From 6d8d0de4df7b1bebcd67aeaf3abd899cc1dd9059 Mon Sep 17 00:00:00 2001 From: "andrii.dudar" Date: Thu, 12 Sep 2024 13:59:28 +0200 Subject: [PATCH] [OPIK-91] [UX improvements] Implement the Experiment configuration section in the compare page --- apps/opik-frontend/package-lock.json | 155 ++++++++ apps/opik-frontend/package.json | 2 + .../src/api/datasets/useExperimenstByIds.ts | 25 ++ .../src/api/datasets/useExperimentById.ts | 4 +- .../CompareExperimentAddHeader.tsx | 8 +- .../CompareExperimentsCell.tsx | 4 +- .../CompareExperimentsHeader.tsx | 19 +- .../CompareExperimentsPage.tsx | 343 +++--------------- .../ConfigurationTab/CompareConfigCell.tsx | 34 ++ .../ConfigurationTab/ConfigurationTab.tsx | 197 ++++++++++ .../ExperimentItemsTab/ExperimentItemsTab.tsx | 314 ++++++++++++++++ .../pages/ExperimentsPage/ExperimentsPage.tsx | 1 + .../FeedbackDefinitionsValueCell.tsx | 13 +- .../shared/DataTableCells/CellWrapper.tsx | 9 +- .../shared/DataTableCells/CodeCell.tsx | 2 +- .../DataTableCells/FeedbackScoresCell.tsx | 1 + .../shared/DataTableCells/IdCell.tsx | 2 +- .../shared/DataTableCells/ListCell.tsx | 1 + .../shared/DataTableCells/TagCell.tsx | 1 - .../shared/DataTableCells/TextCell.tsx | 1 - .../shared/DataTableHeaders/TypeHeader.tsx | 2 +- .../FeedbackScoreValueCell.tsx | 2 +- .../src/components/ui/switch.tsx | 27 ++ .../opik-frontend/src/components/ui/table.tsx | 4 +- apps/opik-frontend/src/constants/shared.ts | 2 +- 25 files changed, 847 insertions(+), 326 deletions(-) create mode 100644 apps/opik-frontend/src/api/datasets/useExperimenstByIds.ts create mode 100644 apps/opik-frontend/src/components/pages/CompareExperimentsPage/ConfigurationTab/CompareConfigCell.tsx create mode 100644 apps/opik-frontend/src/components/pages/CompareExperimentsPage/ConfigurationTab/ConfigurationTab.tsx create mode 100644 apps/opik-frontend/src/components/pages/CompareExperimentsPage/ExperimentItemsTab/ExperimentItemsTab.tsx create mode 100644 apps/opik-frontend/src/components/ui/switch.tsx diff --git a/apps/opik-frontend/package-lock.json b/apps/opik-frontend/package-lock.json index 46cff5e70..7f75512e7 100644 --- a/apps/opik-frontend/package-lock.json +++ b/apps/opik-frontend/package-lock.json @@ -22,6 +22,7 @@ "@radix-ui/react-select": "^2.1.1", "@radix-ui/react-separator": "^1.1.0", "@radix-ui/react-slot": "^1.1.0", + "@radix-ui/react-switch": "^1.1.0", "@radix-ui/react-tabs": "^1.1.0", "@radix-ui/react-toast": "^1.2.1", "@radix-ui/react-toggle": "^1.1.0", @@ -40,6 +41,7 @@ "codemirror": "^6.0.1", "date-fns": "^3.6.0", "dayjs": "^1.11.11", + "flattie": "^1.1.1", "js-yaml": "^4.1.0", "lodash": "^4.17.21", "lucide-react": "^0.395.0", @@ -3451,6 +3453,151 @@ } } }, + "node_modules/@radix-ui/react-switch": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/@radix-ui/react-switch/-/react-switch-1.1.0.tgz", + "integrity": "sha512-OBzy5WAj641k0AOSpKQtreDMe+isX0MQJ1IVyF03ucdF3DunOnROVrjWs8zsXUxC3zfZ6JL9HFVCUlMghz9dJw==", + "dependencies": { + "@radix-ui/primitive": "1.1.0", + "@radix-ui/react-compose-refs": "1.1.0", + "@radix-ui/react-context": "1.1.0", + "@radix-ui/react-primitive": "2.0.0", + "@radix-ui/react-use-controllable-state": "1.1.0", + "@radix-ui/react-use-previous": "1.1.0", + "@radix-ui/react-use-size": "1.1.0" + }, + "peerDependencies": { + "@types/react": "*", + "@types/react-dom": "*", + "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc", + "react-dom": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" + }, + "peerDependenciesMeta": { + "@types/react": { + "optional": true + }, + "@types/react-dom": { + "optional": true + } + } + }, + "node_modules/@radix-ui/react-switch/node_modules/@radix-ui/primitive": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/@radix-ui/primitive/-/primitive-1.1.0.tgz", + "integrity": "sha512-4Z8dn6Upk0qk4P74xBhZ6Hd/w0mPEzOOLxy4xiPXOXqjF7jZS0VAKk7/x/H6FyY2zCkYJqePf1G5KmkmNJ4RBA==" + }, + "node_modules/@radix-ui/react-switch/node_modules/@radix-ui/react-compose-refs": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/@radix-ui/react-compose-refs/-/react-compose-refs-1.1.0.tgz", + "integrity": "sha512-b4inOtiaOnYf9KWyO3jAeeCG6FeyfY6ldiEPanbUjWd+xIk5wZeHa8yVwmrJ2vderhu/BQvzCrJI0lHd+wIiqw==", + "peerDependencies": { + "@types/react": "*", + "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" + }, + "peerDependenciesMeta": { + "@types/react": { + "optional": true + } + } + }, + "node_modules/@radix-ui/react-switch/node_modules/@radix-ui/react-context": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/@radix-ui/react-context/-/react-context-1.1.0.tgz", + "integrity": "sha512-OKrckBy+sMEgYM/sMmqmErVn0kZqrHPJze+Ql3DzYsDDp0hl0L62nx/2122/Bvps1qz645jlcu2tD9lrRSdf8A==", + "peerDependencies": { + "@types/react": "*", + "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" + }, + "peerDependenciesMeta": { + "@types/react": { + "optional": true + } + } + }, + "node_modules/@radix-ui/react-switch/node_modules/@radix-ui/react-primitive": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/@radix-ui/react-primitive/-/react-primitive-2.0.0.tgz", + "integrity": "sha512-ZSpFm0/uHa8zTvKBDjLFWLo8dkr4MBsiDLz0g3gMUwqgLHz9rTaRRGYDgvZPtBJgYCBKXkS9fzmoySgr8CO6Cw==", + "dependencies": { + "@radix-ui/react-slot": "1.1.0" + }, + "peerDependencies": { + "@types/react": "*", + "@types/react-dom": "*", + "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc", + "react-dom": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" + }, + "peerDependenciesMeta": { + "@types/react": { + "optional": true + }, + "@types/react-dom": { + "optional": true + } + } + }, + "node_modules/@radix-ui/react-switch/node_modules/@radix-ui/react-use-callback-ref": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/@radix-ui/react-use-callback-ref/-/react-use-callback-ref-1.1.0.tgz", + "integrity": "sha512-CasTfvsy+frcFkbXtSJ2Zu9JHpN8TYKxkgJGWbjiZhFivxaeW7rMeZt7QELGVLaYVfFMsKHjb7Ak0nMEe+2Vfw==", + "peerDependencies": { + "@types/react": "*", + "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" + }, + "peerDependenciesMeta": { + "@types/react": { + "optional": true + } + } + }, + "node_modules/@radix-ui/react-switch/node_modules/@radix-ui/react-use-controllable-state": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/@radix-ui/react-use-controllable-state/-/react-use-controllable-state-1.1.0.tgz", + "integrity": "sha512-MtfMVJiSr2NjzS0Aa90NPTnvTSg6C/JLCV7ma0W6+OMV78vd8OyRpID+Ng9LxzsPbLeuBnWBA1Nq30AtBIDChw==", + "dependencies": { + "@radix-ui/react-use-callback-ref": "1.1.0" + }, + "peerDependencies": { + "@types/react": "*", + "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" + }, + "peerDependenciesMeta": { + "@types/react": { + "optional": true + } + } + }, + "node_modules/@radix-ui/react-switch/node_modules/@radix-ui/react-use-layout-effect": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/@radix-ui/react-use-layout-effect/-/react-use-layout-effect-1.1.0.tgz", + "integrity": "sha512-+FPE0rOdziWSrH9athwI1R0HDVbWlEhd+FR+aSDk4uWGmSJ9Z54sdZVDQPZAinJhJXwfT+qnj969mCsT2gfm5w==", + "peerDependencies": { + "@types/react": "*", + "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" + }, + "peerDependenciesMeta": { + "@types/react": { + "optional": true + } + } + }, + "node_modules/@radix-ui/react-switch/node_modules/@radix-ui/react-use-size": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/@radix-ui/react-use-size/-/react-use-size-1.1.0.tgz", + "integrity": "sha512-XW3/vWuIXHa+2Uwcc2ABSfcCledmXhhQPlGbfcRXbiUQI5Icjcg19BGCZVKKInYbvUCut/ufbbLLPFC5cbb1hw==", + "dependencies": { + "@radix-ui/react-use-layout-effect": "1.1.0" + }, + "peerDependencies": { + "@types/react": "*", + "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" + }, + "peerDependenciesMeta": { + "@types/react": { + "optional": true + } + } + }, "node_modules/@radix-ui/react-tabs": { "version": "1.1.0", "resolved": "https://registry.npmjs.org/@radix-ui/react-tabs/-/react-tabs-1.1.0.tgz", @@ -7980,6 +8127,14 @@ "resolved": "https://registry.npmjs.org/flatted/-/flatted-3.3.1.tgz", "integrity": "sha512-X8cqMLLie7KsNUDSdzeN8FYK9rEt4Dt67OsG/DNGnYTSDBG4uFAJFBnUeiV+zCVAvwFy56IjM9sH51jVaEhNxw==" }, + "node_modules/flattie": { + "version": "1.1.1", + "resolved": "https://registry.npmjs.org/flattie/-/flattie-1.1.1.tgz", + "integrity": "sha512-9UbaD6XdAL97+k/n+N7JwX46K/M6Zc6KcFYskrYL8wbBV/Uyk0CTAMY0VT+qiK5PM7AIc9aTWYtq65U7T+aCNQ==", + "engines": { + "node": ">=8" + } + }, "node_modules/follow-redirects": { "version": "1.15.6", "resolved": "https://registry.npmjs.org/follow-redirects/-/follow-redirects-1.15.6.tgz", diff --git a/apps/opik-frontend/package.json b/apps/opik-frontend/package.json index ad576c9c5..6f17f8a3e 100644 --- a/apps/opik-frontend/package.json +++ b/apps/opik-frontend/package.json @@ -39,6 +39,7 @@ "@radix-ui/react-select": "^2.1.1", "@radix-ui/react-separator": "^1.1.0", "@radix-ui/react-slot": "^1.1.0", + "@radix-ui/react-switch": "^1.1.0", "@radix-ui/react-tabs": "^1.1.0", "@radix-ui/react-toast": "^1.2.1", "@radix-ui/react-toggle": "^1.1.0", @@ -57,6 +58,7 @@ "codemirror": "^6.0.1", "date-fns": "^3.6.0", "dayjs": "^1.11.11", + "flattie": "^1.1.1", "js-yaml": "^4.1.0", "lodash": "^4.17.21", "lucide-react": "^0.395.0", diff --git a/apps/opik-frontend/src/api/datasets/useExperimenstByIds.ts b/apps/opik-frontend/src/api/datasets/useExperimenstByIds.ts new file mode 100644 index 000000000..00b3102c5 --- /dev/null +++ b/apps/opik-frontend/src/api/datasets/useExperimenstByIds.ts @@ -0,0 +1,25 @@ +import { QueryFunctionContext, useQueries } from "@tanstack/react-query"; +import { + getExperimentById, + UseExperimentByIdParams, +} from "@/api/datasets/useExperimentById"; + +type UseExperimentsByIdsParams = { + experimentsIds: string[]; +}; + +export default function useExperimentsByIds(params: UseExperimentsByIdsParams) { + return useQueries({ + queries: params.experimentsIds.map((experimentId) => { + const p: UseExperimentByIdParams = { + experimentId, + }; + + return { + queryKey: ["experiment", p], + queryFn: (context: QueryFunctionContext) => + getExperimentById(context, p), + }; + }), + }); +} diff --git a/apps/opik-frontend/src/api/datasets/useExperimentById.ts b/apps/opik-frontend/src/api/datasets/useExperimentById.ts index 547d52327..7226b1b8b 100644 --- a/apps/opik-frontend/src/api/datasets/useExperimentById.ts +++ b/apps/opik-frontend/src/api/datasets/useExperimentById.ts @@ -2,7 +2,7 @@ import { QueryFunctionContext, useQuery } from "@tanstack/react-query"; import api, { EXPERIMENTS_REST_ENDPOINT, QueryConfig } from "@/api/api"; import { Experiment } from "@/types/datasets"; -const getExperimentById = async ( +export const getExperimentById = async ( { signal }: QueryFunctionContext, { experimentId }: UseExperimentByIdParams, ) => { @@ -13,7 +13,7 @@ const getExperimentById = async ( return data; }; -type UseExperimentByIdParams = { +export type UseExperimentByIdParams = { experimentId: string; }; diff --git a/apps/opik-frontend/src/components/pages/CompareExperimentsPage/CompareExperimentAddHeader.tsx b/apps/opik-frontend/src/components/pages/CompareExperimentsPage/CompareExperimentAddHeader.tsx index ee8e9fed5..5a84d6fdb 100644 --- a/apps/opik-frontend/src/components/pages/CompareExperimentsPage/CompareExperimentAddHeader.tsx +++ b/apps/opik-frontend/src/components/pages/CompareExperimentsPage/CompareExperimentAddHeader.tsx @@ -9,16 +9,20 @@ import AddExperimentToCompareDialog from "@/components/pages/CompareExperimentsP export const CompareExperimentAddHeader: React.FunctionComponent< HeaderContext -> = () => { +> = (context) => { const datasetId = useDatasetIdFromCompareExperimentsURL(); const resetKeyRef = useRef(0); const [open, setOpen] = useState(false); + const hasData = context.table.getRowCount() > 0; return (
e.stopPropagation()} > + {hasData && ( +
+ )}
@@ -79,7 +79,7 @@ const CompareExperimentsCell: React.FunctionComponent< })}
{isSmall ? ( -
+
{JSON.stringify(item.output, null, 2)}
) : ( diff --git a/apps/opik-frontend/src/components/pages/CompareExperimentsPage/CompareExperimentsHeader.tsx b/apps/opik-frontend/src/components/pages/CompareExperimentsPage/CompareExperimentsHeader.tsx index 72f0c525b..ec3d014c3 100644 --- a/apps/opik-frontend/src/components/pages/CompareExperimentsPage/CompareExperimentsHeader.tsx +++ b/apps/opik-frontend/src/components/pages/CompareExperimentsPage/CompareExperimentsHeader.tsx @@ -9,8 +9,9 @@ import useExperimentById from "@/api/datasets/useExperimentById"; const CompareExperimentsHeader: React.FunctionComponent< HeaderContext -> = ({ header }) => { +> = ({ table, header }) => { const experimentId = header?.id; + const hasData = table.getRowCount() > 0; const [experimentIds, setExperimentsIds] = useQueryParam( "experiments", JsonParam, @@ -19,17 +20,25 @@ const CompareExperimentsHeader: React.FunctionComponent< }, ); - const { data } = useExperimentById({ - experimentId, - }); + const { data } = useExperimentById( + { + experimentId, + }, + { + refetchOnMount: false, + }, + ); const name = data?.name || experimentId; return (
e.stopPropagation()} > + {hasData && ( +
+ )}
{name}
{experimentIds.length > 1 && ( diff --git a/apps/opik-frontend/src/components/pages/CompareExperimentsPage/CompareExperimentsPage.tsx b/apps/opik-frontend/src/components/pages/CompareExperimentsPage/CompareExperimentsPage.tsx index 87a251bb8..d2d9c3f51 100644 --- a/apps/opik-frontend/src/components/pages/CompareExperimentsPage/CompareExperimentsPage.tsx +++ b/apps/opik-frontend/src/components/pages/CompareExperimentsPage/CompareExperimentsPage.tsx @@ -1,233 +1,47 @@ -import React, { useCallback, useEffect, useMemo } from "react"; -import isObject from "lodash/isObject"; -import findIndex from "lodash/findIndex"; -import find from "lodash/find"; -import { - JsonParam, - NumberParam, - StringParam, - useQueryParam, -} from "use-query-params"; -import { keepPreviousData } from "@tanstack/react-query"; -import useLocalStorageState from "use-local-storage-state"; - -import DataTable from "@/components/shared/DataTable/DataTable"; -import DataTablePagination from "@/components/shared/DataTablePagination/DataTablePagination"; -import CodeCell from "@/components/shared/DataTableCells/CodeCell"; -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 { ExperimentsCompare } from "@/types/datasets"; -import Loader from "@/components/shared/Loader/Loader"; -import useAppStore from "@/store/AppStore"; -import { useDatasetIdFromCompareExperimentsURL } from "@/hooks/useDatasetIdFromCompareExperimentsURL"; -import { - COLUMN_TYPE, - ColumnData, - OnChangeFn, - ROW_HEIGHT, -} from "@/types/shared"; -import CompareExperimentsHeader from "@/components/pages/CompareExperimentsPage/CompareExperimentsHeader"; -import CompareExperimentAddHeader from "@/components/pages/CompareExperimentsPage/CompareExperimentAddHeader"; -import CompareExperimentsCell from "@/components/pages/CompareExperimentsPage/CompareExperimentsCell"; -import CompareExperimentsPanel from "@/components/pages/CompareExperimentsPage/CompareExperimentsPanel/CompareExperimentsPanel"; -import { formatDate } from "@/lib/date"; -import { convertColumnDataToColumn } from "@/lib/table"; -import ColumnsButton from "@/components/shared/ColumnsButton/ColumnsButton"; -import TraceDetailsPanel from "@/components/shared/TraceDetailsPanel/TraceDetailsPanel"; -import useExperimentById from "@/api/datasets/useExperimentById"; +import React, { useEffect } from "react"; +import isUndefined from "lodash/isUndefined"; +import { JsonParam, StringParam, useQueryParam } from "use-query-params"; + +import { Tabs, TabsContent, TabsList, TabsTrigger } from "@/components/ui/tabs"; +import ExperimentItemsTab from "@/components/pages/CompareExperimentsPage/ExperimentItemsTab/ExperimentItemsTab"; +import ConfigurationTab from "@/components/pages/CompareExperimentsPage/ConfigurationTab/ConfigurationTab"; +import useExperimentsByIds from "@/api/datasets/useExperimenstByIds"; import useBreadcrumbsStore from "@/store/BreadcrumbsStore"; - -const getRowId = (d: ExperimentsCompare) => d.id; - -const getRowHeightClass = (height: ROW_HEIGHT) => { - switch (height) { - case ROW_HEIGHT.small: - return "h-20"; - case ROW_HEIGHT.medium: - return "h-60"; - case ROW_HEIGHT.large: - return "h-[592px]"; - } -}; - -const SELECTED_COLUMNS_KEY = "compare-experiments-selected-columns"; -const COLUMNS_WIDTH_KEY = "compare-experiments-columns-width"; -const COLUMNS_ORDER_KEY = "compare-experiments-columns-order"; - -export const DEFAULT_COLUMNS: ColumnData[] = [ - { - id: "id", - label: "Item ID", - type: COLUMN_TYPE.string, - cell: IdCell as never, - }, - { - id: "input", - label: "Input", - size: 400, - type: COLUMN_TYPE.string, - iconType: COLUMN_TYPE.dictionary, - accessorFn: (row) => - isObject(row.input) - ? JSON.stringify(row.input, null, 2) - : row.input || "", - cell: CodeCell as never, - }, - { - id: "expected_output", - label: "Expected output", - size: 400, - type: COLUMN_TYPE.string, - iconType: COLUMN_TYPE.dictionary, - accessorFn: (row) => - isObject(row.expected_output) - ? JSON.stringify(row.expected_output, null, 2) - : row.expected_output || "", - cell: CodeCell as never, - }, - { - id: "metadata", - label: "Metadata", - type: COLUMN_TYPE.dictionary, - accessorFn: (row) => - isObject(row.metadata) - ? JSON.stringify(row.metadata, null, 2) - : row.metadata || "", - cell: CodeCell as never, - }, - { - id: "created_at", - label: "Created", - type: COLUMN_TYPE.time, - accessorFn: (row) => formatDate(row.created_at), - }, -]; - -export const DEFAULT_SELECTED_COLUMNS: string[] = ["id", "input"]; +import useDeepMemo from "@/hooks/useDeepMemo"; +import { Experiment } from "@/types/datasets"; const CompareExperimentsPage: React.FunctionComponent = () => { - const datasetId = useDatasetIdFromCompareExperimentsURL(); const setBreadcrumbParam = useBreadcrumbsStore((state) => state.setParam); - const workspaceName = useAppStore((state) => state.activeWorkspaceName); - const [activeRowId = "", setActiveRowId] = useQueryParam("row", StringParam, { + const [tab = "items", setTab] = useQueryParam("tab", StringParam, { updateType: "replaceIn", }); - const [traceId = "", setTraceId] = useQueryParam("trace", StringParam, { - updateType: "replaceIn", - }); - - const [spanId = "", setSpanId] = useQueryParam("span", StringParam, { - updateType: "replaceIn", - }); - - const [page = 1, setPage] = useQueryParam("page", NumberParam, { - updateType: "replaceIn", - }); - - const [size = 10, setSize] = useQueryParam("size", NumberParam, { - updateType: "replaceIn", - }); - - const [height = ROW_HEIGHT.small, setHeight] = useQueryParam( - "height", - StringParam, - { - updateType: "replaceIn", - }, - ); - const [experimentsIds = []] = useQueryParam("experiments", JsonParam, { updateType: "replaceIn", }); const isCompare = experimentsIds.length > 1; - const [columnsWidth, setColumnsWidth] = useLocalStorageState< - Record - >(COLUMNS_WIDTH_KEY, { - defaultValue: {}, + const response = useExperimentsByIds({ + experimentsIds, }); - const [selectedColumns, setSelectedColumns] = useLocalStorageState( - SELECTED_COLUMNS_KEY, - { - defaultValue: DEFAULT_SELECTED_COLUMNS, - }, + const isPending = response.reduce( + (acc, r) => acc || r.isPending, + false, ); - const [columnsOrder, setColumnsOrder] = useLocalStorageState( - COLUMNS_ORDER_KEY, - { - defaultValue: [], - }, - ); - - const columns = useMemo(() => { - const retVal = convertColumnDataToColumn< - ExperimentsCompare, - ExperimentsCompare - >(DEFAULT_COLUMNS, { - columnsWidth, - selectedColumns, - columnsOrder, - }); - - experimentsIds.forEach((id: string) => { - const size = columnsWidth[id] ?? 400; - retVal.push({ - accessorKey: id, - header: CompareExperimentsHeader, - cell: CompareExperimentsCell as never, - meta: { - custom: { - openTrace: setTraceId, - }, - }, - size, - }); - }); + const experiments: Experiment[] = response + .map((r) => r.data) + .filter((e) => !isUndefined(e)); - retVal.push({ - accessorKey: "add_experiment", - enableHiding: false, - enableResizing: false, - size: 48, - header: CompareExperimentAddHeader, - }); + const memorizedExperiments: Experiment[] = useDeepMemo(() => { + return experiments ?? []; + }, [experiments]); - return retVal; - }, [columnsWidth, selectedColumns, columnsOrder, experimentsIds, setTraceId]); + const experiment = memorizedExperiments[0]; - const { data, isPending } = useCompareExperimentsList( - { - workspaceName, - datasetId, - experimentsIds, - page: page as number, - size: size as number, - }, - { - placeholderData: keepPreviousData, - }, - ); - - const { data: experiment } = useExperimentById( - { - experimentId: experimentsIds[0], - }, - { - refetchOnMount: false, - enabled: experimentsIds.length === 1, - }, - ); - - const rows = useMemo(() => data?.content ?? [], [data?.content]); - const total = data?.total ?? 0; - const noDataText = "There is no data for the selected experiments"; const title = !isCompare ? experiment?.name : `Compare (${experimentsIds.length})`; @@ -237,104 +51,31 @@ const CompareExperimentsPage: React.FunctionComponent = () => { return () => setBreadcrumbParam("compare", "compare", ""); }, [title, setBreadcrumbParam]); - const handleRowClick = useCallback( - (row: ExperimentsCompare) => { - setActiveRowId((state) => (row.id === state ? "" : row.id)); - }, - [setActiveRowId], - ); - - const rowIndex = findIndex(rows, (row) => activeRowId === row.id); - - const hasNext = rowIndex >= 0 ? rowIndex < rows.length - 1 : false; - const hasPrevious = rowIndex >= 0 ? rowIndex > 0 : false; - - const handleRowChange = useCallback( - (shift: number) => { - setActiveRowId(rows[rowIndex + shift]?.id ?? ""); - }, - [rowIndex, rows, setActiveRowId], - ); - - const handleClose = useCallback(() => setActiveRowId(""), [setActiveRowId]); - - const activeRow = useMemo( - () => find(rows, (row) => activeRowId === row.id), - [activeRowId, rows], - ); - - const resizeConfig = useMemo( - () => ({ - enabled: true, - onColumnResize: setColumnsWidth, - }), - [setColumnsWidth], - ); - - if (isPending) { - return ; - } - return (

{title}

-
-
-
- + + + Experiment items + + + Configuration + + + + + + + - -
-
- } - /> -
- -
- } - onClose={handleClose} - onRowChange={handleRowChange} - isTraceDetailsOpened={Boolean(traceId)} - /> - { - setTraceId(""); - }} - /> + +
); }; diff --git a/apps/opik-frontend/src/components/pages/CompareExperimentsPage/ConfigurationTab/CompareConfigCell.tsx b/apps/opik-frontend/src/components/pages/CompareExperimentsPage/ConfigurationTab/CompareConfigCell.tsx new file mode 100644 index 000000000..b94b240a2 --- /dev/null +++ b/apps/opik-frontend/src/components/pages/CompareExperimentsPage/ConfigurationTab/CompareConfigCell.tsx @@ -0,0 +1,34 @@ +import React from "react"; +import { CellContext } from "@tanstack/react-table"; + +import CellWrapper from "@/components/shared/DataTableCells/CellWrapper"; +import { CompareConfig } from "@/components/pages/CompareExperimentsPage/ConfigurationTab/ConfigurationTab"; +import { ROW_HEIGHT } from "@/types/shared"; + +const CompareConfigCell: React.FunctionComponent< + CellContext +> = (context) => { + const experimentId = context.column?.id; + const compareConfig = context.row.original; + + const data = compareConfig.data[experimentId]; + + if (data === undefined) { + return null; + } + + return ( + +
{String(data)}
+
+ ); +}; + +export default CompareConfigCell; diff --git a/apps/opik-frontend/src/components/pages/CompareExperimentsPage/ConfigurationTab/ConfigurationTab.tsx b/apps/opik-frontend/src/components/pages/CompareExperimentsPage/ConfigurationTab/ConfigurationTab.tsx new file mode 100644 index 000000000..3c615adf6 --- /dev/null +++ b/apps/opik-frontend/src/components/pages/CompareExperimentsPage/ConfigurationTab/ConfigurationTab.tsx @@ -0,0 +1,197 @@ +import React, { useMemo } from "react"; +import { BooleanParam, StringParam, useQueryParam } from "use-query-params"; +import useLocalStorageState from "use-local-storage-state"; +import isObject from "lodash/isObject"; +import uniq from "lodash/uniq"; +import toLower from "lodash/toLower"; +import { flattie } from "flattie"; + +import { COLUMN_TYPE, ColumnData } from "@/types/shared"; +import DataTable from "@/components/shared/DataTable/DataTable"; +import DataTableNoData from "@/components/shared/DataTableNoData/DataTableNoData"; +import CompareExperimentsHeader from "@/components/pages/CompareExperimentsPage/CompareExperimentsHeader"; +import CompareExperimentAddHeader from "@/components/pages/CompareExperimentsPage/CompareExperimentAddHeader"; +import CompareConfigCell from "@/components/pages/CompareExperimentsPage/ConfigurationTab/CompareConfigCell"; +import Loader from "@/components/shared/Loader/Loader"; +import { convertColumnDataToColumn } from "@/lib/table"; +import SearchInput from "@/components/shared/SearchInput/SearchInput"; +import { Experiment } from "@/types/datasets"; +import { Switch } from "@/components/ui/switch"; +import { Label } from "@/components/ui/label"; + +const COLUMNS_WIDTH_KEY = "compare-experiments-config-columns-width"; + +type FiledValue = string | number | undefined | null; + +export type CompareConfig = { + name: string; + data: Record; + base: string; + different: boolean; +}; + +export const DEFAULT_COLUMNS: ColumnData[] = [ + { + id: "name", + label: "Name", + type: COLUMN_TYPE.string, + }, +]; + +export type ConfigurationTabProps = { + experimentsIds: string[]; + experiments: Experiment[]; + isPending: boolean; +}; + +const ConfigurationTab: React.FunctionComponent = ({ + experimentsIds, + experiments, + isPending, +}) => { + const [search = "", setSearch] = useQueryParam("searchConfig", StringParam, { + updateType: "replaceIn", + }); + + const [onlyDiff = false, setOnlyDiff] = useQueryParam("diff", BooleanParam, { + updateType: "replaceIn", + }); + + const isCompare = experimentsIds.length > 1; + + const [columnsWidth, setColumnsWidth] = useLocalStorageState< + Record + >(COLUMNS_WIDTH_KEY, { + defaultValue: {}, + }); + + const columns = useMemo(() => { + const retVal = convertColumnDataToColumn( + DEFAULT_COLUMNS, + { + columnsWidth, + }, + ); + + experimentsIds.forEach((id: string) => { + const size = columnsWidth[id] ?? 400; + retVal.push({ + accessorKey: id, + header: CompareExperimentsHeader as never, + cell: CompareConfigCell as never, + size, + }); + }); + + retVal.push({ + accessorKey: "add_experiment", + enableHiding: false, + enableResizing: false, + size: 48, + header: CompareExperimentAddHeader as never, + }); + + return retVal; + }, [columnsWidth, experimentsIds]); + + const flattenExperimentMetadataMap = useMemo(() => { + return experiments.reduce>>( + (acc, experiment) => { + acc[experiment.id] = isObject(experiment.metadata) + ? flattie(experiment.metadata, "|", true) + : {}; + + return acc; + }, + {}, + ); + }, [experiments]); + + const rows = useMemo(() => { + const keys = uniq( + Object.values(flattenExperimentMetadataMap).reduce( + (acc, map) => acc.concat(Object.keys(map)), + [], + ), + ).sort(); + + return keys.map((key) => { + const data = experimentsIds.reduce>( + (acc, id: string) => { + acc[id] = flattenExperimentMetadataMap[id]?.[key] ?? undefined; + return acc; + }, + {}, + ); + const values = Object.values(data); + + return { + name: key, + base: experimentsIds[0], + data, + different: !values.every((v) => values[0] === v), + } as CompareConfig; + }); + }, [flattenExperimentMetadataMap, experimentsIds]); + + const filteredRows = useMemo(() => { + return rows.filter((row) => { + if (isCompare && onlyDiff && !row.different) { + return false; + } + + return !(search && !toLower(row.name).includes(toLower(search))); + }); + }, [rows, search, onlyDiff, isCompare]); + + const noDataText = search + ? "No search results" + : "There is no data for the selected experiments"; + + const resizeConfig = useMemo( + () => ({ + enabled: true, + onColumnResize: setColumnsWidth, + }), + [setColumnsWidth], + ); + + if (isPending) { + return ; + } + + return ( +
+
+
+ +
+
+ {isCompare && ( +
+ + +
+ )} +
+
+ } + /> +
+ ); +}; + +export default ConfigurationTab; diff --git a/apps/opik-frontend/src/components/pages/CompareExperimentsPage/ExperimentItemsTab/ExperimentItemsTab.tsx b/apps/opik-frontend/src/components/pages/CompareExperimentsPage/ExperimentItemsTab/ExperimentItemsTab.tsx new file mode 100644 index 000000000..cb6c41b91 --- /dev/null +++ b/apps/opik-frontend/src/components/pages/CompareExperimentsPage/ExperimentItemsTab/ExperimentItemsTab.tsx @@ -0,0 +1,314 @@ +import React, { useCallback, useMemo } from "react"; +import isObject from "lodash/isObject"; +import findIndex from "lodash/findIndex"; +import find from "lodash/find"; +import { NumberParam, StringParam, useQueryParam } from "use-query-params"; +import { keepPreviousData } from "@tanstack/react-query"; +import useLocalStorageState from "use-local-storage-state"; + +import { + COLUMN_TYPE, + ColumnData, + OnChangeFn, + ROW_HEIGHT, +} from "@/types/shared"; +import DataTable from "@/components/shared/DataTable/DataTable"; +import DataTablePagination from "@/components/shared/DataTablePagination/DataTablePagination"; +import DataTableNoData from "@/components/shared/DataTableNoData/DataTableNoData"; +import DataTableRowHeightSelector from "@/components/shared/DataTableRowHeightSelector/DataTableRowHeightSelector"; +import IdCell from "@/components/shared/DataTableCells/IdCell"; +import CodeCell from "@/components/shared/DataTableCells/CodeCell"; +import CompareExperimentsHeader from "@/components/pages/CompareExperimentsPage/CompareExperimentsHeader"; +import CompareExperimentsCell from "@/components/pages/CompareExperimentsPage/CompareExperimentsCell"; +import CompareExperimentAddHeader from "@/components/pages/CompareExperimentsPage/CompareExperimentAddHeader"; +import TraceDetailsPanel from "@/components/shared/TraceDetailsPanel/TraceDetailsPanel"; +import CompareExperimentsPanel from "@/components/pages/CompareExperimentsPage/CompareExperimentsPanel/CompareExperimentsPanel"; +import ColumnsButton from "@/components/shared/ColumnsButton/ColumnsButton"; +import Loader from "@/components/shared/Loader/Loader"; +import useCompareExperimentsList from "@/api/datasets/useCompareExperimentsList"; +import useAppStore from "@/store/AppStore"; +import { ExperimentsCompare } from "@/types/datasets"; +import { useDatasetIdFromCompareExperimentsURL } from "@/hooks/useDatasetIdFromCompareExperimentsURL"; +import { formatDate } from "@/lib/date"; +import { convertColumnDataToColumn } from "@/lib/table"; + +const getRowId = (d: ExperimentsCompare) => d.id; + +const getRowHeightClass = (height: ROW_HEIGHT) => { + switch (height) { + case ROW_HEIGHT.small: + return "h-[88px]"; + case ROW_HEIGHT.medium: + return "h-[196px]"; + case ROW_HEIGHT.large: + return "h-[408px]"; + } +}; + +const SELECTED_COLUMNS_KEY = "compare-experiments-selected-columns"; +const COLUMNS_WIDTH_KEY = "compare-experiments-columns-width"; +const COLUMNS_ORDER_KEY = "compare-experiments-columns-order"; + +export const DEFAULT_COLUMNS: ColumnData[] = [ + { + id: "id", + label: "Item ID", + type: COLUMN_TYPE.string, + cell: IdCell as never, + }, + { + id: "input", + label: "Input", + size: 400, + type: COLUMN_TYPE.string, + iconType: COLUMN_TYPE.dictionary, + accessorFn: (row) => + isObject(row.input) + ? JSON.stringify(row.input, null, 2) + : row.input || "", + cell: CodeCell as never, + }, + { + id: "expected_output", + label: "Expected output", + size: 400, + type: COLUMN_TYPE.string, + iconType: COLUMN_TYPE.dictionary, + accessorFn: (row) => + isObject(row.expected_output) + ? JSON.stringify(row.expected_output, null, 2) + : row.expected_output || "", + cell: CodeCell as never, + }, + { + id: "metadata", + label: "Metadata", + type: COLUMN_TYPE.dictionary, + accessorFn: (row) => + isObject(row.metadata) + ? JSON.stringify(row.metadata, null, 2) + : row.metadata || "", + cell: CodeCell as never, + }, + { + id: "created_at", + label: "Created", + type: COLUMN_TYPE.time, + accessorFn: (row) => formatDate(row.created_at), + }, +]; + +export const DEFAULT_SELECTED_COLUMNS: string[] = ["id", "input"]; + +export type ExperimentItemsTabProps = { + experimentsIds: string[]; +}; + +const ExperimentItemsTab: React.FunctionComponent = ({ + experimentsIds = [], +}) => { + const datasetId = useDatasetIdFromCompareExperimentsURL(); + const workspaceName = useAppStore((state) => state.activeWorkspaceName); + + const [activeRowId = "", setActiveRowId] = useQueryParam("row", StringParam, { + updateType: "replaceIn", + }); + + const [traceId = "", setTraceId] = useQueryParam("trace", StringParam, { + updateType: "replaceIn", + }); + + const [spanId = "", setSpanId] = useQueryParam("span", StringParam, { + updateType: "replaceIn", + }); + + const [page = 1, setPage] = useQueryParam("page", NumberParam, { + updateType: "replaceIn", + }); + + const [size = 10, setSize] = useQueryParam("size", NumberParam, { + updateType: "replaceIn", + }); + + const [height = ROW_HEIGHT.small, setHeight] = useQueryParam( + "height", + StringParam, + { + updateType: "replaceIn", + }, + ); + + const [columnsWidth, setColumnsWidth] = useLocalStorageState< + Record + >(COLUMNS_WIDTH_KEY, { + defaultValue: {}, + }); + + const [selectedColumns, setSelectedColumns] = useLocalStorageState( + SELECTED_COLUMNS_KEY, + { + defaultValue: DEFAULT_SELECTED_COLUMNS, + }, + ); + + const [columnsOrder, setColumnsOrder] = useLocalStorageState( + COLUMNS_ORDER_KEY, + { + defaultValue: [], + }, + ); + + const columns = useMemo(() => { + const retVal = convertColumnDataToColumn< + ExperimentsCompare, + ExperimentsCompare + >(DEFAULT_COLUMNS, { + columnsWidth, + selectedColumns, + columnsOrder, + }); + + experimentsIds.forEach((id: string) => { + const size = columnsWidth[id] ?? 400; + retVal.push({ + accessorKey: id, + header: CompareExperimentsHeader, + cell: CompareExperimentsCell as never, + meta: { + custom: { + openTrace: setTraceId, + }, + }, + size, + }); + }); + + retVal.push({ + accessorKey: "add_experiment", + enableHiding: false, + enableResizing: false, + size: 48, + header: CompareExperimentAddHeader, + }); + + return retVal; + }, [columnsWidth, selectedColumns, columnsOrder, experimentsIds, setTraceId]); + + const { data, isPending } = useCompareExperimentsList( + { + workspaceName, + datasetId, + experimentsIds, + page: page as number, + size: size as number, + }, + { + placeholderData: keepPreviousData, + refetchInterval: 30000, + }, + ); + + const rows = useMemo(() => data?.content ?? [], [data?.content]); + const total = data?.total ?? 0; + const noDataText = "There is no data for the selected experiments"; + + const handleRowClick = useCallback( + (row: ExperimentsCompare) => { + setActiveRowId((state) => (row.id === state ? "" : row.id)); + }, + [setActiveRowId], + ); + + const rowIndex = findIndex(rows, (row) => activeRowId === row.id); + + const hasNext = rowIndex >= 0 ? rowIndex < rows.length - 1 : false; + const hasPrevious = rowIndex >= 0 ? rowIndex > 0 : false; + + const handleRowChange = useCallback( + (shift: number) => { + setActiveRowId(rows[rowIndex + shift]?.id ?? ""); + }, + [rowIndex, rows, setActiveRowId], + ); + + const handleClose = useCallback(() => setActiveRowId(""), [setActiveRowId]); + + const activeRow = useMemo( + () => find(rows, (row) => activeRowId === row.id), + [activeRowId, rows], + ); + + const resizeConfig = useMemo( + () => ({ + enabled: true, + onColumnResize: setColumnsWidth, + }), + [setColumnsWidth], + ); + + if (isPending) { + return ; + } + + return ( +
+
+
+
+ + +
+
+ } + /> +
+ +
+ } + onClose={handleClose} + onRowChange={handleRowChange} + isTraceDetailsOpened={Boolean(traceId)} + /> + { + setTraceId(""); + }} + /> +
+ ); +}; + +export default ExperimentItemsTab; diff --git a/apps/opik-frontend/src/components/pages/ExperimentsPage/ExperimentsPage.tsx b/apps/opik-frontend/src/components/pages/ExperimentsPage/ExperimentsPage.tsx index 70bfe6299..c2ade71e3 100644 --- a/apps/opik-frontend/src/components/pages/ExperimentsPage/ExperimentsPage.tsx +++ b/apps/opik-frontend/src/components/pages/ExperimentsPage/ExperimentsPage.tsx @@ -97,6 +97,7 @@ const ExperimentsPage: React.FunctionComponent = () => { }, { placeholderData: keepPreviousData, + refetchInterval: 30000, }, ); diff --git a/apps/opik-frontend/src/components/pages/FeedbackDefinitionsPage/FeedbackDefinitionsValueCell.tsx b/apps/opik-frontend/src/components/pages/FeedbackDefinitionsPage/FeedbackDefinitionsValueCell.tsx index 8b53e4b81..5c9ba9c35 100644 --- a/apps/opik-frontend/src/components/pages/FeedbackDefinitionsPage/FeedbackDefinitionsValueCell.tsx +++ b/apps/opik-frontend/src/components/pages/FeedbackDefinitionsPage/FeedbackDefinitionsValueCell.tsx @@ -5,6 +5,7 @@ import { FEEDBACK_DEFINITION_TYPE, FeedbackDefinition, } from "@/types/feedback-definitions"; +import CellWrapper from "@/components/shared/DataTableCells/CellWrapper"; const FeedbackDefinitionsValueCell = ( context: CellContext, @@ -41,9 +42,15 @@ const FeedbackDefinitionsValueCell = ( } return ( -
- {items} -
+ +
+ {items} +
+
); }; diff --git a/apps/opik-frontend/src/components/shared/DataTableCells/CellWrapper.tsx b/apps/opik-frontend/src/components/shared/DataTableCells/CellWrapper.tsx index 49c75ac68..e3d8ac2f3 100644 --- a/apps/opik-frontend/src/components/shared/DataTableCells/CellWrapper.tsx +++ b/apps/opik-frontend/src/components/shared/DataTableCells/CellWrapper.tsx @@ -5,7 +5,7 @@ import { CELL_VERTICAL_ALIGNMENT } from "@/types/shared"; import { cn } from "@/lib/utils"; type CellWrapperProps = { - children: React.ReactNode; + children?: React.ReactNode; metadata?: ColumnMeta; tableMetadata?: TableMeta; className?: string; @@ -24,7 +24,12 @@ const CellWrapper: React.FunctionComponent = ({ return (
{children}
diff --git a/apps/opik-frontend/src/components/shared/DataTableCells/CodeCell.tsx b/apps/opik-frontend/src/components/shared/DataTableCells/CodeCell.tsx index b062f2602..06dfc56a0 100644 --- a/apps/opik-frontend/src/components/shared/DataTableCells/CodeCell.tsx +++ b/apps/opik-frontend/src/components/shared/DataTableCells/CodeCell.tsx @@ -36,7 +36,7 @@ const CodeCell = (context: CellContext) => { {content} diff --git a/apps/opik-frontend/src/components/shared/DataTableCells/FeedbackScoresCell.tsx b/apps/opik-frontend/src/components/shared/DataTableCells/FeedbackScoresCell.tsx index b32243c80..1ad113408 100644 --- a/apps/opik-frontend/src/components/shared/DataTableCells/FeedbackScoresCell.tsx +++ b/apps/opik-frontend/src/components/shared/DataTableCells/FeedbackScoresCell.tsx @@ -20,6 +20,7 @@ const FeedbackScoresCell = (context: CellContext) => {
) => { diff --git a/apps/opik-frontend/src/components/shared/DataTableCells/ListCell.tsx b/apps/opik-frontend/src/components/shared/DataTableCells/ListCell.tsx index f42700762..75d998d4a 100644 --- a/apps/opik-frontend/src/components/shared/DataTableCells/ListCell.tsx +++ b/apps/opik-frontend/src/components/shared/DataTableCells/ListCell.tsx @@ -18,6 +18,7 @@ const ListCell = (context: CellContext) => {
) => { {colored ? ( diff --git a/apps/opik-frontend/src/components/shared/DataTableCells/TextCell.tsx b/apps/opik-frontend/src/components/shared/DataTableCells/TextCell.tsx index a41d0a542..0a558da6c 100644 --- a/apps/opik-frontend/src/components/shared/DataTableCells/TextCell.tsx +++ b/apps/opik-frontend/src/components/shared/DataTableCells/TextCell.tsx @@ -8,7 +8,6 @@ const TextCell = (context: CellContext) => { {value} diff --git a/apps/opik-frontend/src/components/shared/DataTableHeaders/TypeHeader.tsx b/apps/opik-frontend/src/components/shared/DataTableHeaders/TypeHeader.tsx index 88c285f56..991170a57 100644 --- a/apps/opik-frontend/src/components/shared/DataTableHeaders/TypeHeader.tsx +++ b/apps/opik-frontend/src/components/shared/DataTableHeaders/TypeHeader.tsx @@ -34,7 +34,7 @@ export const TypeHeader = ({ return (
e.stopPropagation()} > {Boolean(Icon) && } diff --git a/apps/opik-frontend/src/components/shared/TraceDetailsPanel/TraceDataViewer/FeedbackScoreValueCell.tsx b/apps/opik-frontend/src/components/shared/TraceDetailsPanel/TraceDataViewer/FeedbackScoreValueCell.tsx index 7bc2f6218..866a2d87f 100644 --- a/apps/opik-frontend/src/components/shared/TraceDetailsPanel/TraceDataViewer/FeedbackScoreValueCell.tsx +++ b/apps/opik-frontend/src/components/shared/TraceDetailsPanel/TraceDataViewer/FeedbackScoreValueCell.tsx @@ -19,7 +19,7 @@ const FeedbackScoreValueCell = ( {value} {Reason && Reason} diff --git a/apps/opik-frontend/src/components/ui/switch.tsx b/apps/opik-frontend/src/components/ui/switch.tsx new file mode 100644 index 000000000..786152dc7 --- /dev/null +++ b/apps/opik-frontend/src/components/ui/switch.tsx @@ -0,0 +1,27 @@ +import * as React from "react"; +import * as SwitchPrimitives from "@radix-ui/react-switch"; + +import { cn } from "@/lib/utils"; + +const Switch = React.forwardRef< + React.ElementRef, + React.ComponentPropsWithoutRef +>(({ className, ...props }, ref) => ( + + + +)); +Switch.displayName = SwitchPrimitives.Root.displayName; + +export { Switch }; diff --git a/apps/opik-frontend/src/components/ui/table.tsx b/apps/opik-frontend/src/components/ui/table.tsx index 22f5fcf30..87924465a 100644 --- a/apps/opik-frontend/src/components/ui/table.tsx +++ b/apps/opik-frontend/src/components/ui/table.tsx @@ -74,7 +74,7 @@ const TableHead = React.forwardRef<