Skip to content

Commit

Permalink
[OPIK-91] [UX improvements] Implement the Experiment configuration se…
Browse files Browse the repository at this point in the history
…ction in the compare page (#252)
  • Loading branch information
andriidudar authored Sep 16, 2024
1 parent ac91bf0 commit d3f9415
Show file tree
Hide file tree
Showing 6 changed files with 248 additions and 24 deletions.
Original file line number Diff line number Diff line change
@@ -0,0 +1,229 @@
import React, { useEffect, useMemo } from "react";
import sortBy from "lodash/sortBy";
import {
ArrowUpRight,
Clock,
Database,
FlaskConical,
Maximize2,
Minimize2,
PenLine,
} from "lucide-react";

import useAppStore from "@/store/AppStore";
import useBreadcrumbsStore from "@/store/BreadcrumbsStore";
import FeedbackScoreTag from "@/components/shared/FeedbackScoreTag/FeedbackScoreTag";
import { Experiment } from "@/types/datasets";
import { TableBody, TableCell, TableRow } from "@/components/ui/table";
import { Tag } from "@/components/ui/tag";
import { Link } from "@tanstack/react-router";
import { formatDate } from "@/lib/date";
import uniq from "lodash/uniq";
import isUndefined from "lodash/isUndefined";
import { Button } from "@/components/ui/button";
import { BooleanParam, useQueryParam } from "use-query-params";

type CompareExperimentsDetailsProps = {
experimentsIds: string[];
experiments: Experiment[];
};

const CompareExperimentsDetails: React.FunctionComponent<
CompareExperimentsDetailsProps
> = ({ experiments, experimentsIds }) => {
const workspaceName = useAppStore((state) => state.activeWorkspaceName);
const setBreadcrumbParam = useBreadcrumbsStore((state) => state.setParam);

const isCompare = experimentsIds.length > 1;

const experiment = experiments[0];

const title = !isCompare
? experiment?.name
: `Compare (${experimentsIds.length})`;

const [showCompareFeedback = false, setShowCompareFeedback] = useQueryParam(
"scoreTable",
BooleanParam,
{
updateType: "replaceIn",
},
);

useEffect(() => {
title && setBreadcrumbParam("compare", "compare", title);
return () => setBreadcrumbParam("compare", "compare", "");
}, [title, setBreadcrumbParam]);

const scoreMap = useMemo(() => {
return !isCompare
? {}
: experiments.reduce<Record<string, Record<string, number>>>((acc, e) => {
acc[e.id] = (e.feedback_scores || [])?.reduce<Record<string, number>>(
(a, f) => {
a[f.name] = f.value;
return a;
},
{},
);

return acc;
}, {});
}, [isCompare, experiments]);

const scoreColumns = useMemo(() => {
return uniq(
Object.values(scoreMap).reduce<string[]>(
(acc, m) => acc.concat(Object.keys(m)),
[],
),
).sort();
}, [scoreMap]);

const renderCompareFeedbackScoresButton = () => {
if (!isCompare) return null;

const text = showCompareFeedback
? "Collapse feedback scores"
: "Expand feedback scores";
const Icon = showCompareFeedback ? Minimize2 : Maximize2;

return (
<Button
variant="outline"
onClick={() => {
setShowCompareFeedback(!showCompareFeedback);
}}
>
<Icon className="mr-2 size-4 shrink-0" />
{text}
</Button>
);
};

const renderSubSection = () => {
if (isCompare) {
const tag =
experimentsIds.length === 2 ? (
<Tag size="lg" variant="gray" className="flex items-center gap-2">
<FlaskConical className="size-4 shrink-0" />
<div className="truncate">{experiments[1]?.name}</div>
</Tag>
) : (
<Tag size="lg" variant="gray">
{`${experimentsIds.length - 1} experiments`}
</Tag>
);

return (
<div className="flex items-center gap-2">
Baseline of
<Tag size="lg" variant="gray" className="flex items-center gap-2">
<FlaskConical className="size-4 shrink-0" />
<div className="truncate">{experiment?.name}</div>
</Tag>
compared against
{tag}
</div>
);
} else {
return (
<div className="flex items-center gap-2">
<PenLine className="size-4 shrink-0" />
<div className="flex gap-1 overflow-x-auto">
{sortBy(experiment?.feedback_scores ?? [], "name").map(
(feedbackScore) => {
return (
<FeedbackScoreTag
key={feedbackScore.name + feedbackScore.value}
label={feedbackScore.name}
value={feedbackScore.value}
/>
);
},
)}
</div>
</div>
);
}
};

const renderCompareFeedbackScores = () => {
if (!isCompare || !showCompareFeedback) return null;

return (
<div className="mt-4 max-h-[227px] overflow-auto rounded-md border">
{experiments.length ? (
<table className="min-w-full table-fixed caption-bottom text-sm">
<TableBody>
{experiments.map((e) => (
<TableRow key={e.id}>
<TableCell>
<div className="flex h-14 min-w-20 items-center truncate p-2">
{e.name}
</div>
</TableCell>
{scoreColumns.map((id) => {
const value = scoreMap[e.id]?.[id];

return (
<TableCell key={id}>
<div className="flex h-14 min-w-20 items-center truncate p-2">
{isUndefined(value) ? (
"–"
) : (
<FeedbackScoreTag
key={id + value}
label={id}
value={value}
/>
)}
</div>
</TableCell>
);
})}
</TableRow>
))}
</TableBody>
</table>
) : (
<div className="flex h-28 items-center justify-center text-muted-slate">
No feedback scores for selected experiments
</div>
)}
</div>
);
};

return (
<div className="py-6">
<div className="mb-4 flex items-center justify-between">
<h1 className="comet-title-l">{title}</h1>
{renderCompareFeedbackScoresButton()}
</div>
<div className="mb-2 flex gap-4 overflow-x-auto">
{!isCompare && (
<Tag size="lg" variant="gray" className="flex items-center gap-2">
<Clock className="size-4 shrink-0" />
<div className="truncate">{formatDate(experiment?.created_at)}</div>
</Tag>
)}
<Link
to={"/$workspaceName/datasets/$datasetId/items"}
params={{ workspaceName, datasetId: experiment?.dataset_id }}
className="max-w-full"
>
<Tag size="lg" variant="gray" className="flex items-center gap-2">
<Database className="size-4 shrink-0" />
<div className="truncate">{experiment?.dataset_name}</div>
<ArrowUpRight className="size-4 shrink-0" />
</Tag>
</Link>
</div>
{renderSubSection()}
{renderCompareFeedbackScores()}
</div>
);
};

export default CompareExperimentsDetails;
Original file line number Diff line number Diff line change
@@ -1,18 +1,16 @@
import React, { useEffect } from "react";
import React 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 CompareExperimentsDetails from "@/components/pages/CompareExperimentsPage/CompareExperimentsDetails";
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";
import useDeepMemo from "@/hooks/useDeepMemo";
import { Experiment } from "@/types/datasets";

const CompareExperimentsPage: React.FunctionComponent = () => {
const setBreadcrumbParam = useBreadcrumbsStore((state) => state.setParam);

const [tab = "items", setTab] = useQueryParam("tab", StringParam, {
updateType: "replaceIn",
});
Expand All @@ -21,8 +19,6 @@ const CompareExperimentsPage: React.FunctionComponent = () => {
updateType: "replaceIn",
});

const isCompare = experimentsIds.length > 1;

const response = useExperimentsByIds({
experimentsIds,
});
Expand All @@ -40,22 +36,12 @@ const CompareExperimentsPage: React.FunctionComponent = () => {
return experiments ?? [];
}, [experiments]);

const experiment = memorizedExperiments[0];

const title = !isCompare
? experiment?.name
: `Compare (${experimentsIds.length})`;

useEffect(() => {
title && setBreadcrumbParam("compare", "compare", title);
return () => setBreadcrumbParam("compare", "compare", "");
}, [title, setBreadcrumbParam]);

return (
<div className="pt-6">
<div className="mb-4 flex items-center justify-between">
<h1 className="comet-title-l">{title}</h1>
</div>
<div>
<CompareExperimentsDetails
experimentsIds={experimentsIds}
experiments={memorizedExperiments}
/>
<Tabs defaultValue="input" value={tab as string} onValueChange={setTab}>
<TabsList variant="underline">
<TabsTrigger variant="underline" value="items">
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -26,7 +26,9 @@ const CompareConfigCell: React.FunctionComponent<
rowHeightClass: "min-h-14",
}}
>
<div className="max-w-full overflow-hidden">{String(data)}</div>
<div className="max-w-full overflow-hidden break-words">
{String(data)}
</div>
</CellWrapper>
);
};
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -80,6 +80,7 @@ const ConfigurationTab: React.FunctionComponent<ConfigurationTabProps> = ({
header: CompareExperimentsHeader as never,
cell: CompareConfigCell as never,
size,
minSize: 120,
});
});

Expand Down Expand Up @@ -146,7 +147,9 @@ const ConfigurationTab: React.FunctionComponent<ConfigurationTabProps> = ({

const noDataText = search
? "No search results"
: "There is no data for the selected experiments";
: isCompare
? "These experiments have no configuration"
: "This experiment has no configuration";

const resizeConfig = useMemo(
() => ({
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -180,6 +180,7 @@ const ExperimentItemsTab: React.FunctionComponent<ExperimentItemsTabProps> = ({
},
},
size,
minSize: 120,
});
});

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -2,14 +2,17 @@ import React from "react";

type DataTableNoDataProps = {
title: string;
children?: React.ReactNode;
};

const DataTableNoData: React.FunctionComponent<DataTableNoDataProps> = ({
title,
children,
}) => {
return (
<div className="flex h-28 items-center justify-center">
<div className="flex min-h-28 flex-col items-center justify-center">
<span className="text-muted-slate">{title}</span>
<div className="flex flex-col">{children}</div>
</div>
);
};
Expand Down

0 comments on commit d3f9415

Please sign in to comment.