diff --git a/apps/web/src/app/proofs/AgentProofOverview.tsx b/apps/web/src/app/proofs/AgentProofOverview.tsx new file mode 100644 index 0000000..48c703a --- /dev/null +++ b/apps/web/src/app/proofs/AgentProofOverview.tsx @@ -0,0 +1,594 @@ +'use client'; + +import type { ReactNode } from 'react'; +import { useEffect, useMemo, useRef, useState } from 'react'; +import { createPortal } from 'react-dom'; +import { CONTRACTS } from '@/lib/contracts'; +import { buildChainscanUrl } from '@/lib/chainscan'; + +type DemoAgent = { + slug: string; + agentId: string | null; + receiptCount: number; + lastHeartbeat: string | null; +}; + +type ReceiptRow = { + receiptId: string; + agentId: string; + agentName?: string; + actionTag: string; + payloadHash: string; + storageRoot: string; + storageTxHash?: string | undefined; + txHash?: string | undefined; + valueWei: string; + status: 'pending' | 'minted' | 'failed' | string; + createdAt: string; +}; + +type AgentSlug = 'aurora' | 'vesper' | 'helix'; + +type AgentDetail = { + name: string; + role: string; + tokenId: string; + owner: string; + accountAddress: string; + policyId: string; + heartbeatCadence: string; + actionTag: string; + skills: string[]; + accent: string; + iconBg: string; + topBar: string; + badgeActive: string; + dot: string; + cardHover: string; + Icon: ({ className }: { className?: string }) => JSX.Element; +}; + +const CHAIN_ID = 16661 as const; +const CONTRACTS_ARISTOTLE = CONTRACTS[CHAIN_ID]!; + +const POLICY = { + active: true, + maxPerTx: '0.001 OG', + dailyCap: '0.05 OG', + allowedSelector: '0x00000000', +} as const; + +function AuroraIcon({ className }: { className?: string }) { + return ( + + ); +} + +function VesperIcon({ className }: { className?: string }) { + return ( + + ); +} + +function HelixIcon({ className }: { className?: string }) { + return ( + + ); +} + +const AGENT_DETAILS: Record = { + aurora: { + name: 'Aurora', + role: 'Analysis Agent', + tokenId: '1', + owner: '0xA642EDB7F6bBc632132240Daa50503B4F1271cFF', + accountAddress: '0x8AD1Ef8a59554E5537631BfBa9a655A88A803a34', + policyId: '4', + heartbeatCadence: 'Every 10 min', + actionTag: 'agent.heartbeat.analyze', + skills: ['web.search', 'news.aggregate', 'summarize.long', 'chat.embed', 'memory.write', 'chain.send'], + accent: 'text-amber-500', + iconBg: 'bg-amber-400/[0.08] border-amber-400/20', + topBar: 'from-amber-400/40 to-transparent', + badgeActive: 'bg-amber-400/10 text-amber-600', + dot: 'bg-amber-400', + cardHover: 'hover:border-amber-400/40 hover:shadow-lg hover:shadow-amber-400/20', + Icon: AuroraIcon, + }, + vesper: { + name: 'Vesper', + role: 'Creative Agent', + tokenId: '2', + owner: '0xA642EDB7F6bBc632132240Daa50503B4F1271cFF', + accountAddress: '0x4d1d3E14913C050dF9fD68aFaB90D04079C37f90', + policyId: '5', + heartbeatCadence: 'Every 15 min', + actionTag: 'agent.heartbeat.media', + skills: ['memory.search', 'image.generate', 'storage.upload', 'nft.mint', 'chain.send'], + accent: 'text-accent-light', + iconBg: 'bg-accent/[0.08] border-accent/20', + topBar: 'from-accent/40 to-transparent', + badgeActive: 'bg-accent/10 text-accent-light', + dot: 'bg-accent', + cardHover: 'hover:border-accent/40 hover:shadow-lg hover:shadow-accent/20', + Icon: VesperIcon, + }, + helix: { + name: 'Helix', + role: 'Analytics Agent', + tokenId: '3', + owner: '0xA642EDB7F6bBc632132240Daa50503B4F1271cFF', + accountAddress: '0x62283f2064bA32c9797C5c1D7d5F6942229FAf00', + policyId: '6', + heartbeatCadence: 'Every 30 min', + actionTag: 'agent.heartbeat.report', + skills: ['chain.query', 'chat.completion', 'memory.write', 'chain.send'], + accent: 'text-cyan-500', + iconBg: 'bg-cyan-400/[0.08] border-cyan-400/20', + topBar: 'from-cyan-400/40 to-transparent', + badgeActive: 'bg-cyan-400/10 text-cyan-600', + dot: 'bg-cyan-400', + cardHover: 'hover:border-cyan-400/40 hover:shadow-lg hover:shadow-cyan-400/20', + Icon: HelixIcon, + }, +}; + +const isAgentSlug = (slug: string): slug is AgentSlug => slug === 'aurora' || slug === 'vesper' || slug === 'helix'; + +function short(value: string, head = 8, tail = 6): string { + if (value.length <= head + tail + 1) return value; + return `${value.slice(0, head)}…${value.slice(-tail)}`; +} + +function formatTimestamp(value: string | null | undefined): string { + if (!value) return '—'; + return new Date(value).toLocaleString(); +} + +function formatValueWei(valueWei: string): string { + try { + const wei = BigInt(valueWei || '0'); + const whole = wei / 1_000_000_000_000_000_000n; + const frac = wei % 1_000_000_000_000_000_000n; + const fracText = frac.toString().padStart(18, '0').slice(0, 6).replace(/0+$/, ''); + return `${whole.toString()}${fracText ? `.${fracText}` : ''} OG`; + } catch { + return '—'; + } +} + +function statusFor(agent: DemoAgent): { label: string; isActive: boolean; hasActivity: boolean } { + const isActive = Boolean(agent.lastHeartbeat); + const hasActivity = agent.receiptCount > 0; + return { label: isActive ? 'Live' : hasActivity ? 'Running' : 'Awaiting first heartbeat', isActive, hasActivity }; +} + +function CopyButton({ value, label = 'Copy' }: { value: string; label?: string }) { + const [copied, setCopied] = useState(false); + return ( + + ); +} + +function AddressLink({ value, label }: { value: string; label?: string }) { + const href = buildChainscanUrl({ address: value, kind: 'address', chainId: CHAIN_ID }); + return ( + + {href ? ( + + {label ?? short(value, 10, 8)} + + ) : ( + {label ?? short(value, 10, 8)} + )} + + + ); +} + +function HashValue({ value, tx = false }: { value?: string | undefined; tx?: boolean }) { + if (!value) return ; + const href = tx ? buildChainscanUrl({ txHash: value, kind: 'tx', chainId: CHAIN_ID }) : null; + return ( + + {href ? ( + + {short(value, 10, 8)} + + ) : ( + {short(value, 10, 8)} + )} + + + ); +} + +function DetailRow({ label, children }: { label: string; children: ReactNode }) { + return ( +
+ {label} +
{children}
+
+ ); +} + +function Section({ title, children, prominent = false }: { title: string; children: ReactNode; prominent?: boolean }) { + return ( +
+

{title}

+ {children} +
+ ); +} + +function DemoAgentCard({ agent, onOpen }: { agent: DemoAgent; onOpen: () => void }) { + if (!isAgentSlug(agent.slug)) return null; + const detail = AGENT_DETAILS[agent.slug]; + const { Icon } = detail; + const status = statusFor(agent); + + return ( + + ); +} + +function AgentModal({ agent, receipts, onClose }: { agent: DemoAgent; receipts: ReceiptRow[]; onClose: () => void }) { + const dialogRef = useRef(null); + const closeRef = useRef(null); + const previousFocus = useRef(null); + const [portalRoot, setPortalRoot] = useState(null); + + useEffect(() => { + setPortalRoot(document.body); + }, []); + + useEffect(() => { + previousFocus.current = document.activeElement instanceof HTMLElement ? document.activeElement : null; + const previousOverflow = document.body.style.overflow; + document.body.style.overflow = 'hidden'; + const timer = window.setTimeout(() => closeRef.current?.focus(), 0); + + const onKeyDown = (event: KeyboardEvent) => { + if (event.key === 'Escape') { + event.preventDefault(); + onClose(); + return; + } + if (event.key !== 'Tab' || !dialogRef.current) return; + const focusable = Array.from(dialogRef.current.querySelectorAll('a[href], button:not([disabled]), [tabindex]:not([tabindex="-1"])')) + .filter((el) => !el.hasAttribute('disabled') && el.offsetParent !== null); + if (focusable.length === 0) return; + const first = focusable[0]!; + const last = focusable[focusable.length - 1]!; + if (event.shiftKey && document.activeElement === first) { + event.preventDefault(); + last.focus(); + } else if (!event.shiftKey && document.activeElement === last) { + event.preventDefault(); + first.focus(); + } + }; + + document.addEventListener('keydown', onKeyDown); + return () => { + window.clearTimeout(timer); + document.removeEventListener('keydown', onKeyDown); + document.body.style.overflow = previousOverflow; + previousFocus.current?.focus(); + }; + }, [onClose]); + + if (!isAgentSlug(agent.slug)) return null; + const detail = AGENT_DETAILS[agent.slug]; + const { Icon } = detail; + const status = statusFor(agent); + const agentReceipts = receipts.filter((receipt) => receipt.agentId === agent.slug).slice(0, 5); + + if (!portalRoot) return null; + + return createPortal( +
+ +
+ +
+
+ #{detail.tokenId} + + + + Aristotle mainnet · {CHAIN_ID} +
+ +
+ {detail.heartbeatCadence} + {formatTimestamp(agent.lastHeartbeat)} + {agent.receiptCount.toLocaleString()} + {detail.actionTag} +
+ +
+ #{detail.policyId} + {POLICY.active ? 'true' : 'false'} + {POLICY.maxPerTx} + {POLICY.dailyCap} + +
+ +
+
+ {detail.skills.map((skill) => ( + + {skill} + + ))} +
+
+ +
+
+ {agentReceipts.length === 0 ? ( +

No recent receipts for this agent in the current proofs payload.

+ ) : ( +
+ {agentReceipts.map((receipt) => ( +
+
+
+

{receipt.actionTag}

+

{formatTimestamp(receipt.createdAt)}

+
+ + {receipt.status} + +
+ +
+
+ Value + {formatValueWei(receipt.valueWei)} +
+
+ Mint tx + +
+
+ Storage tx + +
+
+ Receipt ID + +
+
+ Payload hash + +
+
+ Storage root + +
+
+
+ ))} +
+ )} +
+
+
+ + , + portalRoot + ); +} + +function ActivityHeatmap({ heatmap }: { heatmap: Record> }) { + const days = Object.keys(heatmap).sort().reverse(); + const allVals = days.flatMap(d => Object.values(heatmap[d] ?? {}).map(Number)); + const maxVal = Math.max(1, ...allVals); + const intensityBg = ['bg-elevated', 'bg-accent/20', 'bg-accent/40', 'bg-accent/65', 'bg-accent/90']; + + return ( +
+
+
+ {Array.from({ length: 24 }, (_, h) => ( +
+ {h % 6 === 0 ? String(h) : ''} +
+ ))} +
+ {days.map(day => ( +
+ {day.slice(5)} +
+ {Array.from({ length: 24 }, (_, h) => { + const count = Number(heatmap[day]?.[String(h)] ?? 0); + const intensity = count === 0 ? 0 : Math.min(4, Math.ceil((count / maxVal) * 4)); + return ( +
+ ); + })} +
+
+ ))} +
+ Less + {intensityBg.map((bg, i) =>
)} + More +
+
+
+ ); +} + +export function AgentProofOverview({ agents, receipts, heatmap, receiptFeed }: { agents: DemoAgent[]; receipts: ReceiptRow[]; heatmap: Record>; receiptFeed: ReactNode }) { + const [selectedSlug, setSelectedSlug] = useState(null); + const selectedAgent = useMemo(() => agents.find((agent) => selectedSlug && agent.slug === selectedSlug) ?? null, [agents, selectedSlug]); + + return ( +
+
+

Demo agents

+
+ {agents.map((agent) => { + if (!isAgentSlug(agent.slug)) return null; + const slug = agent.slug; + return setSelectedSlug(slug)} />; + })} +
+
+ +
+

Activity — last 14 days × 24 h

+
+ +
+
+ + {receiptFeed} + + {selectedAgent && setSelectedSlug(null)} />} +
+ ); +} diff --git a/apps/web/src/app/proofs/page.tsx b/apps/web/src/app/proofs/page.tsx index 0a856f0..8290c19 100644 --- a/apps/web/src/app/proofs/page.tsx +++ b/apps/web/src/app/proofs/page.tsx @@ -3,6 +3,7 @@ import Link from 'next/link'; import { Nav } from '@/components/landing/Nav'; import { CONTRACTS, CHAIN_NAMES, CONTRACT_NAMES } from '@/lib/contracts'; import { buildChainscanUrl, chainscanBase } from '@/lib/chainscan'; +import { AgentProofOverview } from './AgentProofOverview'; import { ReceiptsFeed, StorageProofsClient } from './_client'; export const metadata: Metadata = { title: 'On-chain Proofs — Apogee Protocol' }; @@ -17,7 +18,6 @@ type DemoAgent = { agentId: string | null; receiptCount: number; lastHeartbeat: string | null; - runningForHours: number | null; }; type StorageProofRow = { @@ -33,10 +33,13 @@ type StorageProofRow = { createdAt: string; }; +type ReceiptRow = StorageProofRow & { valueWei: string }; + type ProofsApiResponse = { generatedAt: string; totalReceipts: number; demoAgents: DemoAgent[]; + receipts: ReceiptRow[]; heatmap: Record>; storageProofSample: StorageProofRow[]; }; @@ -64,10 +67,11 @@ function emptyProofs(): ProofsApiResponse { generatedAt: new Date().toISOString(), totalReceipts: 0, demoAgents: [ - { slug: 'aurora', agentId: SEEDED_AGENTS['aurora'] ?? null, receiptCount: 0, lastHeartbeat: null, runningForHours: null }, - { slug: 'vesper', agentId: SEEDED_AGENTS['vesper'] ?? null, receiptCount: 0, lastHeartbeat: null, runningForHours: null }, - { slug: 'helix', agentId: SEEDED_AGENTS['helix'] ?? null, receiptCount: 0, lastHeartbeat: null, runningForHours: null }, + { slug: 'aurora', agentId: SEEDED_AGENTS['aurora'] ?? null, receiptCount: 0, lastHeartbeat: null }, + { slug: 'vesper', agentId: SEEDED_AGENTS['vesper'] ?? null, receiptCount: 0, lastHeartbeat: null }, + { slug: 'helix', agentId: SEEDED_AGENTS['helix'] ?? null, receiptCount: 0, lastHeartbeat: null }, ], + receipts: [] as ReceiptRow[], heatmap: Object.fromEntries( Array.from({ length: 14 }, (_, i) => { const d = new Date(Date.now() - i * 86_400_000).toISOString().slice(0, 10); @@ -88,84 +92,6 @@ function mergeSeededAddresses(proofs: ProofsApiResponse): ProofsApiResponse { }; } -// ── Agent emblems (inline SVG) ──────────────────────────────────────────────── - -function AuroraIcon({ className }: { className?: string }) { - return ( - - ); -} - -function VesperIcon({ className }: { className?: string }) { - return ( - - ); -} - -function HelixIcon({ className }: { className?: string }) { - return ( - - ); -} - -// ── Agent card metadata ─────────────────────────────────────────────────────── - -const AGENT_META = { - aurora: { - Icon: AuroraIcon, - role: 'Analysis Agent', - capability: 'News · web.search · self-billing', - accent: 'text-amber-500', - iconBg: 'bg-amber-400/[0.08] border-amber-400/20', - topBar: 'from-amber-400/40 to-transparent', - badgeActive: 'bg-amber-400/10 text-amber-600', - dot: 'bg-amber-400', - }, - vesper: { - Icon: VesperIcon, - role: 'Creative Agent', - capability: 'Media · image.generate · nft.mint', - accent: 'text-accent-light', - iconBg: 'bg-accent/[0.08] border-accent/20', - topBar: 'from-accent/40 to-transparent', - badgeActive: 'bg-accent/10 text-accent-light', - dot: 'bg-accent', - }, - helix: { - Icon: HelixIcon, - role: 'Analytics Agent', - capability: 'Chain · chain.query · daily report', - accent: 'text-cyan-500', - iconBg: 'bg-cyan-400/[0.08] border-cyan-400/20', - topBar: 'from-cyan-400/40 to-transparent', - badgeActive: 'bg-cyan-400/10 text-cyan-600', - dot: 'bg-cyan-400', - }, -} as const; - // ── Tab navigation ──────────────────────────────────────────────────────────── const TABS = [ @@ -253,149 +179,14 @@ function ContractsTab() { // ── Overview tab ────────────────────────────────────────────────────────────── -function DemoAgentCard({ slug, agentId, receiptCount, lastHeartbeat, runningForHours }: DemoAgent) { - const meta = AGENT_META[slug as keyof typeof AGENT_META]; - if (!meta) return null; - const { Icon, role, capability, accent, iconBg, topBar, badgeActive, dot } = meta; - const isActive = Boolean(lastHeartbeat); - const hasActivity = receiptCount > 0; - // Status label: only claim "Awaiting" if there is genuinely zero evidence of activity. - const statusLabel = isActive ? 'Active' : hasActivity ? 'Running' : 'Awaiting first heartbeat'; - - return ( -
-
- -
-
-
- -
-
-

{slug}

-

{role}

-
-
- {isActive ? ( - - - Live - - ) : hasActivity ? ( - - - Active - - ) : ( - Idle - )} -
-
- -

{capability}

- -
-
- Address - {buildChainscanUrl({ address: agentId, kind: 'address', chainId: 16661 }) ? ( - - {agentId?.slice(0, 6)}…{agentId?.slice(-4)} - - ) : agentId ? {agentId} : not seeded} -
-
- Receipts minted - {receiptCount} -
-
- Last heartbeat - - {lastHeartbeat ? new Date(lastHeartbeat).toLocaleTimeString() : '—'} - -
- {runningForHours !== null && ( -
- Uptime - {runningForHours}h -
- )} -
- -
- - - {statusLabel} - -
-
-
- ); -} - -function ActivityHeatmap({ heatmap }: { heatmap: Record> }) { - const days = Object.keys(heatmap).sort().reverse(); - const allVals = days.flatMap(d => Object.values(heatmap[d] ?? {}).map(Number)); - const maxVal = Math.max(1, ...allVals); - const intensityBg = ['bg-elevated', 'bg-accent/20', 'bg-accent/40', 'bg-accent/65', 'bg-accent/90']; - - return ( -
-
-
- {Array.from({ length: 24 }, (_, h) => ( -
- {h % 6 === 0 ? String(h) : ''} -
- ))} -
- {days.map(day => ( -
- {day.slice(5)} -
- {Array.from({ length: 24 }, (_, h) => { - const count = Number(heatmap[day]?.[String(h)] ?? 0); - const intensity = count === 0 ? 0 : Math.min(4, Math.ceil((count / maxVal) * 4)); - return ( -
- ); - })} -
-
- ))} -
- Less - {intensityBg.map((bg, i) =>
)} - More -
-
-
- ); -} - function OverviewTab({ proofs }: { proofs: ProofsApiResponse }) { return ( -
-
-

Demo agents

-
- {proofs.demoAgents.map(agent => )} -
-
- -
-

Activity — last 14 days × 24 h

-
- -
-
- - -
+ } + /> ); }