Files
greywall-landing-page/components/observability.tsx
2026-04-02 15:36:34 -04:00

170 lines
5.7 KiB
TypeScript

'use client'
import { useState, useEffect, useRef } from 'react'
import Image from 'next/image'
import { Eye } from 'lucide-react'
const slides = [
{
label: 'Dashboard',
src: '/dashboard.png',
alt: 'Greywall dashboard showing total requests, allowed, blocked, and allow rate stats',
},
{
label: 'Pending',
src: '/pending_requests.png',
alt: 'Greywall pending network requests with Allow and Deny controls for each domain',
},
{
label: 'Rules',
src: '/rules.png',
alt: 'Greywall domain rules configuration showing allow and deny policies per source',
},
{
label: 'Activity',
src: '/activity.png',
alt: 'Greywall activity log showing real-time TCP connections with status, source, destination, and duration',
},
{
label: 'Conversations',
src: '/conversations.png',
alt: 'Greywall 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">
<div className="flex items-center gap-2 mb-4">
<Eye className="h-4 w-4 text-primary" />
<span className="text-xs font-sans uppercase tracking-wider text-primary font-medium">
Clarity
</span>
</div>
<h2 className="font-serif text-3xl sm:text-4xl font-semibold tracking-tight mb-4">
See every file access and network connection.
</h2>
<p className="text-muted-foreground font-serif text-lg leading-relaxed">
You can&apos;t predict which files your agent will read or which domains it will reach
for. Greywall learns what the agent needs on your filesystem automatically and
captures every outbound connection, letting you adjust policies in real time
without restarting the session.
</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">
Every outbound request is visible. Allow trusted domains, block unknown ones,
and adjust policies live as your agent works.
</p>
</div>
</div>
</section>
)
}