import { DashboardCard } from "@/src/features/dashboard/components/cards/DashboardCard";
import { DashboardTable } from "@/src/features/dashboard/components/cards/DashboardTable";
import {
type ScoreDataTypeType,
type ScoreSourceType,
type FilterState,
} from "@langfuse/shared";
import { api } from "@/src/utils/api";
import { compactNumberFormatter } from "@/src/utils/numbers";
import { RightAlignedCell } from "./RightAlignedCell";
import { LeftAlignedCell } from "@/src/features/dashboard/components/LeftAlignedCell";
import { TotalMetric } from "./TotalMetric";
import { createTracesTimeFilter } from "@/src/features/dashboard/lib/dashboard-utils";
import { getScoreDataTypeIcon } from "@/src/features/scores/lib/scoreColumns";
import {
isBooleanDataType,
isCategoricalDataType,
isNumericDataType,
} from "@/src/features/scores/lib/helpers";
import { type DatabaseRow } from "@/src/server/api/services/sqlInterface";
import { NoDataOrLoading } from "@/src/components/NoDataOrLoading";
const dropValuesForCategoricalScores = (
value: number,
scoreDataType: ScoreDataTypeType,
): string => {
if (isCategoricalDataType(scoreDataType)) return "-";
if (isBooleanDataType(scoreDataType) || isNumericDataType(scoreDataType)) {
return compactNumberFormatter(value);
}
return "-";
};
const scoreNameSourceDataTypeMatch =
(
scoreName: string,
scoreSource: ScoreSourceType,
scoreDataType: ScoreDataTypeType,
) =>
(item: DatabaseRow) =>
item.scoreName === scoreName &&
item.scoreSource === scoreSource &&
item.scoreDataType === scoreDataType;
export const ScoresTable = ({
className,
projectId,
globalFilterState,
isLoading = false,
}: {
className: string;
projectId: string;
globalFilterState: FilterState;
isLoading?: boolean;
}) => {
const localFilters = createTracesTimeFilter(
globalFilterState,
"scoreTimestamp",
);
const metrics = api.dashboard.chart.useQuery(
{
projectId,
from: "traces_scores",
select: [
{ column: "scoreName" },
{ column: "scoreId", agg: "COUNT" },
{ column: "value", agg: "AVG" },
{ column: "scoreSource" },
{ column: "scoreDataType" },
],
filter: localFilters,
groupBy: [
{ type: "string", column: "scoreName" },
{
type: "string",
column: "scoreSource",
},
{
type: "string",
column: "scoreDataType",
},
],
orderBy: [{ column: "scoreId", direction: "DESC", agg: "COUNT" }],
queryName: "score-aggregate",
},
{
trpc: {
context: {
skipBatch: true,
},
},
enabled: !isLoading,
},
);
const [zeroValueScores, oneValueScores] = [0, 1].map((i) =>
api.dashboard.chart.useQuery(
{
projectId,
from: "traces_scores",
select: [
{ column: "scoreName" },
{ column: "scoreId", agg: "COUNT" },
{ column: "scoreSource" },
{ column: "scoreDataType" },
],
filter: [
...localFilters,
{
column: "value",
operator: "=",
value: i,
type: "number",
},
],
groupBy: [
{ type: "string", column: "scoreName" },
{
type: "string",
column: "scoreSource",
},
{
type: "string",
column: "scoreDataType",
},
],
orderBy: [{ column: "scoreId", direction: "DESC", agg: "COUNT" }],
queryName: "score-aggregate",
},
{
trpc: {
context: {
skipBatch: true,
},
},
enabled: !isLoading,
},
),
);
if (!zeroValueScores || !oneValueScores) {
return (
);
}
const joinRequestData = () => {
if (!metrics.data || !zeroValueScores.data || !oneValueScores.data)
return [];
return metrics.data.map((metric) => {
const scoreName = metric.scoreName as string;
const scoreSource = metric.scoreSource as ScoreSourceType;
const scoreDataType = metric.scoreDataType as ScoreDataTypeType;
const zeroValueScore = zeroValueScores.data.find(
scoreNameSourceDataTypeMatch(scoreName, scoreSource, scoreDataType),
);
const oneValueScore = oneValueScores.data.find(
scoreNameSourceDataTypeMatch(scoreName, scoreSource, scoreDataType),
);
return {
scoreName,
scoreSource,
scoreDataType,
countScoreId: metric.countScoreId ? metric.countScoreId : 0,
avgValue: metric.avgValue ? (metric.avgValue as number) : 0,
zeroValueScore: zeroValueScore?.countScoreId
? zeroValueScore.countScoreId
: 0,
oneValueScore: oneValueScore?.countScoreId
? (oneValueScore.countScoreId as number)
: 0,
};
});
};
const data = joinRequestData();
const totalScores = data.reduce(
(acc, curr) => acc + (curr.countScoreId as number),
0,
);
return (
#,
Avg,
0,
1,
]}
rows={data.map((item, i) => [
{`${getScoreDataTypeIcon(item.scoreDataType)} ${item.scoreName} (${item.scoreSource.toLowerCase()})`},
{compactNumberFormatter(item.countScoreId as number)}
,
{dropValuesForCategoricalScores(item.avgValue, item.scoreDataType)}
,
{dropValuesForCategoricalScores(
item.zeroValueScore as number,
item.scoreDataType,
)}
,
{dropValuesForCategoricalScores(
item.oneValueScore,
item.scoreDataType,
)}
,
])}
collapse={{ collapsed: 5, expanded: 20 }}
isLoading={
isLoading ||
metrics.isPending ||
zeroValueScores.isPending ||
oneValueScores.isPending
}
noDataProps={{
description:
"Scores evaluate LLM quality and can be created manually or using the SDK.",
href: "https://langfuse.com/docs/evaluation/overview",
}}
>
);
};