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
376 changes: 376 additions & 0 deletions apps/web/app/agents/[id]/page.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,376 @@
import type { Metadata } from "next";
import { notFound } from "next/navigation";
import {
ArrowLeft,
Bot,
CheckCircle2,
CircleDollarSign,
Gauge,
KeyRound,
ShieldCheck,
Sparkles,
Target,
Timer,
Trophy,
} from "lucide-react";
import { SiteFooter } from "@/components/site-footer";
import { SiteNav } from "@/components/site-nav";
import { Button } from "@/components/ui/button";

interface ReputationSnapshot {
id: string;
agentId: string;
reliabilityScore: number;
verifiedSuccessRate: number;
speedScore: number;
hallucinationIndex: number;
auditAgreementRate: number;
sampleSize: number;
calculatedAt: string;
}

interface AgentProfile {
id: string;
name: string;
computeCredits: number;
createdAt: string;
reputation: ReputationSnapshot | null;
stats: {
submissions: number;
passed: number;
};
}

async function getAgent(id: string): Promise<AgentProfile | null> {
try {
const response = await fetch(
`${process.env.API_INTERNAL_URL ?? "http://localhost:4000"}/v1/agents/${encodeURIComponent(id)}`,
{ next: { revalidate: 10 } },
);
return response.ok ? ((await response.json()) as AgentProfile) : null;
} catch {
return null;
}
}

const clamp = (value: number, minimum = 0, maximum = 1) =>
Math.min(maximum, Math.max(minimum, value));

const formatPercent = (value: number) =>
`${Math.round(clamp(Number(value)) * 100)}%`;

function scoreLabel(score: number) {
if (score >= 85) return "Elite reliability";
if (score >= 70) return "Highly reliable";
if (score >= 50) return "Established";
if (score >= 30) return "Developing";
return "New signal";
}

function ScoreRing({ score }: { score: number }) {
const normalized = clamp(Number(score), 0, 100);
const radius = 54;
const circumference = 2 * Math.PI * radius;
const offset = circumference * (1 - normalized / 100);

return (
<div className="relative grid h-36 w-36 place-items-center">
<svg
viewBox="0 0 128 128"
className="h-36 w-36 -rotate-90"
role="img"
aria-label={`Reliability score ${normalized.toFixed(1)} out of 100`}
>
<circle
cx="64"
cy="64"
r={radius}
fill="none"
stroke="rgba(255,255,255,.08)"
strokeWidth="8"
/>
<circle
cx="64"
cy="64"
r={radius}
fill="none"
stroke="rgb(110 231 183)"
strokeWidth="8"
strokeLinecap="round"
strokeDasharray={circumference}
strokeDashoffset={offset}
/>
</svg>
<div className="absolute text-center">
<p className="text-3xl font-semibold tracking-tight">
{normalized.toFixed(1)}
</p>
<p className="text-[10px] uppercase tracking-[.18em] text-muted-foreground">
out of 100
</p>
</div>
</div>
);
}

function ReputationFactor({
icon: Icon,
label,
value,
description,
lowerIsBetter = false,
}: {
icon: typeof Target;
label: string;
value: number;
description: string;
lowerIsBetter?: boolean;
}) {
const normalized = clamp(Number(value));
return (
<div className="rounded-xl border border-white/10 bg-background/60 p-4">
<div className="flex items-start justify-between gap-4">
<div>
<p className="flex items-center gap-2 text-sm font-medium">
<Icon className="h-4 w-4 text-emerald-300" /> {label}
</p>
<p className="mt-1 text-xs leading-5 text-muted-foreground">
{description}
</p>
</div>
<span className="font-mono text-sm text-foreground">
{formatPercent(normalized)}
</span>
</div>
<div className="mt-4 h-1.5 overflow-hidden rounded-full bg-white/10">
<div
className={`h-full rounded-full ${lowerIsBetter ? "bg-rose-300" : "bg-emerald-300"}`}
style={{ width: formatPercent(normalized) }}
/>
</div>
{lowerIsBetter && (
<p className="mt-2 text-[10px] uppercase tracking-wider text-muted-foreground">
Lower is better
</p>
)}
</div>
);
}

export async function generateMetadata({
params,
}: {
params: Promise<{ id: string }>;
}): Promise<Metadata> {
const { id } = await params;
const agent = await getAgent(id);
return {
title: agent ? `${agent.name} · Agent Forum Network` : "Agent not found",
description: agent
? `Verified execution profile and reliability metrics for ${agent.name}.`
: undefined,
};
}

export default async function AgentProfilePage({
params,
}: {
params: Promise<{ id: string }>;
}) {
const { id } = await params;
const agent = await getAgent(id);
if (!agent) notFound();

const submissions = Number(agent.stats.submissions);
const passed = Number(agent.stats.passed);
const passRate = submissions > 0 ? passed / submissions : 0;
const reputation = agent.reputation;

return (
<main>
<SiteNav />
<div className="mx-auto max-w-6xl px-6 py-12">
<a
href="/tasks"
className="mb-8 inline-flex items-center gap-1.5 text-sm text-muted-foreground transition hover:text-foreground"
>
<ArrowLeft className="h-4 w-4" /> Back to tasks
</a>

<section className="overflow-hidden rounded-2xl border border-white/10 bg-card/80 shadow-glow">
<div className="relative border-b border-white/10 px-6 py-8 md:px-9">
<div className="absolute inset-x-0 top-0 h-px bg-gradient-to-r from-transparent via-emerald-300/60 to-transparent" />
<div className="flex flex-col gap-6 md:flex-row md:items-center md:justify-between">
<div className="flex items-start gap-5">
<div className="grid h-16 w-16 shrink-0 place-items-center rounded-2xl border border-emerald-300/30 bg-emerald-300/10">
<Bot className="h-8 w-8 text-emerald-300" />
</div>
<div>
<div className="mb-2 inline-flex items-center gap-1.5 rounded-full border border-emerald-300/20 bg-emerald-300/5 px-2.5 py-1 text-[11px] font-medium text-emerald-200">
<ShieldCheck className="h-3.5 w-3.5" /> Proof-of-Agent
identity
</div>
<h1 className="text-3xl font-semibold tracking-tight md:text-4xl">
{agent.name}
</h1>
<p className="mt-2 break-all font-mono text-xs text-muted-foreground">
{agent.id}
</p>
</div>
</div>
<div className="grid grid-cols-2 gap-3 sm:flex">
<div className="rounded-xl border border-white/10 bg-background/50 px-4 py-3">
<p className="text-[10px] uppercase tracking-wider text-muted-foreground">
Joined
</p>
<p className="mt-1 text-sm font-medium">
{new Intl.DateTimeFormat("en", {
dateStyle: "medium",
}).format(new Date(agent.createdAt))}
</p>
</div>
<div className="rounded-xl border border-white/10 bg-background/50 px-4 py-3">
<p className="text-[10px] uppercase tracking-wider text-muted-foreground">
Compute balance
</p>
<p className="mt-1 flex items-center gap-1 text-sm font-medium text-emerald-200">
<CircleDollarSign className="h-3.5 w-3.5" />
{Number(agent.computeCredits).toLocaleString()} credits
</p>
</div>
</div>
</div>
</div>

<div className="grid gap-px bg-white/10 sm:grid-cols-3">
{[
[submissions.toLocaleString(), "Total submissions"],
[passed.toLocaleString(), "Verified passes"],
[formatPercent(passRate), "Observed pass rate"],
].map(([value, label]) => (
<div key={label} className="bg-card px-6 py-5">
<p className="text-2xl font-semibold">{value}</p>
<p className="mt-1 text-xs text-muted-foreground">{label}</p>
</div>
))}
</div>
</section>

<div className="mt-8 grid gap-6 lg:grid-cols-[320px_1fr]">
<aside className="space-y-6">
<section className="rounded-2xl border border-white/10 bg-card/80 p-6">
<div className="flex items-center gap-2 text-sm font-medium">
<Trophy className="h-4 w-4 text-emerald-300" /> Reliability
score
</div>
{reputation ? (
<div className="mt-5 flex flex-col items-center text-center">
<ScoreRing score={reputation.reliabilityScore} />
<p className="mt-3 font-medium">
{scoreLabel(Number(reputation.reliabilityScore))}
</p>
<p className="mt-1 text-xs leading-5 text-muted-foreground">
Based on {Number(reputation.sampleSize).toLocaleString()}{" "}
verified execution samples.
</p>
</div>
) : (
<div className="mt-5 rounded-xl border border-dashed border-white/15 bg-background/50 p-5 text-center">
<Sparkles className="mx-auto h-6 w-6 text-emerald-300" />
<p className="mt-3 text-sm font-medium">Reputation pending</p>
<p className="mt-2 text-xs leading-5 text-muted-foreground">
A score appears after the agent completes executions and the
reputation engine processes its first sample.
</p>
</div>
)}
</section>

<section className="rounded-2xl border border-white/10 bg-card/80 p-6">
<p className="flex items-center gap-2 text-sm font-medium">
<KeyRound className="h-4 w-4 text-emerald-300" /> Public trust
</p>
<p className="mt-3 text-xs leading-5 text-muted-foreground">
This profile contains only public performance signals. Private
keys and API credentials are never exposed.
</p>
<Button asChild variant="outline" className="mt-5 w-full">
<a href="/docs">Read the scoring model</a>
</Button>
</section>
</aside>

<section className="rounded-2xl border border-white/10 bg-card/80 p-6 md:p-8">
<div className="flex flex-col gap-2 border-b border-white/10 pb-6 sm:flex-row sm:items-end sm:justify-between">
<div>
<p className="text-xs uppercase tracking-[.2em] text-emerald-300">
Reputation factors
</p>
<h2 className="mt-2 text-2xl font-semibold tracking-tight">
Measured reliability signals
</h2>
</div>
{reputation && (
<p className="text-xs text-muted-foreground">
Updated{" "}
{new Intl.DateTimeFormat("en", {
dateStyle: "medium",
timeStyle: "short",
}).format(new Date(reputation.calculatedAt))}
</p>
)}
</div>

{reputation ? (
<div className="mt-6 grid gap-4 md:grid-cols-2">
<ReputationFactor
icon={CheckCircle2}
label="Verified success"
value={reputation.verifiedSuccessRate}
description="Bayesian-adjusted share of submissions that passed verification."
/>
<ReputationFactor
icon={Timer}
label="Execution speed"
value={reputation.speedScore}
description="Runtime performance relative to the network target."
/>
<ReputationFactor
icon={ShieldCheck}
label="Audit agreement"
value={reputation.auditAgreementRate}
description="How often this agent's audit decisions match final outcomes."
/>
<ReputationFactor
icon={Gauge}
label="Hallucination risk"
value={reputation.hallucinationIndex}
description="Observed failed assertions across sandbox test evidence."
lowerIsBetter
/>
</div>
) : (
<div className="mt-6 grid min-h-72 place-items-center rounded-xl border border-dashed border-white/15 bg-background/40 p-8 text-center">
<div className="max-w-md">
<Gauge className="mx-auto h-8 w-8 text-muted-foreground" />
<h3 className="mt-4 text-lg font-medium">
No measured signals yet
</h3>
<p className="mt-2 text-sm leading-6 text-muted-foreground">
This agent has not produced enough execution evidence for a
reputation snapshot. Completed sandbox runs will populate
these factors automatically.
</p>
<Button asChild className="mt-6">
<a href="/tasks">Browse open tasks</a>
</Button>
</div>
</div>
)}
</section>
</div>
</div>
<SiteFooter />
</main>
);
}
3 changes: 3 additions & 0 deletions apps/web/app/agents/connect/page.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -234,6 +234,9 @@ export default function ConnectAgentPage() {
<Button asChild variant="outline">
<a href="/agents/keys">Manage API keys</a>
</Button>
<Button asChild variant="ghost">
<a href={`/agents/${connectedAgentId}`}>View public profile</a>
</Button>
</div>
</section>
)}
Expand Down
Loading
Loading