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/trace2/components/IOPreview/IOPreview"; import { JsonSkeleton } from "@/src/components/ui/CodeJsonViewer"; import { Badge } from "@/src/components/ui/badge"; 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 { useRouter } from "next/router"; import React, { useEffect, useState, useCallback, useRef } from "react"; import { useVirtualizer } from "@tanstack/react-virtual"; import { AnnotateDrawer } from "@/src/features/scores/components/AnnotateDrawer"; import { Button } from "@/src/components/ui/button"; import { CommentDrawerButton } from "@/src/features/comments/CommentDrawerButton"; import { useSession } from "next-auth/react"; import { Download, ExternalLinkIcon } from "lucide-react"; import { usePostHogClientCapture } from "@/src/features/posthog-analytics/usePostHogClientCapture"; 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 ScoreDomain } 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 { type WithStringifiedMetadata } from "@/src/utils/clientSideDomainTypes"; import { LazyTraceRow } from "@/src/components/session/TraceRow"; import { useParsedTrace } from "@/src/hooks/useParsedTrace"; import useLocalStorage from "@/src/components/useLocalStorage"; import { Switch } from "@/src/components/ui/switch"; // 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: WithStringifiedMetadata[]; }) => { return (
); }; export const SessionPage: React.FC<{ sessionId: string; projectId: string; }> = ({ sessionId, projectId }) => { const router = useRouter(); const { setDetailPageList } = useDetailPageLists(); const userSession = useSession(); const capture = usePostHogClientCapture(); const utils = api.useUtils(); const parentRef = useRef(null); 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 [showCorrections, setShowCorrections] = useLocalStorage( "showCorrections", false, ); const sessionComments = api.comments.getByObjectId.useQuery({ projectId, objectId: sessionId, objectType: "SESSION", }); const downloadSessionAsJson = useCallback(async () => { // Fetch fresh session and trace comments data const [sessionCommentsData, traceCommentsData] = await Promise.all([ sessionComments.refetch(), utils.comments.getTraceCommentsBySessionId.fetch({ projectId, sessionId, }), ]); // Add comments to each trace const sessionWithTraceComments = session.data ? { ...session.data, traces: session.data.traces.map((trace) => ({ ...trace, comments: traceCommentsData[trace.id] ?? [], })), } : session.data; const exportData = { ...sessionWithTraceComments, comments: sessionCommentsData.data ?? [], }; const jsonString = JSON.stringify(exportData, null, 2); const blob = new Blob([jsonString], { type: "application/json; charset=utf-8", }); const url = URL.createObjectURL(blob); const link = document.createElement("a"); link.href = url; link.download = `session-${sessionId}.json`; document.body.appendChild(link); link.click(); document.body.removeChild(link); URL.revokeObjectURL(url); capture("session_detail:download_button_click"); }, [session.data, sessionId, projectId, capture, sessionComments, utils]); const { openPeek, closePeek, resolveDetailNavigationPath, expandPeek } = usePeekNavigation({ expandConfig: { // Expand peeked traces to the trace detail route; sessions list traces basePath: `/project/${projectId}/traces`, }, queryParams: ["observation", "display", "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 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" }, ); // Virtualizer measures cheap skeleton, then updates once when hydrated const virtualizer = useVirtualizer({ count: session.data?.traces.length ?? 0, getScrollElement: () => parentRef.current, estimateSize: () => 300, overscan: 1, // Render 1 item above/below viewport getItemKey: (index) => session.data?.traces[index]?.id ?? index, measureElement: typeof window !== "undefined" ? (element) => element.getBoundingClientRect().height : undefined, }); if (session.error?.data?.code === "UNAUTHORIZED") return ; if (session.error?.data?.code === "NOT_FOUND") return ( void window.location.reload(), }} /> ); return ( ), actionButtonsRight: ( <> {!router.query.peek && ( `/project/${projectId}/sessions/${encodeURIComponent(entry.id)}` } listKey="sessions" /> )}
Show corrections
), }} >
{session.data?.users?.length ? ( ) : null} Total traces: {session.data?.traces.length} {session.data && ( Total cost: {usdFormatter(session.data.totalCost, 2)} )}
{virtualizer.getVirtualItems().map((virtualItem) => { const trace = session.data?.traces[virtualItem.index]; if (!trace) return null; return (
{ // Force virtualizer to remeasure this specific item virtualizer.measureElement( document.querySelector( `[data-index="${virtualItem.index}"]`, ) as HTMLElement, ); }} />
); })}
, tableDataUpdatedAt: session.dataUpdatedAt, }} />
); }; export const SessionIO = ({ traceId, projectId, timestamp, showCorrections, }: { traceId: string; projectId: string; timestamp: Date; showCorrections: boolean; }) => { const trace = api.traces.byId.useQuery( { traceId, projectId, timestamp }, { enabled: typeof traceId === "string", trpc: { context: { skipBatch: true, }, }, refetchOnMount: false, }, ); // Parse trace data in Web Worker (non-blocking) const { parsedInput, parsedOutput, isParsing } = useParsedTrace({ traceId, input: trace.data?.input, output: trace.data?.output, metadata: undefined, }); return (
{!trace.data ? ( ) : trace.data.input || trace.data.output ? ( ) : (
This trace has no input or output.
)}
); };