Skip to content
Merged
Show file tree
Hide file tree
Changes from 5 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,56 @@
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;
refetchOnWindowFocus?: boolean;
};

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,
});
}
14 changes: 5 additions & 9 deletions apps/opik-frontend/src/hooks/usePromptVersionLabel.ts
Original file line number Diff line number Diff line change
Expand Up @@ -3,12 +3,9 @@ import { useMemo } from "react";
import usePromptVersionsById from "@/api/prompts/usePromptVersionsById";

/**
* Compute the human-facing "v{n}" label for a specific prompt version.
*
* Versions are labeled by their position when sorted by created_at desc:
* the oldest is v1, the newest is v{total}. We need the versions list to
* find that position — version_count on the Prompt object only tells us
* the total (i.e., the latest's label).
* Compute the human-facing label for a specific prompt version, using the
* backend-persisted version_number so it stays correct even after older
* versions are deleted (positional "v{n}" labels shift when that happens).
*/
const usePromptVersionLabel = (
promptId: string | undefined,
Expand All @@ -27,9 +24,8 @@ const usePromptVersionLabel = (

return useMemo(() => {
if (versionId && data?.content) {
const idx = data.content.findIndex((v) => v.id === versionId);
const total = data.total ?? data.content.length;
if (idx >= 0 && total > 0) return `v${total - idx}`;
const version = data.content.find((v) => v.id === versionId);
if (version) return version.version_number ?? version.commit;
}
return fallbackVersionCount && fallbackVersionCount > 0
? `v${fallbackVersionCount}`
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -241,7 +241,6 @@ const PromptVersionsList: React.FC<PromptVersionsListProps> = ({
);

const versions = data?.content ?? [];
const total = data?.total ?? versions.length;

if (isLoading) {
return (
Expand All @@ -259,8 +258,8 @@ const PromptVersionsList: React.FC<PromptVersionsListProps> = ({

return (
<div className="max-h-[40vh] overflow-y-auto">
{versions.map((version, idx) => {
const label = `v${total - idx}`;
{versions.map((version) => {
const label = version.version_number ?? version.commit;
const isActive = version.id === activeVersionId;
const stage = pickHighestStage(version.tags);
return (
Expand Down
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 @@ -46,10 +46,10 @@ export function usePromptVersionsWithLabels(
versions.map((version, index) => ({
version,
index,
label: `v${total - index}`,
label: version.version_number ?? version.commit,
stage: pickHighestStage(version.tags),
})),
[versions, total],
[versions],
);

const getDescriptor = useMemo(() => {
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
8 changes: 7 additions & 1 deletion apps/opik-frontend/src/v2/pages/PromptPage/PromptPage.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -27,7 +27,13 @@ const PromptPage: React.FunctionComponent = () => {

const promptId = usePromptIdFromURL();

const { data: prompt } = usePromptById({ promptId }, { enabled: !!promptId });
// Cheap poll so the (unbounded) paginated version list can detect other
// users' changes without itself refetching every loaded page on a timer —
// see usePromptVersionHistory's version_count watcher.
const { data: prompt } = usePromptById(
{ promptId },
{ enabled: !!promptId, refetchInterval: 30000 },
);
const promptName = prompt?.name || "";
const setBreadcrumbParam = useBreadcrumbsStore((state) => state.setParam);

Expand Down
Loading
Loading