From f53a671aeb03b15db09003fc2d4ef5fee1ac022f Mon Sep 17 00:00:00 2001 From: "andrii.dudar" Date: Wed, 13 Nov 2024 10:25:13 +0100 Subject: [PATCH] [OPIK-350] [Prompts Library][FE] Prompts details: Experiments tab (#621) --- .../src/api/datasets/useDatasetsList.ts | 3 + .../src/api/datasets/useExperimentsList.ts | 3 + .../pages/ExperimentsPage/ExperimentsPage.tsx | 108 ++-------- .../ExperimentsActionsPanel.tsx | 2 +- .../ExperimentsFiltersButton.tsx | 0 .../FilterExperimentsToCompareDialog.tsx | 0 .../table.tsx | 91 ++++++-- .../ExperimentsShared/useExpandingConfig.ts | 42 ++++ .../useGroupLimitsConfig.tsx | 42 ++++ .../{PromptTab => CommitsTab}/CommitsTab.tsx | 0 .../ExperimentsTab/ExperimentsTab.tsx | 202 ++++++++++++++++++ .../pages/PromptPage/PromptPage.tsx | 9 +- .../shared/DataTableCells/ResourceCell.tsx | 6 +- .../shared/ResourceLink/ResourceLink.tsx | 26 ++- .../src/hooks/useGroupedExperimentsList.ts | 38 ++-- apps/opik-frontend/src/types/datasets.ts | 7 + 16 files changed, 449 insertions(+), 130 deletions(-) rename apps/opik-frontend/src/components/pages/{ExperimentsPage => ExperimentsShared}/ExperimentsActionsPanel.tsx (98%) rename apps/opik-frontend/src/components/pages/{ExperimentsPage => ExperimentsShared}/ExperimentsFiltersButton.tsx (100%) rename apps/opik-frontend/src/components/pages/{ExperimentsPage => ExperimentsShared}/FilterExperimentsToCompareDialog.tsx (100%) rename apps/opik-frontend/src/components/pages/{ExperimentsPage => ExperimentsShared}/table.tsx (58%) create mode 100644 apps/opik-frontend/src/components/pages/ExperimentsShared/useExpandingConfig.ts create mode 100644 apps/opik-frontend/src/components/pages/ExperimentsShared/useGroupLimitsConfig.tsx rename apps/opik-frontend/src/components/pages/PromptPage/{PromptTab => CommitsTab}/CommitsTab.tsx (100%) create mode 100644 apps/opik-frontend/src/components/pages/PromptPage/ExperimentsTab/ExperimentsTab.tsx diff --git a/apps/opik-frontend/src/api/datasets/useDatasetsList.ts b/apps/opik-frontend/src/api/datasets/useDatasetsList.ts index 0235c7f11..962d5826e 100644 --- a/apps/opik-frontend/src/api/datasets/useDatasetsList.ts +++ b/apps/opik-frontend/src/api/datasets/useDatasetsList.ts @@ -6,6 +6,7 @@ import { Dataset } from "@/types/datasets"; type UseDatasetsListParams = { workspaceName: string; withExperimentsOnly?: boolean; + promptId?: string; search?: string; page: number; size: number; @@ -21,6 +22,7 @@ const getDatasetsList = async ( { workspaceName, withExperimentsOnly, + promptId, search, size, page, @@ -34,6 +36,7 @@ const getDatasetsList = async ( with_experiments_only: withExperimentsOnly, }), ...(search && { name: search }), + ...(promptId && { prompt_id: promptId }), size, page, }, diff --git a/apps/opik-frontend/src/api/datasets/useExperimentsList.ts b/apps/opik-frontend/src/api/datasets/useExperimentsList.ts index 1db925044..bf8e58906 100644 --- a/apps/opik-frontend/src/api/datasets/useExperimentsList.ts +++ b/apps/opik-frontend/src/api/datasets/useExperimentsList.ts @@ -6,6 +6,7 @@ import { Experiment } from "@/types/datasets"; export type UseExperimentsListParams = { workspaceName: string; datasetId?: string; + promptId?: string; datasetDeleted?: boolean; search?: string; page: number; @@ -22,6 +23,7 @@ export const getExperimentsList = async ( { workspaceName, datasetId, + promptId, datasetDeleted, search, size, @@ -35,6 +37,7 @@ export const getExperimentsList = async ( ...(isBoolean(datasetDeleted) && { dataset_deleted: datasetDeleted }), ...(search && { name: search }), ...(datasetId && { datasetId }), + ...(promptId && { prompt_id: promptId }), size, page, }, diff --git a/apps/opik-frontend/src/components/pages/ExperimentsPage/ExperimentsPage.tsx b/apps/opik-frontend/src/components/pages/ExperimentsPage/ExperimentsPage.tsx index 93f339585..e16cc3789 100644 --- a/apps/opik-frontend/src/components/pages/ExperimentsPage/ExperimentsPage.tsx +++ b/apps/opik-frontend/src/components/pages/ExperimentsPage/ExperimentsPage.tsx @@ -1,19 +1,8 @@ -import React, { - useCallback, - useEffect, - useMemo, - useRef, - useState, -} from "react"; +import React, { useCallback, useMemo, useRef, useState } from "react"; import { Info } from "lucide-react"; import { useNavigate } from "@tanstack/react-router"; import useLocalStorageState from "use-local-storage-state"; -import { - ExpandedState, - GroupingState, - RowSelectionState, - Row, -} from "@tanstack/react-table"; +import { RowSelectionState } from "@tanstack/react-table"; import DataTable from "@/components/shared/DataTable/DataTable"; import DataTablePagination from "@/components/shared/DataTablePagination/DataTablePagination"; @@ -29,8 +18,8 @@ import { COLUMN_TYPE, ColumnData } from "@/types/shared"; import { convertColumnDataToColumn } from "@/lib/table"; import ColumnsButton from "@/components/shared/ColumnsButton/ColumnsButton"; import AddExperimentDialog from "@/components/pages/ExperimentsPage/AddExperimentDialog"; -import ExperimentsActionsPanel from "@/components/pages/ExperimentsPage/ExperimentsActionsPanel"; -import ExperimentsFiltersButton from "@/components/pages/ExperimentsPage/ExperimentsFiltersButton"; +import ExperimentsActionsPanel from "@/components/pages/ExperimentsShared/ExperimentsActionsPanel"; +import ExperimentsFiltersButton from "@/components/pages/ExperimentsShared/ExperimentsFiltersButton"; import ExperimentRowActionsCell from "@/components/pages/ExperimentsPage/ExperimentRowActionsCell"; import ExperimentsChartsWrapper from "@/components/pages/ExperimentsPage/charts/ExperimentsChartsWrapper"; import SearchInput from "@/components/shared/SearchInput/SearchInput"; @@ -38,23 +27,23 @@ import { Button } from "@/components/ui/button"; import { Separator } from "@/components/ui/separator"; import useGroupedExperimentsList, { checkIsMoreRowId, - DEFAULT_EXPERIMENTS_PER_GROUP, GroupedExperiment, GROUPING_COLUMN, } from "@/hooks/useGroupedExperimentsList"; import { generateExperimentNameColumDef, generateGroupedCellDef, -} from "@/components/pages/ExperimentsPage/table"; + getIsMoreRow, + getRowId, + GROUPING_CONFIG, +} from "@/components/pages/ExperimentsShared/table"; +import { useExpandingConfig } from "@/components/pages/ExperimentsShared/useExpandingConfig"; +import { useGroupLimitsConfig } from "@/components/pages/ExperimentsShared/useGroupLimitsConfig"; const SELECTED_COLUMNS_KEY = "experiments-selected-columns"; const COLUMNS_WIDTH_KEY = "experiments-columns-width"; const COLUMNS_ORDER_KEY = "experiments-columns-order"; -const getRowId = (e: GroupedExperiment) => e.id; -const getIsMoreRow = (row: Row) => - checkIsMoreRowId(row?.original?.id || ""); - export const DEFAULT_COLUMNS: ColumnData[] = [ { id: "id", @@ -89,8 +78,6 @@ export const DEFAULT_SELECTED_COLUMNS: string[] = [ const ExperimentsPage: React.FunctionComponent = () => { const navigate = useNavigate(); const workspaceName = useAppStore((state) => state.activeWorkspaceName); - - const openGroupsRef = useRef>({}); const resetDialogKeyRef = useRef(0); const [openDialog, setOpenDialog] = useState(false); @@ -99,8 +86,7 @@ const ExperimentsPage: React.FunctionComponent = () => { const [size, setSize] = useState(5); const [datasetId, setDatasetId] = useState(""); const [rowSelection, setRowSelection] = useState({}); - const [expanded, setExpanded] = useState({}); - const [groupLimit, setGroupLimit] = useState>({}); + const { groupLimit, renderMoreRow } = useGroupLimitsConfig(); const { data, isPending } = useGroupedExperimentsList({ workspaceName, @@ -147,7 +133,9 @@ const ExperimentsPage: React.FunctionComponent = () => { const columns = useMemo(() => { return [ - generateExperimentNameColumDef(), + generateExperimentNameColumDef({ + size: columnsWidth["name"], + }), generateGroupedCellDef({ id: GROUPING_COLUMN, label: "Dataset", @@ -178,27 +166,6 @@ const ExperimentsPage: React.FunctionComponent = () => { ]; }, [selectedColumns, columnsWidth, columnsOrder]); - useEffect(() => { - const updateForExpandedState: Record = {}; - groupIds.forEach((groupId) => { - const id = `${GROUPING_COLUMN}:${groupId}`; - if (!openGroupsRef.current[id]) { - openGroupsRef.current[id] = true; - updateForExpandedState[id] = true; - } - }); - - if (Object.keys(updateForExpandedState).length) { - setExpanded((state) => { - if (state === true) return state; - return { - ...state, - ...updateForExpandedState, - }; - }); - } - }, [groupIds]); - const resizeConfig = useMemo( () => ({ enabled: true, @@ -207,22 +174,9 @@ const ExperimentsPage: React.FunctionComponent = () => { [setColumnsWidth], ); - const groupingConfig = useMemo( - () => ({ - groupedColumnMode: false as const, - grouping: [GROUPING_COLUMN] as GroupingState, - }), - [], - ); - - const expandingConfig = useMemo( - () => ({ - autoResetExpanded: false, - expanded, - setExpanded, - }), - [expanded, setExpanded], - ); + const expandingConfig = useExpandingConfig({ + groupIds, + }); const handleNewExperimentClick = useCallback(() => { setOpenDialog(true); @@ -245,32 +199,6 @@ const ExperimentsPage: React.FunctionComponent = () => { [navigate, workspaceName], ); - const renderMoreRow = useCallback((row: Row) => { - return ( - - - - - - ); - }, []); - if (isPending) { return ; } @@ -322,7 +250,7 @@ const ExperimentsPage: React.FunctionComponent = () => { setRowSelection, }} expandingConfig={expandingConfig} - groupingConfig={groupingConfig} + groupingConfig={GROUPING_CONFIG} getRowId={getRowId} noData={ diff --git a/apps/opik-frontend/src/components/pages/ExperimentsPage/ExperimentsActionsPanel.tsx b/apps/opik-frontend/src/components/pages/ExperimentsShared/ExperimentsActionsPanel.tsx similarity index 98% rename from apps/opik-frontend/src/components/pages/ExperimentsPage/ExperimentsActionsPanel.tsx rename to apps/opik-frontend/src/components/pages/ExperimentsShared/ExperimentsActionsPanel.tsx index 8fa3e0504..3604cb524 100644 --- a/apps/opik-frontend/src/components/pages/ExperimentsPage/ExperimentsActionsPanel.tsx +++ b/apps/opik-frontend/src/components/pages/ExperimentsShared/ExperimentsActionsPanel.tsx @@ -5,7 +5,7 @@ import { Button } from "@/components/ui/button"; import { Experiment } from "@/types/datasets"; import { useNavigate } from "@tanstack/react-router"; import useAppStore from "@/store/AppStore"; -import FilterExperimentsToCompareDialog from "@/components/pages/ExperimentsPage/FilterExperimentsToCompareDialog"; +import FilterExperimentsToCompareDialog from "@/components/pages/ExperimentsShared/FilterExperimentsToCompareDialog"; import useExperimentBatchDeleteMutation from "@/api/datasets/useExperimentBatchDeleteMutation"; import ConfirmDialog from "@/components/shared/ConfirmDialog/ConfirmDialog"; import TooltipWrapper from "@/components/shared/TooltipWrapper/TooltipWrapper"; diff --git a/apps/opik-frontend/src/components/pages/ExperimentsPage/ExperimentsFiltersButton.tsx b/apps/opik-frontend/src/components/pages/ExperimentsShared/ExperimentsFiltersButton.tsx similarity index 100% rename from apps/opik-frontend/src/components/pages/ExperimentsPage/ExperimentsFiltersButton.tsx rename to apps/opik-frontend/src/components/pages/ExperimentsShared/ExperimentsFiltersButton.tsx diff --git a/apps/opik-frontend/src/components/pages/ExperimentsPage/FilterExperimentsToCompareDialog.tsx b/apps/opik-frontend/src/components/pages/ExperimentsShared/FilterExperimentsToCompareDialog.tsx similarity index 100% rename from apps/opik-frontend/src/components/pages/ExperimentsPage/FilterExperimentsToCompareDialog.tsx rename to apps/opik-frontend/src/components/pages/ExperimentsShared/FilterExperimentsToCompareDialog.tsx diff --git a/apps/opik-frontend/src/components/pages/ExperimentsPage/table.tsx b/apps/opik-frontend/src/components/pages/ExperimentsShared/table.tsx similarity index 58% rename from apps/opik-frontend/src/components/pages/ExperimentsPage/table.tsx rename to apps/opik-frontend/src/components/pages/ExperimentsShared/table.tsx index df8561729..7e025187b 100644 --- a/apps/opik-frontend/src/components/pages/ExperimentsPage/table.tsx +++ b/apps/opik-frontend/src/components/pages/ExperimentsShared/table.tsx @@ -1,14 +1,43 @@ -import { Checkbox } from "@/components/ui/checkbox"; import React from "react"; -import { CellContext, ColumnDef, flexRender } from "@tanstack/react-table"; +import { Checkbox } from "@/components/ui/checkbox"; +import { + CellContext, + ColumnDef, + flexRender, + GroupingState, + Row, +} from "@tanstack/react-table"; import { ColumnData } from "@/types/shared"; import { Button } from "@/components/ui/button"; import { ChevronDown, ChevronUp, Text } from "lucide-react"; import { mapColumnDataFields } from "@/lib/table"; import { cn } from "@/lib/utils"; import CellWrapper from "@/components/shared/DataTableCells/CellWrapper"; +import { + checkIsMoreRowId, + GroupedExperiment, + GROUPING_COLUMN, +} from "@/hooks/useGroupedExperimentsList"; +import ResourceLink, { + RESOURCE_TYPE, +} from "@/components/shared/ResourceLink/ResourceLink"; + +export const GROUPING_CONFIG = { + groupedColumnMode: false as const, + grouping: [GROUPING_COLUMN] as GroupingState, +}; + +export const getRowId = (e: GroupedExperiment) => e.id; +export const getIsMoreRow = (row: Row) => + checkIsMoreRowId(row?.original?.id || ""); -export const generateExperimentNameColumDef = () => { +export const generateExperimentNameColumDef = ({ + size, + asResource = false, +}: { + size?: number; + asResource?: boolean; +}) => { return { accessorKey: "name", header: ({ table }) => ( @@ -28,25 +57,43 @@ export const generateExperimentNameColumDef = () => { Name ), - cell: (context) => ( - - event.stopPropagation()} - checked={context.row.getIsSelected()} - disabled={!context.row.getCanSelect()} - onCheckedChange={(value) => context.row.toggleSelected(!!value)} - aria-label="Select row" - /> - {context.getValue() as string} - - ), - size: 180, + cell: (context) => { + const data = context.row.original as GroupedExperiment; + return ( + + event.stopPropagation()} + checked={context.row.getIsSelected()} + disabled={!context.row.getCanSelect()} + onCheckedChange={(value) => context.row.toggleSelected(!!value)} + aria-label="Select row" + /> + {asResource ? ( +
+ +
+ ) : ( + + {context.getValue() as string} + + )} +
+ ); + }, + size: size ?? 180, minSize: 100, enableSorting: false, enableHiding: false, diff --git a/apps/opik-frontend/src/components/pages/ExperimentsShared/useExpandingConfig.ts b/apps/opik-frontend/src/components/pages/ExperimentsShared/useExpandingConfig.ts new file mode 100644 index 000000000..998057d97 --- /dev/null +++ b/apps/opik-frontend/src/components/pages/ExperimentsShared/useExpandingConfig.ts @@ -0,0 +1,42 @@ +import { useEffect, useMemo, useRef, useState } from "react"; +import { ExpandedState } from "@tanstack/react-table"; +import { GROUPING_COLUMN } from "@/hooks/useGroupedExperimentsList"; + +export type UseExpandingConfigProps = { + groupIds: string[]; +}; + +export const useExpandingConfig = ({ groupIds }: UseExpandingConfigProps) => { + const openGroupsRef = useRef>({}); + const [expanded, setExpanded] = useState({}); + + useEffect(() => { + const updateForExpandedState: Record = {}; + groupIds.forEach((groupId) => { + const id = `${GROUPING_COLUMN}:${groupId}`; + if (!openGroupsRef.current[id]) { + openGroupsRef.current[id] = true; + updateForExpandedState[id] = true; + } + }); + + if (Object.keys(updateForExpandedState).length) { + setExpanded((state) => { + if (state === true) return state; + return { + ...state, + ...updateForExpandedState, + }; + }); + } + }, [groupIds]); + + return useMemo( + () => ({ + autoResetExpanded: false, + expanded, + setExpanded, + }), + [expanded, setExpanded], + ); +}; diff --git a/apps/opik-frontend/src/components/pages/ExperimentsShared/useGroupLimitsConfig.tsx b/apps/opik-frontend/src/components/pages/ExperimentsShared/useGroupLimitsConfig.tsx new file mode 100644 index 000000000..f3f700eb7 --- /dev/null +++ b/apps/opik-frontend/src/components/pages/ExperimentsShared/useGroupLimitsConfig.tsx @@ -0,0 +1,42 @@ +import React, { useCallback, useState } from "react"; +import { Row } from "@tanstack/react-table"; +import { + DEFAULT_EXPERIMENTS_PER_GROUP, + GroupedExperiment, +} from "@/hooks/useGroupedExperimentsList"; +import { Button } from "@/components/ui/button"; + +export const useGroupLimitsConfig = () => { + const [groupLimit, setGroupLimit] = useState>({}); + + const renderMoreRow = useCallback((row: Row) => { + return ( + + + + + + ); + }, []); + + return { + groupLimit, + renderMoreRow, + }; +}; diff --git a/apps/opik-frontend/src/components/pages/PromptPage/PromptTab/CommitsTab.tsx b/apps/opik-frontend/src/components/pages/PromptPage/CommitsTab/CommitsTab.tsx similarity index 100% rename from apps/opik-frontend/src/components/pages/PromptPage/PromptTab/CommitsTab.tsx rename to apps/opik-frontend/src/components/pages/PromptPage/CommitsTab/CommitsTab.tsx diff --git a/apps/opik-frontend/src/components/pages/PromptPage/ExperimentsTab/ExperimentsTab.tsx b/apps/opik-frontend/src/components/pages/PromptPage/ExperimentsTab/ExperimentsTab.tsx new file mode 100644 index 000000000..01c088534 --- /dev/null +++ b/apps/opik-frontend/src/components/pages/PromptPage/ExperimentsTab/ExperimentsTab.tsx @@ -0,0 +1,202 @@ +import React, { useMemo, useState } from "react"; +import { RowSelectionState } from "@tanstack/react-table"; +import useLocalStorageState from "use-local-storage-state"; +import get from "lodash/get"; + +import Loader from "@/components/shared/Loader/Loader"; +import SearchInput from "@/components/shared/SearchInput/SearchInput"; +import ExperimentsFiltersButton from "@/components/pages/ExperimentsShared/ExperimentsFiltersButton"; +import ExperimentsActionsPanel from "@/components/pages/ExperimentsShared/ExperimentsActionsPanel"; +import DataTable from "@/components/shared/DataTable/DataTable"; +import DataTableNoData from "@/components/shared/DataTableNoData/DataTableNoData"; +import DataTablePagination from "@/components/shared/DataTablePagination/DataTablePagination"; +import ResourceCell from "@/components/shared/DataTableCells/ResourceCell"; +import FeedbackScoresCell from "@/components/shared/DataTableCells/FeedbackScoresCell"; +import useAppStore from "@/store/AppStore"; +import useGroupedExperimentsList, { + checkIsMoreRowId, + GroupedExperiment, + GROUPING_COLUMN, +} from "@/hooks/useGroupedExperimentsList"; +import { + generateExperimentNameColumDef, + generateGroupedCellDef, + getIsMoreRow, + getRowId, + GROUPING_CONFIG, +} from "@/components/pages/ExperimentsShared/table"; +import { COLUMN_TYPE, ColumnData } from "@/types/shared"; +import { formatDate } from "@/lib/date"; +import { RESOURCE_TYPE } from "@/components/shared/ResourceLink/ResourceLink"; +import { useExpandingConfig } from "@/components/pages/ExperimentsShared/useExpandingConfig"; +import { useGroupLimitsConfig } from "@/components/pages/ExperimentsShared/useGroupLimitsConfig"; +import { convertColumnDataToColumn } from "@/lib/table"; + +const COLUMNS_WIDTH_KEY = "prompt-experiments-columns-width"; + +export const DEFAULT_COLUMNS: ColumnData[] = [ + { + id: "id", + label: "Prompt commit", + type: COLUMN_TYPE.string, + cell: ResourceCell as never, + customMeta: { + nameKey: "prompt_version.commit", + idKey: "prompt_version.prompt_id", + resource: RESOURCE_TYPE.prompt, + getSearch: (data: GroupedExperiment) => ({ + activeVersionId: get(data, "prompt_version.id", null), + }), + }, + }, + { + id: "created_at", + label: "Created", + type: COLUMN_TYPE.time, + accessorFn: (row) => formatDate(row.created_at), + }, + { + id: "feedback_scores", + label: "Feedback scores (average)", + type: COLUMN_TYPE.numberDictionary, + cell: FeedbackScoresCell as never, + }, +]; + +interface ExperimentsTabProps { + promptId: string; +} + +const ExperimentsTab: React.FC = ({ promptId }) => { + const workspaceName = useAppStore((state) => state.activeWorkspaceName); + const [search, setSearch] = useState(""); + const [page, setPage] = useState(1); + const [size, setSize] = useState(5); + const [datasetId, setDatasetId] = useState(""); + const [rowSelection, setRowSelection] = useState({}); + const { groupLimit, renderMoreRow } = useGroupLimitsConfig(); + + const { data, isPending } = useGroupedExperimentsList({ + workspaceName, + groupLimit, + datasetId, + promptId, + search, + page, + size, + }); + + const experiments = useMemo(() => data?.content ?? [], [data?.content]); + const groupIds = useMemo(() => data?.groupIds ?? [], [data?.groupIds]); + const total = data?.total ?? 0; + const noData = !search && !datasetId; + const noDataText = noData + ? "There are no experiments used this prompt" + : "No search results"; + + const [columnsWidth, setColumnsWidth] = useLocalStorageState< + Record + >(COLUMNS_WIDTH_KEY, { + defaultValue: {}, + }); + + const selectedRows: Array = useMemo(() => { + return experiments.filter( + (row) => rowSelection[row.id] && !checkIsMoreRowId(row.id), + ); + }, [rowSelection, experiments]); + + const columns = useMemo(() => { + return [ + generateExperimentNameColumDef({ + size: columnsWidth["name"], + asResource: true, + }), + generateGroupedCellDef({ + id: GROUPING_COLUMN, + label: "Dataset", + type: COLUMN_TYPE.string, + cell: ResourceCell as never, + customMeta: { + nameKey: "dataset_name", + idKey: "dataset_id", + resource: RESOURCE_TYPE.dataset, + }, + }), + ...convertColumnDataToColumn( + DEFAULT_COLUMNS, + { + columnsWidth, + }, + ), + ]; + }, [columnsWidth]); + + const resizeConfig = useMemo( + () => ({ + enabled: true, + onColumnResize: setColumnsWidth, + }), + [setColumnsWidth], + ); + + const expandingConfig = useExpandingConfig({ + groupIds, + }); + + if (isPending) { + return ; + } + + return ( +
+
+

Experiments

+
+
+
+ + +
+
+ +
+
+ } + /> +
+ +
+
+ ); +}; + +export default ExperimentsTab; diff --git a/apps/opik-frontend/src/components/pages/PromptPage/PromptPage.tsx b/apps/opik-frontend/src/components/pages/PromptPage/PromptPage.tsx index 981aba1c2..73bf1caa5 100644 --- a/apps/opik-frontend/src/components/pages/PromptPage/PromptPage.tsx +++ b/apps/opik-frontend/src/components/pages/PromptPage/PromptPage.tsx @@ -7,7 +7,8 @@ import { usePromptIdFromURL } from "@/hooks/usePromptIdFromURL"; import usePromptById from "@/api/prompts/usePromptById"; import DateTag from "@/components/shared/DateTag/DateTag"; import PromptTab from "@/components/pages/PromptPage/PromptTab/PromptTab"; -import CommitsTab from "@/components/pages/PromptPage/PromptTab/CommitsTab"; +import CommitsTab from "@/components/pages/PromptPage/CommitsTab/CommitsTab"; +import ExperimentsTab from "@/components/pages/PromptPage/ExperimentsTab/ExperimentsTab"; const PromptPage: React.FunctionComponent = () => { const [tab, setTab] = useQueryParam("tab", StringParam); @@ -49,6 +50,9 @@ const PromptPage: React.FunctionComponent = () => { Prompt + + Experiments + Commits @@ -56,6 +60,9 @@ const PromptPage: React.FunctionComponent = () => { + + + diff --git a/apps/opik-frontend/src/components/shared/DataTableCells/ResourceCell.tsx b/apps/opik-frontend/src/components/shared/DataTableCells/ResourceCell.tsx index 823145d5b..b1ba9db65 100644 --- a/apps/opik-frontend/src/components/shared/DataTableCells/ResourceCell.tsx +++ b/apps/opik-frontend/src/components/shared/DataTableCells/ResourceCell.tsx @@ -1,5 +1,6 @@ import React from "react"; import get from "lodash/get"; +import isFunction from "lodash/isFunction"; import { CellContext } from "@tanstack/react-table"; import CellWrapper from "@/components/shared/DataTableCells/CellWrapper"; @@ -12,6 +13,7 @@ type CustomMeta = { nameKey?: string; idKey?: string; resource: RESOURCE_TYPE; + getSearch?: (cellData: unknown) => Record; }; const ResourceCell = (context: CellContext) => { @@ -21,10 +23,12 @@ const ResourceCell = (context: CellContext) => { resource, nameKey = "name", idKey = "id", + getSearch, } = (custom ?? {}) as CustomMeta; const name = get(cellData, nameKey, undefined); const id = get(cellData, idKey, undefined); + const search = isFunction(getSearch) ? getSearch(cellData) : undefined; if (!id) return null; @@ -33,7 +37,7 @@ const ResourceCell = (context: CellContext) => { metadata={context.column.columnDef.meta} tableMetadata={context.table.options.meta} > - + ); }; diff --git a/apps/opik-frontend/src/components/shared/ResourceLink/ResourceLink.tsx b/apps/opik-frontend/src/components/shared/ResourceLink/ResourceLink.tsx index fd9e38f67..4a27e65a8 100644 --- a/apps/opik-frontend/src/components/shared/ResourceLink/ResourceLink.tsx +++ b/apps/opik-frontend/src/components/shared/ResourceLink/ResourceLink.tsx @@ -1,15 +1,22 @@ import React from "react"; import { Link } from "@tanstack/react-router"; -import { ArrowUpRight, Database } from "lucide-react"; +import { + ArrowUpRight, + Database, + FileTerminal, + FlaskConical, +} from "lucide-react"; +import isUndefined from "lodash/isUndefined"; import { cn } from "@/lib/utils"; import TooltipWrapper from "@/components/shared/TooltipWrapper/TooltipWrapper"; import useAppStore from "@/store/AppStore"; import { Tag } from "@/components/ui/tag"; -import isUndefined from "lodash/isUndefined"; export enum RESOURCE_TYPE { dataset, + prompt, + experiment, } const RESOURCE_MAP = { @@ -19,18 +26,32 @@ const RESOURCE_MAP = { param: "datasetId", deleted: "Dataset deleted", }, + [RESOURCE_TYPE.prompt]: { + url: "/$workspaceName/prompts/$promptId", + icon: FileTerminal, + param: "promptId", + deleted: "Prompt deleted", + }, + [RESOURCE_TYPE.experiment]: { + url: "/$workspaceName/experiments/$datasetId/compare", + icon: FlaskConical, + param: "datasetId", + deleted: "Experiment deleted", + }, }; type ResourceLinkProps = { name?: string; id: string; resource: RESOURCE_TYPE; + search?: Record; }; const ResourceLink: React.FunctionComponent = ({ resource, name, id, + search, }) => { const workspaceName = useAppStore((state) => state.activeWorkspaceName); const props = RESOURCE_MAP[resource]; @@ -46,6 +67,7 @@ const ResourceLink: React.FunctionComponent = ({ event.stopPropagation()} className={cn("max-w-full", deleted && "opacity-50 cursor-default")} disabled={deleted} diff --git a/apps/opik-frontend/src/hooks/useGroupedExperimentsList.ts b/apps/opik-frontend/src/hooks/useGroupedExperimentsList.ts index 137bc466a..53b930cb8 100644 --- a/apps/opik-frontend/src/hooks/useGroupedExperimentsList.ts +++ b/apps/opik-frontend/src/hooks/useGroupedExperimentsList.ts @@ -14,7 +14,6 @@ import useExperimentsList, { } from "@/api/datasets/useExperimentsList"; import useDatasetById from "@/api/datasets/useDatasetById"; -const RE_FETCH_INTERVAL = 30000; export const DELETED_DATASET_ID = "deleted_dataset_id"; export const DEFAULT_EXPERIMENTS_PER_GROUP = 25; export const GROUPING_COLUMN = "virtual_dataset_id"; @@ -27,10 +26,12 @@ export type GroupedExperiment = { type UseGroupedExperimentsListParams = { workspaceName: string; datasetId?: string; + promptId?: string; search?: string; page: number; size: number; groupLimit?: Record; + pooling?: boolean; }; type UseGroupedExperimentsListResponse = { @@ -81,22 +82,24 @@ const generateMoreRow = (dataset: Dataset) => { export default function useGroupedExperimentsList( params: UseGroupedExperimentsListParams, ) { + const refetchInterval = params.pooling ? 30000 : undefined; const experimentsCache = useRef>( {}, ); - const hasDataset = Boolean(params.datasetId); + const isFilteredByDataset = Boolean(params.datasetId); const { data: deletedDatasetExperiments } = useExperimentsList( { workspaceName: params.workspaceName, search: params.search, datasetDeleted: true, + promptId: params.promptId, page: 1, size: extractPageSize(DELETED_DATASET_ID, params?.groupLimit), }, { placeholderData: keepPreviousData, - refetchInterval: RE_FETCH_INTERVAL, + refetchInterval, }, ); @@ -104,7 +107,7 @@ export default function useGroupedExperimentsList( { datasetId: params.datasetId!, }, - { enabled: hasDataset }, + { enabled: isFilteredByDataset }, ); const hasRemovedDatasetExperiments = @@ -117,27 +120,35 @@ export default function useGroupedExperimentsList( page: params.page, size: params.size, withExperimentsOnly: true, + promptId: params.promptId, }, { placeholderData: keepPreviousData, - refetchInterval: RE_FETCH_INTERVAL, - enabled: !hasDataset, + refetchInterval, + enabled: !isFilteredByDataset, } as never, ); const datasetsData = useMemo(() => { - return (hasDataset && dataset ? [dataset] : datasetsRowData?.content) || []; - }, [dataset, hasDataset, datasetsRowData?.content]); + return ( + (isFilteredByDataset && dataset ? [dataset] : datasetsRowData?.content) || + [] + ); + }, [dataset, isFilteredByDataset, datasetsRowData?.content]); const total = useMemo(() => { const totalDatasets = datasetsRowData?.total || 0; - return hasDataset + return isFilteredByDataset ? 1 : hasRemovedDatasetExperiments ? totalDatasets + 1 : totalDatasets; - }, [datasetsRowData?.total, hasDataset, hasRemovedDatasetExperiments]); + }, [ + datasetsRowData?.total, + isFilteredByDataset, + hasRemovedDatasetExperiments, + ]); const datasetsIds = useMemo(() => { return datasetsData.map(({ id }) => id); @@ -149,6 +160,7 @@ export default function useGroupedExperimentsList( workspaceName: params.workspaceName, search: params.search, datasetId, + promptId: params.promptId, page: 1, size: extractPageSize(datasetId, params?.groupLimit), }; @@ -157,14 +169,14 @@ export default function useGroupedExperimentsList( queryKey: ["experiments", p], queryFn: (context: QueryFunctionContext) => getExperimentsList(context, p), - refetchInterval: RE_FETCH_INTERVAL, + refetchInterval, }; }), }); const needToShowDeletedDataset = hasRemovedDatasetExperiments && - !hasDataset && + !isFilteredByDataset && Math.ceil(total / params.size) === params.page; const deletedDatasetGroupExperiments = useMemo(() => { @@ -249,7 +261,7 @@ export default function useGroupedExperimentsList( ]); const isPending = - (hasDataset ? isDatasetPending : isDatasetsPending) || + (isFilteredByDataset ? isDatasetPending : isDatasetsPending) || (experimentsResponse.length > 0 && experimentsResponse.every((r) => r.isPending) && data.content.length === 0); diff --git a/apps/opik-frontend/src/types/datasets.ts b/apps/opik-frontend/src/types/datasets.ts index 91e2cbf9d..5b4ae48f4 100644 --- a/apps/opik-frontend/src/types/datasets.ts +++ b/apps/opik-frontend/src/types/datasets.ts @@ -39,6 +39,12 @@ export interface AverageFeedbackScore { value: number; } +export interface ExperimentPromptVersion { + id: string; + commit: string; + prompt_id: string; +} + export interface Experiment { id: string; dataset_id: string; @@ -46,6 +52,7 @@ export interface Experiment { metadata?: object; name: string; feedback_scores?: AverageFeedbackScore[]; + prompt_version?: ExperimentPromptVersion; trace_count: number; created_at: string; last_updated_at: string;