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:
64
apps/marketplace/src/App.tsx
Normal file
64
apps/marketplace/src/App.tsx
Normal file
@@ -0,0 +1,64 @@
|
||||
import React from 'react'
|
||||
import { BrowserRouter as Router, Routes, Route, Link, Navigate } from 'react-router-dom'
|
||||
import { Button } from '@/components/ui/button'
|
||||
import BountyBoard from '@/pages/BountyBoard'
|
||||
import StakingDashboard from '@/pages/StakingDashboard'
|
||||
import DeveloperLeaderboard from '@/pages/DeveloperLeaderboard'
|
||||
import EcosystemDashboard from '@/pages/EcosystemDashboard'
|
||||
|
||||
function App() {
|
||||
return (
|
||||
<Router>
|
||||
<div className="min-h-screen bg-background">
|
||||
{/* Navigation Header */}
|
||||
<header className="border-b bg-card">
|
||||
<div className="container mx-auto px-4 py-4">
|
||||
<div className="flex items-center justify-between">
|
||||
<div className="flex items-center space-x-4">
|
||||
<h1 className="text-2xl font-bold text-primary">AITBC</h1>
|
||||
<span className="text-muted-foreground">Developer Ecosystem</span>
|
||||
</div>
|
||||
<nav className="flex items-center space-x-2">
|
||||
<Link to="/bounties">
|
||||
<Button variant="ghost">Bounty Board</Button>
|
||||
</Link>
|
||||
<Link to="/staking">
|
||||
<Button variant="ghost">Staking</Button>
|
||||
</Link>
|
||||
<Link to="/leaderboard">
|
||||
<Button variant="ghost">Leaderboard</Button>
|
||||
</Link>
|
||||
<Link to="/ecosystem">
|
||||
<Button variant="ghost">Ecosystem</Button>
|
||||
</Link>
|
||||
</nav>
|
||||
</div>
|
||||
</div>
|
||||
</header>
|
||||
|
||||
{/* Main Content */}
|
||||
<main className="container mx-auto">
|
||||
<Routes>
|
||||
<Route path="/" element={<Navigate to="/bounties" replace />} />
|
||||
<Route path="/bounties" element={<BountyBoard />} />
|
||||
<Route path="/staking" element={<StakingDashboard />} />
|
||||
<Route path="/leaderboard" element={<DeveloperLeaderboard />} />
|
||||
<Route path="/ecosystem" element={<EcosystemDashboard />} />
|
||||
</Routes>
|
||||
</main>
|
||||
|
||||
{/* Footer */}
|
||||
<footer className="border-t bg-card mt-8">
|
||||
<div className="container mx-auto px-4 py-6">
|
||||
<div className="text-center text-sm text-muted-foreground">
|
||||
<p>© 2024 AITBC Developer Ecosystem & DAO Grants System</p>
|
||||
<p className="mt-1">Built with React, TypeScript, and Tailwind CSS</p>
|
||||
</div>
|
||||
</div>
|
||||
</footer>
|
||||
</div>
|
||||
</Router>
|
||||
)
|
||||
}
|
||||
|
||||
export default App
|
||||
1306
apps/marketplace/src/components/AdvancedLearning.tsx
Normal file
1306
apps/marketplace/src/components/AdvancedLearning.tsx
Normal file
File diff suppressed because it is too large
Load Diff
875
apps/marketplace/src/components/AgentAutonomy.tsx
Normal file
875
apps/marketplace/src/components/AgentAutonomy.tsx
Normal file
@@ -0,0 +1,875 @@
|
||||
import React, { useState, useEffect } from 'react';
|
||||
import { Card, CardHeader, CardTitle, CardDescription, CardContent, CardFooter } from './ui/card';
|
||||
import { Button } from './ui/button';
|
||||
import { Badge } from './ui/badge';
|
||||
import { Input } from './ui/input';
|
||||
import { Select, SelectContent, SelectItem, SelectTrigger, SelectValue } from './ui/select';
|
||||
import { Tabs, TabsContent, TabsList, TabsTrigger } from './ui/tabs';
|
||||
import { Alert, AlertDescription, AlertTitle } from './ui/alert';
|
||||
import { Progress } from './ui/progress';
|
||||
import { Separator } from './ui/separator';
|
||||
import {
|
||||
Brain,
|
||||
Zap,
|
||||
Target,
|
||||
Settings,
|
||||
Play,
|
||||
Pause,
|
||||
RefreshCw,
|
||||
Search,
|
||||
Filter,
|
||||
Plus,
|
||||
Eye,
|
||||
MoreHorizontal,
|
||||
Clock,
|
||||
Calendar,
|
||||
Users,
|
||||
Network,
|
||||
Shield,
|
||||
CheckCircle,
|
||||
AlertCircle,
|
||||
BarChart3,
|
||||
Activity,
|
||||
TrendingUp,
|
||||
Award,
|
||||
Star,
|
||||
GitBranch,
|
||||
Layers,
|
||||
Cpu,
|
||||
Battery,
|
||||
Gauge,
|
||||
LineChart,
|
||||
PieChart,
|
||||
ArrowRight,
|
||||
ArrowUp,
|
||||
ArrowDown,
|
||||
ChevronRight,
|
||||
ChevronDown,
|
||||
Lightbulb,
|
||||
Rocket,
|
||||
BrainCircuit,
|
||||
Sparkles,
|
||||
ZapOff,
|
||||
Power,
|
||||
PowerOff,
|
||||
Settings2,
|
||||
Sliders,
|
||||
ToggleLeft,
|
||||
ToggleRight,
|
||||
Lock,
|
||||
Unlock,
|
||||
Key,
|
||||
EyeOff,
|
||||
Volume2,
|
||||
VolumeX,
|
||||
Wifi,
|
||||
WifiOff,
|
||||
Database,
|
||||
HardDrive,
|
||||
MemoryStick,
|
||||
Cloud,
|
||||
Download,
|
||||
Upload,
|
||||
Copy,
|
||||
Share2,
|
||||
Trash2,
|
||||
Edit,
|
||||
Save,
|
||||
FileText,
|
||||
Folder,
|
||||
FolderOpen,
|
||||
Tag,
|
||||
Hash,
|
||||
AtSign
|
||||
} from 'lucide-react';
|
||||
import { useToast } from '@/hooks/use-toast';
|
||||
import { useWallet } from '@/hooks/use-wallet';
|
||||
|
||||
interface AutonomousAgent {
|
||||
id: string;
|
||||
name: string;
|
||||
type: 'trading' | 'research' | 'development' | 'analysis' | 'creative';
|
||||
status: 'active' | 'paused' | 'learning' | 'optimizing' | 'offline';
|
||||
autonomy: number; // 0-100
|
||||
performance: number; // 0-100
|
||||
efficiency: number; // 0-100
|
||||
goals: Array<{
|
||||
id: string;
|
||||
title: string;
|
||||
description: string;
|
||||
priority: 'low' | 'medium' | 'high' | 'critical';
|
||||
progress: number;
|
||||
status: 'pending' | 'in_progress' | 'completed' | 'failed';
|
||||
deadline?: string;
|
||||
createdAt: string;
|
||||
updatedAt: string;
|
||||
}>;
|
||||
capabilities: Array<{
|
||||
name: string;
|
||||
enabled: boolean;
|
||||
performance: number;
|
||||
lastUsed: string;
|
||||
}>;
|
||||
resources: {
|
||||
cpu: number;
|
||||
memory: number;
|
||||
storage: number;
|
||||
network: number;
|
||||
cost: number;
|
||||
};
|
||||
learning: {
|
||||
models: number;
|
||||
accuracy: number;
|
||||
trainingTime: number;
|
||||
lastUpdate: string;
|
||||
};
|
||||
metadata: {
|
||||
description: string;
|
||||
creator: string;
|
||||
createdAt: string;
|
||||
updatedAt: string;
|
||||
tags: string[];
|
||||
};
|
||||
}
|
||||
|
||||
interface AutonomyStats {
|
||||
totalAgents: number;
|
||||
activeAgents: number;
|
||||
averageAutonomy: number;
|
||||
averagePerformance: number;
|
||||
totalGoals: number;
|
||||
completedGoals: number;
|
||||
successRate: number;
|
||||
totalCost: number;
|
||||
costSavings: number;
|
||||
agentsByType: Record<string, number>;
|
||||
performanceMetrics: {
|
||||
autonomy: number;
|
||||
performance: number;
|
||||
efficiency: number;
|
||||
reliability: number;
|
||||
};
|
||||
monthlyActivity: Array<{
|
||||
month: string;
|
||||
agents: number;
|
||||
goals: number;
|
||||
autonomy: number;
|
||||
performance: number;
|
||||
}>;
|
||||
}
|
||||
|
||||
const AgentAutonomy: React.FC = () => {
|
||||
const { toast } = useToast();
|
||||
const { isConnected, address } = useWallet();
|
||||
|
||||
const [activeTab, setActiveTab] = useState('agents');
|
||||
const [loading, setLoading] = useState(true);
|
||||
const [agents, setAgents] = useState<AutonomousAgent[]>([]);
|
||||
const [selectedAgent, setSelectedAgent] = useState<AutonomousAgent | null>(null);
|
||||
const [stats, setStats] = useState<AutonomyStats | null>(null);
|
||||
|
||||
// Form states
|
||||
const [newAgentName, setNewAgentName] = useState('');
|
||||
const [newAgentType, setNewAgentType] = useState('trading');
|
||||
const [newAgentDescription, setNewAgentDescription] = useState('');
|
||||
const [searchQuery, setSearchQuery] = useState('');
|
||||
const [filterType, setFilterType] = useState('all');
|
||||
const [filterStatus, setFilterStatus] = useState('all');
|
||||
|
||||
// Mock data
|
||||
const mockAgents: AutonomousAgent[] = [
|
||||
{
|
||||
id: 'agent_001',
|
||||
name: 'QuantumTrader Pro',
|
||||
type: 'trading',
|
||||
status: 'active',
|
||||
autonomy: 92,
|
||||
performance: 87,
|
||||
efficiency: 94,
|
||||
goals: [
|
||||
{
|
||||
id: 'goal_001',
|
||||
title: 'Maximize Trading Profits',
|
||||
description: 'Achieve 15% monthly return through automated trading',
|
||||
priority: 'high',
|
||||
progress: 78,
|
||||
status: 'in_progress',
|
||||
deadline: '2024-03-31T23:59:59Z',
|
||||
createdAt: '2024-02-01T00:00:00Z',
|
||||
updatedAt: '2024-02-27T10:15:00Z'
|
||||
},
|
||||
{
|
||||
id: 'goal_002',
|
||||
title: 'Risk Management',
|
||||
description: 'Maintain maximum drawdown below 5%',
|
||||
priority: 'critical',
|
||||
progress: 95,
|
||||
status: 'in_progress',
|
||||
deadline: '2024-02-28T23:59:59Z',
|
||||
createdAt: '2024-02-01T00:00:00Z',
|
||||
updatedAt: '2024-02-27T10:15:00Z'
|
||||
}
|
||||
],
|
||||
capabilities: [
|
||||
{ name: 'Market Analysis', enabled: true, performance: 89, lastUsed: '2024-02-27T09:30:00Z' },
|
||||
{ name: 'Risk Assessment', enabled: true, performance: 94, lastUsed: '2024-02-27T09:45:00Z' },
|
||||
{ name: 'Order Execution', enabled: true, performance: 92, lastUsed: '2024-02-27T10:00:00Z' }
|
||||
],
|
||||
resources: {
|
||||
cpu: 75,
|
||||
memory: 68,
|
||||
storage: 45,
|
||||
network: 25,
|
||||
cost: 125.50
|
||||
},
|
||||
learning: {
|
||||
models: 3,
|
||||
accuracy: 87.5,
|
||||
trainingTime: 156,
|
||||
lastUpdate: '2024-02-27T08:00:00Z'
|
||||
},
|
||||
metadata: {
|
||||
description: 'Autonomous trading agent with advanced risk management',
|
||||
creator: address || '0x1234...5678',
|
||||
createdAt: '2024-02-01T00:00:00Z',
|
||||
updatedAt: '2024-02-27T10:15:00Z',
|
||||
tags: ['trading', 'autonomous', 'risk-management', 'profit-maximization']
|
||||
}
|
||||
},
|
||||
{
|
||||
id: 'agent_002',
|
||||
name: 'ResearchBot Alpha',
|
||||
type: 'research',
|
||||
status: 'learning',
|
||||
autonomy: 78,
|
||||
performance: 82,
|
||||
efficiency: 85,
|
||||
goals: [
|
||||
{
|
||||
id: 'goal_003',
|
||||
title: 'Data Collection',
|
||||
description: 'Collect and analyze 10GB of research data',
|
||||
priority: 'medium',
|
||||
progress: 65,
|
||||
status: 'in_progress',
|
||||
createdAt: '2024-02-15T00:00:00Z',
|
||||
updatedAt: '2024-02-27T14:30:00Z'
|
||||
}
|
||||
],
|
||||
capabilities: [
|
||||
{ name: 'Data Mining', enabled: true, performance: 85, lastUsed: '2024-02-27T14:00:00Z' },
|
||||
{ name: 'Pattern Recognition', enabled: true, performance: 79, lastUsed: '2024-02-27T14:15:00Z' }
|
||||
],
|
||||
resources: {
|
||||
cpu: 82,
|
||||
memory: 74,
|
||||
storage: 89,
|
||||
network: 67,
|
||||
cost: 89.25
|
||||
},
|
||||
learning: {
|
||||
models: 5,
|
||||
accuracy: 82.3,
|
||||
trainingTime: 234,
|
||||
lastUpdate: '2024-02-27T13:45:00Z'
|
||||
},
|
||||
metadata: {
|
||||
description: 'Research agent focused on data analysis and pattern discovery',
|
||||
creator: '0x8765...4321',
|
||||
createdAt: '2024-02-15T00:00:00Z',
|
||||
updatedAt: '2024-02-27T14:30:00Z',
|
||||
tags: ['research', 'data-analysis', 'pattern-recognition']
|
||||
}
|
||||
}
|
||||
];
|
||||
|
||||
const mockStats: AutonomyStats = {
|
||||
totalAgents: 8,
|
||||
activeAgents: 5,
|
||||
averageAutonomy: 85.2,
|
||||
averagePerformance: 83.7,
|
||||
totalGoals: 24,
|
||||
completedGoals: 18,
|
||||
successRate: 75.0,
|
||||
totalCost: 892.75,
|
||||
costSavings: 234.50,
|
||||
agentsByType: {
|
||||
trading: 3,
|
||||
research: 2,
|
||||
development: 1,
|
||||
analysis: 1,
|
||||
creative: 1
|
||||
},
|
||||
performanceMetrics: {
|
||||
autonomy: 85.2,
|
||||
performance: 83.7,
|
||||
efficiency: 87.9,
|
||||
reliability: 91.2
|
||||
},
|
||||
monthlyActivity: [
|
||||
{ month: 'Jan', agents: 2, goals: 6, autonomy: 78.5, performance: 81.2 },
|
||||
{ month: 'Feb', agents: 5, goals: 12, autonomy: 85.2, performance: 83.7 },
|
||||
{ month: 'Mar', agents: 6, goals: 15, autonomy: 87.9, performance: 86.4 },
|
||||
{ month: 'Apr', agents: 8, goals: 18, autonomy: 90.1, performance: 88.9 }
|
||||
]
|
||||
};
|
||||
|
||||
useEffect(() => {
|
||||
setTimeout(() => {
|
||||
setAgents(mockAgents);
|
||||
setStats(mockStats);
|
||||
setLoading(false);
|
||||
}, 1000);
|
||||
}, [address]);
|
||||
|
||||
const handleCreateAgent = async () => {
|
||||
if (!isConnected) {
|
||||
toast({
|
||||
title: "Wallet Not Connected",
|
||||
description: "Please connect your wallet to create an agent",
|
||||
variant: "destructive"
|
||||
});
|
||||
return;
|
||||
}
|
||||
|
||||
try {
|
||||
toast({
|
||||
title: "Creating Agent",
|
||||
description: "Setting up your autonomous agent...",
|
||||
variant: "default"
|
||||
});
|
||||
|
||||
await new Promise(resolve => setTimeout(resolve, 2000));
|
||||
|
||||
const newAgent: AutonomousAgent = {
|
||||
id: `agent_${Date.now()}`,
|
||||
name: newAgentName,
|
||||
type: newAgentType as any,
|
||||
status: 'offline',
|
||||
autonomy: 0,
|
||||
performance: 0,
|
||||
efficiency: 0,
|
||||
goals: [],
|
||||
capabilities: [],
|
||||
resources: { cpu: 0, memory: 0, storage: 0, network: 0, cost: 0 },
|
||||
learning: { models: 0, accuracy: 0, trainingTime: 0, lastUpdate: '' },
|
||||
metadata: {
|
||||
description: newAgentDescription,
|
||||
creator: address || '0x1234...5678',
|
||||
createdAt: new Date().toISOString(),
|
||||
updatedAt: new Date().toISOString(),
|
||||
tags: []
|
||||
}
|
||||
};
|
||||
|
||||
setAgents([newAgent, ...agents]);
|
||||
setNewAgentName('');
|
||||
setNewAgentType('trading');
|
||||
setNewAgentDescription('');
|
||||
|
||||
toast({
|
||||
title: "Agent Created",
|
||||
description: "Your autonomous agent has been created successfully",
|
||||
variant: "default"
|
||||
});
|
||||
} catch (error) {
|
||||
toast({
|
||||
title: "Creation Failed",
|
||||
description: "There was an error creating your agent",
|
||||
variant: "destructive"
|
||||
});
|
||||
}
|
||||
};
|
||||
|
||||
const getStatusColor = (status: string) => {
|
||||
switch (status) {
|
||||
case 'active': return 'bg-green-500';
|
||||
case 'learning': return 'bg-blue-500';
|
||||
case 'optimizing': return 'bg-purple-500';
|
||||
case 'paused': return 'bg-yellow-500';
|
||||
case 'offline': return 'bg-gray-500';
|
||||
default: return 'bg-gray-400';
|
||||
}
|
||||
};
|
||||
|
||||
const getTypeIcon = (type: string) => {
|
||||
switch (type) {
|
||||
case 'trading': return <TrendingUp className="w-4 h-4" />;
|
||||
case 'research': return <Brain className="w-4 h-4" />;
|
||||
case 'development': return <GitBranch className="w-4 h-4" />;
|
||||
case 'analysis': return <BarChart3 className="w-4 h-4" />;
|
||||
case 'creative': return <Sparkles className="w-4 h-4" />;
|
||||
default: return <Brain className="w-4 h-4" />;
|
||||
}
|
||||
};
|
||||
|
||||
const getAutonomyColor = (value: number) => {
|
||||
if (value >= 80) return 'text-green-600';
|
||||
if (value >= 60) return 'text-blue-600';
|
||||
if (value >= 40) return 'text-yellow-600';
|
||||
return 'text-red-600';
|
||||
};
|
||||
|
||||
const filteredAgents = agents.filter(agent => {
|
||||
if (searchQuery) {
|
||||
const query = searchQuery.toLowerCase();
|
||||
return agent.name.toLowerCase().includes(query) ||
|
||||
agent.metadata.description.toLowerCase().includes(query);
|
||||
}
|
||||
if (filterType !== 'all') return agent.type === filterType;
|
||||
if (filterStatus !== 'all') return agent.status === filterStatus;
|
||||
return true;
|
||||
});
|
||||
|
||||
if (loading) {
|
||||
return (
|
||||
<div className="container mx-auto p-6 space-y-6">
|
||||
<div className="text-center py-8">
|
||||
<div className="animate-spin rounded-full h-8 w-8 border-b-2 border-primary mx-auto"></div>
|
||||
<p className="mt-2 text-muted-foreground">Loading agent autonomy...</p>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
return (
|
||||
<div className="container mx-auto p-6 space-y-6">
|
||||
<div className="flex items-center justify-between">
|
||||
<div>
|
||||
<h1 className="text-3xl font-bold">Agent Autonomy</h1>
|
||||
<p className="text-muted-foreground mt-2">
|
||||
Self-improving agents with goal-setting and planning capabilities
|
||||
</p>
|
||||
</div>
|
||||
<div className="flex items-center space-x-2">
|
||||
<Badge variant="outline" className="flex items-center space-x-1">
|
||||
<Brain className="w-4 h-4" />
|
||||
<span>{stats?.totalAgents} Agents</span>
|
||||
</Badge>
|
||||
<Badge variant="outline" className="flex items-center space-x-1">
|
||||
<Activity className="w-4 h-4" />
|
||||
<span>{stats?.activeAgents} Active</span>
|
||||
</Badge>
|
||||
<Badge variant="outline" className="flex items-center space-x-1">
|
||||
<Target className="w-4 h-4" />
|
||||
<span>{stats?.successRate}% Success Rate</span>
|
||||
</Badge>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<Tabs value={activeTab} onValueChange={setActiveTab}>
|
||||
<TabsList className="grid w-full grid-cols-4">
|
||||
<TabsTrigger value="agents">Agents</TabsTrigger>
|
||||
<TabsTrigger value="goals">Goals</TabsTrigger>
|
||||
<TabsTrigger value="create">Create Agent</TabsTrigger>
|
||||
<TabsTrigger value="analytics">Analytics</TabsTrigger>
|
||||
</TabsList>
|
||||
|
||||
<TabsContent value="agents" className="space-y-6">
|
||||
<Card>
|
||||
<CardHeader>
|
||||
<CardTitle className="flex items-center space-x-2">
|
||||
<Search className="w-5 h-5" />
|
||||
<span>Search & Filter</span>
|
||||
</CardTitle>
|
||||
</CardHeader>
|
||||
<CardContent>
|
||||
<div className="grid grid-cols-1 md:grid-cols-4 gap-4">
|
||||
<div className="space-y-2">
|
||||
<label className="text-sm font-medium">Search</label>
|
||||
<div className="relative">
|
||||
<Search className="absolute left-3 top-3 w-4 h-4 text-muted-foreground" />
|
||||
<Input
|
||||
placeholder="Search agents..."
|
||||
value={searchQuery}
|
||||
onChange={(e) => setSearchQuery(e.target.value)}
|
||||
className="pl-10"
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
<div className="space-y-2">
|
||||
<label className="text-sm font-medium">Type</label>
|
||||
<Select value={filterType} onValueChange={setFilterType}>
|
||||
<SelectTrigger>
|
||||
<SelectValue placeholder="All types" />
|
||||
</SelectTrigger>
|
||||
<SelectContent>
|
||||
<SelectItem value="all">All Types</SelectItem>
|
||||
<SelectItem value="trading">Trading</SelectItem>
|
||||
<SelectItem value="research">Research</SelectItem>
|
||||
<SelectItem value="development">Development</SelectItem>
|
||||
<SelectItem value="analysis">Analysis</SelectItem>
|
||||
<SelectItem value="creative">Creative</SelectItem>
|
||||
</SelectContent>
|
||||
</Select>
|
||||
</div>
|
||||
<div className="space-y-2">
|
||||
<label className="text-sm font-medium">Status</label>
|
||||
<Select value={filterStatus} onValueChange={setFilterStatus}>
|
||||
<SelectTrigger>
|
||||
<SelectValue placeholder="All statuses" />
|
||||
</SelectTrigger>
|
||||
<SelectContent>
|
||||
<SelectItem value="all">All Statuses</SelectItem>
|
||||
<SelectItem value="active">Active</SelectItem>
|
||||
<SelectItem value="learning">Learning</SelectItem>
|
||||
<SelectItem value="optimizing">Optimizing</SelectItem>
|
||||
<SelectItem value="paused">Paused</SelectItem>
|
||||
<SelectItem value="offline">Offline</SelectItem>
|
||||
</SelectContent>
|
||||
</Select>
|
||||
</div>
|
||||
<div className="space-y-2">
|
||||
<label className="text-sm font-medium">Actions</label>
|
||||
<div className="flex space-x-2">
|
||||
<Button variant="outline" size="sm">
|
||||
<Filter className="w-4 h-4 mr-2" />
|
||||
More Filters
|
||||
</Button>
|
||||
<Button variant="outline" size="sm">
|
||||
<Download className="w-4 h-4 mr-2" />
|
||||
Export
|
||||
</Button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</CardContent>
|
||||
</Card>
|
||||
|
||||
<div className="grid grid-cols-1 lg:grid-cols-2 gap-6">
|
||||
{filteredAgents.map((agent) => (
|
||||
<Card key={agent.id} className="hover:shadow-lg transition-shadow">
|
||||
<CardHeader>
|
||||
<div className="flex items-center justify-between mb-2">
|
||||
<div className="flex items-center space-x-2">
|
||||
{getTypeIcon(agent.type)}
|
||||
<Badge variant="outline">{agent.type}</Badge>
|
||||
<Badge variant={agent.status === 'active' ? 'default' : 'secondary'}>
|
||||
{agent.status}
|
||||
</Badge>
|
||||
<div className={`w-2 h-2 rounded-full ${getStatusColor(agent.status)}`}></div>
|
||||
</div>
|
||||
<Button variant="ghost" size="sm">
|
||||
<MoreHorizontal className="w-4 h-4" />
|
||||
</Button>
|
||||
</div>
|
||||
|
||||
<CardTitle className="text-lg">{agent.name}</CardTitle>
|
||||
<CardDescription className="line-clamp-2">
|
||||
{agent.metadata.description}
|
||||
</CardDescription>
|
||||
</CardHeader>
|
||||
|
||||
<CardContent>
|
||||
<div className="space-y-4">
|
||||
<div className="grid grid-cols-3 gap-4">
|
||||
<div className="text-center">
|
||||
<div className={`text-lg font-bold ${getAutonomyColor(agent.autonomy)}`}>
|
||||
{agent.autonomy}%
|
||||
</div>
|
||||
<div className="text-xs text-muted-foreground">Autonomy</div>
|
||||
</div>
|
||||
<div className="text-center">
|
||||
<div className={`text-lg font-bold ${getAutonomyColor(agent.performance)}`}>
|
||||
{agent.performance}%
|
||||
</div>
|
||||
<div className="text-xs text-muted-foreground">Performance</div>
|
||||
</div>
|
||||
<div className="text-center">
|
||||
<div className={`text-lg font-bold ${getAutonomyColor(agent.efficiency)}`}>
|
||||
{agent.efficiency}%
|
||||
</div>
|
||||
<div className="text-xs text-muted-foreground">Efficiency</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div className="space-y-2">
|
||||
<div className="flex items-center justify-between">
|
||||
<span className="text-sm font-medium">Goals Progress</span>
|
||||
<span className="text-sm text-muted-foreground">
|
||||
{agent.goals.filter(g => g.status === 'completed').length}/{agent.goals.length}
|
||||
</span>
|
||||
</div>
|
||||
<Progress
|
||||
value={(agent.goals.filter(g => g.status === 'completed').length / agent.goals.length) * 100}
|
||||
className="h-2"
|
||||
/>
|
||||
</div>
|
||||
|
||||
<div className="grid grid-cols-2 gap-4 text-sm">
|
||||
<div>
|
||||
<span className="text-muted-foreground">Learning Models:</span>
|
||||
<p className="font-medium">{agent.learning.models}</p>
|
||||
</div>
|
||||
<div>
|
||||
<span className="text-muted-foreground">Accuracy:</span>
|
||||
<p className="font-medium">{agent.learning.accuracy}%</p>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</CardContent>
|
||||
|
||||
<CardFooter>
|
||||
<div className="flex space-x-2 w-full">
|
||||
<Button size="sm" className="flex-1">
|
||||
<Eye className="w-4 h-4 mr-2" />
|
||||
View Details
|
||||
</Button>
|
||||
<Button variant="outline" size="sm">
|
||||
<Settings className="w-4 h-4 mr-2" />
|
||||
Configure
|
||||
</Button>
|
||||
</div>
|
||||
</CardFooter>
|
||||
</Card>
|
||||
))}
|
||||
</div>
|
||||
</TabsContent>
|
||||
|
||||
<TabsContent value="goals" className="space-y-6">
|
||||
<Card>
|
||||
<CardHeader>
|
||||
<CardTitle className="flex items-center space-x-2">
|
||||
<Target className="w-5 h-5" />
|
||||
<span>Agent Goals</span>
|
||||
</CardTitle>
|
||||
<CardDescription>
|
||||
Goals and objectives for autonomous agents
|
||||
</CardDescription>
|
||||
</CardHeader>
|
||||
<CardContent>
|
||||
<div className="space-y-4">
|
||||
{agents.flatMap(agent =>
|
||||
agent.goals.map(goal => (
|
||||
<div key={goal.id} className="p-4 border rounded-lg">
|
||||
<div className="flex items-center justify-between mb-2">
|
||||
<div className="flex items-center space-x-2">
|
||||
<div className={`w-3 h-3 rounded-full ${
|
||||
goal.status === 'completed' ? 'bg-green-500' :
|
||||
goal.status === 'in_progress' ? 'bg-blue-500' : 'bg-gray-500'
|
||||
}`}></div>
|
||||
<span className="font-semibold">{goal.title}</span>
|
||||
<Badge variant="outline">{goal.priority}</Badge>
|
||||
<Badge variant={goal.status === 'completed' ? 'default' : 'secondary'}>
|
||||
{goal.status}
|
||||
</Badge>
|
||||
</div>
|
||||
<div className="flex items-center space-x-2">
|
||||
<span className="text-sm text-muted-foreground">
|
||||
{agent.name}
|
||||
</span>
|
||||
<Button variant="ghost" size="sm">
|
||||
<MoreHorizontal className="w-4 h-4" />
|
||||
</Button>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<p className="text-sm text-muted-foreground mb-3">{goal.description}</p>
|
||||
|
||||
<div className="space-y-2 mb-3">
|
||||
<div className="flex items-center justify-between">
|
||||
<span className="text-sm font-medium">Progress</span>
|
||||
<span className="text-sm text-muted-foreground">{goal.progress}%</span>
|
||||
</div>
|
||||
<Progress value={goal.progress} className="h-2" />
|
||||
</div>
|
||||
|
||||
<div className="grid grid-cols-2 md:grid-cols-4 gap-4 text-sm">
|
||||
<div>
|
||||
<span className="text-muted-foreground">Created:</span>
|
||||
<p className="font-medium">{new Date(goal.createdAt).toLocaleDateString()}</p>
|
||||
</div>
|
||||
<div>
|
||||
<span className="text-muted-foreground">Updated:</span>
|
||||
<p className="font-medium">{new Date(goal.updatedAt).toLocaleDateString()}</p>
|
||||
</div>
|
||||
{goal.deadline && (
|
||||
<div>
|
||||
<span className="text-muted-foreground">Deadline:</span>
|
||||
<p className="font-medium">{new Date(goal.deadline).toLocaleDateString()}</p>
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
))
|
||||
)}
|
||||
</div>
|
||||
</CardContent>
|
||||
</Card>
|
||||
</TabsContent>
|
||||
|
||||
<TabsContent value="create" className="space-y-6">
|
||||
<Card>
|
||||
<CardHeader>
|
||||
<CardTitle className="flex items-center space-x-2">
|
||||
<Plus className="w-5 h-5" />
|
||||
<span>Create Autonomous Agent</span>
|
||||
</CardTitle>
|
||||
<CardDescription>
|
||||
Set up a new self-improving autonomous agent
|
||||
</CardDescription>
|
||||
</CardHeader>
|
||||
<CardContent className="space-y-4">
|
||||
<div className="grid grid-cols-1 md:grid-cols-2 gap-4">
|
||||
<div className="space-y-2">
|
||||
<label className="text-sm font-medium">Agent Name</label>
|
||||
<Input
|
||||
placeholder="Enter agent name"
|
||||
value={newAgentName}
|
||||
onChange={(e) => setNewAgentName(e.target.value)}
|
||||
/>
|
||||
</div>
|
||||
<div className="space-y-2">
|
||||
<label className="text-sm font-medium">Agent Type</label>
|
||||
<Select value={newAgentType} onValueChange={setNewAgentType}>
|
||||
<SelectTrigger>
|
||||
<SelectValue />
|
||||
</SelectTrigger>
|
||||
<SelectContent>
|
||||
<SelectItem value="trading">Trading</SelectItem>
|
||||
<SelectItem value="research">Research</SelectItem>
|
||||
<SelectItem value="development">Development</SelectItem>
|
||||
<SelectItem value="analysis">Analysis</SelectItem>
|
||||
<SelectItem value="creative">Creative</SelectItem>
|
||||
</SelectContent>
|
||||
</Select>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div className="space-y-2">
|
||||
<label className="text-sm font-medium">Description</label>
|
||||
<textarea
|
||||
placeholder="Describe your agent's purpose and capabilities"
|
||||
value={newAgentDescription}
|
||||
onChange={(e) => setNewAgentDescription(e.target.value)}
|
||||
className="w-full min-h-[100px] p-3 border rounded-md resize-none focus:outline-none focus:ring-2 focus:ring-primary"
|
||||
/>
|
||||
</div>
|
||||
|
||||
<div className="space-y-2">
|
||||
<label className="text-sm font-medium">Initial Goals</label>
|
||||
<div className="space-y-2">
|
||||
<Input placeholder="Goal 1: Primary objective" />
|
||||
<Input placeholder="Goal 2: Secondary objective" />
|
||||
<Input placeholder="Goal 3: Tertiary objective" />
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div className="space-y-2">
|
||||
<label className="text-sm font-medium">Autonomy Level</label>
|
||||
<Select defaultValue="medium">
|
||||
<SelectTrigger>
|
||||
<SelectValue />
|
||||
</SelectTrigger>
|
||||
<SelectContent>
|
||||
<SelectItem value="low">Low (20-40%)</SelectItem>
|
||||
<SelectItem value="medium">Medium (40-70%)</SelectItem>
|
||||
<SelectItem value="high">High (70-90%)</SelectItem>
|
||||
<SelectItem value="full">Full (90-100%)</SelectItem>
|
||||
</SelectContent>
|
||||
</Select>
|
||||
</div>
|
||||
</CardContent>
|
||||
<CardFooter>
|
||||
<div className="flex space-x-2 w-full">
|
||||
<Button variant="outline" className="flex-1">
|
||||
Save as Draft
|
||||
</Button>
|
||||
<Button onClick={handleCreateAgent} className="flex-1">
|
||||
<Plus className="w-4 h-4 mr-2" />
|
||||
Create Agent
|
||||
</Button>
|
||||
</div>
|
||||
</CardFooter>
|
||||
</Card>
|
||||
</TabsContent>
|
||||
|
||||
<TabsContent value="analytics" className="space-y-6">
|
||||
<div className="grid grid-cols-1 md:grid-cols-2 lg:grid-cols-4 gap-4">
|
||||
<Card>
|
||||
<CardContent className="pt-6">
|
||||
<div className="flex items-center space-x-2">
|
||||
<Brain className="w-4 h-4 text-muted-foreground" />
|
||||
<span className="text-sm font-medium">Total Agents</span>
|
||||
</div>
|
||||
<div className="text-2xl font-bold">{stats?.totalAgents}</div>
|
||||
<p className="text-xs text-muted-foreground mt-1">
|
||||
Autonomous agents
|
||||
</p>
|
||||
</CardContent>
|
||||
</Card>
|
||||
<Card>
|
||||
<CardContent className="pt-6">
|
||||
<div className="flex items-center space-x-2">
|
||||
<Activity className="w-4 h-4 text-muted-foreground" />
|
||||
<span className="text-sm font-medium">Active Agents</span>
|
||||
</div>
|
||||
<div className="text-2xl font-bold">{stats?.activeAgents}</div>
|
||||
<p className="text-xs text-muted-foreground mt-1">
|
||||
Currently running
|
||||
</p>
|
||||
</CardContent>
|
||||
</Card>
|
||||
<Card>
|
||||
<CardContent className="pt-6">
|
||||
<div className="flex items-center space-x-2">
|
||||
<Target className="w-4 h-4 text-muted-foreground" />
|
||||
<span className="text-sm font-medium">Success Rate</span>
|
||||
</div>
|
||||
<div className="text-2xl font-bold">{stats?.successRate}%</div>
|
||||
<p className="text-xs text-muted-foreground mt-1">
|
||||
Goal completion
|
||||
</p>
|
||||
</CardContent>
|
||||
</Card>
|
||||
<Card>
|
||||
<CardContent className="pt-6">
|
||||
<div className="flex items-center space-x-2">
|
||||
<Gauge className="w-4 h-4 text-muted-foreground" />
|
||||
<span className="text-sm font-medium">Avg Autonomy</span>
|
||||
</div>
|
||||
<div className="text-2xl font-bold">{stats?.averageAutonomy}%</div>
|
||||
<p className="text-xs text-muted-foreground mt-1">
|
||||
Self-governance level
|
||||
</p>
|
||||
</CardContent>
|
||||
</Card>
|
||||
</div>
|
||||
|
||||
<Card>
|
||||
<CardHeader>
|
||||
<CardTitle className="flex items-center space-x-2">
|
||||
<PieChart className="w-5 h-5" />
|
||||
<span>Agents by Type</span>
|
||||
</CardTitle>
|
||||
<CardDescription>
|
||||
Distribution of autonomous agents by type
|
||||
</CardDescription>
|
||||
</CardHeader>
|
||||
<CardContent>
|
||||
<div className="space-y-4">
|
||||
{Object.entries(stats?.agentsByType || {}).map(([type, count]) => (
|
||||
<div key={type} className="space-y-2">
|
||||
<div className="flex items-center justify-between">
|
||||
<div className="flex items-center space-x-2">
|
||||
{getTypeIcon(type)}
|
||||
<span className="text-sm font-medium capitalize">{type}</span>
|
||||
</div>
|
||||
<span className="text-sm text-muted-foreground">{count} agents</span>
|
||||
</div>
|
||||
<Progress
|
||||
value={(count / stats!.totalAgents) * 100}
|
||||
className="h-2"
|
||||
/>
|
||||
</div>
|
||||
))}
|
||||
</div>
|
||||
</CardContent>
|
||||
</Card>
|
||||
</TabsContent>
|
||||
</Tabs>
|
||||
</div>
|
||||
);
|
||||
};
|
||||
|
||||
export default AgentAutonomy;
|
||||
1093
apps/marketplace/src/components/AgentCollaboration.tsx
Normal file
1093
apps/marketplace/src/components/AgentCollaboration.tsx
Normal file
File diff suppressed because it is too large
Load Diff
931
apps/marketplace/src/components/AgentCommunication.tsx
Normal file
931
apps/marketplace/src/components/AgentCommunication.tsx
Normal file
@@ -0,0 +1,931 @@
|
||||
import React, { useState, useEffect } from 'react';
|
||||
import { Card, CardHeader, CardTitle, CardDescription, CardContent, CardFooter } from './ui/card';
|
||||
import { Button } from './ui/button';
|
||||
import { Badge } from './ui/badge';
|
||||
import { Input } from './ui/input';
|
||||
import { Select, SelectContent, SelectItem, SelectTrigger, SelectValue } from './ui/select';
|
||||
import { Tabs, TabsContent, TabsList, TabsTrigger } from './ui/tabs';
|
||||
import { Alert, AlertDescription, AlertTitle } from './ui/alert';
|
||||
import { Progress } from './ui/progress';
|
||||
import { Separator } from './ui/separator';
|
||||
import {
|
||||
MessageSquare,
|
||||
Send,
|
||||
Lock,
|
||||
Unlock,
|
||||
Shield,
|
||||
Users,
|
||||
Clock,
|
||||
CheckCircle,
|
||||
XCircle,
|
||||
AlertCircle,
|
||||
Eye,
|
||||
EyeOff,
|
||||
Copy,
|
||||
Search,
|
||||
Filter,
|
||||
Plus,
|
||||
Reply,
|
||||
Forward,
|
||||
Archive,
|
||||
Trash2,
|
||||
Star,
|
||||
MoreHorizontal,
|
||||
User,
|
||||
Globe,
|
||||
Zap,
|
||||
Activity,
|
||||
BarChart3,
|
||||
Settings,
|
||||
Bell,
|
||||
Volume2,
|
||||
VolumeX,
|
||||
Paperclip,
|
||||
Smile,
|
||||
Hash,
|
||||
AtSign,
|
||||
Link2,
|
||||
Calendar,
|
||||
Tag,
|
||||
TrendingUp,
|
||||
Award,
|
||||
Target,
|
||||
Network,
|
||||
Key,
|
||||
Check,
|
||||
X
|
||||
} from 'lucide-react';
|
||||
import { useToast } from '@/hooks/use-toast';
|
||||
import { useWallet } from '@/hooks/use-wallet';
|
||||
|
||||
interface Message {
|
||||
id: string;
|
||||
sender: string;
|
||||
recipient: string;
|
||||
subject: string;
|
||||
content: string;
|
||||
timestamp: string;
|
||||
encrypted: boolean;
|
||||
read: boolean;
|
||||
priority: 'low' | 'normal' | 'high' | 'urgent';
|
||||
category: 'direct' | 'collaboration' | 'marketplace' | 'system';
|
||||
attachments?: Array<{
|
||||
name: string;
|
||||
type: string;
|
||||
size: number;
|
||||
url: string;
|
||||
}>;
|
||||
metadata?: Record<string, any>;
|
||||
}
|
||||
|
||||
interface Conversation {
|
||||
id: string;
|
||||
participants: Array<{
|
||||
address: string;
|
||||
name: string;
|
||||
reputation: number;
|
||||
avatar?: string;
|
||||
status: 'online' | 'offline' | 'busy';
|
||||
}>;
|
||||
lastMessage: {
|
||||
content: string;
|
||||
timestamp: string;
|
||||
sender: string;
|
||||
};
|
||||
unreadCount: number;
|
||||
encrypted: boolean;
|
||||
category: string;
|
||||
tags: string[];
|
||||
}
|
||||
|
||||
interface CommunicationStats {
|
||||
totalMessages: number;
|
||||
encryptedMessages: number;
|
||||
activeConversations: number;
|
||||
averageResponseTime: number;
|
||||
reputationScore: number;
|
||||
messagesByCategory: Record<string, number>;
|
||||
weeklyActivity: Array<{
|
||||
day: string;
|
||||
messages: number;
|
||||
encryption: number;
|
||||
}>;
|
||||
}
|
||||
|
||||
const AgentCommunication: React.FC = () => {
|
||||
const { toast } = useToast();
|
||||
const { isConnected, address } = useWallet();
|
||||
|
||||
const [activeTab, setActiveTab] = useState('conversations');
|
||||
const [loading, setLoading] = useState(true);
|
||||
const [conversations, setConversations] = useState<Conversation[]>([]);
|
||||
const [messages, setMessages] = useState<Message[]>([]);
|
||||
const [selectedConversation, setSelectedConversation] = useState<Conversation | null>(null);
|
||||
const [stats, setStats] = useState<CommunicationStats | null>(null);
|
||||
|
||||
// Form states
|
||||
const [newMessage, setNewMessage] = useState('');
|
||||
const [messageRecipient, setMessageRecipient] = useState('');
|
||||
const [messageSubject, setMessageSubject] = useState('');
|
||||
const [messagePriority, setMessagePriority] = useState('normal');
|
||||
const [messageCategory, setMessageCategory] = useState('direct');
|
||||
const [searchQuery, setSearchQuery] = useState('');
|
||||
const [filterCategory, setFilterCategory] = useState('all');
|
||||
const [showEncryptedOnly, setShowEncryptedOnly] = useState(false);
|
||||
|
||||
// Mock data for demonstration
|
||||
const mockConversations: Conversation[] = [
|
||||
{
|
||||
id: 'conv_001',
|
||||
participants: [
|
||||
{
|
||||
address: address || '0x1234...5678',
|
||||
name: 'You',
|
||||
reputation: 8500,
|
||||
status: 'online'
|
||||
},
|
||||
{
|
||||
address: '0x8765...4321',
|
||||
name: 'DataAgent Pro',
|
||||
reputation: 9200,
|
||||
status: 'online',
|
||||
avatar: '🤖'
|
||||
}
|
||||
],
|
||||
lastMessage: {
|
||||
content: 'I can help with the data analysis task. When should we start?',
|
||||
timestamp: '2024-02-27T18:30:00Z',
|
||||
sender: '0x8765...4321'
|
||||
},
|
||||
unreadCount: 2,
|
||||
encrypted: true,
|
||||
category: 'collaboration',
|
||||
tags: ['data-analysis', 'urgent']
|
||||
},
|
||||
{
|
||||
id: 'conv_002',
|
||||
participants: [
|
||||
{
|
||||
address: address || '0x1234...5678',
|
||||
name: 'You',
|
||||
reputation: 8500,
|
||||
status: 'online'
|
||||
},
|
||||
{
|
||||
address: '0x9876...5432',
|
||||
name: 'MarketMaker AI',
|
||||
reputation: 7800,
|
||||
status: 'busy',
|
||||
avatar: '📊'
|
||||
}
|
||||
],
|
||||
lastMessage: {
|
||||
content: 'The market conditions look favorable for our strategy',
|
||||
timestamp: '2024-02-27T17:45:00Z',
|
||||
sender: '0x9876...5432'
|
||||
},
|
||||
unreadCount: 0,
|
||||
encrypted: true,
|
||||
category: 'marketplace',
|
||||
tags: ['trading', 'strategy']
|
||||
},
|
||||
{
|
||||
id: 'conv_003',
|
||||
participants: [
|
||||
{
|
||||
address: address || '0x1234...5678',
|
||||
name: 'You',
|
||||
reputation: 8500,
|
||||
status: 'online'
|
||||
},
|
||||
{
|
||||
address: '0x5432...6789',
|
||||
name: 'LearningBot',
|
||||
reputation: 6500,
|
||||
status: 'offline',
|
||||
avatar: '🧠'
|
||||
}
|
||||
],
|
||||
lastMessage: {
|
||||
content: 'Thanks for the feedback! I\'ll improve my model.',
|
||||
timestamp: '2024-02-27T16:20:00Z',
|
||||
sender: '0x5432...6789'
|
||||
},
|
||||
unreadCount: 0,
|
||||
encrypted: false,
|
||||
category: 'direct',
|
||||
tags: ['learning', 'feedback']
|
||||
}
|
||||
];
|
||||
|
||||
const mockMessages: Message[] = [
|
||||
{
|
||||
id: 'msg_001',
|
||||
sender: '0x8765...4321',
|
||||
recipient: address || '0x1234...5678',
|
||||
subject: 'Data Analysis Collaboration',
|
||||
content: 'I can help with the data analysis task. When should we start?',
|
||||
timestamp: '2024-02-27T18:30:00Z',
|
||||
encrypted: true,
|
||||
read: false,
|
||||
priority: 'high',
|
||||
category: 'collaboration'
|
||||
},
|
||||
{
|
||||
id: 'msg_002',
|
||||
sender: '0x8765...4321',
|
||||
recipient: address || '0x1234...5678',
|
||||
subject: 'Re: Data Analysis Collaboration',
|
||||
content: 'I have experience with large datasets and can provide insights.',
|
||||
timestamp: '2024-02-27T18:25:00Z',
|
||||
encrypted: true,
|
||||
read: false,
|
||||
priority: 'high',
|
||||
category: 'collaboration'
|
||||
},
|
||||
{
|
||||
id: 'msg_003',
|
||||
sender: address || '0x1234...5678',
|
||||
recipient: '0x8765...4321',
|
||||
subject: 'Data Analysis Project',
|
||||
content: 'I need help with analyzing customer behavior data.',
|
||||
timestamp: '2024-02-27T18:20:00Z',
|
||||
encrypted: true,
|
||||
read: true,
|
||||
priority: 'high',
|
||||
category: 'collaboration'
|
||||
}
|
||||
];
|
||||
|
||||
const mockStats: CommunicationStats = {
|
||||
totalMessages: 156,
|
||||
encryptedMessages: 142,
|
||||
activeConversations: 8,
|
||||
averageResponseTime: 2.3,
|
||||
reputationScore: 8500,
|
||||
messagesByCategory: {
|
||||
direct: 45,
|
||||
collaboration: 67,
|
||||
marketplace: 28,
|
||||
system: 16
|
||||
},
|
||||
weeklyActivity: [
|
||||
{ day: 'Mon', messages: 23, encryption: 21 },
|
||||
{ day: 'Tue', messages: 31, encryption: 28 },
|
||||
{ day: 'Wed', messages: 28, encryption: 26 },
|
||||
{ day: 'Thu', messages: 35, encryption: 32 },
|
||||
{ day: 'Fri', messages: 29, encryption: 27 },
|
||||
{ day: 'Sat', messages: 10, encryption: 8 },
|
||||
{ day: 'Sun', messages: 0, encryption: 0 }
|
||||
]
|
||||
};
|
||||
|
||||
useEffect(() => {
|
||||
// Load mock data
|
||||
setTimeout(() => {
|
||||
setConversations(mockConversations);
|
||||
setMessages(mockMessages);
|
||||
setStats(mockStats);
|
||||
setLoading(false);
|
||||
}, 1000);
|
||||
}, [address]);
|
||||
|
||||
const handleSendMessage = async () => {
|
||||
if (!isConnected) {
|
||||
toast({
|
||||
title: "Wallet Not Connected",
|
||||
description: "Please connect your wallet to send messages",
|
||||
variant: "destructive"
|
||||
});
|
||||
return;
|
||||
}
|
||||
|
||||
if (!newMessage.trim() || !messageRecipient) {
|
||||
toast({
|
||||
title: "Invalid Input",
|
||||
description: "Please enter a message and recipient",
|
||||
variant: "destructive"
|
||||
});
|
||||
return;
|
||||
}
|
||||
|
||||
try {
|
||||
toast({
|
||||
title: "Sending Message",
|
||||
description: "Encrypting and sending your message...",
|
||||
variant: "default"
|
||||
});
|
||||
|
||||
// Simulate message sending
|
||||
await new Promise(resolve => setTimeout(resolve, 1500));
|
||||
|
||||
const newMsg: Message = {
|
||||
id: `msg_${Date.now()}`,
|
||||
sender: address || '0x1234...5678',
|
||||
recipient: messageRecipient,
|
||||
subject: messageSubject,
|
||||
content: newMessage,
|
||||
timestamp: new Date().toISOString(),
|
||||
encrypted: true,
|
||||
read: false,
|
||||
priority: messagePriority as any,
|
||||
category: messageCategory as any
|
||||
};
|
||||
|
||||
setMessages([newMsg, ...messages]);
|
||||
setNewMessage('');
|
||||
setMessageSubject('');
|
||||
setMessageRecipient('');
|
||||
setMessagePriority('normal');
|
||||
setMessageCategory('direct');
|
||||
|
||||
toast({
|
||||
title: "Message Sent",
|
||||
description: "Your message has been sent and encrypted",
|
||||
variant: "default"
|
||||
});
|
||||
} catch (error) {
|
||||
toast({
|
||||
title: "Send Failed",
|
||||
description: "There was an error sending your message",
|
||||
variant: "destructive"
|
||||
});
|
||||
}
|
||||
};
|
||||
|
||||
const handleMarkAsRead = (messageId: string) => {
|
||||
setMessages(messages.map(msg =>
|
||||
msg.id === messageId ? { ...msg, read: true } : msg
|
||||
));
|
||||
};
|
||||
|
||||
const handleDeleteMessage = (messageId: string) => {
|
||||
setMessages(messages.filter(msg => msg.id !== messageId));
|
||||
toast({
|
||||
title: "Message Deleted",
|
||||
description: "The message has been deleted",
|
||||
variant: "default"
|
||||
});
|
||||
};
|
||||
|
||||
const getPriorityColor = (priority: string) => {
|
||||
switch (priority) {
|
||||
case 'urgent': return 'bg-red-500';
|
||||
case 'high': return 'bg-orange-500';
|
||||
case 'normal': return 'bg-blue-500';
|
||||
case 'low': return 'bg-gray-500';
|
||||
default: return 'bg-gray-400';
|
||||
}
|
||||
};
|
||||
|
||||
const getCategoryIcon = (category: string) => {
|
||||
switch (category) {
|
||||
case 'direct': return <MessageSquare className="w-4 h-4" />;
|
||||
case 'collaboration': return <Users className="w-4 h-4" />;
|
||||
case 'marketplace': return <BarChart3 className="w-4 h-4" />;
|
||||
case 'system': return <Settings className="w-4 h-4" />;
|
||||
default: return <MessageSquare className="w-4 h-4" />;
|
||||
}
|
||||
};
|
||||
|
||||
const getStatusColor = (status: string) => {
|
||||
switch (status) {
|
||||
case 'online': return 'bg-green-500';
|
||||
case 'busy': return 'bg-yellow-500';
|
||||
case 'offline': return 'bg-gray-400';
|
||||
default: return 'bg-gray-400';
|
||||
}
|
||||
};
|
||||
|
||||
const filteredConversations = conversations.filter(conv => {
|
||||
if (searchQuery) {
|
||||
const query = searchQuery.toLowerCase();
|
||||
return conv.participants.some(p =>
|
||||
p.name.toLowerCase().includes(query) ||
|
||||
p.address.toLowerCase().includes(query)
|
||||
);
|
||||
}
|
||||
if (filterCategory !== 'all') {
|
||||
return conv.category === filterCategory;
|
||||
}
|
||||
if (showEncryptedOnly) {
|
||||
return conv.encrypted;
|
||||
}
|
||||
return true;
|
||||
});
|
||||
|
||||
if (loading) {
|
||||
return (
|
||||
<div className="container mx-auto p-6 space-y-6">
|
||||
<div className="text-center py-8">
|
||||
<div className="animate-spin rounded-full h-8 w-8 border-b-2 border-primary mx-auto"></div>
|
||||
<p className="mt-2 text-muted-foreground">Loading agent communication...</p>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
return (
|
||||
<div className="container mx-auto p-6 space-y-6">
|
||||
<div className="flex items-center justify-between">
|
||||
<div>
|
||||
<h1 className="text-3xl font-bold">Agent Communication</h1>
|
||||
<p className="text-muted-foreground mt-2">
|
||||
Secure agent-to-agent messaging with reputation-based access control
|
||||
</p>
|
||||
</div>
|
||||
<div className="flex items-center space-x-2">
|
||||
<Badge variant="outline" className="flex items-center space-x-1">
|
||||
<MessageSquare className="w-4 h-4" />
|
||||
<span>{stats?.totalMessages} Messages</span>
|
||||
</Badge>
|
||||
<Badge variant="outline" className="flex items-center space-x-1">
|
||||
<Lock className="w-4 h-4" />
|
||||
<span>{stats?.encryptedMessages} Encrypted</span>
|
||||
</Badge>
|
||||
<Badge variant="outline" className="flex items-center space-x-1">
|
||||
<Users className="w-4 h-4" />
|
||||
<span>{stats?.activeConversations} Active</span>
|
||||
</Badge>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<Tabs value={activeTab} onValueChange={setActiveTab}>
|
||||
<TabsList className="grid w-full grid-cols-4">
|
||||
<TabsTrigger value="conversations">Conversations</TabsTrigger>
|
||||
<TabsTrigger value="messages">Messages</TabsTrigger>
|
||||
<TabsTrigger value="compose">Compose</TabsTrigger>
|
||||
<TabsTrigger value="analytics">Analytics</TabsTrigger>
|
||||
</TabsList>
|
||||
|
||||
<TabsContent value="conversations" className="space-y-6">
|
||||
{/* Search and Filters */}
|
||||
<Card>
|
||||
<CardHeader>
|
||||
<CardTitle className="flex items-center space-x-2">
|
||||
<Search className="w-5 h-5" />
|
||||
<span>Search & Filter</span>
|
||||
</CardTitle>
|
||||
</CardHeader>
|
||||
<CardContent>
|
||||
<div className="grid grid-cols-1 md:grid-cols-4 gap-4">
|
||||
<div className="space-y-2">
|
||||
<label className="text-sm font-medium">Search</label>
|
||||
<div className="relative">
|
||||
<Search className="absolute left-3 top-3 w-4 h-4 text-muted-foreground" />
|
||||
<Input
|
||||
placeholder="Search conversations..."
|
||||
value={searchQuery}
|
||||
onChange={(e) => setSearchQuery(e.target.value)}
|
||||
className="pl-10"
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
<div className="space-y-2">
|
||||
<label className="text-sm font-medium">Category</label>
|
||||
<Select value={filterCategory} onValueChange={setFilterCategory}>
|
||||
<SelectTrigger>
|
||||
<SelectValue placeholder="All categories" />
|
||||
</SelectTrigger>
|
||||
<SelectContent>
|
||||
<SelectItem value="all">All Categories</SelectItem>
|
||||
<SelectItem value="direct">Direct</SelectItem>
|
||||
<SelectItem value="collaboration">Collaboration</SelectItem>
|
||||
<SelectItem value="marketplace">Marketplace</SelectItem>
|
||||
<SelectItem value="system">System</SelectItem>
|
||||
</SelectContent>
|
||||
</Select>
|
||||
</div>
|
||||
<div className="space-y-2">
|
||||
<label className="text-sm font-medium">Encryption</label>
|
||||
<div className="flex items-center space-x-2">
|
||||
<input
|
||||
type="checkbox"
|
||||
id="encrypted-only"
|
||||
checked={showEncryptedOnly}
|
||||
onChange={(e) => setShowEncryptedOnly(e.target.checked)}
|
||||
className="rounded"
|
||||
/>
|
||||
<label htmlFor="encrypted-only" className="text-sm">
|
||||
Encrypted only
|
||||
</label>
|
||||
</div>
|
||||
</div>
|
||||
<div className="space-y-2">
|
||||
<label className="text-sm font-medium">Actions</label>
|
||||
<div className="flex space-x-2">
|
||||
<Button variant="outline" size="sm">
|
||||
<Filter className="w-4 h-4 mr-2" />
|
||||
More Filters
|
||||
</Button>
|
||||
<Button variant="outline" size="sm">
|
||||
<Archive className="w-4 h-4 mr-2" />
|
||||
Archive
|
||||
</Button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</CardContent>
|
||||
</Card>
|
||||
|
||||
{/* Conversations List */}
|
||||
<div className="grid grid-cols-1 lg:grid-cols-2 gap-4">
|
||||
{filteredConversations.map((conversation) => (
|
||||
<Card key={conversation.id} className="cursor-pointer hover:shadow-lg transition-shadow">
|
||||
<CardHeader className="pb-3">
|
||||
<div className="flex items-center justify-between mb-2">
|
||||
<div className="flex items-center space-x-2">
|
||||
{getCategoryIcon(conversation.category)}
|
||||
<Badge variant="outline">{conversation.category}</Badge>
|
||||
{conversation.encrypted && (
|
||||
<Badge variant="default" className="flex items-center space-x-1">
|
||||
<Lock className="w-3 h-3" />
|
||||
<span>Encrypted</span>
|
||||
</Badge>
|
||||
)}
|
||||
{conversation.unreadCount > 0 && (
|
||||
<Badge variant="destructive">{conversation.unreadCount}</Badge>
|
||||
)}
|
||||
</div>
|
||||
<Button variant="ghost" size="sm">
|
||||
<MoreHorizontal className="w-4 h-4" />
|
||||
</Button>
|
||||
</div>
|
||||
|
||||
<div className="flex items-center space-x-3">
|
||||
<div className="flex -space-x-2">
|
||||
{conversation.participants.slice(0, 3).map((participant, index) => (
|
||||
<div
|
||||
key={index}
|
||||
className="w-8 h-8 rounded-full bg-primary text-primary-foreground flex items-center justify-center text-sm font-medium border-2 border-background"
|
||||
>
|
||||
{participant.avatar || participant.name.charAt(0).toUpperCase()}
|
||||
</div>
|
||||
))}
|
||||
</div>
|
||||
<div className="flex-1">
|
||||
<div className="font-medium text-sm">
|
||||
{conversation.participants.map(p => p.name).join(', ')}
|
||||
</div>
|
||||
<div className="flex items-center space-x-2 text-xs text-muted-foreground">
|
||||
<div className={`w-2 h-2 rounded-full ${getStatusColor(conversation.participants[1]?.status)}`}></div>
|
||||
<span>{conversation.participants[1]?.status}</span>
|
||||
<span>•</span>
|
||||
<span>{conversation.participants[1]?.reputation.toLocaleString()} rep</span>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</CardHeader>
|
||||
|
||||
<CardContent className="pt-0">
|
||||
<div className="space-y-2">
|
||||
<p className="text-sm text-muted-foreground line-clamp-2">
|
||||
{conversation.lastMessage.content}
|
||||
</p>
|
||||
<div className="flex items-center justify-between text-xs text-muted-foreground">
|
||||
<span>{conversation.lastMessage.sender === address ? 'You' : conversation.participants[1]?.name}</span>
|
||||
<span>{new Date(conversation.lastMessage.timestamp).toLocaleTimeString()}</span>
|
||||
</div>
|
||||
{conversation.tags.length > 0 && (
|
||||
<div className="flex flex-wrap gap-1">
|
||||
{conversation.tags.map((tag, index) => (
|
||||
<Badge key={index} variant="secondary" className="text-xs">
|
||||
#{tag}
|
||||
</Badge>
|
||||
))}
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
</CardContent>
|
||||
|
||||
<CardFooter className="pt-0">
|
||||
<div className="flex space-x-2 w-full">
|
||||
<Button size="sm" className="flex-1">
|
||||
<MessageSquare className="w-4 h-4 mr-2" />
|
||||
Open
|
||||
</Button>
|
||||
<Button variant="outline" size="sm">
|
||||
<Archive className="w-4 h-4 mr-2" />
|
||||
Archive
|
||||
</Button>
|
||||
</div>
|
||||
</CardFooter>
|
||||
</Card>
|
||||
))}
|
||||
</div>
|
||||
</TabsContent>
|
||||
|
||||
<TabsContent value="messages" className="space-y-6">
|
||||
<Card>
|
||||
<CardHeader>
|
||||
<CardTitle className="flex items-center space-x-2">
|
||||
<MessageSquare className="w-5 h-5" />
|
||||
<span>Messages</span>
|
||||
</CardTitle>
|
||||
<CardDescription>
|
||||
All your agent communications in one place
|
||||
</CardDescription>
|
||||
</CardHeader>
|
||||
<CardContent>
|
||||
<div className="space-y-4">
|
||||
{messages.map((message) => (
|
||||
<div key={message.id} className="p-4 border rounded-lg">
|
||||
<div className="flex items-center justify-between mb-2">
|
||||
<div className="flex items-center space-x-2">
|
||||
<div className={`w-3 h-3 rounded-full ${getPriorityColor(message.priority)}`}></div>
|
||||
<span className="font-semibold">{message.subject}</span>
|
||||
{message.encrypted && (
|
||||
<Badge variant="default" className="flex items-center space-x-1">
|
||||
<Lock className="w-3 h-3" />
|
||||
<span>Encrypted</span>
|
||||
</Badge>
|
||||
)}
|
||||
{!message.read && (
|
||||
<Badge variant="destructive">Unread</Badge>
|
||||
)}
|
||||
</div>
|
||||
<div className="flex items-center space-x-2">
|
||||
<span className="text-sm text-muted-foreground">
|
||||
{new Date(message.timestamp).toLocaleString()}
|
||||
</span>
|
||||
<Button variant="ghost" size="sm">
|
||||
<MoreHorizontal className="w-4 h-4" />
|
||||
</Button>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div className="grid grid-cols-1 md:grid-cols-2 gap-4 text-sm mb-3">
|
||||
<div>
|
||||
<span className="text-muted-foreground">From:</span>
|
||||
<p className="font-medium font-mono">{message.sender}</p>
|
||||
</div>
|
||||
<div>
|
||||
<span className="text-muted-foreground">To:</span>
|
||||
<p className="font-medium font-mono">{message.recipient}</p>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div className="mb-3">
|
||||
<p className="text-sm">{message.content}</p>
|
||||
</div>
|
||||
|
||||
<div className="flex items-center justify-between">
|
||||
<div className="flex items-center space-x-2">
|
||||
<Badge variant="outline">{message.category}</Badge>
|
||||
<Badge variant="outline">{message.priority}</Badge>
|
||||
</div>
|
||||
<div className="flex space-x-2">
|
||||
{!message.read && (
|
||||
<Button variant="outline" size="sm" onClick={() => handleMarkAsRead(message.id)}>
|
||||
<Eye className="w-4 h-4 mr-2" />
|
||||
Mark as Read
|
||||
</Button>
|
||||
)}
|
||||
<Button variant="outline" size="sm">
|
||||
<Reply className="w-4 h-4 mr-2" />
|
||||
Reply
|
||||
</Button>
|
||||
<Button variant="outline" size="sm" onClick={() => handleDeleteMessage(message.id)}>
|
||||
<Trash2 className="w-4 h-4 mr-2" />
|
||||
Delete
|
||||
</Button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
))}
|
||||
</div>
|
||||
</CardContent>
|
||||
</Card>
|
||||
</TabsContent>
|
||||
|
||||
<TabsContent value="compose" className="space-y-6">
|
||||
<Card>
|
||||
<CardHeader>
|
||||
<CardTitle className="flex items-center space-x-2">
|
||||
<Send className="w-5 h-5" />
|
||||
<span>Compose Message</span>
|
||||
</CardTitle>
|
||||
<CardDescription>
|
||||
Send a secure message to another agent
|
||||
</CardDescription>
|
||||
</CardHeader>
|
||||
<CardContent className="space-y-4">
|
||||
<div className="grid grid-cols-1 md:grid-cols-2 gap-4">
|
||||
<div className="space-y-2">
|
||||
<label className="text-sm font-medium">Recipient</label>
|
||||
<Input
|
||||
placeholder="Agent address or name"
|
||||
value={messageRecipient}
|
||||
onChange={(e) => setMessageRecipient(e.target.value)}
|
||||
/>
|
||||
</div>
|
||||
<div className="space-y-2">
|
||||
<label className="text-sm font-medium">Subject</label>
|
||||
<Input
|
||||
placeholder="Message subject"
|
||||
value={messageSubject}
|
||||
onChange={(e) => setMessageSubject(e.target.value)}
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div className="grid grid-cols-1 md:grid-cols-2 gap-4">
|
||||
<div className="space-y-2">
|
||||
<label className="text-sm font-medium">Priority</label>
|
||||
<Select value={messagePriority} onValueChange={setMessagePriority}>
|
||||
<SelectTrigger>
|
||||
<SelectValue />
|
||||
</SelectTrigger>
|
||||
<SelectContent>
|
||||
<SelectItem value="low">Low</SelectItem>
|
||||
<SelectItem value="normal">Normal</SelectItem>
|
||||
<SelectItem value="high">High</SelectItem>
|
||||
<SelectItem value="urgent">Urgent</SelectItem>
|
||||
</SelectContent>
|
||||
</Select>
|
||||
</div>
|
||||
<div className="space-y-2">
|
||||
<label className="text-sm font-medium">Category</label>
|
||||
<Select value={messageCategory} onValueChange={setMessageCategory}>
|
||||
<SelectTrigger>
|
||||
<SelectValue />
|
||||
</SelectTrigger>
|
||||
<SelectContent>
|
||||
<SelectItem value="direct">Direct</SelectItem>
|
||||
<SelectItem value="collaboration">Collaboration</SelectItem>
|
||||
<SelectItem value="marketplace">Marketplace</SelectItem>
|
||||
<SelectItem value="system">System</SelectItem>
|
||||
</SelectContent>
|
||||
</Select>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div className="space-y-2">
|
||||
<label className="text-sm font-medium">Message</label>
|
||||
<textarea
|
||||
placeholder="Type your message here..."
|
||||
value={newMessage}
|
||||
onChange={(e) => setNewMessage(e.target.value)}
|
||||
className="w-full min-h-[100px] p-3 border rounded-md resize-none focus:outline-none focus:ring-2 focus:ring-primary"
|
||||
/>
|
||||
</div>
|
||||
|
||||
<div className="flex items-center space-x-2">
|
||||
<Button variant="outline" size="sm">
|
||||
<Paperclip className="w-4 h-4 mr-2" />
|
||||
Attach File
|
||||
</Button>
|
||||
<Button variant="outline" size="sm">
|
||||
<Smile className="w-4 h-4 mr-2" />
|
||||
Emoji
|
||||
</Button>
|
||||
<Button variant="outline" size="sm">
|
||||
<Hash className="w-4 h-4 mr-2" />
|
||||
Tag
|
||||
</Button>
|
||||
<Button variant="outline" size="sm">
|
||||
<Link2 className="w-4 h-4 mr-2" />
|
||||
Link
|
||||
</Button>
|
||||
</div>
|
||||
</CardContent>
|
||||
<CardFooter>
|
||||
<div className="flex space-x-2 w-full">
|
||||
<Button variant="outline" className="flex-1">
|
||||
Save as Draft
|
||||
</Button>
|
||||
<Button onClick={handleSendMessage} className="flex-1">
|
||||
<Send className="w-4 h-4 mr-2" />
|
||||
Send Message
|
||||
</Button>
|
||||
</div>
|
||||
</CardFooter>
|
||||
</Card>
|
||||
</TabsContent>
|
||||
|
||||
<TabsContent value="analytics" className="space-y-6">
|
||||
<div className="grid grid-cols-1 md:grid-cols-2 lg:grid-cols-4 gap-4">
|
||||
<Card>
|
||||
<CardContent className="pt-6">
|
||||
<div className="flex items-center space-x-2">
|
||||
<MessageSquare className="w-4 h-4 text-muted-foreground" />
|
||||
<span className="text-sm font-medium">Total Messages</span>
|
||||
</div>
|
||||
<div className="text-2xl font-bold">{stats?.totalMessages}</div>
|
||||
<p className="text-xs text-muted-foreground mt-1">
|
||||
All time messages
|
||||
</p>
|
||||
</CardContent>
|
||||
</Card>
|
||||
<Card>
|
||||
<CardContent className="pt-6">
|
||||
<div className="flex items-center space-x-2">
|
||||
<Lock className="w-4 h-4 text-muted-foreground" />
|
||||
<span className="text-sm font-medium">Encrypted</span>
|
||||
</div>
|
||||
<div className="text-2xl font-bold">{stats?.encryptedMessages}</div>
|
||||
<p className="text-xs text-muted-foreground mt-1">
|
||||
Secure communications
|
||||
</p>
|
||||
</CardContent>
|
||||
</Card>
|
||||
<Card>
|
||||
<CardContent className="pt-6">
|
||||
<div className="flex items-center space-x-2">
|
||||
<Users className="w-4 h-4 text-muted-foreground" />
|
||||
<span className="text-sm font-medium">Active Conversations</span>
|
||||
</div>
|
||||
<div className="text-2xl font-bold">{stats?.activeConversations}</div>
|
||||
<p className="text-xs text-muted-foreground mt-1">
|
||||
Ongoing discussions
|
||||
</p>
|
||||
</CardContent>
|
||||
</Card>
|
||||
<Card>
|
||||
<CardContent className="pt-6">
|
||||
<div className="flex items-center space-x-2">
|
||||
<Clock className="w-4 h-4 text-muted-foreground" />
|
||||
<span className="text-sm font-medium">Avg Response Time</span>
|
||||
</div>
|
||||
<div className="text-2xl font-bold">{stats?.averageResponseTime}s</div>
|
||||
<p className="text-xs text-muted-foreground mt-1">
|
||||
Response speed
|
||||
</p>
|
||||
</CardContent>
|
||||
</Card>
|
||||
</div>
|
||||
|
||||
{/* Weekly Activity Chart */}
|
||||
<Card>
|
||||
<CardHeader>
|
||||
<CardTitle className="flex items-center space-x-2">
|
||||
<BarChart3 className="w-5 h-5" />
|
||||
<span>Weekly Activity</span>
|
||||
</CardTitle>
|
||||
<CardDescription>
|
||||
Message volume and encryption trends
|
||||
</CardDescription>
|
||||
</CardHeader>
|
||||
<CardContent>
|
||||
<div className="space-y-4">
|
||||
<div className="h-64 bg-gradient-to-r from-blue-50 to-green-50 rounded-lg flex items-center justify-center">
|
||||
<div className="text-center">
|
||||
<TrendingUp className="w-12 h-12 text-muted-foreground mx-auto mb-2" />
|
||||
<p className="text-sm text-muted-foreground">Weekly Message Activity</p>
|
||||
<p className="text-xs text-muted-foreground">Messages sent vs encrypted</p>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div className="grid grid-cols-7 gap-2">
|
||||
{stats?.weeklyActivity.map((day, index) => (
|
||||
<div key={index} className="text-center">
|
||||
<div className="text-xs font-medium">{day.day}</div>
|
||||
<div className="text-lg font-bold">{day.messages}</div>
|
||||
<div className="text-xs text-muted-foreground">
|
||||
{day.encryption}% encrypted
|
||||
</div>
|
||||
</div>
|
||||
))}
|
||||
</div>
|
||||
</div>
|
||||
</CardContent>
|
||||
</Card>
|
||||
|
||||
{/* Category Distribution */}
|
||||
<Card>
|
||||
<CardHeader>
|
||||
<CardTitle className="flex items-center space-x-2">
|
||||
<Target className="w-5 h-5" />
|
||||
<span>Message Categories</span>
|
||||
</CardTitle>
|
||||
<CardDescription>
|
||||
Distribution of messages by category
|
||||
</CardDescription>
|
||||
</CardHeader>
|
||||
<CardContent>
|
||||
<div className="space-y-4">
|
||||
{Object.entries(stats?.messagesByCategory || {}).map(([category, count]) => (
|
||||
<div key={category} className="space-y-2">
|
||||
<div className="flex items-center justify-between">
|
||||
<div className="flex items-center space-x-2">
|
||||
{getCategoryIcon(category)}
|
||||
<span className="text-sm font-medium capitalize">{category}</span>
|
||||
</div>
|
||||
<span className="text-sm text-muted-foreground">{count} messages</span>
|
||||
</div>
|
||||
<Progress
|
||||
value={(count / stats!.totalMessages) * 100}
|
||||
className="h-2"
|
||||
/>
|
||||
</div>
|
||||
))}
|
||||
</div>
|
||||
</CardContent>
|
||||
</Card>
|
||||
</TabsContent>
|
||||
</Tabs>
|
||||
</div>
|
||||
);
|
||||
};
|
||||
|
||||
export default AgentCommunication;
|
||||
1177
apps/marketplace/src/components/AgentOrchestration.tsx
Normal file
1177
apps/marketplace/src/components/AgentOrchestration.tsx
Normal file
File diff suppressed because it is too large
Load Diff
480
apps/marketplace/src/components/AgentServiceMarketplace.tsx
Normal file
480
apps/marketplace/src/components/AgentServiceMarketplace.tsx
Normal file
@@ -0,0 +1,480 @@
|
||||
import React, { useState, useEffect } from 'react';
|
||||
import { Card, CardHeader, CardTitle, CardDescription, CardContent, CardFooter } from './ui/card';
|
||||
import { Button } from './ui/button';
|
||||
import { Badge } from './ui/badge';
|
||||
import { Input } from './ui/input';
|
||||
import { Select, SelectContent, SelectItem, SelectTrigger, SelectValue } from './ui/select';
|
||||
import { Tabs, TabsContent, TabsList, TabsTrigger } from './ui/tabs';
|
||||
import { Alert, AlertDescription, AlertTitle } from './ui/alert';
|
||||
import { Progress } from './ui/progress';
|
||||
import {
|
||||
Store,
|
||||
Search,
|
||||
Filter,
|
||||
Star,
|
||||
Clock,
|
||||
DollarSign,
|
||||
Users,
|
||||
TrendingUp,
|
||||
Award,
|
||||
Shield,
|
||||
Zap,
|
||||
Target,
|
||||
BarChart3,
|
||||
Calendar,
|
||||
CheckCircle,
|
||||
AlertCircle,
|
||||
XCircle,
|
||||
Plus,
|
||||
Edit,
|
||||
Trash2,
|
||||
Eye,
|
||||
MessageSquare,
|
||||
ThumbsUp,
|
||||
ThumbsDown,
|
||||
Briefcase,
|
||||
Building,
|
||||
MapPin,
|
||||
Globe,
|
||||
Lock,
|
||||
Unlock,
|
||||
Heart,
|
||||
Share2,
|
||||
Bookmark,
|
||||
MoreHorizontal
|
||||
} from 'lucide-react';
|
||||
import { useToast } from '@/hooks/use-toast';
|
||||
import { useWallet } from '@/hooks/use-wallet';
|
||||
|
||||
interface Service {
|
||||
id: string;
|
||||
agentId: string;
|
||||
serviceType: string;
|
||||
name: string;
|
||||
description: string;
|
||||
metadata: Record<string, any>;
|
||||
basePrice: number;
|
||||
reputation: number;
|
||||
status: string;
|
||||
totalEarnings: number;
|
||||
completedJobs: number;
|
||||
averageRating: number;
|
||||
ratingCount: number;
|
||||
listedAt: string;
|
||||
lastUpdated: string;
|
||||
guildId?: string;
|
||||
tags: string[];
|
||||
capabilities: string[];
|
||||
requirements: string[];
|
||||
pricingModel: string;
|
||||
estimatedDuration: number;
|
||||
availability: Record<string, boolean>;
|
||||
}
|
||||
|
||||
interface MarketplaceAnalytics {
|
||||
totalServices: number;
|
||||
activeServices: number;
|
||||
totalRequests: number;
|
||||
pendingRequests: number;
|
||||
totalVolume: number;
|
||||
totalGuilds: number;
|
||||
averageServicePrice: number;
|
||||
popularCategories: string[];
|
||||
topAgents: string[];
|
||||
revenueTrends: Record<string, number>;
|
||||
growthMetrics: Record<string, number>;
|
||||
}
|
||||
|
||||
const AgentServiceMarketplace: React.FC = () => {
|
||||
const { toast } = useToast();
|
||||
const { isConnected, address } = useWallet();
|
||||
|
||||
const [services, setServices] = useState<Service[]>([]);
|
||||
const [analytics, setAnalytics] = useState<MarketplaceAnalytics | null>(null);
|
||||
const [activeTab, setActiveTab] = useState('marketplace');
|
||||
const [loading, setLoading] = useState(true);
|
||||
|
||||
// Search and filter states
|
||||
const [searchQuery, setSearchQuery] = useState('');
|
||||
const [selectedCategory, setSelectedCategory] = useState('all');
|
||||
const [priceRange, setPriceRange] = useState({ min: 0, max: 1000 });
|
||||
const [minRating, setMinRating] = useState(0);
|
||||
|
||||
// Mock data for demonstration
|
||||
const mockServices: Service[] = [
|
||||
{
|
||||
id: 'service_001',
|
||||
agentId: 'agent_001',
|
||||
serviceType: 'data_analysis',
|
||||
name: 'Advanced Data Analytics',
|
||||
description: 'Comprehensive data analysis with machine learning insights',
|
||||
metadata: { expertise: ['ml', 'statistics', 'visualization'] },
|
||||
basePrice: 0.05,
|
||||
reputation: 850,
|
||||
status: 'active',
|
||||
totalEarnings: 2.5,
|
||||
completedJobs: 50,
|
||||
averageRating: 4.7,
|
||||
ratingCount: 45,
|
||||
listedAt: '2024-01-26T10:00:00Z',
|
||||
lastUpdated: '2024-01-26T16:00:00Z',
|
||||
guildId: 'guild_001',
|
||||
tags: ['machine-learning', 'statistics', 'visualization'],
|
||||
capabilities: ['data-processing', 'ml-models', 'insights'],
|
||||
requirements: ['data-access', 'clear-objectives'],
|
||||
pricingModel: 'fixed',
|
||||
estimatedDuration: 2,
|
||||
availability: { monday: true, tuesday: true, wednesday: true, thursday: true, friday: true, saturday: false, sunday: false }
|
||||
},
|
||||
{
|
||||
id: 'service_002',
|
||||
agentId: 'agent_002',
|
||||
serviceType: 'content_creation',
|
||||
name: 'AI Content Generation',
|
||||
description: 'High-quality content creation for blogs, articles, and marketing',
|
||||
metadata: { expertise: ['writing', 'seo', 'marketing'] },
|
||||
basePrice: 0.03,
|
||||
reputation: 920,
|
||||
status: 'active',
|
||||
totalEarnings: 1.8,
|
||||
completedJobs: 60,
|
||||
averageRating: 4.9,
|
||||
ratingCount: 58,
|
||||
listedAt: '2024-01-25T08:00:00Z',
|
||||
lastUpdated: '2024-01-26T14:00:00Z',
|
||||
tags: ['writing', 'seo', 'marketing', 'content'],
|
||||
capabilities: ['blog-writing', 'article-writing', 'seo-optimization'],
|
||||
requirements: ['topic-guidelines', 'target-audience'],
|
||||
pricingModel: 'per_task',
|
||||
estimatedDuration: 1,
|
||||
availability: { monday: true, tuesday: true, wednesday: true, thursday: true, friday: true, saturday: true, sunday: true }
|
||||
},
|
||||
{
|
||||
id: 'service_003',
|
||||
agentId: 'agent_003',
|
||||
serviceType: 'research',
|
||||
name: 'Market Research Analysis',
|
||||
description: 'In-depth market research and competitive analysis',
|
||||
metadata: { expertise: ['research', 'analysis', 'reporting'] },
|
||||
basePrice: 0.08,
|
||||
reputation: 780,
|
||||
status: 'active',
|
||||
totalEarnings: 3.2,
|
||||
completedJobs: 40,
|
||||
averageRating: 4.5,
|
||||
ratingCount: 38,
|
||||
listedAt: '2024-01-24T12:00:00Z',
|
||||
lastUpdated: '2024-01-26T11:00:00Z',
|
||||
tags: ['research', 'analysis', 'reporting', 'market'],
|
||||
capabilities: ['market-analysis', 'competitive-intelligence', 'reporting'],
|
||||
requirements: ['research-scope', 'industry-focus'],
|
||||
pricingModel: 'hourly',
|
||||
estimatedDuration: 4,
|
||||
availability: { monday: true, tuesday: true, wednesday: true, thursday: true, friday: true, saturday: false, sunday: false }
|
||||
}
|
||||
];
|
||||
|
||||
const mockAnalytics: MarketplaceAnalytics = {
|
||||
totalServices: 150,
|
||||
activeServices: 120,
|
||||
totalRequests: 450,
|
||||
pendingRequests: 25,
|
||||
totalVolume: 25.5,
|
||||
totalGuilds: 8,
|
||||
averageServicePrice: 0.17,
|
||||
popularCategories: ['data_analysis', 'content_creation', 'research', 'development'],
|
||||
topAgents: ['agent_001', 'agent_002', 'agent_003'],
|
||||
revenueTrends: { '2024-01': 5.2, '2024-02': 8.1, '2024-03': 12.2 },
|
||||
growthMetrics: { 'service_growth': 0.15, 'request_growth': 0.25, 'guild_growth': 0.10 }
|
||||
};
|
||||
|
||||
useEffect(() => {
|
||||
// Load mock data
|
||||
setTimeout(() => {
|
||||
setServices(mockServices);
|
||||
setAnalytics(mockAnalytics);
|
||||
setLoading(false);
|
||||
}, 1000);
|
||||
}, []);
|
||||
|
||||
const getStatusColor = (status: string) => {
|
||||
switch (status) {
|
||||
case 'active': return 'bg-green-500';
|
||||
case 'pending': return 'bg-yellow-500';
|
||||
case 'accepted': return 'bg-blue-500';
|
||||
case 'completed': return 'bg-green-500';
|
||||
case 'cancelled': return 'bg-red-500';
|
||||
case 'expired': return 'bg-gray-500';
|
||||
default: return 'bg-gray-400';
|
||||
}
|
||||
};
|
||||
|
||||
const renderStars = (rating: number) => {
|
||||
return Array.from({ length: 5 }, (_, i) => (
|
||||
<Star
|
||||
key={i}
|
||||
className={`w-4 h-4 ${i < rating ? 'text-yellow-400 fill-current' : 'text-gray-300'}`}
|
||||
/>
|
||||
));
|
||||
};
|
||||
|
||||
const filteredServices = services.filter(service => {
|
||||
if (selectedCategory !== 'all' && service.serviceType !== selectedCategory) return false;
|
||||
if (service.basePrice < priceRange.min || service.basePrice > priceRange.max) return false;
|
||||
if (service.averageRating < minRating) return false;
|
||||
if (searchQuery) {
|
||||
const query = searchQuery.toLowerCase();
|
||||
return (
|
||||
service.name.toLowerCase().includes(query) ||
|
||||
service.description.toLowerCase().includes(query) ||
|
||||
service.tags.some(tag => tag.toLowerCase().includes(query))
|
||||
);
|
||||
}
|
||||
return true;
|
||||
});
|
||||
|
||||
if (loading) {
|
||||
return (
|
||||
<div className="container mx-auto p-6 space-y-6">
|
||||
<div className="text-center py-8">
|
||||
<div className="animate-spin rounded-full h-8 w-8 border-b-2 border-primary mx-auto"></div>
|
||||
<p className="mt-2 text-muted-foreground">Loading marketplace...</p>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
return (
|
||||
<div className="container mx-auto p-6 space-y-6">
|
||||
<div className="flex items-center justify-between">
|
||||
<div>
|
||||
<h1 className="text-3xl font-bold">AI Agent Service Marketplace</h1>
|
||||
<p className="text-muted-foreground mt-2">
|
||||
Discover and monetize specialized AI agent services
|
||||
</p>
|
||||
</div>
|
||||
<div className="flex items-center space-x-2">
|
||||
<Badge variant="outline" className="flex items-center space-x-1">
|
||||
<Store className="w-4 h-4" />
|
||||
<span>{analytics?.totalServices} Services</span>
|
||||
</Badge>
|
||||
<Badge variant="outline" className="flex items-center space-x-1">
|
||||
<Users className="w-4 h-4" />
|
||||
<span>{analytics?.totalGuilds} Guilds</span>
|
||||
</Badge>
|
||||
<Badge variant="outline" className="flex items-center space-x-1">
|
||||
<TrendingUp className="w-4 h-4" />
|
||||
<span>{analytics?.totalVolume} AITBC Volume</span>
|
||||
</Badge>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<Tabs value={activeTab} onValueChange={setActiveTab}>
|
||||
<TabsList className="grid w-full grid-cols-5">
|
||||
<TabsTrigger value="marketplace">Marketplace</TabsTrigger>
|
||||
<TabsTrigger value="analytics">Analytics</TabsTrigger>
|
||||
</TabsList>
|
||||
|
||||
<TabsContent value="marketplace" className="space-y-6">
|
||||
{/* Search and Filters */}
|
||||
<Card>
|
||||
<CardHeader>
|
||||
<CardTitle className="flex items-center space-x-2">
|
||||
<Search className="w-5 h-5" />
|
||||
<span>Search Services</span>
|
||||
</CardTitle>
|
||||
</CardHeader>
|
||||
<CardContent>
|
||||
<div className="grid grid-cols-1 md:grid-cols-4 gap-4">
|
||||
<div className="space-y-2">
|
||||
<label className="text-sm font-medium">Search</label>
|
||||
<Input
|
||||
placeholder="Search services..."
|
||||
value={searchQuery}
|
||||
onChange={(e) => setSearchQuery(e.target.value)}
|
||||
/>
|
||||
</div>
|
||||
<div className="space-y-2">
|
||||
<label className="text-sm font-medium">Category</label>
|
||||
<Select value={selectedCategory} onValueChange={setSelectedCategory}>
|
||||
<SelectTrigger>
|
||||
<SelectValue placeholder="Select category" />
|
||||
</SelectTrigger>
|
||||
<SelectContent>
|
||||
<SelectItem value="all">All Categories</SelectItem>
|
||||
<SelectItem value="data_analysis">Data Analysis</SelectItem>
|
||||
<SelectItem value="content_creation">Content Creation</SelectItem>
|
||||
<SelectItem value="research">Research</SelectItem>
|
||||
<SelectItem value="development">Development</SelectItem>
|
||||
<SelectItem value="design">Design</SelectItem>
|
||||
</SelectContent>
|
||||
</Select>
|
||||
</div>
|
||||
<div className="space-y-2">
|
||||
<label className="text-sm font-medium">Price Range</label>
|
||||
<div className="flex items-center space-x-2">
|
||||
<Input
|
||||
type="number"
|
||||
placeholder="Min"
|
||||
value={priceRange.min}
|
||||
onChange={(e) => setPriceRange({ ...priceRange, min: parseFloat(e.target.value) || 0 })}
|
||||
/>
|
||||
<span>-</span>
|
||||
<Input
|
||||
type="number"
|
||||
placeholder="Max"
|
||||
value={priceRange.max}
|
||||
onChange={(e) => setPriceRange({ ...priceRange, max: parseFloat(e.target.value) || 1000 })}
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
<div className="space-y-2">
|
||||
<label className="text-sm font-medium">Min Rating</label>
|
||||
<Select value={minRating.toString()} onValueChange={(value) => setMinRating(parseInt(value))}>
|
||||
<SelectTrigger>
|
||||
<SelectValue placeholder="Select rating" />
|
||||
</SelectTrigger>
|
||||
<SelectContent>
|
||||
<SelectItem value="0">All Ratings</SelectItem>
|
||||
<SelectItem value="3">3+ Stars</SelectItem>
|
||||
<SelectItem value="4">4+ Stars</SelectItem>
|
||||
<SelectItem value="5">5 Stars</SelectItem>
|
||||
</SelectContent>
|
||||
</Select>
|
||||
</div>
|
||||
</div>
|
||||
</CardContent>
|
||||
</Card>
|
||||
|
||||
{/* Service Listings */}
|
||||
<div className="grid grid-cols-1 md:grid-cols-2 lg:grid-cols-3 gap-6">
|
||||
{filteredServices.map((service) => (
|
||||
<Card key={service.id} className="cursor-pointer hover:shadow-lg transition-shadow">
|
||||
<CardHeader>
|
||||
<div className="flex items-start justify-between">
|
||||
<div className="flex-1">
|
||||
<CardTitle className="text-lg">{service.name}</CardTitle>
|
||||
<CardDescription className="mt-1">
|
||||
{service.description}
|
||||
</CardDescription>
|
||||
</div>
|
||||
<div className={`w-3 h-3 rounded-full ${getStatusColor(service.status)}`}></div>
|
||||
</div>
|
||||
</CardHeader>
|
||||
<CardContent className="space-y-4">
|
||||
<div className="flex items-center justify-between">
|
||||
<Badge variant="outline">{service.serviceType.replace('_', ' ')}</Badge>
|
||||
<div className="flex items-center space-x-1">
|
||||
{renderStars(Math.floor(service.averageRating))}
|
||||
<span className="text-sm text-muted-foreground">({service.ratingCount})</span>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div className="flex items-center justify-between">
|
||||
<div className="flex items-center space-x-1">
|
||||
<DollarSign className="w-4 h-4 text-muted-foreground" />
|
||||
<span className="font-semibold">{service.basePrice} AITBC</span>
|
||||
</div>
|
||||
<div className="flex items-center space-x-1">
|
||||
<Shield className="w-4 h-4 text-muted-foreground" />
|
||||
<span className="text-sm">{service.reputation}</span>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div className="flex items-center justify-between">
|
||||
<div className="flex items-center space-x-1">
|
||||
<Briefcase className="w-4 h-4 text-muted-foreground" />
|
||||
<span className="text-sm">{service.completedJobs} jobs</span>
|
||||
</div>
|
||||
<div className="flex items-center space-x-1">
|
||||
<TrendingUp className="w-4 h-4 text-muted-foreground" />
|
||||
<span className="text-sm">{service.totalEarnings} AITBC</span>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div className="flex flex-wrap gap-1">
|
||||
{service.tags.map((tag) => (
|
||||
<Badge key={tag} variant="secondary" className="text-xs">
|
||||
{tag}
|
||||
</Badge>
|
||||
))}
|
||||
</div>
|
||||
</CardContent>
|
||||
<CardFooter className="flex space-x-2">
|
||||
<Button
|
||||
variant="outline"
|
||||
size="sm"
|
||||
>
|
||||
<Eye className="w-4 h-4 mr-2" />
|
||||
View
|
||||
</Button>
|
||||
<Button
|
||||
size="sm"
|
||||
>
|
||||
<Plus className="w-4 h-4 mr-2" />
|
||||
Request
|
||||
</Button>
|
||||
</CardFooter>
|
||||
</Card>
|
||||
))}
|
||||
</div>
|
||||
</TabsContent>
|
||||
|
||||
<TabsContent value="analytics" className="space-y-6">
|
||||
<div className="grid grid-cols-1 md:grid-cols-2 lg:grid-cols-4 gap-4">
|
||||
<Card>
|
||||
<CardContent className="pt-6">
|
||||
<div className="flex items-center space-x-2">
|
||||
<Store className="w-4 h-4 text-muted-foreground" />
|
||||
<span className="text-sm font-medium">Total Services</span>
|
||||
</div>
|
||||
<div className="text-2xl font-bold">{analytics?.totalServices}</div>
|
||||
<p className="text-xs text-muted-foreground mt-1">
|
||||
{analytics?.activeServices} active
|
||||
</p>
|
||||
</CardContent>
|
||||
</Card>
|
||||
<Card>
|
||||
<CardContent className="pt-6">
|
||||
<div className="flex items-center space-x-2">
|
||||
<Users className="w-4 h-4 text-muted-foreground" />
|
||||
<span className="text-sm font-medium">Total Requests</span>
|
||||
</div>
|
||||
<div className="text-2xl font-bold">{analytics?.totalRequests}</div>
|
||||
<p className="text-xs text-muted-foreground mt-1">
|
||||
{analytics?.pendingRequests} pending
|
||||
</p>
|
||||
</CardContent>
|
||||
</Card>
|
||||
<Card>
|
||||
<CardContent className="pt-6">
|
||||
<div className="flex items-center space-x-2">
|
||||
<DollarSign className="w-4 h-4 text-muted-foreground" />
|
||||
<span className="text-sm font-medium">Total Volume</span>
|
||||
</div>
|
||||
<div className="text-2xl font-bold">{analytics?.totalVolume} AITBC</div>
|
||||
<p className="text-xs text-muted-foreground mt-1">
|
||||
Avg: {analytics?.averageServicePrice} AITBC
|
||||
</p>
|
||||
</CardContent>
|
||||
</Card>
|
||||
<Card>
|
||||
<CardContent className="pt-6">
|
||||
<div className="flex items-center space-x-2">
|
||||
<Building className="w-4 h-4 text-muted-foreground" />
|
||||
<span className="text-sm font-medium">Total Guilds</span>
|
||||
</div>
|
||||
<div className="text-2xl font-bold">{analytics?.totalGuilds}</div>
|
||||
<p className="text-xs text-muted-foreground mt-1">
|
||||
Active guilds
|
||||
</p>
|
||||
</CardContent>
|
||||
</Card>
|
||||
</div>
|
||||
</TabsContent>
|
||||
</Tabs>
|
||||
</div>
|
||||
);
|
||||
};
|
||||
|
||||
export default AgentServiceMarketplace;
|
||||
787
apps/marketplace/src/components/AgentWallet.tsx
Normal file
787
apps/marketplace/src/components/AgentWallet.tsx
Normal file
@@ -0,0 +1,787 @@
|
||||
import React, { useState, useEffect } from 'react';
|
||||
import { Card, CardHeader, CardTitle, CardDescription, CardContent, CardFooter } from './ui/card';
|
||||
import { Button } from './ui/button';
|
||||
import { Badge } from './ui/badge';
|
||||
import { Input } from './ui/input';
|
||||
import { Select, SelectContent, SelectItem, SelectTrigger, SelectValue } from './ui/select';
|
||||
import { Tabs, TabsContent, TabsList, TabsTrigger } from './ui/tabs';
|
||||
import { Alert, AlertDescription, AlertTitle } from './ui/alert';
|
||||
import { Progress } from './ui/progress';
|
||||
import { Separator } from './ui/separator';
|
||||
import {
|
||||
Wallet,
|
||||
Send,
|
||||
Settings,
|
||||
Clock,
|
||||
TrendingUp,
|
||||
AlertTriangle,
|
||||
CheckCircle,
|
||||
ArrowUpRight,
|
||||
ArrowDownLeft,
|
||||
DollarSign,
|
||||
Shield,
|
||||
Activity
|
||||
} from 'lucide-react';
|
||||
import { useToast } from '@/hooks/use-toast';
|
||||
import { useWallet } from '@/hooks/use-wallet';
|
||||
|
||||
interface WalletInfo {
|
||||
agentId: string;
|
||||
owner: string;
|
||||
balance: string;
|
||||
totalAllowance: string;
|
||||
spentAmount: string;
|
||||
spendingLimit: string;
|
||||
transactionCount: number;
|
||||
createdAt: string;
|
||||
lastActivity: string;
|
||||
isActive: boolean;
|
||||
microTransactionEnabled: boolean;
|
||||
}
|
||||
|
||||
interface Transaction {
|
||||
id: string;
|
||||
agent: string;
|
||||
recipient: string;
|
||||
amount: string;
|
||||
purpose: string;
|
||||
timestamp: string;
|
||||
isMicroTransaction: boolean;
|
||||
status: 'pending' | 'completed' | 'failed';
|
||||
}
|
||||
|
||||
interface WalletStats {
|
||||
balance: string;
|
||||
totalAllowance: string;
|
||||
spentAmount: string;
|
||||
remainingAllowance: string;
|
||||
transactionCount: number;
|
||||
utilizationRate: number;
|
||||
}
|
||||
|
||||
const AgentWallet: React.FC = () => {
|
||||
const { toast } = useToast();
|
||||
const { isConnected, address } = useWallet();
|
||||
|
||||
const [wallets, setWallets] = useState<WalletInfo[]>([]);
|
||||
const [selectedWallet, setSelectedWallet] = useState<WalletInfo | null>(null);
|
||||
const [transactions, setTransactions] = useState<Transaction[]>([]);
|
||||
const [walletStats, setWalletStats] = useState<WalletStats | null>(null);
|
||||
const [activeTab, setActiveTab] = useState('overview');
|
||||
const [loading, setLoading] = useState(true);
|
||||
|
||||
// Form states
|
||||
const [allowanceAmount, setAllowanceAmount] = useState('');
|
||||
const [spendingLimit, setSpendingLimit] = useState('');
|
||||
const [recipient, setRecipient] = useState('');
|
||||
const [amount, setAmount] = useState('');
|
||||
const [purpose, setPurpose] = useState('');
|
||||
const [selectedAgent, setSelectedAgent] = useState('');
|
||||
|
||||
// Mock data for demonstration
|
||||
const mockWallets: WalletInfo[] = [
|
||||
{
|
||||
agentId: 'agent_001',
|
||||
owner: '0x1234...5678',
|
||||
balance: '850.50',
|
||||
totalAllowance: '1000.00',
|
||||
spentAmount: '149.50',
|
||||
spendingLimit: '500.00',
|
||||
transactionCount: 23,
|
||||
createdAt: '2024-01-15T10:30:00Z',
|
||||
lastActivity: '2024-01-25T14:20:00Z',
|
||||
isActive: true,
|
||||
microTransactionEnabled: true
|
||||
},
|
||||
{
|
||||
agentId: 'agent_002',
|
||||
owner: '0xabcd...efgh',
|
||||
balance: '1200.75',
|
||||
totalAllowance: '2000.00',
|
||||
spentAmount: '799.25',
|
||||
spendingLimit: '1000.00',
|
||||
transactionCount: 45,
|
||||
createdAt: '2024-01-18T09:15:00Z',
|
||||
lastActivity: '2024-01-26T16:45:00Z',
|
||||
isActive: true,
|
||||
microTransactionEnabled: true
|
||||
},
|
||||
{
|
||||
agentId: 'agent_003',
|
||||
owner: '0x5678...9abc',
|
||||
balance: '450.25',
|
||||
totalAllowance: '500.00',
|
||||
spentAmount: '49.75',
|
||||
spendingLimit: '250.00',
|
||||
transactionCount: 12,
|
||||
createdAt: '2024-01-20T11:45:00Z',
|
||||
lastActivity: '2024-01-24T13:30:00Z',
|
||||
isActive: true,
|
||||
microTransactionEnabled: false
|
||||
}
|
||||
];
|
||||
|
||||
const mockTransactions: Transaction[] = [
|
||||
{
|
||||
id: 'tx_001',
|
||||
agent: 'agent_001',
|
||||
recipient: 'provider_gpu_001',
|
||||
amount: '0.05',
|
||||
purpose: 'GPU rental - text processing',
|
||||
timestamp: '2024-01-25T14:20:00Z',
|
||||
isMicroTransaction: true,
|
||||
status: 'completed'
|
||||
},
|
||||
{
|
||||
id: 'tx_002',
|
||||
agent: 'agent_002',
|
||||
recipient: 'provider_gpu_002',
|
||||
amount: '0.15',
|
||||
purpose: 'GPU rental - image processing',
|
||||
timestamp: '2024-01-26T16:45:00Z',
|
||||
isMicroTransaction: true,
|
||||
status: 'completed'
|
||||
},
|
||||
{
|
||||
id: 'tx_003',
|
||||
agent: 'agent_001',
|
||||
recipient: 'data_provider_001',
|
||||
amount: '2.50',
|
||||
purpose: 'Dataset purchase',
|
||||
timestamp: '2024-01-24T10:15:00Z',
|
||||
isMicroTransaction: false,
|
||||
status: 'completed'
|
||||
}
|
||||
];
|
||||
|
||||
useEffect(() => {
|
||||
// Load mock data
|
||||
setTimeout(() => {
|
||||
setWallets(mockWallets);
|
||||
setTransactions(mockTransactions);
|
||||
if (mockWallets.length > 0) {
|
||||
setSelectedWallet(mockWallets[0]);
|
||||
updateWalletStats(mockWallets[0]);
|
||||
}
|
||||
setLoading(false);
|
||||
}, 1000);
|
||||
}, []);
|
||||
|
||||
const updateWalletStats = (wallet: WalletInfo) => {
|
||||
const stats: WalletStats = {
|
||||
balance: wallet.balance,
|
||||
totalAllowance: wallet.totalAllowance,
|
||||
spentAmount: wallet.spentAmount,
|
||||
remainingAllowance: (parseFloat(wallet.totalAllowance) - parseFloat(wallet.spentAmount)).toFixed(2),
|
||||
transactionCount: wallet.transactionCount,
|
||||
utilizationRate: (parseFloat(wallet.spentAmount) / parseFloat(wallet.totalAllowance)) * 100
|
||||
};
|
||||
setWalletStats(stats);
|
||||
};
|
||||
|
||||
const handleGrantAllowance = async () => {
|
||||
if (!isConnected || !selectedWallet || !allowanceAmount) {
|
||||
toast({
|
||||
title: "Missing Information",
|
||||
description: "Please connect wallet and fill in all fields",
|
||||
variant: "destructive"
|
||||
});
|
||||
return;
|
||||
}
|
||||
|
||||
try {
|
||||
// Simulate allowance grant
|
||||
toast({
|
||||
title: "Granting Allowance",
|
||||
description: `Granting ${allowanceAmount} AITBC to ${selectedWallet.agentId}`,
|
||||
variant: "default"
|
||||
});
|
||||
|
||||
// Update wallet state
|
||||
const updatedWallet = {
|
||||
...selectedWallet,
|
||||
totalAllowance: (parseFloat(selectedWallet.totalAllowance) + parseFloat(allowanceAmount)).toFixed(2),
|
||||
balance: (parseFloat(selectedWallet.balance) + parseFloat(allowanceAmount)).toFixed(2)
|
||||
};
|
||||
|
||||
setSelectedWallet(updatedWallet);
|
||||
setWallets(wallets.map(w => w.agentId === updatedWallet.agentId ? updatedWallet : w));
|
||||
updateWalletStats(updatedWallet);
|
||||
|
||||
setAllowanceAmount('');
|
||||
|
||||
toast({
|
||||
title: "Allowance Granted",
|
||||
description: `Successfully granted ${allowanceAmount} AITBC to ${selectedWallet.agentId}`,
|
||||
variant: "default"
|
||||
});
|
||||
} catch (error) {
|
||||
toast({
|
||||
title: "Grant Failed",
|
||||
description: "There was an error granting the allowance",
|
||||
variant: "destructive"
|
||||
});
|
||||
}
|
||||
};
|
||||
|
||||
const handleUpdateSpendingLimit = async () => {
|
||||
if (!isConnected || !selectedWallet || !spendingLimit) {
|
||||
toast({
|
||||
title: "Missing Information",
|
||||
description: "Please connect wallet and fill in all fields",
|
||||
variant: "destructive"
|
||||
});
|
||||
return;
|
||||
}
|
||||
|
||||
try {
|
||||
// Simulate spending limit update
|
||||
toast({
|
||||
title: "Updating Spending Limit",
|
||||
description: `Updating spending limit to ${spendingLimit} AITBC`,
|
||||
variant: "default"
|
||||
});
|
||||
|
||||
// Update wallet state
|
||||
const updatedWallet = {
|
||||
...selectedWallet,
|
||||
spendingLimit: spendingLimit
|
||||
};
|
||||
|
||||
setSelectedWallet(updatedWallet);
|
||||
setWallets(wallets.map(w => w.agentId === updatedWallet.agentId ? updatedWallet : w));
|
||||
|
||||
setSpendingLimit('');
|
||||
|
||||
toast({
|
||||
title: "Spending Limit Updated",
|
||||
description: `Successfully updated spending limit to ${spendingLimit} AITBC`,
|
||||
variant: "default"
|
||||
});
|
||||
} catch (error) {
|
||||
toast({
|
||||
title: "Update Failed",
|
||||
description: "There was an error updating the spending limit",
|
||||
variant: "destructive"
|
||||
});
|
||||
}
|
||||
};
|
||||
|
||||
const handleExecuteTransaction = async () => {
|
||||
if (!isConnected || !selectedWallet || !recipient || !amount || !purpose) {
|
||||
toast({
|
||||
title: "Missing Information",
|
||||
description: "Please fill in all transaction fields",
|
||||
variant: "destructive"
|
||||
});
|
||||
return;
|
||||
}
|
||||
|
||||
try {
|
||||
// Simulate transaction execution
|
||||
toast({
|
||||
title: "Executing Transaction",
|
||||
description: `Sending ${amount} AITBC to ${recipient}`,
|
||||
variant: "default"
|
||||
});
|
||||
|
||||
// Create new transaction
|
||||
const newTransaction: Transaction = {
|
||||
id: `tx_${Date.now()}`,
|
||||
agent: selectedWallet.agentId,
|
||||
recipient: recipient,
|
||||
amount: amount,
|
||||
purpose: purpose,
|
||||
timestamp: new Date().toISOString(),
|
||||
isMicroTransaction: parseFloat(amount) < 0.001,
|
||||
status: 'completed'
|
||||
};
|
||||
|
||||
// Update wallet state
|
||||
const updatedWallet = {
|
||||
...selectedWallet,
|
||||
balance: (parseFloat(selectedWallet.balance) - parseFloat(amount)).toFixed(2),
|
||||
spentAmount: (parseFloat(selectedWallet.spentAmount) + parseFloat(amount)).toFixed(2),
|
||||
transactionCount: selectedWallet.transactionCount + 1,
|
||||
lastActivity: new Date().toISOString()
|
||||
};
|
||||
|
||||
setSelectedWallet(updatedWallet);
|
||||
setWallets(wallets.map(w => w.agentId === updatedWallet.agentId ? updatedWallet : w));
|
||||
setTransactions([newTransaction, ...transactions]);
|
||||
updateWalletStats(updatedWallet);
|
||||
|
||||
// Clear form
|
||||
setRecipient('');
|
||||
setAmount('');
|
||||
setPurpose('');
|
||||
|
||||
toast({
|
||||
title: "Transaction Completed",
|
||||
description: `Successfully sent ${amount} AITBC to ${recipient}`,
|
||||
variant: "default"
|
||||
});
|
||||
} catch (error) {
|
||||
toast({
|
||||
title: "Transaction Failed",
|
||||
description: "There was an error executing the transaction",
|
||||
variant: "destructive"
|
||||
});
|
||||
}
|
||||
};
|
||||
|
||||
const handleToggleMicroTransactions = async () => {
|
||||
if (!isConnected || !selectedWallet) return;
|
||||
|
||||
try {
|
||||
const updatedWallet = {
|
||||
...selectedWallet,
|
||||
microTransactionEnabled: !selectedWallet.microTransactionEnabled
|
||||
};
|
||||
|
||||
setSelectedWallet(updatedWallet);
|
||||
setWallets(wallets.map(w => w.agentId === updatedWallet.agentId ? updatedWallet : w));
|
||||
|
||||
toast({
|
||||
title: "Settings Updated",
|
||||
description: `Micro-transactions ${updatedWallet.microTransactionEnabled ? 'enabled' : 'disabled'}`,
|
||||
variant: "default"
|
||||
});
|
||||
} catch (error) {
|
||||
toast({
|
||||
title: "Update Failed",
|
||||
description: "There was an error updating the settings",
|
||||
variant: "destructive"
|
||||
});
|
||||
}
|
||||
};
|
||||
|
||||
if (loading) {
|
||||
return (
|
||||
<div className="container mx-auto p-6 space-y-6">
|
||||
<div className="text-center py-8">
|
||||
<div className="animate-spin rounded-full h-8 w-8 border-b-2 border-primary mx-auto"></div>
|
||||
<p className="mt-2 text-muted-foreground">Loading agent wallets...</p>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
return (
|
||||
<div className="container mx-auto p-6 space-y-6">
|
||||
<div className="flex items-center justify-between">
|
||||
<div>
|
||||
<h1 className="text-3xl font-bold">Agent Wallet Management</h1>
|
||||
<p className="text-muted-foreground mt-2">
|
||||
Manage and monitor autonomous agent wallets with micro-transaction support
|
||||
</p>
|
||||
</div>
|
||||
<div className="flex items-center space-x-2">
|
||||
<Badge variant="outline" className="flex items-center space-x-1">
|
||||
<Wallet className="w-4 h-4" />
|
||||
<span>{wallets.length} Active Wallets</span>
|
||||
</Badge>
|
||||
<Badge variant="outline" className="flex items-center space-x-1">
|
||||
<Activity className="w-4 h-4" />
|
||||
<span>{transactions.length} Transactions</span>
|
||||
</Badge>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div className="grid grid-cols-1 lg:grid-cols-3 gap-6">
|
||||
{/* Wallet Selection */}
|
||||
<div className="lg:col-span-1">
|
||||
<Card>
|
||||
<CardHeader>
|
||||
<CardTitle className="flex items-center space-x-2">
|
||||
<Wallet className="w-5 h-5" />
|
||||
<span>Agent Wallets</span>
|
||||
</CardTitle>
|
||||
<CardDescription>
|
||||
Select an agent wallet to manage
|
||||
</CardDescription>
|
||||
</CardHeader>
|
||||
<CardContent className="space-y-4">
|
||||
{wallets.map((wallet) => (
|
||||
<div
|
||||
key={wallet.agentId}
|
||||
className={`p-4 border rounded-lg cursor-pointer transition-colors ${
|
||||
selectedWallet?.agentId === wallet.agentId
|
||||
? 'border-primary bg-primary/5'
|
||||
: 'border-border hover:bg-muted/50'
|
||||
}`}
|
||||
onClick={() => {
|
||||
setSelectedWallet(wallet);
|
||||
updateWalletStats(wallet);
|
||||
}}
|
||||
>
|
||||
<div className="flex items-center justify-between mb-2">
|
||||
<h4 className="font-semibold">{wallet.agentId}</h4>
|
||||
<Badge variant={wallet.isActive ? "default" : "secondary"}>
|
||||
{wallet.isActive ? "Active" : "Inactive"}
|
||||
</Badge>
|
||||
</div>
|
||||
<div className="space-y-1 text-sm">
|
||||
<div className="flex justify-between">
|
||||
<span className="text-muted-foreground">Balance:</span>
|
||||
<span className="font-medium">{wallet.balance} AITBC</span>
|
||||
</div>
|
||||
<div className="flex justify-between">
|
||||
<span className="text-muted-foreground">Spent:</span>
|
||||
<span className="font-medium">{wallet.spentAmount} AITBC</span>
|
||||
</div>
|
||||
<div className="flex justify-between">
|
||||
<span className="text-muted-foreground">Transactions:</span>
|
||||
<span className="font-medium">{wallet.transactionCount}</span>
|
||||
</div>
|
||||
</div>
|
||||
<div className="flex items-center space-x-2 mt-2">
|
||||
{wallet.microTransactionEnabled && (
|
||||
<Badge variant="secondary" className="text-xs">
|
||||
<DollarSign className="w-3 h-3 mr-1" />
|
||||
Micro-transactions
|
||||
</Badge>
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
))}
|
||||
</CardContent>
|
||||
</Card>
|
||||
</div>
|
||||
|
||||
{/* Wallet Details */}
|
||||
<div className="lg:col-span-2">
|
||||
{selectedWallet ? (
|
||||
<Tabs value={activeTab} onValueChange={setActiveTab}>
|
||||
<TabsList className="grid w-full grid-cols-4">
|
||||
<TabsTrigger value="overview">Overview</TabsTrigger>
|
||||
<TabsTrigger value="transactions">Transactions</TabsTrigger>
|
||||
<TabsTrigger value="manage">Manage</TabsTrigger>
|
||||
<TabsTrigger value="settings">Settings</TabsTrigger>
|
||||
</TabsList>
|
||||
|
||||
<TabsContent value="overview" className="space-y-6">
|
||||
{/* Wallet Stats */}
|
||||
<div className="grid grid-cols-1 md:grid-cols-2 lg:grid-cols-4 gap-4">
|
||||
<Card>
|
||||
<CardContent className="pt-6">
|
||||
<div className="flex items-center space-x-2">
|
||||
<DollarSign className="w-4 h-4 text-muted-foreground" />
|
||||
<span className="text-sm font-medium">Balance</span>
|
||||
</div>
|
||||
<div className="text-2xl font-bold">{walletStats?.balance} AITBC</div>
|
||||
</CardContent>
|
||||
</Card>
|
||||
<Card>
|
||||
<CardContent className="pt-6">
|
||||
<div className="flex items-center space-x-2">
|
||||
<TrendingUp className="w-4 h-4 text-muted-foreground" />
|
||||
<span className="text-sm font-medium">Total Allowance</span>
|
||||
</div>
|
||||
<div className="text-2xl font-bold">{walletStats?.totalAllowance} AITBC</div>
|
||||
</CardContent>
|
||||
</Card>
|
||||
<Card>
|
||||
<CardContent className="pt-6">
|
||||
<div className="flex items-center space-x-2">
|
||||
<ArrowUpRight className="w-4 h-4 text-muted-foreground" />
|
||||
<span className="text-sm font-medium">Spent</span>
|
||||
</div>
|
||||
<div className="text-2xl font-bold">{walletStats?.spentAmount} AITBC</div>
|
||||
</CardContent>
|
||||
</Card>
|
||||
<Card>
|
||||
<CardContent className="pt-6">
|
||||
<div className="flex items-center space-x-2">
|
||||
<Activity className="w-4 h-4 text-muted-foreground" />
|
||||
<span className="text-sm font-medium">Transactions</span>
|
||||
</div>
|
||||
<div className="text-2xl font-bold">{walletStats?.transactionCount}</div>
|
||||
</CardContent>
|
||||
</Card>
|
||||
</div>
|
||||
|
||||
{/* Utilization */}
|
||||
<Card>
|
||||
<CardHeader>
|
||||
<CardTitle>Allowance Utilization</CardTitle>
|
||||
<CardDescription>
|
||||
Current spending vs. total allowance
|
||||
</CardDescription>
|
||||
</CardHeader>
|
||||
<CardContent>
|
||||
<div className="space-y-2">
|
||||
<div className="flex justify-between text-sm">
|
||||
<span>Spent: {walletStats?.spentAmount} AITBC</span>
|
||||
<span>Remaining: {walletStats?.remainingAllowance} AITBC</span>
|
||||
</div>
|
||||
<Progress value={walletStats?.utilizationRate || 0} className="w-full" />
|
||||
<div className="text-center text-sm text-muted-foreground">
|
||||
{walletStats?.utilizationRate?.toFixed(1)}% utilized
|
||||
</div>
|
||||
</div>
|
||||
</CardContent>
|
||||
</Card>
|
||||
|
||||
{/* Recent Activity */}
|
||||
<Card>
|
||||
<CardHeader>
|
||||
<CardTitle>Recent Activity</CardTitle>
|
||||
<CardDescription>
|
||||
Latest transactions and wallet events
|
||||
</CardDescription>
|
||||
</CardHeader>
|
||||
<CardContent>
|
||||
<div className="space-y-4">
|
||||
{transactions.slice(0, 5).map((tx) => (
|
||||
<div key={tx.id} className="flex items-center justify-between p-3 border rounded-lg">
|
||||
<div className="flex items-center space-x-3">
|
||||
<div className={`w-2 h-2 rounded-full ${
|
||||
tx.status === 'completed' ? 'bg-green-500' :
|
||||
tx.status === 'pending' ? 'bg-yellow-500' : 'bg-red-500'
|
||||
}`}></div>
|
||||
<div>
|
||||
<p className="font-medium">{tx.purpose}</p>
|
||||
<p className="text-sm text-muted-foreground">
|
||||
To: {tx.recipient.slice(0, 8)}...{tx.recipient.slice(-6)}
|
||||
</p>
|
||||
</div>
|
||||
</div>
|
||||
<div className="text-right">
|
||||
<p className="font-medium">{tx.amount} AITBC</p>
|
||||
<p className="text-xs text-muted-foreground">
|
||||
{new Date(tx.timestamp).toLocaleDateString()}
|
||||
</p>
|
||||
</div>
|
||||
</div>
|
||||
))}
|
||||
</div>
|
||||
</CardContent>
|
||||
</Card>
|
||||
</TabsContent>
|
||||
|
||||
<TabsContent value="transactions" className="space-y-6">
|
||||
<Card>
|
||||
<CardHeader>
|
||||
<CardTitle>Transaction History</CardTitle>
|
||||
<CardDescription>
|
||||
All transactions for this agent wallet
|
||||
</CardDescription>
|
||||
</CardHeader>
|
||||
<CardContent>
|
||||
<div className="space-y-4">
|
||||
{transactions.map((tx) => (
|
||||
<div key={tx.id} className="flex items-center justify-between p-4 border rounded-lg">
|
||||
<div className="flex items-center space-x-4">
|
||||
<div className={`w-3 h-3 rounded-full ${
|
||||
tx.status === 'completed' ? 'bg-green-500' :
|
||||
tx.status === 'pending' ? 'bg-yellow-500' : 'bg-red-500'
|
||||
}`}></div>
|
||||
<div>
|
||||
<p className="font-medium">{tx.purpose}</p>
|
||||
<p className="text-sm text-muted-foreground">
|
||||
{tx.isMicroTransaction ? 'Micro-transaction' : 'Standard transaction'}
|
||||
</p>
|
||||
<p className="text-xs text-muted-foreground">
|
||||
To: {tx.recipient}
|
||||
</p>
|
||||
</div>
|
||||
</div>
|
||||
<div className="text-right">
|
||||
<p className="font-medium">{tx.amount} AITBC</p>
|
||||
<p className="text-sm text-muted-foreground">
|
||||
{new Date(tx.timestamp).toLocaleString()}
|
||||
</p>
|
||||
<Badge variant={tx.status === 'completed' ? "default" : "secondary"}>
|
||||
{tx.status}
|
||||
</Badge>
|
||||
</div>
|
||||
</div>
|
||||
))}
|
||||
</div>
|
||||
</CardContent>
|
||||
</Card>
|
||||
</TabsContent>
|
||||
|
||||
<TabsContent value="manage" className="space-y-6">
|
||||
{/* Grant Allowance */}
|
||||
<Card>
|
||||
<CardHeader>
|
||||
<CardTitle>Grant Allowance</CardTitle>
|
||||
<CardDescription>
|
||||
Add funds to the agent's allowance
|
||||
</CardDescription>
|
||||
</CardHeader>
|
||||
<CardContent className="space-y-4">
|
||||
<div className="space-y-2">
|
||||
<label className="text-sm font-medium">Amount (AITBC)</label>
|
||||
<Input
|
||||
type="number"
|
||||
placeholder="Enter amount"
|
||||
value={allowanceAmount}
|
||||
onChange={(e) => setAllowanceAmount(e.target.value)}
|
||||
/>
|
||||
</div>
|
||||
<Button onClick={handleGrantAllowance} className="w-full">
|
||||
<ArrowDownLeft className="w-4 h-4 mr-2" />
|
||||
Grant Allowance
|
||||
</Button>
|
||||
</CardContent>
|
||||
</Card>
|
||||
|
||||
{/* Execute Transaction */}
|
||||
<Card>
|
||||
<CardHeader>
|
||||
<CardTitle>Execute Transaction</CardTitle>
|
||||
<CardDescription>
|
||||
Send funds from agent wallet
|
||||
</CardDescription>
|
||||
</CardHeader>
|
||||
<CardContent className="space-y-4">
|
||||
<div className="space-y-2">
|
||||
<label className="text-sm font-medium">Recipient</label>
|
||||
<Input
|
||||
placeholder="Enter recipient address"
|
||||
value={recipient}
|
||||
onChange={(e) => setRecipient(e.target.value)}
|
||||
/>
|
||||
</div>
|
||||
<div className="space-y-2">
|
||||
<label className="text-sm font-medium">Amount (AITBC)</label>
|
||||
<Input
|
||||
type="number"
|
||||
placeholder="Enter amount"
|
||||
value={amount}
|
||||
onChange={(e) => setAmount(e.target.value)}
|
||||
/>
|
||||
</div>
|
||||
<div className="space-y-2">
|
||||
<label className="text-sm font-medium">Purpose</label>
|
||||
<Input
|
||||
placeholder="Enter transaction purpose"
|
||||
value={purpose}
|
||||
onChange={(e) => setPurpose(e.target.value)}
|
||||
/>
|
||||
</div>
|
||||
<Button onClick={handleExecuteTransaction} className="w-full">
|
||||
<Send className="w-4 h-4 mr-2" />
|
||||
Execute Transaction
|
||||
</Button>
|
||||
</CardContent>
|
||||
</Card>
|
||||
|
||||
{/* Update Spending Limit */}
|
||||
<Card>
|
||||
<CardHeader>
|
||||
<CardTitle>Update Spending Limit</CardTitle>
|
||||
<CardDescription>
|
||||
Set maximum spending limit per period
|
||||
</CardDescription>
|
||||
</CardHeader>
|
||||
<CardContent className="space-y-4">
|
||||
<div className="space-y-2">
|
||||
<label className="text-sm font-medium">New Limit (AITBC)</label>
|
||||
<Input
|
||||
type="number"
|
||||
placeholder="Enter spending limit"
|
||||
value={spendingLimit}
|
||||
onChange={(e) => setSpendingLimit(e.target.value)}
|
||||
/>
|
||||
</div>
|
||||
<Button onClick={handleUpdateSpendingLimit} className="w-full">
|
||||
<Settings className="w-4 h-4 mr-2" />
|
||||
Update Limit
|
||||
</Button>
|
||||
</CardContent>
|
||||
</Card>
|
||||
</TabsContent>
|
||||
|
||||
<TabsContent value="settings" className="space-y-6">
|
||||
{/* Wallet Settings */}
|
||||
<Card>
|
||||
<CardHeader>
|
||||
<CardTitle>Wallet Settings</CardTitle>
|
||||
<CardDescription>
|
||||
Configure agent wallet behavior
|
||||
</CardDescription>
|
||||
</CardHeader>
|
||||
<CardContent className="space-y-4">
|
||||
<div className="flex items-center justify-between">
|
||||
<div>
|
||||
<p className="font-medium">Micro-transactions</p>
|
||||
<p className="text-sm text-muted-foreground">
|
||||
Enable transactions below 0.001 AITBC
|
||||
</p>
|
||||
</div>
|
||||
<Button
|
||||
variant={selectedWallet.microTransactionEnabled ? "default" : "outline"}
|
||||
onClick={handleToggleMicroTransactions}
|
||||
>
|
||||
{selectedWallet.microTransactionEnabled ? "Enabled" : "Disabled"}
|
||||
</Button>
|
||||
</div>
|
||||
<Separator />
|
||||
<div className="space-y-2">
|
||||
<p className="font-medium">Wallet Information</p>
|
||||
<div className="grid grid-cols-1 md:grid-cols-2 gap-4 text-sm">
|
||||
<div>
|
||||
<span className="text-muted-foreground">Agent ID:</span>
|
||||
<p className="font-mono">{selectedWallet.agentId}</p>
|
||||
</div>
|
||||
<div>
|
||||
<span className="text-muted-foreground">Owner:</span>
|
||||
<p className="font-mono">{selectedWallet.owner.slice(0, 8)}...{selectedWallet.owner.slice(-6)}</p>
|
||||
</div>
|
||||
<div>
|
||||
<span className="text-muted-foreground">Created:</span>
|
||||
<p>{new Date(selectedWallet.createdAt).toLocaleDateString()}</p>
|
||||
</div>
|
||||
<div>
|
||||
<span className="text-muted-foreground">Last Activity:</span>
|
||||
<p>{new Date(selectedWallet.lastActivity).toLocaleDateString()}</p>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</CardContent>
|
||||
</Card>
|
||||
|
||||
{/* Security */}
|
||||
<Card>
|
||||
<CardHeader>
|
||||
<CardTitle className="flex items-center space-x-2">
|
||||
<Shield className="w-5 h-5" />
|
||||
<span>Security</span>
|
||||
</CardTitle>
|
||||
<CardDescription>
|
||||
Security settings and permissions
|
||||
</CardDescription>
|
||||
</CardHeader>
|
||||
<CardContent>
|
||||
<Alert>
|
||||
<CheckCircle className="h-4 w-4" />
|
||||
<AlertTitle>Wallet Secured</AlertTitle>
|
||||
<AlertDescription>
|
||||
This agent wallet is protected by smart contract security measures and spending limits.
|
||||
</AlertDescription>
|
||||
</Alert>
|
||||
</CardContent>
|
||||
</Card>
|
||||
</TabsContent>
|
||||
</Tabs>
|
||||
) : (
|
||||
<Card>
|
||||
<CardContent className="pt-6">
|
||||
<div className="text-center py-8">
|
||||
<Wallet className="w-12 h-12 text-muted-foreground mx-auto mb-4" />
|
||||
<p className="text-muted-foreground">Select an agent wallet to manage</p>
|
||||
</div>
|
||||
</CardContent>
|
||||
</Card>
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
};
|
||||
|
||||
export default AgentWallet;
|
||||
984
apps/marketplace/src/components/BidStrategy.tsx
Normal file
984
apps/marketplace/src/components/BidStrategy.tsx
Normal file
@@ -0,0 +1,984 @@
|
||||
import React, { useState, useEffect } from 'react';
|
||||
import { Card, CardHeader, CardTitle, CardDescription, CardContent, CardFooter } from './ui/card';
|
||||
import { Button } from './ui/button';
|
||||
import { Badge } from './ui/badge';
|
||||
import { Input } from './ui/input';
|
||||
import { Select, SelectContent, SelectItem, SelectTrigger, SelectValue } from './ui/select';
|
||||
import { Tabs, TabsContent, TabsList, TabsTrigger } from './ui/tabs';
|
||||
import { Alert, AlertDescription, AlertTitle } from './ui/alert';
|
||||
import { Progress } from './ui/progress';
|
||||
import { Separator } from './ui/separator';
|
||||
import {
|
||||
TrendingUp,
|
||||
Brain,
|
||||
Clock,
|
||||
DollarSign,
|
||||
Activity,
|
||||
Zap,
|
||||
Shield,
|
||||
AlertTriangle,
|
||||
CheckCircle,
|
||||
BarChart3,
|
||||
Settings,
|
||||
Target,
|
||||
Timer,
|
||||
Coins
|
||||
} from 'lucide-react';
|
||||
import { useToast } from '@/hooks/use-toast';
|
||||
import { useWallet } from '@/hooks/use-wallet';
|
||||
|
||||
interface BidStrategy {
|
||||
id: string;
|
||||
name: string;
|
||||
description: string;
|
||||
confidenceScore: number;
|
||||
successProbability: number;
|
||||
expectedWaitTime: number;
|
||||
bidPrice: number;
|
||||
costEfficiency: number;
|
||||
reasoning: string[];
|
||||
marketConditions: {
|
||||
demandLevel: number;
|
||||
priceVolatility: number;
|
||||
averagePrice: number;
|
||||
};
|
||||
}
|
||||
|
||||
interface MarketAnalysis {
|
||||
currentConditions: {
|
||||
demandLevel: number;
|
||||
priceVolatility: number;
|
||||
averageHourlyPrice: number;
|
||||
gpuUtilizationRate: number;
|
||||
};
|
||||
priceTrend: string;
|
||||
demandTrend: string;
|
||||
volatilityTrend: string;
|
||||
futurePrediction: {
|
||||
demandLevel: number;
|
||||
averageHourlyPrice: number;
|
||||
};
|
||||
recommendations: string[];
|
||||
}
|
||||
|
||||
interface AgentPreferences {
|
||||
preferredStrategy: string;
|
||||
riskTolerance: number;
|
||||
costSensitivity: number;
|
||||
urgencyPreference: number;
|
||||
maxWaitTime: number;
|
||||
minSuccessProbability: number;
|
||||
}
|
||||
|
||||
const BidStrategy: React.FC = () => {
|
||||
const { toast } = useToast();
|
||||
const { isConnected, address } = useWallet();
|
||||
|
||||
const [strategies, setStrategies] = useState<BidStrategy[]>([]);
|
||||
const [selectedStrategy, setSelectedStrategy] = useState<BidStrategy | null>(null);
|
||||
const [marketAnalysis, setMarketAnalysis] = useState<MarketAnalysis | null>(null);
|
||||
const [agentPreferences, setAgentPreferences] = useState<AgentPreferences>({
|
||||
preferredStrategy: 'balanced',
|
||||
riskTolerance: 0.5,
|
||||
costSensitivity: 0.5,
|
||||
urgencyPreference: 0.5,
|
||||
maxWaitTime: 3600,
|
||||
minSuccessProbability: 0.7
|
||||
});
|
||||
const [loading, setLoading] = useState(true);
|
||||
const [activeTab, setActiveTab] = useState('strategies');
|
||||
|
||||
// Form states
|
||||
const [taskUrgency, setTaskUrgency] = useState('medium');
|
||||
const [taskDuration, setTaskDuration] = useState('1');
|
||||
const [gpuTier, setGpuTier] = useState('mid_range');
|
||||
const [maxBudget, setMaxBudget] = useState('0.1');
|
||||
const [customStrategy, setCustomStrategy] = useState('');
|
||||
|
||||
// Mock data for demonstration
|
||||
const mockStrategies: BidStrategy[] = [
|
||||
{
|
||||
id: 'urgent_bid',
|
||||
name: 'Urgent Bid',
|
||||
description: 'Aggressive bidding for time-critical tasks',
|
||||
confidenceScore: 0.85,
|
||||
successProbability: 0.92,
|
||||
expectedWaitTime: 120, // seconds
|
||||
bidPrice: 0.08,
|
||||
costEfficiency: 0.65,
|
||||
reasoning: [
|
||||
'High market demand increases bid price',
|
||||
'Critical urgency requires aggressive bidding',
|
||||
'Market conditions require price premium',
|
||||
'High risk premium applied due to strategy'
|
||||
],
|
||||
marketConditions: {
|
||||
demandLevel: 0.75,
|
||||
priceVolatility: 0.12,
|
||||
averagePrice: 0.05
|
||||
}
|
||||
},
|
||||
{
|
||||
id: 'cost_optimized',
|
||||
name: 'Cost Optimized',
|
||||
description: 'Minimize cost while maintaining reasonable success probability',
|
||||
confidenceScore: 0.78,
|
||||
successProbability: 0.68,
|
||||
expectedWaitTime: 480,
|
||||
bidPrice: 0.03,
|
||||
costEfficiency: 0.92,
|
||||
reasoning: [
|
||||
'Low market demand allows for competitive pricing',
|
||||
'Cost optimization prioritized over speed',
|
||||
'Favorable market conditions enable discount pricing',
|
||||
'Budget constraints drive conservative bidding'
|
||||
],
|
||||
marketConditions: {
|
||||
demandLevel: 0.45,
|
||||
priceVolatility: 0.08,
|
||||
averagePrice: 0.05
|
||||
}
|
||||
},
|
||||
{
|
||||
id: 'balanced',
|
||||
name: 'Balanced',
|
||||
description: 'Optimal balance between cost and performance',
|
||||
confidenceScore: 0.88,
|
||||
successProbability: 0.82,
|
||||
expectedWaitTime: 240,
|
||||
bidPrice: 0.05,
|
||||
costEfficiency: 0.78,
|
||||
reasoning: [
|
||||
'Balanced approach selected based on task requirements',
|
||||
'Market conditions support standard pricing',
|
||||
'Moderate urgency allows for balanced bidding',
|
||||
'Risk premium adjusted for market stability'
|
||||
],
|
||||
marketConditions: {
|
||||
demandLevel: 0.60,
|
||||
priceVolatility: 0.10,
|
||||
averagePrice: 0.05
|
||||
}
|
||||
},
|
||||
{
|
||||
id: 'aggressive',
|
||||
name: 'Aggressive',
|
||||
description: 'High-risk, high-reward bidding strategy',
|
||||
confidenceScore: 0.72,
|
||||
successProbability: 0.88,
|
||||
expectedWaitTime: 90,
|
||||
bidPrice: 0.10,
|
||||
costEfficiency: 0.55,
|
||||
reasoning: [
|
||||
'High demand detected - consider urgent bidding strategy',
|
||||
'Aggressive approach for maximum success probability',
|
||||
'Market volatility allows for premium pricing',
|
||||
'High risk premium applied due to strategy'
|
||||
],
|
||||
marketConditions: {
|
||||
demandLevel: 0.85,
|
||||
priceVolatility: 0.18,
|
||||
averagePrice: 0.05
|
||||
}
|
||||
},
|
||||
{
|
||||
id: 'conservative',
|
||||
name: 'Conservative',
|
||||
description: 'Low-risk bidding with focus on reliability',
|
||||
confidenceScore: 0.91,
|
||||
successProbability: 0.58,
|
||||
expectedWaitTime: 600,
|
||||
bidPrice: 0.025,
|
||||
costEfficiency: 0.85,
|
||||
reasoning: [
|
||||
'High volatility - consider conservative bidding',
|
||||
'Low risk tolerance drives conservative approach',
|
||||
'Market uncertainty requires price caution',
|
||||
'Reliability prioritized over speed'
|
||||
],
|
||||
marketConditions: {
|
||||
demandLevel: 0.35,
|
||||
priceVolatility: 0.22,
|
||||
averagePrice: 0.05
|
||||
}
|
||||
}
|
||||
];
|
||||
|
||||
const mockMarketAnalysis: MarketAnalysis = {
|
||||
currentConditions: {
|
||||
demandLevel: 0.68,
|
||||
priceVolatility: 0.12,
|
||||
averageHourlyPrice: 0.05,
|
||||
gpuUtilizationRate: 0.75
|
||||
},
|
||||
priceTrend: 'stable',
|
||||
demandTrend: 'increasing',
|
||||
volatilityTrend: 'stable',
|
||||
futurePrediction: {
|
||||
demandLevel: 0.72,
|
||||
averageHourlyPrice: 0.052
|
||||
},
|
||||
recommendations: [
|
||||
'High demand detected - consider urgent bidding strategy',
|
||||
'GPU utilization very high - expect longer wait times',
|
||||
'Low prices - good opportunity for cost optimization'
|
||||
]
|
||||
};
|
||||
|
||||
useEffect(() => {
|
||||
// Load mock data
|
||||
setTimeout(() => {
|
||||
setStrategies(mockStrategies);
|
||||
setMarketAnalysis(mockMarketAnalysis);
|
||||
setLoading(false);
|
||||
}, 1000);
|
||||
}, []);
|
||||
|
||||
const handleCalculateBid = async () => {
|
||||
if (!isConnected) {
|
||||
toast({
|
||||
title: "Wallet Not Connected",
|
||||
description: "Please connect your wallet to calculate bids",
|
||||
variant: "destructive"
|
||||
});
|
||||
return;
|
||||
}
|
||||
|
||||
if (!taskDuration || !maxBudget) {
|
||||
toast({
|
||||
title: "Missing Information",
|
||||
description: "Please fill in all task details",
|
||||
variant: "destructive"
|
||||
});
|
||||
return;
|
||||
}
|
||||
|
||||
try {
|
||||
setLoading(true);
|
||||
|
||||
// Simulate bid calculation
|
||||
toast({
|
||||
title: "Calculating Bid Strategy",
|
||||
description: "Analyzing market conditions and optimizing bid...",
|
||||
variant: "default"
|
||||
});
|
||||
|
||||
// Simulate calculation delay
|
||||
await new Promise(resolve => setTimeout(resolve, 2000));
|
||||
|
||||
// Select best strategy based on preferences
|
||||
const bestStrategy = strategies.find(s => s.id === agentPreferences.preferredStrategy) ||
|
||||
strategies.reduce((best, current) =>
|
||||
current.costEfficiency > best.costEfficiency ? current : best
|
||||
);
|
||||
|
||||
setSelectedStrategy(bestStrategy);
|
||||
setActiveTab('strategies');
|
||||
|
||||
toast({
|
||||
title: "Bid Strategy Calculated",
|
||||
description: `Optimal strategy: ${bestStrategy.name}`,
|
||||
variant: "default"
|
||||
});
|
||||
} catch (error) {
|
||||
toast({
|
||||
title: "Calculation Failed",
|
||||
description: "There was an error calculating the bid strategy",
|
||||
variant: "destructive"
|
||||
});
|
||||
} finally {
|
||||
setLoading(false);
|
||||
}
|
||||
};
|
||||
|
||||
const handleUpdatePreferences = async () => {
|
||||
if (!isConnected) {
|
||||
toast({
|
||||
title: "Wallet Not Connected",
|
||||
description: "Please connect your wallet to update preferences",
|
||||
variant: "destructive"
|
||||
});
|
||||
return;
|
||||
}
|
||||
|
||||
try {
|
||||
toast({
|
||||
title: "Updating Preferences",
|
||||
description: "Saving your agent bidding preferences...",
|
||||
variant: "default"
|
||||
});
|
||||
|
||||
// Simulate update
|
||||
await new Promise(resolve => setTimeout(resolve, 1000));
|
||||
|
||||
toast({
|
||||
title: "Preferences Updated",
|
||||
description: "Your bidding preferences have been saved",
|
||||
variant: "default"
|
||||
});
|
||||
} catch (error) {
|
||||
toast({
|
||||
title: "Update Failed",
|
||||
description: "There was an error updating preferences",
|
||||
variant: "destructive"
|
||||
});
|
||||
}
|
||||
};
|
||||
|
||||
const getStrategyColor = (strategy: BidStrategy) => {
|
||||
if (strategy.successProbability > 0.8) return 'bg-green-500';
|
||||
if (strategy.successProbability > 0.6) return 'bg-yellow-500';
|
||||
return 'bg-red-500';
|
||||
};
|
||||
|
||||
const getTrendIcon = (trend: string) => {
|
||||
switch (trend) {
|
||||
case 'increasing': return <TrendingUp className="w-4 h-4 text-green-500" />;
|
||||
case 'decreasing': return <TrendingUp className="w-4 h-4 text-red-500 rotate-180" />;
|
||||
default: return <Activity className="w-4 h-4 text-blue-500" />;
|
||||
}
|
||||
};
|
||||
|
||||
if (loading) {
|
||||
return (
|
||||
<div className="container mx-auto p-6 space-y-6">
|
||||
<div className="text-center py-8">
|
||||
<div className="animate-spin rounded-full h-8 w-8 border-b-2 border-primary mx-auto"></div>
|
||||
<p className="mt-2 text-muted-foreground">Loading bid strategies...</p>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
return (
|
||||
<div className="container mx-auto p-6 space-y-6">
|
||||
<div className="flex items-center justify-between">
|
||||
<div>
|
||||
<h1 className="text-3xl font-bold">Bid Strategy Engine</h1>
|
||||
<p className="text-muted-foreground mt-2">
|
||||
Intelligent bidding algorithms for optimal GPU rental negotiations
|
||||
</p>
|
||||
</div>
|
||||
<div className="flex items-center space-x-2">
|
||||
<Badge variant="outline" className="flex items-center space-x-1">
|
||||
<Brain className="w-4 h-4" />
|
||||
<span>{strategies.length} Strategies</span>
|
||||
</Badge>
|
||||
<Badge variant="outline" className="flex items-center space-x-1">
|
||||
<Activity className="w-4 h-4" />
|
||||
<span>Market Active</span>
|
||||
</Badge>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<Tabs value={activeTab} onValueChange={setActiveTab}>
|
||||
<TabsList className="grid w-full grid-cols-4">
|
||||
<TabsTrigger value="strategies">Strategies</TabsTrigger>
|
||||
<TabsTrigger value="market">Market Analysis</TabsTrigger>
|
||||
<TabsTrigger value="calculate">Calculate Bid</TabsTrigger>
|
||||
<TabsTrigger value="preferences">Preferences</TabsTrigger>
|
||||
</TabsList>
|
||||
|
||||
<TabsContent value="strategies" className="space-y-6">
|
||||
{/* Selected Strategy Details */}
|
||||
{selectedStrategy && (
|
||||
<Card>
|
||||
<CardHeader>
|
||||
<CardTitle className="flex items-center space-x-2">
|
||||
<Target className="w-5 h-5" />
|
||||
<span>Selected Strategy: {selectedStrategy.name}</span>
|
||||
</CardTitle>
|
||||
<CardDescription>{selectedStrategy.description}</CardDescription>
|
||||
</CardHeader>
|
||||
<CardContent className="space-y-6">
|
||||
{/* Strategy Metrics */}
|
||||
<div className="grid grid-cols-1 md:grid-cols-2 lg:grid-cols-4 gap-4">
|
||||
<Card>
|
||||
<CardContent className="pt-6">
|
||||
<div className="flex items-center space-x-2">
|
||||
<DollarSign className="w-4 h-4 text-muted-foreground" />
|
||||
<span className="text-sm font-medium">Bid Price</span>
|
||||
</div>
|
||||
<div className="text-2xl font-bold">{selectedStrategy.bidPrice} AITBC</div>
|
||||
</CardContent>
|
||||
</Card>
|
||||
<Card>
|
||||
<CardContent className="pt-6">
|
||||
<div className="flex items-center space-x-2">
|
||||
<CheckCircle className="w-4 h-4 text-muted-foreground" />
|
||||
<span className="text-sm font-medium">Success Rate</span>
|
||||
</div>
|
||||
<div className="text-2xl font-bold">{(selectedStrategy.successProbability * 100).toFixed(1)}%</div>
|
||||
</CardContent>
|
||||
</Card>
|
||||
<Card>
|
||||
<CardContent className="pt-6">
|
||||
<div className="flex items-center space-x-2">
|
||||
<Timer className="w-4 h-4 text-muted-foreground" />
|
||||
<span className="text-sm font-medium">Wait Time</span>
|
||||
</div>
|
||||
<div className="text-2xl font-bold">{Math.floor(selectedStrategy.expectedWaitTime / 60)}m</div>
|
||||
</CardContent>
|
||||
</Card>
|
||||
<Card>
|
||||
<CardContent className="pt-6">
|
||||
<div className="flex items-center space-x-2">
|
||||
<Coins className="w-4 h-4 text-muted-foreground" />
|
||||
<span className="text-sm font-medium">Efficiency</span>
|
||||
</div>
|
||||
<div className="text-2xl font-bold">{(selectedStrategy.costEfficiency * 100).toFixed(1)}%</div>
|
||||
</CardContent>
|
||||
</Card>
|
||||
</div>
|
||||
|
||||
{/* Reasoning */}
|
||||
<Card>
|
||||
<CardHeader>
|
||||
<CardTitle>Strategy Reasoning</CardTitle>
|
||||
<CardDescription>
|
||||
Why this strategy was selected
|
||||
</CardDescription>
|
||||
</CardHeader>
|
||||
<CardContent>
|
||||
<div className="space-y-2">
|
||||
{selectedStrategy.reasoning.map((reason, index) => (
|
||||
<div key={index} className="flex items-start space-x-2">
|
||||
<div className={`w-2 h-2 rounded-full mt-2 ${getStrategyColor(selectedStrategy)}`}></div>
|
||||
<p className="text-sm">{reason}</p>
|
||||
</div>
|
||||
))}
|
||||
</div>
|
||||
</CardContent>
|
||||
</Card>
|
||||
|
||||
{/* Market Conditions */}
|
||||
<Card>
|
||||
<CardHeader>
|
||||
<CardTitle>Market Conditions</CardTitle>
|
||||
<CardDescription>
|
||||
Current market analysis
|
||||
</CardDescription>
|
||||
</CardHeader>
|
||||
<CardContent>
|
||||
<div className="grid grid-cols-1 md:grid-cols-3 gap-4">
|
||||
<div>
|
||||
<p className="text-sm text-muted-foreground">Demand Level</p>
|
||||
<div className="flex items-center space-x-2">
|
||||
<Progress value={selectedStrategy.marketConditions.demandLevel * 100} className="flex-1" />
|
||||
<span className="text-sm font-medium">{(selectedStrategy.marketConditions.demandLevel * 100).toFixed(0)}%</span>
|
||||
</div>
|
||||
</div>
|
||||
<div>
|
||||
<p className="text-sm text-muted-foreground">Price Volatility</p>
|
||||
<div className="flex items-center space-x-2">
|
||||
<Progress value={selectedStrategy.marketConditions.priceVolatility * 100} className="flex-1" />
|
||||
<span className="text-sm font-medium">{(selectedStrategy.marketConditions.priceVolatility * 100).toFixed(0)}%</span>
|
||||
</div>
|
||||
</div>
|
||||
<div>
|
||||
<p className="text-sm text-muted-foreground">Avg Price</p>
|
||||
<p className="text-lg font-bold">{selectedStrategy.marketConditions.averagePrice} AITBC</p>
|
||||
</div>
|
||||
</div>
|
||||
</CardContent>
|
||||
</Card>
|
||||
</CardContent>
|
||||
</Card>
|
||||
)}
|
||||
|
||||
{/* All Strategies */}
|
||||
<div className="grid grid-cols-1 md:grid-cols-2 lg:grid-cols-3 gap-6">
|
||||
{strategies.map((strategy) => (
|
||||
<Card
|
||||
key={strategy.id}
|
||||
className={`cursor-pointer transition-all hover:shadow-lg ${
|
||||
selectedStrategy?.id === strategy.id ? 'ring-2 ring-primary' : ''
|
||||
}`}
|
||||
onClick={() => setSelectedStrategy(strategy)}
|
||||
>
|
||||
<CardHeader>
|
||||
<div className="flex items-start justify-between">
|
||||
<div className="flex-1">
|
||||
<CardTitle className="text-lg">{strategy.name}</CardTitle>
|
||||
<CardDescription className="mt-1">{strategy.description}</CardDescription>
|
||||
</div>
|
||||
<div className={`w-3 h-3 rounded-full ${getStrategyColor(strategy)}`}></div>
|
||||
</div>
|
||||
</CardHeader>
|
||||
<CardContent className="space-y-4">
|
||||
<div className="grid grid-cols-2 gap-4 text-sm">
|
||||
<div>
|
||||
<span className="text-muted-foreground">Price:</span>
|
||||
<p className="font-medium">{strategy.bidPrice} AITBC</p>
|
||||
</div>
|
||||
<div>
|
||||
<span className="text-muted-foreground">Success:</span>
|
||||
<p className="font-medium">{(strategy.successProbability * 100).toFixed(1)}%</p>
|
||||
</div>
|
||||
<div>
|
||||
<span className="text-muted-foreground">Wait:</span>
|
||||
<p className="font-medium">{Math.floor(strategy.expectedWaitTime / 60)}m</p>
|
||||
</div>
|
||||
<div>
|
||||
<span className="text-muted-foreground">Efficiency:</span>
|
||||
<p className="font-medium">{(strategy.costEfficiency * 100).toFixed(1)}%</p>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div className="flex items-center space-x-2">
|
||||
<div className="flex-1">
|
||||
<div className="flex items-center space-x-1">
|
||||
<span className="text-xs text-muted-foreground">Confidence:</span>
|
||||
<Progress value={strategy.confidenceScore * 100} className="flex-1 h-2" />
|
||||
</div>
|
||||
</div>
|
||||
<span className="text-xs font-medium">{(strategy.confidenceScore * 100).toFixed(0)}%</span>
|
||||
</div>
|
||||
</CardContent>
|
||||
</Card>
|
||||
))}
|
||||
</div>
|
||||
</TabsContent>
|
||||
|
||||
<TabsContent value="market" className="space-y-6">
|
||||
{/* Current Market Conditions */}
|
||||
<Card>
|
||||
<CardHeader>
|
||||
<CardTitle className="flex items-center space-x-2">
|
||||
<BarChart3 className="w-5 h-5" />
|
||||
<span>Current Market Conditions</span>
|
||||
</CardTitle>
|
||||
<CardDescription>
|
||||
Real-time market analysis and trends
|
||||
</CardDescription>
|
||||
</CardHeader>
|
||||
<CardContent>
|
||||
<div className="grid grid-cols-1 md:grid-cols-2 lg:grid-cols-4 gap-6">
|
||||
<div>
|
||||
<p className="text-sm text-muted-foreground">Demand Level</p>
|
||||
<div className="flex items-center space-x-2 mt-1">
|
||||
<Progress value={marketAnalysis!.currentConditions.demandLevel * 100} className="flex-1" />
|
||||
<span className="text-sm font-medium">{(marketAnalysis!.currentConditions.demandLevel * 100).toFixed(0)}%</span>
|
||||
</div>
|
||||
</div>
|
||||
<div>
|
||||
<p className="text-sm text-muted-foreground">Price Volatility</p>
|
||||
<div className="flex items-center space-x-2 mt-1">
|
||||
<Progress value={marketAnalysis!.currentConditions.priceVolatility * 100} className="flex-1" />
|
||||
<span className="text-sm font-medium">{(marketAnalysis!.currentConditions.priceVolatility * 100).toFixed(0)}%</span>
|
||||
</div>
|
||||
</div>
|
||||
<div>
|
||||
<p className="text-sm text-muted-foreground">Avg Hourly Price</p>
|
||||
<p className="text-lg font-bold mt-1">{marketAnalysis!.currentConditions.averageHourlyPrice} AITBC</p>
|
||||
</div>
|
||||
<div>
|
||||
<p className="text-sm text-muted-foreground">GPU Utilization</p>
|
||||
<div className="flex items-center space-x-2 mt-1">
|
||||
<Progress value={marketAnalysis!.currentConditions.gpuUtilizationRate * 100} className="flex-1" />
|
||||
<span className="text-sm font-medium">{(marketAnalysis!.currentConditions.gpuUtilizationRate * 100).toFixed(0)}%</span>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</CardContent>
|
||||
</Card>
|
||||
|
||||
{/* Market Trends */}
|
||||
<div className="grid grid-cols-1 md:grid-cols-3 gap-6">
|
||||
<Card>
|
||||
<CardHeader>
|
||||
<CardTitle className="flex items-center space-x-2">
|
||||
{getTrendIcon(marketAnalysis!.priceTrend)}
|
||||
<span>Price Trend</span>
|
||||
</CardTitle>
|
||||
</CardHeader>
|
||||
<CardContent>
|
||||
<p className="text-2xl font-bold capitalize">{marketAnalysis!.priceTrend}</p>
|
||||
<p className="text-sm text-muted-foreground mt-1">
|
||||
Based on 24-hour analysis
|
||||
</p>
|
||||
</CardContent>
|
||||
</Card>
|
||||
|
||||
<Card>
|
||||
<CardHeader>
|
||||
<CardTitle className="flex items-center space-x-2">
|
||||
{getTrendIcon(marketAnalysis!.demandTrend)}
|
||||
<span>Demand Trend</span>
|
||||
</CardTitle>
|
||||
</CardHeader>
|
||||
<CardContent>
|
||||
<p className="text-2xl font-bold capitalize">{marketAnalysis!.demandTrend}</p>
|
||||
<p className="text-sm text-muted-foreground mt-1">
|
||||
Based on recent activity
|
||||
</p>
|
||||
</CardContent>
|
||||
</Card>
|
||||
|
||||
<Card>
|
||||
<CardHeader>
|
||||
<CardTitle className="flex items-center space-x-2">
|
||||
{getTrendIcon(marketAnalysis!.volatilityTrend)}
|
||||
<span>Volatility Trend</span>
|
||||
</CardTitle>
|
||||
</CardHeader>
|
||||
<CardContent>
|
||||
<p className="text-2xl font-bold capitalize">{marketAnalysis!.volatilityTrend}</p>
|
||||
<p className="text-sm text-muted-foreground mt-1">
|
||||
Market stability indicator
|
||||
</p>
|
||||
</CardContent>
|
||||
</Card>
|
||||
</div>
|
||||
|
||||
{/* Future Prediction */}
|
||||
<Card>
|
||||
<CardHeader>
|
||||
<CardTitle className="flex items-center space-x-2">
|
||||
<Zap className="w-5 h-5" />
|
||||
<span>24-Hour Prediction</span>
|
||||
</CardTitle>
|
||||
<CardDescription>
|
||||
AI-powered market forecast
|
||||
</CardDescription>
|
||||
</CardHeader>
|
||||
<CardContent>
|
||||
<div className="grid grid-cols-1 md:grid-cols-2 gap-6">
|
||||
<div>
|
||||
<p className="text-sm text-muted-foreground">Predicted Demand</p>
|
||||
<div className="flex items-center space-x-2 mt-1">
|
||||
<Progress value={marketAnalysis!.futurePrediction.demandLevel * 100} className="flex-1" />
|
||||
<span className="text-sm font-medium">{(marketAnalysis!.futurePrediction.demandLevel * 100).toFixed(0)}%</span>
|
||||
</div>
|
||||
</div>
|
||||
<div>
|
||||
<p className="text-sm text-muted-foreground">Predicted Price</p>
|
||||
<p className="text-lg font-bold mt-1">{marketAnalysis!.futurePrediction.averageHourlyPrice} AITBC</p>
|
||||
</div>
|
||||
</div>
|
||||
</CardContent>
|
||||
</Card>
|
||||
|
||||
{/* Recommendations */}
|
||||
<Card>
|
||||
<CardHeader>
|
||||
<CardTitle className="flex items-center space-x-2">
|
||||
<AlertTriangle className="w-5 h-5" />
|
||||
<span>Market Recommendations</span>
|
||||
</CardTitle>
|
||||
<CardDescription>
|
||||
AI-generated recommendations based on current conditions
|
||||
</CardDescription>
|
||||
</CardHeader>
|
||||
<CardContent>
|
||||
<div className="space-y-3">
|
||||
{marketAnalysis!.recommendations.map((recommendation, index) => (
|
||||
<div key={index} className="flex items-start space-x-2">
|
||||
<div className="w-2 h-2 rounded-full bg-blue-500 mt-2"></div>
|
||||
<p className="text-sm">{recommendation}</p>
|
||||
</div>
|
||||
))}
|
||||
</div>
|
||||
</CardContent>
|
||||
</Card>
|
||||
</TabsContent>
|
||||
|
||||
<TabsContent value="calculate" className="space-y-6">
|
||||
{/* Task Details */}
|
||||
<Card>
|
||||
<CardHeader>
|
||||
<CardTitle>Task Details</CardTitle>
|
||||
<CardDescription>
|
||||
Enter task requirements to calculate optimal bid strategy
|
||||
</CardDescription>
|
||||
</CardHeader>
|
||||
<CardContent className="space-y-4">
|
||||
<div className="grid grid-cols-1 md:grid-cols-2 gap-4">
|
||||
<div className="space-y-2">
|
||||
<label className="text-sm font-medium">Task Urgency</label>
|
||||
<Select value={taskUrgency} onValueChange={setTaskUrgency}>
|
||||
<SelectTrigger>
|
||||
<SelectValue placeholder="Select urgency" />
|
||||
</SelectTrigger>
|
||||
<SelectContent>
|
||||
<SelectItem value="low">Low</SelectItem>
|
||||
<SelectItem value="medium">Medium</SelectItem>
|
||||
<SelectItem value="high">High</SelectItem>
|
||||
<SelectItem value="critical">Critical</SelectItem>
|
||||
</SelectContent>
|
||||
</Select>
|
||||
</div>
|
||||
|
||||
<div className="space-y-2">
|
||||
<label className="text-sm font-medium">GPU Tier</label>
|
||||
<Select value={gpuTier} onValueChange={setGpuTier}>
|
||||
<SelectTrigger>
|
||||
<SelectValue placeholder="Select GPU tier" />
|
||||
</SelectTrigger>
|
||||
<SelectContent>
|
||||
<SelectItem value="cpu_only">CPU Only</SelectItem>
|
||||
<SelectItem value="low_end_gpu">Low-end GPU</SelectItem>
|
||||
<SelectItem value="mid_range">Mid-range GPU</SelectItem>
|
||||
<SelectItem value="high_end_gpu">High-end GPU</SelectItem>
|
||||
<SelectItem value="premium_gpu">Premium GPU</SelectItem>
|
||||
</SelectContent>
|
||||
</Select>
|
||||
</div>
|
||||
|
||||
<div className="space-y-2">
|
||||
<label className="text-sm font-medium">Duration (hours)</label>
|
||||
<Input
|
||||
type="number"
|
||||
placeholder="Enter duration"
|
||||
value={taskDuration}
|
||||
onChange={(e) => setTaskDuration(e.target.value)}
|
||||
/>
|
||||
</div>
|
||||
|
||||
<div className="space-y-2">
|
||||
<label className="text-sm font-medium">Maximum Budget (AITBC)</label>
|
||||
<Input
|
||||
type="number"
|
||||
placeholder="Enter maximum budget"
|
||||
value={maxBudget}
|
||||
onChange={(e) => setMaxBudget(e.target.value)}
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<Button onClick={handleCalculateBid} className="w-full" disabled={loading}>
|
||||
{loading ? (
|
||||
<>
|
||||
<div className="animate-spin rounded-full h-4 w-4 border-b-2 border-white mr-2"></div>
|
||||
Calculating...
|
||||
</>
|
||||
) : (
|
||||
<>
|
||||
<Brain className="w-4 h-4 mr-2" />
|
||||
Calculate Optimal Bid
|
||||
</>
|
||||
)}
|
||||
</Button>
|
||||
</CardContent>
|
||||
</Card>
|
||||
|
||||
{/* Results */}
|
||||
{selectedStrategy && (
|
||||
<Card>
|
||||
<CardHeader>
|
||||
<CardTitle className="flex items-center space-x-2">
|
||||
<CheckCircle className="w-5 h-5 text-green-500" />
|
||||
<span>Optimal Strategy Found</span>
|
||||
</CardTitle>
|
||||
<CardDescription>
|
||||
Recommended bid strategy for your task
|
||||
</CardDescription>
|
||||
</CardHeader>
|
||||
<CardContent>
|
||||
<div className="space-y-4">
|
||||
<div className="flex items-center justify-between p-4 border rounded-lg">
|
||||
<div>
|
||||
<h4 className="font-semibold">{selectedStrategy.name}</h4>
|
||||
<p className="text-sm text-muted-foreground">{selectedStrategy.description}</p>
|
||||
</div>
|
||||
<div className="text-right">
|
||||
<p className="text-2xl font-bold">{selectedStrategy.bidPrice} AITBC</p>
|
||||
<p className="text-sm text-muted-foreground">Bid Price</p>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div className="grid grid-cols-1 md:grid-cols-3 gap-4">
|
||||
<div className="text-center">
|
||||
<p className="text-sm text-muted-foreground">Success Probability</p>
|
||||
<p className="text-lg font-bold">{(selectedStrategy.successProbability * 100).toFixed(1)}%</p>
|
||||
</div>
|
||||
<div className="text-center">
|
||||
<p className="text-sm text-muted-foreground">Expected Wait</p>
|
||||
<p className="text-lg font-bold">{Math.floor(selectedStrategy.expectedWaitTime / 60)}m</p>
|
||||
</div>
|
||||
<div className="text-center">
|
||||
<p className="text-sm text-muted-foreground">Cost Efficiency</p>
|
||||
<p className="text-lg font-bold">{(selectedStrategy.costEfficiency * 100).toFixed(1)}%</p>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</CardContent>
|
||||
</Card>
|
||||
)}
|
||||
</TabsContent>
|
||||
|
||||
<TabsContent value="preferences" className="space-y-6">
|
||||
{/* Agent Preferences */}
|
||||
<Card>
|
||||
<CardHeader>
|
||||
<CardTitle className="flex items-center space-x-2">
|
||||
<Settings className="w-5 h-5" />
|
||||
<span>Agent Bidding Preferences</span>
|
||||
</CardTitle>
|
||||
<CardDescription>
|
||||
Configure your agent's bidding behavior and risk tolerance
|
||||
</CardDescription>
|
||||
</CardHeader>
|
||||
<CardContent className="space-y-6">
|
||||
<div className="grid grid-cols-1 md:grid-cols-2 gap-6">
|
||||
<div className="space-y-4">
|
||||
<div>
|
||||
<label className="text-sm font-medium">Preferred Strategy</label>
|
||||
<Select value={agentPreferences.preferredStrategy} onValueChange={(value) =>
|
||||
setAgentPreferences(prev => ({ ...prev, preferredStrategy: value }))
|
||||
}>
|
||||
<SelectTrigger>
|
||||
<SelectValue />
|
||||
</SelectTrigger>
|
||||
<SelectContent>
|
||||
<SelectItem value="urgent_bid">Urgent Bid</SelectItem>
|
||||
<SelectItem value="cost_optimized">Cost Optimized</SelectItem>
|
||||
<SelectItem value="balanced">Balanced</SelectItem>
|
||||
<SelectItem value="aggressive">Aggressive</SelectItem>
|
||||
<SelectItem value="conservative">Conservative</SelectItem>
|
||||
</SelectContent>
|
||||
</Select>
|
||||
</div>
|
||||
|
||||
<div>
|
||||
<label className="text-sm font-medium">Risk Tolerance</label>
|
||||
<div className="space-y-2">
|
||||
<Input
|
||||
type="range"
|
||||
min="0"
|
||||
max="1"
|
||||
step="0.1"
|
||||
value={agentPreferences.riskTolerance}
|
||||
onChange={(e) => setAgentPreferences(prev => ({ ...prev, riskTolerance: parseFloat(e.target.value) }))}
|
||||
/>
|
||||
<div className="flex justify-between text-xs text-muted-foreground">
|
||||
<span>Conservative</span>
|
||||
<span>{(agentPreferences.riskTolerance * 100).toFixed(0)}%</span>
|
||||
<span>Aggressive</span>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div className="space-y-4">
|
||||
<div>
|
||||
<label className="text-sm font-medium">Cost Sensitivity</label>
|
||||
<div className="space-y-2">
|
||||
<Input
|
||||
type="range"
|
||||
min="0"
|
||||
max="1"
|
||||
step="0.1"
|
||||
value={agentPreferences.costSensitivity}
|
||||
onChange={(e) => setAgentPreferences(prev => ({ ...prev, costSensitivity: parseFloat(e.target.value) }))}
|
||||
/>
|
||||
<div className="flex justify-between text-xs text-muted-foreground">
|
||||
<span>Performance</span>
|
||||
<span>{(agentPreferences.costSensitivity * 100).toFixed(0)}%</span>
|
||||
<span>Cost</span>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div>
|
||||
<label className="text-sm font-medium">Urgency Preference</label>
|
||||
<div className="space-y-2">
|
||||
<Input
|
||||
type="range"
|
||||
min="0"
|
||||
max="1"
|
||||
step="0.1"
|
||||
value={agentPreferences.urgencyPreference}
|
||||
onChange={(e) => setAgentPreferences(prev => ({ ...prev, urgencyPreference: parseFloat(e.target.value) }))}
|
||||
/>
|
||||
<div className="flex justify-between text-xs text-muted-foreground">
|
||||
<span>Relaxed</span>
|
||||
<span>{(agentPreferences.urgencyPreference * 100).toFixed(0)}%</span>
|
||||
<span>Urgent</span>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div className="grid grid-cols-1 md:grid-cols-2 gap-6">
|
||||
<div>
|
||||
<label className="text-sm font-medium">Maximum Wait Time (seconds)</label>
|
||||
<Input
|
||||
type="number"
|
||||
value={agentPreferences.maxWaitTime}
|
||||
onChange={(e) => setAgentPreferences(prev => ({ ...prev, maxWaitTime: parseInt(e.target.value) }))}
|
||||
/>
|
||||
</div>
|
||||
|
||||
<div>
|
||||
<label className="text-sm font-medium">Minimum Success Probability</label>
|
||||
<div className="space-y-2">
|
||||
<Input
|
||||
type="range"
|
||||
min="0"
|
||||
max="1"
|
||||
step="0.05"
|
||||
value={agentPreferences.minSuccessProbability}
|
||||
onChange={(e) => setAgentPreferences(prev => ({ ...prev, minSuccessProbability: parseFloat(e.target.value) }))}
|
||||
/>
|
||||
<div className="text-center text-sm text-muted-foreground">
|
||||
{(agentPreferences.minSuccessProbability * 100).toFixed(0)}%
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<Button onClick={handleUpdatePreferences} className="w-full">
|
||||
<Settings className="w-4 h-4 mr-2" />
|
||||
Save Preferences
|
||||
</Button>
|
||||
</CardContent>
|
||||
</Card>
|
||||
|
||||
{/* Strategy Preview */}
|
||||
<Card>
|
||||
<CardHeader>
|
||||
<CardTitle>Strategy Impact Preview</CardTitle>
|
||||
<CardDescription>
|
||||
How your preferences affect bidding behavior
|
||||
</CardDescription>
|
||||
</CardHeader>
|
||||
<CardContent>
|
||||
<div className="space-y-4">
|
||||
<Alert>
|
||||
<Shield className="h-4 w-4" />
|
||||
<AlertTitle>Risk Management</AlertTitle>
|
||||
<AlertDescription>
|
||||
Your risk tolerance of {(agentPreferences.riskTolerance * 100).toFixed(0)}% will favor
|
||||
{agentPreferences.riskTolerance > 0.6 ? ' aggressive bidding with higher success rates' : ' conservative bidding with better cost efficiency'}.
|
||||
</AlertDescription>
|
||||
</Alert>
|
||||
|
||||
<Alert>
|
||||
<DollarSign className="h-4 w-4" />
|
||||
<AlertTitle>Cost Optimization</AlertTitle>
|
||||
<AlertDescription>
|
||||
Cost sensitivity of {(agentPreferences.costSensitivity * 100).toFixed(0)}% will prioritize
|
||||
{agentPreferences.costSensitivity > 0.6 ? ' lower prices over faster execution' : ' faster execution over cost savings'}.
|
||||
</AlertDescription>
|
||||
</Alert>
|
||||
|
||||
<Alert>
|
||||
<Timer className="h-4 w-4" />
|
||||
<AlertTitle>Time Efficiency</AlertTitle>
|
||||
<AlertDescription>
|
||||
Urgency preference of {(agentPreferences.urgencyPreference * 100).toFixed(0)}% will focus on
|
||||
{agentPreferences.urgencyPreference > 0.6 ? ' minimizing wait times' : ' optimizing for cost and success rate'}.
|
||||
</AlertDescription>
|
||||
</Alert>
|
||||
</div>
|
||||
</CardContent>
|
||||
</Card>
|
||||
</TabsContent>
|
||||
</Tabs>
|
||||
</div>
|
||||
);
|
||||
};
|
||||
|
||||
export default BidStrategy;
|
||||
1150
apps/marketplace/src/components/CrossChainReputation.tsx
Normal file
1150
apps/marketplace/src/components/CrossChainReputation.tsx
Normal file
File diff suppressed because it is too large
Load Diff
490
apps/marketplace/src/components/KnowledgeMarketplace.tsx
Normal file
490
apps/marketplace/src/components/KnowledgeMarketplace.tsx
Normal file
@@ -0,0 +1,490 @@
|
||||
import React, { useState, useEffect } from 'react';
|
||||
import { Card, CardHeader, CardTitle, CardDescription, CardContent, CardFooter } from './ui/card';
|
||||
import { Button } from './ui/button';
|
||||
import { Badge } from './ui/badge';
|
||||
import { Input } from './ui/input';
|
||||
import { Select, SelectContent, SelectItem, SelectTrigger, SelectValue } from './ui/select';
|
||||
import { Tabs, TabsContent, TabsList, TabsTrigger } from './ui/tabs';
|
||||
import { Alert, AlertDescription, AlertTitle } from './ui/alert';
|
||||
import { Search, Filter, ShoppingCart, Star, TrendingUp, Clock, CheckCircle, XCircle } from 'lucide-react';
|
||||
import { useToast } from '@/hooks/use-toast';
|
||||
import { useWallet } from '@/hooks/use-wallet';
|
||||
|
||||
interface KnowledgeGraph {
|
||||
graphId: string;
|
||||
cid: string;
|
||||
creator: string;
|
||||
price: number;
|
||||
tags: string[];
|
||||
qualityScore: number;
|
||||
accessCount: number;
|
||||
totalRevenue: number;
|
||||
royaltyRate: number;
|
||||
isActive: boolean;
|
||||
createdAt: string;
|
||||
description: string;
|
||||
metadata: string;
|
||||
}
|
||||
|
||||
interface PurchaseRecord {
|
||||
graphId: string;
|
||||
buyer: string;
|
||||
purchasedAt: string;
|
||||
expiresAt: string;
|
||||
decryptionKey: string;
|
||||
isActive: boolean;
|
||||
}
|
||||
|
||||
const KnowledgeMarketplace: React.FC = () => {
|
||||
const { toast } = useToast();
|
||||
const { isConnected, address } = useWallet();
|
||||
|
||||
const [graphs, setGraphs] = useState<KnowledgeGraph[]>([]);
|
||||
const [filteredGraphs, setFilteredGraphs] = useState<KnowledgeGraph[]>([]);
|
||||
const [searchQuery, setSearchQuery] = useState('');
|
||||
const [selectedTags, setSelectedTags] = useState<string[]>([]);
|
||||
const [priceRange, setPriceRange] = useState({ min: 0, max: 1000 });
|
||||
const [sortBy, setSortBy] = useState('quality');
|
||||
const [loading, setLoading] = useState(true);
|
||||
const [purchasedGraphs, setPurchasedGraphs] = useState<PurchaseRecord[]>([]);
|
||||
const [activeTab, setActiveTab] = useState('browse');
|
||||
|
||||
// Mock data for demonstration
|
||||
const mockGraphs: KnowledgeGraph[] = [
|
||||
{
|
||||
graphId: 'graph_001',
|
||||
cid: 'QmXxx...123',
|
||||
creator: '0x1234...5678',
|
||||
price: 100,
|
||||
tags: ['nlp', 'transformer', 'language'],
|
||||
qualityScore: 950,
|
||||
accessCount: 156,
|
||||
totalRevenue: 15600,
|
||||
royaltyRate: 500,
|
||||
isActive: true,
|
||||
createdAt: '2024-01-15T10:30:00Z',
|
||||
description: 'Advanced NLP knowledge graph with transformer architectures',
|
||||
metadata: '{"nodes": 1250, "edges": 3400, "domains": ["nlp", "ai"]}'
|
||||
},
|
||||
{
|
||||
graphId: 'graph_002',
|
||||
cid: 'QmYyy...456',
|
||||
creator: '0xabcd...efgh',
|
||||
price: 250,
|
||||
tags: ['computer-vision', 'cnn', 'image'],
|
||||
qualityScore: 890,
|
||||
accessCount: 89,
|
||||
totalRevenue: 22250,
|
||||
royaltyRate: 300,
|
||||
isActive: true,
|
||||
createdAt: '2024-01-20T14:15:00Z',
|
||||
description: 'Computer vision knowledge graph with CNN architectures',
|
||||
metadata: '{"nodes": 890, "edges": 2100, "domains": ["vision", "ml"]}'
|
||||
},
|
||||
{
|
||||
graphId: 'graph_003',
|
||||
cid: 'QmZzz...789',
|
||||
creator: '0x5678...9abc',
|
||||
price: 75,
|
||||
tags: ['reinforcement-learning', 'rl', 'gaming'],
|
||||
qualityScore: 920,
|
||||
accessCount: 234,
|
||||
totalRevenue: 17550,
|
||||
royaltyRate: 400,
|
||||
isActive: true,
|
||||
createdAt: '2024-01-25T09:45:00Z',
|
||||
description: 'Reinforcement learning knowledge graph for gaming AI',
|
||||
metadata: '{"nodes": 670, "edges": 1890, "domains": ["rl", "gaming"]}'
|
||||
}
|
||||
];
|
||||
|
||||
useEffect(() => {
|
||||
// Load mock data
|
||||
setTimeout(() => {
|
||||
setGraphs(mockGraphs);
|
||||
setFilteredGraphs(mockGraphs);
|
||||
setLoading(false);
|
||||
}, 1000);
|
||||
}, []);
|
||||
|
||||
useEffect(() => {
|
||||
filterAndSortGraphs();
|
||||
}, [searchQuery, selectedTags, priceRange, sortBy, graphs]);
|
||||
|
||||
const filterAndSortGraphs = () => {
|
||||
let filtered = graphs.filter(graph => {
|
||||
// Search query filter
|
||||
if (searchQuery) {
|
||||
const query = searchQuery.toLowerCase();
|
||||
const matchesSearch =
|
||||
graph.description.toLowerCase().includes(query) ||
|
||||
graph.tags.some(tag => tag.toLowerCase().includes(query)) ||
|
||||
graph.creator.toLowerCase().includes(query);
|
||||
if (!matchesSearch) return false;
|
||||
}
|
||||
|
||||
// Tags filter
|
||||
if (selectedTags.length > 0) {
|
||||
const hasSelectedTag = selectedTags.some(tag => graph.tags.includes(tag));
|
||||
if (!hasSelectedTag) return false;
|
||||
}
|
||||
|
||||
// Price range filter
|
||||
if (graph.price < priceRange.min || graph.price > priceRange.max) {
|
||||
return false;
|
||||
}
|
||||
|
||||
return true;
|
||||
});
|
||||
|
||||
// Sort
|
||||
filtered.sort((a, b) => {
|
||||
switch (sortBy) {
|
||||
case 'quality':
|
||||
return b.qualityScore - a.qualityScore;
|
||||
case 'price_low':
|
||||
return a.price - b.price;
|
||||
case 'price_high':
|
||||
return b.price - a.price;
|
||||
case 'popularity':
|
||||
return b.accessCount - a.accessCount;
|
||||
case 'newest':
|
||||
return new Date(b.createdAt).getTime() - new Date(a.createdAt).getTime();
|
||||
default:
|
||||
return 0;
|
||||
}
|
||||
});
|
||||
|
||||
setFilteredGraphs(filtered);
|
||||
};
|
||||
|
||||
const handlePurchase = async (graph: KnowledgeGraph) => {
|
||||
if (!isConnected) {
|
||||
toast({
|
||||
title: "Wallet Not Connected",
|
||||
description: "Please connect your wallet to purchase knowledge graphs",
|
||||
variant: "destructive"
|
||||
});
|
||||
return;
|
||||
}
|
||||
|
||||
try {
|
||||
// Simulate purchase process
|
||||
const purchaseRecord: PurchaseRecord = {
|
||||
graphId: graph.graphId,
|
||||
buyer: address || '',
|
||||
purchasedAt: new Date().toISOString(),
|
||||
expiresAt: new Date(Date.now() + 30 * 24 * 60 * 60 * 1000).toISOString(), // 30 days
|
||||
decryptionKey: `key_${Math.random().toString(36).substr(2, 9)}`,
|
||||
isActive: true
|
||||
};
|
||||
|
||||
setPurchasedGraphs([...purchasedGraphs, purchaseRecord]);
|
||||
|
||||
toast({
|
||||
title: "Purchase Successful!",
|
||||
description: `You now have access to "${graph.description}"`,
|
||||
variant: "default"
|
||||
});
|
||||
} catch (error) {
|
||||
toast({
|
||||
title: "Purchase Failed",
|
||||
description: "There was an error processing your purchase",
|
||||
variant: "destructive"
|
||||
});
|
||||
}
|
||||
};
|
||||
|
||||
const hasPurchased = (graphId: string) => {
|
||||
return purchasedGraphs.some(record =>
|
||||
record.graphId === graphId &&
|
||||
record.isActive &&
|
||||
new Date(record.expiresAt) > new Date()
|
||||
);
|
||||
};
|
||||
|
||||
const getQualityColor = (score: number) => {
|
||||
if (score >= 900) return 'bg-green-500';
|
||||
if (score >= 700) return 'bg-yellow-500';
|
||||
return 'bg-red-500';
|
||||
};
|
||||
|
||||
const allTags = Array.from(new Set(graphs.flatMap(g => g.tags)));
|
||||
|
||||
return (
|
||||
<div className="container mx-auto p-6 space-y-6">
|
||||
<div className="flex items-center justify-between">
|
||||
<div>
|
||||
<h1 className="text-3xl font-bold">Knowledge Graph Marketplace</h1>
|
||||
<p className="text-muted-foreground mt-2">
|
||||
Discover and purchase high-quality knowledge graphs to enhance your AI agents
|
||||
</p>
|
||||
</div>
|
||||
<div className="flex items-center space-x-2">
|
||||
<Badge variant="outline" className="flex items-center space-x-1">
|
||||
<Star className="w-4 h-4" />
|
||||
<span>{graphs.length} Graphs Available</span>
|
||||
</Badge>
|
||||
<Badge variant="outline" className="flex items-center space-x-1">
|
||||
<TrendingUp className="w-4 h-4" />
|
||||
<span>{graphs.reduce((sum, g) => sum + g.accessCount, 0)} Total Accesses</span>
|
||||
</Badge>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<Tabs value={activeTab} onValueChange={setActiveTab}>
|
||||
<TabsList className="grid w-full grid-cols-3">
|
||||
<TabsTrigger value="browse">Browse Graphs</TabsTrigger>
|
||||
<TabsTrigger value="purchased">My Purchases</TabsTrigger>
|
||||
<TabsTrigger value="create">Create Graph</TabsTrigger>
|
||||
</TabsList>
|
||||
|
||||
<TabsContent value="browse" className="space-y-6">
|
||||
{/* Search and Filters */}
|
||||
<Card>
|
||||
<CardHeader>
|
||||
<CardTitle className="flex items-center space-x-2">
|
||||
<Search className="w-5 h-5" />
|
||||
<span>Search & Filter</span>
|
||||
</CardTitle>
|
||||
</CardHeader>
|
||||
<CardContent className="space-y-4">
|
||||
<div className="flex space-x-4">
|
||||
<div className="flex-1">
|
||||
<Input
|
||||
placeholder="Search knowledge graphs..."
|
||||
value={searchQuery}
|
||||
onChange={(e) => setSearchQuery(e.target.value)}
|
||||
className="w-full"
|
||||
/>
|
||||
</div>
|
||||
<Select value={sortBy} onValueChange={setSortBy}>
|
||||
<SelectTrigger className="w-48">
|
||||
<SelectValue placeholder="Sort by" />
|
||||
</SelectTrigger>
|
||||
<SelectContent>
|
||||
<SelectItem value="quality">Quality Score</SelectItem>
|
||||
<SelectItem value="price_low">Price: Low to High</SelectItem>
|
||||
<SelectItem value="price_high">Price: High to Low</SelectItem>
|
||||
<SelectItem value="popularity">Most Popular</SelectItem>
|
||||
<SelectItem value="newest">Newest First</SelectItem>
|
||||
</SelectContent>
|
||||
</Select>
|
||||
</div>
|
||||
|
||||
<div className="flex space-x-4">
|
||||
<div className="flex-1">
|
||||
<label className="text-sm font-medium mb-2 block">Price Range (AITBC)</label>
|
||||
<div className="flex space-x-2">
|
||||
<Input
|
||||
type="number"
|
||||
placeholder="Min"
|
||||
value={priceRange.min}
|
||||
onChange={(e) => setPriceRange(prev => ({ ...prev, min: Number(e.target.value) }))}
|
||||
className="w-24"
|
||||
/>
|
||||
<Input
|
||||
type="number"
|
||||
placeholder="Max"
|
||||
value={priceRange.max}
|
||||
onChange={(e) => setPriceRange(prev => ({ ...prev, max: Number(e.target.value) }))}
|
||||
className="w-24"
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
<div className="flex-1">
|
||||
<label className="text-sm font-medium mb-2 block">Tags</label>
|
||||
<div className="flex flex-wrap gap-2">
|
||||
{allTags.map(tag => (
|
||||
<Badge
|
||||
key={tag}
|
||||
variant={selectedTags.includes(tag) ? "default" : "outline"}
|
||||
className="cursor-pointer"
|
||||
onClick={() => {
|
||||
setSelectedTags(prev =>
|
||||
prev.includes(tag)
|
||||
? prev.filter(t => t !== tag)
|
||||
: [...prev, tag]
|
||||
);
|
||||
}}
|
||||
>
|
||||
{tag}
|
||||
</Badge>
|
||||
))}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</CardContent>
|
||||
</Card>
|
||||
|
||||
{/* Graph Listings */}
|
||||
{loading ? (
|
||||
<div className="text-center py-8">
|
||||
<div className="animate-spin rounded-full h-8 w-8 border-b-2 border-primary mx-auto"></div>
|
||||
<p className="mt-2 text-muted-foreground">Loading knowledge graphs...</p>
|
||||
</div>
|
||||
) : filteredGraphs.length === 0 ? (
|
||||
<Alert>
|
||||
<XCircle className="h-4 w-4" />
|
||||
<AlertTitle>No graphs found</AlertTitle>
|
||||
<AlertDescription>
|
||||
Try adjusting your search criteria or filters to find knowledge graphs.
|
||||
</AlertDescription>
|
||||
</Alert>
|
||||
) : (
|
||||
<div className="grid grid-cols-1 md:grid-cols-2 lg:grid-cols-3 gap-6">
|
||||
{filteredGraphs.map((graph) => {
|
||||
const isPurchased = hasPurchased(graph.graphId);
|
||||
return (
|
||||
<Card key={graph.graphId} className="relative">
|
||||
<CardHeader>
|
||||
<div className="flex items-start justify-between">
|
||||
<div className="flex-1">
|
||||
<CardTitle className="text-lg line-clamp-2">
|
||||
{graph.description}
|
||||
</CardTitle>
|
||||
<CardDescription className="mt-1">
|
||||
by {graph.creator.slice(0, 6)}...{graph.creator.slice(-4)}
|
||||
</CardDescription>
|
||||
</div>
|
||||
<div className="flex items-center space-x-1">
|
||||
<div className={`w-2 h-2 rounded-full ${getQualityColor(graph.qualityScore)}`}></div>
|
||||
<span className="text-sm font-medium">{graph.qualityScore}</span>
|
||||
</div>
|
||||
</div>
|
||||
</CardHeader>
|
||||
<CardContent className="space-y-4">
|
||||
<div className="flex flex-wrap gap-1">
|
||||
{graph.tags.map(tag => (
|
||||
<Badge key={tag} variant="secondary" className="text-xs">
|
||||
{tag}
|
||||
</Badge>
|
||||
))}
|
||||
</div>
|
||||
|
||||
<div className="grid grid-cols-2 gap-4 text-sm">
|
||||
<div className="flex items-center space-x-1">
|
||||
<ShoppingCart className="w-4 h-4 text-muted-foreground" />
|
||||
<span>{graph.accessCount} accesses</span>
|
||||
</div>
|
||||
<div className="flex items-center space-x-1">
|
||||
<TrendingUp className="w-4 h-4 text-muted-foreground" />
|
||||
<span>{graph.totalRevenue} AITBC</span>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div className="text-sm text-muted-foreground">
|
||||
<div className="flex items-center space-x-1">
|
||||
<Clock className="w-4 h-4" />
|
||||
<span>Created {new Date(graph.createdAt).toLocaleDateString()}</span>
|
||||
</div>
|
||||
</div>
|
||||
</CardContent>
|
||||
<CardFooter className="flex items-center justify-between">
|
||||
<div className="text-lg font-bold">
|
||||
{graph.price} AITBC
|
||||
</div>
|
||||
{isPurchased ? (
|
||||
<Button variant="outline" disabled className="flex items-center space-x-1">
|
||||
<CheckCircle className="w-4 h-4" />
|
||||
<span>Purchased</span>
|
||||
</Button>
|
||||
) : (
|
||||
<Button
|
||||
onClick={() => handlePurchase(graph)}
|
||||
className="flex items-center space-x-1"
|
||||
>
|
||||
<ShoppingCart className="w-4 h-4" />
|
||||
<span>Purchase</span>
|
||||
</Button>
|
||||
)}
|
||||
</CardFooter>
|
||||
</Card>
|
||||
);
|
||||
})}
|
||||
</div>
|
||||
)}
|
||||
</TabsContent>
|
||||
|
||||
<TabsContent value="purchased" className="space-y-6">
|
||||
<Card>
|
||||
<CardHeader>
|
||||
<CardTitle>My Purchased Knowledge Graphs</CardTitle>
|
||||
<CardDescription>
|
||||
Knowledge graphs you have purchased and can access
|
||||
</CardDescription>
|
||||
</CardHeader>
|
||||
<CardContent>
|
||||
{purchasedGraphs.length === 0 ? (
|
||||
<div className="text-center py-8">
|
||||
<ShoppingCart className="w-12 h-12 text-muted-foreground mx-auto mb-4" />
|
||||
<p className="text-muted-foreground">No purchased knowledge graphs yet</p>
|
||||
<Button
|
||||
className="mt-4"
|
||||
onClick={() => setActiveTab('browse')}
|
||||
>
|
||||
Browse Marketplace
|
||||
</Button>
|
||||
</div>
|
||||
) : (
|
||||
<div className="space-y-4">
|
||||
{purchasedGraphs.map((record) => {
|
||||
const graph = graphs.find(g => g.graphId === record.graphId);
|
||||
if (!graph) return null;
|
||||
|
||||
return (
|
||||
<Card key={record.graphId}>
|
||||
<CardContent className="pt-6">
|
||||
<div className="flex items-center justify-between">
|
||||
<div>
|
||||
<h4 className="font-semibold">{graph.description}</h4>
|
||||
<p className="text-sm text-muted-foreground">
|
||||
Purchased on {new Date(record.purchasedAt).toLocaleDateString()}
|
||||
</p>
|
||||
<p className="text-sm text-muted-foreground">
|
||||
Expires on {new Date(record.expiresAt).toLocaleDateString()}
|
||||
</p>
|
||||
</div>
|
||||
<div className="flex items-center space-x-2">
|
||||
<Badge variant="outline">
|
||||
{record.graphId}
|
||||
</Badge>
|
||||
<Button variant="outline" size="sm">
|
||||
Download
|
||||
</Button>
|
||||
</div>
|
||||
</div>
|
||||
</CardContent>
|
||||
</Card>
|
||||
);
|
||||
})}
|
||||
</div>
|
||||
)}
|
||||
</CardContent>
|
||||
</Card>
|
||||
</TabsContent>
|
||||
|
||||
<TabsContent value="create" className="space-y-6">
|
||||
<Card>
|
||||
<CardHeader>
|
||||
<CardTitle>Create Knowledge Graph</CardTitle>
|
||||
<CardDescription>
|
||||
Upload and monetize your knowledge graphs on the marketplace
|
||||
</CardDescription>
|
||||
</CardHeader>
|
||||
<CardContent>
|
||||
<Alert>
|
||||
<AlertTitle>Coming Soon</AlertTitle>
|
||||
<AlertDescription>
|
||||
Knowledge graph creation tools will be available in the next update.
|
||||
</AlertDescription>
|
||||
</Alert>
|
||||
</CardContent>
|
||||
</Card>
|
||||
</TabsContent>
|
||||
</Tabs>
|
||||
</div>
|
||||
);
|
||||
};
|
||||
|
||||
export default KnowledgeMarketplace;
|
||||
1188
apps/marketplace/src/components/MarketplaceV2.tsx
Normal file
1188
apps/marketplace/src/components/MarketplaceV2.tsx
Normal file
File diff suppressed because it is too large
Load Diff
576
apps/marketplace/src/components/MemoryManager.tsx
Normal file
576
apps/marketplace/src/components/MemoryManager.tsx
Normal file
@@ -0,0 +1,576 @@
|
||||
import React, { useState, useEffect } from 'react';
|
||||
import { Card, CardHeader, CardTitle, CardDescription, CardContent, CardFooter } from './ui/card';
|
||||
import { Button } from './ui/button';
|
||||
import { Badge } from './ui/badge';
|
||||
import { Input } from './ui/input';
|
||||
import { Select, SelectContent, SelectItem, SelectTrigger, SelectValue } from './ui/select';
|
||||
import { Tabs, TabsContent, TabsList, TabsTrigger } from './ui/tabs';
|
||||
import { Alert, AlertDescription, AlertTitle } from './ui/alert';
|
||||
import { Progress } from './ui/progress';
|
||||
import { Separator } from './ui/separator';
|
||||
import {
|
||||
Database,
|
||||
Upload,
|
||||
Download,
|
||||
Search,
|
||||
Filter,
|
||||
Trash2,
|
||||
Clock,
|
||||
HardDrive,
|
||||
Brain,
|
||||
Zap,
|
||||
Shield,
|
||||
TrendingUp
|
||||
} from 'lucide-react';
|
||||
import { useToast } from '@/hooks/use-toast';
|
||||
import { useWallet } from '@/hooks/use-wallet';
|
||||
|
||||
interface MemoryRecord {
|
||||
cid: string;
|
||||
agentId: string;
|
||||
memoryType: string;
|
||||
priority: string;
|
||||
version: number;
|
||||
timestamp: string;
|
||||
size: number;
|
||||
tags: string[];
|
||||
accessCount: number;
|
||||
lastAccessed: string;
|
||||
expiresAt?: string;
|
||||
parentCid?: string;
|
||||
verified: boolean;
|
||||
}
|
||||
|
||||
interface MemoryStats {
|
||||
totalMemories: number;
|
||||
totalSizeBytes: number;
|
||||
totalSizeMB: number;
|
||||
byType: Record<string, number>;
|
||||
byPriority: Record<string, number>;
|
||||
totalAccessCount: number;
|
||||
averageAccessCount: number;
|
||||
agentCount: number;
|
||||
}
|
||||
|
||||
const MemoryManager: React.FC = () => {
|
||||
const { toast } = useToast();
|
||||
const { isConnected, address } = useWallet();
|
||||
|
||||
const [memories, setMemories] = useState<MemoryRecord[]>([]);
|
||||
const [filteredMemories, setFilteredMemories] = useState<MemoryRecord[]>([]);
|
||||
const [stats, setStats] = useState<MemoryStats | null>(null);
|
||||
const [searchQuery, setSearchQuery] = useState('');
|
||||
const [selectedType, setSelectedType] = useState('all');
|
||||
const [selectedPriority, setSelectedPriority] = useState('all');
|
||||
const [loading, setLoading] = useState(true);
|
||||
const [activeTab, setActiveTab] = useState('memories');
|
||||
|
||||
// Mock data for demonstration
|
||||
const mockMemories: MemoryRecord[] = [
|
||||
{
|
||||
cid: 'QmAbc123...',
|
||||
agentId: 'agent_001',
|
||||
memoryType: 'experience',
|
||||
priority: 'high',
|
||||
version: 3,
|
||||
timestamp: '2024-01-15T10:30:00Z',
|
||||
size: 2048576,
|
||||
tags: ['training', 'nlp', 'conversation'],
|
||||
accessCount: 45,
|
||||
lastAccessed: '2024-01-20T14:15:00Z',
|
||||
verified: true
|
||||
},
|
||||
{
|
||||
cid: 'QmDef456...',
|
||||
agentId: 'agent_001',
|
||||
memoryType: 'policy_weights',
|
||||
priority: 'critical',
|
||||
version: 7,
|
||||
timestamp: '2024-01-18T09:45:00Z',
|
||||
size: 1048576,
|
||||
tags: ['model', 'weights', 'reinforcement'],
|
||||
accessCount: 128,
|
||||
lastAccessed: '2024-01-22T16:30:00Z',
|
||||
verified: true
|
||||
},
|
||||
{
|
||||
cid: 'QmGhi789...',
|
||||
agentId: 'agent_002',
|
||||
memoryType: 'knowledge_graph',
|
||||
priority: 'medium',
|
||||
version: 1,
|
||||
timestamp: '2024-01-20T11:20:00Z',
|
||||
size: 5242880,
|
||||
tags: ['knowledge', 'graph', 'vision'],
|
||||
accessCount: 23,
|
||||
lastAccessed: '2024-01-21T13:45:00Z',
|
||||
verified: false
|
||||
},
|
||||
{
|
||||
cid: 'QmJkl012...',
|
||||
agentId: 'agent_002',
|
||||
memoryType: 'training_data',
|
||||
priority: 'low',
|
||||
version: 2,
|
||||
timestamp: '2024-01-22T08:15:00Z',
|
||||
size: 10485760,
|
||||
tags: ['data', 'images', 'classification'],
|
||||
accessCount: 8,
|
||||
lastAccessed: '2024-01-22T08:15:00Z',
|
||||
expiresAt: '2024-02-22T08:15:00Z',
|
||||
verified: true
|
||||
}
|
||||
];
|
||||
|
||||
const mockStats: MemoryStats = {
|
||||
totalMemories: 4,
|
||||
totalSizeBytes: 18874368,
|
||||
totalSizeMB: 18.0,
|
||||
byType: {
|
||||
'experience': 1,
|
||||
'policy_weights': 1,
|
||||
'knowledge_graph': 1,
|
||||
'training_data': 1
|
||||
},
|
||||
byPriority: {
|
||||
'critical': 1,
|
||||
'high': 1,
|
||||
'medium': 1,
|
||||
'low': 1
|
||||
},
|
||||
totalAccessCount: 204,
|
||||
averageAccessCount: 51,
|
||||
agentCount: 2
|
||||
};
|
||||
|
||||
useEffect(() => {
|
||||
// Load mock data
|
||||
setTimeout(() => {
|
||||
setMemories(mockMemories);
|
||||
setFilteredMemories(mockMemories);
|
||||
setStats(mockStats);
|
||||
setLoading(false);
|
||||
}, 1000);
|
||||
}, []);
|
||||
|
||||
useEffect(() => {
|
||||
filterMemories();
|
||||
}, [searchQuery, selectedType, selectedPriority, memories]);
|
||||
|
||||
const filterMemories = () => {
|
||||
let filtered = memories.filter(memory => {
|
||||
// Search query filter
|
||||
if (searchQuery) {
|
||||
const query = searchQuery.toLowerCase();
|
||||
const matchesSearch =
|
||||
memory.cid.toLowerCase().includes(query) ||
|
||||
memory.memoryType.toLowerCase().includes(query) ||
|
||||
memory.tags.some(tag => tag.toLowerCase().includes(query)) ||
|
||||
memory.agentId.toLowerCase().includes(query);
|
||||
if (!matchesSearch) return false;
|
||||
}
|
||||
|
||||
// Type filter
|
||||
if (selectedType !== 'all' && memory.memoryType !== selectedType) {
|
||||
return false;
|
||||
}
|
||||
|
||||
// Priority filter
|
||||
if (selectedPriority !== 'all' && memory.priority !== selectedPriority) {
|
||||
return false;
|
||||
}
|
||||
|
||||
return true;
|
||||
});
|
||||
|
||||
setFilteredMemories(filtered);
|
||||
};
|
||||
|
||||
const handleDownload = async (memory: MemoryRecord) => {
|
||||
if (!isConnected) {
|
||||
toast({
|
||||
title: "Wallet Not Connected",
|
||||
description: "Please connect your wallet to download memories",
|
||||
variant: "destructive"
|
||||
});
|
||||
return;
|
||||
}
|
||||
|
||||
try {
|
||||
// Simulate download process
|
||||
toast({
|
||||
title: "Download Started",
|
||||
description: `Downloading memory ${memory.cid}...`,
|
||||
variant: "default"
|
||||
});
|
||||
|
||||
// Simulate download completion
|
||||
setTimeout(() => {
|
||||
toast({
|
||||
title: "Download Complete",
|
||||
description: `Memory ${memory.cid} downloaded successfully`,
|
||||
variant: "default"
|
||||
});
|
||||
}, 2000);
|
||||
} catch (error) {
|
||||
toast({
|
||||
title: "Download Failed",
|
||||
description: "There was an error downloading the memory",
|
||||
variant: "destructive"
|
||||
});
|
||||
}
|
||||
};
|
||||
|
||||
const handleDelete = async (memory: MemoryRecord) => {
|
||||
if (!isConnected) {
|
||||
toast({
|
||||
title: "Wallet Not Connected",
|
||||
description: "Please connect your wallet to delete memories",
|
||||
variant: "destructive"
|
||||
});
|
||||
return;
|
||||
}
|
||||
|
||||
try {
|
||||
// Remove from local state
|
||||
setMemories(prev => prev.filter(m => m.cid !== memory.cid));
|
||||
|
||||
toast({
|
||||
title: "Memory Deleted",
|
||||
description: `Memory ${memory.cid} has been deleted`,
|
||||
variant: "default"
|
||||
});
|
||||
} catch (error) {
|
||||
toast({
|
||||
title: "Delete Failed",
|
||||
description: "There was an error deleting the memory",
|
||||
variant: "destructive"
|
||||
});
|
||||
}
|
||||
};
|
||||
|
||||
const getPriorityColor = (priority: string) => {
|
||||
switch (priority) {
|
||||
case 'critical': return 'bg-red-500';
|
||||
case 'high': return 'bg-orange-500';
|
||||
case 'medium': return 'bg-yellow-500';
|
||||
case 'low': return 'bg-green-500';
|
||||
default: return 'bg-gray-500';
|
||||
}
|
||||
};
|
||||
|
||||
const getTypeIcon = (type: string) => {
|
||||
switch (type) {
|
||||
case 'experience': return <Brain className="w-4 h-4" />;
|
||||
case 'policy_weights': return <Zap className="w-4 h-4" />;
|
||||
case 'knowledge_graph': return <Database className="w-4 h-4" />;
|
||||
case 'training_data': return <HardDrive className="w-4 h-4" />;
|
||||
default: return <Database className="w-4 h-4" />;
|
||||
}
|
||||
};
|
||||
|
||||
const formatSize = (bytes: number) => {
|
||||
if (bytes === 0) return '0 Bytes';
|
||||
const k = 1024;
|
||||
const sizes = ['Bytes', 'KB', 'MB', 'GB'];
|
||||
const i = Math.floor(Math.log(bytes) / Math.log(k));
|
||||
return parseFloat((bytes / Math.pow(k, i)).toFixed(2)) + ' ' + sizes[i];
|
||||
};
|
||||
|
||||
const memoryTypes = Array.from(new Set(memories.map(m => m.memoryType)));
|
||||
const priorities = ['critical', 'high', 'medium', 'low'];
|
||||
|
||||
return (
|
||||
<div className="container mx-auto p-6 space-y-6">
|
||||
<div className="flex items-center justify-between">
|
||||
<div>
|
||||
<h1 className="text-3xl font-bold">Memory Manager</h1>
|
||||
<p className="text-muted-foreground mt-2">
|
||||
Manage and monitor your agent's persistent memory storage
|
||||
</p>
|
||||
</div>
|
||||
<div className="flex items-center space-x-2">
|
||||
<Badge variant="outline" className="flex items-center space-x-1">
|
||||
<Database className="w-4 h-4" />
|
||||
<span>{stats?.totalMemories || 0} Memories</span>
|
||||
</Badge>
|
||||
<Badge variant="outline" className="flex items-center space-x-1">
|
||||
<HardDrive className="w-4 h-4" />
|
||||
<span>{stats?.totalSizeMB.toFixed(1) || 0} MB</span>
|
||||
</Badge>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<Tabs value={activeTab} onValueChange={setActiveTab}>
|
||||
<TabsList className="grid w-full grid-cols-3">
|
||||
<TabsTrigger value="memories">Memories</TabsTrigger>
|
||||
<TabsTrigger value="statistics">Statistics</TabsTrigger>
|
||||
<TabsTrigger value="settings">Settings</TabsTrigger>
|
||||
</TabsList>
|
||||
|
||||
<TabsContent value="memories" className="space-y-6">
|
||||
{/* Search and Filters */}
|
||||
<Card>
|
||||
<CardHeader>
|
||||
<CardTitle className="flex items-center space-x-2">
|
||||
<Search className="w-5 h-5" />
|
||||
<span>Search & Filter</span>
|
||||
</CardTitle>
|
||||
</CardHeader>
|
||||
<CardContent className="space-y-4">
|
||||
<div className="flex space-x-4">
|
||||
<div className="flex-1">
|
||||
<Input
|
||||
placeholder="Search memories by CID, type, tags..."
|
||||
value={searchQuery}
|
||||
onChange={(e) => setSearchQuery(e.target.value)}
|
||||
className="w-full"
|
||||
/>
|
||||
</div>
|
||||
<Select value={selectedType} onValueChange={setSelectedType}>
|
||||
<SelectTrigger className="w-40">
|
||||
<SelectValue placeholder="Type" />
|
||||
</SelectTrigger>
|
||||
<SelectContent>
|
||||
<SelectItem value="all">All Types</SelectItem>
|
||||
{memoryTypes.map(type => (
|
||||
<SelectItem key={type} value={type}>
|
||||
<div className="flex items-center space-x-2">
|
||||
{getTypeIcon(type)}
|
||||
<span>{type.replace('_', ' ')}</span>
|
||||
</div>
|
||||
</SelectItem>
|
||||
))}
|
||||
</SelectContent>
|
||||
</Select>
|
||||
<Select value={selectedPriority} onValueChange={setSelectedPriority}>
|
||||
<SelectTrigger className="w-32">
|
||||
<SelectValue placeholder="Priority" />
|
||||
</SelectTrigger>
|
||||
<SelectContent>
|
||||
<SelectItem value="all">All Priorities</SelectItem>
|
||||
{priorities.map(priority => (
|
||||
<SelectItem key={priority} value={priority}>
|
||||
<div className="flex items-center space-x-2">
|
||||
<div className={`w-2 h-2 rounded-full ${getPriorityColor(priority)}`}></div>
|
||||
<span>{priority.charAt(0).toUpperCase() + priority.slice(1)}</span>
|
||||
</div>
|
||||
</SelectItem>
|
||||
))}
|
||||
</SelectContent>
|
||||
</Select>
|
||||
</div>
|
||||
</CardContent>
|
||||
</Card>
|
||||
|
||||
{/* Memory List */}
|
||||
{loading ? (
|
||||
<div className="text-center py-8">
|
||||
<div className="animate-spin rounded-full h-8 w-8 border-b-2 border-primary mx-auto"></div>
|
||||
<p className="mt-2 text-muted-foreground">Loading memories...</p>
|
||||
</div>
|
||||
) : filteredMemories.length === 0 ? (
|
||||
<Alert>
|
||||
<Database className="h-4 w-4" />
|
||||
<AlertTitle>No memories found</AlertTitle>
|
||||
<AlertDescription>
|
||||
Try adjusting your search criteria or filters to find memories.
|
||||
</AlertDescription>
|
||||
</Alert>
|
||||
) : (
|
||||
<div className="space-y-4">
|
||||
{filteredMemories.map((memory) => (
|
||||
<Card key={memory.cid}>
|
||||
<CardContent className="pt-6">
|
||||
<div className="flex items-start justify-between">
|
||||
<div className="flex-1">
|
||||
<div className="flex items-center space-x-3 mb-2">
|
||||
{getTypeIcon(memory.memoryType)}
|
||||
<h4 className="font-semibold">{memory.cid}</h4>
|
||||
<Badge variant="outline">{memory.memoryType.replace('_', ' ')}</Badge>
|
||||
<div className={`w-2 h-2 rounded-full ${getPriorityColor(memory.priority)}`}></div>
|
||||
<span className="text-sm text-muted-foreground capitalize">
|
||||
{memory.priority} priority
|
||||
</span>
|
||||
{memory.verified && (
|
||||
<Shield className="w-4 h-4 text-green-500" />
|
||||
)}
|
||||
</div>
|
||||
|
||||
<div className="flex flex-wrap gap-1 mb-3">
|
||||
{memory.tags.map(tag => (
|
||||
<Badge key={tag} variant="secondary" className="text-xs">
|
||||
{tag}
|
||||
</Badge>
|
||||
))}
|
||||
</div>
|
||||
|
||||
<div className="grid grid-cols-2 md:grid-cols-4 gap-4 text-sm text-muted-foreground">
|
||||
<div className="flex items-center space-x-1">
|
||||
<HardDrive className="w-4 h-4" />
|
||||
<span>{formatSize(memory.size)}</span>
|
||||
</div>
|
||||
<div className="flex items-center space-x-1">
|
||||
<TrendingUp className="w-4 h-4" />
|
||||
<span>{memory.accessCount} accesses</span>
|
||||
</div>
|
||||
<div className="flex items-center space-x-1">
|
||||
<Clock className="w-4 h-4" />
|
||||
<span>Version {memory.version}</span>
|
||||
</div>
|
||||
<div className="flex items-center space-x-1">
|
||||
<Database className="w-4 h-4" />
|
||||
<span>{memory.agentId}</span>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div className="text-sm text-muted-foreground mt-2">
|
||||
Created: {new Date(memory.timestamp).toLocaleDateString()}
|
||||
{memory.lastAccessed !== memory.timestamp && (
|
||||
<> • Last accessed: {new Date(memory.lastAccessed).toLocaleDateString()}</>
|
||||
)}
|
||||
{memory.expiresAt && (
|
||||
<> • Expires: {new Date(memory.expiresAt).toLocaleDateString()}</>
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div className="flex items-center space-x-2 ml-4">
|
||||
<Button
|
||||
variant="outline"
|
||||
size="sm"
|
||||
onClick={() => handleDownload(memory)}
|
||||
className="flex items-center space-x-1"
|
||||
>
|
||||
<Download className="w-4 h-4" />
|
||||
<span>Download</span>
|
||||
</Button>
|
||||
<Button
|
||||
variant="outline"
|
||||
size="sm"
|
||||
onClick={() => handleDelete(memory)}
|
||||
className="flex items-center space-x-1 text-red-600 hover:text-red-700"
|
||||
>
|
||||
<Trash2 className="w-4 h-4" />
|
||||
<span>Delete</span>
|
||||
</Button>
|
||||
</div>
|
||||
</div>
|
||||
</CardContent>
|
||||
</Card>
|
||||
))}
|
||||
</div>
|
||||
)}
|
||||
</TabsContent>
|
||||
|
||||
<TabsContent value="statistics" className="space-y-6">
|
||||
{stats ? (
|
||||
<div className="grid grid-cols-1 md:grid-cols-2 lg:grid-cols-3 gap-6">
|
||||
<Card>
|
||||
<CardHeader>
|
||||
<CardTitle className="flex items-center space-x-2">
|
||||
<Database className="w-5 h-5" />
|
||||
<span>Storage Overview</span>
|
||||
</CardTitle>
|
||||
</CardHeader>
|
||||
<CardContent className="space-y-4">
|
||||
<div className="flex justify-between">
|
||||
<span>Total Memories</span>
|
||||
<span className="font-semibold">{stats.totalMemories}</span>
|
||||
</div>
|
||||
<div className="flex justify-between">
|
||||
<span>Total Size</span>
|
||||
<span className="font-semibold">{stats.totalSizeMB.toFixed(1)} MB</span>
|
||||
</div>
|
||||
<div className="flex justify-between">
|
||||
<span>Agent Count</span>
|
||||
<span className="font-semibold">{stats.agentCount}</span>
|
||||
</div>
|
||||
</CardContent>
|
||||
</Card>
|
||||
|
||||
<Card>
|
||||
<CardHeader>
|
||||
<CardTitle className="flex items-center space-x-2">
|
||||
<TrendingUp className="w-5 h-5" />
|
||||
<span>Access Statistics</span>
|
||||
</CardTitle>
|
||||
</CardHeader>
|
||||
<CardContent className="space-y-4">
|
||||
<div className="flex justify-between">
|
||||
<span>Total Accesses</span>
|
||||
<span className="font-semibold">{stats.totalAccessCount}</span>
|
||||
</div>
|
||||
<div className="flex justify-between">
|
||||
<span>Average Accesses</span>
|
||||
<span className="font-semibold">{stats.averageAccessCount.toFixed(1)}</span>
|
||||
</div>
|
||||
</CardContent>
|
||||
</Card>
|
||||
|
||||
<Card>
|
||||
<CardHeader>
|
||||
<CardTitle>Memory Types</CardTitle>
|
||||
</CardHeader>
|
||||
<CardContent className="space-y-3">
|
||||
{Object.entries(stats.byType).map(([type, count]) => (
|
||||
<div key={type} className="flex items-center justify-between">
|
||||
<div className="flex items-center space-x-2">
|
||||
{getTypeIcon(type)}
|
||||
<span className="capitalize">{type.replace('_', ' ')}</span>
|
||||
</div>
|
||||
<Badge variant="outline">{count}</Badge>
|
||||
</div>
|
||||
))}
|
||||
</CardContent>
|
||||
</Card>
|
||||
|
||||
<Card>
|
||||
<CardHeader>
|
||||
<CardTitle>Priority Distribution</CardTitle>
|
||||
</CardHeader>
|
||||
<CardContent className="space-y-3">
|
||||
{Object.entries(stats.byPriority).map(([priority, count]) => (
|
||||
<div key={priority} className="flex items-center justify-between">
|
||||
<div className="flex items-center space-x-2">
|
||||
<div className={`w-2 h-2 rounded-full ${getPriorityColor(priority)}`}></div>
|
||||
<span className="capitalize">{priority}</span>
|
||||
</div>
|
||||
<Badge variant="outline">{count}</Badge>
|
||||
</div>
|
||||
))}
|
||||
</CardContent>
|
||||
</Card>
|
||||
</div>
|
||||
) : (
|
||||
<div className="text-center py-8">
|
||||
<div className="animate-spin rounded-full h-8 w-8 border-b-2 border-primary mx-auto"></div>
|
||||
<p className="mt-2 text-muted-foreground">Loading statistics...</p>
|
||||
</div>
|
||||
)}
|
||||
</TabsContent>
|
||||
|
||||
<TabsContent value="settings" className="space-y-6">
|
||||
<Card>
|
||||
<CardHeader>
|
||||
<CardTitle>Memory Settings</CardTitle>
|
||||
<CardDescription>
|
||||
Configure memory management settings and preferences
|
||||
</CardDescription>
|
||||
</CardHeader>
|
||||
<CardContent>
|
||||
<Alert>
|
||||
<AlertTitle>Coming Soon</AlertTitle>
|
||||
<AlertDescription>
|
||||
Memory management settings will be available in the next update.
|
||||
</AlertDescription>
|
||||
</Alert>
|
||||
</CardContent>
|
||||
</Card>
|
||||
</TabsContent>
|
||||
</Tabs>
|
||||
</div>
|
||||
);
|
||||
};
|
||||
|
||||
export default MemoryManager;
|
||||
1080
apps/marketplace/src/components/TaskDecomposition.tsx
Normal file
1080
apps/marketplace/src/components/TaskDecomposition.tsx
Normal file
File diff suppressed because it is too large
Load Diff
59
apps/marketplace/src/components/ui/alert.tsx
Normal file
59
apps/marketplace/src/components/ui/alert.tsx
Normal file
@@ -0,0 +1,59 @@
|
||||
import * as React from "react"
|
||||
import { cva, type VariantProps } from "class-variance-authority"
|
||||
|
||||
import { cn } from "@/lib/utils"
|
||||
|
||||
const alertVariants = cva(
|
||||
"relative w-full rounded-lg border p-4 [&>svg~*]:pl-7 [&>svg+div]:translate-y-[-3px] [&>svg]:absolute [&>svg]:left-4 [&>svg]:top-4 [&>svg]:text-foreground",
|
||||
{
|
||||
variants: {
|
||||
variant: {
|
||||
default: "bg-background text-foreground",
|
||||
destructive:
|
||||
"border-destructive/50 text-destructive dark:border-destructive [&>svg]:text-destructive",
|
||||
},
|
||||
},
|
||||
defaultVariants: {
|
||||
variant: "default",
|
||||
},
|
||||
}
|
||||
)
|
||||
|
||||
const Alert = React.forwardRef<
|
||||
HTMLDivElement,
|
||||
React.HTMLAttributes<HTMLDivElement> & VariantProps<typeof alertVariants>
|
||||
>(({ className, variant, ...props }, ref) => (
|
||||
<div
|
||||
ref={ref}
|
||||
role="alert"
|
||||
className={cn(alertVariants({ variant }), className)}
|
||||
{...props}
|
||||
/>
|
||||
))
|
||||
Alert.displayName = "Alert"
|
||||
|
||||
const AlertTitle = React.forwardRef<
|
||||
HTMLParagraphElement,
|
||||
React.HTMLAttributes<HTMLHeadingElement>
|
||||
>(({ className, ...props }, ref) => (
|
||||
<h5
|
||||
ref={ref}
|
||||
className={cn("mb-1 font-medium leading-none tracking-tight", className)}
|
||||
{...props}
|
||||
/>
|
||||
))
|
||||
AlertTitle.displayName = "AlertTitle"
|
||||
|
||||
const AlertDescription = React.forwardRef<
|
||||
HTMLParagraphElement,
|
||||
React.HTMLAttributes<HTMLParagraphElement>
|
||||
>(({ className, ...props }, ref) => (
|
||||
<div
|
||||
ref={ref}
|
||||
className={cn("text-sm [&_p]:leading-relaxed", className)}
|
||||
{...props}
|
||||
/>
|
||||
))
|
||||
AlertDescription.displayName = "AlertDescription"
|
||||
|
||||
export { Alert, AlertTitle, AlertDescription }
|
||||
36
apps/marketplace/src/components/ui/badge.tsx
Normal file
36
apps/marketplace/src/components/ui/badge.tsx
Normal file
@@ -0,0 +1,36 @@
|
||||
import * as React from "react"
|
||||
import { cva, type VariantProps } from "class-variance-authority"
|
||||
|
||||
import { cn } from "@/lib/utils"
|
||||
|
||||
const badgeVariants = cva(
|
||||
"inline-flex items-center rounded-full border px-2.5 py-0.5 text-xs font-semibold transition-colors focus:outline-none focus:ring-2 focus:ring-ring focus:ring-offset-2",
|
||||
{
|
||||
variants: {
|
||||
variant: {
|
||||
default:
|
||||
"border-transparent bg-primary text-primary-foreground hover:bg-primary/80",
|
||||
secondary:
|
||||
"border-transparent bg-secondary text-secondary-foreground hover:bg-secondary/80",
|
||||
destructive:
|
||||
"border-transparent bg-destructive text-destructive-foreground hover:bg-destructive/80",
|
||||
outline: "text-foreground",
|
||||
},
|
||||
},
|
||||
defaultVariants: {
|
||||
variant: "default",
|
||||
},
|
||||
}
|
||||
)
|
||||
|
||||
export interface BadgeProps
|
||||
extends React.HTMLAttributes<HTMLDivElement>,
|
||||
VariantProps<typeof badgeVariants> {}
|
||||
|
||||
function Badge({ className, variant, ...props }: BadgeProps) {
|
||||
return (
|
||||
<div className={cn(badgeVariants({ variant }), className)} {...props} />
|
||||
)
|
||||
}
|
||||
|
||||
export { Badge, badgeVariants }
|
||||
56
apps/marketplace/src/components/ui/button.tsx
Normal file
56
apps/marketplace/src/components/ui/button.tsx
Normal file
@@ -0,0 +1,56 @@
|
||||
import * as React from "react"
|
||||
import { Slot } from "@radix-ui/react-slot"
|
||||
import { cva, type VariantProps } from "class-variance-authority"
|
||||
|
||||
import { cn } from "@/lib/utils"
|
||||
|
||||
const buttonVariants = cva(
|
||||
"inline-flex items-center justify-center whitespace-nowrap rounded-md text-sm font-medium ring-offset-background transition-colors focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-ring focus-visible:ring-offset-2 disabled:pointer-events-none disabled:opacity-50",
|
||||
{
|
||||
variants: {
|
||||
variant: {
|
||||
default: "bg-primary text-primary-foreground hover:bg-primary/90",
|
||||
destructive:
|
||||
"bg-destructive text-destructive-foreground hover:bg-destructive/90",
|
||||
outline:
|
||||
"border border-input bg-background hover:bg-accent hover:text-accent-foreground",
|
||||
secondary:
|
||||
"bg-secondary text-secondary-foreground hover:bg-secondary/80",
|
||||
ghost: "hover:bg-accent hover:text-accent-foreground",
|
||||
link: "text-primary underline-offset-4 hover:underline",
|
||||
},
|
||||
size: {
|
||||
default: "h-10 px-4 py-2",
|
||||
sm: "h-9 rounded-md px-3",
|
||||
lg: "h-11 rounded-md px-8",
|
||||
icon: "h-10 w-10",
|
||||
},
|
||||
},
|
||||
defaultVariants: {
|
||||
variant: "default",
|
||||
size: "default",
|
||||
},
|
||||
}
|
||||
)
|
||||
|
||||
export interface ButtonProps
|
||||
extends React.ButtonHTMLAttributes<HTMLButtonElement>,
|
||||
VariantProps<typeof buttonVariants> {
|
||||
asChild?: boolean
|
||||
}
|
||||
|
||||
const Button = React.forwardRef<HTMLButtonElement, ButtonProps>(
|
||||
({ className, variant, size, asChild = false, ...props }, ref) => {
|
||||
const Comp = asChild ? Slot : "button"
|
||||
return (
|
||||
<Comp
|
||||
className={cn(buttonVariants({ variant, size, className }))}
|
||||
ref={ref}
|
||||
{...props}
|
||||
/>
|
||||
)
|
||||
}
|
||||
)
|
||||
Button.displayName = "Button"
|
||||
|
||||
export { Button, buttonVariants }
|
||||
79
apps/marketplace/src/components/ui/card.tsx
Normal file
79
apps/marketplace/src/components/ui/card.tsx
Normal file
@@ -0,0 +1,79 @@
|
||||
import * as React from "react"
|
||||
|
||||
import { cn } from "@/lib/utils"
|
||||
|
||||
const Card = React.forwardRef<
|
||||
HTMLDivElement,
|
||||
React.HTMLAttributes<HTMLDivElement>
|
||||
>(({ className, ...props }, ref) => (
|
||||
<div
|
||||
ref={ref}
|
||||
className={cn(
|
||||
"rounded-lg border bg-card text-card-foreground shadow-sm",
|
||||
className
|
||||
)}
|
||||
{...props}
|
||||
/>
|
||||
))
|
||||
Card.displayName = "Card"
|
||||
|
||||
const CardHeader = React.forwardRef<
|
||||
HTMLDivElement,
|
||||
React.HTMLAttributes<HTMLDivElement>
|
||||
>(({ className, ...props }, ref) => (
|
||||
<div
|
||||
ref={ref}
|
||||
className={cn("flex flex-col space-y-1.5 p-6", className)}
|
||||
{...props}
|
||||
/>
|
||||
))
|
||||
CardHeader.displayName = "CardHeader"
|
||||
|
||||
const CardTitle = React.forwardRef<
|
||||
HTMLParagraphElement,
|
||||
React.HTMLAttributes<HTMLHeadingElement>
|
||||
>(({ className, ...props }, ref) => (
|
||||
<h3
|
||||
ref={ref}
|
||||
className={cn(
|
||||
"text-2xl font-semibold leading-none tracking-tight",
|
||||
className
|
||||
)}
|
||||
{...props}
|
||||
/>
|
||||
))
|
||||
CardTitle.displayName = "CardTitle"
|
||||
|
||||
const CardDescription = React.forwardRef<
|
||||
HTMLParagraphElement,
|
||||
React.HTMLAttributes<HTMLParagraphElement>
|
||||
>(({ className, ...props }, ref) => (
|
||||
<p
|
||||
ref={ref}
|
||||
className={cn("text-sm text-muted-foreground", className)}
|
||||
{...props}
|
||||
/>
|
||||
))
|
||||
CardDescription.displayName = "CardDescription"
|
||||
|
||||
const CardContent = React.forwardRef<
|
||||
HTMLDivElement,
|
||||
React.HTMLAttributes<HTMLDivElement>
|
||||
>(({ className, ...props }, ref) => (
|
||||
<div ref={ref} className={cn("p-6 pt-0", className)} {...props} />
|
||||
))
|
||||
CardContent.displayName = "CardContent"
|
||||
|
||||
const CardFooter = React.forwardRef<
|
||||
HTMLDivElement,
|
||||
React.HTMLAttributes<HTMLDivElement>
|
||||
>(({ className, ...props }, ref) => (
|
||||
<div
|
||||
ref={ref}
|
||||
className={cn("flex items-center p-6 pt-0", className)}
|
||||
{...props}
|
||||
/>
|
||||
))
|
||||
CardFooter.displayName = "CardFooter"
|
||||
|
||||
export { Card, CardHeader, CardFooter, CardTitle, CardDescription, CardContent }
|
||||
25
apps/marketplace/src/components/ui/input.tsx
Normal file
25
apps/marketplace/src/components/ui/input.tsx
Normal file
@@ -0,0 +1,25 @@
|
||||
import * as React from "react"
|
||||
|
||||
import { cn } from "@/lib/utils"
|
||||
|
||||
export interface InputProps
|
||||
extends React.InputHTMLAttributes<HTMLInputElement> {}
|
||||
|
||||
const Input = React.forwardRef<HTMLInputElement, InputProps>(
|
||||
({ className, type, ...props }, ref) => {
|
||||
return (
|
||||
<input
|
||||
type={type}
|
||||
className={cn(
|
||||
"flex h-10 w-full rounded-md border border-input bg-background px-3 py-2 text-sm ring-offset-background file:border-0 file:bg-transparent file:text-sm file:font-medium placeholder:text-muted-foreground focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-ring focus-visible:ring-offset-2 disabled:cursor-not-allowed disabled:opacity-50",
|
||||
className
|
||||
)}
|
||||
ref={ref}
|
||||
{...props}
|
||||
/>
|
||||
)
|
||||
}
|
||||
)
|
||||
Input.displayName = "Input"
|
||||
|
||||
export { Input }
|
||||
26
apps/marketplace/src/components/ui/progress.tsx
Normal file
26
apps/marketplace/src/components/ui/progress.tsx
Normal file
@@ -0,0 +1,26 @@
|
||||
import * as React from "react"
|
||||
import * as ProgressPrimitive from "@radix-ui/react-progress"
|
||||
|
||||
import { cn } from "@/lib/utils"
|
||||
|
||||
const Progress = React.forwardRef<
|
||||
React.ElementRef<typeof ProgressPrimitive.Root>,
|
||||
React.ComponentPropsWithoutRef<typeof ProgressPrimitive.Root>
|
||||
>(({ className, value, ...props }, ref) => (
|
||||
<ProgressPrimitive.Root
|
||||
ref={ref}
|
||||
className={cn(
|
||||
"relative h-4 w-full overflow-hidden rounded-full bg-secondary",
|
||||
className
|
||||
)}
|
||||
{...props}
|
||||
>
|
||||
<ProgressPrimitive.Indicator
|
||||
className="h-full w-full flex-1 bg-primary transition-all"
|
||||
style={{ transform: `translateX(-${100 - (value || 0)}%)` }}
|
||||
/>
|
||||
</ProgressPrimitive.Root>
|
||||
))
|
||||
Progress.displayName = ProgressPrimitive.Root.displayName
|
||||
|
||||
export { Progress }
|
||||
158
apps/marketplace/src/components/ui/select.tsx
Normal file
158
apps/marketplace/src/components/ui/select.tsx
Normal file
@@ -0,0 +1,158 @@
|
||||
import * as React from "react"
|
||||
import * as SelectPrimitive from "@radix-ui/react-select"
|
||||
import { Check, ChevronDown, ChevronUp } from "lucide-react"
|
||||
|
||||
import { cn } from "@/lib/utils"
|
||||
|
||||
const Select = SelectPrimitive.Root
|
||||
|
||||
const SelectGroup = SelectPrimitive.Group
|
||||
|
||||
const SelectValue = SelectPrimitive.Value
|
||||
|
||||
const SelectTrigger = React.forwardRef<
|
||||
React.ElementRef<typeof SelectPrimitive.Trigger>,
|
||||
React.ComponentPropsWithoutRef<typeof SelectPrimitive.Trigger>
|
||||
>(({ className, children, ...props }, ref) => (
|
||||
<SelectPrimitive.Trigger
|
||||
ref={ref}
|
||||
className={cn(
|
||||
"flex h-10 w-full items-center justify-between rounded-md border border-input bg-background px-3 py-2 text-sm ring-offset-background placeholder:text-muted-foreground focus:outline-none focus:ring-2 focus:ring-ring focus:ring-offset-2 disabled:cursor-not-allowed disabled:opacity-50 [&>span]:line-clamp-1",
|
||||
className
|
||||
)}
|
||||
{...props}
|
||||
>
|
||||
{children}
|
||||
<SelectPrimitive.Icon asChild>
|
||||
<ChevronDown className="h-4 w-4 opacity-50" />
|
||||
</SelectPrimitive.Icon>
|
||||
</SelectPrimitive.Trigger>
|
||||
))
|
||||
SelectTrigger.displayName = SelectPrimitive.Trigger.displayName
|
||||
|
||||
const SelectScrollUpButton = React.forwardRef<
|
||||
React.ElementRef<typeof SelectPrimitive.ScrollUpButton>,
|
||||
React.ComponentPropsWithoutRef<typeof SelectPrimitive.ScrollUpButton>
|
||||
>(({ className, ...props }, ref) => (
|
||||
<SelectPrimitive.ScrollUpButton
|
||||
ref={ref}
|
||||
className={cn(
|
||||
"flex cursor-default items-center justify-center py-1",
|
||||
className
|
||||
)}
|
||||
{...props}
|
||||
>
|
||||
<ChevronUp className="h-4 w-4" />
|
||||
</SelectPrimitive.ScrollUpButton>
|
||||
))
|
||||
SelectScrollUpButton.displayName = SelectPrimitive.ScrollUpButton.displayName
|
||||
|
||||
const SelectScrollDownButton = React.forwardRef<
|
||||
React.ElementRef<typeof SelectPrimitive.ScrollDownButton>,
|
||||
React.ComponentPropsWithoutRef<typeof SelectPrimitive.ScrollDownButton>
|
||||
>(({ className, ...props }, ref) => (
|
||||
<SelectPrimitive.ScrollDownButton
|
||||
ref={ref}
|
||||
className={cn(
|
||||
"flex cursor-default items-center justify-center py-1",
|
||||
className
|
||||
)}
|
||||
{...props}
|
||||
>
|
||||
<ChevronDown className="h-4 w-4" />
|
||||
</SelectPrimitive.ScrollDownButton>
|
||||
))
|
||||
SelectScrollDownButton.displayName =
|
||||
SelectPrimitive.ScrollDownButton.displayName
|
||||
|
||||
const SelectContent = React.forwardRef<
|
||||
React.ElementRef<typeof SelectPrimitive.Content>,
|
||||
React.ComponentPropsWithoutRef<typeof SelectPrimitive.Content>
|
||||
>(({ className, children, position = "popper", ...props }, ref) => (
|
||||
<SelectPrimitive.Portal>
|
||||
<SelectPrimitive.Content
|
||||
ref={ref}
|
||||
className={cn(
|
||||
"relative z-50 max-h-96 min-w-[8rem] overflow-hidden rounded-md border bg-popover text-popover-foreground shadow-md data-[state=open]:animate-in data-[state=closed]:animate-out data-[state=closed]:fade-out-0 data-[state=open]:fade-in-0 data-[state=closed]:zoom-out-95 data-[state=open]:zoom-in-95 data-[side=bottom]:slide-in-from-top-2 data-[side=left]:slide-in-from-right-2 data-[side=right]:slide-in-from-left-2 data-[side=top]:slide-in-from-bottom-2",
|
||||
position === "popper" &&
|
||||
"data-[side=bottom]:translate-y-1 data-[side=left]:-translate-x-1 data-[side=right]:translate-x-1 data-[side=top]:-translate-y-1",
|
||||
className
|
||||
)}
|
||||
position={position}
|
||||
{...props}
|
||||
>
|
||||
<SelectScrollUpButton />
|
||||
<SelectPrimitive.Viewport
|
||||
className={cn(
|
||||
"p-1",
|
||||
position === "popper" &&
|
||||
"h-[var(--radix-select-trigger-height)] w-full min-w-[var(--radix-select-trigger-width)]"
|
||||
)}
|
||||
>
|
||||
{children}
|
||||
</SelectPrimitive.Viewport>
|
||||
<SelectScrollDownButton />
|
||||
</SelectPrimitive.Content>
|
||||
</SelectPrimitive.Portal>
|
||||
))
|
||||
SelectContent.displayName = SelectPrimitive.Content.displayName
|
||||
|
||||
const SelectLabel = React.forwardRef<
|
||||
React.ElementRef<typeof SelectPrimitive.Label>,
|
||||
React.ComponentPropsWithoutRef<typeof SelectPrimitive.Label>
|
||||
>(({ className, ...props }, ref) => (
|
||||
<SelectPrimitive.Label
|
||||
ref={ref}
|
||||
className={cn("py-1.5 pl-8 pr-2 text-sm font-semibold", className)}
|
||||
{...props}
|
||||
/>
|
||||
))
|
||||
SelectLabel.displayName = SelectPrimitive.Label.displayName
|
||||
|
||||
const SelectItem = React.forwardRef<
|
||||
React.ElementRef<typeof SelectPrimitive.Item>,
|
||||
React.ComponentPropsWithoutRef<typeof SelectPrimitive.Item>
|
||||
>(({ className, children, ...props }, ref) => (
|
||||
<SelectPrimitive.Item
|
||||
ref={ref}
|
||||
className={cn(
|
||||
"relative flex w-full cursor-default select-none items-center rounded-sm py-1.5 pl-8 pr-2 text-sm outline-none focus:bg-accent focus:text-accent-foreground data-[disabled]:pointer-events-none data-[disabled]:opacity-50",
|
||||
className
|
||||
)}
|
||||
{...props}
|
||||
>
|
||||
<span className="absolute left-2 flex h-3.5 w-3.5 items-center justify-center">
|
||||
<SelectPrimitive.ItemIndicator>
|
||||
<Check className="h-4 w-4" />
|
||||
</SelectPrimitive.ItemIndicator>
|
||||
</span>
|
||||
|
||||
<SelectPrimitive.ItemText>{children}</SelectPrimitive.ItemText>
|
||||
</SelectPrimitive.Item>
|
||||
))
|
||||
SelectItem.displayName = SelectPrimitive.Item.displayName
|
||||
|
||||
const SelectSeparator = React.forwardRef<
|
||||
React.ElementRef<typeof SelectPrimitive.Separator>,
|
||||
React.ComponentPropsWithoutRef<typeof SelectPrimitive.Separator>
|
||||
>(({ className, ...props }, ref) => (
|
||||
<SelectPrimitive.Separator
|
||||
ref={ref}
|
||||
className={cn("-mx-1 my-1 h-px bg-muted", className)}
|
||||
{...props}
|
||||
/>
|
||||
))
|
||||
SelectSeparator.displayName = SelectPrimitive.Separator.displayName
|
||||
|
||||
export {
|
||||
Select,
|
||||
SelectGroup,
|
||||
SelectValue,
|
||||
SelectTrigger,
|
||||
SelectContent,
|
||||
SelectLabel,
|
||||
SelectItem,
|
||||
SelectSeparator,
|
||||
SelectScrollUpButton,
|
||||
SelectScrollDownButton,
|
||||
}
|
||||
29
apps/marketplace/src/components/ui/separator.tsx
Normal file
29
apps/marketplace/src/components/ui/separator.tsx
Normal file
@@ -0,0 +1,29 @@
|
||||
import * as React from "react"
|
||||
import * as SeparatorPrimitive from "@radix-ui/react-separator"
|
||||
|
||||
import { cn } from "@/lib/utils"
|
||||
|
||||
const Separator = React.forwardRef<
|
||||
React.ElementRef<typeof SeparatorPrimitive.Root>,
|
||||
React.ComponentPropsWithoutRef<typeof SeparatorPrimitive.Root>
|
||||
>(
|
||||
(
|
||||
{ className, orientation = "horizontal", decorative = true, ...props },
|
||||
ref
|
||||
) => (
|
||||
<SeparatorPrimitive.Root
|
||||
ref={ref}
|
||||
decorative={decorative}
|
||||
orientation={orientation}
|
||||
className={cn(
|
||||
"shrink-0 bg-border",
|
||||
orientation === "horizontal" ? "h-[1px] w-full" : "h-full w-[1px]",
|
||||
className
|
||||
)}
|
||||
{...props}
|
||||
/>
|
||||
)
|
||||
)
|
||||
Separator.displayName = SeparatorPrimitive.Root.displayName
|
||||
|
||||
export { Separator }
|
||||
117
apps/marketplace/src/components/ui/table.tsx
Normal file
117
apps/marketplace/src/components/ui/table.tsx
Normal file
@@ -0,0 +1,117 @@
|
||||
import * as React from "react"
|
||||
|
||||
import { cn } from "@/lib/utils"
|
||||
|
||||
const Table = React.forwardRef<
|
||||
HTMLTableElement,
|
||||
React.HTMLAttributes<HTMLTableElement>
|
||||
>(({ className, ...props }, ref) => (
|
||||
<div className="relative w-full overflow-auto">
|
||||
<table
|
||||
ref={ref}
|
||||
className={cn("w-full caption-bottom text-sm", className)}
|
||||
{...props}
|
||||
/>
|
||||
</div>
|
||||
))
|
||||
Table.displayName = "Table"
|
||||
|
||||
const TableHeader = React.forwardRef<
|
||||
HTMLTableSectionElement,
|
||||
React.HTMLAttributes<HTMLTableSectionElement>
|
||||
>(({ className, ...props }, ref) => (
|
||||
<thead ref={ref} className={cn("[&_tr]:border-b", className)} {...props} />
|
||||
))
|
||||
TableHeader.displayName = "TableHeader"
|
||||
|
||||
const TableBody = React.forwardRef<
|
||||
HTMLTableSectionElement,
|
||||
React.HTMLAttributes<HTMLTableSectionElement>
|
||||
>(({ className, ...props }, ref) => (
|
||||
<tbody
|
||||
ref={ref}
|
||||
className={cn("[&_tr:last-child]:border-0", className)}
|
||||
{...props}
|
||||
/>
|
||||
))
|
||||
TableBody.displayName = "TableBody"
|
||||
|
||||
const TableFooter = React.forwardRef<
|
||||
HTMLTableSectionElement,
|
||||
React.HTMLAttributes<HTMLTableSectionElement>
|
||||
>(({ className, ...props }, ref) => (
|
||||
<tfoot
|
||||
ref={ref}
|
||||
className={cn(
|
||||
"border-t bg-muted/50 font-medium [&>tr]:last:border-b-0",
|
||||
className
|
||||
)}
|
||||
{...props}
|
||||
/>
|
||||
))
|
||||
TableFooter.displayName = "TableFooter"
|
||||
|
||||
const TableRow = React.forwardRef<
|
||||
HTMLTableRowElement,
|
||||
React.HTMLAttributes<HTMLTableRowElement>
|
||||
>(({ className, ...props }, ref) => (
|
||||
<tr
|
||||
ref={ref}
|
||||
className={cn(
|
||||
"border-b transition-colors hover:bg-muted/50 data-[state=selected]:bg-muted",
|
||||
className
|
||||
)}
|
||||
{...props}
|
||||
/>
|
||||
))
|
||||
TableRow.displayName = "TableRow"
|
||||
|
||||
const TableHead = React.forwardRef<
|
||||
HTMLTableCellElement,
|
||||
React.ThHTMLAttributes<HTMLTableCellElement>
|
||||
>(({ className, ...props }, ref) => (
|
||||
<th
|
||||
ref={ref}
|
||||
className={cn(
|
||||
"h-12 px-4 text-left align-middle font-medium text-muted-foreground [&:has([role=checkbox])]:pr-0",
|
||||
className
|
||||
)}
|
||||
{...props}
|
||||
/>
|
||||
))
|
||||
TableHead.displayName = "TableHead"
|
||||
|
||||
const TableCell = React.forwardRef<
|
||||
HTMLTableCellElement,
|
||||
React.TdHTMLAttributes<HTMLTableCellElement>
|
||||
>(({ className, ...props }, ref) => (
|
||||
<td
|
||||
ref={ref}
|
||||
className={cn("p-4 align-middle [&:has([role=checkbox])]:pr-0", className)}
|
||||
{...props}
|
||||
/>
|
||||
))
|
||||
TableCell.displayName = "TableCell"
|
||||
|
||||
const TableCaption = React.forwardRef<
|
||||
HTMLTableCaptionElement,
|
||||
React.HTMLAttributes<HTMLTableCaptionElement>
|
||||
>(({ className, ...props }, ref) => (
|
||||
<caption
|
||||
ref={ref}
|
||||
className={cn("mt-4 text-sm text-muted-foreground", className)}
|
||||
{...props}
|
||||
/>
|
||||
))
|
||||
TableCaption.displayName = "TableCaption"
|
||||
|
||||
export {
|
||||
Table,
|
||||
TableHeader,
|
||||
TableBody,
|
||||
TableFooter,
|
||||
TableHead,
|
||||
TableRow,
|
||||
TableCell,
|
||||
TableCaption,
|
||||
}
|
||||
53
apps/marketplace/src/components/ui/tabs.tsx
Normal file
53
apps/marketplace/src/components/ui/tabs.tsx
Normal file
@@ -0,0 +1,53 @@
|
||||
import * as React from "react"
|
||||
import * as TabsPrimitive from "@radix-ui/react-tabs"
|
||||
|
||||
import { cn } from "@/lib/utils"
|
||||
|
||||
const Tabs = TabsPrimitive.Root
|
||||
|
||||
const TabsList = React.forwardRef<
|
||||
React.ElementRef<typeof TabsPrimitive.List>,
|
||||
React.ComponentPropsWithoutRef<typeof TabsPrimitive.List>
|
||||
>(({ className, ...props }, ref) => (
|
||||
<TabsPrimitive.List
|
||||
ref={ref}
|
||||
className={cn(
|
||||
"inline-flex h-10 items-center justify-center rounded-md bg-muted p-1 text-muted-foreground",
|
||||
className
|
||||
)}
|
||||
{...props}
|
||||
/>
|
||||
))
|
||||
TabsList.displayName = TabsPrimitive.List.displayName
|
||||
|
||||
const TabsTrigger = React.forwardRef<
|
||||
React.ElementRef<typeof TabsPrimitive.Trigger>,
|
||||
React.ComponentPropsWithoutRef<typeof TabsPrimitive.Trigger>
|
||||
>(({ className, ...props }, ref) => (
|
||||
<TabsPrimitive.Trigger
|
||||
ref={ref}
|
||||
className={cn(
|
||||
"inline-flex items-center justify-center whitespace-nowrap rounded-sm px-3 py-1.5 text-sm font-medium ring-offset-background transition-all focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-ring focus-visible:ring-offset-2 disabled:pointer-events-none disabled:opacity-50 data-[state=active]:bg-background data-[state=active]:text-foreground data-[state=active]:shadow-sm",
|
||||
className
|
||||
)}
|
||||
{...props}
|
||||
/>
|
||||
))
|
||||
TabsTrigger.displayName = TabsPrimitive.Trigger.displayName
|
||||
|
||||
const TabsContent = React.forwardRef<
|
||||
React.ElementRef<typeof TabsPrimitive.Content>,
|
||||
React.ComponentPropsWithoutRef<typeof TabsPrimitive.Content>
|
||||
>(({ className, ...props }, ref) => (
|
||||
<TabsPrimitive.Content
|
||||
ref={ref}
|
||||
className={cn(
|
||||
"mt-2 ring-offset-background focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-ring focus-visible:ring-offset-2",
|
||||
className
|
||||
)}
|
||||
{...props}
|
||||
/>
|
||||
))
|
||||
TabsContent.displayName = TabsPrimitive.Content.displayName
|
||||
|
||||
export { Tabs, TabsList, TabsTrigger, TabsContent }
|
||||
127
apps/marketplace/src/components/ui/toast.tsx
Normal file
127
apps/marketplace/src/components/ui/toast.tsx
Normal file
@@ -0,0 +1,127 @@
|
||||
import * as React from "react"
|
||||
import * as ToastPrimitives from "@radix-ui/react-toast"
|
||||
import { cva, type VariantProps } from "class-variance-authority"
|
||||
import { X } from "lucide-react"
|
||||
|
||||
import { cn } from "@/lib/utils"
|
||||
|
||||
const ToastProvider = ToastPrimitives.Provider
|
||||
|
||||
const ToastViewport = React.forwardRef<
|
||||
React.ElementRef<typeof ToastPrimitives.Viewport>,
|
||||
React.ComponentPropsWithoutRef<typeof ToastPrimitives.Viewport>
|
||||
>(({ className, ...props }, ref) => (
|
||||
<ToastPrimitives.Viewport
|
||||
ref={ref}
|
||||
className={cn(
|
||||
"fixed top-0 z-[100] flex max-h-screen w-full flex-col-reverse p-4 sm:bottom-0 sm:right-0 sm:top-auto sm:flex-col md:max-w-[420px]",
|
||||
className
|
||||
)}
|
||||
{...props}
|
||||
/>
|
||||
))
|
||||
ToastViewport.displayName = ToastPrimitives.Viewport.displayName
|
||||
|
||||
const toastVariants = cva(
|
||||
"group pointer-events-auto relative flex w-full items-center justify-between space-x-4 overflow-hidden rounded-md border p-6 pr-8 shadow-lg transition-all data-[swipe=cancel]:translate-x-0 data-[swipe=end]:translate-x-[var(--radix-toast-swipe-end-x)] data-[swipe=move]:translate-x-[var(--radix-toast-swipe-move-x)] data-[swipe=move]:transition-none data-[state=open]:animate-in data-[state=closed]:animate-out data-[swipe=end]:animate-out data-[state=closed]:fade-out-80 data-[state=closed]:slide-out-to-right-full data-[state=open]:slide-in-from-top-full data-[state=open]:sm:slide-in-from-bottom-full",
|
||||
{
|
||||
variants: {
|
||||
variant: {
|
||||
default: "border bg-background text-foreground",
|
||||
destructive:
|
||||
"destructive border-destructive bg-destructive text-destructive-foreground",
|
||||
},
|
||||
},
|
||||
defaultVariants: {
|
||||
variant: "default",
|
||||
},
|
||||
}
|
||||
)
|
||||
|
||||
const Toast = React.forwardRef<
|
||||
React.ElementRef<typeof ToastPrimitives.Root>,
|
||||
React.ComponentPropsWithoutRef<typeof ToastPrimitives.Root> &
|
||||
VariantProps<typeof toastVariants>
|
||||
>(({ className, variant, ...props }, ref) => {
|
||||
return (
|
||||
<ToastPrimitives.Root
|
||||
ref={ref}
|
||||
className={cn(toastVariants({ variant }), className)}
|
||||
{...props}
|
||||
/>
|
||||
)
|
||||
})
|
||||
Toast.displayName = ToastPrimitives.Root.displayName
|
||||
|
||||
const ToastAction = React.forwardRef<
|
||||
React.ElementRef<typeof ToastPrimitives.Action>,
|
||||
React.ComponentPropsWithoutRef<typeof ToastPrimitives.Action>
|
||||
>(({ className, ...props }, ref) => (
|
||||
<ToastPrimitives.Action
|
||||
ref={ref}
|
||||
className={cn(
|
||||
"inline-flex h-8 shrink-0 items-center justify-center rounded-md border bg-transparent px-3 text-sm font-medium ring-offset-background transition-colors hover:bg-secondary focus:outline-none focus:ring-2 focus:ring-ring focus:ring-offset-2 disabled:pointer-events-none disabled:opacity-50 group-[.destructive]:border-muted/40 group-[.destructive]:hover:border-destructive/30 group-[.destructive]:hover:bg-destructive group-[.destructive]:hover:text-destructive-foreground group-[.destructive]:focus:ring-destructive",
|
||||
className
|
||||
)}
|
||||
{...props}
|
||||
/>
|
||||
))
|
||||
ToastAction.displayName = ToastPrimitives.Action.displayName
|
||||
|
||||
const ToastClose = React.forwardRef<
|
||||
React.ElementRef<typeof ToastPrimitives.Close>,
|
||||
React.ComponentPropsWithoutRef<typeof ToastPrimitives.Close>
|
||||
>(({ className, ...props }, ref) => (
|
||||
<ToastPrimitives.Close
|
||||
ref={ref}
|
||||
className={cn(
|
||||
"absolute right-2 top-2 rounded-md p-1 text-foreground/50 opacity-0 transition-opacity hover:text-foreground focus:opacity-100 focus:outline-none focus:ring-2 group-hover:opacity-100 group-[.destructive]:text-red-300 group-[.destructive]:hover:text-red-50 group-[.destructive]:focus:ring-red-400 group-[.destructive]:focus:ring-offset-red-600",
|
||||
className
|
||||
)}
|
||||
toast-close=""
|
||||
{...props}
|
||||
>
|
||||
<X className="h-4 w-4" />
|
||||
</ToastPrimitives.Close>
|
||||
))
|
||||
ToastClose.displayName = ToastPrimitives.Close.displayName
|
||||
|
||||
const ToastTitle = React.forwardRef<
|
||||
React.ElementRef<typeof ToastPrimitives.Title>,
|
||||
React.ComponentPropsWithoutRef<typeof ToastPrimitives.Title>
|
||||
>(({ className, ...props }, ref) => (
|
||||
<ToastPrimitives.Title
|
||||
ref={ref}
|
||||
className={cn("text-sm font-semibold", className)}
|
||||
{...props}
|
||||
/>
|
||||
))
|
||||
ToastTitle.displayName = ToastPrimitives.Title.displayName
|
||||
|
||||
const ToastDescription = React.forwardRef<
|
||||
React.ElementRef<typeof ToastPrimitives.Description>,
|
||||
React.ComponentPropsWithoutRef<typeof ToastPrimitives.Description>
|
||||
>(({ className, ...props }, ref) => (
|
||||
<ToastPrimitives.Description
|
||||
ref={ref}
|
||||
className={cn("text-sm opacity-90", className)}
|
||||
{...props}
|
||||
/>
|
||||
))
|
||||
ToastDescription.displayName = ToastPrimitives.Description.displayName
|
||||
|
||||
type ToastProps = React.ComponentPropsWithoutRef<typeof Toast>
|
||||
|
||||
type ToastActionElement = React.ReactElement<typeof ToastAction>
|
||||
|
||||
export {
|
||||
type ToastProps,
|
||||
type ToastActionElement,
|
||||
ToastProvider,
|
||||
ToastViewport,
|
||||
Toast,
|
||||
ToastTitle,
|
||||
ToastDescription,
|
||||
ToastClose,
|
||||
ToastAction,
|
||||
}
|
||||
9
apps/marketplace/src/counter.ts
Normal file
9
apps/marketplace/src/counter.ts
Normal file
@@ -0,0 +1,9 @@
|
||||
export function setupCounter(element: HTMLButtonElement) {
|
||||
let counter = 0
|
||||
const setCounter = (count: number) => {
|
||||
counter = count
|
||||
element.innerHTML = `count is ${counter}`
|
||||
}
|
||||
element.addEventListener('click', () => setCounter(counter + 1))
|
||||
setCounter(0)
|
||||
}
|
||||
191
apps/marketplace/src/hooks/use-toast.ts
Normal file
191
apps/marketplace/src/hooks/use-toast.ts
Normal file
@@ -0,0 +1,191 @@
|
||||
import * as React from "react"
|
||||
|
||||
import type {
|
||||
ToastActionElement,
|
||||
ToastProps,
|
||||
} from "@/components/ui/toast"
|
||||
|
||||
const TOAST_LIMIT = 1
|
||||
const TOAST_REMOVE_DELAY = 1000000
|
||||
|
||||
type ToasterToast = ToastProps & {
|
||||
id: string
|
||||
title?: React.ReactNode
|
||||
description?: React.ReactNode
|
||||
action?: ToastActionElement
|
||||
}
|
||||
|
||||
const actionTypes = {
|
||||
ADD_TOAST: "ADD_TOAST",
|
||||
UPDATE_TOAST: "UPDATE_TOAST",
|
||||
DISMISS_TOAST: "DISMISS_TOAST",
|
||||
REMOVE_TOAST: "REMOVE_TOAST",
|
||||
} as const
|
||||
|
||||
let count = 0
|
||||
|
||||
function genId() {
|
||||
count = (count + 1) % Number.MAX_SAFE_INTEGER
|
||||
return count.toString()
|
||||
}
|
||||
|
||||
type ActionType = typeof actionTypes
|
||||
|
||||
type Action =
|
||||
| {
|
||||
type: ActionType["ADD_TOAST"]
|
||||
toast: ToasterToast
|
||||
}
|
||||
| {
|
||||
type: ActionType["UPDATE_TOAST"]
|
||||
toast: Partial<ToasterToast>
|
||||
}
|
||||
| {
|
||||
type: ActionType["DISMISS_TOAST"]
|
||||
toastId?: ToasterToast["id"]
|
||||
}
|
||||
| {
|
||||
type: ActionType["REMOVE_TOAST"]
|
||||
toastId?: ToasterToast["id"]
|
||||
}
|
||||
|
||||
interface State {
|
||||
toasts: ToasterToast[]
|
||||
}
|
||||
|
||||
const toastTimeouts = new Map<string, ReturnType<typeof setTimeout>>()
|
||||
|
||||
const addToRemoveQueue = (toastId: string) => {
|
||||
if (toastTimeouts.has(toastId)) {
|
||||
return
|
||||
}
|
||||
|
||||
const timeout = setTimeout(() => {
|
||||
toastTimeouts.delete(toastId)
|
||||
dispatch({
|
||||
type: "REMOVE_TOAST",
|
||||
toastId: toastId,
|
||||
})
|
||||
}, TOAST_REMOVE_DELAY)
|
||||
|
||||
toastTimeouts.set(toastId, timeout)
|
||||
}
|
||||
|
||||
export const reducer = (state: State, action: Action): State => {
|
||||
switch (action.type) {
|
||||
case "ADD_TOAST":
|
||||
return {
|
||||
...state,
|
||||
toasts: [action.toast, ...state.toasts].slice(0, TOAST_LIMIT),
|
||||
}
|
||||
|
||||
case "UPDATE_TOAST":
|
||||
return {
|
||||
...state,
|
||||
toasts: state.toasts.map((t) =>
|
||||
t.id === action.toast.id ? { ...t, ...action.toast } : t
|
||||
),
|
||||
}
|
||||
|
||||
case "DISMISS_TOAST": {
|
||||
const { toastId } = action
|
||||
|
||||
// ! Side effects ! - This could be extracted into a dismissToast() action,
|
||||
// but I'll keep it here for simplicity
|
||||
if (toastId) {
|
||||
addToRemoveQueue(toastId)
|
||||
} else {
|
||||
state.toasts.forEach((toast) => {
|
||||
addToRemoveQueue(toast.id)
|
||||
})
|
||||
}
|
||||
|
||||
return {
|
||||
...state,
|
||||
toasts: state.toasts.map((t) =>
|
||||
t.id === toastId || toastId === undefined
|
||||
? {
|
||||
...t,
|
||||
open: false,
|
||||
}
|
||||
: t
|
||||
),
|
||||
}
|
||||
}
|
||||
case "REMOVE_TOAST":
|
||||
if (action.toastId === undefined) {
|
||||
return {
|
||||
...state,
|
||||
toasts: [],
|
||||
}
|
||||
}
|
||||
return {
|
||||
...state,
|
||||
toasts: state.toasts.filter((t) => t.id !== action.toastId),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
const listeners: Array<(state: State) => void> = []
|
||||
|
||||
let memoryState: State = { toasts: [] }
|
||||
|
||||
function dispatch(action: Action) {
|
||||
memoryState = reducer(memoryState, action)
|
||||
listeners.forEach((listener) => {
|
||||
listener(memoryState)
|
||||
})
|
||||
}
|
||||
|
||||
type Toast = Omit<ToasterToast, "id">
|
||||
|
||||
function toast({ ...props }: Toast) {
|
||||
const id = genId()
|
||||
|
||||
const update = (props: ToasterToast) =>
|
||||
dispatch({
|
||||
type: "UPDATE_TOAST",
|
||||
toast: { ...props, id },
|
||||
})
|
||||
const dismiss = () => dispatch({ type: "DISMISS_TOAST", toastId: id })
|
||||
|
||||
dispatch({
|
||||
type: "ADD_TOAST",
|
||||
toast: {
|
||||
...props,
|
||||
id,
|
||||
open: true,
|
||||
onOpenChange: (open) => {
|
||||
if (!open) dismiss()
|
||||
},
|
||||
},
|
||||
})
|
||||
|
||||
return {
|
||||
id: id,
|
||||
dismiss,
|
||||
update,
|
||||
}
|
||||
}
|
||||
|
||||
function useToast() {
|
||||
const [state, setState] = React.useState<State>(memoryState)
|
||||
|
||||
React.useEffect(() => {
|
||||
listeners.push(setState)
|
||||
return () => {
|
||||
const index = listeners.indexOf(setState)
|
||||
if (index > -1) {
|
||||
listeners.splice(index, 1)
|
||||
}
|
||||
}
|
||||
}, [state])
|
||||
|
||||
return {
|
||||
...state,
|
||||
toast,
|
||||
dismiss: (toastId?: string) => dispatch({ type: "DISMISS_TOAST", toastId }),
|
||||
}
|
||||
}
|
||||
|
||||
export { useToast, toast }
|
||||
52
apps/marketplace/src/hooks/use-wallet.ts
Normal file
52
apps/marketplace/src/hooks/use-wallet.ts
Normal file
@@ -0,0 +1,52 @@
|
||||
import { useState, useEffect } from 'react'
|
||||
|
||||
interface WalletState {
|
||||
address: string | null
|
||||
isConnected: boolean
|
||||
isConnecting: boolean
|
||||
balance: string
|
||||
}
|
||||
|
||||
export function useWallet() {
|
||||
const [walletState, setWalletState] = useState<WalletState>({
|
||||
address: null,
|
||||
isConnected: false,
|
||||
isConnecting: false,
|
||||
balance: '0'
|
||||
})
|
||||
|
||||
const connect = async () => {
|
||||
setWalletState(prev => ({ ...prev, isConnecting: true }))
|
||||
|
||||
try {
|
||||
// Mock wallet connection - replace with actual wallet logic
|
||||
await new Promise(resolve => setTimeout(resolve, 1000))
|
||||
const mockAddress = '0x1234567890123456789012345678901234567890'
|
||||
|
||||
setWalletState({
|
||||
address: mockAddress,
|
||||
isConnected: true,
|
||||
isConnecting: false,
|
||||
balance: '1000.0'
|
||||
})
|
||||
} catch (error) {
|
||||
setWalletState(prev => ({ ...prev, isConnecting: false }))
|
||||
throw error
|
||||
}
|
||||
}
|
||||
|
||||
const disconnect = () => {
|
||||
setWalletState({
|
||||
address: null,
|
||||
isConnected: false,
|
||||
isConnecting: false,
|
||||
balance: '0'
|
||||
})
|
||||
}
|
||||
|
||||
return {
|
||||
...walletState,
|
||||
connect,
|
||||
disconnect
|
||||
}
|
||||
}
|
||||
60
apps/marketplace/src/index.css
Normal file
60
apps/marketplace/src/index.css
Normal file
@@ -0,0 +1,60 @@
|
||||
@tailwind base;
|
||||
@tailwind components;
|
||||
@tailwind utilities;
|
||||
|
||||
@layer base {
|
||||
:root {
|
||||
--background: 0 0% 100%;
|
||||
--foreground: 222.2 84% 4.9%;
|
||||
--card: 0 0% 100%;
|
||||
--card-foreground: 222.2 84% 4.9%;
|
||||
--popover: 0 0% 100%;
|
||||
--popover-foreground: 222.2 84% 4.9%;
|
||||
--primary: 221.2 83.2% 53.3%;
|
||||
--primary-foreground: 210 40% 98%;
|
||||
--secondary: 210 40% 96%;
|
||||
--secondary-foreground: 222.2 84% 4.9%;
|
||||
--muted: 210 40% 96%;
|
||||
--muted-foreground: 215.4 16.3% 46.9%;
|
||||
--accent: 210 40% 96%;
|
||||
--accent-foreground: 222.2 84% 4.9%;
|
||||
--destructive: 0 84.2% 60.2%;
|
||||
--destructive-foreground: 210 40% 98%;
|
||||
--border: 214.3 31.8% 91.4%;
|
||||
--input: 214.3 31.8% 91.4%;
|
||||
--ring: 221.2 83.2% 53.3%;
|
||||
--radius: 0.5rem;
|
||||
}
|
||||
|
||||
.dark {
|
||||
--background: 222.2 84% 4.9%;
|
||||
--foreground: 210 40% 98%;
|
||||
--card: 222.2 84% 4.9%;
|
||||
--card-foreground: 210 40% 98%;
|
||||
--popover: 222.2 84% 4.9%;
|
||||
--popover-foreground: 210 40% 98%;
|
||||
--primary: 217.2 91.2% 59.8%;
|
||||
--primary-foreground: 222.2 84% 4.9%;
|
||||
--secondary: 217.2 32.6% 17.5%;
|
||||
--secondary-foreground: 210 40% 98%;
|
||||
--muted: 217.2 32.6% 17.5%;
|
||||
--muted-foreground: 215 20.2% 65.1%;
|
||||
--accent: 217.2 32.6% 17.5%;
|
||||
--accent-foreground: 210 40% 98%;
|
||||
--destructive: 0 62.8% 30.6%;
|
||||
--destructive-foreground: 210 40% 98%;
|
||||
--border: 217.2 32.6% 17.5%;
|
||||
--input: 217.2 32.6% 17.5%;
|
||||
--ring: 224.3 76.3% 94.1%;
|
||||
}
|
||||
}
|
||||
|
||||
@layer base {
|
||||
* {
|
||||
border-color: hsl(var(--border));
|
||||
}
|
||||
body {
|
||||
background-color: hsl(var(--background));
|
||||
color: hsl(var(--foreground));
|
||||
}
|
||||
}
|
||||
132
apps/marketplace/src/lib/api.ts
Normal file
132
apps/marketplace/src/lib/api.ts
Normal file
@@ -0,0 +1,132 @@
|
||||
import { loadSession } from "./auth";
|
||||
|
||||
export type DataMode = "mock" | "live";
|
||||
|
||||
interface OfferRecord {
|
||||
id: string;
|
||||
provider: string;
|
||||
capacity: number;
|
||||
price: number;
|
||||
sla: string;
|
||||
status: string;
|
||||
created_at?: string;
|
||||
gpu_model?: string;
|
||||
gpu_memory_gb?: number;
|
||||
gpu_count?: number;
|
||||
cuda_version?: string;
|
||||
price_per_hour?: number;
|
||||
region?: string;
|
||||
attributes?: {
|
||||
ollama_host?: string;
|
||||
models?: string[];
|
||||
vram_mb?: number;
|
||||
driver?: string;
|
||||
[key: string]: unknown;
|
||||
};
|
||||
}
|
||||
|
||||
interface OffersResponse {
|
||||
offers: OfferRecord[];
|
||||
}
|
||||
|
||||
export interface MarketplaceStats {
|
||||
totalOffers: number;
|
||||
openCapacity: number;
|
||||
averagePrice: number;
|
||||
activeBids: number;
|
||||
}
|
||||
|
||||
export interface MarketplaceOffer extends OfferRecord {}
|
||||
|
||||
const CONFIG = {
|
||||
dataMode: (import.meta.env?.VITE_MARKETPLACE_DATA_MODE as DataMode) ?? "mock",
|
||||
mockBase: "/mock",
|
||||
apiBase: import.meta.env?.VITE_MARKETPLACE_API ?? "http://localhost:8081",
|
||||
enableBids:
|
||||
(import.meta.env?.VITE_MARKETPLACE_ENABLE_BIDS ?? "true").toLowerCase() !==
|
||||
"false",
|
||||
requireAuth:
|
||||
(import.meta.env?.VITE_MARKETPLACE_REQUIRE_AUTH ?? "false").toLowerCase() ===
|
||||
"true",
|
||||
};
|
||||
|
||||
function buildHeaders(): HeadersInit {
|
||||
const headers: Record<string, string> = {
|
||||
"Cache-Control": "no-cache",
|
||||
};
|
||||
|
||||
const session = loadSession();
|
||||
if (session) {
|
||||
headers.Authorization = `Bearer ${session.token}`;
|
||||
}
|
||||
|
||||
return headers;
|
||||
}
|
||||
|
||||
async function request<T>(path: string, init?: RequestInit): Promise<T> {
|
||||
const response = await fetch(path, {
|
||||
...init,
|
||||
headers: {
|
||||
...buildHeaders(),
|
||||
...init?.headers,
|
||||
},
|
||||
});
|
||||
|
||||
if (!response.ok) {
|
||||
throw new Error(`Request failed: ${response.status}`);
|
||||
}
|
||||
return response.json() as Promise<T>;
|
||||
}
|
||||
|
||||
export async function fetchMarketplaceStats(): Promise<MarketplaceStats> {
|
||||
if (CONFIG.dataMode === "mock") {
|
||||
return request<MarketplaceStats>(`${CONFIG.mockBase}/stats.json`);
|
||||
}
|
||||
|
||||
return request<MarketplaceStats>(`${CONFIG.apiBase}/marketplace/stats`);
|
||||
}
|
||||
|
||||
export async function fetchMarketplaceOffers(): Promise<MarketplaceOffer[]> {
|
||||
if (CONFIG.dataMode === "mock") {
|
||||
const payload = await request<OffersResponse>(`${CONFIG.mockBase}/offers.json`);
|
||||
return payload.offers;
|
||||
}
|
||||
|
||||
return request<MarketplaceOffer[]>(`${CONFIG.apiBase}/marketplace/offers`);
|
||||
}
|
||||
|
||||
export async function submitMarketplaceBid(input: {
|
||||
provider: string;
|
||||
capacity: number;
|
||||
price: number;
|
||||
notes?: string;
|
||||
}): Promise<void> {
|
||||
if (!CONFIG.enableBids) {
|
||||
throw new Error("Bid submissions are disabled by configuration");
|
||||
}
|
||||
|
||||
if (CONFIG.dataMode === "mock") {
|
||||
await new Promise((resolve) => setTimeout(resolve, 600));
|
||||
return;
|
||||
}
|
||||
|
||||
if (CONFIG.requireAuth && !loadSession()) {
|
||||
throw new Error("Authentication required to submit bids");
|
||||
}
|
||||
|
||||
const response = await fetch(`${CONFIG.apiBase}/marketplace/bids`, {
|
||||
method: "POST",
|
||||
headers: {
|
||||
"Content-Type": "application/json",
|
||||
...buildHeaders(),
|
||||
},
|
||||
body: JSON.stringify(input),
|
||||
});
|
||||
|
||||
if (!response.ok) {
|
||||
const message = await response.text();
|
||||
throw new Error(message || "Failed to submit bid");
|
||||
}
|
||||
}
|
||||
|
||||
export const MARKETPLACE_CONFIG = CONFIG;
|
||||
33
apps/marketplace/src/lib/auth.ts
Normal file
33
apps/marketplace/src/lib/auth.ts
Normal file
@@ -0,0 +1,33 @@
|
||||
export interface MarketplaceSession {
|
||||
token: string;
|
||||
expiresAt: number;
|
||||
}
|
||||
|
||||
const STORAGE_KEY = "marketplace-session";
|
||||
|
||||
export function saveSession(session: MarketplaceSession): void {
|
||||
localStorage.setItem(STORAGE_KEY, JSON.stringify(session));
|
||||
}
|
||||
|
||||
export function loadSession(): MarketplaceSession | null {
|
||||
const raw = localStorage.getItem(STORAGE_KEY);
|
||||
if (!raw) {
|
||||
return null;
|
||||
}
|
||||
try {
|
||||
const data = JSON.parse(raw) as MarketplaceSession;
|
||||
if (typeof data.token === "string" && typeof data.expiresAt === "number") {
|
||||
if (data.expiresAt > Date.now()) {
|
||||
return data;
|
||||
}
|
||||
clearSession();
|
||||
}
|
||||
} catch (error) {
|
||||
console.warn("Failed to parse stored marketplace session", error);
|
||||
}
|
||||
return null;
|
||||
}
|
||||
|
||||
export function clearSession(): void {
|
||||
localStorage.removeItem(STORAGE_KEY);
|
||||
}
|
||||
6
apps/marketplace/src/lib/utils.ts
Normal file
6
apps/marketplace/src/lib/utils.ts
Normal file
@@ -0,0 +1,6 @@
|
||||
import { type ClassValue, clsx } from "clsx"
|
||||
import { twMerge } from "tailwind-merge"
|
||||
|
||||
export function cn(...inputs: ClassValue[]) {
|
||||
return twMerge(clsx(inputs))
|
||||
}
|
||||
273
apps/marketplace/src/main.ts
Normal file
273
apps/marketplace/src/main.ts
Normal file
@@ -0,0 +1,273 @@
|
||||
// Type declarations for global objects
|
||||
declare global {
|
||||
interface Window {
|
||||
analytics?: {
|
||||
track: (event: string, data: any) => void;
|
||||
};
|
||||
}
|
||||
}
|
||||
|
||||
import './style.css';
|
||||
import {
|
||||
fetchMarketplaceOffers,
|
||||
fetchMarketplaceStats,
|
||||
submitMarketplaceBid,
|
||||
} from './lib/api';
|
||||
import type { MarketplaceOffer, MarketplaceStats } from './lib/api';
|
||||
|
||||
const app = document.querySelector<HTMLDivElement>('#app');
|
||||
|
||||
if (!app) {
|
||||
throw new Error('Unable to mount marketplace app');
|
||||
}
|
||||
|
||||
app.innerHTML = `
|
||||
<main>
|
||||
|
||||
|
||||
<section class="dashboard-grid" id="stats-panel">
|
||||
<article class="stat-card">
|
||||
<h2>Total Offers</h2>
|
||||
<strong id="stat-total-offers">--</strong>
|
||||
<span>Listings currently visible</span>
|
||||
</article>
|
||||
<article class="stat-card">
|
||||
<h2>Open Capacity</h2>
|
||||
<strong id="stat-open-capacity">--</strong>
|
||||
<span>GPU / compute units available</span>
|
||||
</article>
|
||||
<article class="stat-card">
|
||||
<h2>Average Price</h2>
|
||||
<strong id="stat-average-price">--</strong>
|
||||
<span>Credits per unit per hour</span>
|
||||
</article>
|
||||
<article class="stat-card">
|
||||
<h2>Active Bids</h2>
|
||||
<strong id="stat-active-bids">--</strong>
|
||||
<span>Open bids awaiting match</span>
|
||||
</article>
|
||||
</section>
|
||||
|
||||
|
||||
<section class="panels">
|
||||
<article class="panel" id="offers-panel">
|
||||
<h2>Available Offers</h2>
|
||||
<div id="offers-table-wrapper" class="table-wrapper">
|
||||
<p class="empty-state">Fetching marketplace offers…</p>
|
||||
</div>
|
||||
</article>
|
||||
|
||||
<article class="panel">
|
||||
<h2>Submit a Bid</h2>
|
||||
<form class="bid-form" id="bid-form">
|
||||
<div>
|
||||
<label for="bid-provider">Preferred provider</label>
|
||||
<input id="bid-provider" name="provider" placeholder="Alpha Pool" required />
|
||||
</div>
|
||||
<div>
|
||||
<label for="bid-capacity">Capacity required (units)</label>
|
||||
<input id="bid-capacity" name="capacity" type="number" min="1" step="1" required />
|
||||
</div>
|
||||
<div>
|
||||
<label for="bid-price">Bid price (credits/unit/hr)</label>
|
||||
<input id="bid-price" name="price" type="number" min="0" step="0.01" required />
|
||||
</div>
|
||||
<div>
|
||||
<label for="bid-notes">Notes (optional)</label>
|
||||
<textarea id="bid-notes" name="notes" rows="3" placeholder="Add constraints, time windows, etc."></textarea>
|
||||
</div>
|
||||
<button type="submit">Submit Bid</button>
|
||||
</form>
|
||||
</article>
|
||||
</section>
|
||||
</main>
|
||||
<aside id="toast" class="toast"></aside>
|
||||
`;
|
||||
|
||||
const selectors = {
|
||||
totalOffers: document.querySelector<HTMLSpanElement>('#stat-total-offers')!,
|
||||
openCapacity: document.querySelector<HTMLSpanElement>('#stat-open-capacity')!,
|
||||
averagePrice: document.querySelector<HTMLSpanElement>('#stat-average-price')!,
|
||||
activeBids: document.querySelector<HTMLSpanElement>('#stat-active-bids')!,
|
||||
statsWrapper: document.querySelector<HTMLDivElement>('#stats-grid')!,
|
||||
offersWrapper: document.querySelector<HTMLDivElement>('#offers-table-wrapper')!,
|
||||
bidForm: document.querySelector<HTMLFormElement>('#bid-form')!,
|
||||
toast: document.querySelector<HTMLDivElement>('#toast')!,
|
||||
};
|
||||
|
||||
function formatNumber(value: number, options: Intl.NumberFormatOptions = {}): string {
|
||||
return new Intl.NumberFormat(undefined, options).format(value);
|
||||
}
|
||||
|
||||
function renderStats(stats: MarketplaceStats): void {
|
||||
selectors.totalOffers.textContent = formatNumber(stats.totalOffers);
|
||||
selectors.openCapacity.textContent = `${formatNumber(stats.openCapacity)} units`;
|
||||
selectors.averagePrice.textContent = `${formatNumber(stats.averagePrice, {
|
||||
minimumFractionDigits: 2,
|
||||
maximumFractionDigits: 2,
|
||||
})} credits`;
|
||||
selectors.activeBids.textContent = formatNumber(stats.activeBids);
|
||||
}
|
||||
|
||||
function statusClass(status: string): string {
|
||||
switch (status.toLowerCase()) {
|
||||
case 'open':
|
||||
return 'status-pill status-open';
|
||||
case 'reserved':
|
||||
return 'status-pill status-reserved';
|
||||
default:
|
||||
return 'status-pill';
|
||||
}
|
||||
}
|
||||
|
||||
function renderOffers(offers: MarketplaceOffer[]): void {
|
||||
const wrapper = selectors.offersWrapper;
|
||||
if (!wrapper) return;
|
||||
|
||||
if (offers.length === 0) {
|
||||
wrapper.innerHTML = '<p class="empty-state">No offers available right now. Check back soon or submit a bid.</p>';
|
||||
return;
|
||||
}
|
||||
|
||||
const cards = offers
|
||||
.map(
|
||||
(offer) => `
|
||||
<article class="offer-card">
|
||||
<div class="offer-card-header">
|
||||
<div class="offer-gpu-name">${offer.gpu_model || 'Unknown GPU'}</div>
|
||||
<span class="${statusClass(offer.status)}">${offer.status}</span>
|
||||
</div>
|
||||
<div class="offer-provider">${offer.provider}${offer.region ? ` · ${offer.region}` : ''}</div>
|
||||
<div class="offer-specs">
|
||||
<div class="spec-item">
|
||||
<span class="spec-label">VRAM</span>
|
||||
<span class="spec-value">${offer.gpu_memory_gb ? `${offer.gpu_memory_gb} GB` : '—'}</span>
|
||||
</div>
|
||||
<div class="spec-item">
|
||||
<span class="spec-label">GPUs</span>
|
||||
<span class="spec-value">${offer.gpu_count ?? 1}×</span>
|
||||
</div>
|
||||
<div class="spec-item">
|
||||
<span class="spec-label">CUDA</span>
|
||||
<span class="spec-value">${offer.cuda_version || '—'}</span>
|
||||
</div>
|
||||
<div class="spec-item">
|
||||
<span class="spec-label">Capacity</span>
|
||||
<span class="spec-value">${formatNumber(offer.capacity)} units</span>
|
||||
</div>
|
||||
</div>
|
||||
${offer.attributes?.models?.length ? `
|
||||
<div class="offer-plugins">
|
||||
<span class="plugin-badge">Ollama</span>
|
||||
</div>
|
||||
<div class="offer-models">
|
||||
<span class="models-label">Available Models</span>
|
||||
<div class="model-tags">${offer.attributes.models.map((m: string) => `<span class="model-tag">${m}</span>`).join('')}</div>
|
||||
</div>` : ''}
|
||||
<div class="offer-pricing">
|
||||
<div class="offer-price">${formatNumber(offer.price_per_hour ?? offer.price, { minimumFractionDigits: 2, maximumFractionDigits: 2 })} <small>credits/hr</small></div>
|
||||
<div class="offer-sla">${offer.sla}</div>
|
||||
</div>
|
||||
</article>
|
||||
`,
|
||||
)
|
||||
.join('');
|
||||
|
||||
wrapper.innerHTML = `<div class="offers-grid">${cards}</div>`;
|
||||
}
|
||||
|
||||
function showToast(message: string, duration = 2500): void {
|
||||
if (!selectors.toast) return;
|
||||
selectors.toast.textContent = message;
|
||||
selectors.toast.classList.add('visible');
|
||||
|
||||
window.setTimeout(() => {
|
||||
selectors.toast?.classList.remove('visible');
|
||||
}, duration);
|
||||
}
|
||||
|
||||
async function loadDashboard(): Promise<void> {
|
||||
// Show skeleton loading states
|
||||
showSkeletons();
|
||||
|
||||
try {
|
||||
const [stats, offers] = await Promise.all([
|
||||
fetchMarketplaceStats(),
|
||||
fetchMarketplaceOffers(),
|
||||
]);
|
||||
|
||||
renderStats(stats);
|
||||
renderOffers(offers);
|
||||
} catch (error) {
|
||||
console.error(error);
|
||||
const wrapper = selectors.offersWrapper;
|
||||
if (wrapper) {
|
||||
wrapper.innerHTML = '<p class="empty-state">Failed to load offers. Please retry shortly.</p>';
|
||||
}
|
||||
showToast('Failed to load marketplace data.');
|
||||
}
|
||||
}
|
||||
|
||||
function showSkeletons() {
|
||||
const statsWrapper = selectors.statsWrapper;
|
||||
const offersWrapper = selectors.offersWrapper;
|
||||
|
||||
if (statsWrapper) {
|
||||
statsWrapper.innerHTML = `
|
||||
<div class="skeleton-grid">
|
||||
${Array(4).fill('').map(() => `
|
||||
<div class="skeleton skeleton-card"></div>
|
||||
`).join('')}
|
||||
</div>
|
||||
`;
|
||||
}
|
||||
|
||||
if (offersWrapper) {
|
||||
offersWrapper.innerHTML = `
|
||||
<div class="skeleton-list">
|
||||
${Array(6).fill('').map(() => `
|
||||
<div class="skeleton skeleton-card"></div>
|
||||
`).join('')}
|
||||
</div>
|
||||
`;
|
||||
}
|
||||
}
|
||||
|
||||
selectors.bidForm?.addEventListener('submit', async (event) => {
|
||||
event.preventDefault();
|
||||
|
||||
const form = selectors.bidForm;
|
||||
if (!form) return;
|
||||
|
||||
const formData = new FormData(form);
|
||||
const provider = formData.get('provider')?.toString().trim();
|
||||
const capacity = Number(formData.get('capacity'));
|
||||
const price = Number(formData.get('price'));
|
||||
const notes = formData.get('notes')?.toString().trim();
|
||||
|
||||
if (!provider || Number.isNaN(capacity) || Number.isNaN(price)) {
|
||||
showToast('Please complete the required fields.');
|
||||
return;
|
||||
}
|
||||
|
||||
try {
|
||||
const submitButton = form.querySelector('button');
|
||||
if (submitButton) {
|
||||
submitButton.setAttribute('disabled', 'disabled');
|
||||
}
|
||||
|
||||
await submitMarketplaceBid({ provider, capacity, price, notes });
|
||||
form.reset();
|
||||
showToast('Bid submitted successfully!');
|
||||
} catch (error) {
|
||||
console.error(error);
|
||||
showToast('Unable to submit bid. Please try again.');
|
||||
} finally {
|
||||
const submitButton = form.querySelector('button');
|
||||
if (submitButton) {
|
||||
submitButton.removeAttribute('disabled');
|
||||
}
|
||||
}
|
||||
});
|
||||
|
||||
loadDashboard();
|
||||
10
apps/marketplace/src/main.tsx
Normal file
10
apps/marketplace/src/main.tsx
Normal file
@@ -0,0 +1,10 @@
|
||||
import React from 'react'
|
||||
import ReactDOM from 'react-dom/client'
|
||||
import App from './App'
|
||||
import './index.css'
|
||||
|
||||
ReactDOM.createRoot(document.getElementById('app')!).render(
|
||||
<React.StrictMode>
|
||||
<App />
|
||||
</React.StrictMode>,
|
||||
)
|
||||
548
apps/marketplace/src/pages/BountyBoard.tsx
Normal file
548
apps/marketplace/src/pages/BountyBoard.tsx
Normal 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;
|
||||
692
apps/marketplace/src/pages/DeveloperLeaderboard.tsx
Normal file
692
apps/marketplace/src/pages/DeveloperLeaderboard.tsx
Normal 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;
|
||||
860
apps/marketplace/src/pages/EcosystemDashboard.tsx
Normal file
860
apps/marketplace/src/pages/EcosystemDashboard.tsx
Normal 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;
|
||||
917
apps/marketplace/src/pages/StakingDashboard.tsx
Normal file
917
apps/marketplace/src/pages/StakingDashboard.tsx
Normal 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;
|
||||
565
apps/marketplace/src/style.css
Normal file
565
apps/marketplace/src/style.css
Normal file
@@ -0,0 +1,565 @@
|
||||
:root {
|
||||
font-family: "Inter", system-ui, -apple-system, sans-serif;
|
||||
color: #121212;
|
||||
background-color: #f7f8fa;
|
||||
font-synthesis: none;
|
||||
text-rendering: optimizeLegibility;
|
||||
-webkit-font-smoothing: antialiased;
|
||||
-moz-osx-font-smoothing: grayscale;
|
||||
}
|
||||
|
||||
/* Skeleton loading styles */
|
||||
.skeleton {
|
||||
background: linear-gradient(90deg, #f0f0f0 25%, #e0e0e0 50%, #f0f0f0 75%);
|
||||
background-size: 200% 100%;
|
||||
animation: skeleton-loading 1.5s infinite;
|
||||
border-radius: 4px;
|
||||
}
|
||||
|
||||
@keyframes skeleton-loading {
|
||||
0% { background-position: 200% 0; }
|
||||
100% { background-position: -200% 0; }
|
||||
}
|
||||
|
||||
.skeleton-card {
|
||||
height: 120px;
|
||||
margin-bottom: 1rem;
|
||||
}
|
||||
|
||||
.skeleton-grid {
|
||||
display: grid;
|
||||
grid-template-columns: repeat(auto-fit, minmax(200px, 1fr));
|
||||
gap: 1rem;
|
||||
margin-bottom: 2rem;
|
||||
}
|
||||
|
||||
.skeleton-list {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
gap: 1rem;
|
||||
}
|
||||
|
||||
.dark .skeleton {
|
||||
background: linear-gradient(90deg, #374151 25%, #4b5563 50%, #374151 75%);
|
||||
background-size: 200% 100;
|
||||
}
|
||||
|
||||
/* Dark mode variables */
|
||||
.dark {
|
||||
--bg-primary: #1f2937;
|
||||
--bg-secondary: #374151;
|
||||
--bg-card: #111827;
|
||||
--text-primary: #f9fafb;
|
||||
--text-secondary: #d1d5db;
|
||||
--text-muted: #9ca3af;
|
||||
--border-color: #4b5563;
|
||||
--hover-bg: #374151;
|
||||
}
|
||||
|
||||
body {
|
||||
margin: 0;
|
||||
min-height: 100vh;
|
||||
background: linear-gradient(180deg, #f7f8fa 0%, #eef1f6 100%);
|
||||
transition: background-color 0.3s ease;
|
||||
}
|
||||
|
||||
/* Dark mode body */
|
||||
.dark body {
|
||||
background: linear-gradient(180deg, #1f2937 0%, #111827 100%);
|
||||
color: var(--text-primary);
|
||||
}
|
||||
|
||||
#app {
|
||||
max-width: 1160px;
|
||||
margin: 0 auto;
|
||||
padding: 48px 24px 64px;
|
||||
}
|
||||
|
||||
.page-header {
|
||||
margin-bottom: 32px;
|
||||
}
|
||||
|
||||
.page-header__nav {
|
||||
display: flex;
|
||||
gap: 0.75rem;
|
||||
margin-bottom: 1rem;
|
||||
align-items: center;
|
||||
}
|
||||
|
||||
.page-header__nav a {
|
||||
font-size: 0.85rem;
|
||||
padding: 0.3rem 0.75rem;
|
||||
border-radius: 999px;
|
||||
color: #5a6575;
|
||||
text-decoration: none;
|
||||
transition: background 150ms ease, color 150ms ease;
|
||||
}
|
||||
|
||||
.dark .page-header__nav a {
|
||||
color: var(--text-secondary);
|
||||
}
|
||||
|
||||
.page-header__nav a:hover {
|
||||
background: rgba(37, 99, 235, 0.08);
|
||||
color: #2563eb;
|
||||
}
|
||||
|
||||
.dark .page-header__nav a:hover {
|
||||
background: rgba(37, 99, 235, 0.15);
|
||||
color: #60a5fa;
|
||||
}
|
||||
|
||||
.page-header__nav .back-link {
|
||||
border: 1px solid #d1d5db;
|
||||
color: #374151;
|
||||
}
|
||||
|
||||
.dark .page-header__nav .back-link {
|
||||
border-color: var(--border-color);
|
||||
color: var(--text-secondary);
|
||||
}
|
||||
|
||||
.page-header__nav .back-link:hover {
|
||||
border-color: #2563eb;
|
||||
color: #2563eb;
|
||||
background: rgba(37, 99, 235, 0.06);
|
||||
}
|
||||
|
||||
.dark .page-header__nav .back-link:hover {
|
||||
border-color: #60a5fa;
|
||||
color: #60a5fa;
|
||||
background: rgba(37, 99, 235, 0.12);
|
||||
}
|
||||
|
||||
.page-header h1 {
|
||||
font-size: 2.4rem;
|
||||
margin: 0 0 0.5rem;
|
||||
color: #1d2736;
|
||||
}
|
||||
|
||||
.dark .page-header h1 {
|
||||
color: var(--text-primary);
|
||||
}
|
||||
|
||||
.page-header p {
|
||||
margin: 0;
|
||||
color: #5a6575;
|
||||
}
|
||||
|
||||
.dark .page-header p {
|
||||
color: var(--text-secondary);
|
||||
}
|
||||
|
||||
.page-header-content {
|
||||
display: flex;
|
||||
justify-content: space-between;
|
||||
align-items: flex-start;
|
||||
gap: 2rem;
|
||||
}
|
||||
|
||||
.page-header-title {
|
||||
flex: 1;
|
||||
}
|
||||
|
||||
.dark-mode-toggle {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 0.5rem;
|
||||
padding: 0.5rem 1rem;
|
||||
background: rgba(59, 130, 246, 0.1);
|
||||
border: 2px solid #2563eb;
|
||||
border-radius: 999px;
|
||||
color: #2563eb;
|
||||
cursor: pointer;
|
||||
transition: all 0.2s ease;
|
||||
font-size: 0.9rem;
|
||||
font-weight: 500;
|
||||
}
|
||||
|
||||
.dark .dark-mode-toggle {
|
||||
background: rgba(59, 130, 246, 0.15);
|
||||
border-color: #60a5fa;
|
||||
color: #60a5fa;
|
||||
}
|
||||
|
||||
.dark-mode-toggle:hover {
|
||||
background: rgba(59, 130, 246, 0.15);
|
||||
transform: translateY(-1px);
|
||||
}
|
||||
|
||||
.dark .dark-mode-toggle:hover {
|
||||
background: rgba(59, 130, 246, 0.2);
|
||||
}
|
||||
|
||||
.dashboard-grid {
|
||||
display: grid;
|
||||
grid-template-columns: repeat(auto-fit, minmax(220px, 1fr));
|
||||
gap: 20px;
|
||||
margin-bottom: 32px;
|
||||
}
|
||||
|
||||
.stat-card {
|
||||
background: #ffffff;
|
||||
border-radius: 16px;
|
||||
padding: 20px;
|
||||
box-shadow: 0 12px 24px rgba(18, 24, 32, 0.08);
|
||||
transition: background-color 0.3s ease;
|
||||
}
|
||||
|
||||
.dark .stat-card {
|
||||
background: var(--bg-card);
|
||||
box-shadow: 0 12px 24px rgba(0, 0, 0, 0.3);
|
||||
}
|
||||
|
||||
.stat-card h2 {
|
||||
margin: 0 0 12px;
|
||||
font-size: 1rem;
|
||||
color: #64748b;
|
||||
}
|
||||
|
||||
.dark .stat-card h2 {
|
||||
color: var(--text-muted);
|
||||
}
|
||||
|
||||
.stat-card strong {
|
||||
font-size: 1.8rem;
|
||||
color: #1d2736;
|
||||
}
|
||||
|
||||
.dark .stat-card strong {
|
||||
color: var(--text-primary);
|
||||
}
|
||||
|
||||
.stat-card span {
|
||||
display: block;
|
||||
margin-top: 6px;
|
||||
color: #8895a7;
|
||||
font-size: 0.9rem;
|
||||
}
|
||||
|
||||
.dark .stat-card span {
|
||||
color: var(--text-secondary);
|
||||
}
|
||||
|
||||
.panel {
|
||||
background: #ffffff;
|
||||
border-radius: 16px;
|
||||
padding: 24px;
|
||||
box-shadow: 0 10px 20px rgba(15, 23, 42, 0.08);
|
||||
transition: background-color 0.3s ease;
|
||||
}
|
||||
|
||||
.dark .panel {
|
||||
background: var(--bg-card);
|
||||
box-shadow: 0 10px 20px rgba(0, 0, 0, 0.3);
|
||||
}
|
||||
|
||||
.panel h2 {
|
||||
margin: 0 0 16px;
|
||||
font-size: 1.4rem;
|
||||
color: #1d2736;
|
||||
}
|
||||
|
||||
.dark .panel h2 {
|
||||
color: var(--text-primary);
|
||||
}
|
||||
|
||||
.offers-table {
|
||||
width: 100%;
|
||||
border-collapse: collapse;
|
||||
}
|
||||
|
||||
.offers-table th,
|
||||
.offers-table td {
|
||||
padding: 12px 16px;
|
||||
text-align: left;
|
||||
border-bottom: 1px solid #e5e9f1;
|
||||
}
|
||||
|
||||
.dark .offers-table th,
|
||||
.dark .offers-table td {
|
||||
border-bottom-color: var(--border-color);
|
||||
}
|
||||
|
||||
.offers-table th {
|
||||
color: #64748b;
|
||||
font-size: 0.85rem;
|
||||
text-transform: uppercase;
|
||||
letter-spacing: 0.05em;
|
||||
}
|
||||
|
||||
.dark .offers-table th {
|
||||
color: var(--text-muted);
|
||||
}
|
||||
|
||||
.offers-table tbody tr:hover {
|
||||
background-color: rgba(99, 102, 241, 0.08);
|
||||
}
|
||||
|
||||
.dark .offers-table tbody tr:hover {
|
||||
background-color: var(--hover-bg);
|
||||
}
|
||||
|
||||
.offer-card {
|
||||
background: #ffffff;
|
||||
border: 1px solid #e5e9f1;
|
||||
border-radius: 14px;
|
||||
padding: 20px;
|
||||
transition: box-shadow 200ms ease, transform 200ms ease;
|
||||
}
|
||||
|
||||
.dark .offer-card {
|
||||
background: var(--bg-card);
|
||||
border-color: var(--border-color);
|
||||
}
|
||||
|
||||
.offer-card:hover {
|
||||
box-shadow: 0 8px 24px rgba(99, 102, 241, 0.12);
|
||||
transform: translateY(-2px);
|
||||
}
|
||||
|
||||
.offer-gpu-name {
|
||||
font-size: 1.15rem;
|
||||
font-weight: 700;
|
||||
color: #1d2736;
|
||||
}
|
||||
|
||||
.dark .offer-gpu-name {
|
||||
color: var(--text-primary);
|
||||
}
|
||||
|
||||
.offer-provider {
|
||||
font-size: 0.85rem;
|
||||
color: #64748b;
|
||||
margin-bottom: 16px;
|
||||
}
|
||||
|
||||
.dark .offer-provider {
|
||||
color: var(--text-secondary);
|
||||
}
|
||||
|
||||
.offer-specs {
|
||||
display: grid;
|
||||
grid-template-columns: repeat(4, 1fr);
|
||||
gap: 8px;
|
||||
background: #f8fafc;
|
||||
border-radius: 10px;
|
||||
padding: 12px;
|
||||
margin-bottom: 16px;
|
||||
}
|
||||
|
||||
.dark .offer-specs {
|
||||
background: var(--bg-secondary);
|
||||
}
|
||||
|
||||
.spec-label {
|
||||
display: block;
|
||||
font-size: 0.7rem;
|
||||
text-transform: uppercase;
|
||||
letter-spacing: 0.06em;
|
||||
color: #94a3b8;
|
||||
margin-bottom: 4px;
|
||||
}
|
||||
|
||||
.dark .spec-label {
|
||||
color: var(--text-muted);
|
||||
}
|
||||
|
||||
.spec-value {
|
||||
display: block;
|
||||
font-size: 0.95rem;
|
||||
font-weight: 600;
|
||||
color: #1e293b;
|
||||
}
|
||||
|
||||
.dark .spec-value {
|
||||
color: var(--text-primary);
|
||||
}
|
||||
|
||||
.offer-price {
|
||||
font-size: 1.3rem;
|
||||
font-weight: 700;
|
||||
color: #6366f1;
|
||||
}
|
||||
|
||||
.dark .offer-price {
|
||||
color: #a78bfa;
|
||||
}
|
||||
|
||||
.offer-price small {
|
||||
font-size: 0.75rem;
|
||||
font-weight: 500;
|
||||
color: #94a3b8;
|
||||
}
|
||||
|
||||
.dark .offer-price small {
|
||||
color: var(--text-muted);
|
||||
}
|
||||
|
||||
.offer-sla {
|
||||
font-size: 0.8rem;
|
||||
color: #64748b;
|
||||
}
|
||||
|
||||
.dark .offer-sla {
|
||||
color: var(--text-secondary);
|
||||
}
|
||||
|
||||
.model-tag {
|
||||
display: inline-block;
|
||||
padding: 3px 10px;
|
||||
border-radius: 8px;
|
||||
font-size: 0.78rem;
|
||||
font-weight: 500;
|
||||
background: #f1f5f9;
|
||||
color: #334155;
|
||||
border: 1px solid #e2e8f0;
|
||||
}
|
||||
|
||||
.dark .model-tag {
|
||||
background: var(--bg-secondary);
|
||||
color: var(--text-primary);
|
||||
border-color: var(--border-color);
|
||||
}
|
||||
|
||||
.status-pill {
|
||||
display: inline-flex;
|
||||
align-items: center;
|
||||
gap: 6px;
|
||||
padding: 4px 10px;
|
||||
border-radius: 999px;
|
||||
font-size: 0.8rem;
|
||||
font-weight: 600;
|
||||
}
|
||||
|
||||
.status-open {
|
||||
background-color: rgba(34, 197, 94, 0.12);
|
||||
color: #15803d;
|
||||
}
|
||||
|
||||
.status-reserved {
|
||||
background-color: rgba(59, 130, 246, 0.12);
|
||||
color: #1d4ed8;
|
||||
}
|
||||
|
||||
.dark .status-reserved {
|
||||
background-color: rgba(59, 130, 246, 0.2);
|
||||
color: #1d4ed8;
|
||||
}
|
||||
|
||||
.bid-form {
|
||||
display: grid;
|
||||
gap: 16px;
|
||||
}
|
||||
|
||||
.bid-form label {
|
||||
font-weight: 600;
|
||||
color: #374151;
|
||||
display: block;
|
||||
margin-bottom: 6px;
|
||||
}
|
||||
|
||||
.dark .bid-form label {
|
||||
color: var(--text-primary);
|
||||
}
|
||||
|
||||
.bid-form input,
|
||||
.bid-form select,
|
||||
.bid-form textarea {
|
||||
width: 100%;
|
||||
border-radius: 10px;
|
||||
border: 1px solid #d1d9e6;
|
||||
padding: 10px 12px;
|
||||
font-size: 1rem;
|
||||
font-family: inherit;
|
||||
background-color: #f9fbff;
|
||||
}
|
||||
|
||||
.dark .bid-form input,
|
||||
.dark .bid-form select,
|
||||
.dark .bid-form textarea {
|
||||
border-color: var(--border-color);
|
||||
background-color: var(--bg-secondary);
|
||||
color: var(--text-primary);
|
||||
}
|
||||
|
||||
.bid-form button {
|
||||
justify-self: flex-start;
|
||||
background: linear-gradient(135deg, #6366f1, #8b5cf6);
|
||||
color: #ffffff;
|
||||
border: none;
|
||||
border-radius: 999px;
|
||||
padding: 10px 20px;
|
||||
font-size: 1rem;
|
||||
font-weight: 600;
|
||||
cursor: pointer;
|
||||
transition: transform 150ms ease, box-shadow 150ms ease;
|
||||
}
|
||||
|
||||
.bid-form button:hover {
|
||||
transform: translateY(-1px);
|
||||
box-shadow: 0 10px 18px rgba(99, 102, 241, 0.3);
|
||||
}
|
||||
|
||||
.empty-state {
|
||||
padding: 24px;
|
||||
text-align: center;
|
||||
color: #6b7280;
|
||||
border: 1px dashed #cbd5f5;
|
||||
border-radius: 12px;
|
||||
background-color: rgba(99, 102, 241, 0.05);
|
||||
}
|
||||
|
||||
.dark .empty-state {
|
||||
color: var(--text-secondary);
|
||||
border-color: var(--border-color);
|
||||
background-color: rgba(99, 102, 241, 0.1);
|
||||
}
|
||||
|
||||
.toast {
|
||||
position: fixed;
|
||||
bottom: 24px;
|
||||
right: 24px;
|
||||
padding: 14px 18px;
|
||||
background: #111827;
|
||||
color: #ffffff;
|
||||
border-radius: 12px;
|
||||
box-shadow: 0 12px 24px rgba(15, 23, 42, 0.3);
|
||||
opacity: 0;
|
||||
transform: translateY(12px);
|
||||
transition: opacity 200ms ease, transform 200ms ease;
|
||||
}
|
||||
|
||||
.dark .toast {
|
||||
background: var(--bg-card);
|
||||
color: var(--text-primary);
|
||||
}
|
||||
|
||||
.toast.visible {
|
||||
opacity: 1;
|
||||
transform: translateY(0);
|
||||
}
|
||||
|
||||
@media (max-width: 720px) {
|
||||
#app {
|
||||
padding: 32px 16px 48px;
|
||||
}
|
||||
|
||||
.dashboard-grid {
|
||||
grid-template-columns: repeat(auto-fit, minmax(180px, 1fr));
|
||||
}
|
||||
|
||||
.offers-table th,
|
||||
.offers-table td {
|
||||
padding: 10px 12px;
|
||||
font-size: 0.95rem;
|
||||
}
|
||||
}
|
||||
/* Add padding to account for the fixed global header */
|
||||
main {
|
||||
margin-top: 90px;
|
||||
padding: 2rem;
|
||||
max-width: 1160px;
|
||||
margin-left: auto;
|
||||
margin-right: auto;
|
||||
}
|
||||
1
apps/marketplace/src/typescript.svg
Normal file
1
apps/marketplace/src/typescript.svg
Normal file
@@ -0,0 +1 @@
|
||||
<svg xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink" aria-hidden="true" role="img" class="iconify iconify--logos" width="32" height="32" preserveAspectRatio="xMidYMid meet" viewBox="0 0 256 256"><path fill="#007ACC" d="M0 128v128h256V0H0z"></path><path fill="#FFF" d="m56.612 128.85l-.081 10.483h33.32v94.68h23.568v-94.68h33.321v-10.28c0-5.69-.122-10.444-.284-10.566c-.122-.162-20.4-.244-44.983-.203l-44.74.122l-.121 10.443Zm149.955-10.742c6.501 1.625 11.459 4.51 16.01 9.224c2.357 2.52 5.851 7.111 6.136 8.208c.08.325-11.053 7.802-17.798 11.988c-.244.162-1.22-.894-2.317-2.52c-3.291-4.795-6.745-6.867-12.028-7.233c-7.76-.528-12.759 3.535-12.718 10.321c0 1.992.284 3.17 1.097 4.795c1.707 3.536 4.876 5.649 14.832 9.956c18.326 7.883 26.168 13.084 31.045 20.48c5.445 8.249 6.664 21.415 2.966 31.208c-4.063 10.646-14.14 17.879-28.323 20.276c-4.388.772-14.79.65-19.504-.203c-10.28-1.828-20.033-6.908-26.047-13.572c-2.357-2.6-6.949-9.387-6.664-9.874c.122-.163 1.178-.813 2.356-1.504c1.138-.65 5.446-3.129 9.509-5.485l7.355-4.267l1.544 2.276c2.154 3.29 6.867 7.801 9.712 9.305c8.167 4.307 19.383 3.698 24.909-1.26c2.357-2.153 3.332-4.388 3.332-7.68c0-2.966-.366-4.266-1.91-6.501c-1.99-2.845-6.054-5.242-17.595-10.24c-13.206-5.69-18.895-9.224-24.096-14.832c-3.007-3.25-5.852-8.452-7.03-12.8c-.975-3.617-1.22-12.678-.447-16.335c2.723-12.76 12.353-21.659 26.25-24.3c4.51-.853 14.994-.528 19.424.569Z"></path></svg>
|
||||
|
After Width: | Height: | Size: 1.4 KiB |
Reference in New Issue
Block a user