SurfSense/surfsense_web/components/tool-ui/crypto/live-token-data.tsx
API Test Bot d20cb8a538 feat(crypto): implement hybrid approach with real-time DexScreener tools
- Add crypto_realtime.py with get_live_token_price and get_live_token_data tools
- Register real-time tools in registry.py (no dependencies required)
- Export tool factories in __init__.py
- Create LiveTokenPriceToolUI component for real-time price display
- Create LiveTokenDataToolUI component for comprehensive market data
- Register tool-ui components in new-chat page

Hybrid Architecture:
- RAG (search_knowledge_base): Historical context, trends from indexed data
- Real-time tools: Current prices, live market data via direct API calls
- AI agent decides which to use based on query intent
2026-02-04 00:12:32 +07:00

246 lines
12 KiB
TypeScript
Raw Blame History

This file contains invisible Unicode characters

This file contains invisible Unicode characters that are indistinguishable to humans but may be processed differently by a computer. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

"use client";
import { makeAssistantToolUI } from "@assistant-ui/react";
import { z } from "zod";
import { cn } from "@/lib/utils";
import { TrendingUp, TrendingDown, ExternalLink, Zap, RefreshCw, Activity, Droplets, BarChart3 } from "lucide-react";
import { Button } from "@/components/ui/button";
import { Card, CardContent, CardHeader, CardTitle } from "@/components/ui/card";
import { Badge } from "@/components/ui/badge";
import { ChainIcon } from "@/components/crypto/ChainIcon";
// Schema for live token data tool arguments
export const LiveTokenDataArgsSchema = z.object({
chain: z.string(),
token_address: z.string(),
token_symbol: z.string().optional(),
include_all_pairs: z.boolean().optional(),
});
export type LiveTokenDataArgs = z.infer<typeof LiveTokenDataArgsSchema>;
// Schema for live token data result (matches backend response)
export const LiveTokenDataResultSchema = z.object({
id: z.string(),
kind: z.literal("live_token_data"),
chain: z.string(),
token_address: z.string(),
token_symbol: z.string().optional(),
token_name: z.string().optional(),
price_usd: z.string().optional(),
price_native: z.string().optional(),
price_change_5m: z.number().optional(),
price_change_1h: z.number().optional(),
price_change_6h: z.number().optional(),
price_change_24h: z.number().optional(),
volume_24h: z.number().optional(),
volume_6h: z.number().optional(),
volume_1h: z.number().optional(),
liquidity_usd: z.number().optional(),
market_cap: z.number().optional(),
fdv: z.number().optional(),
txns_24h_buys: z.number().optional(),
txns_24h_sells: z.number().optional(),
txns_6h_buys: z.number().optional(),
txns_6h_sells: z.number().optional(),
txns_1h_buys: z.number().optional(),
txns_1h_sells: z.number().optional(),
total_volume_24h_all_pairs: z.number().optional(),
total_liquidity_all_pairs: z.number().optional(),
total_buys_24h_all_pairs: z.number().optional(),
total_sells_24h_all_pairs: z.number().optional(),
dex: z.string().optional(),
pair_url: z.string().optional(),
total_pairs: z.number().optional(),
data_source: z.string().optional(),
error: z.string().optional(),
});
export type LiveTokenDataResult = z.infer<typeof LiveTokenDataResultSchema>;
const formatPrice = (price: string | undefined): string => {
if (!price || price === "N/A") return "N/A";
const num = parseFloat(price);
if (isNaN(num)) return price;
if (num < 0.00001) return `$${num.toExponential(2)}`;
if (num < 1) return `$${num.toFixed(6)}`;
return `$${num.toLocaleString(undefined, { minimumFractionDigits: 2, maximumFractionDigits: 2 })}`;
};
const formatLargeNumber = (num: number | undefined): string => {
if (num === undefined || num === null || num === 0) return "N/A";
if (num >= 1e9) return `$${(num / 1e9).toFixed(2)}B`;
if (num >= 1e6) return `$${(num / 1e6).toFixed(2)}M`;
if (num >= 1e3) return `$${(num / 1e3).toFixed(2)}K`;
return `$${num.toFixed(2)}`;
};
const formatNumber = (num: number | undefined): string => {
if (num === undefined || num === null) return "0";
return num.toLocaleString();
};
const PriceChange = ({ value, label }: { value: number | undefined; label: string }) => {
if (value === undefined || value === null) return null;
const isPositive = value >= 0;
return (
<div className="text-center">
<p className="text-xs text-muted-foreground">{label}</p>
<p className={cn("text-sm font-medium", isPositive ? "text-green-500" : "text-red-500")}>
{isPositive ? "+" : ""}{value.toFixed(2)}%
</p>
</div>
);
};
/**
* LiveTokenDataToolUI - Displays comprehensive real-time market data
* Used when AI fetches detailed live market information
*/
export const LiveTokenDataToolUI = makeAssistantToolUI<LiveTokenDataArgs, LiveTokenDataResult>({
toolName: "get_live_token_data",
render: ({ args, result, status }) => {
const isLoading = status.type === "running";
const hasError = result?.error;
const handleOpenDexScreener = () => {
if (result?.pair_url) {
window.open(result.pair_url, "_blank");
} else if (args.token_address) {
window.open(`https://dexscreener.com/${args.chain}/${args.token_address}`, "_blank");
}
};
const totalTxns24h = (result?.txns_24h_buys || 0) + (result?.txns_24h_sells || 0);
const buyRatio = totalTxns24h > 0 ? ((result?.txns_24h_buys || 0) / totalTxns24h) * 100 : 50;
return (
<Card className="my-3 overflow-hidden border-purple-500/20">
<CardHeader className="pb-3 bg-gradient-to-r from-purple-500/5 to-transparent">
<CardTitle className="text-lg flex items-center gap-2">
<Activity className="h-5 w-5 text-purple-500" />
Live Market Data
{isLoading && <Badge variant="secondary" className="animate-pulse">Fetching...</Badge>}
{!isLoading && !hasError && (
<Badge variant="outline" className="text-xs text-purple-500 border-purple-500/30">
<RefreshCw className="h-3 w-3 mr-1" />
Real-time
</Badge>
)}
</CardTitle>
</CardHeader>
<CardContent className="space-y-4 pt-4">
{hasError ? (
<div className="text-red-500 text-sm p-3 bg-red-500/10 rounded-lg">
{result.error}
</div>
) : (
<>
{/* Token Header */}
<div className="flex items-center justify-between">
<div className="flex items-center gap-3">
<ChainIcon chain={result?.chain || args.chain} size="md" />
<div>
<div className="flex items-center gap-2">
<span className="font-bold text-xl">
{result?.token_symbol || args.token_symbol || "Token"}
</span>
{result?.token_name && (
<span className="text-muted-foreground text-sm">{result.token_name}</span>
)}
</div>
<div className="flex items-center gap-2">
<span className="font-semibold text-2xl">{formatPrice(result?.price_usd)}</span>
{result?.price_change_24h !== undefined && (
<span className={cn(
"flex items-center gap-0.5 text-sm font-medium",
result.price_change_24h >= 0 ? "text-green-500" : "text-red-500"
)}>
{result.price_change_24h >= 0 ? <TrendingUp className="h-4 w-4" /> : <TrendingDown className="h-4 w-4" />}
{result.price_change_24h >= 0 ? "+" : ""}{result.price_change_24h.toFixed(2)}%
</span>
)}
</div>
</div>
</div>
</div>
{/* Price Changes */}
<div className="flex justify-around py-2 bg-muted/30 rounded-lg">
<PriceChange value={result?.price_change_5m} label="5m" />
<PriceChange value={result?.price_change_1h} label="1h" />
<PriceChange value={result?.price_change_6h} label="6h" />
<PriceChange value={result?.price_change_24h} label="24h" />
</div>
{/* Metrics Grid */}
<div className="grid grid-cols-2 md:grid-cols-4 gap-3">
<div className="bg-muted/50 rounded-lg p-3">
<p className="text-xs text-muted-foreground flex items-center gap-1">
<BarChart3 className="h-3 w-3" /> 24h Volume
</p>
<p className="font-medium">{formatLargeNumber(result?.volume_24h)}</p>
</div>
<div className="bg-muted/50 rounded-lg p-3">
<p className="text-xs text-muted-foreground flex items-center gap-1">
<Droplets className="h-3 w-3" /> Liquidity
</p>
<p className="font-medium">{formatLargeNumber(result?.liquidity_usd)}</p>
</div>
<div className="bg-muted/50 rounded-lg p-3">
<p className="text-xs text-muted-foreground">Market Cap</p>
<p className="font-medium">{formatLargeNumber(result?.market_cap)}</p>
</div>
<div className="bg-muted/50 rounded-lg p-3">
<p className="text-xs text-muted-foreground">FDV</p>
<p className="font-medium">{formatLargeNumber(result?.fdv)}</p>
</div>
</div>
{/* Transaction Activity */}
<div className="space-y-2">
<p className="text-sm font-medium flex items-center gap-2">
<Activity className="h-4 w-4" /> 24h Transactions
</p>
<div className="flex items-center gap-2">
<div className="flex-1 h-2 bg-muted rounded-full overflow-hidden">
<div
className="h-full bg-green-500 transition-all"
style={{ width: `${buyRatio}%` }}
/>
</div>
</div>
<div className="flex justify-between text-sm">
<span className="text-green-500">
{formatNumber(result?.txns_24h_buys)} buys
</span>
<span className="text-muted-foreground">
{formatNumber(totalTxns24h)} total
</span>
<span className="text-red-500">
{formatNumber(result?.txns_24h_sells)} sells
</span>
</div>
</div>
{/* DEX Info & Actions */}
<div className="flex items-center justify-between pt-2 border-t">
<div className="text-sm text-muted-foreground">
<span>DEX: {result?.dex || "Unknown"}</span>
{result?.total_pairs && result.total_pairs > 1 && (
<span className="ml-2"> {result.total_pairs} pairs</span>
)}
</div>
<Button variant="outline" size="sm" onClick={handleOpenDexScreener}>
<ExternalLink className="h-4 w-4 mr-1" />
DexScreener
</Button>
</div>
</>
)}
</CardContent>
</Card>
);
},
});