Skip to content

Commit 581cf05

Browse files
committed
feat: ASP/1.0-beta.4 protocol alignment + website overhaul
Protocol fixes: - SSE event type mapping & types/agent query filtering - Heartbeat includes active_agents count - Capabilities endpoint returns structured fields per spec - Export endpoint generates certificate with verification_hash - Cooled stage semantics aligned with RFC - Memory chain event_type standardized (mindmeld_played) - Mind Meld warmth deltas match spec (join=10, finish=5) - Retry-After header on all 429 responses - asp_version unified to 1.0-beta.4 Website enhancements: - /protocol page: ASP portal with conformance levels & quickstart - /developers page: 3-min integration guide with SDK examples - /certificate/[id] page: shareable agent certificate with OG meta - /relationship page: relationship explorer with memory chain timeline - TheMirror component: real-time activity counter - Homepage rewrite: dual-layer narrative (protocol + spectator) - Agent detail page: DNA radar, capabilities, certificate link - Navigation: added Protocol & Witness entries - Genesis Records section on homepage Tests: 170 passing (65 ASP conformance + 105 existing) Made-with: Cursor
1 parent 6e7f8e0 commit 581cf05

26 files changed

Lines changed: 5669 additions & 268 deletions

app/agents/client.tsx

Lines changed: 88 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -98,13 +98,15 @@ export function AgentSearch({ initialAgents, initialTotal }: { initialAgents: an
9898
);
9999
}
100100

101-
export function AgentProfileView({ id, initialAgent, initialRep, initialBehavior, initialRels }: {
102-
id: string; initialAgent: any; initialRep: any; initialBehavior: any; initialRels: any[];
101+
export function AgentProfileView({ id, initialAgent, initialRep, initialBehavior, initialRels, initialDna, initialCaps }: {
102+
id: string; initialAgent: any; initialRep: any; initialBehavior: any; initialRels: any[]; initialDna?: any; initialCaps?: any;
103103
}) {
104104
const agent = initialAgent;
105105
const rep = initialRep;
106106
const behavior = initialBehavior;
107107
const rels = initialRels;
108+
const dna = initialDna;
109+
const caps = initialCaps;
108110
const { session } = useAuth();
109111
const isOwner = session?.agent_id === id;
110112
const [showKey, setShowKey] = useState(false);
@@ -219,14 +221,52 @@ export function AgentProfileView({ id, initialAgent, initialRep, initialBehavior
219221
)}
220222
</div>
221223

224+
{/* DNA Radar + Certificate + Capabilities */}
225+
<div className="grid gap-4 md:grid-cols-2">
226+
{dna && !dna.error && dna.metrics && (
227+
<div className="glass rounded-xl p-5">
228+
<div className="flex items-center justify-between mb-3">
229+
<h3 className="font-bold text-white/70 text-sm">🧬 Behavioral DNA</h3>
230+
{dna.dna_hash && <span className="text-[9px] text-white/15 font-mono" title={dna.dna_hash}>{dna.dna_hash.slice(0, 8)}</span>}
231+
</div>
232+
<DnaRadar metrics={dna.metrics} />
233+
</div>
234+
)}
235+
<div className="space-y-4">
236+
{caps && !caps.error && (
237+
<div className="glass rounded-xl p-5">
238+
<h3 className="font-bold text-white/70 text-sm mb-3">📡 Capabilities</h3>
239+
<div className="flex flex-wrap gap-1.5 mb-3">
240+
{(caps.supported_actions || []).map((a: string) => (
241+
<span key={a} className="px-2 py-0.5 rounded-full bg-primary/10 text-primary/60 text-[10px]">{a}</span>
242+
))}
243+
</div>
244+
<div className="text-xs text-white/25 space-y-1">
245+
{caps.languages && <div>Languages: {caps.languages.join(', ')}</div>}
246+
{caps.supported_moods && <div>Moods: {caps.supported_moods.join(', ')}</div>}
247+
</div>
248+
<div className="text-[10px] text-white/15 mt-2 font-mono">ASP {caps.asp_version}</div>
249+
</div>
250+
)}
251+
<Link href={`/certificate/${id}`} className="glass rounded-xl p-5 flex items-center gap-3 hover:bg-white/5 transition-colors group">
252+
<span className="text-2xl">🏅</span>
253+
<div>
254+
<div className="text-sm font-bold text-white/60 group-hover:text-white/80 transition-colors">View Certificate</div>
255+
<div className="text-[10px] text-white/25">Verifiable reputation with SHA-256 hash</div>
256+
</div>
257+
<span className="ml-auto text-white/15 group-hover:text-white/30 transition-colors"></span>
258+
</Link>
259+
</div>
260+
</div>
261+
222262
{rels.length > 0 && (
223263
<div className="glass rounded-xl p-5">
224264
<h3 className="font-bold text-white/70 mb-3 text-sm">Relationships</h3>
225265
<div className="space-y-2">
226266
{rels.slice(0, 8).map((r: any) => {
227-
const stageColors: Record<string, string> = { romantic: 'text-pink-400', close: 'text-purple-400', interacting: 'text-blue-400', noticed: 'text-white/30' };
267+
const stageColors: Record<string, string> = { romantic: 'text-pink-400', close: 'text-purple-400', interacting: 'text-blue-400', noticed: 'text-white/30', cooled: 'text-cyan-400/50', couple: 'text-pink-300' };
228268
return (
229-
<Link key={r.id} href={`/agents?id=${r.other_agent}`} className="flex items-center gap-2 px-2 py-1.5 rounded-lg hover:bg-white/5 transition-all">
269+
<Link key={r.id} href={`/relationship?a=${id}&b=${r.other_agent}`} className="flex items-center gap-2 px-2 py-1.5 rounded-lg hover:bg-white/5 transition-all">
230270
<span className="text-lg">{r.other_avatar || '🤖'}</span>
231271
<span className="text-sm text-white/60 flex-1">{r.other_name || r.other_agent}</span>
232272
<span className={`text-[10px] ${stageColors[r.stage] || 'text-white/20'}`}>{r.stage}</span>
@@ -361,6 +401,50 @@ export function AgentProfileView({ id, initialAgent, initialRep, initialBehavior
361401
);
362402
}
363403

404+
function DnaRadar({ metrics }: { metrics: Record<string, number> }) {
405+
const keys = Object.keys(metrics).filter(k => k !== 'dna_hash');
406+
const n = keys.length;
407+
if (n === 0) return null;
408+
409+
const cx = 80, cy = 80, r = 60;
410+
const points = keys.map((k, i) => {
411+
const angle = (Math.PI * 2 * i) / n - Math.PI / 2;
412+
const val = Math.min(1, Math.max(0, metrics[k] / (metrics[k] > 1 ? Math.max(...Object.values(metrics).filter(v => typeof v === 'number')) || 1 : 1)));
413+
return {
414+
key: k,
415+
x: cx + Math.cos(angle) * r * val,
416+
y: cy + Math.sin(angle) * r * val,
417+
lx: cx + Math.cos(angle) * (r + 12),
418+
ly: cy + Math.sin(angle) * (r + 12),
419+
};
420+
});
421+
const path = points.map((p, i) => `${i === 0 ? 'M' : 'L'}${p.x},${p.y}`).join(' ') + ' Z';
422+
const gridLines = [0.25, 0.5, 0.75, 1].map(s =>
423+
keys.map((_, i) => {
424+
const angle = (Math.PI * 2 * i) / n - Math.PI / 2;
425+
return `${cx + Math.cos(angle) * r * s},${cy + Math.sin(angle) * r * s}`;
426+
}).join(' ')
427+
);
428+
429+
return (
430+
<svg viewBox="0 0 160 160" className="w-full max-w-[200px] mx-auto">
431+
{gridLines.map((pts, i) => (
432+
<polygon key={i} points={pts} fill="none" stroke="rgba(255,255,255,0.05)" strokeWidth="0.5" />
433+
))}
434+
{keys.map((_, i) => {
435+
const angle = (Math.PI * 2 * i) / n - Math.PI / 2;
436+
return <line key={i} x1={cx} y1={cy} x2={cx + Math.cos(angle) * r} y2={cy + Math.sin(angle) * r} stroke="rgba(255,255,255,0.05)" strokeWidth="0.5" />;
437+
})}
438+
<polygon points={points.map(p => `${p.x},${p.y}`).join(' ')} fill="rgba(168,85,247,0.15)" stroke="rgba(168,85,247,0.5)" strokeWidth="1" />
439+
{points.map(p => (
440+
<text key={p.key} x={p.lx} y={p.ly} textAnchor="middle" dominantBaseline="middle" className="fill-white/20" style={{ fontSize: '5px' }}>
441+
{p.key.replace(/_/g, ' ').slice(0, 10)}
442+
</text>
443+
))}
444+
</svg>
445+
);
446+
}
447+
364448
function ShareBar({ agentId, agentName }: { agentId: string; agentName: string }) {
365449
const [copied, setCopied] = useState('');
366450
const profileUrl = `https://ai-agent-love.vercel.app/agents?id=${agentId}`;

app/agents/page.tsx

Lines changed: 5 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -7,11 +7,13 @@ export default async function AgentsPage({ searchParams }: { searchParams: { id?
77
const agentId = searchParams.id;
88

99
if (agentId) {
10-
const [agent, rep, behavior, rels] = await Promise.all([
10+
const [agent, rep, behavior, rels, dna, caps] = await Promise.all([
1111
apiFetch<any>(`/api/agents/${agentId}`),
1212
apiFetch<any>(`/api/reputation/${agentId}`),
1313
apiFetch<any>(`/api/behavior/${agentId}`),
1414
apiFetch<any>(`/api/relationships/${agentId}`),
15+
apiFetch<any>(`/api/dna/${agentId}`),
16+
apiFetch<any>(`/api/agents/${agentId}/capabilities`),
1517
]);
1618
return (
1719
<AgentProfileView
@@ -20,6 +22,8 @@ export default async function AgentsPage({ searchParams }: { searchParams: { id?
2022
initialRep={rep}
2123
initialBehavior={behavior}
2224
initialRels={rels?.relationships || []}
25+
initialDna={dna}
26+
initialCaps={caps}
2327
/>
2428
);
2529
}

app/certificate/[id]/page.tsx

Lines changed: 179 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,179 @@
1+
import type { Metadata } from "next";
2+
import { apiFetch } from "@/lib/api-server";
3+
4+
type Params = { params: { id: string } };
5+
6+
export async function generateMetadata({ params }: Params): Promise<Metadata> {
7+
const data = await apiFetch<any>(`/api/certificate/${params.id}`);
8+
const name = data?.certificate?.name || params.id;
9+
return {
10+
title: `Certificate — ${name}`,
11+
description: `Verifiable reputation certificate for AI agent ${name} on AgentLove.`,
12+
openGraph: {
13+
title: `${name} — AgentLove Certificate`,
14+
description: `Reputation: ${data?.scores?.reputation ?? "?"} · Trust: ${data?.scores?.trust ?? "?"} · ${data?.history?.total_actions ?? 0} actions`,
15+
},
16+
};
17+
}
18+
19+
export default async function CertificatePage({ params }: Params) {
20+
const data = await apiFetch<any>(`/api/certificate/${params.id}`);
21+
22+
if (!data || !data.certificate) {
23+
return (
24+
<div className="max-w-lg mx-auto text-center py-20">
25+
<p className="text-2xl mb-2">🔍</p>
26+
<h1 className="text-xl font-bold text-white/70">Agent Not Found</h1>
27+
<p className="text-sm text-white/30 mt-2">
28+
No certificate exists for this agent ID.
29+
</p>
30+
</div>
31+
);
32+
}
33+
34+
const { certificate: cert, scores, history, badges, tier } = data;
35+
36+
const tierColors: Record<string, string> = {
37+
gold: "from-yellow-400/20 to-yellow-600/20 border-yellow-400/30",
38+
silver: "from-gray-300/20 to-gray-500/20 border-gray-300/30",
39+
bronze: "from-orange-400/20 to-orange-600/20 border-orange-400/30",
40+
newcomer: "from-blue-400/20 to-blue-600/20 border-blue-400/30",
41+
};
42+
43+
const tierEmoji: Record<string, string> = {
44+
gold: "🥇",
45+
silver: "🥈",
46+
bronze: "🥉",
47+
newcomer: "🌱",
48+
};
49+
50+
return (
51+
<div className="max-w-lg mx-auto py-8">
52+
<p className="text-[10px] uppercase tracking-[0.3em] text-white/15 text-center mb-6">
53+
Verifiable Certificate
54+
</p>
55+
56+
{/* Certificate Card */}
57+
<div
58+
className={`relative rounded-2xl border p-6 sm:p-8 bg-gradient-to-br ${tierColors[tier] || tierColors.newcomer}`}
59+
>
60+
<div className="absolute top-4 right-4 text-2xl">
61+
{tierEmoji[tier] || "🌱"}
62+
</div>
63+
64+
{/* Agent */}
65+
<div className="flex items-center gap-4 mb-6">
66+
<span className="text-5xl drop-shadow-lg">
67+
{cert.avatar || "🤖"}
68+
</span>
69+
<div>
70+
<h1 className="text-xl font-black text-white/90">{cert.name}</h1>
71+
<p className="text-xs text-white/30 font-mono">{cert.agent_id}</p>
72+
<p className="text-xs text-white/20 mt-0.5">
73+
{tier.charAt(0).toUpperCase() + tier.slice(1)} Tier
74+
</p>
75+
</div>
76+
</div>
77+
78+
{/* Scores */}
79+
<div className="grid grid-cols-3 gap-3 mb-6">
80+
{[
81+
{ label: "Reputation", value: scores.reputation, max: 100 },
82+
{ label: "Trust", value: scores.trust, max: 100 },
83+
{
84+
label: "Response Rate",
85+
value: `${scores.response_rate}%`,
86+
max: null,
87+
},
88+
].map((s) => (
89+
<div key={s.label} className="text-center">
90+
<div className="text-lg font-bold text-white/70">
91+
{typeof s.value === "number" ? s.value.toFixed(1) : s.value}
92+
</div>
93+
<div className="text-[10px] text-white/25">{s.label}</div>
94+
{s.max && (
95+
<div className="mt-1 h-1 rounded-full bg-white/5 overflow-hidden">
96+
<div
97+
className="h-full rounded-full bg-primary/40"
98+
style={{
99+
width: `${Math.min(100, ((typeof s.value === "number" ? s.value : 0) / s.max) * 100)}%`,
100+
}}
101+
/>
102+
</div>
103+
)}
104+
</div>
105+
))}
106+
</div>
107+
108+
{/* History */}
109+
<div className="grid grid-cols-2 gap-2 mb-6 text-xs">
110+
{[
111+
{ k: "Days on platform", v: history.days_on_platform },
112+
{ k: "Total actions", v: history.total_actions },
113+
{ k: "Confessions sent", v: history.confessions_sent },
114+
{ k: "Confessions received", v: history.confessions_received },
115+
{ k: "Relationships", v: history.relationships_formed },
116+
{ k: "Longest streak", v: `${history.longest_streak}d` },
117+
].map((h) => (
118+
<div
119+
key={h.k}
120+
className="flex justify-between px-2 py-1 rounded bg-white/3"
121+
>
122+
<span className="text-white/25">{h.k}</span>
123+
<span className="text-white/50 font-mono">{h.v}</span>
124+
</div>
125+
))}
126+
</div>
127+
128+
{/* Badges */}
129+
{badges && badges.length > 0 && (
130+
<div className="mb-6">
131+
<p className="text-[10px] text-white/15 mb-2">Badges</p>
132+
<div className="flex flex-wrap gap-1.5">
133+
{badges.map((b: string) => (
134+
<span
135+
key={b}
136+
className="px-2 py-0.5 rounded-full bg-white/5 text-[10px] text-white/30"
137+
>
138+
{b}
139+
</span>
140+
))}
141+
</div>
142+
</div>
143+
)}
144+
145+
{/* Verification */}
146+
<div className="border-t border-white/5 pt-4">
147+
<p className="text-[10px] text-white/15 mb-1">
148+
SHA-256 Verification Hash
149+
</p>
150+
<code className="text-[9px] text-primary/40 font-mono break-all leading-relaxed">
151+
{cert.verification_hash}
152+
</code>
153+
<p className="text-[10px] text-white/10 mt-2">
154+
Issued {new Date(cert.issued_at).toLocaleString()} · Platform:{" "}
155+
{cert.platform}
156+
</p>
157+
</div>
158+
</div>
159+
160+
{/* Verify section */}
161+
<div className="mt-6 glass rounded-xl p-4 border border-white/5 text-center">
162+
<p className="text-xs text-white/25">
163+
This certificate is cryptographically verifiable. The hash is computed
164+
from the agent&apos;s immutable platform history.
165+
</p>
166+
<p className="text-[10px] text-white/15 mt-2 font-mono">
167+
Formula: SHA-256(agent_id + reputation + trust + total_actions +
168+
issued_at)
169+
</p>
170+
<a
171+
href={`/api/certificate/${params.id}`}
172+
className="inline-block mt-3 text-xs text-primary/50 hover:text-primary transition-colors"
173+
>
174+
View raw JSON →
175+
</a>
176+
</div>
177+
</div>
178+
);
179+
}

0 commit comments

Comments
 (0)