Skip to content

Commit cd9e429

Browse files
committed
[PRO-135] Add AI token volume chart in project overview page (#8472)
<!-- ## title your PR with this format: "[SDK/Dashboard/Portal] Feature/Fix: Concise title for the changes" If you did not copy the branch name from Linear, paste the issue tag here (format is TEAM-0000): ## Notes for the reviewer Anything important to call out? Be sure to also clarify these in your comments. ## How to test Unit tests, playground, etc. --> <!-- start pr-codex --> --- ## PR-Codex overview This PR focuses on enhancing the AI features in the dashboard by updating UI components, improving navigation, and adding analytics capabilities for AI token usage. ### Detailed summary - Changed `variant` of a `Button` in `ChatBar.tsx` from `primary` to `default`. - Added `viewMoreLink` prop to `AiTokenUsageChartCardUI` in `index.tsx`. - Updated sidebar structure in `ProjectSidebarLayout.tsx` to include submenus and links for AI. - Modified redirection logic in `page.tsx` for missing sessions. - Adjusted styling and structure of `ExportToCSVButton` in `ExportToCSVButton.tsx`. - Removed the `Analytics` link from `ChatSidebar.tsx`. - Removed unused state for token symbol in `project-wallet-details.tsx`. - Introduced `AsyncAiAnalytics` function to fetch AI usage data in `page.tsx`. - Updated `AiTokenUsageChartCardUI` to handle new props and improve chart presentation. - Enhanced empty state message in `AiTokenUsageChartCardUI` with a button linking to AI chat documentation. > ✨ Ask PR-Codex anything about this PR by commenting with `/codex {your question}` <!-- end pr-codex --> <!-- This is an auto-generated comment: release notes by coderabbit.ai --> ## Summary by CodeRabbit * **New Features** * AI token usage analytics card added to project dashboard with totals and chart. * AI sidebar reorganized into Chat and Analytics entries; AI analytics accessible from project pages. * **Bug Fixes** * Missing chat session now redirects back to the AI section instead of showing an error. * **Style** * Button and spinner visuals refined (export and send controls); chart sizing and empty-state text adjusted; chat sidebar streamlined. <sub>✏️ Tip: You can customize this high-level summary in your review settings.</sub> <!-- end of auto-generated comment: release notes by coderabbit.ai -->
1 parent 41e2ccb commit cd9e429

File tree

9 files changed

+138
-68
lines changed

9 files changed

+138
-68
lines changed

apps/dashboard/src/@/components/blocks/ExportToCSVButton.tsx

Lines changed: 7 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -34,24 +34,22 @@ export function ExportToCSVButton(props: {
3434

3535
return (
3636
<Button
37-
className={cn("flex items-center gap-2 border text-xs", props.className)}
37+
className={cn(
38+
"flex items-center gap-2 border text-sm rounded-full",
39+
props.className,
40+
)}
3841
disabled={props.disabled || exportMutation.isPending}
3942
onClick={async () => {
4043
exportMutation.mutate();
4144
}}
4245
variant="outline"
4346
>
4447
{exportMutation.isPending ? (
45-
<>
46-
Downloading
47-
<Spinner className="size-3" />
48-
</>
48+
<Spinner className="size-3.5" />
4949
) : (
50-
<>
51-
<DownloadIcon className="size-3" />
52-
Export as CSV
53-
</>
50+
<DownloadIcon className="size-3.5" />
5451
)}
52+
Export
5553
</Button>
5654
);
5755
}

apps/dashboard/src/app/(app)/team/[team_slug]/[project_slug]/(sidebar)/ai/analytics/chart/AiTokenUsageChartCard.tsx

Lines changed: 48 additions & 33 deletions
Original file line numberDiff line numberDiff line change
@@ -1,10 +1,12 @@
11
"use client";
22
import { format } from "date-fns";
3-
import { BotIcon } from "lucide-react";
3+
import { ArrowUpRightIcon } from "lucide-react";
4+
import Link from "next/link";
45
import { useMemo } from "react";
6+
import { TotalValueChartHeader } from "@/components/blocks/charts/area-chart";
57
import { ThirdwebBarChart } from "@/components/blocks/charts/bar-chart";
6-
import { DocLink } from "@/components/blocks/DocLink";
78
import { ExportToCSVButton } from "@/components/blocks/ExportToCSVButton";
9+
import { Button } from "@/components/ui/button";
810
import type { ChartConfig } from "@/components/ui/chart";
911
import type { AIUsageStats } from "@/types/analytics";
1012

@@ -17,7 +19,7 @@ export function AiTokenUsageChartCardUI(props: {
1719
aiUsageStats: AIUsageStats[];
1820
isPending: boolean;
1921
title: string;
20-
description: string;
22+
viewMoreLink: string | undefined;
2123
}) {
2224
const { aiUsageStats } = props;
2325

@@ -52,33 +54,43 @@ export function AiTokenUsageChartCardUI(props: {
5254
chartData.length === 0 ||
5355
chartData.every((data) => data.tokens === 0);
5456

57+
const total = useMemo(() => {
58+
return chartData.reduce((acc, curr) => acc + curr.tokens, 0);
59+
}, [chartData]);
60+
5561
return (
5662
<ThirdwebBarChart
57-
chartClassName="aspect-[1.5] lg:aspect-[3.5]"
63+
chartClassName="h-[275px] w-full"
5864
config={chartConfig}
5965
customHeader={
60-
<div className="relative px-6 pt-6">
61-
<h3 className="mb-0.5 font-semibold text-xl tracking-tight">
62-
{props.title}
63-
</h3>
64-
<p className="mb-3 text-muted-foreground text-sm">
65-
{props.description}
66-
</p>
67-
68-
<ExportToCSVButton
69-
className="top-6 right-6 mb-4 w-full bg-background md:absolute md:mb-0 md:flex md:w-auto"
70-
disabled={disableActions}
71-
fileName="AI Token Usage"
72-
getData={async () => {
73-
const header = ["Date", "Tokens"];
74-
const rows = chartData.map((data) => [
75-
data.time,
76-
data.tokens.toString(),
77-
]);
78-
return { header, rows };
79-
}}
66+
props.viewMoreLink ? (
67+
<TotalValueChartHeader
68+
isPending={props.isPending}
69+
total={total}
70+
title={props.title}
71+
viewMoreLink={props.viewMoreLink}
8072
/>
81-
</div>
73+
) : (
74+
<div className="relative px-6 pt-6">
75+
<h3 className="font-semibold text-xl tracking-tight">
76+
{props.title}
77+
</h3>
78+
79+
<ExportToCSVButton
80+
className="top-6 right-6 mb-4 w-full bg-background md:absolute md:mb-0 md:flex md:w-auto"
81+
disabled={disableActions}
82+
fileName="AI Token Usage"
83+
getData={async () => {
84+
const header = ["Date", "Tokens"];
85+
const rows = chartData.map((data) => [
86+
data.time,
87+
data.tokens.toString(),
88+
]);
89+
return { header, rows };
90+
}}
91+
/>
92+
</div>
93+
)
8294
}
8395
data={chartData}
8496
emptyChartState={<AiTokenUsageEmptyChartState />}
@@ -100,17 +112,20 @@ export function AiTokenUsageChartCardUI(props: {
100112
function AiTokenUsageEmptyChartState() {
101113
return (
102114
<div className="flex flex-col items-center justify-center px-4">
103-
<span className="mb-6 text-center text-lg">
115+
<span className="mb-3 text-center text-sm">
104116
Integrate thirdweb AI to interact with any EVM chain using natural
105117
language
106118
</span>
107-
<div className="flex max-w-md flex-wrap items-center justify-center gap-x-6 gap-y-4">
108-
<DocLink
109-
icon={BotIcon}
110-
label="Get Started"
111-
link="https://portal.thirdweb.com/ai/chat"
112-
/>
113-
</div>
119+
<Button
120+
asChild
121+
variant="outline"
122+
className="rounded-full bg-background gap-2"
123+
>
124+
<Link href="https://portal.thirdweb.com/ai/chat" target="_blank">
125+
Get Started
126+
<ArrowUpRightIcon className="size-4" />
127+
</Link>
128+
</Button>
114129
</div>
115130
);
116131
}

apps/dashboard/src/app/(app)/team/[team_slug]/[project_slug]/(sidebar)/ai/analytics/chart/index.tsx

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -18,9 +18,9 @@ function AiAnalyticsUI({ stats, isPending }: AiAnalyticsProps) {
1818
return (
1919
<AiTokenUsageChartCardUI
2020
title="Token Usage"
21-
description="The total number of tokens used for AI interactions on your project."
2221
aiUsageStats={stats || []}
2322
isPending={isPending}
23+
viewMoreLink={undefined}
2424
/>
2525
);
2626
}

apps/dashboard/src/app/(app)/team/[team_slug]/[project_slug]/(sidebar)/ai/chat/[session_id]/page.tsx

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,4 @@
1-
import { notFound } from "next/navigation";
1+
import { notFound, redirect } from "next/navigation";
22
import { getAuthToken, getUserThirdwebClient } from "@/api/auth-token";
33
import { getProject } from "@/api/project/projects";
44
import { getSessionById, getSessions } from "../../api/session";
@@ -39,7 +39,7 @@ export default async function Page(props: {
3939
]);
4040

4141
if (!session) {
42-
notFound();
42+
redirect(`/team/${params.team_slug}/${params.project_slug}/ai`);
4343
}
4444

4545
return (

apps/dashboard/src/app/(app)/team/[team_slug]/[project_slug]/(sidebar)/ai/components/ChatBar.tsx

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -410,7 +410,7 @@ export function ChatBar(props: {
410410
if (message.trim() === "" && images.length === 0) return;
411411
handleSubmit(message);
412412
}}
413-
variant="primary"
413+
variant="default"
414414
>
415415
<ArrowUpIcon className="size-4" />
416416
</Button>

apps/dashboard/src/app/(app)/team/[team_slug]/[project_slug]/(sidebar)/ai/components/ChatSidebar.tsx

Lines changed: 0 additions & 11 deletions
Original file line numberDiff line numberDiff line change
@@ -1,7 +1,6 @@
11
"use client";
22
import {
33
BotIcon,
4-
ChartLineIcon,
54
ChevronDownIcon,
65
ChevronRightIcon,
76
FileCode2Icon,
@@ -83,16 +82,6 @@ export function ChatSidebar(props: {
8382
<div className="h-5" />
8483

8584
<div className="flex flex-col gap-3 border-t border-dashed px-4 py-4">
86-
<Link
87-
className="flex items-center gap-1 rounded-full text-foreground text-sm hover:underline justify-between"
88-
href={`/team/${props.team_slug}/${props.project.slug}/ai/analytics`}
89-
>
90-
<div className="flex items-center gap-2">
91-
<ChartLineIcon className="size-3.5 text-muted-foreground" />
92-
Analytics
93-
</div>
94-
<ChevronRightIcon className="size-3.5 text-muted-foreground" />
95-
</Link>
9685
<Link
9786
className="flex items-center gap-1 rounded-full text-foreground text-sm hover:underline justify-between"
9887
href={"https://portal.thirdweb.com/ai/chat"}

apps/dashboard/src/app/(app)/team/[team_slug]/[project_slug]/(sidebar)/components/ProjectSidebarLayout.tsx

Lines changed: 20 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -83,9 +83,26 @@ export function ProjectSidebarLayout(props: {
8383
label: "Tokens",
8484
},
8585
{
86-
href: `${props.layoutPath}/ai`,
87-
icon: BotIcon,
88-
label: "AI",
86+
subMenu: {
87+
icon: BotIcon,
88+
label: "AI",
89+
},
90+
links: [
91+
{
92+
href: `${props.layoutPath}/ai`,
93+
label: "Chat",
94+
isActive: (pathname) => {
95+
return (
96+
pathname === `${props.layoutPath}/ai` ||
97+
pathname.startsWith(`${props.layoutPath}/ai/chat`)
98+
);
99+
},
100+
},
101+
{
102+
href: `${props.layoutPath}/ai/analytics`,
103+
label: "Analytics",
104+
},
105+
],
89106
},
90107
{
91108
subMenu: {

apps/dashboard/src/app/(app)/team/[team_slug]/[project_slug]/(sidebar)/components/project-wallet/project-wallet-details.tsx

Lines changed: 0 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -664,12 +664,6 @@ function SendProjectWalletModalContent(props: SendProjectWalletModalProps) {
664664

665665
const selectedChain = useV5DashboardChain(form.watch("chainId"));
666666
const selectedFormChainId = form.watch("chainId");
667-
const _selectedFormTokenAddress = form.watch("tokenAddress");
668-
669-
// Track the selected token symbol for display
670-
const [_selectedTokenSymbol, setSelectedTokenSymbol] = useState<
671-
string | undefined
672-
>(undefined);
673667

674668
const sendMutation = useMutation({
675669
mutationFn: async (values: SendFormValues) => {
@@ -760,7 +754,6 @@ function SendProjectWalletModalContent(props: SendProjectWalletModalProps) {
760754
field.onChange(nextChainId);
761755
// Reset token to native when chain changes
762756
form.setValue("tokenAddress", undefined);
763-
setSelectedTokenSymbol(undefined);
764757
}}
765758
placeholder="Select network"
766759
/>
@@ -788,7 +781,6 @@ function SendProjectWalletModalContent(props: SendProjectWalletModalProps) {
788781
}
789782
onChange={(token) => {
790783
field.onChange(token.address);
791-
setSelectedTokenSymbol(token.symbol);
792784
}}
793785
chainId={selectedFormChainId}
794786
client={client}

apps/dashboard/src/app/(app)/team/[team_slug]/[project_slug]/(sidebar)/page.tsx

Lines changed: 59 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -7,6 +7,7 @@ import {
77
import type { ThirdwebClient } from "thirdweb";
88
import { getWalletInfo, type WalletId } from "thirdweb/wallets";
99
import {
10+
getAiUsage,
1011
getInAppWalletUsage,
1112
getInsightStatusCodeUsage,
1213
getRpcUsageByType,
@@ -30,6 +31,7 @@ import { getFiltersFromSearchParams } from "@/lib/time";
3031
import type { InAppWalletStats, WalletStats } from "@/types/analytics";
3132
import { loginRedirect } from "@/utils/redirects";
3233
import { PieChartCard } from "../../../components/Analytics/PieChartCard";
34+
import { AiTokenUsageChartCardUI } from "./ai/analytics/chart/AiTokenUsageChartCard";
3335
import { EngineCloudChartCardAsync } from "./components/EngineCloudChartCard";
3436
import { ProjectFTUX } from "./components/ProjectFTUX/ProjectFTUX";
3537
import { ProjectWalletSection } from "./components/project-wallet/project-wallet";
@@ -287,6 +289,29 @@ async function ProjectAnalytics(props: {
287289
</ResponsiveSuspense>
288290
</div>
289291

292+
<ResponsiveSuspense
293+
fallback={
294+
<AiTokenUsageChartCardUI
295+
isPending={true}
296+
title="AI token volume"
297+
viewMoreLink={`/team/${params.team_slug}/${params.project_slug}/ai/analytics`}
298+
aiUsageStats={[]}
299+
/>
300+
}
301+
searchParamsUsed={["from", "to", "interval"]}
302+
>
303+
<AsyncAiAnalytics
304+
teamSlug={params.team_slug}
305+
projectSlug={params.project_slug}
306+
from={range.from}
307+
to={range.to}
308+
interval={interval}
309+
projectId={project.id}
310+
teamId={project.teamId}
311+
authToken={authToken}
312+
/>
313+
</ResponsiveSuspense>
314+
290315
<ResponsiveSuspense
291316
fallback={<LoadingChartState className="h-[377px] border" />}
292317
searchParamsUsed={["from", "to", "interval"]}
@@ -560,3 +585,37 @@ export function Header(props: {
560585
</div>
561586
);
562587
}
588+
589+
async function AsyncAiAnalytics(props: {
590+
from: Date;
591+
to: Date;
592+
interval: "day" | "week";
593+
projectId: string;
594+
teamId: string;
595+
authToken: string;
596+
teamSlug: string;
597+
projectSlug: string;
598+
}) {
599+
const stats = await getAiUsage(
600+
{
601+
from: props.from,
602+
period: props.interval,
603+
projectId: props.projectId,
604+
teamId: props.teamId,
605+
to: props.to,
606+
},
607+
props.authToken,
608+
).catch((error) => {
609+
console.error(error);
610+
return [];
611+
});
612+
613+
return (
614+
<AiTokenUsageChartCardUI
615+
title="AI token volume"
616+
isPending={false}
617+
aiUsageStats={stats}
618+
viewMoreLink={`/team/${props.teamSlug}/${props.projectSlug}/ai/analytics`}
619+
/>
620+
);
621+
}

0 commit comments

Comments
 (0)