From 16a819d9f17f698efbb80c3d4d5d275fb346d3e8 Mon Sep 17 00:00:00 2001 From: dylanzuber-scale3 <116033320+dylanzuber-scale3@users.noreply.github.com> Date: Sat, 13 Apr 2024 19:14:44 -0400 Subject: [PATCH] Dylan/s3en 2127 loading states for all pages (#57) * loading for data set * adding loading page for promptset * dataset loading * prompts loading * prompts loading * metrics loading, chart loading * evaluations loading * refactor traces * trace row loading * more loading * Rename loading to skeleton * Minor fix --------- Co-authored-by: Karthik Kalyanaraman --- app/(protected)/layout.tsx | 29 +- .../datasets/dataset/[dataset_id]/page.tsx | 32 +- .../promptset/[promptset_id]/page.tsx | 51 +- .../[project_id]/evaluations/page-client.tsx | 554 ++---------------- .../[project_id]/prompts/page-client.tsx | 60 +- .../project/[project_id]/traces/page.tsx | 2 +- app/(protected)/projects/page-client.tsx | 8 +- .../settings/members/page-client.tsx | 4 +- components/charts/eval-chart.tsx | 3 +- components/charts/large-chart-skeleton.tsx | 19 + components/charts/latency-chart.tsx | 3 +- components/charts/model-accuracy-chart.tsx | 3 +- components/charts/small-chart-skeleton.tsx | 22 + components/charts/token-chart.tsx | 5 +- components/charts/trace-chart.tsx | 5 +- components/evaluations/evaluation-row.tsx | 334 +++++++++++ components/evaluations/evaluation-table.tsx | 204 +++++++ components/project/dataset/create-data.tsx | 12 +- components/project/dataset/create.tsx | 12 +- components/project/dataset/data-set.tsx | 29 +- .../project/dataset/dataset-row-skeleton.tsx | 29 + components/project/dataset/prompt-set.tsx | 29 +- components/project/metrics.tsx | 44 +- components/project/playground.tsx | 18 +- components/project/traces.tsx | 552 ----------------- components/project/traces/logs-view.tsx | 98 ++++ .../project/traces/trace-row-skeleton.tsx | 29 + components/project/traces/trace-row.tsx | 250 ++++++++ components/project/traces/traces.tsx | 243 ++++++++ components/shared/add-to-dataset.tsx | 14 +- components/shared/add-to-promptset.tsx | 14 +- .../{card-loading.tsx => card-skeleton.tsx} | 2 +- lib/utils.ts | 10 + 33 files changed, 1631 insertions(+), 1092 deletions(-) create mode 100644 components/charts/large-chart-skeleton.tsx create mode 100644 components/charts/small-chart-skeleton.tsx create mode 100644 components/evaluations/evaluation-row.tsx create mode 100644 components/evaluations/evaluation-table.tsx create mode 100644 components/project/dataset/dataset-row-skeleton.tsx delete mode 100644 components/project/traces.tsx create mode 100644 components/project/traces/logs-view.tsx create mode 100644 components/project/traces/trace-row-skeleton.tsx create mode 100644 components/project/traces/trace-row.tsx create mode 100644 components/project/traces/traces.tsx rename components/shared/{card-loading.tsx => card-skeleton.tsx} (95%) diff --git a/app/(protected)/layout.tsx b/app/(protected)/layout.tsx index 8c2fe3ad..622e9cf9 100644 --- a/app/(protected)/layout.tsx +++ b/app/(protected)/layout.tsx @@ -1,9 +1,11 @@ import { Header } from "@/components/shared/header"; import { Separator } from "@/components/ui/separator"; +import { Skeleton } from "@/components/ui/skeleton"; import { authOptions } from "@/lib/auth/options"; import { getServerSession } from "next-auth"; import { redirect } from "next/navigation"; import { Suspense } from "react"; +import { PageSkeleton } from "./projects/page-client"; export default async function Layout({ children, @@ -16,7 +18,7 @@ export default async function Layout({ } return ( - Loading...}> + }>
@@ -25,3 +27,28 @@ export default async function Layout({ ); } + +function PageLoading() { + return ( +
+
+
+
+ Langtrace AI +
+
+ +
+

+ +

+
+ +
+
+ +
+ +
+ ); +} diff --git a/app/(protected)/project/[project_id]/datasets/dataset/[dataset_id]/page.tsx b/app/(protected)/project/[project_id]/datasets/dataset/[dataset_id]/page.tsx index 72e05874..d41e00cb 100644 --- a/app/(protected)/project/[project_id]/datasets/dataset/[dataset_id]/page.tsx +++ b/app/(protected)/project/[project_id]/datasets/dataset/[dataset_id]/page.tsx @@ -2,6 +2,7 @@ import { CreateData } from "@/components/project/dataset/create-data"; import { EditData } from "@/components/project/dataset/edit-data"; +import DatasetRowSkeleton from "@/components/project/dataset/dataset-row-skeleton"; import { Spinner } from "@/components/shared/spinner"; import { Button } from "@/components/ui/button"; import { Separator } from "@/components/ui/separator"; @@ -69,7 +70,7 @@ export default function Dataset() { }); if (fetchDataset.isLoading || !fetchDataset.data || !currentData) { - return
Loading...
; + return ; } else { return (
@@ -130,3 +131,32 @@ export default function Dataset() { ); } } + +function PageSkeleton() { + return ( +
+
+ + +
+
+
+

Created at

+

Input

+

Output

+

Note

+
+ {Array.from({ length: 5 }).map((_, index) => ( + + ))} +
+
+ ); +} diff --git a/app/(protected)/project/[project_id]/datasets/promptset/[promptset_id]/page.tsx b/app/(protected)/project/[project_id]/datasets/promptset/[promptset_id]/page.tsx index 93743712..ca37cac3 100644 --- a/app/(protected)/project/[project_id]/datasets/promptset/[promptset_id]/page.tsx +++ b/app/(protected)/project/[project_id]/datasets/promptset/[promptset_id]/page.tsx @@ -5,6 +5,7 @@ import { EditPrompt } from "@/components/project/dataset/edit-data"; import { Spinner } from "@/components/shared/spinner"; import { Button } from "@/components/ui/button"; import { Separator } from "@/components/ui/separator"; +import { Skeleton } from "@/components/ui/skeleton"; import { PAGE_SIZE } from "@/lib/constants"; import { Prompt } from "@prisma/client"; import { ChevronLeft } from "lucide-react"; @@ -69,7 +70,7 @@ export default function Promptset() { }); if (fetchPromptset.isLoading || !fetchPromptset.data || !currentData) { - return
Loading...
; + return ; } else { return (
@@ -121,3 +122,51 @@ export default function Promptset() { ); } } + +function PageSkeleton() { + return ( +
+
+ + +
+
+
+

Created at

+

Value

+

Note

+

+
+ {Array.from({ length: 3 }).map((_, index) => ( + + ))} +
+
+ ); +} + +function PromptsetRowSkeleton() { + return ( +
+
+

+ +

+

+ +

+

+ +

+
+ +
+ ); +} diff --git a/app/(protected)/project/[project_id]/evaluations/page-client.tsx b/app/(protected)/project/[project_id]/evaluations/page-client.tsx index 1d1ccf76..bdcb6a9b 100644 --- a/app/(protected)/project/[project_id]/evaluations/page-client.tsx +++ b/app/(protected)/project/[project_id]/evaluations/page-client.tsx @@ -1,31 +1,19 @@ "use client"; import { EvalChart } from "@/components/charts/eval-chart"; +import LargeChartSkeleton from "@/components/charts/large-chart-skeleton"; +import EvaluationTable, { + EvaluationTableSkeleton, +} from "@/components/evaluations/evaluation-table"; import { AddtoDataset } from "@/components/shared/add-to-dataset"; -import { HoverCell } from "@/components/shared/hover-cell"; -import { LLMView } from "@/components/shared/llm-view"; -import { TestSetupInstructions } from "@/components/shared/setup-instructions"; -import { Spinner } from "@/components/shared/spinner"; -import { Button } from "@/components/ui/button"; -import { Checkbox } from "@/components/ui/checkbox"; import { Separator } from "@/components/ui/separator"; -import { PAGE_SIZE } from "@/lib/constants"; -import detectPII from "@/lib/pii"; -import { correctTimestampFormat } from "@/lib/trace_utils"; -import { - calculatePriceFromUsage, - cn, - extractPromptFromLlmInputs, - formatDateTime, -} from "@/lib/utils"; -import { Evaluation, Test } from "@prisma/client"; -import { CheckCircledIcon, DotFilledIcon } from "@radix-ui/react-icons"; +import { Skeleton } from "@/components/ui/skeleton"; +import { cn, getChartColor } from "@/lib/utils"; +import { Test } from "@prisma/client"; import { ProgressCircle } from "@tremor/react"; -import { ChevronDown, ChevronRight, ThumbsDown, ThumbsUp } from "lucide-react"; import { useParams } from "next/navigation"; import { useState } from "react"; -import { useBottomScrollListener } from "react-bottom-scroll-listener"; -import { useQuery, useQueryClient } from "react-query"; +import { useQuery } from "react-query"; interface CheckedData { input: string; @@ -77,7 +65,7 @@ export default function PageClient({ email }: { email: string }) {
{fetchTests.isLoading || !fetchTests.data ? ( -
Loading...
+ ) : ( fetchTests?.data?.tests?.length > 0 && (
@@ -154,7 +142,6 @@ export default function PageClient({ email }: { email: string }) { {selectedTest?.description}

- {!selectedTest &&
Loading...
} {selectedTest && ( )} @@ -181,484 +168,61 @@ export default function PageClient({ email }: { email: string }) { ); } -function EvaluationTable({ - projectId, - test, - selectedData, - setSelectedData, - currentData, - setCurrentData, - page, - setPage, - totalPages, - setTotalPages, -}: { - projectId: string; - test: Test; - selectedData: CheckedData[]; - setSelectedData: (data: CheckedData[]) => void; - currentData: any; - setCurrentData: (data: any) => void; - page: number; - setPage: (page: number) => void; - totalPages: number; - setTotalPages: (totalPages: number) => void; -}) { - const [showLoader, setShowLoader] = useState(false); - - const onCheckedChange = (data: CheckedData, checked: boolean) => { - if (checked) { - setSelectedData([...selectedData, data]); - } else { - setSelectedData(selectedData.filter((d) => d.spanId !== data.spanId)); - } - }; - - const fetchLlmPromptSpans = useQuery({ - queryKey: [`fetch-llm-prompt-spans-${test.id}-query`], - queryFn: async () => { - const filters = [ - { - key: "llm.prompts", - operation: "NOT_EQUALS", - value: "", - }, - // Accuracy is the default test. So no need to - // send the testId with the spans when using the SDK. - { - key: "langtrace.testId", - operation: "EQUALS", - value: test.name.toLowerCase() !== "factual accuracy" ? test.id : "", - }, - ]; - - // convert filterserviceType to a string - const apiEndpoint = "/api/spans"; - const body = { - page, - pageSize: PAGE_SIZE, - projectId: projectId, - filters: filters, - filterOperation: "AND", - }; - - const response = await fetch(apiEndpoint, { - method: "POST", - headers: { - "Content-Type": "application/json", - }, - body: JSON.stringify(body), - }); - const result = await response.json(); - return result; - }, - onSuccess: (data) => { - // Get the newly fetched data and metadata - const newData = data?.spans?.result || []; - const metadata = data?.spans?.metadata || {}; - - // Update the total pages and current page number - setTotalPages(parseInt(metadata?.total_pages) || 1); - if (parseInt(metadata?.page) <= parseInt(metadata?.total_pages)) { - setPage(parseInt(metadata?.page) + 1); - } - - // Merge the new data with the existing data - if (currentData.length > 0) { - const updatedData = [...currentData, ...newData]; - - // Remove duplicates - const uniqueData = updatedData.filter( - (v: any, i: number, a: any) => - a.findIndex((t: any) => t.span_id === v.span_id) === i - ); - - setCurrentData(uniqueData); - } else { - setCurrentData(newData); - } - setShowLoader(false); - }, - }); - - useBottomScrollListener(() => { - if (fetchLlmPromptSpans.isRefetching) { - return; - } - if (page <= totalPages) { - setShowLoader(true); - fetchLlmPromptSpans.refetch(); - } - }); - +function PageSkeleton() { return ( -
- {currentData.length > 0 && ( -
-

- Timestamp (UTC) -

-

Model

-

Input

-

Output

-

Cost

-

PII Detected

-

Duration

-

Evaluate

-

User Score

-

Added to Dataset

-
- )} - {fetchLlmPromptSpans.isLoading || - !fetchLlmPromptSpans.data || - !currentData ? ( -
Loading...
- ) : ( - currentData.map((span: any, i: number) => ( - - )) - )} - {showLoader && ( -
- +
+
+
+ {Array.from({ length: 5 }).map((_, i) => { + return ( +
+
+

+ +

+ + + +
+ +
+ ); + })}
- )} - {!fetchLlmPromptSpans.isLoading && - fetchLlmPromptSpans.data && - currentData.length === 0 && ( -
-

- Setup instructions 👇 -

- +
+
+
+
+

+ +

+ + + +
+ + + +

+ +

+
+
- )} -
- ); -} - -function EvaluationRow({ - key, - span, - projectId, - testId, - onCheckedChange, - selectedData, -}: { - key: number; - span: any; - projectId: string; - testId: string; - onCheckedChange: (data: CheckedData, checked: boolean) => void; - selectedData: CheckedData[]; -}) { - const queryClient = useQueryClient(); - - const [score, setScore] = useState(-100); // 0: neutral, 1: thumbs up, -1: thumbs down - const [collapsed, setCollapsed] = useState(true); - const [evaluation, setEvaluation] = useState(); - const [addedToDataset, setAddedToDataset] = useState(false); - - useQuery({ - queryKey: [`fetch-evaluation-query-${span.span_id}`], - queryFn: async () => { - const response = await fetch(`/api/evaluation?spanId=${span.span_id}`); - const result = await response.json(); - setEvaluation(result.evaluations.length > 0 ? result.evaluations[0] : {}); - setScore( - result.evaluations.length > 0 ? result.evaluations[0].score : -100 - ); - return result; - }, - }); - - useQuery({ - queryKey: [`fetch-data-query-${span.span_id}`], - queryFn: async () => { - const response = await fetch(`/api/data?spanId=${span.span_id}`); - const result = await response.json(); - setAddedToDataset(result.data.length > 0); - return result; - }, - }); - - const attributes = span.attributes ? JSON.parse(span.attributes) : {}; - if (!attributes) return null; - - // extract the metrics - const userScore = attributes["user.feedback.rating"] || ""; - const startTimeMs = new Date( - correctTimestampFormat(span.start_time) - ).getTime(); - const endTimeMs = new Date(correctTimestampFormat(span.end_time)).getTime(); - const durationMs = endTimeMs - startTimeMs; - const prompts = attributes["llm.prompts"]; - const responses = attributes["llm.responses"]; - let model = ""; - let vendor = ""; - let tokenCounts: any = {}; - let cost = { total: 0, input: 0, output: 0 }; - if (attributes["llm.token.counts"]) { - model = attributes["llm.model"]; - vendor = attributes["langtrace.service.name"]; - tokenCounts = JSON.parse(attributes["llm.token.counts"]); - cost = calculatePriceFromUsage(vendor.toLowerCase(), model, tokenCounts); - } - const promptContent = extractPromptFromLlmInputs(prompts); - - // check for pii detections - let piiDetected = false; - for (const prompt of JSON.parse(prompts)) { - if (detectPII(prompt.content || "").length > 0) { - piiDetected = true; - break; - } - } - piiDetected = - piiDetected || - detectPII( - JSON.parse(responses)[0]?.message?.content || - JSON.parse(responses)[0]?.text || - JSON.parse(responses)[0]?.content || - "" - ).length > 0; - - // score evaluation - const evaluateSpan = async (newScore: number) => { - if (!evaluation?.id) { - // Evaluate - await fetch("/api/evaluation", { - method: "POST", - headers: { - "Content-Type": "application/json", - }, - body: JSON.stringify({ - projectId: projectId, - spanId: span.span_id, - traceId: span.trace_id, - spanStartTime: new Date(correctTimestampFormat(span.start_time)), - score: newScore, - model: model, - prompt: promptContent, - testId: testId, - }), - }); - } else { - await fetch("/api/evaluation", { - method: "PUT", - headers: { - "Content-Type": "application/json", - }, - body: JSON.stringify({ - id: evaluation?.id, - score: newScore, - }), - }); - } - - // Invalidate the evaluations query to refetch the updated evaluations - queryClient.invalidateQueries(`fetch-evaluation-query-${span.span_id}`); - queryClient.invalidateQueries(`fetch-test-averages-${projectId}-query`); - queryClient.invalidateQueries( - `fetch-accuracy-${projectId}-${testId}-query` - ); - }; - - const LlmViewEvaluation = () => { - return ( -
- - -
- ); - }; - - return ( -
-
setCollapsed(!collapsed)} - > -
e.stopPropagation()} - > - { - const input = JSON.parse(prompts).find( - (prompt: any) => prompt.role === "user" - ); - if (!input) return; - const checkedData = { - spanId: span.span_id, - input: input?.content || "", - output: - responses?.length > 0 - ? JSON.parse(responses)[0]?.message?.content || - JSON.parse(responses)[0]?.text || - JSON.parse(responses)[0]?.content - : "", - }; - onCheckedChange(checkedData, state); - }} - checked={selectedData.some((d) => d.spanId === span.span_id)} - /> - -

- {formatDateTime(correctTimestampFormat(span.start_time))} -

-
-

{model}

- 0 ? JSON.parse(prompts)[0]?.content : ""} - /> - 0 - ? JSON.parse(responses)[0]?.message?.content || - JSON.parse(responses)[0]?.text || - JSON.parse(responses)[0]?.content - : "" - } - /> -

- {cost.total.toFixed(6) !== "0.000000" - ? `\$${cost.total.toFixed(6)}` - : ""} -

-
- {piiDetected ? ( - - ) : ( - - )} -

{piiDetected ? "Yes" : "No"}

-
-

- {durationMs}ms -

-
- - +
-

{userScore}

- {addedToDataset ? ( - - ) : ( - "" - )}
- {!collapsed && ( - - )}
); } - -const getChartColor = (value: number) => { - if (value < 50) { - return "red"; - } else if (value < 90 && value >= 50) { - return "yellow"; - } else { - return "green"; - } -}; diff --git a/app/(protected)/project/[project_id]/prompts/page-client.tsx b/app/(protected)/project/[project_id]/prompts/page-client.tsx index b5c9d5f9..0b5f0880 100644 --- a/app/(protected)/project/[project_id]/prompts/page-client.tsx +++ b/app/(protected)/project/[project_id]/prompts/page-client.tsx @@ -5,6 +5,7 @@ import { Spinner } from "@/components/shared/spinner"; import { Button } from "@/components/ui/button"; import { Checkbox } from "@/components/ui/checkbox"; import { Separator } from "@/components/ui/separator"; +import { Skeleton } from "@/components/ui/skeleton"; import { PAGE_SIZE } from "@/lib/constants"; import { extractPromptFromLlmInputs } from "@/lib/utils"; import { CheckCircledIcon } from "@radix-ui/react-icons"; @@ -85,7 +86,7 @@ export default function PageClient({ email }: { email: string }) { }); if (fetchPrompts.isLoading || !fetchPrompts.data || !currentData) { - return
Loading...
; + return ; } else { // Deduplicate prompts const seenPrompts: string[] = []; @@ -280,3 +281,60 @@ const PromptRow = ({
); }; + +function PageLoading() { + return ( +
+
+ +
+

+ These prompts are automatically captured from your traces. The accuracy + of these prompts are calculated based on the evaluation done in the + evaluate tab. +

+
+
+

LLM Vendor

+

Model

+

Interactions

+

Prompt

+

Accuracy

+

Added to Dataset

+
+ {Array.from({ length: 3 }).map((_, index) => ( + + ))} +
+
+ ); +} + +function PromptRowSkeleton() { + return ( +
+
+
e.stopPropagation()} + > + +
+

+ +

+

+ +

+

+ +

+

+ +

+ +
+ +
+ ); +} diff --git a/app/(protected)/project/[project_id]/traces/page.tsx b/app/(protected)/project/[project_id]/traces/page.tsx index 700bfc1e..3f45bc86 100644 --- a/app/(protected)/project/[project_id]/traces/page.tsx +++ b/app/(protected)/project/[project_id]/traces/page.tsx @@ -1,4 +1,4 @@ -import Traces from "@/components/project/traces"; +import Traces from "@/components/project/traces/traces"; import { authOptions } from "@/lib/auth/options"; import { Metadata } from "next"; import { getServerSession } from "next-auth"; diff --git a/app/(protected)/projects/page-client.tsx b/app/(protected)/projects/page-client.tsx index 48d90a9f..388316cf 100644 --- a/app/(protected)/projects/page-client.tsx +++ b/app/(protected)/projects/page-client.tsx @@ -2,7 +2,7 @@ import { Create } from "@/components/project/create"; import { Edit } from "@/components/project/edit"; -import CardLoading from "@/components/shared/card-loading"; +import CardSkeleton from "@/components/shared/card-skeleton"; import { Card, CardContent, @@ -41,7 +41,7 @@ export default function PageClient({ email }: { email: string }) { fetchUser.isLoading || !fetchUser.data ) { - return ; + return ; } return ( @@ -190,7 +190,7 @@ function ProjectCard({ ); } -function PageLoading() { +export function PageSkeleton() { return (
@@ -208,7 +208,7 @@ function PageLoading() { )} > {Array.from({ length: 3 }).map((_, index) => ( - + ))}
diff --git a/app/(protected)/settings/members/page-client.tsx b/app/(protected)/settings/members/page-client.tsx index 351f0efd..995a6127 100644 --- a/app/(protected)/settings/members/page-client.tsx +++ b/app/(protected)/settings/members/page-client.tsx @@ -371,7 +371,7 @@ export default function MembersView({ fetchUser.isLoading || !fetchUser.data ) { - return ; + return ; } return ( @@ -424,7 +424,7 @@ export default function MembersView({ ); } -function MembersSettingsLoading() { +function MembersSettingsSkeleton() { return ( <> + +
+ ); + }; + + return ( +
+
setCollapsed(!collapsed)} + > +
e.stopPropagation()} + > + { + const input = JSON.parse(prompts).find( + (prompt: any) => prompt.role === "user" + ); + if (!input) return; + const checkedData = { + spanId: span.span_id, + input: input?.content || "", + output: + responses?.length > 0 + ? JSON.parse(responses)[0]?.message?.content || + JSON.parse(responses)[0]?.text || + JSON.parse(responses)[0]?.content + : "", + }; + onCheckedChange(checkedData, state); + }} + checked={selectedData.some((d) => d.spanId === span.span_id)} + /> + +

+ {formatDateTime(correctTimestampFormat(span.start_time))} +

+
+

{model}

+ 0 ? JSON.parse(prompts)[0]?.content : ""} + /> + 0 + ? JSON.parse(responses)[0]?.message?.content || + JSON.parse(responses)[0]?.text || + JSON.parse(responses)[0]?.content + : "" + } + /> +

+ {cost.total.toFixed(6) !== "0.000000" + ? `\$${cost.total.toFixed(6)}` + : ""} +

+
+ {piiDetected ? ( + + ) : ( + + )} +

{piiDetected ? "Yes" : "No"}

+
+

+ {durationMs}ms +

+
+ + +
+

{userScore}

+ {addedToDataset ? ( + + ) : ( + "" + )} +
+ {!collapsed && ( + + )} +
+ ); +} diff --git a/components/evaluations/evaluation-table.tsx b/components/evaluations/evaluation-table.tsx new file mode 100644 index 00000000..3e104e0c --- /dev/null +++ b/components/evaluations/evaluation-table.tsx @@ -0,0 +1,204 @@ +"use client"; + +import { TestSetupInstructions } from "@/components/shared/setup-instructions"; +import { Spinner } from "@/components/shared/spinner"; +import { PAGE_SIZE } from "@/lib/constants"; +import { Test } from "@prisma/client"; +import { useState } from "react"; +import { useBottomScrollListener } from "react-bottom-scroll-listener"; +import { useQuery } from "react-query"; +import TraceRowSkeleton from "../project/traces/trace-row-skeleton"; +import EvaluationRow from "./evaluation-row"; + +interface CheckedData { + input: string; + output: string; + spanId: string; +} + +export default function EvaluationTable({ + projectId, + test, + selectedData, + setSelectedData, + currentData, + setCurrentData, + page, + setPage, + totalPages, + setTotalPages, +}: { + projectId: string; + test: Test; + selectedData: CheckedData[]; + setSelectedData: (data: CheckedData[]) => void; + currentData: any; + setCurrentData: (data: any) => void; + page: number; + setPage: (page: number) => void; + totalPages: number; + setTotalPages: (totalPages: number) => void; +}) { + const [showLoader, setShowLoader] = useState(false); + + const onCheckedChange = (data: CheckedData, checked: boolean) => { + if (checked) { + setSelectedData([...selectedData, data]); + } else { + setSelectedData(selectedData.filter((d) => d.spanId !== data.spanId)); + } + }; + + const fetchLlmPromptSpans = useQuery({ + queryKey: [`fetch-llm-prompt-spans-${test.id}-query`], + queryFn: async () => { + const filters = [ + { + key: "llm.prompts", + operation: "NOT_EQUALS", + value: "", + }, + // Accuracy is the default test. So no need to + // send the testId with the spans when using the SDK. + { + key: "langtrace.testId", + operation: "EQUALS", + value: test.name.toLowerCase() !== "factual accuracy" ? test.id : "", + }, + ]; + + // convert filterserviceType to a string + const apiEndpoint = "/api/spans"; + const body = { + page, + pageSize: PAGE_SIZE, + projectId: projectId, + filters: filters, + filterOperation: "AND", + }; + + const response = await fetch(apiEndpoint, { + method: "POST", + headers: { + "Content-Type": "application/json", + }, + body: JSON.stringify(body), + }); + const result = await response.json(); + return result; + }, + onSuccess: (data) => { + // Get the newly fetched data and metadata + const newData = data?.spans?.result || []; + const metadata = data?.spans?.metadata || {}; + + // Update the total pages and current page number + setTotalPages(parseInt(metadata?.total_pages) || 1); + if (parseInt(metadata?.page) <= parseInt(metadata?.total_pages)) { + setPage(parseInt(metadata?.page) + 1); + } + + // Merge the new data with the existing data + if (currentData.length > 0) { + const updatedData = [...currentData, ...newData]; + + // Remove duplicates + const uniqueData = updatedData.filter( + (v: any, i: number, a: any) => + a.findIndex((t: any) => t.span_id === v.span_id) === i + ); + + setCurrentData(uniqueData); + } else { + setCurrentData(newData); + } + setShowLoader(false); + }, + }); + + useBottomScrollListener(() => { + if (fetchLlmPromptSpans.isRefetching) { + return; + } + if (page <= totalPages) { + setShowLoader(true); + fetchLlmPromptSpans.refetch(); + } + }); + + return ( +
+ {currentData.length > 0 && ( +
+

+ Timestamp (UTC) +

+

Model

+

Input

+

Output

+

Cost

+

PII Detected

+

Duration

+

Evaluate

+

User Score

+

Added to Dataset

+
+ )} + {fetchLlmPromptSpans.isLoading || + !fetchLlmPromptSpans.data || + !currentData ? ( + + ) : ( + currentData.map((span: any, i: number) => ( + + )) + )} + {showLoader && ( +
+ +
+ )} + {!fetchLlmPromptSpans.isLoading && + fetchLlmPromptSpans.data && + currentData.length === 0 && ( +
+

+ Setup instructions 👇 +

+ +
+ )} +
+ ); +} + +export function EvaluationTableSkeleton() { + return ( +
+
+

+ Timestamp (UTC) +

+

Model

+

Input

+

Output

+

Cost

+

PII Detected

+

Duration

+

Evaluate

+

User Score

+

Added to Dataset

+
+ {Array.from({ length: 5 }).map((span: any, i: number) => ( + + ))} +
+ ); +} diff --git a/components/project/dataset/create-data.tsx b/components/project/dataset/create-data.tsx index ba4d558f..5ce00b62 100644 --- a/components/project/dataset/create-data.tsx +++ b/components/project/dataset/create-data.tsx @@ -28,10 +28,12 @@ import { z } from "zod"; export function CreateData({ datasetId, + disabled = false, variant = "default", className = "", }: { - datasetId: string; + datasetId?: string; + disabled?: boolean; variant?: any; className?: string; }) { @@ -49,7 +51,7 @@ export function CreateData({ return ( - @@ -175,10 +177,12 @@ export function CreateData({ export function CreatePrompt({ promptsetId, + disabled = false, variant = "default", className = "", }: { - promptsetId: string; + promptsetId?: string; + disabled?: boolean; variant?: any; className?: string; }) { @@ -195,7 +199,7 @@ export function CreatePrompt({ return ( - diff --git a/components/project/dataset/create.tsx b/components/project/dataset/create.tsx index b75e0631..8b592954 100644 --- a/components/project/dataset/create.tsx +++ b/components/project/dataset/create.tsx @@ -28,10 +28,12 @@ import { z } from "zod"; export function CreateDataset({ projectId, + disabled = false, variant = "default", className = "", }: { - projectId: string; + projectId?: string; + disabled?: boolean; variant?: any; className?: string; }) { @@ -48,7 +50,7 @@ export function CreateDataset({ return ( - @@ -156,10 +158,12 @@ export function CreateDataset({ export function CreatePromptset({ projectId, + disabled = false, variant = "default", className = "", }: { - projectId: string; + projectId?: string; + disabled?: boolean; variant?: any; className?: string; }) { @@ -176,7 +180,7 @@ export function CreatePromptset({ return ( - diff --git a/components/project/dataset/data-set.tsx b/components/project/dataset/data-set.tsx index 0d72f691..af1ada71 100644 --- a/components/project/dataset/data-set.tsx +++ b/components/project/dataset/data-set.tsx @@ -1,9 +1,11 @@ +import CardLoading from "@/components/shared/card-skeleton"; import { Card, CardDescription, CardHeader, CardTitle, } from "@/components/ui/card"; +import { cn } from "@/lib/utils"; import Link from "next/link"; import { useParams } from "next/navigation"; import { useQuery } from "react-query"; @@ -28,7 +30,7 @@ export default function DataSet({ email }: { email: string }) { fetchDatasets.isLoading || !fetchDatasets.data ) { - return
Loading...
; + return ; } else { return (
@@ -77,3 +79,28 @@ export default function DataSet({ email }: { email: string }) { ); } } + +function PageLoading() { + return ( +
+
+ +
+
+
+ {Array.from({ length: 3 }).map((_, index) => ( + + ))} +
+
+
+ ); +} diff --git a/components/project/dataset/dataset-row-skeleton.tsx b/components/project/dataset/dataset-row-skeleton.tsx new file mode 100644 index 00000000..60e82a9c --- /dev/null +++ b/components/project/dataset/dataset-row-skeleton.tsx @@ -0,0 +1,29 @@ +"use client"; + +import { Separator } from "@/components/ui/separator"; +import { Skeleton } from "@/components/ui/skeleton"; + +export default function DatasetRowSkeleton() { + return ( +
+
+

+ +

+

+ +

+

+ +

+

+ +

+
+ +
+
+ +
+ ); +} diff --git a/components/project/dataset/prompt-set.tsx b/components/project/dataset/prompt-set.tsx index add8d501..53a851f3 100644 --- a/components/project/dataset/prompt-set.tsx +++ b/components/project/dataset/prompt-set.tsx @@ -1,9 +1,11 @@ +import CardLoading from "@/components/shared/card-skeleton"; import { Card, CardDescription, CardHeader, CardTitle, } from "@/components/ui/card"; +import { cn } from "@/lib/utils"; import Link from "next/link"; import { useParams } from "next/navigation"; import { useQuery } from "react-query"; @@ -28,7 +30,7 @@ export default function PromptSet({ email }: { email: string }) { fetchPromptsets.isLoading || !fetchPromptsets.data ) { - return
Loading...
; + return ; } else { return (
@@ -78,3 +80,28 @@ export default function PromptSet({ email }: { email: string }) { ); } } + +function PageLoading() { + return ( +
+
+ +
+
+
+ {Array.from({ length: 3 }).map((_, index) => ( + + ))} +
+
+
+ ); +} diff --git a/components/project/metrics.tsx b/components/project/metrics.tsx index 186bbeda..9bc48aa9 100644 --- a/components/project/metrics.tsx +++ b/components/project/metrics.tsx @@ -3,8 +3,10 @@ import { useParams } from "next/navigation"; import { useQuery } from "react-query"; import { EvalChart } from "../charts/eval-chart"; +import LargeChartSkeleton from "../charts/large-chart-skeleton"; import { TraceLatencyChart } from "../charts/latency-chart"; import { ModelAccuracyChart } from "../charts/model-accuracy-chart"; +import SmallChartSkeleton from "../charts/small-chart-skeleton"; import { CostChart, TokenChart } from "../charts/token-chart"; import { TraceSpanChart } from "../charts/trace-chart"; import { Info } from "../shared/info"; @@ -37,7 +39,7 @@ export default function Metrics({ email }: { email: string }) { fetchTests.isLoading || !fetchTests.data ) { - return
Loading...
; + return ; } else { // get test obj of factual accuracy test const test = fetchTests?.data?.tests?.find( @@ -82,3 +84,43 @@ export default function Metrics({ email }: { email: string }) { ); } } + +function PageSkeleton() { + return ( +
+
+

Usage

+ +
+ + + +
+
+
+
+
+

Latency

+
+ + +
+
+
+
+

Evaluated Accuracy

+ +
+ + +
+
+

Evaluated Accuracy per Model

+ +
+ +
+
+
+ ); +} diff --git a/components/project/playground.tsx b/components/project/playground.tsx index a8b81d93..0084a5d4 100644 --- a/components/project/playground.tsx +++ b/components/project/playground.tsx @@ -3,12 +3,11 @@ import { ApiKeyDialog } from "@/components/apiKey/api-dialog"; import { useParams } from "next/navigation"; import { useQuery } from "react-query"; +import { Skeleton } from "../ui/skeleton"; export default function Playground({ email }: { email: string }) { const project_id = useParams()?.project_id as string; - - const fetchProject = useQuery({ queryKey: ["fetch-project-query"], queryFn: async () => { @@ -33,7 +32,7 @@ export default function Playground({ email }: { email: string }) { fetchUser.isLoading || !fetchUser.data ) { - return
Loading...
; + return ; } return ( @@ -49,3 +48,16 @@ export default function Playground({ email }: { email: string }) {
); } + +function PageLoading() { + return ( +
+
+
+ +
+ +
+
+ ); +} diff --git a/components/project/traces.tsx b/components/project/traces.tsx deleted file mode 100644 index 8a86b232..00000000 --- a/components/project/traces.tsx +++ /dev/null @@ -1,552 +0,0 @@ -"use client"; - -import { PAGE_SIZE } from "@/lib/constants"; -import { AttributesFilter } from "@/lib/services/query_builder_service"; -import { - calculateTotalTime, - convertTracesToHierarchy, - correctTimestampFormat, -} from "@/lib/trace_utils"; -import { - calculatePriceFromUsage, - cn, - formatDateTime, - parseNestedJsonFields, -} from "@/lib/utils"; -import { ChevronDown, ChevronRight } from "lucide-react"; -import { useParams } from "next/navigation"; -import { useEffect, useState } from "react"; -import { useBottomScrollListener } from "react-bottom-scroll-listener"; -import { useQuery } from "react-query"; -import { HoverCell } from "../shared/hover-cell"; -import { LLMView } from "../shared/llm-view"; -import { SetupInstructions } from "../shared/setup-instructions"; -import { Spinner } from "../shared/spinner"; -import { serviceTypeColor, vendorBadgeColor } from "../shared/vendor-metadata"; -import TraceGraph from "../traces/trace_graph"; -import { Button } from "../ui/button"; -import { Checkbox } from "../ui/checkbox"; -import { Label } from "../ui/label"; -import { Separator } from "../ui/separator"; -import { Switch } from "../ui/switch"; - -export default function Traces({ email }: { email: string }) { - const project_id = useParams()?.project_id as string; - const [page, setPage] = useState(1); - const [totalPages, setTotalPages] = useState(1); - const [currentData, setCurrentData] = useState([]); - const [showLoader, setShowLoader] = useState(false); - const [filters, setFilters] = useState([]); - const [enableFetch, setEnableFetch] = useState(false); - const [utcTime, setUtcTime] = useState(true); - - useEffect(() => { - setShowLoader(true); - setCurrentData([]); - setPage(1); - setTotalPages(1); - setEnableFetch(true); - }, [filters]); - - const scrollableDivRef = useBottomScrollListener(() => { - if (fetchTraces.isRefetching) { - return; - } - if (page <= totalPages) { - setShowLoader(true); - fetchTraces.refetch(); - } - }); - - const fetchProject = useQuery({ - queryKey: ["fetch-project-query"], - queryFn: async () => { - const response = await fetch(`/api/project?id=${project_id}`); - const result = await response.json(); - return result; - }, - }); - - const fetchUser = useQuery({ - queryKey: ["fetch-user-query"], - queryFn: async () => { - const response = await fetch(`/api/user?email=${email}`); - const result = await response.json(); - return result; - }, - }); - - const fetchTraces = useQuery({ - queryKey: ["fetch-traces-query"], - queryFn: async () => { - // convert filterserviceType to a string - const apiEndpoint = "/api/traces"; - const body = { - page, - pageSize: PAGE_SIZE, - projectId: project_id, - filters: filters, - }; - - const response = await fetch(apiEndpoint, { - method: "POST", - headers: { - "Content-Type": "application/json", - }, - body: JSON.stringify(body), - }); - const result = await response.json(); - return result; - }, - onSuccess: (data) => { - // Get the newly fetched data and metadata - const newData = data?.traces?.result || []; - const metadata = data?.traces?.metadata || {}; - - // Update the total pages and current page number - setTotalPages(parseInt(metadata?.total_pages) || 1); - if (parseInt(metadata?.page) <= parseInt(metadata?.total_pages)) { - setPage(parseInt(metadata?.page) + 1); - } - - // Merge the new data with the existing data - if (currentData.length > 0) { - const updatedData = [...currentData, ...newData]; - - // TODO(Karthik): The results are an array of span arrays, so - // we need to figure out how to merge them correctly. - // Remove duplicates - // const uniqueData = updatedData.filter( - // (v: any, i: number, a: any) => - // a.findIndex((t: any) => t.span_id === v.span_id) === i - // ); - - setCurrentData(updatedData); - } else { - setCurrentData(newData); - } - - setEnableFetch(false); - setShowLoader(false); - }, - refetchOnWindowFocus: false, - enabled: enableFetch, - }); - - const FILTERS = [ - { - key: "llm", - value: "LLM Requests", - }, - { - key: "vectordb", - value: "VectorDB Requests", - }, - { - key: "framework", - value: "Framework Requests", - }, - ]; - - return ( -
-
-
- {FILTERS.map((item, i) => ( -
- filter.value === item.key)} - onCheckedChange={(checked) => { - if (checked) { - setFilters([ - ...filters, - { - key: "langtrace.service.type", - operation: "EQUALS", - value: item.key, - }, - ]); - } else { - setFilters( - filters.filter((filter) => filter.value !== item.key) - ); - } - }} - /> - -
- ))} -
-
- - setUtcTime(check)} - /> - -
-
-
-

Timestamp (UTC)

-

Namespace

-

Model

-

Input

-

Output

-

User ID

-

Input / Output / Total Tokens

-

Token Cost

-

Duration(ms)

-
- {fetchProject.isLoading || - !fetchProject.data || - fetchUser.isLoading || - !fetchUser.data || - fetchTraces.isLoading || - !fetchTraces.data || - !currentData ? ( -
Loading...
- ) : ( -
- {!fetchTraces.isLoading && - fetchTraces.data && - currentData.map((trace: any, i: number) => { - return ( -
- -
- ); - })} - {showLoader && ( -
- -
- )} - {!fetchTraces.isLoading && - fetchTraces.data && - currentData.length === 0 && - !showLoader && ( -
-

- No traces available. Get started by setting up Langtrace in - your application. -

- -
- )} -
- )} -
- ); -} - -const TraceRow = ({ trace, utcTime }: { trace: any; utcTime: boolean }) => { - const traceHierarchy = convertTracesToHierarchy(trace); - const totalTime = calculateTotalTime(trace); - const startTime = trace[0].start_time; - const [collapsed, setCollapsed] = useState(true); - const [selectedTab, setSelectedTab] = useState("trace"); - - // capture the token counts from the trace - let tokenCounts: any = {}; - let model: string = ""; - let vendor: string = ""; - let userId: string = ""; - let prompts: any = {}; - let responses: any = {}; - let cost = { total: 0, input: 0, output: 0 }; - for (const span of trace) { - if (span.attributes) { - const attributes = JSON.parse(span.attributes); - userId = attributes["user.id"]; - if (attributes["llm.prompts"] && attributes["llm.responses"]) { - prompts = attributes["llm.prompts"]; - responses = attributes["llm.responses"]; - } - if (attributes["llm.token.counts"]) { - model = attributes["llm.model"]; - vendor = attributes["langtrace.service.name"].toLowerCase(); - const currentcounts = JSON.parse(attributes["llm.token.counts"]); - tokenCounts = { - input_tokens: tokenCounts.input_tokens - ? tokenCounts.input_tokens + currentcounts.input_tokens - : currentcounts.input_tokens, - output_tokens: tokenCounts.output_tokens - ? tokenCounts.output_tokens + currentcounts.output_tokens - : currentcounts.output_tokens, - total_tokens: tokenCounts.total_tokens - ? tokenCounts.total_tokens + currentcounts.total_tokens - : currentcounts.total_tokens, - }; - - const currentcost = calculatePriceFromUsage( - vendor, - model, - currentcounts - ); - // add the cost of the current span to the total cost - cost.total += currentcost.total; - cost.input += currentcost.input; - cost.output += currentcost.output; - } - } - } - - // Sort the trace based on start_time, then end_time - trace.sort((a: any, b: any) => { - if (a.start_time === b.start_time) { - return a.end_time < b.end_time ? 1 : -1; - } - return a.start_time < b.start_time ? -1 : 1; - }); - - return ( -
-
setCollapsed(!collapsed)} - > -
- -

- {formatDateTime( - correctTimestampFormat(traceHierarchy[0].start_time), - !utcTime - )} -

-
-
- {traceHierarchy[0].status !== "ERROR" && ( - - )} - {traceHierarchy[0].status === "ERROR" && ( - - )} -

{traceHierarchy[0].name}

-
-

{model}

- 0 ? JSON.parse(prompts)[0]?.content : ""} - className="text-xs h-10 truncate overflow-y-scroll font-semibold col-span-2" - /> - 0 - ? JSON.parse(responses)[0]?.message?.content || - JSON.parse(responses)[0]?.text || - JSON.parse(responses)[0]?.content - : "" - } - className="text-xs h-10 truncate overflow-y-scroll font-semibold col-span-2" - /> -

{userId}

-
-

- {tokenCounts?.input_tokens || tokenCounts?.prompt_tokens} -

- {tokenCounts?.input_tokens || tokenCounts?.prompt_tokens ? "+" : ""} -

- {tokenCounts?.output_tokens || tokenCounts?.completion_tokens}{" "} -

- {tokenCounts?.output_tokens || tokenCounts?.completion_tokens - ? "=" - : ""} -

{tokenCounts?.total_tokens}

-
-

- {cost.total.toFixed(6) !== "0.000000" - ? `\$${cost.total.toFixed(6)}` - : ""} -

-
- {totalTime}ms -
-
- {!collapsed && ( -
-
- - - -
- - {selectedTab === "trace" && ( - - )} - {selectedTab === "logs" && ( -
- {trace.map((span: any, i: number) => { - return ; - })} -
- )} - {selectedTab === "llm" && ( -
- -
- )} -
- )} -
- ); -}; - -const LogsView = ({ span, utcTime }: { span: any; utcTime: boolean }) => { - const [collapsed, setCollapsed] = useState(true); - const servTypeColor = serviceTypeColor( - JSON.parse(span.attributes)["langtrace.service.type"] - ); - - const servColor = vendorBadgeColor( - JSON.parse(span.attributes)["langtrace.service.name"]?.toLowerCase() - ); - return ( -
-
setCollapsed(!collapsed)} - > - -

- {formatDateTime(correctTimestampFormat(span.start_time), !utcTime)} -

-

- {span.name} -

- {JSON.parse(span.attributes)["langtrace.service.type"] && ( -

- {JSON.parse(span.attributes)["langtrace.service.type"]} -

- )} - {JSON.parse(span.attributes)["langtrace.service.name"] && ( -

- {JSON.parse(span.attributes)["langtrace.service.name"]} -

- )} - {JSON.parse(span.attributes)["langtrace.service.version"] && ( -

- {JSON.parse(span.attributes)["langtrace.service.version"]} -

- )} - {JSON.parse(span.attributes)["langtrace.service.type"] === "llm" && ( -

- {JSON.parse(span.attributes)["llm.model"]} -

- )} -
- {!collapsed && ( -
-          {parseNestedJsonFields(span.attributes)}
-        
- )} - -
- ); -}; diff --git a/components/project/traces/logs-view.tsx b/components/project/traces/logs-view.tsx new file mode 100644 index 00000000..a942d2b2 --- /dev/null +++ b/components/project/traces/logs-view.tsx @@ -0,0 +1,98 @@ +"use client"; +"use client"; + +import { correctTimestampFormat } from "@/lib/trace_utils"; +import { cn, formatDateTime, parseNestedJsonFields } from "@/lib/utils"; +import { ChevronDown, ChevronRight } from "lucide-react"; +import { useState } from "react"; +import { + serviceTypeColor, + vendorBadgeColor, +} from "../../shared/vendor-metadata"; +import { Button } from "../../ui/button"; +import { Separator } from "../../ui/separator"; + +export const LogsView = ({ + span, + utcTime, +}: { + span: any; + utcTime: boolean; +}) => { + const [collapsed, setCollapsed] = useState(true); + const servTypeColor = serviceTypeColor( + JSON.parse(span.attributes)["langtrace.service.type"] + ); + + const servColor = vendorBadgeColor( + JSON.parse(span.attributes)["langtrace.service.name"]?.toLowerCase() + ); + return ( +
+
setCollapsed(!collapsed)} + > + +

+ {formatDateTime(correctTimestampFormat(span.start_time), !utcTime)} +

+

+ {span.name} +

+ {JSON.parse(span.attributes)["langtrace.service.type"] && ( +

+ {JSON.parse(span.attributes)["langtrace.service.type"]} +

+ )} + {JSON.parse(span.attributes)["langtrace.service.name"] && ( +

+ {JSON.parse(span.attributes)["langtrace.service.name"]} +

+ )} + {JSON.parse(span.attributes)["langtrace.service.version"] && ( +

+ {JSON.parse(span.attributes)["langtrace.service.version"]} +

+ )} + {JSON.parse(span.attributes)["langtrace.service.type"] === "llm" && ( +

+ {JSON.parse(span.attributes)["llm.model"]} +

+ )} +
+ {!collapsed && ( +
+          {parseNestedJsonFields(span.attributes)}
+        
+ )} + +
+ ); +}; diff --git a/components/project/traces/trace-row-skeleton.tsx b/components/project/traces/trace-row-skeleton.tsx new file mode 100644 index 00000000..e6f61e26 --- /dev/null +++ b/components/project/traces/trace-row-skeleton.tsx @@ -0,0 +1,29 @@ +"use client"; + +import { Separator } from "@/components/ui/separator"; +import { Skeleton } from "@/components/ui/skeleton"; + +export default function TraceRowSkeleton() { + return ( +
+
+

+ +

+

+ +

+

+ +

+

+ +

+
+ +
+
+ +
+ ); +} diff --git a/components/project/traces/trace-row.tsx b/components/project/traces/trace-row.tsx new file mode 100644 index 00000000..93bd6f7e --- /dev/null +++ b/components/project/traces/trace-row.tsx @@ -0,0 +1,250 @@ +"use client"; + +import { + calculateTotalTime, + convertTracesToHierarchy, + correctTimestampFormat, +} from "@/lib/trace_utils"; +import { calculatePriceFromUsage, formatDateTime } from "@/lib/utils"; +import { ChevronDown, ChevronRight } from "lucide-react"; +import { useState } from "react"; +import { HoverCell } from "../../shared/hover-cell"; +import { LLMView } from "../../shared/llm-view"; +import TraceGraph from "../../traces/trace_graph"; +import { Button } from "../../ui/button"; +import { Separator } from "../../ui/separator"; +import { LogsView } from "./logs-view"; + +export const TraceRow = ({ + trace, + utcTime, +}: { + trace: any; + utcTime: boolean; +}) => { + const traceHierarchy = convertTracesToHierarchy(trace); + const totalTime = calculateTotalTime(trace); + const startTime = trace[0].start_time; + const [collapsed, setCollapsed] = useState(true); + const [selectedTab, setSelectedTab] = useState("trace"); + + // capture the token counts from the trace + let tokenCounts: any = {}; + let model: string = ""; + let vendor: string = ""; + let userId: string = ""; + let prompts: any = {}; + let responses: any = {}; + let cost = { total: 0, input: 0, output: 0 }; + for (const span of trace) { + if (span.attributes) { + const attributes = JSON.parse(span.attributes); + userId = attributes["user.id"]; + if (attributes["llm.prompts"] && attributes["llm.responses"]) { + prompts = attributes["llm.prompts"]; + responses = attributes["llm.responses"]; + } + if (attributes["llm.token.counts"]) { + model = attributes["llm.model"]; + vendor = attributes["langtrace.service.name"].toLowerCase(); + const currentcounts = JSON.parse(attributes["llm.token.counts"]); + tokenCounts = { + input_tokens: tokenCounts.input_tokens + ? tokenCounts.input_tokens + currentcounts.input_tokens + : currentcounts.input_tokens, + output_tokens: tokenCounts.output_tokens + ? tokenCounts.output_tokens + currentcounts.output_tokens + : currentcounts.output_tokens, + total_tokens: tokenCounts.total_tokens + ? tokenCounts.total_tokens + currentcounts.total_tokens + : currentcounts.total_tokens, + }; + + const currentcost = calculatePriceFromUsage( + vendor, + model, + currentcounts + ); + // add the cost of the current span to the total cost + cost.total += currentcost.total; + cost.input += currentcost.input; + cost.output += currentcost.output; + } + } + } + + // Sort the trace based on start_time, then end_time + trace.sort((a: any, b: any) => { + if (a.start_time === b.start_time) { + return a.end_time < b.end_time ? 1 : -1; + } + return a.start_time < b.start_time ? -1 : 1; + }); + + return ( +
+
setCollapsed(!collapsed)} + > +
+ +

+ {formatDateTime( + correctTimestampFormat(traceHierarchy[0].start_time), + !utcTime + )} +

+
+
+ {traceHierarchy[0].status !== "ERROR" && ( + + )} + {traceHierarchy[0].status === "ERROR" && ( + + )} +

{traceHierarchy[0].name}

+
+

{model}

+ 0 ? JSON.parse(prompts)[0]?.content : ""} + className="text-xs h-10 truncate overflow-y-scroll font-semibold col-span-2" + /> + 0 + ? JSON.parse(responses)[0]?.message?.content || + JSON.parse(responses)[0]?.text || + JSON.parse(responses)[0]?.content + : "" + } + className="text-xs h-10 truncate overflow-y-scroll font-semibold col-span-2" + /> +

{userId}

+
+

+ {tokenCounts?.input_tokens || tokenCounts?.prompt_tokens} +

+ {tokenCounts?.input_tokens || tokenCounts?.prompt_tokens ? "+" : ""} +

+ {tokenCounts?.output_tokens || tokenCounts?.completion_tokens}{" "} +

+ {tokenCounts?.output_tokens || tokenCounts?.completion_tokens + ? "=" + : ""} +

{tokenCounts?.total_tokens}

+
+

+ {cost.total.toFixed(6) !== "0.000000" + ? `\$${cost.total.toFixed(6)}` + : ""} +

+
+ {totalTime}ms +
+
+ {!collapsed && ( +
+
+ + + +
+ + {selectedTab === "trace" && ( + + )} + {selectedTab === "logs" && ( +
+ {trace.map((span: any, i: number) => { + return ; + })} +
+ )} + {selectedTab === "llm" && ( +
+ +
+ )} +
+ )} +
+ ); +}; diff --git a/components/project/traces/traces.tsx b/components/project/traces/traces.tsx new file mode 100644 index 00000000..ff9cc9d7 --- /dev/null +++ b/components/project/traces/traces.tsx @@ -0,0 +1,243 @@ +"use client"; + +import { TraceRow } from "@/components/project/traces/trace-row"; +import { PAGE_SIZE } from "@/lib/constants"; +import { AttributesFilter } from "@/lib/services/query_builder_service"; +import { useParams } from "next/navigation"; +import { useEffect, useState } from "react"; +import { useBottomScrollListener } from "react-bottom-scroll-listener"; +import { useQuery } from "react-query"; +import { SetupInstructions } from "../../shared/setup-instructions"; +import { Spinner } from "../../shared/spinner"; +import { Checkbox } from "../../ui/checkbox"; +import { Label } from "../../ui/label"; +import { Separator } from "../../ui/separator"; +import { Switch } from "../../ui/switch"; +import TraceRowSkeleton from "./trace-row-skeleton"; + +export default function Traces({ email }: { email: string }) { + const project_id = useParams()?.project_id as string; + const [page, setPage] = useState(1); + const [totalPages, setTotalPages] = useState(1); + const [currentData, setCurrentData] = useState([]); + const [showLoader, setShowLoader] = useState(false); + const [filters, setFilters] = useState([]); + const [enableFetch, setEnableFetch] = useState(false); + const [utcTime, setUtcTime] = useState(true); + + useEffect(() => { + setShowLoader(true); + setCurrentData([]); + setPage(1); + setTotalPages(1); + setEnableFetch(true); + }, [filters]); + + const scrollableDivRef = useBottomScrollListener(() => { + if (fetchTraces.isRefetching) { + return; + } + if (page <= totalPages) { + setShowLoader(true); + fetchTraces.refetch(); + } + }); + + const fetchProject = useQuery({ + queryKey: ["fetch-project-query"], + queryFn: async () => { + const response = await fetch(`/api/project?id=${project_id}`); + const result = await response.json(); + return result; + }, + }); + + const fetchUser = useQuery({ + queryKey: ["fetch-user-query"], + queryFn: async () => { + const response = await fetch(`/api/user?email=${email}`); + const result = await response.json(); + return result; + }, + }); + + const fetchTraces = useQuery({ + queryKey: ["fetch-traces-query"], + queryFn: async () => { + // convert filterserviceType to a string + const apiEndpoint = "/api/traces"; + const body = { + page, + pageSize: PAGE_SIZE, + projectId: project_id, + filters: filters, + }; + + const response = await fetch(apiEndpoint, { + method: "POST", + headers: { + "Content-Type": "application/json", + }, + body: JSON.stringify(body), + }); + const result = await response.json(); + return result; + }, + onSuccess: (data) => { + // Get the newly fetched data and metadata + const newData = data?.traces?.result || []; + const metadata = data?.traces?.metadata || {}; + + // Update the total pages and current page number + setTotalPages(parseInt(metadata?.total_pages) || 1); + if (parseInt(metadata?.page) <= parseInt(metadata?.total_pages)) { + setPage(parseInt(metadata?.page) + 1); + } + + // Merge the new data with the existing data + if (currentData.length > 0) { + const updatedData = [...currentData, ...newData]; + + // TODO(Karthik): The results are an array of span arrays, so + // we need to figure out how to merge them correctly. + // Remove duplicates + // const uniqueData = updatedData.filter( + // (v: any, i: number, a: any) => + // a.findIndex((t: any) => t.span_id === v.span_id) === i + // ); + + setCurrentData(updatedData); + } else { + setCurrentData(newData); + } + + setEnableFetch(false); + setShowLoader(false); + }, + refetchOnWindowFocus: false, + enabled: enableFetch, + }); + + const FILTERS = [ + { + key: "llm", + value: "LLM Requests", + }, + { + key: "vectordb", + value: "VectorDB Requests", + }, + { + key: "framework", + value: "Framework Requests", + }, + ]; + + return ( +
+
+
+ {FILTERS.map((item, i) => ( +
+ filter.value === item.key)} + onCheckedChange={(checked) => { + if (checked) { + setFilters([ + ...filters, + { + key: "langtrace.service.type", + operation: "EQUALS", + value: item.key, + }, + ]); + } else { + setFilters( + filters.filter((filter) => filter.value !== item.key) + ); + } + }} + /> + +
+ ))} +
+
+ + setUtcTime(check)} + /> + +
+
+
+

Timestamp (UTC)

+

Namespace

+

Model

+

Input

+

Output

+

User ID

+

Input / Output / Total Tokens

+

Token Cost

+

Duration(ms)

+
+ {fetchProject.isLoading || + !fetchProject.data || + fetchUser.isLoading || + !fetchUser.data || + fetchTraces.isLoading || + !fetchTraces.data || + !currentData ? ( + + ) : ( +
+ {!fetchTraces.isLoading && + fetchTraces.data && + currentData.map((trace: any, i: number) => { + return ( +
+ +
+ ); + })} + {showLoader && ( +
+ +
+ )} + {!fetchTraces.isLoading && + fetchTraces.data && + currentData.length === 0 && + !showLoader && ( +
+

+ No traces available. Get started by setting up Langtrace in + your application. +

+ +
+ )} +
+ )} +
+ ); +} + +function PageSkeleton() { + return ( +
+ {Array.from({ length: 3 }).map((_, index) => ( + + ))} +
+ ); +} diff --git a/components/shared/add-to-dataset.tsx b/components/shared/add-to-dataset.tsx index d336901d..92493194 100644 --- a/components/shared/add-to-dataset.tsx +++ b/components/shared/add-to-dataset.tsx @@ -42,9 +42,11 @@ interface CheckedData { export function AddtoDataset({ projectId, selectedData, + disabled = false, }: { - projectId: string; - selectedData: CheckedData[]; + projectId?: string; + selectedData?: CheckedData[]; + disabled?: boolean; }) { const queryClient = useQueryClient(); const [open, setOpen] = React.useState(false); @@ -53,7 +55,7 @@ export function AddtoDataset({ return ( - @@ -70,7 +72,7 @@ export function AddtoDataset({ Select a dataset
@@ -90,7 +92,7 @@ export function AddtoDataset({ datasetId: selectedDatasetId, }), }); - selectedData.forEach((data) => { + selectedData!.forEach((data) => { queryClient.invalidateQueries({ queryKey: [`fetch-data-query-${data.spanId}`], }); @@ -131,7 +133,7 @@ export default function DatasetCombobox({ }); if (fetchDatasets.isLoading || !fetchDatasets.data) { - return
Loading...
; + return
Loading...
; // this componenet isn't being used, will add updated loading later } else { return ( diff --git a/components/shared/add-to-promptset.tsx b/components/shared/add-to-promptset.tsx index d0caee22..cc6f73d8 100644 --- a/components/shared/add-to-promptset.tsx +++ b/components/shared/add-to-promptset.tsx @@ -41,9 +41,11 @@ interface CheckedData { export function AddtoPromptset({ projectId, selectedData, + disabled = false, }: { - projectId: string; - selectedData: CheckedData[]; + projectId?: string; + selectedData?: CheckedData[]; + disabled?: boolean; }) { const queryClient = useQueryClient(); const [open, setOpen] = React.useState(false); @@ -52,7 +54,7 @@ export function AddtoPromptset({ return ( - @@ -69,7 +71,7 @@ export function AddtoPromptset({ Select a prompt set
@@ -89,7 +91,7 @@ export function AddtoPromptset({ promptsetId: selectedPromptsetId, }), }); - selectedData.forEach((data) => { + selectedData!.forEach((data) => { queryClient.invalidateQueries({ queryKey: [`fetch-promptdata-query-${data.spanId}`], }); @@ -130,7 +132,7 @@ export default function PromptsetCombobox({ }); if (fetchPromptsets.isLoading || !fetchPromptsets.data) { - return
Loading...
; + return
Loading...
; // this componenet isn't being used, will add updated loading later } else { return ( diff --git a/components/shared/card-loading.tsx b/components/shared/card-skeleton.tsx similarity index 95% rename from components/shared/card-loading.tsx rename to components/shared/card-skeleton.tsx index 8a1072f2..08e5a649 100644 --- a/components/shared/card-loading.tsx +++ b/components/shared/card-skeleton.tsx @@ -8,7 +8,7 @@ import { } from "@/components/ui/card"; import { Skeleton } from "@/components/ui/skeleton"; -export default function CardLoading() { +export default function CardSkeleton() { return (
diff --git a/lib/utils.ts b/lib/utils.ts index f46e9cbb..8b2809c9 100644 --- a/lib/utils.ts +++ b/lib/utils.ts @@ -411,3 +411,13 @@ export function extractPromptFromLlmInputs(inputs: any[]): string { } return prompt; } + +export const getChartColor = (value: number) => { + if (value < 50) { + return "red"; + } else if (value < 90 && value >= 50) { + return "yellow"; + } else { + return "green"; + } +};