Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
224 changes: 159 additions & 65 deletions apps/ui/src/components/dashboard/dashboard-client.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -15,6 +15,10 @@ import {
ArrowUpFromLine,
Server,
Crown,
ExternalLink,
BookOpen,
FlaskConical,
MessageSquare,
} from "lucide-react";
import Link from "next/link";
import { useRouter, useSearchParams } from "next/navigation";
Expand All @@ -32,6 +36,7 @@ import {
DateRangePicker,
getDateRangeFromParams,
} from "@/components/date-range-picker";
import { QuickStartSection } from "@/components/shared/quick-start-snippet";
import { useDashboardNavigation } from "@/hooks/useDashboardNavigation";
import { Button } from "@/lib/components/button";
import {
Expand Down Expand Up @@ -136,6 +141,13 @@ export function DashboardClient({ initialActivityData }: DashboardClientProps) {

const totalRequests =
activityData.reduce((sum, day) => sum + day.requestCount, 0) || 0;

// Track when user reaches 50+ calls for invite banner eligibility
useEffect(() => {
if (totalRequests >= 50) {
localStorage.setItem("user_has_50_plus_calls", "true");
}
}, [totalRequests]);
Comment thread
smakosh marked this conversation as resolved.
const totalCost = activityData.reduce((sum, day) => sum + day.cost, 0) || 0;
const totalInputCost =
activityData.reduce((sum, day) => sum + day.inputCost, 0) || 0;
Expand Down Expand Up @@ -426,73 +438,155 @@ export function DashboardClient({ initialActivityData }: DashboardClientProps) {
accent="blue"
/>
</div>
<div className="grid gap-4 md:grid-cols-2 lg:grid-cols-7">
<Card className="col-span-4">
<CardHeader>
<div className="flex items-start justify-between">
<div className="flex-1">
<CardTitle>Usage Overview</CardTitle>
<CardDescription>
{metric === "costs"
? "Provider pricing for reference"
: "Total Requests"}
{selectedProject && (
<span className="block mt-1 text-sm">
Filtered by project: {selectedProject.name}
</span>
)}
</CardDescription>
{!isLoading && totalRequests < 5 ? (
Comment thread
smakosh marked this conversation as resolved.
<div className="grid gap-4 md:grid-cols-2 lg:grid-cols-7">
<Card className="col-span-4">
<CardHeader>
<CardTitle>Get Started</CardTitle>
<CardDescription>
{totalRequests > 0
? `You made ${totalRequests === 1 ? "your first call" : `${totalRequests} calls`} during setup! Now integrate LLM Gateway in your own code.`
: "Integrate LLM Gateway in 1 line — just change your base URL."}
</CardDescription>
</CardHeader>
<CardContent className="space-y-4">
<QuickStartSection />
<div className="flex flex-wrap gap-2">
<Button asChild variant="outline" size="sm">
<a
href="https://docs.llmgateway.io"
target="_blank"
rel="noopener noreferrer"
>
<BookOpen className="mr-2 h-4 w-4" />
Docs
<ExternalLink className="ml-1.5 h-3 w-3" />
</a>
</Button>
<Button asChild variant="outline" size="sm">
<a
href={
process.env.NODE_ENV === "development"
? "http://localhost:3003"
: "https://chat.llmgateway.io"
}
target="_blank"
rel="noopener noreferrer"
>
<FlaskConical className="mr-2 h-4 w-4" />
Playground
<ExternalLink className="ml-1.5 h-3 w-3" />
</a>
</Button>
<Button asChild variant="outline" size="sm">
<Link href="/models" prefetch={true}>
<MessageSquare className="mr-2 h-4 w-4" />
Models
</Link>
</Button>
</div>
<Select value={metric} onValueChange={updateMetricInUrl}>
<SelectTrigger className="w-[140px]">
<SelectValue />
</SelectTrigger>
<SelectContent>
<SelectItem value="costs">Costs</SelectItem>
<SelectItem value="requests">Requests</SelectItem>
</SelectContent>
</Select>
</div>
</CardHeader>
<CardContent className="pl-2">
<Overview
data={activityData}
isLoading={isLoading}
metric={metric}
/>
</CardContent>
</Card>
<Card className="col-span-3">
<CardHeader>
<CardTitle>Quick Actions</CardTitle>
<CardDescription>
Common tasks you might want to perform
</CardDescription>
</CardHeader>
<CardContent className="space-y-2">
{quickActions.map((action) => (
<Button
key={action.href}
asChild
variant="outline"
className="w-full justify-start"
>
<Link
href={
action.href === "provider-keys"
? buildOrgUrl("org/provider-keys")
: buildUrl(action.href)
}
prefetch={true}
</CardContent>
</Card>
<Card className="col-span-3">
<CardHeader>
<CardTitle>Quick Actions</CardTitle>
<CardDescription>
Common tasks you might want to perform
</CardDescription>
</CardHeader>
<CardContent className="space-y-2">
{quickActions.map((action) => (
<Button
key={action.href}
asChild
variant="outline"
className="w-full justify-start"
>
<action.icon className="mr-2 h-4 w-4" />
{action.label}
</Link>
</Button>
))}
</CardContent>
</Card>
</div>
<Link
href={
action.href === "provider-keys"
? buildOrgUrl("org/provider-keys")
: buildUrl(action.href)
}
prefetch={true}
>
<action.icon className="mr-2 h-4 w-4" />
{action.label}
</Link>
</Button>
))}
</CardContent>
</Card>
</div>
) : (
<div className="grid gap-4 md:grid-cols-2 lg:grid-cols-7">
<Card className="col-span-4">
<CardHeader>
<div className="flex items-start justify-between">
<div className="flex-1">
<CardTitle>Usage Overview</CardTitle>
<CardDescription>
{metric === "costs"
? "Provider pricing for reference"
: "Total Requests"}
{selectedProject && (
<span className="block mt-1 text-sm">
Filtered by project: {selectedProject.name}
</span>
)}
</CardDescription>
</div>
<Select value={metric} onValueChange={updateMetricInUrl}>
<SelectTrigger className="w-[140px]">
<SelectValue />
</SelectTrigger>
<SelectContent>
<SelectItem value="costs">Costs</SelectItem>
<SelectItem value="requests">Requests</SelectItem>
</SelectContent>
</Select>
</div>
</CardHeader>
<CardContent className="pl-2">
<Overview
data={activityData}
isLoading={isLoading}
metric={metric}
/>
</CardContent>
</Card>
<Card className="col-span-3">
<CardHeader>
<CardTitle>Quick Actions</CardTitle>
<CardDescription>
Common tasks you might want to perform
</CardDescription>
</CardHeader>
<CardContent className="space-y-2">
{quickActions.map((action) => (
<Button
key={action.href}
asChild
variant="outline"
className="w-full justify-start"
>
<Link
href={
action.href === "provider-keys"
? buildOrgUrl("org/provider-keys")
: buildUrl(action.href)
}
prefetch={true}
>
<action.icon className="mr-2 h-4 w-4" />
{action.label}
</Link>
</Button>
))}
</CardContent>
</Card>
</div>
)}

<div className="grid gap-4 md:grid-cols-2 lg:grid-cols-7">
<div className="col-span-4 space-y-4">
Expand Down
43 changes: 42 additions & 1 deletion apps/ui/src/components/dashboard/dashboard-sidebar.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -646,6 +646,45 @@ function UserDropdownMenu({
);
}

function useInviteBannerEligible(
selectedOrganization: Organization | null,
): boolean {
const [eligible, setEligible] = useState(false);

useEffect(() => {
if (!selectedOrganization) {
return;
}

// Check if user has been active for at least 7 days
const orgCreatedAt = new Date(selectedOrganization.createdAt);
const daysSinceCreation =
(Date.now() - orgCreatedAt.getTime()) / (1000 * 60 * 60 * 24);
if (daysSinceCreation >= 7) {
setEligible(true);
return;
}

// Check if user has purchased credits (credits > 0)
if (Number(selectedOrganization.credits) > 0) {
setEligible(true);
return;
}

// Check if user has made 50+ API calls (set by dashboard)
const hasEnoughCalls =
localStorage.getItem("user_has_50_plus_calls") === "true";
if (hasEnoughCalls) {
setEligible(true);
return;
}

setEligible(false);
}, [selectedOrganization]);

return eligible;
}

function UpgradeCTA({
show,
onHide,
Expand All @@ -655,7 +694,9 @@ function UpgradeCTA({
onHide: () => void;
selectedOrganization: Organization | null;
}) {
if (!show || !selectedOrganization) {
const eligible = useInviteBannerEligible(selectedOrganization);

if (!show || !selectedOrganization || !eligible) {
return null;
}

Expand Down
9 changes: 6 additions & 3 deletions apps/ui/src/components/shared/quick-start-snippet.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -22,7 +22,8 @@ export function QuickStartSection({
-H "Content-Type: application/json" \\
-H "Authorization: Bearer ${keyPlaceholder}" \\
-d '{
"model": "gemini-3-flash-preview",
"model": "auto",
"free_models_only": true,
"messages": [
{"role": "user", "content": "Hello!"}
]
Expand All @@ -36,8 +37,10 @@ const client = new OpenAI({
});

const response = await client.chat.completions.create({
model: "gemini-3-flash-preview",
messages: [{ role: "user", content: "Hello!" }]
model: "auto",
messages: [{ role: "user", content: "Hello!" }],
// @ts-expect-error LLM Gateway extension
free_models_only: true,
});`;

const code = activeTab === "curl" ? curlExample : tsExample;
Expand Down
Loading