Template
Organize Vertex starter into reusable sections
Use remote logos and avatars, refine landing copy, and document the template structure.
This commit is contained in:
@@ -0,0 +1,34 @@
|
||||
"use client"
|
||||
|
||||
import {
|
||||
AnimatedSpan,
|
||||
Terminal,
|
||||
TypingAnimation,
|
||||
} from "@/components/ui/terminal"
|
||||
|
||||
export function ApiTerminal() {
|
||||
return (
|
||||
<div className="flex flex-1 items-center justify-center overflow-hidden bg-background p-5 sm:p-10">
|
||||
<Terminal className="max-w-xl bg-background shadow-[0_24px_70px_rgba(0,0,0,.12)]" sequence>
|
||||
<TypingAnimation duration={22}>
|
||||
{"> curl https://api.vertex.ai/v1/chat/completions \\"}
|
||||
</TypingAnimation>
|
||||
<AnimatedSpan className="text-muted-foreground">
|
||||
{` -d '{ "model": "vertex/auto", "messages": [...] }'`}
|
||||
</AnimatedSpan>
|
||||
<AnimatedSpan className="text-blue-500">
|
||||
→ evaluating 14 available models
|
||||
</AnimatedSpan>
|
||||
<AnimatedSpan className="text-blue-600 dark:text-blue-400">
|
||||
→ route selected: anthropic/claude-sonnet
|
||||
</AnimatedSpan>
|
||||
<AnimatedSpan className="text-emerald-500">
|
||||
✓ response streamed in 342ms
|
||||
</AnimatedSpan>
|
||||
<TypingAnimation duration={18} className="text-muted-foreground">
|
||||
One endpoint. Any model. No rewrites.
|
||||
</TypingAnimation>
|
||||
</Terminal>
|
||||
</div>
|
||||
)
|
||||
}
|
||||
@@ -0,0 +1,40 @@
|
||||
"use client"
|
||||
|
||||
import { Globe } from "@/components/ui/globe"
|
||||
|
||||
const globeConfig = {
|
||||
width: 800,
|
||||
height: 800,
|
||||
onRender: () => {},
|
||||
devicePixelRatio: 2,
|
||||
phi: 0,
|
||||
theta: 0.28,
|
||||
dark: 1,
|
||||
diffuse: 1.1,
|
||||
mapSamples: 12000,
|
||||
mapBrightness: 4.5,
|
||||
baseColor: [0.05, 0.12, 0.32] as [number, number, number],
|
||||
markerColor: [0.15, 0.46, 1] as [number, number, number],
|
||||
glowColor: [0.08, 0.18, 0.48] as [number, number, number],
|
||||
markers: [
|
||||
{ location: [37.7749, -122.4194] as [number, number], size: 0.08 },
|
||||
{ location: [40.7128, -74.006] as [number, number], size: 0.06 },
|
||||
{ location: [51.5072, -0.1276] as [number, number], size: 0.07 },
|
||||
{ location: [48.8566, 2.3522] as [number, number], size: 0.05 },
|
||||
{ location: [1.3521, 103.8198] as [number, number], size: 0.08 },
|
||||
{ location: [35.6762, 139.6503] as [number, number], size: 0.06 },
|
||||
{ location: [-33.8688, 151.2093] as [number, number], size: 0.05 },
|
||||
{ location: [-23.5505, -46.6333] as [number, number], size: 0.06 },
|
||||
],
|
||||
}
|
||||
|
||||
export function GlobalGlobe() {
|
||||
return (
|
||||
<div className="relative h-[360px] shrink-0 overflow-hidden bg-background">
|
||||
<Globe className="top-6 max-w-[360px]" config={globeConfig} />
|
||||
<div className="absolute left-5 top-5 flex items-center gap-2 rounded-full border bg-background/85 px-3 py-1.5 font-mono text-[10px] shadow-sm backdrop-blur">
|
||||
<span className="size-1.5 animate-pulse rounded-full bg-emerald-500" /> 8 regions online
|
||||
</div>
|
||||
</div>
|
||||
)
|
||||
}
|
||||
@@ -0,0 +1,79 @@
|
||||
import Image from "next/image"
|
||||
|
||||
import { VertexMark } from "@/components/shared/brand"
|
||||
import { OrbitingCircles } from "@/components/ui/orbiting-circles"
|
||||
|
||||
const integrations = [
|
||||
{
|
||||
name: "Supabase",
|
||||
src: "https://sb.frontend.co/storage/v1/object/public/assets/logos/square/supabase.svg",
|
||||
},
|
||||
{
|
||||
name: "Notion",
|
||||
src: "https://sb.frontend.co/storage/v1/object/public/assets/logos/square/notion.svg",
|
||||
},
|
||||
{
|
||||
name: "Figma",
|
||||
src: "https://sb.frontend.co/storage/v1/object/public/assets/logos/square/figma.svg",
|
||||
},
|
||||
{
|
||||
name: "Dropbox",
|
||||
src: "https://sb.frontend.co/storage/v1/object/public/assets/logos/square/dropbox.svg",
|
||||
},
|
||||
{
|
||||
name: "Google",
|
||||
src: "https://sb.frontend.co/storage/v1/object/public/assets/logos/square/google.svg",
|
||||
},
|
||||
{
|
||||
name: "OpenAI",
|
||||
src: "https://sb.frontend.co/storage/v1/object/public/assets/logos/square/openai.svg",
|
||||
},
|
||||
{
|
||||
name: "Stripe",
|
||||
src: "https://sb.frontend.co/storage/v1/object/public/assets/logos/square/stripe.svg",
|
||||
},
|
||||
{
|
||||
name: "GitHub",
|
||||
src: "https://sb.frontend.co/storage/v1/object/public/assets/logos/square/github.svg",
|
||||
},
|
||||
]
|
||||
|
||||
function IntegrationMark({ src, name }: { src: string; name: string }) {
|
||||
return (
|
||||
<span
|
||||
className="inline-flex size-full items-center justify-center rounded-full border border-black/10 bg-white p-2 shadow-sm"
|
||||
title={name}
|
||||
>
|
||||
<Image
|
||||
src={src}
|
||||
alt={`${name} logo`}
|
||||
width={28}
|
||||
height={28}
|
||||
unoptimized
|
||||
className="size-full object-contain"
|
||||
/>
|
||||
</span>
|
||||
)
|
||||
}
|
||||
|
||||
export function IntegrationOrbit() {
|
||||
return (
|
||||
<div className="relative flex flex-1 items-center justify-center overflow-hidden bg-background">
|
||||
<VertexMark className="absolute left-1/2 top-1/2 z-10 size-12 -translate-x-1/2 -translate-y-1/2" />
|
||||
<OrbitingCircles className="z-10" radius={84} iconSize={44} duration={16}>
|
||||
{integrations.slice(0, 4).map((integration) => (
|
||||
<IntegrationMark key={integration.name} {...integration} />
|
||||
))}
|
||||
</OrbitingCircles>
|
||||
<OrbitingCircles className="z-10" radius={150} iconSize={44} duration={24} reverse>
|
||||
{integrations.slice(4).map((integration) => (
|
||||
<IntegrationMark key={integration.name} {...integration} />
|
||||
))}
|
||||
</OrbitingCircles>
|
||||
<div
|
||||
aria-hidden="true"
|
||||
className="pointer-events-none absolute inset-x-0 bottom-0 z-20 h-32 bg-gradient-to-t from-background via-background/80 to-transparent"
|
||||
/>
|
||||
</div>
|
||||
)
|
||||
}
|
||||
@@ -0,0 +1,60 @@
|
||||
"use client"
|
||||
|
||||
import { Activity, CircleCheck, Route, ShieldCheck, Zap } from "lucide-react"
|
||||
|
||||
import { AnimatedList } from "@/components/ui/animated-list"
|
||||
import { cn } from "@/lib/utils"
|
||||
|
||||
const routingEvents = [
|
||||
{
|
||||
icon: <Route className="size-4" />,
|
||||
title: "Routed to Claude Sonnet",
|
||||
meta: "342ms · 8.2k tokens",
|
||||
accent: "bg-blue-600/15 text-blue-600 dark:text-blue-400",
|
||||
},
|
||||
{
|
||||
icon: <Zap className="size-4" />,
|
||||
title: "Prompt cache hit",
|
||||
meta: "Saved $0.018 · 42ms",
|
||||
accent: "bg-amber-500/15 text-amber-500",
|
||||
},
|
||||
{
|
||||
icon: <Activity className="size-4" />,
|
||||
title: "Latency policy applied",
|
||||
meta: "Gemini Flash · 118ms",
|
||||
accent: "bg-blue-500/15 text-blue-500",
|
||||
},
|
||||
{
|
||||
icon: <CircleCheck className="size-4" />,
|
||||
title: "Fallback completed",
|
||||
meta: "OpenAI → Mistral · 621ms",
|
||||
accent: "bg-emerald-500/15 text-emerald-500",
|
||||
},
|
||||
{
|
||||
icon: <ShieldCheck className="size-4" />,
|
||||
title: "Guardrail passed",
|
||||
meta: "No sensitive data detected",
|
||||
accent: "bg-cyan-500/15 text-cyan-500",
|
||||
},
|
||||
]
|
||||
|
||||
export function RoutingActivity() {
|
||||
return (
|
||||
<div className="relative h-[360px] shrink-0 overflow-hidden bg-background">
|
||||
<AnimatedList delay={1150} className="absolute inset-x-4 top-4 gap-2">
|
||||
{routingEvents.map((event) => (
|
||||
<div key={event.title} className="mx-auto flex w-full max-w-sm items-center gap-3 rounded-xl border bg-background/95 p-3 shadow-sm">
|
||||
<span className={cn("flex size-9 shrink-0 items-center justify-center rounded-lg", event.accent)}>
|
||||
{event.icon}
|
||||
</span>
|
||||
<span className="min-w-0 text-left">
|
||||
<span className="block truncate text-xs font-medium">{event.title}</span>
|
||||
<span className="mt-0.5 block truncate font-mono text-[10px] text-muted-foreground">{event.meta}</span>
|
||||
</span>
|
||||
</div>
|
||||
))}
|
||||
</AnimatedList>
|
||||
<div className="pointer-events-none absolute inset-x-0 bottom-0 h-24 bg-gradient-to-t from-background to-transparent" />
|
||||
</div>
|
||||
)
|
||||
}
|
||||
@@ -0,0 +1,96 @@
|
||||
"use client"
|
||||
|
||||
import { useId } from "react"
|
||||
import { Area, AreaChart, XAxis, YAxis, usePlotArea, type DotProps } from "recharts"
|
||||
|
||||
import { ChartContainer, ChartTooltip, type ChartConfig } from "@/components/ui/chart"
|
||||
|
||||
const usageData = [
|
||||
{ time: "08:00", requests: 2350 },
|
||||
{ time: "10:00", requests: 2910 },
|
||||
{ time: "12:00", requests: 2560 },
|
||||
{ time: "14:00", requests: 3833 },
|
||||
{ time: "16:00", requests: 3540 },
|
||||
{ time: "18:00", requests: 4370 },
|
||||
{ time: "20:00", requests: 5680 },
|
||||
]
|
||||
|
||||
const chartConfig = {
|
||||
requests: { label: "Requests", color: "var(--brand)" },
|
||||
} satisfies ChartConfig
|
||||
|
||||
function UsageHighlight({ cx, cy, value }: DotProps & { value?: number | number[] }) {
|
||||
const plot = usePlotArea()
|
||||
const guideId = useId()
|
||||
const amount = Array.isArray(value) ? value[value.length - 1] : value
|
||||
|
||||
if (cx == null || cy == null || !plot || amount == null) return null
|
||||
|
||||
const labelX = Math.max(plot.x + 8, Math.min(cx - 82, plot.x + plot.width - 84))
|
||||
const labelY = Math.max(8, cy - 46)
|
||||
const formattedValue = amount.toLocaleString("en-US")
|
||||
|
||||
return (
|
||||
<g className="pointer-events-none">
|
||||
<title>{formattedValue} requests</title>
|
||||
<defs>
|
||||
<linearGradient id={guideId} x1="0" y1="0" x2="0" y2="1">
|
||||
<stop offset="0%" stopColor="var(--color-requests)" stopOpacity={0.95} />
|
||||
<stop offset="100%" stopColor="var(--color-requests)" stopOpacity={0} />
|
||||
</linearGradient>
|
||||
</defs>
|
||||
<line x1={cx} y1={cy} x2={cx} y2={plot.y + plot.height} stroke={`url(#${guideId})`} strokeWidth={1.5} />
|
||||
<circle cx={cx} cy={cy} r={16} fill="none" stroke="var(--color-requests)" strokeOpacity={0.25} />
|
||||
<circle cx={cx} cy={cy} r={10} fill="var(--background)" fillOpacity={0.5} stroke="var(--color-requests)" strokeOpacity={0.65} />
|
||||
<circle cx={cx} cy={cy} r={4} fill="var(--color-requests)" />
|
||||
<rect x={labelX} y={labelY} width={76} height={30} rx={15} fill="var(--card)" stroke="var(--border)" />
|
||||
<text
|
||||
x={labelX + 38}
|
||||
y={labelY + 15}
|
||||
dy="0.35em"
|
||||
textAnchor="middle"
|
||||
fill="var(--foreground)"
|
||||
className="font-mono text-xs"
|
||||
>
|
||||
{formattedValue}
|
||||
</text>
|
||||
</g>
|
||||
)
|
||||
}
|
||||
|
||||
export function UsageChart() {
|
||||
const fillId = useId()
|
||||
|
||||
return (
|
||||
<ChartContainer config={chartConfig} className="h-full min-h-[270px] w-full aspect-auto">
|
||||
<AreaChart
|
||||
accessibilityLayer
|
||||
aria-label="Example model usage: requests increase from 2,350 to 5,680 over the day. Use the arrow keys to explore."
|
||||
data={usageData}
|
||||
margin={{ left: 0, right: 0, top: 96, bottom: 24 }}
|
||||
>
|
||||
<defs>
|
||||
<linearGradient id={fillId} x1="0" y1="0" x2="0" y2="1">
|
||||
<stop offset="0%" stopColor="var(--color-requests)" stopOpacity={0.42} />
|
||||
<stop offset="65%" stopColor="var(--color-requests)" stopOpacity={0.12} />
|
||||
<stop offset="100%" stopColor="var(--color-requests)" stopOpacity={0} />
|
||||
</linearGradient>
|
||||
</defs>
|
||||
<XAxis dataKey="time" hide padding={{ left: 0, right: 0 }} />
|
||||
<YAxis hide domain={[0, 6400]} />
|
||||
<ChartTooltip active defaultIndex={3} cursor={false} content={() => null} />
|
||||
<Area
|
||||
dataKey="requests"
|
||||
type="monotone"
|
||||
fill={`url(#${fillId})`}
|
||||
fillOpacity={1}
|
||||
stroke="var(--color-requests)"
|
||||
strokeWidth={1.75}
|
||||
dot={false}
|
||||
activeDot={<UsageHighlight />}
|
||||
isAnimationActive={false}
|
||||
/>
|
||||
</AreaChart>
|
||||
</ChartContainer>
|
||||
)
|
||||
}
|
||||
Reference in New Issue
Block a user