Files
greywall-landing-page/components/observability.tsx
2026-04-13 13:09:46 -04:00

162 lines
5.4 KiB
TypeScript

'use client'
import { useState, useEffect, useRef } from 'react'
import Image from 'next/image'
const slides = [
{
label: 'Dashboard',
src: '/dashboard.png',
alt: 'GreyProxy dashboard showing total requests, allowed, blocked, and allow rate stats',
},
{
label: 'Pending',
src: '/pending_requests.png',
alt: 'GreyProxy pending network requests with Allow and Deny controls for each domain',
},
{
label: 'Rules',
src: '/rules.png',
alt: 'GreyProxy domain rules configuration showing allow and deny policies per source',
},
{
label: 'Activity',
src: '/activity.png',
alt: 'GreyProxy activity log showing real-time TCP connections with status, source, destination, and duration',
},
{
label: 'Conversations',
src: '/conversations.png',
alt: 'GreyProxy conversations view showing agent interactions with tool calls and results',
},
]
const INTERVAL = 4000
export function Observability() {
const [active, setActive] = useState(0)
const [paused, setPaused] = useState(false)
const timerRef = useRef<ReturnType<typeof setInterval> | null>(null)
// Key to force re-mount of the progress bar so animation restarts
const [tick, setTick] = useState(0)
function goTo(i: number) {
setActive(i)
setTick((t) => t + 1)
resetTimer()
}
function advance() {
setActive((i) => (i + 1) % slides.length)
setTick((t) => t + 1)
}
function resetTimer() {
if (timerRef.current) clearInterval(timerRef.current)
if (!paused) {
timerRef.current = setInterval(advance, INTERVAL)
}
}
useEffect(() => {
if (paused) {
if (timerRef.current) clearInterval(timerRef.current)
return
}
timerRef.current = setInterval(advance, INTERVAL)
return () => {
if (timerRef.current) clearInterval(timerRef.current)
}
// eslint-disable-next-line react-hooks/exhaustive-deps
}, [paused])
return (
<section id="features" className="py-24 px-4 sm:px-6 border-t border-border/30">
<div className="mx-auto max-w-5xl">
<div className="max-w-2xl mb-16">
<span className="text-serif text-[12px] font-bold uppercase tracking-[0.22em] text-primary mb-4 block">
Clarity
</span>
<h2 className="title-serif text-[36px] md:text-[48px] leading-none mb-4">
See every network connection.
</h2>
<p className="text-serif font-normal text-[15px] md:text-[16px] leading-[1.55] text-muted-foreground">
GreyProxy records each outbound request as it happens. You can allow known domains, deny unknown ones, and keep the session running while you decide.
</p>
</div>
<div
className="mx-auto max-w-3xl"
onMouseEnter={() => setPaused(true)}
onMouseLeave={() => setPaused(false)}
>
{/* Screenshot with crossfade */}
<div className="relative rounded-lg border border-border/40 overflow-hidden bg-white">
{/* Hidden reference image to lock container height */}
<Image
src={slides[0].src}
alt=""
aria-hidden="true"
width={2480}
height={1810}
className="w-full h-auto invisible"
priority
/>
{slides.map((slide, i) => (
<Image
key={slide.label}
src={slide.src}
alt={slide.alt}
width={2480}
height={1810}
className={`absolute inset-0 w-full h-full object-contain object-top transition-opacity duration-700 ${
i === active ? 'opacity-100' : 'opacity-0'
}`}
priority={i === 0}
/>
))}
</div>
{/* Progress indicators + labels */}
<div className="flex items-center justify-center gap-4 mt-5">
{slides.map((slide, i) => (
<button
key={slide.label}
onClick={() => goTo(i)}
className="flex items-center gap-2 group"
>
<div className="relative h-1.5 w-8 rounded-full bg-border/50 overflow-hidden">
{i === active ? (
<div
key={tick}
className="absolute inset-y-0 left-0 rounded-full bg-primary"
style={
paused
? { width: '100%' }
: { animation: `progress ${INTERVAL}ms linear forwards` }
}
/>
) : (
<div className="absolute inset-0 rounded-full bg-transparent group-hover:bg-muted-foreground/30 transition-colors" />
)}
</div>
<span
className={`text-xs font-sans transition-colors hidden sm:inline ${
i === active ? 'text-foreground font-medium' : 'text-muted-foreground'
}`}
>
{slide.label}
</span>
</button>
))}
</div>
<p className="text-xs text-muted-foreground font-serif leading-relaxed mt-5 text-center">
Each outbound request stays visible. Policy changes apply while the agent keeps running.
</p>
</div>
</div>
</section>
)
}