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

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

24
apps/marketplace/.gitignore vendored Normal file
View File

@@ -0,0 +1,24 @@
# Logs
logs
*.log
npm-debug.log*
yarn-debug.log*
yarn-error.log*
pnpm-debug.log*
lerna-debug.log*
node_modules
dist
dist-ssr
*.local
# Editor directories and files
.vscode/*
!.vscode/extensions.json
.idea
.DS_Store
*.suo
*.ntvs*
*.njsproj
*.sln
*.sw?

View File

@@ -0,0 +1,41 @@
# Marketplace Web
Mock UI for exploring marketplace offers and submitting bids.
## Development
```bash
npm install
npm run dev
```
The dev server listens on `http://localhost:5173/` by default. Adjust via `--host`/`--port` flags in the `systemd` unit or `package.json` script.
## Data Modes
Marketplace web reuses the explorer pattern of mock vs. live data:
- Set `VITE_MARKETPLACE_DATA_MODE=mock` (default) to consume JSON fixtures under `public/mock/`.
- Set `VITE_MARKETPLACE_DATA_MODE=live` and point `VITE_MARKETPLACE_API` to the coordinator backend when integration-ready.
### Feature Flags & Auth
- `VITE_MARKETPLACE_ENABLE_BIDS` (default `true`) gates whether the bid form submits to the backend. Set to `false` to keep the UI read-only during phased rollouts.
- `VITE_MARKETPLACE_REQUIRE_AUTH` (default `false`) enforces a bearer token session before live bid submissions. Tokens are stored in `localStorage` by `src/lib/auth.ts`; the API helpers automatically attach the `Authorization` header when a session is present.
- Session JSON is expected to include `token` (string) and `expiresAt` (epoch ms). Expired or malformed entries are cleared automatically.
Document any backend expectations (e.g., coordinator accepting bearer tokens) alongside the environment variables in deployment manifests.
## Structure
- `public/mock/offers.json` sample marketplace offers.
- `public/mock/stats.json` summary dashboard statistics.
- `src/lib/api.ts` data-mode-aware fetch helpers.
- `src/main.ts` renders dashboard, offers table, and bid form.
- `src/style.css` layout and visual styling.
## Submitting Bids
When in mock mode, bid submissions simulate latency and always succeed.
When in live mode, ensure the coordinator exposes `/v1/marketplace/offers`, `/v1/marketplace/stats`, and `/v1/marketplace/bids` endpoints compatible with the JSON shapes defined in `src/lib/api.ts`.

View File

@@ -0,0 +1,404 @@
#!/usr/bin/env python3
"""
AITBC Agent-First GPU Marketplace
Miners register GPU offerings, choose chains, and confirm deals
"""
import json
import uuid
from datetime import datetime, timedelta
from typing import Dict, List, Any, Optional
from fastapi import FastAPI, HTTPException, BackgroundTasks
from fastapi.middleware.cors import CORSMiddleware
from fastapi.responses import JSONResponse
from pydantic import BaseModel
import uvicorn
app = FastAPI(
title="AITBC Agent-First GPU Marketplace",
description="GPU trading marketplace where miners register offerings and confirm deals",
version="1.0.0"
)
# Add CORS middleware
app.add_middleware(
CORSMiddleware,
allow_origins=["*"],
allow_credentials=True,
allow_methods=["*"],
allow_headers=["*"],
)
# In-memory storage (replace with database in production)
gpu_offerings = {}
marketplace_deals = {}
miner_registrations = {}
chain_offerings = {}
# Supported chains
SUPPORTED_CHAINS = ["ait-devnet", "ait-testnet", "ait-mainnet"]
class GPUOffering(BaseModel):
miner_id: str
gpu_model: str
gpu_memory: int
cuda_cores: int
price_per_hour: float
available_hours: int
chains: List[str]
capabilities: List[str]
min_rental_hours: int = 1
max_concurrent_jobs: int = 1
class DealRequest(BaseModel):
offering_id: str
buyer_id: str
rental_hours: int
chain: str
special_requirements: Optional[str] = None
class DealConfirmation(BaseModel):
deal_id: str
miner_confirmation: bool
chain: str
class MinerRegistration(BaseModel):
miner_id: str
wallet_address: str
preferred_chains: List[str]
gpu_specs: Dict[str, Any]
pricing_model: str = "hourly"
@app.get("/health")
async def health_check():
return JSONResponse({
"status": "ok",
"service": "agent-marketplace",
"version": "1.0.0",
"supported_chains": SUPPORTED_CHAINS,
"total_offerings": len(gpu_offerings),
"active_deals": len([d for d in marketplace_deals.values() if d["status"] == "active"]),
"timestamp": datetime.now().isoformat()
})
@app.get("/api/v1/chains")
async def get_supported_chains():
return JSONResponse({
"chains": [
{
"chain_id": chain,
"name": chain.replace("ait-", "").upper(),
"status": "active" if chain == "ait-devnet" else "available",
"offerings_count": len([o for o in gpu_offerings.values() if chain in o["chains"]])
}
for chain in SUPPORTED_CHAINS
]
})
@app.post("/api/v1/miners/register")
async def register_miner(registration: MinerRegistration):
"""Register a miner in the marketplace"""
try:
miner_id = registration.miner_id
if miner_id in miner_registrations:
# Update existing registration
miner_registrations[miner_id].update(registration.dict())
else:
# New registration
miner_registrations[miner_id] = registration.dict()
miner_registrations[miner_id]["registered_at"] = datetime.now().isoformat()
return JSONResponse({
"success": True,
"miner_id": miner_id,
"status": "registered",
"registered_chains": registration.preferred_chains,
"message": "Miner registered successfully in marketplace"
})
except Exception as e:
raise HTTPException(status_code=500, detail=f"Registration failed: {str(e)}")
@app.post("/api/v1/offerings/create")
async def create_gpu_offering(offering: GPUOffering):
"""Miners create GPU offerings with chain selection"""
try:
offering_id = str(uuid.uuid4())
# Validate chains
invalid_chains = [c for c in offering.chains if c not in SUPPORTED_CHAINS]
if invalid_chains:
raise HTTPException(status_code=400, detail=f"Invalid chains: {invalid_chains}")
# Store offering
gpu_offerings[offering_id] = {
"offering_id": offering_id,
"created_at": datetime.now().isoformat(),
"status": "available",
**offering.dict()
}
# Update chain offerings
for chain in offering.chains:
if chain not in chain_offerings:
chain_offerings[chain] = []
chain_offerings[chain].append(offering_id)
return JSONResponse({
"success": True,
"offering_id": offering_id,
"status": "created",
"chains": offering.chains,
"price_per_hour": offering.price_per_hour,
"message": "GPU offering created successfully"
})
except Exception as e:
raise HTTPException(status_code=500, detail=f"Offering creation failed: {str(e)}")
@app.get("/api/v1/offerings")
async def get_gpu_offerings(chain: Optional[str] = None, gpu_model: Optional[str] = None):
"""Get available GPU offerings, filtered by chain and model"""
try:
filtered_offerings = gpu_offerings.copy()
if chain:
filtered_offerings = {
k: v for k, v in filtered_offerings.items()
if chain in v["chains"] and v["status"] == "available"
}
if gpu_model:
filtered_offerings = {
k: v for k, v in filtered_offerings.items()
if gpu_model.lower() in v["gpu_model"].lower()
}
return JSONResponse({
"offerings": list(filtered_offerings.values()),
"total_count": len(filtered_offerings),
"filters": {
"chain": chain,
"gpu_model": gpu_model
}
})
except Exception as e:
raise HTTPException(status_code=500, detail=f"Failed to get offerings: {str(e)}")
@app.get("/api/v1/offerings/{offering_id}")
async def get_gpu_offering(offering_id: str):
"""Get specific GPU offering details"""
if offering_id not in gpu_offerings:
raise HTTPException(status_code=404, detail="Offering not found")
offering = gpu_offerings[offering_id]
return JSONResponse(offering)
@app.post("/api/v1/deals/request")
async def request_deal(deal_request: DealRequest):
"""Buyers request GPU deals"""
try:
offering_id = deal_request.offering_id
if offering_id not in gpu_offerings:
raise HTTPException(status_code=404, detail="GPU offering not found")
offering = gpu_offerings[offering_id]
if offering["status"] != "available":
raise HTTPException(status_code=400, detail="GPU offering not available")
if deal_request.chain not in offering["chains"]:
raise HTTPException(status_code=400, detail="Chain not supported by this offering")
# Calculate total cost
total_cost = offering["price_per_hour"] * deal_request.rental_hours
# Create deal
deal_id = str(uuid.uuid4())
marketplace_deals[deal_id] = {
"deal_id": deal_id,
"offering_id": offering_id,
"buyer_id": deal_request.buyer_id,
"miner_id": offering["miner_id"],
"chain": deal_request.chain,
"rental_hours": deal_request.rental_hours,
"total_cost": total_cost,
"special_requirements": deal_request.special_requirements,
"status": "pending_confirmation",
"created_at": datetime.now().isoformat(),
"expires_at": (datetime.now() + timedelta(hours=1)).isoformat()
}
return JSONResponse({
"success": True,
"deal_id": deal_id,
"status": "pending_confirmation",
"total_cost": total_cost,
"expires_at": marketplace_deals[deal_id]["expires_at"],
"message": "Deal request sent to miner for confirmation"
})
except Exception as e:
raise HTTPException(status_code=500, detail=f"Deal request failed: {str(e)}")
@app.post("/api/v1/deals/{deal_id}/confirm")
async def confirm_deal(deal_id: str, confirmation: DealConfirmation):
"""Miners confirm or reject deal requests"""
try:
if deal_id not in marketplace_deals:
raise HTTPException(status_code=404, detail="Deal not found")
deal = marketplace_deals[deal_id]
if deal["status"] != "pending_confirmation":
raise HTTPException(status_code=400, detail="Deal cannot be confirmed")
if confirmation.chain != deal["chain"]:
raise HTTPException(status_code=400, detail="Chain mismatch")
if confirmation.miner_confirmation:
# Accept deal
deal["status"] = "confirmed"
deal["confirmed_at"] = datetime.now().isoformat()
deal["starts_at"] = datetime.now().isoformat()
deal["ends_at"] = (datetime.now() + timedelta(hours=deal["rental_hours"])).isoformat()
# Update offering status
offering_id = deal["offering_id"]
if offering_id in gpu_offerings:
gpu_offerings[offering_id]["status"] = "occupied"
message = "Deal confirmed successfully"
else:
# Reject deal
deal["status"] = "rejected"
deal["rejected_at"] = datetime.now().isoformat()
message = "Deal rejected by miner"
return JSONResponse({
"success": True,
"deal_id": deal_id,
"status": deal["status"],
"miner_confirmation": confirmation.miner_confirmation,
"message": message
})
except Exception as e:
raise HTTPException(status_code=500, detail=f"Deal confirmation failed: {str(e)}")
@app.get("/api/v1/deals")
async def get_deals(miner_id: Optional[str] = None, buyer_id: Optional[str] = None):
"""Get deals, filtered by miner or buyer"""
try:
filtered_deals = marketplace_deals.copy()
if miner_id:
filtered_deals = {
k: v for k, v in filtered_deals.items()
if v["miner_id"] == miner_id
}
if buyer_id:
filtered_deals = {
k: v for k, v in filtered_deals.items()
if v["buyer_id"] == buyer_id
}
return JSONResponse({
"deals": list(filtered_deals.values()),
"total_count": len(filtered_deals)
})
except Exception as e:
raise HTTPException(status_code=500, detail=f"Failed to get deals: {str(e)}")
@app.get("/api/v1/miners/{miner_id}/offerings")
async def get_miner_offerings(miner_id: str):
"""Get all offerings for a specific miner"""
try:
miner_offerings = {
k: v for k, v in gpu_offerings.items()
if v["miner_id"] == miner_id
}
return JSONResponse({
"miner_id": miner_id,
"offerings": list(miner_offerings.values()),
"total_count": len(miner_offerings)
})
except Exception as e:
raise HTTPException(status_code=500, detail=f"Failed to get miner offerings: {str(e)}")
@app.get("/api/v1/chains/{chain}/offerings")
async def get_chain_offerings(chain: str):
"""Get all offerings for a specific chain"""
try:
if chain not in SUPPORTED_CHAINS:
raise HTTPException(status_code=400, detail=f"Unsupported chain: {chain}")
chain_offering_ids = chain_offerings.get(chain, [])
chain_offs = {
k: v for k, v in gpu_offerings.items()
if k in chain_offering_ids and v["status"] == "available"
}
return JSONResponse({
"chain": chain,
"offerings": list(chain_offs.values()),
"total_count": len(chain_offs)
})
except Exception as e:
raise HTTPException(status_code=500, detail=f"Failed to get chain offerings: {str(e)}")
@app.delete("/api/v1/offerings/{offering_id}")
async def remove_offering(offering_id: str):
"""Miners remove their GPU offerings"""
try:
if offering_id not in gpu_offerings:
raise HTTPException(status_code=404, detail="Offering not found")
offering = gpu_offerings[offering_id]
# Remove from chain offerings
for chain in offering["chains"]:
if chain in chain_offerings and offering_id in chain_offerings[chain]:
chain_offerings[chain].remove(offering_id)
# Remove offering
del gpu_offerings[offering_id]
return JSONResponse({
"success": True,
"message": "GPU offering removed successfully"
})
except Exception as e:
raise HTTPException(status_code=500, detail=f"Failed to remove offering: {str(e)}")
@app.get("/api/v1/stats")
async def get_marketplace_stats():
"""Get marketplace statistics"""
try:
active_offerings = len([o for o in gpu_offerings.values() if o["status"] == "available"])
active_deals = len([d for d in marketplace_deals.values() if d["status"] in ["confirmed", "active"]])
chain_stats = {}
for chain in SUPPORTED_CHAINS:
chain_offerings = len([o for o in gpu_offerings.values() if chain in o["chains"] and o["status"] == "available"])
chain_deals = len([d for d in marketplace_deals.values() if d["chain"] == chain and d["status"] in ["confirmed", "active"]])
chain_stats[chain] = {
"offerings": chain_offerings,
"active_deals": chain_deals,
"total_gpu_hours": sum([o["available_hours"] for o in gpu_offerings.values() if chain in o["chains"]])
}
return JSONResponse({
"total_offerings": active_offerings,
"active_deals": active_deals,
"registered_miners": len(miner_registrations),
"supported_chains": SUPPORTED_CHAINS,
"chain_stats": chain_stats,
"timestamp": datetime.now().isoformat()
})
except Exception as e:
raise HTTPException(status_code=500, detail=f"Failed to get stats: {str(e)}")
if __name__ == "__main__":
uvicorn.run(app, host="0.0.0.0", port=8005, log_level="info")

View File

@@ -0,0 +1,288 @@
import { test, expect } from '@playwright/test';
test.describe('Bounty Board', () => {
test.beforeEach(async ({ page }) => {
await page.goto('/');
});
test('should display bounty board page correctly', async ({ page }) => {
// Navigate to bounty board
await page.click('text=Bounty Board');
// Check page title and header
await expect(page.locator('h1')).toContainText('Bounty Board');
await expect(page.locator('text=Developer Ecosystem')).toBeVisible();
// Check navigation is active
await expect(page.locator('button:has-text("Bounty Board")')).toHaveClass(/variant=default/);
});
test('should display bounty statistics cards', async ({ page }) => {
await page.click('text=Bounty Board');
// Check stats cards are present
await expect(page.locator('text=Active Bounties')).toBeVisible();
await expect(page.locator('text=Total Value')).toBeVisible();
await expect(page.locator('text=Completed Today')).toBeVisible();
await expect(page.locator('text=My Earnings')).toBeVisible();
});
test('should display bounty filters', async ({ page }) => {
await page.click('text=Bounty Board');
// Check filter elements
await expect(page.locator('input[placeholder*="Search"]')).toBeVisible();
await expect(page.locator('button:has-text("Filter")')).toBeVisible();
// Check status filter dropdown
await page.click('button:has-text("Status")');
await expect(page.locator('text=Active')).toBeVisible();
await expect(page.locator('text=Completed')).toBeVisible();
await expect(page.locator('text=Expired')).toBeVisible();
});
test('should display bounty list', async ({ page }) => {
await page.click('text=Bounty Board');
// Wait for bounties to load
await page.waitForSelector('[data-testid="bounty-list"]', { timeout: 10000 });
// Check bounty items
const bountyItems = page.locator('[data-testid="bounty-item"]');
const count = await bountyItems.count();
if (count > 0) {
// Check first bounty has required elements
const firstBounty = bountyItems.first();
await expect(firstBounty.locator('[data-testid="bounty-title"]')).toBeVisible();
await expect(firstBounty.locator('[data-testid="bounty-reward"]')).toBeVisible();
await expect(firstBounty.locator('[data-testid="bounty-status"]')).toBeVisible();
await expect(firstBounty.locator('[data-testid="bounty-deadline"]')).toBeVisible();
}
});
test('should filter bounties by status', async ({ page }) => {
await page.click('text=Bounty Board');
// Wait for bounties to load
await page.waitForSelector('[data-testid="bounty-list"]', { timeout: 10000 });
// Get initial count
const initialBounties = page.locator('[data-testid="bounty-item"]');
const initialCount = await initialBounties.count();
if (initialCount > 0) {
// Filter by active status
await page.click('button:has-text("Status")');
await page.click('text=Active');
// Wait for filter to apply
await page.waitForTimeout(1000);
// Check filtered results
const filteredBounties = page.locator('[data-testid="bounty-item"]');
const filteredCount = await filteredBounties.count();
// Should have same or fewer bounties
expect(filteredCount).toBeLessThanOrEqual(initialCount);
}
});
test('should search bounties', async ({ page }) => {
await page.click('text=Bounty Board');
// Wait for bounties to load
await page.waitForSelector('[data-testid="bounty-list"]', { timeout: 10000 });
// Get initial count
const initialBounties = page.locator('[data-testid="bounty-item"]');
const initialCount = await initialBounties.count();
if (initialCount > 0) {
// Search for specific term
await page.fill('input[placeholder*="Search"]', 'test');
// Wait for search to apply
await page.waitForTimeout(1000);
// Check search results
const searchResults = page.locator('[data-testid="bounty-item"]');
const searchCount = await searchResults.count();
// Should have same or fewer bounties
expect(searchCount).toBeLessThanOrEqual(initialCount);
}
});
test('should display bounty details modal', async ({ page }) => {
await page.click('text=Bounty Board');
// Wait for bounties to load
await page.waitForSelector('[data-testid="bounty-list"]', { timeout: 10000 });
const bountyItems = page.locator('[data-testid="bounty-item"]');
const count = await bountyItems.count();
if (count > 0) {
// Click on first bounty
await bountyItems.first().click();
// Check modal appears
await expect(page.locator('[data-testid="bounty-details-modal"]')).toBeVisible();
await expect(page.locator('text=Bounty Details')).toBeVisible();
// Check modal content
await expect(page.locator('[data-testid="bounty-description"]')).toBeVisible();
await expect(page.locator('[data-testid="bounty-requirements"]')).toBeVisible();
await expect(page.locator('button:has-text("Submit Solution")')).toBeVisible();
// Close modal
await page.click('button:has-text("Close")');
await expect(page.locator('[data-testid="bounty-details-modal"]')).not.toBeVisible();
}
});
test('should handle wallet connection', async ({ page }) => {
await page.click('text=Bounty Board');
// Check wallet connection button
await expect(page.locator('button:has-text("Connect Wallet")')).toBeVisible();
// Click connect wallet
await page.click('button:has-text("Connect Wallet")');
// Check wallet modal appears
await expect(page.locator('[data-testid="wallet-modal"]')).toBeVisible();
await expect(page.locator('text=Connect Wallet')).toBeVisible();
// Close wallet modal
await page.keyboard.press('Escape');
await expect(page.locator('[data-testid="wallet-modal"]')).not.toBeVisible();
});
test('should display bounty creation form', async ({ page }) => {
await page.click('text=Bounty Board');
// Check create bounty button
await expect(page.locator('button:has-text("Create Bounty")')).toBeVisible();
// Click create bounty
await page.click('button:has-text("Create Bounty")');
// Check form appears
await expect(page.locator('[data-testid="create-bounty-form"]')).toBeVisible();
await expect(page.locator('text=Create New Bounty')).toBeVisible();
// Check form fields
await expect(page.locator('input[name="title"]')).toBeVisible();
await expect(page.locator('textarea[name="description"]')).toBeVisible();
await expect(page.locator('input[name="reward"]')).toBeVisible();
await expect(page.locator('select[name="tier"]')).toBeVisible();
await expect(page.locator('select[name="difficulty"]')).toBeVisible();
// Check form buttons
await expect(page.locator('button:has-text("Create Bounty")')).toBeVisible();
await expect(page.locator('button:has-text("Cancel")')).toBeVisible();
});
test('should validate bounty creation form', async ({ page }) => {
await page.click('text=Bounty Board');
await page.click('button:has-text("Create Bounty")');
// Try to submit empty form
await page.click('button:has-text("Create Bounty")');
// Check validation errors
await expect(page.locator('text=Title is required')).toBeVisible();
await expect(page.locator('text=Description is required')).toBeVisible();
await expect(page.locator('text=Reward amount is required')).toBeVisible();
});
test('should handle pagination', async ({ page }) => {
await page.click('text=Bounty Board');
// Wait for bounties to load
await page.waitForSelector('[data-testid="bounty-list"]', { timeout: 10000 });
// Check pagination controls
const pagination = page.locator('[data-testid="pagination"]');
const isVisible = await pagination.isVisible();
if (isVisible) {
// Check page buttons
await expect(page.locator('button:has-text("Previous")')).toBeVisible();
await expect(page.locator('button:has-text("Next")')).toBeVisible();
// Check page numbers
const pageNumbers = page.locator('[data-testid="page-number"]');
const pageCount = await pageNumbers.count();
if (pageCount > 1) {
// Click next page
await page.click('button:has-text("Next")');
// Wait for page to load
await page.waitForTimeout(1000);
// Check URL or content changed
const currentUrl = page.url();
expect(currentUrl).toContain('page=');
}
}
});
test('should be responsive on mobile', async ({ page }) => {
// Set mobile viewport
await page.setViewportSize({ width: 375, height: 667 });
await page.click('text=Bounty Board');
// Check mobile layout
await expect(page.locator('h1')).toContainText('Bounty Board');
// Check mobile navigation
await expect(page.locator('button:has-text("☰")')).toBeVisible();
// Open mobile menu
await page.click('button:has-text("☰")');
await expect(page.locator('text=Staking')).toBeVisible();
await expect(page.locator('text=Leaderboard')).toBeVisible();
await expect(page.locator('text=Ecosystem')).toBeVisible();
// Close mobile menu
await page.click('button:has-text("☰")');
});
test('should handle loading states', async ({ page }) => {
await page.click('text=Bounty Board');
// Check loading skeleton
await expect(page.locator('[data-testid="loading-skeleton"]')).toBeVisible();
// Wait for content to load
await page.waitForSelector('[data-testid="bounty-list"]', { timeout: 10000 });
// Check loading skeleton is gone
await expect(page.locator('[data-testid="loading-skeleton"]')).not.toBeVisible();
});
test('should handle error states', async ({ page }) => {
// Mock API error
await page.route('**/api/v1/bounties*', route => {
route.fulfill({
status: 500,
contentType: 'application/json',
body: JSON.stringify({ error: 'Internal server error' })
});
});
await page.click('text=Bounty Board');
// Check error message
await expect(page.locator('[data-testid="error-message"]')).toBeVisible();
await expect(page.locator('text=Failed to load bounties')).toBeVisible();
// Check retry button
await expect(page.locator('button:has-text("Retry")')).toBeVisible();
});
});

View File

@@ -0,0 +1,351 @@
import { test, expect } from '@playwright/test';
test.describe('Staking Dashboard', () => {
test.beforeEach(async ({ page }) => {
await page.goto('/');
});
test('should display staking dashboard page correctly', async ({ page }) => {
// Navigate to staking dashboard
await page.click('text=Staking');
// Check page title and header
await expect(page.locator('h1')).toContainText('Staking Dashboard');
await expect(page.locator('text=Developer Ecosystem')).toBeVisible();
// Check navigation is active
await expect(page.locator('button:has-text("Staking")')).toHaveClass(/variant=default/);
});
test('should display staking overview cards', async ({ page }) => {
await page.click('text=Staking');
// Check overview cards are present
await expect(page.locator('text=Total Staked')).toBeVisible();
await expect(page.locator('text=My Stakes')).toBeVisible();
await expect(page.locator('text=Available Rewards')).toBeVisible();
await expect(page.locator('text=Average APY')).toBeVisible();
});
test('should display staking tabs', async ({ page }) => {
await page.click('text=Staking');
// Check tab navigation
await expect(page.locator('button:has-text("My Stakes")')).toBeVisible();
await expect(page.locator('button:has-text("Available Agents")')).toBeVisible();
await expect(page.locator('button:has-text("Staking Pools")')).toBeVisible();
await expect(page.locator('button:has-text("Rewards")')).toBeVisible();
});
test('should display my stakes tab', async ({ page }) => {
await page.click('text=Staking');
// My Stakes tab should be active by default
await expect(page.locator('button:has-text("My Stakes")')).toHaveClass(/data-state=active/);
// Check stakes table
await expect(page.locator('[data-testid="stakes-table"]')).toBeVisible();
// Check table headers
await expect(page.locator('text=Agent')).toBeVisible();
await expect(page.locator('text=Amount Staked')).toBeVisible();
await expect(page.locator('text=APY')).toBeVisible();
await expect(page.locator('text=Rewards')).toBeVisible();
await expect(page.locator('text=Actions')).toBeVisible();
});
test('should display available agents tab', async ({ page }) => {
await page.click('text=Staking');
await page.click('button:has-text("Available Agents")');
// Check agents list
await expect(page.locator('[data-testid="agents-list"]')).toBeVisible();
// Check agent cards
const agentCards = page.locator('[data-testid="agent-card"]');
const count = await agentCards.count();
if (count > 0) {
// Check first agent card elements
const firstAgent = agentCards.first();
await expect(firstAgent.locator('[data-testid="agent-name"]')).toBeVisible();
await expect(firstAgent.locator('[data-testid="agent-performance"]')).toBeVisible();
await expect(firstAgent.locator('[data-testid="agent-apy"]')).toBeVisible();
await expect(firstAgent.locator('button:has-text("Stake")')).toBeVisible();
}
});
test('should display staking pools tab', async ({ page }) => {
await page.click('text=Staking');
await page.click('button:has-text("Staking Pools")');
// Check pools table
await expect(page.locator('[data-testid="pools-table"]')).toBeVisible();
// Check table headers
await expect(page.locator('text=Agent Address')).toBeVisible();
await expect(page.locator('text=Total Staked')).toBeVisible();
await expect(page.locator('text=Stakers')).toBeVisible();
await expect(page.locator('text=APY')).toBeVisible();
await expect(page.locator('text=Utilization')).toBeVisible();
});
test('should display rewards tab', async ({ page }) => {
await page.click('text=Staking');
await page.click('button:has-text("Rewards")');
// Check rewards section
await expect(page.locator('[data-testid="rewards-section"]')).toBeVisible();
// Check rewards summary
await expect(page.locator('text=Total Earned')).toBeVisible();
await expect(page.locator('text=Pending Rewards')).toBeVisible();
await expect(page.locator('text=Claim History')).toBeVisible();
// Check claim button
await expect(page.locator('button:has-text("Claim Rewards")')).toBeVisible();
});
test('should handle staking modal', async ({ page }) => {
await page.click('text=Staking');
await page.click('button:has-text("Available Agents")');
// Wait for agents to load
await page.waitForSelector('[data-testid="agents-list"]', { timeout: 10000 });
const agentCards = page.locator('[data-testid="agent-card"]');
const count = await agentCards.count();
if (count > 0) {
// Click stake button on first agent
await agentCards.first().locator('button:has-text("Stake")').click();
// Check staking modal appears
await expect(page.locator('[data-testid="staking-modal"]')).toBeVisible();
await expect(page.locator('text=Stake Tokens')).toBeVisible();
// Check modal content
await expect(page.locator('input[name="amount"]')).toBeVisible();
await expect(page.locator('text=Available Balance')).toBeVisible();
await expect(page.locator('text=Estimated APY')).toBeVisible();
// Check modal buttons
await expect(page.locator('button:has-text("Confirm Stake")')).toBeVisible();
await expect(page.locator('button:has-text("Cancel")')).toBeVisible();
// Close modal
await page.click('button:has-text("Cancel")');
await expect(page.locator('[data-testid="staking-modal"]')).not.toBeVisible();
}
});
test('should validate staking amount', async ({ page }) => {
await page.click('text=Staking');
await page.click('button:has-text("Available Agents")');
await page.waitForSelector('[data-testid="agents-list"]', { timeout: 10000 });
const agentCards = page.locator('[data-testid="agent-card"]');
const count = await agentCards.count();
if (count > 0) {
await agentCards.first().locator('button:has-text("Stake")').click();
// Try to stake without amount
await page.click('button:has-text("Confirm Stake")');
// Check validation error
await expect(page.locator('text=Amount is required')).toBeVisible();
// Try to stake invalid amount
await page.fill('input[name="amount"]', '0');
await page.click('button:has-text("Confirm Stake")');
// Check validation error
await expect(page.locator('text=Amount must be greater than 0')).toBeVisible();
// Try to stake more than available
await page.fill('input[name="amount"]', '999999999');
await page.click('button:has-text("Confirm Stake")');
// Check validation error
await expect(page.locator('text=Insufficient balance')).toBeVisible();
}
});
test('should handle unstaking', async ({ page }) => {
await page.click('text=Staking');
// Wait for stakes to load
await page.waitForSelector('[data-testid="stakes-table"]', { timeout: 10000 });
const stakeRows = page.locator('[data-testid="stake-row"]');
const count = await stakeRows.count();
if (count > 0) {
// Click unstake button on first stake
await stakeRows.first().locator('button:has-text("Unstake")').click();
// Check unstaking modal appears
await expect(page.locator('[data-testid="unstaking-modal"]')).toBeVisible();
await expect(page.locator('text=Unstake Tokens')).toBeVisible();
// Check modal content
await expect(page.locator('text=Staked Amount')).toBeVisible();
await expect(page.locator('text=Unstaking Period')).toBeVisible();
await expect(page.locator('text=Early Unstaking Penalty')).toBeVisible();
// Check modal buttons
await expect(page.locator('button:has-text("Confirm Unstake")')).toBeVisible();
await expect(page.locator('button:has-text("Cancel")')).toBeVisible();
// Close modal
await page.click('button:has-text("Cancel")');
await expect(page.locator('[data-testid="unstaking-modal"]')).not.toBeVisible();
}
});
test('should display agent performance metrics', async ({ page }) => {
await page.click('text=Staking');
await page.click('button:has-text("Available Agents")');
await page.waitForSelector('[data-testid="agents-list"]', { timeout: 10000 });
const agentCards = page.locator('[data-testid="agent-card"]');
const count = await agentCards.count();
if (count > 0) {
const firstAgent = agentCards.first();
// Check performance metrics
await expect(firstAgent.locator('[data-testid="success-rate"]')).toBeVisible();
await expect(firstAgent.locator('[data-testid="total-tasks"]')).toBeVisible();
await expect(firstAgent.locator('[data-testid="average-accuracy"]')).toBeVisible();
await expect(firstAgent.locator('[data-testid="reliability-score"]')).toBeVisible();
}
});
test('should handle rewards claiming', async ({ page }) => {
await page.click('text=Staking');
await page.click('button:has-text("Rewards")');
// Wait for rewards to load
await page.waitForSelector('[data-testid="rewards-section"]', { timeout: 10000 });
// Check if there are claimable rewards
const claimButton = page.locator('button:has-text("Claim Rewards")');
const isDisabled = await claimButton.isDisabled();
if (!isDisabled) {
// Click claim rewards
await claimButton.click();
// Check confirmation modal
await expect(page.locator('[data-testid="claim-modal"]')).toBeVisible();
await expect(page.locator('text=Claim Rewards')).toBeVisible();
// Check claim details
await expect(page.locator('text=Total Rewards')).toBeVisible();
await expect(page.locator('text=Gas Fee')).toBeVisible();
await expect(page.locator('text=Net Amount')).toBeVisible();
// Confirm claim
await page.click('button:has-text("Confirm Claim")');
// Check success message
await expect(page.locator('[data-testid="success-message"]')).toBeVisible();
await expect(page.locator('text=Rewards claimed successfully')).toBeVisible();
}
});
test('should display staking statistics', async ({ page }) => {
await page.click('text=Staking');
// Check statistics section
await expect(page.locator('[data-testid="staking-stats"]')).toBeVisible();
// Check stat items
await expect(page.locator('text=Total Value Locked')).toBeVisible();
await expect(page.locator('text=Number of Stakers')).toBeVisible();
await expect(page.locator('text=Average Stake Amount')).toBeVisible();
await expect(page.locator('text=Total Rewards Distributed')).toBeVisible();
});
test('should handle wallet connection for staking', async ({ page }) => {
await page.click('text=Staking');
// Check wallet connection button
await expect(page.locator('button:has-text("Connect Wallet")')).toBeVisible();
// Try to stake without wallet connection
await page.click('button:has-text("Available Agents")');
await page.waitForSelector('[data-testid="agents-list"]', { timeout: 10000 });
const agentCards = page.locator('[data-testid="agent-card"]');
const count = await agentCards.count();
if (count > 0) {
await agentCards.first().locator('button:has-text("Stake")').click();
// Should show wallet connection required
await expect(page.locator('text=Connect wallet to stake')).toBeVisible();
await expect(page.locator('button:has-text("Connect Wallet")')).toBeVisible();
}
});
test('should be responsive on mobile', async ({ page }) => {
// Set mobile viewport
await page.setViewportSize({ width: 375, height: 667 });
await page.click('text=Staking');
// Check mobile layout
await expect(page.locator('h1')).toContainText('Staking Dashboard');
// Check mobile navigation
await expect(page.locator('button:has-text("☰")')).toBeVisible();
// Check mobile tabs
await expect(page.locator('button:has-text("My Stakes")')).toBeVisible();
await expect(page.locator('button:has-text("Agents")')).toBeVisible();
await expect(page.locator('button:has-text("Pools")')).toBeVisible();
await expect(page.locator('button:has-text("Rewards")')).toBeVisible();
// Check mobile table layout
await expect(page.locator('[data-testid="mobile-stakes-table"]')).toBeVisible();
});
test('should handle loading states', async ({ page }) => {
await page.click('text=Staking');
// Check loading skeleton
await expect(page.locator('[data-testid="loading-skeleton"]')).toBeVisible();
// Wait for content to load
await page.waitForSelector('[data-testid="stakes-table"]', { timeout: 10000 });
// Check loading skeleton is gone
await expect(page.locator('[data-testid="loading-skeleton"]')).not.toBeVisible();
});
test('should handle error states', async ({ page }) => {
// Mock API error
await page.route('**/api/v1/staking/**', route => {
route.fulfill({
status: 500,
contentType: 'application/json',
body: JSON.stringify({ error: 'Internal server error' })
});
});
await page.click('text=Staking');
// Check error message
await expect(page.locator('[data-testid="error-message"]')).toBeVisible();
await expect(page.locator('text=Failed to load staking data')).toBeVisible();
// Check retry button
await expect(page.locator('button:has-text("Retry")')).toBeVisible();
});
});

View File

@@ -0,0 +1,19 @@
<!doctype html>
<html lang="en" data-theme="dark">
<head>
<meta charset="UTF-8" />
<link rel="icon" type="image/svg+xml" href="/vite.svg" />
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
<title>marketplace-web</title>
<link rel="stylesheet" href="/assets/css/site-header.css">
<link rel="preload" href="/assets/css/font-awesome.min.css" as="style" onload="this.onload=null;this.rel='stylesheet'">
<noscript><link rel="stylesheet" href="/assets/css/font-awesome.min.css"></noscript>
<link rel="stylesheet" href="https://cdnjs.cloudflare.com/ajax/libs/font-awesome/6.5.1/css/all.min.css" crossorigin="anonymous" media="print" onload="this.media='all'; this.onload=null;">
</head>
<body>
<div data-global-header></div>
<div id="app"></div>
<script type="module" src="/src/main.ts"></script>
<script src="/assets/js/global-header.js"></script>
</body>
</html>

View File

@@ -0,0 +1,41 @@
import { defineConfig, devices } from '@playwright/test';
export default defineConfig({
testDir: './e2e',
fullyParallel: true,
forbidOnly: !!process.env.CI,
retries: process.env.CI ? 2 : 0,
workers: process.env.CI ? 1 : undefined,
reporter: 'html',
use: {
baseURL: 'http://localhost:5173/marketplace',
trace: 'on-first-retry',
},
projects: [
{
name: 'chromium',
use: { ...devices['Desktop Chrome'] },
},
{
name: 'firefox',
use: { ...devices['Desktop Firefox'] },
},
{
name: 'webkit',
use: { ...devices['Desktop Safari'] },
},
{
name: 'Mobile Chrome',
use: { ...devices['Pixel 5'] },
},
{
name: 'Mobile Safari',
use: { ...devices['iPhone 12'] },
},
],
webServer: {
command: 'npm run dev',
url: 'http://localhost:5173',
reuseExistingServer: !process.env.CI,
},
});

View File

@@ -0,0 +1,6 @@
export default {
plugins: {
'@tailwindcss/postcss': {},
autoprefixer: {},
},
}

View 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="31.88" height="32" preserveAspectRatio="xMidYMid meet" viewBox="0 0 256 257"><defs><linearGradient id="IconifyId1813088fe1fbc01fb466" x1="-.828%" x2="57.636%" y1="7.652%" y2="78.411%"><stop offset="0%" stop-color="#41D1FF"></stop><stop offset="100%" stop-color="#BD34FE"></stop></linearGradient><linearGradient id="IconifyId1813088fe1fbc01fb467" x1="43.376%" x2="50.316%" y1="2.242%" y2="89.03%"><stop offset="0%" stop-color="#FFEA83"></stop><stop offset="8.333%" stop-color="#FFDD35"></stop><stop offset="100%" stop-color="#FFA800"></stop></linearGradient></defs><path fill="url(#IconifyId1813088fe1fbc01fb466)" d="M255.153 37.938L134.897 252.976c-2.483 4.44-8.862 4.466-11.382.048L.875 37.958c-2.746-4.814 1.371-10.646 6.827-9.67l120.385 21.517a6.537 6.537 0 0 0 2.322-.004l117.867-21.483c5.438-.991 9.574 4.796 6.877 9.62Z"></path><path fill="url(#IconifyId1813088fe1fbc01fb467)" d="M185.432.063L96.44 17.501a3.268 3.268 0 0 0-2.634 3.014l-5.474 92.456a3.268 3.268 0 0 0 3.997 3.378l24.777-5.718c2.318-.535 4.413 1.507 3.936 3.838l-7.361 36.047c-.495 2.426 1.782 4.5 4.151 3.78l15.304-4.649c2.372-.72 4.652 1.36 4.15 3.788l-11.698 56.621c-.732 3.542 3.979 5.473 5.943 2.437l1.313-2.028l72.516-144.72c1.215-2.423-.88-5.186-3.54-4.672l-25.505 4.922c-2.396.462-4.435-1.77-3.759-4.114l16.646-57.705c.677-2.35-1.37-4.583-3.769-4.113Z"></path></svg>

After

Width:  |  Height:  |  Size: 1.5 KiB

View File

@@ -0,0 +1,302 @@
#!/usr/bin/env bash
# AITBC Developer Ecosystem Frontend Deployment Script
# Deploys the React frontend application to production
#
# Usage: ./deploy-frontend.sh [environment] [server]
# Environment: development, staging, production
# Server: aitbc-cascade (default), custom-server
set -euo pipefail
# Colors for output
RED='\033[0;31m'
GREEN='\033[0;32m'
YELLOW='\033[1;33m'
BLUE='\033[0;34m'
NC='\033[0m' # No Color
print_status() {
echo -e "${BLUE}[INFO]${NC} $1"
}
print_success() {
echo -e "${GREEN}[SUCCESS]${NC} $1"
}
print_warning() {
echo -e "${YELLOW}[WARNING]${NC} $1"
}
print_error() {
echo -e "${RED}[ERROR]${NC} $1"
}
# Parse arguments
ENVIRONMENT="${1:-production}"
SERVER="${2:-aitbc-cascade}"
SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)"
APP_DIR="$(dirname "$SCRIPT_DIR")"
ROOT_DIR="$(dirname "$APP_DIR)"
echo "🚀 AITBC Frontend Deployment"
echo "============================="
echo "Environment: $ENVIRONMENT"
echo "Server: $SERVER"
echo "App Directory: $APP_DIR"
echo ""
# Check prerequisites
check_prerequisites() {
print_status "Checking prerequisites..."
# Check if we're in the correct directory
if [[ ! -f "$APP_DIR/package.json" ]]; then
print_error "package.json not found in $APP_DIR"
exit 1
fi
# Check if build exists
if [[ ! -d "$APP_DIR/dist" ]]; then
print_warning "Build directory not found. Building application..."
build_application
fi
# Check SSH connection to server
if ! ssh -o ConnectTimeout=5 "$SERVER" "echo 'SSH connection successful'" 2>/dev/null; then
print_error "Cannot connect to server $SERVER"
print_error "Please ensure SSH keys are properly configured"
exit 1
fi
print_success "Prerequisites check completed"
}
# Build application
build_application() {
print_status "Building frontend application..."
cd "$APP_DIR"
# Set environment variables
if [[ "$ENVIRONMENT" == "production" ]]; then
export NODE_ENV=production
elif [[ "$ENVIRONMENT" == "staging" ]]; then
export NODE_ENV=staging
else
export NODE_ENV=development
fi
# Build the application
if npm run build; then
print_success "Application built successfully"
else
print_error "Application build failed"
exit 1
fi
}
# Deploy to server
deploy_to_server() {
print_status "Deploying frontend to $SERVER..."
# Create remote directory if it doesn't exist
ssh "$SERVER" "mkdir -p /var/www/aitbc.bubuit.net/marketplace"
# Copy files to server
print_status "Copying files to server..."
# Copy HTML files
scp "$APP_DIR/dist/index.html" "$SERVER:/var/www/aitbc.bubuit.net/marketplace/"
# Copy JavaScript files
scp "$APP_DIR/dist/assets/"*.js "$SERVER:/var/www/aitbc.bubuit.net/marketplace/assets/" 2>/dev/null || true
# Copy CSS files
scp "$APP_DIR/dist/assets/"*.css "$SERVER:/var/www/aitbc.bubuit.net/marketplace/assets/" 2>/dev/null || true
# Copy other assets
scp -r "$APP_DIR/dist/assets/"* "$SERVER:/var/www/aitbc.bubuit.net/marketplace/assets/" 2>/dev/null || true
print_success "Files copied to server"
}
# Configure nginx
configure_nginx() {
print_status "Configuring nginx..."
# Create nginx configuration
ssh "$SERVER" "cat > /etc/nginx/sites-available/marketplace.conf << 'EOF'
server {
listen 80;
server_name aitbc.bubuit.net;
location /marketplace/ {
alias /var/www/aitbc.bubuit.net/marketplace/;
try_files \$uri \$uri/ /marketplace/index.html;
# Cache static assets
location ~* \.(js|css|png|jpg|jpeg|gif|ico|svg)$ {
expires 1y;
add_header Cache-Control \"public, immutable\";
}
# Security headers
add_header X-Frame-Options \"SAMEORIGIN\" always;
add_header X-Content-Type-Options \"nosniff\" always;
add_header X-XSS-Protection \"1; mode=block\" always;
add_header Referrer-Policy \"strict-origin-when-cross-origin\" always;
}
# Redirect root to marketplace
location = / {
return 301 /marketplace/;
}
}
EOF"
# Enable site
ssh "$SERVER" "ln -sf /etc/nginx/sites-available/marketplace.conf /etc/nginx/sites-enabled/"
# Test nginx configuration
if ssh "$SERVER" "nginx -t"; then
print_success "Nginx configuration is valid"
else
print_error "Nginx configuration is invalid"
exit 1
fi
# Reload nginx
ssh "$SERVER" "systemctl reload nginx"
print_success "Nginx configured and reloaded"
}
# Set permissions
set_permissions() {
print_status "Setting file permissions..."
ssh "$SERVER" "chown -R www-data:www-data /var/www/aitbc.bubuit.net/marketplace"
ssh "$SERVER" "chmod -R 755 /var/www/aitbc.bubuit.net/marketplace"
print_success "Permissions set"
}
# Health check
health_check() {
print_status "Performing health check..."
# Wait a moment for nginx to reload
sleep 5
# Check if the site is accessible
if curl -s -o /dev/null -w "%{http_code}" "http://aitbc.bubuit.net/marketplace/" | grep -q "200"; then
print_success "Site is accessible and responding correctly"
else
print_warning "Site may not be accessible. Please check manually."
fi
# Check nginx status
if ssh "$SERVER" "systemctl is-active nginx" | grep -q "active"; then
print_success "Nginx is running"
else
print_error "Nginx is not running"
exit 1
fi
}
# Generate deployment report
generate_deployment_report() {
print_status "Generating deployment report..."
local report_file="$ROOT_DIR/frontend-deployment-report-$(date +%Y%m%d-%H%M%S).json"
cat > "$report_file" << EOF
{
"deployment": {
"environment": "$ENVIRONMENT",
"server": "$SERVER",
"timestamp": "$(date -Iseconds)",
"app_directory": "$APP_DIR",
"build_directory": "$APP_DIR/dist"
},
"configuration": {
"nginx_config": "/etc/nginx/sites-available/marketplace.conf",
"web_root": "/var/www/aitbc.bubuit.net/marketplace",
"server_name": "aitbc.bubuit.net"
},
"urls": {
"production": "http://aitbc.bubuit.net/marketplace/",
"health_check": "http://aitbc.bubuit.net/marketplace/"
}
}
EOF
print_success "Deployment report saved to $report_file"
}
# Rollback function
rollback() {
print_warning "Rolling back deployment..."
# Restore previous version if exists
if ssh "$SERVER" "test -d /var/www/aitbc.bubuit.net/marketplace.backup"; then
ssh "$SERVER" "rm -rf /var/www/aitbc.bubuit.net/marketplace"
ssh "$SERVER" "mv /var/www/aitbc.bubuit.net/marketplace.backup /var/www/aitbc.bubuit.net/marketplace"
ssh "$SERVER" "systemctl reload nginx"
print_success "Rollback completed"
else
print_error "No backup found for rollback"
exit 1
fi
}
# Main execution
main() {
print_status "Starting frontend deployment..."
# Create backup of current deployment
if ssh "$SERVER" "test -d /var/www/aitbc.bubuit.net/marketplace"; then
print_status "Creating backup of current deployment..."
ssh "$SERVER" "cp -r /var/www/aitbc.bubuit.net/marketplace /var/www/aitbc.bubuit.net/marketplace.backup"
fi
# Check prerequisites
check_prerequisites
# Deploy to server
deploy_to_server
# Configure nginx
configure_nginx
# Set permissions
set_permissions
# Health check
health_check
# Generate deployment report
generate_deployment_report
print_success "🎉 Frontend deployment completed successfully!"
echo ""
echo "🌐 Application URL: http://aitbc.bubuit.net/marketplace/"
echo "📊 Deployment report: $report_file"
echo ""
echo "Next steps:"
echo "1. Test the application in browser"
echo "2. Verify all functionality works"
echo "3. Monitor application logs"
echo "4. Update DNS if needed"
}
# Handle script interruption
trap 'print_error "Deployment interrupted"; rollback; exit 1' INT TERM
# Handle rollback on error
trap 'print_error "Deployment failed, initiating rollback..."; rollback; exit 1' ERR
# Run main function
main "$@"

View File

@@ -0,0 +1,248 @@
#!/usr/bin/env python3
"""
Edge Node Deployment Script for AITBC Marketplace
Deploys edge node configuration and services
"""
import yaml
import subprocess
import sys
import os
import json
from datetime import datetime
def load_config(config_file):
"""Load edge node configuration from YAML file"""
with open(config_file, 'r') as f:
return yaml.safe_load(f)
def deploy_redis_cache(config):
"""Deploy Redis cache layer"""
print(f"🔧 Deploying Redis cache for {config['edge_node_config']['node_id']}")
# Check if Redis is running
try:
result = subprocess.run(['redis-cli', 'ping'], capture_output=True, text=True)
if result.stdout.strip() == 'PONG':
print("✅ Redis is already running")
else:
print("⚠️ Redis not responding, attempting to start...")
# Start Redis if not running
subprocess.run(['sudo', 'systemctl', 'start', 'redis-server'], check=True)
print("✅ Redis started")
except FileNotFoundError:
print("❌ Redis not installed, installing...")
subprocess.run(['sudo', 'apt-get', 'update'], check=True)
subprocess.run(['sudo', 'apt-get', 'install', '-y', 'redis-server'], check=True)
subprocess.run(['sudo', 'systemctl', 'start', 'redis-server'], check=True)
print("✅ Redis installed and started")
# Configure Redis
redis_config = config['edge_node_config']['caching']
# Set Redis configuration
redis_commands = [
f"CONFIG SET maxmemory {redis_config['max_memory_mb']}mb",
f"CONFIG SET maxmemory-policy allkeys-lru",
f"CONFIG SET timeout {redis_config['cache_ttl_seconds']}"
]
for cmd in redis_commands:
try:
subprocess.run(['redis-cli', *cmd.split()], check=True, capture_output=True)
except subprocess.CalledProcessError:
print(f"⚠️ Could not set Redis config: {cmd}")
def deploy_monitoring(config):
"""Deploy monitoring agent"""
print(f"📊 Deploying monitoring for {config['edge_node_config']['node_id']}")
monitoring_config = config['edge_node_config']['monitoring']
# Create monitoring directory
os.makedirs('/tmp/aitbc-monitoring', exist_ok=True)
# Create monitoring script
monitoring_script = f"""#!/bin/bash
# Monitoring script for {config['edge_node_config']['node_id']}
echo "{{{{'timestamp': '$(date -Iseconds)', 'node_id': '{config['edge_node_config']['node_id']}', 'status': 'monitoring'}}}}" > /tmp/aitbc-monitoring/status.json
# Check marketplace API health
curl -s http://localhost:{config['edge_node_config']['services'][0]['port']}/health/live > /dev/null
if [ $? -eq 0 ]; then
echo "marketplace_healthy=true" >> /tmp/aitbc-monitoring/status.json
else
echo "marketplace_healthy=false" >> /tmp/aitbc-monitoring/status.json
fi
# Check Redis health
redis-cli ping > /dev/null
if [ $? -eq 0 ]; then
echo "redis_healthy=true" >> /tmp/aitbc-monitoring/status.json
else
echo "redis_healthy=false" >> /tmp/aitbc-monitoring/status.json
fi
"""
with open('/tmp/aitbc-monitoring/monitor.sh', 'w') as f:
f.write(monitoring_script)
os.chmod('/tmp/aitbc-monitoring/monitor.sh', 0o755)
# Create systemd service for monitoring
monitoring_service = f"""[Unit]
Description=AITBC Edge Node Monitoring - {config['edge_node_config']['node_id']}
After=network.target
[Service]
Type=simple
User=root
ExecStart=/tmp/aitbc-monitoring/monitor.sh
Restart=always
RestartSec=30
[Install]
WantedBy=multi-user.target
"""
service_file = f"/etc/systemd/system/aitbc-edge-monitoring-{config['edge_node_config']['node_id']}.service"
with open(service_file, 'w') as f:
f.write(monitoring_service)
# Enable and start monitoring service
subprocess.run(['sudo', 'systemctl', 'daemon-reload'], check=True)
subprocess.run(['sudo', 'systemctl', 'enable', f'aitbc-edge-monitoring-{config["edge_node_config"]["node_id"]}.service'], check=True)
subprocess.run(['sudo', 'systemctl', 'start', f'aitbc-edge-monitoring-{config["edge_node_config"]["node_id"]}.service'], check=True)
print("✅ Monitoring agent deployed")
def optimize_network(config):
"""Apply network optimizations"""
print(f"🌐 Optimizing network for {config['edge_node_config']['node_id']}")
network_config = config['edge_node_config']['network']
# TCP optimizations
tcp_params = {
'net.core.rmem_max': '16777216',
'net.core.wmem_max': '16777216',
'net.ipv4.tcp_rmem': '4096 87380 16777216',
'net.ipv4.tcp_wmem': '4096 65536 16777216',
'net.ipv4.tcp_congestion_control': 'bbr',
'net.core.netdev_max_backlog': '5000'
}
for param, value in tcp_params.items():
try:
subprocess.run(['sudo', 'sysctl', '-w', f'{param}={value}'], check=True, capture_output=True)
print(f"✅ Set {param}={value}")
except subprocess.CalledProcessError:
print(f"⚠️ Could not set {param}")
def deploy_edge_services(config):
"""Deploy edge node services"""
print(f"🚀 Deploying edge services for {config['edge_node_config']['node_id']}")
# Create edge service configuration
edge_service_config = {
'node_id': config['edge_node_config']['node_id'],
'region': config['edge_node_config']['region'],
'services': config['edge_node_config']['services'],
'performance_targets': config['edge_node_config']['performance_targets'],
'deployed_at': datetime.now().isoformat()
}
# Save configuration
with open(f'/tmp/aitbc-edge-{config["edge_node_config"]["node_id"]}-config.json', 'w') as f:
json.dump(edge_service_config, f, indent=2)
print(f"✅ Edge services configuration saved")
def validate_deployment(config):
"""Validate edge node deployment"""
print(f"✅ Validating deployment for {config['edge_node_config']['node_id']}")
validation_results = {}
# Check marketplace API
try:
response = subprocess.run(['curl', '-s', f'http://localhost:{config["edge_node_config"]["services"][0]["port"]}/health/live'],
capture_output=True, text=True, timeout=10)
if response.status_code == 0:
validation_results['marketplace_api'] = 'healthy'
else:
validation_results['marketplace_api'] = 'unhealthy'
except Exception as e:
validation_results['marketplace_api'] = f'error: {str(e)}'
# Check Redis
try:
result = subprocess.run(['redis-cli', 'ping'], capture_output=True, text=True, timeout=5)
if result.stdout.strip() == 'PONG':
validation_results['redis'] = 'healthy'
else:
validation_results['redis'] = 'unhealthy'
except Exception as e:
validation_results['redis'] = f'error: {str(e)}'
# Check monitoring
try:
result = subprocess.run(['systemctl', 'is-active', f'aitbc-edge-monitoring-{config["edge_node_config"]["node_id"]}.service'],
capture_output=True, text=True, timeout=5)
validation_results['monitoring'] = result.stdout.strip()
except Exception as e:
validation_results['monitoring'] = f'error: {str(e)}'
print(f"📊 Validation Results:")
for service, status in validation_results.items():
print(f" {service}: {status}")
return validation_results
def main():
if len(sys.argv) != 2:
print("Usage: python deploy_edge_node.py <config_file>")
sys.exit(1)
config_file = sys.argv[1]
if not os.path.exists(config_file):
print(f"❌ Configuration file {config_file} not found")
sys.exit(1)
try:
config = load_config(config_file)
print(f"🚀 Deploying edge node: {config['edge_node_config']['node_id']}")
print(f"📍 Region: {config['edge_node_config']['region']}")
print(f"🌍 Location: {config['edge_node_config']['location']}")
# Deploy components
deploy_redis_cache(config)
deploy_monitoring(config)
optimize_network(config)
deploy_edge_services(config)
# Validate deployment
validation_results = validate_deployment(config)
# Save deployment status
deployment_status = {
'node_id': config['edge_node_config']['node_id'],
'deployment_time': datetime.now().isoformat(),
'validation_results': validation_results,
'status': 'completed'
}
with open(f'/tmp/aitbc-edge-{config["edge_node_config"]["node_id"]}-deployment.json', 'w') as f:
json.dump(deployment_status, f, indent=2)
print(f"✅ Edge node deployment completed for {config['edge_node_config']['node_id']}")
except Exception as e:
print(f"❌ Deployment failed: {str(e)}")
sys.exit(1)
if __name__ == "__main__":
main()

View 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

File diff suppressed because it is too large Load Diff

View 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;

File diff suppressed because it is too large Load Diff

View 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;

File diff suppressed because it is too large Load Diff

View 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;

View 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;

View 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;

File diff suppressed because it is too large Load Diff

View 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;

File diff suppressed because it is too large Load Diff

View 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;

File diff suppressed because it is too large Load Diff

View 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 }

View 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 }

View 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 }

View 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 }

View 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 }

View 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 }

View 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,
}

View 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 }

View 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,
}

View 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 }

View 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,
}

View 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)
}

View 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 }

View 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
}
}

View 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));
}
}

View 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;

View 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);
}

View 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))
}

View 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();

View 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>,
)

View File

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

View File

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

View File

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

View File

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

View 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;
}

View 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

View File

@@ -0,0 +1,52 @@
/** @type {import(tailwindcss).Config} */
export default {
content: [
"./index.html",
"./src/**/*.{js,ts,jsx,tsx}",
],
theme: {
extend: {
colors: {
border: "hsl(var(--border))",
input: "hsl(var(--input))",
ring: "hsl(var(--ring))",
background: "hsl(var(--background))",
foreground: "hsl(var(--foreground))",
primary: {
DEFAULT: "hsl(var(--primary))",
foreground: "hsl(var(--primary-foreground))",
},
secondary: {
DEFAULT: "hsl(var(--secondary))",
foreground: "hsl(var(--secondary-foreground))",
},
destructive: {
DEFAULT: "hsl(var(--destructive))",
foreground: "hsl(var(--destructive-foreground))",
},
muted: {
DEFAULT: "hsl(var(--muted))",
foreground: "hsl(var(--muted-foreground))",
},
accent: {
DEFAULT: "hsl(var(--accent))",
foreground: "hsl(var(--accent-foreground))",
},
popover: {
DEFAULT: "hsl(var(--popover))",
foreground: "hsl(var(--popover-foreground))",
},
card: {
DEFAULT: "hsl(var(--card))",
foreground: "hsl(var(--card-foreground))",
},
},
borderRadius: {
lg: "var(--radius)",
md: "calc(var(--radius) - 2px)",
sm: "calc(var(--radius) - 4px)",
},
},
},
plugins: [],
}

View File

@@ -0,0 +1,5 @@
import { defineConfig } from 'vite'
export default defineConfig({
base: '/marketplace/',
})