Skip to content

Commit 5c7daeb

Browse files
committed
Create AI/LLM metrics dashboard, with support for a model filter
1 parent e9f13b9 commit 5c7daeb

File tree

10 files changed

+601
-11
lines changed

10 files changed

+601
-11
lines changed

apps/webapp/app/components/code/QueryResultsChart.tsx

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1217,6 +1217,11 @@ function createYAxisFormatter(
12171217
if (format === "costInDollars" || format === "cost") {
12181218
return (value: number): string => {
12191219
const dollars = format === "cost" ? value / 100 : value;
1220+
if (dollars === 0) return "$0";
1221+
if (Math.abs(dollars) >= 1000) return `$${(dollars / 1000).toFixed(1)}K`;
1222+
if (Math.abs(dollars) >= 1) return `$${dollars.toFixed(2)}`;
1223+
if (Math.abs(dollars) >= 0.01) return `$${dollars.toFixed(4)}`;
1224+
if (Math.abs(dollars) >= 0.0001) return `$${dollars.toFixed(6)}`;
12201225
return formatCurrencyAccurate(dollars);
12211226
};
12221227
}
Lines changed: 159 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,159 @@
1+
import { CubeIcon } from "@heroicons/react/20/solid";
2+
import * as Ariakit from "@ariakit/react";
3+
import { type ReactNode, useMemo } from "react";
4+
import { AppliedFilter } from "~/components/primitives/AppliedFilter";
5+
import {
6+
ComboBox,
7+
SelectItem,
8+
SelectList,
9+
SelectPopover,
10+
SelectProvider,
11+
SelectTrigger,
12+
} from "~/components/primitives/Select";
13+
import { useSearchParams } from "~/hooks/useSearchParam";
14+
import { appliedSummary, FilterMenuProvider } from "~/components/runs/v3/SharedFilters";
15+
import { tablerIcons } from "~/utils/tablerIcons";
16+
import tablerSpritePath from "~/components/primitives/tabler-sprite.svg";
17+
import { AnthropicLogoIcon } from "~/assets/icons/AnthropicLogoIcon";
18+
19+
const shortcut = { key: "m" };
20+
21+
export type ModelOption = {
22+
model: string;
23+
system: string;
24+
};
25+
26+
interface ModelsFilterProps {
27+
possibleModels: ModelOption[];
28+
}
29+
30+
function modelIcon(system: string, model: string): ReactNode {
31+
// For gateway/openrouter, derive provider from model prefix
32+
let provider = system.split(".")[0];
33+
if (provider === "gateway" || provider === "openrouter") {
34+
if (model.includes("/")) {
35+
provider = model.split("/")[0].replace(/-/g, "");
36+
}
37+
}
38+
39+
// Special case: Anthropic uses a custom SVG icon
40+
if (provider === "anthropic") {
41+
return <AnthropicLogoIcon className="size-4 shrink-0" />;
42+
}
43+
44+
const iconName = `tabler-brand-${provider}`;
45+
if (tablerIcons.has(iconName)) {
46+
return (
47+
<svg className="size-4 shrink-0 stroke-[1.5]">
48+
<use xlinkHref={`${tablerSpritePath}#${iconName}`} />
49+
</svg>
50+
);
51+
}
52+
53+
return <CubeIcon className="size-4 shrink-0" />;
54+
}
55+
56+
export function ModelsFilter({ possibleModels }: ModelsFilterProps) {
57+
const { values, replace, del } = useSearchParams();
58+
const selectedModels = values("models");
59+
60+
if (selectedModels.length === 0 || selectedModels.every((v) => v === "")) {
61+
return (
62+
<FilterMenuProvider>
63+
{(search, setSearch) => (
64+
<ModelsDropdown
65+
trigger={
66+
<SelectTrigger
67+
icon={<CubeIcon className="size-4" />}
68+
variant="secondary/small"
69+
shortcut={shortcut}
70+
tooltipTitle="Filter by model"
71+
>
72+
<span className="ml-0.5">Models</span>
73+
</SelectTrigger>
74+
}
75+
searchValue={search}
76+
clearSearchValue={() => setSearch("")}
77+
possibleModels={possibleModels}
78+
/>
79+
)}
80+
</FilterMenuProvider>
81+
);
82+
}
83+
84+
return (
85+
<FilterMenuProvider>
86+
{(search, setSearch) => (
87+
<ModelsDropdown
88+
trigger={
89+
<Ariakit.Select render={<div className="group cursor-pointer focus-custom" />}>
90+
<AppliedFilter
91+
label="Model"
92+
icon={<CubeIcon className="size-4" />}
93+
value={appliedSummary(selectedModels)}
94+
onRemove={() => del(["models"])}
95+
variant="secondary/small"
96+
/>
97+
</Ariakit.Select>
98+
}
99+
searchValue={search}
100+
clearSearchValue={() => setSearch("")}
101+
possibleModels={possibleModels}
102+
/>
103+
)}
104+
</FilterMenuProvider>
105+
);
106+
}
107+
108+
function ModelsDropdown({
109+
trigger,
110+
clearSearchValue,
111+
searchValue,
112+
onClose,
113+
possibleModels,
114+
}: {
115+
trigger: ReactNode;
116+
clearSearchValue: () => void;
117+
searchValue: string;
118+
onClose?: () => void;
119+
possibleModels: ModelOption[];
120+
}) {
121+
const { values, replace } = useSearchParams();
122+
123+
const handleChange = (values: string[]) => {
124+
clearSearchValue();
125+
replace({ models: values });
126+
};
127+
128+
const filtered = useMemo(() => {
129+
return possibleModels.filter((m) => {
130+
return m.model?.toLowerCase().includes(searchValue.toLowerCase());
131+
});
132+
}, [searchValue, possibleModels]);
133+
134+
return (
135+
<SelectProvider value={values("models")} setValue={handleChange} virtualFocus={true}>
136+
{trigger}
137+
<SelectPopover
138+
className="min-w-0 max-w-[min(360px,var(--popover-available-width))]"
139+
hideOnEscape={() => {
140+
if (onClose) {
141+
onClose();
142+
return false;
143+
}
144+
return true;
145+
}}
146+
>
147+
<ComboBox placeholder="Filter by model..." value={searchValue} />
148+
<SelectList>
149+
{filtered.map((m) => (
150+
<SelectItem key={m.model} value={m.model} icon={modelIcon(m.system, m.model)}>
151+
{m.model}
152+
</SelectItem>
153+
))}
154+
{filtered.length === 0 && <SelectItem disabled>No models found</SelectItem>}
155+
</SelectList>
156+
</SelectPopover>
157+
</SelectProvider>
158+
);
159+
}

0 commit comments

Comments
 (0)