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
3 changes: 2 additions & 1 deletion freebuff/web/src/app/api/live/route.ts
Original file line number Diff line number Diff line change
Expand Up @@ -9,7 +9,8 @@ export async function GET() {
const stats = await getFreebuffLiveStats()
return NextResponse.json(stats, {
headers: {
'Cache-Control': 'no-store, max-age=0',
'Cache-Control':
'public, max-age=0, s-maxage=60, stale-while-revalidate=30',
},
})
}
3 changes: 3 additions & 0 deletions freebuff/web/src/app/home-client.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -13,6 +13,7 @@ import { CopyButton } from '@/components/copy-button'
import { HeroGrid } from '@/components/hero-grid'
import { Icons } from '@/components/icons'
import { cn } from '@/lib/utils'
import { CompactLiveStats } from './live/live-client'

const INSTALL_COMMAND = 'npm install -g freebuff'

Expand Down Expand Up @@ -567,6 +568,8 @@ export default function HomeClient() {
</div>
</div>
</div>

<CompactLiveStats />
</div>
)
}
100 changes: 86 additions & 14 deletions freebuff/web/src/app/live/live-client.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -7,16 +7,23 @@ import Link from 'next/link'
import { useEffect, useState } from 'react'

import { CopyButton } from '@/components/copy-button'
import { cn } from '@/lib/utils'

import { COUNTRY_POINTS, WORLD_LAND_PATHS } from './world-map-data'

import type { FreebuffLiveStats } from '@/server/live-stats'
import type { LucideIcon } from 'lucide-react'

const INSTALL_COMMAND = 'npm install -g freebuff'
const POLL_MS = 15_000
const POLL_MS = 60_000
const MAP_SIZE = { width: 1000, height: 520 }
const REGION_NAMES = new Intl.DisplayNames(['en'], { type: 'region' })
const EMPTY_LIVE_STATS: FreebuffLiveStats = {
totalLiveUsers: 0,
countries: [],
models: [],
generatedAt: '1970-01-01T00:00:00.000Z',
}
type CountryPoint = readonly [lat: number, lon: number]
type PlottedCountry = FreebuffLiveStats['countries'][number] & {
point: CountryPoint
Expand Down Expand Up @@ -106,7 +113,10 @@ function isPlottedCountry(
return country !== null
}

function useLiveStats(initialStats: FreebuffLiveStats) {
function useLiveStats(
initialStats: FreebuffLiveStats,
options: { refreshOnMount?: boolean } = {},
) {
const [stats, setStats] = useState(initialStats)

useEffect(() => {
Expand All @@ -123,12 +133,16 @@ function useLiveStats(initialStats: FreebuffLiveStats) {
}
}

if (options.refreshOnMount) {
void refresh()
}

const interval = window.setInterval(refresh, POLL_MS)
return () => {
isMounted = false
window.clearInterval(interval)
}
}, [])
}, [options.refreshOnMount])

return stats
}
Expand Down Expand Up @@ -186,7 +200,15 @@ function EmptyState({ children }: { children: React.ReactNode }) {
)
}

function WorldMap({ stats }: { stats: FreebuffLiveStats }) {
function WorldMap({
stats,
compact = false,
isLoading = false,
}: {
stats: FreebuffLiveStats
compact?: boolean
isLoading?: boolean
}) {
const maxCount = Math.max(1, ...stats.countries.map((row) => row.count))
const plottedCountries = stats.countries
.map((country) => {
Expand All @@ -199,20 +221,25 @@ function WorldMap({ stats }: { stats: FreebuffLiveStats }) {
return (
<section className="relative self-start overflow-hidden rounded-lg border border-white/10 bg-[#020807] shadow-[0_24px_90px_rgba(0,0,0,0.34),inset_0_1px_0_rgba(255,255,255,0.05)]">
<div className="pointer-events-none absolute inset-0 bg-[radial-gradient(circle_at_50%_28%,rgba(34,211,238,0.14),transparent_38%),linear-gradient(180deg,rgba(124,255,63,0.04),rgba(0,0,0,0.2))]" />
<div className="pointer-events-none absolute left-4 top-4 z-10 rounded-md border border-white/10 bg-black/45 px-3 py-2 backdrop-blur md:left-5 md:top-5">
<div className="font-mono text-[10px] uppercase tracking-[0.22em] text-white/45">
Active countries
</div>
<div className="mt-1 text-2xl font-serif leading-none text-white">
{stats.countries.length.toLocaleString()}
{!compact && (
<div className="pointer-events-none absolute left-4 top-4 z-10 rounded-md border border-white/10 bg-black/45 px-3 py-2 backdrop-blur md:left-5 md:top-5">
<div className="font-mono text-[10px] uppercase tracking-[0.22em] text-white/45">
Active countries
</div>
<div className="mt-1 text-2xl font-serif leading-none text-white">
{stats.countries.length.toLocaleString()}
</div>
</div>
</div>
)}

<svg
viewBox={`0 0 ${MAP_SIZE.width} ${MAP_SIZE.height}`}
role="img"
aria-label="World map of live Freebuff users by country"
className="relative h-[300px] w-full md:h-[520px]"
className={cn(
'relative w-full',
compact ? 'h-[230px] md:h-[380px]' : 'h-[300px] md:h-[520px]',
)}
>
<defs>
<pattern
Expand Down Expand Up @@ -355,15 +382,20 @@ function WorldMap({ stats }: { stats: FreebuffLiveStats }) {
})}
</svg>

{plottedCountries.length === 0 && (
{plottedCountries.length === 0 && isLoading && (
<div className="absolute inset-x-6 top-1/2 mx-auto max-w-sm -translate-y-1/2 rounded-lg border border-white/10 bg-black/55 px-5 py-4 text-center backdrop-blur">
<div className="font-serif text-2xl text-white">Loading live map</div>
</div>
)}
{plottedCountries.length === 0 && !isLoading && (
<div className="absolute inset-x-6 top-1/2 mx-auto max-w-sm -translate-y-1/2 rounded-lg border border-white/10 bg-black/55 px-5 py-4 text-center backdrop-blur">
<div className="font-serif text-2xl text-white">Standing by</div>
<div className="mt-1 text-sm text-white/50">
Live sessions will appear here as users start Freebuff.
</div>
</div>
)}
{unplottedCount > 0 && (
{!compact && unplottedCount > 0 && (
<div className="absolute bottom-4 right-4 rounded-md border border-white/10 bg-black/45 px-3 py-2 text-xs text-white/48 backdrop-blur">
{unplottedCount} region{unplottedCount === 1 ? '' : 's'} listed
off-map
Expand All @@ -373,6 +405,46 @@ function WorldMap({ stats }: { stats: FreebuffLiveStats }) {
)
}

export function CompactLiveStats({
initialStats = EMPTY_LIVE_STATS,
}: {
initialStats?: FreebuffLiveStats
}) {
const stats = useLiveStats(initialStats, { refreshOnMount: true })
const isLoading = stats.generatedAt === EMPTY_LIVE_STATS.generatedAt

return (
<section className="relative overflow-hidden bg-black py-14 md:py-20">
<div className="absolute inset-0 bg-[linear-gradient(rgba(124,255,63,0.04)_1px,transparent_1px),linear-gradient(90deg,rgba(34,211,238,0.035)_1px,transparent_1px)] bg-[size:56px_56px]" />
<div className="relative container mx-auto px-4">
<div className="mb-6 flex flex-col gap-3 md:mb-8 md:flex-row md:items-end md:justify-between">
<div>
<div className="flex items-center gap-3">
<motion.span
className="h-2.5 w-2.5 rounded-full bg-acid-matrix shadow-[0_0_20px_rgba(124,255,63,0.95)]"
animate={{ opacity: [0.45, 1, 0.45], scale: [0.8, 1.2, 0.8] }}
transition={{
duration: 1.9,
repeat: Infinity,
ease: 'easeInOut',
}}
/>
<span className="font-mono text-xs uppercase tracking-[0.22em] text-white/48">
Active users
</span>
</div>
<div className="mt-2 font-mono text-5xl font-medium leading-none text-acid-matrix neon-text md:text-7xl">
{isLoading ? '...' : stats.totalLiveUsers.toLocaleString()}
</div>
</div>
</div>

<WorldMap stats={stats} compact isLoading={isLoading} />
</div>
</section>
)
}

function ModelBars({ stats }: { stats: FreebuffLiveStats }) {
const maxCount = Math.max(1, ...stats.models.map((model) => model.count))

Expand Down
2 changes: 1 addition & 1 deletion freebuff/web/src/app/page.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -8,7 +8,7 @@ import { siteConfig } from '@/lib/constant'

export async function generateMetadata(): Promise<Metadata> {
const canonicalUrl = env.NEXT_PUBLIC_CODEBUFF_APP_URL
const title = "Freebuff — the free coding agent"
const title = 'Freebuff — the free coding agent'
const description = siteConfig.description

return {
Expand Down
33 changes: 28 additions & 5 deletions freebuff/web/src/server/live-stats.ts
Original file line number Diff line number Diff line change
Expand Up @@ -21,6 +21,12 @@ export interface FreebuffLiveStats {
generatedAt: string
}

const LIVE_STATS_CACHE_MS = 60_000
let cachedLiveStats: {
expiresAt: number
stats: FreebuffLiveStats
} | null = null

const MODEL_LABELS = Object.fromEntries(
SUPPORTED_FREEBUFF_MODELS.map(
(model) => [model.id, model.displayName] as const,
Expand Down Expand Up @@ -48,24 +54,32 @@ function sortCounts<T extends { count: number }>(rows: T[]): T[] {
}

export async function getFreebuffLiveStats(
now = new Date(),
now?: Date,
options: { cache?: boolean } = {},
): Promise<FreebuffLiveStats> {
const useCache = options.cache ?? now === undefined
const requestTime = now ?? new Date()

if (useCache && cachedLiveStats && cachedLiveStats.expiresAt > Date.now()) {
return cachedLiveStats.stats
}

const [countryRows, modelRows] = await Promise.all([
db
.select({
countryCode: schema.freeSession.country_code,
count: count(),
})
.from(schema.freeSession)
.where(liveSessionWhere(now))
.where(liveSessionWhere(requestTime))
.groupBy(schema.freeSession.country_code),
db
.select({
modelId: schema.freeSession.model,
count: count(),
})
.from(schema.freeSession)
.where(liveSessionWhere(now))
.where(liveSessionWhere(requestTime))
.groupBy(schema.freeSession.model),
])

Expand All @@ -84,10 +98,19 @@ export async function getFreebuffLiveStats(
})),
)

return {
const stats = {
totalLiveUsers: models.reduce((sum, row) => sum + row.count, 0),
countries,
models,
generatedAt: now.toISOString(),
generatedAt: requestTime.toISOString(),
}

if (useCache) {
cachedLiveStats = {
expiresAt: Date.now() + LIVE_STATS_CACHE_MS,
stats,
}
}

return stats
}
Loading