Skip to content

Commit 66ba485

Browse files
authored
Merge pull request #4 from tinybirdco/project-scaffolding-01
add timeserieschart
2 parents a76c0d3 + 14f792c commit 66ba485

File tree

14 files changed

+7322
-3237
lines changed

14 files changed

+7322
-3237
lines changed

dashboard/ai-analytics/package-lock.json

Lines changed: 1913 additions & 553 deletions
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

dashboard/ai-analytics/package.json

Lines changed: 14 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -9,19 +9,24 @@
99
"lint": "next lint"
1010
},
1111
"dependencies": {
12-
"react": "^19.0.0",
13-
"react-dom": "^19.0.0",
14-
"next": "15.2.1"
12+
"@remixicon/react": "^4.6.0",
13+
"@tanstack/react-query": "^5.67.2",
14+
"@tremor/react": "^3.18.7",
15+
"next": "15.2.1",
16+
"react": "^18.2.0",
17+
"react-dom": "^18.2.0",
18+
"zustand": "^5.0.3"
1519
},
1620
"devDependencies": {
17-
"typescript": "^5",
21+
"@eslint/eslintrc": "^3",
1822
"@types/node": "^20",
19-
"@types/react": "^19",
20-
"@types/react-dom": "^19",
21-
"@tailwindcss/postcss": "^4",
22-
"tailwindcss": "^4",
23+
"@types/react": "^18",
24+
"@types/react-dom": "^18",
25+
"autoprefixer": "^10.0.1",
2326
"eslint": "^9",
2427
"eslint-config-next": "15.2.1",
25-
"@eslint/eslintrc": "^3"
28+
"postcss": "^8",
29+
"tailwindcss": "^3.3.0",
30+
"typescript": "^5"
2631
}
2732
}
Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,6 @@
1+
module.exports = {
2+
plugins: {
3+
tailwindcss: {},
4+
autoprefixer: {},
5+
},
6+
}
Lines changed: 254 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -1,10 +1,257 @@
1-
export default function TimeseriesChart() {
1+
'use client';
2+
3+
import { RiExternalLinkLine } from '@remixicon/react';
4+
import {
5+
BarChart,
6+
Card,
7+
Tab,
8+
TabGroup,
9+
TabList,
10+
TabPanel,
11+
TabPanels,
12+
} from '@tremor/react';
13+
import { useFilters } from '@/hooks/useTinybirdData';
14+
import { useSearchParams, useRouter } from 'next/navigation';
15+
16+
function classNames(...classes: (string | undefined | null | false)[]) {
17+
return classes.filter(Boolean).join(' ');
18+
}
19+
20+
const valueFormatter = (number: number) =>
21+
Intl.NumberFormat('us').format(number).toString();
22+
23+
interface TimeseriesData {
24+
date: string;
25+
category: string; // model name
26+
total_requests: number;
27+
total_errors: number;
28+
total_tokens: number;
29+
total_completion_tokens: number;
30+
total_prompt_tokens: number;
31+
total_cost: number;
32+
avg_duration: number;
33+
avg_response_time: number;
34+
}
35+
36+
interface TimeseriesChartProps {
37+
data: {
38+
data: TimeseriesData[];
39+
};
40+
}
41+
42+
export default function TimeseriesChart({ data }: TimeseriesChartProps) {
43+
const setFilters = useFilters((state) => state.setFilters);
44+
const router = useRouter();
45+
const searchParams = useSearchParams();
46+
47+
const dates = [...new Set(data.data.map(d => d.date))].sort();
48+
const models = [...new Set(data.data.map(d => d.category))];
49+
50+
// Create consistent color mapping with divergent colors
51+
const colorMap = {
52+
'gpt-4': 'orange',
53+
'gpt-3.5-turbo': 'cyan',
54+
'gpt-4-turbo': 'amber',
55+
'claude-2': 'teal',
56+
// Add more models as needed
57+
};
58+
59+
// Default colors for unknown models
60+
const defaultColors = ['orange', 'cyan', 'amber', 'teal', 'lime', 'pink'];
61+
62+
const transformedData = dates.map(date => {
63+
const dayData = data.data.filter(d => d.date === date);
64+
return {
65+
date: new Date(date).toLocaleDateString('en-US', {
66+
month: 'short',
67+
day: '2-digit'
68+
}),
69+
...models.reduce((acc, model) => ({
70+
...acc,
71+
[model]: dayData.find(d => d.category === model)?.total_cost || 0
72+
}), {})
73+
};
74+
});
75+
76+
const tabs = [
77+
{
78+
name: 'Model',
79+
key: 'model',
80+
data: transformedData,
81+
categories: models,
82+
colors: models.map(model => colorMap[model as keyof typeof colorMap] || defaultColors[models.indexOf(model) % defaultColors.length]),
83+
summary: models.map(model => ({
84+
name: model,
85+
total: data.data
86+
.filter(d => d.category === model)
87+
.reduce((sum, item) => sum + item.total_cost, 0),
88+
color: `bg-${colorMap[model as keyof typeof colorMap] || defaultColors[models.indexOf(model) % defaultColors.length]}-500`,
89+
})),
90+
},
91+
{
92+
name: 'Provider',
93+
key: 'provider',
94+
data: transformedData,
95+
categories: models,
96+
colors: defaultColors,
97+
summary: models.map(model => ({
98+
name: model,
99+
total: data.data
100+
.filter(d => d.category === model)
101+
.reduce((sum, item) => sum + item.total_cost, 0),
102+
color: `bg-${defaultColors[models.indexOf(model) % defaultColors.length]}-500`,
103+
})),
104+
},
105+
{
106+
name: 'Organization',
107+
key: 'organization',
108+
data: transformedData,
109+
categories: models,
110+
colors: defaultColors,
111+
summary: models.map(model => ({
112+
name: model,
113+
total: data.data
114+
.filter(d => d.category === model)
115+
.reduce((sum, item) => sum + item.total_cost, 0),
116+
color: `bg-${defaultColors[models.indexOf(model) % defaultColors.length]}-500`,
117+
})),
118+
},
119+
{
120+
name: 'Environment',
121+
key: 'environment',
122+
data: transformedData,
123+
categories: models,
124+
colors: defaultColors,
125+
summary: models.map(model => ({
126+
name: model,
127+
total: data.data
128+
.filter(d => d.category === model)
129+
.reduce((sum, item) => sum + item.total_cost, 0),
130+
color: `bg-${defaultColors[models.indexOf(model) % defaultColors.length]}-500`,
131+
})),
132+
}
133+
];
134+
135+
const handleTabChange = (index: number) => {
136+
const tab = tabs[index];
137+
// Update URL
138+
const params = new URLSearchParams(searchParams);
139+
params.set('column_name', tab.key);
140+
router.push(`?${params.toString()}`);
141+
// Update filters which will trigger data refetch
142+
setFilters({ column_name: tab.key });
143+
};
144+
2145
return (
3-
<div className="h-full">
4-
<h2 className="text-lg font-semibold h-10 flex items-center px-4">Usage Over Time</h2>
5-
<div className="h-[calc(100%-2.5rem)]">
6-
{/* Chart implementation will go here */}
146+
<Card className="h-full p-0">
147+
<div className="flex h-full flex-col">
148+
<div className="p-6">
149+
<h3 className="font-medium text-tremor-content-strong dark:text-dark-tremor-content-strong">
150+
Requests
151+
</h3>
152+
<p className="text-tremor-default text-tremor-content dark:text-dark-tremor-content">
153+
Lorem ipsum dolor sit amet, consetetur sadipscing elitr, sed diam
154+
nonumy eirmod tempor invidunt.{' '}
155+
<a
156+
href="#"
157+
className="inline-flex items-center gap-1 text-tremor-default text-tremor-brand dark:text-dark-tremor-brand"
158+
>
159+
Learn more
160+
<RiExternalLinkLine className="size-4" aria-hidden={true} />
161+
</a>
162+
</p>
163+
</div>
164+
<div className="flex-1 border-t border-tremor-border p-6 dark:border-dark-tremor-border">
165+
<TabGroup
166+
className="h-full"
167+
onIndexChange={handleTabChange}
168+
defaultIndex={tabs.findIndex(t => t.key === searchParams.get('column_name')) || 0}
169+
>
170+
<div className="md:flex md:items-center md:justify-between">
171+
<TabList
172+
variant="solid"
173+
className="w-full rounded-tremor-small md:w-[400px]"
174+
>
175+
{tabs.map((tab) => (
176+
<Tab
177+
key={tab.name}
178+
className="w-full whitespace-nowrap px-3 justify-center ui-selected:text-tremor-content-strong ui-selected:dark:text-dark-tremor-content-strong"
179+
>
180+
{tab.name}
181+
</Tab>
182+
))}
183+
</TabList>
184+
<div className="hidden md:flex md:items-center md:space-x-2">
185+
<span
186+
className="shrink-0 animate-pulse rounded-tremor-full bg-emerald-500/30 p-1"
187+
aria-hidden={true}
188+
>
189+
<span className="block size-2 rounded-tremor-full bg-emerald-500" />
190+
</span>
191+
<p className="mt-4 text-tremor-default text-tremor-content dark:text-dark-tremor-content md:mt-0">
192+
Updated just now
193+
</p>
194+
</div>
195+
</div>
196+
<TabPanels className="h-[calc(100%-4rem)]">
197+
{tabs.map((tab) => (
198+
<TabPanel key={tab.name} className="h-full">
199+
<ul
200+
role="list"
201+
className="mt-6 flex flex-wrap gap-x-20 gap-y-10"
202+
>
203+
{tab.summary.map((item) => (
204+
<li key={item.name}>
205+
<div className="flex items-center space-x-2">
206+
<span
207+
className={classNames(
208+
item.color,
209+
'size-3 shrink-0 rounded-sm',
210+
)}
211+
aria-hidden={true}
212+
/>
213+
<p className="font-semibold text-tremor-content-strong dark:text-dark-tremor-content-strong">
214+
{valueFormatter(item.total)}
215+
</p>
216+
</div>
217+
<p className="whitespace-nowrap text-tremor-default text-tremor-content dark:text-dark-tremor-content">
218+
{item.name}
219+
</p>
220+
</li>
221+
))}
222+
</ul>
223+
<BarChart
224+
data={tab.data}
225+
index="date"
226+
categories={tab.categories}
227+
colors={tab.colors}
228+
stack={true}
229+
showLegend={false}
230+
yAxisWidth={45}
231+
valueFormatter={valueFormatter}
232+
className="h-[calc(100%-8rem)] mt-10 hidden md:block"
233+
showTooltip={true}
234+
showAnimation={true}
235+
/>
236+
<BarChart
237+
data={tab.data}
238+
index="date"
239+
categories={tab.categories}
240+
colors={tab.colors}
241+
stack={true}
242+
showLegend={false}
243+
showYAxis={false}
244+
valueFormatter={valueFormatter}
245+
className="h-[calc(100%-8rem)] mt-6 md:hidden"
246+
showTooltip={true}
247+
showAnimation={true}
248+
/>
249+
</TabPanel>
250+
))}
251+
</TabPanels>
252+
</TabGroup>
253+
</div>
7254
</div>
8-
</div>
255+
</Card>
9256
);
10-
}
257+
}
Lines changed: 13 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,13 @@
1+
'use client';
2+
3+
import { useLLMUsage } from '@/hooks/useTinybirdData';
4+
import TimeseriesChart from '../components/TimeseriesChart';
5+
6+
export default function TimeseriesChartContainer() {
7+
const { data, isLoading, error } = useLLMUsage();
8+
9+
if (isLoading) return <div>Loading...</div>;
10+
if (error) return <div>Error loading data</div>;
11+
12+
return <TimeseriesChart data={data} />;
13+
}

dashboard/ai-analytics/src/app/globals.css

Lines changed: 3 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,6 @@
1-
@import "tailwindcss";
1+
@tailwind base;
2+
@tailwind components;
3+
@tailwind utilities;
24

35
:root {
46
--background: #ffffff;

dashboard/ai-analytics/src/app/layout.tsx

Lines changed: 6 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,7 @@
11
import type { Metadata } from "next";
22
import { Inter } from "next/font/google";
33
import "./globals.css";
4+
import { TinybirdProvider } from '@/providers/TinybirdProvider';
45

56
const inter = Inter({ subsets: ["latin"] });
67

@@ -16,7 +17,11 @@ export default function RootLayout({
1617
}) {
1718
return (
1819
<html lang="en" className="dark">
19-
<body className={inter.className}>{children}</body>
20+
<body className={inter.className}>
21+
<TinybirdProvider>
22+
{children}
23+
</TinybirdProvider>
24+
</body>
2025
</html>
2126
);
2227
}

dashboard/ai-analytics/src/app/page.tsx

Lines changed: 5 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1,10 +1,13 @@
1+
'use client';
2+
13
import TopBar from './components/TopBar';
2-
import TimeseriesChart from './components/TimeseriesChart';
4+
import TimeseriesChartContainer from './containers/TimeseriesChartContainer';
35
import MetricsCards from './components/MetricsCards';
46
import DataTable from './components/DataTable';
57
import TabbedPane from './components/TabbedPane';
68

79
export default function Dashboard() {
10+
811
return (
912
<div className="h-screen flex flex-col bg-gray-900 text-white">
1013
<TopBar />
@@ -14,7 +17,7 @@ export default function Dashboard() {
1417
<div className="grid grid-cols-[1fr_minmax(0,max(33.333%,400px))]">
1518
{/* Timeseries Chart */}
1619
<div className="border border-gray-700">
17-
<TimeseriesChart />
20+
<TimeseriesChartContainer />
1821
</div>
1922

2023
{/* Metrics Cards */}

0 commit comments

Comments
 (0)