import { GroupedScoreBadges } from "@/src/components/grouped-score-badge"; import { ErrorPage } from "@/src/components/error-page"; import { PublishSessionSwitch } from "@/src/components/publish-object-switch"; import { StarSessionToggle } from "@/src/components/star-toggle"; import { IOPreview } from "@/src/components/trace/IOPreview"; import { JsonSkeleton } from "@/src/components/ui/CodeJsonViewer"; import { Badge } from "@/src/components/ui/badge"; import { Card } from "@/src/components/ui/card"; import { DetailPageNav } from "@/src/features/navigate-detail-pages/DetailPageNav"; import { useDetailPageLists } from "@/src/features/navigate-detail-pages/context"; import { api } from "@/src/utils/api"; import { usdFormatter } from "@/src/utils/numbers"; import { getNumberFromMap } from "@/src/utils/map-utils"; import Link from "next/link"; import { useEffect, useState } from "react"; import { AnnotateDrawer } from "@/src/features/scores/components/AnnotateDrawer"; import { Button } from "@/src/components/ui/button"; import useLocalStorage from "@/src/components/useLocalStorage"; import { CommentDrawerButton } from "@/src/features/comments/CommentDrawerButton"; import { useSession } from "next-auth/react"; import Page from "@/src/components/layouts/page"; import { Popover, PopoverContent, PopoverTrigger, } from "@/src/components/ui/popover"; import { ScrollArea } from "@/src/components/ui/scroll-area"; import { Label } from "@/src/components/ui/label"; import { AnnotationQueueObjectType, type APIScoreV2 } from "@langfuse/shared"; import { CreateNewAnnotationQueueItem } from "@/src/features/annotation-queues/components/CreateNewAnnotationQueueItem"; import { TablePeekView } from "@/src/components/table/peek"; import { PeekViewTraceDetail } from "@/src/components/table/peek/peek-trace-detail"; import { usePeekNavigation } from "@/src/components/table/peek/hooks/usePeekNavigation"; import { NewDatasetItemFromExistingObject } from "@/src/features/datasets/components/NewDatasetItemFromExistingObject"; import { ItemBadge } from "@/src/components/ItemBadge"; // some projects have thousands of traces in a sessions, paginate to avoid rendering all at once const PAGE_SIZE = 50; // some projects have thousands of users in a session, paginate to avoid rendering all at once const INITIAL_USERS_DISPLAY_COUNT = 10; const USERS_PER_PAGE_IN_POPOVER = 50; export function SessionUsers({ projectId, users, }: { projectId: string; users?: string[]; }) { const [page, setPage] = useState(0); if (!users) return null; const initialUsers = users?.slice(0, INITIAL_USERS_DISPLAY_COUNT); const remainingUsers = users?.slice(INITIAL_USERS_DISPLAY_COUNT); return (
{initialUsers.map((userId: string) => ( User ID: {userId} ))} {remainingUsers.length > 0 && (
{remainingUsers .slice( page * USERS_PER_PAGE_IN_POPOVER, (page + 1) * USERS_PER_PAGE_IN_POPOVER, ) .map((userId: string) => ( User ID: {userId} ))}
{remainingUsers.length > USERS_PER_PAGE_IN_POPOVER && (
Page {page + 1} of{" "} {Math.ceil(remainingUsers.length / USERS_PER_PAGE_IN_POPOVER)}
)}
)}
); } const SessionScores = ({ scores }: { scores: APIScoreV2[] }) => { return (
); }; export const SessionPage: React.FC<{ sessionId: string; projectId: string; }> = ({ sessionId, projectId }) => { const { setDetailPageList } = useDetailPageLists(); const userSession = useSession(); const [visibleTraces, setVisibleTraces] = useState(PAGE_SIZE); const session = api.sessions.byIdWithScores.useQuery( { sessionId, projectId: projectId, }, { retry(failureCount, error) { if ( error.data?.code === "UNAUTHORIZED" || error.data?.code === "NOT_FOUND" ) return false; return failureCount < 3; }, }, ); const { openPeek, closePeek, resolveDetailNavigationPath, expandPeek } = usePeekNavigation({ expandConfig: { // Expand peeked traces to the trace detail route; sessions list traces basePath: `/project/${projectId}/traces`, }, queryParams: ["timestamp"], extractParamsValuesFromRow: (row: any) => ({ timestamp: row.timestamp.toISOString(), }), }); useEffect(() => { if (session.isSuccess) { setDetailPageList( "traces", session.data.traces.map((t) => ({ id: t.id, params: { timestamp: t.timestamp.toISOString() }, })), ); } // eslint-disable-next-line react-hooks/exhaustive-deps }, [session.isSuccess, session.data]); const [emptySelectedConfigIds, setEmptySelectedConfigIds] = useLocalStorage< string[] >("emptySelectedConfigIds", []); const sessionCommentCounts = api.comments.getCountByObjectId.useQuery( { projectId, objectId: sessionId, objectType: "SESSION", }, { enabled: session.isSuccess && userSession.status === "authenticated" }, ); const traceCommentCounts = api.comments.getTraceCommentCountsBySessionId.useQuery( { projectId, sessionId, }, { enabled: session.isSuccess && userSession.status === "authenticated" }, ); if (session.error?.data?.code === "UNAUTHORIZED") return ; if (session.error?.data?.code === "NOT_FOUND") return ( void window.location.reload(), }} /> ); return ( ), actionButtonsRight: ( <> `/project/${projectId}/sessions/${encodeURIComponent(entry.id)}` } listKey="sessions" />
({ ...score, timestamp: new Date(score.timestamp), createdAt: new Date(score.createdAt), updatedAt: new Date(score.updatedAt), })) ?? [] } emptySelectedConfigIds={emptySelectedConfigIds} setEmptySelectedConfigIds={setEmptySelectedConfigIds} buttonVariant="outline" hasGroupedButton={true} />
), }} >
{session.data?.users?.length ? ( ) : null} Total traces: {session.data?.traces.length} {session.data && ( Total cost: {usdFormatter(session.data.totalCost, 2)} )} ({ ...score, timestamp: new Date(score.timestamp), createdAt: new Date(score.createdAt), updatedAt: new Date(score.updatedAt), })) ?? [] } />
{session.data?.traces.slice(0, visibleTraces).map((trace) => (
{ // Only prevent default for normal clicks, allow modifier key clicks through if (!e.metaKey && !e.ctrlKey && !e.shiftKey) { e.preventDefault(); openPeek(trace.id, trace); } }} >
{trace.name} ({trace.id}) ↗ {trace.timestamp.toLocaleString()}

Scores

))} {session.data?.traces && session.data.traces.length > visibleTraces && ( )}
, tableDataUpdatedAt: session.dataUpdatedAt, }} />
); }; export const SessionIO = ({ traceId, projectId, timestamp, }: { traceId: string; projectId: string; timestamp: Date; }) => { const trace = api.traces.byId.useQuery( { traceId, projectId, timestamp }, { enabled: typeof traceId === "string", trpc: { context: { skipBatch: true, }, }, refetchOnMount: false, }, ); return (
{!trace.data ? ( ) : trace.data.input || trace.data.output ? ( ) : (
This trace has no input or output.
)}
); }; const NewDatasetItemFromTraceId = (props: { projectId: string; traceId: string; timestamp: Date; buttonVariant?: "outline" | "secondary"; }) => { // SessionIO already fetches the trace, so this doesn't add an extra request const trace = api.traces.byId.useQuery( { traceId: props.traceId, projectId: props.projectId, timestamp: props.timestamp, }, { enabled: typeof props.traceId === "string", trpc: { context: { skipBatch: true, }, }, refetchOnMount: false, }, ); if (!trace.data) return null; return ( ); };