151 lines
5.4 KiB
TypeScript
151 lines
5.4 KiB
TypeScript
import type { ReactNode } from "react";
|
|
import { formatCurrencyAccurate } from "~/utils/numberFormatter";
|
|
import { Header3 } from "~/components/primitives/Headers";
|
|
import { TextLink } from "~/components/primitives/TextLink";
|
|
import { useEnvironment } from "~/hooks/useEnvironment";
|
|
import { useOrganization } from "~/hooks/useOrganizations";
|
|
import { useProject } from "~/hooks/useProject";
|
|
import { v3PromptPath } from "~/utils/pathBuilder";
|
|
import { TruncatedCopyableValue } from "~/components/primitives/TruncatedCopyableValue";
|
|
import type { AISpanData } from "./types";
|
|
|
|
export function AITagsRow({ aiData }: { aiData: AISpanData }) {
|
|
const organization = useOrganization();
|
|
const project = useProject();
|
|
const environment = useEnvironment();
|
|
|
|
const promptLink =
|
|
aiData.promptSlug && organization && project && environment
|
|
? v3PromptPath(organization, project, environment, aiData.promptSlug, aiData.promptVersion)
|
|
: undefined;
|
|
|
|
return (
|
|
<div className="flex flex-col gap-1 py-2.5">
|
|
<div className="flex flex-col text-xs @container">
|
|
{aiData.responseId && (
|
|
<MetricRow
|
|
label="Response ID"
|
|
value={<TruncatedCopyableValue value={aiData.responseId} />}
|
|
/>
|
|
)}
|
|
<MetricRow label="Model" value={aiData.model} />
|
|
{aiData.provider !== "unknown" && <MetricRow label="Provider" value={aiData.provider} />}
|
|
{aiData.resolvedProvider && (
|
|
<MetricRow label="Resolved provider" value={aiData.resolvedProvider} />
|
|
)}
|
|
{aiData.promptSlug && (
|
|
<MetricRow
|
|
label="Prompt"
|
|
value={
|
|
promptLink ? (
|
|
<TextLink to={promptLink}>
|
|
{aiData.promptSlug}
|
|
{aiData.promptVersion ? ` v${aiData.promptVersion}` : ""}
|
|
</TextLink>
|
|
) : (
|
|
`${aiData.promptSlug}${aiData.promptVersion ? ` v${aiData.promptVersion}` : ""}`
|
|
)
|
|
}
|
|
/>
|
|
)}
|
|
{aiData.finishReason && <MetricRow label="Finish reason" value={aiData.finishReason} />}
|
|
{aiData.serviceTier && <MetricRow label="Service tier" value={aiData.serviceTier} />}
|
|
{aiData.toolChoice && <MetricRow label="Tool choice" value={aiData.toolChoice} />}
|
|
{aiData.toolCount != null && aiData.toolCount > 0 && (
|
|
<MetricRow
|
|
label="Tools provided"
|
|
value={`${aiData.toolCount} ${aiData.toolCount === 1 ? "tool" : "tools"}`}
|
|
/>
|
|
)}
|
|
{aiData.messageCount != null && (
|
|
<MetricRow
|
|
label="Messages"
|
|
value={`${aiData.messageCount} ${aiData.messageCount === 1 ? "message" : "messages"}`}
|
|
/>
|
|
)}
|
|
{aiData.telemetryMetadata &&
|
|
Object.entries(aiData.telemetryMetadata)
|
|
.filter(([key]) => key !== "prompt")
|
|
.map(([key, value]) => <MetricRow key={key} label={key} value={value} />)}
|
|
</div>
|
|
</div>
|
|
);
|
|
}
|
|
|
|
export function AIStatsSummary({ aiData }: { aiData: AISpanData }) {
|
|
return (
|
|
<div className="flex flex-col gap-1 py-2.5">
|
|
<Header3>Stats</Header3>
|
|
<div className="flex flex-col text-xs @container">
|
|
<MetricRow label="Input" value={aiData.inputTokens.toLocaleString()} unit="tokens" />
|
|
<MetricRow label="Output" value={aiData.outputTokens.toLocaleString()} unit="tokens" />
|
|
{aiData.cachedTokens != null && aiData.cachedTokens > 0 && (
|
|
<MetricRow
|
|
label="Cache read"
|
|
value={aiData.cachedTokens.toLocaleString()}
|
|
unit="tokens"
|
|
/>
|
|
)}
|
|
{aiData.cacheCreationTokens != null && aiData.cacheCreationTokens > 0 && (
|
|
<MetricRow
|
|
label="Cache write"
|
|
value={aiData.cacheCreationTokens.toLocaleString()}
|
|
unit="tokens"
|
|
/>
|
|
)}
|
|
{aiData.reasoningTokens != null && aiData.reasoningTokens > 0 && (
|
|
<MetricRow
|
|
label="Reasoning"
|
|
value={aiData.reasoningTokens.toLocaleString()}
|
|
unit="tokens"
|
|
/>
|
|
)}
|
|
<MetricRow label="Total" value={aiData.totalTokens.toLocaleString()} unit="tokens" bold />
|
|
|
|
{aiData.totalCost != null && (
|
|
<MetricRow label="Cost" value={formatCurrencyAccurate(aiData.totalCost)} />
|
|
)}
|
|
{aiData.msToFirstChunk != null && (
|
|
<MetricRow label="TTFC" value={formatTtfc(aiData.msToFirstChunk)} />
|
|
)}
|
|
{aiData.tokensPerSecond != null && (
|
|
<MetricRow label="Speed" value={`${Math.round(aiData.tokensPerSecond)} tok/s`} />
|
|
)}
|
|
</div>
|
|
</div>
|
|
);
|
|
}
|
|
|
|
function MetricRow({
|
|
label,
|
|
value,
|
|
unit,
|
|
bold,
|
|
}: {
|
|
label: string;
|
|
value: ReactNode;
|
|
unit?: string;
|
|
bold?: boolean;
|
|
}) {
|
|
return (
|
|
<div className="grid h-7 grid-cols-[1fr_auto] items-center gap-4 rounded-sm px-1.5 transition odd:bg-background-hover/40 @[28rem]:grid-cols-[8rem_1fr] hover:bg-white/4">
|
|
<span className="text-text-dimmed">{label}</span>
|
|
<span
|
|
className={`text-right @[28rem]:text-left ${
|
|
bold ? "font-medium text-text-bright" : "text-text-bright"
|
|
}`}
|
|
>
|
|
{value}
|
|
{unit && <span className="ml-1 text-text-dimmed">{unit}</span>}
|
|
</span>
|
|
</div>
|
|
);
|
|
}
|
|
|
|
function formatTtfc(ms: number): string {
|
|
if (ms >= 10_000) {
|
|
return `${(ms / 1000).toFixed(1)}s`;
|
|
}
|
|
return `${Math.round(ms)}ms`;
|
|
}
|