refactor: consolidate blockchain explorer into single app and update backup ignore patterns

- Remove standalone explorer-web app (README, HTML, package files)
- Add /web endpoint to blockchain-explorer for web interface access
- Update .gitignore to exclude application backup archives (*.tar.gz, *.zip)
- Add backup documentation files to .gitignore (BACKUP_INDEX.md, README.md)
- Consolidate explorer functionality into main blockchain-explorer application
This commit is contained in:
oib
2026-03-06 18:14:49 +01:00
parent dc1561d457
commit bb5363bebc
295 changed files with 35501 additions and 3734 deletions

View File

@@ -0,0 +1,548 @@
import React, { useState, useEffect } from 'react';
import {
Card,
CardContent,
CardDescription,
CardFooter,
CardHeader,
CardTitle,
} from '@/components/ui/card';
import { Button } from '@/components/ui/button';
import { Badge } from '@/components/ui/badge';
import { Input } from '@/components/ui/input';
import { Select, SelectContent, SelectItem, SelectTrigger, SelectValue } from '@/components/ui/select';
import { Tabs, TabsContent, TabsList, TabsTrigger } from '@/components/ui/tabs';
import { Progress } from '@/components/ui/progress';
import { Separator } from '@/components/ui/separator';
import { Clock, Users, Trophy, Filter, Search, TrendingUp, AlertCircle } from 'lucide-react';
import { useWallet } from '@/hooks/use-wallet';
import { useToast } from '@/hooks/use-toast';
import { formatDistanceToNow } from 'date-fns';
interface Bounty {
bounty_id: string;
title: string;
description: string;
reward_amount: number;
creator_id: string;
tier: 'bronze' | 'silver' | 'gold' | 'platinum';
status: 'created' | 'active' | 'submitted' | 'verified' | 'completed' | 'expired' | 'disputed';
performance_criteria: Record<string, any>;
min_accuracy: number;
max_response_time?: number;
deadline: string;
creation_time: string;
max_submissions: number;
submission_count: number;
requires_zk_proof: boolean;
tags: string[];
category?: string;
difficulty?: string;
winning_submission_id?: string;
winner_address?: string;
}
interface BountyFilters {
status?: string;
tier?: string;
category?: string;
min_reward?: number;
max_reward?: number;
tags?: string[];
requires_zk_proof?: boolean;
}
const BountyBoard: React.FC = () => {
const { address, isConnected } = useWallet();
const { toast } = useToast();
const [bounties, setBounties] = useState<Bounty[]>([]);
const [loading, setLoading] = useState(true);
const [activeTab, setActiveTab] = useState('all');
const [searchQuery, setSearchQuery] = useState('');
const [filters, setFilters] = useState<BountyFilters>({});
const [showCreateModal, setShowCreateModal] = useState(false);
const [selectedBounty, setSelectedBounty] = useState<Bounty | null>(null);
const [mySubmissions, setMySubmissions] = useState<string[]>([]);
// Load bounties on component mount
useEffect(() => {
loadBounties();
if (isConnected) {
loadMySubmissions();
}
}, [isConnected]);
const loadBounties = async () => {
try {
setLoading(true);
const response = await fetch('/api/v1/bounties', {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify({
...filters,
page: 1,
limit: 50
})
});
if (response.ok) {
const data = await response.json();
setBounties(data);
} else {
throw new Error('Failed to load bounties');
}
} catch (error) {
console.error('Error loading bounties:', error);
toast({
title: 'Error',
description: 'Failed to load bounties',
variant: 'destructive'
});
} finally {
setLoading(false);
}
};
const loadMySubmissions = async () => {
try {
const response = await fetch('/api/v1/bounties/my/submissions', {
headers: { 'Authorization': `Bearer ${address}` }
});
if (response.ok) {
const submissions = await response.json();
setMySubmissions(submissions.map((s: any) => s.bounty_id));
}
} catch (error) {
console.error('Error loading submissions:', error);
}
};
const handleBountySubmit = async (bountyId: string) => {
if (!isConnected) {
toast({
title: 'Wallet Required',
description: 'Please connect your wallet to submit to bounties',
variant: 'destructive'
});
return;
}
// Navigate to submission page or open modal
setSelectedBounty(bounties.find(b => b.bounty_id === bountyId) || null);
};
const getTierColor = (tier: string) => {
const colors = {
bronze: 'bg-orange-100 text-orange-800 border-orange-200',
silver: 'bg-gray-100 text-gray-800 border-gray-200',
gold: 'bg-yellow-100 text-yellow-800 border-yellow-200',
platinum: 'bg-purple-100 text-purple-800 border-purple-200'
};
return colors[tier as keyof typeof colors] || colors.bronze;
};
const getStatusColor = (status: string) => {
const colors = {
created: 'bg-gray-100 text-gray-800',
active: 'bg-green-100 text-green-800',
submitted: 'bg-blue-100 text-blue-800',
verified: 'bg-purple-100 text-purple-800',
completed: 'bg-emerald-100 text-emerald-800',
expired: 'bg-red-100 text-red-800',
disputed: 'bg-orange-100 text-orange-800'
};
return colors[status as keyof typeof colors] || colors.created;
};
const getTimeRemaining = (deadline: string) => {
const deadlineDate = new Date(deadline);
const now = new Date();
const timeRemaining = deadlineDate.getTime() - now.getTime();
if (timeRemaining <= 0) return 'Expired';
return formatDistanceToNow(deadlineDate, { addSuffix: true });
};
const filteredBounties = bounties.filter(bounty => {
const matchesSearch = bounty.title.toLowerCase().includes(searchQuery.toLowerCase()) ||
bounty.description.toLowerCase().includes(searchQuery.toLowerCase());
const matchesTab = activeTab === 'all' ||
(activeTab === 'my-submissions' && mySubmissions.includes(bounty.bounty_id)) ||
(activeTab === 'active' && bounty.status === 'active') ||
(activeTab === 'completed' && bounty.status === 'completed');
return matchesSearch && matchesTab;
});
if (loading) {
return (
<div className="container mx-auto py-8">
<div className="flex items-center justify-center h-64">
<div className="animate-spin rounded-full h-8 w-8 border-b-2 border-blue-600"></div>
</div>
</div>
);
}
return (
<div className="container mx-auto py-8 space-y-6">
{/* Header */}
<div className="flex justify-between items-center">
<div>
<h1 className="text-3xl font-bold">Bounty Board</h1>
<p className="text-muted-foreground">
Discover and participate in AI agent development challenges
</p>
</div>
{isConnected && (
<Button onClick={() => setShowCreateModal(true)}>
Create Bounty
</Button>
)}
</div>
{/* Stats Cards */}
<div className="grid grid-cols-1 md:grid-cols-4 gap-4">
<Card>
<CardContent className="p-4">
<div className="flex items-center justify-between">
<div>
<p className="text-sm font-medium text-muted-foreground">Active Bounties</p>
<p className="text-2xl font-bold">{bounties.filter(b => b.status === 'active').length}</p>
</div>
<Trophy className="h-8 w-8 text-blue-600" />
</div>
</CardContent>
</Card>
<Card>
<CardContent className="p-4">
<div className="flex items-center justify-between">
<div>
<p className="text-sm font-medium text-muted-foreground">Total Rewards</p>
<p className="text-2xl font-bold">
{bounties.reduce((sum, b) => sum + b.reward_amount, 0).toLocaleString()} AITBC
</p>
</div>
<TrendingUp className="h-8 w-8 text-green-600" />
</div>
</CardContent>
</Card>
<Card>
<CardContent className="p-4">
<div className="flex items-center justify-between">
<div>
<p className="text-sm font-medium text-muted-foreground">Completion Rate</p>
<p className="text-2xl font-bold">
{bounties.length > 0
? Math.round((bounties.filter(b => b.status === 'completed').length / bounties.length) * 100)
: 0}%
</p>
</div>
<Users className="h-8 w-8 text-purple-600" />
</div>
</CardContent>
</Card>
<Card>
<CardContent className="p-4">
<div className="flex items-center justify-between">
<div>
<p className="text-sm font-medium text-muted-foreground">My Submissions</p>
<p className="text-2xl font-bold">{mySubmissions.length}</p>
</div>
<AlertCircle className="h-8 w-8 text-orange-600" />
</div>
</CardContent>
</Card>
</div>
{/* Search and Filters */}
<div className="flex flex-col md:flex-row gap-4">
<div className="relative flex-1">
<Search className="absolute left-3 top-3 h-4 w-4 text-muted-foreground" />
<Input
placeholder="Search bounties..."
value={searchQuery}
onChange={(e) => setSearchQuery(e.target.value)}
className="pl-10"
/>
</div>
<Select value={filters.tier || ''} onValueChange={(value) => setFilters(prev => ({ ...prev, tier: value || undefined }))}>
<SelectTrigger className="w-32">
<SelectValue placeholder="Tier" />
</SelectTrigger>
<SelectContent>
<SelectItem value="">All Tiers</SelectItem>
<SelectItem value="bronze">Bronze</SelectItem>
<SelectItem value="silver">Silver</SelectItem>
<SelectItem value="gold">Gold</SelectItem>
<SelectItem value="platinum">Platinum</SelectItem>
</SelectContent>
</Select>
<Select value={filters.category || ''} onValueChange={(value) => setFilters(prev => ({ ...prev, category: value || undefined }))}>
<SelectTrigger className="w-40">
<SelectValue placeholder="Category" />
</SelectTrigger>
<SelectContent>
<SelectItem value="">All Categories</SelectItem>
<SelectItem value="computer-vision">Computer Vision</SelectItem>
<SelectItem value="nlp">NLP</SelectItem>
<SelectItem value="robotics">Robotics</SelectItem>
<SelectItem value="gaming">Gaming</SelectItem>
<SelectItem value="finance">Finance</SelectItem>
</SelectContent>
</Select>
<Button variant="outline" onClick={loadBounties}>
<Filter className="h-4 w-4 mr-2" />
Apply Filters
</Button>
</div>
{/* Tabs */}
<Tabs value={activeTab} onValueChange={setActiveTab}>
<TabsList>
<TabsTrigger value="all">All Bounties</TabsTrigger>
<TabsTrigger value="active">Active</TabsTrigger>
<TabsTrigger value="completed">Completed</TabsTrigger>
{isConnected && <TabsTrigger value="my-submissions">My Submissions</TabsTrigger>}
</TabsList>
<TabsContent value={activeTab} className="space-y-4">
{/* Bounty Grid */}
<div className="grid grid-cols-1 md:grid-cols-2 lg:grid-cols-3 gap-6">
{filteredBounties.map((bounty) => (
<Card key={bounty.bounty_id} className="hover:shadow-lg transition-shadow">
<CardHeader>
<div className="flex justify-between items-start">
<div className="space-y-2">
<CardTitle className="text-lg line-clamp-2">{bounty.title}</CardTitle>
<div className="flex gap-2">
<Badge className={getTierColor(bounty.tier)}>
{bounty.tier.charAt(0).toUpperCase() + bounty.tier.slice(1)}
</Badge>
<Badge className={getStatusColor(bounty.status)}>
{bounty.status.charAt(0).toUpperCase() + bounty.status.slice(1)}
</Badge>
</div>
</div>
<div className="text-right">
<p className="text-2xl font-bold text-blue-600">
{bounty.reward_amount.toLocaleString()}
</p>
<p className="text-sm text-muted-foreground">AITBC</p>
</div>
</div>
</CardHeader>
<CardContent className="space-y-4">
<CardDescription className="line-clamp-3">
{bounty.description}
</CardDescription>
<div className="space-y-2">
<div className="flex justify-between text-sm">
<span className="text-muted-foreground">Min Accuracy</span>
<span className="font-medium">{bounty.min_accuracy}%</span>
</div>
{bounty.max_response_time && (
<div className="flex justify-between text-sm">
<span className="text-muted-foreground">Max Response Time</span>
<span className="font-medium">{bounty.max_response_time}ms</span>
</div>
)}
<div className="flex justify-between text-sm">
<span className="text-muted-foreground">Submissions</span>
<span className="font-medium">{bounty.submission_count}/{bounty.max_submissions}</span>
</div>
<div className="flex justify-between text-sm">
<span className="text-muted-foreground">Time Remaining</span>
<span className="font-medium">{getTimeRemaining(bounty.deadline)}</span>
</div>
</div>
{/* Progress bar for submissions */}
<Progress
value={(bounty.submission_count / bounty.max_submissions) * 100}
className="h-2"
/>
{/* Tags */}
{bounty.tags.length > 0 && (
<div className="flex flex-wrap gap-1">
{bounty.tags.slice(0, 3).map((tag) => (
<Badge key={tag} variant="secondary" className="text-xs">
{tag}
</Badge>
))}
{bounty.tags.length > 3 && (
<Badge variant="secondary" className="text-xs">
+{bounty.tags.length - 3}
</Badge>
)}
</div>
)}
{/* ZK Proof indicator */}
{bounty.requires_zk_proof && (
<div className="flex items-center gap-2 text-sm text-blue-600">
<AlertCircle className="h-4 w-4" />
<span>ZK-Proof Required</span>
</div>
)}
</CardContent>
<CardFooter className="space-y-2">
{bounty.status === 'active' && (
<Button
className="w-full"
onClick={() => handleBountySubmit(bounty.bounty_id)}
disabled={!isConnected}
>
{isConnected ? 'Submit Solution' : 'Connect Wallet'}
</Button>
)}
{bounty.status === 'completed' && bounty.winner_address && (
<div className="w-full text-center">
<p className="text-sm text-muted-foreground">Won by</p>
<p className="font-mono text-xs">{bounty.winner_address.slice(0, 8)}...{bounty.winner_address.slice(-6)}</p>
</div>
)}
<Button
variant="outline"
className="w-full"
onClick={() => setSelectedBounty(bounty)}
>
View Details
</Button>
</CardFooter>
</Card>
))}
</div>
{filteredBounties.length === 0 && (
<div className="text-center py-12">
<Trophy className="h-12 w-12 text-muted-foreground mx-auto mb-4" />
<h3 className="text-lg font-semibold mb-2">No bounties found</h3>
<p className="text-muted-foreground">
{searchQuery ? 'Try adjusting your search terms' : 'Check back later for new opportunities'}
</p>
</div>
)}
</TabsContent>
</Tabs>
{/* Bounty Detail Modal */}
{selectedBounty && (
<div className="fixed inset-0 bg-black/50 flex items-center justify-center p-4 z-50">
<Card className="max-w-2xl w-full max-h-[80vh] overflow-y-auto">
<CardHeader>
<div className="flex justify-between items-start">
<div className="space-y-2">
<CardTitle className="text-xl">{selectedBounty.title}</CardTitle>
<div className="flex gap-2">
<Badge className={getTierColor(selectedBounty.tier)}>
{selectedBounty.tier.charAt(0).toUpperCase() + selectedBounty.tier.slice(1)}
</Badge>
<Badge className={getStatusColor(selectedBounty.status)}>
{selectedBounty.status.charAt(0).toUpperCase() + selectedBounty.status.slice(1)}
</Badge>
</div>
</div>
<div className="text-right">
<p className="text-3xl font-bold text-blue-600">
{selectedBounty.reward_amount.toLocaleString()}
</p>
<p className="text-sm text-muted-foreground">AITBC</p>
</div>
</div>
</CardHeader>
<CardContent className="space-y-6">
<div>
<h4 className="font-semibold mb-2">Description</h4>
<p className="text-muted-foreground">{selectedBounty.description}</p>
</div>
<div>
<h4 className="font-semibold mb-2">Requirements</h4>
<div className="grid grid-cols-2 gap-4">
<div>
<p className="text-sm text-muted-foreground">Minimum Accuracy</p>
<p className="font-medium">{selectedBounty.min_accuracy}%</p>
</div>
{selectedBounty.max_response_time && (
<div>
<p className="text-sm text-muted-foreground">Max Response Time</p>
<p className="font-medium">{selectedBounty.max_response_time}ms</p>
</div>
)}
<div>
<p className="text-sm text-muted-foreground">Submissions</p>
<p className="font-medium">{selectedBounty.submission_count}/{selectedBounty.max_submissions}</p>
</div>
<div>
<p className="text-sm text-muted-foreground">Deadline</p>
<p className="font-medium">{new Date(selectedBounty.deadline).toLocaleDateString()}</p>
</div>
</div>
</div>
{selectedBounty.performance_criteria && (
<div>
<h4 className="font-semibold mb-2">Performance Criteria</h4>
<pre className="bg-muted p-3 rounded text-sm overflow-x-auto">
{JSON.stringify(selectedBounty.performance_criteria, null, 2)}
</pre>
</div>
)}
{selectedBounty.tags.length > 0 && (
<div>
<h4 className="font-semibold mb-2">Tags</h4>
<div className="flex flex-wrap gap-2">
{selectedBounty.tags.map((tag) => (
<Badge key={tag} variant="secondary">
{tag}
</Badge>
))}
</div>
</div>
)}
</CardContent>
<CardFooter className="flex gap-2">
{selectedBounty.status === 'active' && (
<Button
className="flex-1"
onClick={() => handleBountySubmit(selectedBounty.bounty_id)}
disabled={!isConnected}
>
{isConnected ? 'Submit Solution' : 'Connect Wallet'}
</Button>
)}
<Button variant="outline" onClick={() => setSelectedBounty(null)}>
Close
</Button>
</CardFooter>
</Card>
</div>
)}
</div>
);
};
export default BountyBoard;

View File

@@ -0,0 +1,692 @@
import React, { useState, useEffect } from 'react';
import {
Card,
CardContent,
CardDescription,
CardHeader,
CardTitle,
} from '@/components/ui/card';
import { Button } from '@/components/ui/button';
import { Badge } from '@/components/ui/badge';
import { Select, SelectContent, SelectItem, SelectTrigger, SelectValue } from '@/components/ui/select';
import { Tabs, TabsContent, TabsList, TabsTrigger } from '@/components/ui/tabs';
import { Progress } from '@/components/ui/progress';
import {
Table,
TableBody,
TableCell,
TableHead,
TableHeader,
TableRow,
} from '@/components/ui/table';
import {
Trophy,
Medal,
Award,
TrendingUp,
Users,
Target,
Zap,
Shield,
Star,
Crown,
Gem,
Flame,
Rocket,
Calendar,
Filter,
Download,
RefreshCw
} from 'lucide-react';
import { useToast } from '@/hooks/use-toast';
import { formatDistanceToNow } from 'date-fns';
interface LeaderboardEntry {
address: string;
rank: number;
total_earned: number;
submissions: number;
avg_accuracy: number;
success_rate: number;
bounties_completed: number;
tier: 'bronze' | 'silver' | 'gold' | 'platinum' | 'diamond';
reputation_score: number;
last_active: string;
streak_days: number;
weekly_growth: number;
monthly_growth: number;
}
interface TopPerformer {
address: string;
rank: number;
metric: string;
value: number;
change: number;
badge?: string;
}
interface CategoryStats {
category: string;
total_earnings: number;
participant_count: number;
avg_earnings: number;
top_performer: string;
growth_rate: number;
}
const DeveloperLeaderboard: React.FC = () => {
const { toast } = useToast();
const [leaderboard, setLeaderboard] = useState<LeaderboardEntry[]>([]);
const [topPerformers, setTopPerformers] = useState<TopPerformer[]>([]);
const [categoryStats, setCategoryStats] = useState<CategoryStats[]>([]);
const [loading, setLoading] = useState(true);
const [activeTab, setActiveTab] = useState('earnings');
const [period, setPeriod] = useState('weekly');
const [category, setCategory] = useState('all');
const [metric, setMetric] = useState('total_earned');
const [lastUpdated, setLastUpdated] = useState<Date>(new Date());
// Load leaderboard data on component mount
useEffect(() => {
loadLeaderboard();
loadTopPerformers();
loadCategoryStats();
}, [period, category, metric]);
const loadLeaderboard = async () => {
try {
setLoading(true);
const response = await fetch(`/api/v1/bounties/leaderboard?period=${period}&limit=100`);
if (response.ok) {
const data = await response.json();
setLeaderboard(data);
setLastUpdated(new Date());
} else {
throw new Error('Failed to load leaderboard');
}
} catch (error) {
console.error('Error loading leaderboard:', error);
toast({
title: 'Error',
description: 'Failed to load leaderboard data',
variant: 'destructive'
});
} finally {
setLoading(false);
}
};
const loadTopPerformers = async () => {
try {
const response = await fetch(`/api/v1/ecosystem/top-performers?category=${category}&period=${period}&limit=10`);
if (response.ok) {
const data = await response.json();
setTopPerformers(data.performers);
}
} catch (error) {
console.error('Error loading top performers:', error);
}
};
const loadCategoryStats = async () => {
try {
const response = await fetch(`/api/v1/ecosystem/category-stats?period=${period}`);
if (response.ok) {
const data = await response.json();
setCategoryStats(data.categories);
}
} catch (error) {
console.error('Error loading category stats:', error);
}
};
const getRankIcon = (rank: number) => {
if (rank === 1) return <Crown className="h-5 w-5 text-yellow-500" />;
if (rank === 2) return <Medal className="h-5 w-5 text-gray-400" />;
if (rank === 3) return <Award className="h-5 w-5 text-amber-600" />;
return <span className="text-sm font-bold text-muted-foreground">#{rank}</span>;
};
const getTierColor = (tier: string) => {
const colors = {
bronze: 'bg-orange-100 text-orange-800 border-orange-200',
silver: 'bg-gray-100 text-gray-800 border-gray-200',
gold: 'bg-yellow-100 text-yellow-800 border-yellow-200',
platinum: 'bg-purple-100 text-purple-800 border-purple-200',
diamond: 'bg-blue-100 text-blue-800 border-blue-200'
};
return colors[tier as keyof typeof colors] || colors.bronze;
};
const getGrowthIcon = (growth: number) => {
if (growth > 0) return <TrendingUp className="h-4 w-4 text-green-600" />;
if (growth < 0) return <TrendingUp className="h-4 w-4 text-red-600 rotate-180" />;
return <div className="h-4 w-4" />;
};
const getGrowthColor = (growth: number) => {
if (growth > 0) return 'text-green-600';
if (growth < 0) return 'text-red-600';
return 'text-gray-600';
};
const exportLeaderboard = async () => {
try {
const response = await fetch(`/api/v1/ecosystem/export?format=csv&period=${period}`);
if (response.ok) {
const data = await response.json();
// Create download link
const link = document.createElement('a');
link.href = data.url;
link.download = `leaderboard_${period}.csv`;
link.click();
toast({
title: 'Export Started',
description: 'Leaderboard data is being downloaded',
});
}
} catch (error) {
console.error('Error exporting leaderboard:', error);
toast({
title: 'Error',
description: 'Failed to export leaderboard',
variant: 'destructive'
});
}
};
const refreshData = () => {
loadLeaderboard();
loadTopPerformers();
loadCategoryStats();
};
if (loading && leaderboard.length === 0) {
return (
<div className="container mx-auto py-8">
<div className="flex items-center justify-center h-64">
<div className="animate-spin rounded-full h-8 w-8 border-b-2 border-blue-600"></div>
</div>
</div>
);
}
return (
<div className="container mx-auto py-8 space-y-6">
{/* Header */}
<div className="flex justify-between items-center">
<div>
<h1 className="text-3xl font-bold">Developer Leaderboard</h1>
<p className="text-muted-foreground">
Top performers in the AITBC developer ecosystem
</p>
</div>
<div className="flex gap-2">
<Button variant="outline" onClick={refreshData}>
<RefreshCw className="h-4 w-4 mr-2" />
Refresh
</Button>
<Button variant="outline" onClick={exportLeaderboard}>
<Download className="h-4 w-4 mr-2" />
Export
</Button>
</div>
</div>
{/* Top 3 Performers */}
<div className="grid grid-cols-1 md:grid-cols-3 gap-6">
{leaderboard.slice(0, 3).map((performer, index) => (
<Card key={performer.address} className="relative overflow-hidden">
<div className={`absolute inset-0 bg-gradient-to-br ${
index === 0 ? 'from-yellow-100 to-amber-100' :
index === 1 ? 'from-gray-100 to-slate-100' :
'from-amber-100 to-orange-100'
} opacity-10`}></div>
<CardHeader className="relative">
<div className="flex items-center justify-between">
<div className="flex items-center gap-3">
{getRankIcon(performer.rank)}
<div>
<CardTitle className="text-lg">
{performer.address.slice(0, 8)}...{performer.address.slice(-6)}
</CardTitle>
<Badge className={getTierColor(performer.tier)}>
{performer.tier.charAt(0).toUpperCase() + performer.tier.slice(1)}
</Badge>
</div>
</div>
</div>
</CardHeader>
<CardContent className="relative space-y-4">
<div className="text-center">
<p className="text-3xl font-bold text-blue-600">
{performer.total_earned.toLocaleString()}
</p>
<p className="text-sm text-muted-foreground">AITBC Earned</p>
</div>
<div className="grid grid-cols-2 gap-4 text-sm">
<div>
<p className="text-muted-foreground">Submissions</p>
<p className="font-medium">{performer.submissions}</p>
</div>
<div>
<p className="text-muted-foreground">Success Rate</p>
<p className="font-medium">{performer.success_rate.toFixed(1)}%</p>
</div>
<div>
<p className="text-muted-foreground">Avg Accuracy</p>
<p className="font-medium">{performer.avg_accuracy.toFixed(1)}%</p>
</div>
<div>
<p className="text-muted-foreground">Streak</p>
<p className="font-medium flex items-center gap-1">
<Flame className="h-3 w-3 text-orange-500" />
{performer.streak_days} days
</p>
</div>
</div>
<div className="flex items-center justify-between">
<span className="text-sm text-muted-foreground">Weekly Growth</span>
<div className={`flex items-center gap-1 ${getGrowthColor(performer.weekly_growth)}`}>
{getGrowthIcon(performer.weekly_growth)}
<span className="font-medium">
{performer.weekly_growth > 0 ? '+' : ''}{performer.weekly_growth.toFixed(1)}%
</span>
</div>
</div>
</CardContent>
</Card>
))}
</div>
{/* Filters */}
<div className="flex flex-wrap gap-4 items-center">
<Select value={period} onValueChange={setPeriod}>
<SelectTrigger className="w-32">
<SelectValue />
</SelectTrigger>
<SelectContent>
<SelectItem value="daily">Daily</SelectItem>
<SelectItem value="weekly">Weekly</SelectItem>
<SelectItem value="monthly">Monthly</SelectItem>
</SelectContent>
</Select>
<Select value={category} onValueChange={setCategory}>
<SelectTrigger className="w-40">
<SelectValue />
</SelectTrigger>
<SelectContent>
<SelectItem value="all">All Categories</SelectItem>
<SelectItem value="developers">Developers</SelectItem>
<SelectItem value="agents">Agents</SelectItem>
<SelectItem value="stakers">Stakers</SelectItem>
</SelectContent>
</Select>
<Select value={metric} onValueChange={setMetric}>
<SelectTrigger className="w-40">
<SelectValue />
</SelectTrigger>
<SelectContent>
<SelectItem value="total_earned">Total Earned</SelectItem>
<SelectItem value="submissions">Submissions</SelectItem>
<SelectItem value="success_rate">Success Rate</SelectItem>
<SelectItem value="avg_accuracy">Accuracy</SelectItem>
</SelectContent>
</Select>
<div className="text-sm text-muted-foreground">
Last updated: {formatDistanceToNow(lastUpdated, { addSuffix: true })}
</div>
</div>
{/* Tabs */}
<Tabs value={activeTab} onValueChange={setActiveTab}>
<TabsList>
<TabsTrigger value="earnings">Earnings</TabsTrigger>
<TabsTrigger value="performance">Performance</TabsTrigger>
<TabsTrigger value="categories">Categories</TabsTrigger>
<TabsTrigger value="trends">Trends</TabsTrigger>
</TabsList>
{/* Earnings Tab */}
<TabsContent value="earnings" className="space-y-4">
<Card>
<CardHeader>
<CardTitle className="flex items-center gap-2">
<Trophy className="h-5 w-5" />
Earnings Leaderboard
</CardTitle>
<CardDescription>
Top developers by total AITBC earnings
</CardDescription>
</CardHeader>
<CardContent>
<Table>
<TableHeader>
<TableRow>
<TableHead>Rank</TableHead>
<TableHead>Developer</TableHead>
<TableHead>Tier</TableHead>
<TableHead>Total Earned</TableHead>
<TableHead>Submissions</TableHead>
<TableHead>Success Rate</TableHead>
<TableHead>Accuracy</TableHead>
<TableHead>Growth</TableHead>
</TableRow>
</TableHeader>
<TableBody>
{leaderboard.map((entry) => (
<TableRow key={entry.address}>
<TableCell>
<div className="flex items-center gap-2">
{getRankIcon(entry.rank)}
</div>
</TableCell>
<TableCell className="font-mono">
{entry.address.slice(0, 8)}...{entry.address.slice(-6)}
</TableCell>
<TableCell>
<Badge className={getTierColor(entry.tier)}>
{entry.tier.charAt(0).toUpperCase() + entry.tier.slice(1)}
</Badge>
</TableCell>
<TableCell className="font-bold text-blue-600">
{entry.total_earned.toLocaleString()} AITBC
</TableCell>
<TableCell>{entry.submissions}</TableCell>
<TableCell>
<div className="flex items-center gap-2">
<Progress value={entry.success_rate} className="w-16 h-2" />
<span className="text-sm">{entry.success_rate.toFixed(1)}%</span>
</div>
</TableCell>
<TableCell>
<div className="flex items-center gap-2">
<Target className="h-4 w-4 text-green-600" />
<span>{entry.avg_accuracy.toFixed(1)}%</span>
</div>
</TableCell>
<TableCell>
<div className={`flex items-center gap-1 ${getGrowthColor(entry.weekly_growth)}`}>
{getGrowthIcon(entry.weekly_growth)}
<span className="text-sm">
{entry.weekly_growth > 0 ? '+' : ''}{entry.weekly_growth.toFixed(1)}%
</span>
</div>
</TableCell>
</TableRow>
))}
</TableBody>
</Table>
</CardContent>
</Card>
</TabsContent>
{/* Performance Tab */}
<TabsContent value="performance" className="space-y-4">
<div className="grid grid-cols-1 md:grid-cols-2 gap-6">
<Card>
<CardHeader>
<CardTitle className="flex items-center gap-2">
<Target className="h-5 w-5" />
Top Accuracy
</CardTitle>
</CardHeader>
<CardContent>
<div className="space-y-3">
{leaderboard
.sort((a, b) => b.avg_accuracy - a.avg_accuracy)
.slice(0, 5)
.map((entry, index) => (
<div key={entry.address} className="flex items-center justify-between">
<div className="flex items-center gap-3">
<span className="text-sm font-bold">#{index + 1}</span>
<span className="font-mono text-sm">
{entry.address.slice(0, 8)}...{entry.address.slice(-6)}
</span>
</div>
<div className="flex items-center gap-2">
<Progress value={entry.avg_accuracy} className="w-20 h-2" />
<span className="text-sm font-medium">{entry.avg_accuracy.toFixed(1)}%</span>
</div>
</div>
))}
</div>
</CardContent>
</Card>
<Card>
<CardHeader>
<CardTitle className="flex items-center gap-2">
<Rocket className="h-5 w-5" />
Fastest Growth
</CardTitle>
</CardHeader>
<CardContent>
<div className="space-y-3">
{leaderboard
.sort((a, b) => b.weekly_growth - a.weekly_growth)
.slice(0, 5)
.map((entry, index) => (
<div key={entry.address} className="flex items-center justify-between">
<div className="flex items-center gap-3">
<span className="text-sm font-bold">#{index + 1}</span>
<span className="font-mono text-sm">
{entry.address.slice(0, 8)}...{entry.address.slice(-6)}
</span>
</div>
<div className={`flex items-center gap-1 ${getGrowthColor(entry.weekly_growth)}`}>
{getGrowthIcon(entry.weekly_growth)}
<span className="text-sm font-medium">
{entry.weekly_growth > 0 ? '+' : ''}{entry.weekly_growth.toFixed(1)}%
</span>
</div>
</div>
))}
</div>
</CardContent>
</Card>
<Card>
<CardHeader>
<CardTitle className="flex items-center gap-2">
<Flame className="h-5 w-5" />
Longest Streaks
</CardTitle>
</CardHeader>
<CardContent>
<div className="space-y-3">
{leaderboard
.sort((a, b) => b.streak_days - a.streak_days)
.slice(0, 5)
.map((entry, index) => (
<div key={entry.address} className="flex items-center justify-between">
<div className="flex items-center gap-3">
<span className="text-sm font-bold">#{index + 1}</span>
<span className="font-mono text-sm">
{entry.address.slice(0, 8)}...{entry.address.slice(-6)}
</span>
</div>
<div className="flex items-center gap-1">
<Flame className="h-4 w-4 text-orange-500" />
<span className="text-sm font-medium">{entry.streak_days} days</span>
</div>
</div>
))}
</div>
</CardContent>
</Card>
<Card>
<CardHeader>
<CardTitle className="flex items-center gap-2">
<Shield className="h-5 w-5" />
Reputation Leaders
</CardTitle>
</CardHeader>
<CardContent>
<div className="space-y-3">
{leaderboard
.sort((a, b) => b.reputation_score - a.reputation_score)
.slice(0, 5)
.map((entry, index) => (
<div key={entry.address} className="flex items-center justify-between">
<div className="flex items-center gap-3">
<span className="text-sm font-bold">#{index + 1}</span>
<span className="font-mono text-sm">
{entry.address.slice(0, 8)}...{entry.address.slice(-6)}
</span>
<Badge className={getTierColor(entry.tier)}>
{entry.tier.charAt(0).toUpperCase() + entry.tier.slice(1)}
</Badge>
</div>
<div className="flex items-center gap-1">
<Star className="h-4 w-4 text-yellow-500" />
<span className="text-sm font-medium">{entry.reputation_score.toFixed(1)}</span>
</div>
</div>
))}
</div>
</CardContent>
</Card>
</div>
</TabsContent>
{/* Categories Tab */}
<TabsContent value="categories" className="space-y-4">
<div className="grid grid-cols-1 md:grid-cols-2 lg:grid-cols-3 gap-6">
{categoryStats.map((category) => (
<Card key={category.category}>
<CardHeader>
<CardTitle className="capitalize">{category.category}</CardTitle>
<CardDescription>
{category.participant_count} participants
</CardDescription>
</CardHeader>
<CardContent className="space-y-4">
<div className="text-center">
<p className="text-2xl font-bold text-blue-600">
{category.total_earnings.toLocaleString()}
</p>
<p className="text-sm text-muted-foreground">Total Earnings</p>
</div>
<div className="space-y-2">
<div className="flex justify-between text-sm">
<span className="text-muted-foreground">Average Earnings</span>
<span className="font-medium">{category.avg_earnings.toLocaleString()} AITBC</span>
</div>
<div className="flex justify-between text-sm">
<span className="text-muted-foreground">Growth Rate</span>
<span className={`font-medium ${getGrowthColor(category.growth_rate)}`}>
{category.growth_rate > 0 ? '+' : ''}{category.growth_rate.toFixed(1)}%
</span>
</div>
</div>
<div className="pt-2 border-t">
<p className="text-xs text-muted-foreground mb-1">Top Performer</p>
<p className="font-mono text-sm">
{category.top_performer.slice(0, 8)}...{category.top_performer.slice(-6)}
</p>
</div>
</CardContent>
</Card>
))}
</div>
</TabsContent>
{/* Trends Tab */}
<TabsContent value="trends" className="space-y-4">
<div className="grid grid-cols-1 md:grid-cols-2 gap-6">
<Card>
<CardHeader>
<CardTitle className="flex items-center gap-2">
<TrendingUp className="h-5 w-5" />
Weekly Trends
</CardTitle>
</CardHeader>
<CardContent>
<div className="space-y-4">
<div className="flex justify-between items-center">
<span className="text-sm text-muted-foreground">Total Participants</span>
<span className="font-bold">{leaderboard.length}</span>
</div>
<div className="flex justify-between items-center">
<span className="text-sm text-muted-foreground">Average Earnings</span>
<span className="font-bold">
{leaderboard.length > 0
? (leaderboard.reduce((sum, e) => sum + e.total_earned, 0) / leaderboard.length).toLocaleString()
: '0'
} AITBC
</span>
</div>
<div className="flex justify-between items-center">
<span className="text-sm text-muted-foreground">Success Rate</span>
<span className="font-bold">
{leaderboard.length > 0
? (leaderboard.reduce((sum, e) => sum + e.success_rate, 0) / leaderboard.length).toFixed(1)
: '0'
}%
</span>
</div>
<div className="flex justify-between items-center">
<span className="text-sm text-muted-foreground">Average Accuracy</span>
<span className="font-bold">
{leaderboard.length > 0
? (leaderboard.reduce((sum, e) => sum + e.avg_accuracy, 0) / leaderboard.length).toFixed(1)
: '0'
}%
</span>
</div>
</div>
</CardContent>
</Card>
<Card>
<CardHeader>
<CardTitle className="flex items-center gap-2">
<Users className="h-5 w-5" />
Participant Distribution
</CardTitle>
</CardHeader>
<CardContent>
<div className="space-y-4">
{['bronze', 'silver', 'gold', 'platinum', 'diamond'].map((tier) => {
const count = leaderboard.filter(e => e.tier === tier).length;
const percentage = leaderboard.length > 0 ? (count / leaderboard.length) * 100 : 0;
return (
<div key={tier} className="space-y-2">
<div className="flex justify-between text-sm">
<span className="capitalize">{tier}</span>
<span className="font-medium">{count} ({percentage.toFixed(1)}%)</span>
</div>
<Progress value={percentage} className="h-2" />
</div>
);
})}
</div>
</CardContent>
</Card>
</div>
</TabsContent>
</Tabs>
</div>
);
};
export default DeveloperLeaderboard;

View File

@@ -0,0 +1,860 @@
import React, { useState, useEffect } from 'react';
import {
Card,
CardContent,
CardDescription,
CardHeader,
CardTitle,
} from '@/components/ui/card';
import { Button } from '@/components/ui/button';
import { Badge } from '@/components/ui/badge';
import { Select, SelectContent, SelectItem, SelectTrigger, SelectValue } from '@/components/ui/select';
import { Tabs, TabsContent, TabsList, TabsTrigger } from '@/components/ui/tabs';
import { Progress } from '@/components/ui/progress';
import {
Table,
TableBody,
TableCell,
TableHead,
TableHeader,
TableRow,
} from '@/components/ui/table';
import {
TrendingUp,
TrendingDown,
Users,
DollarSign,
Activity,
PieChart,
BarChart3,
Zap,
Shield,
Target,
Coins,
Calendar,
Download,
RefreshCw,
Globe,
Cpu,
Database,
Network,
Award,
Star,
AlertTriangle,
CheckCircle,
Info
} from 'lucide-react';
import { useToast } from '@/hooks/use-toast';
import { formatDistanceToNow } from 'date-fns';
interface EcosystemOverview {
total_developers: number;
total_agents: number;
total_stakers: number;
total_bounties: number;
active_bounties: number;
completed_bounties: number;
total_value_locked: number;
total_rewards_distributed: number;
daily_volume: number;
weekly_growth: number;
monthly_growth: number;
ecosystem_health_score: number;
last_updated: string;
}
interface DeveloperEarnings {
address: string;
total_earned: number;
bounties_completed: number;
success_rate: number;
tier: string;
weekly_earnings: number;
monthly_earnings: number;
rank: number;
growth_rate: number;
}
interface AgentUtilization {
agent_address: string;
total_submissions: number;
success_rate: number;
average_accuracy: number;
total_earnings: number;
utilization_rate: number;
current_tier: string;
performance_score: number;
last_active: string;
}
interface TreasuryAllocation {
category: string;
amount: number;
percentage: number;
description: string;
trend: 'up' | 'down' | 'stable';
monthly_change: number;
}
interface StakingMetrics {
total_staked: number;
total_stakers: number;
average_stake_amount: number;
total_rewards_distributed: number;
average_apy: number;
staking_participation_rate: number;
top_stakers: Array<{
address: string;
amount: number;
rewards: number;
}>;
pool_distribution: Array<{
agent_address: string;
total_staked: number;
staker_count: number;
apy: number;
}>;
}
interface BountyAnalytics {
total_bounties: number;
active_bounties: number;
completed_bounties: number;
average_completion_time: number;
success_rate: number;
total_value: number;
category_distribution: Array<{
category: string;
count: number;
value: number;
}>;
difficulty_distribution: Array<{
difficulty: string;
count: number;
success_rate: number;
}>;
completion_trends: Array<{
date: string;
completed: number;
value: number;
}>;
}
const EcosystemDashboard: React.FC = () => {
const { toast } = useToast();
const [overview, setOverview] = useState<EcosystemOverview | null>(null);
const [developerEarnings, setDeveloperEarnings] = useState<DeveloperEarnings[]>([]);
const [agentUtilization, setAgentUtilization] = useState<AgentUtilization[]>([]);
const [treasuryAllocation, setTreasuryAllocation] = useState<TreasuryAllocation[]>([]);
const [stakingMetrics, setStakingMetrics] = useState<StakingMetrics | null>(null);
const [bountyAnalytics, setBountyAnalytics] = useState<BountyAnalytics | null>(null);
const [loading, setLoading] = useState(true);
const [activeTab, setActiveTab] = useState('overview');
const [period, setPeriod] = useState('weekly');
const [lastUpdated, setLastUpdated] = useState<Date>(new Date());
// Load ecosystem data on component mount
useEffect(() => {
loadEcosystemData();
}, [period]);
const loadEcosystemData = async () => {
try {
setLoading(true);
// Load overview
const overviewResponse = await fetch('/api/v1/ecosystem/overview');
if (overviewResponse.ok) {
const overviewData = await overviewResponse.json();
setOverview(overviewData);
}
// Load developer earnings
const earningsResponse = await fetch(`/api/v1/ecosystem/developer-earnings?period=${period}&limit=50`);
if (earningsResponse.ok) {
const earningsData = await earningsResponse.json();
setDeveloperEarnings(earningsData);
}
// Load agent utilization
const utilizationResponse = await fetch(`/api/v1/ecosystem/agent-utilization?period=${period}&limit=50`);
if (utilizationResponse.ok) {
const utilizationData = await utilizationResponse.json();
setAgentUtilization(utilizationData);
}
// Load treasury allocation
const treasuryResponse = await fetch('/api/v1/ecosystem/treasury-allocation');
if (treasuryResponse.ok) {
const treasuryData = await treasuryResponse.json();
setTreasuryAllocation(treasuryData);
}
// Load staking metrics
const stakingResponse = await fetch('/api/v1/ecosystem/staking-metrics');
if (stakingResponse.ok) {
const stakingData = await stakingResponse.json();
setStakingMetrics(stakingData);
}
// Load bounty analytics
const bountyResponse = await fetch('/api/v1/ecosystem/bounty-analytics');
if (bountyResponse.ok) {
const bountyData = await bountyResponse.json();
setBountyAnalytics(bountyData);
}
setLastUpdated(new Date());
} catch (error) {
console.error('Error loading ecosystem data:', error);
toast({
title: 'Error',
description: 'Failed to load ecosystem data',
variant: 'destructive'
});
} finally {
setLoading(false);
}
};
const getTierColor = (tier: string) => {
const colors = {
bronze: 'bg-orange-100 text-orange-800 border-orange-200',
silver: 'bg-gray-100 text-gray-800 border-gray-200',
gold: 'bg-yellow-100 text-yellow-800 border-yellow-200',
platinum: 'bg-purple-100 text-purple-800 border-purple-200',
diamond: 'bg-blue-100 text-blue-800 border-blue-200'
};
return colors[tier as keyof typeof colors] || colors.bronze;
};
const getHealthColor = (score: number) => {
if (score >= 80) return 'text-green-600';
if (score >= 60) return 'text-yellow-600';
return 'text-red-600';
};
const getHealthIcon = (score: number) => {
if (score >= 80) return <CheckCircle className="h-5 w-5 text-green-600" />;
if (score >= 60) return <AlertTriangle className="h-5 w-5 text-yellow-600" />;
return <AlertTriangle className="h-5 w-5 text-red-600" />;
};
const getTrendIcon = (trend: string) => {
if (trend === 'up') return <TrendingUp className="h-4 w-4 text-green-600" />;
if (trend === 'down') return <TrendingDown className="h-4 w-4 text-red-600" />;
return <div className="h-4 w-4" />;
};
const exportData = async (dataType: string) => {
try {
const response = await fetch(`/api/v1/ecosystem/export?format=csv&type=${dataType}`);
if (response.ok) {
const data = await response.json();
// Create download link
const link = document.createElement('a');
link.href = data.url;
link.download = `${dataType}_export_${period}.csv`;
link.click();
toast({
title: 'Export Started',
description: `${dataType} data is being downloaded`,
});
}
} catch (error) {
console.error('Error exporting data:', error);
toast({
title: 'Error',
description: 'Failed to export data',
variant: 'destructive'
});
}
};
const refreshData = () => {
loadEcosystemData();
};
if (loading && !overview) {
return (
<div className="container mx-auto py-8">
<div className="flex items-center justify-center h-64">
<div className="animate-spin rounded-full h-8 w-8 border-b-2 border-blue-600"></div>
</div>
</div>
);
}
return (
<div className="container mx-auto py-8 space-y-6">
{/* Header */}
<div className="flex justify-between items-center">
<div>
<h1 className="text-3xl font-bold">Ecosystem Dashboard</h1>
<p className="text-muted-foreground">
Comprehensive overview of the AITBC ecosystem health and performance
</p>
</div>
<div className="flex gap-2">
<Select value={period} onValueChange={setPeriod}>
<SelectTrigger className="w-32">
<SelectValue />
</SelectTrigger>
<SelectContent>
<SelectItem value="daily">Daily</SelectItem>
<SelectItem value="weekly">Weekly</SelectItem>
<SelectItem value="monthly">Monthly</SelectItem>
</SelectContent>
</Select>
<Button variant="outline" onClick={refreshData}>
<RefreshCw className="h-4 w-4 mr-2" />
Refresh
</Button>
<Button variant="outline" onClick={() => exportData('ecosystem')}>
<Download className="h-4 w-4 mr-2" />
Export
</Button>
</div>
</div>
{/* Ecosystem Health Score */}
{overview && (
<Card className="bg-gradient-to-r from-blue-50 to-purple-50">
<CardHeader>
<div className="flex items-center justify-between">
<div className="flex items-center gap-3">
{getHealthIcon(overview.ecosystem_health_score)}
<div>
<CardTitle className="text-2xl">Ecosystem Health</CardTitle>
<CardDescription>
Overall system health and performance indicator
</CardDescription>
</div>
</div>
<div className="text-right">
<p className={`text-4xl font-bold ${getHealthColor(overview.ecosystem_health_score)}`}>
{overview.ecosystem_health_score}
</p>
<p className="text-sm text-muted-foreground">Health Score</p>
</div>
</div>
</CardHeader>
<CardContent>
<div className="grid grid-cols-2 md:grid-cols-4 gap-4">
<div className="text-center">
<p className="text-2xl font-bold text-blue-600">{overview.total_developers.toLocaleString()}</p>
<p className="text-sm text-muted-foreground">Developers</p>
</div>
<div className="text-center">
<p className="text-2xl font-bold text-green-600">{overview.total_agents.toLocaleString()}</p>
<p className="text-sm text-muted-foreground">AI Agents</p>
</div>
<div className="text-center">
<p className="text-2xl font-bold text-purple-600">{overview.total_stakers.toLocaleString()}</p>
<p className="text-sm text-muted-foreground">Stakers</p>
</div>
<div className="text-center">
<p className="text-2xl font-bold text-orange-600">{overview.total_bounties.toLocaleString()}</p>
<p className="text-sm text-muted-foreground">Bounties</p>
</div>
</div>
</CardContent>
</Card>
)}
{/* Key Metrics */}
{overview && (
<div className="grid grid-cols-1 md:grid-cols-2 lg:grid-cols-4 gap-4">
<Card>
<CardContent className="p-4">
<div className="flex items-center justify-between">
<div>
<p className="text-sm font-medium text-muted-foreground">Total Value Locked</p>
<p className="text-2xl font-bold">{overview.total_value_locked.toLocaleString()}</p>
<p className="text-xs text-muted-foreground">AITBC</p>
</div>
<Coins className="h-8 w-8 text-blue-600" />
</div>
</CardContent>
</Card>
<Card>
<CardContent className="p-4">
<div className="flex items-center justify-between">
<div>
<p className="text-sm font-medium text-muted-foreground">Rewards Distributed</p>
<p className="text-2xl font-bold">{overview.total_rewards_distributed.toLocaleString()}</p>
<p className="text-xs text-muted-foreground">AITBC</p>
</div>
<Award className="h-8 w-8 text-green-600" />
</div>
</CardContent>
</Card>
<Card>
<CardContent className="p-4">
<div className="flex items-center justify-between">
<div>
<p className="text-sm font-medium text-muted-foreground">Daily Volume</p>
<p className="text-2xl font-bold">{overview.daily_volume.toLocaleString()}</p>
<p className="text-xs text-muted-foreground">AITBC</p>
</div>
<Activity className="h-8 w-8 text-purple-600" />
</div>
</CardContent>
</Card>
<Card>
<CardContent className="p-4">
<div className="flex items-center justify-between">
<div>
<p className="text-sm font-medium text-muted-foreground">Active Bounties</p>
<p className="text-2xl font-bold">{overview.active_bounties.toLocaleString()}</p>
<p className="text-xs text-muted-foreground">Open</p>
</div>
<Target className="h-8 w-8 text-orange-600" />
</div>
</CardContent>
</Card>
</div>
)}
{/* Tabs */}
<Tabs value={activeTab} onValueChange={setActiveTab}>
<TabsList>
<TabsTrigger value="overview">Overview</TabsTrigger>
<TabsTrigger value="developers">Developers</TabsTrigger>
<TabsTrigger value="agents">Agents</TabsTrigger>
<TabsTrigger value="treasury">Treasury</TabsTrigger>
<TabsTrigger value="staking">Staking</TabsTrigger>
<TabsTrigger value="bounties">Bounties</TabsTrigger>
</TabsList>
{/* Overview Tab */}
<TabsContent value="overview" className="space-y-4">
<div className="grid grid-cols-1 md:grid-cols-2 gap-6">
<Card>
<CardHeader>
<CardTitle className="flex items-center gap-2">
<TrendingUp className="h-5 w-5" />
Growth Metrics
</CardTitle>
</CardHeader>
<CardContent className="space-y-4">
<div className="flex justify-between items-center">
<span className="text-sm text-muted-foreground">Weekly Growth</span>
<div className={`flex items-center gap-1 ${overview?.weekly_growth && overview.weekly_growth > 0 ? 'text-green-600' : 'text-red-600'}`}>
{overview?.weekly_growth && overview.weekly_growth > 0 ? <TrendingUp className="h-4 w-4" /> : <TrendingDown className="h-4 w-4" />}
<span className="font-medium">
{overview?.weekly_growth ? (overview.weekly_growth > 0 ? '+' : '') : ''}{overview?.weekly_growth?.toFixed(1) || '0.0'}%
</span>
</div>
</div>
<div className="flex justify-between items-center">
<span className="text-sm text-muted-foreground">Monthly Growth</span>
<div className={`flex items-center gap-1 ${overview?.monthly_growth && overview.monthly_growth > 0 ? 'text-green-600' : 'text-red-600'}`}>
{overview?.monthly_growth && overview.monthly_growth > 0 ? <TrendingUp className="h-4 w-4" /> : <TrendingDown className="h-4 w-4" />}
<span className="font-medium">
{overview?.monthly_growth ? (overview.monthly_growth > 0 ? '+' : '') : ''}{overview?.monthly_growth?.toFixed(1) || '0.0'}%
</span>
</div>
</div>
<div className="flex justify-between items-center">
<span className="text-sm text-muted-foreground">Completion Rate</span>
<span className="font-medium">
{overview ? ((overview.completed_bounties / overview.total_bounties) * 100).toFixed(1) : '0.0'}%
</span>
</div>
<div className="flex justify-between items-center">
<span className="text-sm text-muted-foreground">Participation Rate</span>
<span className="font-medium">
{overview ? ((overview.total_stakers / (overview.total_developers + overview.total_agents)) * 100).toFixed(1) : '0.0'}%
</span>
</div>
</CardContent>
</Card>
<Card>
<CardHeader>
<CardTitle className="flex items-center gap-2">
<PieChart className="h-5 w-5" />
Ecosystem Distribution
</CardTitle>
</CardHeader>
<CardContent>
<div className="space-y-4">
<div className="space-y-2">
<div className="flex justify-between text-sm">
<span className="text-muted-foreground">Developers</span>
<span className="font-medium">{overview?.total_developers.toLocaleString()}</span>
</div>
<Progress value={overview ? (overview.total_developers / (overview.total_developers + overview.total_agents + overview.total_stakers)) * 100 : 0} className="h-2" />
</div>
<div className="space-y-2">
<div className="flex justify-between text-sm">
<span className="text-muted-foreground">AI Agents</span>
<span className="font-medium">{overview?.total_agents.toLocaleString()}</span>
</div>
<Progress value={overview ? (overview.total_agents / (overview.total_developers + overview.total_agents + overview.total_stakers)) * 100 : 0} className="h-2" />
</div>
<div className="space-y-2">
<div className="flex justify-between text-sm">
<span className="text-muted-foreground">Stakers</span>
<span className="font-medium">{overview?.total_stakers.toLocaleString()}</span>
</div>
<Progress value={overview ? (overview.total_stakers / (overview.total_developers + overview.total_agents + overview.total_stakers)) * 100 : 0} className="h-2" />
</div>
</div>
</CardContent>
</Card>
</div>
</TabsContent>
{/* Developers Tab */}
<TabsContent value="developers" className="space-y-4">
<Card>
<CardHeader>
<CardTitle className="flex items-center gap-2">
<Users className="h-5 w-5" />
Top Developer Earnings
</CardTitle>
<CardDescription>
Highest earning developers in the ecosystem
</CardDescription>
</CardHeader>
<CardContent>
<Table>
<TableHeader>
<TableRow>
<TableHead>Rank</TableHead>
<TableHead>Developer</TableHead>
<TableHead>Tier</TableHead>
<TableHead>Total Earned</TableHead>
<TableHead>Bounties</TableHead>
<TableHead>Success Rate</TableHead>
<TableHead>Growth</TableHead>
</TableRow>
</TableHeader>
<TableBody>
{developerEarnings.slice(0, 10).map((developer) => (
<TableRow key={developer.address}>
<TableCell className="font-bold">#{developer.rank}</TableCell>
<TableCell className="font-mono">
{developer.address.slice(0, 8)}...{developer.address.slice(-6)}
</TableCell>
<TableCell>
<Badge className={getTierColor(developer.tier)}>
{developer.tier.charAt(0).toUpperCase() + developer.tier.slice(1)}
</Badge>
</TableCell>
<TableCell className="font-bold text-blue-600">
{developer.total_earned.toLocaleString()} AITBC
</TableCell>
<TableCell>{developer.bounties_completed}</TableCell>
<TableCell>
<div className="flex items-center gap-2">
<Progress value={developer.success_rate} className="w-16 h-2" />
<span className="text-sm">{developer.success_rate.toFixed(1)}%</span>
</div>
</TableCell>
<TableCell>
<div className={`flex items-center gap-1 ${developer.growth_rate > 0 ? 'text-green-600' : 'text-red-600'}`}>
{developer.growth_rate > 0 ? <TrendingUp className="h-4 w-4" /> : <TrendingDown className="h-4 w-4" />}
<span className="text-sm">
{developer.growth_rate > 0 ? '+' : ''}{developer.growth_rate.toFixed(1)}%
</span>
</div>
</TableCell>
</TableRow>
))}
</TableBody>
</Table>
</CardContent>
</Card>
</TabsContent>
{/* Agents Tab */}
<TabsContent value="agents" className="space-y-4">
<Card>
<CardHeader>
<CardTitle className="flex items-center gap-2">
<Cpu className="h-5 w-5" />
AI Agent Utilization
</CardTitle>
<CardDescription>
Performance metrics for AI agents in the ecosystem
</CardDescription>
</CardHeader>
<CardContent>
<Table>
<TableHeader>
<TableRow>
<TableHead>Agent</TableHead>
<TableHead>Tier</TableHead>
<TableHead>Submissions</TableHead>
<TableHead>Success Rate</TableHead>
<TableHead>Accuracy</TableHead>
<TableHead>Utilization</TableHead>
<TableHead>Earnings</TableHead>
</TableRow>
</TableHeader>
<TableBody>
{agentUtilization.slice(0, 10).map((agent) => (
<TableRow key={agent.agent_address}>
<TableCell className="font-mono">
{agent.agent_address.slice(0, 8)}...{agent.agent_address.slice(-6)}
</TableCell>
<TableCell>
<Badge className={getTierColor(agent.current_tier)}>
{agent.current_tier.charAt(0).toUpperCase() + agent.current_tier.slice(1)}
</Badge>
</TableCell>
<TableCell>{agent.total_submissions}</TableCell>
<TableCell>
<div className="flex items-center gap-2">
<Progress value={agent.success_rate} className="w-16 h-2" />
<span className="text-sm">{agent.success_rate.toFixed(1)}%</span>
</div>
</TableCell>
<TableCell>
<div className="flex items-center gap-2">
<Target className="h-4 w-4 text-green-600" />
<span>{agent.average_accuracy.toFixed(1)}%</span>
</div>
</TableCell>
<TableCell>
<div className="flex items-center gap-2">
<Progress value={agent.utilization_rate} className="w-16 h-2" />
<span className="text-sm">{agent.utilization_rate.toFixed(1)}%</span>
</div>
</TableCell>
<TableCell className="font-bold text-green-600">
{agent.total_earnings.toLocaleString()} AITBC
</TableCell>
</TableRow>
))}
</TableBody>
</Table>
</CardContent>
</Card>
</TabsContent>
{/* Treasury Tab */}
<TabsContent value="treasury" className="space-y-4">
<div className="grid grid-cols-1 md:grid-cols-2 gap-6">
<Card>
<CardHeader>
<CardTitle className="flex items-center gap-2">
<Database className="h-5 w-5" />
Treasury Allocation
</CardTitle>
</CardHeader>
<CardContent>
<div className="space-y-4">
{treasuryAllocation.map((allocation) => (
<div key={allocation.category} className="space-y-2">
<div className="flex justify-between items-center">
<div className="flex items-center gap-2">
{getTrendIcon(allocation.trend)}
<span className="font-medium">{allocation.category}</span>
</div>
<div className="text-right">
<p className="font-bold">{allocation.amount.toLocaleString()} AITBC</p>
<p className="text-sm text-muted-foreground">{allocation.percentage.toFixed(1)}%</p>
</div>
</div>
<Progress value={allocation.percentage} className="h-2" />
<p className="text-xs text-muted-foreground">{allocation.description}</p>
<div className="flex justify-between text-xs">
<span className="text-muted-foreground">Monthly Change</span>
<span className={allocation.monthly_change > 0 ? 'text-green-600' : 'text-red-600'}>
{allocation.monthly_change > 0 ? '+' : ''}{allocation.monthly_change.toFixed(1)}%
</span>
</div>
</div>
))}
</div>
</CardContent>
</Card>
<Card>
<CardHeader>
<CardTitle className="flex items-center gap-2">
<DollarSign className="h-5 w-5" />
Treasury Metrics
</CardTitle>
</CardHeader>
<CardContent className="space-y-4">
<div className="flex justify-between items-center">
<span className="text-sm text-muted-foreground">Total Treasury</span>
<span className="font-bold">
{treasuryAllocation.reduce((sum, a) => sum + a.amount, 0).toLocaleString()} AITBC
</span>
</div>
<div className="flex justify-between items-center">
<span className="text-sm text-muted-foreground">Monthly Revenue</span>
<span className="font-bold text-green-600">
+{treasuryAllocation.reduce((sum, a) => sum + a.monthly_change, 0).toFixed(1)}%
</span>
</div>
<div className="flex justify-between items-center">
<span className="text-sm text-muted-foreground">Burn Rate</span>
<span className="font-bold text-orange-600">2.3% monthly</span>
</div>
<div className="flex justify-between items-center">
<span className="text-sm text-muted-foreground">Runway</span>
<span className="font-bold">18 months</span>
</div>
</CardContent>
</Card>
</div>
</TabsContent>
{/* Staking Tab */}
<TabsContent value="staking" className="space-y-4">
{stakingMetrics && (
<div className="grid grid-cols-1 md:grid-cols-2 gap-6">
<Card>
<CardHeader>
<CardTitle className="flex items-center gap-2">
<Shield className="h-5 w-5" />
Staking Overview
</CardTitle>
</CardHeader>
<CardContent className="space-y-4">
<div className="flex justify-between items-center">
<span className="text-sm text-muted-foreground">Total Staked</span>
<span className="font-bold">{stakingMetrics.total_staked.toLocaleString()} AITBC</span>
</div>
<div className="flex justify-between items-center">
<span className="text-sm text-muted-foreground">Total Stakers</span>
<span className="font-bold">{stakingMetrics.total_stakers.toLocaleString()}</span>
</div>
<div className="flex justify-between items-center">
<span className="text-sm text-muted-foreground">Average Stake</span>
<span className="font-bold">{stakingMetrics.average_stake_amount.toLocaleString()} AITBC</span>
</div>
<div className="flex justify-between items-center">
<span className="text-sm text-muted-foreground">Average APY</span>
<span className="font-bold text-green-600">{stakingMetrics.average_apy.toFixed(1)}%</span>
</div>
<div className="flex justify-between items-center">
<span className="text-sm text-muted-foreground">Participation Rate</span>
<div className="flex items-center gap-2">
<Progress value={stakingMetrics.staking_participation_rate} className="w-16 h-2" />
<span className="text-sm">{stakingMetrics.staking_participation_rate.toFixed(1)}%</span>
</div>
</div>
</CardContent>
</Card>
<Card>
<CardHeader>
<CardTitle className="flex items-center gap-2">
<Star className="h-5 w-5" />
Top Stakers
</CardTitle>
</CardHeader>
<CardContent>
<div className="space-y-3">
{stakingMetrics.top_stakers.slice(0, 5).map((staker, index) => (
<div key={staker.address} className="flex items-center justify-between">
<div className="flex items-center gap-3">
<span className="text-sm font-bold">#{index + 1}</span>
<span className="font-mono text-sm">
{staker.address.slice(0, 8)}...{staker.address.slice(-6)}
</span>
</div>
<div className="text-right">
<p className="text-sm font-medium">{staker.amount.toLocaleString()} AITBC</p>
<p className="text-xs text-green-600">{staker.rewards.toLocaleString()} rewards</p>
</div>
</div>
))}
</div>
</CardContent>
</Card>
</div>
)}
</TabsContent>
{/* Bounties Tab */}
<TabsContent value="bounties" className="space-y-4">
{bountyAnalytics && (
<div className="grid grid-cols-1 md:grid-cols-2 gap-6">
<Card>
<CardHeader>
<CardTitle className="flex items-center gap-2">
<Target className="h-5 w-5" />
Bounty Analytics
</CardTitle>
</CardHeader>
<CardContent className="space-y-4">
<div className="flex justify-between items-center">
<span className="text-sm text-muted-foreground">Total Bounties</span>
<span className="font-bold">{bountyAnalytics.total_bounties.toLocaleString()}</span>
</div>
<div className="flex justify-between items-center">
<span className="text-sm text-muted-foreground">Active Bounties</span>
<span className="font-bold text-blue-600">{bountyAnalytics.active_bounties.toLocaleString()}</span>
</div>
<div className="flex justify-between items-center">
<span className="text-sm text-muted-foreground">Completed Bounties</span>
<span className="font-bold text-green-600">{bountyAnalytics.completed_bounties.toLocaleString()}</span>
</div>
<div className="flex justify-between items-center">
<span className="text-sm text-muted-foreground">Success Rate</span>
<div className="flex items-center gap-2">
<Progress value={bountyAnalytics.success_rate} className="w-16 h-2" />
<span className="text-sm">{bountyAnalytics.success_rate.toFixed(1)}%</span>
</div>
</div>
<div className="flex justify-between items-center">
<span className="text-sm text-muted-foreground">Avg Completion Time</span>
<span className="font-bold">{bountyAnalytics.average_completion_time.toFixed(1)} days</span>
</div>
</CardContent>
</Card>
<Card>
<CardHeader>
<CardTitle className="flex items-center gap-2">
<BarChart3 className="h-5 w-5" />
Category Distribution
</CardTitle>
</CardHeader>
<CardContent>
<div className="space-y-3">
{bountyAnalytics.category_distribution.map((category) => (
<div key={category.category} className="space-y-2">
<div className="flex justify-between text-sm">
<span className="font-medium">{category.category}</span>
<span className="text-muted-foreground">{category.count} bounties</span>
</div>
<Progress value={(category.count / bountyAnalytics.total_bounties) * 100} className="h-2" />
<p className="text-xs text-muted-foreground">
{category.value.toLocaleString()} AITBC total value
</p>
</div>
))}
</div>
</CardContent>
</Card>
</div>
)}
</TabsContent>
</Tabs>
{/* Footer */}
<div className="text-center text-sm text-muted-foreground">
<p>Last updated: {formatDistanceToNow(lastUpdated, { addSuffix: true })}</p>
<p>AITBC Ecosystem Dashboard - Real-time metrics and analytics</p>
</div>
</div>
);
};
export default EcosystemDashboard;

View File

@@ -0,0 +1,917 @@
import React, { useState, useEffect } from 'react';
import {
Card,
CardContent,
CardDescription,
CardFooter,
CardHeader,
CardTitle,
} from '@/components/ui/card';
import { Button } from '@/components/ui/button';
import { Badge } from '@/components/ui/badge';
import { Input } from '@/components/ui/input';
import { Select, SelectContent, SelectItem, SelectTrigger, SelectValue } from '@/components/ui/select';
import { Tabs, TabsContent, TabsList, TabsTrigger } from '@/components/ui/tabs';
import { Progress } from '@/components/ui/progress';
import { Alert, AlertDescription } from '@/components/ui/alert';
import {
Table,
TableBody,
TableCell,
TableHead,
TableHeader,
TableRow,
} from '@/components/ui/table';
import { Separator } from '@/components/ui/separator';
import {
TrendingUp,
TrendingDown,
Wallet,
Clock,
AlertTriangle,
CheckCircle,
XCircle,
Calculator,
Shield,
Zap,
Star,
Info,
ArrowUpRight,
ArrowDownRight,
Coins,
BarChart3,
PieChart,
Activity
} from 'lucide-react';
import { useWallet } from '@/hooks/use-wallet';
import { useToast } from '@/hooks/use-toast';
import { formatDistanceToNow, format } from 'date-fns';
interface Stake {
stake_id: string;
staker_address: string;
agent_wallet: string;
amount: number;
lock_period: number;
start_time: string;
end_time: string;
status: 'active' | 'unbonding' | 'completed' | 'slashed';
accumulated_rewards: number;
last_reward_time: string;
current_apy: number;
agent_tier: 'bronze' | 'silver' | 'gold' | 'platinum' | 'diamond';
performance_multiplier: number;
auto_compound: boolean;
unbonding_time?: string;
early_unbond_penalty: number;
lock_bonus_multiplier: number;
}
interface AgentMetrics {
agent_wallet: string;
total_staked: number;
staker_count: number;
total_rewards_distributed: number;
average_accuracy: number;
total_submissions: number;
successful_submissions: number;
success_rate: number;
current_tier: 'bronze' | 'silver' | 'gold' | 'platinum' | 'diamond';
tier_score: number;
reputation_score: number;
last_update_time: string;
average_response_time?: number;
energy_efficiency_score?: number;
}
interface StakingPool {
agent_wallet: string;
total_staked: number;
total_rewards: number;
pool_apy: number;
staker_count: number;
active_stakers: string[];
last_distribution_time: string;
min_stake_amount: number;
max_stake_amount: number;
auto_compound_enabled: boolean;
pool_performance_score: number;
volatility_score: number;
}
const StakingDashboard: React.FC = () => {
const { address, isConnected } = useWallet();
const { toast } = useToast();
const [stakes, setStakes] = useState<Stake[]>([]);
const [supportedAgents, setSupportedAgents] = useState<AgentMetrics[]>([]);
const [stakingPools, setStakingPools] = useState<StakingPool[]>([]);
const [loading, setLoading] = useState(true);
const [activeTab, setActiveTab] = useState('my-stakes');
const [showCreateStakeModal, setShowCreateStakeModal] = useState(false);
const [selectedAgent, setSelectedAgent] = useState<AgentMetrics | null>(null);
const [stakeForm, setStakeForm] = useState({
agent_wallet: '',
amount: '',
lock_period: '30',
auto_compound: false
});
const [totalRewards, setTotalRewards] = useState(0);
const [totalStaked, setTotalStaked] = useState(0);
// Load data on component mount
useEffect(() => {
if (isConnected) {
loadMyStakes();
loadMyRewards();
}
loadSupportedAgents();
loadStakingPools();
}, [isConnected]);
const loadMyStakes = async () => {
try {
const response = await fetch('/api/v1/staking/my-positions', {
headers: { 'Authorization': `Bearer ${address}` }
});
if (response.ok) {
const data = await response.json();
setStakes(data);
// Calculate total staked
const total = data.reduce((sum: number, stake: Stake) => sum + stake.amount, 0);
setTotalStaked(total);
}
} catch (error) {
console.error('Error loading stakes:', error);
}
};
const loadMyRewards = async () => {
try {
const response = await fetch('/api/v1/staking/my-rewards?period=monthly', {
headers: { 'Authorization': `Bearer ${address}` }
});
if (response.ok) {
const data = await response.json();
setTotalRewards(data.total_rewards);
}
} catch (error) {
console.error('Error loading rewards:', error);
}
};
const loadSupportedAgents = async () => {
try {
setLoading(true);
const response = await fetch('/api/v1/staking/agents/supported?limit=50');
if (response.ok) {
const data = await response.json();
setSupportedAgents(data.agents);
}
} catch (error) {
console.error('Error loading agents:', error);
} finally {
setLoading(false);
}
};
const loadStakingPools = async () => {
try {
const response = await fetch('/api/v1/staking/pools');
if (response.ok) {
const data = await response.json();
setStakingPools(data);
}
} catch (error) {
console.error('Error loading pools:', error);
}
};
const handleCreateStake = async () => {
if (!isConnected) {
toast({
title: 'Wallet Required',
description: 'Please connect your wallet to create stakes',
variant: 'destructive'
});
return;
}
try {
const response = await fetch('/api/v1/stake', {
method: 'POST',
headers: {
'Content-Type': 'application/json',
'Authorization': `Bearer ${address}`
},
body: JSON.stringify(stakeForm)
});
if (response.ok) {
const newStake = await response.json();
setStakes(prev => [newStake, ...prev]);
setShowCreateStakeModal(false);
setStakeForm({ agent_wallet: '', amount: '', lock_period: '30', auto_compound: false });
toast({
title: 'Stake Created',
description: `Successfully staked ${stakeForm.amount} AITBC`,
});
// Reload data
loadMyStakes();
loadStakingPools();
} else {
throw new Error('Failed to create stake');
}
} catch (error) {
console.error('Error creating stake:', error);
toast({
title: 'Error',
description: 'Failed to create stake',
variant: 'destructive'
});
}
};
const handleUnbondStake = async (stakeId: string) => {
try {
const response = await fetch(`/api/v1/stake/${stakeId}/unbond`, {
method: 'POST',
headers: { 'Authorization': `Bearer ${address}` }
});
if (response.ok) {
toast({
title: 'Unbonding Initiated',
description: 'Your stake is now in the unbonding period',
});
// Reload stakes
loadMyStakes();
} else {
throw new Error('Failed to unbond stake');
}
} catch (error) {
console.error('Error unbonding stake:', error);
toast({
title: 'Error',
description: 'Failed to unbond stake',
variant: 'destructive'
});
}
};
const handleCompleteUnbonding = async (stakeId: string) => {
try {
const response = await fetch(`/api/v1/stake/${stakeId}/complete`, {
method: 'POST',
headers: { 'Authorization': `Bearer ${address}` }
});
if (response.ok) {
const result = await response.json();
toast({
title: 'Unbonding Completed',
description: `Received ${result.total_amount + result.total_rewards} AITBC`,
});
// Reload stakes and rewards
loadMyStakes();
loadMyRewards();
} else {
throw new Error('Failed to complete unbonding');
}
} catch (error) {
console.error('Error completing unbonding:', error);
toast({
title: 'Error',
description: 'Failed to complete unbonding',
variant: 'destructive'
});
}
};
const getTierColor = (tier: string) => {
const colors = {
bronze: 'bg-orange-100 text-orange-800 border-orange-200',
silver: 'bg-gray-100 text-gray-800 border-gray-200',
gold: 'bg-yellow-100 text-yellow-800 border-yellow-200',
platinum: 'bg-purple-100 text-purple-800 border-purple-200',
diamond: 'bg-blue-100 text-blue-800 border-blue-200'
};
return colors[tier as keyof typeof colors] || colors.bronze;
};
const getStatusColor = (status: string) => {
const colors = {
active: 'bg-green-100 text-green-800',
unbonding: 'bg-yellow-100 text-yellow-800',
completed: 'bg-blue-100 text-blue-800',
slashed: 'bg-red-100 text-red-800'
};
return colors[status as keyof typeof colors] || colors.active;
};
const getTimeRemaining = (endTime: string) => {
const endDate = new Date(endTime);
const now = new Date();
const timeRemaining = endDate.getTime() - now.getTime();
if (timeRemaining <= 0) return 'Expired';
return formatDistanceToNow(endDate, { addSuffix: true });
};
const calculateAPY = (agent: AgentMetrics, lockPeriod: number) => {
const baseAPY = 5.0;
const tierMultipliers = {
bronze: 1.0,
silver: 1.2,
gold: 1.5,
platinum: 2.0,
diamond: 3.0
};
const lockMultipliers = {
30: 1.1,
90: 1.25,
180: 1.5,
365: 2.0
};
const tierMultiplier = tierMultipliers[agent.current_tier as keyof typeof tierMultipliers];
const lockMultiplier = lockMultipliers[lockPeriod as keyof typeof lockMultipliers] || 1.0;
const apy = baseAPY * tierMultiplier * lockMultiplier;
return Math.min(apy, 20.0); // Cap at 20%
};
const getRiskLevel = (agent: AgentMetrics) => {
if (agent.success_rate >= 90 && agent.average_accuracy >= 90) return 'low';
if (agent.success_rate >= 70 && agent.average_accuracy >= 70) return 'medium';
return 'high';
};
const getRiskColor = (risk: string) => {
const colors = {
low: 'text-green-600',
medium: 'text-yellow-600',
high: 'text-red-600'
};
return colors[risk as keyof typeof colors] || colors.medium;
};
if (loading) {
return (
<div className="container mx-auto py-8">
<div className="flex items-center justify-center h-64">
<div className="animate-spin rounded-full h-8 w-8 border-b-2 border-blue-600"></div>
</div>
</div>
);
}
return (
<div className="container mx-auto py-8 space-y-6">
{/* Header */}
<div className="flex justify-between items-center">
<div>
<h1 className="text-3xl font-bold">Staking Dashboard</h1>
<p className="text-muted-foreground">
Stake AITBC tokens on AI agents and earn rewards based on performance
</p>
</div>
{isConnected && (
<Button onClick={() => setShowCreateStakeModal(true)}>
Create Stake
</Button>
)}
</div>
{/* Stats Cards */}
<div className="grid grid-cols-1 md:grid-cols-4 gap-4">
<Card>
<CardContent className="p-4">
<div className="flex items-center justify-between">
<div>
<p className="text-sm font-medium text-muted-foreground">Total Staked</p>
<p className="text-2xl font-bold">{totalStaked.toLocaleString()}</p>
<p className="text-xs text-muted-foreground">AITBC</p>
</div>
<Coins className="h-8 w-8 text-blue-600" />
</div>
</CardContent>
</Card>
<Card>
<CardContent className="p-4">
<div className="flex items-center justify-between">
<div>
<p className="text-sm font-medium text-muted-foreground">Total Rewards</p>
<p className="text-2xl font-bold">{totalRewards.toLocaleString()}</p>
<p className="text-xs text-muted-foreground">AITBC</p>
</div>
<TrendingUp className="h-8 w-8 text-green-600" />
</div>
</CardContent>
</Card>
<Card>
<CardContent className="p-4">
<div className="flex items-center justify-between">
<div>
<p className="text-sm font-medium text-muted-foreground">Active Stakes</p>
<p className="text-2xl font-bold">{stakes.filter(s => s.status === 'active').length}</p>
<p className="text-xs text-muted-foreground">Positions</p>
</div>
<Shield className="h-8 w-8 text-purple-600" />
</div>
</CardContent>
</Card>
<Card>
<CardContent className="p-4">
<div className="flex items-center justify-between">
<div>
<p className="text-sm font-medium text-muted-foreground">Average APY</p>
<p className="text-2xl font-bold">
{stakes.length > 0
? (stakes.reduce((sum, s) => sum + s.current_apy, 0) / stakes.length).toFixed(1)
: '0.0'
}%
</p>
<p className="text-xs text-muted-foreground">Annual Yield</p>
</div>
<BarChart3 className="h-8 w-8 text-orange-600" />
</div>
</CardContent>
</Card>
</div>
{/* Tabs */}
<Tabs value={activeTab} onValueChange={setActiveTab}>
<TabsList>
<TabsTrigger value="my-stakes">My Stakes</TabsTrigger>
<TabsTrigger value="agents">Available Agents</TabsTrigger>
<TabsTrigger value="pools">Staking Pools</TabsTrigger>
{isConnected && <TabsTrigger value="rewards">Rewards</TabsTrigger>}
</TabsList>
{/* My Stakes Tab */}
<TabsContent value="my-stakes" className="space-y-4">
{!isConnected ? (
<Alert>
<Wallet className="h-4 w-4" />
<AlertDescription>
Connect your wallet to view your staking positions
</AlertDescription>
</Alert>
) : stakes.length === 0 ? (
<div className="text-center py-12">
<Shield className="h-12 w-12 text-muted-foreground mx-auto mb-4" />
<h3 className="text-lg font-semibold mb-2">No Stakes Found</h3>
<p className="text-muted-foreground mb-4">
Start staking on AI agents to earn rewards
</p>
<Button onClick={() => setShowCreateStakeModal(true)}>
Create Your First Stake
</Button>
</div>
) : (
<div className="space-y-4">
{stakes.map((stake) => (
<Card key={stake.stake_id}>
<CardHeader>
<div className="flex justify-between items-start">
<div className="space-y-2">
<CardTitle className="text-lg">
{stake.agent_wallet.slice(0, 8)}...{stake.agent_wallet.slice(-6)}
</CardTitle>
<div className="flex gap-2">
<Badge className={getTierColor(stake.agent_tier)}>
{stake.agent_tier.charAt(0).toUpperCase() + stake.agent_tier.slice(1)}
</Badge>
<Badge className={getStatusColor(stake.status)}>
{stake.status.charAt(0).toUpperCase() + stake.status.slice(1)}
</Badge>
{stake.auto_compound && (
<Badge variant="secondary">
<Zap className="h-3 w-3 mr-1" />
Auto-Compound
</Badge>
)}
</div>
</div>
<div className="text-right">
<p className="text-2xl font-bold text-blue-600">
{stake.amount.toLocaleString()}
</p>
<p className="text-sm text-muted-foreground">AITBC</p>
<p className="text-sm font-medium text-green-600">
{stake.current_apy.toFixed(1)}% APY
</p>
</div>
</div>
</CardHeader>
<CardContent className="space-y-4">
<div className="grid grid-cols-2 md:grid-cols-4 gap-4">
<div>
<p className="text-sm text-muted-foreground">Lock Period</p>
<p className="font-medium">{stake.lock_period} days</p>
</div>
<div>
<p className="text-sm text-muted-foreground">Time Remaining</p>
<p className="font-medium">{getTimeRemaining(stake.end_time)}</p>
</div>
<div>
<p className="text-sm text-muted-foreground">Accumulated Rewards</p>
<p className="font-medium text-green-600">
{stake.accumulated_rewards.toFixed(2)} AITBC
</p>
</div>
<div>
<p className="text-sm text-muted-foreground">Performance Multiplier</p>
<p className="font-medium">{stake.performance_multiplier}x</p>
</div>
</div>
{/* Progress bar for lock period */}
<div>
<div className="flex justify-between text-sm mb-1">
<span className="text-muted-foreground">Lock Progress</span>
<span className="font-medium">
{Math.max(0, 100 - ((new Date(stake.end_time).getTime() - Date.now()) / (stake.lock_period * 24 * 60 * 60 * 1000) * 100)).toFixed(1)}%
</span>
</div>
<Progress
value={Math.max(0, 100 - ((new Date(stake.end_time).getTime() - Date.now()) / (stake.lock_period * 24 * 60 * 60 * 1000) * 100))}
className="h-2"
/>
</div>
</CardContent>
<CardFooter className="flex gap-2">
{stake.status === 'active' && new Date(stake.end_time) <= new Date() && (
<Button
variant="outline"
onClick={() => handleUnbondStake(stake.stake_id)}
>
<Clock className="h-4 w-4 mr-2" />
Initiate Unbonding
</Button>
)}
{stake.status === 'unbonding' && (
<Button
onClick={() => handleCompleteUnbonding(stake.stake_id)}
>
<CheckCircle className="h-4 w-4 mr-2" />
Complete Unbonding
</Button>
)}
<Button variant="outline" size="sm">
View Details
</Button>
</CardFooter>
</Card>
))}
</div>
)}
</TabsContent>
{/* Available Agents Tab */}
<TabsContent value="agents" className="space-y-4">
<div className="grid grid-cols-1 md:grid-cols-2 lg:grid-cols-3 gap-6">
{supportedAgents.map((agent) => (
<Card key={agent.agent_wallet} className="hover:shadow-lg transition-shadow">
<CardHeader>
<div className="flex justify-between items-start">
<div className="space-y-2">
<CardTitle className="text-lg">
{agent.agent_wallet.slice(0, 8)}...{agent.agent_wallet.slice(-6)}
</CardTitle>
<div className="flex gap-2">
<Badge className={getTierColor(agent.current_tier)}>
{agent.current_tier.charAt(0).toUpperCase() + agent.current_tier.slice(1)}
</Badge>
<Badge className={getRiskColor(getRiskLevel(agent))}>
{getRiskLevel(agent).toUpperCase()} RISK
</Badge>
</div>
</div>
<div className="text-right">
<p className="text-2xl font-bold text-blue-600">
{calculateAPY(agent, 30).toFixed(1)}%
</p>
<p className="text-sm text-muted-foreground">APY</p>
</div>
</div>
</CardHeader>
<CardContent className="space-y-4">
<div className="grid grid-cols-2 gap-4">
<div>
<p className="text-sm text-muted-foreground">Total Staked</p>
<p className="font-medium">{agent.total_staked.toLocaleString()}</p>
</div>
<div>
<p className="text-sm text-muted-foreground">Stakers</p>
<p className="font-medium">{agent.staker_count}</p>
</div>
<div>
<p className="text-sm text-muted-foreground">Accuracy</p>
<p className="font-medium">{agent.average_accuracy.toFixed(1)}%</p>
</div>
<div>
<p className="text-sm text-muted-foreground">Success Rate</p>
<p className="font-medium">{agent.success_rate.toFixed(1)}%</p>
</div>
</div>
<div className="space-y-2">
<div className="flex justify-between text-sm">
<span className="text-muted-foreground">Total Submissions</span>
<span className="font-medium">{agent.total_submissions}</span>
</div>
<div className="flex justify-between text-sm">
<span className="text-muted-foreground">Rewards Distributed</span>
<span className="font-medium text-green-600">
{agent.total_rewards_distributed.toLocaleString()} AITBC
</span>
</div>
</div>
{/* Performance indicators */}
<div className="flex items-center gap-2 text-sm">
<Activity className="h-4 w-4 text-blue-600" />
<span>Performance Score: {agent.tier_score.toFixed(1)}</span>
</div>
</CardContent>
<CardFooter>
<Button
className="w-full"
onClick={() => {
setSelectedAgent(agent);
setStakeForm(prev => ({ ...prev, agent_wallet: agent.agent_wallet }));
setShowCreateStakeModal(true);
}}
disabled={!isConnected}
>
{isConnected ? 'Stake on Agent' : 'Connect Wallet'}
</Button>
</CardFooter>
</Card>
))}
</div>
</TabsContent>
{/* Staking Pools Tab */}
<TabsContent value="pools" className="space-y-4">
<Table>
<TableHeader>
<TableRow>
<TableHead>Agent</TableHead>
<TableHead>Total Staked</TableHead>
<TableHead>Pool APY</TableHead>
<TableHead>Stakers</TableHead>
<TableHead>Total Rewards</TableHead>
<TableHead>Performance</TableHead>
<TableHead>Volatility</TableHead>
</TableRow>
</TableHeader>
<TableBody>
{stakingPools.map((pool) => (
<TableRow key={pool.agent_wallet}>
<TableCell className="font-mono">
{pool.agent_wallet.slice(0, 8)}...{pool.agent_wallet.slice(-6)}
</TableCell>
<TableCell>{pool.total_staked.toLocaleString()} AITBC</TableCell>
<TableCell>
<span className="text-green-600 font-medium">{pool.pool_apy.toFixed(1)}%</span>
</TableCell>
<TableCell>{pool.staker_count}</TableCell>
<TableCell className="text-green-600">
{pool.total_rewards.toLocaleString()} AITBC
</TableCell>
<TableCell>
<div className="flex items-center gap-2">
<Progress value={pool.pool_performance_score} className="w-16 h-2" />
<span className="text-sm">{pool.pool_performance_score.toFixed(0)}</span>
</div>
</TableCell>
<TableCell>
<Badge variant={pool.volatility_score < 30 ? 'secondary' : pool.volatility_score < 70 ? 'default' : 'destructive'}>
{pool.volatility_score < 30 ? 'Low' : pool.volatility_score < 70 ? 'Medium' : 'High'}
</Badge>
</TableCell>
</TableRow>
))}
</TableBody>
</Table>
</TabsContent>
{/* Rewards Tab */}
<TabsContent value="rewards" className="space-y-4">
{!isConnected ? (
<Alert>
<Wallet className="h-4 w-4" />
<AlertDescription>
Connect your wallet to view your rewards
</AlertDescription>
</Alert>
) : (
<div className="grid grid-cols-1 md:grid-cols-2 gap-6">
<Card>
<CardHeader>
<CardTitle className="flex items-center gap-2">
<Coins className="h-5 w-5" />
Reward Summary
</CardTitle>
</CardHeader>
<CardContent className="space-y-4">
<div className="space-y-2">
<div className="flex justify-between">
<span className="text-muted-foreground">Total Earned</span>
<span className="font-bold text-green-600">
{totalRewards.toLocaleString()} AITBC
</span>
</div>
<div className="flex justify-between">
<span className="text-muted-foreground">Pending Rewards</span>
<span className="font-bold">
{stakes.reduce((sum, s) => sum + s.accumulated_rewards, 0).toLocaleString()} AITBC
</span>
</div>
<div className="flex justify-between">
<span className="text-muted-foreground">Average APY</span>
<span className="font-bold">
{stakes.length > 0
? (stakes.reduce((sum, s) => sum + s.current_apy, 0) / stakes.length).toFixed(1)
: '0.0'
}%
</span>
</div>
</div>
<Separator />
<Button className="w-full">
Claim All Rewards
</Button>
</CardContent>
</Card>
<Card>
<CardHeader>
<CardTitle className="flex items-center gap-2">
<PieChart className="h-5 w-5" />
Reward History
</CardTitle>
</CardHeader>
<CardContent>
<div className="text-center py-8 text-muted-foreground">
<Info className="h-8 w-8 mx-auto mb-2" />
<p>Reward history will be available soon</p>
</div>
</CardContent>
</Card>
</div>
)}
</TabsContent>
</Tabs>
{/* Create Stake Modal */}
{showCreateStakeModal && (
<div className="fixed inset-0 bg-black/50 flex items-center justify-center p-4 z-50">
<Card className="max-w-md w-full">
<CardHeader>
<CardTitle>Create New Stake</CardTitle>
<CardDescription>
Stake AITBC tokens on an AI agent to earn rewards
</CardDescription>
</CardHeader>
<CardContent className="space-y-4">
<div>
<label className="text-sm font-medium">Agent</label>
<Select
value={stakeForm.agent_wallet}
onValueChange={(value) => setStakeForm(prev => ({ ...prev, agent_wallet: value }))}
>
<SelectTrigger>
<SelectValue placeholder="Select an agent" />
</SelectTrigger>
<SelectContent>
{supportedAgents.map((agent) => (
<SelectItem key={agent.agent_wallet} value={agent.agent_wallet}>
<div className="flex items-center justify-between w-full">
<span>{agent.agent_wallet.slice(0, 8)}...{agent.agent_wallet.slice(-6)}</span>
<span className="text-green-600">{calculateAPY(agent, parseInt(stakeForm.lock_period)).toFixed(1)}% APY</span>
</div>
</SelectItem>
))}
</SelectContent>
</Select>
</div>
<div>
<label className="text-sm font-medium">Amount (AITBC)</label>
<Input
type="number"
placeholder="100.0"
value={stakeForm.amount}
onChange={(e) => setStakeForm(prev => ({ ...prev, amount: e.target.value }))}
min="100"
max="100000"
/>
<p className="text-xs text-muted-foreground mt-1">
Min: 100 AITBC, Max: 100,000 AITBC
</p>
</div>
<div>
<label className="text-sm font-medium">Lock Period</label>
<Select
value={stakeForm.lock_period}
onValueChange={(value) => setStakeForm(prev => ({ ...prev, lock_period: value }))}
>
<SelectTrigger>
<SelectValue />
</SelectTrigger>
<SelectContent>
<SelectItem value="30">30 days (1.1x multiplier)</SelectItem>
<SelectItem value="90">90 days (1.25x multiplier)</SelectItem>
<SelectItem value="180">180 days (1.5x multiplier)</SelectItem>
<SelectItem value="365">365 days (2.0x multiplier)</SelectItem>
</SelectContent>
</Select>
</div>
{selectedAgent && (
<div className="bg-muted p-3 rounded">
<h4 className="font-medium mb-2">Estimated Returns</h4>
<div className="space-y-1 text-sm">
<div className="flex justify-between">
<span>Base APY:</span>
<span>5.0%</span>
</div>
<div className="flex justify-between">
<span>Tier Multiplier:</span>
<span>{selectedAgent.current_tier} tier</span>
</div>
<div className="flex justify-between">
<span>Lock Multiplier:</span>
<span>{stakeForm.lock_period === '30' ? '1.1x' : stakeForm.lock_period === '90' ? '1.25x' : stakeForm.lock_period === '180' ? '1.5x' : '2.0x'}</span>
</div>
<div className="flex justify-between font-bold">
<span>Estimated APY:</span>
<span className="text-green-600">
{calculateAPY(selectedAgent, parseInt(stakeForm.lock_period)).toFixed(1)}%
</span>
</div>
</div>
</div>
)}
<div className="flex items-center space-x-2">
<input
type="checkbox"
id="auto-compound"
checked={stakeForm.auto_compound}
onChange={(e) => setStakeForm(prev => ({ ...prev, auto_compound: e.target.checked }))}
/>
<label htmlFor="auto-compound" className="text-sm">
Enable auto-compounding
</label>
</div>
</CardContent>
<CardFooter className="flex gap-2">
<Button
className="flex-1"
onClick={handleCreateStake}
disabled={!stakeForm.agent_wallet || !stakeForm.amount || parseFloat(stakeForm.amount) < 100}
>
Create Stake
</Button>
<Button variant="outline" onClick={() => setShowCreateStakeModal(false)}>
Cancel
</Button>
</CardFooter>
</Card>
</div>
)}
</div>
);
};
export default StakingDashboard;