Skip to content
Merged
Show file tree
Hide file tree
Changes from 2 commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
52 changes: 52 additions & 0 deletions apps/opik-frontend/src/api/prompts/getPromptVersionsById.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,52 @@
import { QueryFunctionContext } from "@tanstack/react-query";
import api, { PROMPTS_REST_ENDPOINT } from "@/api/api";
import { PromptVersion } from "@/types/prompts";
import { Sorting } from "@/types/sorting";
import { processSorting } from "@/lib/sorting";
import { Filter } from "@/types/filters";
import { processFilters } from "@/lib/filters";

export type GetPromptVersionsByIdParams = {
promptId: string;
page: number;
size: number;
sorting?: Sorting;
filters?: Filter[];
search?: string;
};

export type PromptVersionsByIdResponse = {
content: PromptVersion[];
page: number;
size: number;
total: number;
sortable_by: string[];
};

export const getPromptVersionsById = async (
{ signal }: QueryFunctionContext,
{
promptId,
size,
page,
sorting,
filters,
search,
}: GetPromptVersionsByIdParams,
): Promise<PromptVersionsByIdResponse> => {
const { data } = await api.get(
`${PROMPTS_REST_ENDPOINT}${promptId}/versions`,
{
signal,
params: {
...processFilters(filters),
...processSorting(sorting),
size,
page,
...(search && { search }),
},
},
);

return data;
};
63 changes: 9 additions & 54 deletions apps/opik-frontend/src/api/prompts/usePromptVersionsById.ts
Original file line number Diff line number Diff line change
@@ -1,59 +1,14 @@
import { QueryFunctionContext, useQuery } from "@tanstack/react-query";
import api, { PROMPTS_REST_ENDPOINT, QueryConfig } from "@/api/api";
import { PromptVersion } from "@/types/prompts";
import { Sorting } from "@/types/sorting";
import { processSorting } from "@/lib/sorting";
import { Filter } from "@/types/filters";
import { processFilters } from "@/lib/filters";

type UsePromptVersionsByIdParams = {
promptId: string;
page: number;
size: number;
sorting?: Sorting;
filters?: Filter[];
search?: string;
};

type UsePromptsVersionsByIdResponse = {
content: PromptVersion[];
page: number;
size: number;
total: number;
sortable_by: string[];
};

const getPromptVersionsById = async (
{ signal }: QueryFunctionContext,
{
promptId,
size,
page,
sorting,
filters,
search,
}: UsePromptVersionsByIdParams,
) => {
const { data } = await api.get(
`${PROMPTS_REST_ENDPOINT}${promptId}/versions`,
{
signal,
params: {
...processFilters(filters),
...processSorting(sorting),
size,
page,
...(search && { search }),
},
},
);

return data;
};
import { useQuery } from "@tanstack/react-query";
import { QueryConfig } from "@/api/api";
import {
getPromptVersionsById,
GetPromptVersionsByIdParams,
PromptVersionsByIdResponse,
} from "./getPromptVersionsById";

export default function usePromptVersionsById(
params: UsePromptVersionsByIdParams,
options?: QueryConfig<UsePromptsVersionsByIdResponse>,
params: GetPromptVersionsByIdParams,
options?: QueryConfig<PromptVersionsByIdResponse>,
) {
return useQuery({
queryKey: ["prompt-versions", params],
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,55 @@
import { useInfiniteQuery } from "@tanstack/react-query";
import { Sorting } from "@/types/sorting";
import { Filter } from "@/types/filters";
import {
getPromptVersionsById,
PromptVersionsByIdResponse,
} from "./getPromptVersionsById";

const PAGE_SIZE = 25;

type UsePromptVersionsByIdInfiniteParams = {
promptId: string;
sorting?: Sorting;
filters?: Filter[];
search?: string;
};

type UsePromptVersionsByIdInfiniteOptions = {
enabled?: boolean;
refetchInterval?: number;
};

export default function usePromptVersionsByIdInfinite(
params: UsePromptVersionsByIdInfiniteParams,
options?: UsePromptVersionsByIdInfiniteOptions,
) {
return useInfiniteQuery<PromptVersionsByIdResponse>({
// Shares the "prompt-versions" key prefix (and the same
// `{ promptId, ... }` params shape) with usePromptVersionsById so the
// mutation hooks that invalidate that prefix (create/delete/deploy a
// version) also invalidate this list — otherwise the sidebar goes stale
// after every write. Doesn't collide in the cache: this hook's params
// never include `page`/`size`, which usePromptVersionsById always does.
queryKey: ["prompt-versions", params],
queryFn: (context) =>
getPromptVersionsById(context, {
...params,
size: PAGE_SIZE,
page: context.pageParam as number,
}),
// `size` on the response is the actual item count returned (not the
// requested page size), so it shrinks on the last page and hits 0 past
// it — `page * size` is not a valid "items seen so far" once that
// happens. Sum each page's real content length instead.
getNextPageParam: (lastPage, allPages) => {
const fetchedCount = allPages.reduce(
(sum, p) => sum + p.content.length,
0,
);
return fetchedCount < lastPage.total ? allPages.length + 1 : undefined;
Comment thread
natagh23 marked this conversation as resolved.
Comment thread
natagh23 marked this conversation as resolved.
},
initialPageParam: 1,
...options,
});
}
Original file line number Diff line number Diff line change
@@ -1,5 +1,5 @@
import React from "react";
import { Clock, GitCompareArrows } from "lucide-react";
import { Clock, GitCompareArrows, Loader2 } from "lucide-react";

import { getTimeFromNow } from "@/lib/date";
import { Button } from "@/ui/button";
Expand All @@ -20,18 +20,22 @@ interface DiffVersionMenuProps {
versions: VersionHistoryItem[];
onSelectVersion: (item: VersionHistoryItem) => void;
triggerLabel?: string;
onOpenChange?: (open: boolean) => void;
isLoadingMore?: boolean;
}

const DiffVersionMenu: React.FC<DiffVersionMenuProps> = ({
currentItemId,
versions,
onSelectVersion,
triggerLabel = "Show diff",
onOpenChange,
isLoadingMore = false,
}) => {
const selectableVersions = versions.filter((v) => v.id !== currentItemId);

return (
<DropdownMenu>
<DropdownMenu onOpenChange={onOpenChange}>
<DropdownMenuTrigger asChild>
<Button
size="sm"
Expand Down Expand Up @@ -73,6 +77,11 @@ const DiffVersionMenu: React.FC<DiffVersionMenuProps> = ({
</span>
</DropdownMenuItem>
))}
{isLoadingMore && (
<div className="flex justify-center py-2">
<Loader2 className="size-4 animate-spin text-light-slate" />
</div>
)}
</div>
</DropdownMenuContent>
</DropdownMenu>
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -25,6 +25,16 @@ interface VersionHistoryTimelineProps {
onSelect: (item: VersionHistoryItem) => void;
hasNextPage?: boolean;
isFetchingNextPage?: boolean;
// Broader than isFetchingNextPage — also true during a background refetch
// (e.g. the 30s poll, or a mutation's invalidation of already-loaded
// pages). Used only to gate the auto-load trigger, not the spinner: firing
// onLoadMore while an unrelated fetch is in flight races it and can
// produce a duplicate/overlapping row once both resolve.
isFetching?: boolean;
// True once a page fetch has failed. hasNextPage still reflects the last
// *successful* page, so without this the sentinel retries a permanently
// failing request forever, the moment isFetching settles back to false.
hasError?: boolean;
onLoadMore?: () => void;
emptyTitle?: string;
}
Expand All @@ -35,16 +45,18 @@ const VersionHistoryTimeline: React.FC<VersionHistoryTimelineProps> = ({
onSelect,
hasNextPage = false,
isFetchingNextPage = false,
isFetching = false,
hasError = false,
onLoadMore,
emptyTitle = "No version history",
}) => {
const { ref: sentinelRef, inView } = useInView();

useEffect(() => {
if (inView && hasNextPage && !isFetchingNextPage && onLoadMore) {
if (inView && hasNextPage && !isFetching && !hasError && onLoadMore) {
onLoadMore();
}
}, [inView, hasNextPage, isFetchingNextPage, onLoadMore]);
}, [inView, hasNextPage, isFetching, hasError, onLoadMore]);

if (items.length === 0) {
return <DataTableNoData title={emptyTitle} />;
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -312,12 +312,8 @@ const ComparePromptVersionDialog: React.FunctionComponent<
const anyMediaChanged = mediaChanges.some((m) => m.changed);

const versionLabelByCommit = useMemo(() => {
const sortedDesc = [...versions].sort((a, b) =>
b.created_at.localeCompare(a.created_at),
);
const total = sortedDesc.length;
const map = new Map<string, string>();
sortedDesc.forEach((v, idx) => map.set(v.commit, `v${total - idx}`));
versions.forEach((v) => map.set(v.commit, v.version_number ?? v.commit));
return map;
}, [versions]);

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -30,7 +30,6 @@ type DeployToEnvironmentMenuProps = {
versionId: string;
versionLabel: string;
versions: PromptVersion[] | undefined;
totalVersions: number;
activeEnvironments: string[];
};

Expand All @@ -39,7 +38,6 @@ const DeployToEnvironmentMenu: React.FC<DeployToEnvironmentMenuProps> = ({
versionId,
versionLabel,
versions,
totalVersions,
activeEnvironments,
}) => {
const { toast } = useToast();
Expand All @@ -64,13 +62,13 @@ const DeployToEnvironmentMenu: React.FC<DeployToEnvironmentMenuProps> = ({
);

const environmentOwners = useMemo(() => {
const map = new Map<string, { version: PromptVersion; index: number }>();
const map = new Map<string, PromptVersion>();
// `versions` is newest-first; only keep the first writer per environment so
// the "Currently vN" label reflects the newest version assigned to that env,
// not whichever historical version was iterated last.
versions?.forEach((v, index) => {
versions?.forEach((v) => {
v.environments?.forEach((env) => {
if (!map.has(env)) map.set(env, { version: v, index });
if (!map.has(env)) map.set(env, v);
});
});
return map;
Expand Down Expand Up @@ -143,8 +141,8 @@ const DeployToEnvironmentMenu: React.FC<DeployToEnvironmentMenuProps> = ({
const owner = environmentOwners.get(env.name);
const isActiveHere = activeEnvSet.has(env.name);
const ownerLabel =
!isActiveHere && owner && totalVersions > 0
? `Currently v${totalVersions - owner.index}`
!isActiveHere && owner
? `Currently ${owner.version_number ?? owner.commit}`
: "";
return (
<DropdownMenuItem
Expand Down
Loading